@ltht-react/table 2.0.3 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -9
- package/src/atoms/questionnaire-withdrawn-table-cell.tsx +15 -0
- package/src/index.tsx +7 -0
- package/src/molecules/table-cell.tsx +93 -0
- package/src/molecules/table-component.tsx +146 -0
- package/src/molecules/table-methods.tsx +234 -0
- package/src/molecules/table-styled-components.tsx +233 -0
- package/src/molecules/table.tsx +161 -0
- package/src/molecules/useDimensionRef.tsx +37 -0
- package/src/molecules/useScrollRef.tsx +36 -0
- package/src/organisms/generic-table.tsx +33 -0
- package/src/organisms/questionnaire-table-methods.tsx +334 -0
- package/src/organisms/questionnaire-table.tsx +55 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { ActionMenuOption } from '@ltht-react/menu'
|
|
2
|
+
import {
|
|
3
|
+
Axis,
|
|
4
|
+
Maybe,
|
|
5
|
+
Questionnaire,
|
|
6
|
+
QuestionnaireItem,
|
|
7
|
+
QuestionnaireResponse,
|
|
8
|
+
QuestionnaireResponseItem,
|
|
9
|
+
QuestionnaireResponseItemAnswer,
|
|
10
|
+
QuestionnaireResponseStatus,
|
|
11
|
+
} from '@ltht-react/types'
|
|
12
|
+
import { EnsureMaybe, EnsureMaybeArray, partialDateTimeText } from '@ltht-react/utils'
|
|
13
|
+
import { getZIndex, TableDataWithPopUp } from '@ltht-react/styles'
|
|
14
|
+
import { DataEntity, Header, TableData } from '../molecules/table'
|
|
15
|
+
import { CellProps } from '../molecules/table-cell'
|
|
16
|
+
import QuestionnaireWithdrawnTableCell from '../atoms/questionnaire-withdrawn-table-cell'
|
|
17
|
+
|
|
18
|
+
const withdrawnWrapper = (text: string): JSX.Element => <QuestionnaireWithdrawnTableCell text={text} />
|
|
19
|
+
|
|
20
|
+
const mapQuestionnaireDefinitionAndResponsesToTableData = (
|
|
21
|
+
definition: Questionnaire,
|
|
22
|
+
questionnaireResponses: QuestionnaireResponse[],
|
|
23
|
+
axis: Axis,
|
|
24
|
+
adminActions?: AdminActionsForQuestionnaire[]
|
|
25
|
+
): TableData | undefined => {
|
|
26
|
+
const definitionItems = EnsureMaybeArray<QuestionnaireItem>(
|
|
27
|
+
EnsureMaybe<Maybe<QuestionnaireItem>[]>(definition.item, [])
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if (definitionItems.length === 0) {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (axis === 'y') {
|
|
35
|
+
return mapQuestionnaireObjectsToVerticalTableData(definitionItems, questionnaireResponses, adminActions)
|
|
36
|
+
}
|
|
37
|
+
return mapQuestionnaireObjectsToHorizontalTableData(definitionItems, questionnaireResponses, adminActions)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mapQuestionnaireObjectsToHorizontalTableData = (
|
|
41
|
+
definitionItems: Array<QuestionnaireItem>,
|
|
42
|
+
records: QuestionnaireResponse[],
|
|
43
|
+
adminActions?: AdminActionsForQuestionnaire[]
|
|
44
|
+
): TableData => {
|
|
45
|
+
const tableData: TableData = {
|
|
46
|
+
headers: [
|
|
47
|
+
{
|
|
48
|
+
id: 'date',
|
|
49
|
+
type: 'accessor',
|
|
50
|
+
cellProps: { text: 'Record Date' },
|
|
51
|
+
},
|
|
52
|
+
...recursivelyMapQuestionnaireItemsIntoHeaders(definitionItems),
|
|
53
|
+
],
|
|
54
|
+
rows: mapQuestionnaireResponsesIntoDataEntities(records, adminActions),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (adminActions) {
|
|
58
|
+
tableData.headers.splice(1, 0, {
|
|
59
|
+
id: 'adminactions',
|
|
60
|
+
type: 'accessor',
|
|
61
|
+
cellProps: { text: 'Actions' },
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
return tableData
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const recursivelyMapQuestionnaireItemsIntoHeaders = (questionnaireItems: QuestionnaireItem[]): Header[] =>
|
|
68
|
+
questionnaireItems.map((questionnaireItem) => {
|
|
69
|
+
const subItems = EnsureMaybeArray<QuestionnaireItem>(questionnaireItem.item ?? [])
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: questionnaireItem?.linkId ?? '',
|
|
73
|
+
type: subItems.length > 0 ? 'group' : 'accessor',
|
|
74
|
+
cellProps: { text: questionnaireItem?.text ?? '' },
|
|
75
|
+
subHeaders: recursivelyMapQuestionnaireItemsIntoHeaders(subItems),
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const mapQuestionnaireResponsesIntoDataEntities = (
|
|
80
|
+
records: QuestionnaireResponse[],
|
|
81
|
+
adminActions?: AdminActionsForQuestionnaire[]
|
|
82
|
+
): DataEntity[] =>
|
|
83
|
+
records
|
|
84
|
+
.filter((record) => !!record.item)
|
|
85
|
+
.map((record) => {
|
|
86
|
+
let dataEntity: DataEntity = {}
|
|
87
|
+
|
|
88
|
+
dataEntity.date = {
|
|
89
|
+
customComponentOverride:
|
|
90
|
+
record.status === QuestionnaireResponseStatus.EnteredInError
|
|
91
|
+
? withdrawnWrapper(partialDateTimeText(record.authored))
|
|
92
|
+
: undefined,
|
|
93
|
+
text: partialDateTimeText(record.authored),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (adminActions) {
|
|
97
|
+
const adminActionsForThisDataEntity = adminActions.find(
|
|
98
|
+
(actionForForm) => actionForForm.questionnaire === record.id
|
|
99
|
+
)
|
|
100
|
+
if (adminActionsForThisDataEntity) {
|
|
101
|
+
dataEntity.adminactions = {
|
|
102
|
+
adminActions: adminActionsForThisDataEntity.adminActions,
|
|
103
|
+
parentStyle: { zIndex: getZIndex(TableDataWithPopUp) },
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
record.item
|
|
108
|
+
?.filter((item) => item?.linkId && item?.answer)
|
|
109
|
+
.forEach((item) => {
|
|
110
|
+
const linkId = EnsureMaybe<string>(item?.linkId)
|
|
111
|
+
const answer = EnsureMaybe<QuestionnaireResponseItemAnswer>(item?.answer?.find((answer) => !!answer))
|
|
112
|
+
|
|
113
|
+
dataEntity[linkId] = createCellPropsForAnswer(
|
|
114
|
+
answer,
|
|
115
|
+
false,
|
|
116
|
+
record.status === QuestionnaireResponseStatus.EnteredInError
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if (answer.item) {
|
|
120
|
+
dataEntity = recursivelyMapResponseItemsOntoData(
|
|
121
|
+
EnsureMaybeArray<QuestionnaireResponseItem>(answer.item),
|
|
122
|
+
dataEntity,
|
|
123
|
+
record.status
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return dataEntity
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const recursivelyMapResponseItemsOntoData = (
|
|
132
|
+
items: QuestionnaireResponseItem[],
|
|
133
|
+
dataEntity: DataEntity,
|
|
134
|
+
status: QuestionnaireResponseStatus
|
|
135
|
+
): DataEntity => {
|
|
136
|
+
let updatedDataEntity = { ...dataEntity }
|
|
137
|
+
items.forEach((item) => {
|
|
138
|
+
const firstAnswer = item.answer ? item.answer[0] : undefined
|
|
139
|
+
|
|
140
|
+
if (item.linkId && firstAnswer) {
|
|
141
|
+
const props = createCellPropsForAnswer(firstAnswer, false, status === QuestionnaireResponseStatus.EnteredInError)
|
|
142
|
+
updatedDataEntity[item.linkId] = {
|
|
143
|
+
customComponentOverride: props.customComponentOverride,
|
|
144
|
+
text: props.text,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (firstAnswer.item) {
|
|
148
|
+
updatedDataEntity = recursivelyMapResponseItemsOntoData(
|
|
149
|
+
EnsureMaybeArray<QuestionnaireResponseItem>(firstAnswer.item),
|
|
150
|
+
updatedDataEntity,
|
|
151
|
+
status
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
return updatedDataEntity
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const mapQuestionnaireObjectsToVerticalTableData = (
|
|
160
|
+
definitionItems: Array<QuestionnaireItem>,
|
|
161
|
+
records: QuestionnaireResponse[],
|
|
162
|
+
adminActions?: AdminActionsForQuestionnaire[]
|
|
163
|
+
): TableData => ({
|
|
164
|
+
headers: [
|
|
165
|
+
{
|
|
166
|
+
id: 'property',
|
|
167
|
+
type: 'accessor',
|
|
168
|
+
cellProps: { text: '' },
|
|
169
|
+
},
|
|
170
|
+
...records.map(
|
|
171
|
+
(record): Header => ({
|
|
172
|
+
id: record?.id ?? '',
|
|
173
|
+
type: 'accessor',
|
|
174
|
+
cellProps: {
|
|
175
|
+
customComponentOverride:
|
|
176
|
+
record.status === QuestionnaireResponseStatus.EnteredInError
|
|
177
|
+
? withdrawnWrapper(partialDateTimeText(record.authored) ?? '')
|
|
178
|
+
: undefined,
|
|
179
|
+
text: partialDateTimeText(record.authored) ?? '',
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
),
|
|
183
|
+
],
|
|
184
|
+
rows: buildVerticalCellRows(definitionItems, records, adminActions),
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const buildVerticalCellRows = (
|
|
188
|
+
definitionItems: QuestionnaireItem[],
|
|
189
|
+
records: QuestionnaireResponse[],
|
|
190
|
+
adminActions?: AdminActionsForQuestionnaire[]
|
|
191
|
+
): DataEntity[] => {
|
|
192
|
+
const dataEntities = definitionItems.map((item) => {
|
|
193
|
+
let dataEntity: DataEntity = {}
|
|
194
|
+
|
|
195
|
+
dataEntity = buildVerticalCellRowsRecursive(EnsureMaybeArray<QuestionnaireItem>([item]), records, dataEntity)
|
|
196
|
+
|
|
197
|
+
return dataEntity
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
if (adminActions) {
|
|
201
|
+
const actionsDataEntity: DataEntity = {}
|
|
202
|
+
actionsDataEntity.property = { text: 'Actions' }
|
|
203
|
+
|
|
204
|
+
records.forEach((record) => {
|
|
205
|
+
const adminActionsForThisDataEntity = adminActions.find(
|
|
206
|
+
(actionForForm) => actionForForm.questionnaire === record.id
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if (adminActionsForThisDataEntity) {
|
|
210
|
+
actionsDataEntity[record.id] = {
|
|
211
|
+
adminActions: adminActionsForThisDataEntity.adminActions,
|
|
212
|
+
parentStyle: { zIndex: getZIndex(TableDataWithPopUp) },
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
return [actionsDataEntity].concat(dataEntities)
|
|
218
|
+
}
|
|
219
|
+
return dataEntities
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const buildVerticalCellRowsRecursive = (
|
|
223
|
+
definitionItems: QuestionnaireItem[],
|
|
224
|
+
records: QuestionnaireResponse[],
|
|
225
|
+
dataEntity: DataEntity
|
|
226
|
+
): DataEntity => {
|
|
227
|
+
let updatedDataEntity = { ...dataEntity }
|
|
228
|
+
definitionItems.forEach((definitionItem) => {
|
|
229
|
+
updatedDataEntity.property = { text: definitionItem.text ?? '' }
|
|
230
|
+
|
|
231
|
+
if (definitionItem.linkId) {
|
|
232
|
+
records.forEach((record) => {
|
|
233
|
+
updatedDataEntity = getRecordItemByLinkId(
|
|
234
|
+
EnsureMaybeArray<QuestionnaireResponseItem>(record.item ?? []),
|
|
235
|
+
EnsureMaybe<string>(definitionItem.linkId),
|
|
236
|
+
updatedDataEntity,
|
|
237
|
+
record.id,
|
|
238
|
+
record.status
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (definitionItem.item && definitionItem.item.length > 0) {
|
|
244
|
+
updatedDataEntity.subRows = buildVerticalCellRows(
|
|
245
|
+
EnsureMaybeArray<QuestionnaireItem>(definitionItem.item),
|
|
246
|
+
records
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
return updatedDataEntity
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const getRecordItemByLinkId = (
|
|
255
|
+
recordItems: QuestionnaireResponseItem[],
|
|
256
|
+
linkId: string,
|
|
257
|
+
dataEntity: DataEntity,
|
|
258
|
+
recordId: string,
|
|
259
|
+
status: QuestionnaireResponseStatus
|
|
260
|
+
) => {
|
|
261
|
+
let updatedDataEntity = { ...dataEntity }
|
|
262
|
+
recordItems.forEach((recordItem) => {
|
|
263
|
+
const recordItemAnswer =
|
|
264
|
+
recordItem.answer && recordItem.answer.length > 0 ? EnsureMaybe(recordItem.answer[0]) : undefined
|
|
265
|
+
|
|
266
|
+
if (recordItemAnswer) {
|
|
267
|
+
if (recordItem?.linkId && recordItem?.linkId === linkId) {
|
|
268
|
+
updatedDataEntity[recordId] = createCellPropsForAnswer(
|
|
269
|
+
recordItemAnswer,
|
|
270
|
+
true,
|
|
271
|
+
status === QuestionnaireResponseStatus.EnteredInError
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
if (recordItemAnswer.item && recordItemAnswer.item.length > 0) {
|
|
275
|
+
updatedDataEntity = getRecordItemByLinkId(
|
|
276
|
+
EnsureMaybeArray<QuestionnaireResponseItem>(recordItemAnswer.item),
|
|
277
|
+
linkId,
|
|
278
|
+
updatedDataEntity,
|
|
279
|
+
recordId,
|
|
280
|
+
status
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return updatedDataEntity
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const createCellPropsForAnswer = (
|
|
290
|
+
answer: QuestionnaireResponseItemAnswer,
|
|
291
|
+
shouldRenderCheckbox: boolean,
|
|
292
|
+
isEnteredInError: boolean
|
|
293
|
+
): CellProps => {
|
|
294
|
+
if (answer.valueString) {
|
|
295
|
+
if (shouldRenderCheckbox && answer.valueString === 'CHECKBOX') {
|
|
296
|
+
return {
|
|
297
|
+
iconProps: { type: 'checkbox', size: 'medium' },
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
customComponentOverride: isEnteredInError ? withdrawnWrapper(answer.valueString) : undefined,
|
|
302
|
+
text: answer.valueString,
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (answer.valueBoolean != null) {
|
|
306
|
+
const parsedBoolean = answer.valueBoolean ? 'Yes' : 'No'
|
|
307
|
+
return {
|
|
308
|
+
customComponentOverride: isEnteredInError ? withdrawnWrapper(parsedBoolean) : undefined,
|
|
309
|
+
text: parsedBoolean,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (answer.valueInteger != null) {
|
|
313
|
+
return {
|
|
314
|
+
customComponentOverride: isEnteredInError ? withdrawnWrapper(answer.valueInteger.toString()) : undefined,
|
|
315
|
+
text: answer.valueInteger.toString(),
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (answer.valueDecimal != null) {
|
|
319
|
+
return {
|
|
320
|
+
customComponentOverride: isEnteredInError ? withdrawnWrapper(answer.valueDecimal.toString()) : undefined,
|
|
321
|
+
text: answer.valueDecimal.toString(),
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
text: '',
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface AdminActionsForQuestionnaire {
|
|
330
|
+
questionnaire: string
|
|
331
|
+
adminActions: ActionMenuOption[]
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export default mapQuestionnaireDefinitionAndResponsesToTableData
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { QuestionnaireResponse, Questionnaire } from '@ltht-react/types'
|
|
2
|
+
import { FC, useMemo } from 'react'
|
|
3
|
+
import Icon from '@ltht-react/icon'
|
|
4
|
+
import Table, { IPaginationProps, ITableDimensionProps, ITableConfig } from '../molecules/table'
|
|
5
|
+
import mapQuestionnaireDefinitionAndResponsesToTableData, {
|
|
6
|
+
AdminActionsForQuestionnaire,
|
|
7
|
+
} from './questionnaire-table-methods'
|
|
8
|
+
|
|
9
|
+
const QuestionnaireTable: FC<IProps> = ({
|
|
10
|
+
definition,
|
|
11
|
+
records,
|
|
12
|
+
headerAxis = 'y',
|
|
13
|
+
staticColumns = 0,
|
|
14
|
+
adminActions,
|
|
15
|
+
pageSize = 10,
|
|
16
|
+
currentPage = 1,
|
|
17
|
+
keepPreviousData = true,
|
|
18
|
+
...props
|
|
19
|
+
}) => {
|
|
20
|
+
const tableData = useMemo(
|
|
21
|
+
() => mapQuestionnaireDefinitionAndResponsesToTableData(definition, records, headerAxis, adminActions),
|
|
22
|
+
[headerAxis, definition, records, adminActions]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// TODO: Replace this fragment with a properly styled error component.
|
|
26
|
+
// Maybe this could be a re-usable atom?
|
|
27
|
+
if (!tableData) {
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<Icon type="exclamation" size="large" color="red" />
|
|
31
|
+
<div>An error occurred whilst loading this table.</div>
|
|
32
|
+
</>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Table
|
|
38
|
+
tableData={tableData}
|
|
39
|
+
staticColumns={staticColumns}
|
|
40
|
+
headerAxis={headerAxis}
|
|
41
|
+
pageSize={pageSize}
|
|
42
|
+
currentPage={currentPage}
|
|
43
|
+
keepPreviousData={keepPreviousData}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface IProps extends ITableConfig, IPaginationProps, ITableDimensionProps {
|
|
50
|
+
definition: Questionnaire
|
|
51
|
+
records: QuestionnaireResponse[]
|
|
52
|
+
adminActions?: AdminActionsForQuestionnaire[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default QuestionnaireTable
|