Compare commits

...

1 Commits

Author SHA1 Message Date
David Joy
261769cca9 Using hooks in DeleteAccount - goodnight, redux! 2019-07-09 17:10:13 -04:00
15 changed files with 165 additions and 595 deletions

View File

@@ -51,7 +51,7 @@ export class ConfirmationModal extends Component {
render() {
const {
status,
open,
errorType,
intl,
onCancel,
@@ -59,7 +59,6 @@ export class ConfirmationModal extends Component {
onSubmit,
password,
} = this.props;
const open = ['confirming', 'pending', 'failed'].includes(status);
const passwordFieldId = 'passwordFieldId';
const invalidMessage = messages[this.getShortErrorMessageId(errorType)];
return (
@@ -113,7 +112,7 @@ export class ConfirmationModal extends Component {
}
ConfirmationModal.propTypes = {
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),
open: PropTypes.bool,
errorType: PropTypes.oneOf(['empty-password', 'server']),
intl: intlShape.isRequired,
onCancel: PropTypes.func.isRequired,
@@ -123,7 +122,7 @@ ConfirmationModal.propTypes = {
};
ConfirmationModal.defaultProps = {
status: null,
open: false,
errorType: null,
};

View File

@@ -18,7 +18,7 @@ describe('ConfirmationModal', () => {
onCancel: jest.fn(),
onChange: jest.fn(),
onSubmit: jest.fn(),
status: null,
open: false,
errorType: null,
password: 'fluffy bunnies',
logoutUrl: 'http://localhost/logout',
@@ -44,7 +44,8 @@ describe('ConfirmationModal', () => {
<IntlProvider locale="en">
<IntlConfirmationModal
{...props}
status="pending" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
// This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
open
/>
</IntlProvider>
))
@@ -59,7 +60,8 @@ describe('ConfirmationModal', () => {
<IntlConfirmationModal
{...props}
errorType="empty-password"
status="pending" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
// This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
open
/>
</IntlProvider>
))

View File

@@ -1,18 +1,8 @@
import React from 'react';
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-i18n';
import { Button, Hyperlink } from '@edx/paragon';
// Actions
import {
deleteAccount,
deleteAccountConfirmation,
deleteAccountFailure,
deleteAccountReset,
deleteAccountCancel,
} from './data/actions';
// Messages
import messages from './messages';
@@ -21,106 +11,116 @@ import ConnectedConfirmationModal from './ConfirmationModal';
import PrintingInstructions from './PrintingInstructions';
import ConnectedSuccessModal from './SuccessModal';
import BeforeProceedingBanner from './BeforeProceedingBanner';
import { postDeleteAccount } from './data/service';
import useAction from '../../common/hooks';
export class DeleteAccount extends React.Component {
state = {
password: '',
};
function DeleteAccount(props) {
const {
logoutUrl, hasLinkedTPA, isVerifiedAccount, intl,
} = props;
handleSubmit = () => {
if (this.state.password === '') {
this.props.deleteAccountFailure('empty-password');
} else {
this.props.deleteAccount(this.state.password);
const [password, setPassword] = useState('');
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const [validationError, setValidationError] = useState(null);
const [
deleteAccountState,
performDeleteAccount,
resetDeleteAccount,
] = useAction(postDeleteAccount);
const canDelete = isVerifiedAccount && !hasLinkedTPA;
const successDialogOpen = deleteAccountState.loaded;
const error = deleteAccountState.error !== null ? 'server' : validationError;
// Event handlers
const handleDeleteAccountClick = useCallback(() => {
if (canDelete) {
setConfirmationDialogOpen(true);
}
};
});
handleCancel = () => {
this.setState({ password: '' });
this.props.deleteAccountCancel();
};
const handleSubmit = useCallback(() => {
if (password === '') {
setValidationError('empty-password');
} else {
performDeleteAccount(password);
}
});
handlePasswordChange = (e) => {
this.setState({ password: e.target.value.trim() });
this.props.deleteAccountReset();
};
const handleCancel = useCallback(() => {
setPassword('');
setConfirmationDialogOpen(false);
resetDeleteAccount();
});
handleFinalClose = () => {
global.location = this.props.logoutUrl;
};
const handlePasswordChange = useCallback((e) => {
setPassword(e.target.value.trim());
setValidationError(null);
});
render() {
const {
hasLinkedTPA, isVerifiedAccount, status, errorType, intl,
} = this.props;
const canDelete = isVerifiedAccount && !hasLinkedTPA;
const handleFinalClose = useCallback(() => {
global.location = logoutUrl;
});
return (
<div>
<h2 className="section-heading">
{intl.formatMessage(messages['account.settings.delete.account.header'])}
</h2>
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
<p>{intl.formatMessage(messages['account.settings.delete.account.text.1'])}</p>
<p>{intl.formatMessage(messages['account.settings.delete.account.text.2'])}</p>
<p>
<PrintingInstructions />
</p>
<p className="text-danger h6">
{intl.formatMessage(messages['account.settings.delete.account.text.warning'])}
</p>
<p>
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
</Hyperlink>
</p>
<p>
<Button
className="btn-outline-danger"
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
disabled={!canDelete}
>
{intl.formatMessage(messages['account.settings.delete.account.button'])}
</Button>
</p>
return (
<div>
<h2 className="section-heading">
{intl.formatMessage(messages['account.settings.delete.account.header'])}
</h2>
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
<p>{intl.formatMessage(messages['account.settings.delete.account.text.1'])}</p>
<p>{intl.formatMessage(messages['account.settings.delete.account.text.2'])}</p>
<p>
<PrintingInstructions />
</p>
<p className="text-danger h6">
{intl.formatMessage(messages['account.settings.delete.account.text.warning'])}
</p>
<p>
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
</Hyperlink>
</p>
<p>
<Button
className="btn-outline-danger"
onClick={handleDeleteAccountClick}
disabled={!canDelete}
>
{intl.formatMessage(messages['account.settings.delete.account.button'])}
</Button>
</p>
{isVerifiedAccount ? null : (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.activate"
supportUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
/>
)}
{hasLinkedTPA ? (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.unlink"
supportUrl="https://support.edx.org/hc/en-us/articles/207206067"
/>
) : null}
<ConnectedConfirmationModal
status={status}
errorType={errorType}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
onChange={this.handlePasswordChange}
password={this.state.password}
{isVerifiedAccount ? null : (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.activate"
supportUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
/>
)}
<ConnectedSuccessModal status={status} onClose={this.handleFinalClose} />
</div>
);
}
{hasLinkedTPA ? (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.unlink"
supportUrl="https://support.edx.org/hc/en-us/articles/207206067"
/>
) : null}
<ConnectedConfirmationModal
open={confirmationDialogOpen}
errorType={error}
onSubmit={handleSubmit}
onCancel={handleCancel}
onChange={handlePasswordChange}
password={password}
/>
<ConnectedSuccessModal open={successDialogOpen} onClose={handleFinalClose} />
</div>
);
}
DeleteAccount.propTypes = {
deleteAccount: PropTypes.func.isRequired,
deleteAccountConfirmation: PropTypes.func.isRequired,
deleteAccountFailure: PropTypes.func.isRequired,
deleteAccountReset: PropTypes.func.isRequired,
deleteAccountCancel: PropTypes.func.isRequired,
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),
errorType: PropTypes.oneOf(['empty-password', 'server']),
hasLinkedTPA: PropTypes.bool,
isVerifiedAccount: PropTypes.bool,
logoutUrl: PropTypes.string.isRequired,
@@ -130,20 +130,6 @@ DeleteAccount.propTypes = {
DeleteAccount.defaultProps = {
hasLinkedTPA: false,
isVerifiedAccount: true,
status: null,
errorType: null,
};
// Assume we're part of the accountSettings state.
const mapStateToProps = state => state.accountSettings.deleteAccount;
export default connect(
mapStateToProps,
{
deleteAccount,
deleteAccountConfirmation,
deleteAccountFailure,
deleteAccountReset,
deleteAccountCancel,
},
)(injectIntl(DeleteAccount));
export default injectIntl(DeleteAccount);

View File

@@ -1,27 +1,18 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl } from '@edx/frontend-i18n';
import { IntlProvider } from '@edx/frontend-i18n';
// Testing the modals separately, they just clutter up the snapshots if included here.
jest.mock('./ConfirmationModal');
jest.mock('./SuccessModal');
import { DeleteAccount } from './DeleteAccount'; // eslint-disable-line import/first
const IntlDeleteAccount = injectIntl(DeleteAccount);
import DeleteAccount from './DeleteAccount'; // eslint-disable-line import/first
describe('DeleteAccount', () => {
let props = {};
beforeEach(() => {
props = {
deleteAccount: jest.fn(),
deleteAccountConfirmation: jest.fn(),
deleteAccountFailure: jest.fn(),
deleteAccountReset: jest.fn(),
deleteAccountCancel: jest.fn(),
status: null,
errorType: null,
hasLinkedTPA: false,
isVerifiedAccount: true,
logoutUrl: 'http://localhost/logout',
@@ -32,7 +23,7 @@ describe('DeleteAccount', () => {
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlDeleteAccount
<DeleteAccount
{...props}
/>
</IntlProvider>
@@ -45,7 +36,7 @@ describe('DeleteAccount', () => {
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlDeleteAccount
<DeleteAccount
{...props}
isVerifiedAccount={false}
/>
@@ -59,7 +50,7 @@ describe('DeleteAccount', () => {
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlDeleteAccount
<DeleteAccount
{...props}
hasLinkedTPA
/>

View File

@@ -6,10 +6,10 @@ import { Modal } from '@edx/paragon';
import messages from './messages';
export const SuccessModal = (props) => {
const { status, intl, onClose } = props;
const { open, intl, onClose } = props;
return (
<Modal
open={status === 'deleted'}
open={open}
title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
body={
<div>
@@ -26,13 +26,13 @@ export const SuccessModal = (props) => {
};
SuccessModal.propTypes = {
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),
open: PropTypes.bool,
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired,
};
SuccessModal.defaultProps = {
status: null,
open: false,
};
export default injectIntl(SuccessModal);

View File

@@ -16,30 +16,15 @@ describe('SuccessModal', () => {
beforeEach(() => {
props = {
onClose: jest.fn(),
status: null,
open: false,
};
});
it('should match default closed success modal snapshot', () => {
let tree = renderer.create((
const tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} /></IntlProvider>))
.toJSON();
expect(tree).toMatchSnapshot();
tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} status="confirming" /></IntlProvider>))
.toJSON();
expect(tree).toMatchSnapshot();
tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} status="pending" /></IntlProvider>))
.toJSON();
expect(tree).toMatchSnapshot();
tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} status="failed" /></IntlProvider>))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should match open success modal snapshot', () => {
@@ -48,7 +33,8 @@ describe('SuccessModal', () => {
<IntlProvider locale="en">
<IntlSuccessModal
{...props}
status="deleted" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
// This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
open
/>
</IntlProvider>
))

View File

@@ -61,21 +61,21 @@ exports[`SuccessModal should match default closed success modal snapshot 1`] = `
</div>
`;
exports[`SuccessModal should match default closed success modal snapshot 2`] = `
exports[`SuccessModal should match open success modal snapshot 1`] = `
<div>
<div
className="fade"
className="modal-backdrop show"
role="presentation"
/>
<div
className="modal js-close-modal-on-click fade"
className="modal js-close-modal-on-click show d-block"
onClick={[Function]}
role="presentation"
>
<div
aria-labelledby="id4"
aria-modal={true}
className=""
className="modal-dialog"
role="dialog"
tabIndex="-1"
>
@@ -121,186 +121,3 @@ exports[`SuccessModal should match default closed success modal snapshot 2`] = `
</div>
</div>
`;
exports[`SuccessModal should match default closed success modal snapshot 3`] = `
<div>
<div
className="fade"
role="presentation"
/>
<div
className="modal js-close-modal-on-click fade"
onClick={[Function]}
role="presentation"
>
<div
aria-labelledby="id5"
aria-modal={true}
className=""
role="dialog"
tabIndex="-1"
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id5"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
<div>
<div
className="fade"
role="presentation"
/>
<div
className="modal js-close-modal-on-click fade"
onClick={[Function]}
role="presentation"
>
<div
aria-labelledby="id6"
aria-modal={true}
className=""
role="dialog"
tabIndex="-1"
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id6"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`SuccessModal should match open success modal snapshot 1`] = `
<div>
<div
className="modal-backdrop show"
role="presentation"
/>
<div
className="modal js-close-modal-on-click show d-block"
onClick={[Function]}
role="presentation"
>
<div
aria-labelledby="id7"
aria-modal={true}
className="modal-dialog"
role="dialog"
tabIndex="-1"
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id7"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -1,39 +0,0 @@
import { utils } from '../../../common';
const { AsyncActionType } = utils;
export const DELETE_ACCOUNT = new AsyncActionType('ACCOUNT_SETTINGS', 'DELETE_ACCOUNT');
DELETE_ACCOUNT.CONFIRMATION = 'ACCOUNT_SETTINGS__DELETE_ACCOUNT__CONFIRMATION';
DELETE_ACCOUNT.CANCEL = 'ACCOUNT_SETTINGS__DELETE_ACCOUNT__CANCEL';
export const deleteAccount = password => ({
type: DELETE_ACCOUNT.BASE,
payload: { password },
});
export const deleteAccountConfirmation = () => ({
type: DELETE_ACCOUNT.CONFIRMATION,
});
export const deleteAccountBegin = () => ({
type: DELETE_ACCOUNT.BEGIN,
});
export const deleteAccountSuccess = () => ({
type: DELETE_ACCOUNT.SUCCESS,
});
export const deleteAccountFailure = reason => ({
type: DELETE_ACCOUNT.FAILURE,
payload: { reason },
});
// to clear errors from the confirmation modal
export const deleteAccountReset = () => ({
type: DELETE_ACCOUNT.RESET,
});
// to close the modal
export const deleteAccountCancel = () => ({
type: DELETE_ACCOUNT.CANCEL,
});

View File

@@ -1,60 +0,0 @@
import { DELETE_ACCOUNT } from './actions';
export const defaultState = {
status: null,
errorType: null,
};
const reducer = (state = defaultState, action = null) => {
if (action !== null) {
switch (action.type) {
case DELETE_ACCOUNT.CONFIRMATION:
return {
...state,
status: 'confirming',
};
case DELETE_ACCOUNT.BEGIN:
return {
...state,
status: 'pending',
};
case DELETE_ACCOUNT.SUCCESS:
return {
...state,
status: 'deleted',
};
case DELETE_ACCOUNT.FAILURE:
return {
...state,
status: 'failed',
errorType: action.payload.reason || 'server',
};
case DELETE_ACCOUNT.RESET: {
const oldStatus = state.status;
return {
...state,
// clear the error state if applicable, otherwise don't change state
status: oldStatus === 'failed' ? 'confirming' : oldStatus,
errorType: null,
};
}
case DELETE_ACCOUNT.CANCEL:
return {
...state,
status: null,
errorType: null,
};
default:
}
}
return state;
};
export default reducer;

View File

@@ -1,107 +0,0 @@
import reducer from './reducers';
import {
deleteAccountConfirmation,
deleteAccountBegin,
deleteAccountSuccess,
deleteAccountFailure,
deleteAccountReset,
deleteAccountCancel,
} from './actions';
describe('delete-account reducer', () => {
let state = null;
beforeEach(() => {
state = reducer();
});
it('should process DELETE_ACCOUNT.CONFIRMATION', () => {
const result = reducer(state, deleteAccountConfirmation());
expect(result).toEqual({
errorType: null,
status: 'confirming',
});
});
it('should process DELETE_ACCOUNT.BEGIN', () => {
const result = reducer(state, deleteAccountBegin());
expect(result).toEqual({
errorType: null,
status: 'pending',
});
});
it('should process DELETE_ACCOUNT.SUCCESS', () => {
const result = reducer(state, deleteAccountSuccess());
expect(result).toEqual({
errorType: null,
status: 'deleted',
});
});
it('should process DELETE_ACCOUNT.FAILURE no reason', () => {
const result = reducer(state, deleteAccountFailure());
expect(result).toEqual({
errorType: 'server',
status: 'failed',
});
});
it('should process DELETE_ACCOUNT.FAILURE with reason', () => {
const result = reducer(state, deleteAccountFailure('carnivorous buns'));
expect(result).toEqual({
errorType: 'carnivorous buns',
status: 'failed',
});
});
it('should process DELETE_ACCOUNT.RESET no status', () => {
const result = reducer(state, deleteAccountReset());
expect(result).toEqual({
errorType: null,
status: null,
});
});
it('should process DELETE_ACCOUNT.RESET with failed old status', () => {
const result = reducer(
{
errorType: 'carnivorous buns',
status: 'failed',
},
deleteAccountReset(),
);
expect(result).toEqual({
errorType: null,
status: 'confirming',
});
});
it('should process DELETE_ACCOUNT.RESET with pending old status', () => {
const result = reducer(
{
errorType: 'carnivorous buns',
status: 'pending',
},
deleteAccountReset(),
);
expect(result).toEqual({
errorType: null,
status: 'pending',
});
});
it('should process DELETE_ACCOUNT.CANCEL', () => {
const result = reducer(
{
errorType: 'carnivorous buns',
status: 'failed',
},
deleteAccountCancel(),
);
expect(result).toEqual({
errorType: null,
status: null,
});
});
});

View File

@@ -1,28 +0,0 @@
import { put, call, takeEvery } from 'redux-saga/effects';
import {
DELETE_ACCOUNT,
deleteAccountBegin,
deleteAccountSuccess,
deleteAccountFailure,
} from './actions';
import { postDeleteAccount } from './service';
export function* handleDeleteAccount(action) {
try {
yield put(deleteAccountBegin());
const response = yield call(postDeleteAccount, action.payload.password);
yield put(deleteAccountSuccess(response));
} catch (e) {
if (typeof e.response.data === 'string') {
yield put(deleteAccountFailure());
} else {
throw e;
}
}
}
export default function* saga() {
yield takeEvery(DELETE_ACCOUNT.BASE, handleDeleteAccount);
}

View File

@@ -1,5 +1,2 @@
export { default } from './DeleteAccount';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { configureService } from './data/service';
export { DELETE_ACCOUNT } from './data/actions';

View File

@@ -9,7 +9,6 @@ import {
RESET_DRAFTS,
} from './actions';
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from './delete-account';
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from './site-language';
import { reducer as resetPasswordReducer, RESET_PASSWORD } from './reset-password';
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from './third-party-auth';
@@ -27,7 +26,6 @@ export const defaultState = {
timeZones: [],
countryTimeZones: [],
previousSiteLanguage: null,
deleteAccount: deleteAccountReducer(),
siteLanguage: siteLanguageReducer(),
resetPassword: resetPasswordReducer(),
thirdPartyAuth: thirdPartyAuthReducer(),
@@ -154,18 +152,6 @@ const reducer = (state = defaultState, action) => {
// TODO: Once all the above cases have been converted into sub-reducers, we can use
// combineReducers in this file to greatly simplify it.
// Delete My Account
case DELETE_ACCOUNT.CONFIRMATION:
case DELETE_ACCOUNT.BEGIN:
case DELETE_ACCOUNT.SUCCESS:
case DELETE_ACCOUNT.FAILURE:
case DELETE_ACCOUNT.RESET:
case DELETE_ACCOUNT.CANCEL:
return {
...state,
deleteAccount: deleteAccountReducer(state.deleteAccount, action),
};
case FETCH_SITE_LANGUAGES.BEGIN:
case FETCH_SITE_LANGUAGES.SUCCESS:
case FETCH_SITE_LANGUAGES.FAILURE:

View File

@@ -19,7 +19,6 @@ import {
import { usernameSelector, userRolesSelector, siteLanguageSelector } from './selectors';
// Sub-modules
import { saga as deleteAccountSaga } from './delete-account';
import { saga as resetPasswordSaga } from './reset-password';
import { saga as siteLanguageSaga, ApiService as SiteLanguageApiService } from './site-language';
import { saga as thirdPartyAuthSaga } from './third-party-auth';
@@ -103,7 +102,6 @@ export default function* saga() {
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
yield takeEvery(FETCH_TIME_ZONES.BASE, handleFetchTimeZones);
yield all([
deleteAccountSaga(),
siteLanguageSaga(),
resetPasswordSaga(),
thirdPartyAuthSaga(),

42
src/common/hooks.js Normal file
View File

@@ -0,0 +1,42 @@
import { useState } from 'react';
const useAction = (action) => {
const [loaded, setLoaded] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const performAction = async (body = null) => {
try {
setLoading(true);
setLoaded(false);
setData(null);
setError(null);
const result = await action(body);
setData(result);
setLoaded(true);
} catch (e) {
if (e.response.data) {
setError(e.response.data);
} else {
throw e;
}
setLoaded(false);
} finally {
setLoading(false);
}
};
const resetAction = () => {
setLoading(false);
setLoaded(false);
setData(null);
setError(null);
};
return [{
loaded, loading, data, error,
}, performAction, resetAction];
};
export default useAction;