mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52a69b662d | |||
| 7e34d89069 | |||
| b0024b28a5 | |||
| ae5450f75d | |||
| 1d1aca1a04 | |||
| 3a55316ada | |||
| 9ef7c2a0b4 | |||
| e759f3fd0b | |||
| 03429db877 | |||
| bb5f421e38 | |||
| 7dd3ed815a | |||
| cc56482819 | |||
| 40f41847fd | |||
| 8485fc8c75 | |||
| f3026df65d | |||
| cfd25e974f | |||
| c52f9c5ec4 | |||
| b91517afd5 | |||
| ee8aaadb29 | |||
| 0694f2fa76 | |||
| 2c81f3be0f | |||
| 8fb2f64e98 | |||
| efd2b80e10 | |||
| a9bbe06966 | |||
| 4c6f80b308 | |||
| c8299f7e57 | |||
| 445babbca8 | |||
| e5137d13e9 |
+1
-1
@@ -9,7 +9,7 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.22.2'
|
majorVersion: '5.22.3'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
|||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
import { InputChanged } from 'typings/inputs';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function QueueOptions() {
|
function QueueOptions() {
|
||||||
@@ -16,7 +16,7 @@ function QueueOptions() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleOptionChange = useCallback(
|
const handleOptionChange = useCallback(
|
||||||
({ name, value }: CheckInputChanged) => {
|
({ name, value }: InputChanged<boolean>) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setQueueOption({
|
setQueueOption({
|
||||||
[name]: value,
|
[name]: value,
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ class AddNewMovieModalContent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<div className={styles.overview}>
|
{overview ? (
|
||||||
{overview}
|
<div className={styles.overview}>{overview}</div>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
@@ -98,7 +98,9 @@ class AddNewMovieModalContent extends Component {
|
|||||||
movieFolder: folder,
|
movieFolder: folder,
|
||||||
isWindows
|
isWindows
|
||||||
}}
|
}}
|
||||||
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
|
helpText={translate('AddNewMovieRootFolderHelpText', {
|
||||||
|
folder
|
||||||
|
})}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...rootFolderPath}
|
{...rootFolderPath}
|
||||||
/>
|
/>
|
||||||
@@ -110,7 +112,7 @@ class AddNewMovieModalContent extends Component {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
name="monitor"
|
name="monitor"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...monitor}
|
{...monitor}
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
.inputContainer {
|
.inputContainer {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
|
|
||||||
div {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-bottom: 3px;
|
margin-bottom: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class ImportMovieFooter extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
name="monitor"
|
name="monitor"
|
||||||
value={monitor}
|
value={monitor}
|
||||||
isDisabled={!selectedCount}
|
isDisabled={!selectedCount}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function ImportMovieRow(props) {
|
|||||||
|
|
||||||
<VirtualTableRowCell className={styles.monitor}>
|
<VirtualTableRowCell className={styles.monitor}>
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
name="monitor"
|
name="monitor"
|
||||||
value={monitor}
|
value={monitor}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DocumentTitle from 'react-document-title';
|
import DocumentTitle from 'react-document-title';
|
||||||
@@ -12,17 +13,21 @@ interface AppProps {
|
|||||||
history: ConnectedRouterProps['history'];
|
history: ConnectedRouterProps['history'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
function App({ store, history }: AppProps) {
|
function App({ store, history }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<DocumentTitle title={window.Radarr.instanceName}>
|
<DocumentTitle title={window.Radarr.instanceName}>
|
||||||
<Provider store={store}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ConnectedRouter history={history}>
|
<Provider store={store}>
|
||||||
<ApplyTheme />
|
<ConnectedRouter history={history}>
|
||||||
<PageConnector>
|
<ApplyTheme />
|
||||||
<AppRoutes />
|
<PageConnector>
|
||||||
</PageConnector>
|
<AppRoutes />
|
||||||
</ConnectedRouter>
|
</PageConnector>
|
||||||
</Provider>
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
|
import CaptchaAppState from './CaptchaAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import ExtraFilesAppState from './ExtraFilesAppState';
|
import ExtraFilesAppState from './ExtraFilesAppState';
|
||||||
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
|
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
|
||||||
@@ -9,9 +10,11 @@ import MovieCollectionAppState from './MovieCollectionAppState';
|
|||||||
import MovieCreditAppState from './MovieCreditAppState';
|
import MovieCreditAppState from './MovieCreditAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||||
|
import OAuthAppState from './OAuthAppState';
|
||||||
import OrganizePreviewAppState from './OrganizePreviewAppState';
|
import OrganizePreviewAppState from './OrganizePreviewAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
import PathsAppState from './PathsAppState';
|
import PathsAppState from './PathsAppState';
|
||||||
|
import ProviderOptionsAppState from './ProviderOptionsAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
import ReleasesAppState from './ReleasesAppState';
|
import ReleasesAppState from './ReleasesAppState';
|
||||||
import RootFolderAppState from './RootFolderAppState';
|
import RootFolderAppState from './RootFolderAppState';
|
||||||
@@ -68,6 +71,7 @@ interface AppState {
|
|||||||
app: AppSectionState;
|
app: AppSectionState;
|
||||||
blocklist: BlocklistAppState;
|
blocklist: BlocklistAppState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
|
captcha: CaptchaAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
extraFiles: ExtraFilesAppState;
|
extraFiles: ExtraFilesAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
@@ -79,9 +83,11 @@ interface AppState {
|
|||||||
movieHistory: MovieHistoryAppState;
|
movieHistory: MovieHistoryAppState;
|
||||||
movieIndex: MovieIndexAppState;
|
movieIndex: MovieIndexAppState;
|
||||||
movies: MoviesAppState;
|
movies: MoviesAppState;
|
||||||
|
oAuth: OAuthAppState;
|
||||||
organizePreview: OrganizePreviewAppState;
|
organizePreview: OrganizePreviewAppState;
|
||||||
parse: ParseAppState;
|
parse: ParseAppState;
|
||||||
paths: PathsAppState;
|
paths: PathsAppState;
|
||||||
|
providerOptions: ProviderOptionsAppState;
|
||||||
queue: QueueAppState;
|
queue: QueueAppState;
|
||||||
releases: ReleasesAppState;
|
releases: ReleasesAppState;
|
||||||
rootFolders: RootFolderAppState;
|
rootFolders: RootFolderAppState;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
interface CaptchaAppState {
|
||||||
|
refreshing: false;
|
||||||
|
token: string;
|
||||||
|
siteKey: unknown;
|
||||||
|
secretToken: unknown;
|
||||||
|
ray: unknown;
|
||||||
|
stoken: unknown;
|
||||||
|
responseUrl: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CaptchaAppState;
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
|
Error,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import MovieCollection from 'typings/MovieCollection';
|
import MovieCollection from 'typings/MovieCollection';
|
||||||
|
|
||||||
interface MovieCollectionAppState
|
interface MovieCollectionAppState
|
||||||
extends AppSectionState<MovieCollection>,
|
extends AppSectionState<MovieCollection>,
|
||||||
|
AppSectionFilterState<MovieCollection>,
|
||||||
AppSectionSaveState {
|
AppSectionSaveState {
|
||||||
itemMap: Record<number, number>;
|
itemMap: Record<number, number>;
|
||||||
|
|
||||||
|
isAdding: boolean;
|
||||||
|
addError: Error;
|
||||||
|
|
||||||
|
pendingChanges: Partial<MovieCollection>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MovieCollectionAppState;
|
export default MovieCollectionAppState;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Error } from './AppSectionState';
|
||||||
|
|
||||||
|
interface OAuthAppState {
|
||||||
|
authorizing: boolean;
|
||||||
|
result: Record<string, unknown> | null;
|
||||||
|
error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OAuthAppState;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import Field, { FieldSelectOption } from 'typings/Field';
|
||||||
|
|
||||||
|
export interface ProviderOptions {
|
||||||
|
fields?: Field[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionsDevice {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionsAppState {
|
||||||
|
devices: AppSectionState<ProviderOptionsDevice>;
|
||||||
|
servers: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
newznabCategories: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getProfiles: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getTags: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getRootFolders: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProviderOptionsAppState;
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import AddNewCollectionMovieModalContentConnector from './AddNewCollectionMovieModalContentConnector';
|
|
||||||
|
|
||||||
function AddNewCollectionMovieModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<AddNewCollectionMovieModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewCollectionMovieModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddNewCollectionMovieModal;
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AddNewCollectionMovieModalContent.css';
|
|
||||||
|
|
||||||
class AddNewCollectionMovieModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onQualityProfileIdChange = ({ value }) => {
|
|
||||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddMoviePress = () => {
|
|
||||||
this.props.onAddMoviePress();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
year,
|
|
||||||
overview,
|
|
||||||
images,
|
|
||||||
isAdding,
|
|
||||||
folder,
|
|
||||||
tags,
|
|
||||||
isSmallScreen,
|
|
||||||
isWindows,
|
|
||||||
onModalClose,
|
|
||||||
onInputChange,
|
|
||||||
rootFolderPath,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
searchForMovie
|
|
||||||
} = 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}>
|
|
||||||
<MoviePoster
|
|
||||||
className={styles.poster}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div className={styles.overview}>
|
|
||||||
{overview}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
|
||||||
name="rootFolderPath"
|
|
||||||
valueOptions={{
|
|
||||||
movieFolder: folder,
|
|
||||||
isWindows
|
|
||||||
}}
|
|
||||||
selectedValueOptions={{
|
|
||||||
movieFolder: folder,
|
|
||||||
isWindows
|
|
||||||
}}
|
|
||||||
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...rootFolderPath}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('Monitor')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
|
||||||
name="monitor"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...monitor}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
|
||||||
name="minimumAvailability"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...minimumAvailability}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
|
||||||
name="qualityProfileId"
|
|
||||||
onChange={this.onQualityProfileIdChange}
|
|
||||||
{...qualityProfileId}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Tags')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TAG}
|
|
||||||
name="tags"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...tags}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
|
||||||
<label className={styles.searchForMissingMovieLabelContainer}>
|
|
||||||
<span className={styles.searchForMissingMovieLabel}>
|
|
||||||
{translate('StartSearchForMissingMovie')}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<CheckInput
|
|
||||||
containerClassName={styles.searchForMissingMovieContainer}
|
|
||||||
className={styles.searchForMissingMovieInput}
|
|
||||||
name="searchForMovie"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...searchForMovie}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<SpinnerButton
|
|
||||||
className={styles.addButton}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
isSpinning={isAdding}
|
|
||||||
onPress={this.onAddMoviePress}
|
|
||||||
>
|
|
||||||
{translate('AddMovie')}
|
|
||||||
</SpinnerButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewCollectionMovieModalContent.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,
|
|
||||||
minimumAvailability: PropTypes.object.isRequired,
|
|
||||||
searchForMovie: PropTypes.object.isRequired,
|
|
||||||
folder: PropTypes.string.isRequired,
|
|
||||||
tags: PropTypes.object.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
isWindows: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onAddMoviePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddNewCollectionMovieModalContent;
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { addMovie, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
|
|
||||||
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
import AddNewMovieModalContent from './AddNewCollectionMovieModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieCollections,
|
|
||||||
createCollectionSelector(),
|
|
||||||
createDimensionsSelector(),
|
|
||||||
createSystemStatusSelector(),
|
|
||||||
(discoverMovieState, collection, dimensions, systemStatus) => {
|
|
||||||
const {
|
|
||||||
isAdding,
|
|
||||||
addError,
|
|
||||||
pendingChanges
|
|
||||||
} = discoverMovieState;
|
|
||||||
|
|
||||||
const collectionDefaults = {
|
|
||||||
rootFolderPath: collection.rootFolderPath,
|
|
||||||
monitor: 'movieOnly',
|
|
||||||
qualityProfileId: collection.qualityProfileId,
|
|
||||||
minimumAvailability: collection.minimumAvailability,
|
|
||||||
searchForMovie: collection.searchOnAdd,
|
|
||||||
tags: collection.tags || []
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
settings,
|
|
||||||
validationErrors,
|
|
||||||
validationWarnings
|
|
||||||
} = selectSettings(collectionDefaults, pendingChanges, addError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isAdding,
|
|
||||||
addError,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
|
||||||
validationErrors,
|
|
||||||
validationWarnings,
|
|
||||||
isWindows: systemStatus.isWindows,
|
|
||||||
...settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
addMovie,
|
|
||||||
setMovieCollectionValue
|
|
||||||
};
|
|
||||||
|
|
||||||
class AddNewCollectionMovieModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.setMovieCollectionValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddMoviePress = () => {
|
|
||||||
const {
|
|
||||||
tmdbId,
|
|
||||||
title,
|
|
||||||
rootFolderPath,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
searchForMovie,
|
|
||||||
tags
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.addMovie({
|
|
||||||
tmdbId,
|
|
||||||
title,
|
|
||||||
rootFolderPath: rootFolderPath.value,
|
|
||||||
monitor: monitor.value,
|
|
||||||
qualityProfileId: qualityProfileId.value,
|
|
||||||
minimumAvailability: minimumAvailability.value,
|
|
||||||
searchForMovie: searchForMovie.value,
|
|
||||||
tags: tags.value
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.onModalClose(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<AddNewMovieModalContent
|
|
||||||
{...this.props}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onAddMoviePress={this.onAddMoviePress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewCollectionMovieModalContentConnector.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
rootFolderPath: PropTypes.object,
|
|
||||||
monitor: PropTypes.object.isRequired,
|
|
||||||
qualityProfileId: PropTypes.object,
|
|
||||||
minimumAvailability: PropTypes.object.isRequired,
|
|
||||||
searchForMovie: PropTypes.object.isRequired,
|
|
||||||
tags: PropTypes.object.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
addMovie: PropTypes.func.isRequired,
|
|
||||||
setMovieCollectionValue: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewCollectionMovieModalContentConnector);
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import AddNewMovieCollectionMovieModalContent, {
|
||||||
|
AddNewMovieCollectionMovieModalContentProps,
|
||||||
|
} from './AddNewMovieCollectionMovieModalContent';
|
||||||
|
|
||||||
|
interface AddNewCollectionMovieModalProps
|
||||||
|
extends AddNewMovieCollectionMovieModalContentProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddNewMovieCollectionMovieModal({
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: AddNewCollectionMovieModalProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
dispatch(clearPendingChanges({ section: 'movieCollections' }));
|
||||||
|
onModalClose();
|
||||||
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
|
<AddNewMovieCollectionMovieModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddNewMovieCollectionMovieModal;
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import useMovieCollection from 'Collection/useMovieCollection';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import { Image } from 'Movie/Movie';
|
||||||
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import {
|
||||||
|
addMovie,
|
||||||
|
setMovieCollectionValue,
|
||||||
|
} from 'Store/Actions/movieCollectionActions';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import useIsWindows from 'System/useIsWindows';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AddNewMovieCollectionMovieModalContent.css';
|
||||||
|
|
||||||
|
export interface AddNewMovieCollectionMovieModalContentProps {
|
||||||
|
tmdbId: number;
|
||||||
|
title: string;
|
||||||
|
year: number;
|
||||||
|
overview?: string;
|
||||||
|
images: Image[];
|
||||||
|
collectionId: number;
|
||||||
|
folder: string;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddNewMovieCollectionMovieModalContent({
|
||||||
|
tmdbId,
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
overview,
|
||||||
|
images,
|
||||||
|
collectionId,
|
||||||
|
folder,
|
||||||
|
onModalClose,
|
||||||
|
}: AddNewMovieCollectionMovieModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const collection = useMovieCollection(collectionId)!;
|
||||||
|
|
||||||
|
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||||
|
const isWindows = useIsWindows();
|
||||||
|
|
||||||
|
const { isAdding, addError, pendingChanges } = useSelector(
|
||||||
|
(state: AppState) => state.movieCollections
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasAdding = usePrevious(isAdding);
|
||||||
|
|
||||||
|
const { settings, validationErrors, validationWarnings } = useMemo(() => {
|
||||||
|
const options = {
|
||||||
|
rootFolderPath: collection.rootFolderPath,
|
||||||
|
monitor: collection.monitored ? 'movieOnly' : 'none',
|
||||||
|
qualityProfileId: collection.qualityProfileId,
|
||||||
|
minimumAvailability: collection.minimumAvailability,
|
||||||
|
searchForMovie: collection.searchOnAdd,
|
||||||
|
tags: collection.tags || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return selectSettings(options, pendingChanges, addError);
|
||||||
|
}, [collection, pendingChanges, addError]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
rootFolderPath,
|
||||||
|
searchForMovie,
|
||||||
|
tags,
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
// @ts-expect-error actions aren't typed
|
||||||
|
dispatch(setMovieCollectionValue({ name, value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddMoviePress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
addMovie({
|
||||||
|
tmdbId,
|
||||||
|
title,
|
||||||
|
rootFolderPath: rootFolderPath.value,
|
||||||
|
monitor: monitor.value,
|
||||||
|
qualityProfileId: qualityProfileId.value,
|
||||||
|
minimumAvailability: minimumAvailability.value,
|
||||||
|
searchForMovie: searchForMovie.value,
|
||||||
|
tags: tags.value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
tmdbId,
|
||||||
|
title,
|
||||||
|
rootFolderPath,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
searchForMovie,
|
||||||
|
tags,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAdding && wasAdding && !addError) {
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
}, [isAdding, wasAdding, addError, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
{!title.includes(String(year)) && year ? (
|
||||||
|
<span className={styles.year}>({year})</span>
|
||||||
|
) : null}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{isSmallScreen ? null : (
|
||||||
|
<div className={styles.poster}>
|
||||||
|
<MoviePoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
{overview ? (
|
||||||
|
<div className={styles.overview}>{overview}</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Form
|
||||||
|
validationErrors={validationErrors}
|
||||||
|
validationWarnings={validationWarnings}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
valueOptions={{
|
||||||
|
movieFolder: folder,
|
||||||
|
isWindows,
|
||||||
|
}}
|
||||||
|
selectedValueOptions={{
|
||||||
|
movieFolder: folder,
|
||||||
|
isWindows,
|
||||||
|
}}
|
||||||
|
helpText={translate('AddNewMovieRootFolderHelpText', {
|
||||||
|
folder,
|
||||||
|
})}
|
||||||
|
{...rootFolderPath}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Monitor')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
|
name="monitor"
|
||||||
|
{...monitor}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon className={styles.labelIcon} name={icons.INFO} />
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
|
name="minimumAvailability"
|
||||||
|
{...minimumAvailability}
|
||||||
|
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
{...qualityProfileId}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
{...tags}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<label className={styles.searchForMissingMovieLabelContainer}>
|
||||||
|
<span className={styles.searchForMissingMovieLabel}>
|
||||||
|
{translate('StartSearchForMissingMovie')}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckInput
|
||||||
|
containerClassName={styles.searchForMissingMovieContainer}
|
||||||
|
className={styles.searchForMissingMovieInput}
|
||||||
|
name="searchForMovie"
|
||||||
|
{...searchForMovie}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.addButton}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
isSpinning={isAdding}
|
||||||
|
onPress={handleAddMoviePress}
|
||||||
|
>
|
||||||
|
{translate('AddMovie')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddNewMovieCollectionMovieModalContent;
|
||||||
@@ -18,9 +18,9 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import CollectionFooter from './CollectionFooter';
|
import CollectionFooter from './CollectionFooter';
|
||||||
import CollectionFilterMenu from './Menus/CollectionFilterMenu';
|
import MovieCollectionFilterMenu from './Menus/MovieCollectionFilterMenu';
|
||||||
import CollectionSortMenu from './Menus/CollectionSortMenu';
|
import MovieCollectionSortMenu from './Menus/MovieCollectionSortMenu';
|
||||||
import NoCollection from './NoCollection';
|
import NoMovieCollections from './NoMovieCollections';
|
||||||
import CollectionOverviewsConnector from './Overview/CollectionOverviewsConnector';
|
import CollectionOverviewsConnector from './Overview/CollectionOverviewsConnector';
|
||||||
import CollectionOverviewOptionsModal from './Overview/Options/CollectionOverviewOptionsModal';
|
import CollectionOverviewOptionsModal from './Overview/Options/CollectionOverviewOptionsModal';
|
||||||
|
|
||||||
@@ -284,14 +284,14 @@ class Collection extends Component {
|
|||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
}
|
}
|
||||||
|
|
||||||
<CollectionSortMenu
|
<MovieCollectionSortMenu
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
isDisabled={hasNoCollection}
|
isDisabled={hasNoCollection}
|
||||||
onSortSelect={onSortSelect}
|
onSortSelect={onSortSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CollectionFilterMenu
|
<MovieCollectionFilterMenu
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={customFilters}
|
customFilters={customFilters}
|
||||||
@@ -341,7 +341,7 @@ class Collection extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!error && isPopulated && !items.length &&
|
!error && isPopulated && !items.length &&
|
||||||
<NoCollection totalItems={totalItems} />
|
<NoMovieCollections totalItems={totalItems} />
|
||||||
}
|
}
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import FilterModal from 'Components/Filter/FilterModal';
|
|
||||||
import { setMovieCollectionsFilter } from 'Store/Actions/movieCollectionActions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieCollections.items,
|
|
||||||
(state) => state.movieCollections.filterBuilderProps,
|
|
||||||
(sectionItems, filterBuilderProps) => {
|
|
||||||
return {
|
|
||||||
sectionItems,
|
|
||||||
filterBuilderProps,
|
|
||||||
customFilterType: 'movieCollections'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchSetFilter: setMovieCollectionsFilter
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
|
|
||||||
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
|
|
||||||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
|
||||||
import SelectInput from 'Components/Form/SelectInput';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import CollectionFooterLabel from './CollectionFooterLabel';
|
|
||||||
import styles from './CollectionFooter.css';
|
|
||||||
|
|
||||||
const NO_CHANGE = 'noChange';
|
|
||||||
|
|
||||||
const monitoredOptions = [
|
|
||||||
{
|
|
||||||
key: NO_CHANGE,
|
|
||||||
get value() {
|
|
||||||
return translate('NoChange');
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'monitored',
|
|
||||||
get value() {
|
|
||||||
return translate('Monitored');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'unmonitored',
|
|
||||||
get value() {
|
|
||||||
return translate('Unmonitored');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const searchOnAddOptions = [
|
|
||||||
{
|
|
||||||
key: NO_CHANGE,
|
|
||||||
get value() {
|
|
||||||
return translate('NoChange');
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'yes',
|
|
||||||
get value() {
|
|
||||||
return translate('Yes');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'no',
|
|
||||||
get value() {
|
|
||||||
return translate('No');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class CollectionFooter extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
monitored: NO_CHANGE,
|
|
||||||
monitor: NO_CHANGE,
|
|
||||||
qualityProfileId: NO_CHANGE,
|
|
||||||
minimumAvailability: NO_CHANGE,
|
|
||||||
rootFolderPath: NO_CHANGE,
|
|
||||||
searchOnAdd: NO_CHANGE
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
isSaving,
|
|
||||||
saveError
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newState = {};
|
|
||||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
|
||||||
this.setState({
|
|
||||||
monitored: NO_CHANGE,
|
|
||||||
monitor: NO_CHANGE,
|
|
||||||
qualityProfileId: NO_CHANGE,
|
|
||||||
minimumAvailability: NO_CHANGE,
|
|
||||||
rootFolderPath: NO_CHANGE,
|
|
||||||
searchOnAdd: NO_CHANGE
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEmpty(newState)) {
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.setState({ [name]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onUpdateSelectedPress = () => {
|
|
||||||
const {
|
|
||||||
monitored,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
rootFolderPath,
|
|
||||||
searchOnAdd
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const changes = {};
|
|
||||||
|
|
||||||
if (monitored !== NO_CHANGE) {
|
|
||||||
changes.monitored = monitored === 'monitored';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monitor !== NO_CHANGE) {
|
|
||||||
changes.monitor = monitor;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (qualityProfileId !== NO_CHANGE) {
|
|
||||||
changes.qualityProfileId = qualityProfileId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minimumAvailability !== NO_CHANGE) {
|
|
||||||
changes.minimumAvailability = minimumAvailability;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rootFolderPath !== NO_CHANGE) {
|
|
||||||
changes.rootFolderPath = rootFolderPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchOnAdd !== NO_CHANGE) {
|
|
||||||
changes.searchOnAdd = searchOnAdd === 'yes';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onUpdateSelectedPress(changes);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
selectedIds,
|
|
||||||
isSaving
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
monitored,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
rootFolderPath,
|
|
||||||
searchOnAdd
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const selectedCount = selectedIds.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContentFooter>
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('MonitorCollection')}
|
|
||||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
name="monitored"
|
|
||||||
value={monitored}
|
|
||||||
values={monitoredOptions}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('MonitorMovies')}
|
|
||||||
isSaving={isSaving && monitor !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
name="monitor"
|
|
||||||
value={monitor}
|
|
||||||
values={monitoredOptions}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('QualityProfile')}
|
|
||||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<QualityProfileSelectInputConnector
|
|
||||||
name="qualityProfileId"
|
|
||||||
value={qualityProfileId}
|
|
||||||
includeNoChange={true}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('MinimumAvailability')}
|
|
||||||
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AvailabilitySelectInput
|
|
||||||
name="minimumAvailability"
|
|
||||||
value={minimumAvailability}
|
|
||||||
includeNoChange={true}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('RootFolder')}
|
|
||||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RootFolderSelectInputConnector
|
|
||||||
name="rootFolderPath"
|
|
||||||
value={rootFolderPath}
|
|
||||||
includeNoChange={true}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
selectedValueOptions={{ includeFreeSpace: false }}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('SearchMoviesOnAdd')}
|
|
||||||
isSaving={isSaving && searchOnAdd !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
name="searchOnAdd"
|
|
||||||
value={searchOnAdd}
|
|
||||||
values={searchOnAddOptions}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
|
||||||
<div className={styles.buttonContainerContent}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('CountCollectionsSelected', { count: selectedCount })}
|
|
||||||
isSaving={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.buttons}>
|
|
||||||
<div>
|
|
||||||
<SpinnerButton
|
|
||||||
className={styles.addSelectedButton}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
isSpinning={isSaving}
|
|
||||||
isDisabled={!selectedCount || isSaving}
|
|
||||||
onPress={this.onUpdateSelectedPress}
|
|
||||||
>
|
|
||||||
{translate('UpdateSelected')}
|
|
||||||
</SpinnerButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageContentFooter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CollectionFooter.propTypes = {
|
|
||||||
selectedIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
isAdding: PropTypes.bool.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
onUpdateSelectedPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CollectionFooter;
|
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Error } from 'App/State/AppSectionState';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import CollectionFooterLabel from './CollectionFooterLabel';
|
||||||
|
import styles from './CollectionFooter.css';
|
||||||
|
|
||||||
|
interface SavePayload {
|
||||||
|
monitored?: boolean;
|
||||||
|
monitor?: string;
|
||||||
|
qualityProfileId?: number;
|
||||||
|
minimumAvailability?: string;
|
||||||
|
rootFolderPath?: string;
|
||||||
|
searchOnAdd?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CollectionFooterProps {
|
||||||
|
selectedIds: number[];
|
||||||
|
isAdding: boolean;
|
||||||
|
isSaving: boolean;
|
||||||
|
saveError: Error;
|
||||||
|
onUpdateSelectedPress(payload: object): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
|
const monitoredOptions: EnhancedSelectInputValue<string>[] = [
|
||||||
|
{
|
||||||
|
key: NO_CHANGE,
|
||||||
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
get value() {
|
||||||
|
return translate('Monitored');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'unmonitored',
|
||||||
|
get value() {
|
||||||
|
return translate('Unmonitored');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchOnAddOptions: EnhancedSelectInputValue<string>[] = [
|
||||||
|
{
|
||||||
|
key: NO_CHANGE,
|
||||||
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'yes',
|
||||||
|
get value() {
|
||||||
|
return translate('Yes');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
get value() {
|
||||||
|
return translate('No');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function CollectionFooter({
|
||||||
|
selectedIds,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
onUpdateSelectedPress,
|
||||||
|
}: CollectionFooterProps) {
|
||||||
|
const [monitored, setMonitored] = useState(NO_CHANGE);
|
||||||
|
const [monitor, setMonitor] = useState(NO_CHANGE);
|
||||||
|
const [qualityProfileId, setQualityProfileId] = useState<string | number>(
|
||||||
|
NO_CHANGE
|
||||||
|
);
|
||||||
|
const [minimumAvailability, setMinimumAvailability] = useState(NO_CHANGE);
|
||||||
|
const [rootFolderPath, setRootFolderPath] = useState(NO_CHANGE);
|
||||||
|
const [searchOnAdd, setSearchOnAdd] = useState(NO_CHANGE);
|
||||||
|
|
||||||
|
const wasSaving = usePrevious(isSaving);
|
||||||
|
|
||||||
|
const handleSavePress = useCallback(() => {
|
||||||
|
let hasChanges = false;
|
||||||
|
const payload: SavePayload = {};
|
||||||
|
|
||||||
|
if (monitored !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.monitored = monitored === 'monitored';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.monitor = monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qualityProfileId !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.qualityProfileId = qualityProfileId as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minimumAvailability !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.minimumAvailability = minimumAvailability as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootFolderPath !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.rootFolderPath = rootFolderPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchOnAdd !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.searchOnAdd = searchOnAdd === 'yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
onUpdateSelectedPress(payload);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
monitor,
|
||||||
|
monitored,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
rootFolderPath,
|
||||||
|
searchOnAdd,
|
||||||
|
onUpdateSelectedPress,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(({ name, value }: InputChanged) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'monitored':
|
||||||
|
setMonitored(value as string);
|
||||||
|
break;
|
||||||
|
case 'monitor':
|
||||||
|
setMonitor(value as string);
|
||||||
|
break;
|
||||||
|
case 'qualityProfileId':
|
||||||
|
setQualityProfileId(value as string);
|
||||||
|
break;
|
||||||
|
case 'minimumAvailability':
|
||||||
|
setMinimumAvailability(value as string);
|
||||||
|
break;
|
||||||
|
case 'rootFolderPath':
|
||||||
|
setRootFolderPath(value as string);
|
||||||
|
break;
|
||||||
|
case 'searchOnAdd':
|
||||||
|
setSearchOnAdd(value as string);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`CollectionFooter Unknown Input: '${name}'`);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSaving && wasSaving && !saveError) {
|
||||||
|
setMonitored(NO_CHANGE);
|
||||||
|
setMonitor(NO_CHANGE);
|
||||||
|
setQualityProfileId(NO_CHANGE);
|
||||||
|
setMinimumAvailability(NO_CHANGE);
|
||||||
|
setRootFolderPath(NO_CHANGE);
|
||||||
|
setSearchOnAdd(NO_CHANGE);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isSaving,
|
||||||
|
wasSaving,
|
||||||
|
saveError,
|
||||||
|
setMonitored,
|
||||||
|
setMonitor,
|
||||||
|
setQualityProfileId,
|
||||||
|
setMinimumAvailability,
|
||||||
|
setRootFolderPath,
|
||||||
|
setSearchOnAdd,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContentFooter>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('MonitorCollection')}
|
||||||
|
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="monitored"
|
||||||
|
value={monitored}
|
||||||
|
values={monitoredOptions}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('MonitorMovies')}
|
||||||
|
isSaving={isSaving && monitor !== NO_CHANGE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="monitor"
|
||||||
|
value={monitor}
|
||||||
|
values={monitoredOptions}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('QualityProfile')}
|
||||||
|
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
value={qualityProfileId}
|
||||||
|
includeNoChange={true}
|
||||||
|
includeNoChangeDisabled={false}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('MinimumAvailability')}
|
||||||
|
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
|
name="minimumAvailability"
|
||||||
|
value={minimumAvailability}
|
||||||
|
includeNoChange={true}
|
||||||
|
includeNoChangeDisabled={false}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('RootFolder')}
|
||||||
|
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
value={rootFolderPath}
|
||||||
|
includeNoChange={true}
|
||||||
|
includeNoChangeDisabled={false}
|
||||||
|
selectedValueOptions={{ includeFreeSpace: false }}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('SearchMoviesOnAdd')}
|
||||||
|
isSaving={isSaving && searchOnAdd !== NO_CHANGE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="searchOnAdd"
|
||||||
|
value={searchOnAdd}
|
||||||
|
values={searchOnAddOptions}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<div className={styles.buttonContainerContent}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('CountCollectionsSelected', {
|
||||||
|
count: selectedCount,
|
||||||
|
})}
|
||||||
|
isSaving={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<div>
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.addSelectedButton}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
isDisabled={!selectedCount || isSaving}
|
||||||
|
onPress={handleSavePress}
|
||||||
|
>
|
||||||
|
{translate('UpdateSelected')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContentFooter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CollectionFooter;
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import styles from './CollectionFooterLabel.css';
|
|
||||||
|
|
||||||
function CollectionFooterLabel(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
label,
|
|
||||||
isSaving
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{label}
|
|
||||||
|
|
||||||
{
|
|
||||||
isSaving &&
|
|
||||||
<SpinnerIcon
|
|
||||||
className={styles.savingIcon}
|
|
||||||
name={icons.SPINNER}
|
|
||||||
isSpinning={true}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CollectionFooterLabel.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
CollectionFooterLabel.defaultProps = {
|
|
||||||
className: styles.label
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CollectionFooterLabel;
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import styles from './CollectionFooterLabel.css';
|
||||||
|
|
||||||
|
interface CollectionFooterLabelProps {
|
||||||
|
className?: string;
|
||||||
|
label: string;
|
||||||
|
isSaving: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollectionFooterLabel({
|
||||||
|
className = styles.label,
|
||||||
|
label,
|
||||||
|
isSaving,
|
||||||
|
}: CollectionFooterLabelProps) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
{label}
|
||||||
|
|
||||||
|
{isSaving ? (
|
||||||
|
<SpinnerIcon
|
||||||
|
className={styles.savingIcon}
|
||||||
|
name={icons.SPINNER}
|
||||||
|
isSpinning={true}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CollectionFooterLabel;
|
||||||
@@ -9,7 +9,7 @@ function createMapStateToProps() {
|
|||||||
createCollectionSelector(),
|
createCollectionSelector(),
|
||||||
(collection) => {
|
(collection) => {
|
||||||
// If a movie is deleted this selector may fire before the parent
|
// If a movie is deleted this selector may fire before the parent
|
||||||
// selecors, which will result in an undefined movie, if that happens
|
// selectors, which will result in an undefined movie, if that happens
|
||||||
// we want to return early here and again in the render function to avoid
|
// we want to return early here and again in the render function to avoid
|
||||||
// trying to show a movie that has no information available.
|
// trying to show a movie that has no information available.
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import EditCollectionModalContentConnector from './EditCollectionModalContentConnector';
|
|
||||||
|
|
||||||
function EditCollectionModal({ isOpen, onModalClose, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<EditCollectionModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditCollectionModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditCollectionModal;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
|
||||||
import EditCollectionModal from './EditCollectionModal';
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
clearPendingChanges
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditCollectionModalConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.props.clearPendingChanges({ section: 'movieCollections' });
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EditCollectionModal
|
|
||||||
{...this.props}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditCollectionModalConnector.propTypes = {
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(undefined, mapDispatchToProps)(EditCollectionModalConnector);
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './EditCollectionModalContent.css';
|
|
||||||
|
|
||||||
class EditCollectionModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
const {
|
|
||||||
onSavePress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSavePress(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
images,
|
|
||||||
overview,
|
|
||||||
item,
|
|
||||||
isSaving,
|
|
||||||
onInputChange,
|
|
||||||
onModalClose,
|
|
||||||
isSmallScreen,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
monitored,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
// Id,
|
|
||||||
rootFolderPath,
|
|
||||||
tags,
|
|
||||||
searchOnAdd
|
|
||||||
} = item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('Edit')} - {title}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div className={styles.container}>
|
|
||||||
{
|
|
||||||
!isSmallScreen &&
|
|
||||||
<div className={styles.poster}>
|
|
||||||
<MoviePoster
|
|
||||||
className={styles.poster}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div className={styles.overview}>
|
|
||||||
{overview}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Monitored')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="monitored"
|
|
||||||
helpText={translate('MonitoredCollectionHelpText')}
|
|
||||||
{...monitored}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
|
||||||
name="minimumAvailability"
|
|
||||||
{...minimumAvailability}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
|
||||||
name="qualityProfileId"
|
|
||||||
{...qualityProfileId}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
|
||||||
name="rootFolderPath"
|
|
||||||
{...rootFolderPath}
|
|
||||||
includeMissingValue={true}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Tags')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TAG}
|
|
||||||
name="tags"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...tags}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="searchOnAdd"
|
|
||||||
helpText={translate('SearchOnAddCollectionHelpText')}
|
|
||||||
{...searchOnAdd}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerButton
|
|
||||||
isSpinning={isSaving}
|
|
||||||
onPress={this.onSavePress}
|
|
||||||
>
|
|
||||||
{translate('Save')}
|
|
||||||
</SpinnerButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditCollectionModalContent.propTypes = {
|
|
||||||
collectionId: PropTypes.number.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
overview: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
item: PropTypes.object.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
isPathChanging: PropTypes.bool.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onSavePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditCollectionModalContent;
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { saveMovieCollection, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
|
|
||||||
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
import EditCollectionModalContent from './EditCollectionModalContent';
|
|
||||||
|
|
||||||
function createIsPathChangingSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieCollections.pendingChanges,
|
|
||||||
createCollectionSelector(),
|
|
||||||
(pendingChanges, collection) => {
|
|
||||||
const rootFolderPath = pendingChanges.rootFolderPath;
|
|
||||||
|
|
||||||
if (rootFolderPath == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return collection.rootFolderPath !== rootFolderPath;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieCollections,
|
|
||||||
createCollectionSelector(),
|
|
||||||
createIsPathChangingSelector(),
|
|
||||||
createDimensionsSelector(),
|
|
||||||
(moviesState, collection, isPathChanging, dimensions) => {
|
|
||||||
const {
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
pendingChanges
|
|
||||||
} = moviesState;
|
|
||||||
|
|
||||||
const movieSettings = {
|
|
||||||
monitored: collection.monitored,
|
|
||||||
qualityProfileId: collection.qualityProfileId,
|
|
||||||
minimumAvailability: collection.minimumAvailability,
|
|
||||||
rootFolderPath: collection.rootFolderPath,
|
|
||||||
tags: collection.tags,
|
|
||||||
searchOnAdd: collection.searchOnAdd
|
|
||||||
};
|
|
||||||
|
|
||||||
const settings = selectSettings(movieSettings, pendingChanges, saveError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: collection.title,
|
|
||||||
images: collection.images,
|
|
||||||
overview: collection.overview,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isPathChanging,
|
|
||||||
originalPath: collection.path,
|
|
||||||
item: settings.settings,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
|
||||||
...settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchSetMovieCollectionValue: setMovieCollectionValue,
|
|
||||||
dispatchSaveMovieCollection: saveMovieCollection
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditCollectionModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
|
||||||
this.props.onModalClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.dispatchSetMovieCollectionValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
this.props.dispatchSaveMovieCollection({
|
|
||||||
id: this.props.collectionId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EditCollectionModalContent
|
|
||||||
{...this.props}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
onMoveMoviePress={this.onMoveMoviePress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditCollectionModalContentConnector.propTypes = {
|
|
||||||
collectionId: PropTypes.number,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
dispatchSetMovieCollectionValue: PropTypes.func.isRequired,
|
|
||||||
dispatchSaveMovieCollection: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditCollectionModalContentConnector);
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import EditMovieCollectionModalContent, {
|
||||||
|
EditMovieCollectionModalContentProps,
|
||||||
|
} from './EditMovieCollectionModalContent';
|
||||||
|
|
||||||
|
interface EditMovieCollectionModalProps
|
||||||
|
extends EditMovieCollectionModalContentProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditMovieCollectionModal({
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: EditMovieCollectionModalProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
dispatch(clearPendingChanges({ section: 'movieCollections' }));
|
||||||
|
onModalClose();
|
||||||
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
|
<EditMovieCollectionModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMovieCollectionModal;
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import useMovieCollection from 'Collection/useMovieCollection';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import {
|
||||||
|
saveMovieCollection,
|
||||||
|
setMovieCollectionValue,
|
||||||
|
} from 'Store/Actions/movieCollectionActions';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './EditMovieCollectionModalContent.css';
|
||||||
|
|
||||||
|
export interface EditMovieCollectionModalContentProps {
|
||||||
|
collectionId: number;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditMovieCollectionModalContent({
|
||||||
|
collectionId,
|
||||||
|
onModalClose,
|
||||||
|
}: EditMovieCollectionModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
overview,
|
||||||
|
monitored,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
rootFolderPath,
|
||||||
|
searchOnAdd,
|
||||||
|
images,
|
||||||
|
tags,
|
||||||
|
} = useMovieCollection(collectionId)!;
|
||||||
|
|
||||||
|
const { isSaving, saveError, pendingChanges } = useSelector(
|
||||||
|
(state: AppState) => state.movieCollections
|
||||||
|
);
|
||||||
|
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||||
|
|
||||||
|
const wasSaving = usePrevious(isSaving);
|
||||||
|
|
||||||
|
const { settings, ...otherSettings } = useMemo(() => {
|
||||||
|
return selectSettings(
|
||||||
|
{
|
||||||
|
monitored,
|
||||||
|
minimumAvailability,
|
||||||
|
qualityProfileId,
|
||||||
|
rootFolderPath,
|
||||||
|
searchOnAdd,
|
||||||
|
tags,
|
||||||
|
},
|
||||||
|
pendingChanges,
|
||||||
|
saveError
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
monitored,
|
||||||
|
minimumAvailability,
|
||||||
|
qualityProfileId,
|
||||||
|
rootFolderPath,
|
||||||
|
searchOnAdd,
|
||||||
|
tags,
|
||||||
|
pendingChanges,
|
||||||
|
saveError,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
// @ts-expect-error actions aren't typed
|
||||||
|
dispatch(setMovieCollectionValue({ name, value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSavePress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
saveMovieCollection({
|
||||||
|
id: collectionId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [collectionId, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSaving && wasSaving && !saveError) {
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('EditMovieCollectionModalHeader', { title })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{isSmallScreen ? null : (
|
||||||
|
<div className={styles.poster}>
|
||||||
|
<MoviePoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.overview}>{overview}</div>
|
||||||
|
|
||||||
|
<Form {...otherSettings}>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="monitored"
|
||||||
|
helpText={translate('MonitoredCollectionHelpText')}
|
||||||
|
{...settings.monitored}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
|
name="minimumAvailability"
|
||||||
|
{...settings.minimumAvailability}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
{...settings.qualityProfileId}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
{...settings.rootFolderPath}
|
||||||
|
includeMissingValue={true}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
{...settings.tags}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="searchOnAdd"
|
||||||
|
helpText={translate('SearchOnAddCollectionHelpText')}
|
||||||
|
{...settings.searchOnAdd}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
error={saveError}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
onPress={handleSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMovieCollectionModalContent;
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import CollectionFilterModalConnector from 'Collection/CollectionFilterModalConnector';
|
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
||||||
import { align } from 'Helpers/Props';
|
|
||||||
|
|
||||||
function CollectionFilterMenu(props) {
|
|
||||||
const {
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
isDisabled,
|
|
||||||
onFilterSelect
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={CollectionFilterModalConnector}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CollectionFilterMenu.propTypes = {
|
|
||||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
CollectionFilterMenu.defaultProps = {
|
|
||||||
showCustomFilters: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CollectionFilterMenu;
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomFilter, Filter } from 'App/State/AppState';
|
||||||
|
import MovieCollectionFilterModal from 'Collection/MovieCollectionFilterModal';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
|
||||||
|
interface MovieCollectionFilterMenuProps {
|
||||||
|
selectedFilterKey: string | number;
|
||||||
|
filters: Filter[];
|
||||||
|
customFilters: CustomFilter[];
|
||||||
|
isDisabled: boolean;
|
||||||
|
onFilterSelect: (filter: number | string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCollectionFilterMenu({
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
isDisabled,
|
||||||
|
onFilterSelect,
|
||||||
|
}: MovieCollectionFilterMenuProps) {
|
||||||
|
return (
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu="right"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={MovieCollectionFilterModal}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCollectionFilterMenu;
|
||||||
+17
-21
@@ -1,24 +1,26 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
import SortMenu from 'Components/Menu/SortMenu';
|
import SortMenu from 'Components/Menu/SortMenu';
|
||||||
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||||
import { align, sortDirections } from 'Helpers/Props';
|
import { align } from 'Helpers/Props';
|
||||||
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function CollectionSortMenu(props) {
|
interface MovieCollectionSortMenuProps {
|
||||||
const {
|
sortKey?: string;
|
||||||
sortKey,
|
sortDirection?: SortDirection;
|
||||||
sortDirection,
|
isDisabled: boolean;
|
||||||
isDisabled,
|
onSortSelect(sortKey: string): void;
|
||||||
onSortSelect
|
}
|
||||||
} = props;
|
|
||||||
|
|
||||||
|
function MovieCollectionSortMenu({
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
isDisabled,
|
||||||
|
onSortSelect,
|
||||||
|
}: MovieCollectionSortMenuProps) {
|
||||||
return (
|
return (
|
||||||
<SortMenu
|
<SortMenu isDisabled={isDisabled} alignMenu={align.RIGHT}>
|
||||||
isDisabled={isDisabled}
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
>
|
|
||||||
<MenuContent>
|
<MenuContent>
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
name="sortTitle"
|
name="sortTitle"
|
||||||
@@ -28,6 +30,7 @@ function CollectionSortMenu(props) {
|
|||||||
>
|
>
|
||||||
{translate('Title')}
|
{translate('Title')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
name="missingMovies"
|
name="missingMovies"
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
@@ -41,11 +44,4 @@ function CollectionSortMenu(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionSortMenu.propTypes = {
|
export default MovieCollectionSortMenu;
|
||||||
sortKey: PropTypes.string,
|
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
onSortSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CollectionSortMenu;
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setMovieCollectionsFilter } from 'Store/Actions/movieCollectionActions';
|
||||||
|
|
||||||
|
interface MovieCollectionFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MovieCollectionFilterModal(
|
||||||
|
props: MovieCollectionFilterModalProps
|
||||||
|
) {
|
||||||
|
const sectionItems = useSelector(
|
||||||
|
(state: AppState) => state.movieCollections.items
|
||||||
|
);
|
||||||
|
const filterBuilderProps = useSelector(
|
||||||
|
(state: AppState) => state.movieCollections.filterBuilderProps
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: { selectedFilterKey: string | number }) => {
|
||||||
|
dispatch(setMovieCollectionsFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType="movieCollections"
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Vendored
+9
-20
@@ -1,13 +1,14 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './NoCollection.css';
|
import styles from './NoMovieCollections.css';
|
||||||
|
|
||||||
function NoCollection(props) {
|
interface NoMovieCollectionsProps {
|
||||||
const { totalItems } = props;
|
totalItems: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoMovieCollections({ totalItems }: NoMovieCollectionsProps) {
|
||||||
if (totalItems > 0) {
|
if (totalItems > 0) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -20,24 +21,16 @@ function NoCollection(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>{translate('NoCollections')}</div>
|
||||||
{translate('NoCollections')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<Button
|
<Button to="/add/import" kind={kinds.PRIMARY}>
|
||||||
to="/add/import"
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
>
|
|
||||||
{translate('ImportExistingMovies')}
|
{translate('ImportExistingMovies')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<Button
|
<Button to="/add/new" kind={kinds.PRIMARY}>
|
||||||
to="/add/new"
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
>
|
|
||||||
{translate('AddNewMovie')}
|
{translate('AddNewMovie')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,8 +38,4 @@ function NoCollection(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
NoCollection.propTypes = {
|
export default NoMovieCollections;
|
||||||
totalItems: PropTypes.number.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NoCollection;
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import AddNewMovieCollectionMovieModal from 'Collection/AddNewMovieCollectionMovieModal';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import EditMovieModal from 'Movie/Edit/EditMovieModal';
|
import EditMovieModal from 'Movie/Edit/EditMovieModal';
|
||||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AddNewCollectionMovieModal from './../AddNewCollectionMovieModal';
|
|
||||||
import styles from './CollectionMovie.css';
|
import styles from './CollectionMovie.css';
|
||||||
|
|
||||||
class CollectionMovie extends Component {
|
class CollectionMovie extends Component {
|
||||||
@@ -160,7 +160,7 @@ class CollectionMovie extends Component {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddNewCollectionMovieModal
|
<AddNewMovieCollectionMovieModal
|
||||||
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
|
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
|
||||||
tmdbId={tmdbId}
|
tmdbId={tmdbId}
|
||||||
title={title}
|
title={title}
|
||||||
@@ -188,7 +188,7 @@ CollectionMovie.propTypes = {
|
|||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string.isRequired,
|
overview: PropTypes.string,
|
||||||
monitored: PropTypes.bool,
|
monitored: PropTypes.bool,
|
||||||
collectionId: PropTypes.number.isRequired,
|
collectionId: PropTypes.number.isRequired,
|
||||||
hasFile: PropTypes.bool,
|
hasFile: PropTypes.bool,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import TextTruncate from 'react-text-truncate';
|
import TextTruncate from 'react-text-truncate';
|
||||||
import { Navigation } from 'swiper';
|
import { Navigation } from 'swiper';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import EditCollectionModalConnector from 'Collection/Edit/EditCollectionModalConnector';
|
import EditMovieCollectionModal from 'Collection/Edit/EditMovieCollectionModal';
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
@@ -311,7 +311,7 @@ class CollectionOverview extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditCollectionModalConnector
|
<EditMovieCollectionModal
|
||||||
isOpen={isEditCollectionModalOpen}
|
isOpen={isEditCollectionModalOpen}
|
||||||
collectionId={id}
|
collectionId={id}
|
||||||
onModalClose={this.onEditCollectionModalClose}
|
onModalClose={this.onEditCollectionModalClose}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
export function createMovieCollectionSelector(collectionId?: number) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.movieCollections.itemMap,
|
||||||
|
(state: AppState) => state.movieCollections.items,
|
||||||
|
(itemMap, allMovieCollections) => {
|
||||||
|
return collectionId
|
||||||
|
? allMovieCollections[itemMap[collectionId]]
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMovieCollection(collectionId: number | undefined) {
|
||||||
|
return useSelector(createMovieCollectionSelector(collectionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMovieCollection;
|
||||||
@@ -4,6 +4,7 @@ import React, { Component, ErrorInfo } from 'react';
|
|||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
errorComponent: React.ElementType;
|
errorComponent: React.ElementType;
|
||||||
|
onModalClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorBoundaryState {
|
interface ErrorBoundaryState {
|
||||||
@@ -32,11 +33,17 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, errorComponent: ErrorComponent } = this.props;
|
const {
|
||||||
|
children,
|
||||||
|
errorComponent: ErrorComponent,
|
||||||
|
onModalClose,
|
||||||
|
} = this.props;
|
||||||
const { error, info } = this.state;
|
const { error, info } = this.state;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorComponent error={error} info={info} />;
|
return (
|
||||||
|
<ErrorComponent error={error} info={info} onModalClose={onModalClose} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import StackTrace from 'stacktrace-js';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ErrorBoundaryError.css';
|
import styles from './ErrorBoundaryError.css';
|
||||||
|
|
||||||
interface ErrorBoundaryErrorProps {
|
export interface ErrorBoundaryErrorProps {
|
||||||
className: string;
|
className: string;
|
||||||
messageClassName: string;
|
messageClassName: string;
|
||||||
detailsClassName: string;
|
detailsClassName: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import PathInput from 'Components/Form/PathInput';
|
import { PathInputInternal } from 'Components/Form/PathInput';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
@@ -151,7 +151,7 @@ function FileBrowserModalContent(props: FileBrowserModalContentProps) {
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<PathInput
|
<PathInputInternal
|
||||||
className={styles.pathInput}
|
className={styles.pathInput}
|
||||||
placeholder={translate('FileBrowserPlaceholderText')}
|
placeholder={translate('FileBrowserPlaceholderText')}
|
||||||
hasFileBrowser={false}
|
hasFileBrowser={false}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import TagInput from 'Components/Form/TagInput';
|
import TagInput from 'Components/Form/Tag/TagInput';
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, kinds } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, kinds } from 'Helpers/Props';
|
||||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||||
import convertToBytes from 'Utilities/Number/convertToBytes';
|
import convertToBytes from 'Utilities/Number/convertToBytes';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TagInputTag from 'Components/Form/TagInputTag';
|
import TagInputTag from 'Components/Form/Tag/TagInputTag';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './FilterBuilderRowValueTag.css';
|
import styles from './FilterBuilderRowValueTag.css';
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import jdu from 'jdu';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import AutoSuggestInput from './AutoSuggestInput';
|
|
||||||
|
|
||||||
class AutoCompleteInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
suggestions: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
getSuggestionValue(item) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSuggestion(item) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = (event, { newValue }) => {
|
|
||||||
this.props.onChange({
|
|
||||||
name: this.props.name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputBlur = () => {
|
|
||||||
this.setState({ suggestions: [] });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionsFetchRequested = ({ value }) => {
|
|
||||||
const { values } = this.props;
|
|
||||||
const lowerCaseValue = jdu.replace(value).toLowerCase();
|
|
||||||
|
|
||||||
const filteredValues = values.filter((v) => {
|
|
||||||
return jdu.replace(v).toLowerCase().contains(lowerCaseValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({ suggestions: filteredValues });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionsClearRequested = () => {
|
|
||||||
this.setState({ suggestions: [] });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { suggestions } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AutoSuggestInput
|
|
||||||
{...otherProps}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
suggestions={suggestions}
|
|
||||||
getSuggestionValue={this.getSuggestionValue}
|
|
||||||
renderSuggestion={this.renderSuggestion}
|
|
||||||
onInputBlur={this.onInputBlur}
|
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AutoCompleteInput.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoCompleteInput.defaultProps = {
|
|
||||||
value: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AutoCompleteInput;
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import jdu from 'jdu';
|
||||||
|
import React, { SyntheticEvent, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
SuggestionsFetchRequestedParams,
|
||||||
|
} from 'react-autosuggest';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import AutoSuggestInput from './AutoSuggestInput';
|
||||||
|
|
||||||
|
export interface AutoCompleteInputProps {
|
||||||
|
name: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
value?: string;
|
||||||
|
values: string[];
|
||||||
|
onChange: (change: InputChanged<string>) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AutoCompleteInput({
|
||||||
|
name,
|
||||||
|
value = '',
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
...otherProps
|
||||||
|
}: AutoCompleteInputProps) {
|
||||||
|
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const getSuggestionValue = useCallback((item: string) => {
|
||||||
|
return item;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderSuggestion = useCallback((item: string) => {
|
||||||
|
return item;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(_event: SyntheticEvent, { newValue }: ChangeEvent) => {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[name, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputBlur = useCallback(() => {
|
||||||
|
setSuggestions([]);
|
||||||
|
}, [setSuggestions]);
|
||||||
|
|
||||||
|
const handleSuggestionsFetchRequested = useCallback(
|
||||||
|
({ value: newValue }: SuggestionsFetchRequestedParams) => {
|
||||||
|
const lowerCaseValue = jdu.replace(newValue).toLowerCase();
|
||||||
|
|
||||||
|
const filteredValues = values.filter((v) => {
|
||||||
|
return jdu.replace(v).toLowerCase().includes(lowerCaseValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
setSuggestions(filteredValues);
|
||||||
|
},
|
||||||
|
[values, setSuggestions]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSuggestionsClearRequested = useCallback(() => {
|
||||||
|
setSuggestions([]);
|
||||||
|
}, [setSuggestions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoSuggestInput
|
||||||
|
{...otherProps}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
suggestions={suggestions}
|
||||||
|
getSuggestionValue={getSuggestionValue}
|
||||||
|
renderSuggestion={renderSuggestion}
|
||||||
|
onInputChange={handleInputChange}
|
||||||
|
onInputBlur={handleInputBlur}
|
||||||
|
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={handleSuggestionsClearRequested}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoCompleteInput;
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Autosuggest from 'react-autosuggest';
|
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
|
||||||
import Portal from 'Components/Portal';
|
|
||||||
import styles from './AutoSuggestInput.css';
|
|
||||||
|
|
||||||
class AutoSuggestInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._scheduleUpdate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (
|
|
||||||
this._scheduleUpdate &&
|
|
||||||
prevProps.suggestions !== this.props.suggestions
|
|
||||||
) {
|
|
||||||
this._scheduleUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
renderInputComponent = (inputProps) => {
|
|
||||||
const { renderInputComponent } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Reference>
|
|
||||||
{({ ref }) => {
|
|
||||||
if (renderInputComponent) {
|
|
||||||
return renderInputComponent(inputProps, ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref}>
|
|
||||||
<input
|
|
||||||
{...inputProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Reference>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderSuggestionsContainer = ({ containerProps, children }) => {
|
|
||||||
return (
|
|
||||||
<Portal>
|
|
||||||
<Popper
|
|
||||||
placement='bottom-start'
|
|
||||||
modifiers={{
|
|
||||||
computeMaxHeight: {
|
|
||||||
order: 851,
|
|
||||||
enabled: true,
|
|
||||||
fn: this.onComputeMaxHeight
|
|
||||||
},
|
|
||||||
flip: {
|
|
||||||
padding: this.props.minHeight
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ ref: popperRef, style, scheduleUpdate }) => {
|
|
||||||
this._scheduleUpdate = scheduleUpdate;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={popperRef}
|
|
||||||
style={style}
|
|
||||||
className={children ? styles.suggestionsContainerOpen : undefined}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
{...containerProps}
|
|
||||||
style={{
|
|
||||||
maxHeight: style.maxHeight
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Popper>
|
|
||||||
</Portal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
|
||||||
const {
|
|
||||||
top,
|
|
||||||
bottom,
|
|
||||||
width
|
|
||||||
} = data.offsets.reference;
|
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
|
||||||
|
|
||||||
if ((/^botton/).test(data.placement)) {
|
|
||||||
data.styles.maxHeight = windowHeight - bottom;
|
|
||||||
} else {
|
|
||||||
data.styles.maxHeight = top;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.styles.width = width;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputChange = (event, { newValue }) => {
|
|
||||||
this.props.onChange({
|
|
||||||
name: this.props.name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputKeyDown = (event) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
suggestions,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
event.key === 'Tab' &&
|
|
||||||
suggestions.length &&
|
|
||||||
suggestions[0] !== this.props.value
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: suggestions[0]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
forwardedRef,
|
|
||||||
className,
|
|
||||||
inputContainerClassName,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
placeholder,
|
|
||||||
suggestions,
|
|
||||||
hasError,
|
|
||||||
hasWarning,
|
|
||||||
getSuggestionValue,
|
|
||||||
renderSuggestion,
|
|
||||||
onInputChange,
|
|
||||||
onInputKeyDown,
|
|
||||||
onInputFocus,
|
|
||||||
onInputBlur,
|
|
||||||
onSuggestionsFetchRequested,
|
|
||||||
onSuggestionsClearRequested,
|
|
||||||
onSuggestionSelected,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const inputProps = {
|
|
||||||
className: classNames(
|
|
||||||
className,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning
|
|
||||||
),
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
placeholder,
|
|
||||||
autoComplete: 'off',
|
|
||||||
spellCheck: false,
|
|
||||||
onChange: onInputChange || this.onInputChange,
|
|
||||||
onKeyDown: onInputKeyDown || this.onInputKeyDown,
|
|
||||||
onFocus: onInputFocus,
|
|
||||||
onBlur: onInputBlur
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = {
|
|
||||||
container: inputContainerClassName,
|
|
||||||
containerOpen: styles.suggestionsContainerOpen,
|
|
||||||
suggestionsContainer: styles.suggestionsContainer,
|
|
||||||
suggestionsList: styles.suggestionsList,
|
|
||||||
suggestion: styles.suggestion,
|
|
||||||
suggestionHighlighted: styles.suggestionHighlighted
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Manager>
|
|
||||||
<Autosuggest
|
|
||||||
{...otherProps}
|
|
||||||
ref={forwardedRef}
|
|
||||||
id={name}
|
|
||||||
inputProps={inputProps}
|
|
||||||
theme={theme}
|
|
||||||
suggestions={suggestions}
|
|
||||||
getSuggestionValue={getSuggestionValue}
|
|
||||||
renderInputComponent={this.renderInputComponent}
|
|
||||||
renderSuggestionsContainer={this.renderSuggestionsContainer}
|
|
||||||
renderSuggestion={renderSuggestion}
|
|
||||||
onSuggestionSelected={onSuggestionSelected}
|
|
||||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
|
||||||
/>
|
|
||||||
</Manager>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AutoSuggestInput.propTypes = {
|
|
||||||
forwardedRef: PropTypes.func,
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
inputContainerClassName: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
suggestions: PropTypes.array.isRequired,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
hasWarning: PropTypes.bool,
|
|
||||||
enforceMaxHeight: PropTypes.bool.isRequired,
|
|
||||||
minHeight: PropTypes.number.isRequired,
|
|
||||||
maxHeight: PropTypes.number.isRequired,
|
|
||||||
getSuggestionValue: PropTypes.func.isRequired,
|
|
||||||
renderInputComponent: PropTypes.elementType,
|
|
||||||
renderSuggestion: PropTypes.func.isRequired,
|
|
||||||
onInputChange: PropTypes.func,
|
|
||||||
onInputKeyDown: PropTypes.func,
|
|
||||||
onInputFocus: PropTypes.func,
|
|
||||||
onInputBlur: PropTypes.func.isRequired,
|
|
||||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
|
||||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
|
||||||
onSuggestionSelected: PropTypes.func,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoSuggestInput.defaultProps = {
|
|
||||||
className: styles.input,
|
|
||||||
inputContainerClassName: styles.inputContainer,
|
|
||||||
enforceMaxHeight: true,
|
|
||||||
minHeight: 50,
|
|
||||||
maxHeight: 200
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AutoSuggestInput;
|
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {
|
||||||
|
FocusEvent,
|
||||||
|
FormEvent,
|
||||||
|
KeyboardEvent,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
MutableRefObject,
|
||||||
|
ReactNode,
|
||||||
|
Ref,
|
||||||
|
SyntheticEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import Autosuggest, {
|
||||||
|
AutosuggestPropsBase,
|
||||||
|
BlurEvent,
|
||||||
|
ChangeEvent,
|
||||||
|
RenderInputComponentProps,
|
||||||
|
RenderSuggestionsContainerParams,
|
||||||
|
} from 'react-autosuggest';
|
||||||
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
|
import Portal from 'Components/Portal';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import styles from './AutoSuggestInput.css';
|
||||||
|
|
||||||
|
interface AutoSuggestInputProps<T>
|
||||||
|
extends Omit<AutosuggestPropsBase<T>, 'renderInputComponent' | 'inputProps'> {
|
||||||
|
forwardedRef?: MutableRefObject<Autosuggest<T> | null>;
|
||||||
|
className?: string;
|
||||||
|
inputContainerClassName?: string;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
suggestions: T[];
|
||||||
|
hasError?: boolean;
|
||||||
|
hasWarning?: boolean;
|
||||||
|
enforceMaxHeight?: boolean;
|
||||||
|
minHeight?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
renderInputComponent?: (
|
||||||
|
inputProps: RenderInputComponentProps,
|
||||||
|
ref: Ref<HTMLDivElement>
|
||||||
|
) => ReactNode;
|
||||||
|
onInputChange: (
|
||||||
|
event: FormEvent<HTMLElement>,
|
||||||
|
params: ChangeEvent
|
||||||
|
) => unknown;
|
||||||
|
onInputKeyDown?: KeyboardEventHandler<HTMLElement>;
|
||||||
|
onInputFocus?: (event: SyntheticEvent) => unknown;
|
||||||
|
onInputBlur: (
|
||||||
|
event: FocusEvent<HTMLElement>,
|
||||||
|
params?: BlurEvent<T>
|
||||||
|
) => unknown;
|
||||||
|
onChange?: (change: InputChanged<T>) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function AutoSuggestInput<T = any>(props: AutoSuggestInputProps<T>) {
|
||||||
|
const {
|
||||||
|
// TODO: forwaredRef should be replaces with React.forwardRef
|
||||||
|
forwardedRef,
|
||||||
|
className = styles.input,
|
||||||
|
inputContainerClassName = styles.inputContainer,
|
||||||
|
name,
|
||||||
|
value = '',
|
||||||
|
placeholder,
|
||||||
|
suggestions,
|
||||||
|
enforceMaxHeight = true,
|
||||||
|
hasError,
|
||||||
|
hasWarning,
|
||||||
|
minHeight = 50,
|
||||||
|
maxHeight = 200,
|
||||||
|
getSuggestionValue,
|
||||||
|
renderSuggestion,
|
||||||
|
renderInputComponent,
|
||||||
|
onInputChange,
|
||||||
|
onInputKeyDown,
|
||||||
|
onInputFocus,
|
||||||
|
onInputBlur,
|
||||||
|
onSuggestionsFetchRequested,
|
||||||
|
onSuggestionsClearRequested,
|
||||||
|
onSuggestionSelected,
|
||||||
|
onChange,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const updater = useRef<(() => void) | null>(null);
|
||||||
|
const previousSuggestions = usePrevious(suggestions);
|
||||||
|
|
||||||
|
const handleComputeMaxHeight = useCallback(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(data: any) => {
|
||||||
|
const { top, bottom, width } = data.offsets.reference;
|
||||||
|
|
||||||
|
if (enforceMaxHeight) {
|
||||||
|
data.styles.maxHeight = maxHeight;
|
||||||
|
} else {
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
if (/^botton/.test(data.placement)) {
|
||||||
|
data.styles.maxHeight = windowHeight - bottom;
|
||||||
|
} else {
|
||||||
|
data.styles.maxHeight = top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.styles.width = width;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
[enforceMaxHeight, maxHeight]
|
||||||
|
);
|
||||||
|
|
||||||
|
const createRenderInputComponent = useCallback(
|
||||||
|
(inputProps: RenderInputComponentProps) => {
|
||||||
|
return (
|
||||||
|
<Reference>
|
||||||
|
{({ ref }) => {
|
||||||
|
if (renderInputComponent) {
|
||||||
|
return renderInputComponent(inputProps, ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<input {...inputProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Reference>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[renderInputComponent]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSuggestionsContainer = useCallback(
|
||||||
|
({ containerProps, children }: RenderSuggestionsContainerParams) => {
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Popper
|
||||||
|
placement="bottom-start"
|
||||||
|
modifiers={{
|
||||||
|
computeMaxHeight: {
|
||||||
|
order: 851,
|
||||||
|
enabled: true,
|
||||||
|
fn: handleComputeMaxHeight,
|
||||||
|
},
|
||||||
|
flip: {
|
||||||
|
padding: minHeight,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ ref: popperRef, style, scheduleUpdate }) => {
|
||||||
|
updater.current = scheduleUpdate;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={popperRef}
|
||||||
|
style={style}
|
||||||
|
className={
|
||||||
|
children ? styles.suggestionsContainerOpen : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...containerProps}
|
||||||
|
style={{
|
||||||
|
maxHeight: style.maxHeight,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Popper>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[minHeight, handleComputeMaxHeight]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (
|
||||||
|
event.key === 'Tab' &&
|
||||||
|
suggestions.length &&
|
||||||
|
suggestions[0] !== value
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
onSuggestionSelected?.(event, {
|
||||||
|
suggestion: suggestions[0],
|
||||||
|
suggestionValue: value,
|
||||||
|
suggestionIndex: 0,
|
||||||
|
sectionIndex: null,
|
||||||
|
method: 'enter',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[value, suggestions, onSuggestionSelected]
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputProps = {
|
||||||
|
className: classNames(
|
||||||
|
className,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
|
),
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
autoComplete: 'off',
|
||||||
|
spellCheck: false,
|
||||||
|
onChange: onInputChange,
|
||||||
|
onKeyDown: onInputKeyDown || handleInputKeyDown,
|
||||||
|
onFocus: onInputFocus,
|
||||||
|
onBlur: onInputBlur,
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
container: inputContainerClassName,
|
||||||
|
containerOpen: styles.suggestionsContainerOpen,
|
||||||
|
suggestionsContainer: styles.suggestionsContainer,
|
||||||
|
suggestionsList: styles.suggestionsList,
|
||||||
|
suggestion: styles.suggestion,
|
||||||
|
suggestionHighlighted: styles.suggestionHighlighted,
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updater.current && suggestions !== previousSuggestions) {
|
||||||
|
updater.current();
|
||||||
|
}
|
||||||
|
}, [suggestions, previousSuggestions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Manager>
|
||||||
|
<Autosuggest
|
||||||
|
{...otherProps}
|
||||||
|
ref={forwardedRef}
|
||||||
|
id={name}
|
||||||
|
inputProps={inputProps}
|
||||||
|
theme={theme}
|
||||||
|
suggestions={suggestions}
|
||||||
|
getSuggestionValue={getSuggestionValue}
|
||||||
|
renderInputComponent={createRenderInputComponent}
|
||||||
|
renderSuggestionsContainer={renderSuggestionsContainer}
|
||||||
|
renderSuggestion={renderSuggestion}
|
||||||
|
onSuggestionSelected={onSuggestionSelected}
|
||||||
|
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||||
|
/>
|
||||||
|
</Manager>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoSuggestInput;
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import FormInputButton from './FormInputButton';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
import styles from './CaptchaInput.css';
|
|
||||||
|
|
||||||
function CaptchaInput(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
hasError,
|
|
||||||
hasWarning,
|
|
||||||
refreshing,
|
|
||||||
siteKey,
|
|
||||||
secretToken,
|
|
||||||
onChange,
|
|
||||||
onRefreshPress,
|
|
||||||
onCaptchaChange
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.captchaInputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles.hasButton,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning
|
|
||||||
)}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormInputButton
|
|
||||||
onPress={onRefreshPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={icons.REFRESH}
|
|
||||||
isSpinning={refreshing}
|
|
||||||
/>
|
|
||||||
</FormInputButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
!!siteKey && !!secretToken &&
|
|
||||||
<div className={styles.recaptchaWrapper}>
|
|
||||||
<ReCAPTCHA
|
|
||||||
sitekey={siteKey}
|
|
||||||
stoken={secretToken}
|
|
||||||
onChange={onCaptchaChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CaptchaInput.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
hasWarning: PropTypes.bool,
|
|
||||||
refreshing: PropTypes.bool.isRequired,
|
|
||||||
siteKey: PropTypes.string,
|
|
||||||
secretToken: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onRefreshPress: PropTypes.func.isRequired,
|
|
||||||
onCaptchaChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
CaptchaInput.defaultProps = {
|
|
||||||
className: styles.input,
|
|
||||||
value: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CaptchaInput;
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import ReCAPTCHA from 'react-google-recaptcha';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
getCaptchaCookie,
|
||||||
|
refreshCaptcha,
|
||||||
|
resetCaptcha,
|
||||||
|
} from 'Store/Actions/captchaActions';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import FormInputButton from './FormInputButton';
|
||||||
|
import TextInput from './TextInput';
|
||||||
|
import styles from './CaptchaInput.css';
|
||||||
|
|
||||||
|
export interface CaptchaInputProps {
|
||||||
|
className?: string;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
provider: string;
|
||||||
|
providerData: object;
|
||||||
|
hasError?: boolean;
|
||||||
|
hasWarning?: boolean;
|
||||||
|
refreshing: boolean;
|
||||||
|
siteKey?: string;
|
||||||
|
secretToken?: string;
|
||||||
|
onChange: (change: InputChanged<string>) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CaptchaInput({
|
||||||
|
className = styles.input,
|
||||||
|
name,
|
||||||
|
value = '',
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
hasError,
|
||||||
|
hasWarning,
|
||||||
|
refreshing,
|
||||||
|
siteKey,
|
||||||
|
secretToken,
|
||||||
|
onChange,
|
||||||
|
}: CaptchaInputProps) {
|
||||||
|
const { token } = useSelector((state: AppState) => state.captcha);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const previousToken = usePrevious(token);
|
||||||
|
|
||||||
|
const handleCaptchaChange = useCallback(
|
||||||
|
(token: string | null) => {
|
||||||
|
// If the captcha has expired `captchaResponse` will be null.
|
||||||
|
// In the event it's null don't try to get the captchaCookie.
|
||||||
|
// TODO: Should we clear the cookie? or reset the captcha?
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
getCaptchaCookie({
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
captchaResponse: token,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[provider, providerData, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRefreshPress = useCallback(() => {
|
||||||
|
dispatch(refreshCaptcha({ provider, providerData }));
|
||||||
|
}, [provider, providerData, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token && token !== previousToken) {
|
||||||
|
onChange({ name, value: token });
|
||||||
|
}
|
||||||
|
}, [name, token, previousToken, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(resetCaptcha());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.captchaInputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
styles.hasButton,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
|
)}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputButton onPress={handleRefreshPress}>
|
||||||
|
<Icon name={icons.REFRESH} isSpinning={refreshing} />
|
||||||
|
</FormInputButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{siteKey && secretToken ? (
|
||||||
|
<div className={styles.recaptchaWrapper}>
|
||||||
|
<ReCAPTCHA
|
||||||
|
sitekey={siteKey}
|
||||||
|
stoken={secretToken}
|
||||||
|
onChange={handleCaptchaChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CaptchaInput;
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { getCaptchaCookie, refreshCaptcha, resetCaptcha } from 'Store/Actions/captchaActions';
|
|
||||||
import CaptchaInput from './CaptchaInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.captcha,
|
|
||||||
(captcha) => {
|
|
||||||
return captcha;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
refreshCaptcha,
|
|
||||||
getCaptchaCookie,
|
|
||||||
resetCaptcha
|
|
||||||
};
|
|
||||||
|
|
||||||
class CaptchaInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
token,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (token && token !== prevProps.token) {
|
|
||||||
onChange({ name, value: token });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
this.props.resetCaptcha();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRefreshPress = () => {
|
|
||||||
const {
|
|
||||||
provider,
|
|
||||||
providerData
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.refreshCaptcha({ provider, providerData });
|
|
||||||
};
|
|
||||||
|
|
||||||
onCaptchaChange = (captchaResponse) => {
|
|
||||||
// If the captcha has expired `captchaResponse` will be null.
|
|
||||||
// In the event it's null don't try to get the captchaCookie.
|
|
||||||
// TODO: Should we clear the cookie? or reset the captcha?
|
|
||||||
|
|
||||||
if (!captchaResponse) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
provider,
|
|
||||||
providerData
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.getCaptchaCookie({ provider, providerData, captchaResponse });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<CaptchaInput
|
|
||||||
{...this.props}
|
|
||||||
onRefreshPress={this.onRefreshPress}
|
|
||||||
onCaptchaChange={this.onCaptchaChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CaptchaInputConnector.propTypes = {
|
|
||||||
provider: PropTypes.string.isRequired,
|
|
||||||
providerData: PropTypes.object.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
token: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
refreshCaptcha: PropTypes.func.isRequired,
|
|
||||||
getCaptchaCookie: PropTypes.func.isRequired,
|
|
||||||
resetCaptcha: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(CaptchaInputConnector);
|
|
||||||
@@ -41,10 +41,11 @@
|
|||||||
.checkbox:focus + .input {
|
.checkbox:focus + .input {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
border-color: var(--inputFocusBorderColor);
|
border-color: var(--inputFocusBorderColor);
|
||||||
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), 0 0 8px var(--inputFocusBoxShadowColor);
|
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor),
|
||||||
|
0 0 8px var(--inputFocusBoxShadowColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dangerIsChecked {
|
.danger {
|
||||||
border-color: var(--dangerColor);
|
border-color: var(--dangerColor);
|
||||||
background-color: var(--dangerColor);
|
background-color: var(--dangerColor);
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.primaryIsChecked {
|
.primary {
|
||||||
border-color: var(--primaryColor);
|
border-color: var(--primaryColor);
|
||||||
background-color: var(--primaryColor);
|
background-color: var(--primaryColor);
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.successIsChecked {
|
.success {
|
||||||
border-color: var(--successColor);
|
border-color: var(--successColor);
|
||||||
background-color: var(--successColor);
|
background-color: var(--successColor);
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.warningIsChecked {
|
.warning {
|
||||||
border-color: var(--warningColor);
|
border-color: var(--warningColor);
|
||||||
background-color: var(--warningColor);
|
background-color: var(--warningColor);
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -3,16 +3,16 @@
|
|||||||
interface CssExports {
|
interface CssExports {
|
||||||
'checkbox': string;
|
'checkbox': string;
|
||||||
'container': string;
|
'container': string;
|
||||||
'dangerIsChecked': string;
|
'danger': string;
|
||||||
'helpText': string;
|
'helpText': string;
|
||||||
'input': string;
|
'input': string;
|
||||||
'isDisabled': string;
|
'isDisabled': string;
|
||||||
'isIndeterminate': string;
|
'isIndeterminate': string;
|
||||||
'isNotChecked': string;
|
'isNotChecked': string;
|
||||||
'label': string;
|
'label': string;
|
||||||
'primaryIsChecked': string;
|
'primary': string;
|
||||||
'successIsChecked': string;
|
'success': string;
|
||||||
'warningIsChecked': string;
|
'warning': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
|
||||||
import styles from './CheckInput.css';
|
|
||||||
|
|
||||||
class CheckInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._checkbox = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setIndeterminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.setIndeterminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setIndeterminate() {
|
|
||||||
if (!this._checkbox) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
uncheckedValue,
|
|
||||||
checkedValue
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this._checkbox.indeterminate = value !== uncheckedValue && value !== checkedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleChecked = (checked, shiftKey) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
checkedValue,
|
|
||||||
uncheckedValue
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newValue = checked ? checkedValue : uncheckedValue;
|
|
||||||
|
|
||||||
if (value !== newValue) {
|
|
||||||
this.props.onChange({
|
|
||||||
name,
|
|
||||||
value: newValue,
|
|
||||||
shiftKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
setRef = (ref) => {
|
|
||||||
this._checkbox = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
onClick = (event) => {
|
|
||||||
if (this.props.isDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shiftKey = event.nativeEvent.shiftKey;
|
|
||||||
const checked = !this._checkbox.checked;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
this.toggleChecked(checked, shiftKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange = (event) => {
|
|
||||||
const checked = event.target.checked;
|
|
||||||
const shiftKey = event.nativeEvent.shiftKey;
|
|
||||||
|
|
||||||
this.toggleChecked(checked, shiftKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
containerClassName,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
checkedValue,
|
|
||||||
uncheckedValue,
|
|
||||||
helpText,
|
|
||||||
helpTextWarning,
|
|
||||||
isDisabled,
|
|
||||||
kind
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isChecked = value === checkedValue;
|
|
||||||
const isUnchecked = value === uncheckedValue;
|
|
||||||
const isIndeterminate = !isChecked && !isUnchecked;
|
|
||||||
const isCheckClass = `${kind}IsChecked`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={containerClassName}>
|
|
||||||
<label
|
|
||||||
className={styles.label}
|
|
||||||
onClick={this.onClick}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={this.setRef}
|
|
||||||
className={styles.checkbox}
|
|
||||||
type="checkbox"
|
|
||||||
name={name}
|
|
||||||
checked={isChecked}
|
|
||||||
disabled={isDisabled}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
isChecked ? styles[isCheckClass] : styles.isNotChecked,
|
|
||||||
isIndeterminate && styles.isIndeterminate,
|
|
||||||
isDisabled && styles.isDisabled
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isChecked &&
|
|
||||||
<Icon name={icons.CHECK} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isIndeterminate &&
|
|
||||||
<Icon name={icons.CHECK_INDETERMINATE} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
helpText &&
|
|
||||||
<FormInputHelpText
|
|
||||||
className={styles.helpText}
|
|
||||||
text={helpText}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!helpText && helpTextWarning &&
|
|
||||||
<FormInputHelpText
|
|
||||||
className={styles.helpText}
|
|
||||||
text={helpTextWarning}
|
|
||||||
isWarning={true}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckInput.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
containerClassName: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
checkedValue: PropTypes.bool,
|
|
||||||
uncheckedValue: PropTypes.bool,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
|
||||||
helpText: PropTypes.string,
|
|
||||||
helpTextWarning: PropTypes.string,
|
|
||||||
isDisabled: PropTypes.bool,
|
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
CheckInput.defaultProps = {
|
|
||||||
className: styles.input,
|
|
||||||
containerClassName: styles.container,
|
|
||||||
checkedValue: true,
|
|
||||||
uncheckedValue: false,
|
|
||||||
kind: kinds.PRIMARY
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CheckInput;
|
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { SyntheticEvent, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
|
import styles from './CheckInput.css';
|
||||||
|
|
||||||
|
interface ChangeEvent<T = Element> extends SyntheticEvent<T, MouseEvent> {
|
||||||
|
target: EventTarget & T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckInputProps {
|
||||||
|
className?: string;
|
||||||
|
containerClassName?: string;
|
||||||
|
name: string;
|
||||||
|
checkedValue?: boolean;
|
||||||
|
uncheckedValue?: boolean;
|
||||||
|
value?: string | boolean | null;
|
||||||
|
helpText?: string;
|
||||||
|
helpTextWarning?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
kind?: Extract<Kind, keyof typeof styles>;
|
||||||
|
onChange: (changes: CheckInputChanged) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckInput(props: CheckInputProps) {
|
||||||
|
const {
|
||||||
|
className = styles.input,
|
||||||
|
containerClassName = styles.container,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
checkedValue = true,
|
||||||
|
uncheckedValue = false,
|
||||||
|
helpText,
|
||||||
|
helpTextWarning,
|
||||||
|
isDisabled,
|
||||||
|
kind = 'primary',
|
||||||
|
onChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const isChecked = value === checkedValue;
|
||||||
|
const isUnchecked = value === uncheckedValue;
|
||||||
|
const isIndeterminate = !isChecked && !isUnchecked;
|
||||||
|
|
||||||
|
const toggleChecked = useCallback(
|
||||||
|
(checked: boolean, shiftKey: boolean) => {
|
||||||
|
const newValue = checked ? checkedValue : uncheckedValue;
|
||||||
|
|
||||||
|
if (value !== newValue) {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[name, value, checkedValue, uncheckedValue, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(event: SyntheticEvent<HTMLElement, MouseEvent>) => {
|
||||||
|
if (isDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shiftKey = event.nativeEvent.shiftKey;
|
||||||
|
const checked = !(inputRef.current?.checked ?? false);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
toggleChecked(checked, shiftKey);
|
||||||
|
},
|
||||||
|
[isDisabled, toggleChecked]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
const shiftKey = event.nativeEvent.shiftKey;
|
||||||
|
|
||||||
|
toggleChecked(checked, shiftKey);
|
||||||
|
},
|
||||||
|
[toggleChecked]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inputRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRef.current.indeterminate =
|
||||||
|
value !== uncheckedValue && value !== checkedValue;
|
||||||
|
}, [value, uncheckedValue, checkedValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClassName}>
|
||||||
|
<label className={styles.label} onClick={handleClick}>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className={styles.checkbox}
|
||||||
|
type="checkbox"
|
||||||
|
name={name}
|
||||||
|
checked={isChecked}
|
||||||
|
disabled={isDisabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
isChecked ? styles[kind] : styles.isNotChecked,
|
||||||
|
isIndeterminate && styles.isIndeterminate,
|
||||||
|
isDisabled && styles.isDisabled
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isChecked ? <Icon name={icons.CHECK} /> : null}
|
||||||
|
|
||||||
|
{isIndeterminate ? <Icon name={icons.CHECK_INDETERMINATE} /> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{helpText ? (
|
||||||
|
<FormInputHelpText className={styles.helpText} text={helpText} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!helpText && helpTextWarning ? (
|
||||||
|
<FormInputHelpText
|
||||||
|
className={styles.helpText}
|
||||||
|
text={helpTextWarning}
|
||||||
|
isWarning={true}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CheckInput;
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
|
||||||
import FormInputButton from './FormInputButton';
|
|
||||||
import TagInput from './TagInput';
|
|
||||||
import styles from './DeviceInput.css';
|
|
||||||
|
|
||||||
class DeviceInput extends Component {
|
|
||||||
|
|
||||||
onTagAdd = (device) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// New tags won't have an ID, only a name.
|
|
||||||
const deviceId = device.id || device.name;
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: [...value, deviceId]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onTagDelete = ({ index }) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newValue = value.slice();
|
|
||||||
newValue.splice(index, 1);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
name,
|
|
||||||
items,
|
|
||||||
selectedDevices,
|
|
||||||
hasError,
|
|
||||||
hasWarning,
|
|
||||||
isFetching,
|
|
||||||
onRefreshPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<TagInput
|
|
||||||
inputContainerClassName={styles.input}
|
|
||||||
name={name}
|
|
||||||
tags={selectedDevices}
|
|
||||||
tagList={items}
|
|
||||||
allowNew={true}
|
|
||||||
minQueryLength={0}
|
|
||||||
hasError={hasError}
|
|
||||||
hasWarning={hasWarning}
|
|
||||||
onTagAdd={this.onTagAdd}
|
|
||||||
onTagDelete={this.onTagDelete}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormInputButton
|
|
||||||
onPress={onRefreshPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={icons.REFRESH}
|
|
||||||
isSpinning={isFetching}
|
|
||||||
/>
|
|
||||||
</FormInputButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceInput.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
|
||||||
selectedDevices: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
hasWarning: PropTypes.bool,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onRefreshPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
DeviceInput.defaultProps = {
|
|
||||||
className: styles.deviceInputWrapper,
|
|
||||||
inputClassName: styles.input
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeviceInput;
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
|
|
||||||
import DeviceInput from './DeviceInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { value }) => value,
|
|
||||||
(state) => state.providerOptions.devices || defaultState,
|
|
||||||
(value, devices) => {
|
|
||||||
|
|
||||||
return {
|
|
||||||
...devices,
|
|
||||||
selectedDevices: value.map((valueDevice) => {
|
|
||||||
// Disable equality ESLint rule so we don't need to worry about
|
|
||||||
// a type mismatch between the value items and the device ID.
|
|
||||||
// eslint-disable-next-line eqeqeq
|
|
||||||
const device = devices.items.find((d) => d.id == valueDevice);
|
|
||||||
|
|
||||||
if (device) {
|
|
||||||
return {
|
|
||||||
id: device.id,
|
|
||||||
name: `${device.name} (${device.id})`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: valueDevice,
|
|
||||||
name: `Unknown (${valueDevice})`
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchOptions: fetchOptions,
|
|
||||||
dispatchClearOptions: clearOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
class DeviceInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
this._populate();
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
this.props.dispatchClearOptions({ section: 'devices' });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_populate() {
|
|
||||||
const {
|
|
||||||
provider,
|
|
||||||
providerData,
|
|
||||||
dispatchFetchOptions
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchFetchOptions({
|
|
||||||
section: 'devices',
|
|
||||||
action: 'getDevices',
|
|
||||||
provider,
|
|
||||||
providerData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRefreshPress = () => {
|
|
||||||
this._populate();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<DeviceInput
|
|
||||||
{...this.props}
|
|
||||||
onRefreshPress={this.onRefreshPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceInputConnector.propTypes = {
|
|
||||||
provider: PropTypes.string.isRequired,
|
|
||||||
providerData: PropTypes.object.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchOptions: PropTypes.func.isRequired,
|
|
||||||
dispatchClearOptions: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector);
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.downloadClients,
|
|
||||||
(state, { includeAny }) => includeAny,
|
|
||||||
(state, { protocol }) => protocol,
|
|
||||||
(downloadClients, includeAny, protocolFilter) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = downloadClients;
|
|
||||||
|
|
||||||
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
|
||||||
|
|
||||||
const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => {
|
|
||||||
return {
|
|
||||||
key: downloadClient.id,
|
|
||||||
value: downloadClient.name,
|
|
||||||
hint: `(${downloadClient.id})`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (includeAny) {
|
|
||||||
values.unshift({
|
|
||||||
key: 0,
|
|
||||||
value: `(${translate('Any')})`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
values
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchDownloadClients: fetchDownloadClients
|
|
||||||
};
|
|
||||||
|
|
||||||
class DownloadClientSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (!this.props.isPopulated) {
|
|
||||||
this.props.dispatchFetchDownloadClients();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
this.props.onChange({ name, value: parseInt(value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadClientSelectInputConnector.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
includeAny: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchDownloadClients: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
DownloadClientSelectInputConnector.defaultProps = {
|
|
||||||
includeAny: false,
|
|
||||||
protocol: 'torrent'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector);
|
|
||||||
@@ -1,613 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import Portal from 'Components/Portal';
|
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import { icons, scrollDirections, sizes } from 'Helpers/Props';
|
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
|
||||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
|
||||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
import styles from './EnhancedSelectInput.css';
|
|
||||||
|
|
||||||
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
|
||||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedOption(selectedIndex, values) {
|
|
||||||
return values[selectedIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
function findIndex(startingIndex, direction, values) {
|
|
||||||
let indexToTest = startingIndex + direction;
|
|
||||||
|
|
||||||
while (indexToTest !== startingIndex) {
|
|
||||||
if (indexToTest < 0) {
|
|
||||||
indexToTest = values.length - 1;
|
|
||||||
} else if (indexToTest >= values.length) {
|
|
||||||
indexToTest = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getSelectedOption(indexToTest, values).isDisabled) {
|
|
||||||
indexToTest = indexToTest + direction;
|
|
||||||
} else {
|
|
||||||
return indexToTest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function previousIndex(selectedIndex, values) {
|
|
||||||
return findIndex(selectedIndex, -1, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextIndex(selectedIndex, values) {
|
|
||||||
return findIndex(selectedIndex, 1, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedIndex(props) {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
values
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return values.findIndex((v) => {
|
|
||||||
return value.size && v.key === value[0];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return values.findIndex((v) => {
|
|
||||||
return v.key === value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSelectedItem(index, props) {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
values
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.includes(values[index].key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return values[index].key === value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getKey(selectedIndex, values) {
|
|
||||||
return values[selectedIndex].key;
|
|
||||||
}
|
|
||||||
|
|
||||||
class EnhancedSelectInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._scheduleUpdate = null;
|
|
||||||
this._buttonId = getUniqueElememtId();
|
|
||||||
this._optionsId = getUniqueElememtId();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isOpen: false,
|
|
||||||
selectedIndex: getSelectedIndex(props),
|
|
||||||
width: 0,
|
|
||||||
isMobile: isMobileUtil()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this._scheduleUpdate) {
|
|
||||||
this._scheduleUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(this.props.value)) {
|
|
||||||
if (prevProps.value !== this.props.value || prevProps.values !== this.props.values) {
|
|
||||||
this.setState({
|
|
||||||
selectedIndex: getSelectedIndex(this.props)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_addListener() {
|
|
||||||
window.addEventListener('click', this.onWindowClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeListener() {
|
|
||||||
window.removeEventListener('click', this.onWindowClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
|
||||||
const windowHeight = window.innerHeight;
|
|
||||||
|
|
||||||
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
onWindowClick = (event) => {
|
|
||||||
const button = document.getElementById(this._buttonId);
|
|
||||||
const options = document.getElementById(this._optionsId);
|
|
||||||
|
|
||||||
if (!button || !event.target.isConnected || this.state.isMobile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!button.contains(event.target) &&
|
|
||||||
options &&
|
|
||||||
!options.contains(event.target) &&
|
|
||||||
this.state.isOpen
|
|
||||||
) {
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
this._removeListener();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onFocus = () => {
|
|
||||||
if (this.state.isOpen) {
|
|
||||||
this._removeListener();
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlur = () => {
|
|
||||||
if (!this.props.isEditable) {
|
|
||||||
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
|
||||||
const origIndex = getSelectedIndex(this.props);
|
|
||||||
|
|
||||||
if (origIndex !== this.state.selectedIndex) {
|
|
||||||
this.setState({ selectedIndex: origIndex });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onKeyDown = (event) => {
|
|
||||||
const {
|
|
||||||
values
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
selectedIndex
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const keyCode = event.keyCode;
|
|
||||||
const newState = {};
|
|
||||||
|
|
||||||
if (!isOpen) {
|
|
||||||
if (isArrowKey(keyCode)) {
|
|
||||||
event.preventDefault();
|
|
||||||
newState.isOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
selectedIndex == null || selectedIndex === -1 ||
|
|
||||||
getSelectedOption(selectedIndex, values).isDisabled
|
|
||||||
) {
|
|
||||||
if (keyCode === keyCodes.UP_ARROW) {
|
|
||||||
newState.selectedIndex = previousIndex(0, values);
|
|
||||||
} else if (keyCode === keyCodes.DOWN_ARROW) {
|
|
||||||
newState.selectedIndex = nextIndex(values.length - 1, values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(newState);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.UP_ARROW) {
|
|
||||||
event.preventDefault();
|
|
||||||
newState.selectedIndex = previousIndex(selectedIndex, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.DOWN_ARROW) {
|
|
||||||
event.preventDefault();
|
|
||||||
newState.selectedIndex = nextIndex(selectedIndex, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.ENTER) {
|
|
||||||
event.preventDefault();
|
|
||||||
newState.isOpen = false;
|
|
||||||
this.onSelect(getKey(selectedIndex, values));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.TAB) {
|
|
||||||
newState.isOpen = false;
|
|
||||||
this.onSelect(getKey(selectedIndex, values));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.ESCAPE) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
newState.isOpen = false;
|
|
||||||
newState.selectedIndex = getSelectedIndex(this.props);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEmpty(newState)) {
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
if (this.state.isOpen) {
|
|
||||||
this._removeListener();
|
|
||||||
} else {
|
|
||||||
this._addListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.isOpen && this.props.onOpen) {
|
|
||||||
this.props.onOpen();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelect = (newValue) => {
|
|
||||||
const { name, value, values, onChange } = this.props;
|
|
||||||
const additionalProperties = values.find((v) => v.key === newValue)?.additionalProperties;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
let arrayValue = null;
|
|
||||||
const index = value.indexOf(newValue);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
|
|
||||||
} else {
|
|
||||||
arrayValue = [...value];
|
|
||||||
arrayValue.splice(index, 1);
|
|
||||||
}
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: arrayValue,
|
|
||||||
additionalProperties
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue,
|
|
||||||
additionalProperties
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMeasure = ({ width }) => {
|
|
||||||
this.setState({ width });
|
|
||||||
};
|
|
||||||
|
|
||||||
onOptionsModalClose = () => {
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
disabledClassName,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
values,
|
|
||||||
isDisabled,
|
|
||||||
isEditable,
|
|
||||||
isFetching,
|
|
||||||
hasError,
|
|
||||||
hasWarning,
|
|
||||||
valueOptions,
|
|
||||||
selectedValueOptions,
|
|
||||||
selectedValueComponent: SelectedValueComponent,
|
|
||||||
optionComponent: OptionComponent,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
selectedIndex,
|
|
||||||
width,
|
|
||||||
isOpen,
|
|
||||||
isMobile
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isMultiSelect = Array.isArray(value);
|
|
||||||
const selectedOption = getSelectedOption(selectedIndex, values);
|
|
||||||
let selectedValue = value;
|
|
||||||
|
|
||||||
if (!values.length) {
|
|
||||||
selectedValue = isMultiSelect ? [] : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Manager>
|
|
||||||
<Reference>
|
|
||||||
{({ ref }) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
id={this._buttonId}
|
|
||||||
>
|
|
||||||
<Measure
|
|
||||||
whitelist={['width']}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isEditable ?
|
|
||||||
<div
|
|
||||||
className={styles.editableContainer}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
className={className}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
readOnly={isDisabled}
|
|
||||||
hasError={hasError}
|
|
||||||
hasWarning={hasWarning}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
styles.dropdownArrowContainerEditable,
|
|
||||||
isDisabled ?
|
|
||||||
styles.dropdownArrowContainerDisabled :
|
|
||||||
styles.dropdownArrowContainer)
|
|
||||||
}
|
|
||||||
onPress={this.onPress}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isFetching ?
|
|
||||||
<LoadingIndicator
|
|
||||||
className={styles.loading}
|
|
||||||
size={20}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isFetching ?
|
|
||||||
null :
|
|
||||||
<Icon
|
|
||||||
name={icons.CARET_DOWN}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</Link>
|
|
||||||
</div> :
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning,
|
|
||||||
isDisabled && disabledClassName
|
|
||||||
)}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
onKeyDown={this.onKeyDown}
|
|
||||||
onPress={this.onPress}
|
|
||||||
>
|
|
||||||
<SelectedValueComponent
|
|
||||||
value={selectedValue}
|
|
||||||
values={values}
|
|
||||||
{...selectedValueOptions}
|
|
||||||
{...selectedOption}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
isMultiSelect={isMultiSelect}
|
|
||||||
>
|
|
||||||
{selectedOption ? selectedOption.value : null}
|
|
||||||
</SelectedValueComponent>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={isDisabled ?
|
|
||||||
styles.dropdownArrowContainerDisabled :
|
|
||||||
styles.dropdownArrowContainer
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
{
|
|
||||||
isFetching ?
|
|
||||||
<LoadingIndicator
|
|
||||||
className={styles.loading}
|
|
||||||
size={20}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isFetching ?
|
|
||||||
null :
|
|
||||||
<Icon
|
|
||||||
name={icons.CARET_DOWN}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
</Measure>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Reference>
|
|
||||||
<Portal>
|
|
||||||
<Popper
|
|
||||||
placement="bottom-start"
|
|
||||||
modifiers={{
|
|
||||||
computeMaxHeight: {
|
|
||||||
order: 851,
|
|
||||||
enabled: true,
|
|
||||||
fn: this.onComputeMaxHeight
|
|
||||||
},
|
|
||||||
preventOverflow: {
|
|
||||||
enabled: true,
|
|
||||||
boundariesElement: 'viewport'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ ref, style, scheduleUpdate }) => {
|
|
||||||
this._scheduleUpdate = scheduleUpdate;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
id={this._optionsId}
|
|
||||||
className={styles.optionsContainer}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
minWidth: width
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isOpen && !isMobile ?
|
|
||||||
<Scroller
|
|
||||||
className={styles.options}
|
|
||||||
style={{
|
|
||||||
maxHeight: style.maxHeight
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
values.map((v, index) => {
|
|
||||||
const hasParent = v.parentKey !== undefined;
|
|
||||||
const depth = hasParent ? 1 : 0;
|
|
||||||
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
|
|
||||||
return (
|
|
||||||
<OptionComponent
|
|
||||||
key={v.key}
|
|
||||||
id={v.key}
|
|
||||||
dividerAfter={v.dividerAfter}
|
|
||||||
depth={depth}
|
|
||||||
isSelected={isSelectedItem(index, this.props)}
|
|
||||||
isDisabled={parentSelected}
|
|
||||||
isMultiSelect={isMultiSelect}
|
|
||||||
{...valueOptions}
|
|
||||||
{...v}
|
|
||||||
isMobile={false}
|
|
||||||
onSelect={this.onSelect}
|
|
||||||
>
|
|
||||||
{v.value}
|
|
||||||
</OptionComponent>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Scroller> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</Popper>
|
|
||||||
</Portal>
|
|
||||||
</Manager>
|
|
||||||
|
|
||||||
{
|
|
||||||
isMobile ?
|
|
||||||
<Modal
|
|
||||||
className={styles.optionsModal}
|
|
||||||
size={sizes.EXTRA_SMALL}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={this.onOptionsModalClose}
|
|
||||||
>
|
|
||||||
<ModalBody
|
|
||||||
className={styles.optionsModalBody}
|
|
||||||
innerClassName={styles.optionsInnerModalBody}
|
|
||||||
scrollDirection={scrollDirections.NONE}
|
|
||||||
>
|
|
||||||
<Scroller className={styles.optionsModalScroller}>
|
|
||||||
<div className={styles.mobileCloseButtonContainer}>
|
|
||||||
<Link
|
|
||||||
className={styles.mobileCloseButton}
|
|
||||||
onPress={this.onOptionsModalClose}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={icons.CLOSE}
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
values.map((v, index) => {
|
|
||||||
const hasParent = v.parentKey !== undefined;
|
|
||||||
const depth = hasParent ? 1 : 0;
|
|
||||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
|
||||||
return (
|
|
||||||
<OptionComponent
|
|
||||||
key={v.key}
|
|
||||||
id={v.key}
|
|
||||||
dividerAfter={v.dividerAfter}
|
|
||||||
depth={depth}
|
|
||||||
isSelected={isSelectedItem(index, this.props)}
|
|
||||||
isMultiSelect={isMultiSelect}
|
|
||||||
isDisabled={parentSelected}
|
|
||||||
{...valueOptions}
|
|
||||||
{...v}
|
|
||||||
isMobile={true}
|
|
||||||
onSelect={this.onSelect}
|
|
||||||
>
|
|
||||||
{v.value}
|
|
||||||
</OptionComponent>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Scroller>
|
|
||||||
</ModalBody>
|
|
||||||
</Modal> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EnhancedSelectInput.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
disabledClassName: PropTypes.string,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isEditable: PropTypes.bool.isRequired,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
hasWarning: PropTypes.bool,
|
|
||||||
valueOptions: PropTypes.object.isRequired,
|
|
||||||
selectedValueOptions: PropTypes.object.isRequired,
|
|
||||||
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
|
||||||
optionComponent: PropTypes.elementType,
|
|
||||||
onOpen: PropTypes.func,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
EnhancedSelectInput.defaultProps = {
|
|
||||||
className: styles.enhancedSelect,
|
|
||||||
disabledClassName: styles.isDisabled,
|
|
||||||
isDisabled: false,
|
|
||||||
isFetching: false,
|
|
||||||
isEditable: false,
|
|
||||||
valueOptions: {},
|
|
||||||
selectedValueOptions: {},
|
|
||||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
|
||||||
optionComponent: HintedSelectInputOption
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnhancedSelectInput;
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
const importantFieldNames = [
|
|
||||||
'baseUrl',
|
|
||||||
'apiPath',
|
|
||||||
'apiKey',
|
|
||||||
'authToken'
|
|
||||||
];
|
|
||||||
|
|
||||||
function getProviderDataKey(providerData) {
|
|
||||||
if (!providerData || !providerData.fields) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = providerData.fields
|
|
||||||
.filter((f) => importantFieldNames.includes(f.name))
|
|
||||||
.map((f) => f.value);
|
|
||||||
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectOptions(items) {
|
|
||||||
if (!items) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.map((option) => {
|
|
||||||
return {
|
|
||||||
key: option.value,
|
|
||||||
value: option.name,
|
|
||||||
hint: option.hint,
|
|
||||||
parentKey: option.parentValue,
|
|
||||||
isDisabled: option.isDisabled,
|
|
||||||
additionalProperties: option.additionalProperties
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { selectOptionsProviderAction }) => state.providerOptions[selectOptionsProviderAction] || defaultState,
|
|
||||||
(options) => {
|
|
||||||
if (options) {
|
|
||||||
return {
|
|
||||||
isFetching: options.isFetching,
|
|
||||||
values: getSelectOptions(options.items)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchOptions: fetchOptions,
|
|
||||||
dispatchClearOptions: clearOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
class EnhancedSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
refetchRequired: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
this._populate();
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidUpdate = (prevProps) => {
|
|
||||||
const prevKey = getProviderDataKey(prevProps.providerData);
|
|
||||||
const nextKey = getProviderDataKey(this.props.providerData);
|
|
||||||
|
|
||||||
if (!_.isEqual(prevKey, nextKey)) {
|
|
||||||
this.setState({ refetchRequired: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
this._cleanup();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onOpen = () => {
|
|
||||||
if (this.state.refetchRequired) {
|
|
||||||
this._populate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_populate() {
|
|
||||||
const {
|
|
||||||
provider,
|
|
||||||
providerData,
|
|
||||||
selectOptionsProviderAction,
|
|
||||||
dispatchFetchOptions
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (selectOptionsProviderAction) {
|
|
||||||
this.setState({ refetchRequired: false });
|
|
||||||
dispatchFetchOptions({
|
|
||||||
section: selectOptionsProviderAction,
|
|
||||||
action: selectOptionsProviderAction,
|
|
||||||
provider,
|
|
||||||
providerData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_cleanup() {
|
|
||||||
const {
|
|
||||||
selectOptionsProviderAction,
|
|
||||||
dispatchClearOptions
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (selectOptionsProviderAction) {
|
|
||||||
dispatchClearOptions({ section: selectOptionsProviderAction });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onOpen={this.onOpen}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EnhancedSelectInputConnector.propTypes = {
|
|
||||||
provider: PropTypes.string.isRequired,
|
|
||||||
providerData: PropTypes.object.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
selectOptionsProviderAction: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
dispatchFetchOptions: PropTypes.func.isRequired,
|
|
||||||
dispatchClearOptions: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(EnhancedSelectInputConnector);
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import CheckInput from './CheckInput';
|
|
||||||
import styles from './EnhancedSelectInputOption.css';
|
|
||||||
|
|
||||||
class EnhancedSelectInputOption extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
onSelect
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSelect(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
onCheckPress = () => {
|
|
||||||
// CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation.
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
id,
|
|
||||||
depth,
|
|
||||||
isSelected,
|
|
||||||
isDisabled,
|
|
||||||
isHidden,
|
|
||||||
isMultiSelect,
|
|
||||||
isMobile,
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
isSelected && !isMultiSelect && styles.isSelected,
|
|
||||||
isDisabled && !isMultiSelect && styles.isDisabled,
|
|
||||||
isHidden && styles.isHidden,
|
|
||||||
isMobile && styles.isMobile
|
|
||||||
)}
|
|
||||||
component="div"
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
onPress={this.onPress}
|
|
||||||
>
|
|
||||||
|
|
||||||
{
|
|
||||||
depth !== 0 &&
|
|
||||||
<div style={{ width: `${depth * 20}px` }} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isMultiSelect &&
|
|
||||||
<CheckInput
|
|
||||||
className={styles.optionCheckInput}
|
|
||||||
containerClassName={styles.optionCheck}
|
|
||||||
name={`select-${id}`}
|
|
||||||
value={isSelected}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
onChange={this.onCheckPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{
|
|
||||||
isMobile &&
|
|
||||||
<div className={styles.iconContainer}>
|
|
||||||
<Icon
|
|
||||||
name={isSelected ? icons.CHECK_CIRCLE : icons.CIRCLE_OUTLINE}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EnhancedSelectInputOption.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
depth: PropTypes.number.isRequired,
|
|
||||||
isSelected: PropTypes.bool.isRequired,
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
isHidden: PropTypes.bool.isRequired,
|
|
||||||
isMultiSelect: PropTypes.bool.isRequired,
|
|
||||||
isMobile: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
onSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
EnhancedSelectInputOption.defaultProps = {
|
|
||||||
className: styles.option,
|
|
||||||
depth: 0,
|
|
||||||
isDisabled: false,
|
|
||||||
isHidden: false,
|
|
||||||
isMultiSelect: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnhancedSelectInputOption;
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './EnhancedSelectInputSelectedValue.css';
|
|
||||||
|
|
||||||
function EnhancedSelectInputSelectedValue(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
isDisabled
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
className,
|
|
||||||
isDisabled && styles.isDisabled
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EnhancedSelectInputSelectedValue.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
isDisabled: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
EnhancedSelectInputSelectedValue.defaultProps = {
|
|
||||||
className: styles.selectedValue,
|
|
||||||
isDisabled: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnhancedSelectInputSelectedValue;
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import styles from './Form.css';
|
|
||||||
|
|
||||||
function Form(props) {
|
|
||||||
const {
|
|
||||||
children,
|
|
||||||
validationErrors,
|
|
||||||
validationWarnings,
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
validationErrors.length || validationWarnings.length ?
|
|
||||||
<div className={styles.validationFailures}>
|
|
||||||
{
|
|
||||||
validationErrors.map((error, index) => {
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
key={index}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
>
|
|
||||||
{error.errorMessage}
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
validationWarnings.map((warning, index) => {
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
key={index}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
>
|
|
||||||
{warning.errorMessage}
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Form.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
validationErrors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
validationWarnings: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Form.defaultProps = {
|
|
||||||
validationErrors: [],
|
|
||||||
validationWarnings: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Form;
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { ValidationError, ValidationWarning } from 'typings/pending';
|
||||||
|
import styles from './Form.css';
|
||||||
|
|
||||||
|
export interface FormProps {
|
||||||
|
children: ReactNode;
|
||||||
|
validationErrors?: ValidationError[];
|
||||||
|
validationWarnings?: ValidationWarning[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Form({
|
||||||
|
children,
|
||||||
|
validationErrors = [],
|
||||||
|
validationWarnings = [],
|
||||||
|
}: FormProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{validationErrors.length || validationWarnings.length ? (
|
||||||
|
<div className={styles.validationFailures}>
|
||||||
|
{validationErrors.map((error, index) => {
|
||||||
|
return (
|
||||||
|
<Alert key={index} kind={kinds.DANGER}>
|
||||||
|
{error.errorMessage}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{validationWarnings.map((warning, index) => {
|
||||||
|
return (
|
||||||
|
<Alert key={index} kind={kinds.WARNING}>
|
||||||
|
{warning.errorMessage}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form;
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { map } from 'Helpers/elementChildren';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import styles from './FormGroup.css';
|
|
||||||
|
|
||||||
function FormGroup(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
size,
|
|
||||||
advancedSettings,
|
|
||||||
isAdvanced,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (!advancedSettings && isAdvanced) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const childProps = isAdvanced ? { isAdvanced } : {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles[size]
|
|
||||||
)}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
map(children, (child) => {
|
|
||||||
return React.cloneElement(child, childProps);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormGroup.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
|
||||||
isAdvanced: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
FormGroup.defaultProps = {
|
|
||||||
className: styles.group,
|
|
||||||
size: sizes.SMALL,
|
|
||||||
advancedSettings: false,
|
|
||||||
isAdvanced: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormGroup;
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { Children, ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||||
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
|
import styles from './FormGroup.css';
|
||||||
|
|
||||||
|
interface FormGroupProps extends ComponentPropsWithoutRef<'div'> {
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
size?: Extract<Size, keyof typeof styles>;
|
||||||
|
advancedSettings?: boolean;
|
||||||
|
isAdvanced?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormGroup(props: FormGroupProps) {
|
||||||
|
const {
|
||||||
|
className = styles.group,
|
||||||
|
children,
|
||||||
|
size = 'small',
|
||||||
|
advancedSettings = false,
|
||||||
|
isAdvanced = false,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!advancedSettings && isAdvanced) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childProps = isAdvanced ? { isAdvanced } : {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(className, styles[size])} {...otherProps}>
|
||||||
|
{Children.map(children, (child) => {
|
||||||
|
if (!React.isValidElement(child)) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement(child, childProps);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormGroup;
|
||||||
@@ -14,13 +14,14 @@ function FormInputButton({
|
|||||||
className = styles.button,
|
className = styles.button,
|
||||||
canSpin = false,
|
canSpin = false,
|
||||||
isLastButton = true,
|
isLastButton = true,
|
||||||
|
kind = kinds.PRIMARY,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: FormInputButtonProps) {
|
}: FormInputButtonProps) {
|
||||||
if (canSpin) {
|
if (canSpin) {
|
||||||
return (
|
return (
|
||||||
<SpinnerButton
|
<SpinnerButton
|
||||||
className={classNames(className, !isLastButton && styles.middleButton)}
|
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||||
kind={kinds.PRIMARY}
|
kind={kind}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -29,7 +30,7 @@ function FormInputButton({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={classNames(className, !isLastButton && styles.middleButton)}
|
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||||
kind={kinds.PRIMARY}
|
kind={kind}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,311 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AutoCompleteInput from './AutoCompleteInput';
|
|
||||||
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
|
||||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
|
||||||
import CheckInput from './CheckInput';
|
|
||||||
import DeviceInputConnector from './DeviceInputConnector';
|
|
||||||
import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
|
||||||
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
|
||||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
|
||||||
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
|
||||||
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
|
||||||
import MovieTagInput from './MovieTagInput';
|
|
||||||
import NumberInput from './NumberInput';
|
|
||||||
import OAuthInputConnector from './OAuthInputConnector';
|
|
||||||
import PasswordInput from './PasswordInput';
|
|
||||||
import PathInputConnector from './PathInputConnector';
|
|
||||||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
|
||||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
|
||||||
import TagInputConnector from './TagInputConnector';
|
|
||||||
import TagSelectInputConnector from './TagSelectInputConnector';
|
|
||||||
import TextArea from './TextArea';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
import TextTagInputConnector from './TextTagInputConnector';
|
|
||||||
import UMaskInput from './UMaskInput';
|
|
||||||
import styles from './FormInputGroup.css';
|
|
||||||
|
|
||||||
function getComponent(type) {
|
|
||||||
switch (type) {
|
|
||||||
case inputTypes.AUTO_COMPLETE:
|
|
||||||
return AutoCompleteInput;
|
|
||||||
|
|
||||||
case inputTypes.AVAILABILITY_SELECT:
|
|
||||||
return AvailabilitySelectInput;
|
|
||||||
|
|
||||||
case inputTypes.CAPTCHA:
|
|
||||||
return CaptchaInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.CHECK:
|
|
||||||
return CheckInput;
|
|
||||||
|
|
||||||
case inputTypes.DEVICE:
|
|
||||||
return DeviceInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.KEY_VALUE_LIST:
|
|
||||||
return KeyValueListInput;
|
|
||||||
|
|
||||||
case inputTypes.NUMBER:
|
|
||||||
return NumberInput;
|
|
||||||
|
|
||||||
case inputTypes.OAUTH:
|
|
||||||
return OAuthInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.PASSWORD:
|
|
||||||
return PasswordInput;
|
|
||||||
|
|
||||||
case inputTypes.PATH:
|
|
||||||
return PathInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.QUALITY_PROFILE_SELECT:
|
|
||||||
return QualityProfileSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.INDEXER_SELECT:
|
|
||||||
return IndexerSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.MOVIE_MONITORED_SELECT:
|
|
||||||
return MovieMonitoredSelectInput;
|
|
||||||
|
|
||||||
case inputTypes.ROOT_FOLDER_SELECT:
|
|
||||||
return RootFolderSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
|
||||||
return IndexerFlagsSelectInput;
|
|
||||||
|
|
||||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
|
||||||
return DownloadClientSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.LANGUAGE_SELECT:
|
|
||||||
return LanguageSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.SELECT:
|
|
||||||
return EnhancedSelectInput;
|
|
||||||
|
|
||||||
case inputTypes.DYNAMIC_SELECT:
|
|
||||||
return EnhancedSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.MOVIE_TAG:
|
|
||||||
return MovieTagInput;
|
|
||||||
|
|
||||||
case inputTypes.TAG:
|
|
||||||
return TagInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.TEXT_AREA:
|
|
||||||
return TextArea;
|
|
||||||
|
|
||||||
case inputTypes.TEXT_TAG:
|
|
||||||
return TextTagInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.TAG_SELECT:
|
|
||||||
return TagSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.UMASK:
|
|
||||||
return UMaskInput;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return TextInput;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormInputGroup(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
containerClassName,
|
|
||||||
inputClassName,
|
|
||||||
type,
|
|
||||||
unit,
|
|
||||||
buttons,
|
|
||||||
helpText,
|
|
||||||
helpTexts,
|
|
||||||
helpTextWarning,
|
|
||||||
helpLink,
|
|
||||||
pending,
|
|
||||||
errors,
|
|
||||||
warnings,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const InputComponent = getComponent(type);
|
|
||||||
const checkInput = type === inputTypes.CHECK;
|
|
||||||
const hasError = !!errors.length;
|
|
||||||
const hasWarning = !hasError && !!warnings.length;
|
|
||||||
const buttonsArray = React.Children.toArray(buttons);
|
|
||||||
const lastButtonIndex = buttonsArray.length - 1;
|
|
||||||
const hasButton = !!buttonsArray.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={containerClassName}>
|
|
||||||
<div className={className}>
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<InputComponent
|
|
||||||
className={inputClassName}
|
|
||||||
helpText={helpText}
|
|
||||||
helpTextWarning={helpTextWarning}
|
|
||||||
hasError={hasError}
|
|
||||||
hasWarning={hasWarning}
|
|
||||||
hasButton={hasButton}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
unit &&
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
type === inputTypes.NUMBER ?
|
|
||||||
styles.inputUnitNumber :
|
|
||||||
styles.inputUnit
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{unit}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
buttonsArray.map((button, index) => {
|
|
||||||
return React.cloneElement(
|
|
||||||
button,
|
|
||||||
{
|
|
||||||
isLastButton: index === lastButtonIndex
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* <div className={styles.pendingChangesContainer}>
|
|
||||||
{
|
|
||||||
pending &&
|
|
||||||
<Icon
|
|
||||||
name={icons.UNSAVED_SETTING}
|
|
||||||
className={styles.pendingChangesIcon}
|
|
||||||
title={translate('ChangeHasNotBeenSavedYet')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
!checkInput && helpText &&
|
|
||||||
<FormInputHelpText
|
|
||||||
text={helpText}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!checkInput && helpTexts &&
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
helpTexts.map((text, index) => {
|
|
||||||
return (
|
|
||||||
<FormInputHelpText
|
|
||||||
key={index}
|
|
||||||
text={text}
|
|
||||||
isCheckInput={checkInput}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
(!checkInput || helpText) && helpTextWarning &&
|
|
||||||
<FormInputHelpText
|
|
||||||
text={helpTextWarning}
|
|
||||||
isWarning={true}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
helpLink &&
|
|
||||||
<Link
|
|
||||||
to={helpLink}
|
|
||||||
>
|
|
||||||
{translate('MoreInfo')}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
errors.map((error, index) => {
|
|
||||||
return (
|
|
||||||
<FormInputHelpText
|
|
||||||
key={index}
|
|
||||||
text={error.message}
|
|
||||||
link={error.link}
|
|
||||||
tooltip={error.detailedMessage}
|
|
||||||
isError={true}
|
|
||||||
isCheckInput={checkInput}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
warnings.map((warning, index) => {
|
|
||||||
return (
|
|
||||||
<FormInputHelpText
|
|
||||||
key={index}
|
|
||||||
text={warning.message}
|
|
||||||
link={warning.link}
|
|
||||||
tooltip={warning.detailedMessage}
|
|
||||||
isWarning={true}
|
|
||||||
isCheckInput={checkInput}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormInputGroup.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
containerClassName: PropTypes.string.isRequired,
|
|
||||||
inputClassName: PropTypes.string,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.any,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.any),
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
delimiters: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
isDisabled: PropTypes.bool,
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
|
||||||
min: PropTypes.number,
|
|
||||||
max: PropTypes.number,
|
|
||||||
unit: PropTypes.string,
|
|
||||||
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
|
||||||
helpText: PropTypes.string,
|
|
||||||
helpTexts: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
helpTextWarning: PropTypes.string,
|
|
||||||
helpLink: PropTypes.string,
|
|
||||||
autoFocus: PropTypes.bool,
|
|
||||||
canEdit: PropTypes.bool,
|
|
||||||
includeNoChange: PropTypes.bool,
|
|
||||||
includeNoChangeDisabled: PropTypes.bool,
|
|
||||||
includeAny: PropTypes.bool,
|
|
||||||
selectedValueOptions: PropTypes.object,
|
|
||||||
indexerFlags: PropTypes.number,
|
|
||||||
pending: PropTypes.bool,
|
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
FormInputGroup.defaultProps = {
|
|
||||||
className: styles.inputGroup,
|
|
||||||
containerClassName: styles.inputGroupContainer,
|
|
||||||
type: inputTypes.TEXT,
|
|
||||||
buttons: [],
|
|
||||||
helpTexts: [],
|
|
||||||
errors: [],
|
|
||||||
warnings: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormInputGroup;
|
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
import React, { ElementType, ReactNode } from 'react';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import { InputType } from 'Helpers/Props/inputTypes';
|
||||||
|
import { ValidationError, ValidationWarning } from 'typings/pending';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AutoCompleteInput, { AutoCompleteInputProps } from './AutoCompleteInput';
|
||||||
|
import CaptchaInput, { CaptchaInputProps } from './CaptchaInput';
|
||||||
|
import CheckInput, { CheckInputProps } from './CheckInput';
|
||||||
|
import { FormInputButtonProps } from './FormInputButton';
|
||||||
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
|
import KeyValueListInput, { KeyValueListInputProps } from './KeyValueListInput';
|
||||||
|
import NumberInput, { NumberInputProps } from './NumberInput';
|
||||||
|
import OAuthInput, { OAuthInputProps } from './OAuthInput';
|
||||||
|
import PasswordInput from './PasswordInput';
|
||||||
|
import PathInput, { PathInputProps } from './PathInput';
|
||||||
|
import AvailabilitySelectInput, {
|
||||||
|
AvailabilitySelectInputProps,
|
||||||
|
} from './Select/AvailabilitySelectInput';
|
||||||
|
import DownloadClientSelectInput, {
|
||||||
|
DownloadClientSelectInputProps,
|
||||||
|
} from './Select/DownloadClientSelectInput';
|
||||||
|
import EnhancedSelectInput, {
|
||||||
|
EnhancedSelectInputProps,
|
||||||
|
} from './Select/EnhancedSelectInput';
|
||||||
|
import IndexerFlagsSelectInput, {
|
||||||
|
IndexerFlagsSelectInputProps,
|
||||||
|
} from './Select/IndexerFlagsSelectInput';
|
||||||
|
import IndexerSelectInput, {
|
||||||
|
IndexerSelectInputProps,
|
||||||
|
} from './Select/IndexerSelectInput';
|
||||||
|
import LanguageSelectInput, {
|
||||||
|
LanguageSelectInputProps,
|
||||||
|
} from './Select/LanguageSelectInput';
|
||||||
|
import MonitorMoviesSelectInput, {
|
||||||
|
MonitorMoviesSelectInputProps,
|
||||||
|
} from './Select/MonitorMoviesSelectInput';
|
||||||
|
import ProviderDataSelectInput, {
|
||||||
|
ProviderOptionSelectInputProps,
|
||||||
|
} from './Select/ProviderOptionSelectInput';
|
||||||
|
import QualityProfileSelectInput, {
|
||||||
|
QualityProfileSelectInputProps,
|
||||||
|
} from './Select/QualityProfileSelectInput';
|
||||||
|
import RootFolderSelectInput, {
|
||||||
|
RootFolderSelectInputProps,
|
||||||
|
} from './Select/RootFolderSelectInput';
|
||||||
|
import UMaskInput, { UMaskInputProps } from './Select/UMaskInput';
|
||||||
|
import DeviceInput, { DeviceInputProps } from './Tag/DeviceInput';
|
||||||
|
import MovieTagInput, { MovieTagInputProps } from './Tag/MovieTagInput';
|
||||||
|
import TagSelectInput, { TagSelectInputProps } from './Tag/TagSelectInput';
|
||||||
|
import TextTagInput, { TextTagInputProps } from './Tag/TextTagInput';
|
||||||
|
import TextArea, { TextAreaProps } from './TextArea';
|
||||||
|
import TextInput, { TextInputProps } from './TextInput';
|
||||||
|
import styles from './FormInputGroup.css';
|
||||||
|
|
||||||
|
const componentMap: Record<InputType, ElementType> = {
|
||||||
|
autoComplete: AutoCompleteInput,
|
||||||
|
availabilitySelect: AvailabilitySelectInput,
|
||||||
|
captcha: CaptchaInput,
|
||||||
|
check: CheckInput,
|
||||||
|
date: TextInput,
|
||||||
|
device: DeviceInput,
|
||||||
|
downloadClientSelect: DownloadClientSelectInput,
|
||||||
|
dynamicSelect: ProviderDataSelectInput,
|
||||||
|
file: TextInput,
|
||||||
|
float: NumberInput,
|
||||||
|
indexerFlagsSelect: IndexerFlagsSelectInput,
|
||||||
|
indexerSelect: IndexerSelectInput,
|
||||||
|
keyValueList: KeyValueListInput,
|
||||||
|
languageSelect: LanguageSelectInput,
|
||||||
|
monitorMoviesSelect: MonitorMoviesSelectInput,
|
||||||
|
movieTag: MovieTagInput,
|
||||||
|
number: NumberInput,
|
||||||
|
oauth: OAuthInput,
|
||||||
|
password: PasswordInput,
|
||||||
|
path: PathInput,
|
||||||
|
qualityProfileSelect: QualityProfileSelectInput,
|
||||||
|
rootFolderSelect: RootFolderSelectInput,
|
||||||
|
select: EnhancedSelectInput,
|
||||||
|
tag: MovieTagInput,
|
||||||
|
tagSelect: TagSelectInput,
|
||||||
|
text: TextInput,
|
||||||
|
textArea: TextArea,
|
||||||
|
textTag: TextTagInput,
|
||||||
|
umask: UMaskInput,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// type Components = typeof componentMap;
|
||||||
|
|
||||||
|
type PickProps<V, C extends InputType> = C extends 'text'
|
||||||
|
? TextInputProps
|
||||||
|
: C extends 'autoComplete'
|
||||||
|
? AutoCompleteInputProps
|
||||||
|
: C extends 'availabilitySelect'
|
||||||
|
? AvailabilitySelectInputProps
|
||||||
|
: C extends 'captcha'
|
||||||
|
? CaptchaInputProps
|
||||||
|
: C extends 'check'
|
||||||
|
? CheckInputProps
|
||||||
|
: C extends 'date'
|
||||||
|
? TextInputProps
|
||||||
|
: C extends 'device'
|
||||||
|
? DeviceInputProps
|
||||||
|
: C extends 'downloadClientSelect'
|
||||||
|
? DownloadClientSelectInputProps
|
||||||
|
: C extends 'dynamicSelect'
|
||||||
|
? ProviderOptionSelectInputProps
|
||||||
|
: C extends 'file'
|
||||||
|
? TextInputProps
|
||||||
|
: C extends 'float'
|
||||||
|
? TextInputProps
|
||||||
|
: C extends 'indexerFlagsSelect'
|
||||||
|
? IndexerFlagsSelectInputProps
|
||||||
|
: C extends 'indexerSelect'
|
||||||
|
? IndexerSelectInputProps
|
||||||
|
: C extends 'keyValueList'
|
||||||
|
? KeyValueListInputProps
|
||||||
|
: C extends 'languageSelect'
|
||||||
|
? LanguageSelectInputProps
|
||||||
|
: C extends 'monitorMoviesSelect'
|
||||||
|
? MonitorMoviesSelectInputProps
|
||||||
|
: C extends 'movieTag'
|
||||||
|
? MovieTagInputProps<V>
|
||||||
|
: C extends 'number'
|
||||||
|
? NumberInputProps
|
||||||
|
: C extends 'oauth'
|
||||||
|
? OAuthInputProps
|
||||||
|
: C extends 'password'
|
||||||
|
? TextInputProps
|
||||||
|
: C extends 'path'
|
||||||
|
? PathInputProps
|
||||||
|
: C extends 'qualityProfileSelect'
|
||||||
|
? QualityProfileSelectInputProps
|
||||||
|
: C extends 'rootFolderSelect'
|
||||||
|
? RootFolderSelectInputProps
|
||||||
|
: C extends 'select'
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
EnhancedSelectInputProps<any, V>
|
||||||
|
: C extends 'tag'
|
||||||
|
? MovieTagInputProps<V>
|
||||||
|
: C extends 'tagSelect'
|
||||||
|
? TagSelectInputProps
|
||||||
|
: C extends 'text'
|
||||||
|
? TextInputProps
|
||||||
|
: C extends 'textArea'
|
||||||
|
? TextAreaProps
|
||||||
|
: C extends 'textTag'
|
||||||
|
? TextTagInputProps
|
||||||
|
: C extends 'umask'
|
||||||
|
? UMaskInputProps
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export interface FormInputGroupValues<T> {
|
||||||
|
key: T;
|
||||||
|
value: string;
|
||||||
|
hint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove once all parent components are updated to TSX and we can refactor to a consistent type
|
||||||
|
export interface ValidationMessage {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormInputGroupProps<V, C extends InputType> = Omit<
|
||||||
|
PickProps<V, C>,
|
||||||
|
'className'
|
||||||
|
> & {
|
||||||
|
type: C;
|
||||||
|
className?: string;
|
||||||
|
containerClassName?: string;
|
||||||
|
inputClassName?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
autocomplete?: string;
|
||||||
|
name: string;
|
||||||
|
buttons?: ReactNode | ReactNode[];
|
||||||
|
helpText?: string;
|
||||||
|
helpTexts?: string[];
|
||||||
|
helpTextWarning?: string;
|
||||||
|
helpLink?: string;
|
||||||
|
pending?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
unit?: string;
|
||||||
|
errors?: (ValidationMessage | ValidationError)[];
|
||||||
|
warnings?: (ValidationMessage | ValidationWarning)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function FormInputGroup<T, C extends InputType>(
|
||||||
|
props: FormInputGroupProps<T, C>
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
className = styles.inputGroup,
|
||||||
|
containerClassName = styles.inputGroupContainer,
|
||||||
|
inputClassName,
|
||||||
|
type,
|
||||||
|
unit,
|
||||||
|
buttons = [],
|
||||||
|
helpText,
|
||||||
|
helpTexts = [],
|
||||||
|
helpTextWarning,
|
||||||
|
helpLink,
|
||||||
|
pending,
|
||||||
|
errors = [],
|
||||||
|
warnings = [],
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const InputComponent = componentMap[type];
|
||||||
|
const checkInput = type === inputTypes.CHECK;
|
||||||
|
const hasError = !!errors.length;
|
||||||
|
const hasWarning = !hasError && !!warnings.length;
|
||||||
|
const buttonsArray = React.Children.toArray(buttons);
|
||||||
|
const lastButtonIndex = buttonsArray.length - 1;
|
||||||
|
const hasButton = !!buttonsArray.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClassName}>
|
||||||
|
<div className={className}>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
{/* @ts-expect-error - types are validated already */}
|
||||||
|
<InputComponent
|
||||||
|
className={inputClassName}
|
||||||
|
helpText={helpText}
|
||||||
|
helpTextWarning={helpTextWarning}
|
||||||
|
hasError={hasError}
|
||||||
|
hasWarning={hasWarning}
|
||||||
|
hasButton={hasButton}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{unit && (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
type === inputTypes.NUMBER
|
||||||
|
? styles.inputUnitNumber
|
||||||
|
: styles.inputUnit
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{unit}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{buttonsArray.map((button, index) => {
|
||||||
|
if (!React.isValidElement<FormInputButtonProps>(button)) {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement(button, {
|
||||||
|
isLastButton: index === lastButtonIndex,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* <div className={styles.pendingChangesContainer}>
|
||||||
|
{
|
||||||
|
pending &&
|
||||||
|
<Icon
|
||||||
|
name={icons.UNSAVED_SETTING}
|
||||||
|
className={styles.pendingChangesIcon}
|
||||||
|
title="Change has not been saved yet"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!checkInput && helpText ? <FormInputHelpText text={helpText} /> : null}
|
||||||
|
|
||||||
|
{!checkInput && helpTexts ? (
|
||||||
|
<div>
|
||||||
|
{helpTexts.map((text, index) => {
|
||||||
|
return (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={text}
|
||||||
|
isCheckInput={checkInput}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{(!checkInput || helpText) && helpTextWarning ? (
|
||||||
|
<FormInputHelpText text={helpTextWarning} isWarning={true} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{helpLink ? <Link to={helpLink}>{translate('MoreInfo')}</Link> : null}
|
||||||
|
|
||||||
|
{errors.map((error, index) => {
|
||||||
|
return 'errorMessage' in error ? (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={error.errorMessage}
|
||||||
|
link={error.infoLink}
|
||||||
|
tooltip={error.detailedDescription}
|
||||||
|
isError={true}
|
||||||
|
isCheckInput={checkInput}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={error.message}
|
||||||
|
isError={true}
|
||||||
|
isCheckInput={checkInput}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{warnings.map((warning, index) => {
|
||||||
|
return 'errorMessage' in warning ? (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={warning.errorMessage}
|
||||||
|
link={warning.infoLink}
|
||||||
|
tooltip={warning.detailedDescription}
|
||||||
|
isWarning={true}
|
||||||
|
isCheckInput={checkInput}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={warning.message}
|
||||||
|
isWarning={true}
|
||||||
|
isCheckInput={checkInput}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormInputGroup;
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import styles from './FormInputHelpText.css';
|
|
||||||
|
|
||||||
function FormInputHelpText(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
text,
|
|
||||||
link,
|
|
||||||
tooltip,
|
|
||||||
isError,
|
|
||||||
isWarning,
|
|
||||||
isCheckInput
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
className,
|
|
||||||
isError && styles.isError,
|
|
||||||
isWarning && styles.isWarning,
|
|
||||||
isCheckInput && styles.isCheckInput
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
|
|
||||||
{
|
|
||||||
link ?
|
|
||||||
<Link
|
|
||||||
className={styles.link}
|
|
||||||
to={link}
|
|
||||||
title={tooltip}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={icons.EXTERNAL_LINK}
|
|
||||||
/>
|
|
||||||
</Link> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!link && tooltip ?
|
|
||||||
<Icon
|
|
||||||
containerClassName={styles.details}
|
|
||||||
name={icons.INFO}
|
|
||||||
title={tooltip}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormInputHelpText.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
text: PropTypes.string.isRequired,
|
|
||||||
link: PropTypes.string,
|
|
||||||
tooltip: PropTypes.string,
|
|
||||||
isError: PropTypes.bool,
|
|
||||||
isWarning: PropTypes.bool,
|
|
||||||
isCheckInput: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
FormInputHelpText.defaultProps = {
|
|
||||||
className: styles.helpText,
|
|
||||||
isError: false,
|
|
||||||
isWarning: false,
|
|
||||||
isCheckInput: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormInputHelpText;
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import styles from './FormInputHelpText.css';
|
||||||
|
|
||||||
|
interface FormInputHelpTextProps {
|
||||||
|
className?: string;
|
||||||
|
text: string;
|
||||||
|
link?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
isError?: boolean;
|
||||||
|
isWarning?: boolean;
|
||||||
|
isCheckInput?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormInputHelpText({
|
||||||
|
className = styles.helpText,
|
||||||
|
text,
|
||||||
|
link,
|
||||||
|
tooltip,
|
||||||
|
isError = false,
|
||||||
|
isWarning = false,
|
||||||
|
isCheckInput = false,
|
||||||
|
}: FormInputHelpTextProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
isError && styles.isError,
|
||||||
|
isWarning && styles.isWarning,
|
||||||
|
isCheckInput && styles.isCheckInput
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
|
||||||
|
{link ? (
|
||||||
|
<Link className={styles.link} to={link} title={tooltip}>
|
||||||
|
<Icon name={icons.EXTERNAL_LINK} />
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!link && tooltip ? (
|
||||||
|
<Icon
|
||||||
|
containerClassName={styles.details}
|
||||||
|
name={icons.INFO}
|
||||||
|
title={tooltip}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormInputHelpText;
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import styles from './FormLabel.css';
|
|
||||||
|
|
||||||
function FormLabel(props) {
|
|
||||||
const {
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
errorClassName,
|
|
||||||
size,
|
|
||||||
name,
|
|
||||||
hasError,
|
|
||||||
isAdvanced,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
{...otherProps}
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles[size],
|
|
||||||
hasError && errorClassName,
|
|
||||||
isAdvanced && styles.isAdvanced
|
|
||||||
)}
|
|
||||||
htmlFor={name}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormLabel.propTypes = {
|
|
||||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired,
|
|
||||||
className: PropTypes.string,
|
|
||||||
errorClassName: PropTypes.string,
|
|
||||||
size: PropTypes.oneOf(sizes.all),
|
|
||||||
name: PropTypes.string,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
isAdvanced: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
FormLabel.defaultProps = {
|
|
||||||
className: styles.label,
|
|
||||||
errorClassName: styles.hasError,
|
|
||||||
isAdvanced: false,
|
|
||||||
size: sizes.LARGE
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormLabel;
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
|
import styles from './FormLabel.css';
|
||||||
|
|
||||||
|
interface FormLabelProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
errorClassName?: string;
|
||||||
|
size?: Extract<Size, keyof typeof styles>;
|
||||||
|
name?: string;
|
||||||
|
hasError?: boolean;
|
||||||
|
isAdvanced?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormLabel(props: FormLabelProps) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
className = styles.label,
|
||||||
|
errorClassName = styles.hasError,
|
||||||
|
size = 'large',
|
||||||
|
name,
|
||||||
|
hasError,
|
||||||
|
isAdvanced = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
styles[size],
|
||||||
|
hasError && errorClassName,
|
||||||
|
isAdvanced && styles.isAdvanced
|
||||||
|
)}
|
||||||
|
htmlFor={name}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormLabel;
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
|
|
||||||
import styles from './HintedSelectInputOption.css';
|
|
||||||
|
|
||||||
function HintedSelectInputOption(props) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
value,
|
|
||||||
hint,
|
|
||||||
depth,
|
|
||||||
isSelected,
|
|
||||||
isDisabled,
|
|
||||||
dividerAfter,
|
|
||||||
isMultiSelect,
|
|
||||||
isMobile,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<EnhancedSelectInputOption
|
|
||||||
id={id}
|
|
||||||
depth={depth}
|
|
||||||
isSelected={isSelected}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
isHidden={isDisabled}
|
|
||||||
isMultiSelect={isMultiSelect}
|
|
||||||
isMobile={isMobile}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={classNames(
|
|
||||||
styles.optionText,
|
|
||||||
isMobile && styles.isMobile
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div>{value}</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
hint != null &&
|
|
||||||
<div className={styles.hintText}>
|
|
||||||
{hint}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</EnhancedSelectInputOption>
|
|
||||||
|
|
||||||
{
|
|
||||||
dividerAfter ?
|
|
||||||
<div className={styles.divider} /> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HintedSelectInputOption.propTypes = {
|
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
hint: PropTypes.node,
|
|
||||||
name: PropTypes.string,
|
|
||||||
depth: PropTypes.number,
|
|
||||||
isSelected: PropTypes.bool.isRequired,
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
dividerAfter: PropTypes.bool.isRequired,
|
|
||||||
isMultiSelect: PropTypes.bool.isRequired,
|
|
||||||
isMobile: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
HintedSelectInputOption.defaultProps = {
|
|
||||||
isDisabled: false,
|
|
||||||
dividerAfter: false,
|
|
||||||
isHidden: false,
|
|
||||||
isMultiSelect: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HintedSelectInputOption;
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
|
|
||||||
import styles from './HintedSelectInputSelectedValue.css';
|
|
||||||
|
|
||||||
function HintedSelectInputSelectedValue(props) {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
values,
|
|
||||||
hint,
|
|
||||||
isMultiSelect,
|
|
||||||
includeHint,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const valuesMap = isMultiSelect && _.keyBy(values, 'key');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInputSelectedValue
|
|
||||||
className={styles.selectedValue}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.valueText}>
|
|
||||||
{
|
|
||||||
isMultiSelect ?
|
|
||||||
value.map((key, index) => {
|
|
||||||
const v = valuesMap[key];
|
|
||||||
return (
|
|
||||||
<Label key={key}>
|
|
||||||
{v ? v.value : key}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}) :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isMultiSelect ? null : value
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
hint != null && includeHint ?
|
|
||||||
<div className={styles.hintText}>
|
|
||||||
{hint}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</EnhancedSelectInputSelectedValue>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HintedSelectInputSelectedValue.propTypes = {
|
|
||||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
hint: PropTypes.string,
|
|
||||||
isMultiSelect: PropTypes.bool.isRequired,
|
|
||||||
includeHint: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
HintedSelectInputSelectedValue.defaultProps = {
|
|
||||||
isMultiSelect: false,
|
|
||||||
includeHint: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HintedSelectInputSelectedValue;
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.indexers,
|
|
||||||
(state, { includeAny }) => includeAny,
|
|
||||||
(indexers, includeAny) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = indexers;
|
|
||||||
|
|
||||||
const values = _.map(items.sort(sortByProp('name')), (indexer) => {
|
|
||||||
return {
|
|
||||||
key: indexer.id,
|
|
||||||
value: indexer.name
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (includeAny) {
|
|
||||||
values.unshift({
|
|
||||||
key: 0,
|
|
||||||
value: `(${translate('Any')})`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
values
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchIndexers: fetchIndexers
|
|
||||||
};
|
|
||||||
|
|
||||||
class IndexerSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (!this.props.isPopulated) {
|
|
||||||
this.props.dispatchFetchIndexers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
this.props.onChange({ name, value: parseInt(value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IndexerSelectInputConnector.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
includeAny: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
IndexerSelectInputConnector.defaultProps = {
|
|
||||||
includeAny: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { values }) => values,
|
|
||||||
( languages ) => {
|
|
||||||
|
|
||||||
const minId = languages.reduce((min, v) => (v.key < 1 ? v.key : min), languages[0].key);
|
|
||||||
|
|
||||||
const values = languages.map(({ key, value }) => {
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
dividerAfter: minId < 1 ? key === minId : false
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
values
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class LanguageSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LanguageSelectInputConnector.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(LanguageSelectInputConnector);
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import monitorOptions from 'Utilities/Movie/monitorOptions';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function MovieMonitoredSelectInput(props) {
|
|
||||||
const {
|
|
||||||
includeNoChange,
|
|
||||||
includeMixed,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const values = [...monitorOptions];
|
|
||||||
|
|
||||||
if (includeNoChange) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'noChange',
|
|
||||||
get value() {
|
|
||||||
return translate('NoChange');
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMixed) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'mixed',
|
|
||||||
get value() {
|
|
||||||
return `(${translate('Mixed')})`;
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...otherProps}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieMonitoredSelectInput.propTypes = {
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
includeMixed: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
MovieMonitoredSelectInput.defaultProps = {
|
|
||||||
includeNoChange: false,
|
|
||||||
includeMixed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieMonitoredSelectInput;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import TagInputConnector from './TagInputConnector';
|
|
||||||
|
|
||||||
interface MovieTagInputProps {
|
|
||||||
name: string;
|
|
||||||
value: number | number[];
|
|
||||||
onChange: ({
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
value: number | number[];
|
|
||||||
}) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MovieTagInput(props: MovieTagInputProps) {
|
|
||||||
const { value, onChange, ...otherProps } = props;
|
|
||||||
const isArray = Array.isArray(value);
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
|
||||||
({ name, value: newValue }: { name: string; value: number[] }) => {
|
|
||||||
if (isArray) {
|
|
||||||
onChange({ name, value: newValue });
|
|
||||||
} else {
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue.length ? newValue[newValue.length - 1] : 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isArray, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
let finalValue: number[] = [];
|
|
||||||
|
|
||||||
if (isArray) {
|
|
||||||
finalValue = value;
|
|
||||||
} else if (value === 0) {
|
|
||||||
finalValue = [];
|
|
||||||
} else {
|
|
||||||
finalValue = [value];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
|
|
||||||
<TagInputConnector
|
|
||||||
{...otherProps}
|
|
||||||
value={finalValue}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
|
|
||||||
function parseValue(props, value) {
|
|
||||||
const {
|
|
||||||
isFloat,
|
|
||||||
min,
|
|
||||||
max
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (value == null || value === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newValue = isFloat ? parseFloat(value) : parseInt(value);
|
|
||||||
|
|
||||||
if (min != null && newValue != null && newValue < min) {
|
|
||||||
newValue = min;
|
|
||||||
} else if (max != null && newValue != null && newValue > max) {
|
|
||||||
newValue = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
class NumberInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
value: props.value == null ? '' : props.value.toString(),
|
|
||||||
isFocused: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const { value } = this.props;
|
|
||||||
|
|
||||||
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
|
|
||||||
this.setState({
|
|
||||||
value: value == null ? '' : value.toString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
this.setState({ value });
|
|
||||||
|
|
||||||
this.props.onChange({
|
|
||||||
name,
|
|
||||||
value: parseValue(this.props, value)
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
onFocus = () => {
|
|
||||||
this.setState({ isFocused: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlur = () => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { value } = this.state;
|
|
||||||
const parsedValue = parseValue(this.props, value);
|
|
||||||
const stringValue = parsedValue == null ? '' : parsedValue.toString();
|
|
||||||
|
|
||||||
if (stringValue === value) {
|
|
||||||
this.setState({ isFocused: false });
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
value: stringValue,
|
|
||||||
isFocused: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: parsedValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const value = this.state.value;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
{...this.props}
|
|
||||||
type="number"
|
|
||||||
value={value == null ? '' : value}
|
|
||||||
onChange={this.onChange}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberInput.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.number,
|
|
||||||
min: PropTypes.number,
|
|
||||||
max: PropTypes.number,
|
|
||||||
isFloat: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
NumberInput.defaultProps = {
|
|
||||||
value: null,
|
|
||||||
isFloat: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NumberInput;
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import TextInput, { TextInputProps } from './TextInput';
|
||||||
|
|
||||||
|
function parseValue(
|
||||||
|
value: string | null | undefined,
|
||||||
|
isFloat: boolean,
|
||||||
|
min: number | undefined,
|
||||||
|
max: number | undefined
|
||||||
|
) {
|
||||||
|
if (value == null || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newValue = isFloat ? parseFloat(value) : parseInt(value);
|
||||||
|
|
||||||
|
if (min != null && newValue != null && newValue < min) {
|
||||||
|
newValue = min;
|
||||||
|
} else if (max != null && newValue != null && newValue > max) {
|
||||||
|
newValue = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NumberInputProps
|
||||||
|
extends Omit<TextInputProps, 'value' | 'onChange'> {
|
||||||
|
value?: number | null;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
isFloat?: boolean;
|
||||||
|
onChange: (input: InputChanged<number | null>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumberInput({
|
||||||
|
name,
|
||||||
|
value: inputValue = null,
|
||||||
|
isFloat = false,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
onChange,
|
||||||
|
...otherProps
|
||||||
|
}: NumberInputProps) {
|
||||||
|
const [value, setValue] = useState(
|
||||||
|
inputValue == null ? '' : inputValue.toString()
|
||||||
|
);
|
||||||
|
const isFocused = useRef(false);
|
||||||
|
const previousValue = usePrevious(inputValue);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ name, value: newValue }: InputChanged<string>) => {
|
||||||
|
setValue(newValue);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: parseValue(newValue, isFloat, min, max),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[isFloat, min, max, onChange, setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
isFocused.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
const parsedValue = parseValue(value, isFloat, min, max);
|
||||||
|
const stringValue = parsedValue == null ? '' : parsedValue.toString();
|
||||||
|
|
||||||
|
if (stringValue !== value) {
|
||||||
|
setValue(stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: parsedValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
isFocused.current = false;
|
||||||
|
}, [name, value, isFloat, min, max, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
// @ts-expect-error inputValue may be null
|
||||||
|
!isNaN(inputValue) &&
|
||||||
|
inputValue !== previousValue &&
|
||||||
|
!isFocused.current
|
||||||
|
) {
|
||||||
|
setValue(inputValue == null ? '' : inputValue.toString());
|
||||||
|
}
|
||||||
|
}, [inputValue, previousValue, setValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
{...otherProps}
|
||||||
|
name={name}
|
||||||
|
type="number"
|
||||||
|
value={value == null ? '' : value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NumberInput;
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
|
|
||||||
function OAuthInput(props) {
|
|
||||||
const {
|
|
||||||
label,
|
|
||||||
authorizing,
|
|
||||||
error,
|
|
||||||
onPress
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SpinnerErrorButton
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
isSpinning={authorizing}
|
|
||||||
error={error}
|
|
||||||
onPress={onPress}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</SpinnerErrorButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
OAuthInput.propTypes = {
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
authorizing: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
OAuthInput.defaultProps = {
|
|
||||||
label: 'Start OAuth'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OAuthInput;
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { resetOAuth, startOAuth } from 'Store/Actions/oAuthActions';
|
||||||
|
import { InputOnChange } from 'typings/inputs';
|
||||||
|
|
||||||
|
export interface OAuthInputProps {
|
||||||
|
label?: string;
|
||||||
|
name: string;
|
||||||
|
provider: string;
|
||||||
|
providerData: object;
|
||||||
|
section: string;
|
||||||
|
onChange: InputOnChange<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OAuthInput({
|
||||||
|
label = 'Start OAuth',
|
||||||
|
name,
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
section,
|
||||||
|
onChange,
|
||||||
|
}: OAuthInputProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { authorizing, error, result } = useSelector(
|
||||||
|
(state: AppState) => state.oAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
startOAuth({
|
||||||
|
name,
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
section,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [name, provider, providerData, section, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(result).forEach((key) => {
|
||||||
|
onChange({ name: key, value: result[key] });
|
||||||
|
});
|
||||||
|
}, [result, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
dispatch(resetOAuth());
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SpinnerErrorButton
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={authorizing}
|
||||||
|
error={error}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OAuthInput;
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { resetOAuth, startOAuth } from 'Store/Actions/oAuthActions';
|
|
||||||
import OAuthInput from './OAuthInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.oAuth,
|
|
||||||
(oAuth) => {
|
|
||||||
return oAuth;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
startOAuth,
|
|
||||||
resetOAuth
|
|
||||||
};
|
|
||||||
|
|
||||||
class OAuthInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
result,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!result || result === prevProps.result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(result).forEach((key) => {
|
|
||||||
onChange({ name: key, value: result[key] });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
this.props.resetOAuth();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
provider,
|
|
||||||
providerData,
|
|
||||||
section
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.startOAuth({
|
|
||||||
name,
|
|
||||||
provider,
|
|
||||||
providerData,
|
|
||||||
section
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<OAuthInput
|
|
||||||
{...this.props}
|
|
||||||
onPress={this.onPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OAuthInputConnector.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
result: PropTypes.object,
|
|
||||||
provider: PropTypes.string.isRequired,
|
|
||||||
providerData: PropTypes.object.isRequired,
|
|
||||||
section: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
startOAuth: PropTypes.func.isRequired,
|
|
||||||
resetOAuth: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(OAuthInputConnector);
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
|
|
||||||
// Prevent a user from copying (or cutting) the password from the input
|
|
||||||
function onCopy(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.nativeEvent.stopImmediatePropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
function PasswordInput(props) {
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
{...props}
|
|
||||||
type="password"
|
|
||||||
onCopy={onCopy}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
|
||||||
...TextInput.props
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PasswordInput;
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React, { SyntheticEvent } from 'react';
|
||||||
|
import TextInput, { TextInputProps } from './TextInput';
|
||||||
|
|
||||||
|
// Prevent a user from copying (or cutting) the password from the input
|
||||||
|
function onCopy(e: SyntheticEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function PasswordInput(props: TextInputProps) {
|
||||||
|
return <TextInput {...props} type="password" onCopy={onCopy} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PasswordInput;
|
||||||
@@ -16,3 +16,7 @@
|
|||||||
|
|
||||||
height: 35px;
|
height: 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fileBrowserMiddleButton {
|
||||||
|
composes: middleButton from '~./FormInputButton.css';
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'fileBrowserButton': string;
|
'fileBrowserButton': string;
|
||||||
|
'fileBrowserMiddleButton': string;
|
||||||
'hasFileBrowser': string;
|
'hasFileBrowser': string;
|
||||||
'inputWrapper': string;
|
'inputWrapper': string;
|
||||||
'pathMatch': string;
|
'pathMatch': string;
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import AutoSuggestInput from './AutoSuggestInput';
|
|
||||||
import FormInputButton from './FormInputButton';
|
|
||||||
import styles from './PathInput.css';
|
|
||||||
|
|
||||||
class PathInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._node = document.getElementById('portal-root');
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
value: props.value,
|
|
||||||
isFileBrowserModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { value } = this.props;
|
|
||||||
|
|
||||||
if (prevProps.value !== value) {
|
|
||||||
this.setState({ value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
getSuggestionValue({ path }) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSuggestion({ path }, { query }) {
|
|
||||||
const lastSeparatorIndex = query.lastIndexOf('\\') || query.lastIndexOf('/');
|
|
||||||
|
|
||||||
if (lastSeparatorIndex === -1) {
|
|
||||||
return (
|
|
||||||
<span>{path}</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<span className={styles.pathMatch}>
|
|
||||||
{path.substr(0, lastSeparatorIndex)}
|
|
||||||
</span>
|
|
||||||
{path.substr(lastSeparatorIndex)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ value }) => {
|
|
||||||
this.setState({ value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputKeyDown = (event) => {
|
|
||||||
if (event.key === 'Tab') {
|
|
||||||
event.preventDefault();
|
|
||||||
const path = this.props.paths[0];
|
|
||||||
|
|
||||||
if (path) {
|
|
||||||
this.props.onChange({
|
|
||||||
name: this.props.name,
|
|
||||||
value: path.path
|
|
||||||
});
|
|
||||||
|
|
||||||
if (path.type !== 'file') {
|
|
||||||
this.props.onFetchPaths(path.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputBlur = () => {
|
|
||||||
this.props.onChange({
|
|
||||||
name: this.props.name,
|
|
||||||
value: this.state.value
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.onClearPaths();
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionsFetchRequested = ({ value }) => {
|
|
||||||
this.props.onFetchPaths(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionsClearRequested = () => {
|
|
||||||
// Required because props aren't always rendered, but no-op
|
|
||||||
// because we don't want to reset the paths after a path is selected.
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionSelected = (event, { suggestionValue }) => {
|
|
||||||
this.props.onFetchPaths(suggestionValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
onFileBrowserOpenPress = () => {
|
|
||||||
this.setState({ isFileBrowserModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFileBrowserModalClose = () => {
|
|
||||||
this.setState({ isFileBrowserModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
name,
|
|
||||||
paths,
|
|
||||||
includeFiles,
|
|
||||||
hasFileBrowser,
|
|
||||||
onChange,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
isFileBrowserModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<AutoSuggestInput
|
|
||||||
{...otherProps}
|
|
||||||
className={hasFileBrowser ? styles.hasFileBrowser : undefined}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
suggestions={paths}
|
|
||||||
getSuggestionValue={this.getSuggestionValue}
|
|
||||||
renderSuggestion={this.renderSuggestion}
|
|
||||||
onInputKeyDown={this.onInputKeyDown}
|
|
||||||
onInputBlur={this.onInputBlur}
|
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
hasFileBrowser &&
|
|
||||||
<div>
|
|
||||||
<FormInputButton
|
|
||||||
className={styles.fileBrowserButton}
|
|
||||||
onPress={this.onFileBrowserOpenPress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.FOLDER_OPEN} />
|
|
||||||
</FormInputButton>
|
|
||||||
|
|
||||||
<FileBrowserModal
|
|
||||||
isOpen={isFileBrowserModalOpen}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
includeFiles={includeFiles}
|
|
||||||
onChange={onChange}
|
|
||||||
onModalClose={this.onFileBrowserModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PathInput.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
paths: PropTypes.array.isRequired,
|
|
||||||
includeFiles: PropTypes.bool.isRequired,
|
|
||||||
hasFileBrowser: PropTypes.bool,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onFetchPaths: PropTypes.func.isRequired,
|
|
||||||
onClearPaths: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
PathInput.defaultProps = {
|
|
||||||
className: styles.inputWrapper,
|
|
||||||
value: '',
|
|
||||||
hasFileBrowser: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PathInput;
|
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {
|
||||||
|
KeyboardEvent,
|
||||||
|
SyntheticEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
SuggestionsFetchRequestedParams,
|
||||||
|
} from 'react-autosuggest';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import { Path } from 'App/State/PathsAppState';
|
||||||
|
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { clearPaths, fetchPaths } from 'Store/Actions/pathActions';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import AutoSuggestInput from './AutoSuggestInput';
|
||||||
|
import FormInputButton from './FormInputButton';
|
||||||
|
import styles from './PathInput.css';
|
||||||
|
|
||||||
|
export interface PathInputProps {
|
||||||
|
className?: string;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
includeFiles: boolean;
|
||||||
|
hasButton?: boolean;
|
||||||
|
hasFileBrowser?: boolean;
|
||||||
|
onChange: (change: InputChanged<string>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PathInputInternalProps extends PathInputProps {
|
||||||
|
paths: Path[];
|
||||||
|
onFetchPaths: (path: string) => void;
|
||||||
|
onClearPaths: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSuggestionsClearRequested() {
|
||||||
|
// Required because props aren't always rendered, but no-op
|
||||||
|
// because we don't want to reset the paths after a path is selected.
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPathsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.paths,
|
||||||
|
(paths) => {
|
||||||
|
const { currentPath, directories, files } = paths;
|
||||||
|
|
||||||
|
const filteredPaths = [...directories, ...files].filter(({ path }) => {
|
||||||
|
return path.toLowerCase().startsWith(currentPath.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredPaths;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PathInput(props: PathInputProps) {
|
||||||
|
const { includeFiles } = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const paths = useSelector(createPathsSelector());
|
||||||
|
|
||||||
|
const handleFetchPaths = useCallback(
|
||||||
|
(path: string) => {
|
||||||
|
dispatch(fetchPaths({ path, includeFiles }));
|
||||||
|
},
|
||||||
|
[includeFiles, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClearPaths = useCallback(() => {
|
||||||
|
dispatch(clearPaths);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PathInputInternal
|
||||||
|
{...props}
|
||||||
|
paths={paths}
|
||||||
|
onFetchPaths={handleFetchPaths}
|
||||||
|
onClearPaths={handleClearPaths}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PathInput;
|
||||||
|
|
||||||
|
export function PathInputInternal(props: PathInputInternalProps) {
|
||||||
|
const {
|
||||||
|
className = styles.inputWrapper,
|
||||||
|
name,
|
||||||
|
value: inputValue = '',
|
||||||
|
paths,
|
||||||
|
includeFiles,
|
||||||
|
hasButton,
|
||||||
|
hasFileBrowser = true,
|
||||||
|
onChange,
|
||||||
|
onFetchPaths,
|
||||||
|
onClearPaths,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [value, setValue] = useState(inputValue);
|
||||||
|
const [isFileBrowserModalOpen, setIsFileBrowserModalOpen] = useState(false);
|
||||||
|
const previousInputValue = usePrevious(inputValue);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleFetchPaths = useCallback(
|
||||||
|
(path: string) => {
|
||||||
|
dispatch(fetchPaths({ path, includeFiles }));
|
||||||
|
},
|
||||||
|
[includeFiles, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(_event: SyntheticEvent, { newValue }: ChangeEvent) => {
|
||||||
|
setValue(newValue);
|
||||||
|
},
|
||||||
|
[setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
event.preventDefault();
|
||||||
|
const path = paths[0];
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: path.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (path.type !== 'file') {
|
||||||
|
handleFetchPaths(path.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[name, paths, handleFetchPaths, onChange]
|
||||||
|
);
|
||||||
|
const handleInputBlur = useCallback(() => {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
onClearPaths();
|
||||||
|
}, [name, value, onClearPaths, onChange]);
|
||||||
|
|
||||||
|
const handleSuggestionSelected = useCallback(
|
||||||
|
(_event: SyntheticEvent, { suggestion }: { suggestion: Path }) => {
|
||||||
|
handleFetchPaths(suggestion.path);
|
||||||
|
},
|
||||||
|
[handleFetchPaths]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSuggestionsFetchRequested = useCallback(
|
||||||
|
({ value: newValue }: SuggestionsFetchRequestedParams) => {
|
||||||
|
handleFetchPaths(newValue);
|
||||||
|
},
|
||||||
|
[handleFetchPaths]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFileBrowserOpenPress = useCallback(() => {
|
||||||
|
setIsFileBrowserModalOpen(true);
|
||||||
|
}, [setIsFileBrowserModalOpen]);
|
||||||
|
|
||||||
|
const handleFileBrowserModalClose = useCallback(() => {
|
||||||
|
setIsFileBrowserModalOpen(false);
|
||||||
|
}, [setIsFileBrowserModalOpen]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(change: InputChanged<Path>) => {
|
||||||
|
onChange({ name, value: change.value.path });
|
||||||
|
},
|
||||||
|
[name, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSuggestionValue = useCallback(({ path }: Path) => path, []);
|
||||||
|
|
||||||
|
const renderSuggestion = useCallback(
|
||||||
|
({ path }: Path, { query }: { query: string }) => {
|
||||||
|
const lastSeparatorIndex =
|
||||||
|
query.lastIndexOf('\\') || query.lastIndexOf('/');
|
||||||
|
|
||||||
|
if (lastSeparatorIndex === -1) {
|
||||||
|
return <span>{path}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span className={styles.pathMatch}>
|
||||||
|
{path.substring(0, lastSeparatorIndex)}
|
||||||
|
</span>
|
||||||
|
{path.substring(lastSeparatorIndex)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputValue !== previousInputValue) {
|
||||||
|
setValue(inputValue);
|
||||||
|
}
|
||||||
|
}, [inputValue, previousInputValue, setValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<AutoSuggestInput
|
||||||
|
{...otherProps}
|
||||||
|
className={hasFileBrowser ? styles.hasFileBrowser : undefined}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
suggestions={paths}
|
||||||
|
getSuggestionValue={getSuggestionValue}
|
||||||
|
renderSuggestion={renderSuggestion}
|
||||||
|
onInputKeyDown={handleInputKeyDown}
|
||||||
|
onInputChange={handleInputChange}
|
||||||
|
onInputBlur={handleInputBlur}
|
||||||
|
onSuggestionSelected={handleSuggestionSelected}
|
||||||
|
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={handleSuggestionsClearRequested}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasFileBrowser ? (
|
||||||
|
<>
|
||||||
|
<FormInputButton
|
||||||
|
className={classNames(
|
||||||
|
styles.fileBrowserButton,
|
||||||
|
hasButton && styles.fileBrowserMiddleButton
|
||||||
|
)}
|
||||||
|
onPress={handleFileBrowserOpenPress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.FOLDER_OPEN} />
|
||||||
|
</FormInputButton>
|
||||||
|
|
||||||
|
<FileBrowserModal
|
||||||
|
isOpen={isFileBrowserModalOpen}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
includeFiles={includeFiles}
|
||||||
|
onChange={onChange}
|
||||||
|
onModalClose={handleFileBrowserModalClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearPaths, fetchPaths } from 'Store/Actions/pathActions';
|
|
||||||
import PathInput from './PathInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.paths,
|
|
||||||
(paths) => {
|
|
||||||
const {
|
|
||||||
currentPath,
|
|
||||||
directories,
|
|
||||||
files
|
|
||||||
} = paths;
|
|
||||||
|
|
||||||
const filteredPaths = _.filter([...directories, ...files], ({ path }) => {
|
|
||||||
return path.toLowerCase().startsWith(currentPath.toLowerCase());
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
paths: filteredPaths
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchPaths: fetchPaths,
|
|
||||||
dispatchClearPaths: clearPaths
|
|
||||||
};
|
|
||||||
|
|
||||||
class PathInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFetchPaths = (path) => {
|
|
||||||
const {
|
|
||||||
includeFiles,
|
|
||||||
dispatchFetchPaths
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchFetchPaths({
|
|
||||||
path,
|
|
||||||
includeFiles
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onClearPaths = () => {
|
|
||||||
this.props.dispatchClearPaths();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PathInput
|
|
||||||
onFetchPaths={this.onFetchPaths}
|
|
||||||
onClearPaths={this.onClearPaths}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PathInputConnector.propTypes = {
|
|
||||||
...PathInput.props,
|
|
||||||
includeFiles: PropTypes.bool.isRequired,
|
|
||||||
dispatchFetchPaths: PropTypes.func.isRequired,
|
|
||||||
dispatchClearPaths: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
PathInputConnector.defaultProps = {
|
|
||||||
includeFiles: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(PathInputConnector);
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
|
|
||||||
function getType({ type, selectOptionsProviderAction }) {
|
|
||||||
switch (type) {
|
|
||||||
case 'captcha':
|
|
||||||
return inputTypes.CAPTCHA;
|
|
||||||
case 'checkbox':
|
|
||||||
return inputTypes.CHECK;
|
|
||||||
case 'device':
|
|
||||||
return inputTypes.DEVICE;
|
|
||||||
case 'keyValueList':
|
|
||||||
return inputTypes.KEY_VALUE_LIST;
|
|
||||||
case 'password':
|
|
||||||
return inputTypes.PASSWORD;
|
|
||||||
case 'number':
|
|
||||||
return inputTypes.NUMBER;
|
|
||||||
case 'path':
|
|
||||||
return inputTypes.PATH;
|
|
||||||
case 'filePath':
|
|
||||||
return inputTypes.PATH;
|
|
||||||
case 'select':
|
|
||||||
if (selectOptionsProviderAction) {
|
|
||||||
return inputTypes.DYNAMIC_SELECT;
|
|
||||||
}
|
|
||||||
return inputTypes.SELECT;
|
|
||||||
case 'movieTag':
|
|
||||||
return inputTypes.MOVIE_TAG;
|
|
||||||
case 'tag':
|
|
||||||
return inputTypes.TEXT_TAG;
|
|
||||||
case 'tagSelect':
|
|
||||||
return inputTypes.TAG_SELECT;
|
|
||||||
case 'textbox':
|
|
||||||
return inputTypes.TEXT;
|
|
||||||
case 'oAuth':
|
|
||||||
return inputTypes.OAUTH;
|
|
||||||
case 'rootFolder':
|
|
||||||
return inputTypes.ROOT_FOLDER_SELECT;
|
|
||||||
case 'qualityProfile':
|
|
||||||
return inputTypes.QUALITY_PROFILE_SELECT;
|
|
||||||
default:
|
|
||||||
return inputTypes.TEXT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectValues(selectOptions) {
|
|
||||||
if (!selectOptions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.reduce(selectOptions, (result, option) => {
|
|
||||||
result.push({
|
|
||||||
key: option.value,
|
|
||||||
value: option.name,
|
|
||||||
dividerAfter: option.dividerAfter,
|
|
||||||
hint: option.hint
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProviderFieldFormGroup(props) {
|
|
||||||
const {
|
|
||||||
advancedSettings,
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
helpText,
|
|
||||||
helpTextWarning,
|
|
||||||
helpLink,
|
|
||||||
placeholder,
|
|
||||||
value,
|
|
||||||
type,
|
|
||||||
advanced,
|
|
||||||
hidden,
|
|
||||||
pending,
|
|
||||||
errors,
|
|
||||||
warnings,
|
|
||||||
selectOptions,
|
|
||||||
onChange,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
hidden === 'hidden' ||
|
|
||||||
(hidden === 'hiddenIfNotSet' && !value)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
isAdvanced={advanced}
|
|
||||||
>
|
|
||||||
<FormLabel>{label}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={getType(props)}
|
|
||||||
name={name}
|
|
||||||
label={label}
|
|
||||||
helpText={helpText}
|
|
||||||
helpTextWarning={helpTextWarning}
|
|
||||||
helpLink={helpLink}
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={value}
|
|
||||||
values={getSelectValues(selectOptions)}
|
|
||||||
errors={errors}
|
|
||||||
warnings={warnings}
|
|
||||||
pending={pending}
|
|
||||||
includeFiles={type === 'filePath' ? true : undefined}
|
|
||||||
onChange={onChange}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectOptionsShape = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.number.isRequired,
|
|
||||||
hint: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
ProviderFieldFormGroup.propTypes = {
|
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
helpText: PropTypes.string,
|
|
||||||
helpTextWarning: PropTypes.string,
|
|
||||||
helpLink: PropTypes.string,
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
value: PropTypes.any,
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
advanced: PropTypes.bool.isRequired,
|
|
||||||
hidden: PropTypes.string,
|
|
||||||
isDisabled: PropTypes.bool,
|
|
||||||
provider: PropTypes.string,
|
|
||||||
pending: PropTypes.bool.isRequired,
|
|
||||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)),
|
|
||||||
selectOptionsProviderAction: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ProviderFieldFormGroup.defaultProps = {
|
|
||||||
advancedSettings: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProviderFieldFormGroup;
|
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import { FieldSelectOption } from 'typings/Field';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import { Failure } from 'typings/pending';
|
||||||
|
|
||||||
|
interface ProviderFieldFormGroupProps<T> {
|
||||||
|
advancedSettings: boolean;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
helpText?: string;
|
||||||
|
helpTextWarning?: string;
|
||||||
|
helpLink?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
value?: T;
|
||||||
|
type: string;
|
||||||
|
advanced: boolean;
|
||||||
|
hidden?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
provider?: string;
|
||||||
|
providerData?: object;
|
||||||
|
pending: boolean;
|
||||||
|
errors: Failure[];
|
||||||
|
warnings: Failure[];
|
||||||
|
selectOptions?: FieldSelectOption<T>[];
|
||||||
|
selectOptionsProviderAction?: string;
|
||||||
|
onChange: (change: InputChanged<T>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProviderFieldFormGroup<T>({
|
||||||
|
advancedSettings = false,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
helpText,
|
||||||
|
helpTextWarning,
|
||||||
|
helpLink,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
type: providerType,
|
||||||
|
advanced,
|
||||||
|
hidden,
|
||||||
|
pending,
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
selectOptions,
|
||||||
|
selectOptionsProviderAction,
|
||||||
|
onChange,
|
||||||
|
...otherProps
|
||||||
|
}: ProviderFieldFormGroupProps<T>) {
|
||||||
|
const type = useMemo(() => {
|
||||||
|
switch (providerType) {
|
||||||
|
case 'captcha':
|
||||||
|
return 'captcha';
|
||||||
|
case 'checkbox':
|
||||||
|
return 'check';
|
||||||
|
case 'device':
|
||||||
|
return 'device';
|
||||||
|
case 'keyValueList':
|
||||||
|
return 'keyValueList';
|
||||||
|
case 'password':
|
||||||
|
return 'password';
|
||||||
|
case 'number':
|
||||||
|
return 'number';
|
||||||
|
case 'path':
|
||||||
|
return 'path';
|
||||||
|
case 'filePath':
|
||||||
|
return 'path';
|
||||||
|
case 'select':
|
||||||
|
if (selectOptionsProviderAction) {
|
||||||
|
return 'dynamicSelect';
|
||||||
|
}
|
||||||
|
return 'select';
|
||||||
|
case 'movieTag':
|
||||||
|
return 'movieTag';
|
||||||
|
case 'tag':
|
||||||
|
return 'textTag';
|
||||||
|
case 'tagSelect':
|
||||||
|
return 'tagSelect';
|
||||||
|
case 'textbox':
|
||||||
|
return 'text';
|
||||||
|
case 'oAuth':
|
||||||
|
return 'oauth';
|
||||||
|
case 'rootFolder':
|
||||||
|
return 'rootFolderSelect';
|
||||||
|
case 'qualityProfile':
|
||||||
|
return 'qualityProfileSelect';
|
||||||
|
default:
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
}, [providerType, selectOptionsProviderAction]);
|
||||||
|
|
||||||
|
const selectValues = useMemo(() => {
|
||||||
|
if (!selectOptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectOptions.map((option) => {
|
||||||
|
return {
|
||||||
|
key: option.value,
|
||||||
|
value: option.name,
|
||||||
|
hint: option.hint,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [selectOptions]);
|
||||||
|
|
||||||
|
if (hidden === 'hidden' || (hidden === 'hiddenIfNotSet' && !value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup advancedSettings={advancedSettings} isAdvanced={advanced}>
|
||||||
|
<FormLabel>{label}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={type}
|
||||||
|
name={name}
|
||||||
|
helpText={helpText}
|
||||||
|
helpTextWarning={helpTextWarning}
|
||||||
|
helpLink={helpLink}
|
||||||
|
placeholder={placeholder}
|
||||||
|
// @ts-expect-error - this isn't available on all types
|
||||||
|
selectOptionsProviderAction={selectOptionsProviderAction}
|
||||||
|
value={value}
|
||||||
|
values={selectValues}
|
||||||
|
errors={errors}
|
||||||
|
warnings={warnings}
|
||||||
|
pending={pending}
|
||||||
|
includeFiles={providerType === 'filePath' ? true : undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProviderFieldFormGroup;
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')),
|
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
|
||||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
|
||||||
(state, { includeMixed }) => includeMixed,
|
|
||||||
(qualityProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed) => {
|
|
||||||
const values = _.map(qualityProfiles.items, (qualityProfile) => {
|
|
||||||
return {
|
|
||||||
key: qualityProfile.id,
|
|
||||||
value: qualityProfile.name
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (includeNoChange) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'noChange',
|
|
||||||
value: translate('NoChange'),
|
|
||||||
isDisabled: includeNoChangeDisabled
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMixed) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'mixed',
|
|
||||||
value: `(${translate('Mixed')})`,
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
values
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class QualityProfileSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
values
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
|
|
||||||
const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
|
|
||||||
|
|
||||||
if (firstValue) {
|
|
||||||
this.onChange({ name, value: firstValue.key });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityProfileSelectInputConnector.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QualityProfileSelectInputConnector.defaultProps = {
|
|
||||||
includeNoChange: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(QualityProfileSelectInputConnector);
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
import RootFolderSelectInputOption from './RootFolderSelectInputOption';
|
|
||||||
import RootFolderSelectInputSelectedValue from './RootFolderSelectInputSelectedValue';
|
|
||||||
|
|
||||||
class RootFolderSelectInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAddNewRootFolderModalOpen: false,
|
|
||||||
newRootFolderPath: ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newRootFolderPath = this.state.newRootFolderPath;
|
|
||||||
|
|
||||||
if (
|
|
||||||
prevProps.isSaving &&
|
|
||||||
!isSaving &&
|
|
||||||
!saveError &&
|
|
||||||
newRootFolderPath
|
|
||||||
) {
|
|
||||||
onChange({ name, value: newRootFolderPath });
|
|
||||||
this.setState({ newRootFolderPath: '' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
if (value === 'addNew') {
|
|
||||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
|
||||||
} else {
|
|
||||||
this.props.onChange({ name, value });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onNewRootFolderSelect = ({ value }) => {
|
|
||||||
this.setState({ newRootFolderPath: value }, () => {
|
|
||||||
this.props.onNewRootFolderSelect(value);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddRootFolderModalClose = () => {
|
|
||||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
includeNoChange,
|
|
||||||
onNewRootFolderSelect,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...otherProps}
|
|
||||||
selectedValueComponent={RootFolderSelectInputSelectedValue}
|
|
||||||
optionComponent={RootFolderSelectInputOption}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FileBrowserModal
|
|
||||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
|
||||||
name="rootFolderPath"
|
|
||||||
value=""
|
|
||||||
onChange={this.onNewRootFolderSelect}
|
|
||||||
onModalClose={this.onAddRootFolderModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RootFolderSelectInput.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
RootFolderSelectInput.defaultProps = {
|
|
||||||
includeNoChange: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RootFolderSelectInput;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user