@truedat/core 8.13.4 → 8.13.5

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.13.4",
3
+ "version": "8.13.5",
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.4",
60
+ "@truedat/test": "8.13.5",
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": "4ec142f1c856aa669b232b12250c800a19ab9ace"
102
+ "gitHead": "9b87cbb7143f20b305b84077c1fcf004e5b27726"
103
103
  }
@@ -15,7 +15,7 @@ import {
15
15
  CONCEPTS_DEPRECATED,
16
16
  CONCEPT_LINKS_MANAGEMENT,
17
17
  CONCEPT_LINKS_APPROVALS,
18
- CONCEPTS_BULK_UPLOAD_EVENTS,
18
+ CONCEPTS_UPLOAD_JOBS,
19
19
  CONCEPTS_SUBSCOPE,
20
20
  CONCEPTS_SIDEMENU_SUBSCOPE,
21
21
  CONCEPTS_SIDEMENU_SUBSCOPE_DEPRECATED,
@@ -101,9 +101,9 @@ function buildMainConceptsSubmenuItems(
101
101
  };
102
102
  const manageUploads = {
103
103
  name: "concepts_upload_events",
104
- routes: [CONCEPTS_BULK_UPLOAD_EVENTS],
104
+ routes: [CONCEPTS_UPLOAD_JOBS],
105
105
  groups: ["business_glossary_management"],
106
- isActive: () => pathname === CONCEPTS_BULK_UPLOAD_EVENTS,
106
+ isActive: () => pathname === CONCEPTS_UPLOAD_JOBS,
107
107
  };
108
108
 
109
109
  return [
@@ -171,7 +171,7 @@ function buildManagementSubmenuItems() {
171
171
  },
172
172
  {
173
173
  name: "concepts_upload_events",
174
- routes: [CONCEPTS_BULK_UPLOAD_EVENTS],
174
+ routes: [CONCEPTS_UPLOAD_JOBS],
175
175
  groups: ["business_glossary_management"],
176
176
  },
177
177
  ];
@@ -168,7 +168,7 @@ export const ChangesDetail = ({ changes, changeDetails, scope }) => {
168
168
  const detailsId = useId();
169
169
  const details = changeDetails ?? changes;
170
170
 
171
- if (!details) return null;
171
+ if (!details || _.isEmpty(details)) return null;
172
172
 
173
173
  const changesList = flattenChanges(details);
174
174
 
@@ -1,9 +1,11 @@
1
1
  import _ from "lodash/fp";
2
2
  import PropTypes from "prop-types";
3
+ import { Link } from "react-router";
3
4
  import { FormattedMessage, useIntl } from "react-intl";
4
5
  import Label from "semantic-ui-react/dist/commonjs/elements/Label";
5
6
  import RuleImplementationLink from "@truedat/dq/components/RuleImplementationLink";
6
7
  import StructureNoteLink from "@truedat/dd/components/StructureNoteLink";
8
+ import { linkTo } from "../routes";
7
9
  import { ChangesDetail, ErrorDetailsDisclosure } from "./UploadJobDetails";
8
10
  import { classifyUploadJobErrorMessage } from "./UploadJobSummary";
9
11
 
@@ -69,6 +71,19 @@ export const ResponseCell = ({
69
71
  }
70
72
  };
71
73
 
74
+ const ConceptLink = ({ business_concept_id, id, name }) => (
75
+ <Link to={linkTo.CONCEPT_VERSION({ business_concept_id, id })}>{name}</Link>
76
+ );
77
+
78
+ ConceptLink.propTypes = {
79
+ business_concept_id: PropTypes.oneOfType([
80
+ PropTypes.number,
81
+ PropTypes.string,
82
+ ]),
83
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
84
+ name: PropTypes.string,
85
+ };
86
+
72
87
  export const ResultEntityCell = ({ response, status }) => {
73
88
  if (!response?.details || !["INFO", "ERROR"].includes(status)) return null;
74
89
 
@@ -78,6 +93,8 @@ export const ResultEntityCell = ({ response, status }) => {
78
93
 
79
94
  return response.details.implementation_key != null ? (
80
95
  <RuleImplementationLink {...response.details} />
96
+ ) : response.details.business_concept_id != null ? (
97
+ <ConceptLink {...response.details} />
81
98
  ) : status === "INFO" &&
82
99
  (response.details.data_structure_id != null ||
83
100
  response.details.external_id != null) ? (
@@ -113,6 +130,11 @@ ResultEntityCell.propTypes = {
113
130
  PropTypes.number,
114
131
  PropTypes.string,
115
132
  ]),
133
+ business_concept_id: PropTypes.oneOfType([
134
+ PropTypes.number,
135
+ PropTypes.string,
136
+ ]),
137
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
116
138
  name: PropTypes.string,
117
139
  }),
118
140
  }),
@@ -204,6 +226,51 @@ const ErrorDetail = ({ response, showOrigin }) => {
204
226
  );
205
227
  }
206
228
 
229
+ if (!showOrigin && type === "name_not_available") {
230
+ const name = response.details?.name;
231
+
232
+ return (
233
+ <span className="upload-job-simple-error">
234
+ <FormattedMessage id="uploadJob.parser.error.name_not_available" />
235
+ {name != null ? `: ${name}` : null}
236
+ </span>
237
+ );
238
+ }
239
+
240
+ if (!showOrigin && type === "unexpected_error") {
241
+ const { type: errorType } = classifyUploadJobErrorMessage(
242
+ response.details?.message,
243
+ );
244
+
245
+ if (errorType === "unexpected_error") {
246
+ return (
247
+ <span className="upload-job-simple-error">
248
+ <FormattedMessage id="uploadJob.parser.error.unexpected_error" />
249
+ </span>
250
+ );
251
+ }
252
+ }
253
+
254
+ const validationErrorPairs = (errors) => {
255
+ if (_.isArray(errors)) {
256
+ return errors.map((e) => [
257
+ e?.field ?? formatMessage({ id: `${messagePrefix}.message` }),
258
+ e?.message ?? "",
259
+ ]);
260
+ }
261
+
262
+ if (_.isObject(errors)) {
263
+ return Object.entries(errors).flatMap(([field, messages]) =>
264
+ (_.isArray(messages) ? messages : [messages]).map((message) => [
265
+ field,
266
+ message ?? "",
267
+ ]),
268
+ );
269
+ }
270
+
271
+ return null;
272
+ };
273
+
207
274
  const detailBuilders = {
208
275
  missing_required_headers: (details) => [
209
276
  [
@@ -276,10 +343,9 @@ const ErrorDetail = ({ response, showOrigin }) => {
276
343
  [null, externalIdDetailValue(details)],
277
344
  ].filter(([, v]) => v !== undefined && v !== ""),
278
345
  field_validation_error: (details) =>
279
- details?.errors?.map((e) => [
280
- e?.field ?? formatMessage({ id: `${messagePrefix}.message` }),
281
- e?.message ?? "",
282
- ]) ?? [[null, externalIdDetailValue(details)]],
346
+ validationErrorPairs(details?.errors) ?? [
347
+ [null, externalIdDetailValue(details)],
348
+ ],
283
349
  unprocessable_entity: (details) => [[null, externalIdDetailValue(details)]],
284
350
  structure_note_creation_error: (details) => {
285
351
  const reason = details?.reason ?? details?.error;
@@ -289,12 +355,10 @@ const ErrorDetail = ({ response, showOrigin }) => {
289
355
  ? `structures.uploadStructures.failed.note.${reason}`
290
356
  : null;
291
357
 
292
- const errorPairs = details?.errors?.length
293
- ? details.errors.map((e) => [
294
- e?.field ?? formatMessage({ id: `${messagePrefix}.message` }),
295
- e?.message ?? "",
296
- ])
297
- : [
358
+ const validationPairs = validationErrorPairs(details?.errors);
359
+
360
+ const errorPairs = _.isEmpty(validationPairs)
361
+ ? [
298
362
  [
299
363
  formatMessage({ id: `${messagePrefix}.reason` }),
300
364
  reasonMessageId
@@ -304,7 +368,8 @@ const ErrorDetail = ({ response, showOrigin }) => {
304
368
  })
305
369
  : String(reason ?? ""),
306
370
  ],
307
- ];
371
+ ]
372
+ : validationPairs;
308
373
 
309
374
  const externalIdPair = details?.external_id
310
375
  ? [[null, externalIdDetailValue(details)]]
@@ -1,10 +1,19 @@
1
1
  import {
2
+ CONCEPTS_UPLOAD_JOBS,
2
3
  IMPLEMENTATIONS_UPLOAD_JOBS,
3
4
  STRUCTURE_NOTES_UPLOAD_JOBS,
4
5
  linkTo,
5
6
  } from "@truedat/core/routes";
6
7
 
7
8
  export const uploadJobScopes = {
9
+ concepts: {
10
+ listPath: CONCEPTS_UPLOAD_JOBS,
11
+ detailPath: (id) => linkTo.CONCEPTS_UPLOAD_JOB({ id }),
12
+ listHeaderId: "uploadJobs.concepts.header",
13
+ detailHeaderId: "uploadJob.concepts.header",
14
+ entityColumnMessageId: "uploadJob.results.column.concept",
15
+ fieldMessagePrefix: "concepts.props",
16
+ },
8
17
  implementations: {
9
18
  listPath: IMPLEMENTATIONS_UPLOAD_JOBS,
10
19
  detailPath: (id) => linkTo.IMPLEMENTATIONS_UPLOAD_JOB({ id }),
@@ -52,7 +52,8 @@ export const classifyUploadJobErrorMessage = (message) => {
52
52
  const isTechnicalError =
53
53
  /^%[A-Z][A-Za-z0-9_.]*\{/.test(normalizedMessage) ||
54
54
  /^\{:/.test(normalizedMessage) ||
55
- /^\*\* \(/.test(normalizedMessage);
55
+ /^\*\* \(/.test(normalizedMessage) ||
56
+ /^protocol \S+ not implemented/.test(normalizedMessage);
56
57
 
57
58
  return isTechnicalError
58
59
  ? { type: "unexpected_error" }
@@ -37,4 +37,14 @@ describe("ImplementationUploadJobBreadcrumbs", () => {
37
37
  expect(rendered.getByText(filename)).toBeInTheDocument();
38
38
  expect(rendered.container).toMatchSnapshot();
39
39
  });
40
+
41
+ it("renders breadcrumbs for concepts scope", async () => {
42
+ const rendered = render(<UploadJobBreadcrumbs scope="concepts" />);
43
+ await waitForLoad(rendered);
44
+
45
+ expect(
46
+ rendered.getByText(/uploadJobs.concepts.header/i),
47
+ ).toBeInTheDocument();
48
+ expect(rendered.container).toMatchSnapshot();
49
+ });
40
50
  });
@@ -32,6 +32,10 @@ jest.mock("@truedat/dd/components/StructureNoteLink", () => ({
32
32
  );
33
33
  },
34
34
  }));
35
+ jest.mock("react-router", () => ({
36
+ ...jest.requireActual("react-router"),
37
+ Link: ({ to, children }) => <a href={to}>{children}</a>,
38
+ }));
35
39
 
36
40
  const changeMessages = {
37
41
  en: {
@@ -50,6 +54,8 @@ const changeMessages = {
50
54
  "uploadJob.parser.error.field_validation_error": "Field validation error",
51
55
  "uploadJob.parser.error.data_structure_not_found":
52
56
  "Data structure not found",
57
+ "uploadJob.parser.error.unexpected_error": "Unexpected error",
58
+ "uploadJob.parser.error.name_not_available": "Name not available",
53
59
  "uploadJob.parser.detail.message": "Message",
54
60
  },
55
61
  };
@@ -99,6 +105,98 @@ describe("<ResultEntityCell />", () => {
99
105
  );
100
106
  });
101
107
 
108
+ it("renders INFO concept links from row details", () => {
109
+ const rendered = render(
110
+ <ResultEntityCell
111
+ status="INFO"
112
+ response={{
113
+ details: {
114
+ id: 5,
115
+ business_concept_id: 3,
116
+ name: "My Concept",
117
+ },
118
+ }}
119
+ />,
120
+ );
121
+ expect(rendered.getByRole("link", { name: "My Concept" })).toHaveAttribute(
122
+ "href",
123
+ "/concepts/3/versions/5",
124
+ );
125
+ });
126
+
127
+ it("renders ERROR concept links from row details", () => {
128
+ const rendered = render(
129
+ <ResultEntityCell
130
+ status="ERROR"
131
+ response={{
132
+ type: "name_not_available",
133
+ details: {
134
+ id: 5,
135
+ business_concept_id: 3,
136
+ name: "My Concept",
137
+ },
138
+ }}
139
+ />,
140
+ );
141
+ expect(rendered.getByRole("link", { name: "My Concept" })).toHaveAttribute(
142
+ "href",
143
+ "/concepts/3/versions/5",
144
+ );
145
+ });
146
+
147
+ it("renders concept links for unexpected_error rows with entity data", () => {
148
+ const rendered = render(
149
+ <ResultEntityCell
150
+ status="ERROR"
151
+ response={{
152
+ type: "unexpected_error",
153
+ details: {
154
+ id: 5,
155
+ business_concept_id: 3,
156
+ name: "My Concept",
157
+ },
158
+ }}
159
+ />,
160
+ );
161
+ expect(rendered.getByRole("link", { name: "My Concept" })).toHaveAttribute(
162
+ "href",
163
+ "/concepts/3/versions/5",
164
+ );
165
+ });
166
+
167
+ it("renders concept links for unchanged rows", () => {
168
+ const rendered = render(
169
+ <ResultEntityCell
170
+ status="INFO"
171
+ response={{
172
+ type: "unchanged",
173
+ details: {
174
+ id: 5,
175
+ business_concept_id: 3,
176
+ name: "My Concept",
177
+ },
178
+ }}
179
+ />,
180
+ );
181
+ expect(rendered.getByRole("link", { name: "My Concept" })).toHaveAttribute(
182
+ "href",
183
+ "/concepts/3/versions/5",
184
+ );
185
+ });
186
+
187
+ it("renders no entity for historical unchanged rows without entity data", () => {
188
+ const rendered = render(
189
+ <ResultEntityCell
190
+ status="INFO"
191
+ response={{
192
+ type: "unchanged",
193
+ details: { row_number: 3, sheet: "date template" },
194
+ }}
195
+ />,
196
+ );
197
+ expect(rendered.container).toBeEmptyDOMElement();
198
+ });
199
+
102
200
  it("renders implementation refs and preferred structure labels with external ids", () => {
103
201
  const implementation = render(
104
202
  <ResultEntityCell
@@ -512,6 +610,43 @@ describe("<ResponseCell />", () => {
512
610
  expect(rendered.getByText(/Already exists/i)).toBeInTheDocument();
513
611
  });
514
612
 
613
+ it("renders ERROR status with field_validation_error details as an object", () => {
614
+ const response = {
615
+ type: "field_validation_error",
616
+ details: {
617
+ id: 693,
618
+ name: "prueba2",
619
+ sheet: "termino",
620
+ errors: { adjuntos: ["is invalid"] },
621
+ row_number: 3,
622
+ business_concept_id: 337,
623
+ },
624
+ };
625
+ const rendered = render(
626
+ <ResponseCell response={response} status="ERROR" />,
627
+ );
628
+ expect(
629
+ rendered.getByText(/uploadJob.parser.error.field_validation_error/i),
630
+ ).toBeInTheDocument();
631
+ expect(rendered.getByText(/adjuntos/i)).toBeInTheDocument();
632
+ expect(rendered.getByText(/is invalid/i)).toBeInTheDocument();
633
+ });
634
+
635
+ it("renders ERROR status with field_validation_error object details with multiple messages", () => {
636
+ const response = {
637
+ type: "field_validation_error",
638
+ details: {
639
+ errors: { name: ["Required", "Too long"], type: "is invalid" },
640
+ },
641
+ };
642
+ const rendered = render(
643
+ <ResponseCell response={response} status="ERROR" />,
644
+ );
645
+ expect(rendered.getByText(/Required/i)).toBeInTheDocument();
646
+ expect(rendered.getByText(/Too long/i)).toBeInTheDocument();
647
+ expect(rendered.getByText(/is invalid/i)).toBeInTheDocument();
648
+ });
649
+
515
650
  it("renders ERROR status with deprecated_implementation details", () => {
516
651
  const response = {
517
652
  type: "deprecated_implementation",
@@ -706,6 +841,27 @@ describe("<ResponseCell />", () => {
706
841
  });
707
842
  });
708
843
 
844
+ it("renders no changes disclosure when change_details is empty", () => {
845
+ const rendered = render(
846
+ <ResponseCell
847
+ response={{
848
+ type: "updated",
849
+ details: {
850
+ change_details: {},
851
+ },
852
+ }}
853
+ status="INFO"
854
+ />,
855
+ {
856
+ messages: changeMessages,
857
+ },
858
+ );
859
+
860
+ expect(
861
+ rendered.queryByRole("button", { name: /modified fields?/i }),
862
+ ).not.toBeInTheDocument();
863
+ });
864
+
709
865
  it("renders boolean, null and zero change values without losing their values", async () => {
710
866
  const user = userEvent.setup({ delay: null });
711
867
  const rendered = render(
@@ -850,6 +1006,67 @@ describe("<ResultDetailCell /> errors", () => {
850
1006
  expect(rendered.queryByText("unknown-note")).not.toBeInTheDocument();
851
1007
  });
852
1008
 
1009
+ it("renders unexpected_error with empty details as a generic message only", () => {
1010
+ const rendered = render(
1011
+ <ResultDetailCell
1012
+ response={{ type: "unexpected_error", details: {} }}
1013
+ status="ERROR"
1014
+ />,
1015
+ { messages: changeMessages },
1016
+ );
1017
+
1018
+ expect(rendered.getByText("Unexpected error")).toBeInTheDocument();
1019
+ expect(rendered.queryByRole("button")).not.toBeInTheDocument();
1020
+ expect(
1021
+ rendered.queryByText(/protocol String\.Chars/),
1022
+ ).not.toBeInTheDocument();
1023
+ });
1024
+
1025
+ it("renders unexpected_error events with technical messages as a generic message only", () => {
1026
+ const technicalMessage =
1027
+ "protocol String.Chars not implemented for type Tuple. This protocol is implemented for the following type(s): Atom, BitString\n\nGot value:\n\n {:array, :map}\n";
1028
+
1029
+ const rendered = render(
1030
+ <ResultDetailCell
1031
+ response={{
1032
+ type: "unexpected_error",
1033
+ details: { message: technicalMessage },
1034
+ }}
1035
+ status="ERROR"
1036
+ />,
1037
+ { messages: changeMessages },
1038
+ );
1039
+
1040
+ expect(rendered.getByText("Unexpected error")).toBeInTheDocument();
1041
+ expect(rendered.queryByRole("button")).not.toBeInTheDocument();
1042
+ expect(
1043
+ rendered.queryByText(/protocol String\.Chars/),
1044
+ ).not.toBeInTheDocument();
1045
+ });
1046
+
1047
+ it("renders name_not_available with the conflicting name as a simple message", () => {
1048
+ const rendered = render(
1049
+ <ResultDetailCell
1050
+ response={{
1051
+ type: "name_not_available",
1052
+ details: {
1053
+ name: "TEST CREACION",
1054
+ row_number: 3,
1055
+ sheet: "date template",
1056
+ },
1057
+ }}
1058
+ status="ERROR"
1059
+ />,
1060
+ { messages: changeMessages },
1061
+ );
1062
+
1063
+ expect(
1064
+ rendered.getByText("Name not available: TEST CREACION"),
1065
+ ).toBeInTheDocument();
1066
+ expect(rendered.queryByRole("button")).not.toBeInTheDocument();
1067
+ expect(rendered.queryByText(/row_number/)).not.toBeInTheDocument();
1068
+ });
1069
+
853
1070
  it("uses the singular error count for one field", () => {
854
1071
  const rendered = render(
855
1072
  <ResultDetailCell
@@ -25,6 +25,23 @@ describe("Upload job scopes", () => {
25
25
  );
26
26
  });
27
27
 
28
+ it("defines concepts upload routes and presentation metadata", () => {
29
+ expect(uploadJobScopes.concepts.listPath).toBe("/concepts/uploadJobs");
30
+ expect(uploadJobScopes.concepts.listHeaderId).toBe(
31
+ "uploadJobs.concepts.header",
32
+ );
33
+ expect(uploadJobScopes.concepts.detailHeaderId).toBe(
34
+ "uploadJob.concepts.header",
35
+ );
36
+ expect(uploadJobScopes.concepts.detailPath(12)).toBe(
37
+ "/concepts/uploadJobs/12",
38
+ );
39
+ expect(uploadJobScopes.concepts.entityColumnMessageId).toBe(
40
+ "uploadJob.results.column.concept",
41
+ );
42
+ expect(uploadJobScopes.concepts.fieldMessagePrefix).toBe("concepts.props");
43
+ });
44
+
28
45
  it("falls back to implementation semantics for unknown scopes", () => {
29
46
  expect(getUploadJobScope("other")).toMatchObject({
30
47
  listPath: null,
@@ -130,6 +130,33 @@ describe("<UploadJobSummary />", () => {
130
130
  });
131
131
  });
132
132
 
133
+ it("classifies protocol not implemented messages as unexpected errors", () => {
134
+ expect(
135
+ classifyUploadJobErrorMessage(
136
+ "protocol String.Chars not implemented for type Tuple. This protocol is implemented for the following type(s): Atom, BitString\n\nGot value:\n\n {:array, :map}\n",
137
+ ),
138
+ ).toEqual({ type: "unexpected_error" });
139
+ });
140
+
141
+ it("replaces protocol failure details with an unexpected error", () => {
142
+ const technicalMessage =
143
+ "protocol String.Chars not implemented for type Tuple. This protocol is implemented for the following type(s): Atom, BitString\n\nGot value:\n\n {:array, :map}\n";
144
+ const rendered = render(
145
+ <UploadJobSummary
146
+ events={[{ status: "FAILED", response: { message: technicalMessage } }]}
147
+ latestStatus="FAILED"
148
+ />,
149
+ { messages: summaryMessages },
150
+ );
151
+
152
+ expect(rendered.getByLabelText("Summary")).toHaveTextContent(
153
+ "Error processing the file: Unexpected error",
154
+ );
155
+ expect(rendered.getByLabelText("Summary")).not.toHaveTextContent(
156
+ "protocol String.Chars not implemented",
157
+ );
158
+ });
159
+
133
160
  it("renders a human failure message", () => {
134
161
  const rendered = render(
135
162
  <UploadJobSummary
@@ -189,4 +189,19 @@ describe("<UploadJobs />", () => {
189
189
  expect(rendered.getByText(/completed/i)).toBeInTheDocument();
190
190
  expect(rendered.getByText(/success/i)).toBeInTheDocument();
191
191
  });
192
+
193
+ it("renders concepts scope header", async () => {
194
+ useUploadJobs.mockReturnValue({
195
+ loading: false,
196
+ data: {
197
+ data: [],
198
+ },
199
+ });
200
+
201
+ const rendered = render(<UploadJobs scope="concepts" />);
202
+ await waitForLoad(rendered);
203
+
204
+ const elements = rendered.getAllByText(/uploadJobs.concepts.header/i);
205
+ expect(elements.length).toBeGreaterThan(0);
206
+ });
192
207
  });
@@ -132,7 +132,7 @@ exports[`<GlossaryMenu /> matches the latest snapshot, finds fixed and subscopes
132
132
  aria-checked="false"
133
133
  class="item"
134
134
  data-discover="true"
135
- href="/concepts/management/bulk_upload_event"
135
+ href="/concepts/uploadJobs"
136
136
  name="concepts_upload_events"
137
137
  role="option"
138
138
  >
@@ -125,7 +125,7 @@ exports[`<SideMenu /> matches the latest snapshot 1`] = `
125
125
  aria-checked="false"
126
126
  class="item"
127
127
  data-discover="true"
128
- href="/concepts/management/bulk_upload_event"
128
+ href="/concepts/uploadJobs"
129
129
  name="concepts_upload_events"
130
130
  role="option"
131
131
  >
@@ -1,5 +1,21 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
+ exports[`ImplementationUploadJobBreadcrumbs renders breadcrumbs for concepts scope 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui breadcrumb"
7
+ >
8
+ <a
9
+ class="section"
10
+ data-discover="true"
11
+ href="/concepts/uploadJobs"
12
+ >
13
+ uploadJobs.concepts.header
14
+ </a>
15
+ </div>
16
+ </div>
17
+ `;
18
+
3
19
  exports[`ImplementationUploadJobBreadcrumbs renders breadcrumbs with filename 1`] = `
4
20
  <div>
5
21
  <div
package/src/routes.js CHANGED
@@ -14,8 +14,8 @@ export const BUCKET_VIEW_CONFIG_EDIT = "/bucketViewConfigs/:id/edit";
14
14
  export const BUCKET_VIEW_CONFIG_NEW = "/bucketViewConfigs/new";
15
15
  export const CONCEPTS = "/concepts";
16
16
  export const CONCEPTS_BULK_UPDATE = "/concepts/bulk_update";
17
- export const CONCEPTS_BULK_UPLOAD_EVENTS =
18
- "/concepts/management/bulk_upload_event";
17
+ export const CONCEPTS_UPLOAD_JOBS = "/concepts/uploadJobs";
18
+ export const CONCEPTS_UPLOAD_JOB = "/concepts/uploadJobs/:id";
19
19
  export const CONCEPTS_NEW = "/concepts/new";
20
20
  export const CONCEPTS_PENDING = "/concepts/pending";
21
21
  export const CONCEPTS_DEPRECATED = "/concepts/deprecated";
@@ -313,6 +313,8 @@ const routes = {
313
313
  BUCKET_VIEW_CONFIGS,
314
314
  CONCEPTS,
315
315
  CONCEPTS_BULK_UPDATE,
316
+ CONCEPTS_UPLOAD_JOBS,
317
+ CONCEPTS_UPLOAD_JOB,
316
318
  CONCEPT_LINKS_APPROVALS,
317
319
  CONCEPT_LINKS_APPROVALS_RESULTS,
318
320
  CONCEPTS_NEW,