Skip to content

Commit 10a5a2b

Browse files
authored
Merge pull request #293 from kbss-cvut/290-configurable-columns
Configurable columns
2 parents cdcf01d + 414eced commit 10a5a2b

File tree

13 files changed

+570
-281
lines changed

13 files changed

+570
-281
lines changed

src/components/institution/InstitutionPatients.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import RecordTable from "../record/RecordTable";
44
import PropTypes from "prop-types";
55
import ExportRecordsDropdown from "../record/ExportRecordsDropdown";
66
import { useI18n } from "../../hooks/useI18n";
7+
import { COLUMNS } from "../../constants/DefaultConstants.js";
78

89
const InstitutionPatients = (props) => {
910
const { recordsLoaded, formTemplatesLoaded, onEdit, onExport, currentUser, filterAndSort } = props;
@@ -16,6 +17,7 @@ const InstitutionPatients = (props) => {
1617
</Card.Header>
1718
<Card.Body>
1819
<RecordTable
20+
visibleColumns={Object.values(COLUMNS)}
1921
recordsLoaded={recordsLoaded}
2022
formTemplatesLoaded={formTemplatesLoaded}
2123
handlers={{ onEdit: onEdit }}

src/components/record/RecordRow.jsx

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { formatDate } from "../../utils/Utils";
33
import HelpIcon from "../HelpIcon";
44
import { Button } from "react-bootstrap";
55
import PropTypes from "prop-types";
6-
import { RECORD_PHASE, ROLE } from "../../constants/DefaultConstants";
6+
import { COLUMNS, RECORD_PHASE, ROLE } from "../../constants/DefaultConstants";
77
import { useI18n } from "../../hooks/useI18n";
8-
import { IfGranted } from "react-authorization";
98
import PromiseTrackingMask from "../misc/PromiseTrackingMask";
9+
import { useHistory } from "react-router-dom";
1010

1111
const StatusInfo = {};
1212
StatusInfo[RECORD_PHASE.OPEN] = {
@@ -28,6 +28,8 @@ StatusInfo[RECORD_PHASE.REJECTED] = {
2828

2929
const RecordRow = (props) => {
3030
const { i18n } = useI18n();
31+
const history = useHistory();
32+
3133
const record = props.record,
3234
formTemplateOptions = props.formTemplateOptions,
3335
deleteButton = props.disableDelete ? null : (
@@ -47,30 +49,55 @@ const RecordRow = (props) => {
4749

4850
return (
4951
<tr className="position-relative">
50-
<IfGranted expected={ROLE.READ_ALL_RECORDS} actual={props.currentUser.roles}>
52+
{props.visibleColumns.includes(COLUMNS.ID) && (
5153
<td className="report-row">
5254
<Button variant="link" size="sm" onClick={() => props.onEdit(record)}>
5355
{record.key}
5456
</Button>
5557
</td>
56-
</IfGranted>
57-
<td className="report-row">
58-
<Button variant="link" size="sm" onClick={() => props.onEdit(record)}>
59-
{record.localName}
60-
</Button>
61-
</td>
62-
<IfGranted expected={ROLE.READ_ALL_RECORDS} actual={props.currentUser.roles}>
58+
)}
59+
60+
{props.visibleColumns.includes(COLUMNS.NAME) && (
61+
<td className="report-row">
62+
<Button variant="link" size="sm" onClick={() => props.onEdit(record)}>
63+
{record.localName}
64+
</Button>
65+
</td>
66+
)}
67+
68+
{props.visibleColumns.includes(COLUMNS.AUTHOR) && (
69+
<td className="report-row content-center">
70+
{record.author.firstName && record.author.lastName ? (
71+
<Button variant="link" size="sm" onClick={() => history.push(`/users/${record.author.username}`)}>
72+
{`${record.author.firstName} ${record.author.lastName}`}
73+
</Button>
74+
) : (
75+
<span className="text-warning">Not Found</span>
76+
)}
77+
</td>
78+
)}
79+
80+
{props.visibleColumns.includes(COLUMNS.INSTITUTION) && (
6381
<td className="report-row content-center">{record.institution.name}</td>
82+
)}
83+
84+
{props.visibleColumns.includes(COLUMNS.TEMPLATE) && (
6485
<td className="report-row content-center">
6586
{getFormTemplateOptionName(record.formTemplate, formTemplateOptions)}
6687
</td>
67-
</IfGranted>
68-
<td className="report-row content-center">
69-
{formatDate(new Date(record.lastModified ? record.lastModified : record.dateCreated))}
70-
</td>
71-
<td className="report-row content-center">
72-
{statusInfo ? <HelpIcon text={statusInfoText()} glyph={statusInfo.glyph} /> : "N/A"}
73-
</td>
88+
)}
89+
90+
{props.visibleColumns.includes(COLUMNS.LAST_MODIFIED) && (
91+
<td className="report-row content-center">
92+
{formatDate(new Date(record.lastModified ? record.lastModified : record.dateCreated))}
93+
</td>
94+
)}
95+
96+
{props.visibleColumns.includes(COLUMNS.STATUS) && (
97+
<td className="report-row content-center">
98+
{statusInfo ? <HelpIcon text={statusInfoText()} glyph={statusInfo.glyph} /> : "N/A"}
99+
</td>
100+
)}
74101
<td className="report-row actions">
75102
<PromiseTrackingMask area={`record-${record.key}`} />
76103
<Button variant="primary" size="sm" title={i18n("records.open-tooltip")} onClick={() => props.onEdit(record)}>
@@ -97,6 +124,7 @@ RecordRow.propTypes = {
97124
onDelete: PropTypes.func.isRequired,
98125
disableDelete: PropTypes.bool.isRequired,
99126
currentUser: PropTypes.object.isRequired,
127+
visibleColumns: PropTypes.array.isRequired,
100128
};
101129

102130
export default RecordRow;

src/components/record/RecordTable.jsx

Lines changed: 118 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import React from "react";
2-
import { OverlayTrigger, Popover, Table } from "react-bootstrap";
1+
import React, { useState } from "react";
2+
import { Alert, OverlayTrigger, Popover, Table } from "react-bootstrap";
33
import DeleteItemDialog from "../DeleteItemDialog";
44
import { injectIntl } from "react-intl";
55
import withI18n from "../../i18n/withI18n";
66
import RecordRow from "./RecordRow";
77
import PropTypes from "prop-types";
88
import { processTypeaheadOptions } from "./TypeaheadAnswer";
9-
import { IfGranted } from "react-authorization";
10-
import { ROLE } from "../../constants/DefaultConstants";
9+
import { COLUMNS } from "../../constants/DefaultConstants";
1110
import DateIntervalFilter from "./filter/DateIntervalFilter";
1211
import PhaseFilter from "./filter/PhaseFilter";
1312
import InstitutionFilter from "./filter/InstitutionFilter";
@@ -17,125 +16,142 @@ import { useI18n } from "../../hooks/useI18n";
1716
import FilterIndicator from "../misc/FilterIndicator";
1817
import { sanitizeArray } from "../../utils/Utils";
1918

20-
class RecordTable extends React.Component {
21-
static propTypes = {
22-
intl: PropTypes.shape({
23-
messages: PropTypes.object,
24-
formatMessage: PropTypes.func,
25-
locale: PropTypes.string,
26-
}),
27-
i18n: PropTypes.func,
28-
recordsLoaded: PropTypes.object.isRequired,
29-
formTemplate: PropTypes.string,
30-
formTemplatesLoaded: PropTypes.object.isRequired,
31-
handlers: PropTypes.object.isRequired,
32-
recordDeleted: PropTypes.object,
33-
disableDelete: PropTypes.bool,
34-
currentUser: PropTypes.object.isRequired,
35-
filterAndSort: PropTypes.object.isRequired,
36-
};
19+
const RecordTable = ({
20+
intl,
21+
i18n,
22+
recordsLoaded,
23+
formTemplate,
24+
formTemplatesLoaded,
25+
handlers,
26+
disableDelete = false,
27+
currentUser,
28+
filterAndSort,
29+
visibleColumns,
30+
}) => {
31+
const [selectedRecord, setSelectedRecord] = useState(null);
3732

38-
static defaultProps = {
39-
disableDelete: false,
33+
const onDelete = (record) => {
34+
setSelectedRecord(record);
4035
};
4136

42-
constructor(props) {
43-
super(props);
44-
this.i18n = this.props.i18n;
45-
this.state = {
46-
showDialog: false,
47-
};
48-
}
49-
50-
_onDelete = (record) => {
51-
this.setState({ selectedRecord: record });
37+
const onCancelDelete = () => {
38+
setSelectedRecord(null);
5239
};
5340

54-
_onCancelDelete = () => {
55-
this.setState({ selectedRecord: null });
41+
const onSubmitDelete = () => {
42+
handlers.onDelete(selectedRecord);
43+
setSelectedRecord(null);
5644
};
5745

58-
_onSubmitDelete = () => {
59-
this.props.handlers.onDelete(this.state.selectedRecord);
60-
this.setState({ selectedRecord: null });
46+
const getDeleteLabel = () => {
47+
return selectedRecord ? selectedRecord.localName : "";
6148
};
6249

63-
render() {
64-
const filteredRecords = this._getFormTemplateRecords();
65-
return (
66-
<div>
67-
<DeleteItemDialog
68-
onClose={this._onCancelDelete}
69-
onSubmit={this._onSubmitDelete}
70-
show={this.state.selectedRecord !== null}
71-
item={this.state.selectedRecord}
72-
itemLabel={this._getDeleteLabel()}
73-
/>
74-
<Table size="sm" responsive striped bordered hover>
75-
{this._renderHeader()}
76-
<tbody>{this._renderRows(filteredRecords)}</tbody>
77-
</Table>
78-
</div>
79-
);
80-
}
81-
82-
_getDeleteLabel() {
83-
return this.state.selectedRecord ? this.state.selectedRecord.localName : "";
84-
}
50+
const getFormTemplateRecords = () => {
51+
const records = sanitizeArray(recordsLoaded.records);
52+
if (!formTemplate) {
53+
return records;
54+
}
55+
return records.filter((r) => r.formTemplate === formTemplate);
56+
};
8557

86-
_renderHeader() {
87-
const { filters, sort, onChange } = this.props.filterAndSort;
58+
const renderHeader = () => {
59+
const { filters, sort, onChange } = filterAndSort;
8860
return (
8961
<thead>
9062
<tr>
91-
<IfGranted expected={ROLE.READ_ALL_RECORDS} actual={this.props.currentUser.roles}>
92-
<th className="col-1 content-center">{this.i18n("records.id")}</th>
93-
</IfGranted>
94-
<th className="col-2 content-center">{this.i18n("records.local-name")}</th>
95-
<IfGranted expected={ROLE.READ_ALL_RECORDS} actual={this.props.currentUser.roles}>
63+
{visibleColumns.includes(COLUMNS.ID) && <th className="col-1 content-center">{i18n("records.id")}</th>}
64+
{visibleColumns.includes(COLUMNS.NAME) && (
65+
<th className="col-2 content-center">{i18n("records.local-name")}</th>
66+
)}
67+
{visibleColumns.includes(COLUMNS.AUTHOR) && (
68+
<th className="col-2 content-center">{i18n("records.author")}</th>
69+
)}
70+
{visibleColumns.includes(COLUMNS.INSTITUTION) && (
9671
<FilterableInstitutionHeader filters={filters} onFilterChange={onChange} />
72+
)}
73+
{visibleColumns.includes(COLUMNS.TEMPLATE) && (
9774
<FilterableTemplateHeader filters={filters} onFilterChange={onChange} />
98-
</IfGranted>
99-
<FilterableLastModifiedHeader filters={filters} sort={sort} onFilterAndSortChange={onChange} />
100-
<FilterablePhaseHeader filters={filters} onFilterChange={onChange} />
101-
<th className="col-1 content-center">{this.i18n("actions")}</th>
75+
)}
76+
{visibleColumns.includes(COLUMNS.LAST_MODIFIED) && (
77+
<FilterableLastModifiedHeader filters={filters} sort={sort} onFilterAndSortChange={onChange} />
78+
)}
79+
{visibleColumns.includes(COLUMNS.STATUS) && (
80+
<FilterablePhaseHeader filters={filters} onFilterChange={onChange} />
81+
)}
82+
<th className="col-1 content-center">{i18n("actions")}</th>
10283
</tr>
10384
</thead>
10485
);
105-
}
86+
};
10687

107-
_renderRows(filteredRecords) {
108-
const { formTemplatesLoaded, handlers, intl } = this.props;
88+
const renderRows = (filteredRecords) => {
10989
const formTemplateOptions = formTemplatesLoaded.formTemplates
11090
? processTypeaheadOptions(formTemplatesLoaded.formTemplates, intl)
11191
: [];
112-
let rows = [];
113-
for (let i = 0, len = filteredRecords.length; i < len; i++) {
114-
rows.push(
115-
<RecordRow
116-
key={filteredRecords[i].key}
117-
record={filteredRecords[i]}
118-
onEdit={handlers.onEdit}
119-
onDelete={this._onDelete}
120-
formTemplateOptions={formTemplateOptions}
121-
currentUser={this.props.currentUser}
122-
disableDelete={this.props.disableDelete}
123-
/>,
124-
);
125-
}
126-
return rows;
127-
}
12892

129-
_getFormTemplateRecords() {
130-
const records = sanitizeArray(this.props.recordsLoaded.records),
131-
formTemplate = this.props.formTemplate;
93+
return filteredRecords.map((record) => (
94+
<RecordRow
95+
key={record.key}
96+
record={record}
97+
onEdit={handlers.onEdit}
98+
onDelete={onDelete}
99+
formTemplateOptions={formTemplateOptions}
100+
currentUser={currentUser}
101+
disableDelete={disableDelete}
102+
visibleColumns={visibleColumns}
103+
/>
104+
));
105+
};
106+
107+
const filteredRecords = getFormTemplateRecords();
132108

133-
if (!formTemplate) {
134-
return records;
135-
}
136-
return records.filter((r) => r.formTemplate === formTemplate);
137-
}
138-
}
109+
return (
110+
<div>
111+
<DeleteItemDialog
112+
onClose={onCancelDelete}
113+
onSubmit={onSubmitDelete}
114+
show={selectedRecord !== null}
115+
item={selectedRecord}
116+
itemLabel={getDeleteLabel()}
117+
/>
118+
<Table className="mb-0" size="sm" responsive striped bordered hover>
119+
{renderHeader()}
120+
{recordsLoaded.records && recordsLoaded.records.length > 0 ? (
121+
<tbody>{renderRows(filteredRecords)}</tbody>
122+
) : null}
123+
</Table>
124+
125+
{(!recordsLoaded.records || recordsLoaded.records.length === 0) && (
126+
<Alert variant="warning" className="w-100">
127+
{i18n("records.no-records")}
128+
</Alert>
129+
)}
130+
</div>
131+
);
132+
};
133+
134+
RecordTable.propTypes = {
135+
intl: PropTypes.shape({
136+
messages: PropTypes.object,
137+
formatMessage: PropTypes.func,
138+
locale: PropTypes.string,
139+
}),
140+
i18n: PropTypes.func,
141+
recordsLoaded: PropTypes.object.isRequired,
142+
formTemplate: PropTypes.string,
143+
formTemplatesLoaded: PropTypes.object.isRequired,
144+
handlers: PropTypes.object.isRequired,
145+
recordDeleted: PropTypes.object,
146+
disableDelete: PropTypes.bool,
147+
currentUser: PropTypes.object.isRequired,
148+
filterAndSort: PropTypes.object.isRequired,
149+
visibleColumns: PropTypes.array,
150+
};
151+
152+
RecordTable.defaultProps = {
153+
disableDelete: false,
154+
};
139155

140156
const FilterableInstitutionHeader = ({ filters, onFilterChange }) => {
141157
const { i18n } = useI18n();
@@ -158,7 +174,7 @@ const FilterableInstitutionHeader = ({ filters, onFilterChange }) => {
158174
title={i18n("table.column.filterable")}
159175
>
160176
{i18n("institution.panel-title")}
161-
<FilterIndicator filterValue={filters.institution} />
177+
{sanitizeArray(filters.institution).length > 0 && <FilterIndicator filterValue={filters.institution} />}
162178
</th>
163179
</OverlayTrigger>
164180
);
@@ -239,7 +255,7 @@ const FilterablePhaseHeader = ({ filters, onFilterChange }) => {
239255
>
240256
<th id="records-phase" className="col-1 content-center cursor-pointer" title={i18n("table.column.filterable")}>
241257
{i18n("records.completion-status")}
242-
<FilterIndicator filterValue={filters.phase} />
258+
{sanitizeArray(filters.phase).length > 0 && <FilterIndicator filterValue={filters.phase} />}
243259
</th>
244260
</OverlayTrigger>
245261
);
@@ -273,7 +289,7 @@ const FilterableTemplateHeader = ({ filters, onFilterChange }) => {
273289
>
274290
<th id="records-template" className="col-2 content-center cursor-pointer" title={i18n("table.column.filterable")}>
275291
{i18n("records.form-template")}
276-
<FilterIndicator filterValue={filters.formTemplate} />
292+
{sanitizeArray(filters.formTemplate).length > 0 && <FilterIndicator filterValue={filters.formTemplate} />}
277293
</th>
278294
</OverlayTrigger>
279295
);

0 commit comments

Comments
 (0)