Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
veraPDF#25: Implemented option for conditional settings
Browse files Browse the repository at this point in the history
Igor Poplavsky committed Jun 10, 2020
1 parent 44eac05 commit 89f8c81
Showing 13 changed files with 264 additions and 52 deletions.
54 changes: 41 additions & 13 deletions src/components/layouts/pages/upload/Upload.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,67 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { hasFilesAttached } from '../../../../store/pdfFiles/selectors';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';

import AppPages from '../../../AppPages';
import { validate } from '../../../../store/job/actions';
import { hasFilesAttached } from '../../../../store/pdfFiles/selectors';
import { getUseSettings } from '../../../../store/application/selectors';
import { getJobId } from '../../../../store/job/selectors';
import Dropzone from './dropzone/Dropzone';
import WizardStep from '../../wizardStep/WizardStep';
import PageNavigation from '../../../shared/pageNavigation/PageNavigation';
import SettingsCheckbox from './settingsCheckbox/SettingsCheckbox';

function Upload(props) {
const { filesAttached } = props;
const forwardButton = useMemo(
() => ({
label: 'Configure job',
to: AppPages.SETTINGS,
disabled: !filesAttached,
}),
[filesAttached]
);
function Upload({ filesAttached, isUseSettings, jobId, onValidateClick }) {
const forwardButton = useMemo(() => {
const button = { disabled: !filesAttached };
if (isUseSettings) {
return {
...button,
label: 'Configure job',
to: AppPages.SETTINGS,
};
}
return {
...button,
label: 'Validate',
onClick: onValidateClick,
};
}, [filesAttached, onValidateClick, isUseSettings]);

if (!isUseSettings && jobId) {
// Once job is initialized and we know its ID redirect to status page to track its progress
return <Redirect push to={AppPages.STATUS.url(jobId)} />;
}

return (
<WizardStep stepIndex={AppPages.UPLOAD}>
<Dropzone />
<PageNavigation forward={forwardButton} />
<PageNavigation back={<SettingsCheckbox />} forward={forwardButton} />
</WizardStep>
);
}

Upload.propTypes = {
jobId: PropTypes.string,
isUseSettings: PropTypes.bool.isRequired,
filesAttached: PropTypes.bool.isRequired,
onValidateClick: PropTypes.func.isRequired,
};

const mapStateToProps = state => {
return {
filesAttached: hasFilesAttached(state),
isUseSettings: getUseSettings(state),
jobId: getJobId(state),
};
};

const mapDispatchToProps = dispatch => {
return {
onValidateClick: () => dispatch(validate()),
};
};

export default connect(mapStateToProps)(Upload);
export default connect(mapStateToProps, mapDispatchToProps)(Upload);
1 change: 0 additions & 1 deletion src/components/layouts/pages/upload/dropzone/Dropzone.scss
Original file line number Diff line number Diff line change
@@ -11,7 +11,6 @@

&__container {
width: 70%;
max-width: 700px;
height: 100px;
margin: auto;
padding: 20px;
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import { getUseSettings } from '../../../../../store/application/selectors';
import { getProfile } from '../../../../../store/job/settings/selectors';
import { getDefaultProfileLabel, getDefaultProfileName } from '../../../../../store/validationProfiles/selectors';
import { toggleUseSettings } from '../../../../../store/application/actions';
import { resetProfile } from '../../../../../store/job/settings/actions';
import Checkbox from '../../../../shared/checkbox/Checkbox';
import Dialog from '../../../../shared/dialog/Dialog';

const CHECK_SETTINGS = 'Use custom validation settings';

function SettingsCheckbox({
isUseSettings,
toggleSettings,
profile,
defaultProfile,
defaultProfileLabel,
resetProfile,
}) {
const [resetSettingsDialogOpened, setResetSettingsDialogOpened] = useState(false);
const onSettingsToggle = useCallback(() => {
if (profile !== defaultProfile && isUseSettings) {
return setResetSettingsDialogOpened(true);
}
return toggleSettings();
}, [defaultProfile, profile, toggleSettings, isUseSettings]);

const onResetSettingsClose = useCallback(() => {
setResetSettingsDialogOpened(false);
}, []);
const dialogActions = [
{
label: 'Cancel',
color: 'primary',
align: 'start',
onClick: onResetSettingsClose,
},
{
label: 'Reset settings',
color: 'primary',
variant: 'contained',
onClick: () => {
resetProfile();
toggleSettings();
onResetSettingsClose();
},
},
];

return (
<section className="settings-checkbox">
<Checkbox checked={isUseSettings} label={CHECK_SETTINGS} onChange={onSettingsToggle} />
<Dialog
onClose={onResetSettingsClose}
open={resetSettingsDialogOpened}
actions={dialogActions}
title={`You are about to reset profile to default ${defaultProfileLabel}.`}
>
Proceed?
</Dialog>
</section>
);
}

SettingsCheckbox.propTypes = {
isUseSettings: PropTypes.bool.isRequired,
profile: PropTypes.string,
defaultProfile: PropTypes.string,
defaultProfileLabel: PropTypes.string,
toggleSettings: PropTypes.func.isRequired,
resetProfile: PropTypes.func.isRequired,
};

const mapStateToProps = state => {
return {
isUseSettings: getUseSettings(state),
profile: getProfile(state),
defaultProfile: getDefaultProfileName(state),
defaultProfileLabel: getDefaultProfileLabel(state),
};
};

const mapDispatchToProps = dispatch => {
return {
toggleSettings: () => dispatch(toggleUseSettings()),
resetProfile: () => dispatch(resetProfile()),
};
};

export default connect(mapStateToProps, mapDispatchToProps)(SettingsCheckbox);
32 changes: 32 additions & 0 deletions src/components/shared/checkbox/Checkbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';

import MaterialCheckbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';

function Checkbox({ checked, label, disabled, onChange }) {
const handleChange = useCallback(e => onChange(e.target.checked), [onChange]);

return (
<FormControlLabel
className="checkbox-container"
disabled={disabled}
label={label}
control={<MaterialCheckbox checked={checked} color="primary" onChange={handleChange} />}
/>
);
}

Checkbox.propTypes = {
checked: PropTypes.bool.isRequired,
label: PropTypes.string,
disabled: PropTypes.bool,
onChange: PropTypes.func.isRequired,
};

Checkbox.defaultProps = {
label: '',
disabled: false,
};

export default Checkbox;
30 changes: 17 additions & 13 deletions src/components/shared/pageNavigation/PageNavigation.js
Original file line number Diff line number Diff line change
@@ -19,31 +19,35 @@ function PageNavigation(props) {
const { back, forward, center } = props;
return (
<nav className="page-navigation">
<section className="page-navigation__start">{getButton(back, TYPE.BACK)}</section>
<section className="page-navigation__center">{getButton(center, TYPE.CENTER)}</section>
<section className="page-navigation__end">{getButton(forward, TYPE.FORWARD)}</section>
<section className="page-navigation__start">{getComponent(back, TYPE.BACK)}</section>
<section className="page-navigation__center">{getComponent(center, TYPE.CENTER)}</section>
<section className="page-navigation__end">{getComponent(forward, TYPE.FORWARD)}</section>
</nav>
);
}

function getButton(buttonObject, type) {
if (buttonObject?.to) {
function getComponent(componentObject, type) {
if (React.isValidElement(componentObject)) {
return componentObject;
}

if (componentObject?.to) {
return (
<NavButton to={buttonObject.to} type={type} disabled={buttonObject.disabled} variant={VARIANTS[type]}>
{buttonObject.label}
<NavButton to={componentObject.to} type={type} disabled={componentObject.disabled} variant={VARIANTS[type]}>
{componentObject.label}
</NavButton>
);
}

if (buttonObject?.onClick) {
if (componentObject?.onClick) {
return (
<Button
variant={VARIANTS[type]}
color="primary"
disabled={buttonObject.disabled}
onClick={buttonObject.onClick}
disabled={componentObject.disabled}
onClick={componentObject.onClick}
>
{buttonObject.label}
{componentObject.label}
</Button>
);
}
@@ -59,8 +63,8 @@ const ButtonInterface = PropTypes.shape({
});

PageNavigation.propTypes = {
back: ButtonInterface,
forward: ButtonInterface,
back: PropTypes.oneOfType([ButtonInterface, PropTypes.element]),
forward: PropTypes.oneOfType([ButtonInterface, PropTypes.element]),
};

export default PageNavigation;
1 change: 1 addition & 0 deletions src/components/shared/pageNavigation/PageNavigation.scss
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
flex-grow: 1;
flex-basis: 0;
justify-content: center;
align-items: start;

.app-link {
color: inherit;
69 changes: 45 additions & 24 deletions src/components/shared/stepper/Stepper.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,52 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import _ from 'lodash';

import AppPages from '../../AppPages';
import { getUseSettings } from '../../../store/application/selectors';
import MaterialStepper from '@material-ui/core/Stepper';
import Step from '@material-ui/core/Step';
import StepLabel from '@material-ui/core/StepLabel';
import StepIcon from './StepIcon';
import AppPages from '../../AppPages';

import './Stepper.scss';

const STEPS = [
{
key: AppPages.UPLOAD,
label: 'Upload PDF',
},
{
key: AppPages.SETTINGS,
label: 'Select settings',
},
{
key: AppPages.STATUS.route,
label: 'Validation',
},
{
key: AppPages.RESULTS.route,
label: 'Verification results',
},
];
function Stepper({ activeStep, useSettings }) {
const STEPS = useMemo(
() => [
{
key: AppPages.UPLOAD,
label: 'Upload PDF',
},
{
key: AppPages.SETTINGS,
label: 'Select settings',
skip: !useSettings,
},
{
key: AppPages.STATUS.route,
label: 'Validation',
},
{
key: AppPages.RESULTS.route,
label: 'Verification results',
},
],
[useSettings]
);
const activeIndex = useMemo(
() =>
_.findIndex(
STEPS.filter(step => !step.skip),
{ key: activeStep }
),
[STEPS, activeStep]
);

function Stepper(props) {
const { activeStep } = props;
const activeIndex = useMemo(() => _.findIndex(STEPS, { key: activeStep }), [activeStep]);
return (
<MaterialStepper className="stepper" activeStep={activeIndex} alternativeLabel>
{STEPS.map(({ label }) => (
{STEPS.filter(step => !step.skip).map(({ label }) => (
<Step key={label}>
<StepLabel StepIconComponent={StepIcon}>{label}</StepLabel>
</Step>
@@ -43,6 +57,13 @@ function Stepper(props) {

Stepper.propTypes = {
activeStep: PropTypes.string.isRequired,
useSettings: PropTypes.bool.isRequired,
};

const mapStateToProps = state => {
return {
useSettings: getUseSettings(state),
};
};

export default Stepper;
export default connect(mapStateToProps)(Stepper);
2 changes: 2 additions & 0 deletions src/store/application/actions.js
Original file line number Diff line number Diff line change
@@ -14,6 +14,8 @@ export const unlockApp = createAction('APP_LOCK_SET', () => false);

export const resetApp = createAction('APP_RESET');

export const toggleUseSettings = createAction('USE_SETTINGS_TOGGLE');

export const reset = () => async (dispatch, getState) => {
const file = getFile(getState());
const profile = getDefaultProfileName(getState());
23 changes: 22 additions & 1 deletion src/store/application/reducer.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import { handleActions } from 'redux-actions';
import { USE_SETTINGS_FLAG } from '../constants';

const DEFAULT_STATE = {
initialized: false,
locked: false,
useSettings: false,
};

export const getDefaultState = () => {
let useSettings = sessionStorage.getItem(USE_SETTINGS_FLAG);

if (!useSettings) {
return DEFAULT_STATE;
}

useSettings = JSON.parse(useSettings);
return {
...DEFAULT_STATE,
useSettings,
};
};

export default handleActions(
{
APP_RESET: () => DEFAULT_STATE,
APP_STARTUP_FINISH: state => ({ ...state, initialized: true }),
APP_LOCK_SET: (state, { payload: locked }) => ({ ...state, locked }),
USE_SETTINGS_TOGGLE: state => {
const useSettings = !state.useSettings;
sessionStorage.setItem(USE_SETTINGS_FLAG, useSettings);
return { ...state, useSettings };
},
},
DEFAULT_STATE
getDefaultState()
);
2 changes: 2 additions & 0 deletions src/store/application/selectors.js
Original file line number Diff line number Diff line change
@@ -5,3 +5,5 @@ const getAppState = state => state.appState;
export const isInitialized = createSelector(getAppState, ({ initialized }) => initialized);

export const isLocked = createSelector(getAppState, ({ locked }) => locked);

export const getUseSettings = createSelector(getAppState, ({ useSettings }) => useSettings);
1 change: 1 addition & 0 deletions src/store/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const JOB_FILE = 'JOB_FILE';
export const JOB_SETTINGS = 'JOB_SETTINGS';
export const USE_SETTINGS_FLAG = 'USE_SETTINGS_FLAG';

export const JOB_STATUS = {
CREATED: 'CREATED',
6 changes: 6 additions & 0 deletions src/store/job/settings/actions.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { createAction } from 'redux-actions';
import { JOB_SETTINGS } from '../../constants';
import { getDefaultProfileName } from '../../validationProfiles/selectors';

const updateSettings = createAction('SETTINGS_UPDATE');

export const setSetting = (setting, value) => async (dispatch, getState) => {
await dispatch(updateSettings({ [setting]: value }));
sessionStorage.setItem(JOB_SETTINGS, JSON.stringify(getState().jobSettings));
};

export const resetProfile = () => async (dispatch, getState) => {
const profile = getDefaultProfileName(getState());
dispatch(updateSettings({ profile }));
};
2 changes: 2 additions & 0 deletions src/store/validationProfiles/selectors.js
Original file line number Diff line number Diff line change
@@ -17,4 +17,6 @@ export const getProfileOptions = createSelector(getProfiles, profiles => {

export const getDefaultProfileName = createSelector(getProfiles, profiles => _.first(profiles)?.profileName);

export const getDefaultProfileLabel = createSelector(getProfiles, profiles => _.first(profiles)?.humanReadableName);

export const getProfilesError = createSelector(getProfiles, profiles => _.get(profiles, 'error'));

0 comments on commit 89f8c81

Please sign in to comment.