Convert Preview Rename to TypeScript

pull/7640/head
Mark McDowall 2 months ago
parent 32ce09648c
commit a2fd23c84d
No known key found for this signature in database

@ -12,6 +12,7 @@ import EpisodesAppState from './EpisodesAppState';
import HistoryAppState from './HistoryAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import OAuthAppState from './OAuthAppState';
import OrganizePreviewAppState from './OrganizePreviewAppState';
import ParseAppState from './ParseAppState';
import PathsAppState from './PathsAppState';
import ProviderOptionsAppState from './ProviderOptionsAppState';
@ -90,6 +91,7 @@ interface AppState {
history: HistoryAppState;
interactiveImport: InteractiveImportAppState;
oAuth: OAuthAppState;
organizePreview: OrganizePreviewAppState;
parse: ParseAppState;
paths: PathsAppState;
providerOptions: ProviderOptionsAppState;

@ -0,0 +1,15 @@
import ModelBase from 'App/ModelBase';
import AppSectionState from 'App/State/AppSectionState';
export interface OrganizePreviewModel extends ModelBase {
seriesId: number;
seasonNumber: number;
episodeNumbers: number[];
episodeFileId: number;
existingPath: string;
newPath: string;
}
type OrganizePreviewAppState = AppSectionState<OrganizePreviewModel>;
export default OrganizePreviewAppState;

@ -17,7 +17,7 @@ interface CheckInputProps {
name: string;
checkedValue?: boolean;
uncheckedValue?: boolean;
value?: string | boolean;
value?: string | boolean | null;
helpText?: string;
helpTextWarning?: string;
isDisabled?: boolean;

@ -1,34 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import OrganizePreviewModalContentConnector from './OrganizePreviewModalContentConnector';
function OrganizePreviewModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
{
isOpen &&
<OrganizePreviewModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
}
</Modal>
);
}
OrganizePreviewModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default OrganizePreviewModal;

