mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: App Sync Profiles
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.appProfiles,
|
||||
(appProfiles) => {
|
||||
const tagList = appProfiles.items.map((appProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = appProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
@@ -47,6 +48,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
const valueType = selectedFilterBuilderProp.valueType;
|
||||
|
||||
switch (valueType) {
|
||||
case filterBuilderValueTypes.APP_PROFILE:
|
||||
return AppProfileFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.BOOL:
|
||||
return BoolFilterBuilderRowValue;
|
||||
|
||||
|
@@ -0,0 +1,99 @@
|
||||
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 sortByName from 'Utilities/Array/sortByName';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(appProfiles, includeNoChange, includeMixed) => {
|
||||
const values = _.map(appProfiles.items, (appProfile) => {
|
||||
return {
|
||||
key: appProfile.id,
|
||||
value: appProfile.name
|
||||
};
|
||||
});
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class AppProfileSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
values
|
||||
} = this.props;
|
||||
|
||||
if (!value || !values.some((v) => v.key === value) ) {
|
||||
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key)));
|
||||
|
||||
if (firstValue) {
|
||||
this.onChange({ name, value: firstValue.key });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
this.props.onChange({ name, value: parseInt(value) });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppProfileSelectInputConnector.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
|
||||
};
|
||||
|
||||
AppProfileSelectInputConnector.defaultProps = {
|
||||
includeNoChange: false
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(AppProfileSelectInputConnector);
|
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AppProfileSelectInputConnector from './AppProfileSelectInputConnector';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
@@ -29,6 +30,9 @@ import styles from './FormInputGroup.css';
|
||||
|
||||
function getComponent(type) {
|
||||
switch (type) {
|
||||
case inputTypes.APP_PROFILE_SELECT:
|
||||
return AppProfileSelectInputConnector;
|
||||
|
||||
case inputTypes.AUTO_COMPLETE:
|
||||
return AutoCompleteInput;
|
||||
|
||||
|
@@ -7,7 +7,7 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
import { fetchIndexers } from 'Store/Actions/indexerActions';
|
||||
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
|
||||
import { fetchGeneralSettings, fetchIndexerCategories, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
@@ -49,6 +49,7 @@ const selectIsPopulated = createSelector(
|
||||
(state) => state.settings.ui.isPopulated,
|
||||
(state) => state.settings.general.isPopulated,
|
||||
(state) => state.settings.languages.isPopulated,
|
||||
(state) => state.settings.appProfiles.isPopulated,
|
||||
(state) => state.indexers.isPopulated,
|
||||
(state) => state.indexerStatus.isPopulated,
|
||||
(state) => state.settings.indexerCategories.isPopulated,
|
||||
@@ -59,6 +60,7 @@ const selectIsPopulated = createSelector(
|
||||
uiSettingsIsPopulated,
|
||||
generalSettingsIsPopulated,
|
||||
languagesIsPopulated,
|
||||
appProfilesIsPopulated,
|
||||
indexersIsPopulated,
|
||||
indexerStatusIsPopulated,
|
||||
indexerCategoriesIsPopulated,
|
||||
@@ -70,6 +72,7 @@ const selectIsPopulated = createSelector(
|
||||
uiSettingsIsPopulated &&
|
||||
generalSettingsIsPopulated &&
|
||||
languagesIsPopulated &&
|
||||
appProfilesIsPopulated &&
|
||||
indexersIsPopulated &&
|
||||
indexerStatusIsPopulated &&
|
||||
indexerCategoriesIsPopulated &&
|
||||
@@ -84,6 +87,7 @@ const selectErrors = createSelector(
|
||||
(state) => state.settings.ui.error,
|
||||
(state) => state.settings.general.error,
|
||||
(state) => state.settings.languages.error,
|
||||
(state) => state.settings.appProfiles.error,
|
||||
(state) => state.indexers.error,
|
||||
(state) => state.indexerStatus.error,
|
||||
(state) => state.settings.indexerCategories.error,
|
||||
@@ -94,6 +98,7 @@ const selectErrors = createSelector(
|
||||
uiSettingsError,
|
||||
generalSettingsError,
|
||||
languagesError,
|
||||
appProfilesError,
|
||||
indexersError,
|
||||
indexerStatusError,
|
||||
indexerCategoriesError,
|
||||
@@ -105,6 +110,7 @@ const selectErrors = createSelector(
|
||||
uiSettingsError ||
|
||||
generalSettingsError ||
|
||||
languagesError ||
|
||||
appProfilesError ||
|
||||
indexersError ||
|
||||
indexerStatusError ||
|
||||
indexerCategoriesError ||
|
||||
@@ -118,6 +124,7 @@ const selectErrors = createSelector(
|
||||
uiSettingsError,
|
||||
generalSettingsError,
|
||||
languagesError,
|
||||
appProfilesError,
|
||||
indexersError,
|
||||
indexerStatusError,
|
||||
indexerCategoriesError,
|
||||
@@ -174,6 +181,9 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatchFetchUISettings() {
|
||||
dispatch(fetchUISettings());
|
||||
},
|
||||
dispatchFetchAppProfiles() {
|
||||
dispatch(fetchAppProfiles());
|
||||
},
|
||||
dispatchFetchGeneralSettings() {
|
||||
dispatch(fetchGeneralSettings());
|
||||
},
|
||||
@@ -207,6 +217,7 @@ class PageConnector extends Component {
|
||||
this.props.dispatchFetchCustomFilters();
|
||||
this.props.dispatchFetchTags();
|
||||
this.props.dispatchFetchLanguages();
|
||||
this.props.dispatchFetchAppProfiles();
|
||||
this.props.dispatchFetchIndexers();
|
||||
this.props.dispatchFetchIndexerStatus();
|
||||
this.props.dispatchFetchIndexerCategories();
|
||||
@@ -232,6 +243,7 @@ class PageConnector extends Component {
|
||||
hasError,
|
||||
dispatchFetchTags,
|
||||
dispatchFetchLanguages,
|
||||
dispatchFetchAppProfiles,
|
||||
dispatchFetchIndexers,
|
||||
dispatchFetchIndexerStatus,
|
||||
dispatchFetchIndexerCategories,
|
||||
@@ -272,6 +284,7 @@ PageConnector.propTypes = {
|
||||
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||
dispatchFetchAppProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerStatus: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerCategories: PropTypes.func.isRequired,
|
||||
|
@@ -4,5 +4,6 @@ export const DATE = 'date';
|
||||
export const DEFAULT = 'default';
|
||||
export const INDEXER = 'indexer';
|
||||
export const PROTOCOL = 'protocol';
|
||||
export const APP_PROFILE = 'appProfile';
|
||||
export const MOVIE_STATUS = 'movieStatus';
|
||||
export const TAG = 'tag';
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export const AUTO_COMPLETE = 'autoComplete';
|
||||
export const APP_PROFILE_SELECT = 'appProfileSelect';
|
||||
export const AVAILABILITY_SELECT = 'availabilitySelect';
|
||||
export const CAPTCHA = 'captcha';
|
||||
export const CARDIGANNCAPTCHA = 'cardigannCaptcha';
|
||||
@@ -22,6 +23,7 @@ export const TAG_SELECT = 'tagSelect';
|
||||
|
||||
export const all = [
|
||||
AUTO_COMPLETE,
|
||||
APP_PROFILE_SELECT,
|
||||
AVAILABILITY_SELECT,
|
||||
CAPTCHA,
|
||||
CARDIGANNCAPTCHA,
|
||||
|
@@ -42,6 +42,7 @@ function EditIndexerModalContent(props) {
|
||||
redirect,
|
||||
supportsRss,
|
||||
supportsRedirect,
|
||||
appProfileId,
|
||||
fields,
|
||||
priority
|
||||
} = item;
|
||||
@@ -105,6 +106,17 @@ function EditIndexerModalContent(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AppProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.APP_PROFILE_SELECT}
|
||||
name="appProfileId"
|
||||
{...appProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
fields ?
|
||||
fields.map((field) => {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AppProfileSelectInputConnector from 'Components/Form/AppProfileSelectInputConnector';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
@@ -22,6 +23,7 @@ class IndexerEditorFooter extends Component {
|
||||
|
||||
this.state = {
|
||||
enable: NO_CHANGE,
|
||||
appProfileId: NO_CHANGE,
|
||||
savingTags: false,
|
||||
isDeleteMovieModalOpen: false,
|
||||
isTagsModalOpen: false
|
||||
@@ -37,6 +39,7 @@ class IndexerEditorFooter extends Component {
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
enable: NO_CHANGE,
|
||||
appProfileId: NO_CHANGE,
|
||||
savingTags: false
|
||||
});
|
||||
}
|
||||
@@ -99,6 +102,7 @@ class IndexerEditorFooter extends Component {
|
||||
|
||||
const {
|
||||
enable,
|
||||
appProfileId,
|
||||
savingTags,
|
||||
isTagsModalOpen,
|
||||
isDeleteMovieModalOpen
|
||||
@@ -127,6 +131,21 @@ class IndexerEditorFooter extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<IndexerEditorFooterLabel
|
||||
label={translate('AppProfile')}
|
||||
isSaving={isSaving && appProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<AppProfileSelectInputConnector
|
||||
name="appProfileId"
|
||||
value={appProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<IndexerEditorFooterLabel
|
||||
|
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createIndexerAppProfileSelector from 'Store/Selectors/createIndexerAppProfileSelector';
|
||||
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
|
||||
import createIndexerStatusSelector from 'Store/Selectors/createIndexerStatusSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
@@ -19,11 +20,13 @@ function selectShowSearchAction() {
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createIndexerSelector(),
|
||||
createIndexerAppProfileSelector(),
|
||||
createIndexerStatusSelector(),
|
||||
selectShowSearchAction(),
|
||||
createUISettingsSelector(),
|
||||
(
|
||||
movie,
|
||||
appProfile,
|
||||
status,
|
||||
showSearchAction,
|
||||
uiSettings
|
||||
@@ -40,6 +43,7 @@ function createMapStateToProps() {
|
||||
|
||||
return {
|
||||
...movie,
|
||||
appProfile,
|
||||
status,
|
||||
showSearchAction,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
|
@@ -47,6 +47,15 @@ function IndexerIndexSortMenu(props) {
|
||||
{translate('Added')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="appProfileId"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('AppProfile')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="priority"
|
||||
sortKey={sortKey}
|
||||
|
@@ -18,6 +18,12 @@
|
||||
flex: 0 0 90px;
|
||||
}
|
||||
|
||||
.appProfileId {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.capabilities {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
|
@@ -25,6 +25,12 @@
|
||||
flex: 0 0 90px;
|
||||
}
|
||||
|
||||
.appProfileId {
|
||||
composes: cell;
|
||||
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.capabilities {
|
||||
composes: cell;
|
||||
|
||||
|
@@ -79,6 +79,7 @@ class IndexerIndexRow extends Component {
|
||||
privacy,
|
||||
priority,
|
||||
status,
|
||||
appProfile,
|
||||
added,
|
||||
capabilities,
|
||||
columns,
|
||||
@@ -183,6 +184,17 @@ class IndexerIndexRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'appProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
{appProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'capabilities') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
@@ -284,6 +296,7 @@ IndexerIndexRow.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
enable: PropTypes.bool.isRequired,
|
||||
redirect: PropTypes.bool.isRequired,
|
||||
appProfile: PropTypes.object.isRequired,
|
||||
status: PropTypes.object,
|
||||
capabilities: PropTypes.object.isRequired,
|
||||
added: PropTypes.string.isRequired,
|
||||
|
@@ -5,6 +5,7 @@ import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import AppProfilesConnector from 'Settings/Profiles/App/AppProfilesConnector';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ApplicationsConnector from './Applications/ApplicationsConnector';
|
||||
@@ -45,6 +46,7 @@ class ApplicationSettings extends Component {
|
||||
|
||||
<PageContentBody>
|
||||
<ApplicationsConnector />
|
||||
<AppProfilesConnector />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
31
frontend/src/Settings/Profiles/App/AppProfile.css
Normal file
31
frontend/src/Settings/Profiles/App/AppProfile.css
Normal file
@@ -0,0 +1,31 @@
|
||||
.appProfile {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.tooltipLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
155
frontend/src/Settings/Profiles/App/AppProfile.js
Normal file
155
frontend/src/Settings/Profiles/App/AppProfile.js
Normal file
@@ -0,0 +1,155 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditAppProfileModalConnector from './EditAppProfileModalConnector';
|
||||
import styles from './AppProfile.css';
|
||||
|
||||
class AppProfile extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditAppProfileModalOpen: false,
|
||||
isDeleteAppProfileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditAppProfilePress = () => {
|
||||
this.setState({ isEditAppProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onEditAppProfileModalClose = () => {
|
||||
this.setState({ isEditAppProfileModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteAppProfilePress = () => {
|
||||
this.setState({
|
||||
isEditAppProfileModalOpen: false,
|
||||
isDeleteAppProfileModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteAppProfileModalClose = () => {
|
||||
this.setState({ isDeleteAppProfileModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmDeleteAppProfile = () => {
|
||||
this.props.onConfirmDeleteAppProfile(this.props.id);
|
||||
}
|
||||
|
||||
onCloneAppProfilePress = () => {
|
||||
const {
|
||||
id,
|
||||
onCloneAppProfilePress
|
||||
} = this.props;
|
||||
|
||||
onCloneAppProfilePress(id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enableRss,
|
||||
enableAutomaticSearch,
|
||||
enableInteractiveSearch,
|
||||
isDeleting
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.appProfile}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditAppProfilePress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneProfile')}
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneAppProfilePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
{
|
||||
<Label
|
||||
kind={enableRss ? kinds.SUCCESS : kinds.DISABLED}
|
||||
outline={!enableRss}
|
||||
>
|
||||
{translate('RSS')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
<Label
|
||||
kind={enableAutomaticSearch ? kinds.SUCCESS : kinds.DISABLED}
|
||||
outline={!enableAutomaticSearch}
|
||||
>
|
||||
{translate('AutomaticSearch')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
<Label
|
||||
kind={enableInteractiveSearch ? kinds.SUCCESS : kinds.DISABLED}
|
||||
outline={!enableInteractiveSearch}
|
||||
>
|
||||
{translate('InteractiveSearch')}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditAppProfileModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditAppProfileModalOpen}
|
||||
onModalClose={this.onEditAppProfileModalClose}
|
||||
onDeleteAppProfilePress={this.onDeleteAppProfilePress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteAppProfileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteAppProfile')}
|
||||
message={translate('AppProfileDeleteConfirm', [name])}
|
||||
confirmLabel={translate('Delete')}
|
||||
isSpinning={isDeleting}
|
||||
onConfirm={this.onConfirmDeleteAppProfile}
|
||||
onCancel={this.onDeleteAppProfileModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppProfile.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
enableRss: PropTypes.bool.isRequired,
|
||||
enableAutomaticSearch: PropTypes.bool.isRequired,
|
||||
enableInteractiveSearch: PropTypes.bool.isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteAppProfile: PropTypes.func.isRequired,
|
||||
onCloneAppProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AppProfile;
|
@@ -0,0 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAppProfileSelector from 'Store/Selectors/createAppProfileSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createAppProfileSelector(),
|
||||
(appProfile) => {
|
||||
return {
|
||||
name: appProfile.name
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function AppProfileNameConnector({ name, ...otherProps }) {
|
||||
return (
|
||||
<span>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
AppProfileNameConnector.propTypes = {
|
||||
appProfileId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(AppProfileNameConnector);
|
21
frontend/src/Settings/Profiles/App/AppProfiles.css
Normal file
21
frontend/src/Settings/Profiles/App/AppProfiles.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.appProfiles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.addAppProfile {
|
||||
composes: appProfile from '~./AppProfile.css';
|
||||
|
||||
background-color: $cardAlternateBackgroundColor;
|
||||
color: $gray;
|
||||
text-align: center;
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 0;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
107
frontend/src/Settings/Profiles/App/AppProfiles.js
Normal file
107
frontend/src/Settings/Profiles/App/AppProfiles.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AppProfile from './AppProfile';
|
||||
import EditAppProfileModalConnector from './EditAppProfileModalConnector';
|
||||
import styles from './AppProfiles.css';
|
||||
|
||||
class AppProfiles extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAppProfileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCloneAppProfilePress = (id) => {
|
||||
this.props.onCloneAppProfilePress(id);
|
||||
this.setState({ isAppProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onEditAppProfilePress = () => {
|
||||
this.setState({ isAppProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ isAppProfileModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
isDeleting,
|
||||
onConfirmDeleteAppProfile,
|
||||
onCloneAppProfilePress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('AppProfiles')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('UnableToLoadAppProfiles')}
|
||||
{...otherProps}c={true}
|
||||
>
|
||||
<div className={styles.appProfiles}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<AppProfile
|
||||
key={item.id}
|
||||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
onConfirmDeleteAppProfile={onConfirmDeleteAppProfile}
|
||||
onCloneAppProfilePress={this.onCloneAppProfilePress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addAppProfile}
|
||||
onPress={this.onEditAppProfilePress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<EditAppProfileModalConnector
|
||||
isOpen={this.state.isAppProfileModalOpen}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppProfiles.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteAppProfile: PropTypes.func.isRequired,
|
||||
onCloneAppProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AppProfiles;
|
63
frontend/src/Settings/Profiles/App/AppProfilesConnector.js
Normal file
63
frontend/src/Settings/Profiles/App/AppProfilesConnector.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cloneAppProfile, deleteAppProfile, fetchAppProfiles } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import AppProfiles from './AppProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
||||
(appProfiles) => appProfiles
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchAppProfiles: fetchAppProfiles,
|
||||
dispatchDeleteAppProfile: deleteAppProfile,
|
||||
dispatchCloneAppProfile: cloneAppProfile
|
||||
};
|
||||
|
||||
class AppProfilesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchAppProfiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteAppProfile = (id) => {
|
||||
this.props.dispatchDeleteAppProfile({ id });
|
||||
}
|
||||
|
||||
onCloneAppProfilePress = (id) => {
|
||||
this.props.dispatchCloneAppProfile({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AppProfiles
|
||||
onConfirmDeleteAppProfile={this.onConfirmDeleteAppProfile}
|
||||
onCloneAppProfilePress={this.onCloneAppProfilePress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppProfilesConnector.propTypes = {
|
||||
dispatchFetchAppProfiles: PropTypes.func.isRequired,
|
||||
dispatchDeleteAppProfile: PropTypes.func.isRequired,
|
||||
dispatchCloneAppProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AppProfilesConnector);
|
37
frontend/src/Settings/Profiles/App/EditAppProfileModal.js
Normal file
37
frontend/src/Settings/Profiles/App/EditAppProfileModal.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditAppProfileModalContentConnector from './EditAppProfileModalContentConnector';
|
||||
|
||||
class EditAppProfileModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditAppProfileModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAppProfileModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditAppProfileModal;
|
@@ -0,0 +1,43 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditAppProfileModal from './EditAppProfileModal';
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditAppProfileModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'settings.appProfiles' });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditAppProfileModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAppProfileModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EditAppProfileModalConnector);
|
@@ -0,0 +1,3 @@
|
||||
.deleteButtonContainer {
|
||||
margin-right: auto;
|
||||
}
|
184
frontend/src/Settings/Profiles/App/EditAppProfileModalContent.js
Normal file
184
frontend/src/Settings/Profiles/App/EditAppProfileModalContent.js
Normal file
@@ -0,0 +1,184 @@
|
||||
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 SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
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 translate from 'Utilities/String/translate';
|
||||
import styles from './EditAppProfileModalContent.css';
|
||||
|
||||
class EditAppProfileModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
isInUse,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
onDeleteAppProfilePress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enableRss,
|
||||
enableInteractiveSearch,
|
||||
enableAutomaticSearch
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
|
||||
<ModalHeader>
|
||||
{id ? translate('EditAppProfile') : translate('AddAppProfile')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
{translate('UnableToAddANewAppProfilePleaseTryAgain')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Name')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('EnableRss')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableRss"
|
||||
{...enableRss}
|
||||
helpText={translate('EnableRssHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('EnableInteractiveSearch')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableInteractiveSearch"
|
||||
{...enableInteractiveSearch}
|
||||
helpText={translate('EnableInteractiveSearchHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('EnableAutomaticSearch')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableAutomaticSearch"
|
||||
{...enableAutomaticSearch}
|
||||
helpText={translate('EnableAutomaticSearchHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id ?
|
||||
<div
|
||||
className={styles.deleteButtonContainer}
|
||||
title={
|
||||
isInUse ?
|
||||
translate('AppProfileInUse') :
|
||||
undefined
|
||||
}
|
||||
>
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
isDisabled={isInUse}
|
||||
onPress={onDeleteAppProfilePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAppProfileModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
isInUse: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteAppProfilePress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditAppProfileModalContent;
|
@@ -0,0 +1,82 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchAppProfileSchema, saveAppProfile, setAppProfileValue } from 'Store/Actions/settingsActions';
|
||||
import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditAppProfileModalContent from './EditAppProfileModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createProviderSettingsSelector('appProfiles'),
|
||||
createProfileInUseSelector('appProfileId'),
|
||||
(appProfile, isInUse) => {
|
||||
return {
|
||||
...appProfile,
|
||||
isInUse
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchAppProfileSchema,
|
||||
setAppProfileValue,
|
||||
saveAppProfile
|
||||
};
|
||||
|
||||
class EditAppProfileModalContentConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.id && !this.props.isPopulated) {
|
||||
this.props.fetchAppProfileSchema();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setAppProfileValue({ name, value });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveAppProfile({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditAppProfileModalContent
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAppProfileModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setAppProfileValue: PropTypes.func.isRequired,
|
||||
fetchAppProfileSchema: PropTypes.func.isRequired,
|
||||
saveAppProfile: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditAppProfileModalContentConnector);
|
97
frontend/src/Store/Actions/Settings/appProfiles.js
Normal file
97
frontend/src/Store/Actions/Settings/appProfiles.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.appProfiles';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_APP_PROFILES = 'settings/appProfiles/fetchAppProfiles';
|
||||
export const FETCH_APP_PROFILE_SCHEMA = 'settings/appProfiles/fetchAppProfileSchema';
|
||||
export const SAVE_APP_PROFILE = 'settings/appProfiles/saveAppProfile';
|
||||
export const DELETE_APP_PROFILE = 'settings/appProfiles/deleteAppProfile';
|
||||
export const SET_APP_PROFILE_VALUE = 'settings/appProfiles/setAppProfileValue';
|
||||
export const CLONE_APP_PROFILE = 'settings/appProfiles/cloneAppProfile';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchAppProfiles = createThunk(FETCH_APP_PROFILES);
|
||||
export const fetchAppProfileSchema = createThunk(FETCH_APP_PROFILE_SCHEMA);
|
||||
export const saveAppProfile = createThunk(SAVE_APP_PROFILE);
|
||||
export const deleteAppProfile = createThunk(DELETE_APP_PROFILE);
|
||||
|
||||
export const setAppProfileValue = createAction(SET_APP_PROFILE_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const cloneAppProfile = createAction(CLONE_APP_PROFILE);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_APP_PROFILES]: createFetchHandler(section, '/appprofile'),
|
||||
[FETCH_APP_PROFILE_SCHEMA]: createFetchSchemaHandler(section, '/appprofile/schema'),
|
||||
[SAVE_APP_PROFILE]: createSaveProviderHandler(section, '/appprofile'),
|
||||
[DELETE_APP_PROFILE]: createRemoveItemHandler(section, '/appprofile')
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_APP_PROFILE_VALUE]: createSetSettingValueReducer(section),
|
||||
|
||||
[CLONE_APP_PROFILE]: function(state, { payload }) {
|
||||
const id = payload.id;
|
||||
const newState = getSectionState(state, section);
|
||||
const item = newState.items.find((i) => i.id === id);
|
||||
const pendingChanges = { ...item, id: 0 };
|
||||
delete pendingChanges.id;
|
||||
|
||||
pendingChanges.name = `${pendingChanges.name} - Copy`;
|
||||
newState.pendingChanges = pendingChanges;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
@@ -74,6 +74,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'appProfileId',
|
||||
label: translate('AppProfile'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'added',
|
||||
label: translate('Added'),
|
||||
@@ -138,6 +144,12 @@ export const defaultState = {
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.PROTOCOL
|
||||
},
|
||||
{
|
||||
name: 'appProfileId',
|
||||
label: translate('AppProfile'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.APP_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: translate('Tags'),
|
||||
|
@@ -2,6 +2,7 @@ import { createAction } from 'redux-actions';
|
||||
import { handleThunks } from 'Store/thunks';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import applications from './Settings/applications';
|
||||
import appProfiles from './Settings/appProfiles';
|
||||
import development from './Settings/development';
|
||||
import downloadClients from './Settings/downloadClients';
|
||||
import general from './Settings/general';
|
||||
@@ -16,6 +17,7 @@ export * from './Settings/indexerCategories';
|
||||
export * from './Settings/languages';
|
||||
export * from './Settings/notifications';
|
||||
export * from './Settings/applications';
|
||||
export * from './Settings/appProfiles';
|
||||
export * from './Settings/development';
|
||||
export * from './Settings/ui';
|
||||
|
||||
@@ -36,6 +38,7 @@ export const defaultState = {
|
||||
languages: languages.defaultState,
|
||||
notifications: notifications.defaultState,
|
||||
applications: applications.defaultState,
|
||||
appProfiles: appProfiles.defaultState,
|
||||
development: development.defaultState,
|
||||
ui: ui.defaultState
|
||||
};
|
||||
@@ -64,6 +67,7 @@ export const actionHandlers = handleThunks({
|
||||
...languages.actionHandlers,
|
||||
...notifications.actionHandlers,
|
||||
...applications.actionHandlers,
|
||||
...appProfiles.actionHandlers,
|
||||
...development.actionHandlers,
|
||||
...ui.actionHandlers
|
||||
});
|
||||
@@ -83,6 +87,7 @@ export const reducers = createHandleActions({
|
||||
...languages.reducers,
|
||||
...notifications.reducers,
|
||||
...applications.reducers,
|
||||
...appProfiles.reducers,
|
||||
...development.reducers,
|
||||
...ui.reducers
|
||||
|
||||
|
15
frontend/src/Store/Selectors/createAppProfileSelector.js
Normal file
15
frontend/src/Store/Selectors/createAppProfileSelector.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
function createAppProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { appProfileId }) => appProfileId,
|
||||
(state) => state.settings.appProfiles.items,
|
||||
(appProfileId, appProfiles) => {
|
||||
return appProfiles.find((profile) => {
|
||||
return profile.id === appProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createAppProfileSelector;
|
@@ -0,0 +1,16 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import createIndexerSelector from './createIndexerSelector';
|
||||
|
||||
function createIndexerAppProfileSelector() {
|
||||
return createSelector(
|
||||
(state) => state.settings.appProfiles.items,
|
||||
createIndexerSelector(),
|
||||
(appProfiles, indexer = {}) => {
|
||||
return appProfiles.find((profile) => {
|
||||
return profile.id === indexer.appProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createIndexerAppProfileSelector;
|
23
frontend/src/Store/Selectors/createProfileInUseSelector.js
Normal file
23
frontend/src/Store/Selectors/createProfileInUseSelector.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllIndexersSelector from './createAllIndexersSelector';
|
||||
|
||||
function createProfileInUseSelector(profileProp) {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
createAllIndexersSelector(),
|
||||
(id, indexers) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_.some(indexers, { [profileProp]: id })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createProfileInUseSelector;
|
@@ -32,8 +32,8 @@ module.exports = {
|
||||
|
||||
// Drag
|
||||
dragHandleWidth: '40px',
|
||||
qualityProfileItemHeight: '30px',
|
||||
qualityProfileItemDragSourcePadding: '4px',
|
||||
appProfileItemHeight: '30px',
|
||||
appProfileItemDragSourcePadding: '4px',
|
||||
|
||||
// Progress Bar
|
||||
progressBarSmallHeight: '5px',
|
||||
|
Reference in New Issue
Block a user