WIP fix-general

This commit is contained in:
elforjani3
2020-05-03 23:08:47 +02:00
parent a807cf6bb8
commit b91718f350
13 changed files with 380 additions and 139 deletions

View File

@@ -3,10 +3,12 @@ import AccountFormDialog from 'containers/Dashboard/Dialogs/AccountFormDialog';
import UserFormDialog from 'containers/Dashboard/Dialogs/UserFormDialog'; import UserFormDialog from 'containers/Dashboard/Dialogs/UserFormDialog';
import ItemCategoryDialog from 'containers/Dashboard/Dialogs/ItemCategoryDialog'; import ItemCategoryDialog from 'containers/Dashboard/Dialogs/ItemCategoryDialog';
import CurrencyDialog from 'containers/Dashboard/Dialogs/CurrencyDialog'; import CurrencyDialog from 'containers/Dashboard/Dialogs/CurrencyDialog';
import InviteUserDialog from 'containers/Dashboard/Dialogs/InviteUserDialog';
export default function DialogsContainer() { export default function DialogsContainer() {
return ( return (
<React.Fragment> <React.Fragment>
<InviteUserDialog />
<CurrencyDialog /> <CurrencyDialog />
<ItemCategoryDialog /> <ItemCategoryDialog />
<AccountFormDialog /> <AccountFormDialog />

View File

@@ -1,12 +1,6 @@
import {connect} from 'react-redux'; import { connect } from 'react-redux';
import { import { submitInvite, editUser, fetchUser } from 'store/users/users.actions';
submitUser, import { getUserDetails } from 'store/users/users.reducer';
editUser,
fetchUser,
} from 'store/users/users.actions';
import {
getUserDetails
} from 'store/users/users.reducer';
import { getDialogPayload } from 'store/dashboard/dashboard.reducer'; import { getDialogPayload } from 'store/dashboard/dashboard.reducer';
import t from 'store/types'; import t from 'store/types';
@@ -15,18 +9,22 @@ export const mapStateToProps = (state, props) => {
return { return {
name: 'user-form', name: 'user-form',
payload: {action: 'new', id: null}, payload: { action: 'new', id: null },
userDetails: dialogPayload.action === 'edit' userDetails:
? getUserDetails(state, dialogPayload.user.id) : {}, dialogPayload.action === 'edit'
? getUserDetails(state, dialogPayload.user.id)
: {},
}; };
}; };
export const mapDispatchToProps = (dispatch) => ({ export const mapDispatchToProps = (dispatch) => ({
openDialog: (name, payload) => dispatch({ type: t.OPEN_DIALOG, name, payload }), openDialog: (name, payload) =>
closeDialog: (name, payload) => dispatch({ type: t.CLOSE_DIALOG, name, payload }), dispatch({ type: t.OPEN_DIALOG, name, payload }),
submitUser: (form) => dispatch(submitUser({ form })), closeDialog: (name, payload) =>
editUser: (id, form) => dispatch(editUser({ form, id })), dispatch({ type: t.CLOSE_DIALOG, name, payload }),
fetchUser: (id) => dispatch(fetchUser({ id })), requestSubmitInvite: (form) => dispatch(submitInvite({ form })),
requestEditUser: (id, form) => dispatch(editUser({ form, id })),
requestFetchUser: (id) => dispatch(fetchUser({ id })),
}); });
export default connect(mapStateToProps, mapDispatchToProps); export default connect(mapStateToProps, mapDispatchToProps);

View File

@@ -1,10 +1,32 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchUsers, fetchUser, deleteUser } from 'store/users/users.actions'; import {
fetchUsers,
fetchUser,
deleteUser,
inactiveUser,
editUser,
} from 'store/users/users.actions';
import t from 'store/types'; import t from 'store/types';
import { getUserDetails } from 'store/users/users.reducer';
import { getDialogPayload } from 'store/dashboard/dashboard.reducer';
export const mapStateToProps = (state, props) => ({ export const mapStateToProps = (state, props) => {
usersList: state.users.list.results, const dialogPayload = getDialogPayload(state, 'userList-form');
});
return {
name: 'userList-form',
payload: { action: 'new', id: null },
userDetails:
dialogPayload.action === 'edit'
? getUserDetails(state, dialogPayload.user.id)
: {},
editUser:
dialogPayload && dialogPayload.action === 'edit'
? state.users.list.results[dialogPayload.user.id]
: {},
usersList: state.users.list.results,
};
};
export const mapDispatchToProps = (dispatch) => ({ export const mapDispatchToProps = (dispatch) => ({
openDialog: (name, payload) => openDialog: (name, payload) =>
@@ -12,9 +34,11 @@ export const mapDispatchToProps = (dispatch) => ({
closeDialog: (name, payload) => closeDialog: (name, payload) =>
dispatch({ type: t.CLOSE_DIALOG, name, payload }), dispatch({ type: t.CLOSE_DIALOG, name, payload }),
fetchUsers: () => dispatch(fetchUsers({})), requestFetchUsers: () => dispatch(fetchUsers({})),
fetchUser: (id) => dispatch(fetchUser({ id })), requestFetchUser: (id) => dispatch(fetchUser({ id })),
deleteUser: (id) => dispatch(deleteUser({ id })), requestDeleteUser: (id) => dispatch(deleteUser({ id })),
requestInactiveUser: (id) => dispatch(inactiveUser({ id })),
requestEditUser: (id, form) => dispatch(editUser({ form, id })),
}); });
export default connect(mapStateToProps, mapDispatchToProps); export default connect(mapStateToProps, mapDispatchToProps);

View File

@@ -0,0 +1,192 @@
import React, { useMemo } from 'react';
import { useIntl } from 'react-intl';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import {
Dialog,
Button,
FormGroup,
InputGroup,
Intent,
Classes,
} from '@blueprintjs/core';
import UserListDialogConnect from 'connectors/UsersList.connector';
import DialogReduxConnect from 'components/DialogReduxConnect';
import useAsync from 'hooks/async';
import { objectKeysTransform } from 'utils';
import { pick, snakeCase } from 'lodash';
import ErrorMessage from 'components/ErrorMessage';
import classNames from 'classnames';
import AppToaster from 'components/AppToaster';
import { compose } from 'utils';
function InviteUserDialog({
name,
payload,
isOpen,
closeDialog,
requestFetchUser,
requestEditUser,
}) {
const intl = useIntl();
const fetchHook = useAsync(async () => {
await Promise.all([
...(payload.action === 'edit' ? [requestFetchUser(payload.user.id)] : []),
]);
}, false);
const validationSchema = Yup.object().shape({
first_name: Yup.string().required(intl.formatMessage({ id: 'required' })),
last_name: Yup.string().required(intl.formatMessage({ id: 'required' })),
email: Yup.string()
.email()
.required(intl.formatMessage({ id: 'required' })),
phone_number: Yup.number().required(intl.formatMessage({ id: 'required' })),
});
const initialValues = useMemo(
() => ({
first_name: '',
last_name: '',
email: '',
phone_number: '',
}),
[]
);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...(payload.action === 'edit' &&
pick(
objectKeysTransform(payload.user, snakeCase),
Object.keys(initialValues)
)),
},
validationSchema,
onSubmit: (values, { setSubmitting }) => {
const form = {
...values,
};
if (payload.action === 'edit') {
requestEditUser(payload.user.id, form)
.then((response) => {
closeDialog(name);
AppToaster.show({
message: 'the_user_details_has_been_updated',
});
setSubmitting(false);
})
.catch((error) => {
setSubmitting(false);
});
}
},
});
const { values, errors, touched } = useMemo(() => formik, [formik]);
const onDialogOpening = () => {
fetchHook.execute();
};
const onDialogClosed = () => {
formik.resetForm();
};
const handleClose = () => {
closeDialog(name);
};
return (
<Dialog
name={name}
title={payload.action === 'edit' ? 'Edit invite' : ''}
className={classNames({
'dialog--loading': fetchHook.pending,
'dialog--invite-user': true,
})}
autoFocus={true}
canEscapeKeyClose={true}
isOpen={isOpen}
isLoading={fetchHook.pending}
onClosed={onDialogClosed}
onOpening={onDialogOpening}
>
<form onSubmit={formik.handleSubmit}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={'First Name'}
className={'form-group--first-name'}
intent={errors.first_name && touched.first_name && Intent.DANGER}
helperText={<ErrorMessage name='first_name' {...formik} />}
inline={true}
>
<InputGroup
intent={errors.first_name && touched.first_name && Intent.DANGER}
{...formik.getFieldProps('first_name')}
/>
</FormGroup>
<FormGroup
label={'Last Name'}
className={'form-group--last-name'}
intent={errors.last_name && touched.last_name && Intent.DANGER}
helperText={<ErrorMessage name='last_name' {...formik} />}
inline={true}
>
<InputGroup
intent={errors.last_name && touched.last_name && Intent.DANGER}
{...formik.getFieldProps('last_name')}
/>
</FormGroup>
<FormGroup
label={'Email'}
className={'form-group--email'}
intent={errors.email && touched.email && Intent.DANGER}
helperText={<ErrorMessage name='email' {...formik} />}
inline={true}
>
<InputGroup
medium={true}
intent={errors.email && touched.email && Intent.DANGER}
{...formik.getFieldProps('email')}
/>
</FormGroup>
<FormGroup
label={'Phone Number'}
className={'form-group--phone-number'}
intent={
errors.phone_number && touched.phone_number && Intent.DANGER
}
helperText={<ErrorMessage name='phone_number' {...formik} />}
inline={true}
>
<InputGroup
intent={
errors.phone_number && touched.phone_number && Intent.DANGER
}
{...formik.getFieldProps('phone_number')}
/>
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}>Close</Button>
<Button intent={Intent.PRIMARY} type='submit'>
{payload.action === 'edit' ? 'Edit' : ''}
</Button>
</div>
</div>
</form>
</Dialog>
);
}
export default compose(
UserListDialogConnect,
DialogReduxConnect
)(InviteUserDialog);

