Skip to content

Commit

Permalink
Merge pull request #52 from akaene/main
Browse files Browse the repository at this point in the history
37 Export records to JSON
  • Loading branch information
blcham authored Dec 15, 2023
2 parents 7ffa550 + c1769f6 commit 72c674e
Show file tree
Hide file tree
Showing 17 changed files with 179 additions and 55 deletions.
14 changes: 14 additions & 0 deletions js/actions/AsyncActionUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function asyncRequest(type) {
return {type};
}

export function asyncError(type, error) {
return {
type,
error
};
}

export function asyncSuccess(type) {
return {type};
}
34 changes: 27 additions & 7 deletions js/actions/RecordsActions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as ActionConstants from "../constants/ActionConstants";
import {ROLE} from "../constants/DefaultConstants";
import {HttpHeaders, ROLE} from "../constants/DefaultConstants";
import {axiosBackend} from "./index";
import {API_URL} from '../../config';
import {asyncError, asyncRequest, asyncSuccess} from "./AsyncActionUtils";
import {fileDownload} from "../utils/Utils";

export function loadRecords(currentUser, institutionKey = null) {
//console.log("Loading records");
Expand All @@ -22,9 +24,7 @@ export function loadRecords(currentUser, institutionKey = null) {
}

export function loadRecordsPending() {
return {
type: ActionConstants.LOAD_RECORDS_PENDING
}
return asyncRequest(ActionConstants.LOAD_RECORDS_PENDING);
}

export function loadRecordsSuccess(records) {
Expand All @@ -35,8 +35,28 @@ export function loadRecordsSuccess(records) {
}

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

export function exportRecords(exportType, institutionKey) {
return (dispatch) => {
dispatch(asyncRequest(ActionConstants.EXPORT_RECORDS_PENDING));
const urlSuffix = institutionKey ? `?institution=${institutionKey}` : '';
return axiosBackend.get(`${API_URL}/rest/records/export${urlSuffix}`, {
headers: {
accept: exportType.mediaType
},
responseType: 'arraybuffer'
}).then((resp) => {
const disposition = resp.headers[HttpHeaders.CONTENT_DISPOSITION];
const filenameMatch = disposition
? disposition.match(/filename="(.+\..+)"/)
: null;
const fileName = filenameMatch ? filenameMatch[1] : "records" + exportType.fileExtension;
fileDownload(resp.data, fileName, exportType.mediaType);
return dispatch(asyncSuccess(ActionConstants.EXPORT_RECORDS_SUCCESS));
}).catch((error) => {
dispatch(asyncError(ActionConstants.EXPORT_RECORDS_ERROR, error.response.data));
});
}
}
25 changes: 17 additions & 8 deletions js/components/institution/Institution.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,15 @@ class Institution extends React.Component {
};

render() {
const {showAlert, currentUser, institution, recordsLoaded, institutionLoaded, institutionSaved, formTemplatesLoaded} = this.props;
const {
showAlert,
currentUser,
institution,
recordsLoaded,
institutionLoaded,
institutionSaved,
formTemplatesLoaded
} = this.props;

if (institutionLoaded.status === ACTION_STATUS.ERROR) {
return <AlertMessage type={ALERT_TYPES.DANGER}
Expand Down Expand Up @@ -75,16 +83,17 @@ class Institution extends React.Component {
{this._renderAddedDate()}
{this._renderButtons()}
{showAlert && institutionSaved.status === ACTION_STATUS.ERROR &&
<AlertMessage type={ALERT_TYPES.DANGER}
message={this.props.formatMessage('institution.save-error', {error: institutionSaved.error.message})}/>}
<AlertMessage type={ALERT_TYPES.DANGER}
message={this.props.formatMessage('institution.save-error', {error: institutionSaved.error.message})}/>}
{showAlert && institutionSaved.status === ACTION_STATUS.SUCCESS &&
<AlertMessage type={ALERT_TYPES.SUCCESS} message={this.i18n('institution.save-success')}/>}
<AlertMessage type={ALERT_TYPES.SUCCESS} message={this.i18n('institution.save-success')}/>}
</form>
{!institution.isNew && this._renderMembers()}
{!institution.isNew &&
<InstitutionPatients
recordsLoaded={recordsLoaded} formTemplatesLoaded={formTemplatesLoaded}
onEdit={this.props.handlers.onEditPatient} currentUser={currentUser}/>}
<InstitutionPatients
recordsLoaded={recordsLoaded} formTemplatesLoaded={formTemplatesLoaded}
onEdit={this.props.handlers.onEditPatient} onExport={this.props.handlers.onExportRecords}
currentUser={currentUser}/>}
</Card.Body>
</Card>;
}
Expand Down Expand Up @@ -117,7 +126,7 @@ class Institution extends React.Component {
disabled={!InstitutionValidator.isValid(this.props.institution) || this.props.institutionSaved.status === ACTION_STATUS.PENDING}
onClick={handlers.onSave} className="d-inline-flex">{this.i18n('save')}
{!InstitutionValidator.isValid(this.props.institution) &&
<HelpIcon className="align-self-center" text={this.i18n('required')} glyph="help"/>}
<HelpIcon className="align-self-center" text={this.i18n('required')} glyph="help"/>}
{institutionSaved.status === ACTION_STATUS.PENDING && <LoaderSmall/>}</Button>
<Button variant='link' size='sm' onClick={handlers.onCancel}>{this.i18n('cancel')}</Button>
</div>;
Expand Down
11 changes: 9 additions & 2 deletions js/components/institution/InstitutionController.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
updateInstitution
} from "../../actions/InstitutionActions";
import * as EntityFactory from "../../utils/EntityFactory";
import {loadRecords} from "../../actions/RecordsActions";
import {exportRecords, loadRecords} from "../../actions/RecordsActions";
import omit from 'lodash/omit';
import {loadFormTemplates} from "../../actions/FormTemplatesActions";

