@visns-studio/visns-components 6.1.7 → 6.2.0
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/README.md +161 -1
- package/package.json +1 -1
- package/src/components/Fetch.jsx +49 -19
- package/src/components/TableFilter.jsx +59 -45
- package/src/components/generic/GenericReport.jsx +835 -68
- package/src/components/generic/SectionGroupedReport.jsx +199 -226
- package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +81 -0
- package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +337 -0
- package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +483 -0
- package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +202 -0
- package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +157 -0
- package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +352 -0
- package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +154 -0
- package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +153 -0
- package/src/components/generic/reportSemantics.js +971 -0
- package/src/components/generic/useSemanticModel.js +99 -0
- package/src/components/styles/ReportSemantic.module.scss +444 -0
- package/src/index.js +4 -0
|
@@ -1,13 +1,118 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { ChevronDown, ChevronRight
|
|
4
|
-
import { formatCellValue
|
|
3
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { formatCellValue } from './shared/groupingUtils';
|
|
5
5
|
import debugLog from '../utils/debugLog';
|
|
6
6
|
import './groupedReport.css';
|
|
7
7
|
|
|
8
|
+
// Resolve a dotted path against nested objects, e.g. 'adviser.name'.
|
|
9
|
+
const getNestedProperty = (obj, path) => {
|
|
10
|
+
return path.split('.').reduce((current, key) => {
|
|
11
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
12
|
+
}, obj);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// The same logical column can arrive under a handful of key shapes depending
|
|
16
|
+
// on which backend produced the rows, so exact-key candidates are tried in
|
|
17
|
+
// priority order. `column.key` is the explicit opt-out: when the caller knows
|
|
18
|
+
// the row key it wins over every heuristic below it.
|
|
19
|
+
const CANDIDATE_KEY_BUILDERS = [
|
|
20
|
+
(column) => column.key,
|
|
21
|
+
(column) => column.displayName,
|
|
22
|
+
(column) => (column.displayName ? `\`${column.displayName}\`` : null),
|
|
23
|
+
(column) =>
|
|
24
|
+
column.table && column.column
|
|
25
|
+
? `${column.table}_${column.column}`
|
|
26
|
+
: null,
|
|
27
|
+
(column) =>
|
|
28
|
+
column.table && column.column
|
|
29
|
+
? `\`${column.table}_${column.column}\``
|
|
30
|
+
: null,
|
|
31
|
+
(column) => column.column,
|
|
32
|
+
(column) => (column.column ? `\`${column.column}\`` : null),
|
|
33
|
+
(column) => column.id,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Dotted paths are walked into nested objects rather than looked up literally.
|
|
37
|
+
const buildNestedPatterns = (column) => {
|
|
38
|
+
const patterns = [];
|
|
39
|
+
|
|
40
|
+
if (column.table && column.column) {
|
|
41
|
+
patterns.push(
|
|
42
|
+
`${column.table}.${column.column}`,
|
|
43
|
+
`${column.table}.${column.column}.name`,
|
|
44
|
+
`${column.table}.${column.column}.label`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof column.column === 'string' && column.column.includes('.')) {
|
|
49
|
+
patterns.push(column.column);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return patterns;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const resolveColumnValue = (row, column) => {
|
|
56
|
+
for (const buildKey of CANDIDATE_KEY_BUILDERS) {
|
|
57
|
+
const key = buildKey(column);
|
|
58
|
+
if (key && row[key] !== undefined) {
|
|
59
|
+
return row[key];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const nestedPatterns = buildNestedPatterns(column);
|
|
64
|
+
for (const pattern of nestedPatterns) {
|
|
65
|
+
const nestedValue = getNestedProperty(row, pattern);
|
|
66
|
+
if (nestedValue !== undefined) {
|
|
67
|
+
return nestedValue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Last resort: any row key that case-insensitively mentions the column.
|
|
72
|
+
const needles = [column.column, column.displayName, column.header]
|
|
73
|
+
.filter((name) => typeof name === 'string' && name !== '')
|
|
74
|
+
.map((name) => name.toLowerCase());
|
|
75
|
+
const matchedKey = Object.keys(row).find((key) =>
|
|
76
|
+
needles.some((needle) => key.toLowerCase().includes(needle))
|
|
77
|
+
);
|
|
78
|
+
if (matchedKey) {
|
|
79
|
+
return row[matchedKey];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Object.keys(row).length > 0) {
|
|
83
|
+
debugLog('Grouped report: no value found for column', {
|
|
84
|
+
column: column.header || column.displayName || column.column,
|
|
85
|
+
availableKeys: Object.keys(row),
|
|
86
|
+
testedPatterns: nestedPatterns,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return undefined;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const getRowClassName = (row, config) => {
|
|
94
|
+
if (row.__empty_row__) {
|
|
95
|
+
return `empty-row empty-row--${config.emptyRowStyle || 'light'}`;
|
|
96
|
+
}
|
|
97
|
+
return 'data-row';
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Columns may carry no table/column at all (semantic mode supplies key/header
|
|
101
|
+
// only), so React keys fall back through whatever identifies the column.
|
|
102
|
+
const getColumnKey = (col, colIndex) =>
|
|
103
|
+
col.key || col.displayName || col.column || colIndex;
|
|
104
|
+
|
|
105
|
+
const renderCellValue = (row, column) => {
|
|
106
|
+
if (row.__empty_row__) {
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return formatCellValue(resolveColumnValue(row, column), column);
|
|
111
|
+
};
|
|
112
|
+
|
|
8
113
|
const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
9
114
|
const [collapsedGroups, setCollapsedGroups] = useState(new Set());
|
|
10
|
-
|
|
115
|
+
|
|
11
116
|
const toggleGroupCollapse = (groupName) => {
|
|
12
117
|
const newCollapsed = new Set(collapsedGroups);
|
|
13
118
|
if (newCollapsed.has(groupName)) {
|
|
@@ -17,178 +122,10 @@ const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
|
17
122
|
}
|
|
18
123
|
setCollapsedGroups(newCollapsed);
|
|
19
124
|
};
|
|
20
|
-
// Helper function to get nested object property
|
|
21
|
-
const getNestedProperty = (obj, path) => {
|
|
22
|
-
return path.split('.').reduce((current, key) => {
|
|
23
|
-
return current && current[key] !== undefined ? current[key] : undefined;
|
|
24
|
-
}, obj);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const renderCellValue = (row, column) => {
|
|
28
|
-
if (row.__empty_row__) {
|
|
29
|
-
return '';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Try multiple field access patterns to find the data
|
|
33
|
-
let value;
|
|
34
|
-
|
|
35
|
-
// First try displayName with backticks (as seen in debug)
|
|
36
|
-
if (column.displayName && row[`\`${column.displayName}\``] !== undefined) {
|
|
37
|
-
value = row[`\`${column.displayName}\``];
|
|
38
|
-
}
|
|
39
|
-
// Then try displayName without backticks
|
|
40
|
-
else if (column.displayName && row[column.displayName] !== undefined) {
|
|
41
|
-
value = row[column.displayName];
|
|
42
|
-
}
|
|
43
|
-
// Then try table_column format
|
|
44
|
-
else if (column.table && row[`${column.table}_${column.column}`] !== undefined) {
|
|
45
|
-
value = row[`${column.table}_${column.column}`];
|
|
46
|
-
}
|
|
47
|
-
// Try column with table prefix and backticks
|
|
48
|
-
else if (column.table && row[`\`${column.table}_${column.column}\``] !== undefined) {
|
|
49
|
-
value = row[`\`${column.table}_${column.column}\``];
|
|
50
|
-
}
|
|
51
|
-
// Then try just column name
|
|
52
|
-
else if (row[column.column] !== undefined) {
|
|
53
|
-
value = row[column.column];
|
|
54
|
-
}
|
|
55
|
-
// Try column with backticks
|
|
56
|
-
else if (row[`\`${column.column}\``] !== undefined) {
|
|
57
|
-
value = row[`\`${column.column}\``];
|
|
58
|
-
}
|
|
59
|
-
// Handle calculated fields
|
|
60
|
-
else if (column.id && row[column.id] !== undefined) {
|
|
61
|
-
value = row[column.id];
|
|
62
|
-
}
|
|
63
|
-
// Try nested property access based on common patterns
|
|
64
|
-
let nestedPatterns = [];
|
|
65
|
-
if (column.table && column.column) {
|
|
66
|
-
// Try patterns like lead.client.name, users.name, etc.
|
|
67
|
-
nestedPatterns = [
|
|
68
|
-
`${column.table}.${column.column}`,
|
|
69
|
-
`${column.table}.${column.column}.name`,
|
|
70
|
-
`${column.table}.${column.column}.label`,
|
|
71
|
-
column.displayName === 'User' ? 'lead.client.name' : null,
|
|
72
|
-
column.displayName === 'Project' ? 'lead.project_name' : null,
|
|
73
|
-
column.displayName === 'Start Date' ? 'lead.start_date' : null,
|
|
74
|
-
column.displayName === 'End Date' ? 'lead.end_date' : null,
|
|
75
|
-
column.displayName === 'Database Updated' ? 'lead.updated_at' : null,
|
|
76
|
-
column.displayName === 'Comments' ? 'lead.comments' : null,
|
|
77
|
-
// Try alternative field patterns
|
|
78
|
-
column.column === 'name' && column.table === 'users' ? 'lead.client.name' : null,
|
|
79
|
-
column.column === 'updated_at' && column.table === 'leads' ? 'lead.updated_at' : null
|
|
80
|
-
].filter(Boolean);
|
|
81
|
-
|
|
82
|
-
for (const pattern of nestedPatterns) {
|
|
83
|
-
const nestedValue = getNestedProperty(row, pattern);
|
|
84
|
-
if (nestedValue !== undefined) {
|
|
85
|
-
value = nestedValue;
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Special handling for Comments field (appears in multiple formats)
|
|
92
|
-
if (value === undefined && column.displayName === 'Comments') {
|
|
93
|
-
value = row['Comments'] || row['comments'] || row['latest_comment'] || '';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Format Comments field with enhanced styling
|
|
97
|
-
if (column.displayName === 'Comments' && value) {
|
|
98
|
-
const comments = value.split('• ').filter(comment => comment.trim());
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<div style={{
|
|
102
|
-
display: 'flex',
|
|
103
|
-
alignItems: 'flex-start',
|
|
104
|
-
gap: '8px',
|
|
105
|
-
maxWidth: '300px'
|
|
106
|
-
}}>
|
|
107
|
-
<MessageSquare
|
|
108
|
-
size={16}
|
|
109
|
-
style={{
|
|
110
|
-
marginTop: '4px',
|
|
111
|
-
color: '#3b82f6',
|
|
112
|
-
flexShrink: 0
|
|
113
|
-
}}
|
|
114
|
-
/>
|
|
115
|
-
<div style={{
|
|
116
|
-
flex: 1,
|
|
117
|
-
fontSize: '13px',
|
|
118
|
-
lineHeight: '1.5'
|
|
119
|
-
}}>
|
|
120
|
-
{comments.length > 1 ? (
|
|
121
|
-
<div>
|
|
122
|
-
{comments.map((comment, index) => (
|
|
123
|
-
<div
|
|
124
|
-
key={index}
|
|
125
|
-
style={{
|
|
126
|
-
marginBottom: index < comments.length - 1 ? '8px' : '0',
|
|
127
|
-
padding: '6px 10px',
|
|
128
|
-
backgroundColor: '#f8fafc',
|
|
129
|
-
border: '1px solid #e2e8f0',
|
|
130
|
-
borderRadius: '6px',
|
|
131
|
-
fontSize: '12px',
|
|
132
|
-
color: '#475569'
|
|
133
|
-
}}
|
|
134
|
-
>
|
|
135
|
-
{comment.trim()}
|
|
136
|
-
</div>
|
|
137
|
-
))}
|
|
138
|
-
</div>
|
|
139
|
-
) : (
|
|
140
|
-
<div style={{
|
|
141
|
-
padding: '6px 10px',
|
|
142
|
-
backgroundColor: '#f8fafc',
|
|
143
|
-
border: '1px solid #e2e8f0',
|
|
144
|
-
borderRadius: '6px',
|
|
145
|
-
fontSize: '12px',
|
|
146
|
-
color: '#475569'
|
|
147
|
-
}}>
|
|
148
|
-
{value}
|
|
149
|
-
</div>
|
|
150
|
-
)}
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Try to find any key that contains the column name
|
|
157
|
-
if (value === undefined) {
|
|
158
|
-
const possibleKeys = Object.keys(row).filter(key =>
|
|
159
|
-
key.toLowerCase().includes(column.column?.toLowerCase()) ||
|
|
160
|
-
key.toLowerCase().includes(column.displayName?.toLowerCase())
|
|
161
|
-
);
|
|
162
|
-
if (possibleKeys.length > 0) {
|
|
163
|
-
value = row[possibleKeys[0]];
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
value === undefined &&
|
|
169
|
-
!row.__empty_row__ &&
|
|
170
|
-
Object.keys(row).length > 0
|
|
171
|
-
) {
|
|
172
|
-
debugLog('Grouped report: no value found for column', {
|
|
173
|
-
column: column.displayName || column.column,
|
|
174
|
-
availableKeys: Object.keys(row),
|
|
175
|
-
testedPatterns: nestedPatterns,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
125
|
|
|
179
|
-
return formatCellValue(value, column);
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const getRowClassName = (row, config) => {
|
|
183
|
-
if (row.__empty_row__) {
|
|
184
|
-
return `empty-row empty-row--${config.emptyRowStyle || 'light'}`;
|
|
185
|
-
}
|
|
186
|
-
return 'data-row';
|
|
187
|
-
};
|
|
188
|
-
|
|
189
126
|
const renderExportButton = () => {
|
|
190
127
|
if (!onExport) return null;
|
|
191
|
-
|
|
128
|
+
|
|
192
129
|
return (
|
|
193
130
|
<div className="grouped-report-actions">
|
|
194
131
|
<button
|
|
@@ -202,18 +139,19 @@ const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
|
202
139
|
</div>
|
|
203
140
|
);
|
|
204
141
|
};
|
|
205
|
-
|
|
142
|
+
|
|
206
143
|
return (
|
|
207
144
|
<div className="section-grouped-report">
|
|
208
145
|
<div className="grouped-report-header">
|
|
209
146
|
<div className="grouped-report-summary">
|
|
210
147
|
<span className="summary-text">
|
|
211
|
-
{data.totalRecords} records across {data.totalGroups}
|
|
148
|
+
{data.totalRecords} records across {data.totalGroups}{' '}
|
|
149
|
+
{config.groupDisplayName?.toLowerCase() || 'group'}(s)
|
|
212
150
|
</span>
|
|
213
151
|
</div>
|
|
214
152
|
{renderExportButton()}
|
|
215
153
|
</div>
|
|
216
|
-
|
|
154
|
+
|
|
217
155
|
<div className="grouped-report-content">
|
|
218
156
|
{(data.groups || []).map((group, groupIndex) => (
|
|
219
157
|
<GroupSection
|
|
@@ -224,7 +162,9 @@ const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
|
224
162
|
index={groupIndex}
|
|
225
163
|
renderCellValue={renderCellValue}
|
|
226
164
|
isCollapsed={collapsedGroups.has(group.groupName)}
|
|
227
|
-
onToggleCollapse={() =>
|
|
165
|
+
onToggleCollapse={() =>
|
|
166
|
+
toggleGroupCollapse(group.groupName)
|
|
167
|
+
}
|
|
228
168
|
/>
|
|
229
169
|
))}
|
|
230
170
|
</div>
|
|
@@ -232,19 +172,19 @@ const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
|
232
172
|
);
|
|
233
173
|
};
|
|
234
174
|
|
|
235
|
-
const GroupSection = ({
|
|
175
|
+
const GroupSection = ({
|
|
176
|
+
group,
|
|
177
|
+
config,
|
|
178
|
+
columns,
|
|
179
|
+
renderCellValue,
|
|
180
|
+
isCollapsed,
|
|
181
|
+
onToggleCollapse,
|
|
182
|
+
}) => {
|
|
236
183
|
const allRows = [...(group.rows || []), ...(group.emptyRows || [])];
|
|
237
184
|
const headerStyleClass = `group-section--${
|
|
238
185
|
config.groupHeaderStyle || 'primary'
|
|
239
186
|
}`;
|
|
240
|
-
|
|
241
|
-
const getRowClassName = (row, config) => {
|
|
242
|
-
if (row.__empty_row__) {
|
|
243
|
-
return `empty-row empty-row--${config.emptyRowStyle || 'light'}`;
|
|
244
|
-
}
|
|
245
|
-
return 'data-row';
|
|
246
|
-
};
|
|
247
|
-
|
|
187
|
+
|
|
248
188
|
return (
|
|
249
189
|
<div className={`group-section ${headerStyleClass}`}>
|
|
250
190
|
<button
|
|
@@ -273,62 +213,92 @@ const GroupSection = ({ group, config, columns, index, renderCellValue, isCollap
|
|
|
273
213
|
)}
|
|
274
214
|
</span>
|
|
275
215
|
</button>
|
|
276
|
-
|
|
216
|
+
|
|
277
217
|
{!isCollapsed && (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
218
|
+
<div className="group-content">
|
|
219
|
+
<div className="table-container">
|
|
220
|
+
<table className="group-table">
|
|
221
|
+
<thead>
|
|
222
|
+
<tr>
|
|
223
|
+
{columns.map((col, colIndex) => (
|
|
224
|
+
<th
|
|
225
|
+
key={getColumnKey(col, colIndex)}
|
|
226
|
+
className={
|
|
227
|
+
col.required
|
|
228
|
+
? 'required-column'
|
|
229
|
+
: ''
|
|
230
|
+
}
|
|
231
|
+
>
|
|
232
|
+
{col.header ||
|
|
233
|
+
col.displayName ||
|
|
234
|
+
col.column}
|
|
235
|
+
</th>
|
|
236
|
+
))}
|
|
237
|
+
</tr>
|
|
238
|
+
</thead>
|
|
239
|
+
<tbody>
|
|
240
|
+
{allRows.map((row, rowIndex) => (
|
|
241
|
+
<tr
|
|
242
|
+
key={
|
|
243
|
+
row.id ||
|
|
244
|
+
`${group.groupName}_row_${rowIndex}`
|
|
245
|
+
}
|
|
246
|
+
className={getRowClassName(row, config)}
|
|
303
247
|
>
|
|
304
|
-
{
|
|
305
|
-
|
|
248
|
+
{columns.map((col, colIndex) => (
|
|
249
|
+
<td
|
|
250
|
+
key={`${rowIndex}_${getColumnKey(
|
|
251
|
+
col,
|
|
252
|
+
colIndex
|
|
253
|
+
)}`}
|
|
254
|
+
className={
|
|
255
|
+
col.required
|
|
256
|
+
? 'required-column'
|
|
257
|
+
: ''
|
|
258
|
+
}
|
|
259
|
+
>
|
|
260
|
+
{renderCellValue(row, col)}
|
|
261
|
+
</td>
|
|
262
|
+
))}
|
|
263
|
+
</tr>
|
|
306
264
|
))}
|
|
307
|
-
</
|
|
308
|
-
|
|
309
|
-
</
|
|
310
|
-
</table>
|
|
265
|
+
</tbody>
|
|
266
|
+
</table>
|
|
267
|
+
</div>
|
|
311
268
|
</div>
|
|
312
|
-
</div>
|
|
313
269
|
)}
|
|
314
270
|
</div>
|
|
315
271
|
);
|
|
316
272
|
};
|
|
317
273
|
|
|
274
|
+
// Columns stay intentionally loose: callers supply either the legacy
|
|
275
|
+
// table/column pair or a semantic key/header pair, and unknown extras are fine.
|
|
276
|
+
const columnsPropType = PropTypes.arrayOf(
|
|
277
|
+
PropTypes.shape({
|
|
278
|
+
key: PropTypes.string,
|
|
279
|
+
header: PropTypes.string,
|
|
280
|
+
displayName: PropTypes.string,
|
|
281
|
+
table: PropTypes.string,
|
|
282
|
+
column: PropTypes.string,
|
|
283
|
+
required: PropTypes.bool,
|
|
284
|
+
type: PropTypes.string,
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
|
|
318
288
|
SectionGroupedReport.propTypes = {
|
|
319
289
|
data: PropTypes.shape({
|
|
320
290
|
groups: PropTypes.array,
|
|
321
291
|
totalGroups: PropTypes.number,
|
|
322
|
-
totalRecords: PropTypes.number
|
|
292
|
+
totalRecords: PropTypes.number,
|
|
323
293
|
}).isRequired,
|
|
324
294
|
config: PropTypes.shape({
|
|
325
295
|
groupDisplayName: PropTypes.string,
|
|
326
296
|
showGroupTotals: PropTypes.bool,
|
|
327
297
|
groupHeaderStyle: PropTypes.string,
|
|
328
|
-
emptyRowStyle: PropTypes.string
|
|
298
|
+
emptyRowStyle: PropTypes.string,
|
|
329
299
|
}).isRequired,
|
|
330
|
-
columns:
|
|
331
|
-
onExport: PropTypes.func
|
|
300
|
+
columns: columnsPropType.isRequired,
|
|
301
|
+
onExport: PropTypes.func,
|
|
332
302
|
};
|
|
333
303
|
|
|
334
304
|
GroupSection.propTypes = {
|
|
@@ -338,11 +308,14 @@ GroupSection.propTypes = {
|
|
|
338
308
|
rows: PropTypes.array,
|
|
339
309
|
emptyRows: PropTypes.array,
|
|
340
310
|
totalRows: PropTypes.number,
|
|
341
|
-
maxRows: PropTypes.number
|
|
311
|
+
maxRows: PropTypes.number,
|
|
342
312
|
}).isRequired,
|
|
343
313
|
config: PropTypes.object.isRequired,
|
|
344
|
-
columns:
|
|
345
|
-
index: PropTypes.number
|
|
314
|
+
columns: columnsPropType.isRequired,
|
|
315
|
+
index: PropTypes.number,
|
|
316
|
+
renderCellValue: PropTypes.func.isRequired,
|
|
317
|
+
isCollapsed: PropTypes.bool,
|
|
318
|
+
onToggleCollapse: PropTypes.func,
|
|
346
319
|
};
|
|
347
320
|
|
|
348
|
-
export default SectionGroupedReport;
|
|
321
|
+
export default SectionGroupedReport;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { CheckCircle } from 'lucide-react';
|
|
4
|
+
import { listEntities } from '../reportSemantics';
|
|
5
|
+
import styles from '../../styles/ReportSemantic.module.scss';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Step 1 in semantic mode: pick the thing the report is about.
|
|
9
|
+
*
|
|
10
|
+
* Only business labels and descriptions are shown — the entity id is never
|
|
11
|
+
* rendered, because it is an opaque server handle.
|
|
12
|
+
*/
|
|
13
|
+
const SemanticEntityStep = ({ model, selectedEntity, onSelect }) => {
|
|
14
|
+
const entities = listEntities(model);
|
|
15
|
+
|
|
16
|
+
if (entities.length === 0) {
|
|
17
|
+
return (
|
|
18
|
+
<div className={styles.empty}>
|
|
19
|
+
No data is available to report on yet.
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className={styles.section}>
|
|
26
|
+
<div className={styles.sectionIntro}>
|
|
27
|
+
<h3>What is this report about?</h3>
|
|
28
|
+
<p>
|
|
29
|
+
Pick the main thing you want to list. You can pull in
|
|
30
|
+
related information at the next step.
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className={styles.cardGrid}>
|
|
35
|
+
{entities.map((entity) => {
|
|
36
|
+
const isSelected = entity.id === selectedEntity;
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
key={entity.id}
|
|
40
|
+
type="button"
|
|
41
|
+
className={`${styles.card} ${
|
|
42
|
+
isSelected ? styles.selected : ''
|
|
43
|
+
}`}
|
|
44
|
+
aria-pressed={isSelected}
|
|
45
|
+
onClick={() => onSelect(entity.id)}
|
|
46
|
+
>
|
|
47
|
+
<span className={styles.cardTitle}>
|
|
48
|
+
<span>{entity.plural}</span>
|
|
49
|
+
{isSelected && <CheckCircle size={16} />}
|
|
50
|
+
</span>
|
|
51
|
+
{entity.description && (
|
|
52
|
+
<span className={styles.cardDescription}>
|
|
53
|
+
{entity.description}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
<span className={styles.cardMeta}>
|
|
57
|
+
{entity.fieldCount} details available
|
|
58
|
+
{entity.relationCount > 0
|
|
59
|
+
? ` · ${entity.relationCount} related areas`
|
|
60
|
+
: ''}
|
|
61
|
+
</span>
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
SemanticEntityStep.propTypes = {
|
|
71
|
+
model: PropTypes.object,
|
|
72
|
+
selectedEntity: PropTypes.string,
|
|
73
|
+
onSelect: PropTypes.func.isRequired,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
SemanticEntityStep.defaultProps = {
|
|
77
|
+
model: null,
|
|
78
|
+
selectedEntity: '',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default SemanticEntityStep;
|