View File

@@ -21,9 +21,9 @@ import classNames from 'classnames';
import { compose } from 'utils'; import { compose } from 'utils';
function UserFormDialog({ function UserFormDialog({
fetchUser, requestFetchUser,
submitUser, requestSubmitInvite,
editUser, requestEditUser,
name, name,
payload, payload,
isOpen, isOpen,
@@ -32,12 +32,12 @@ function UserFormDialog({
const intl = useIntl(); const intl = useIntl();
const fetchHook = useAsync(async () => { const fetchHook = useAsync(async () => {
await Promise.all([ await Promise.all([
...(payload.action === 'edit' ? [fetchUser(payload.user.id)] : []), ...(payload.action === 'edit' ? [requestFetchUser(payload.user.id)] : []),
]); ]);
}, false); }, false);
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
email: Yup.string().email().required(), email: Yup.string().email().required(intl.formatMessage({id:'required'})),
}); });
const initialValues = { const initialValues = {
@@ -47,7 +47,6 @@ function UserFormDialog({
objectKeysTransform(payload.user, snakeCase), objectKeysTransform(payload.user, snakeCase),
Object.keys(validationSchema.fields) Object.keys(validationSchema.fields)
)), )),
password: '',
}; };
const formik = useFormik({ const formik = useFormik({
@@ -57,17 +56,16 @@ function UserFormDialog({
onSubmit: (values) => { onSubmit: (values) => {
const form = { const form = {
...values, ...values,
confirm_password: values.password,
}; };
if (payload.action === 'edit') { if (payload.action === 'edit') {
editUser(payload.user.id, form).then((response) => { requestEditUser(payload.user.id, form).then((response) => {
AppToaster.show({ AppToaster.show({
message: 'the_user_details_has_been_updated', message: 'the_user_details_has_been_updated',
}); });
closeDialog(name); closeDialog(name);
}); });
} else { } else {
submitUser(form).then((response) => { requestSubmitInvite(form).then((response) => {
AppToaster.show({ AppToaster.show({
message: 'the_user_has_been_invited', message: 'the_user_has_been_invited',
}); });
@@ -93,7 +91,7 @@ function UserFormDialog({
return ( return (
<Dialog <Dialog
name={name} name={name}
title={payload.action === 'edit' ? 'Edit invite' : 'New invite'} title={payload.action === 'edit' ? 'Edit invite' : 'invite User'}
className={classNames({ className={classNames({
'dialog--loading': fetchHook.pending, 'dialog--loading': fetchHook.pending,
'dialog--invite-form': true, 'dialog--invite-form': true,
@@ -120,19 +118,6 @@ function UserFormDialog({
{...formik.getFieldProps('email')} {...formik.getFieldProps('email')}
/> />
</FormGroup> </FormGroup>
<FormGroup
label={'Email'}
className={'form-group--email'}
intent={errors.email && touched.email && Intent.DANGER}
helperText={<ErrorMessage name='email' {...formik} />}
inline={true}
>
<InputGroup
medium={true}
intent={errors.email && touched.email && Intent.DANGER}
{...formik.getFieldProps('email')}
/>
</FormGroup>
</div> </div>
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>

View File

@@ -1,49 +1,36 @@
import React, {useCallback} from 'react'; import React, { useCallback } from 'react';
import { import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
Tabs,
Tab,
Button,
Intent,
} from '@blueprintjs/core';
import PreferencesSubContent from 'components/Preferences/PreferencesSubContent'; import PreferencesSubContent from 'components/Preferences/PreferencesSubContent';
import connector from 'connectors/UsersPreferences.connector'; import connector from 'connectors/UsersPreferences.connector';
function UsersPreferences({ function UsersPreferences({ openDialog }) {
openDialog, const onChangeTabs = (currentTabId) => {};
}) {
const onChangeTabs = (currentTabId) => {
};
const onClickNewUser = useCallback(() => { const onClickNewUser = useCallback(() => {
openDialog('user-form'); openDialog('user-form');
}, [openDialog]); }, [openDialog]);
return ( return (
<div class="preferences__inside-content preferences__inside-content--users-roles"> <div class='preferences__inside-content preferences__inside-content--users-roles'>
<div class="preferences__tabs"> <div class='preferences__tabs'>
<Tabs <Tabs animate={true} large={true} onChange={onChangeTabs}>
animate={true} <Tab id='users' title='Users' />
large={true} <Tab id='roles' title='Roles' />
onChange={onChangeTabs}>
<Tab id="users" title="Users" />
<Tab id="roles" title="Roles" />
<div class="preferences__tabs-extra-actions"> <div class='preferences__tabs-extra-actions'>
<Button <Button intent={Intent.PRIMARY} onClick={onClickNewUser}>
intent={Intent.PRIMARY} Invite User
onClick={onClickNewUser}>New invite</Button> </Button>
<Button <Button intent={Intent.PRIMARY} onClick={onClickNewUser}>
intent={Intent.PRIMARY} New Role
onClick={onClickNewUser}>New Role</Button> </Button>
</div> </div>
</Tabs> </Tabs>
</div> </div>
<PreferencesSubContent preferenceTab="users" /> <PreferencesSubContent preferenceTab='users' />
</div> </div>
); );
} }
export default connector(UsersPreferences); export default connector(UsersPreferences);

View File

@@ -21,30 +21,48 @@ import DialogConnect from 'connectors/Dialog.connector';
import DashboardConnect from 'connectors/Dashboard.connector'; import DashboardConnect from 'connectors/Dashboard.connector';
function UsersListPreferences({ function UsersListPreferences({
fetchUsers, requestFetchUsers,
usersList, usersList,
openDialog, openDialog,
closeDialog, closeDialog,
deleteUser, requestDeleteUser,
requestInactiveUser,
onFetchData, onFetchData,
}) { }) {
const [deleteUserState, setDeleteUserState] = useState(false); const [deleteUserState, setDeleteUserState] = useState(false);
const [inactiveUserState, setInactiveUserState] = useState(false); const [inactiveUserState, setInactiveUserState] = useState(false);
const asyncHook = useAsync(async () => { const asyncHook = useAsync(async () => {
await Promise.all([fetchUsers()]); await Promise.all([requestFetchUsers()]);
}, []); }, []);
const onInactiveUser = (user) => {}; const onInactiveUser = (user) => {
setInactiveUserState(user);
};
// Handle cancel inactive user alert
const handleCancelInactiveUser = useCallback(() => {
setInactiveUserState(false);
}, []);
// handel confirm user activation
const handleConfirmUserActive = useCallback(() => {
requestInactiveUser(inactiveUserState.id).then(() => {
setInactiveUserState(false);
requestFetchUsers();
AppToaster.show({ message: 'the_user_has_been_inactivated' });
});
}, [inactiveUserState, requestInactiveUser, requestFetchUsers]);
const onDeleteUser = (user) => { const onDeleteUser = (user) => {
setDeleteUserState(user); setDeleteUserState(user);
}; };
const handleCancelUserDelete = () => { const handleCancelUserDelete = () => {
setDeleteUserState(false); setDeleteUserState(false);
}; };
const onEditUser = (user) => () => { const onEditInviteUser = (user) => () => {
const form = Object.keys(user).reduce((obj, key) => { const form = Object.keys(user).reduce((obj, key) => {
const camelKey = snakeCase(key); const camelKey = snakeCase(key);
obj[camelKey] = user[key]; obj[camelKey] = user[key];
@@ -54,12 +72,22 @@ function UsersListPreferences({
openDialog('user-form', { action: 'edit', user: form }); openDialog('user-form', { action: 'edit', user: form });
}; };
const onEditUser = (user) => () => {
const form = Object.keys(user).reduce((obj, key) => {
const camelKey = snakeCase(key);
obj[camelKey] = user[key];
return obj;
}, {});
openDialog('userList-form', { action: 'edit', user: form });
};
const handleConfirmUserDelete = () => { const handleConfirmUserDelete = () => {
if (!deleteUserState) { if (!deleteUserState) {
return; return;
} }
deleteUser(deleteUserState.id).then((response) => { requestDeleteUser(deleteUserState.id).then((response) => {
setDeleteUserState(false); setDeleteUserState(false);
AppToaster.show({ AppToaster.show({
message: 'the_user_has_been_deleted', message: 'the_user_has_been_deleted',
@@ -67,16 +95,18 @@ function UsersListPreferences({
}); });
}; };
const actionMenuList = (user) => ( const actionMenuList = useCallback(
<Menu> (user) => (
<MenuItem text='Edit User' onClick={onEditUser(user)} /> <Menu>
<MenuItem text='New Account' /> <MenuItem text='Edit User' onClick={onEditUser(user)} />
<MenuDivider /> <MenuDivider />
<MenuItem text='Inactivate User' onClick={() => onInactiveUser(user)} /> <MenuItem text='Edit Invite ' onClick={onEditInviteUser(user)} />
<MenuItem text='Delete User' onClick={() => onDeleteUser(user)} /> <MenuItem text='Inactivate User' onClick={() => onInactiveUser(user)} />
</Menu> <MenuItem text='Delete User' onClick={() => onDeleteUser(user)} />
</Menu>
),
[]
); );
console.log(usersList, 'X');
const columns = useMemo( const columns = useMemo(
() => [ () => [
@@ -151,6 +181,21 @@ function UsersListPreferences({
able to restore it later, but it will become private to you. able to restore it later, but it will become private to you.
</p> </p>
</Alert> </Alert>
<Alert
cancelButtonText='Cancel'
confirmButtonText='Inactivate'
icon='trash'
intent={Intent.WARNING}
isOpen={inactiveUserState}
onCancel={handleCancelInactiveUser}
onConfirm={handleConfirmUserActive}
>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be
able to restore it later, but it will become private to you.
</p>
</Alert>
</LoadingIndicator> </LoadingIndicator>
); );
} }

View File

@@ -1,36 +1,46 @@
import ApiService from "services/ApiService"; import ApiService from 'services/ApiService';
import t from 'store/types'; import t from 'store/types';
export const fetchUsers = () => { export const fetchUsers = () => {
return (dispatch) => new Promise((resolve, reject) => { return (dispatch) =>
ApiService.get(`users`).then((response) => { new Promise((resolve, reject) => {
dispatch({ ApiService.get(`users`)
type: t.USERS_LIST_SET, .then((response) => {
users: response.data.users, dispatch({
}); type: t.USERS_LIST_SET,
resolve(response); users: response.data.users,
}).catch((error) => { reject(error); }); });
}); resolve(response);
})
.catch((error) => {
reject(error);
});
});
}; };
export const fetchUser = ({ id }) => { export const fetchUser = ({ id }) => {
return (dispatch) => new Promise((resolve, reject) => { return (dispatch) =>
ApiService.get(`users/${id}`).then((response) => { new Promise((resolve, reject) => {
dispatch({ ApiService.get(`users/${id}`)
type: t.USER_DETAILS_SET, .then((response) => {
user: response.data.user, dispatch({
}); type: t.USER_DETAILS_SET,
resolve(response); user: response.data.user,
}).catch(error => { reject(error); }); });
}); resolve(response);
})
.catch((error) => {
reject(error);
});
});
}; };
export const deleteUser = ({ id }) => { export const deleteUser = ({ id }) => {
return (dispatch) => ApiService.delete(`users/${id}`); return (dispatch) => ApiService.delete(`users/${id}`);
}; };
export const submitUser = ({ form }) => { export const submitInvite = ({ form }) => {
return (dispatch) => ApiService.post(`users`, form); return (dispatch) => ApiService.post(`invite/send`, form);
}; };
export const editUser = ({ form, id }) => { export const editUser = ({ form, id }) => {
@@ -38,9 +48,9 @@ export const editUser = ({ form, id }) => {
}; };
export const inactiveUser = ({ id }) => { export const inactiveUser = ({ id }) => {
return (dispatch) => ApiService.post(`users/${id}/inactive`); return (dispatch) => ApiService.put(`users/${id}/inactive`);
}; };
export const activeUser = ({ id }) => { export const activeUser = ({ id }) => {
return (dispatch) => ApiService.post(`users/${id}/active`); return (dispatch) => ApiService.put(`users/${id}/active`);
} };

View File

@@ -46,6 +46,7 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
@import 'pages/items'; @import 'pages/items';
@import 'pages/invite-form.scss'; @import 'pages/invite-form.scss';
@import "pages/currency"; @import "pages/currency";
@import "pages/invite-user.scss";
// Views // Views
@import 'views/filter-dropdown'; @import 'views/filter-dropdown';

View File

@@ -8,5 +8,8 @@
width: 250px; width: 250px;
} }
} }
.bp3-dialog-header {
height: 170px;
}
} }
} }

View File

@@ -1,6 +1,3 @@
// .dialog--invite-form {
// }
.dialog--invite-form { .dialog--invite-form {
&.bp3-dialog { &.bp3-dialog {
@@ -30,18 +27,3 @@
} }
} }
// .bp3-dialog-body {
// .bp3-form-group.bp3-inline {
// .bp3-label {
// min-width: 100px;
// }
// .bp3-form-content {
// width: 250px;
// }
// }
// }
// .bp3-dialog-footer-actions {
// display: flex;
// justify-content: flex-end;
// margin-right: 100px;
// }

View File

@@ -0,0 +1,12 @@
.dialog--invite-user {
.bp3-dialog-body {
.bp3-form-group.bp3-inline {
.bp3-label {
min-width: 140px;
}
.bp3-form-content {
width: 250px;
}
}
}
}

View File

@@ -22,7 +22,7 @@
} }
} }
&-menu { &-menu {
width: 240px; width: 374px;
} }
&-button { &-button {
margin-right: 10px; margin-right: 10px;
@@ -86,7 +86,7 @@
min-width: 140px; min-width: 140px;
} }
.bp3-form-content { .bp3-form-content {
width: 250px; width: 384px;
} }
} }
} }