Expand Down Expand Up @@ -125,6 +125,11 @@ class InstitutionController extends React.Component {
this.props.transitionToWithOpts(Routes.editRecord, {params: {key: patient.key}});
};

_onExportRecords = (exportType) => {
const institutionKey = this.state.institution.key;
this.props.exportRecords(exportType, institutionKey);
};

_onAddNewUser = (institution) => {
this.props.transitionToWithOpts(Routes.createUser, {
payload: {institution: institution}
Expand All @@ -146,6 +151,7 @@ class InstitutionController extends React.Component {
onEditUser: this._onEditUser,
onAddNewUser: this._onAddNewUser,
onEditPatient: this._onEditPatient,
onExportRecords: this._onExportRecords,
onDelete: this._onDeleteUser
};
return <Institution handlers={handlers} institution={this.state.institution}
Expand Down Expand Up @@ -186,6 +192,7 @@ function mapDispatchToProps(dispatch) {
loadFormTemplates: bindActionCreators(loadFormTemplates, dispatch),
deleteUser: bindActionCreators(deleteUser, dispatch),
transitionToWithOpts: bindActionCreators(transitionToWithOpts, dispatch),
unloadInstitutionMembers: bindActionCreators(unloadInstitutionMembers, dispatch)
unloadInstitutionMembers: bindActionCreators(unloadInstitutionMembers, dispatch),
exportRecords: bindActionCreators(exportRecords, dispatch)
}
}
19 changes: 9 additions & 10 deletions js/components/institution/InstitutionPatients.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
'use strict';

import React from 'react';
import {Button, Card} from 'react-bootstrap';

import withI18n from '../../i18n/withI18n';
import {injectIntl} from "react-intl";
import {Card} from 'react-bootstrap';
import RecordTable from '../record/RecordTable';
import PropTypes from "prop-types";
import ExportRecordsDropdown from "../record/ExportRecordsDropdown";
import {useI18n} from "../../hooks/useI18n";

const InstitutionPatients = (props) => {
const {recordsLoaded, formTemplatesLoaded, onEdit, currentUser} = props;
const {recordsLoaded, formTemplatesLoaded, onEdit, onExport, currentUser} = props;
const {i18n} = useI18n();

return <Card variant='info' className="mt-3">
<Card.Header className="text-light bg-primary"
as="h6">{props.i18n('institution.patients.panel-title')}</Card.Header>
as="h6">{i18n('institution.patients.panel-title')}</Card.Header>
<Card.Body>
<RecordTable recordsLoaded={recordsLoaded}
formTemplatesLoaded={formTemplatesLoaded}
handlers={{onEdit: onEdit}}
disableDelete={true}
currentUser={currentUser}/>
<div className="d-flex justify-content-end">
<Button className="mx-1" variant='primary' size='sm'>{props.i18n('export')}</Button>
<ExportRecordsDropdown onExport={onExport} records={recordsLoaded.records}/>
</div>
</Card.Body>
</Card>;
Expand All @@ -31,7 +29,8 @@ InstitutionPatients.propTypes = {
recordsLoaded: PropTypes.object.isRequired,
formTemplatesLoaded: PropTypes.object.isRequired,
onEdit: PropTypes.func.isRequired,
onExport: PropTypes.func.isRequired,
currentUser: PropTypes.object.isRequired
};

export default injectIntl(withI18n(InstitutionPatients));
export default InstitutionPatients;
24 changes: 24 additions & 0 deletions js/components/record/ExportRecordsDropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";
import PropTypes from "prop-types";
import {sanitizeArray} from "../../utils/Utils";
import {Dropdown, DropdownButton} from "react-bootstrap";
import {ExportType} from "../../constants/ExportType";
import {useI18n} from "../../hooks/useI18n";

const ExportRecordsDropdown = ({records, onExport}) => {
const {i18n} = useI18n();
if (sanitizeArray(records).length === 0) {
return null;
}
return <DropdownButton id="records-export" title={i18n("records.export")} size="sm" variant="primary">
<Dropdown.Item onClick={() => onExport(ExportType.EXCEL)}>{i18n("records.export.excel")}</Dropdown.Item>
<Dropdown.Item onClick={() => onExport(ExportType.JSON)}>{i18n("records.export.json")}</Dropdown.Item>
</DropdownButton>;
}

ExportRecordsDropdown.propTypes = {
records: PropTypes.array,
onExport: PropTypes.func.isRequired
};

export default ExportRecordsDropdown;
24 changes: 10 additions & 14 deletions js/components/record/Records.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use strict';

import React from "react";
import {Button, Card} from "react-bootstrap";
import {injectIntl} from "react-intl";
Expand All @@ -10,6 +8,7 @@ import AlertMessage from "../AlertMessage";
import {LoaderSmall} from "../Loader";
import PropTypes from "prop-types";
import {processTypeaheadOptions} from "./TypeaheadAnswer";
import ExportRecordsDropdown from "./ExportRecordsDropdown";

const STUDY_CLOSED_FOR_ADDITION = false;
const STUDY_CREATE_AT_MOST_ONE_RECORD = false;
Expand All @@ -34,9 +33,8 @@ class Records extends React.Component {
render() {
const {showAlert, recordDeleted, formTemplate, recordsLoaded} = this.props;
const showCreateButton = STUDY_CREATE_AT_MOST_ONE_RECORD
? (!this.props.recordsLoaded.records || (this.props.recordsLoaded.records.length < 1))
: true;
const showExportButton = !!recordsLoaded.records;
? (!recordsLoaded.records || (recordsLoaded.records.length < 1))
: true;
const createRecordDisabled =
STUDY_CLOSED_FOR_ADDITION
&& (!this._isAdmin());
Expand All @@ -49,8 +47,8 @@ class Records extends React.Component {
return <Card variant='primary'>
<Card.Header className="text-light bg-primary" as="h6">
{this._getPanelTitle()}
{this.props.recordsLoaded.records && this.props.recordsLoaded.status === ACTION_STATUS.PENDING &&
<LoaderSmall/>}
{recordsLoaded.records && recordsLoaded.status === ACTION_STATUS.PENDING &&
<LoaderSmall/>}
</Card.Header>
<Card.Body>
<RecordTable {...this.props}/>
Expand All @@ -61,15 +59,13 @@ class Records extends React.Component {
title={createRecordTooltip}
onClick={onCreateWithFormTemplate}>{this.i18n('records.create-tile')}</Button>
: null}
{showExportButton ?
<Button className="mx-1" variant='primary' size='sm'>{this.i18n('export')}</Button>
: null}
<ExportRecordsDropdown onExport={this.props.handlers.onExport} records={recordsLoaded.records}/>
</div>
{showAlert && recordDeleted.status === ACTION_STATUS.ERROR &&
<AlertMessage type={ALERT_TYPES.DANGER}
message={this.props.formatMessage('record.delete-error', {error: this.i18n(this.props.recordDeleted.error.message)})}/>}
<AlertMessage type={ALERT_TYPES.DANGER}
message={this.props.formatMessage('record.delete-error', {error: this.i18n(this.props.recordDeleted.error.message)})}/>}
{showAlert && recordDeleted.status === ACTION_STATUS.SUCCESS &&
<AlertMessage type={ALERT_TYPES.SUCCESS} message={this.i18n('record.delete-success')}/>}
<AlertMessage type={ALERT_TYPES.SUCCESS} message={this.i18n('record.delete-success')}/>}
</Card.Body>
</Card>;
}
Expand All @@ -89,7 +85,7 @@ class Records extends React.Component {
if (!this._isAdmin() && this.props.formTemplate) {
const formTemplateName = this._getFormTemplateName();
if (formTemplateName) {
return formTemplateName;
return formTemplateName;
}
}
return this.i18n('records.panel-title');
Expand Down
12 changes: 9 additions & 3 deletions js/components/record/RecordsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import Records from "./Records";
import Routes from "../../constants/RoutesConstants";
import {transitionToWithOpts} from "../../utils/Routing";
import {loadRecords} from "../../actions/RecordsActions";
import {exportRecords, loadRecords} from "../../actions/RecordsActions";
import {injectIntl} from "react-intl";
import withI18n from "../../i18n/withI18n";
import {connect} from "react-redux";
Expand Down Expand Up @@ -53,6 +53,10 @@ class RecordsController extends React.Component {
this.setState({showAlert: true});
};

_onExportRecords = (exportType) => {
this.props.exportRecords(exportType);
};

render() {
const {formTemplatesLoaded, recordsLoaded, recordDeleted, recordsDeleting, currentUser} = this.props;
const formTemplate = extractQueryParam(this.props.location.search, "formTemplate");
Expand All @@ -62,7 +66,8 @@ class RecordsController extends React.Component {
const handlers = {
onEdit: this._onEditRecord,
onCreate: this._onAddRecord,
onDelete: this._onDeleteRecord
onDelete: this._onDeleteRecord,
onExport: this._onExportRecords
};
return <Records recordsLoaded={recordsLoaded} showAlert={this.state.showAlert} handlers={handlers}
recordDeleted={recordDeleted} recordsDeleting={recordsDeleting} currentUser={currentUser}
Expand All @@ -87,7 +92,8 @@ function mapDispatchToProps(dispatch) {
return {
deleteRecord: bindActionCreators(deleteRecord, dispatch),
loadRecords: bindActionCreators(loadRecords, dispatch),
exportRecords: bindActionCreators(exportRecords, dispatch),
loadFormTemplates: bindActionCreators(loadFormTemplates, dispatch),
transitionToWithOpts: bindActionCreators(transitionToWithOpts, dispatch)
transitionToWithOpts: bindActionCreators(transitionToWithOpts, dispatch),
}
}
3 changes: 3 additions & 0 deletions js/constants/ActionConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export const DELETE_RECORD_ERROR = "DELETE_RECORD_ERROR";
export const LOAD_RECORDS_PENDING = "LOAD_RECORDS_PENDING";
export const LOAD_RECORDS_SUCCESS = "LOAD_RECORDS_SUCCESS";
export const LOAD_RECORDS_ERROR = "LOAD_RECORDS_ERROR";
export const EXPORT_RECORDS_PENDING = "EXPORT_RECORDS_PENDING";
export const EXPORT_RECORDS_SUCCESS = "EXPORT_RECORDS_SUCCESS";
export const EXPORT_RECORDS_ERROR = "EXPORT_RECORDS_ERROR";

export const LOAD_FORM_TEMPLATES_PENDING = "LOAD_FORM_TEMPLATES_PENDING";
export const LOAD_FORM_TEMPLATES_SUCCESS = "LOAD_FORM_TEMPLATES_SUCCESS";
Expand Down
7 changes: 5 additions & 2 deletions js/constants/DefaultConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,12 @@ export const STATISTICS_TYPE = {
export const SCRIPT_ERROR = 'SCRIPT_ERROR';

export const HttpHeaders = {
AUTHORIZATION: "Authorization"
AUTHORIZATION: "Authorization",
CONTENT_DISPOSITION: "content-disposition"
}

export const MediaType = {
FORM_URLENCODED: "application/x-www-form-urlencoded"
FORM_URLENCODED: "application/x-www-form-urlencoded",
EXCEL: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
JSON: "application/json"
}
12 changes: 12 additions & 0 deletions js/constants/ExportType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {MediaType} from "./DefaultConstants";

export const ExportType = {
EXCEL: {
mediaType: MediaType.EXCEL,
fileExtension: ".xslx"
},
JSON: {
mediaType: MediaType.JSON,
fileExtension: ".json"
}
};
4 changes: 3 additions & 1 deletion js/i18n/cs.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export default {
'please-wait': 'Prosím, čekejte...',
'actions': 'Akce',
'required': 'Políčka označená * jsou povinná',
'export': 'Export',

'login.title': Constants.APP_NAME + ' - Přihlášení',
'login.username': 'Uživatelské jméno',
Expand Down Expand Up @@ -154,6 +153,9 @@ export default {
'records.create-tile': 'Vytvořit',
'records.opened-study.create-tooltip': 'Vytvořit nový záznam',
'records.closed-study.create-tooltip': 'Studie je uzavřená na přidávání pacientů. Prosím, kontaktujte studijního koordinátora v připadě, že potřebujete přidat nový záznam pacienta.',
'records.export': 'Export',
'records.export.excel': 'Export do MS Excel',
'records.export.json': 'Export do JSON',

'record.panel-title': 'Záznam {identifier}',
'record.form-title': 'Detail',
Expand Down
Loading

0 comments on commit 72c674e

Please sign in to comment.