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

Add extension buttons (simplified variant) #53

Merged
merged 13 commits into from
Dec 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion js/components/HelpIcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import React from 'react';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import PropTypes from "prop-types";
import {FaQuestionCircle} from 'react-icons/fa';
import {FaEnvelope, FaQuestionCircle} from 'react-icons/fa';
import {FaCheck} from 'react-icons/fa';
import {FaTimes} from 'react-icons/fa';
import {FaTasks} from "react-icons/fa";

const HelpIcon = (props) => {
const tooltip = <Tooltip id='help-tooltip'>{props.text}</Tooltip>;
Expand All @@ -18,6 +19,10 @@ const HelpIcon = (props) => {
return <FaCheck className={'ok-icon ' + props.className}/>;
case "remove":
return <FaTimes className={'remove-icon ' + props.className}/>;
case "to-do":
return <FaTasks className={'to-do-icon ' + props.className}/>;
case "envelope":
return <FaEnvelope className={'publish-icon' + props.className}/>
default:
return null;
}
Expand Down
56 changes: 39 additions & 17 deletions js/components/record/Record.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import HorizontalInput from "../HorizontalInput";
import RecordForm from "./RecordForm";
import RecordProvenance from "./RecordProvenance";
import RequiredAttributes from "./RequiredAttributes";
import {ACTION_STATUS, ALERT_TYPES, ROLE} from "../../constants/DefaultConstants";
import {ACTION_STATUS, ALERT_TYPES, EXTENSION_CONSTANTS, RECORD_PHASE, ROLE} from "../../constants/DefaultConstants";
import AlertMessage from "../AlertMessage";
import {LoaderCard, LoaderSmall} from "../Loader";
import {processTypeaheadOptions} from "./TypeaheadAnswer";
import {EXTENSIONS} from "../../../config";

class Record extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -56,25 +57,25 @@ class Record extends React.Component {
}

return <div className={"record"}>
<form>
<RequiredAttributes record={record} onChange={this._onChange}
formTemplate={formTemplate}
currentUser={currentUser}
completed={record.state.isComplete()}/>
{this._showInstitution() && this._renderInstitution()}
<RecordProvenance record={record}/>
</form>
{this._renderForm()}
{this._renderButtons()}
{showAlert && recordSaved.status === ACTION_STATUS.ERROR &&
<form>
<RequiredAttributes record={record} onChange={this._onChange}
formTemplate={formTemplate}
currentUser={currentUser}
completed={record.state.isComplete()}/>
{this._showInstitution() && this._renderInstitution()}
<RecordProvenance record={record}/>
</form>
{this._renderForm()}
{this._renderButtons()}
{showAlert && recordSaved.status === ACTION_STATUS.ERROR &&
<div>
<AlertMessage type={ALERT_TYPES.DANGER}
message={this.props.formatMessage('record.save-error', {error: this.i18n(recordSaved.error.messageId)})}/>
<br/>
</div>}
{showAlert && recordSaved.status === ACTION_STATUS.SUCCESS &&
{showAlert && recordSaved.status === ACTION_STATUS.SUCCESS &&
<div><AlertMessage type={ALERT_TYPES.SUCCESS} message={this.i18n('record.save-success')}/><br/></div>}
</div>;
</div>;
}

_renderHeader() {
Expand Down Expand Up @@ -105,13 +106,34 @@ class Record extends React.Component {
const {record, recordSaved, formgen} = this.props;

return <div className="mt-3 text-center">
<Button variant='success' size='sm'
{record.phase === RECORD_PHASE.COMPLETED
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LaChope why are we using different mechanism to hide buttons as couple lines bellow where we use hidden attribute? I.e.:

<Button className="mx-1" variant='success' size='sm'
                    disabled={formgen.status === ACTION_STATUS.PENDING || recordSaved.status === ACTION_STATUS.PENDING
                        || !this.state.isFormValid || !record.state.isComplete()}
                    hidden={(record.phase === RECORD_PHASE.COMPLETED && !this._isAdmin()) ||
                        record.phase === RECORD_PHASE.PUBLISHED}
                    onClick={this.props.handlers.onSave}>
                {this.i18n('save')}{recordSaved.status === ACTION_STATUS.PENDING && <LoaderSmall/>}
            </Button>

Copy link
Collaborator

@LaChope LaChope Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blcham for consistency, the mechanism should be the same everywhere, I just forgot to change it in all places. It should be like in line 109{record.phase === RECORD_PHASE.COMPLETED for testing reasons. When using the hidden prop, the buttons are still present in the DOM, and thus breaking the tests.

|| EXTENSIONS === EXTENSION_CONSTANTS.OPERATOR
&& <Button className="mx-1" variant='danger' size='sm'
disabled={formgen.status === ACTION_STATUS.PENDING || recordSaved.status === ACTION_STATUS.PENDING
|| !this.state.isFormValid || !record.state.isComplete() || record.phase === RECORD_PHASE.REJECTED}
onClick={this.props.handlers.onReject}>
{this.i18n('reject')}{recordSaved.status === ACTION_STATUS.PENDING && <LoaderSmall/>}
</Button>}

{record.phase === RECORD_PHASE.COMPLETED
|| record.phase === RECORD_PHASE.PUBLISHED
&& <Button className="mx-1" variant='success' size='sm'
disabled={formgen.status === ACTION_STATUS.PENDING || recordSaved.status === ACTION_STATUS.PENDING
|| !this.state.isFormValid || !record.state.isComplete() || record.phase === RECORD_PHASE.COMPLETED}
onClick={this.props.handlers.onComplete}>
{this.i18n('complete')}{recordSaved.status === ACTION_STATUS.PENDING && <LoaderSmall/>}
</Button>}

<Button className="mx-1" variant='success' size='sm'
disabled={formgen.status === ACTION_STATUS.PENDING || recordSaved.status === ACTION_STATUS.PENDING
|| !this.state.isFormValid || !record.state.isComplete()}
|| !this.state.isFormValid || !record.state.isComplete()}
hidden={(record.phase === RECORD_PHASE.COMPLETED && !this._isAdmin()) ||
record.phase === RECORD_PHASE.PUBLISHED}
onClick={this.props.handlers.onSave}>
{this.i18n('save')}{recordSaved.status === ACTION_STATUS.PENDING && <LoaderSmall/>}
</Button>
<Button variant='link' size='sm'
<Button className="mx-1" variant='link' size='sm'
hidden={record.phase === RECORD_PHASE.COMPLETED || record.phase === RECORD_PHASE.PUBLISHED}
onClick={this.props.handlers.onCancel}>{this.i18n('cancel')}</Button>
</div>
}
Expand Down
27 changes: 25 additions & 2 deletions js/components/record/RecordController.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import withI18n from '../../i18n/withI18n';
import Record from './Record';
import Routes from "../../constants/RoutesConstants";
import {transitionToWithOpts} from '../../utils/Routing';
import {ACTION_FLAG, ACTION_STATUS} from "../../constants/DefaultConstants";
import {ACTION_FLAG, ACTION_STATUS, RECORD_PHASE} from "../../constants/DefaultConstants";
import {connect} from "react-redux";
import {bindActionCreators} from "redux";
import {
Expand Down Expand Up @@ -126,6 +126,27 @@ class RecordController extends React.Component {
this.setState({record: update});
};

_onComplete = () => {
this._handlePhaseChange(RECORD_PHASE.COMPLETED);
};

_onReject = () => {
this._handlePhaseChange(RECORD_PHASE.REJECTED);
};

_handlePhaseChange = (newPhase) => {
const currentUser = this.props.currentUser;

this.setState((prevState) => {
const update = {...prevState.record};
update.phase = newPhase;
return {record: update};
}, () => {
const updatedRecord = this.state.record;
this.props.updateRecord(updatedRecord, currentUser);
});
};

_getLocalName() {
if (EXTENSIONS.split(",").includes("kodi")) { // return name of the record based on answer of specific question
return this._getKodiLocaLName();
Expand Down Expand Up @@ -158,7 +179,9 @@ class RecordController extends React.Component {
const handlers = {
onSave: this._onSave,
onCancel: this._onCancel,
onChange: this._onChange
onChange: this._onChange,
onComplete: this._onComplete,
onReject: this._onReject
};

return <Record
Expand Down
48 changes: 36 additions & 12 deletions js/components/record/RecordRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,50 @@ import HelpIcon from "../HelpIcon";
import {Button} from "react-bootstrap";
import {injectIntl} from "react-intl";
import withI18n from "../../i18n/withI18n";
import RecordValidator from "../../validation/RecordValidator";
import {LoaderSmall} from "../Loader";
import PropTypes from "prop-types";
import {ROLE} from "../../constants/DefaultConstants";
import {RECORD_PHASE, ROLE} from "../../constants/DefaultConstants";

let RecordRow = (props) => {
const record = props.record,
formTemplateOptions = props.formTemplateOptions,
isComplete = RecordValidator.isComplete(record),
completionTooltip = props.i18n(isComplete ? 'records.completion-status-tooltip.complete' : 'records.completion-status-tooltip.incomplete'),
recordPhase = props.record.phase,
isAdmin = props.currentUser.role === ROLE.ADMIN,
deleteButton = props.disableDelete ? null :
<Button variant='warning' size='sm' title={props.i18n('records.delete-tooltip')}
onClick={() => props.onDelete(record)}>{props.i18n('delete')}{props.deletionLoading &&
<LoaderSmall/>}</Button>;

const getGlyph = () => {
switch (recordPhase) {
case RECORD_PHASE.OPEN:
return 'to-do';
case RECORD_PHASE.COMPLETED:
return 'ok';
case RECORD_PHASE.PUBLISHED:
return 'envelope';
case RECORD_PHASE.REJECTED:
return 'remove';
default:
return '';
}
};

const getCompletionStatusTooltip = () => {
switch (recordPhase) {
case RECORD_PHASE.COMPLETED:
return props.i18n('records.completion-status-tooltip.complete');
case RECORD_PHASE.OPEN:
return props.i18n('records.completion-status-tooltip.incomplete');
case RECORD_PHASE.REJECTED:
return props.i18n('records.completion-status-tooltip.rejected');
case RECORD_PHASE.PUBLISHED:
return props.i18n('records.completion-status-tooltip.published');
default:
return "";
}
};

return <tr>
{isAdmin &&
<td className='report-row'>
Expand All @@ -42,11 +70,10 @@ let RecordRow = (props) => {
<td className='report-row content-center'>
{formatDate(new Date(record.lastModified ? record.lastModified : record.dateCreated))}
</td>
{ isAdmin &&
<td className='report-row content-center'>
<HelpIcon text={completionTooltip} glyph={isComplete ? 'ok' : 'remove'}/>
</td>
}
<td className='report-row content-center'>
<HelpIcon text={getCompletionStatusTooltip()} glyph={getGlyph()}/>
</td>

<td className='report-row actions'>
<Button variant='primary' size='sm' title={props.i18n('records.open-tooltip')}
onClick={() => props.onEdit(record)}>{props.i18n('open')}</Button>
Expand All @@ -55,9 +82,6 @@ let RecordRow = (props) => {
</tr>
};

const isAdvancedView = {}


const getFormTemplateOptionName = (formTemplate, formTemplatesOptions) => {
if (!formTemplate) {
return "";
Expand Down
4 changes: 1 addition & 3 deletions js/components/record/RecordTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ class RecordTable extends React.Component {
&& <th className='w-25 content-center'>{this.i18n('records.form-template')}</th>
}
<th className='w-25 content-center'>{this.i18n('records.last-modified')}</th>
{(this._isAdmin())
&& <th className='w-15 content-center'>{this.i18n('records.completion-status')}</th>
}
<th className='w-15 content-center'>{this.i18n('records.completion-status')}</th>
<th className='w-20 content-center'>{this.i18n('actions')}</th>
</tr>
</thead>
Expand Down
16 changes: 14 additions & 2 deletions js/components/record/Records.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
'use strict';

import React from "react";
import {Button, Card} from "react-bootstrap";
import {injectIntl} from "react-intl";
import withI18n from "../../i18n/withI18n";
import RecordTable from "./RecordTable";
import {ACTION_STATUS, ALERT_TYPES, ROLE} from "../../constants/DefaultConstants";
import {ACTION_STATUS, ALERT_TYPES, EXTENSION_CONSTANTS, ROLE} from "../../constants/DefaultConstants";
import AlertMessage from "../AlertMessage";
import {LoaderSmall} from "../Loader";
import PropTypes from "prop-types";
import {processTypeaheadOptions} from "./TypeaheadAnswer";
import {EXTENSIONS} from "../../../config";
import ExportRecordsDropdown from "./ExportRecordsDropdown";

const STUDY_CLOSED_FOR_ADDITION = false;
Expand Down Expand Up @@ -35,6 +38,9 @@ class Records extends React.Component {
const showCreateButton = STUDY_CREATE_AT_MOST_ONE_RECORD
? (!recordsLoaded.records || (recordsLoaded.records.length < 1))
: true;
const showPublishButton =
this.props.currentUser.role === ROLE.ADMIN
&& EXTENSIONS === EXTENSION_CONSTANTS.OPERATOR;
const createRecordDisabled =
STUDY_CLOSED_FOR_ADDITION
&& (!this._isAdmin());
Expand All @@ -54,11 +60,17 @@ class Records extends React.Component {
<RecordTable {...this.props}/>
<div className="d-flex justify-content-between">
{showCreateButton
? <Button variant='primary' size='sm'
? <Button className="mx-1" variant='primary' size='sm'
disabled={createRecordDisabled}
title={createRecordTooltip}
onClick={onCreateWithFormTemplate}>{this.i18n('records.create-tile')}</Button>
: null}
{showPublishButton ?
<Button className="mx-1" variant='success' size='sm'
onClick={this.props.handlers.onPublish}>
{this.i18n('publish')}
</Button>
: null}
<ExportRecordsDropdown onExport={this.props.handlers.onExport} records={recordsLoaded.records}/>
</div>
{showAlert && recordDeleted.status === ACTION_STATUS.ERROR &&
Expand Down
26 changes: 24 additions & 2 deletions js/components/record/RecordsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {injectIntl} from "react-intl";
import withI18n from "../../i18n/withI18n";
import {connect} from "react-redux";
import {bindActionCreators} from "redux";
import {deleteRecord} from "../../actions/RecordActions";
import {deleteRecord, updateRecord} from "../../actions/RecordActions";
import {loadFormTemplates} from "../../actions/FormTemplatesActions";
import {extractQueryParam} from "../../utils/Utils"
import {RECORD_PHASE} from "../../constants/DefaultConstants";

class RecordsController extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -53,6 +54,25 @@ class RecordsController extends React.Component {
this.setState({showAlert: true});
};

_onPublishRecords = async () => {
const currentUser = this.props.currentUser;

this.setState({
records: this.props.recordsLoaded.records
}, async () => {
const updatedRecords = this.state.records.map(async (record) => {
if (record.phase === RECORD_PHASE.COMPLETED) {
const updatedRecord = {...record, phase: RECORD_PHASE.PUBLISHED};
await this.props.updateRecord(updatedRecord, currentUser);
return updatedRecord;
}
});

return await Promise.all(updatedRecords);
})

};

_onExportRecords = (exportType) => {
this.props.exportRecords(exportType);
};
Expand All @@ -67,6 +87,7 @@ class RecordsController extends React.Component {
onEdit: this._onEditRecord,
onCreate: this._onAddRecord,
onDelete: this._onDeleteRecord,
onPublish: this._onPublishRecords,
onExport: this._onExportRecords
};
return <Records recordsLoaded={recordsLoaded} showAlert={this.state.showAlert} handlers={handlers}
Expand All @@ -91,9 +112,10 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
deleteRecord: bindActionCreators(deleteRecord, dispatch),
updateRecord: bindActionCreators(updateRecord, dispatch),
loadRecords: bindActionCreators(loadRecords, dispatch),
exportRecords: bindActionCreators(exportRecords, dispatch),
loadFormTemplates: bindActionCreators(loadFormTemplates, dispatch),
transitionToWithOpts: bindActionCreators(transitionToWithOpts, dispatch),
transitionToWithOpts: bindActionCreators(transitionToWithOpts, dispatch)
}
}
13 changes: 13 additions & 0 deletions js/constants/DefaultConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,16 @@ export const MediaType = {
EXCEL: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
JSON: "application/json"
}

export const EXTENSION_CONSTANTS = {
SUPPLIER: "supplier",
OPERATOR: "operator"
}

export const RECORD_PHASE = {
OPEN: 'open',
VALID: 'valid',
COMPLETED: 'completed',
PUBLISHED: 'published',
REJECTED: 'rejected'
}
7 changes: 6 additions & 1 deletion js/i18n/cs.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export default {
'please-wait': 'Prosím, čekejte...',
'actions': 'Akce',
'required': 'Políčka označená * jsou povinná',
'reject': 'Odmítnout',
'complete': 'Dokončit',
'publish': 'Publikovat',

'login.title': Constants.APP_NAME + ' - Přihlášení',
'login.username': 'Uživatelské jméno',
Expand Down Expand Up @@ -142,9 +145,11 @@ export default {
'records.id': 'Id',
'records.form-template': 'Šablona',
'records.local-name': 'Název',
'records.completion-status': 'Stav vyplnění',
'records.completion-status': 'Stav',
'records.completion-status-tooltip.complete': 'Všechny povinné informace ze záznamu byly vyplněny.',
'records.completion-status-tooltip.incomplete': 'Některé povinné informace ze záznamu ještě nebyly vyplněny.',
'records.completion-status-tooltip.rejected': 'Formulář byl odmítnut',
'records.completion-status-tooltip.published': 'Formulář byl zveřejněn',
'records.last-modified': 'Naposledy upraveno',
'records.open-tooltip': 'Zobrazit či upravit tento záznam',
'records.delete-tooltip': 'Smazat tento záznam',
Expand Down
Loading
Loading