Skip to content

Commit

Permalink
[kbss-cvut#66] Refactor users to use the new messaging component.
Browse files Browse the repository at this point in the history
Also fix messaging component style (before it is removed) broken by adding the new messaging component styles.
  • Loading branch information
ledsoft committed Jan 14, 2024
1 parent 92f2577 commit 3366134
Show file tree
Hide file tree
Showing 9 changed files with 51 additions and 160 deletions.
5 changes: 4 additions & 1 deletion js/actions/UsersActions.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as ActionConstants from "../constants/ActionConstants";
import {axiosBackend} from "./index";
import {API_URL} from '../../config';
import {publishMessage} from "./MessageActions";
import {errorMessage} from "../model/Message";

export function loadUsers() {
return function (dispatch) {
dispatch(loadUsersPending());
axiosBackend.get(`${API_URL}/rest/users`).then((response) => {
return axiosBackend.get(`${API_URL}/rest/users`).then((response) => {
dispatch(loadUsersSuccess(response.data));
}).catch((error) => {
dispatch(loadUsersError(error.response.data));
dispatch(publishMessage(errorMessage('users.loading-error', {error: error.response.data.message})));
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion js/components/AlertMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Alert} from "react-bootstrap";
import PropTypes from "prop-types";

const AlertMessage = (props) => (
<div className='message-container'>
<div>
<Alert className={`alert-position-${props.alertPosition || 'down'}`} variant={props.type}>
{props.message}
</Alert>
Expand Down
2 changes: 1 addition & 1 deletion js/components/login/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class Login extends React.Component {
<Card.Header className="text-light bg-primary" as="h6">{this.i18n('login.title')}</Card.Header>
<Card.Body>
{!this.state.deviceSupported &&
<div className='message-container'>
<div>
<Alert className={`alert-browser-support`} variant="warning">
{this.i18n('Your browser is not fully supported! Some parts of web may not work properly.')}<br/>
{this.i18n('We recommend using the latest version of ')}
Expand Down
21 changes: 9 additions & 12 deletions js/components/user/UserRow.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import React from "react";
import {injectIntl} from "react-intl";
import withI18n from "../../i18n/withI18n";
import {Button} from "react-bootstrap";
import {LoaderSmall} from "../Loader";
import PropTypes from "prop-types";
import IfInternalAuth from "../misc/oidc/IfInternalAuth";
import {useI18n} from "../../hooks/useI18n";

let UserRow = (props) => {
const user = props.user;
const {i18n} = useI18n();
return <tr>
<td className='report-row'>
<Button variant="link" size="sm" className="text-left"
onClick={() => props.onEdit(props.user)}
title={props.i18n('users.open-tooltip')}>{user.firstName + ' ' + user.lastName}
title={i18n('users.open-tooltip')}>{user.firstName + ' ' + user.lastName}
</Button>
</td>
<td className='report-row'>{user.username}</td>
<td className='report-row'>{user.institution ? user.institution.name : ''}</td>
<td className='report-row'>{user.emailAddress}</td>
<IfInternalAuth>
<td className='report-row actions'>
<Button variant='primary' size='sm' title={props.i18n('users.open-tooltip')}
onClick={() => props.onEdit(props.user)}>{props.i18n('open')}</Button>
<Button variant='warning' size='sm' title={props.i18n('users.delete-tooltip')}
onClick={() => props.onDelete(props.user)}>{props.i18n('delete')}
{props.deletionLoading && <LoaderSmall/>}</Button>
<Button variant='primary' size='sm' title={i18n('users.open-tooltip')}
onClick={() => props.onEdit(props.user)}>{i18n('open')}</Button>
<Button variant='warning' size='sm' title={i18n('users.delete-tooltip')}
onClick={() => props.onDelete(props.user)}>{i18n('delete')}
</Button>
</td>
</IfInternalAuth>
</tr>;
Expand All @@ -34,8 +33,6 @@ UserRow.propTypes = {
user: PropTypes.object.isRequired,
onEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
deletionLoading: PropTypes.bool.isRequired,
i18n: PropTypes.func.isRequired,
};

export default injectIntl(withI18n(UserRow));
export default UserRow;
12 changes: 3 additions & 9 deletions js/components/user/UserTable.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
'use strict';

import React from "react";
import {Table} from "react-bootstrap";
import DeleteItemDialog from "../DeleteItemDialog";
import {injectIntl} from "react-intl";
import withI18n from "../../i18n/withI18n";
import UserRow from "./UserRow";
import {ACTION_STATUS} from "../../constants/DefaultConstants";
import PropTypes from "prop-types";
import IfInternalAuth from "../misc/oidc/IfInternalAuth";

class UserTable extends React.Component {
static propTypes = {
users: PropTypes.array.isRequired,
handlers: PropTypes.object.isRequired,
userDeleted: PropTypes.object
handlers: PropTypes.object.isRequired
};

constructor(props) {
Expand Down Expand Up @@ -77,13 +73,11 @@ class UserTable extends React.Component {
}

_renderUsers() {
const {users, userDeleted} = this.props;
const {users} = this.props;
const onEdit = this.props.handlers.onEdit;
let rows = [];
for (let i = 0, len = users.length; i < len; i++) {
rows.push(<UserRow key={users[i].username} user={users[i]} onEdit={onEdit} onDelete={this._onDelete}
deletionLoading={!!(userDeleted.status === ACTION_STATUS.PENDING
&& userDeleted.username === users[i].username)}/>);
rows.push(<UserRow key={users[i].username} user={users[i]} onEdit={onEdit} onDelete={this._onDelete}/>);
}
return rows;
}
Expand Down
81 changes: 25 additions & 56 deletions js/components/user/Users.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,33 @@
'use strict';

import React from 'react';
import {Button, Card} from 'react-bootstrap';
import {injectIntl} from "react-intl";
import withI18n from '../../i18n/withI18n';
import UserTable from './UserTable';
import {ACTION_STATUS, ALERT_TYPES} from "../../constants/DefaultConstants";
import AlertMessage from "../AlertMessage";
import {LoaderCard, LoaderSmall} from "../Loader";
import PropTypes from "prop-types";
import IfInternalAuth from "../misc/oidc/IfInternalAuth";
import PromiseTrackingMask from "../misc/PromiseTrackingMask";
import {useI18n} from "../../hooks/useI18n";

class Users extends React.Component {
static propTypes = {
usersLoaded: PropTypes.object,
handlers: PropTypes.object.isRequired,
userDeleted: PropTypes.object,
showAlert: PropTypes.bool.isRequired,
};

constructor(props) {
super(props);
this.i18n = this.props.i18n;
}

render() {
const {usersLoaded, showAlert, userDeleted} = this.props;
if (!usersLoaded.users && (!usersLoaded.status || usersLoaded.status === ACTION_STATUS.PENDING)) {
return <LoaderCard header={this.i18n('users.panel-title')}/>;
} else if (usersLoaded.status === ACTION_STATUS.ERROR) {
return <AlertMessage type={ALERT_TYPES.DANGER}
message={this.props.formatMessage('users.loading-error', {error: usersLoaded.error.message})}/>
}
return <Card>
<Card.Header className="text-light bg-primary" as="h6">
{this.i18n('users.panel-title')}
{this.props.usersLoaded.status === ACTION_STATUS.PENDING && <LoaderSmall/>}
</Card.Header>
<Card.Body>
<UserTable users={usersLoaded.users} {...this.props}/>
<IfInternalAuth>
<div>
<Button variant='primary' size='sm'
onClick={this.props.handlers.onCreate}>{this.i18n('users.create-user')}</Button>
</div>
</IfInternalAuth>
{showAlert && userDeleted.status === ACTION_STATUS.ERROR &&
<AlertMessage type={ALERT_TYPES.DANGER}
message={this.props.formatMessage('user.delete-error', {error: this.i18n(this.props.userDeleted.error.message)})}/>}
{showAlert && userDeleted.status === ACTION_STATUS.SUCCESS &&
<AlertMessage type={ALERT_TYPES.SUCCESS} message={this.i18n('user.delete-success')}/>}
</Card.Body>
</Card>;
}
const Users = ({usersLoaded, handlers}) => {
const {i18n} = useI18n();
return <Card>
<Card.Header className="text-light bg-primary" as="h6">
{i18n('users.panel-title')}
</Card.Header>
<Card.Body>
<PromiseTrackingMask area="users"/>
{usersLoaded.users && <UserTable users={usersLoaded.users} handlers={handlers}/>}
<IfInternalAuth>
<div>
<Button variant='primary' size='sm'
onClick={handlers.onCreate}>{i18n('users.create-user')}</Button>
</div>
</IfInternalAuth>
</Card.Body>
</Card>;
};

_renderHeader() {
return <span>
{this.i18n('users.panel-title')}{this.props.usersLoaded.status === ACTION_STATUS.PENDING && <LoaderSmall/>}
</span>;
}
}
Users.propTypes = {
usersLoaded: PropTypes.object,
handlers: PropTypes.object.isRequired
};

export default injectIntl(withI18n(Users));
export default Users;
3 changes: 2 additions & 1 deletion js/components/user/UsersController.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {bindActionCreators} from "redux";
import {loadUsers} from "../../actions/UsersActions";
import {ROLE} from "../../constants/DefaultConstants";
import {deleteUser} from "../../actions/UserActions";
import {trackPromise} from "react-promise-tracker";

class UsersController extends React.Component {
constructor(props) {
Expand All @@ -21,7 +22,7 @@ class UsersController extends React.Component {
}

componentDidMount() {
this.props.loadUsers();
trackPromise(this.props.loadUsers(), "users");
}

_onEditUser = (user) => {
Expand Down
6 changes: 3 additions & 3 deletions tests/__tests__/actions/UsersActions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {axiosBackend} from "../../../js/actions";
import {loadUsers, loadUsersError, loadUsersPending, loadUsersSuccess} from "../../../js/actions/UsersActions";
import {API_URL} from '../../../config';

describe('Users synchronize actions', function () {
describe('Users synchronous actions', function () {
it('creates an action to fetch all users', () => {
const expectedAction = {
type: ActionConstants.LOAD_USERS_PENDING,
Expand Down Expand Up @@ -37,7 +37,7 @@ describe('Users synchronize actions', function () {
const middlewares = [thunk.withExtraArgument(axiosBackend)];
const mockStore = configureMockStore(middlewares);

describe('Users asynchronize actions', function () {
describe('Users asynchronous actions', function () {
let store,
mockApi;
const users = [{username: 'test1'}, {username: 'test2'}],
Expand Down Expand Up @@ -78,7 +78,7 @@ describe('Users asynchronize actions', function () {
store.dispatch(loadUsers());

setTimeout(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(store.getActions().slice(0, 2)).toEqual(expectedActions);
done();
}, TEST_TIMEOUT);
});
Expand Down
79 changes: 3 additions & 76 deletions tests/__tests__/components/Users.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use strict';

import React from 'react';
import {IntlProvider} from 'react-intl';
import TestUtils from 'react-dom/test-utils';
Expand All @@ -12,8 +10,6 @@ describe('Users', function () {
let users,
usersLoaded,
usersLoadedEmpty,
userDeleted,
showAlert,
handlers;

users = [{
Expand All @@ -23,10 +19,6 @@ describe('Users', function () {
}];

beforeEach(() => {
showAlert = false;
userDeleted = {
status: ACTION_STATUS.SUCCESS
};
handlers = {
onEdit: jest.fn(),
onCreate: jest.fn(),
Expand All @@ -42,40 +34,10 @@ describe('Users', function () {
};
});

it('shows loader', function () {
usersLoaded = {
status: ACTION_STATUS.PENDING
};
const tree = TestUtils.renderIntoDocument(
<IntlProvider locale="en" {...intlData}>
<Users usersLoaded={usersLoaded} showAlert={showAlert}
userDeleted={userDeleted} handlers={handlers}/>
</IntlProvider>);
const result = TestUtils.findRenderedDOMComponentWithClass(tree, 'loader-spin');
expect(result).not.toBeNull();
});

it('shows error about institutions were not loaded', function () {
usersLoaded = {
status: ACTION_STATUS.ERROR,
error: {
message: "Error"
}
};
const tree = TestUtils.renderIntoDocument(
<IntlProvider locale="en" {...intlData}>
<Users usersLoaded={usersLoaded} showAlert={showAlert}
userDeleted={userDeleted} handlers={handlers}/>
</IntlProvider>);
const alert = TestUtils.scryRenderedDOMComponentsWithClass(tree, "alert-danger");
expect(alert).not.toBeNull();
});

it('renders card with text, that no users were found', function () {
const tree = TestUtils.renderIntoDocument(
<IntlProvider locale="en" {...intlData}>
<Users usersLoaded={usersLoadedEmpty} showAlert={showAlert}
userDeleted={userDeleted} handlers={handlers}/>
<Users usersLoaded={usersLoadedEmpty} handlers={handlers}/>
</IntlProvider>);
const cardHeading = TestUtils.findRenderedDOMComponentWithClass(tree, 'card');
expect(cardHeading).not.toBeNull();
Expand All @@ -88,8 +50,7 @@ describe('Users', function () {
it('renders card with table and table headers', function () {
const tree = TestUtils.renderIntoDocument(
<IntlProvider locale="en" {...intlData}>
<Users usersLoaded={usersLoaded} showAlert={showAlert}
userDeleted={userDeleted} handlers={handlers}/>
<Users usersLoaded={usersLoaded} handlers={handlers}/>
</IntlProvider>);
const cardHeading = TestUtils.findRenderedDOMComponentWithClass(tree, 'card');
expect(cardHeading).not.toBeNull();
Expand All @@ -104,46 +65,12 @@ describe('Users', function () {
it('renders "Create user" button and click on it', function () {
const tree = TestUtils.renderIntoDocument(
<IntlProvider locale="en" {...intlData}>
<Users usersLoaded={usersLoaded} showAlert={showAlert}
userDeleted={userDeleted} handlers={handlers}/>
<Users usersLoaded={usersLoaded} handlers={handlers}/>
</IntlProvider>);
const buttons = TestUtils.scryRenderedDOMComponentsWithTag(tree, "Button");
expect(buttons.length).toEqual(7);

TestUtils.Simulate.click(buttons[6]); // Create User
expect(handlers.onCreate).toHaveBeenCalled();
});

it('shows successful alert that user was successfully deleted', function () {
showAlert = true;
userDeleted = {
...userDeleted,
status: ACTION_STATUS.SUCCESS
};
const tree = TestUtils.renderIntoDocument(
<IntlProvider locale="en" {...intlData}>
<Users usersLoaded={usersLoaded} showAlert={showAlert}
userDeleted={userDeleted} handlers={handlers}/>
</IntlProvider>);
const alert = TestUtils.scryRenderedDOMComponentsWithClass(tree, "alert-success");
expect(alert).not.toBeNull();
});

it('shows unsuccessful alert that user was not deleted', function () {
showAlert = true;
userDeleted = {
...userDeleted,
status: ACTION_STATUS.ERROR,
error: {
message: "Error"
}
};
const tree = TestUtils.renderIntoDocument(
<IntlProvider locale="en" {...intlData}>
<Users usersLoaded={usersLoaded} showAlert={showAlert}
userDeleted={userDeleted} handlers={handlers}/>
</IntlProvider>);
const alert = TestUtils.scryRenderedDOMComponentsWithClass(tree, "alert-danger");
expect(alert).not.toBeNull();
});
});

0 comments on commit 3366134

Please sign in to comment.