Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#71 Record filtering, paging, sorting #82

Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
781cebd
[Ref] Simplify RecordTable and RecordRow.
ledsoft Feb 6, 2024
5ff3a9f
[kbss-cvut/record-manager-ui#71] Refactor action history pagination t…
ledsoft Feb 6, 2024
b54a69c
[Ref] Refactor HistoryTable to hooks.
ledsoft Feb 6, 2024
2f3fd84
[kbss-cvut/record-manager-ui#71] Support paging of records.
ledsoft Feb 6, 2024
2ac5e75
[kbss-cvut/record-manager-ui#71] Support sorting of records by date.
ledsoft Feb 6, 2024
bb07890
[Ref] Unify minimum width of buttons.
ledsoft Feb 6, 2024
f4dba4b
[kbss-cvut/record-manager-ui#71] Implement record filtering.
ledsoft Feb 8, 2024
a58af89
[kbss-cvut/record-manager-ui#71] Indicate that filters are active, al…
ledsoft Feb 8, 2024
448e51e
[kbss-cvut/record-manager-ui#71] Export records based on selected fil…
ledsoft Feb 8, 2024
c467ae9
[kbss-cvut/record-manager-ui#71] Support filtering by multiple phases.
ledsoft Feb 8, 2024
3941b73
[kbss-cvut/record-manager-ui#71] Move filters into popups in column h…
ledsoft Feb 14, 2024
887913c
[kbss-cvut/record-manager-ui#71] Indicate that filters/sorting is act…
ledsoft Feb 14, 2024
fda9043
[kbss-cvut/record-manager-ui#71] Refactor records table header into s…
ledsoft Feb 14, 2024
82efe25
[kbss-cvut/record-manager-ui#71] Allow selecting page size for tables.
ledsoft Feb 14, 2024
f5b677d
[Fix] Prevent application crash when records are not loaded.
ledsoft Feb 14, 2024
670ab66
[kbss-cvut/record-manager-ui#71] Set default page size to 10 so that …
ledsoft Feb 14, 2024
6bc3fb2
Merge branch 'main' into kbss-cvut/record-manager-ui#71-record-filter…
blcham Feb 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion js/actions/HistoryActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as ActionConstants from "../constants/ActionConstants";
import {endsWith, omit, startsWith} from 'lodash';
import {API_URL} from '../../config';
import {showServerResponseErrorMessage} from "./AsyncActionUtils";
import {DEFAULT_PAGE_SIZE, STORAGE_TABLE_PAGE_SIZE_KEY} from "../constants/DefaultConstants";
import BrowserStorage from "../utils/BrowserStorage";

const URL_PREFIX = 'rest/history';

Expand All @@ -24,7 +26,7 @@ export function logAction(action, author, timestamp) {
}

export function loadActions(pageNumber, searchData) {
let urlSuffix = `?page=${pageNumber}`;
let urlSuffix = `?page=${pageNumber}&size=${BrowserStorage.get(STORAGE_TABLE_PAGE_SIZE_KEY, DEFAULT_PAGE_SIZE)}`;
if (searchData && searchData.author && searchData.action) {
urlSuffix = `${urlSuffix}&author=${searchData.author}&type=${searchData.action}`;
} else if (searchData && searchData.author) {
Expand Down
12 changes: 6 additions & 6 deletions js/actions/RecordActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {publishMessage} from "./MessageActions";
import {errorMessage, successMessage} from "../model/Message";
import {asyncError, asyncRequest, asyncSuccess, showServerResponseErrorMessage} from "./AsyncActionUtils";

export function deleteRecord(record, currentUser) {
export function deleteRecord(record) {
return function (dispatch) {
dispatch(deleteRecordPending(record.key));
return axiosBackend.delete(`${API_URL}/rest/records/${record.key}`, {
...record
}).then(() => {
dispatch(loadRecords(currentUser));
dispatch(loadRecords());
dispatch(deleteRecordSuccess(record, record.key));
dispatch(publishMessage(successMessage("record.delete-success")));
}).catch((error) => {
Expand Down Expand Up @@ -86,15 +86,15 @@ export function unloadRecord() {
}
}

export function createRecord(record, currentUser) {
export function createRecord(record) {
return function (dispatch, getState) {
dispatch(saveRecordPending(ACTION_FLAG.CREATE_ENTITY));
return axiosBackend.post(`${API_URL}/rest/records`, {
...record
}).then((response) => {
const key = Utils.extractKeyFromLocationHeader(response);
dispatch(saveRecordSuccess(record, key, ACTION_FLAG.CREATE_ENTITY));
dispatch(loadRecords(currentUser));
dispatch(loadRecords());
dispatch(publishMessage(successMessage("record.save-success")));
}).catch((error) => {
dispatch(saveRecordError(error.response.data, record, ACTION_FLAG.CREATE_ENTITY));
Expand All @@ -103,14 +103,14 @@ export function createRecord(record, currentUser) {
}
}

export function updateRecord(record, currentUser) {
export function updateRecord(record) {
return function (dispatch, getState) {
dispatch(saveRecordPending(ACTION_FLAG.UPDATE_ENTITY));
return axiosBackend.put(`${API_URL}/rest/records/${record.key}`, {
...record
}).then(() => {
dispatch(saveRecordSuccess(record, null, ACTION_FLAG.UPDATE_ENTITY));
dispatch(loadRecords(currentUser));
dispatch(loadRecords());
dispatch(publishMessage(successMessage("record.save-success")));
}).catch((error) => {
dispatch(saveRecordError(error.response.data, record, ACTION_FLAG.UPDATE_ENTITY));
Expand Down
52 changes: 32 additions & 20 deletions js/actions/RecordsActions.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import * as ActionConstants from "../constants/ActionConstants";
import {HttpHeaders, ROLE} from "../constants/DefaultConstants";
import {HttpHeaders} from "../constants/DefaultConstants";
import {axiosBackend} from "./index";
import {API_URL} from '../../config';
import {asyncError, asyncRequest, asyncSuccess, showServerResponseErrorMessage} from "./AsyncActionUtils";
import {fileDownload} from "../utils/Utils";
import {extractLastPageNumber, fileDownload, paramsSerializer} from "../utils/Utils";
import {publishMessage} from "./MessageActions";
import {infoMessage, successMessage} from "../model/Message";
import {isAdmin} from "../utils/SecurityUtils";

export function loadRecords(currentUser, institutionKey = null) {
let urlSuffix = '';
if (institutionKey) {
urlSuffix = `?institution=${institutionKey}`;
} else if (currentUser.role !== ROLE.ADMIN && currentUser.institution) {
urlSuffix = `?institution=${currentUser.institution.key}`;
}
return function (dispatch) {
export function loadRecordsByInstitution(institutionKey) {
return loadRecords({institution: institutionKey});
}

export function loadRecords(params = {}) {
return function (dispatch, getState) {
const currentUser = getState().auth.user;
if (currentUser && !isAdmin(currentUser) && currentUser.institution) {
params.institution = currentUser.institution.key;
}
dispatch(loadRecordsPending());
return axiosBackend.get(`${API_URL}/rest/records${urlSuffix}`).then((response) => {
dispatch(loadRecordsSuccess(response.data));
return axiosBackend.get(`${API_URL}/rest/records`, {
params,
paramsSerializer
}).then((response) => {
dispatch(loadRecordsSuccess(response.data, extractLastPageNumber(response)));
}).catch((error) => {
dispatch(loadRecordsError(error.response.data));
dispatch(showServerResponseErrorMessage(error, 'records.loading-error'));
Expand All @@ -29,22 +35,28 @@ export function loadRecordsPending() {
return asyncRequest(ActionConstants.LOAD_RECORDS_PENDING);
}

export function loadRecordsSuccess(records) {
export function loadRecordsSuccess(records, pageCount) {
return {
type: ActionConstants.LOAD_RECORDS_SUCCESS,
records
records,
pageCount
}
}

export function loadRecordsError(error) {
return asyncError(ActionConstants.LOAD_RECORDS_ERROR, error);
}

export function exportRecords(exportType, institutionKey) {
return (dispatch) => {
export function exportRecords(exportType, params = {}) {
return (dispatch, getState) => {
dispatch(asyncRequest(ActionConstants.EXPORT_RECORDS_PENDING));
const urlSuffix = institutionKey ? `?institution=${institutionKey}` : '';
return axiosBackend.get(`${API_URL}/rest/records/export${urlSuffix}`, {
const currentUser = getState().auth.user;
if (currentUser && !isAdmin(currentUser) && currentUser.institution) {
params.institution = currentUser.institution.key;
}
return axiosBackend.get(`${API_URL}/rest/records/export`, {
params,
paramsSerializer,
headers: {
accept: exportType.mediaType
},
Expand All @@ -64,13 +76,13 @@ export function exportRecords(exportType, institutionKey) {
}

export function importRecords(file) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(asyncRequest(ActionConstants.IMPORT_RECORDS_PENDING));
return file.text().then(content => {
return axiosBackend.post(`${API_URL}/rest/records/import`, JSON.parse(content))
}).then((resp) => {
dispatch(asyncSuccess(ActionConstants.IMPORT_RECORDS_SUCCESS));
dispatch(loadRecords(getState().auth.user));
dispatch(loadRecords());
if (resp.data.importedCount < resp.data.totalCount) {
dispatch(publishMessage(infoMessage("records.import.partialSuccess.message", {
importedCount: resp.data.importedCount,
Expand Down
18 changes: 8 additions & 10 deletions js/components/DeleteItemDialog.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
'use strict';

import React from 'react';
import {injectIntl, FormattedMessage} from "react-intl";
import {FormattedMessage} from "react-intl";
import {Button, Modal} from 'react-bootstrap';
import PropTypes from "prop-types";

import withI18n from '../i18n/withI18n';
import {useI18n} from "../hooks/useI18n";

const DeleteItemDialog = (props) => {
const {i18n} = useI18n();
if (!props.item) {
return null;
}
return <Modal show={props.show} onHide={props.onClose}>
<Modal.Header closeButton>
<Modal.Title>
{props.i18n('delete.dialog-title')}
{i18n('delete.dialog-title')}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<FormattedMessage id='delete.dialog-content'
values={{itemLabel: props.itemLabel}}/>
</Modal.Body>
<Modal.Footer>
<Button variant='warning' size='sm'
onClick={props.onSubmit}>{props.i18n('delete')}</Button>
<Button size='sm' onClick={props.onClose}>{props.i18n('cancel')}</Button>
<Button variant='warning' size='sm' className="action-button"
onClick={props.onSubmit}>{i18n('delete')}</Button>
<Button size='sm' className="action-button" onClick={props.onClose}>{i18n('cancel')}</Button>
</Modal.Footer>
</Modal>
};
Expand All @@ -37,4 +35,4 @@ DeleteItemDialog.propTypes = {
itemLabel: PropTypes.string
};

export default injectIntl(withI18n(DeleteItemDialog));
export default DeleteItemDialog;
6 changes: 3 additions & 3 deletions js/components/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default class Input extends React.Component {
}

_renderSelect() {
return <FormGroup size='sm' validationState={this.props.validation}>
return <FormGroup size='sm'>
{this._renderLabel()}
<FormControl as='select' ref={c => this.input = c} {...this.props}>
{this.props.children}
Expand All @@ -70,7 +70,7 @@ export default class Input extends React.Component {
}

_renderTextArea() {
return <FormGroup size='sm' validationState={this.props.validation}>
return <FormGroup size='sm'>
{this._renderLabel()}
<FormControl as='textarea' style={{height: 'auto'}} ref={c => this.input = c} {...this.props}/>
{this.props.validation && <FormControl.Feedback/>}
Expand All @@ -83,7 +83,7 @@ export default class Input extends React.Component {
}

_renderInput() {
return <FormGroup size='sm' validationState={this.props.validation}>
return <FormGroup size='sm'>
{this._renderLabel()}
<FormControl ref={c => this.input = c} as='input' {...this.props}/>
{this.props.validation && <FormControl.Feedback/>}
Expand Down
31 changes: 12 additions & 19 deletions js/components/history/HistoryList.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import withI18n from "../../i18n/withI18n";
import {Card} from "react-bootstrap";
import {loadActions} from "../../actions/HistoryActions";
import {bindActionCreators} from "redux";
import {ACTIONS_PER_PAGE} from "../../constants/DefaultConstants";
import HistoryTable from "./HistoryTable";
import Routes from "../../constants/RoutesConstants";
import {transitionToWithOpts} from "../../utils/Routing";
import HistoryPagination from "./HistoryPagination";
import Pagination, {INITIAL_PAGE} from "../misc/Pagination";
import PromiseTrackingMask from "../misc/PromiseTrackingMask";
import {trackPromise} from "react-promise-tracker";

Expand All @@ -19,12 +18,12 @@ class HistoryList extends React.Component {
this.i18n = this.props.i18n;
this.state = {
searchData: {},
pageNumber: 1
pageNumber: INITIAL_PAGE
}
}

componentDidMount() {
trackPromise(this.props.loadActions(1), "history");
trackPromise(this.props.loadActions(INITIAL_PAGE), "history");
}

_onOpen = (key) => {
Expand All @@ -45,23 +44,17 @@ class HistoryList extends React.Component {
}
};

_handleSearch = (newPageNumber = 1) => {
this.props.loadActions(newPageNumber, this.state.searchData);
_handleSearch = (newPageNumber = INITIAL_PAGE) => {
trackPromise(this.props.loadActions(newPageNumber, this.state.searchData), "history");
};

_handleReset = () => {
this.setState({searchData: {}, pageNumber: 1});
this.props.loadActions(1, {});
this.setState({searchData: {}, pageNumber: INITIAL_PAGE}, () => this._handleSearch());
};

_handlePagination = (direction) => {
if (this.props.actionsLoaded.actions.length <= ACTIONS_PER_PAGE && direction === 1 ||
this.state.pageNumber === 1 && direction === -1) {
return;
}
const newPageNumber = this.state.pageNumber + direction;
this.setState({pageNumber: newPageNumber});
this._handleSearch(newPageNumber);
_handlePagination = (pageNumber) => {
this.setState({pageNumber: pageNumber});
this._handleSearch(pageNumber);
};

render() {
Expand All @@ -83,9 +76,9 @@ class HistoryList extends React.Component {
<>
<HistoryTable handlers={handlers} searchData={this.state.searchData}
actions={actionsLoaded.actions} i18n={this.i18n}/>
<HistoryPagination pageNumber={this.state.pageNumber}
numberOfActions={actionsLoaded.actions.length}
handlePagination={this._handlePagination}/>
<Pagination pageNumber={this.state.pageNumber}
itemCount={actionsLoaded.actions.length}
handlePagination={this._handlePagination}/>
</>
}
</Card.Body>
Expand Down
32 changes: 0 additions & 32 deletions js/components/history/HistoryPagination.js

This file was deleted.

12 changes: 6 additions & 6 deletions js/components/history/HistoryRow.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import React from "react";
import {injectIntl} from "react-intl";
import withI18n from "../../i18n/withI18n";
import {Button} from "react-bootstrap";
import PropTypes from "prop-types";
import {formatDateWithMilliseconds} from "../../utils/Utils";
import {useI18n} from "../../hooks/useI18n";

let HistoryRow = (props) => {
const {i18n} = useI18n();
const action = props.action;
const username = action.author ? action.author.username : props.i18n('history.non-logged');
const username = action.author ? action.author.username : i18n('history.non-logged');
return <tr>
<td className='report-row'>{action.type}</td>
<td className='report-row'>{username}</td>
<td className='report-row'>{formatDateWithMilliseconds(action.timestamp)}</td>
<td className='report-row actions'>
<Button variant='primary' size='sm' title={props.i18n('history.open-tooltip')}
onClick={() => props.onOpen(action.key)}>{props.i18n('open')}</Button>
<Button variant='primary' size='sm' title={i18n('history.open-tooltip')}
onClick={() => props.onOpen(action.key)}>{i18n('open')}</Button>
</td>
</tr>;
};
Expand All @@ -24,5 +24,5 @@ HistoryRow.propTypes = {
onOpen: PropTypes.func.isRequired
};

export default injectIntl(withI18n(HistoryRow));
export default HistoryRow;

Loading
Loading