From a2fd23c84d0a9d01864119d2e643970845c9e49e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 22 Dec 2024 21:46:10 -0800 Subject: [PATCH] Convert Preview Rename to TypeScript --- frontend/src/App/State/AppState.ts | 2 + .../src/App/State/OrganizePreviewAppState.ts | 15 ++ frontend/src/Components/Form/CheckInput.tsx | 2 +- frontend/src/Organize/OrganizePreviewModal.js | 34 --- .../src/Organize/OrganizePreviewModal.tsx | 37 ++++ .../Organize/OrganizePreviewModalConnector.js | 39 ---- .../Organize/OrganizePreviewModalContent.js | 202 ------------------ .../Organize/OrganizePreviewModalContent.tsx | 201 +++++++++++++++++ .../OrganizePreviewModalContentConnector.js | 91 -------- frontend/src/Organize/OrganizePreviewRow.js | 90 -------- frontend/src/Organize/OrganizePreviewRow.tsx | 61 ++++++ frontend/src/Series/Details/SeriesDetails.js | 4 +- .../src/Series/Details/SeriesDetailsSeason.js | 4 +- 13 files changed, 321 insertions(+), 461 deletions(-) create mode 100644 frontend/src/App/State/OrganizePreviewAppState.ts delete mode 100644 frontend/src/Organize/OrganizePreviewModal.js create mode 100644 frontend/src/Organize/OrganizePreviewModal.tsx delete mode 100644 frontend/src/Organize/OrganizePreviewModalConnector.js delete mode 100644 frontend/src/Organize/OrganizePreviewModalContent.js create mode 100644 frontend/src/Organize/OrganizePreviewModalContent.tsx delete mode 100644 frontend/src/Organize/OrganizePreviewModalContentConnector.js delete mode 100644 frontend/src/Organize/OrganizePreviewRow.js create mode 100644 frontend/src/Organize/OrganizePreviewRow.tsx diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index e34a745cc..e3183833b 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -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; diff --git a/frontend/src/App/State/OrganizePreviewAppState.ts b/frontend/src/App/State/OrganizePreviewAppState.ts new file mode 100644 index 000000000..7f1f62370 --- /dev/null +++ b/frontend/src/App/State/OrganizePreviewAppState.ts @@ -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; + +export default OrganizePreviewAppState; diff --git a/frontend/src/Components/Form/CheckInput.tsx b/frontend/src/Components/Form/CheckInput.tsx index b7080cfdd..6f5dd0242 100644 --- a/frontend/src/Components/Form/CheckInput.tsx +++ b/frontend/src/Components/Form/CheckInput.tsx @@ -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; diff --git a/frontend/src/Organize/OrganizePreviewModal.js b/frontend/src/Organize/OrganizePreviewModal.js deleted file mode 100644 index 647f4ddf8..000000000 --- a/frontend/src/Organize/OrganizePreviewModal.js +++ /dev/null @@ -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 ( - - { - isOpen && - - } - - ); -} - -OrganizePreviewModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default OrganizePreviewModal; diff --git a/frontend/src/Organize/OrganizePreviewModal.tsx b/frontend/src/Organize/OrganizePreviewModal.tsx new file mode 100644 index 000000000..4160a5885 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModal.tsx @@ -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 ( + + {isOpen ? ( + + ) : null} + + ); +} +export default OrganizePreviewModal; diff --git a/frontend/src/Organize/OrganizePreviewModalConnector.js b/frontend/src/Organize/OrganizePreviewModalConnector.js deleted file mode 100644 index 4abcaf842..000000000 --- a/frontend/src/Organize/OrganizePreviewModalConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -OrganizePreviewModalConnector.propTypes = { - clearOrganizePreview: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(undefined, mapDispatchToProps)(OrganizePreviewModalConnector); diff --git a/frontend/src/Organize/OrganizePreviewModalContent.js b/frontend/src/Organize/OrganizePreviewModalContent.js deleted file mode 100644 index 6bb35bc79..000000000 --- a/frontend/src/Organize/OrganizePreviewModalContent.js +++ /dev/null @@ -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 ( - - - { seasonNumber == null ? - translate('OrganizeModalHeader') : - translate('OrganizeModalHeaderSeason', { season: formatSeason(seasonNumber) }) - } - - - - { - isFetching && - - } - - { - !isFetching && error && - {translate('OrganizeLoadError')} - } - - { - !isFetching && isPopulated && !items.length && -
- { - renameEpisodes ? -
{translate('OrganizeNothingToRename')}
: -
{translate('OrganizeRenamingDisabled')}
- } -
- } - - { - !isFetching && isPopulated && !!items.length && -
- -
- -
- -
- -
-
- -
- { - items.map((item) => { - return ( - - ); - }) - } -
-
- } -
- - - { - isPopulated && !!items.length && - - } - - - - - -
- ); - } -} - -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; diff --git a/frontend/src/Organize/OrganizePreviewModalContent.tsx b/frontend/src/Organize/OrganizePreviewModalContent.tsx new file mode 100644 index 000000000..0a2684a0e --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModalContent.tsx @@ -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 ( + + + {seasonNumber == null + ? translate('OrganizeModalHeader') + : translate('OrganizeModalHeaderSeason', { + season: formatSeason(seasonNumber) ?? '', + })} + + + + {isFetching ? : null} + + {!isFetching && error ? ( + {translate('OrganizeLoadError')} + ) : null} + + {!isFetching && isPopulated && !items.length ? ( +
+ {renameEpisodes ? ( +
{translate('OrganizeNothingToRename')}
+ ) : ( +
{translate('OrganizeRenamingDisabled')}
+ )} +
+ ) : null} + + {!isFetching && isPopulated && items.length ? ( +
+ +
+ +
+ +
+ +
+
+ +
+ {items.map((item) => { + return ( + + ); + })} +
+
+ ) : null} +
+ + + {isPopulated && items.length ? ( + + ) : null} + + + + + +
+ ); +} + +export default OrganizePreviewModalContent; diff --git a/frontend/src/Organize/OrganizePreviewModalContentConnector.js b/frontend/src/Organize/OrganizePreviewModalContentConnector.js deleted file mode 100644 index e592f2a65..000000000 --- a/frontend/src/Organize/OrganizePreviewModalContentConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -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); diff --git a/frontend/src/Organize/OrganizePreviewRow.js b/frontend/src/Organize/OrganizePreviewRow.js deleted file mode 100644 index 00040ba3e..000000000 --- a/frontend/src/Organize/OrganizePreviewRow.js +++ /dev/null @@ -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 ( -
- - -
-
- - - - {existingPath} - -
- -
- - - - {newPath} - -
-
-
- ); - } -} - -OrganizePreviewRow.propTypes = { - id: PropTypes.number.isRequired, - existingPath: PropTypes.string.isRequired, - newPath: PropTypes.string.isRequired, - isSelected: PropTypes.bool, - onSelectedChange: PropTypes.func.isRequired -}; - -export default OrganizePreviewRow; diff --git a/frontend/src/Organize/OrganizePreviewRow.tsx b/frontend/src/Organize/OrganizePreviewRow.tsx new file mode 100644 index 000000000..398ea31ea --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewRow.tsx @@ -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 ( +
+ + +
+
+ + + {existingPath} +
+ +
+ + + {newPath} +
+
+
+ ); +} + +export default OrganizePreviewRow; diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index d416f4792..c30e55900 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -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 { - -