New: Many UI Updates and Performance Tweaks

pull/3564/head
Qstick 5 years ago
parent b24a40797f
commit 6275737ced

@ -28,6 +28,12 @@
"react" "react"
], ],
"settings": {
"react": {
"version": "detect"
}
},
"rules": { "rules": {
"filenames/match-exported": ["error"], "filenames/match-exported": ["error"],

@ -0,0 +1,35 @@
const loose = true;
module.exports = {
plugins: [
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
// Stage 2
'@babel/plugin-proposal-export-namespace-from',
// Stage 3
['@babel/plugin-proposal-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import'
],
env: {
development: {
presets: [
['@babel/preset-react', { development: true }]
],
plugins: [
'babel-plugin-inline-classnames'
]
},
production: {
presets: [
'@babel/preset-react'
],
plugins: [
'babel-plugin-transform-react-remove-prop-types'
]
}
}
};

@ -0,0 +1,6 @@
module.exports = [
'>0.25%',
'not ie 11',
'not op_mini all',
'not chrome < 60'
];

@ -1,15 +1,18 @@
const gulp = require('gulp'); const gulp = require('gulp');
const runSequence = require('run-sequence');
require('./clean'); require('./clean');
require('./copy'); require('./copy');
require('./webpack');
gulp.task('build',
gulp.series('clean',
gulp.parallel(
'webpack',
'copyHtml',
'copyFonts',
'copyImages',
'copyJs'
)
)
);
gulp.task('build', () => {
return runSequence('clean', [
'webpack',
'copyHtml',
'copyFonts',
'copyImages',
'copyJs'
]);
});

@ -1,15 +1,15 @@
var path = require('path'); const path = require('path');
var gulp = require('gulp'); const gulp = require('gulp');
var print = require('gulp-print').default; const print = require('gulp-print').default;
var cache = require('gulp-cached'); const cache = require('gulp-cached');
var livereload = require('gulp-livereload'); const livereload = require('gulp-livereload');
var paths = require('./helpers/paths.js'); const paths = require('./helpers/paths.js');
gulp.task('copyJs', () => { gulp.task('copyJs', () => {
return gulp.src( return gulp.src(
[ [
path.join(paths.src.root, 'polyfills.js') path.join(paths.src.root, 'polyfills.js')
]) ], { base: paths.src.root })
.pipe(cache('copyJs')) .pipe(cache('copyJs'))
.pipe(print()) .pipe(print())
.pipe(gulp.dest(paths.dest.root)) .pipe(gulp.dest(paths.dest.root))
@ -17,7 +17,7 @@ gulp.task('copyJs', () => {
}); });
gulp.task('copyHtml', () => { gulp.task('copyHtml', () => {
return gulp.src(paths.src.html) return gulp.src(paths.src.html, { base: paths.src.root })
.pipe(cache('copyHtml')) .pipe(cache('copyHtml'))
.pipe(print()) .pipe(print())
.pipe(gulp.dest(paths.dest.root)) .pipe(gulp.dest(paths.dest.root))
@ -26,20 +26,20 @@ gulp.task('copyHtml', () => {
gulp.task('copyFonts', () => { gulp.task('copyFonts', () => {
return gulp.src( return gulp.src(
path.join(paths.src.fonts, '**', '*.*') path.join(paths.src.fonts, '**', '*.*'), { base: paths.src.root }
) )
.pipe(cache('copyFonts')) .pipe(cache('copyFonts'))
.pipe(print()) .pipe(print())
.pipe(gulp.dest(paths.dest.fonts)) .pipe(gulp.dest(paths.dest.root))
.pipe(livereload()); .pipe(livereload());
}); });
gulp.task('copyImages', () => { gulp.task('copyImages', () => {
return gulp.src( return gulp.src(
path.join(paths.src.images, '**', '*.*') path.join(paths.src.images, '**', '*.*'), { base: paths.src.root }
) )
.pipe(cache('copyImages')) .pipe(cache('copyImages'))
.pipe(print()) .pipe(print())
.pipe(gulp.dest(paths.dest.images)) .pipe(gulp.dest(paths.dest.root))
.pipe(livereload()); .pipe(livereload());
}); });

@ -1,8 +1,5 @@
require('./build.js'); require('./build.js');
require('./clean.js'); require('./clean.js');
require('./copy.js'); require('./copy.js');
require('./imageMin.js');
require('./start.js');
require('./stripBom.js');
require('./watch.js'); require('./watch.js');
require('./webpack.js'); require('./webpack.js');

@ -1,6 +1,6 @@
const gulpUtil = require('gulp-util'); const colors = require('ansi-colors');
module.exports = function errorHandler(error) { module.exports = function errorHandler(error) {
gulpUtil.log(gulpUtil.colors.red(`Error (${error.plugin}): ${error.message}`)); console.log(colors.red(`Error (${error.plugin}): ${error.message}`));
this.emit('end'); this.emit('end');
}; };

@ -1,15 +0,0 @@
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;
};

@ -1,15 +1,15 @@
const root = './frontend/src/'; const root = './frontend/src';
const paths = { const paths = {
src: { src: {
root, root,
html: root + '*.html', html: `${root}/*.html`,
scripts: root + '**/*.js', scripts: `${root}/**/*.js`,
content: root + 'Content/', content: `${root}/Content/`,
fonts: root + 'Content/Fonts/', fonts: `${root}/Content/Fonts/`,
images: root + 'Content/Images/', images: `${root}/Content/Images/`,
exclude: { exclude: {
libs: `!${root}JsLibraries/**` libs: `!${root}/JsLibraries/**`
} }
}, },
dest: { dest: {

@ -1,15 +0,0 @@
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/'));
});

@ -1,104 +0,0 @@
// will download and run radarr (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://radarr.aeonlucid.com/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/Radarr.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);
});
});

@ -1,13 +0,0 @@
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.task('stripBom', () => {
stripBom(paths.src.root);
});

@ -1,27 +1,18 @@
const gulp = require('gulp'); const gulp = require('gulp');
const livereload = require('gulp-livereload'); const livereload = require('gulp-livereload');
const watch = require('gulp-watch'); const gulpWatch = require('gulp-watch');
const paths = require('./helpers/paths.js'); const paths = require('./helpers/paths.js');
require('./copy.js'); require('./copy.js');
require('./webpack.js'); require('./webpack.js');
function watchTask(glob, task) { function watch() {
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 }); livereload.listen({ start: true });
gulp.start('webpackWatch'); gulp.task('webpackWatch')();
gulpWatch(paths.src.html, gulp.series('copyHtml'));
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
}
watchTask(paths.src.html, 'copyHtml'); gulp.task('watch', gulp.series('build', watch));
watchTask(`${paths.src.fonts}**/*.*`, 'copyFonts');
watchTask(`${paths.src.images}**/*.*`, 'copyImages');
});

@ -4,14 +4,15 @@ 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 ExtractTextPlugin = require('extract-text-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const browsers = require('../browsers');
const uiFolder = 'UI'; const uiFolder = 'UI';
const root = path.join(__dirname, '..', 'src'); const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = process.argv.indexOf('--production') > -1; const isProduction = process.argv.indexOf('--production') > -1;
console.log('ROOT:', root); console.log('Source Folder:', srcFolder);
console.log('isProduction:', isProduction); console.log('isProduction:', isProduction);
const cssVarsFiles = [ const cssVarsFiles = [
@ -21,40 +22,19 @@ const cssVarsFiles = [
'../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 = [ const plugins = [
extractCSSPlugin,
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
__DEV__: !isProduction, __DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development') 'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: path.join('_output', uiFolder, 'Content', 'styles.css')
}) })
]; ];
if (isProduction) {
plugins.push(new UglifyJSPlugin({
sourceMap: true,
uglifyOptions: {
mangle: false,
output: {
comments: false,
beautify: true
}
}
}));
}
const config = { const config = {
mode: isProduction ? 'production' : 'development',
devtool: '#source-map', devtool: '#source-map',
stats: { stats: {
@ -73,8 +53,8 @@ const config = {
resolve: { resolve: {
modules: [ modules: [
root, srcFolder,
path.join(root, 'Shims'), path.join(srcFolder, 'Shims'),
'node_modules' 'node_modules'
], ],
alias: { alias: {
@ -87,6 +67,10 @@ const config = {
sourceMapFilename: '[file].map' sourceMapFilename: '[file].map'
}, },
optimization: {
chunkIds: 'named'
},
plugins, plugins,
resolveLoader: { resolveLoader: {
@ -101,53 +85,56 @@ const config = {
{ {
test: /\.js?$/, test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/, exclude: /(node_modules|JsLibraries)/,
loader: 'babel-loader', use: [
query: { {
plugins: ['transform-class-properties'], loader: 'babel-loader',
presets: ['es2015', 'decorators-legacy', 'react', 'stage-2'], options: {
env: { configFile: `${frontendFolder}/babel.config.js`,
development: { envName: isProduction ? 'production' : 'development',
plugins: ['transform-react-jsx-source'] presets: [
[
'@babel/preset-env',
{
modules: false,
loose: true,
debug: false,
useBuiltIns: 'entry',
targets: browsers
}
]
]
} }
} }
} ]
}, },
// CSS Modules // CSS Modules
{ {
test: /\.css$/, test: /\.css$/,
exclude: /(node_modules|globals.css)/, exclude: /(node_modules|globals.css)/,
use: extractCSSPlugin.extract({ use: [
fallback: 'style-loader', { loader: MiniCssExtractPlugin.loader },
use: [ {
{ loader: 'css-loader',
loader: 'css-variables-loader', options: {
options: { importLoaders: 1,
cssVarsFiles localIdentName: '[name]/[local]/[hash:base64:5]',
} modules: true
}, }
{ },
loader: 'css-loader', {
options: { loader: 'postcss-loader',
modules: true, options: {
importLoaders: 1, ident: 'postcss',
localIdentName: '[name]-[local]-[hash:base64:5]', config: {
sourceMap: true ctx: {
} cssVarsFiles
}, },
{ path: 'frontend/postcss.config.js'
loader: 'postcss-loader',
options: {
config: {
ctx: {
cssVarsFiles
},
path: 'frontend/postcss.config.js'
}
} }
} }
] }
}) ]
}, },
// Global styles // Global styles
@ -195,17 +182,16 @@ const config = {
}; };
gulp.task('webpack', () => { gulp.task('webpack', () => {
return gulp.src('index.js') return webpackStream(config)
.pipe(webpackStream(config)) .pipe(gulp.dest('./'));
.pipe(gulp.dest(''));
}); });
gulp.task('webpackWatch', () => { gulp.task('webpackWatch', () => {
config.watch = true; config.watch = true;
return gulp.src('')
.pipe(webpackStream(config)) return webpackStream(config)
.on('error', errorHandler) .on('error', errorHandler)
.pipe(gulp.dest('')) .pipe(gulp.dest('./'))
.on('error', errorHandler) .on('error', errorHandler)
.pipe(livereload()) .pipe(livereload())
.on('error', errorHandler); .on('error', errorHandler);

@ -1,4 +1,5 @@
const reload = require('require-nocache')(module); const reload = require('require-nocache')(module);
const browsers = require('./browsers');
module.exports = (ctx, configPath, options) => { module.exports = (ctx, configPath, options) => {
const config = { const config = {
@ -17,15 +18,7 @@ module.exports = (ctx, configPath, options) => {
'postcss-color-function': {}, 'postcss-color-function': {},
'postcss-nested': {}, 'postcss-nested': {},
autoprefixer: { autoprefixer: {
browsers: [ browsers
'Chrome >= 30',
'Firefox >= 30',
'Safari >= 6',
'Edge >= 12',
'Explorer >= 11',
'iOS >= 7',
'Android >= 4.4'
]
} }
} }
}; };

@ -1,18 +1,18 @@
.language, .language,
.quality { .quality {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 100px; width: 100px;
} }
.indexer { .indexer {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px; width: 80px;
} }
.actions { .actions {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px; width: 70px;
} }

@ -1,5 +1,5 @@
.description { .description {
composes: title from 'Components/DescriptionList/DescriptionListItemDescription.css'; composes: title from '~Components/DescriptionList/DescriptionListItemDescription.css';
overflow-wrap: break-word; overflow-wrap: break-word;
} }

@ -231,6 +231,16 @@ function HistoryDetails(props) {
</DescriptionList> </DescriptionList>
); );
} }
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title="Name"
data={sourceTitle}
/>
</DescriptionList>
);
} }
HistoryDetails.propTypes = { HistoryDetails.propTypes = {

@ -1,5 +1,5 @@
.markAsFailedButton { .markAsFailedButton {
composes: button from 'Components/Link/Button.css'; composes: button from '~Components/Link/Button.css';
margin-right: auto; margin-right: auto;
} }

@ -1,5 +1,5 @@
.cell { .cell {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 35px; width: 35px;
text-align: center; text-align: center;

@ -1,23 +1,23 @@
.downloadClient { .downloadClient {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 120px; width: 120px;
} }
.indexer { .indexer {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px; width: 80px;
} }
.releaseGroup { .releaseGroup {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 110px; width: 110px;
} }
.details { .details {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 30px; width: 30px;
} }

@ -1,12 +1,12 @@
.torrent { .torrent {
composes: label from 'Components/Label.css'; composes: label from '~Components/Label.css';
border-color: $torrentColor; border-color: $torrentColor;
background-color: $torrentColor; background-color: $torrentColor;
} }
.usenet { .usenet {
composes: label from 'Components/Label.css'; composes: label from '~Components/Label.css';
border-color: $usenetColor; border-color: $usenetColor;
background-color: $usenetColor; background-color: $usenetColor;

@ -144,6 +144,8 @@ class QueueConnector extends Component {
} }
QueueConnector.propTypes = { QueueConnector.propTypes = {
includeUnknownMovieItems: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchQueue: PropTypes.func.isRequired, fetchQueue: PropTypes.func.isRequired,
gotoQueueFirstPage: PropTypes.func.isRequired, gotoQueueFirstPage: PropTypes.func.isRequired,

@ -1,23 +1,23 @@
.quality { .quality {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 150px; width: 150px;
} }
.protocol { .protocol {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 100px; width: 100px;
} }
.progress { .progress {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 150px; width: 150px;
} }
.actions { .actions {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px; width: 70px;
} }

@ -197,6 +197,14 @@ class QueueRow extends Component {
); );
} }
if (name === 'title') {
return (
<TableRowCell key={name}>
{title}
</TableRowCell>
);
}
if (name === 'estimatedCompletionTime') { if (name === 'estimatedCompletionTime') {
return ( return (
<TimeleftCell <TimeleftCell

@ -1,5 +1,5 @@
.status { .status {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 30px; width: 30px;
} }

@ -1,5 +1,5 @@
.timeleft { .timeleft {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 100px; width: 100px;
} }

@ -17,7 +17,7 @@
} }
.searchInput { .searchInput {
composes: input from 'Components/Form/TextInput.css'; composes: input from '~Components/Form/TextInput.css';
height: 46px; height: 46px;
border-radius: 0; border-radius: 0;

@ -99,7 +99,7 @@ class AddNewMovie extends Component {
className={styles.searchInput} className={styles.searchInput}
name="movieLookup" name="movieLookup"
value={term} value={term}
placeholder="eg. Some Movie, tmdb:####, imdb:####" placeholder="eg. The Dark Knight, tmdb:155, imdb:tt0468569"
onChange={this.onSearchInputChange} onChange={this.onSearchInputChange}
/> />

@ -10,7 +10,7 @@ import AddNewMovie from './AddNewMovie';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.addMovie, (state) => state.addMovie,
(state) => state.routing.location, (state) => state.router.location,
(addMovie, location) => { (addMovie, location) => {
const { params } = parseUrl(location.search); const { params } = parseUrl(location.search);

@ -36,24 +36,24 @@
} }
.searchForMissingEpisodesContainer { .searchForMissingEpisodesContainer {
composes: container from 'Components/Form/CheckInput.css'; composes: container from '~Components/Form/CheckInput.css';
flex: 0 1 0; flex: 0 1 0;
} }
.searchForMissingEpisodesInput { .searchForMissingEpisodesInput {
composes: input from 'Components/Form/CheckInput.css'; composes: input from '~Components/Form/CheckInput.css';
margin-top: 0; margin-top: 0;
} }
.modalFooter { .modalFooter {
composes: modalFooter from 'Components/Modal/ModalFooter.css'; composes: modalFooter from '~Components/Modal/ModalFooter.css';
} }
.addButton { .addButton {
@add-mixin truncate; @add-mixin truncate;
composes: button from 'Components/Link/SpinnerButton.css'; composes: button from '~Components/Link/SpinnerButton.css';
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointSmall) {

@ -97,7 +97,7 @@ class ImportMovie extends Component {
} = this.state; } = this.state;
return ( return (
<PageContent title="Import Series"> <PageContent title="Import Movies">
<PageContentBodyConnector <PageContentBodyConnector
ref={this.setContentBodyRef} ref={this.setContentBodyRef}
onScroll={this.onScroll} onScroll={this.onScroll}
@ -115,7 +115,7 @@ class ImportMovie extends Component {
{ {
!rootFoldersError && rootFoldersPopulated && !unmappedFolders.length && !rootFoldersError && rootFoldersPopulated && !unmappedFolders.length &&
<div> <div>
All series in {path} have been imported All movies in {path} have been imported
</div> </div>
} }

@ -14,7 +14,7 @@
} }
.importButton { .importButton {
composes: button from 'Components/Link/SpinnerButton.css'; composes: button from '~Components/Link/SpinnerButton.css';
height: 35px; height: 35px;
} }
@ -26,7 +26,7 @@
} }
.loading { .loading {
composes: loading from 'Components/Loading/LoadingIndicator.css'; composes: loading from '~Components/Loading/LoadingIndicator.css';
margin: 0 10px 0 12px; margin: 0 10px 0 12px;
text-align: left; text-align: left;

@ -1,25 +1,25 @@
.folder { .folder {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 1 0 200px; flex: 1 0 200px;
} }
.monitor { .monitor {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 200px; flex: 0 1 200px;
min-width: 185px; min-width: 185px;
} }
.qualityProfile { .qualityProfile {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 250px; flex: 0 1 250px;
min-width: 170px; min-width: 170px;
} }
.movie { .movie {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 400px; flex: 0 1 400px;
min-width: 300px; min-width: 300px;

@ -1,30 +1,30 @@
.selectInput { .selectInput {
composes: input from 'Components/Form/CheckInput.css'; composes: input from '~Components/Form/CheckInput.css';
} }
.folder { .folder {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 1 0 200px; flex: 1 0 200px;
line-height: 36px; line-height: 36px;
} }
.monitor { .monitor {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 200px; flex: 0 1 200px;
min-width: 185px; min-width: 185px;
} }
.qualityProfile { .qualityProfile {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 250px; flex: 0 1 250px;
min-width: 170px; min-width: 170px;
} }
.series { .series {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 400px; flex: 0 1 400px;
min-width: 300px; min-width: 300px;

@ -1,3 +1,3 @@
.input { .input {
composes: input from 'Components/Form/CheckInput.css'; composes: input from '~Components/Form/CheckInput.css';
} }

@ -3,7 +3,7 @@
} }
.button { .button {
composes: link from 'Components/Link/Link.css'; composes: link from '~Components/Link/Link.css';
position: relative; position: relative;
display: flex; display: flex;
@ -64,7 +64,7 @@
} }
.searchInput { .searchInput {
composes: input from 'Components/Form/TextInput.css'; composes: input from '~Components/Form/TextInput.css';
border-radius: 0; border-radius: 0;
} }

@ -34,6 +34,8 @@ class ImportMovieSelectMovie extends Component {
super(props, context); super(props, context);
this._movieLookupTimeout = null; this._movieLookupTimeout = null;
this._buttonRef = {};
this._contentRef = {};
this.state = { this.state = {
term: props.id, term: props.id,
@ -44,14 +46,6 @@ class ImportMovieSelectMovie extends Component {
// //
// Control // Control
_setButtonRef = (ref) => {
this._buttonRef = ref;
}
_setContentRef = (ref) => {
this._contentRef = ref;
}
_addListener() { _addListener() {
window.addEventListener('click', this.onWindowClick); window.addEventListener('click', this.onWindowClick);
} }
@ -64,14 +58,18 @@ class ImportMovieSelectMovie extends Component {
// Listeners // Listeners
onWindowClick = (event) => { onWindowClick = (event) => {
const button = ReactDOM.findDOMNode(this._buttonRef); const button = ReactDOM.findDOMNode(this._buttonRef.current);
const content = ReactDOM.findDOMNode(this._contentRef); const content = ReactDOM.findDOMNode(this._contentRef.current);
if (!button) { if (!button || !content) {
return; return;
} }
if (!button.contains(event.target) && content && !content.contains(event.target) && this.state.isOpen) { if (
!button.contains(event.target) &&
!content.contains(event.target) &&
this.state.isOpen
) {
this.setState({ isOpen: false }); this.setState({ isOpen: false });
this._removeListener(); this._removeListener();
} }
@ -120,7 +118,7 @@ class ImportMovieSelectMovie extends Component {
isPopulated, isPopulated,
error, error,
items, items,
queued, isQueued,
isLookingUpMovie isLookingUpMovie
} = this.props; } = this.props;
@ -134,124 +132,145 @@ class ImportMovieSelectMovie extends Component {
element: styles.tether element: styles.tether
}} }}
{...tetherOptions} {...tetherOptions}
> renderTarget={
<Link (ref) => {
ref={this._setButtonRef} this._buttonRef = ref;
className={styles.button}
component="div" return (
onPress={this.onPress} <div ref={ref}>
> <Link
{ className={styles.button}
isLookingUpMovie && queued && !isPopulated && component="div"
<LoadingIndicator onPress={this.onPress}
className={styles.loading} >
size={20} {
/> isLookingUpMovie && isQueued && !isPopulated ?
} <LoadingIndicator
className={styles.loading}
size={20}
/> :
null
}
{ {
isPopulated && selectedMovie && isExistingMovie && isPopulated && selectedMovie && isExistingMovie ?
<Icon <Icon
className={styles.warningIcon} className={styles.warningIcon}
name={icons.WARNING} name={icons.WARNING}
kind={kinds.WARNING} kind={kinds.WARNING}
/> /> :
} null
}
{ {
isPopulated && selectedMovie && isPopulated && selectedMovie ?
<ImportMovieTitle <ImportMovieTitle
title={selectedMovie.title} title={selectedMovie.title}
year={selectedMovie.year} year={selectedMovie.year}
network={selectedMovie.network} network={selectedMovie.network}
isExistingMovie={isExistingMovie} isExistingMovie={isExistingMovie}
/> /> :
} null
}
{ {
isPopulated && !selectedMovie && isPopulated && !selectedMovie ?
<div> <div>
<Icon <Icon
className={styles.warningIcon} className={styles.warningIcon}
name={icons.WARNING} name={icons.WARNING}
kind={kinds.WARNING} kind={kinds.WARNING}
/> />
No match found! No match found!
</div> </div> :
} null
}
{ {
!isFetching && !!error && !isFetching && !!error ?
<div> <div>
<Icon <Icon
className={styles.warningIcon} className={styles.warningIcon}
title={errorMessage} title={errorMessage}
name={icons.WARNING} name={icons.WARNING}
kind={kinds.WARNING} kind={kinds.WARNING}
/> />
Search failed, please try again later. Search failed, please try again later.
</div> :
null
}
<div className={styles.dropdownArrowContainer}>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</div> </div>
);
} }
}
<div className={styles.dropdownArrowContainer}> renderElement={
<Icon (ref) => {
name={icons.CARET_DOWN} this._contentRef = ref;
/>
</div> if (!this.state.isOpen) {
</Link> return;
}
{
this.state.isOpen && return (
<div <div
ref={this._setContentRef} ref={ref}
className={styles.contentContainer} className={styles.contentContainer}
> >
<div className={styles.content}> <div className={styles.content}>
<div className={styles.searchContainer}> <div className={styles.searchContainer}>
<div className={styles.searchIconContainer}> <div className={styles.searchIconContainer}>
<Icon name={icons.SEARCH} /> <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>
<TextInput <div className={styles.results}>
className={styles.searchInput} {
name={`${name}_textInput`} items.map((item) => {
value={this.state.term} return (
onChange={this.onSearchInputChange} <ImportMovieSearchResultConnector
/> key={item.tmdbId}
tmdbId={item.tmdbId}
<FormInputButton title={item.title}
kind={kinds.DEFAULT} year={item.year}
spinnerIcon={icons.REFRESH} studio={item.studio}
canSpin={true} onPress={this.onMovieSelect}
isSpinning={isFetching} />
onPress={this.onRefreshPress} );
> })
<Icon name={icons.REFRESH} /> }
</FormInputButton> </div>
</div>
<div className={styles.results}>
{
items.map((item) => {
return (
<ImportMovieSearchResultConnector
key={item.tmdbId}
tmdbId={item.tmdbId}
title={item.title}
year={item.year}
studio={item.studio}
onPress={this.onMovieSelect}
/>
);
})
}
</div> </div>
</div> </div>
</div> );
}
} }
</TetherComponent> />
); );
} }
} }
@ -264,7 +283,7 @@ ImportMovieSelectMovie.propTypes = {
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
queued: PropTypes.bool.isRequired, isQueued: PropTypes.bool.isRequired,
isLookingUpMovie: PropTypes.bool.isRequired, isLookingUpMovie: PropTypes.bool.isRequired,
onSearchInputChange: PropTypes.func.isRequired, onSearchInputChange: PropTypes.func.isRequired,
onMovieSelect: PropTypes.func.isRequired onMovieSelect: PropTypes.func.isRequired
@ -274,7 +293,7 @@ ImportMovieSelectMovie.defaultProps = {
isFetching: true, isFetching: true,
isPopulated: false, isPopulated: false,
items: [], items: [],
queued: true isQueued: true
}; };
export default ImportMovieSelectMovie; export default ImportMovieSelectMovie;

@ -1,18 +1,18 @@
.link { .link {
composes: link from 'Components/Link/Link.css'; composes: link from '~Components/Link/Link.css';
display: block; display: block;
} }
.freeSpace, .freeSpace,
.unmappedFolders { .unmappedFolders {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 150px; width: 150px;
} }
.actions { .actions {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 45px; width: 45px;
} }

@ -76,7 +76,7 @@ class ImportMovieSelectFolder extends Component {
} = this.props; } = this.props;
return ( return (
<PageContent title="Import Series"> <PageContent title="Import Movies">
<PageContentBodyConnector> <PageContentBodyConnector>
{ {
isFetching && !isPopulated && isFetching && !isPopulated &&
@ -99,7 +99,7 @@ class ImportMovieSelectFolder extends Component {
Some tips to ensure the import goes smoothly: Some tips to ensure the import goes smoothly:
<ul> <ul>
<li className={styles.tip}> <li className={styles.tip}>
Make sure your files include the quality in the name. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span> Make sure your files include the quality in the name. eg. <span className={styles.code}>movie.2008.bluray.mkv</span>
</li> </li>
<li className={styles.tip}> <li className={styles.tip}>
Point Radarr to the folder containing all of your movies not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\movies' : '/movies'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}"</span> Point Radarr to the folder containing all of your movies not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\movies' : '/movies'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}"</span>

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { push } from 'react-router-redux'; import { push } from 'connected-react-router';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import { fetchRootFolders, addRootFolder, deleteRootFolder } from 'Store/Actions/rootFolderActions'; import { fetchRootFolders, addRootFolder, deleteRootFolder } from 'Store/Actions/rootFolderActions';
import ImportMovieSelectFolder from './ImportMovieSelectFolder'; import ImportMovieSelectFolder from './ImportMovieSelectFolder';

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import DocumentTitle from 'react-document-title'; import DocumentTitle from 'react-document-title';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux'; import { ConnectedRouter } from 'connected-react-router';
import PageConnector from 'Components/Page/PageConnector'; import PageConnector from 'Components/Page/PageConnector';
import AppRoutes from './AppRoutes'; import AppRoutes from './AppRoutes';

@ -17,6 +17,7 @@ import Settings from 'Settings/Settings';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector'; import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import Profiles from 'Settings/Profiles/Profiles'; import Profiles from 'Settings/Profiles/Profiles';
import Quality from 'Settings/Quality/Quality'; import Quality from 'Settings/Quality/Quality';
import CustomFormatsConnector from 'Settings/CustomFormats/CustomFormatsConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector'; import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import NetImportSettingsConnector from 'Settings/NetImport/NetImportSettingsConnector'; import NetImportSettingsConnector from 'Settings/NetImport/NetImportSettingsConnector';
@ -139,6 +140,11 @@ function AppRoutes(props) {
component={Quality} component={Quality}
/> />
<Route
path="/settings/customformats"
component={CustomFormatsConnector}
/>
<Route <Route
path="/settings/indexers" path="/settings/indexers"
component={IndexerSettingsConnector} component={IndexerSettingsConnector}

@ -3,7 +3,7 @@
overflow-x: hidden; overflow-x: hidden;
padding: 5px; padding: 5px;
border-bottom: 1px solid $borderColor; border-bottom: 1px solid $borderColor;
font-size: 14px; font-size: $defaultFontSize;
&:hover { &:hover {
background-color: $tableRowHoverBackgroundColor; background-color: $tableRowHoverBackgroundColor;
@ -63,27 +63,27 @@
*/ */
.downloaded { .downloaded {
composes: downloaded from 'Calendar/Events/CalendarEvent.css'; composes: downloaded from '~Calendar/Events/CalendarEvent.css';
} }
.downloading { .downloading {
composes: downloading from 'Calendar/Events/CalendarEvent.css'; composes: downloading from '~Calendar/Events/CalendarEvent.css';
} }
.unmonitored { .unmonitored {
composes: unmonitored from 'Calendar/Events/CalendarEvent.css'; composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
} }
.onAir { .onAir {
composes: onAir from 'Calendar/Events/CalendarEvent.css'; composes: onAir from '~Calendar/Events/CalendarEvent.css';
} }
.missing { .missing {
composes: missing from 'Calendar/Events/CalendarEvent.css'; composes: missing from '~Calendar/Events/CalendarEvent.css';
} }
.premiere { .premiere {
composes: premiere from 'Calendar/Events/CalendarEvent.css'; composes: premiere from '~Calendar/Events/CalendarEvent.css';
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointSmall) {

@ -123,7 +123,7 @@ class AgendaEvent extends Component {
AgendaEvent.propTypes = { AgendaEvent.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
episodeFile: PropTypes.object, movieFile: PropTypes.object,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
inCinemas: PropTypes.string.isRequired, inCinemas: PropTypes.string.isRequired,

@ -49,8 +49,20 @@ class CalendarConnector extends Component {
} }
componentDidMount() { componentDidMount() {
const {
useCurrentPage,
fetchCalendar,
gotoCalendarToday
} = this.props;
registerPagePopulator(this.repopulate); registerPagePopulator(this.repopulate);
this.props.gotoCalendarToday();
if (useCurrentPage) {
fetchCalendar();
} else {
gotoCalendarToday();
}
this.scheduleUpdate(); this.scheduleUpdate();
} }
@ -163,6 +175,7 @@ class CalendarConnector extends Component {
} }
CalendarConnector.propTypes = { CalendarConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
time: PropTypes.string, time: PropTypes.string,
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
firstDayOfWeek: PropTypes.number.isRequired, firstDayOfWeek: PropTypes.number.isRequired,

@ -1,11 +1,11 @@
.calendarPageBody { .calendarPageBody {
composes: contentBody from 'Components/Page/PageContentBody.css'; composes: contentBody from '~Components/Page/PageContentBody.css';
display: flex; display: flex;
} }
.calendarInnerPageBody { .calendarInnerPageBody {
composes: innerContentBody from 'Components/Page/PageContentBody.css'; composes: innerContentBody from '~Components/Page/PageContentBody.css';
display: flex; display: flex;
flex-direction: column; flex-direction: column;

@ -58,6 +58,15 @@ class CalendarPage extends Component {
this.setState({ isOptionsModalOpen: false }); this.setState({ isOptionsModalOpen: false });
} }
onSearchMissingPress = () => {
const {
missingEpisodeIds,
onSearchMissingPress
} = this.props;
onSearchMissingPress(missingEpisodeIds);
}
// //
// Render // Render
@ -65,7 +74,10 @@ class CalendarPage extends Component {
const { const {
selectedFilterKey, selectedFilterKey,
filters, filters,
hasSeries, hasMovie,
missingEpisodeIds,
isSearchingForMissing,
useCurrentPage,
onFilterSelect onFilterSelect
} = this.props; } = this.props;
@ -75,11 +87,7 @@ class CalendarPage extends Component {
} = this.state; } = this.state;
const isMeasured = this.state.width > 0; const isMeasured = this.state.width > 0;
let PageComponent = 'div'; const PageComponent = hasMovie ? CalendarConnector : NoMovie;
if (isMeasured) {
PageComponent = hasSeries ? CalendarConnector : NoMovie;
}
return ( return (
<PageContent title="Calendar"> <PageContent title="Calendar">
@ -90,6 +98,14 @@ class CalendarPage extends Component {
iconName={icons.CALENDAR} iconName={icons.CALENDAR}
onPress={this.onGetCalendarLinkPress} onPress={this.onGetCalendarLinkPress}
/> />
<PageToolbarButton
label="Search for Missing"
iconName={icons.SEARCH}
isDisabled={!missingEpisodeIds.length}
isSpinning={isSearchingForMissing}
onPress={this.onSearchMissingPress}
/>
</PageToolbarSection> </PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}> <PageToolbarSection alignContent={align.RIGHT}>
@ -101,7 +117,7 @@ class CalendarPage extends Component {
<FilterMenu <FilterMenu
alignMenu={align.RIGHT} alignMenu={align.RIGHT}
isDisabled={!hasSeries} isDisabled={!hasMovie}
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
customFilters={[]} customFilters={[]}
@ -118,11 +134,17 @@ class CalendarPage extends Component {
whitelist={['width']} whitelist={['width']}
onMeasure={this.onMeasure} onMeasure={this.onMeasure}
> >
<PageComponent /> {
isMeasured ?
<PageComponent
useCurrentPage={useCurrentPage}
/> :
<div />
}
</Measure> </Measure>
{ {
hasSeries && hasMovie &&
<LegendConnector /> <LegendConnector />
} }
</PageContentBodyConnector> </PageContentBodyConnector>
@ -144,7 +166,11 @@ class CalendarPage extends Component {
CalendarPage.propTypes = { CalendarPage.propTypes = {
selectedFilterKey: PropTypes.string.isRequired, selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasSeries: PropTypes.bool.isRequired, hasMovie: PropTypes.bool.isRequired,
missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired,
isSearchingForMissing: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
onSearchMissingPress: PropTypes.func.isRequired,
onDaysCountChange: PropTypes.func.isRequired, onDaysCountChange: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired onFilterSelect: PropTypes.func.isRequired
}; };

@ -1,21 +1,80 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions'; import moment from 'moment';
import { isCommandExecuting } from 'Utilities/Command';
import isBefore from 'Utilities/Date/isBefore';
import withCurrentPage from 'Components/withCurrentPage';
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector'; import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import CalendarPage from './CalendarPage'; import CalendarPage from './CalendarPage';
function createMissingEpisodeIdsSelector() {
return createSelector(
(state) => state.calendar.start,
(state) => state.calendar.end,
(state) => state.calendar.items,
(state) => state.queue.details.items,
(start, end, episodes, queueDetails) => {
return episodes.reduce((acc, episode) => {
const airDateUtc = episode.airDateUtc;
if (
!episode.episodeFileId &&
moment(airDateUtc).isAfter(start) &&
moment(airDateUtc).isBefore(end) &&
isBefore(episode.airDateUtc) &&
!queueDetails.some((details) => !!details.episode && details.episode.id === episode.id)
) {
acc.push(episode.id);
}
return acc;
}, []);
}
);
}
function createIsSearchingSelector() {
return createSelector(
(state) => state.calendar.searchMissingCommandId,
createCommandsSelector(),
(searchMissingCommandId, commands) => {
if (searchMissingCommandId == null) {
return false;
}
return isCommandExecuting(commands.find((command) => {
return command.id === searchMissingCommandId;
}));
}
);
}
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.calendar, (state) => state.calendar.selectedFilterKey,
(state) => state.calendar.filters,
createMovieCountSelector(), createMovieCountSelector(),
createUISettingsSelector(), createUISettingsSelector(),
(calendar, seriesCount, uiSettings) => { createMissingEpisodeIdsSelector(),
createIsSearchingSelector(),
(
selectedFilterKey,
filters,
movieCount,
uiSettings,
missingEpisodeIds,
isSearchingForMissing
) => {
return { return {
selectedFilterKey: calendar.selectedFilterKey, selectedFilterKey,
filters: calendar.filters, filters,
colorImpairedMode: uiSettings.enableColorImpairedMode, colorImpairedMode: uiSettings.enableColorImpairedMode,
hasSeries: !!seriesCount hasMovie: !!movieCount,
missingEpisodeIds,
isSearchingForMissing
}; };
} }
); );
@ -23,6 +82,10 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onSearchMissingPress(episodeIds) {
dispatch(searchMissing({ episodeIds }));
},
onDaysCountChange(dayCount) { onDaysCountChange(dayCount) {
dispatch(setCalendarDaysCount({ dayCount })); dispatch(setCalendarDaysCount({ dayCount }));
}, },
@ -33,4 +96,6 @@ function createMapDispatchToProps(dispatch, props) {
}; };
} }
export default connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage); export default withCurrentPage(
connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage)
);

@ -5,6 +5,10 @@
border-bottom: 1px solid $borderColor; border-bottom: 1px solid $borderColor;
border-left: 4px solid $borderColor; border-left: 4px solid $borderColor;
font-size: 12px; font-size: 12px;
&:global(.colorImpaired) {
border-left-width: 5px;
}
} }
.info, .info,
@ -22,7 +26,7 @@
.seriesTitle { .seriesTitle {
color: #3a3f51; color: #3a3f51;
font-size: 14px; font-size: $defaultFontSize;
} }
.absoluteEpisodeNumber { .absoluteEpisodeNumber {
@ -39,6 +43,10 @@
.downloaded { .downloaded {
border-left-color: $successColor !important; border-left-color: $successColor !important;
&:global(.colorImpaired) {
border-left-color: color($successColor, saturation(+15%)) !important;
}
} }
.downloading { .downloading {
@ -49,7 +57,7 @@
border-left-color: $gray !important; border-left-color: $gray !important;
&:global(.colorImpaired) { &:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px); background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
} }
} }
@ -57,7 +65,7 @@
border-left-color: $warningColor !important; border-left-color: $warningColor !important;
&:global(.colorImpaired) { &:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px); background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
} }
} }
@ -65,7 +73,8 @@
border-left-color: $dangerColor !important; border-left-color: $dangerColor !important;
&:global(.colorImpaired) { &:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px); border-left-color: color($dangerColor saturation(+15%)) !important;
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
} }
} }
@ -73,6 +82,6 @@
border-left-color: $primaryColor !important; border-left-color: $primaryColor !important;
&:global(.colorImpaired) { &:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px); background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
} }
} }

@ -18,7 +18,7 @@
flex: 1 0 1px; flex: 1 0 1px;
margin-right: 10px; margin-right: 10px;
color: #3a3f51; color: #3a3f51;
font-size: 14px; font-size: $defaultFontSize;
} }
.airTime { .airTime {
@ -58,25 +58,25 @@
*/ */
.downloaded { .downloaded {
composes: downloaded from 'Calendar/Events/CalendarEvent.css'; composes: downloaded from '~Calendar/Events/CalendarEvent.css';
} }
.downloading { .downloading {
composes: downloading from 'Calendar/Events/CalendarEvent.css'; composes: downloading from '~Calendar/Events/CalendarEvent.css';
} }
.unmonitored { .unmonitored {
composes: unmonitored from 'Calendar/Events/CalendarEvent.css'; composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
} }
.onAir { .onAir {
composes: onAir from 'Calendar/Events/CalendarEvent.css'; composes: onAir from '~Calendar/Events/CalendarEvent.css';
} }
.missing { .missing {
composes: missing from 'Calendar/Events/CalendarEvent.css'; composes: missing from '~Calendar/Events/CalendarEvent.css';
} }
.premiere { .premiere {
composes: premiere from 'Calendar/Events/CalendarEvent.css'; composes: premiere from '~Calendar/Events/CalendarEvent.css';
} }

@ -8,7 +8,7 @@
} }
.todayButton { .todayButton {
composes: button from 'Components/Link/Button.css'; composes: button from '~Components/Link/Button.css';
margin-left: 5px; margin-left: 5px;
} }
@ -30,13 +30,13 @@
} }
.viewMenu { .viewMenu {
composes: menu from 'Components/Menu/Menu.css'; composes: menu from '~Components/Menu/Menu.css';
line-height: 31px; line-height: 31px;
} }
.loading { .loading {
composes: loading from 'Components/Loading/LoadingIndicator.css'; composes: loading from '~Components/Loading/LoadingIndicator.css';
margin-top: 5px; margin-top: 5px;
margin-right: 10px; margin-right: 10px;

@ -158,6 +158,14 @@ class CalendarHeader extends Component {
</MenuButton> </MenuButton>
<MenuContent> <MenuContent>
<ViewMenuItem
name={calendarViews.WEEK}
selectedView={view}
onPress={this.onViewChange}
>
Week
</ViewMenuItem>
<ViewMenuItem <ViewMenuItem
name={calendarViews.FORECAST} name={calendarViews.FORECAST}
selectedView={view} selectedView={view}
@ -192,6 +200,27 @@ class CalendarHeader extends Component {
onPress={this.onViewChange} onPress={this.onViewChange}
/> />
<CalendarHeaderViewButton
view={calendarViews.WEEK}
selectedView={view}
buttonGroupPosition={align.CENTER}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton
view={calendarViews.FORECAST}
selectedView={view}
buttonGroupPosition={align.CENTER}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton
view={calendarViews.DAY}
selectedView={view}
buttonGroupPosition={align.CENTER}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton <CalendarHeaderViewButton
view={calendarViews.AGENDA} view={calendarViews.AGENDA}
selectedView={view} selectedView={view}

@ -13,29 +13,29 @@
*/ */
.downloaded { .downloaded {
composes: downloaded from 'Calendar/Events/CalendarEvent.css'; composes: downloaded from '~Calendar/Events/CalendarEvent.css';
} }
.downloading { .downloading {
composes: downloading from 'Calendar/Events/CalendarEvent.css'; composes: downloading from '~Calendar/Events/CalendarEvent.css';
} }
.unmonitored { .unmonitored {
composes: unmonitored from 'Calendar/Events/CalendarEvent.css'; composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
} }
.onAir { .onAir {
composes: onAir from 'Calendar/Events/CalendarEvent.css'; composes: onAir from '~Calendar/Events/CalendarEvent.css';
} }
.missing { .missing {
composes: missing from 'Calendar/Events/CalendarEvent.css'; composes: missing from '~Calendar/Events/CalendarEvent.css';
} }
.premiere { .premiere {
composes: premiere from 'Calendar/Events/CalendarEvent.css'; composes: premiere from '~Calendar/Events/CalendarEvent.css';
} }
.unaired { .unaired {
composes: unaired from 'Calendar/Events/CalendarEvent.css'; composes: unaired from '~Calendar/Events/CalendarEvent.css';
} }

@ -1,4 +1,7 @@
export const DAY = 'day';
export const WEEK = 'week';
export const MONTH = 'month'; export const MONTH = 'month';
export const FORECAST = 'forecast';
export const AGENDA = 'agenda'; export const AGENDA = 'agenda';
export const all = [MONTH, AGENDA]; export const all = [DAY, WEEK, MONTH, FORECAST, AGENDA];

@ -1,5 +1,5 @@
.modal { .modal {
composes: modal from 'Components/Modal/Modal.css'; composes: modal from '~Components/Modal/Modal.css';
height: 600px; height: 600px;
} }

@ -1,12 +1,12 @@
.modalBody { .modalBody {
composes: modalBody from 'Components/Modal/ModalBody.css'; composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.mappedDrivesWarning { .mappedDrivesWarning {
composes: alert from 'Components/Alert.css'; composes: alert from '~Components/Alert.css';
margin: 0; margin: 0;
margin-bottom: 20px; margin-bottom: 20px;
@ -18,7 +18,7 @@
} }
.pathInput { .pathInput {
composes: pathInputWrapper from 'Components/Form/PathInput.css'; composes: inputWrapper from '~Components/Form/PathInput.css';
flex: 0 0 auto; flex: 0 0 auto;
} }

@ -42,8 +42,8 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchPaths, dispatchFetchPaths: fetchPaths,
clearPaths dispatchClearPaths: clearPaths
}; };
class FileBrowserModalContentConnector extends Component { class FileBrowserModalContentConnector extends Component {
@ -51,9 +51,16 @@ class FileBrowserModalContentConnector extends Component {
// Lifecycle // Lifecycle
componentDidMount() { componentDidMount() {
this.props.fetchPaths({ const {
path: this.props.value, value,
allowFoldersWithoutTrailingSlashes: true includeFiles,
dispatchFetchPaths
} = this.props;
dispatchFetchPaths({
path: value,
allowFoldersWithoutTrailingSlashes: true,
includeFiles
}); });
} }
@ -61,18 +68,24 @@ class FileBrowserModalContentConnector extends Component {
// Listeners // Listeners
onFetchPaths = (path) => { onFetchPaths = (path) => {
this.props.fetchPaths({ const {
includeFiles,
dispatchFetchPaths
} = this.props;
dispatchFetchPaths({
path, path,
allowFoldersWithoutTrailingSlashes: true allowFoldersWithoutTrailingSlashes: true,
includeFiles
}); });
} }
onClearPaths = () => { onClearPaths = () => {
// this.props.clearPaths(); // this.props.dispatchClearPaths();
} }
onModalClose = () => { onModalClose = () => {
this.props.clearPaths(); this.props.dispatchClearPaths();
this.props.onModalClose(); this.props.onModalClose();
} }
@ -93,9 +106,14 @@ class FileBrowserModalContentConnector extends Component {
FileBrowserModalContentConnector.propTypes = { FileBrowserModalContentConnector.propTypes = {
value: PropTypes.string, value: PropTypes.string,
fetchPaths: PropTypes.func.isRequired, includeFiles: PropTypes.bool.isRequired,
clearPaths: PropTypes.func.isRequired, dispatchFetchPaths: PropTypes.func.isRequired,
dispatchClearPaths: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
FileBrowserModalContentConnector.defaultProps = {
includeFiles: false
};
export default connect(createMapStateToProps, mapDispatchToProps)(FileBrowserModalContentConnector); export default connect(createMapStateToProps, mapDispatchToProps)(FileBrowserModalContentConnector);

@ -1,5 +1,5 @@
.type { .type {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 32px; width: 32px;
} }

@ -3,13 +3,13 @@
} }
.numberInput { .numberInput {
composes: input from 'Components/Form/TextInput.css'; composes: input from '~Components/Form/TextInput.css';
margin-right: 3px; margin-right: 3px;
} }
.selectInput { .selectInput {
composes: select from 'Components/Form/SelectInput.css'; composes: select from '~Components/Form/SelectInput.css';
margin-left: 3px; margin-left: 3px;
} }

@ -3,7 +3,8 @@ import React, { Component } from 'react';
import convertToBytes from 'Utilities/Number/convertToBytes'; import convertToBytes from 'Utilities/Number/convertToBytes';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import { kinds, filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props'; import { kinds, filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
import TagInput, { tagShape } from 'Components/Form/TagInput'; import tagShape from 'Helpers/Props/Shapes/tagShape';
import TagInput from 'Components/Form/TagInput';
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag'; import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
export const NAME = 'value'; export const NAME = 'value';

@ -7,7 +7,7 @@
} }
.label { .label {
composes: label from 'Components/Label.css'; composes: label from '~Components/Label.css';
border-style: none; border-style: none;
font-size: 13px; font-size: 13px;

@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import { fetchIndexers } from 'Store/Actions/settingsActions'; import { fetchIndexers } from 'Store/Actions/settingsActions';
import { tagShape } from 'Components/Form/TagInput';
import FilterBuilderRowValue from './FilterBuilderRowValue'; import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() { function createMapStateToProps() {

@ -3,8 +3,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import getQualities from 'Utilities/Quality/getQualities'; import getQualities from 'Utilities/Quality/getQualities';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import { tagShape } from 'Components/Form/TagInput';
import FilterBuilderRowValue from './FilterBuilderRowValue'; import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() { function createMapStateToProps() {

@ -1,13 +1,13 @@
.input { .input {
composes: input from 'Components/Form/Input.css'; composes: input from '~Components/Form/Input.css';
} }
.hasError { .hasError {
composes: hasError from 'Components/Form/Input.css'; composes: hasError from '~Components/Form/Input.css';
} }
.hasWarning { .hasWarning {
composes: hasWarning from 'Components/Form/Input.css'; composes: hasWarning from '~Components/Form/Input.css';
} }
.inputWrapper { .inputWrapper {

@ -3,19 +3,19 @@
} }
.input { .input {
composes: input from 'Components/Form/Input.css'; composes: input from '~Components/Form/Input.css';
} }
.hasError { .hasError {
composes: hasError from 'Components/Form/Input.css'; composes: hasError from '~Components/Form/Input.css';
} }
.hasWarning { .hasWarning {
composes: hasWarning from 'Components/Form/Input.css'; composes: hasWarning from '~Components/Form/Input.css';
} }
.hasButton { .hasButton {
composes: hasButton from 'Components/Form/Input.css'; composes: hasButton from '~Components/Form/Input.css';
} }
.recaptchaWrapper { .recaptchaWrapper {

@ -94,7 +94,7 @@
} }
.helpText { .helpText {
composes: helpText from 'Components/Form/FormInputHelpText.css'; composes: helpText from '~Components/Form/FormInputHelpText.css';
margin-top: 8px; margin-top: 8px;
margin-left: 5px; margin-left: 5px;

@ -3,6 +3,6 @@
} }
.inputContainer { .inputContainer {
composes: inputContainer from './TagInput.css'; composes: inputContainer from '~./TagInput.css';
composes: hasButton from 'Components/Form/Input.css'; composes: hasButton from '~Components/Form/Input.css';
} }

@ -1,9 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import FormInputButton from './FormInputButton'; import FormInputButton from './FormInputButton';
import TagInput, { tagShape } from './TagInput'; import TagInput from './TagInput';
import styles from './DeviceInput.css'; import styles from './DeviceInput.css';
class DeviceInput extends Component { class DeviceInput extends Component {

@ -3,8 +3,8 @@
} }
.enhancedSelect { .enhancedSelect {
composes: input from 'Components/Form/Input.css'; composes: input from '~Components/Form/Input.css';
composes: link from 'Components/Link/Link.css'; composes: link from '~Components/Link/Link.css';
position: relative; position: relative;
display: flex; display: flex;
@ -21,11 +21,11 @@
} }
.hasError { .hasError {
composes: hasError from 'Components/Form/Input.css'; composes: hasError from '~Components/Form/Input.css';
} }
.hasWarning { .hasWarning {
composes: hasWarning from 'Components/Form/Input.css'; composes: hasWarning from '~Components/Form/Input.css';
} }
.isDisabled { .isDisabled {
@ -62,7 +62,7 @@
} }
.optionsModalBody { .optionsModalBody {
composes: modalBody from 'Components/Modal/ModalBody.css'; composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -71,7 +71,7 @@
} }
.optionsModalScroller { .optionsModalScroller {
composes: scroller from 'Components/Scroller/Scroller.css'; composes: scroller from '~Components/Scroller/Scroller.css';
border: 1px solid $inputBorderColor; border: 1px solid $inputBorderColor;
border-radius: 4px; border-radius: 4px;

@ -87,6 +87,9 @@ class EnhancedSelectInput extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this._buttonRef = {};
this._optionsRef = {};
this.state = { this.state = {
isOpen: false, isOpen: false,
selectedIndex: getSelectedIndex(props), selectedIndex: getSelectedIndex(props),
@ -106,14 +109,6 @@ class EnhancedSelectInput extends Component {
// //
// Control // Control
_setButtonRef = (ref) => {
this._buttonRef = ref;
}
_setOptionsRef = (ref) => {
this._optionsRef = ref;
}
_addListener() { _addListener() {
window.addEventListener('click', this.onWindowClick); window.addEventListener('click', this.onWindowClick);
} }
@ -126,8 +121,8 @@ class EnhancedSelectInput extends Component {
// Listeners // Listeners
onWindowClick = (event) => { onWindowClick = (event) => {
const button = ReactDOM.findDOMNode(this._buttonRef); const button = ReactDOM.findDOMNode(this._buttonRef.current);
const options = ReactDOM.findDOMNode(this._optionsRef); const options = ReactDOM.findDOMNode(this._optionsRef.current);
if (!button || this.state.isMobile) { if (!button || this.state.isMobile) {
return; return;
@ -276,75 +271,91 @@ class EnhancedSelectInput extends Component {
element: styles.tether element: styles.tether
}} }}
{...tetherOptions} {...tetherOptions}
> renderTarget={
<Measure (ref) => {
whitelist={['width']} this._buttonRef = ref;
onMeasure={this.onMeasure}
> return (
<Link <Measure
ref={this._setButtonRef} whitelist={['width']}
className={classNames( onMeasure={this.onMeasure}
className, >
hasError && styles.hasError, <div ref={ref}>
hasWarning && styles.hasWarning, <Link
isDisabled && disabledClassName className={classNames(
)} className,
isDisabled={isDisabled} hasError && styles.hasError,
onBlur={this.onBlur} hasWarning && styles.hasWarning,
onKeyDown={this.onKeyDown} isDisabled && disabledClassName
onPress={this.onPress} )}
> isDisabled={isDisabled}
<SelectedValueComponent onBlur={this.onBlur}
{...selectedValueOptions} onKeyDown={this.onKeyDown}
{...selectedOption} onPress={this.onPress}
isDisabled={isDisabled} >
> <SelectedValueComponent
{selectedOption ? selectedOption.value : null} {...selectedValueOptions}
</SelectedValueComponent> {...selectedOption}
isDisabled={isDisabled}
<div >
className={isDisabled ? {selectedOption ? selectedOption.value : null}
styles.dropdownArrowContainerDisabled : </SelectedValueComponent>
styles.dropdownArrowContainer
} <div
> className={isDisabled ?
<Icon styles.dropdownArrowContainerDisabled :
name={icons.CARET_DOWN} styles.dropdownArrowContainer
/> }
</div> >
</Link> <Icon
</Measure> name={icons.CARET_DOWN}
/>
{ </div>
isOpen && !isMobile && </Link>
<div </div>
ref={this._setOptionsRef} </Measure>
className={styles.optionsContainer} );
style={{ }
minWidth: width }
}} renderElement={
> (ref) => {
<div className={styles.options}> this._optionsRef = ref;
{
values.map((v, index) => { if (!isOpen || isMobile) {
return ( return;
<OptionComponent }
key={v.key}
id={v.key} return (
isSelected={index === selectedIndex} <div
{...v} ref={ref}
isMobile={false} className={styles.optionsContainer}
onSelect={this.onSelect} style={{
> minWidth: width
{v.value} }}
</OptionComponent> >
); <div className={styles.options}>
}) {
} values.map((v, index) => {
return (
<OptionComponent
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
{...v}
isMobile={false}
onSelect={this.onSelect}
>
{v.value}
</OptionComponent>
);
})
}
</div>
</div> </div>
</div> );
}
} }
</TetherComponent> />
{ {
isMobile && isMobile &&

@ -1,5 +1,5 @@
.button { .button {
composes: button from 'Components/Link/Button.css'; composes: button from '~Components/Link/Button.css';
border-left: none; border-left: none;
border-top-left-radius: 0; border-top-left-radius: 0;

@ -6,6 +6,7 @@ import AutoCompleteInput from './AutoCompleteInput';
import CaptchaInputConnector from './CaptchaInputConnector'; import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput'; import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector'; import DeviceInputConnector from './DeviceInputConnector';
import KeyValueListInput from './KeyValueListInput';
import NumberInput from './NumberInput'; import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector'; import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput'; import PasswordInput from './PasswordInput';
@ -34,6 +35,9 @@ function getComponent(type) {
case inputTypes.DEVICE: case inputTypes.DEVICE:
return DeviceInputConnector; return DeviceInputConnector;
case inputTypes.KEY_VALUE_LIST:
return KeyValueListInput;
case inputTypes.NUMBER: case inputTypes.NUMBER:
return NumberInput; return NumberInput;

@ -33,7 +33,7 @@
} }
.link { .link {
composes: link from 'Components/Link/Link.css'; composes: link from '~Components/Link/Link.css';
margin-left: 5px; margin-left: 5px;
} }

@ -0,0 +1,21 @@
.inputContainer {
composes: input from '~Components/Form/Input.css';
position: relative;
min-height: 35px;
height: auto;
&.isFocused {
outline: 0;
border-color: $inputFocusBorderColor;
box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
}
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from '~Components/Form/Input.css';
}

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

@ -0,0 +1,14 @@
.itemContainer {
display: flex;
margin-bottom: 3px;
border-bottom: 1px solid $inputBorderColor;
&:last-child {
margin-bottom: 0;
}
}
.keyInput,
.valueInput {
border: none;
}

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

@ -1,5 +1,5 @@
.input { .input {
composes: input from 'Components/Form/TextInput.css'; composes: input from '~Components/Form/TextInput.css';
font-family: $passwordFamily; font-family: $passwordFamily;
} }

@ -1,17 +1,17 @@
.path { .path {
composes: input from 'Components/Form/Input.css'; composes: input from '~Components/Form/Input.css';
} }
.hasError { .hasError {
composes: hasError from 'Components/Form/Input.css'; composes: hasError from '~Components/Form/Input.css';
} }
.hasWarning { .hasWarning {
composes: hasWarning from 'Components/Form/Input.css'; composes: hasWarning from '~Components/Form/Input.css';
} }
.hasFileBrowser { .hasFileBrowser {
composes: hasButton from 'Components/Form/Input.css'; composes: hasButton from '~Components/Form/Input.css';
} }
.pathInputWrapper { .pathInputWrapper {
@ -62,7 +62,7 @@
} }
.fileBrowserButton { .fileBrowserButton {
composes: button from './FormInputButton.css'; composes: button from '~./FormInputButton.css';
height: 35px; height: 35px;
} }

@ -111,6 +111,7 @@ class PathInput extends Component {
value, value,
placeholder, placeholder,
paths, paths,
includeFiles,
hasError, hasError,
hasWarning, hasWarning,
hasFileBrowser, hasFileBrowser,
@ -171,6 +172,7 @@ class PathInput extends Component {
isOpen={this.state.isFileBrowserModalOpen} isOpen={this.state.isFileBrowserModalOpen}
name={name} name={name}
value={value} value={value}
includeFiles={includeFiles}
onChange={onChange} onChange={onChange}
onModalClose={this.onFileBrowserModalClose} onModalClose={this.onFileBrowserModalClose}
/> />
@ -188,6 +190,7 @@ PathInput.propTypes = {
value: PropTypes.string, value: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
paths: PropTypes.array.isRequired, paths: PropTypes.array.isRequired,
includeFiles: PropTypes.bool.isRequired,
hasError: PropTypes.bool, hasError: PropTypes.bool,
hasWarning: PropTypes.bool, hasWarning: PropTypes.bool,
hasFileBrowser: PropTypes.bool, hasFileBrowser: PropTypes.bool,

@ -28,8 +28,8 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchPaths, dispatchFetchPaths: fetchPaths,
clearPaths dispatchClearPaths: clearPaths
}; };
class PathInputConnector extends Component { class PathInputConnector extends Component {
@ -38,11 +38,19 @@ class PathInputConnector extends Component {
// Listeners // Listeners
onFetchPaths = (path) => { onFetchPaths = (path) => {
this.props.fetchPaths({ path }); const {
includeFiles,
dispatchFetchPaths
} = this.props;
dispatchFetchPaths({
path,
includeFiles
});
} }
onClearPaths = () => { onClearPaths = () => {
this.props.clearPaths(); this.props.dispatchClearPaths();
} }
// //
@ -60,8 +68,13 @@ class PathInputConnector extends Component {
} }
PathInputConnector.propTypes = { PathInputConnector.propTypes = {
fetchPaths: PropTypes.func.isRequired, includeFiles: PropTypes.bool.isRequired,
clearPaths: PropTypes.func.isRequired dispatchFetchPaths: PropTypes.func.isRequired,
dispatchClearPaths: PropTypes.func.isRequired
};
PathInputConnector.defaultProps = {
includeFiles: false
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(PathInputConnector); export default connect(createMapStateToProps, mapDispatchToProps)(PathInputConnector);

@ -20,6 +20,8 @@ function getType(type) {
return inputTypes.NUMBER; return inputTypes.NUMBER;
case 'path': case 'path':
return inputTypes.PATH; return inputTypes.PATH;
case 'filepath':
return inputTypes.PATH;
case 'select': case 'select':
return inputTypes.SELECT; return inputTypes.SELECT;
case 'tag': case 'tag':
@ -84,7 +86,7 @@ function ProviderFieldFormGroup(props) {
errors={errors} errors={errors}
warnings={warnings} warnings={warnings}
pending={pending} pending={pending}
hasFileBrowser={false} includeFiles={type === 'filepath' ? true : undefined}
onChange={onChange} onChange={onChange}
{...otherProps} {...otherProps}
/> />

@ -1,5 +1,5 @@
.selectedValue { .selectedValue {
composes: selectedValue from './EnhancedSelectInputSelectedValue.css'; composes: selectedValue from '~./EnhancedSelectInputSelectedValue.css';
display: flex; display: flex;
align-items: center; align-items: center;

@ -1,15 +1,15 @@
.select { .select {
composes: input from 'Components/Form/Input.css'; composes: input from '~Components/Form/Input.css';
padding: 0 11px; padding: 0 11px;
} }
.hasError { .hasError {
composes: hasError from 'Components/Form/Input.css'; composes: hasError from '~Components/Form/Input.css';
} }
.hasWarning { .hasWarning {
composes: hasWarning from 'Components/Form/Input.css'; composes: hasWarning from '~Components/Form/Input.css';
} }
.isDisabled { .isDisabled {

@ -1,5 +1,5 @@
.inputContainer { .inputContainer {
composes: input from 'Components/Form/Input.css'; composes: input from '~Components/Form/Input.css';
position: relative; position: relative;
padding: 0; padding: 0;
@ -14,11 +14,11 @@
} }
.hasError { .hasError {
composes: hasError from 'Components/Form/Input.css'; composes: hasError from '~Components/Form/Input.css';
} }
.hasWarning { .hasWarning {
composes: hasWarning from 'Components/Form/Input.css'; composes: hasWarning from '~Components/Form/Input.css';
} }
.tags { .tags {

@ -4,6 +4,7 @@ import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest'; import Autosuggest from 'react-autosuggest';
import classNames from 'classnames'; import classNames from 'classnames';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import TagInputInput from './TagInputInput'; import TagInputInput from './TagInputInput';
import TagInputTag from './TagInputTag'; import TagInputTag from './TagInputTag';
import styles from './TagInput.css'; import styles from './TagInput.css';
@ -266,11 +267,6 @@ class TagInput extends Component {
} }
} }
export const tagShape = {
id: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]).isRequired,
name: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired
};
TagInput.propTypes = { TagInput.propTypes = {
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
inputClassName: PropTypes.string.isRequired, inputClassName: PropTypes.string.isRequired,

@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import { tagShape } from './TagInput'; import tagShape from 'Helpers/Props/Shapes/tagShape';
import styles from './TagInputInput.css'; import styles from './TagInputInput.css';
class TagInputInput extends Component { class TagInputInput extends Component {

@ -1,9 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import Label from 'Components/Label'; import Label from 'Components/Label';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { tagShape } from './TagInput';
class TagInputTag extends Component { class TagInputTag extends Component {

@ -1,5 +1,5 @@
.input { .input {
composes: input from 'Components/Form/Input.css'; composes: input from '~Components/Form/Input.css';
} }
.readOnly { .readOnly {
@ -7,13 +7,13 @@
} }
.hasError { .hasError {
composes: hasError from 'Components/Form/Input.css'; composes: hasError from '~Components/Form/Input.css';
} }
.hasWarning { .hasWarning {
composes: hasWarning from 'Components/Form/Input.css'; composes: hasWarning from '~Components/Form/Input.css';
} }
.hasButton { .hasButton {
composes: hasButton from 'Components/Form/Input.css'; composes: hasButton from '~Components/Form/Input.css';
} }

@ -1,11 +1,15 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React, { PureComponent } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import classNames from 'classnames'; import classNames from 'classnames';
import styles from './Icon.css'; import styles from './Icon.css';
class Icon extends React.PureComponent { class Icon extends PureComponent {
//
// Render
render() { render() {
const { const {
containerClassName, containerClassName,

@ -0,0 +1,40 @@
.label {
display: inline-block;
margin: 2px;
color: $white;
/** text-align: center; **/
white-space: nowrap;
line-height: 1;
cursor: default;
}
.title {
margin-bottom: 2px;
color: $helpTextColor;
font-size: 10px;
}
/** Kinds **/
/** Sizes **/
.small {
padding: 1px 3px;
font-size: 11px;
}
.medium {
padding: 2px 5px;
font-size: 12px;
}
.large {
padding: 3px 7px;
font-size: 14px;
}
/** Outline **/
.outline {
background-color: $white;
}

@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { kinds, sizes } from 'Helpers/Props';
import styles from './InfoLabel.css';
function InfoLabel(props) {
const {
className,
title,
kind,
size,
outline,
children,
...otherProps
} = props;
return (
<span
className={classNames(
className,
styles[kind],
styles[size],
outline && styles.outline
)}
{...otherProps}
>
<div className={styles.title}>
{title}
</div>
<div>
{children}
</div>
</span>
);
}
InfoLabel.propTypes = {
className: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired,
size: PropTypes.oneOf(sizes.all).isRequired,
outline: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired
};
InfoLabel.defaultProps = {
className: styles.label,
kind: kinds.DEFAULT,
size: sizes.SMALL,
outline: false
};
export default InfoLabel;

@ -1,5 +1,5 @@
.button { .button {
composes: link from './Link.css'; composes: link from '~./Link.css';
overflow: hidden; overflow: hidden;
border: 1px solid; border: 1px solid;

@ -1,5 +1,5 @@
.button { .button {
composes: button from 'Components/Form/FormInputButton.css'; composes: button from '~Components/Form/FormInputButton.css';
position: relative; position: relative;
} }

@ -1,5 +1,5 @@
.button { .button {
composes: link from 'Components/Link/Link.css'; composes: link from '~Components/Link/Link.css';
display: inline-block; display: inline-block;
margin: 0 2px; margin: 0 2px;

@ -1,5 +1,5 @@
.button { .button {
composes: button from 'Components/Link/Button.css'; composes: button from '~Components/Link/Button.css';
position: relative; position: relative;
} }

@ -1,5 +1,5 @@
.iconContainer { .iconContainer {
composes: spinnerContainer from 'Components/Link/SpinnerButton.css'; composes: spinnerContainer from '~Components/Link/SpinnerButton.css';
} }
.icon { .icon {
@ -7,7 +7,7 @@
} }
.label { .label {
composes: label from 'Components/Link/SpinnerButton.css'; composes: label from '~Components/Link/SpinnerButton.css';
} }
.showIcon { .showIcon {

@ -6,9 +6,13 @@ const messages = [
// TODO Add some messages here // TODO Add some messages here
]; ];
let message = null;
function LoadingMessage() { function LoadingMessage() {
const index = Math.floor(Math.random() * messages.length); if (!message) {
const message = messages[index]; const index = Math.floor(Math.random() * messages.length);
message = messages[index];
}
return ( return (
<div className={styles.loadingMessage}> <div className={styles.loadingMessage}>

@ -1,5 +1,5 @@
.filterMenu { .filterMenu {
composes: menu from './Menu.css'; composes: menu from '~./Menu.css';
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointSmall) {

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

Loading…
Cancel
Save