parent
64160866c3
commit
86c785ffa0
@ -1,70 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AdvancedSettingsButton.css';
|
||||
|
||||
function AdvancedSettingsButton(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
onAdvancedSettingsPress,
|
||||
showLabel
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.button}
|
||||
title={advancedSettings ? translate('ShownClickToHide') : translate('HiddenClickToShow')}
|
||||
onPress={onAdvancedSettingsPress}
|
||||
>
|
||||
<Icon
|
||||
name={icons.ADVANCED_SETTINGS}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<span
|
||||
className={classNames(
|
||||
styles.indicatorContainer,
|
||||
'fa-layers fa-fw'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={styles.indicatorBackground}
|
||||
name={icons.CIRCLE}
|
||||
size={16}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
className={advancedSettings ? styles.enabled : styles.disabled}
|
||||
name={advancedSettings ? icons.CHECK : icons.CLOSE}
|
||||
size={10}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{
|
||||
showLabel ?
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
AdvancedSettingsButton.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
showLabel: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
AdvancedSettingsButton.defaultProps = {
|
||||
showLabel: true
|
||||
};
|
||||
|
||||
export default AdvancedSettingsButton;
|
@ -0,0 +1,67 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AdvancedSettingsButton.css';
|
||||
|
||||
interface AdvancedSettingsButtonProps {
|
||||
showLabel: boolean;
|
||||
}
|
||||
|
||||
function AdvancedSettingsButton({ showLabel }: AdvancedSettingsButtonProps) {
|
||||
const showAdvancedSettings = useSelector(
|
||||
(state: AppState) => state.settings.advancedSettings
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
dispatch(toggleAdvancedSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.button}
|
||||
title={
|
||||
showAdvancedSettings
|
||||
? translate('ShownClickToHide')
|
||||
: translate('HiddenClickToShow')
|
||||
}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<Icon name={icons.ADVANCED_SETTINGS} size={21} />
|
||||
|
||||
<span
|
||||
className={classNames(styles.indicatorContainer, 'fa-layers fa-fw')}
|
||||
>
|
||||
<Icon
|
||||
className={styles.indicatorBackground}
|
||||
name={icons.CIRCLE}
|
||||
size={16}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
className={showAdvancedSettings ? styles.enabled : styles.disabled}
|
||||
name={showAdvancedSettings ? icons.CHECK : icons.CLOSE}
|
||||
size={10}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{showLabel ? (
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{showAdvancedSettings
|
||||
? translate('HideAdvanced')
|
||||
: translate('ShowAdvanced')}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdvancedSettingsButton;
|
@ -1,77 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect } from 'react';
|
||||
import keyboardShortcuts from 'Components/keyboardShortcuts';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function PendingChangesModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
bindShortcut,
|
||||
unbindShortcut
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
bindShortcut('enter', onConfirm);
|
||||
|
||||
return () => unbindShortcut('enter', onConfirm);
|
||||
}
|
||||
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onCancel}
|
||||
>
|
||||
<ModalContent onModalClose={onCancel}>
|
||||
<ModalHeader>{translate('UnsavedChanges')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{translate('PendingChangesMessage')}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
kind={kinds.DEFAULT}
|
||||
onPress={onCancel}
|
||||
>
|
||||
{translate('PendingChangesStayReview')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
autoFocus={true}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onConfirm}
|
||||
>
|
||||
{translate('PendingChangesDiscardChanges')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
PendingChangesModal.propTypes = {
|
||||
className: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired,
|
||||
unbindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
PendingChangesModal.defaultProps = {
|
||||
kind: kinds.PRIMARY
|
||||
};
|
||||
|
||||
export default keyboardShortcuts(PendingChangesModal);
|
@ -0,0 +1,55 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
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 useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface PendingChangesModalProps {
|
||||
className?: string;
|
||||
isOpen: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function PendingChangesModal({
|
||||
isOpen,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: PendingChangesModalProps) {
|
||||
const { bindShortcut, unbindShortcut } = useKeyboardShortcuts();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
bindShortcut('acceptConfirmModal', onConfirm);
|
||||
}
|
||||
|
||||
return () => unbindShortcut('acceptConfirmModal');
|
||||
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onCancel}>
|
||||
<ModalContent onModalClose={onCancel}>
|
||||
<ModalHeader>{translate('UnsavedChanges')}</ModalHeader>
|
||||
|
||||
<ModalBody>{translate('PendingChangesMessage')}</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button kind={kinds.DEFAULT} onPress={onCancel}>
|
||||
{translate('PendingChangesStayReview')}
|
||||
</Button>
|
||||
|
||||
<Button autoFocus={true} kind={kinds.DANGER} onPress={onConfirm}>
|
||||
{translate('PendingChangesDiscardChanges')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default PendingChangesModal;
|
@ -1,106 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AdvancedSettingsButton from './AdvancedSettingsButton';
|
||||
import PendingChangesModal from './PendingChangesModal';
|
||||
|
||||
class SettingsToolbar extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.bindShortcut(shortcuts.SAVE_SETTINGS.key, this.saveSettings, { isGlobal: true });
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
saveSettings = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const {
|
||||
hasPendingChanges,
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
if (hasPendingChanges) {
|
||||
onSavePress();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
advancedSettings,
|
||||
showSave,
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
hasPendingLocation,
|
||||
additionalButtons,
|
||||
onSavePress,
|
||||
onConfirmNavigation,
|
||||
onCancelNavigation,
|
||||
onAdvancedSettingsPress
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
/>
|
||||
|
||||
{
|
||||
showSave &&
|
||||
<PageToolbarButton
|
||||
label={hasPendingChanges ? translate('SaveChanges') : translate('NoChanges')}
|
||||
iconName={icons.SAVE}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!hasPendingChanges}
|
||||
onPress={onSavePress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
additionalButtons
|
||||
}
|
||||
</PageToolbarSection>
|
||||
|
||||
<PendingChangesModal
|
||||
isOpen={hasPendingLocation}
|
||||
onConfirm={onConfirmNavigation}
|
||||
onCancel={onCancelNavigation}
|
||||
/>
|
||||
</PageToolbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToolbar.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
showSave: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
hasPendingLocation: PropTypes.bool.isRequired,
|
||||
hasPendingChanges: PropTypes.bool,
|
||||
additionalButtons: PropTypes.node,
|
||||
onSavePress: PropTypes.func,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onConfirmNavigation: PropTypes.func.isRequired,
|
||||
onCancelNavigation: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
SettingsToolbar.defaultProps = {
|
||||
showSave: true
|
||||
};
|
||||
|
||||
export default keyboardShortcuts(SettingsToolbar);
|
@ -0,0 +1,149 @@
|
||||
import { Action, Location, UnregisterCallback } from 'history';
|
||||
import React, {
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AdvancedSettingsButton from './AdvancedSettingsButton';
|
||||
import PendingChangesModal from './PendingChangesModal';
|
||||
|
||||
interface SettingsToolbarProps {
|
||||
showSave?: boolean;
|
||||
isSaving?: boolean;
|
||||
hasPendingChanges?: boolean;
|
||||
// TODO: This should do type checking like PageToolbarSectionProps,
|
||||
// but this works for the time being.
|
||||
additionalButtons?: ReactElement | null;
|
||||
onSavePress?: () => void;
|
||||
}
|
||||
|
||||
function SettingsToolbar({
|
||||
showSave = true,
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
additionalButtons = null,
|
||||
onSavePress,
|
||||
}: SettingsToolbarProps) {
|
||||
const { bindShortcut, unbindShortcut } = useKeyboardShortcuts();
|
||||
const history = useHistory();
|
||||
const [nextLocation, setNextLocation] = useState<Location | null>(null);
|
||||
const [nextLocationAction, setNextLocationAction] = useState<Action | null>(
|
||||
null
|
||||
);
|
||||
const hasConfirmed = useRef(false);
|
||||
const unblocker = useRef<UnregisterCallback>();
|
||||
|
||||
const handleConfirmNavigation = useCallback(() => {
|
||||
if (!nextLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = `${nextLocation.pathname}${nextLocation.search}`;
|
||||
|
||||
hasConfirmed.current = true;
|
||||
|
||||
if (nextLocationAction === 'PUSH') {
|
||||
history.push(path);
|
||||
} else {
|
||||
// Unfortunately back and forward both use POP,
|
||||
// which means we don't actually know which direction
|
||||
// the user wanted to go, assuming back.
|
||||
|
||||
history.goBack();
|
||||
}
|
||||
}, [nextLocation, nextLocationAction, history]);
|
||||
|
||||
const handleCancelNavigation = useCallback(() => {
|
||||
setNextLocation(null);
|
||||
setNextLocationAction(null);
|
||||
hasConfirmed.current = false;
|
||||
}, []);
|
||||
|
||||
const handleRouterLeaving = useCallback(
|
||||
(routerLocation: Location, routerAction: Action) => {
|
||||
if (hasConfirmed.current) {
|
||||
setNextLocation(null);
|
||||
setNextLocationAction(null);
|
||||
hasConfirmed.current = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasPendingChanges) {
|
||||
setNextLocation(routerLocation);
|
||||
setNextLocationAction(routerAction);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
[hasPendingChanges]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
unblocker.current = history.block(handleRouterLeaving);
|
||||
|
||||
return () => {
|
||||
unblocker.current?.();
|
||||
};
|
||||
}, [history, handleRouterLeaving]);
|
||||
|
||||
useEffect(() => {
|
||||
bindShortcut(
|
||||
'saveSettings',
|
||||
() => {
|
||||
if (hasPendingChanges) {
|
||||
onSavePress?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
isGlobal: true,
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unbindShortcut('saveSettings');
|
||||
};
|
||||
}, [hasPendingChanges, bindShortcut, unbindShortcut, onSavePress]);
|
||||
|
||||
return (
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<AdvancedSettingsButton showLabel={true} />
|
||||
{showSave ? (
|
||||
<PageToolbarButton
|
||||
label={
|
||||
hasPendingChanges
|
||||
? translate('SaveChanges')
|
||||
: translate('NoChanges')
|
||||
}
|
||||
iconName={icons.SAVE}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!hasPendingChanges}
|
||||
onPress={onSavePress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{additionalButtons}
|
||||
</PageToolbarSection>
|
||||
|
||||
<PendingChangesModal
|
||||
isOpen={nextLocation !== null}
|
||||
onConfirm={handleConfirmNavigation}
|
||||
onCancel={handleCancelNavigation}
|
||||
/>
|
||||
</PageToolbar>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsToolbar;
|
@ -1,148 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
|
||||
import SettingsToolbar from './SettingsToolbar';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
advancedSettings: state.settings.advancedSettings
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class SettingsToolbarConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
nextLocation: null,
|
||||
nextLocationAction: null,
|
||||
confirmed: false
|
||||
};
|
||||
|
||||
this._unblock = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._unblock = this.props.history.block(this.routerWillLeave);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._unblock) {
|
||||
this._unblock();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
routerWillLeave = (nextLocation, nextLocationAction) => {
|
||||
if (this.state.confirmed) {
|
||||
this.setState({
|
||||
nextLocation: null,
|
||||
nextLocationAction: null,
|
||||
confirmed: false
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.props.hasPendingChanges ) {
|
||||
this.setState({
|
||||
nextLocation,
|
||||
nextLocationAction
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
onConfirmNavigation = () => {
|
||||
const {
|
||||
nextLocation,
|
||||
nextLocationAction
|
||||
} = this.state;
|
||||
|
||||
const history = this.props.history;
|
||||
|
||||
const path = `${nextLocation.pathname}${nextLocation.search}`;
|
||||
|
||||
this.setState({
|
||||
confirmed: true
|
||||
}, () => {
|
||||
if (nextLocationAction === 'PUSH') {
|
||||
history.push(path);
|
||||
} else {
|
||||
// Unfortunately back and forward both use POP,
|
||||
// which means we don't actually know which direction
|
||||
// the user wanted to go, assuming back.
|
||||
|
||||
history.goBack();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onCancelNavigation = () => {
|
||||
this.setState({
|
||||
nextLocation: null,
|
||||
nextLocationAction: null,
|
||||
confirmed: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const hasPendingLocation = this.state.nextLocation !== null;
|
||||
|
||||
return (
|
||||
<SettingsToolbar
|
||||
hasPendingLocation={hasPendingLocation}
|
||||
onSavePress={this.props.onSavePress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onConfirmNavigation={this.onConfirmNavigation}
|
||||
onCancelNavigation={this.onCancelNavigation}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const historyShape = {
|
||||
block: PropTypes.func.isRequired,
|
||||
goBack: PropTypes.func.isRequired,
|
||||
push: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
SettingsToolbarConnector.propTypes = {
|
||||
showSave: PropTypes.bool,
|
||||
hasPendingChanges: PropTypes.bool.isRequired,
|
||||
history: PropTypes.shape(historyShape).isRequired,
|
||||
onSavePress: PropTypes.func,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
SettingsToolbarConnector.defaultProps = {
|
||||
hasPendingChanges: false
|
||||
};
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SettingsToolbarConnector));
|
Loading…
Reference in new issue