Convert Collection Footer to TypeScript

This commit is contained in:
Bogdan 2025-04-18 16:02:31 +03:00
parent 3a55316ada
commit 1d1aca1a04
6 changed files with 359 additions and 361 deletions

View file

@ -20,7 +20,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
import CollectionFooter from './CollectionFooter';
import CollectionFilterMenu from './Menus/CollectionFilterMenu';
import CollectionSortMenu from './Menus/CollectionSortMenu';
import NoCollection from './NoCollection';
import NoCollections from './NoCollections';
import CollectionOverviewsConnector from './Overview/CollectionOverviewsConnector';
import CollectionOverviewOptionsModal from './Overview/Options/CollectionOverviewOptionsModal';
@ -341,7 +341,7 @@ class Collection extends Component {
{
!error && isPopulated && !items.length &&
<NoCollection totalItems={totalItems} />
<NoCollections totalItems={totalItems} />
}
</PageContentBody>

View file

@ -1,300 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormInputGroup from 'Components/Form/FormInputGroup';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { inputTypes, 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');
}
},
{
key: 'monitored',
get value() {
return translate('Monitored');
}
},
{
key: 'unmonitored',
get value() {
return translate('Unmonitored');
}
}
];
const searchOnAddOptions = [
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
}
},
{
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}
/>
<FormInputGroup
type={inputTypes.SELECT}
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}
/>
<FormInputGroup
type={inputTypes.SELECT}
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}
/>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
includeNoChangeDisabled={false}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</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={this.onInputChange}
/>
</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={this.onInputChange}
/>
</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={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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,13 +1,14 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './NoCollection.css';
function NoCollection(props) {
const { totalItems } = props;
interface NoCollectionsProps {
totalItems: number;
}
function NoCollections({ totalItems }: NoCollectionsProps) {
if (totalItems > 0) {
return (
<div>
@ -20,24 +21,16 @@ function NoCollection(props) {
return (
<div>
<div className={styles.message}>
{translate('NoCollections')}
</div>
<div className={styles.message}>{translate('NoCollections')}</div>
<div className={styles.buttonContainer}>
<Button
to="/add/import"
kind={kinds.PRIMARY}
>
<Button to="/add/import" kind={kinds.PRIMARY}>
{translate('ImportExistingMovies')}
</Button>
</div>
<div className={styles.buttonContainer}>
<Button
to="/add/new"
kind={kinds.PRIMARY}
>
<Button to="/add/new" kind={kinds.PRIMARY}>
{translate('AddNewMovie')}
</Button>
</div>
@ -45,8 +38,4 @@ function NoCollection(props) {
);
}
NoCollection.propTypes = {
totalItems: PropTypes.number.isRequired
};
export default NoCollection;
export default NoCollections;