@truedat/core 8.12.12 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "8.12.12",
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.12.12",
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": "b6e3413dae81b2e28e98a21cf371174caa30ab4b"
102
+ "gitHead": "09ce6d3a8693835de75577e43d99d86addb2edf7"
103
103
  }
@@ -0,0 +1,137 @@
1
+ import { useId } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { useIntl, FormattedMessage } from "react-intl";
4
+ import { Button, Icon, Modal } from "semantic-ui-react";
5
+ import _ from "lodash/fp";
6
+
7
+ const bannerIcons = {
8
+ warning: "warning sign",
9
+ danger: "times circle",
10
+ success: "check circle",
11
+ info: "info circle",
12
+ };
13
+
14
+ export const TrueModal = ({
15
+ title,
16
+ subtitle,
17
+ icon,
18
+ headerColor,
19
+ headerTextColor,
20
+ children,
21
+ banner,
22
+ bannerType = "warning",
23
+ bannerIcon,
24
+ actions,
25
+ footer,
26
+ onCancel,
27
+ onCreate,
28
+ className,
29
+ ...modalProps
30
+ }) => {
31
+ const titleId = useId();
32
+ const { formatMessage } = useIntl();
33
+ const defaultActions = _.compact([
34
+ onCancel && {
35
+ key: "cancel",
36
+ content: <FormattedMessage id="actions.cancel" defaultMessage="Cancel" />,
37
+ secondary: true,
38
+ onClick: onCancel,
39
+ },
40
+ onCreate && {
41
+ key: "create",
42
+ content: <FormattedMessage id="actions.create" defaultMessage="Create" />,
43
+ primary: true,
44
+ onClick: onCreate,
45
+ },
46
+ ]);
47
+ const resolvedActions = actions?.length ? actions : defaultActions;
48
+
49
+ return (
50
+ <Modal
51
+ {..._.omit(["header", "content", "closeIcon"])(modalProps)}
52
+ className={_.compact(["true-modal", className]).join(" ")}
53
+ role="dialog"
54
+ aria-labelledby={
55
+ modalProps["aria-labelledby"] ||
56
+ (modalProps["aria-label"] ? undefined : titleId)
57
+ }
58
+ closeIcon={
59
+ <button
60
+ type="button"
61
+ className="true-modal-close"
62
+ style={{ color: headerTextColor }}
63
+ aria-label={formatMessage({
64
+ id: "actions.close",
65
+ defaultMessage: "Close",
66
+ })}
67
+ >
68
+ &times;
69
+ </button>
70
+ }
71
+ >
72
+ <Modal.Header style={{ background: headerColor, color: headerTextColor }}>
73
+ {icon ? <Icon name={icon} size="large" /> : null}
74
+ <div className="true-modal-heading">
75
+ <span id={titleId}>{title}</span>
76
+ {subtitle != null ? (
77
+ <p className="true-modal-subtitle">{subtitle}</p>
78
+ ) : null}
79
+ </div>
80
+ </Modal.Header>
81
+ <Modal.Content className="true-modal-content">{children}</Modal.Content>
82
+ {banner != null && banner !== false ? (
83
+ <div className={`true-modal-banner ${bannerType}`}>
84
+ <Icon name={bannerIcon || bannerIcons[bannerType]} />
85
+ <div>{banner}</div>
86
+ </div>
87
+ ) : null}
88
+ {footer != null ? (
89
+ footer
90
+ ) : resolvedActions.length ? (
91
+ <Modal.Actions className="true-modal-actions">
92
+ {resolvedActions.map(({ key, disabled, loading, ...buttonProps }) => (
93
+ <Button
94
+ key={key}
95
+ type="button"
96
+ {...buttonProps}
97
+ disabled={disabled || loading}
98
+ loading={loading}
99
+ />
100
+ ))}
101
+ </Modal.Actions>
102
+ ) : null}
103
+ </Modal>
104
+ );
105
+ };
106
+
107
+ TrueModal.propTypes = {
108
+ title: PropTypes.node.isRequired,
109
+ subtitle: PropTypes.node,
110
+ icon: PropTypes.string,
111
+ headerColor: PropTypes.string,
112
+ headerTextColor: PropTypes.string,
113
+ children: PropTypes.node,
114
+ banner: PropTypes.node,
115
+ bannerType: PropTypes.oneOf(["warning", "danger", "success", "info"]),
116
+ bannerIcon: PropTypes.string,
117
+ actions: PropTypes.arrayOf(
118
+ PropTypes.shape({
119
+ key: PropTypes.string.isRequired,
120
+ content: PropTypes.node,
121
+ onClick: PropTypes.func,
122
+ disabled: PropTypes.bool,
123
+ loading: PropTypes.bool,
124
+ primary: PropTypes.bool,
125
+ secondary: PropTypes.bool,
126
+ negative: PropTypes.bool,
127
+ type: PropTypes.string,
128
+ "aria-label": PropTypes.string,
129
+ }),
130
+ ),
131
+ footer: PropTypes.node,
132
+ onCancel: PropTypes.func,
133
+ onCreate: PropTypes.func,
134
+ className: PropTypes.string,
135
+ };
136
+
137
+ export default TrueModal;
@@ -1,113 +1,119 @@
1
- import _ from "lodash/fp";
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
- Button,
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 default function UploadJob({ scope }) {
21
- const { id } = useParams();
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
- const { formatMessage } = useIntl();
26
- const [expandedGroups, setExpandedGroups] = useState({});
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
- // Group events by status
29
- const groupedEvents = job?.events ? _.groupBy("status", job.events) : {};
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
- const toggleGroup = (status) => {
32
- setExpandedGroups((prev) => ({
33
- ...prev,
34
- [status]: !prev[status],
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
- const renderEventGroup = (status, events) => {
39
- const isExpanded = expandedGroups[status];
40
- const shouldCollapse = status === "INFO" || status === "ERROR";
77
+ UploadJobContent.defaultProps = {
78
+ constrainResults: false,
79
+ job: undefined,
80
+ loading: false,
81
+ scope: "implementations",
82
+ };
41
83
 
42
- if (!shouldCollapse) {
43
- // Render non-collapsible events normally
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
- // Render collapsible groups for INFO and ERROR
60
- const groupRows = [];
87
+ return (
88
+ <UploadJobContent
89
+ constrainResults
90
+ job={data?.data}
91
+ loading={loading}
92
+ scope={scope}
93
+ />
94
+ );
95
+ };
61
96
 
62
- // Add group header row
63
- groupRows.push(
64
- <Table.Row
65
- key={`${status}-header`}
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
- // Add expanded events if group is expanded
86
- if (isExpanded) {
87
- events.forEach((event, idx) => {
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
- return groupRows;
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={`uploadJob.${scope}.header`} />
126
+ <FormattedMessage id={scopeConfig.detailHeaderId} />
121
127
  </Header.Content>
122
128
  </Header>
123
- <Dimmer.Dimmable dimmed={loading}>
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 uploadJobsPath = (scope) => {
12
- switch (scope) {
13
- case "implementations":
14
- return IMPLEMENTATIONS_UPLOAD_JOBS;
15
- case "notes":
16
- return STRUCTURE_NOTES_UPLOAD_JOBS;
17
- default:
18
- return null;
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
- <Breadcrumb.Section as={Link} to={uploadJobsPath(scope)}>
24
- <FormattedMessage id={`uploadJobs.${scope}.header`} />
25
- </Breadcrumb.Section>
21
+ {uploadJobsSection}
26
22
  {filename ? (
27
23
  <>
28
24
  <Breadcrumb.Divider icon="right angle" />