diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index c0dcecc8a..cbf224c77 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -23,7 +23,7 @@ import Profiles from 'Settings/Profiles/Profiles'; import QualityConnector from 'Settings/Quality/QualityConnector'; import Settings from 'Settings/Settings'; import TagSettings from 'Settings/Tags/TagSettings'; -import UISettingsConnector from 'Settings/UI/UISettingsConnector'; +import UISettings from 'Settings/UI/UISettings'; import Backups from 'System/Backup/Backups'; import LogsTable from 'System/Events/LogsTable'; import Logs from 'System/Logs/Logs'; @@ -137,7 +137,7 @@ function AppRoutes() { - + {/* System diff --git a/frontend/src/Components/Form/Form.tsx b/frontend/src/Components/Form/Form.tsx index d522019e7..055c8f80a 100644 --- a/frontend/src/Components/Form/Form.tsx +++ b/frontend/src/Components/Form/Form.tsx @@ -5,18 +5,20 @@ import { ValidationError, ValidationWarning } from 'typings/pending'; import styles from './Form.css'; export interface FormProps { + id?: string; children: ReactNode; validationErrors?: ValidationError[]; validationWarnings?: ValidationWarning[]; } function Form({ + id, children, validationErrors = [], validationWarnings = [], }: FormProps) { return ( -
+
{validationErrors.length || validationWarnings.length ? (
{validationErrors.map((error, index) => { diff --git a/frontend/src/Components/Form/Select/LanguageSelectInput.tsx b/frontend/src/Components/Form/Select/LanguageSelectInput.tsx index 3c9bbc150..61bfbf586 100644 --- a/frontend/src/Components/Form/Select/LanguageSelectInput.tsx +++ b/frontend/src/Components/Form/Select/LanguageSelectInput.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import Language from 'Language/Language'; -import createFilteredLanguagesSelector from 'Store/Selectors/createFilteredLanguagesSelector'; +import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector'; import translate from 'Utilities/String/translate'; import EnhancedSelectInput, { EnhancedSelectInputValue, @@ -29,7 +29,13 @@ export default function LanguageSelectInput({ onChange, ...otherProps }: LanguageSelectInputProps) { - const { items } = useSelector(createFilteredLanguagesSelector(true)); + const { items } = useSelector( + createLanguagesSelector({ + Any: true, + Original: true, + Unknown: true, + }) + ); const values = useMemo(() => { const result: EnhancedSelectInputValue[] = items.map( diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.tsx b/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.tsx index a0ae1e4dc..0d1f668e6 100644 --- a/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.tsx +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.tsx @@ -1,7 +1,5 @@ import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import { LanguageSettingsAppState } from 'App/State/SettingsAppState'; import Alert from 'Components/Alert'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; @@ -26,30 +24,14 @@ interface SelectLanguageModalContentProps { onModalClose(): void; } -function createFilteredLanguagesSelector() { - return createSelector(createLanguagesSelector(), (languages) => { - const { isFetching, isPopulated, error, items } = - languages as LanguageSettingsAppState; - - const filterItems = ['Any', 'Original']; - const filteredLanguages = items.filter( - (lang: Language) => !filterItems.includes(lang.name) - ); - - return { - isFetching, - isPopulated, - error, - items: filteredLanguages, - }; - }); -} - function SelectLanguageModalContent(props: SelectLanguageModalContentProps) { const { modalTitle, onLanguagesSelect, onModalClose } = props; const { isFetching, isPopulated, error, items } = useSelector( - createFilteredLanguagesSelector() + createLanguagesSelector({ + Any: true, + Original: true, + }) ); const [languageIds, setLanguageIds] = useState(props.languageIds); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx index 33947cc8f..8835a9be1 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx @@ -44,7 +44,6 @@ function createImportListExclusionSelector(id?: number) { const settings = selectSettings(mapping, pendingChanges, saveError); return { - id, isFetching, error, isSaving, @@ -119,15 +118,15 @@ function EditImportListExclusionModalContent({ - {isFetching && } + {isFetching ? : null} - {!isFetching && !!error && ( + {!isFetching && error ? ( {translate('AddImportListExclusionError')} - )} + ) : null} - {!isFetching && !error && ( + {!isFetching && !error ? (
{translate('Title')} @@ -153,11 +152,11 @@ function EditImportListExclusionModalContent({ />
- )} + ) : null}
- {id && ( + {id ? ( - )} + ) : null} diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx index 36ad57c47..9fd5a68ba 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx @@ -50,7 +50,6 @@ function createReleaseProfileSelector(id?: number) { ); return { - id, isFetching, error, isSaving, diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js deleted file mode 100644 index 69ac2a9c7..000000000 --- a/frontend/src/Settings/UI/UISettings.js +++ /dev/null @@ -1,252 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import FieldSet from 'Components/FieldSet'; -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 LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import { inputTypes, kinds } from 'Helpers/Props'; -import SettingsToolbar from 'Settings/SettingsToolbar'; -import themes from 'Styles/Themes'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; - -export const firstDayOfWeekOptions = [ - { - key: 0, - get value() { - return translate('Sunday'); - } - }, - { - key: 1, - get value() { - return translate('Monday'); - } - } -]; - -export const weekColumnOptions = [ - { key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' }, - { key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' }, - { key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' }, - { key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' } -]; - -const shortDateFormatOptions = [ - { key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' }, - { key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' }, - { key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' }, - { key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' }, - { key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' }, - { key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' } -]; - -const longDateFormatOptions = [ - { key: 'dddd, MMMM D YYYY', value: 'Tuesday, March 25, 2014' }, - { key: 'dddd, D MMMM YYYY', value: 'Tuesday, 25 March, 2014' } -]; - -export const timeFormatOptions = [ - { key: 'h(:mm)a', value: '5pm/5:30pm' }, - { key: 'HH:mm', value: '17:00/17:30' } -]; - -class UISettings extends Component { - - // - // Render - - render() { - const { - isFetching, - error, - settings, - languages, - hasSettings, - onInputChange, - onSavePress, - ...otherProps - } = this.props; - - const themeOptions = Object.keys(themes) - .map((theme) => ({ key: theme, value: titleCase(theme) })); - - return ( - - - - - { - isFetching ? - : - null - } - - { - !isFetching && error ? - - {translate('UiSettingsLoadError')} - : - null - } - - { - hasSettings && !isFetching && !error ? -
-
- - {translate('FirstDayOfWeek')} - - - - - - {translate('WeekColumnHeader')} - - - -
- -
- - {translate('ShortDateFormat')} - - - - - - {translate('LongDateFormat')} - - - - - - {translate('TimeFormat')} - - - - - - {translate('ShowRelativeDates')} - - -
- -
- - {translate('Theme')} - - - - - {translate('EnableColorImpairedMode')} - - -
- -
- - {translate('UiLanguage')} - language.key === settings.uiLanguage.value) ? - settings.uiLanguage.errors : - [ - ...settings.uiLanguage.errors, - { message: translate('InvalidUILanguage') } - ]} - /> - -
-
: - null - } -
-
- ); - } - -} - -UISettings.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - settings: PropTypes.object.isRequired, - languages: PropTypes.arrayOf(PropTypes.object).isRequired, - hasSettings: PropTypes.bool.isRequired, - onSavePress: PropTypes.func.isRequired, - onInputChange: PropTypes.func.isRequired -}; - -export default UISettings; diff --git a/frontend/src/Settings/UI/UISettings.tsx b/frontend/src/Settings/UI/UISettings.tsx new file mode 100644 index 000000000..4d686b6f6 --- /dev/null +++ b/frontend/src/Settings/UI/UISettings.tsx @@ -0,0 +1,287 @@ +import React, { useCallback, useEffect, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Alert from 'Components/Alert'; +import FieldSet from 'Components/FieldSet'; +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 LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import { inputTypes, kinds } from 'Helpers/Props'; +import SettingsToolbar from 'Settings/SettingsToolbar'; +import { + fetchUISettings, + saveUISettings, + setUISettingsValue, +} from 'Store/Actions/settingsActions'; +import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import themes from 'Styles/Themes'; +import { InputChanged } from 'typings/inputs'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; + +const SECTION = 'ui'; + +export const firstDayOfWeekOptions = [ + { + key: 0, + get value() { + return translate('Sunday'); + }, + }, + { + key: 1, + get value() { + return translate('Monday'); + }, + }, +]; + +export const weekColumnOptions = [ + { key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' }, + { key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' }, + { key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' }, + { key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' }, +]; + +const shortDateFormatOptions = [ + { key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' }, + { key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' }, + { key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' }, + { key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' }, + { key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' }, + { key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' }, +]; + +const longDateFormatOptions = [ + { key: 'dddd, MMMM D YYYY', value: 'Tuesday, March 25, 2014' }, + { key: 'dddd, D MMMM YYYY', value: 'Tuesday, 25 March, 2014' }, +]; + +export const timeFormatOptions = [ + { key: 'h(:mm)a', value: '5pm/5:30pm' }, + { key: 'HH:mm', value: '17:00/17:30' }, +]; + +function UISettings() { + const dispatch = useDispatch(); + + const { + items, + isFetching: isLanguagesFetching, + isPopulated: isLanguagesPopulated, + error: languagesError, + } = useSelector( + createLanguagesSelector({ + Any: true, + Original: true, + Unknown: true, + }) + ); + + const { + isFetching: isSettingsFetching, + isPopulated: isSettingsPopulated, + error: settingsError, + hasSettings, + settings, + hasPendingChanges, + isSaving, + validationErrors, + validationWarnings, + } = useSelector(createSettingsSectionSelector(SECTION)); + + const isFetching = isLanguagesFetching || isSettingsFetching; + const isPopulated = isLanguagesPopulated && isSettingsPopulated; + const error = languagesError || settingsError; + + const languages = useMemo(() => { + return items.map((item) => { + return { + key: item.id, + value: item.name, + }; + }); + }, [items]); + + const themeOptions = Object.keys(themes).map((theme) => ({ + key: theme, + value: titleCase(theme), + })); + + const handleInputChange = useCallback( + (change: InputChanged) => { + // @ts-expect-error - actions aren't typed + dispatch(setUISettingsValue(change)); + }, + [dispatch] + ); + const handleSavePress = useCallback(() => { + dispatch(saveUISettings()); + }, [dispatch]); + + useEffect(() => { + dispatch(fetchUISettings()); + + return () => { + // @ts-expect-error - actions aren't typed + dispatch(setUISettingsValue({ section: `settings.${SECTION}` })); + }; + }, [dispatch]); + + return ( + + + + + {isFetching && isPopulated ? : null} + + {!isFetching && error ? ( + {translate('UiSettingsLoadError')} + ) : null} + + {hasSettings && isPopulated && !error ? ( +
+
+ + {translate('FirstDayOfWeek')} + + + + + + {translate('WeekColumnHeader')} + + + +
+ +
+ + {translate('ShortDateFormat')} + + + + + + {translate('LongDateFormat')} + + + + + + {translate('TimeFormat')} + + + + + + {translate('ShowRelativeDates')} + + +
+ +
+ + {translate('Theme')} + + + + + {translate('EnableColorImpairedMode')} + + +
+ +
+ + {translate('UiLanguage')} + language.key === settings.uiLanguage.value + ) + ? settings.uiLanguage.errors + : [ + ...settings.uiLanguage.errors, + { message: translate('InvalidUILanguage') }, + ] + } + /> + +
+
+ ) : null} +
+
+ ); +} + +export default UISettings; diff --git a/frontend/src/Settings/UI/UISettingsConnector.js b/frontend/src/Settings/UI/UISettingsConnector.js deleted file mode 100644 index 585effea3..000000000 --- a/frontend/src/Settings/UI/UISettingsConnector.js +++ /dev/null @@ -1,114 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions'; -import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; -import UISettings from './UISettings'; - -const SECTION = 'ui'; -const FILTER_LANGUAGES = ['Any', 'Unknown', 'Original']; - -function createFilteredLanguagesSelector() { - return createSelector( - createLanguagesSelector(), - (languages) => { - if (!languages || !languages.items) { - return []; - } - - const newItems = languages.items - .filter((lang) => !FILTER_LANGUAGES.includes(lang.name)) - .map((item) => { - return { - key: item.id, - value: item.name - }; - }); - - return { - ...languages, - items: newItems - }; - } - ); -} - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.advancedSettings, - createSettingsSectionSelector(SECTION), - createFilteredLanguagesSelector(), - (advancedSettings, sectionSettings, languages) => { - return { - advancedSettings, - languages: languages.items, - isLanguagesPopulated: languages.isPopulated, - ...sectionSettings, - isFetching: sectionSettings.isFetching || languages.isFetching, - error: sectionSettings.error || languages.error - }; - } - ); -} - -const mapDispatchToProps = { - dispatchSetUISettingsValue: setUISettingsValue, - dispatchSaveUISettings: saveUISettings, - dispatchFetchUISettings: fetchUISettings, - dispatchClearPendingChanges: clearPendingChanges -}; - -class UISettingsConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - dispatchFetchUISettings - } = this.props; - - dispatchFetchUISettings(); - } - - componentWillUnmount() { - this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` }); - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.dispatchSetUISettingsValue({ name, value }); - }; - - onSavePress = () => { - this.props.dispatchSaveUISettings(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -UISettingsConnector.propTypes = { - isLanguagesPopulated: PropTypes.bool.isRequired, - dispatchSetUISettingsValue: PropTypes.func.isRequired, - dispatchSaveUISettings: PropTypes.func.isRequired, - dispatchFetchUISettings: PropTypes.func.isRequired, - dispatchClearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(UISettingsConnector); diff --git a/frontend/src/Store/Selectors/createFilteredLanguagesSelector.ts b/frontend/src/Store/Selectors/createFilteredLanguagesSelector.ts deleted file mode 100644 index f5c87388e..000000000 --- a/frontend/src/Store/Selectors/createFilteredLanguagesSelector.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createSelector } from 'reselect'; -import { LanguageSettingsAppState } from 'App/State/SettingsAppState'; -import Language from 'Language/Language'; -import createLanguagesSelector from './createLanguagesSelector'; - -export default function createFilteredLanguagesSelector(filterUnknown = false) { - const filterItems = ['Any', 'Original']; - - if (filterUnknown) { - filterItems.push('Unknown'); - } - - return createSelector(createLanguagesSelector(), (languages) => { - const { isFetching, isPopulated, error, items } = - languages as LanguageSettingsAppState; - - const filteredLanguages = items.filter( - (lang: Language) => !filterItems.includes(lang.name) - ); - - return { - isFetching, - isPopulated, - error, - items: filteredLanguages, - }; - }); -} diff --git a/frontend/src/Store/Selectors/createLanguagesSelector.ts b/frontend/src/Store/Selectors/createLanguagesSelector.ts index 831922103..37a6c6135 100644 --- a/frontend/src/Store/Selectors/createLanguagesSelector.ts +++ b/frontend/src/Store/Selectors/createLanguagesSelector.ts @@ -1,15 +1,23 @@ import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; -function createLanguagesSelector() { +interface LanguageFilter { + [key: string]: boolean | undefined; + Any: boolean; + Original?: boolean; + Unknown?: boolean; +} + +function createLanguagesSelector( + excludeLanguages: LanguageFilter = { Any: true } +) { return createSelector( (state: AppState) => state.settings.languages, (languages) => { const { isFetching, isPopulated, error, items } = languages; - const filterItems = ['Any']; const filteredLanguages = items.filter( - (lang) => !filterItems.includes(lang.name) + (lang) => !excludeLanguages[lang.name] ); return { diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.ts b/frontend/src/Store/Selectors/createSettingsSectionSelector.ts index ad1e9cd6b..91983edc2 100644 --- a/frontend/src/Store/Selectors/createSettingsSectionSelector.ts +++ b/frontend/src/Store/Selectors/createSettingsSectionSelector.ts @@ -28,6 +28,9 @@ function createSettingsSectionSelector< const saveError = 'saveError' in sectionSettings ? sectionSettings.saveError : undefined; + const isSaving = + 'isSaving' in sectionSettings ? sectionSettings.isSaving : false; + const { settings, pendingChanges: selectedPendingChanges, @@ -36,6 +39,7 @@ function createSettingsSectionSelector< return { ...other, + isSaving, saveError, settings: settings as PendingSection, pendingChanges: selectedPendingChanges as Partial, diff --git a/frontend/src/typings/Settings/UiSettings.ts b/frontend/src/typings/Settings/UiSettings.ts index 1ba62187b..c0f892939 100644 --- a/frontend/src/typings/Settings/UiSettings.ts +++ b/frontend/src/typings/Settings/UiSettings.ts @@ -7,4 +7,5 @@ export default interface UiSettings { firstDayOfWeek: number; enableColorImpairedMode: boolean; calendarWeekColumnHeader: string; + uiLanguage: number; }