fix(preferences): fix preferences users page.

This commit is contained in:
a.bouhuolia
2021-03-22 19:23:36 +02:00
parent d79be910f9
commit a0f4947138
20 changed files with 451 additions and 307 deletions

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Switch, Route } from 'react-router';
import { useQuery } from 'react-query';
import 'style/pages/Dashboard/Dashboard.scss';

View File

@@ -4,7 +4,7 @@ import {Switch, Route, useRouteMatch} from 'react-router-dom';
export default function PreferencesSubContent({ preferenceTab }) {
const routes = preferencesTabs[preferenceTab];
const {path} = useRouteMatch();
const { path } = useRouteMatch();
if (routes.length <= 0) { return null; }

View File

@@ -0,0 +1,6 @@
function UserActivateAlert() {
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { useDeleteUser } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'redux';
/**
* User delete alert.
*/
function UserDeleteAlert({
// #ownProps
name,
// #withAlertStoreConnect
isOpen,
payload: { userId },
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const { mutateAsync: deleteUserMutate, isLoading } = useDeleteUser();
const handleCancelUserDelete = () => {
closeAlert(name);
};
const handleConfirmUserDelete = () => {
deleteUserMutate(userId)
.then((response) => {
AppToaster.show({
message: formatMessage({
id: 'the_user_has_been_deleted_successfully',
}),
intent: Intent.SUCCESS,
});
})
.catch(({ response: { data: { errors } } }) => {
if (errors.find(e => e.type === 'CANNOT_DELETE_LAST_USER')) {
AppToaster.show({
message: 'Cannot delete the last user in the system.',
intent: Intent.DANGER,
});
}
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelUserDelete}
onConfirm={handleConfirmUserDelete}
loading={isLoading}
>
<p>
Once you delete this user, you won't be able to restore it later. Are
you sure you want to delete ?
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(UserDeleteAlert);

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Alert, Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import { useInactivateUser } from 'hooks/query';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
/**
* User inactivate alert.
*/
function UserInactivateAlert({
// #ownProps
name,
// #withAlertStoreConnect
isOpen,
payload: { userId },
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const { mutateAsync: userInactivateMutate } = useInactivateUser();
const handleConfirmInactivate = () => {
userInactivateMutate(userId)
.then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_user_has_been_inactivated_successfully',
}),
intent: Intent.SUCCESS,
});
})
.catch((error) => {
});
};
const handleCancel = () => {
closeAlert(name);
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'inactivate'} />}
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancel}
onConfirm={handleConfirmInactivate}
>
<p>
<T id={'are_sure_to_inactive_this_account'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(UserInactivateAlert);

View File

@@ -1,19 +1,14 @@
import React, { useMemo, useCallback } from 'react';
import React from 'react';
import {
Button,
Classes,
FormGroup,
InputGroup,
Intent,
} from '@blueprintjs/core';
import { Form, useFormikContext, FastField } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { pick } from 'lodash';
import { FastField } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import {
ErrorMessage,
AppToaster,
FieldRequiredHint,
DialogContent,
} from 'components';
import { useAutofocus } from 'hooks';
@@ -33,7 +28,7 @@ export default function CurrencyFormFields() {
className={'form-group--currency-name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="currency_name" />}
inline={true}
// inline={true}
>
<InputGroup
inputRef={(ref) => (currencyNameFieldRef.current = ref)}
@@ -51,7 +46,7 @@ export default function CurrencyFormFields() {
className={'form-group--currency-code'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="currency_code" />}
inline={true}
// inline={true}
>
<InputGroup {...field} />
</FormGroup>

View File

@@ -14,7 +14,7 @@ function CurrencyFormProvider({ isEditMode, currency, dialogName, ...props }) {
const { mutateAsync: editCurrencyMutate } = useEditCurrency();
// fetch Currencies list.
const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies();
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
// Provider state.
const provider = {

View File

@@ -1,5 +1,5 @@
import React, { lazy } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormattedMessage as T } from 'react-intl';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
@@ -30,6 +30,7 @@ function CurrencyFormDialog({
isOpen={isOpen}
autoFocus={true}
canEscapeKeyClose={true}
style={{ width: '450px' }}
>
<DialogSuspense>
<CurrencyFormDialogContent

View File

@@ -12,7 +12,7 @@ function InviteUserFormProvider({ userId, isEditMode, dialogName, ...props }) {
const { mutateAsync: inviteUserMutate } = useCreateInviteUser();
// fetch users list.
const { isFetching: isUsersLoading } = useUsers();
const { isLoading: isUsersLoading } = useUsers();
// Provider state.
const provider = {

View File

@@ -1,10 +1,10 @@
import { formatMessage } from 'services/intl';
export const transformApiErrors = (errors) => {
const fields = {};
if (errors.find(error => error.type === 'EMAIL.ALREADY.INVITED')) {
fields.email = formatMessage({ id: 'email_is_already_used' });
}
return fields;
}
const fields = {};
if (errors.find((error) => error.type === 'EMAIL.ALREADY.INVITED')) {
fields.email = formatMessage({ id: 'email_is_already_used' });
}
return fields;
};

View File

@@ -8,7 +8,7 @@ const CurrenciesContext = createContext();
*/
function CurrenciesProvider({ ...props }) {
// fetches the currencies list.
const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies();
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
const state = {
currencies,
@@ -16,9 +16,7 @@ function CurrenciesProvider({ ...props }) {
};
return (
<>
<CurrenciesContext.Provider value={state} {...props} />
</>
<CurrenciesContext.Provider value={state} {...props} />
);
}

View File

@@ -2,10 +2,16 @@ import React from 'react';
import { Tabs, Tab } from '@blueprintjs/core';
import classNames from 'classnames';
import 'style/pages/Preferences/Users.scss'
import { CLASSES } from 'common/classes';
import PreferencesSubContent from 'components/Preferences/PreferencesSubContent';
import withUserPreferences from 'containers/Preferences/Users/withUserPreferences';
/**
* Preferences page - Users page.
*/
function UsersPreferences({ openDialog }) {
const onChangeTabs = (currentTabId) => {};

View File

@@ -0,0 +1,14 @@
import React from 'react';
import UserDeleteAlert from 'containers/Alerts/Users/UserDeleteAlert';
import UserInactivateAlert from 'containers/Alerts/Users/UserInactivateAlert';
// import UserActivateAlert from 'containers/Alerts/UserActivateAlert';
export default function UsersAlerts() {
return (
<>
<UserDeleteAlert name={'user-delete'} />
<UserInactivateAlert name={'user-inactivate'} />
{/* <UserActivateAlert name={'user-activate'} /> */}
</>
);
}

View File

@@ -1,178 +1,78 @@
import React, { useCallback, useState, useMemo } from 'react';
import {
Intent,
Button,
Popover,
Menu,
MenuDivider,
Tag,
MenuItem,
Position,
} from '@blueprintjs/core';
import { withRouter } from 'react-router';
import { snakeCase } from 'lodash';
import React, { useCallback } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { compose, firstLettersArgs } from 'utils';
import { DataTable, Icon, If } from 'components';
import { compose } from 'utils';
import { DataTable } from 'components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withUsers from 'containers/Users/withUsers';
import withAlertActions from 'containers/Alert/withAlertActions';
const AvatarCell = (row) => {
return <span className={'avatar'}>{ firstLettersArgs(row.email) }</span>;
}
import { ActionsMenu, useUsersListColumns } from './components';
import { useUsersListContext } from './UsersProvider';
/**
* Users datatable.
*/
function UsersDataTable({
// #withDialogActions
openDialog,
// #withUsers
usersList,
usersLoading,
// #ownProps
loading,
onFetchData,
onInactiveUser,
onDeleteUser,
onSelectedRowsChange,
// #withAlertActions
openAlert,
}) {
const { formatMessage } = useIntl();
const onEditUser = useCallback(
(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 });
// Handle edit user action.
const handleEditUserAction = useCallback(
(user) => {
openDialog('userList-form', { action: 'edit', userId: user.id });
},
[openDialog],
);
const actionMenuList = useCallback(
(user) => (
<Menu>
<If condition={user.invite_accepted_at}>
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_user' })}
onClick={onEditUser(user)}
/>
<MenuDivider />
<MenuItem
text={formatMessage({ id: 'inactivate_user' })}
onClick={() => onInactiveUser(user)}
/>
</If>
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={formatMessage({ id: 'delete_user' })}
onClick={() => onDeleteUser(user)}
intent={Intent.DANGER}
/>
</Menu>
),
[onInactiveUser, onDeleteUser, onEditUser, formatMessage],
);
const onRowContextMenu = useCallback(
(cell) => {
return actionMenuList(cell.row.original);
// Handle inactivate user action.
const handleInactivateUser = useCallback(
(user) => {
openAlert('user-inactivate', { userId: user.id });
},
[actionMenuList],
[openAlert]
);
const columns = useMemo(
() => [
{
id: 'avatar',
Header: '',
accessor: AvatarCell,
width: 100,
},
{
id: 'full_name',
Header: formatMessage({ id: 'full_name' }),
accessor: 'full_name',
width: 150,
},
{
id: 'email',
Header: formatMessage({ id: 'email' }),
accessor: 'email',
width: 150,
},
{
id: 'phone_number',
Header: formatMessage({ id: 'phone_number' }),
accessor: 'phone_number',
width: 120,
},
{
id: 'status',
Header: 'Status',
accessor: (user) =>
!user.invite_accepted_at ? (
<Tag minimal={true}>
<T id={'inviting'} />
</Tag>
) : user.active ? (
<Tag intent={Intent.SUCCESS} minimal={true}>
<T id={'activate'} />
</Tag>
) : (
<Tag intent={Intent.WARNING} minimal={true}>
<T id={'inactivate'} />
</Tag>
),
width: 80,
className: 'status',
},
{
id: 'actions',
Header: '',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
),
className: 'actions',
width: 50,
disableResizing: true,
},
],
[actionMenuList, formatMessage],
);
const handelDataTableFetchData = useCallback(
(...args) => {
onFetchData && onFetchData(...args);
// Handle activate user action.
const handleActivateuser = useCallback(
(user) => {
openAlert('user-activate', { userId: user.id });
},
[onFetchData],
[openAlert]
);
// Handle delete user action.
const handleDeleteUser = useCallback(
(user) => {
openAlert('user-delete', { userId: user.id });
},
[openAlert]
);
// Users list columns.
const columns = useUsersListColumns();
// Users list context.
const { users, isUsersLoading, isUsersFetching } = useUsersListContext();
return (
<DataTable
columns={columns}
data={usersList}
loading={loading}
onFetchData={handelDataTableFetchData}
data={users}
loading={isUsersLoading}
headerLoading={isUsersLoading}
progressBarLoading={isUsersFetching}
noInitialFetch={true}
rowContextMenu={onRowContextMenu}
ContextMenu={ActionsMenu}
payload={{
onEdit: handleEditUserAction,
onActivate: handleInactivateUser,
onInactivate: handleActivateuser,
onDelete: handleDeleteUser
}}
/>
);
}
export default compose(
withRouter,
withDialogActions,
withUsers(({ usersList, usersLoading }) => ({ usersList, usersLoading })),
withAlertActions
)(UsersDataTable);

View File

@@ -1,140 +1,34 @@
import React, { useState, useCallback, useEffect } from 'react';
import { queryCache, useQuery } from 'react-query';
import { Alert, Intent } from '@blueprintjs/core';
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import {UsersListProvider } from './UsersProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withUsersActions from 'containers/Users/withUsersActions';
import UsersDataTable from './UsersDataTable';
import {
FormattedMessage as T,
FormattedHTMLMessage,
useIntl,
} from 'react-intl';
import AppToaster from 'components/AppToaster';
import UsersAlerts from './UsersAlerts';
import { compose } from 'utils';
/**
* Users list.
*/
function UsersListPreferences({
// #withDashboardActions
changePreferencesPageTitle,
// #withUsersActions
requestDeleteUser,
requestInactiveUser,
requestFetchUsers,
}) {
const [deleteUserState, setDeleteUserState] = useState(false);
const [inactiveUserState, setInactiveUserState] = useState(false);
const { formatMessage } = useIntl();
const fetchUsers = useQuery('users-table', () => requestFetchUsers());
useEffect(() => {
changePreferencesPageTitle(formatMessage({ id: 'users' }));
}, [changePreferencesPageTitle, formatMessage]);
// Handle cancel/confirm user inactive.
const handleInactiveUser = useCallback((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);
AppToaster.show({
message: formatMessage({
id: 'the_user_has_been_inactivated_successfully',
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('users-table');
})
.catch((error) => {
setInactiveUserState(false);
});
}, [inactiveUserState, requestInactiveUser, formatMessage]);
// Handle click and cancel/confirm user delete
const handleDeleteUser = useCallback((user) => {
setDeleteUserState(user);
}, []);
// handle cancel delete user alert.
const handleCancelUserDelete = () => {
setDeleteUserState(false);
};
const handleEditUser = useCallback(() => {}, []);
// Handle confirm User delete
const handleConfirmUserDelete = useCallback(() => {
if (!deleteUserState) {
return;
}
requestDeleteUser(deleteUserState.id)
.then((response) => {
setDeleteUserState(false);
AppToaster.show({
message: formatMessage({
id: 'the_user_has_been_deleted_successfully',
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('users-table');
})
.catch((errors) => {
setDeleteUserState(false);
});
}, [deleteUserState, requestDeleteUser, formatMessage]);
return (
<>
<UsersDataTable
loading={fetchUsers.isFetching}
onDeleteUser={handleDeleteUser}
onInactiveUser={handleInactiveUser}
onEditUser={handleEditUser}
/>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
intent={Intent.DANGER}
isOpen={deleteUserState}
onCancel={handleCancelUserDelete}
onConfirm={handleConfirmUserDelete}
>
<p>
Once you delete this user, you won't be able to restore it later. Are you sure you want to delete ?
</p>
</Alert>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'inactivate'} />}
intent={Intent.WARNING}
isOpen={inactiveUserState}
onCancel={handleCancelInactiveUser}
onConfirm={handleConfirmUserActive}
>
<p>
<T id={'are_sure_to_inactive_this_account'} />
</p>
</Alert>
</>
<UsersListProvider>
<UsersDataTable />
<UsersAlerts />
</UsersListProvider>
);
}
export default compose(
withDashboardActions,
withUsersActions,
)(UsersListPreferences);

View File

@@ -0,0 +1,25 @@
import React, { createContext } from 'react';
import { useUsers } from 'hooks/query';
const UsersListContext = createContext();
/**
* Users list provider.
*/
function UsersListProvider(props) {
const { data: users, isLoading, isFetching } = useUsers();
const state = {
isUsersLoading: isLoading,
isUsersFetching: isFetching,
users,
};
return (
<UsersListContext.Provider value={state} {...props} />
);
}
const useUsersListContext = () => React.useContext(UsersListContext);
export { UsersListProvider, useUsersListContext };

View File

@@ -0,0 +1,143 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import {
Intent,
Button,
Popover,
Menu,
MenuDivider,
Tag,
MenuItem,
Position,
} from '@blueprintjs/core';
import { safeCallback, firstLettersArgs } from 'utils';
import { Icon, If } from 'components';
/**
* Avatar cell.
*/
function AvatarCell(row) {
return <span className={'avatar'}>{firstLettersArgs(row.email)}</span>;
}
/**
* Users table actions menu.
*/
export function ActionsMenu({
row: { original },
payload: {
onEdit,
onInactivate,
onActivate,
onDelete
}
}) {
const { formatMessage } = useIntl();
return (
<Menu>
<If condition={original.invite_accepted_at}>
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_user' })}
onClick={safeCallback(onEdit, original)}
/>
<MenuDivider />
<MenuItem
text={formatMessage({ id: 'inactivate_user' })}
onClick={safeCallback(onInactivate, original)}
/>
</If>
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={formatMessage({ id: 'delete_user' })}
onClick={safeCallback(onDelete, original)}
intent={Intent.DANGER}
/>
</Menu>
);
}
/**
* Status accessor.
*/
function StatusAccessor(user) {
return !user.invite_accepted_at ? (
<Tag minimal={true}>
<T id={'inviting'} />
</Tag>
) : user.active ? (
<Tag intent={Intent.SUCCESS} minimal={true}>
<T id={'activate'} />
</Tag>
) : (
<Tag intent={Intent.WARNING} minimal={true}>
<T id={'inactivate'} />
</Tag>
);
}
/**
* Actions cell.
*/
function ActionsCell(props) {
return (
<Popover
content={<ActionsMenu {...props} />}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
}
export const useUsersListColumns = () => {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
id: 'avatar',
Header: '',
accessor: AvatarCell,
width: 40,
},
{
id: 'full_name',
Header: formatMessage({ id: 'full_name' }),
accessor: 'full_name',
width: 150,
},
{
id: 'email',
Header: formatMessage({ id: 'email' }),
accessor: 'email',
width: 150,
},
{
id: 'phone_number',
Header: formatMessage({ id: 'phone_number' }),
accessor: 'phone_number',
width: 120,
},
{
id: 'status',
Header: 'Status',
accessor: StatusAccessor,
width: 80,
className: 'status',
},
{
id: 'actions',
Header: '',
Cell: ActionsCell,
className: 'actions',
width: 50,
disableResizing: true,
},
],
[formatMessage],
);
};

View File

@@ -1,6 +1,5 @@
import { useMutation, useQueryClient } from 'react-query';
import { defaultTo } from 'lodash';
import { useQueryTenant } from '../useQueryRequest';
import { useQueryTenant, useRequestQuery } from '../useQueryRequest';
import useApiRequest from '../useRequest';
import t from './types';
@@ -43,6 +42,24 @@ export function useEditUser(props) {
});
}
export function useInactivateUser(props) {
const apiRequest = useApiRequest();
const queryClient = useQueryClient();
return useMutation(
([id, values]) => apiRequest.post(`users/${id}/inactivate`, values),
{
onSuccess: (res, [id, values]) => {
queryClient.invalidateQueries([t.USER, id]);
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
/**
* Deletes the given user.
*/
@@ -53,7 +70,7 @@ export function useDeleteUser(props) {
return useMutation((id) => apiRequest.delete(`users/${id}`), {
onSuccess: (res, id) => {
queryClient.invalidateQueries([t.USER, id]);
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
@@ -65,18 +82,18 @@ export function useDeleteUser(props) {
* Retrieves users list.
*/
export function useUsers(props) {
const apiRequest = useApiRequest();
const result = useQueryTenant(
return useRequestQuery(
[t.USERS],
() => apiRequest.get(`USERS`).then((response) => response.data.users),
props,
{
method: 'get',
url: 'users',
},
{
select: (res) => res.data.users,
defaultData: [],
...props,
},
);
return {
...result,
data: defaultTo(result.data, {}),
};
}
/**

View File

@@ -41,6 +41,7 @@
align-items: center;
height: 60px;
padding: 0 22px;
border-bottom: 1px solid #c9d9de;
h2 {
font-size: 22px;

View File

@@ -3,10 +3,11 @@
// ---------------------------------
.preferences-page__inside-content--users{
.bigcapital-datatable {
.bigcapital-datatable {
.td{
.avatar{
display: block;
height: 28px;
width: 28px;
text-align: center;