v3 UI
This commit is contained in:
parent
99feff549d
commit
5894b4fd95
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"paths": [
|
||||||
|
"frontend/src/**/*.js"
|
||||||
|
],
|
||||||
|
"ignored": [
|
||||||
|
"**/node_modules/**/*"
|
||||||
|
],
|
||||||
|
"port": 5004
|
||||||
|
}
|
8
build.sh
8
build.sh
|
@ -90,16 +90,16 @@ Build()
|
||||||
|
|
||||||
RunGulp()
|
RunGulp()
|
||||||
{
|
{
|
||||||
echo "##teamcity[progressStart 'npm install']"
|
ProgressStart 'yarn install'
|
||||||
npm-cache install npm || CheckExitCode npm install --no-optional --no-bin-links
|
yarn install
|
||||||
echo "##teamcity[progressFinish 'npm install']"
|
ProgressEnd 'yarn install'
|
||||||
|
|
||||||
echo "##teamcity[progressStart 'Running gulp']"
|
echo "##teamcity[progressStart 'Running gulp']"
|
||||||
CheckExitCode npm run build
|
CheckExitCode npm run build
|
||||||
echo "##teamcity[progressFinish 'Running gulp']"
|
echo "##teamcity[progressFinish 'Running gulp']"
|
||||||
|
|
||||||
echo "##teamcity[progressStart 'Running gulp (phantom)']"
|
echo "##teamcity[progressStart 'Running gulp (phantom)']"
|
||||||
CheckExitCode npm run build -- --phantom --production
|
CheckExitCode yarn run build -- --production
|
||||||
echo "##teamcity[progressFinish 'Running gulp (phantom)']"
|
echo "##teamcity[progressFinish 'Running gulp (phantom)']"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"remove-empty-rulesets": true,
|
||||||
|
"always-semicolon": true,
|
||||||
|
"color-case": "lower",
|
||||||
|
"block-indent": " ",
|
||||||
|
"color-shorthand": false,
|
||||||
|
"element-case": "lower",
|
||||||
|
"eof-newline": true,
|
||||||
|
"leading-zero": true,
|
||||||
|
"quotes": "double",
|
||||||
|
"sort-order-fallback": "abc",
|
||||||
|
"space-before-colon": "",
|
||||||
|
"space-after-colon": " ",
|
||||||
|
"space-before-combinator": " ",
|
||||||
|
"space-after-combinator": " ",
|
||||||
|
"space-between-declarations": "\n",
|
||||||
|
"space-before-opening-brace": " ",
|
||||||
|
"space-after-opening-brace": "\n",
|
||||||
|
"space-after-selector-delimiter": " ",
|
||||||
|
"space-before-selector-delimiter": "",
|
||||||
|
"space-before-closing-brace": "\n",
|
||||||
|
"strip-spaces": true,
|
||||||
|
"tab-size": true,
|
||||||
|
"unitless-zero": false
|
||||||
|
}
|
|
@ -0,0 +1,335 @@
|
||||||
|
{
|
||||||
|
"indent": {
|
||||||
|
"value": " ",
|
||||||
|
"FunctionExpression": 1,
|
||||||
|
"ArrayExpression": 1,
|
||||||
|
"ObjectExpression": 1
|
||||||
|
},
|
||||||
|
"lineBreak": {
|
||||||
|
"value": "\n",
|
||||||
|
|
||||||
|
"before": {
|
||||||
|
"ArrayPatternClosing": 0,
|
||||||
|
"ArrayPatternComma": 0,
|
||||||
|
"ArrayPatternOpening": 0,
|
||||||
|
"ArrowFunctionExpressionArrow": 0,
|
||||||
|
"ArrowFunctionExpressionClosingBrace": ">=1",
|
||||||
|
"ArrowFunctionExpressionOpeningBrace": 0,
|
||||||
|
"AssignmentExpression": ">=1",
|
||||||
|
"AssignmentOperator": 0,
|
||||||
|
"BlockStatement": 0,
|
||||||
|
"BreakKeyword": ">=1",
|
||||||
|
"CallExpression": -1,
|
||||||
|
"CallExpressionClosingParentheses": -1,
|
||||||
|
"CallExpressionOpeningParentheses": 0,
|
||||||
|
"CatchClosingBrace": ">=1",
|
||||||
|
"CatchKeyword": 0,
|
||||||
|
"CatchOpeningBrace": 0,
|
||||||
|
"ClassDeclaration": ">=1",
|
||||||
|
"ClassDeclarationClosingBrace": ">=1",
|
||||||
|
"ClassDeclarationOpeningBrace": 0,
|
||||||
|
"ConditionalExpression": ">=1",
|
||||||
|
"DeleteOperator": ">=1",
|
||||||
|
"DoWhileStatement": ">=1",
|
||||||
|
"DoWhileStatementClosingBrace": ">=1",
|
||||||
|
"DoWhileStatementOpeningBrace": 0,
|
||||||
|
"ElseIfStatement": 0,
|
||||||
|
"ElseIfStatementClosingBrace": ">=1",
|
||||||
|
"ElseIfStatementOpeningBrace": 0,
|
||||||
|
"ElseStatement": 0,
|
||||||
|
"ElseStatementClosingBrace": ">=1",
|
||||||
|
"ElseStatementOpeningBrace": 0,
|
||||||
|
"EmptyStatement": -1,
|
||||||
|
"EndOfFile": -1,
|
||||||
|
"FinallyClosingBrace": ">=1",
|
||||||
|
"FinallyKeyword": -1,
|
||||||
|
"FinallyOpeningBrace": 0,
|
||||||
|
"ForInStatement": ">=1",
|
||||||
|
"ForInStatementClosingBrace": ">=1",
|
||||||
|
"ForInStatementExpressionClosing": 0,
|
||||||
|
"ForInStatementExpressionOpening": 0,
|
||||||
|
"ForInStatementOpeningBrace": 0,
|
||||||
|
"ForStatement": ">=1",
|
||||||
|
"ForStatementClosingBrace": ">=1",
|
||||||
|
"ForStatementExpressionClosing": "<2",
|
||||||
|
"ForStatementExpressionOpening": 0,
|
||||||
|
"ForStatementOpeningBrace": 0,
|
||||||
|
"FunctionDeclaration": ">=1",
|
||||||
|
"FunctionDeclarationClosingBrace": ">=1",
|
||||||
|
"FunctionDeclarationOpeningBrace": 0,
|
||||||
|
"FunctionExpression": 0,
|
||||||
|
"FunctionExpressionClosingBrace": 1,
|
||||||
|
"FunctionExpressionOpeningBrace":0,
|
||||||
|
"IIFEClosingParentheses": 0,
|
||||||
|
"IfStatement": ">=1",
|
||||||
|
"IfStatementClosingBrace": ">=1",
|
||||||
|
"IfStatementOpeningBrace": 0,
|
||||||
|
"LogicalExpression": -1,
|
||||||
|
"MemberExpressionClosing": 0,
|
||||||
|
"MemberExpressionOpening": 0,
|
||||||
|
"MemberExpressionPeriod": -1,
|
||||||
|
"MethodDefinition": ">=1",
|
||||||
|
"ObjectExpressionClosingBrace": "<=1",
|
||||||
|
"ObjectPatternClosingBrace": 0,
|
||||||
|
"ObjectPatternComma": 0,
|
||||||
|
"ObjectPatternOpeningBrace": 0,
|
||||||
|
"ParameterDefault": 0,
|
||||||
|
"Property": "<=2",
|
||||||
|
"PropertyValue": 0,
|
||||||
|
"ReturnStatement": -1,
|
||||||
|
"SwitchClosingBrace": ">=1",
|
||||||
|
"SwitchOpeningBrace": 0,
|
||||||
|
"ThisExpression": -1,
|
||||||
|
"ThrowStatement": ">=1",
|
||||||
|
"TryClosingBrace": ">=1",
|
||||||
|
"TryKeyword": -1,
|
||||||
|
"TryOpeningBrace": 0,
|
||||||
|
"VariableDeclaration": ">=1",
|
||||||
|
"VariableDeclarationSemiColon": 0,
|
||||||
|
"VariableDeclarationWithoutInit": ">=1",
|
||||||
|
"VariableName": ">=1",
|
||||||
|
"VariableValue": 0,
|
||||||
|
"WhileStatement": ">=1",
|
||||||
|
"WhileStatementClosingBrace": ">=1",
|
||||||
|
"WhileStatementOpeningBrace": 0
|
||||||
|
},
|
||||||
|
|
||||||
|
"after": {
|
||||||
|
"ArrayPatternClosing": 0,
|
||||||
|
"ArrayPatternComma": 0,
|
||||||
|
"ArrayPatternOpening": 0,
|
||||||
|
"ArrowFunctionExpressionArrow": 0,
|
||||||
|
"ArrowFunctionExpressionClosingBrace": -1,
|
||||||
|
"ArrowFunctionExpressionOpeningBrace": ">=1",
|
||||||
|
"AssignmentExpression": ">=1",
|
||||||
|
"AssignmentOperator": 0,
|
||||||
|
"BlockStatement": 0,
|
||||||
|
"BreakKeyword": -1,
|
||||||
|
"CallExpression": -1,
|
||||||
|
"CallExpressionClosingParentheses": -1,
|
||||||
|
"CallExpressionOpeningParentheses": -1,
|
||||||
|
"CatchClosingBrace": ">=0",
|
||||||
|
"CatchKeyword": 0,
|
||||||
|
"CatchOpeningBrace": ">=1",
|
||||||
|
"ClassDeclaration": ">=1",
|
||||||
|
"ClassDeclarationClosingBrace": ">=1",
|
||||||
|
"ClassDeclarationOpeningBrace": ">=1",
|
||||||
|
"ConditionalExpression": ">=1",
|
||||||
|
"DeleteOperator": ">=1",
|
||||||
|
"DoWhileStatement": ">=1",
|
||||||
|
"DoWhileStatementClosingBrace": 0,
|
||||||
|
"DoWhileStatementOpeningBrace": ">=1",
|
||||||
|
"ElseIfStatement": ">=1",
|
||||||
|
"ElseIfStatementClosingBrace": ">=1",
|
||||||
|
"ElseIfStatementOpeningBrace": ">=1",
|
||||||
|
"ElseStatement": ">=1",
|
||||||
|
"ElseStatementClosingBrace": ">=1",
|
||||||
|
"ElseStatementOpeningBrace": ">=1",
|
||||||
|
"EmptyStatement": -1,
|
||||||
|
"FinallyClosingBrace": ">=1",
|
||||||
|
"FinallyKeyword": -1,
|
||||||
|
"FinallyOpeningBrace": ">=1",
|
||||||
|
"ForInStatement": ">=1",
|
||||||
|
"ForInStatementClosingBrace": ">=1",
|
||||||
|
"ForInStatementExpressionClosing": -1,
|
||||||
|
"ForInStatementExpressionOpening": "<2",
|
||||||
|
"ForInStatementOpeningBrace": ">=1",
|
||||||
|
"ForStatement": ">=1",
|
||||||
|
"ForStatementClosingBrace": ">=1",
|
||||||
|
"ForStatementExpressionClosing": -1,
|
||||||
|
"ForStatementExpressionOpening": "<2",
|
||||||
|
"ForStatementOpeningBrace": ">=1",
|
||||||
|
"FunctionDeclaration": ">=1",
|
||||||
|
"FunctionDeclarationClosingBrace": ">=1",
|
||||||
|
"FunctionDeclarationOpeningBrace": ">=1",
|
||||||
|
"FunctionExpression": 0,
|
||||||
|
"FunctionExpressionClosingBrace": -1,
|
||||||
|
"FunctionExpressionOpeningBrace": 1,
|
||||||
|
"IIFEOpeningParentheses": 0,
|
||||||
|
"IfStatement": ">=1",
|
||||||
|
"IfStatementClosingBrace": ">=1",
|
||||||
|
"IfStatementOpeningBrace": ">=1",
|
||||||
|
"LogicalExpression": -1,
|
||||||
|
"MemberExpressionClosing": 0,
|
||||||
|
"MemberExpressionOpening": 0,
|
||||||
|
"MemberExpressionPeriod": 0,
|
||||||
|
"MethodDefinition": ">=1",
|
||||||
|
"ObjectExpressionOpeningBrace": "<=1",
|
||||||
|
"ObjectPatternClosingBrace": 0,
|
||||||
|
"ObjectPatternComma": 0,
|
||||||
|
"ObjectPatternOpeningBrace": 0,
|
||||||
|
"ParameterDefault": 0,
|
||||||
|
"Property": -1,
|
||||||
|
"PropertyName": 0,
|
||||||
|
"ReturnStatement": -1,
|
||||||
|
"SwitchCaseColon": ">=1",
|
||||||
|
"SwitchClosingBrace": ">=1",
|
||||||
|
"SwitchOpeningBrace": ">=1",
|
||||||
|
"ThisExpression": 0,
|
||||||
|
"ThrowStatement": ">=1",
|
||||||
|
"TryClosingBrace": 0,
|
||||||
|
"TryKeyword": -1,
|
||||||
|
"TryOpeningBrace": ">=1",
|
||||||
|
"VariableDeclaration": ">=1",
|
||||||
|
"VariableDeclarationSemiColon": ">=1",
|
||||||
|
"VariableValue": -1,
|
||||||
|
"WhileStatement": ">=1",
|
||||||
|
"WhileStatementClosingBrace": ">=1",
|
||||||
|
"WhileStatementOpeningBrace": ">=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"whiteSpace": {
|
||||||
|
"value": " ",
|
||||||
|
"removeTrailing": 1,
|
||||||
|
"before": {
|
||||||
|
"ArgumentComma": 0,
|
||||||
|
"ArgumentList": 0,
|
||||||
|
"ArgumentListArrayExpression": 0,
|
||||||
|
"ArgumentListFunctionExpression": 1,
|
||||||
|
"ArgumentListObjectExpression": 0,
|
||||||
|
"ArrayExpressionClosing": 0,
|
||||||
|
"ArrayExpressionComma": 0,
|
||||||
|
"ArrayExpressionOpening": 1,
|
||||||
|
"AssignmentOperator": 1,
|
||||||
|
"BinaryExpression": 0,
|
||||||
|
"BinaryExpressionOperator": 1,
|
||||||
|
"BlockComment": 1,
|
||||||
|
"CallExpression": 1,
|
||||||
|
"CatchClosingBrace": 1,
|
||||||
|
"CatchKeyword": 1,
|
||||||
|
"CatchOpeningBrace": 1,
|
||||||
|
"CatchParameterList": 0,
|
||||||
|
"CommaOperator": 0,
|
||||||
|
"ConditionalExpressionAlternate": 1,
|
||||||
|
"ConditionalExpressionConsequent": 1,
|
||||||
|
"DoWhileStatementClosingBrace": 1,
|
||||||
|
"DoWhileStatementConditional": 1,
|
||||||
|
"DoWhileStatementOpeningBrace": 1,
|
||||||
|
"ElseIfStatementClosingBrace": 1,
|
||||||
|
"ElseIfStatementOpeningBrace": 1,
|
||||||
|
"ElseStatementClosingBrace": 1,
|
||||||
|
"ElseStatementOpeningBrace": 1,
|
||||||
|
"EmptyStatement": 0,
|
||||||
|
"ExpressionClosingParentheses": 0,
|
||||||
|
"FinallyClosingBrace": 1,
|
||||||
|
"FinallyKeyword": -1,
|
||||||
|
"FinallyOpeningBrace": 1,
|
||||||
|
"ForInStatement": 1,
|
||||||
|
"ForInStatementClosingBrace": 1,
|
||||||
|
"ForInStatementExpressionClosing": 0,
|
||||||
|
"ForInStatementExpressionOpening": 1,
|
||||||
|
"ForInStatementOpeningBrace": 1,
|
||||||
|
"ForStatement": 1,
|
||||||
|
"ForStatementClosingBrace": 1,
|
||||||
|
"ForStatementExpressionClosing": 0,
|
||||||
|
"ForStatementExpressionOpening": 1,
|
||||||
|
"ForStatementOpeningBrace": 1,
|
||||||
|
"ForStatementSemicolon": 0,
|
||||||
|
"FunctionDeclarationClosingBrace": 1,
|
||||||
|
"FunctionDeclarationOpeningBrace": 1,
|
||||||
|
"FunctionExpressionClosingBrace": 1,
|
||||||
|
"FunctionExpressionOpeningBrace": 1,
|
||||||
|
"IfStatementClosingBrace": 1,
|
||||||
|
"IfStatementConditionalClosing": 0,
|
||||||
|
"IfStatementConditionalOpening": 1,
|
||||||
|
"IfStatementOpeningBrace": 1,
|
||||||
|
"LineComment": 1,
|
||||||
|
"LogicalExpressionOperator": 1,
|
||||||
|
"MemberExpressionClosing": 0,
|
||||||
|
"ObjectExpressionClosingBrace": 1,
|
||||||
|
"ParameterComma": 0,
|
||||||
|
"ParameterList": 0,
|
||||||
|
"Property": 1,
|
||||||
|
"PropertyName": 1,
|
||||||
|
"PropertyValue": 1,
|
||||||
|
"SwitchDiscriminantClosing": 0,
|
||||||
|
"SwitchDiscriminantOpening": 1,
|
||||||
|
"ThrowKeyword": 1,
|
||||||
|
"TryClosingBrace": 1,
|
||||||
|
"TryKeyword": -1,
|
||||||
|
"TryOpeningBrace": 1,
|
||||||
|
"UnaryExpressionOperator": 0,
|
||||||
|
"VariableName": 1,
|
||||||
|
"VariableValue": 1,
|
||||||
|
"WhileStatementClosingBrace": 1,
|
||||||
|
"WhileStatementConditionalClosing": 0,
|
||||||
|
"WhileStatementConditionalOpening": 1,
|
||||||
|
"WhileStatementOpeningBrace": 1
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"ArgumentComma": 1,
|
||||||
|
"ArgumentList": 0,
|
||||||
|
"ArgumentListArrayExpression": 1,
|
||||||
|
"ArgumentListFunctionExpression": 1,
|
||||||
|
"ArgumentListObjectExpression": 0,
|
||||||
|
"ArrayExpressionClosing": 0,
|
||||||
|
"ArrayExpressionComma": 1,
|
||||||
|
"ArrayExpressionOpening": 0,
|
||||||
|
"AssignmentOperator": 1,
|
||||||
|
"BinaryExpression": 0,
|
||||||
|
"BinaryExpressionOperator": 1,
|
||||||
|
"BlockComment": 1,
|
||||||
|
"CallExpression": 0,
|
||||||
|
"CatchClosingBrace": 1,
|
||||||
|
"CatchKeyword": 1,
|
||||||
|
"CatchOpeningBrace": 1,
|
||||||
|
"CatchParameterList": 0,
|
||||||
|
"CommaOperator": 1,
|
||||||
|
"ConditionalExpressionConsequent": 1,
|
||||||
|
"ConditionalExpressionTest": 1,
|
||||||
|
"DoWhileStatementBody": 1,
|
||||||
|
"DoWhileStatementClosingBrace": 1,
|
||||||
|
"DoWhileStatementOpeningBrace": 1,
|
||||||
|
"ElseIfStatementClosingBrace": 1,
|
||||||
|
"ElseIfStatementOpeningBrace": 1,
|
||||||
|
"ElseStatementClosingBrace": 1,
|
||||||
|
"ElseStatementOpeningBrace": 1,
|
||||||
|
"EmptyStatement": 0,
|
||||||
|
"ExpressionOpeningParentheses": 0,
|
||||||
|
"FinallyClosingBrace": 1,
|
||||||
|
"FinallyKeyword": -1,
|
||||||
|
"FinallyOpeningBrace": 1,
|
||||||
|
"ForInStatement": 1,
|
||||||
|
"ForInStatementClosingBrace": 1,
|
||||||
|
"ForInStatementExpressionClosing": 1,
|
||||||
|
"ForInStatementExpressionOpening": 0,
|
||||||
|
"ForInStatementOpeningBrace": 1,
|
||||||
|
"ForStatement": 1,
|
||||||
|
"ForStatementClosingBrace": 1,
|
||||||
|
"ForStatementExpressionClosing": 1,
|
||||||
|
"ForStatementExpressionOpening": 0,
|
||||||
|
"ForStatementOpeningBrace": 1,
|
||||||
|
"ForStatementSemicolon": 1,
|
||||||
|
"FunctionDeclarationClosingBrace": 0,
|
||||||
|
"FunctionDeclarationOpeningBrace": 0,
|
||||||
|
"FunctionExpressionClosingBrace": 0,
|
||||||
|
"FunctionExpressionOpeningBrace": 0,
|
||||||
|
"FunctionName": 0,
|
||||||
|
"FunctionReservedWord": 0,
|
||||||
|
"IfStatementClosingBrace": 1,
|
||||||
|
"IfStatementConditionalClosing": 0,
|
||||||
|
"IfStatementConditionalOpening": 0,
|
||||||
|
"IfStatementOpeningBrace": 1,
|
||||||
|
"LogicalExpressionOperator": 1,
|
||||||
|
"MemberExpressionOpening": 0,
|
||||||
|
"ObjectExpressionClosingBrace": 0,
|
||||||
|
"ObjectExpressionOpeningBrace": 1,
|
||||||
|
"ParameterComma": 1,
|
||||||
|
"ParameterList": 0,
|
||||||
|
"PropertyName": 0,
|
||||||
|
"PropertyValue": 0,
|
||||||
|
"SwitchDiscriminantClosing": 1,
|
||||||
|
"SwitchDiscriminantOpening": 0,
|
||||||
|
"ThrowKeyword": 1,
|
||||||
|
"TryClosingBrace": 1,
|
||||||
|
"TryKeyword": -1,
|
||||||
|
"TryOpeningBrace": 1,
|
||||||
|
"UnaryExpressionOperator": 0,
|
||||||
|
"VariableName": 1,
|
||||||
|
"WhileStatementClosingBrace": 1,
|
||||||
|
"WhileStatementConditionalClosing": 1,
|
||||||
|
"WhileStatementConditionalOpening": 0,
|
||||||
|
"WhileStatementOpeningBrace": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
**/JsLibraries/**
|
|
@ -0,0 +1,288 @@
|
||||||
|
{
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"commonjs": true,
|
||||||
|
"node": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"globals": {
|
||||||
|
"expect": false,
|
||||||
|
"chai": false,
|
||||||
|
"sinon": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 6,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"modules": true,
|
||||||
|
"impliedStrict": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"plugins": [
|
||||||
|
"filenames",
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
|
||||||
|
"rules": {
|
||||||
|
"filenames/match-exported": ["error"],
|
||||||
|
|
||||||
|
# ECMAScript 6
|
||||||
|
|
||||||
|
"arrow-body-style": [0],
|
||||||
|
"arrow-parens": ["error", "always"],
|
||||||
|
"arrow-spacing": ["error", { "before": true, "after": true }],
|
||||||
|
"constructor-super": "error",
|
||||||
|
"generator-star-spacing": "off",
|
||||||
|
"no-class-assign": "error",
|
||||||
|
"no-confusing-arrow": "error",
|
||||||
|
"no-const-assign": "error",
|
||||||
|
"no-dupe-class-members": "error",
|
||||||
|
"no-duplicate-imports": "error",
|
||||||
|
"no-new-symbol": "error",
|
||||||
|
"no-this-before-super": "error",
|
||||||
|
"no-useless-escape": "error",
|
||||||
|
"no-useless-computed-key": "error",
|
||||||
|
"no-useless-constructor": "error",
|
||||||
|
"no-var": "warn",
|
||||||
|
"object-shorthand": ["error", "properties"],
|
||||||
|
"prefer-arrow-callback": "error",
|
||||||
|
"prefer-const": "warn",
|
||||||
|
"prefer-reflect": "off",
|
||||||
|
"prefer-rest-params": "off",
|
||||||
|
"prefer-spread": "warn",
|
||||||
|
"prefer-template": "error",
|
||||||
|
"require-yield": "off",
|
||||||
|
"template-curly-spacing": ["error", "never"],
|
||||||
|
"yield-star-spacing": "off",
|
||||||
|
|
||||||
|
# Possible Errors
|
||||||
|
|
||||||
|
"comma-dangle": "error",
|
||||||
|
"no-cond-assign": "error",
|
||||||
|
"no-console": "off",
|
||||||
|
"no-constant-condition": "warn",
|
||||||
|
"no-control-regex": "error",
|
||||||
|
"no-debugger": "off",
|
||||||
|
"no-dupe-args": "error",
|
||||||
|
"no-dupe-keys": "error",
|
||||||
|
"no-duplicate-case": "error",
|
||||||
|
"no-empty": "warn",
|
||||||
|
"no-empty-character-class": "error",
|
||||||
|
"no-ex-assign": "error",
|
||||||
|
"no-extra-boolean-cast": "error",
|
||||||
|
"no-extra-parens": ["error", "functions"],
|
||||||
|
"no-extra-semi": "error",
|
||||||
|
"no-func-assign": "error",
|
||||||
|
"no-inner-declarations": "error",
|
||||||
|
"no-invalid-regexp": "error",
|
||||||
|
"no-irregular-whitespace": "error",
|
||||||
|
"no-negated-in-lhs": "error",
|
||||||
|
"no-obj-calls": "error",
|
||||||
|
"no-regex-spaces": "error",
|
||||||
|
"no-sparse-arrays": "error",
|
||||||
|
"no-unexpected-multiline": "error",
|
||||||
|
"no-unreachable": "warn",
|
||||||
|
"no-unsafe-finally": "error",
|
||||||
|
"use-isnan": "error",
|
||||||
|
"valid-jsdoc": "off",
|
||||||
|
"valid-typeof": "error",
|
||||||
|
|
||||||
|
# Best Practices
|
||||||
|
|
||||||
|
"accessor-pairs": "off",
|
||||||
|
"array-callback-return": "warn",
|
||||||
|
"block-scoped-var": "warn",
|
||||||
|
"consistent-return": "off",
|
||||||
|
"curly": "error",
|
||||||
|
"default-case": "error",
|
||||||
|
"dot-location": ["error", "property"],
|
||||||
|
"dot-notation": "error",
|
||||||
|
"eqeqeq": ["error", "smart"],
|
||||||
|
"guard-for-in": "error",
|
||||||
|
"no-alert": "warn",
|
||||||
|
"no-caller": "error",
|
||||||
|
"no-case-declarations": "error",
|
||||||
|
"no-div-regex": "error",
|
||||||
|
"no-else-return": "error",
|
||||||
|
"no-empty-function": ["error", {"allow": ["arrowFunctions"]}],
|
||||||
|
"no-empty-pattern": "error",
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-extend-native": "error",
|
||||||
|
"no-extra-bind": "error",
|
||||||
|
"no-fallthrough": "error",
|
||||||
|
"no-floating-decimal": "error",
|
||||||
|
"no-implicit-coercion": ["error", {
|
||||||
|
"boolean": false,
|
||||||
|
"number": true,
|
||||||
|
"string": true,
|
||||||
|
"allow": [/* "!!", "~", "*", "+" */]
|
||||||
|
}],
|
||||||
|
"no-implicit-globals": "error",
|
||||||
|
"no-implied-eval": "error",
|
||||||
|
"no-invalid-this": "off",
|
||||||
|
"no-iterator": "error",
|
||||||
|
"no-labels": "error",
|
||||||
|
"no-lone-blocks": "error",
|
||||||
|
"no-loop-func": "error",
|
||||||
|
"no-magic-numbers": ["off", {"ignoreArrayIndexes": true, "ignore": [0, 1] }],
|
||||||
|
"no-multi-spaces": "error",
|
||||||
|
"no-multi-str": "error",
|
||||||
|
"no-native-reassign": ["error", {"exceptions": ["console"]}],
|
||||||
|
"no-new": "off",
|
||||||
|
"no-new-func": "error",
|
||||||
|
"no-new-wrappers": "error",
|
||||||
|
"no-octal": "error",
|
||||||
|
"no-octal-escape": "error",
|
||||||
|
"no-param-reassign": "off",
|
||||||
|
"no-process-env": "off",
|
||||||
|
"no-proto": "error",
|
||||||
|
"no-redeclare": "error",
|
||||||
|
"no-return-assign": "warn",
|
||||||
|
"no-script-url": "error",
|
||||||
|
"no-self-assign": "error",
|
||||||
|
"no-self-compare": "error",
|
||||||
|
"no-sequences": "error",
|
||||||
|
"no-throw-literal": "error",
|
||||||
|
"no-unmodified-loop-condition": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"no-unused-labels": "error",
|
||||||
|
"no-useless-call": "error",
|
||||||
|
"no-useless-concat": "error",
|
||||||
|
"no-void": "error",
|
||||||
|
"no-warning-comments": "off",
|
||||||
|
"no-with": "error",
|
||||||
|
"radix": ["error", "as-needed"],
|
||||||
|
"vars-on-top": "off",
|
||||||
|
"wrap-iife": ["error", "inside"],
|
||||||
|
"yoda": "error",
|
||||||
|
|
||||||
|
# Strict Mode
|
||||||
|
|
||||||
|
"strict": ["error", "never"],
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
|
||||||
|
"init-declarations": ["error", "always"],
|
||||||
|
"no-catch-shadow": "error",
|
||||||
|
"no-delete-var": "error",
|
||||||
|
"no-label-var": "error",
|
||||||
|
"no-restricted-globals": "off",
|
||||||
|
"no-shadow": "error",
|
||||||
|
"no-shadow-restricted-names": "error",
|
||||||
|
"no-undef": "error",
|
||||||
|
"no-undef-init": "off",
|
||||||
|
"no-undefined": "off",
|
||||||
|
"no-unused-vars": ["error", { "args": "none", "ignoreRestSiblings": true }],
|
||||||
|
"no-use-before-define": "error",
|
||||||
|
|
||||||
|
# Node.js and CommonJS
|
||||||
|
|
||||||
|
"callback-return": "warn",
|
||||||
|
"global-require": "error",
|
||||||
|
"handle-callback-err": "warn",
|
||||||
|
"no-mixed-requires": "error",
|
||||||
|
"no-new-require": "error",
|
||||||
|
"no-path-concat": "error",
|
||||||
|
"no-process-exit": "error",
|
||||||
|
|
||||||
|
# Stylistic Issues
|
||||||
|
|
||||||
|
"array-bracket-spacing": ["error", "never"],
|
||||||
|
"block-spacing": ["error", "always"],
|
||||||
|
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
|
||||||
|
"camelcase": "off",
|
||||||
|
"comma-spacing": ["error", {"before": false, "after": true}],
|
||||||
|
"comma-style": ["error", "last"],
|
||||||
|
"computed-property-spacing": ["error", "never"],
|
||||||
|
"consistent-this": ["error", "self"],
|
||||||
|
"eol-last": "error",
|
||||||
|
"func-names": "off",
|
||||||
|
"func-style": ["error", "declaration"],
|
||||||
|
"indent": ["error", 2, {"SwitchCase": 1}],
|
||||||
|
"key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
|
||||||
|
"keyword-spacing": ["error", { "before": true, "after": true}],
|
||||||
|
"lines-around-comment": ["error", { "beforeBlockComment": true, "afterBlockComment": false }],
|
||||||
|
"max-depth": ["error", {"maximum": 5}],
|
||||||
|
"max-nested-callbacks": ["error", 4],
|
||||||
|
"max-params": ["error", 6],
|
||||||
|
"max-statements": "off",
|
||||||
|
"max-statements-per-line": ["error", { "max": 1 }],
|
||||||
|
"new-cap": ["error", {"capIsNewExceptions": ["$.Deferred", "DragDropContext", "DragLayer", "DragSource", "DropTarget"]}],
|
||||||
|
"new-parens": "error",
|
||||||
|
"newline-after-var": "off",
|
||||||
|
"newline-before-return": "off",
|
||||||
|
"newline-per-chained-call": "off",
|
||||||
|
"no-array-constructor": "error",
|
||||||
|
"no-bitwise": "error",
|
||||||
|
"no-continue": "error",
|
||||||
|
"no-inline-comments": "off",
|
||||||
|
"no-lonely-if": "warn",
|
||||||
|
"no-mixed-spaces-and-tabs": "error",
|
||||||
|
"no-multiple-empty-lines": ["error", { "max": 1 }],
|
||||||
|
"no-negated-condition": "warn",
|
||||||
|
"no-nested-ternary": "error",
|
||||||
|
"no-new-object": "error",
|
||||||
|
"no-plusplus": "off",
|
||||||
|
"no-restricted-syntax": "off",
|
||||||
|
"no-spaced-func": "error",
|
||||||
|
"no-ternary": "off",
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
|
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
|
||||||
|
"no-unneeded-ternary": "error",
|
||||||
|
"no-whitespace-before-property": "error",
|
||||||
|
"object-curly-spacing": ["error", "always"],
|
||||||
|
"one-var": ["error", "never"],
|
||||||
|
"one-var-declaration-per-line": ["error", "always"],
|
||||||
|
"operator-assignment": ["off", "never"],
|
||||||
|
"operator-linebreak": ["error", "after"],
|
||||||
|
"quote-props": ["error", "as-needed"],
|
||||||
|
"quotes": ["error", "single"],
|
||||||
|
"require-jsdoc": "off",
|
||||||
|
"semi": "error",
|
||||||
|
"semi-spacing": ["error", { "before": false, "after": true }],
|
||||||
|
"sort-vars": "off",
|
||||||
|
"space-before-blocks": ["error", "always"],
|
||||||
|
"space-before-function-paren": ["error", "never"],
|
||||||
|
"space-in-parens": "off",
|
||||||
|
"space-infix-ops": "off",
|
||||||
|
"space-unary-ops": "off",
|
||||||
|
"spaced-comment": "error",
|
||||||
|
"wrap-regex": "error",
|
||||||
|
|
||||||
|
# React
|
||||||
|
|
||||||
|
"react/jsx-boolean-value": [2, "always"],
|
||||||
|
"react/jsx-uses-vars": 2,
|
||||||
|
"react/jsx-closing-bracket-location": 2,
|
||||||
|
"react/jsx-tag-spacing": ["error"],
|
||||||
|
"react/jsx-curly-spacing": [2, "never"],
|
||||||
|
"react/jsx-equals-spacing": [2, "never"],
|
||||||
|
"react/jsx-indent-props": [2, 2],
|
||||||
|
"react/jsx-indent": [2, 2],
|
||||||
|
"react/jsx-key": 2,
|
||||||
|
"react/jsx-no-bind": [2, { "allowArrowFunctions": true }],
|
||||||
|
"react/jsx-no-duplicate-props": [2, { "ignoreCase": true }],
|
||||||
|
"react/jsx-max-props-per-line": [2, { "maximum": 2 }],
|
||||||
|
"react/jsx-handler-names": [2, { "eventHandlerPrefix": "(on|dispatch)", "eventHandlerPropPrefix": "on" }],
|
||||||
|
"react/jsx-no-undef": 2,
|
||||||
|
"react/jsx-pascal-case": 2,
|
||||||
|
"react/jsx-uses-react": 2,
|
||||||
|
// Explicitly disabled in case we want to enable them again
|
||||||
|
"react/no-did-mount-set-state": 0,
|
||||||
|
"react/no-did-update-set-state": 0,
|
||||||
|
"react/no-direct-mutation-state": 2,
|
||||||
|
"react/no-multi-comp": [2, { "ignoreStateless": true }],
|
||||||
|
"react/no-unknown-property": 2,
|
||||||
|
"react/prefer-es6-class": 2,
|
||||||
|
"react/prop-types": 2,
|
||||||
|
"react/react-in-jsx-scope": 2,
|
||||||
|
"react/self-closing-comp": 2,
|
||||||
|
"react/sort-comp": 2,
|
||||||
|
"react/jsx-wrap-multilines": 2
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"js": {
|
||||||
|
"indent_size": 2,
|
||||||
|
"indent_char": " ",
|
||||||
|
"indent_level": 2,
|
||||||
|
"indent_with_tabs": false,
|
||||||
|
"preserve_newlines": true,
|
||||||
|
"brace_style": "collapse",
|
||||||
|
"max_preserve_newlines": 2,
|
||||||
|
"jslint_happy": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,396 @@
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"stylelint-order"
|
||||||
|
],
|
||||||
|
"ignoreFiles": [
|
||||||
|
"frontend/src/Styles/scaffolding.css",
|
||||||
|
"**/*.js"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"at-rule-empty-line-before": [
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"except": [
|
||||||
|
"inside-block"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"at-rule-name-case": "lower",
|
||||||
|
"at-rule-name-newline-after": "always-multi-line",
|
||||||
|
"at-rule-name-space-after": "always",
|
||||||
|
"at-rule-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignoreAtRules": [
|
||||||
|
"/^add\\-mixin$/",
|
||||||
|
"/^define\\-mixin$/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"at-rule-no-vendor-prefix": true,
|
||||||
|
"at-rule-semicolon-newline-after": "always",
|
||||||
|
"at-rule-semicolon-space-before": "never",
|
||||||
|
"block-closing-brace-empty-line-before": "never",
|
||||||
|
"block-closing-brace-newline-after": "always",
|
||||||
|
"block-closing-brace-newline-before": "always",
|
||||||
|
"block-closing-brace-space-after": "always-single-line",
|
||||||
|
"block-closing-brace-space-before": "always-single-line",
|
||||||
|
"block-no-empty": true,
|
||||||
|
"block-opening-brace-newline-after": "always",
|
||||||
|
"block-opening-brace-newline-before": "never-single-line",
|
||||||
|
"block-opening-brace-space-after": "always-single-line",
|
||||||
|
"block-opening-brace-space-before": "always",
|
||||||
|
"color-hex-case": "lower",
|
||||||
|
"color-hex-length": "short",
|
||||||
|
"color-named": "never",
|
||||||
|
"color-no-invalid-hex": true,
|
||||||
|
"comment-whitespace-inside": "always",
|
||||||
|
"declaration-bang-space-after": "never",
|
||||||
|
"declaration-bang-space-before": "always",
|
||||||
|
"declaration-block-no-duplicate-properties": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignoreProperties": [
|
||||||
|
"composes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"declaration-block-no-redundant-longhand-properties": true,
|
||||||
|
"declaration-block-no-shorthand-property-overrides": true,
|
||||||
|
"declaration-block-semicolon-newline-after": "always",
|
||||||
|
"declaration-block-semicolon-newline-before": "never-multi-line",
|
||||||
|
"declaration-block-semicolon-space-before": "never",
|
||||||
|
"declaration-block-single-line-max-declarations": 1,
|
||||||
|
"declaration-block-trailing-semicolon": "always",
|
||||||
|
"declaration-colon-space-after": "always",
|
||||||
|
"declaration-colon-space-before": "never",
|
||||||
|
"font-family-name-quotes": "always-unless-keyword",
|
||||||
|
"function-calc-no-unspaced-operator": true,
|
||||||
|
"function-comma-newline-after": "never-multi-line",
|
||||||
|
"function-comma-newline-before": "never-multi-line",
|
||||||
|
"function-comma-space-after": "always",
|
||||||
|
"function-comma-space-before": "never",
|
||||||
|
"function-linear-gradient-no-nonstandard-direction": true,
|
||||||
|
"function-name-case": "lower",
|
||||||
|
"function-parentheses-newline-inside": "never-multi-line",
|
||||||
|
"function-parentheses-space-inside": "never",
|
||||||
|
"function-url-quotes": "always",
|
||||||
|
"function-url-scheme-blacklist": [
|
||||||
|
"data"
|
||||||
|
],
|
||||||
|
"function-whitespace-after": "always",
|
||||||
|
"indentation": 2,
|
||||||
|
"keyframe-declaration-no-important": true,
|
||||||
|
"length-zero-no-unit": true,
|
||||||
|
"max-empty-lines": 1,
|
||||||
|
"max-line-length": [
|
||||||
|
100,
|
||||||
|
{
|
||||||
|
"ignore": [
|
||||||
|
"non-comments"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max-nesting-depth": 2,
|
||||||
|
"media-feature-colon-space-after": "always",
|
||||||
|
"media-feature-colon-space-before": "never",
|
||||||
|
"media-feature-name-case": "lower",
|
||||||
|
"media-feature-name-no-vendor-prefix": true,
|
||||||
|
"media-feature-range-operator-space-after": "always",
|
||||||
|
"media-feature-range-operator-space-before": "always",
|
||||||
|
"no-empty-source": true,
|
||||||
|
"no-eol-whitespace": true,
|
||||||
|
"no-extra-semicolons": true,
|
||||||
|
"no-invalid-double-slash-comments": true,
|
||||||
|
"no-missing-end-of-source-newline": true,
|
||||||
|
"number-leading-zero": "always",
|
||||||
|
"number-no-trailing-zeros": true,
|
||||||
|
"order/order": [
|
||||||
|
"custom-properties",
|
||||||
|
"dollar-variables",
|
||||||
|
{
|
||||||
|
"hasBlock": false,
|
||||||
|
"name": "add-mixin",
|
||||||
|
"type": "at-rule"
|
||||||
|
},
|
||||||
|
"declarations",
|
||||||
|
"rules",
|
||||||
|
"at-rules"
|
||||||
|
],
|
||||||
|
"order/properties-order": [
|
||||||
|
{
|
||||||
|
"emptyLineBefore": "always",
|
||||||
|
"properties": [
|
||||||
|
"composes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"emptyLineBefore": "always",
|
||||||
|
"properties": [
|
||||||
|
"position",
|
||||||
|
"top",
|
||||||
|
"right",
|
||||||
|
"bottom",
|
||||||
|
"left",
|
||||||
|
"z-index",
|
||||||
|
"display",
|
||||||
|
"visibility",
|
||||||
|
"align-content",
|
||||||
|
"align-items",
|
||||||
|
"align-self",
|
||||||
|
"justify-content",
|
||||||
|
"flex",
|
||||||
|
"flex-direction",
|
||||||
|
"flex-order",
|
||||||
|
"flex-pack",
|
||||||
|
"flex-align",
|
||||||
|
"flex-grow",
|
||||||
|
"flex-shrink",
|
||||||
|
"flex-basis",
|
||||||
|
"flex-wrap",
|
||||||
|
"flex-flow",
|
||||||
|
"float",
|
||||||
|
"clear",
|
||||||
|
"overflow",
|
||||||
|
"overflow-x",
|
||||||
|
"overflow-y",
|
||||||
|
"-webkit-overflow-scrolling",
|
||||||
|
"clip",
|
||||||
|
"box-sizing",
|
||||||
|
"margin",
|
||||||
|
"margin-top",
|
||||||
|
"margin-right",
|
||||||
|
"margin-bottom",
|
||||||
|
"margin-left",
|
||||||
|
"padding",
|
||||||
|
"padding-top",
|
||||||
|
"padding-right",
|
||||||
|
"padding-bottom",
|
||||||
|
"padding-left",
|
||||||
|
"min-width",
|
||||||
|
"min-height",
|
||||||
|
"max-width",
|
||||||
|
"max-height",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"outline",
|
||||||
|
"outline-width",
|
||||||
|
"outline-style",
|
||||||
|
"outline-color",
|
||||||
|
"outline-offset",
|
||||||
|
"border",
|
||||||
|
"border-spacing",
|
||||||
|
"border-collapse",
|
||||||
|
"border-width",
|
||||||
|
"border-style",
|
||||||
|
"border-color",
|
||||||
|
"border-top",
|
||||||
|
"border-top-width",
|
||||||
|
"border-top-style",
|
||||||
|
"border-top-color",
|
||||||
|
"border-right",
|
||||||
|
"border-right-width",
|
||||||
|
"border-right-style",
|
||||||
|
"border-right-color",
|
||||||
|
"border-bottom",
|
||||||
|
"border-bottom-width",
|
||||||
|
"border-bottom-style",
|
||||||
|
"border-bottom-color",
|
||||||
|
"border-left",
|
||||||
|
"border-left-width",
|
||||||
|
"border-left-style",
|
||||||
|
"border-left-color",
|
||||||
|
"border-radius",
|
||||||
|
"border-top-left-radius",
|
||||||
|
"border-top-right-radius",
|
||||||
|
"border-bottom-right-radius",
|
||||||
|
"border-bottom-left-radius",
|
||||||
|
"border-image",
|
||||||
|
"border-image-source",
|
||||||
|
"border-image-slice",
|
||||||
|
"border-image-width",
|
||||||
|
"border-image-outset",
|
||||||
|
"border-image-repeat",
|
||||||
|
"border-top-image",
|
||||||
|
"border-right-image",
|
||||||
|
"border-bottom-image",
|
||||||
|
"border-left-image",
|
||||||
|
"border-corner-image",
|
||||||
|
"border-top-left-image",
|
||||||
|
"border-top-right-image",
|
||||||
|
"border-bottom-right-image",
|
||||||
|
"border-bottom-left-image",
|
||||||
|
"background",
|
||||||
|
"background-color",
|
||||||
|
"background-image",
|
||||||
|
"background-attachment",
|
||||||
|
"background-position",
|
||||||
|
"background-position-x",
|
||||||
|
"background-position-y",
|
||||||
|
"background-clip",
|
||||||
|
"background-origin",
|
||||||
|
"background-size",
|
||||||
|
"background-repeat",
|
||||||
|
"box-decoration-break",
|
||||||
|
"box-shadow",
|
||||||
|
"color",
|
||||||
|
"table-layout",
|
||||||
|
"caption-side",
|
||||||
|
"empty-cells",
|
||||||
|
"list-style",
|
||||||
|
"list-style-position",
|
||||||
|
"list-style-type",
|
||||||
|
"list-style-image",
|
||||||
|
"quotes",
|
||||||
|
"content",
|
||||||
|
"counter-increment",
|
||||||
|
"counter-reset",
|
||||||
|
"-ms-writing-mode",
|
||||||
|
"vertical-align",
|
||||||
|
"text-align",
|
||||||
|
"text-align-last",
|
||||||
|
"text-decoration",
|
||||||
|
"text-emphasis",
|
||||||
|
"text-emphasis-position",
|
||||||
|
"text-emphasis-style",
|
||||||
|
"text-emphasis-color",
|
||||||
|
"text-indent",
|
||||||
|
"text-justify",
|
||||||
|
"text-outline",
|
||||||
|
"text-transform",
|
||||||
|
"text-wrap",
|
||||||
|
"text-overflow",
|
||||||
|
"text-overflow-ellipsis",
|
||||||
|
"text-overflow-mode",
|
||||||
|
"text-shadow",
|
||||||
|
"white-space",
|
||||||
|
"word-spacing",
|
||||||
|
"word-wrap",
|
||||||
|
"word-break",
|
||||||
|
"tab-size",
|
||||||
|
"hyphens",
|
||||||
|
"letter-spacing",
|
||||||
|
"font",
|
||||||
|
"font-weight",
|
||||||
|
"font-style",
|
||||||
|
"font-variant",
|
||||||
|
"font-size-adjust",
|
||||||
|
"font-stretch",
|
||||||
|
"font-size",
|
||||||
|
"font-family",
|
||||||
|
"font-smoothing",
|
||||||
|
"-moz-osx-font-smoothing",
|
||||||
|
"-webkit-font-smoothing",
|
||||||
|
"src",
|
||||||
|
"line-height",
|
||||||
|
"opacity",
|
||||||
|
"filter",
|
||||||
|
"resize",
|
||||||
|
"cursor",
|
||||||
|
"appearance",
|
||||||
|
"nav-index",
|
||||||
|
"nav-up",
|
||||||
|
"nav-right",
|
||||||
|
"nav-down",
|
||||||
|
"nav-left",
|
||||||
|
"transition",
|
||||||
|
"transition-delay",
|
||||||
|
"transition-timing-function",
|
||||||
|
"transition-duration",
|
||||||
|
"transition-property",
|
||||||
|
"transform",
|
||||||
|
"transform-origin",
|
||||||
|
"transform-style",
|
||||||
|
"backface-visibility",
|
||||||
|
"animation",
|
||||||
|
"animation-name",
|
||||||
|
"animation-duration",
|
||||||
|
"animation-play-state",
|
||||||
|
"animation-timing-function",
|
||||||
|
"animation-delay",
|
||||||
|
"animation-iteration-count",
|
||||||
|
"animation-direction",
|
||||||
|
"animation-fill-mode",
|
||||||
|
"pointer-events",
|
||||||
|
"user-select",
|
||||||
|
"touch-action",
|
||||||
|
"-webkit-tap-highlight-color",
|
||||||
|
"unicode-bidi",
|
||||||
|
"direction",
|
||||||
|
"columns",
|
||||||
|
"column-span",
|
||||||
|
"column-width",
|
||||||
|
"column-count",
|
||||||
|
"column-fill",
|
||||||
|
"column-gap",
|
||||||
|
"column-rule",
|
||||||
|
"column-rule-width",
|
||||||
|
"column-rule-style",
|
||||||
|
"column-rule-color",
|
||||||
|
"break-before",
|
||||||
|
"break-inside",
|
||||||
|
"break-after",
|
||||||
|
"page-break-before",
|
||||||
|
"page-break-inside",
|
||||||
|
"page-break-after",
|
||||||
|
"orphans",
|
||||||
|
"widows",
|
||||||
|
"zoom",
|
||||||
|
"max-zoom",
|
||||||
|
"min-zoom",
|
||||||
|
"user-zoom",
|
||||||
|
"orientation"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"property-case": "lower",
|
||||||
|
"property-no-vendor-prefix": true,
|
||||||
|
"rule-empty-line-before": [
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"except": [
|
||||||
|
"first-nested"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"after-comment"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selector-attribute-brackets-space-inside": "never",
|
||||||
|
"selector-attribute-operator-space-after": "never",
|
||||||
|
"selector-attribute-operator-space-before": "never",
|
||||||
|
"selector-attribute-quotes": "never",
|
||||||
|
"selector-class-pattern": "^[A-Za-z0-9]+$",
|
||||||
|
"selector-combinator-space-after": "always",
|
||||||
|
"selector-combinator-space-before": "always",
|
||||||
|
"selector-descendant-combinator-no-non-space": true,
|
||||||
|
"selector-list-comma-newline-after": "always",
|
||||||
|
"selector-list-comma-newline-before": "never-multi-line",
|
||||||
|
"selector-list-comma-space-before": "never",
|
||||||
|
"selector-max-attribute": 0,
|
||||||
|
"selector-max-class": 3,
|
||||||
|
"selector-max-compound-selectors": 3,
|
||||||
|
"selector-max-empty-lines": 0,
|
||||||
|
"selector-max-id": 0,
|
||||||
|
"selector-max-universal": 0,
|
||||||
|
"selector-pseudo-class-case": "lower",
|
||||||
|
"selector-pseudo-class-parentheses-space-inside": "never",
|
||||||
|
"selector-pseudo-element-case": "lower",
|
||||||
|
"selector-pseudo-element-colon-notation": "double",
|
||||||
|
"selector-pseudo-element-no-unknown": true,
|
||||||
|
"selector-type-case": "lower",
|
||||||
|
"selector-type-no-unknown": true,
|
||||||
|
"shorthand-property-no-redundant-values": true,
|
||||||
|
"string-no-newline": true,
|
||||||
|
"string-quotes": "single",
|
||||||
|
"time-min-milliseconds": 100,
|
||||||
|
"unit-case": "lower",
|
||||||
|
"unit-no-unknown": true,
|
||||||
|
"value-list-comma-newline-after": "never-multi-line",
|
||||||
|
"value-list-comma-newline-before": "never-multi-line",
|
||||||
|
"value-list-comma-space-after": "always",
|
||||||
|
"value-list-comma-space-before": "never",
|
||||||
|
"value-list-max-empty-lines": 0,
|
||||||
|
"value-no-vendor-prefix": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"ecmaVersion": 6,
|
||||||
|
"libs": [
|
||||||
|
"browser",
|
||||||
|
"jquery"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
const gulp = require('gulp');
|
||||||
|
const runSequence = require('run-sequence');
|
||||||
|
|
||||||
|
require('./clean');
|
||||||
|
require('./copy');
|
||||||
|
|
||||||
|
gulp.task('build', () => {
|
||||||
|
return runSequence('clean', [
|
||||||
|
'webpack',
|
||||||
|
'copyHtml',
|
||||||
|
'copyFonts',
|
||||||
|
'copyImages',
|
||||||
|
'copyJs'
|
||||||
|
]);
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
const gulp = require('gulp');
|
||||||
|
const del = require('del');
|
||||||
|
|
||||||
|
const paths = require('./helpers/paths');
|
||||||
|
|
||||||
|
gulp.task('clean', () => {
|
||||||
|
return del([paths.dest.root]);
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
var path = require('path');
|
||||||
|
var gulp = require('gulp');
|
||||||
|
var print = require('gulp-print').default;
|
||||||
|
var cache = require('gulp-cached');
|
||||||
|
var livereload = require('gulp-livereload');
|
||||||
|
var paths = require('./helpers/paths.js');
|
||||||
|
|
||||||
|
gulp.task('copyJs', () => {
|
||||||
|
return gulp.src(
|
||||||
|
[
|
||||||
|
path.join(paths.src.root, 'polyfills.js')
|
||||||
|
])
|
||||||
|
.pipe(cache('copyJs'))
|
||||||
|
.pipe(print())
|
||||||
|
.pipe(gulp.dest(paths.dest.root))
|
||||||
|
.pipe(livereload());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('copyHtml', () => {
|
||||||
|
return gulp.src(paths.src.html)
|
||||||
|
.pipe(cache('copyHtml'))
|
||||||
|
.pipe(print())
|
||||||
|
.pipe(gulp.dest(paths.dest.root))
|
||||||
|
.pipe(livereload());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('copyFonts', () => {
|
||||||
|
return gulp.src(
|
||||||
|
path.join(paths.src.fonts, '**', '*.*')
|
||||||
|
)
|
||||||
|
.pipe(cache('copyFonts'))
|
||||||
|
.pipe(print())
|
||||||
|
.pipe(gulp.dest(paths.dest.fonts))
|
||||||
|
.pipe(livereload());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('copyImages', () => {
|
||||||
|
return gulp.src(
|
||||||
|
path.join(paths.src.images, '**', '*.*')
|
||||||
|
)
|
||||||
|
.pipe(cache('copyImages'))
|
||||||
|
.pipe(print())
|
||||||
|
.pipe(gulp.dest(paths.dest.images))
|
||||||
|
.pipe(livereload());
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
require('./build.js');
|
||||||
|
require('./clean.js');
|
||||||
|
require('./copy.js');
|
||||||
|
require('./handlebars.js');
|
||||||
|
require('./imageMin.js');
|
||||||
|
require('./less.js');
|
||||||
|
require('./start.js');
|
||||||
|
require('./stripBom.js');
|
||||||
|
require('./watch.js');
|
||||||
|
require('./webpack.js');
|
|
@ -0,0 +1,70 @@
|
||||||
|
var gulp = require('gulp');
|
||||||
|
var handlebars = require('gulp-handlebars');
|
||||||
|
var declare = require('gulp-declare');
|
||||||
|
var concat = require('gulp-concat');
|
||||||
|
var wrap = require('gulp-wrap');
|
||||||
|
var livereload = require('gulp-livereload');
|
||||||
|
var path = require('path');
|
||||||
|
var streamqueue = require('streamqueue');
|
||||||
|
var stripbom = require('gulp-stripbom');
|
||||||
|
var compliler = require('handlebars');
|
||||||
|
|
||||||
|
var errorHandler = require('./helpers/errorHandler');
|
||||||
|
var paths = require('./helpers/paths.js');
|
||||||
|
|
||||||
|
console.log('Handlebars (gulp) Version: ', compliler.VERSION);
|
||||||
|
console.log('Handlebars (gulp) Compiler: ', compliler.COMPILER_REVISION);
|
||||||
|
|
||||||
|
gulp.task('handlebars', () => {
|
||||||
|
var coreStream = gulp.src([
|
||||||
|
paths.src.templates,
|
||||||
|
'!*/**/*Partial.*'
|
||||||
|
])
|
||||||
|
.pipe(stripbom({
|
||||||
|
showLog: false
|
||||||
|
}))
|
||||||
|
.pipe(handlebars({
|
||||||
|
handlebars: compliler
|
||||||
|
}))
|
||||||
|
.on('error', errorHandler)
|
||||||
|
.pipe(declare({
|
||||||
|
namespace: 'T',
|
||||||
|
noRedeclare: true,
|
||||||
|
processName: (filePath) => {
|
||||||
|
filePath = path.relative(paths.src.root, filePath);
|
||||||
|
|
||||||
|
return filePath.replace(/\\/g, '/')
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.replace('template', '')
|
||||||
|
.replace('.js', '');
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
var partialStream = gulp.src([paths.src.partials])
|
||||||
|
.pipe(stripbom({
|
||||||
|
showLog: false
|
||||||
|
}))
|
||||||
|
.pipe(handlebars({
|
||||||
|
handlebars: compliler
|
||||||
|
}))
|
||||||
|
.on('error', errorHandler)
|
||||||
|
.pipe(wrap('Handlebars.template(<%= contents %>)'))
|
||||||
|
.pipe(wrap('Handlebars.registerPartial(<%= processPartialName(file.relative) %>, <%= contents %>)', {}, {
|
||||||
|
imports: {
|
||||||
|
processPartialName: function(fileName) {
|
||||||
|
return JSON.stringify(
|
||||||
|
path.basename(fileName, '.js')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return streamqueue({
|
||||||
|
objectMode: true
|
||||||
|
},
|
||||||
|
partialStream,
|
||||||
|
coreStream
|
||||||
|
).pipe(concat('templates.js'))
|
||||||
|
.pipe(gulp.dest(paths.dest.root))
|
||||||
|
.pipe(livereload());
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
const gulpUtil = require('gulp-util');
|
||||||
|
|
||||||
|
module.exports = function errorHandler(error) {
|
||||||
|
gulpUtil.log(gulpUtil.colors.red(`Error (${error.plugin}): ${error.message}`));
|
||||||
|
this.emit('end');
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
const path = require('path');
|
||||||
|
const rootPath = path.resolve(__dirname + '/../../src/');
|
||||||
|
module.exports = function(source) {
|
||||||
|
if (this.cacheable) {
|
||||||
|
this.cacheable();
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourcePath = this.resourcePath.replace(rootPath, '');
|
||||||
|
const wrappedSource =`
|
||||||
|
<!-- begin ${resourcePath} -->
|
||||||
|
${source}
|
||||||
|
<!-- end ${resourcePath} -->`;
|
||||||
|
|
||||||
|
return wrappedSource;
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
const root = './frontend/src/';
|
||||||
|
|
||||||
|
const paths = {
|
||||||
|
src: {
|
||||||
|
root,
|
||||||
|
templates: root + '**/*.hbs',
|
||||||
|
html: root + '*.html',
|
||||||
|
partials: root + '**/*Partial.hbs',
|
||||||
|
scripts: root + '**/*.js',
|
||||||
|
less: [root + '**/*.less'],
|
||||||
|
content: root + 'Content/',
|
||||||
|
fonts: root + 'Content/Fonts/',
|
||||||
|
images: root + 'Content/Images/',
|
||||||
|
exclude: {
|
||||||
|
libs: `!${root}JsLibraries/**`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dest: {
|
||||||
|
root: './_output/UI.Phantom/',
|
||||||
|
content: './_output/UI.Phantom/Content/',
|
||||||
|
fonts: './_output/UI.Phantom/Content/Fonts/',
|
||||||
|
images: './_output/UI.Phantom/Content/Images/'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = paths;
|
|
@ -0,0 +1,10 @@
|
||||||
|
var phantom = false;
|
||||||
|
process.argv.forEach((val) => {
|
||||||
|
if (val === '--phantom') {
|
||||||
|
phantom = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Phantom:', phantom);
|
||||||
|
|
||||||
|
module.exports = phantom;
|
|
@ -0,0 +1,15 @@
|
||||||
|
var gulp = require('gulp');
|
||||||
|
var print = require('gulp-print').default;
|
||||||
|
var paths = require('./helpers/paths.js');
|
||||||
|
|
||||||
|
gulp.task('imageMin', () => {
|
||||||
|
var imagemin = require('gulp-imagemin');
|
||||||
|
return gulp.src(paths.src.images)
|
||||||
|
.pipe(imagemin({
|
||||||
|
progressive: false,
|
||||||
|
optimizationLevel: 4,
|
||||||
|
svgoPlugins: [{ removeViewBox: false }]
|
||||||
|
}))
|
||||||
|
.pipe(print())
|
||||||
|
.pipe(gulp.dest(paths.src.content + 'Images/'));
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
const gulp = require('gulp');
|
||||||
|
|
||||||
|
const less = require('gulp-less');
|
||||||
|
const postcss = require('gulp-postcss');
|
||||||
|
const sourcemaps = require('gulp-sourcemaps');
|
||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
const livereload = require('gulp-livereload');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const print = require('gulp-print');
|
||||||
|
const paths = require('./helpers/paths');
|
||||||
|
const errorHandler = require('./helpers/errorHandler');
|
||||||
|
|
||||||
|
gulp.task('less', () => {
|
||||||
|
const src = [
|
||||||
|
path.join(paths.src.content, 'Bootstrap', 'bootstrap.less'),
|
||||||
|
path.join(paths.src.content, 'Vendor', 'vendor.less'),
|
||||||
|
path.join(paths.src.content, 'sonarr.less')
|
||||||
|
];
|
||||||
|
|
||||||
|
return gulp.src(src)
|
||||||
|
.pipe(print())
|
||||||
|
.pipe(sourcemaps.init())
|
||||||
|
.pipe(less({
|
||||||
|
paths: [paths.src.root],
|
||||||
|
dumpLineNumbers: 'false',
|
||||||
|
compress: true,
|
||||||
|
yuicompress: true,
|
||||||
|
ieCompat: true,
|
||||||
|
strictImports: true
|
||||||
|
}))
|
||||||
|
.on('error', errorHandler)
|
||||||
|
.pipe(postcss([autoprefixer({
|
||||||
|
browsers: ['last 2 versions']
|
||||||
|
})]))
|
||||||
|
.on('error', errorHandler)
|
||||||
|
|
||||||
|
// not providing a path will cause the source map
|
||||||
|
// to be embeded. which makes livereload much happier
|
||||||
|
// since it doesn't reload the whole page to load the map.
|
||||||
|
// this should be switched to sourcemaps.write('./') for production builds
|
||||||
|
.pipe(sourcemaps.write())
|
||||||
|
.pipe(gulp.dest(paths.dest.content))
|
||||||
|
.on('error', errorHandler)
|
||||||
|
.pipe(livereload());
|
||||||
|
});
|
|
@ -0,0 +1,104 @@
|
||||||
|
// will download and run sonarr (server) in a non-windows enviroment
|
||||||
|
// you can use this if you don't care about the server code and just want to work
|
||||||
|
// with the web code.
|
||||||
|
|
||||||
|
var http = require('http');
|
||||||
|
var gulp = require('gulp');
|
||||||
|
var fs = require('fs');
|
||||||
|
var targz = require('tar.gz');
|
||||||
|
var del = require('del');
|
||||||
|
var spawn = require('child_process').spawn;
|
||||||
|
|
||||||
|
function download(url, dest, cb) {
|
||||||
|
console.log('Downloading ' + url + ' to ' + dest);
|
||||||
|
var file = fs.createWriteStream(dest);
|
||||||
|
http.get(url, function(response) {
|
||||||
|
response.pipe(file);
|
||||||
|
file.on('finish', function() {
|
||||||
|
console.log('Download completed');
|
||||||
|
file.close(cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatest(cb) {
|
||||||
|
var branch = 'develop';
|
||||||
|
process.argv.forEach(function(val) {
|
||||||
|
var branchMatch = /branch=([\S]*)/.exec(val);
|
||||||
|
if (branchMatch && branchMatch.length > 1) {
|
||||||
|
branch = branchMatch[1];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var url = 'http://services.sonarr.tv/v1/update/' + branch + '?os=osx';
|
||||||
|
|
||||||
|
console.log('Checking for latest version:', url);
|
||||||
|
|
||||||
|
http.get(url, function(res) {
|
||||||
|
var data = '';
|
||||||
|
|
||||||
|
res.on('data', function(chunk) {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', function() {
|
||||||
|
var updatePackage = JSON.parse(data).updatePackage;
|
||||||
|
console.log('Latest version available: ' + updatePackage.version + ' Release Date: ' + updatePackage.releaseDate);
|
||||||
|
cb(updatePackage);
|
||||||
|
});
|
||||||
|
}).on('error', function(e) {
|
||||||
|
console.log('problem with request: ' + e.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function extract(source, dest, cb) {
|
||||||
|
console.log('extracting download page to ' + dest);
|
||||||
|
new targz().extract(source, dest, function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
console.log('Update package extracted.');
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
gulp.task('getSonarr', function() {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync('./_start/');
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code !== 'EEXIST') {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLatest(function(updatePackage) {
|
||||||
|
var packagePath = './_start/' + updatePackage.filename;
|
||||||
|
var dirName = './_start/' + updatePackage.version;
|
||||||
|
download(updatePackage.url, packagePath, function() {
|
||||||
|
extract(packagePath, dirName, function() {
|
||||||
|
// clean old binaries
|
||||||
|
console.log('Cleaning old binaries');
|
||||||
|
del.sync(['./_output/*', '!./_output/UI/']);
|
||||||
|
console.log('copying binaries to target');
|
||||||
|
gulp.src(dirName + '/NzbDrone/*.*')
|
||||||
|
.pipe(gulp.dest('./_output/'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('startSonarr', function() {
|
||||||
|
var ls = spawn('mono', ['--debug', './_output/NzbDrone.exe']);
|
||||||
|
|
||||||
|
ls.stdout.on('data', function(data) {
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
ls.stderr.on('data', function(data) {
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
ls.on('close', function(code) {
|
||||||
|
console.log('child process exited with code ' + code);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
const gulp = require('gulp');
|
||||||
|
const paths = require('./helpers/paths.js');
|
||||||
|
const stripbom = require('gulp-stripbom');
|
||||||
|
|
||||||
|
function stripBom(dest) {
|
||||||
|
gulp.src([paths.src.scripts, paths.src.exclude.libs])
|
||||||
|
.pipe(stripbom({ showLog: false }))
|
||||||
|
.pipe(gulp.dest(dest));
|
||||||
|
|
||||||
|
gulp.src(paths.src.less)
|
||||||
|
.pipe(stripbom({ showLog: false }))
|
||||||
|
.pipe(gulp.dest(dest));
|
||||||
|
|
||||||
|
gulp.src(paths.src.templates)
|
||||||
|
.pipe(stripbom({ showLog: false }))
|
||||||
|
.pipe(gulp.dest(dest));
|
||||||
|
}
|
||||||
|
|
||||||
|
gulp.task('stripBom', () => {
|
||||||
|
stripBom(paths.src.root);
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
const gulp = require('gulp');
|
||||||
|
const livereload = require('gulp-livereload');
|
||||||
|
const watch = require('gulp-watch');
|
||||||
|
const paths = require('./helpers/paths.js');
|
||||||
|
|
||||||
|
require('./copy.js');
|
||||||
|
require('./webpack.js');
|
||||||
|
|
||||||
|
function watchTask(glob, task) {
|
||||||
|
const options = {
|
||||||
|
name: `watch: ${task}`,
|
||||||
|
verbose: true
|
||||||
|
};
|
||||||
|
return watch(glob, options, () => {
|
||||||
|
gulp.start(task);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
gulp.task('watch', ['copyHtml', 'copyFonts', 'copyImages', 'copyJs'], () => {
|
||||||
|
livereload.listen({ start: true });
|
||||||
|
|
||||||
|
gulp.start('webpackWatch');
|
||||||
|
|
||||||
|
watchTask(paths.src.html, 'copyHtml');
|
||||||
|
watchTask(`${paths.src.fonts}**/*.*`, 'copyFonts');
|
||||||
|
watchTask(`${paths.src.images}**/*.*`, 'copyImages');
|
||||||
|
});
|
|
@ -1,15 +1,11 @@
|
||||||
const _ = require('lodash');
|
|
||||||
const gulp = require('gulp');
|
const gulp = require('gulp');
|
||||||
const simpleVars = require('postcss-simple-vars');
|
|
||||||
const nested = require('postcss-nested');
|
|
||||||
const autoprefixer = require('autoprefixer');
|
|
||||||
const webpackStream = require('webpack-stream');
|
const webpackStream = require('webpack-stream');
|
||||||
const livereload = require('gulp-livereload');
|
const livereload = require('gulp-livereload');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const errorHandler = require('./helpers/errorHandler');
|
const errorHandler = require('./helpers/errorHandler');
|
||||||
const reload = require('require-nocache')(module);
|
|
||||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||||
|
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||||
|
|
||||||
const uiFolder = 'UI';
|
const uiFolder = 'UI';
|
||||||
const root = path.join(__dirname, '..', 'src');
|
const root = path.join(__dirname, '..', 'src');
|
||||||
|
@ -18,66 +14,94 @@ const isProduction = process.argv.indexOf('--production') > -1;
|
||||||
console.log('ROOT:', root);
|
console.log('ROOT:', root);
|
||||||
console.log('isProduction:', isProduction);
|
console.log('isProduction:', isProduction);
|
||||||
|
|
||||||
const cssVariables = [
|
const cssVarsFiles = [
|
||||||
'../src/Styles/Variables/colors',
|
'../src/Styles/Variables/colors',
|
||||||
'../src/Styles/Variables/dimensions',
|
'../src/Styles/Variables/dimensions',
|
||||||
'../src/Styles/Variables/fonts',
|
'../src/Styles/Variables/fonts',
|
||||||
'../src/Styles/Variables/animations'
|
'../src/Styles/Variables/animations'
|
||||||
].map(require.resolve);
|
].map(require.resolve);
|
||||||
|
|
||||||
|
const extractCSSPlugin = new ExtractTextPlugin({
|
||||||
|
filename: path.join('_output', uiFolder, 'Content', 'styles.css'),
|
||||||
|
allChunks: true,
|
||||||
|
disable: false,
|
||||||
|
ignoreOrder: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
extractCSSPlugin,
|
||||||
|
|
||||||
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
|
name: 'vendor'
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
__DEV__: !isProduction,
|
||||||
|
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isProduction) {
|
||||||
|
plugins.push(new UglifyJSPlugin({
|
||||||
|
sourceMap: true,
|
||||||
|
uglifyOptions: {
|
||||||
|
mangle: false,
|
||||||
|
output: {
|
||||||
|
comments: false,
|
||||||
|
beautify: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
devtool: '#source-map',
|
devtool: '#source-map',
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
children: false
|
children: false
|
||||||
},
|
},
|
||||||
|
|
||||||
watchOptions: {
|
watchOptions: {
|
||||||
ignored: /node_modules/
|
ignored: /node_modules/
|
||||||
},
|
},
|
||||||
|
|
||||||
entry: {
|
entry: {
|
||||||
preload: 'preload.js',
|
preload: 'preload.js',
|
||||||
vendor: 'vendor.js',
|
vendor: 'vendor.js',
|
||||||
index: 'index.js'
|
index: 'index.js'
|
||||||
},
|
},
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
root: [
|
modules: [
|
||||||
root,
|
root,
|
||||||
path.join(root, 'Shims'),
|
path.join(root, 'Shims'),
|
||||||
path.join(root, 'JsLibraries')
|
'node_modules'
|
||||||
]
|
],
|
||||||
},
|
alias: {
|
||||||
output: {
|
jquery: 'jquery/src/jquery'
|
||||||
filename: path.join('_output', uiFolder, '[name].js'),
|
|
||||||
sourceMapFilename: path.join('_output', uiFolder, '[file].map')
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new ExtractTextPlugin(path.join('_output', uiFolder, 'Content', 'styles.css'), { allChunks: true }),
|
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
|
||||||
name: 'vendor'
|
|
||||||
}),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
__DEV__: !isProduction,
|
|
||||||
'process.env': {
|
|
||||||
NODE_ENV: isProduction ? JSON.stringify('production') : JSON.stringify('development')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
resolveLoader: {
|
|
||||||
modulesDirectories: [
|
|
||||||
'node_modules',
|
|
||||||
'gulp/webpack/'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
eslint: {
|
|
||||||
formatter: function(results) {
|
|
||||||
return JSON.stringify(results);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
filename: path.join('_output', uiFolder, '[name].js'),
|
||||||
|
sourceMapFilename: '[file].map'
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins,
|
||||||
|
|
||||||
|
resolveLoader: {
|
||||||
|
modules: [
|
||||||
|
'node_modules',
|
||||||
|
'frontend/gulp/webpack/'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
module: {
|
module: {
|
||||||
loaders: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.js?$/,
|
test: /\.js?$/,
|
||||||
exclude: /(node_modules|JsLibraries)/,
|
exclude: /(node_modules|JsLibraries)/,
|
||||||
loader: 'babel',
|
loader: 'babel-loader',
|
||||||
query: {
|
query: {
|
||||||
plugins: ['transform-class-properties'],
|
plugins: ['transform-class-properties'],
|
||||||
presets: ['es2015', 'decorators-legacy', 'react', 'stage-2'],
|
presets: ['es2015', 'decorators-legacy', 'react', 'stage-2'],
|
||||||
|
@ -93,51 +117,80 @@ const config = {
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
exclude: /(node_modules|globals.css)/,
|
exclude: /(node_modules|globals.css)/,
|
||||||
loader: ExtractTextPlugin.extract('style', 'css-loader?modules&importLoaders=1&sourceMap&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader')
|
use: extractCSSPlugin.extract({
|
||||||
|
fallback: 'style-loader',
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'css-variables-loader',
|
||||||
|
options: {
|
||||||
|
cssVarsFiles
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: true,
|
||||||
|
importLoaders: 1,
|
||||||
|
localIdentName: '[name]-[local]-[hash:base64:5]',
|
||||||
|
sourceMap: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
config: {
|
||||||
|
ctx: {
|
||||||
|
cssVarsFiles
|
||||||
|
},
|
||||||
|
path: 'frontend/postcss.config.js'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// Global styles
|
// Global styles
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
include: /(node_modules|globals.css)/,
|
include: /(node_modules|globals.css)/,
|
||||||
loader: 'style!css-loader'
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
{
|
||||||
|
loader: 'css-loader'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
{
|
{
|
||||||
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
loader: 'url?limit=10240&mimetype=application/font-woff&emitFile=false&name=Content/Fonts/[name].[ext]'
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'url-loader',
|
||||||
|
options: {
|
||||||
|
limit: 10240,
|
||||||
|
mimetype: 'application/font-woff',
|
||||||
|
emitFile: false,
|
||||||
|
name: 'Content/Fonts/[name].[ext]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
loader: 'file-loader?emitFile=false&name=Content/Fonts/[name].[ext]'
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
emitFile: false,
|
||||||
|
name: 'Content/Fonts/[name].[ext]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
postcss: function(wpack) {
|
|
||||||
cssVariables.forEach(wpack.addDependency);
|
|
||||||
|
|
||||||
return [
|
|
||||||
simpleVars({
|
|
||||||
variables: function() {
|
|
||||||
return cssVariables.reduce(function(obj, vars) {
|
|
||||||
return _.extend(obj, reload(vars));
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
nested(),
|
|
||||||
autoprefixer({
|
|
||||||
browsers: [
|
|
||||||
'Chrome >= 30',
|
|
||||||
'Firefox >= 30',
|
|
||||||
'Safari >= 6',
|
|
||||||
'Edge >= 12',
|
|
||||||
'Explorer >= 10',
|
|
||||||
'iOS >= 7',
|
|
||||||
'Android >= 4.4'
|
|
||||||
]
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
const loaderUtils = require('loader-utils');
|
||||||
|
|
||||||
|
module.exports = function cssVariablesLoader(source) {
|
||||||
|
const options = loaderUtils.getOptions(this);
|
||||||
|
|
||||||
|
options.cssVarsFiles.forEach((cssVarsFile) => {
|
||||||
|
this.addDependency(cssVarsFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
return source;
|
||||||
|
};
|
|
@ -0,0 +1,33 @@
|
||||||
|
const reload = require('require-nocache')(module);
|
||||||
|
|
||||||
|
module.exports = (ctx, configPath, options) => {
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
'postcss-mixins': {
|
||||||
|
mixinsDir: [
|
||||||
|
'frontend/src/Styles/Mixins'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'postcss-simple-vars': {
|
||||||
|
variables: () =>
|
||||||
|
ctx.options.cssVarsFiles.reduce((acc, vars) => {
|
||||||
|
return Object.assign(acc, reload(vars));
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
'postcss-nested': {},
|
||||||
|
autoprefixer: {
|
||||||
|
browsers: [
|
||||||
|
'Chrome >= 30',
|
||||||
|
'Firefox >= 30',
|
||||||
|
'Safari >= 6',
|
||||||
|
'Edge >= 12',
|
||||||
|
'Explorer >= 11',
|
||||||
|
'iOS >= 7',
|
||||||
|
'Android >= 4.4'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
// Place your settings in this file to overwrite default and user settings.
|
||||||
|
{
|
||||||
|
"files.insertFinalNewline": true
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import BlacklistRowConnector from './BlacklistRowConnector';
|
||||||
|
|
||||||
|
class Blacklist extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
totalRecords,
|
||||||
|
isClearingBlacklistExecuting,
|
||||||
|
onClearBlacklistPress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title="Blacklist">
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Clear"
|
||||||
|
iconName={icons.CLEAR}
|
||||||
|
isSpinning={isClearingBlacklistExecuting}
|
||||||
|
onPress={onClearBlacklistPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBodyConnector>
|
||||||
|
{
|
||||||
|
isFetching && !isPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>Unable to load blacklist</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !items.length &&
|
||||||
|
<div>
|
||||||
|
No history blacklist
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !!items.length &&
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<BlacklistRowConnector
|
||||||
|
key={item.id}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</PageContentBodyConnector>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Blacklist.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
totalRecords: PropTypes.number,
|
||||||
|
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||||
|
onClearBlacklistPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Blacklist;
|
|
@ -0,0 +1,145 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import * as blacklistActions from 'Store/Actions/blacklistActions';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import Blacklist from './Blacklist';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.blacklist,
|
||||||
|
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
|
||||||
|
(blacklist, isClearingBlacklistExecuting) => {
|
||||||
|
return {
|
||||||
|
isClearingBlacklistExecuting,
|
||||||
|
...blacklist
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
...blacklistActions,
|
||||||
|
executeCommand
|
||||||
|
};
|
||||||
|
|
||||||
|
class BlacklistConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchBlacklist,
|
||||||
|
gotoBlacklistFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
registerPagePopulator(this.repopulate);
|
||||||
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchBlacklist();
|
||||||
|
} else {
|
||||||
|
gotoBlacklistFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
|
||||||
|
this.props.gotoBlacklistFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.clearBlacklist();
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.fetchBlacklist();
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFirstPagePress = () => {
|
||||||
|
this.props.gotoBlacklistFirstPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreviousPagePress = () => {
|
||||||
|
this.props.gotoBlacklistPreviousPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onNextPagePress = () => {
|
||||||
|
this.props.gotoBlacklistNextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onLastPagePress = () => {
|
||||||
|
this.props.gotoBlacklistLastPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageSelect = (page) => {
|
||||||
|
this.props.gotoBlacklistPage({ page });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortPress = (sortKey) => {
|
||||||
|
this.props.setBlacklistSort({ sortKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setBlacklistTableOption(payload);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
this.props.gotoBlacklistFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClearBlacklistPress = () => {
|
||||||
|
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Blacklist
|
||||||
|
onFirstPagePress={this.onFirstPagePress}
|
||||||
|
onPreviousPagePress={this.onPreviousPagePress}
|
||||||
|
onNextPagePress={this.onNextPagePress}
|
||||||
|
onLastPagePress={this.onLastPagePress}
|
||||||
|
onPageSelect={this.onPageSelect}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onClearBlacklistPress={this.onClearBlacklistPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlacklistConnector.propTypes = {
|
||||||
|
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
fetchBlacklist: PropTypes.func.isRequired,
|
||||||
|
gotoBlacklistFirstPage: PropTypes.func.isRequired,
|
||||||
|
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
|
||||||
|
gotoBlacklistNextPage: PropTypes.func.isRequired,
|
||||||
|
gotoBlacklistLastPage: PropTypes.func.isRequired,
|
||||||
|
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||||
|
setBlacklistSort: PropTypes.func.isRequired,
|
||||||
|
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||||
|
clearBlacklist: PropTypes.func.isRequired,
|
||||||
|
executeCommand: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
|
||||||
|
);
|
|
@ -0,0 +1,89 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
|
||||||
|
class BlacklistDetailsModal extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
sourceTitle,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
message,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
Details
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Name"
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Protocol"
|
||||||
|
data={protocol}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!!message &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Indexer"
|
||||||
|
data={indexer}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!message &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Message"
|
||||||
|
data={message}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</DescriptionList>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlacklistDetailsModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
protocol: PropTypes.string.isRequired,
|
||||||
|
indexer: PropTypes.string,
|
||||||
|
message: PropTypes.string,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlacklistDetailsModal;
|
|
@ -0,0 +1,18 @@
|
||||||
|
.language,
|
||||||
|
.quality {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indexer {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 70px;
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
||||||
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
|
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||||
|
import styles from './BlacklistRow.css';
|
||||||
|
|
||||||
|
class BlacklistRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isDetailsModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onDetailsPress = () => {
|
||||||
|
this.setState({ isDetailsModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDetailsModalClose = () => {
|
||||||
|
this.setState({ isDetailsModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
series,
|
||||||
|
sourceTitle,
|
||||||
|
language,
|
||||||
|
quality,
|
||||||
|
date,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
message,
|
||||||
|
columns,
|
||||||
|
onRemovePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'series.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<SeriesTitleLink
|
||||||
|
titleSlug={series.titleSlug}
|
||||||
|
title={series.title}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{sourceTitle}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'language') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.language}
|
||||||
|
>
|
||||||
|
<EpisodeLanguage
|
||||||
|
language={language}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.quality}
|
||||||
|
>
|
||||||
|
<EpisodeQuality
|
||||||
|
quality={quality}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'date') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
date={date}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.indexer}
|
||||||
|
>
|
||||||
|
{indexer}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.actions}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
name={icons.INFO}
|
||||||
|
onPress={this.onDetailsPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
title="Remove from blacklist"
|
||||||
|
name={icons.REMOVE}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onRemovePress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<BlacklistDetailsModal
|
||||||
|
isOpen={this.state.isDetailsModalOpen}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
protocol={protocol}
|
||||||
|
indexer={indexer}
|
||||||
|
message={message}
|
||||||
|
onModalClose={this.onDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
BlacklistRow.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
series: PropTypes.object.isRequired,
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
language: PropTypes.object.isRequired,
|
||||||
|
quality: PropTypes.object.isRequired,
|
||||||
|
date: PropTypes.string.isRequired,
|
||||||
|
protocol: PropTypes.string.isRequired,
|
||||||
|
indexer: PropTypes.string,
|
||||||
|
message: PropTypes.string,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onRemovePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlacklistRow;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
|
||||||
|
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||||
|
import BlacklistRow from './BlacklistRow';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createSeriesSelector(),
|
||||||
|
(series) => {
|
||||||
|
return {
|
||||||
|
series
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onRemovePress() {
|
||||||
|
dispatch(removeFromBlacklist({ id: props.id }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);
|
|
@ -0,0 +1,5 @@
|
||||||
|
.description {
|
||||||
|
composes: title from 'Components/DescriptionList/DescriptionListItemDescription.css';
|
||||||
|
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
|
@ -0,0 +1,244 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||||
|
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||||
|
import styles from './HistoryDetails.css';
|
||||||
|
|
||||||
|
function HistoryDetails(props) {
|
||||||
|
const {
|
||||||
|
eventType,
|
||||||
|
sourceTitle,
|
||||||
|
data,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (eventType === 'grabbed') {
|
||||||
|
const {
|
||||||
|
indexer,
|
||||||
|
releaseGroup,
|
||||||
|
nzbInfoUrl,
|
||||||
|
downloadClient,
|
||||||
|
downloadId,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
publishedDate
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Name"
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!!indexer &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Indexer"
|
||||||
|
data={indexer}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!releaseGroup &&
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Release Group"
|
||||||
|
data={releaseGroup}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!nzbInfoUrl &&
|
||||||
|
<span>
|
||||||
|
<DescriptionListItemTitle>
|
||||||
|
Info URL
|
||||||
|
</DescriptionListItemTitle>
|
||||||
|
|
||||||
|
<DescriptionListItemDescription>
|
||||||
|
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||||
|
</DescriptionListItemDescription>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!downloadClient &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Download Client"
|
||||||
|
data={downloadClient}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!downloadId &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Grab ID"
|
||||||
|
data={downloadId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!indexer &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Age (when grabbed)"
|
||||||
|
data={formatAge(age, ageHours, ageMinutes)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!publishedDate &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Published Date"
|
||||||
|
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadFailed') {
|
||||||
|
const {
|
||||||
|
message
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Name"
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!!message &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Message"
|
||||||
|
data={message}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadFolderImported') {
|
||||||
|
const {
|
||||||
|
droppedPath,
|
||||||
|
importedPath
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Name"
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!!droppedPath &&
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Source"
|
||||||
|
data={droppedPath}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!importedPath &&
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Imported To"
|
||||||
|
data={importedPath}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'episodeFileDeleted') {
|
||||||
|
const {
|
||||||
|
reason
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
let reasonMessage = '';
|
||||||
|
|
||||||
|
switch (reason) {
|
||||||
|
case 'Manual':
|
||||||
|
reasonMessage = 'File was deleted by via UI';
|
||||||
|
break;
|
||||||
|
case 'MissingFromDisk':
|
||||||
|
reasonMessage = 'Sonarr was unable to find the file on disk so it was removed';
|
||||||
|
break;
|
||||||
|
case 'Upgrade':
|
||||||
|
reasonMessage = 'File was deleted to import an upgrade';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reasonMessage = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Name"
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Reason"
|
||||||
|
data={reasonMessage}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'episodeFileRenamed') {
|
||||||
|
const {
|
||||||
|
sourcePath,
|
||||||
|
sourceRelativePath,
|
||||||
|
path,
|
||||||
|
relativePath
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Source Path"
|
||||||
|
data={sourcePath}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Source Relative Path"
|
||||||
|
data={sourceRelativePath}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Destination Path"
|
||||||
|
data={path}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Destination Relative Path"
|
||||||
|
data={relativePath}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryDetails.propTypes = {
|
||||||
|
eventType: PropTypes.string.isRequired,
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
data: PropTypes.object.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryDetails;
|
|
@ -0,0 +1,21 @@
|
||||||
|
var Handlebars = require('handlebars');
|
||||||
|
var FormatHelpers = require('Shared/FormatHelpers');
|
||||||
|
|
||||||
|
Handlebars.registerHelper('historyAge', function() {
|
||||||
|
var age = this.age;
|
||||||
|
var unit = FormatHelpers.plural(Math.round(age), 'day');
|
||||||
|
var ageHours = parseFloat(this.ageHours);
|
||||||
|
var ageMinutes = this.ageMinutes ? parseFloat(this.ageMinutes) : null;
|
||||||
|
|
||||||
|
if (age < 2) {
|
||||||
|
age = ageHours.toFixed(1);
|
||||||
|
unit = FormatHelpers.plural(Math.round(ageHours), 'hour');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (age < 2 && ageMinutes) {
|
||||||
|
age = parseFloat(ageMinutes).toFixed(1);
|
||||||
|
unit = FormatHelpers.plural(Math.round(ageMinutes), 'minute');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Handlebars.SafeString(`<dt>Age (when grabbed):</dt><dd>${age} ${unit}</dd>`);
|
||||||
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import HistoryDetails from './HistoryDetails';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createUISettingsSelector(),
|
||||||
|
(uiSettings) => {
|
||||||
|
return _.pick(uiSettings, [
|
||||||
|
'shortDateFormat',
|
||||||
|
'timeFormat'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(HistoryDetails);
|
|
@ -0,0 +1,5 @@
|
||||||
|
.markAsFailedButton {
|
||||||
|
composes: button from 'Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import HistoryDetails from './HistoryDetails';
|
||||||
|
import styles from './HistoryDetailsModal.css';
|
||||||
|
|
||||||
|
function getHeaderTitle(eventType) {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'grabbed':
|
||||||
|
return 'Grabbed';
|
||||||
|
case 'downloadFailed':
|
||||||
|
return 'Download Failed';
|
||||||
|
case 'downloadFolderImported':
|
||||||
|
return 'Episode Imported';
|
||||||
|
case 'episodeFileDeleted':
|
||||||
|
return 'Episode File Deleted';
|
||||||
|
case 'episodeFileRenamed':
|
||||||
|
return 'Episode File Renamed';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryDetailsModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
eventType,
|
||||||
|
sourceTitle,
|
||||||
|
data,
|
||||||
|
isMarkingAsFailed,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
onMarkAsFailedPress,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{getHeaderTitle(eventType)}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<HistoryDetails
|
||||||
|
eventType={eventType}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
data={data}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
{
|
||||||
|
eventType === 'grabbed' &&
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.markAsFailedButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
isSpinning={isMarkingAsFailed}
|
||||||
|
onPress={onMarkAsFailedPress}
|
||||||
|
>
|
||||||
|
Mark as Failed
|
||||||
|
</SpinnerButton>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryDetailsModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
eventType: PropTypes.string.isRequired,
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
data: PropTypes.object.isRequired,
|
||||||
|
isMarkingAsFailed: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
onMarkAsFailedPress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
HistoryDetailsModal.defaultProps = {
|
||||||
|
isMarkingAsFailed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryDetailsModal;
|
|
@ -0,0 +1,161 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { align, icons } from 'Helpers/Props';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import HistoryRowConnector from './HistoryRowConnector';
|
||||||
|
|
||||||
|
class History extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
// Don't update when fetching has completed if items have changed,
|
||||||
|
// before episodes start fetching or when episodes start fetching.
|
||||||
|
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
this.props.isFetching &&
|
||||||
|
nextProps.isPopulated &&
|
||||||
|
hasDifferentItems(this.props.items, nextProps.items)
|
||||||
|
) ||
|
||||||
|
(!this.props.isEpisodesFetching && nextProps.isEpisodesFetching)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
totalRecords,
|
||||||
|
isEpisodesFetching,
|
||||||
|
isEpisodesPopulated,
|
||||||
|
episodesError,
|
||||||
|
onFilterSelect,
|
||||||
|
onFirstPagePress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const isFetchingAny = isFetching || isEpisodesFetching;
|
||||||
|
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
|
||||||
|
const hasError = error || episodesError;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title="History">
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Refresh"
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isFetching}
|
||||||
|
onPress={onFirstPagePress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={[]}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBodyConnector>
|
||||||
|
{
|
||||||
|
isFetchingAny && !isAllPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetchingAny && hasError &&
|
||||||
|
<div>Unable to load history</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// If history isPopulated and it's empty show no history found and don't
|
||||||
|
// wait for the episodes to populate because they are never coming.
|
||||||
|
|
||||||
|
isPopulated && !hasError && !items.length &&
|
||||||
|
<div>
|
||||||
|
No history found
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isAllPopulated && !hasError && !!items.length &&
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<HistoryRowConnector
|
||||||
|
key={item.id}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetchingAny}
|
||||||
|
onFirstPagePress={onFirstPagePress}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</PageContentBodyConnector>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
History.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
totalRecords: PropTypes.number,
|
||||||
|
isEpisodesFetching: PropTypes.bool.isRequired,
|
||||||
|
isEpisodesPopulated: PropTypes.bool.isRequired,
|
||||||
|
episodesError: PropTypes.object,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
onFirstPagePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default History;
|
|
@ -0,0 +1,157 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
|
import * as historyActions from 'Store/Actions/historyActions';
|
||||||
|
import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions';
|
||||||
|
import History from './History';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.history,
|
||||||
|
(state) => state.episodes,
|
||||||
|
(history, episodes) => {
|
||||||
|
return {
|
||||||
|
isEpisodesFetching: episodes.isFetching,
|
||||||
|
isEpisodesPopulated: episodes.isPopulated,
|
||||||
|
episodesError: episodes.error,
|
||||||
|
...history
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
...historyActions,
|
||||||
|
fetchEpisodes,
|
||||||
|
clearEpisodes
|
||||||
|
};
|
||||||
|
|
||||||
|
class HistoryConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchHistory,
|
||||||
|
gotoHistoryFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
registerPagePopulator(this.repopulate);
|
||||||
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchHistory();
|
||||||
|
} else {
|
||||||
|
gotoHistoryFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
const episodeIds = selectUniqueIds(this.props.items, 'episodeId');
|
||||||
|
|
||||||
|
if (episodeIds.length) {
|
||||||
|
this.props.fetchEpisodes({ episodeIds });
|
||||||
|
} else {
|
||||||
|
this.props.clearEpisodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
this.props.clearHistory();
|
||||||
|
this.props.clearEpisodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.fetchHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFirstPagePress = () => {
|
||||||
|
this.props.gotoHistoryFirstPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreviousPagePress = () => {
|
||||||
|
this.props.gotoHistoryPreviousPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onNextPagePress = () => {
|
||||||
|
this.props.gotoHistoryNextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onLastPagePress = () => {
|
||||||
|
this.props.gotoHistoryLastPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageSelect = (page) => {
|
||||||
|
this.props.gotoHistoryPage({ page });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortPress = (sortKey) => {
|
||||||
|
this.props.setHistorySort({ sortKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setHistoryFilter({ selectedFilterKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setHistoryTableOption(payload);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
this.props.gotoHistoryFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<History
|
||||||
|
onFirstPagePress={this.onFirstPagePress}
|
||||||
|
onPreviousPagePress={this.onPreviousPagePress}
|
||||||
|
onNextPagePress={this.onNextPagePress}
|
||||||
|
onLastPagePress={this.onLastPagePress}
|
||||||
|
onPageSelect={this.onPageSelect}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryConnector.propTypes = {
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
fetchHistory: PropTypes.func.isRequired,
|
||||||
|
gotoHistoryFirstPage: PropTypes.func.isRequired,
|
||||||
|
gotoHistoryPreviousPage: PropTypes.func.isRequired,
|
||||||
|
gotoHistoryNextPage: PropTypes.func.isRequired,
|
||||||
|
gotoHistoryLastPage: PropTypes.func.isRequired,
|
||||||
|
gotoHistoryPage: PropTypes.func.isRequired,
|
||||||
|
setHistorySort: PropTypes.func.isRequired,
|
||||||
|
setHistoryFilter: PropTypes.func.isRequired,
|
||||||
|
setHistoryTableOption: PropTypes.func.isRequired,
|
||||||
|
clearHistory: PropTypes.func.isRequired,
|
||||||
|
fetchEpisodes: PropTypes.func.isRequired,
|
||||||
|
clearEpisodes: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector)
|
||||||
|
);
|
|
@ -0,0 +1,6 @@
|
||||||
|
.cell {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 35px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
|
function getIconName(eventType) {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'grabbed':
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
case 'seriesFolderImported':
|
||||||
|
return icons.DRIVE;
|
||||||
|
case 'downloadFolderImported':
|
||||||
|
return icons.DOWNLOADED;
|
||||||
|
case 'downloadFailed':
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
case 'episodeFileDeleted':
|
||||||
|
return icons.DELETE;
|
||||||
|
case 'episodeFileRenamed':
|
||||||
|
return icons.ORGANIZE;
|
||||||
|
default:
|
||||||
|
return icons.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconKind(eventType) {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'downloadFailed':
|
||||||
|
return kinds.DANGER;
|
||||||
|
default:
|
||||||
|
return kinds.DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTooltip(eventType, data) {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'grabbed':
|
||||||
|
return `Episode grabbed from ${data.indexer} and sent to ${data.downloadClient}`;
|
||||||
|
case 'seriesFolderImported':
|
||||||
|
return 'Episode imported from series folder';
|
||||||
|
case 'downloadFolderImported':
|
||||||
|
return 'Episode downloaded successfully and picked up from download client';
|
||||||
|
case 'downloadFailed':
|
||||||
|
return 'Episode download failed';
|
||||||
|
case 'episodeFileDeleted':
|
||||||
|
return 'Episode file deleted';
|
||||||
|
case 'episodeFileRenamed':
|
||||||
|
return 'Episode file renamed';
|
||||||
|
default:
|
||||||
|
return 'Unknown event';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryEventTypeCell({ eventType, data }) {
|
||||||
|
const iconName = getIconName(eventType);
|
||||||
|
const iconKind = getIconKind(eventType);
|
||||||
|
const tooltip = getTooltip(eventType, data);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.cell}
|
||||||
|
title={tooltip}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={iconName}
|
||||||
|
kind={iconKind}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryEventTypeCell.propTypes = {
|
||||||
|
eventType: PropTypes.string.isRequired,
|
||||||
|
data: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
HistoryEventTypeCell.defaultProps = {
|
||||||
|
data: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryEventTypeCell;
|
|
@ -0,0 +1,23 @@
|
||||||
|
.downloadClient {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indexer {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.releaseGroup {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 30px;
|
||||||
|
}
|
|
@ -0,0 +1,263 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import episodeEntities from 'Episode/episodeEntities';
|
||||||
|
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||||
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
|
import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
||||||
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
|
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||||
|
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||||
|
import styles from './HistoryRow.css';
|
||||||
|
|
||||||
|
class HistoryRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isDetailsModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (
|
||||||
|
prevProps.isMarkingAsFailed &&
|
||||||
|
!this.props.isMarkingAsFailed &&
|
||||||
|
!this.props.markAsFailedError
|
||||||
|
) {
|
||||||
|
this.setState({ isDetailsModalOpen: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onDetailsPress = () => {
|
||||||
|
this.setState({ isDetailsModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDetailsModalClose = () => {
|
||||||
|
this.setState({ isDetailsModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
episodeId,
|
||||||
|
series,
|
||||||
|
episode,
|
||||||
|
language,
|
||||||
|
languageCutoffNotMet,
|
||||||
|
quality,
|
||||||
|
qualityCutoffNotMet,
|
||||||
|
eventType,
|
||||||
|
sourceTitle,
|
||||||
|
date,
|
||||||
|
data,
|
||||||
|
isMarkingAsFailed,
|
||||||
|
columns,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
onMarkAsFailedPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!episode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'eventType') {
|
||||||
|
return (
|
||||||
|
<HistoryEventTypeCell
|
||||||
|
key={name}
|
||||||
|
eventType={eventType}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'series.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<SeriesTitleLink
|
||||||
|
titleSlug={series.titleSlug}
|
||||||
|
title={series.title}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episode') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<SeasonEpisodeNumber
|
||||||
|
seasonNumber={episode.seasonNumber}
|
||||||
|
episodeNumber={episode.episodeNumber}
|
||||||
|
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
|
||||||
|
seriesType={series.seriesType}
|
||||||
|
alternateTitles={series.alternateTitles}
|
||||||
|
sceneSeasonNumber={episode.sceneSeasonNumber}
|
||||||
|
sceneEpisodeNumber={episode.sceneEpisodeNumber}
|
||||||
|
sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episodeTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeTitleLink
|
||||||
|
episodeId={episodeId}
|
||||||
|
episodeEntity={episodeEntities.EPISODES}
|
||||||
|
seriesId={series.id}
|
||||||
|
episodeTitle={episode.title}
|
||||||
|
showOpenSeriesButton={true}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'language') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeLanguage
|
||||||
|
language={language}
|
||||||
|
isCutoffMet={languageCutoffNotMet}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeQuality
|
||||||
|
quality={quality}
|
||||||
|
isCutoffMet={qualityCutoffNotMet}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'date') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
date={date}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadClient') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.downloadClient}
|
||||||
|
>
|
||||||
|
{data.downloadClient}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.indexer}
|
||||||
|
>
|
||||||
|
{data.indexer}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'releaseGroup') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.releaseGroup}
|
||||||
|
>
|
||||||
|
{data.releaseGroup}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'details') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.details}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
name={icons.INFO}
|
||||||
|
onPress={this.onDetailsPress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<HistoryDetailsModal
|
||||||
|
isOpen={this.state.isDetailsModalOpen}
|
||||||
|
eventType={eventType}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
data={data}
|
||||||
|
isMarkingAsFailed={isMarkingAsFailed}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||||
|
onModalClose={this.onDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryRow.propTypes = {
|
||||||
|
episodeId: PropTypes.number,
|
||||||
|
series: PropTypes.object.isRequired,
|
||||||
|
episode: PropTypes.object,
|
||||||
|
language: PropTypes.object.isRequired,
|
||||||
|
languageCutoffNotMet: PropTypes.bool.isRequired,
|
||||||
|
quality: PropTypes.object.isRequired,
|
||||||
|
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||||
|
eventType: PropTypes.string.isRequired,
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
date: PropTypes.string.isRequired,
|
||||||
|
data: PropTypes.object.isRequired,
|
||||||
|
isMarkingAsFailed: PropTypes.bool,
|
||||||
|
markAsFailedError: PropTypes.object,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryRow;
|
|
@ -0,0 +1,76 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||||
|
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||||
|
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import HistoryRow from './HistoryRow';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createSeriesSelector(),
|
||||||
|
createEpisodeSelector(),
|
||||||
|
createUISettingsSelector(),
|
||||||
|
(series, episode, uiSettings) => {
|
||||||
|
return {
|
||||||
|
series,
|
||||||
|
episode,
|
||||||
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
|
timeFormat: uiSettings.timeFormat
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchHistory,
|
||||||
|
markAsFailed
|
||||||
|
};
|
||||||
|
|
||||||
|
class HistoryRowConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (
|
||||||
|
prevProps.isMarkingAsFailed &&
|
||||||
|
!this.props.isMarkingAsFailed &&
|
||||||
|
!this.props.markAsFailedError
|
||||||
|
) {
|
||||||
|
this.props.fetchHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMarkAsFailedPress = () => {
|
||||||
|
this.props.markAsFailed({ id: this.props.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<HistoryRow
|
||||||
|
{...this.props}
|
||||||
|
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryRowConnector.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
isMarkingAsFailed: PropTypes.bool,
|
||||||
|
markAsFailedError: PropTypes.object,
|
||||||
|
fetchHistory: PropTypes.func.isRequired,
|
||||||
|
markAsFailed: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector);
|
|
@ -0,0 +1,13 @@
|
||||||
|
.torrent {
|
||||||
|
composes: label from 'Components/Label.css';
|
||||||
|
|
||||||
|
border-color: $torrentColor;
|
||||||
|
background-color: $torrentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usenet {
|
||||||
|
composes: label from 'Components/Label.css';
|
||||||
|
|
||||||
|
border-color: $usenetColor;
|
||||||
|
background-color: $usenetColor;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import styles from './ProtocolLabel.css';
|
||||||
|
|
||||||
|
function ProtocolLabel({ protocol }) {
|
||||||
|
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label className={styles[protocol]}>
|
||||||
|
{protocolName}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtocolLabel.propTypes = {
|
||||||
|
protocol: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtocolLabel;
|
|
@ -0,0 +1,266 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||||
|
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||||
|
import QueueRowConnector from './QueueRowConnector';
|
||||||
|
|
||||||
|
class Queue extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {},
|
||||||
|
isPendingSelected: false,
|
||||||
|
isConfirmRemoveModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
// Don't update when fetching has completed if items have changed,
|
||||||
|
// before episodes start fetching or when episodes start fetching.
|
||||||
|
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
this.props.isFetching &&
|
||||||
|
nextProps.isPopulated &&
|
||||||
|
hasDifferentItems(this.props.items, nextProps.items)
|
||||||
|
) ||
|
||||||
|
(!this.props.isEpisodesFetching && nextProps.isEpisodesFetching)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
this.setState({ selectedState: {} });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIds = this.getSelectedIds();
|
||||||
|
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||||
|
return selectedIds.indexOf(item.id) > -1 && item.status === 'Delay';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||||
|
this.setState({ isPendingSelected });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onGrabSelectedPress = () => {
|
||||||
|
this.props.onGrabSelectedPress(this.getSelectedIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveSelectedPress = () => {
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveSelectedConfirmed = (blacklist) => {
|
||||||
|
this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist);
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmRemoveModalClose = () => {
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
isEpisodesFetching,
|
||||||
|
isEpisodesPopulated,
|
||||||
|
episodesError,
|
||||||
|
columns,
|
||||||
|
totalRecords,
|
||||||
|
isGrabbing,
|
||||||
|
isRemoving,
|
||||||
|
isCheckForFinishedDownloadExecuting,
|
||||||
|
onRefreshPress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState,
|
||||||
|
isConfirmRemoveModalOpen,
|
||||||
|
isPendingSelected
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting;
|
||||||
|
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
|
||||||
|
const hasError = error || episodesError;
|
||||||
|
const selectedCount = this.getSelectedIds().length;
|
||||||
|
const disableSelectedActions = selectedCount === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title="Queue">
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Refresh"
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isRefreshing}
|
||||||
|
onPress={onRefreshPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Grab Selected"
|
||||||
|
iconName={icons.DOWNLOAD}
|
||||||
|
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={this.onGrabSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Remove Selected"
|
||||||
|
iconName={icons.REMOVE}
|
||||||
|
isDisabled={disableSelectedActions}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={this.onRemoveSelectedPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBodyConnector>
|
||||||
|
{
|
||||||
|
isRefreshing && !isAllPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isRefreshing && hasError &&
|
||||||
|
<div>
|
||||||
|
Failed to load Queue
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !hasError && !items.length &&
|
||||||
|
<div>
|
||||||
|
Queue is empty
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isAllPopulated && !hasError && !!items.length &&
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
{...otherProps}
|
||||||
|
optionsComponent={QueueOptionsConnector}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<QueueRowConnector
|
||||||
|
key={item.id}
|
||||||
|
episodeId={item.episodeId}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isRefreshing}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</PageContentBodyConnector>
|
||||||
|
|
||||||
|
<RemoveQueueItemsModal
|
||||||
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
|
selectedCount={selectedCount}
|
||||||
|
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||||
|
onModalClose={this.onConfirmRemoveModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Queue.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isEpisodesFetching: PropTypes.bool.isRequired,
|
||||||
|
isEpisodesPopulated: PropTypes.bool.isRequired,
|
||||||
|
episodesError: PropTypes.object,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
totalRecords: PropTypes.number,
|
||||||
|
isGrabbing: PropTypes.bool.isRequired,
|
||||||
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
|
isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired,
|
||||||
|
onRefreshPress: PropTypes.func.isRequired,
|
||||||
|
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||||
|
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Queue;
|
|
@ -0,0 +1,186 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import * as queueActions from 'Store/Actions/queueActions';
|
||||||
|
import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import Queue from './Queue';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.episodes,
|
||||||
|
(state) => state.queue.options,
|
||||||
|
(state) => state.queue.paged,
|
||||||
|
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
|
||||||
|
(episodes, options, queue, isCheckForFinishedDownloadExecuting) => {
|
||||||
|
return {
|
||||||
|
isEpisodesFetching: episodes.isFetching,
|
||||||
|
isEpisodesPopulated: episodes.isPopulated,
|
||||||
|
episodesError: episodes.error,
|
||||||
|
isCheckForFinishedDownloadExecuting,
|
||||||
|
...options,
|
||||||
|
...queue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
...queueActions,
|
||||||
|
fetchEpisodes,
|
||||||
|
clearEpisodes,
|
||||||
|
executeCommand
|
||||||
|
};
|
||||||
|
|
||||||
|
class QueueConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchQueue,
|
||||||
|
gotoQueueFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
registerPagePopulator(this.repopulate);
|
||||||
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchQueue();
|
||||||
|
} else {
|
||||||
|
gotoQueueFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
const episodeIds = selectUniqueIds(this.props.items, 'episodeId');
|
||||||
|
|
||||||
|
if (episodeIds.length) {
|
||||||
|
this.props.fetchEpisodes({ episodeIds });
|
||||||
|
} else {
|
||||||
|
this.props.clearEpisodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.props.includeUnknownSeriesItems !==
|
||||||
|
prevProps.includeUnknownSeriesItems
|
||||||
|
) {
|
||||||
|
this.repopulate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
this.props.clearQueue();
|
||||||
|
this.props.clearEpisodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.fetchQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFirstPagePress = () => {
|
||||||
|
this.props.gotoQueueFirstPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreviousPagePress = () => {
|
||||||
|
this.props.gotoQueuePreviousPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onNextPagePress = () => {
|
||||||
|
this.props.gotoQueueNextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onLastPagePress = () => {
|
||||||
|
this.props.gotoQueueLastPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageSelect = (page) => {
|
||||||
|
this.props.gotoQueuePage({ page });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortPress = (sortKey) => {
|
||||||
|
this.props.setQueueSort({ sortKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setQueueTableOption(payload);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
this.props.gotoQueueFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRefreshPress = () => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onGrabSelectedPress = (ids) => {
|
||||||
|
this.props.grabQueueItems({ ids });
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveSelectedPress = (ids, blacklist) => {
|
||||||
|
this.props.removeQueueItems({ ids, blacklist });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Queue
|
||||||
|
onFirstPagePress={this.onFirstPagePress}
|
||||||
|
onPreviousPagePress={this.onPreviousPagePress}
|
||||||
|
onNextPagePress={this.onNextPagePress}
|
||||||
|
onLastPagePress={this.onLastPagePress}
|
||||||
|
onPageSelect={this.onPageSelect}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onRefreshPress={this.onRefreshPress}
|
||||||
|
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||||
|
onRemoveSelectedPress={this.onRemoveSelectedPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueConnector.propTypes = {
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
fetchQueue: PropTypes.func.isRequired,
|
||||||
|
gotoQueueFirstPage: PropTypes.func.isRequired,
|
||||||
|
gotoQueuePreviousPage: PropTypes.func.isRequired,
|
||||||
|
gotoQueueNextPage: PropTypes.func.isRequired,
|
||||||
|
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||||
|
gotoQueuePage: PropTypes.func.isRequired,
|
||||||
|
setQueueSort: PropTypes.func.isRequired,
|
||||||
|
setQueueTableOption: PropTypes.func.isRequired,
|
||||||
|
clearQueue: PropTypes.func.isRequired,
|
||||||
|
grabQueueItems: PropTypes.func.isRequired,
|
||||||
|
removeQueueItems: PropTypes.func.isRequired,
|
||||||
|
fetchEpisodes: PropTypes.func.isRequired,
|
||||||
|
clearEpisodes: PropTypes.func.isRequired,
|
||||||
|
executeCommand: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
|
||||||
|
);
|
|
@ -0,0 +1,97 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
|
||||||
|
function QueueDetails(props) {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
size,
|
||||||
|
sizeleft,
|
||||||
|
estimatedCompletionTime,
|
||||||
|
status: queueStatus,
|
||||||
|
errorMessage,
|
||||||
|
progressBar
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const status = queueStatus.toLowerCase();
|
||||||
|
|
||||||
|
const progress = (100 - sizeleft / size * 100);
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={icons.PENDING}
|
||||||
|
title={`Release will be processed ${moment(estimatedCompletionTime).fromNow()}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'completed') {
|
||||||
|
if (errorMessage) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOAD}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={`Import failed: ${errorMessage}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: show an icon when download is complete, but not imported yet?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={`Download failed: ${errorMessage}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'failed') {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Download failed: check download client for more details"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'warning') {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title="Download warning: check download client for more details"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress < 5) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
title={`Episode is downloading - ${progress.toFixed(1)}% ${title}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return progressBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueDetails.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
sizeleft: PropTypes.number.isRequired,
|
||||||
|
estimatedCompletionTime: PropTypes.string,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
progressBar: PropTypes.node.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueueDetails;
|
|
@ -0,0 +1,77 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
|
||||||
|
class QueueOptions extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
includeUnknownSeriesItems: props.includeUnknownSeriesItems
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
includeUnknownSeriesItems
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (includeUnknownSeriesItems !== prevProps.includeUnknownSeriesItems) {
|
||||||
|
this.setState({
|
||||||
|
includeUnknownSeriesItems
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onOptionChange = ({ name, value }) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => {
|
||||||
|
this.props.onOptionChange({
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
includeUnknownSeriesItems
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Show Unknown Series Items</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeUnknownSeriesItems"
|
||||||
|
value={includeUnknownSeriesItems}
|
||||||
|
helpText="Show items without a series in the queue, this could include removed series, movies or anything else in Sonarr's category"
|
||||||
|
onChange={this.onOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueOptions.propTypes = {
|
||||||
|
includeUnknownSeriesItems: PropTypes.bool.isRequired,
|
||||||
|
onOptionChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueueOptions;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setQueueOption } from 'Store/Actions/queueActions';
|
||||||
|
import QueueOptions from './QueueOptions';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.queue.options,
|
||||||
|
(options) => {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
onOptionChange: setQueueOption
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);
|
|
@ -0,0 +1,23 @@
|
||||||
|
.quality {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 70px;
|
||||||
|
}
|
|
@ -0,0 +1,369 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||||
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||||
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
|
import QueueStatusCell from './QueueStatusCell';
|
||||||
|
import TimeleftCell from './TimeleftCell';
|
||||||
|
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||||
|
import styles from './QueueRow.css';
|
||||||
|
|
||||||
|
class QueueRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isRemoveQueueItemModalOpen: false,
|
||||||
|
isInteractiveImportModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onRemoveQueueItemPress = () => {
|
||||||
|
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveQueueItemModalConfirmed = (blacklist) => {
|
||||||
|
this.props.onRemoveQueueItemPress(blacklist);
|
||||||
|
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveQueueItemModalClose = () => {
|
||||||
|
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onInteractiveImportPress = () => {
|
||||||
|
this.setState({ isInteractiveImportModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onInteractiveImportModalClose = () => {
|
||||||
|
this.setState({ isInteractiveImportModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
downloadId,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus,
|
||||||
|
statusMessages,
|
||||||
|
errorMessage,
|
||||||
|
series,
|
||||||
|
episode,
|
||||||
|
quality,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
downloadClient,
|
||||||
|
estimatedCompletionTime,
|
||||||
|
timeleft,
|
||||||
|
size,
|
||||||
|
sizeleft,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
isGrabbing,
|
||||||
|
grabError,
|
||||||
|
isRemoving,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange,
|
||||||
|
onGrabPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isRemoveQueueItemModalOpen,
|
||||||
|
isInteractiveImportModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const progress = 100 - (sizeleft / size * 100);
|
||||||
|
const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning';
|
||||||
|
const isPending = status === 'Delay' || status === 'DownloadClientUnavailable';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'status') {
|
||||||
|
return (
|
||||||
|
<QueueStatusCell
|
||||||
|
key={name}
|
||||||
|
sourceTitle={title}
|
||||||
|
status={status}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
statusMessages={statusMessages}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'series.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{
|
||||||
|
series ?
|
||||||
|
<SeriesTitleLink
|
||||||
|
titleSlug={series.titleSlug}
|
||||||
|
title={series.title}
|
||||||
|
/> :
|
||||||
|
title
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episode') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{
|
||||||
|
episode ?
|
||||||
|
<SeasonEpisodeNumber
|
||||||
|
seasonNumber={episode.seasonNumber}
|
||||||
|
episodeNumber={episode.episodeNumber}
|
||||||
|
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
|
||||||
|
seriesType={series.seriesType}
|
||||||
|
alternateTitles={series.alternateTitles}
|
||||||
|
sceneSeasonNumber={episode.sceneSeasonNumber}
|
||||||
|
sceneEpisodeNumber={episode.sceneEpisodeNumber}
|
||||||
|
sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber}
|
||||||
|
unverifiedSceneNumbering={episode.unverifiedSceneNumbering}
|
||||||
|
/> :
|
||||||
|
'-'
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episode.title') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{
|
||||||
|
episode ?
|
||||||
|
<EpisodeTitleLink
|
||||||
|
episodeId={episode.id}
|
||||||
|
seriesId={series.id}
|
||||||
|
episodeFileId={episode.episodeFileId}
|
||||||
|
episodeTitle={episode.title}
|
||||||
|
showOpenSeriesButton={true}
|
||||||
|
/> :
|
||||||
|
'-'
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episode.airDateUtc') {
|
||||||
|
if (episode) {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
date={episode.airDateUtc}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
-
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeQuality
|
||||||
|
quality={quality}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'protocol') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<ProtocolLabel
|
||||||
|
protocol={protocol}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{indexer}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadClient') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{downloadClient}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'estimatedCompletionTime') {
|
||||||
|
return (
|
||||||
|
<TimeleftCell
|
||||||
|
key={name}
|
||||||
|
status={status}
|
||||||
|
estimatedCompletionTime={estimatedCompletionTime}
|
||||||
|
timeleft={timeleft}
|
||||||
|
size={size}
|
||||||
|
sizeleft={sizeleft}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'progress') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.progress}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
!!progress &&
|
||||||
|
<ProgressBar
|
||||||
|
progress={progress}
|
||||||
|
title={`${progress.toFixed(1)}%`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.actions}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
showInteractiveImport &&
|
||||||
|
<IconButton
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
onPress={this.onInteractiveImportPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPending &&
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.DOWNLOAD}
|
||||||
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={onGrabPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<SpinnerIconButton
|
||||||
|
title="Remove from queue"
|
||||||
|
name={icons.REMOVE}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={this.onRemoveQueueItemPress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<InteractiveImportModal
|
||||||
|
isOpen={isInteractiveImportModalOpen}
|
||||||
|
downloadId={downloadId}
|
||||||
|
title={title}
|
||||||
|
onModalClose={this.onInteractiveImportModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RemoveQueueItemModal
|
||||||
|
isOpen={isRemoveQueueItemModalOpen}
|
||||||
|
sourceTitle={title}
|
||||||
|
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||||
|
onModalClose={this.onRemoveQueueItemModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueRow.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
downloadId: PropTypes.string,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadStatus: PropTypes.string,
|
||||||
|
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
series: PropTypes.object,
|
||||||
|
episode: PropTypes.object,
|
||||||
|
quality: PropTypes.object.isRequired,
|
||||||
|
protocol: PropTypes.string.isRequired,
|
||||||
|
indexer: PropTypes.string,
|
||||||
|
downloadClient: PropTypes.string,
|
||||||
|
estimatedCompletionTime: PropTypes.string,
|
||||||
|
timeleft: PropTypes.string,
|
||||||
|
size: PropTypes.number,
|
||||||
|
sizeleft: PropTypes.number,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
isGrabbing: PropTypes.bool.isRequired,
|
||||||
|
grabError: PropTypes.object,
|
||||||
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
|
onGrabPress: PropTypes.func.isRequired,
|
||||||
|
onRemoveQueueItemPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
QueueRow.defaultProps = {
|
||||||
|
isGrabbing: false,
|
||||||
|
isRemoving: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueueRow;
|
|
@ -0,0 +1,71 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||||
|
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||||
|
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import QueueRow from './QueueRow';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createSeriesSelector(),
|
||||||
|
createEpisodeSelector(),
|
||||||
|
createUISettingsSelector(),
|
||||||
|
(series, episode, uiSettings) => {
|
||||||
|
const result = _.pick(uiSettings, [
|
||||||
|
'showRelativeDates',
|
||||||
|
'shortDateFormat',
|
||||||
|
'timeFormat'
|
||||||
|
]);
|
||||||
|
|
||||||
|
result.series = series;
|
||||||
|
result.episode = episode;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
grabQueueItem,
|
||||||
|
removeQueueItem
|
||||||
|
};
|
||||||
|
|
||||||
|
class QueueRowConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onGrabPress = () => {
|
||||||
|
this.props.grabQueueItem({ id: this.props.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveQueueItemPress = (blacklist) => {
|
||||||
|
this.props.removeQueueItem({ id: this.props.id, blacklist });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<QueueRow
|
||||||
|
{...this.props}
|
||||||
|
onGrabPress={this.onGrabPress}
|
||||||
|
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueRowConnector.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
episode: PropTypes.object,
|
||||||
|
grabQueueItem: PropTypes.func.isRequired,
|
||||||
|
removeQueueItem: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);
|
|
@ -0,0 +1,5 @@
|
||||||
|
.status {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 30px;
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import styles from './QueueStatusCell.css';
|
||||||
|
|
||||||
|
function getDetailedPopoverBody(statusMessages) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
statusMessages.map(({ title, messages }) => {
|
||||||
|
return (
|
||||||
|
<div key={title}>
|
||||||
|
{title}
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
messages.map((message) => {
|
||||||
|
return (
|
||||||
|
<li key={message}>
|
||||||
|
{message}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueStatusCell(props) {
|
||||||
|
const {
|
||||||
|
sourceTitle,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus = 'Ok',
|
||||||
|
statusMessages,
|
||||||
|
errorMessage
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const hasWarning = trackedDownloadStatus === 'Warning';
|
||||||
|
const hasError = trackedDownloadStatus === 'Error';
|
||||||
|
|
||||||
|
// status === 'downloading'
|
||||||
|
let iconName = icons.DOWNLOADING;
|
||||||
|
let iconKind = kinds.DEFAULT;
|
||||||
|
let title = 'Downloading';
|
||||||
|
|
||||||
|
if (hasWarning) {
|
||||||
|
iconKind = kinds.WARNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'Paused') {
|
||||||
|
iconName = icons.PAUSED;
|
||||||
|
title = 'Paused';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'Queued') {
|
||||||
|
iconName = icons.QUEUED;
|
||||||
|
title = 'Queued';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'Completed') {
|
||||||
|
iconName = icons.DOWNLOADED;
|
||||||
|
title = 'Downloaded';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'Delay') {
|
||||||
|
iconName = icons.PENDING;
|
||||||
|
title = 'Pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'DownloadClientUnavailable') {
|
||||||
|
iconName = icons.PENDING;
|
||||||
|
iconKind = kinds.WARNING;
|
||||||
|
title = 'Pending - Download client is unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'Failed') {
|
||||||
|
iconName = icons.DOWNLOADING;
|
||||||
|
iconKind = kinds.DANGER;
|
||||||
|
title = 'Download failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'Warning') {
|
||||||
|
iconName = icons.DOWNLOADING;
|
||||||
|
iconKind = kinds.WARNING;
|
||||||
|
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
if (status === 'Completed') {
|
||||||
|
iconName = icons.DOWNLOAD;
|
||||||
|
iconKind = kinds.DANGER;
|
||||||
|
title = `Import failed: ${sourceTitle}`;
|
||||||
|
} else {
|
||||||
|
iconName = icons.DOWNLOADING;
|
||||||
|
iconKind = kinds.DANGER;
|
||||||
|
title = 'Download failed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.status}>
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
name={iconName}
|
||||||
|
kind={iconKind}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={title}
|
||||||
|
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueStatusCell.propTypes = {
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadStatus: PropTypes.string,
|
||||||
|
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
errorMessage: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueueStatusCell;
|
|
@ -0,0 +1,3 @@
|
||||||
|
.message {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import styles from './RemoveQueueItemModal.css';
|
||||||
|
|
||||||
|
class RemoveQueueItemModal extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
blacklist: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onBlacklistChange = ({ value }) => {
|
||||||
|
this.setState({ blacklist: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveQueueItemConfirmed = () => {
|
||||||
|
const blacklist = this.state.blacklist;
|
||||||
|
|
||||||
|
this.setState({ blacklist: false });
|
||||||
|
this.props.onRemovePress(blacklist);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.setState({ blacklist: false });
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
sourceTitle
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const blacklist = this.state.blacklist;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
Remove - {sourceTitle}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.message}>
|
||||||
|
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Blacklist Release</FormLabel>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="blacklist"
|
||||||
|
value={blacklist}
|
||||||
|
helpText="Prevents Sonarr from automatically grabbing this episode again"
|
||||||
|
onChange={this.onBlacklistChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={this.onModalClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={this.onRemoveQueueItemConfirmed}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveQueueItemModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
onRemovePress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RemoveQueueItemModal;
|
|
@ -0,0 +1,3 @@
|
||||||
|
.message {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import styles from './RemoveQueueItemsModal.css';
|
||||||
|
|
||||||
|
class RemoveQueueItemsModal extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
blacklist: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onBlacklistChange = ({ value }) => {
|
||||||
|
this.setState({ blacklist: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveQueueItemConfirmed = () => {
|
||||||
|
const blacklist = this.state.blacklist;
|
||||||
|
|
||||||
|
this.setState({ blacklist: false });
|
||||||
|
this.props.onRemovePress(blacklist);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.setState({ blacklist: false });
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
selectedCount
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const blacklist = this.state.blacklist;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
Remove Selected Item{selectedCount > 1 ? 's' : ''}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.message}>
|
||||||
|
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Blacklist Release</FormLabel>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="blacklist"
|
||||||
|
value={blacklist}
|
||||||
|
helpText="Prevents Sonarr from automatically grabbing this episode again"
|
||||||
|
onChange={this.onBlacklistChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={this.onModalClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={this.onRemoveQueueItemConfirmed}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveQueueItemsModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
selectedCount: PropTypes.number.isRequired,
|
||||||
|
onRemovePress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RemoveQueueItemsModal;
|
|
@ -0,0 +1,70 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||||
|
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.app,
|
||||||
|
(state) => state.queue.status,
|
||||||
|
(state) => state.queue.options.includeUnknownSeriesItems,
|
||||||
|
(app, status, includeUnknownSeriesItems) => {
|
||||||
|
const {
|
||||||
|
count,
|
||||||
|
unknownCount
|
||||||
|
} = status.item;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected: app.isConnected,
|
||||||
|
isReconnecting: app.isReconnecting,
|
||||||
|
isPopulated: status.isPopulated,
|
||||||
|
...status.item,
|
||||||
|
count: includeUnknownSeriesItems ? count : count - unknownCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchQueueStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
class QueueStatusConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.isPopulated) {
|
||||||
|
this.props.fetchQueueStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.isConnected && prevProps.isReconnecting) {
|
||||||
|
this.props.fetchQueueStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<PageSidebarStatus
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueStatusConnector.propTypes = {
|
||||||
|
isConnected: PropTypes.bool.isRequired,
|
||||||
|
isReconnecting: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
fetchQueueStatus: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);
|
|
@ -0,0 +1,5 @@
|
||||||
|
.timeleft {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 100px;
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import formatTime from 'Utilities/Date/formatTime';
|
||||||
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import styles from './TimeleftCell.css';
|
||||||
|
|
||||||
|
function TimeleftCell(props) {
|
||||||
|
const {
|
||||||
|
estimatedCompletionTime,
|
||||||
|
timeleft,
|
||||||
|
status,
|
||||||
|
size,
|
||||||
|
sizeleft,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (status === 'Delay') {
|
||||||
|
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.timeleft}
|
||||||
|
title={`Delaying download until ${date} at ${time}`}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'DownloadClientUnavailable') {
|
||||||
|
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.timeleft}
|
||||||
|
title={`Retrying download ${date} at ${time}`}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timeleft) {
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.timeleft}>
|
||||||
|
-
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = formatBytes(size);
|
||||||
|
const remainingSize = formatBytes(sizeleft);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.timeleft}
|
||||||
|
title={`${remainingSize} / ${totalSize}`}
|
||||||
|
>
|
||||||
|
{formatTimeSpan(timeleft)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeleftCell.propTypes = {
|
||||||
|
estimatedCompletionTime: PropTypes.string,
|
||||||
|
timeleft: PropTypes.string,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
sizeleft: PropTypes.number.isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimeleftCell;
|
|
@ -0,0 +1,54 @@
|
||||||
|
.searchContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchIconContainer {
|
||||||
|
width: 58px;
|
||||||
|
height: 46px;
|
||||||
|
border: 1px solid $inputBorderColor;
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
background-color: #edf1f2;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput {
|
||||||
|
composes: input from 'Components/Form/TextInput.css';
|
||||||
|
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearLookupButton {
|
||||||
|
border: 1px solid $inputBorderColor;
|
||||||
|
border-left: none;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noResults {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchResults {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
|
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
|
||||||
|
import styles from './AddNewSeries.css';
|
||||||
|
|
||||||
|
class AddNewSeries extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
term: props.term || '',
|
||||||
|
isFetching: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const term = this.state.term;
|
||||||
|
|
||||||
|
if (term) {
|
||||||
|
this.props.onSeriesLookupChange(term);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
term,
|
||||||
|
isFetching
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (term && term !== prevProps.term) {
|
||||||
|
this.setState({
|
||||||
|
term,
|
||||||
|
isFetching: true
|
||||||
|
});
|
||||||
|
this.props.onSeriesLookupChange(term);
|
||||||
|
} else if (isFetching !== prevProps.isFetching) {
|
||||||
|
this.setState({
|
||||||
|
isFetching
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSearchInputChange = ({ value }) => {
|
||||||
|
const hasValue = !!value.trim();
|
||||||
|
|
||||||
|
this.setState({ term: value, isFetching: hasValue }, () => {
|
||||||
|
if (hasValue) {
|
||||||
|
this.props.onSeriesLookupChange(value);
|
||||||
|
} else {
|
||||||
|
this.props.onClearSeriesLookup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClearSeriesLookupPress = () => {
|
||||||
|
this.setState({ term: '' });
|
||||||
|
this.props.onClearSeriesLookup();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
error,
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const term = this.state.term;
|
||||||
|
const isFetching = this.state.isFetching;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title="Add New Series">
|
||||||
|
<PageContentBodyConnector>
|
||||||
|
<div className={styles.searchContainer}>
|
||||||
|
<div className={styles.searchIconContainer}>
|
||||||
|
<Icon
|
||||||
|
name={icons.SEARCH}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
className={styles.searchInput}
|
||||||
|
name="seriesLookup"
|
||||||
|
value={term}
|
||||||
|
placeholder="eg. Breaking Bad, tvdb:####"
|
||||||
|
onChange={this.onSearchInputChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={styles.clearLookupButton}
|
||||||
|
onPress={this.onClearSeriesLookupPress}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.REMOVE}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>Failed to load search results, please try again.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !error && !!items.length &&
|
||||||
|
<div className={styles.searchResults}>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<AddNewSeriesSearchResultConnector
|
||||||
|
key={item.tvdbId}
|
||||||
|
{...item}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !error && !items.length && !!term &&
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.noResults}>Couldn't find any results for '{term}'</div>
|
||||||
|
<div>You can also search using TVDB ID of a show. eg. tvdb:71663</div>
|
||||||
|
<div>
|
||||||
|
<Link to="https://github.com/Sonarr/Sonarr/wiki/FAQ#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
|
||||||
|
Why can't I find my show?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!term &&
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.helpText}>It's easy to add a new series, just start typing the name the series you want to add.</div>
|
||||||
|
<div>You can also search using TVDB ID of a show. eg. tvdb:71663</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div />
|
||||||
|
</PageContentBodyConnector>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewSeries.propTypes = {
|
||||||
|
term: PropTypes.string,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isAdding: PropTypes.bool.isRequired,
|
||||||
|
addError: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onSeriesLookupChange: PropTypes.func.isRequired,
|
||||||
|
onClearSeriesLookup: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddNewSeries;
|
|
@ -0,0 +1,102 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import parseUrl from 'Utilities/String/parseUrl';
|
||||||
|
import { lookupSeries, clearAddSeries } from 'Store/Actions/addSeriesActions';
|
||||||
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import AddNewSeries from './AddNewSeries';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.addSeries,
|
||||||
|
(state) => state.routing.location,
|
||||||
|
(addSeries, location) => {
|
||||||
|
const { params } = parseUrl(location.search);
|
||||||
|
|
||||||
|
return {
|
||||||
|
term: params.term,
|
||||||
|
...addSeries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
lookupSeries,
|
||||||
|
clearAddSeries,
|
||||||
|
fetchRootFolders
|
||||||
|
};
|
||||||
|
|
||||||
|
class AddNewSeriesConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this._seriesLookupTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchRootFolders();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this._seriesLookupTimeout) {
|
||||||
|
clearTimeout(this._seriesLookupTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.clearAddSeries();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSeriesLookupChange = (term) => {
|
||||||
|
if (this._seriesLookupTimeout) {
|
||||||
|
clearTimeout(this._seriesLookupTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (term.trim() === '') {
|
||||||
|
this.props.clearAddSeries();
|
||||||
|
} else {
|
||||||
|
this._seriesLookupTimeout = setTimeout(() => {
|
||||||
|
this.props.lookupSeries({ term });
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClearSeriesLookup = () => {
|
||||||
|
this.props.clearAddSeries();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
term,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AddNewSeries
|
||||||
|
term={term}
|
||||||
|
{...otherProps}
|
||||||
|
onSeriesLookupChange={this.onSeriesLookupChange}
|
||||||
|
onClearSeriesLookup={this.onClearSeriesLookup}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewSeriesConnector.propTypes = {
|
||||||
|
term: PropTypes.string,
|
||||||
|
lookupSeries: PropTypes.func.isRequired,
|
||||||
|
clearAddSeries: PropTypes.func.isRequired,
|
||||||
|
fetchRootFolders: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewSeriesConnector);
|
|
@ -0,0 +1,31 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import AddNewSeriesModalContentConnector from './AddNewSeriesModalContentConnector';
|
||||||
|
|
||||||
|
function AddNewSeriesModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AddNewSeriesModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewSeriesModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddNewSeriesModal;
|
|
@ -0,0 +1,74 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: $disabledColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
flex: 0 0 170px;
|
||||||
|
margin-right: 20px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingEpisodesLabelContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingEpisodesLabel {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingEpisodesContainer {
|
||||||
|
composes: container from 'Components/Form/CheckInput.css';
|
||||||
|
|
||||||
|
flex: 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingEpisodesInput {
|
||||||
|
composes: input from 'Components/Form/CheckInput.css';
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter {
|
||||||
|
composes: modalFooter from 'Components/Modal/ModalFooter.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
@add-mixin truncate;
|
||||||
|
composes: button from 'Components/Link/SpinnerButton.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideLanguageProfile {
|
||||||
|
composes: group from 'Components/Form/FormGroup.css';
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.modalFooter {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,265 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons, kinds, inputTypes, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import SeriesPoster from 'Series/SeriesPoster';
|
||||||
|
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
||||||
|
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
||||||
|
import styles from './AddNewSeriesModalContent.css';
|
||||||
|
|
||||||
|
class AddNewSeriesModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
searchForMissingEpisodes: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSearchForMissingEpisodesChange = ({ value }) => {
|
||||||
|
this.setState({ searchForMissingEpisodes: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onQualityProfileIdChange = ({ value }) => {
|
||||||
|
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||||
|
}
|
||||||
|
|
||||||
|
onLanguageProfileIdChange = ({ value }) => {
|
||||||
|
this.props.onInputChange({ name: 'languageProfileId', value: parseInt(value) });
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddSeriesPress = () => {
|
||||||
|
this.props.onAddSeriesPress(this.state.searchForMissingEpisodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
overview,
|
||||||
|
images,
|
||||||
|
isAdding,
|
||||||
|
rootFolderPath,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
languageProfileId,
|
||||||
|
seriesType,
|
||||||
|
seasonFolder,
|
||||||
|
tags,
|
||||||
|
showLanguageProfile,
|
||||||
|
isSmallScreen,
|
||||||
|
onModalClose,
|
||||||
|
onInputChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
{
|
||||||
|
!title.contains(year) && !!year &&
|
||||||
|
<span className={styles.year}>({year})</span>
|
||||||
|
}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<div className={styles.poster}>
|
||||||
|
<SeriesPoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.overview}>
|
||||||
|
{overview}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Root Folder</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...rootFolderPath}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
Monitor
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Monitoring Options"
|
||||||
|
body={<SeriesMonitoringOptionsPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_EPISODES_SELECT}
|
||||||
|
name="monitor"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...monitor}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Quality Profile</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
onChange={this.onQualityProfileIdChange}
|
||||||
|
{...qualityProfileId}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup className={showLanguageProfile ? undefined : styles.hideLanguageProfile}>
|
||||||
|
<FormLabel>Language Profile</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.LANGUAGE_PROFILE_SELECT}
|
||||||
|
name="languageProfileId"
|
||||||
|
onChange={this.onLanguageProfileIdChange}
|
||||||
|
{...languageProfileId}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
Series Type
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Series Types"
|
||||||
|
body={<SeriesTypePopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SERIES_TYPE_SELECT}
|
||||||
|
name="seriesType"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...seriesType}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Season Folder</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="seasonFolder"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...seasonFolder}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Tags</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...tags}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<label className={styles.searchForMissingEpisodesLabelContainer}>
|
||||||
|
<span className={styles.searchForMissingEpisodesLabel}>
|
||||||
|
Start search for missing episodes
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckInput
|
||||||
|
containerClassName={styles.searchForMissingEpisodesContainer}
|
||||||
|
className={styles.searchForMissingEpisodesInput}
|
||||||
|
name="searchForMissingEpisodes"
|
||||||
|
value={this.state.searchForMissingEpisodes}
|
||||||
|
onChange={this.onSearchForMissingEpisodesChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.addButton}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
isSpinning={isAdding}
|
||||||
|
onPress={this.onAddSeriesPress}
|
||||||
|
>
|
||||||
|
Add {title}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewSeriesModalContent.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
overview: PropTypes.string,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isAdding: PropTypes.bool.isRequired,
|
||||||
|
addError: PropTypes.object,
|
||||||
|
rootFolderPath: PropTypes.object,
|
||||||
|
monitor: PropTypes.object.isRequired,
|
||||||
|
qualityProfileId: PropTypes.object,
|
||||||
|
languageProfileId: PropTypes.object,
|
||||||
|
seriesType: PropTypes.object.isRequired,
|
||||||
|
seasonFolder: PropTypes.object.isRequired,
|
||||||
|
tags: PropTypes.object.isRequired,
|
||||||
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onAddSeriesPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddNewSeriesModalContent;
|
|
@ -0,0 +1,108 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setAddSeriesDefault, addSeries } from 'Store/Actions/addSeriesActions';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import AddNewSeriesModalContent from './AddNewSeriesModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.addSeries,
|
||||||
|
(state) => state.settings.languageProfiles,
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(addSeriesState, languageProfiles, dimensions) => {
|
||||||
|
const {
|
||||||
|
isAdding,
|
||||||
|
addError,
|
||||||
|
defaults
|
||||||
|
} = addSeriesState;
|
||||||
|
|
||||||
|
const {
|
||||||
|
settings,
|
||||||
|
validationErrors,
|
||||||
|
validationWarnings
|
||||||
|
} = selectSettings(defaults, {}, addError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAdding,
|
||||||
|
addError,
|
||||||
|
showLanguageProfile: languageProfiles.items.length > 1,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
validationErrors,
|
||||||
|
validationWarnings,
|
||||||
|
...settings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setAddSeriesDefault,
|
||||||
|
addSeries
|
||||||
|
};
|
||||||
|
|
||||||
|
class AddNewSeriesModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setAddSeriesDefault({ [name]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddSeriesPress = (searchForMissingEpisodes) => {
|
||||||
|
const {
|
||||||
|
tvdbId,
|
||||||
|
rootFolderPath,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
languageProfileId,
|
||||||
|
seriesType,
|
||||||
|
seasonFolder,
|
||||||
|
tags
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.props.addSeries({
|
||||||
|
tvdbId,
|
||||||
|
rootFolderPath: rootFolderPath.value,
|
||||||
|
monitor: monitor.value,
|
||||||
|
qualityProfileId: qualityProfileId.value,
|
||||||
|
languageProfileId: languageProfileId.value,
|
||||||
|
seriesType: seriesType.value,
|
||||||
|
seasonFolder: seasonFolder.value,
|
||||||
|
tags: tags.value,
|
||||||
|
searchForMissingEpisodes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AddNewSeriesModalContent
|
||||||
|
{...this.props}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onAddSeriesPress={this.onAddSeriesPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewSeriesModalContentConnector.propTypes = {
|
||||||
|
tvdbId: PropTypes.number.isRequired,
|
||||||
|
rootFolderPath: PropTypes.object,
|
||||||
|
monitor: PropTypes.object.isRequired,
|
||||||
|
qualityProfileId: PropTypes.object,
|
||||||
|
languageProfileId: PropTypes.object,
|
||||||
|
seriesType: PropTypes.object.isRequired,
|
||||||
|
seasonFolder: PropTypes.object.isRequired,
|
||||||
|
tags: PropTypes.object.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
setAddSeriesDefault: PropTypes.func.isRequired,
|
||||||
|
addSeries: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewSeriesModalContentConnector);
|
|
@ -0,0 +1,40 @@
|
||||||
|
.searchResult {
|
||||||
|
display: flex;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: $white;
|
||||||
|
color: inherit;
|
||||||
|
transition: background 500ms;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eaf2ff;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
flex: 0 0 170px;
|
||||||
|
margin-right: 20px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: $disabledColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alreadyExistsIcon {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #37bc9b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import HeartRating from 'Components/HeartRating';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import SeriesPoster from 'Series/SeriesPoster';
|
||||||
|
import AddNewSeriesModal from './AddNewSeriesModal';
|
||||||
|
import styles from './AddNewSeriesSearchResult.css';
|
||||||
|
|
||||||
|
class AddNewSeriesSearchResult extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isNewAddSeriesModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (!prevProps.isExistingSeries && this.props.isExistingSeries) {
|
||||||
|
this.onAddSerisModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onPress = () => {
|
||||||
|
this.setState({ isNewAddSeriesModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddSerisModalClose = () => {
|
||||||
|
this.setState({ isNewAddSeriesModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
tvdbId,
|
||||||
|
title,
|
||||||
|
titleSlug,
|
||||||
|
year,
|
||||||
|
network,
|
||||||
|
status,
|
||||||
|
overview,
|
||||||
|
statistics,
|
||||||
|
ratings,
|
||||||
|
images,
|
||||||
|
isExistingSeries,
|
||||||
|
isSmallScreen
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const seasonCount = statistics.seasonCount;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isNewAddSeriesModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const linkProps = isExistingSeries ? { to: `/series/${titleSlug}` } : { onPress: this.onPress };
|
||||||
|
let seasons = '1 Season';
|
||||||
|
|
||||||
|
if (seasonCount > 1) {
|
||||||
|
seasons = `${seasonCount} Seasons`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
className={styles.searchResult}
|
||||||
|
{...linkProps}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<SeriesPoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
{
|
||||||
|
!title.contains(year) && !!year &&
|
||||||
|
<span className={styles.year}>({year})</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isExistingSeries &&
|
||||||
|
<Icon
|
||||||
|
className={styles.alreadyExistsIcon}
|
||||||
|
name={icons.CHECK_CIRCLE}
|
||||||
|
size={36}
|
||||||
|
title="Already in your library"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<HeartRating
|
||||||
|
rating={ratings.value}
|
||||||
|
iconSize={13}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{
|
||||||
|
!!network &&
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
{network}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!seasonCount &&
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
{seasons}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
status === 'ended' &&
|
||||||
|
<Label
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
Ended
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.overview}>
|
||||||
|
{overview}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<AddNewSeriesModal
|
||||||
|
isOpen={isNewAddSeriesModalOpen && !isExistingSeries}
|
||||||
|
tvdbId={tvdbId}
|
||||||
|
title={title}
|
||||||
|
year={year}
|
||||||
|
overview={overview}
|
||||||
|
images={images}
|
||||||
|
onModalClose={this.onAddSerisModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewSeriesSearchResult.propTypes = {
|
||||||
|
tvdbId: PropTypes.number.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
titleSlug: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
network: PropTypes.string,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
overview: PropTypes.string,
|
||||||
|
statistics: PropTypes.object.isRequired,
|
||||||
|
ratings: PropTypes.object.isRequired,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isExistingSeries: PropTypes.bool.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddNewSeriesSearchResult;
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import AddNewSeriesSearchResult from './AddNewSeriesSearchResult';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createExistingSeriesSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(isExistingSeries, dimensions) => {
|
||||||
|
return {
|
||||||
|
isExistingSeries,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(AddNewSeriesSearchResult);
|
|
@ -0,0 +1,173 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
|
import ImportSeriesTableConnector from './ImportSeriesTableConnector';
|
||||||
|
import ImportSeriesFooterConnector from './ImportSeriesFooterConnector';
|
||||||
|
|
||||||
|
class ImportSeries extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {},
|
||||||
|
contentBody: null,
|
||||||
|
scrollTop: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setContentBodyRef = (ref) => {
|
||||||
|
this.setState({ contentBody: ref });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
return getSelectedIds(this.state.selectedState, { parseIds: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
// Only select non-dupes
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveSelectedStateItem = (id) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
const selectedState = Object.assign({}, state.selectedState);
|
||||||
|
delete selectedState[id];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedState
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.onInputChange(this.getSelectedIds(), name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onImportPress = () => {
|
||||||
|
this.props.onImportPress(this.getSelectedIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll = ({ scrollTop }) => {
|
||||||
|
this.setState({ scrollTop });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
rootFolderId,
|
||||||
|
path,
|
||||||
|
rootFoldersFetching,
|
||||||
|
rootFoldersPopulated,
|
||||||
|
rootFoldersError,
|
||||||
|
unmappedFolders,
|
||||||
|
showLanguageProfile
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState,
|
||||||
|
contentBody
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title="Import Series">
|
||||||
|
<PageContentBodyConnector
|
||||||
|
ref={this.setContentBodyRef}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
rootFoldersFetching && !rootFoldersPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!rootFoldersFetching && !!rootFoldersError &&
|
||||||
|
<div>Unable to load root folders</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!rootFoldersError && rootFoldersPopulated && !unmappedFolders.length &&
|
||||||
|
<div>
|
||||||
|
All series in {path} have been imported
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody &&
|
||||||
|
<ImportSeriesTableConnector
|
||||||
|
rootFolderId={rootFolderId}
|
||||||
|
unmappedFolders={unmappedFolders}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
selectedState={selectedState}
|
||||||
|
contentBody={contentBody}
|
||||||
|
showLanguageProfile={showLanguageProfile}
|
||||||
|
scrollTop={this.state.scrollTop}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</PageContentBodyConnector>
|
||||||
|
|
||||||
|
{
|
||||||
|
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
||||||
|
<ImportSeriesFooterConnector
|
||||||
|
selectedIds={this.getSelectedIds()}
|
||||||
|
showLanguageProfile={showLanguageProfile}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onImportPress={this.onImportPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportSeries.propTypes = {
|
||||||
|
rootFolderId: PropTypes.number.isRequired,
|
||||||
|
path: PropTypes.string,
|
||||||
|
rootFoldersFetching: PropTypes.bool.isRequired,
|
||||||
|
rootFoldersPopulated: PropTypes.bool.isRequired,
|
||||||
|
rootFoldersError: PropTypes.object,
|
||||||
|
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onImportPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ImportSeries.defaultProps = {
|
||||||
|
unmappedFolders: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportSeries;
|
|
@ -0,0 +1,169 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setImportSeriesValue, importSeries, clearImportSeries } from 'Store/Actions/importSeriesActions';
|
||||||
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import { setAddSeriesDefault } from 'Store/Actions/addSeriesActions';
|
||||||
|
import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape';
|
||||||
|
import ImportSeries from './ImportSeries';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { match }) => match,
|
||||||
|
(state) => state.rootFolders,
|
||||||
|
(state) => state.addSeries,
|
||||||
|
(state) => state.importSeries,
|
||||||
|
(state) => state.settings.qualityProfiles,
|
||||||
|
(state) => state.settings.languageProfiles,
|
||||||
|
(
|
||||||
|
match,
|
||||||
|
rootFolders,
|
||||||
|
addSeries,
|
||||||
|
importSeriesState,
|
||||||
|
qualityProfiles,
|
||||||
|
languageProfiles
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
isFetching: rootFoldersFetching,
|
||||||
|
isPopulated: rootFoldersPopulated,
|
||||||
|
error: rootFoldersError,
|
||||||
|
items
|
||||||
|
} = rootFolders;
|
||||||
|
|
||||||
|
const rootFolderId = parseInt(match.params.rootFolderId);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
rootFolderId,
|
||||||
|
rootFoldersFetching,
|
||||||
|
rootFoldersPopulated,
|
||||||
|
rootFoldersError,
|
||||||
|
qualityProfiles: qualityProfiles.items,
|
||||||
|
languageProfiles: languageProfiles.items,
|
||||||
|
showLanguageProfile: languageProfiles.items.length > 1,
|
||||||
|
defaultQualityProfileId: addSeries.defaults.qualityProfileId,
|
||||||
|
defaultLanguageProfileId: addSeries.defaults.languageProfileId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (items.length) {
|
||||||
|
const rootFolder = _.find(items, { id: rootFolderId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
...rootFolder,
|
||||||
|
items: importSeriesState.items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchSetImportSeriesValue: setImportSeriesValue,
|
||||||
|
dispatchImportSeries: importSeries,
|
||||||
|
dispatchClearImportSeries: clearImportSeries,
|
||||||
|
dispatchFetchRootFolders: fetchRootFolders,
|
||||||
|
dispatchSetAddSeriesDefault: setAddSeriesDefault
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImportSeriesConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
qualityProfiles,
|
||||||
|
languageProfiles,
|
||||||
|
defaultQualityProfileId,
|
||||||
|
defaultLanguageProfileId,
|
||||||
|
dispatchFetchRootFolders,
|
||||||
|
dispatchSetAddSeriesDefault
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!this.props.rootFoldersPopulated) {
|
||||||
|
dispatchFetchRootFolders();
|
||||||
|
}
|
||||||
|
|
||||||
|
let setDefaults = false;
|
||||||
|
const setDefaultPayload = {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!defaultQualityProfileId ||
|
||||||
|
!qualityProfiles.some((p) => p.id === defaultQualityProfileId)
|
||||||
|
) {
|
||||||
|
setDefaults = true;
|
||||||
|
setDefaultPayload.qualityProfileId = qualityProfiles[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!defaultLanguageProfileId ||
|
||||||
|
!languageProfiles.some((p) => p.id === defaultLanguageProfileId)
|
||||||
|
) {
|
||||||
|
setDefaults = true;
|
||||||
|
setDefaultPayload.languageProfileId = languageProfiles[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setDefaults) {
|
||||||
|
dispatchSetAddSeriesDefault(setDefaultPayload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.dispatchClearImportSeries();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = (ids, name, value) => {
|
||||||
|
this.props.dispatchSetAddSeriesDefault({ [name]: value });
|
||||||
|
|
||||||
|
ids.forEach((id) => {
|
||||||
|
this.props.dispatchSetImportSeriesValue({
|
||||||
|
id,
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onImportPress = (ids) => {
|
||||||
|
this.props.dispatchImportSeries({ ids });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ImportSeries
|
||||||
|
{...this.props}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onImportPress={this.onImportPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeMatchShape = createRouteMatchShape({
|
||||||
|
rootFolderId: PropTypes.string.isRequired
|
||||||
|
});
|
||||||
|
|
||||||
|
ImportSeriesConnector.propTypes = {
|
||||||
|
match: routeMatchShape.isRequired,
|
||||||
|
rootFoldersPopulated: PropTypes.bool.isRequired,
|
||||||
|
qualityProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
languageProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
defaultQualityProfileId: PropTypes.number.isRequired,
|
||||||
|
defaultLanguageProfileId: PropTypes.number.isRequired,
|
||||||
|
dispatchSetImportSeriesValue: PropTypes.func.isRequired,
|
||||||
|
dispatchImportSeries: PropTypes.func.isRequired,
|
||||||
|
dispatchClearImportSeries: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||||
|
dispatchSetAddSeriesDefault: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesConnector);
|
|
@ -0,0 +1,33 @@
|
||||||
|
.inputContainer {
|
||||||
|
margin-right: 20px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.importButtonContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.importButton {
|
||||||
|
composes: button from 'Components/Link/SpinnerButton.css';
|
||||||
|
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingButton {
|
||||||
|
composes: importButton;
|
||||||
|
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
composes: loading from 'Components/Loading/LoadingIndicator.css';
|
||||||
|
|
||||||
|
margin: 0 10px 0 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
|
@ -0,0 +1,291 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
|
import styles from './ImportSeriesFooter.css';
|
||||||
|
|
||||||
|
const MIXED = 'mixed';
|
||||||
|
|
||||||
|
class ImportSeriesFooter extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const {
|
||||||
|
defaultMonitor,
|
||||||
|
defaultQualityProfileId,
|
||||||
|
defaultLanguageProfileId,
|
||||||
|
defaultSeasonFolder,
|
||||||
|
defaultSeriesType
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
monitor: defaultMonitor,
|
||||||
|
qualityProfileId: defaultQualityProfileId,
|
||||||
|
languageProfileId: defaultLanguageProfileId,
|
||||||
|
seriesType: defaultSeriesType,
|
||||||
|
seasonFolder: defaultSeasonFolder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const {
|
||||||
|
defaultMonitor,
|
||||||
|
defaultQualityProfileId,
|
||||||
|
defaultLanguageProfileId,
|
||||||
|
defaultSeriesType,
|
||||||
|
defaultSeasonFolder,
|
||||||
|
isMonitorMixed,
|
||||||
|
isQualityProfileIdMixed,
|
||||||
|
isLanguageProfileIdMixed,
|
||||||
|
isSeriesTypeMixed,
|
||||||
|
isSeasonFolderMixed
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
languageProfileId,
|
||||||
|
seriesType,
|
||||||
|
seasonFolder
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const newState = {};
|
||||||
|
|
||||||
|
if (isMonitorMixed && monitor !== MIXED) {
|
||||||
|
newState.monitor = MIXED;
|
||||||
|
} else if (!isMonitorMixed && monitor !== defaultMonitor) {
|
||||||
|
newState.monitor = defaultMonitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isQualityProfileIdMixed && qualityProfileId !== MIXED) {
|
||||||
|
newState.qualityProfileId = MIXED;
|
||||||
|
} else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) {
|
||||||
|
newState.qualityProfileId = defaultQualityProfileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLanguageProfileIdMixed && languageProfileId !== MIXED) {
|
||||||
|
newState.languageProfileId = MIXED;
|
||||||
|
} else if (!isLanguageProfileIdMixed && languageProfileId !== defaultLanguageProfileId) {
|
||||||
|
newState.languageProfileId = defaultLanguageProfileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSeriesTypeMixed && seriesType !== MIXED) {
|
||||||
|
newState.seriesType = MIXED;
|
||||||
|
} else if (!isSeriesTypeMixed && seriesType !== defaultSeriesType) {
|
||||||
|
newState.seriesType = defaultSeriesType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSeasonFolderMixed && seasonFolder != null) {
|
||||||
|
newState.seasonFolder = null;
|
||||||
|
} else if (!isSeasonFolderMixed && seasonFolder !== defaultSeasonFolder) {
|
||||||
|
newState.seasonFolder = defaultSeasonFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isEmpty(newState)) {
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.setState({ [name]: value });
|
||||||
|
this.props.onInputChange({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
selectedCount,
|
||||||
|
isImporting,
|
||||||
|
isLookingUpSeries,
|
||||||
|
isMonitorMixed,
|
||||||
|
isQualityProfileIdMixed,
|
||||||
|
isLanguageProfileIdMixed,
|
||||||
|
isSeriesTypeMixed,
|
||||||
|
hasUnsearchedItems,
|
||||||
|
showLanguageProfile,
|
||||||
|
onImportPress,
|
||||||
|
onLookupPress,
|
||||||
|
onCancelLookupPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
languageProfileId,
|
||||||
|
seriesType,
|
||||||
|
seasonFolder
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContentFooter>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
Monitor
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_EPISODES_SELECT}
|
||||||
|
name="monitor"
|
||||||
|
value={monitor}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
includeMixed={isMonitorMixed}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
Quality Profile
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
value={qualityProfileId}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
includeMixed={isQualityProfileIdMixed}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
showLanguageProfile &&
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
Language Profile
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.LANGUAGE_PROFILE_SELECT}
|
||||||
|
name="languageProfileId"
|
||||||
|
value={languageProfileId}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
includeMixed={isLanguageProfileIdMixed}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
Series Type
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SERIES_TYPE_SELECT}
|
||||||
|
name="seriesType"
|
||||||
|
value={seriesType}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
includeMixed={isSeriesTypeMixed}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
Season Folder
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CheckInput
|
||||||
|
name="seasonFolder"
|
||||||
|
value={seasonFolder}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className={styles.label}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.importButtonContainer}>
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.importButton}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={isImporting}
|
||||||
|
isDisabled={!selectedCount || isLookingUpSeries}
|
||||||
|
onPress={onImportPress}
|
||||||
|
>
|
||||||
|
Import {selectedCount} Series
|
||||||
|
</SpinnerButton>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLookingUpSeries &&
|
||||||
|
<Button
|
||||||
|
className={styles.loadingButton}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
onPress={onCancelLookupPress}
|
||||||
|
>
|
||||||
|
Cancel Processing
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
hasUnsearchedItems &&
|
||||||
|
<Button
|
||||||
|
className={styles.loadingButton}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
onPress={onLookupPress}
|
||||||
|
>
|
||||||
|
Start Processing
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isLookingUpSeries &&
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isLookingUpSeries &&
|
||||||
|
'Processing Folders'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContentFooter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportSeriesFooter.propTypes = {
|
||||||
|
selectedCount: PropTypes.number.isRequired,
|
||||||
|
isImporting: PropTypes.bool.isRequired,
|
||||||
|
isLookingUpSeries: PropTypes.bool.isRequired,
|
||||||
|
defaultMonitor: PropTypes.string.isRequired,
|
||||||
|
defaultQualityProfileId: PropTypes.number,
|
||||||
|
defaultLanguageProfileId: PropTypes.number,
|
||||||
|
defaultSeriesType: PropTypes.string.isRequired,
|
||||||
|
defaultSeasonFolder: PropTypes.bool.isRequired,
|
||||||
|
isMonitorMixed: PropTypes.bool.isRequired,
|
||||||
|
isQualityProfileIdMixed: PropTypes.bool.isRequired,
|
||||||
|
isLanguageProfileIdMixed: PropTypes.bool.isRequired,
|
||||||
|
isSeriesTypeMixed: PropTypes.bool.isRequired,
|
||||||
|
isSeasonFolderMixed: PropTypes.bool.isRequired,
|
||||||
|
hasUnsearchedItems: PropTypes.bool.isRequired,
|
||||||
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onImportPress: PropTypes.func.isRequired,
|
||||||
|
onLookupPress: PropTypes.func.isRequired,
|
||||||
|
onCancelLookupPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportSeriesFooter;
|
|
@ -0,0 +1,65 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { lookupUnsearchedSeries, cancelLookupSeries } from 'Store/Actions/importSeriesActions';
|
||||||
|
import ImportSeriesFooter from './ImportSeriesFooter';
|
||||||
|
|
||||||
|
function isMixed(items, selectedIds, defaultValue, key) {
|
||||||
|
return _.some(items, (series) => {
|
||||||
|
return selectedIds.indexOf(series.id) > -1 && series[key] !== defaultValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.addSeries,
|
||||||
|
(state) => state.importSeries,
|
||||||
|
(state, { selectedIds }) => selectedIds,
|
||||||
|
(addSeries, importSeries, selectedIds) => {
|
||||||
|
const {
|
||||||
|
monitor: defaultMonitor,
|
||||||
|
qualityProfileId: defaultQualityProfileId,
|
||||||
|
languageProfileId: defaultLanguageProfileId,
|
||||||
|
seriesType: defaultSeriesType,
|
||||||
|
seasonFolder: defaultSeasonFolder
|
||||||
|
} = addSeries.defaults;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLookingUpSeries,
|
||||||
|
isImporting,
|
||||||
|
items
|
||||||
|
} = importSeries;
|
||||||
|
|
||||||
|
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
||||||
|
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
|
||||||
|
const isLanguageProfileIdMixed = isMixed(items, selectedIds, defaultLanguageProfileId, 'languageProfileId');
|
||||||
|
const isSeriesTypeMixed = isMixed(items, selectedIds, defaultSeriesType, 'seriesType');
|
||||||
|
const isSeasonFolderMixed = isMixed(items, selectedIds, defaultSeasonFolder, 'seasonFolder');
|
||||||
|
const hasUnsearchedItems = !isLookingUpSeries && items.some((item) => !item.isPopulated);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedCount: selectedIds.length,
|
||||||
|
isLookingUpSeries,
|
||||||
|
isImporting,
|
||||||
|
defaultMonitor,
|
||||||
|
defaultQualityProfileId,
|
||||||
|
defaultLanguageProfileId,
|
||||||
|
defaultSeriesType,
|
||||||
|
defaultSeasonFolder,
|
||||||
|
isMonitorMixed,
|
||||||
|
isQualityProfileIdMixed,
|
||||||
|
isLanguageProfileIdMixed,
|
||||||
|
isSeriesTypeMixed,
|
||||||
|
isSeasonFolderMixed,
|
||||||
|
hasUnsearchedItems
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
onLookupPress: lookupUnsearchedSeries,
|
||||||
|
onCancelLookupPress: cancelLookupSeries
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesFooter);
|
|
@ -0,0 +1,45 @@
|
||||||
|
.folder {
|
||||||
|
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
flex: 1 0 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor {
|
||||||
|
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 200px;
|
||||||
|
min-width: 185px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityProfile,
|
||||||
|
.languageProfile {
|
||||||
|
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 250px;
|
||||||
|
min-width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seriesType {
|
||||||
|
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 200px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seasonFolder {
|
||||||
|
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 150px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series {
|
||||||
|
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 400px;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
|
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||||
|
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
||||||
|
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
||||||
|
import styles from './ImportSeriesHeader.css';
|
||||||
|
|
||||||
|
function ImportSeriesHeader(props) {
|
||||||
|
const {
|
||||||
|
showLanguageProfile,
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
onSelectAllChange
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableHeader>
|
||||||
|
<VirtualTableSelectAllHeaderCell
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
className={styles.folder}
|
||||||
|
name="folder"
|
||||||
|
>
|
||||||
|
Folder
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
className={styles.monitor}
|
||||||
|
name="monitor"
|
||||||
|
>
|
||||||
|
Monitor
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.detailsIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Monitoring Options"
|
||||||
|
body={<SeriesMonitoringOptionsPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
className={styles.qualityProfile}
|
||||||
|
name="qualityProfileId"
|
||||||
|
>
|
||||||
|
Quality Profile
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
|
||||||
|
{
|
||||||
|
showLanguageProfile &&
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
className={styles.languageProfile}
|
||||||
|
name="languageProfileId"
|
||||||
|
>
|
||||||
|
Language Profile
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
}
|
||||||
|
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
className={styles.seriesType}
|
||||||
|
name="seriesType"
|
||||||
|
>
|
||||||
|
Series Type
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.detailsIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Series Type"
|
||||||
|
body={<SeriesTypePopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
className={styles.seasonFolder}
|
||||||
|
name="seasonFolder"
|
||||||
|
>
|
||||||
|
Season Folder
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
className={styles.series}
|
||||||
|
name="series"
|
||||||
|
>
|
||||||
|
Series
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
</VirtualTableHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportSeriesHeader.propTypes = {
|
||||||
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
|
allSelected: PropTypes.bool.isRequired,
|
||||||
|
allUnselected: PropTypes.bool.isRequired,
|
||||||
|
onSelectAllChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportSeriesHeader;
|
|
@ -0,0 +1,52 @@
|
||||||
|
.selectInput {
|
||||||
|
composes: input from 'Components/Form/CheckInput.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder {
|
||||||
|
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
flex: 1 0 200px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor {
|
||||||
|
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 200px;
|
||||||
|
min-width: 185px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityProfile,
|
||||||
|
.languageProfile {
|
||||||
|
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 250px;
|
||||||
|
min-width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seriesType {
|
||||||
|
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 200px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seasonFolder {
|
||||||
|
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 150px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series {
|
||||||
|
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 400px;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideLanguageProfile {
|
||||||
|
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||||
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
|
import ImportSeriesSelectSeriesConnector from './SelectSeries/ImportSeriesSelectSeriesConnector';
|
||||||
|
import styles from './ImportSeriesRow.css';
|
||||||
|
|
||||||
|
function ImportSeriesRow(props) {
|
||||||
|
const {
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
languageProfileId,
|
||||||
|
seasonFolder,
|
||||||
|
seriesType,
|
||||||
|
selectedSeries,
|
||||||
|
isExistingSeries,
|
||||||
|
showLanguageProfile,
|
||||||
|
isSelected,
|
||||||
|
onSelectedChange,
|
||||||
|
onInputChange
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableRow style={style}>
|
||||||
|
<VirtualTableSelectCell
|
||||||
|
inputClassName={styles.selectInput}
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isDisabled={!selectedSeries || isExistingSeries}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VirtualTableRowCell className={styles.folder}>
|
||||||
|
{id}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
|
<VirtualTableRowCell className={styles.monitor}>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_EPISODES_SELECT}
|
||||||
|
name="monitor"
|
||||||
|
value={monitor}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
|
<VirtualTableRowCell className={styles.qualityProfile}>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
value={qualityProfileId}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
|
<VirtualTableRowCell
|
||||||
|
className={showLanguageProfile ? styles.languageProfile : styles.hideLanguageProfile}
|
||||||
|
>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.LANGUAGE_PROFILE_SELECT}
|
||||||
|
name="languageProfileId"
|
||||||
|
value={languageProfileId}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
|
<VirtualTableRowCell className={styles.seriesType}>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SERIES_TYPE_SELECT}
|
||||||
|
name="seriesType"
|
||||||
|
value={seriesType}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
|
<VirtualTableRowCell className={styles.seasonFolder}>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="seasonFolder"
|
||||||
|
value={seasonFolder}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
|
<VirtualTableRowCell className={styles.series}>
|
||||||
|
<ImportSeriesSelectSeriesConnector
|
||||||
|
id={id}
|
||||||
|
isExistingSeries={isExistingSeries}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
</VirtualTableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportSeriesRow.propTypes = {
|
||||||
|
style: PropTypes.object.isRequired,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
monitor: PropTypes.string.isRequired,
|
||||||
|
qualityProfileId: PropTypes.number.isRequired,
|
||||||
|
languageProfileId: PropTypes.number.isRequired,
|
||||||
|
seriesType: PropTypes.string.isRequired,
|
||||||
|
seasonFolder: PropTypes.bool.isRequired,
|
||||||
|
selectedSeries: PropTypes.object,
|
||||||
|
isExistingSeries: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ImportSeriesRow.defaultsProps = {
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportSeriesRow;
|
|
@ -0,0 +1,89 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||||
|
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||||
|
import ImportSeriesRow from './ImportSeriesRow';
|
||||||
|
|
||||||
|
function createImportSeriesItemSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { id }) => id,
|
||||||
|
(state) => state.importSeries.items,
|
||||||
|
(id, items) => {
|
||||||
|
return _.find(items, { id }) || {};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createImportSeriesItemSelector(),
|
||||||
|
createAllSeriesSelector(),
|
||||||
|
(item, series) => {
|
||||||
|
const selectedSeries = item && item.selectedSeries;
|
||||||
|
const isExistingSeries = !!selectedSeries && _.some(series, { tvdbId: selectedSeries.tvdbId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
isExistingSeries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setImportSeriesValue
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImportSeriesRowConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setImportSeriesValue({
|
||||||
|
id: this.props.id,
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// Don't show the row until we have the information we require for it.
|
||||||
|
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
monitor,
|
||||||
|
seriesType,
|
||||||
|
seasonFolder
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!items || !monitor || !seriesType || !seasonFolder == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImportSeriesRow
|
||||||
|
{...this.props}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onSeriesSelect={this.onSeriesSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportSeriesRowConnector.propTypes = {
|
||||||
|
rootFolderId: PropTypes.number.isRequired,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
monitor: PropTypes.string,
|
||||||
|
seriesType: PropTypes.string,
|
||||||
|
seasonFolder: PropTypes.bool,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
setImportSeriesValue: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesRowConnector);
|
|
@ -0,0 +1,3 @@
|
||||||
|
.input {
|
||||||
|
composes: input from 'Components/Form/CheckInput.css';
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import VirtualTable from 'Components/Table/VirtualTable';
|
||||||
|
import ImportSeriesHeader from './ImportSeriesHeader';
|
||||||
|
import ImportSeriesRowConnector from './ImportSeriesRowConnector';
|
||||||
|
|
||||||
|
class ImportSeriesTable extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
unmappedFolders,
|
||||||
|
defaultMonitor,
|
||||||
|
defaultQualityProfileId,
|
||||||
|
defaultLanguageProfileId,
|
||||||
|
defaultSeriesType,
|
||||||
|
defaultSeasonFolder,
|
||||||
|
onSeriesLookup,
|
||||||
|
onSetImportSeriesValue
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
monitor: defaultMonitor,
|
||||||
|
qualityProfileId: defaultQualityProfileId,
|
||||||
|
languageProfileId: defaultLanguageProfileId,
|
||||||
|
seriesType: defaultSeriesType,
|
||||||
|
seasonFolder: defaultSeasonFolder
|
||||||
|
};
|
||||||
|
|
||||||
|
unmappedFolders.forEach((unmappedFolder) => {
|
||||||
|
const id = unmappedFolder.name;
|
||||||
|
|
||||||
|
onSeriesLookup(id, unmappedFolder.path);
|
||||||
|
|
||||||
|
onSetImportSeriesValue({
|
||||||
|
id,
|
||||||
|
...values
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This isn't great, but it's the most reliable way to ensure the items
|
||||||
|
// are checked off even if they aren't actually visible since the cells
|
||||||
|
// are virtualized.
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
selectedState,
|
||||||
|
onSelectedChange,
|
||||||
|
onRemoveSelectedStateItem
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
prevProps.items.forEach((prevItem) => {
|
||||||
|
const {
|
||||||
|
id
|
||||||
|
} = prevItem;
|
||||||
|
|
||||||
|
const item = _.find(items, { id });
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
onRemoveSelectedStateItem(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedSeries = item.selectedSeries;
|
||||||
|
const isSelected = selectedState[id];
|
||||||
|
|
||||||
|
const isExistingSeries = !!selectedSeries &&
|
||||||
|
_.some(prevProps.allSeries, { tvdbId: selectedSeries.tvdbId });
|
||||||
|
|
||||||
|
// Props doesn't have a selected series or
|
||||||
|
// the selected series is an existing series.
|
||||||
|
if ((!selectedSeries && prevItem.selectedSeries) || (isExistingSeries && !prevItem.selectedSeries)) {
|
||||||
|
onSelectedChange({ id, value: false });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// State is selected, but a series isn't selected or
|
||||||
|
// the selected series is an existing series.
|
||||||
|
if (isSelected && (!selectedSeries || isExistingSeries)) {
|
||||||
|
onSelectedChange({ id, value: false });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A series is being selected that wasn't previously selected.
|
||||||
|
if (selectedSeries && selectedSeries !== prevItem.selectedSeries) {
|
||||||
|
onSelectedChange({ id, value: true });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
rowRenderer = ({ key, rowIndex, style }) => {
|
||||||
|
const {
|
||||||
|
rootFolderId,
|
||||||
|
items,
|
||||||
|
selectedState,
|
||||||
|
showLanguageProfile,
|
||||||
|
onSelectedChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const item = items[rowIndex];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImportSeriesRowConnector
|
||||||
|
key={key}
|
||||||
|
style={style}
|
||||||
|
rootFolderId={rootFolderId}
|
||||||
|
showLanguageProfile={showLanguageProfile}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
id={item.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
isSmallScreen,
|
||||||
|
contentBody,
|
||||||
|
showLanguageProfile,
|
||||||
|
scrollTop,
|
||||||
|
selectedState,
|
||||||
|
onSelectAllChange,
|
||||||
|
onScroll
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTable
|
||||||
|
items={items}
|
||||||
|
contentBody={contentBody}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
rowHeight={52}
|
||||||
|
scrollTop={scrollTop}
|
||||||
|
overscanRowCount={2}
|
||||||
|
rowRenderer={this.rowRenderer}
|
||||||
|
header={
|
||||||
|
<ImportSeriesHeader
|
||||||
|
showLanguageProfile={showLanguageProfile}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
selectedState={selectedState}
|
||||||
|
onScroll={onScroll}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportSeriesTable.propTypes = {
|
||||||
|
rootFolderId: PropTypes.number.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
defaultMonitor: PropTypes.string.isRequired,
|
||||||
|
defaultQualityProfileId: PropTypes.number,
|
||||||
|
defaultLanguageProfileId: PropTypes.number,
|
||||||
|
defaultSeriesType: PropTypes.string.isRequired,
|
||||||
|
defaultSeasonFolder: PropTypes.bool.isRequired,
|
||||||
|
allSelected: PropTypes.bool.isRequired,
|
||||||
|
allUnselected: PropTypes.bool.isRequired,
|
||||||
|
selectedState: PropTypes.object.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
allSeries: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
contentBody: PropTypes.object.isRequired,
|
||||||
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
onSelectAllChange: PropTypes.func.isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
|
onRemoveSelectedStateItem: PropTypes.func.isRequired,
|
||||||
|
onSeriesLookup: PropTypes.func.isRequired,
|
||||||
|
onSetImportSeriesValue: PropTypes.func.isRequired,
|
||||||
|
onScroll: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportSeriesTable;
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||||
|
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||||
|
import ImportSeriesTable from './ImportSeriesTable';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.addSeries,
|
||||||
|
(state) => state.importSeries,
|
||||||
|
(state) => state.app.dimensions,
|
||||||
|
createAllSeriesSelector(),
|
||||||
|
(addSeries, importSeries, dimensions, allSeries) => {
|
||||||
|
return {
|
||||||
|
defaultMonitor: addSeries.defaults.monitor,
|
||||||
|
defaultQualityProfileId: addSeries.defaults.qualityProfileId,
|
||||||
|
defaultLanguageProfileId: addSeries.defaults.languageProfileId,
|
||||||
|
defaultSeriesType: addSeries.defaults.seriesType,
|
||||||
|
defaultSeasonFolder: addSeries.defaults.seasonFolder,
|
||||||
|
items: importSeries.items,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
allSeries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onSeriesLookup(name, path) {
|
||||||
|
dispatch(queueLookupSeries({
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
term: name
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
onSetImportSeriesValue(values) {
|
||||||
|
dispatch(setImportSeriesValue(values));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(ImportSeriesTable);
|
|
@ -0,0 +1,8 @@
|
||||||
|
.series {
|
||||||
|
padding: 10px 20px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $menuItemHoverBackgroundColor;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||||
|
import styles from './ImportSeriesSearchResult.css';
|
||||||
|
|
||||||
|
class ImportSeriesSearchResult extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onPress = () => {
|
||||||
|
this.props.onPress(this.props.tvdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
network,
|
||||||
|
isExistingSeries
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={styles.series}
|
||||||
|
onPress={this.onPress}
|
||||||
|
>
|
||||||
|
<ImportSeriesTitle
|
||||||
|
title={title}
|
||||||
|
year={year}
|
||||||
|
network={network}
|
||||||
|
isExistingSeries={isExistingSeries}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportSeriesSearchResult.propTypes = {
|
||||||
|
tvdbId: PropTypes.number.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
network: PropTypes.string,
|
||||||
|
isExistingSeries: PropTypes.bool.isRequired,
|
||||||
|
onPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportSeriesSearchResult;
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector';
|
||||||
|
import ImportSeriesSearchResult from './ImportSeriesSearchResult';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createExistingSeriesSelector(),
|
||||||
|
(isExistingSeries) => {
|
||||||
|
return {
|
||||||
|
isExistingSeries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(ImportSeriesSearchResult);
|
|
@ -0,0 +1,81 @@
|
||||||
|
.tether {
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
composes: link from 'Components/Link/Link.css';
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: 35px;
|
||||||
|
border: 1px solid $inputBorderColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $white;
|
||||||
|
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warningIcon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.existing {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownArrowContainer {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
margin-left: 5px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentContainer {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 0 8px;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid $inputBorderColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchContainer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchIconContainer {
|
||||||
|
width: 58px;
|
||||||
|
border: 1px solid $inputBorderColor;
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
background-color: #edf1f2;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 33px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput {
|
||||||
|
composes: input from 'Components/Form/TextInput.css';
|
||||||
|
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
@add-mixin scrollbar;
|
||||||
|
@add-mixin scrollbarTrack;
|
||||||
|
@add-mixin scrollbarThumb;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
max-height: 165px;
|
||||||
|
}
|
|
@ -0,0 +1,280 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import TetherComponent from 'react-tether';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import FormInputButton from 'Components/Form/FormInputButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import ImportSeriesSearchResultConnector from './ImportSeriesSearchResultConnector';
|
||||||
|
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||||
|
import styles from './ImportSeriesSelectSeries.css';
|
||||||
|
|
||||||
|
const tetherOptions = {
|
||||||
|
skipMoveElement: true,
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
to: 'window',
|
||||||
|
attachment: 'together',
|
||||||
|
pin: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
attachment: 'top center',
|
||||||
|
targetAttachment: 'bottom center'
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImportSeriesSelectSeries extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this._seriesLookupTimeout = null;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
term: props.id,
|
||||||
|
isOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
_setButtonRef = (ref) => {
|
||||||
|
this._buttonRef = ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setContentRef = (ref) => {
|
||||||
|
this._contentRef = ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
_addListener() {
|
||||||
|
window.addEventListener('click', this.onWindowClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeListener() {
|
||||||
|
window.removeEventListener('click', this.onWindowClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onWindowClick = (event) => {
|
||||||
|
const button = ReactDOM.findDOMNode(this._buttonRef);
|
||||||
|
const content = ReactDOM.findDOMNode(this._contentRef);
|
||||||
|
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!button.contains(event.target) && content && !content.contains(event.target) && this.state.isOpen) {
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
this._removeListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPress = () => {
|
||||||
|
if (this.state.isOpen) {
|
||||||
|
this._removeListener();
|
||||||
|
} else {
|
||||||
|
this._addListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ isOpen: !this.state.isOpen });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchInputChange = ({ value }) => {
|
||||||
|
if (this._seriesLookupTimeout) {
|
||||||
|
clearTimeout(this._seriesLookupTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ term: value }, () => {
|
||||||
|
this._seriesLookupTimeout = setTimeout(() => {
|
||||||
|
this.props.onSearchInputChange(value);
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onRefreshPress = () => {
|
||||||
|
this.props.onSearchInputChange(this.state.term);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSeriesSelect = (tvdbId) => {
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
|
||||||
|
this.props.onSeriesSelect(tvdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
selectedSeries,
|
||||||
|
isExistingSeries,
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
isQueued,
|
||||||
|
isLookingUpSeries
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const errorMessage = error &&
|
||||||
|
error.responseJSON &&
|
||||||
|
error.responseJSON.message;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TetherComponent
|
||||||
|
classes={{
|
||||||
|
element: styles.tether
|
||||||
|
}}
|
||||||
|
{...tetherOptions}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
ref={this._setButtonRef}
|
||||||
|
className={styles.button}
|
||||||
|
component="div"
|
||||||
|
onPress={this.onPress}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isLookingUpSeries && isQueued && !isPopulated &&
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && selectedSeries && isExistingSeries &&
|
||||||
|
<Icon
|
||||||
|
className={styles.warningIcon}
|
||||||
|
name={icons.WARNING}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && selectedSeries &&
|
||||||
|
<ImportSeriesTitle
|
||||||
|
title={selectedSeries.title}
|
||||||
|
year={selectedSeries.year}
|
||||||
|
network={selectedSeries.network}
|
||||||
|
isExistingSeries={isExistingSeries}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !selectedSeries &&
|
||||||
|
<div className={styles.noMatches}>
|
||||||
|
<Icon
|
||||||
|
className={styles.warningIcon}
|
||||||
|
name={icons.WARNING}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
/>
|
||||||
|
|
||||||
|
No match found!
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>
|
||||||
|
<Icon
|
||||||
|
className={styles.warningIcon}
|
||||||
|
title={errorMessage}
|
||||||
|
name={icons.WARNING}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Search failed, please try again later.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.dropdownArrowContainer}>
|
||||||
|
<Icon
|
||||||
|
name={icons.CARET_DOWN}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{
|
||||||
|
this.state.isOpen &&
|
||||||
|
<div
|
||||||
|
ref={this._setContentRef}
|
||||||
|
className={styles.contentContainer}
|
||||||
|
>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.searchContainer}>
|
||||||
|
<div className={styles.searchIconContainer}>
|
||||||
|
<Icon name={icons.SEARCH} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
className={styles.searchInput}
|
||||||
|
name={`${name}_textInput`}
|
||||||
|
value={this.state.term}
|
||||||
|
onChange={this.onSearchInputChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputButton
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
spinnerIcon={icons.REFRESH}
|
||||||
|
canSpin={true}
|
||||||
|
isSpinning={isFetching}
|
||||||
|
onPress={this.onRefreshPress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.REFRESH} />
|
||||||
|
</FormInputButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.results}>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<ImportSeriesSearchResultConnector
|
||||||
|
key={item.tvdbId}
|
||||||
|
tvdbId={item.tvdbId}
|
||||||
|
title={item.title}
|
||||||
|
year={item.year}
|
||||||
|
network={item.network}
|
||||||
|
onPress={this.onSeriesSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</TetherComponent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportSeriesSelectSeries.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
selectedSeries: PropTypes.object,
|
||||||
|
isExistingSeries: PropTypes.bool.isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isQueued: PropTypes.bool.isRequired,
|
||||||
|
isLookingUpSeries: PropTypes.bool.isRequired,
|
||||||
|
onSearchInputChange: PropTypes.func.isRequired,
|
||||||
|
onSeriesSelect: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ImportSeriesSelectSeries.defaultProps = {
|
||||||
|
isFetching: true,
|
||||||
|
isPopulated: false,
|
||||||
|
items: [],
|
||||||
|
isQueued: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportSeriesSelectSeries;
|
|
@ -0,0 +1,76 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||||
|
import createImportSeriesItemSelector from 'Store/Selectors/createImportSeriesItemSelector';
|
||||||
|
import ImportSeriesSelectSeries from './ImportSeriesSelectSeries';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.importSeries.isLookingUpSeries,
|
||||||
|
createImportSeriesItemSelector(),
|
||||||
|
(isLookingUpSeries, item) => {
|
||||||
|
return {
|
||||||
|
isLookingUpSeries,
|
||||||
|
...item
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
queueLookupSeries,
|
||||||
|
setImportSeriesValue
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImportSeriesSelectSeriesConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSearchInputChange = (term) => {
|
||||||
|
this.props.queueLookupSeries({
|
||||||
|
name: this.props.id,
|
||||||
|
term,
|
||||||
|
topOfQueue: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSeriesSelect = (tvdbId) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.props.setImportSeriesValue({
|
||||||
|
id,
|
||||||
|
selectedSeries: _.find(items, { tvdbId })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ImportSeriesSelectSeries
|
||||||
|
{...this.props}
|
||||||
|
onSearchInputChange={this.onSearchInputChange}
|
||||||
|
onSeriesSelect={this.onSeriesSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportSeriesSelectSeriesConnector.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
selectedSeries: PropTypes.object,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
queueLookupSeries: PropTypes.func.isRequired,
|
||||||
|
setImportSeriesValue: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesSelectSeriesConnector);
|
|
@ -0,0 +1,20 @@
|
||||||
|
.titleContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@add-mixin truncate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year {
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-left: 5px;
|
||||||
|
color: $disabledColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.existing {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import styles from './ImportSeriesTitle.css';
|
||||||
|
|
||||||
|
function ImportSeriesTitle(props) {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
network,
|
||||||
|
isExistingSeries
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.titleContainer}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
!title.contains(year) &&
|
||||||
|
year > 0 &&
|
||||||
|
<span className={styles.year}>
|
||||||
|
({year})
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!network &&
|
||||||
|
<Label>{network}</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isExistingSeries &&
|
||||||
|
<Label
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
>
|
||||||
|
Existing
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportSeriesTitle.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
network: PropTypes.string,
|
||||||
|
isExistingSeries: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportSeriesTitle;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Route } from 'react-router-dom';
|
||||||
|
import Switch from 'Components/Router/Switch';
|
||||||
|
import ImportSeriesSelectFolderConnector from 'AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector';
|
||||||
|
import ImportSeriesConnector from 'AddSeries/ImportSeries/Import/ImportSeriesConnector';
|
||||||
|
|
||||||
|
class ImportSeries extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route
|
||||||
|
exact={true}
|
||||||
|
path="/add/import"
|
||||||
|
component={ImportSeriesSelectFolderConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/add/import/:rootFolderId"
|
||||||
|
component={ImportSeriesConnector}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportSeries;
|
|
@ -0,0 +1,18 @@
|
||||||
|
.link {
|
||||||
|
composes: link from 'Components/Link/Link.css';
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeSpace,
|
||||||
|
.unmappedFolders {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 45px;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue