New: App Sync Profiles

This commit is contained in:
Qstick
2021-05-18 00:17:04 -04:00
parent 29c4849bef
commit f64f8e915f
51 changed files with 1509 additions and 19 deletions

View File

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

View 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;
}

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

View File

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

View 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;
}

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

View 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);

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

View File

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

View File

@@ -0,0 +1,3 @@
.deleteButtonContainer {
margin-right: auto;
}

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

View File

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