@ -0,0 +1,37 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions';
import OrganizePreviewModalContent, {
OrganizePreviewModalContentProps,
} from './OrganizePreviewModalContent';
interface OrganizePreviewModalProps extends OrganizePreviewModalContentProps {
isOpen: boolean;
onModalClose: () => void;
}
function OrganizePreviewModal({
isOpen,
onModalClose,
...otherProps
}: OrganizePreviewModalProps) {
const dispatch = useDispatch();
const handleOnModalClose = useCallback(() => {
dispatch(clearOrganizePreview());
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal isOpen={isOpen} onModalClose={handleOnModalClose}>
{isOpen ? (
<OrganizePreviewModalContent
{...otherProps}
onModalClose={handleOnModalClose}
/>
) : null}
</Modal>
);
}
export default OrganizePreviewModal;

@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions';
import OrganizePreviewModal from './OrganizePreviewModal';
const mapDispatchToProps = {
clearOrganizePreview
};
class OrganizePreviewModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearOrganizePreview();
this.props.onModalClose();
};
//
// Render
render() {
return (
<OrganizePreviewModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
OrganizePreviewModalConnector.propTypes = {
clearOrganizePreview: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(OrganizePreviewModalConnector);

@ -1,202 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import CheckInput from 'Components/Form/CheckInput';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
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 { kinds } from 'Helpers/Props';
import formatSeason from 'Season/formatSeason';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import OrganizePreviewRow from './OrganizePreviewRow';
import styles from './OrganizePreviewModalContent.css';
function getValue(allSelected, allUnselected) {
if (allSelected) {
return true;
} else if (allUnselected) {
return false;
}
return null;
}
class OrganizePreviewModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
};
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
onOrganizePress = () => {
this.props.onOrganizePress(this.getSelectedIds());
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
seasonNumber,
renameEpisodes,
episodeFormat,
path,
onModalClose
} = this.props;
const {
allSelected,
allUnselected,
selectedState
} = this.state;
const selectAllValue = getValue(allSelected, allUnselected);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{ seasonNumber == null ?
translate('OrganizeModalHeader') :
translate('OrganizeModalHeaderSeason', { season: formatSeason(seasonNumber) })
}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
}
{
!isFetching && isPopulated && !items.length &&
<div>
{
renameEpisodes ?
<div>{translate('OrganizeNothingToRename')}</div> :
<div>{translate('OrganizeRenamingDisabled')}</div>
}
</div>
}
{
!isFetching && isPopulated && !!items.length &&
<div>
<Alert>
<div>
<InlineMarkdown data={translate('OrganizeRelativePaths', { path })} blockClassName={styles.path} />
</div>
<div>
<InlineMarkdown data={translate('OrganizeNamingPattern', { episodeFormat })} blockClassName={styles.episodeFormat} />
</div>
</Alert>
<div className={styles.previews}>
{
items.map((item) => {
return (
<OrganizePreviewRow
key={item.episodeFileId}
id={item.episodeFileId}
existingPath={item.existingPath}
newPath={item.newPath}
isSelected={selectedState[item.episodeFileId]}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</div>
</div>
}
</ModalBody>
<ModalFooter>
{
isPopulated && !!items.length &&
<CheckInput
className={styles.selectAllInput}
containerClassName={styles.selectAllInputContainer}
name="selectAll"
value={selectAllValue}
onChange={this.onSelectAllChange}
/>
}
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={this.onOrganizePress}
>
{translate('Organize')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
OrganizePreviewModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
seasonNumber: PropTypes.number,
path: PropTypes.string.isRequired,
renameEpisodes: PropTypes.bool,
episodeFormat: PropTypes.string,
onOrganizePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default OrganizePreviewModalContent;

@ -0,0 +1,201 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import CheckInput from 'Components/Form/CheckInput';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
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 useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import formatSeason from 'Season/formatSeason';
import useSeries from 'Series/useSeries';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions';
import { fetchNamingSettings } from 'Store/Actions/settingsActions';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import OrganizePreviewRow from './OrganizePreviewRow';
import styles from './OrganizePreviewModalContent.css';
function getValue(allSelected: boolean, allUnselected: boolean) {
if (allSelected) {
return true;
} else if (allUnselected) {
return false;
}
return null;
}
export interface OrganizePreviewModalContentProps {
seriesId: number;
seasonNumber?: number;
onModalClose: () => void;
}
function OrganizePreviewModalContent({
seriesId,
seasonNumber,
onModalClose,
}: OrganizePreviewModalContentProps) {
const dispatch = useDispatch();
const {
items,
isFetching: isPreviewFetching,
isPopulated: isPreviewPopulated,
error: previewError,
} = useSelector((state: AppState) => state.organizePreview);
const {
isFetching: isNamingFetching,
isPopulated: isNamingPopulated,
error: namingError,
item: naming,
} = useSelector((state: AppState) => state.settings.naming);
const series = useSeries(seriesId)!;
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const isFetching = isPreviewFetching || isNamingFetching;
const isPopulated = isPreviewPopulated && isNamingPopulated;
const error = previewError || namingError;
const { renameEpisodes } = naming;
const episodeFormat = naming[`${series.seriesType}EpisodeFormat`];
const selectAllValue = getValue(allSelected, allUnselected);
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const handleSelectedChange = useCallback(
({ id, value, shiftKey = false }: SelectStateInputProps) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const handleOrganizePress = useCallback(() => {
const files = getSelectedIds(selectedState);
dispatch(
executeCommand({
name: commandNames.RENAME_FILES,
files,
seriesId,
})
);
onModalClose();
}, [seriesId, selectedState, dispatch, onModalClose]);
useEffect(() => {
dispatch(fetchOrganizePreview({ seriesId, seasonNumber }));
dispatch(fetchNamingSettings());
}, [seriesId, seasonNumber, dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{seasonNumber == null
? translate('OrganizeModalHeader')
: translate('OrganizeModalHeaderSeason', {
season: formatSeason(seasonNumber) ?? '',
})}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
) : null}
{!isFetching && isPopulated && !items.length ? (
<div>
{renameEpisodes ? (
<div>{translate('OrganizeNothingToRename')}</div>
) : (
<div>{translate('OrganizeRenamingDisabled')}</div>
)}
</div>
) : null}
{!isFetching && isPopulated && items.length ? (
<div>
<Alert>
<div>
<InlineMarkdown
data={translate('OrganizeRelativePaths', {
path: series.path,
})}
blockClassName={styles.path}
/>
</div>
<div>
<InlineMarkdown
data={translate('OrganizeNamingPattern', { episodeFormat })}
blockClassName={styles.episodeFormat}
/>
</div>
</Alert>
<div className={styles.previews}>
{items.map((item) => {
return (
<OrganizePreviewRow
key={item.episodeFileId}
id={item.episodeFileId}
existingPath={item.existingPath}
newPath={item.newPath}
isSelected={selectedState[item.episodeFileId]}
onSelectedChange={handleSelectedChange}
/>
);
})}
</div>
</div>
) : null}
</ModalBody>
<ModalFooter>
{isPopulated && items.length ? (
<CheckInput
className={styles.selectAllInput}
containerClassName={styles.selectAllInputContainer}
name="selectAll"
value={selectAllValue}
onChange={handleSelectAllChange}
/>
) : null}
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.PRIMARY} onPress={handleOrganizePress}>
{translate('Organize')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default OrganizePreviewModalContent;

@ -1,91 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions';
import { fetchNamingSettings } from 'Store/Actions/settingsActions';
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
import OrganizePreviewModalContent from './OrganizePreviewModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.organizePreview,
(state) => state.settings.naming,
createSeriesSelector(),
(organizePreview, naming, series) => {
const props = { ...organizePreview };
props.isFetching = organizePreview.isFetching || naming.isFetching;
props.isPopulated = organizePreview.isPopulated && naming.isPopulated;
props.error = organizePreview.error || naming.error;
props.renameEpisodes = naming.item.renameEpisodes;
props.episodeFormat = naming.item[`${series.seriesType}EpisodeFormat`];
props.path = series.path;
return props;
}
);
}
const mapDispatchToProps = {
fetchOrganizePreview,
fetchNamingSettings,
executeCommand
};
class OrganizePreviewModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
seriesId,
seasonNumber
} = this.props;
this.props.fetchOrganizePreview({
seriesId,
seasonNumber
});
this.props.fetchNamingSettings();
}
//
// Listeners
onOrganizePress = (files) => {
this.props.executeCommand({
name: commandNames.RENAME_FILES,
seriesId: this.props.seriesId,
files
});
this.props.onModalClose();
};
//
// Render
render() {
return (
<OrganizePreviewModalContent
{...this.props}
onOrganizePress={this.onOrganizePress}
/>
);
}
}
OrganizePreviewModalContentConnector.propTypes = {
seriesId: PropTypes.number.isRequired,
seasonNumber: PropTypes.number,
fetchOrganizePreview: PropTypes.func.isRequired,
fetchNamingSettings: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(OrganizePreviewModalContentConnector);

@ -1,90 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CheckInput from 'Components/Form/CheckInput';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import styles from './OrganizePreviewRow.css';
class OrganizePreviewRow extends Component {
//
// Lifecycle
componentDidMount() {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value: true });
}
//
// Listeners
onSelectedChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
};
//
// Render
render() {
const {
id,
existingPath,
newPath,
isSelected
} = this.props;
return (
<div className={styles.row}>
<CheckInput
containerClassName={styles.selectedContainer}
name={id.toString()}
value={isSelected}
onChange={this.onSelectedChange}
/>
<div>
<div>
<Icon
name={icons.SUBTRACT}
kind={kinds.DANGER}
/>
<span className={styles.path}>
{existingPath}
</span>
</div>
<div>
<Icon
name={icons.ADD}
kind={kinds.SUCCESS}
/>
<span className={styles.path}>
{newPath}
</span>
</div>
</div>
</div>
);
}
}
OrganizePreviewRow.propTypes = {
id: PropTypes.number.isRequired,
existingPath: PropTypes.string.isRequired,
newPath: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
export default OrganizePreviewRow;

@ -0,0 +1,61 @@
import React, { useCallback, useEffect } from 'react';
import CheckInput from 'Components/Form/CheckInput';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import styles from './OrganizePreviewRow.css';
interface OrganizePreviewRowProps {
id: number;
existingPath: string;
newPath: string;
isSelected?: boolean;
onSelectedChange: (props: SelectStateInputProps) => void;
}
function OrganizePreviewRow({
id,
existingPath,
newPath,
isSelected,
onSelectedChange,
}: OrganizePreviewRowProps) {
const handleSelectedChange = useCallback(
({ value, shiftKey }: CheckInputChanged) => {
onSelectedChange({ id, value, shiftKey });
},
[id, onSelectedChange]
);
useEffect(() => {
onSelectedChange({ id, value: true, shiftKey: false });
}, [id, onSelectedChange]);
return (
<div className={styles.row}>
<CheckInput
containerClassName={styles.selectedContainer}
name={id.toString()}
value={isSelected}
onChange={handleSelectedChange}
/>
<div>
<div>
<Icon name={icons.SUBTRACT} kind={kinds.DANGER} />
<span className={styles.path}>{existingPath}</span>
</div>
<div>
<Icon name={icons.ADD} kind={kinds.SUCCESS} />
<span className={styles.path}>{newPath}</span>
</div>
</div>
</div>
);
}
export default OrganizePreviewRow;

@ -22,7 +22,7 @@ import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { align, icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import OrganizePreviewModal from 'Organize/OrganizePreviewModal';
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
import EditSeriesModal from 'Series/Edit/EditSeriesModal';
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
@ -682,7 +682,7 @@ class SeriesDetails extends Component {
</div>
<OrganizePreviewModalConnector
<OrganizePreviewModal
isOpen={isOrganizeModalOpen}
seriesId={id}
onModalClose={this.onOrganizeModalClose}

@ -16,7 +16,7 @@ import TableBody from 'Components/Table/TableBody';
import Popover from 'Components/Tooltip/Popover';
import { align, icons, sortDirections, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import OrganizePreviewModal from 'Organize/OrganizePreviewModal';
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
import SeasonInteractiveSearchModal from 'Series/Search/SeasonInteractiveSearchModal';
import isAfter from 'Utilities/Date/isAfter';
@ -475,7 +475,7 @@ class SeriesDetailsSeason extends Component {
}
</div>
<OrganizePreviewModalConnector
<OrganizePreviewModal
isOpen={isOrganizeModalOpen}
seriesId={seriesId}
seasonNumber={seasonNumber}

Loading…
Cancel
Save