@truedat/core 8.13.0 → 8.13.1
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 +3 -3
- package/src/components/UploadJob.js +102 -178
- package/src/components/UploadJobBreadcrumbs.js +13 -17
- package/src/components/UploadJobDetails.js +256 -0
- package/src/components/UploadJobHistory.js +90 -0
- package/src/components/UploadJobParser.js +155 -89
- package/src/components/UploadJobResults.js +278 -0
- package/src/components/UploadJobScope.js +37 -0
- package/src/components/UploadJobSummary.js +211 -0
- package/src/components/UploadJobs.js +48 -17
- package/src/components/__tests__/UploadJob.spec.js +118 -42
- package/src/components/__tests__/UploadJobBreadcrumbs.spec.js +3 -0
- package/src/components/__tests__/UploadJobHistory.spec.js +71 -0
- package/src/components/__tests__/UploadJobParser.spec.js +560 -26
- package/src/components/__tests__/UploadJobResults.spec.js +289 -0
- package/src/components/__tests__/UploadJobScope.spec.js +38 -0
- package/src/components/__tests__/UploadJobSummary.spec.js +208 -0
- package/src/components/__tests__/UploadJobs.spec.js +133 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/core",
|
|
3
|
-
"version": "8.13.
|
|
3
|
+
"version": "8.13.1",
|
|
4
4
|
"description": "Truedat Web Core",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@testing-library/jest-dom": "^6.6.3",
|
|
58
58
|
"@testing-library/react": "^16.3.0",
|
|
59
59
|
"@testing-library/user-event": "^14.6.1",
|
|
60
|
-
"@truedat/test": "8.13.
|
|
60
|
+
"@truedat/test": "8.13.1",
|
|
61
61
|
"identity-obj-proxy": "^3.0.0",
|
|
62
62
|
"jest": "^29.7.0",
|
|
63
63
|
"redux-saga-test-plan": "^4.0.6"
|
|
@@ -99,5 +99,5 @@
|
|
|
99
99
|
"swr": "^2.3.3",
|
|
100
100
|
"turndown": "^7.2.2"
|
|
101
101
|
},
|
|
102
|
-
"gitHead": "
|
|
102
|
+
"gitHead": "09ce6d3a8693835de75577e43d99d86addb2edf7"
|
|
103
103
|
}
|
|
@@ -1,113 +1,119 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { useState } from "react";
|
|
1
|
+
import PropTypes from "prop-types";
|
|
3
2
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
4
3
|
import { useParams } from "react-router";
|
|
5
4
|
import {
|
|
5
|
+
Dimmer,
|
|
6
6
|
Header,
|
|
7
7
|
Icon,
|
|
8
|
-
Segment,
|
|
9
|
-
Dimmer,
|
|
10
|
-
Loader,
|
|
11
|
-
Table,
|
|
12
8
|
Label,
|
|
13
|
-
|
|
9
|
+
Loader,
|
|
10
|
+
Segment,
|
|
14
11
|
} from "semantic-ui-react";
|
|
15
12
|
import DateTime from "@truedat/core/components/DateTime";
|
|
16
13
|
import { useUploadJob } from "../hooks/useUploadJobs";
|
|
17
|
-
import { StatusPill, ResponseCell } from "./UploadJobParser";
|
|
18
14
|
import UploadJobBreadcrumbs from "./UploadJobBreadcrumbs";
|
|
15
|
+
import UploadJobHistory from "./UploadJobHistory";
|
|
16
|
+
import { StatusPill } from "./UploadJobParser";
|
|
17
|
+
import UploadJobResults from "./UploadJobResults";
|
|
18
|
+
import UploadJobSummary, { getUploadJobStatus } from "./UploadJobSummary";
|
|
19
|
+
import { getUploadJobScope } from "./UploadJobScope";
|
|
19
20
|
|
|
20
|
-
export
|
|
21
|
-
const
|
|
22
|
-
const { data, loading } = useUploadJob(id);
|
|
23
|
-
const job = data?.data;
|
|
21
|
+
export const UploadJobContent = ({ constrainResults, job, loading, scope }) => {
|
|
22
|
+
const lifecycleStatus = getUploadJobStatus(job?.events, job?.latest_status);
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
return (
|
|
25
|
+
<Dimmer.Dimmable dimmed={loading}>
|
|
26
|
+
<Segment attached="bottom" className="upload-job-card">
|
|
27
|
+
{job ? (
|
|
28
|
+
<>
|
|
29
|
+
<div className="upload-job-meta">
|
|
30
|
+
<div className="upload-job-file">
|
|
31
|
+
<strong>{job.filename}</strong>
|
|
32
|
+
<span>{job.hash}</span>
|
|
33
|
+
</div>
|
|
34
|
+
<StatusPill status={job.latest_status} />
|
|
35
|
+
<span>
|
|
36
|
+
<Label basic size="small" className="upload-job-date-label">
|
|
37
|
+
<FormattedMessage
|
|
38
|
+
id="uploadJob.table.inserted_at"
|
|
39
|
+
defaultMessage="Inserted At"
|
|
40
|
+
/>
|
|
41
|
+
</Label>
|
|
42
|
+
<DateTime value={job.latest_event_at} />
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
27
45
|
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
<UploadJobSummary
|
|
47
|
+
events={job.events}
|
|
48
|
+
latestStatus={job.latest_status}
|
|
49
|
+
latestResponse={job.latest_event_response}
|
|
50
|
+
/>
|
|
51
|
+
<UploadJobHistory events={job.events} />
|
|
52
|
+
<UploadJobResults
|
|
53
|
+
constrainResults={constrainResults}
|
|
54
|
+
events={job.events}
|
|
55
|
+
lifecycleStatus={lifecycleStatus}
|
|
56
|
+
scope={scope}
|
|
57
|
+
/>
|
|
58
|
+
</>
|
|
59
|
+
) : null}
|
|
60
|
+
{loading ? (
|
|
61
|
+
<Dimmer active inverted>
|
|
62
|
+
<Loader />
|
|
63
|
+
</Dimmer>
|
|
64
|
+
) : null}
|
|
65
|
+
</Segment>
|
|
66
|
+
</Dimmer.Dimmable>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
30
69
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
70
|
+
UploadJobContent.propTypes = {
|
|
71
|
+
constrainResults: PropTypes.bool,
|
|
72
|
+
job: PropTypes.object,
|
|
73
|
+
loading: PropTypes.bool,
|
|
74
|
+
scope: PropTypes.string,
|
|
75
|
+
};
|
|
37
76
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
77
|
+
UploadJobContent.defaultProps = {
|
|
78
|
+
constrainResults: false,
|
|
79
|
+
job: undefined,
|
|
80
|
+
loading: false,
|
|
81
|
+
scope: "implementations",
|
|
82
|
+
};
|
|
41
83
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return events.map((event, idx) => (
|
|
45
|
-
<Table.Row key={`${status}-${idx}`}>
|
|
46
|
-
<Table.Cell>
|
|
47
|
-
<StatusPill status={event.status} />
|
|
48
|
-
</Table.Cell>
|
|
49
|
-
<Table.Cell>
|
|
50
|
-
<DateTime value={event.inserted_at} />
|
|
51
|
-
</Table.Cell>
|
|
52
|
-
<Table.Cell>
|
|
53
|
-
<ResponseCell {...event} />
|
|
54
|
-
</Table.Cell>
|
|
55
|
-
</Table.Row>
|
|
56
|
-
));
|
|
57
|
-
}
|
|
84
|
+
export const UploadJobModalContent = ({ id, scope }) => {
|
|
85
|
+
const { data, loading } = useUploadJob(id);
|
|
58
86
|
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
return (
|
|
88
|
+
<UploadJobContent
|
|
89
|
+
constrainResults
|
|
90
|
+
job={data?.data}
|
|
91
|
+
loading={loading}
|
|
92
|
+
scope={scope}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
61
96
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
style={{ backgroundColor: "#f8f9fa" }}
|
|
67
|
-
>
|
|
68
|
-
<Table.Cell>
|
|
69
|
-
<Button
|
|
70
|
-
basic
|
|
71
|
-
size="mini"
|
|
72
|
-
icon={isExpanded ? "chevron down" : "chevron right"}
|
|
73
|
-
content={` ${formatMessage({ id: `uploadJob.event.status.${status}` })} (${events.length})`}
|
|
74
|
-
onClick={() => toggleGroup(status)}
|
|
75
|
-
style={{ padding: "0.5em" }}
|
|
76
|
-
/>
|
|
77
|
-
</Table.Cell>
|
|
78
|
-
<Table.Cell>
|
|
79
|
-
<DateTime value={events[0]?.inserted_at} />
|
|
80
|
-
</Table.Cell>
|
|
81
|
-
<Table.Cell></Table.Cell>
|
|
82
|
-
</Table.Row>,
|
|
83
|
-
);
|
|
97
|
+
UploadJobModalContent.propTypes = {
|
|
98
|
+
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
99
|
+
scope: PropTypes.string,
|
|
100
|
+
};
|
|
84
101
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
groupRows.push(
|
|
89
|
-
<Table.Row key={`${status}-${idx}`} style={{ paddingLeft: "2em" }}>
|
|
90
|
-
<Table.Cell style={{ paddingLeft: "2em" }}>
|
|
91
|
-
<StatusPill status={event.status} />
|
|
92
|
-
</Table.Cell>
|
|
93
|
-
<Table.Cell>
|
|
94
|
-
<DateTime value={event.inserted_at} />
|
|
95
|
-
</Table.Cell>
|
|
96
|
-
<Table.Cell>
|
|
97
|
-
<ResponseCell {...event} />
|
|
98
|
-
</Table.Cell>
|
|
99
|
-
</Table.Row>,
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
102
|
+
UploadJobModalContent.defaultProps = {
|
|
103
|
+
scope: "implementations",
|
|
104
|
+
};
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
};
|
|
106
|
+
export default function UploadJob({ scope }) {
|
|
107
|
+
const { id } = useParams();
|
|
108
|
+
const { data, loading } = useUploadJob(id);
|
|
109
|
+
const job = data?.data;
|
|
110
|
+
const { formatMessage } = useIntl();
|
|
111
|
+
const scopeConfig = getUploadJobScope(scope);
|
|
106
112
|
|
|
107
113
|
return (
|
|
108
114
|
<>
|
|
109
115
|
<UploadJobBreadcrumbs filename={job?.filename} scope={scope} />
|
|
110
|
-
<Segment>
|
|
116
|
+
<Segment className="upload-job-page">
|
|
111
117
|
<Header as="h2">
|
|
112
118
|
<Icon
|
|
113
119
|
circular
|
|
@@ -117,101 +123,19 @@ export default function UploadJob({ scope }) {
|
|
|
117
123
|
})}
|
|
118
124
|
/>
|
|
119
125
|
<Header.Content>
|
|
120
|
-
<FormattedMessage id={
|
|
126
|
+
<FormattedMessage id={scopeConfig.detailHeaderId} />
|
|
121
127
|
</Header.Content>
|
|
122
128
|
</Header>
|
|
123
|
-
<
|
|
124
|
-
<Segment attached="bottom">
|
|
125
|
-
{job ? (
|
|
126
|
-
<>
|
|
127
|
-
<div
|
|
128
|
-
style={{
|
|
129
|
-
display: "flex",
|
|
130
|
-
alignItems: "center",
|
|
131
|
-
gap: "1.5em",
|
|
132
|
-
marginBottom: "1.5em",
|
|
133
|
-
flexWrap: "wrap",
|
|
134
|
-
}}
|
|
135
|
-
>
|
|
136
|
-
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
137
|
-
<span style={{ fontWeight: 600, fontSize: "1.3em" }}>
|
|
138
|
-
{job.filename}
|
|
139
|
-
</span>
|
|
140
|
-
<span style={{ color: "gray", fontSize: "0.7em" }}>
|
|
141
|
-
{job.hash}
|
|
142
|
-
</span>
|
|
143
|
-
</div>
|
|
144
|
-
<span>
|
|
145
|
-
<Label basic size="small" style={{ marginRight: 4 }}>
|
|
146
|
-
<FormattedMessage
|
|
147
|
-
id="uploadJob.table.status"
|
|
148
|
-
defaultMessage="Status"
|
|
149
|
-
/>
|
|
150
|
-
</Label>
|
|
151
|
-
<StatusPill status={job.latest_status} />
|
|
152
|
-
</span>
|
|
153
|
-
<span>
|
|
154
|
-
<Label basic size="small" style={{ marginRight: 4 }}>
|
|
155
|
-
<FormattedMessage
|
|
156
|
-
id="uploadJob.table.inserted_at"
|
|
157
|
-
defaultMessage="Inserted At"
|
|
158
|
-
/>
|
|
159
|
-
</Label>
|
|
160
|
-
<DateTime value={job.latest_event_at} />
|
|
161
|
-
</span>
|
|
162
|
-
</div>
|
|
163
|
-
<Table>
|
|
164
|
-
<Table.Header>
|
|
165
|
-
<Table.Row>
|
|
166
|
-
<Table.HeaderCell>
|
|
167
|
-
<FormattedMessage
|
|
168
|
-
id="uploadJob.table.status"
|
|
169
|
-
defaultMessage="Status"
|
|
170
|
-
/>
|
|
171
|
-
</Table.HeaderCell>
|
|
172
|
-
<Table.HeaderCell>
|
|
173
|
-
<FormattedMessage
|
|
174
|
-
id="uploadJob.table.inserted_at"
|
|
175
|
-
defaultMessage="Inserted At"
|
|
176
|
-
/>
|
|
177
|
-
</Table.HeaderCell>
|
|
178
|
-
<Table.HeaderCell>
|
|
179
|
-
<FormattedMessage
|
|
180
|
-
id="uploadJob.table.detail"
|
|
181
|
-
defaultMessage="Detail"
|
|
182
|
-
/>
|
|
183
|
-
</Table.HeaderCell>
|
|
184
|
-
</Table.Row>
|
|
185
|
-
</Table.Header>
|
|
186
|
-
<Table.Body>
|
|
187
|
-
{_.isArray(job.events) && !_.isEmpty(job.events) ? (
|
|
188
|
-
Object.entries(groupedEvents)
|
|
189
|
-
.map(([status, events]) =>
|
|
190
|
-
renderEventGroup(status, events),
|
|
191
|
-
)
|
|
192
|
-
.flat()
|
|
193
|
-
) : (
|
|
194
|
-
<Table.Row>
|
|
195
|
-
<Table.Cell colSpan={3}>
|
|
196
|
-
<FormattedMessage
|
|
197
|
-
id="uploadJob.table.no_events"
|
|
198
|
-
defaultMessage="No events found."
|
|
199
|
-
/>
|
|
200
|
-
</Table.Cell>
|
|
201
|
-
</Table.Row>
|
|
202
|
-
)}
|
|
203
|
-
</Table.Body>
|
|
204
|
-
</Table>
|
|
205
|
-
</>
|
|
206
|
-
) : null}
|
|
207
|
-
{loading ? (
|
|
208
|
-
<Dimmer active inverted>
|
|
209
|
-
<Loader />
|
|
210
|
-
</Dimmer>
|
|
211
|
-
) : null}
|
|
212
|
-
</Segment>
|
|
213
|
-
</Dimmer.Dimmable>
|
|
129
|
+
<UploadJobContent job={job} loading={loading} scope={scope} />
|
|
214
130
|
</Segment>
|
|
215
131
|
</>
|
|
216
132
|
);
|
|
217
133
|
}
|
|
134
|
+
|
|
135
|
+
UploadJob.propTypes = {
|
|
136
|
+
scope: PropTypes.string,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
UploadJob.defaultProps = {
|
|
140
|
+
scope: "implementations",
|
|
141
|
+
};
|
|
@@ -2,27 +2,23 @@ import PropTypes from "prop-types";
|
|
|
2
2
|
import { Breadcrumb } from "semantic-ui-react";
|
|
3
3
|
import { Link } from "react-router";
|
|
4
4
|
import { FormattedMessage } from "react-intl";
|
|
5
|
-
import {
|
|
6
|
-
IMPLEMENTATIONS_UPLOAD_JOBS,
|
|
7
|
-
STRUCTURE_NOTES_UPLOAD_JOBS,
|
|
8
|
-
} from "@truedat/core/routes";
|
|
5
|
+
import { getUploadJobScope } from "./UploadJobScope";
|
|
9
6
|
|
|
10
7
|
export const UploadJobBreadcrumbs = ({ filename, scope }) => {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
const scopeConfig = getUploadJobScope(scope);
|
|
9
|
+
const uploadJobsSection = scopeConfig.listPath ? (
|
|
10
|
+
<Breadcrumb.Section as={Link} to={scopeConfig.listPath}>
|
|
11
|
+
<FormattedMessage id={scopeConfig.listHeaderId} />
|
|
12
|
+
</Breadcrumb.Section>
|
|
13
|
+
) : (
|
|
14
|
+
<Breadcrumb.Section>
|
|
15
|
+
<FormattedMessage id={scopeConfig.listHeaderId} />
|
|
16
|
+
</Breadcrumb.Section>
|
|
17
|
+
);
|
|
18
|
+
|
|
21
19
|
return (
|
|
22
20
|
<Breadcrumb>
|
|
23
|
-
|
|
24
|
-
<FormattedMessage id={`uploadJobs.${scope}.header`} />
|
|
25
|
-
</Breadcrumb.Section>
|
|
21
|
+
{uploadJobsSection}
|
|
26
22
|
{filename ? (
|
|
27
23
|
<>
|
|
28
24
|
<Breadcrumb.Divider icon="right angle" />
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { FormattedMessage } from "react-intl";
|
|
5
|
+
import { getUploadJobScope } from "./UploadJobScope";
|
|
6
|
+
|
|
7
|
+
export const ErrorDetailsDisclosure = ({ details }) => {
|
|
8
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
9
|
+
const detailsId = useId();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="upload-job-change-details upload-job-error-details">
|
|
13
|
+
<button
|
|
14
|
+
type="button"
|
|
15
|
+
className="upload-job-change-toggle upload-job-error-toggle"
|
|
16
|
+
aria-controls={detailsId}
|
|
17
|
+
aria-expanded={isExpanded}
|
|
18
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
19
|
+
>
|
|
20
|
+
<span aria-hidden="true">{isExpanded ? "▼" : "▶"}</span>
|
|
21
|
+
<FormattedMessage
|
|
22
|
+
id="uploadJob.results.errors.errorFields"
|
|
23
|
+
values={{ count: details.length }}
|
|
24
|
+
/>
|
|
25
|
+
</button>
|
|
26
|
+
<div
|
|
27
|
+
id={detailsId}
|
|
28
|
+
className="upload-job-change-table-wrap"
|
|
29
|
+
hidden={!isExpanded}
|
|
30
|
+
>
|
|
31
|
+
{isExpanded ? (
|
|
32
|
+
<table className="upload-job-change-table upload-job-error-table">
|
|
33
|
+
<thead>
|
|
34
|
+
<tr>
|
|
35
|
+
<th>
|
|
36
|
+
<FormattedMessage id="uploadJob.results.errors.field" />
|
|
37
|
+
</th>
|
|
38
|
+
<th>
|
|
39
|
+
<FormattedMessage id="uploadJob.results.errors.error" />
|
|
40
|
+
</th>
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
{details.map(([field, error], index) => (
|
|
45
|
+
<tr key={`${field ?? "message"}-${index}`}>
|
|
46
|
+
<th scope="row">
|
|
47
|
+
{field ?? (
|
|
48
|
+
<FormattedMessage id="uploadJob.parser.detail.message" />
|
|
49
|
+
)}
|
|
50
|
+
</th>
|
|
51
|
+
<td>{error}</td>
|
|
52
|
+
</tr>
|
|
53
|
+
))}
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
ErrorDetailsDisclosure.propTypes = {
|
|
63
|
+
details: PropTypes.arrayOf(PropTypes.array),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
ErrorDetailsDisclosure.defaultProps = {
|
|
67
|
+
details: [],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const isChangePair = (value) =>
|
|
71
|
+
_.isObject(value) &&
|
|
72
|
+
(Object.prototype.hasOwnProperty.call(value, "old") ||
|
|
73
|
+
Object.prototype.hasOwnProperty.call(value, "new"));
|
|
74
|
+
|
|
75
|
+
export const flattenChanges = (changes, parent = "") =>
|
|
76
|
+
_.toPairs(changes).flatMap(([key, value]) => {
|
|
77
|
+
const path = parent ? `${parent}.${key}` : key;
|
|
78
|
+
if (isChangePair(value)) {
|
|
79
|
+
return [{ key: path, oldValue: value.old, newValue: value.new }];
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
_.isObject(value) &&
|
|
83
|
+
!_.isArray(value) &&
|
|
84
|
+
(key === "df_content" || containsChangePair(value))
|
|
85
|
+
) {
|
|
86
|
+
return flattenChanges(value, path);
|
|
87
|
+
}
|
|
88
|
+
return [{ key: path, oldValue: undefined, newValue: value }];
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const containsChangePair = (value) =>
|
|
92
|
+
_.isObject(value) &&
|
|
93
|
+
(isChangePair(value) ||
|
|
94
|
+
_.toPairs(value).some(([, nestedValue]) =>
|
|
95
|
+
containsChangePair(nestedValue),
|
|
96
|
+
));
|
|
97
|
+
|
|
98
|
+
export const ChangeField = ({ path, scope }) => {
|
|
99
|
+
const [root, ...nested] = path.split(".");
|
|
100
|
+
const field = nested.at(-1) ?? root;
|
|
101
|
+
const config = getUploadJobScope(scope);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<FormattedMessage
|
|
105
|
+
id={
|
|
106
|
+
config.fieldMessageOverrides?.[field] ??
|
|
107
|
+
`${config.fieldMessagePrefix}.${field}`
|
|
108
|
+
}
|
|
109
|
+
defaultMessage={field}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
ChangeField.propTypes = {
|
|
115
|
+
path: PropTypes.string.isRequired,
|
|
116
|
+
scope: PropTypes.string,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
ChangeField.defaultProps = {
|
|
120
|
+
scope: "implementations",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const formatValue = (value, key) => {
|
|
124
|
+
if (_.has("value")(value)) return formatValue(value.value, key);
|
|
125
|
+
if (_.isArray(value)) {
|
|
126
|
+
return _.flow(
|
|
127
|
+
_.map((v) => formatValue(v, key)),
|
|
128
|
+
_.join(", "),
|
|
129
|
+
)(value);
|
|
130
|
+
}
|
|
131
|
+
if (_.isObject(value)) {
|
|
132
|
+
if (_.has("url_value")(value))
|
|
133
|
+
return `[${value.url_name}] (${value.url_value})`;
|
|
134
|
+
if (_.has("name")(value)) return value.name;
|
|
135
|
+
if (_.has("external_id")(value)) return value.external_id;
|
|
136
|
+
return _.flow(_.keys, _.join(", "))(value);
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const ChangeValue = ({ className, value }) => {
|
|
142
|
+
const displayValue = value == null ? "—" : String(formatValue(value));
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<span className={className} title={displayValue}>
|
|
146
|
+
{displayValue}
|
|
147
|
+
</span>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
ChangeValue.propTypes = {
|
|
152
|
+
className: PropTypes.string.isRequired,
|
|
153
|
+
value: PropTypes.oneOfType([
|
|
154
|
+
PropTypes.string,
|
|
155
|
+
PropTypes.number,
|
|
156
|
+
PropTypes.bool,
|
|
157
|
+
PropTypes.array,
|
|
158
|
+
PropTypes.object,
|
|
159
|
+
]),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
ChangeValue.defaultProps = {
|
|
163
|
+
value: undefined,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const ChangesDetail = ({ changes, changeDetails, scope }) => {
|
|
167
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
168
|
+
const detailsId = useId();
|
|
169
|
+
const details = changeDetails ?? changes;
|
|
170
|
+
|
|
171
|
+
if (!details) return null;
|
|
172
|
+
|
|
173
|
+
const changesList = flattenChanges(details);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div className="upload-job-change-details">
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
className="upload-job-change-toggle"
|
|
180
|
+
aria-controls={detailsId}
|
|
181
|
+
aria-expanded={isExpanded}
|
|
182
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
183
|
+
>
|
|
184
|
+
<span aria-hidden="true">{isExpanded ? "▼" : "▶"}</span>
|
|
185
|
+
<FormattedMessage
|
|
186
|
+
id="uploadJob.results.changes.modifiedFields"
|
|
187
|
+
values={{ count: changesList.length }}
|
|
188
|
+
/>
|
|
189
|
+
</button>
|
|
190
|
+
<div
|
|
191
|
+
id={detailsId}
|
|
192
|
+
className="upload-job-change-table-wrap"
|
|
193
|
+
hidden={!isExpanded}
|
|
194
|
+
>
|
|
195
|
+
{isExpanded ? (
|
|
196
|
+
<table className="upload-job-change-table">
|
|
197
|
+
<thead>
|
|
198
|
+
<tr>
|
|
199
|
+
<th>
|
|
200
|
+
<FormattedMessage id="uploadJob.results.changes.field" />
|
|
201
|
+
</th>
|
|
202
|
+
<th>
|
|
203
|
+
<FormattedMessage id="uploadJob.results.changes.previous" />
|
|
204
|
+
</th>
|
|
205
|
+
<th>
|
|
206
|
+
<span className="upload-job-visually-hidden">
|
|
207
|
+
<FormattedMessage id="uploadJob.results.changes.change" />
|
|
208
|
+
</span>
|
|
209
|
+
</th>
|
|
210
|
+
<th>
|
|
211
|
+
<FormattedMessage id="uploadJob.results.changes.new" />
|
|
212
|
+
</th>
|
|
213
|
+
</tr>
|
|
214
|
+
</thead>
|
|
215
|
+
<tbody>
|
|
216
|
+
{changesList.map(({ key, oldValue, newValue }) => (
|
|
217
|
+
<tr key={key}>
|
|
218
|
+
<th scope="row">
|
|
219
|
+
<ChangeField path={key} scope={scope} />
|
|
220
|
+
</th>
|
|
221
|
+
<td>
|
|
222
|
+
<ChangeValue
|
|
223
|
+
className="upload-job-old-value upload-job-change-value"
|
|
224
|
+
value={oldValue}
|
|
225
|
+
/>
|
|
226
|
+
</td>
|
|
227
|
+
<td className="upload-job-change-arrow">
|
|
228
|
+
<span aria-hidden="true">→</span>
|
|
229
|
+
</td>
|
|
230
|
+
<td>
|
|
231
|
+
<ChangeValue
|
|
232
|
+
className="upload-job-new-value upload-job-change-value"
|
|
233
|
+
value={newValue}
|
|
234
|
+
/>
|
|
235
|
+
</td>
|
|
236
|
+
</tr>
|
|
237
|
+
))}
|
|
238
|
+
</tbody>
|
|
239
|
+
</table>
|
|
240
|
+
) : null}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
ChangesDetail.propTypes = {
|
|
247
|
+
changeDetails: PropTypes.object,
|
|
248
|
+
changes: PropTypes.object,
|
|
249
|
+
scope: PropTypes.string,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
ChangesDetail.defaultProps = {
|
|
253
|
+
changeDetails: undefined,
|
|
254
|
+
changes: undefined,
|
|
255
|
+
scope: "implementations",
|
|
256
|
+
};
|