@truedat/dd 4.43.3 → 4.43.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.43.6] 2022-05-06
4
+
5
+ ### Changed
6
+
7
+ - [TD-4586] Simplify accepted CSV message and include link to 'my loads'.
8
+
9
+ ## [4.43.4] 2022-05-05
10
+
11
+ ### Added
12
+
13
+ - [TD-4586] CSV bulk update events table
14
+
3
15
  ## [4.43.3] 2022-05-04
4
16
 
5
17
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dd",
3
- "version": "4.43.3",
3
+ "version": "4.43.6",
4
4
  "description": "Truedat Web Data Dictionary",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -34,7 +34,7 @@
34
34
  "@testing-library/jest-dom": "^5.14.1",
35
35
  "@testing-library/react": "^12.0.0",
36
36
  "@testing-library/user-event": "^13.2.1",
37
- "@truedat/test": "4.43.3",
37
+ "@truedat/test": "4.43.6",
38
38
  "babel-jest": "^27.0.6",
39
39
  "babel-plugin-dynamic-import-node": "^2.3.3",
40
40
  "babel-plugin-lodash": "^3.3.4",
@@ -84,9 +84,9 @@
84
84
  ]
85
85
  },
86
86
  "dependencies": {
87
- "@truedat/auth": "4.43.3",
88
- "@truedat/core": "4.43.3",
89
- "@truedat/df": "4.43.3",
87
+ "@truedat/auth": "4.43.6",
88
+ "@truedat/core": "4.43.6",
89
+ "@truedat/df": "4.43.6",
90
90
  "lodash": "^4.17.21",
91
91
  "path-to-regexp": "^1.7.0",
92
92
  "prop-types": "^15.7.2",
@@ -107,5 +107,5 @@
107
107
  "react-dom": ">= 16.8.6 < 17",
108
108
  "semantic-ui-react": ">= 0.88.2 < 2.1"
109
109
  },
110
- "gitHead": "072eb5d04ec38033c17e030c365502484815a96b"
110
+ "gitHead": "848143d79486499dd9deadea6a545929b5cdb39c"
111
111
  }
package/src/api.js CHANGED
@@ -40,6 +40,8 @@ const API_USER_SEARCH_FILTER = "/api/user_search_filters/:id";
40
40
  const API_GET_USER_SEARCH_FILTERS = "/api/user_search_filters/me";
41
41
  const API_STRUCTURES_UPLOAD =
42
42
  "/api/data_structures/bulk_update_template_content";
43
+ const API_STRUCTURES_UPLOAD_EVENTS =
44
+ "/api/data_structures/bulk_update_template_content_events";
43
45
  const API_STRUCTURE_NOTE = "/api/data_structures/:data_structure_id/notes/:id";
44
46
  const API_STRUCTURE_NOTES = "/api/data_structures/:data_structure_id/notes";
45
47
  const API_UNIT_DOMAINS = "/api/unit_domains";
@@ -85,6 +87,7 @@ export {
85
87
  API_STRUCTURES_TAG,
86
88
  API_STRUCTURES_TAGS,
87
89
  API_STRUCTURES_UPLOAD,
90
+ API_STRUCTURES_UPLOAD_EVENTS,
88
91
  API_STRUCTURE_NOTE,
89
92
  API_STRUCTURE_NOTES,
90
93
  API_UNIT_DOMAINS,
@@ -8,6 +8,7 @@ import {
8
8
  STRUCTURE_TAGS,
9
9
  STRUCTURE_TYPES,
10
10
  STRUCTURES,
11
+ STRUCTURES_UPLOAD_EVENTS,
11
12
  SYSTEMS,
12
13
  } from "@truedat/core/routes";
13
14
  import PendingStructureNotes from "./PendingStructureNotes";
@@ -19,6 +20,7 @@ import ProfileGroup from "./ProfileGroup";
19
20
  import StructureFiltersLoader from "./StructureFiltersLoader";
20
21
  import StructuresLoader from "./StructuresLoader";
21
22
  import StructuresRoutes from "./StructuresRoutes";
23
+ import StructuresUploadEvents from "./StructuresUploadEvents";
22
24
  import StructureTypesLoader from "./StructureTypesLoader";
23
25
  import StructureTagsRoutes from "./StructureTagsRoutes";
24
26
  import StructureTypesRoutes from "./StructureTypesRoutes";
@@ -88,6 +90,14 @@ export const DictionaryRoutes = () => {
88
90
  </>
89
91
  )}
90
92
  />
93
+ <Route
94
+ path={STRUCTURES_UPLOAD_EVENTS}
95
+ render={() => (
96
+ <>
97
+ <StructuresUploadEvents />
98
+ </>
99
+ )}
100
+ />
91
101
  <Route
92
102
  path={PENDING_STRUCTURE_NOTES}
93
103
  render={() => (
@@ -0,0 +1,38 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import { connect } from "react-redux";
4
+ import { Header, Icon, Segment, Dimmer, Loader } from "semantic-ui-react";
5
+ import { FormattedMessage } from "react-intl";
6
+ import StructuresUploadEventsTable from "./StructuresUploadEventsTable";
7
+
8
+ export const StructuresUploadEvents = ({ structuresUploadEventsLoading }) => (
9
+ <Segment>
10
+ <Header as="h2">
11
+ <Icon circular name="cogs" />
12
+ <Header.Content>
13
+ <FormattedMessage id="structures.bulkUploadEvents.header" />
14
+ <Header.Subheader>
15
+ <FormattedMessage id="structures.bulkUploadEvents.subheader" />
16
+ </Header.Subheader>
17
+ </Header.Content>
18
+ </Header>
19
+ <Dimmer.Dimmable dimmed={structuresUploadEventsLoading}>
20
+ <Segment attached="bottom">
21
+ <StructuresUploadEventsTable />
22
+ <Dimmer inverted active={structuresUploadEventsLoading}>
23
+ <Loader />
24
+ </Dimmer>
25
+ </Segment>
26
+ </Dimmer.Dimmable>
27
+ </Segment>
28
+ );
29
+
30
+ StructuresUploadEvents.propTypes = {
31
+ structuresUploadEventsLoading: PropTypes.bool,
32
+ };
33
+
34
+ const mapStateToProps = ({ structuresUploadEventsLoading }) => ({
35
+ structuresUploadEventsLoading,
36
+ });
37
+
38
+ export default connect(mapStateToProps)(StructuresUploadEvents);
@@ -0,0 +1,183 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useEffect, useState } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { FormattedMessage } from "react-intl";
5
+ import { Header, Icon, Table } from "semantic-ui-react";
6
+ import { connect } from "react-redux";
7
+ import { useIntl } from "react-intl";
8
+ import { columnDecorator } from "@truedat/core/services";
9
+ import { fetchStructuresUploadEvents } from "../routines";
10
+ import { getStructuresUploadEventColumns } from "../selectors";
11
+
12
+ import "../styles/structuresUploadEventColumns.less";
13
+
14
+ export const getColumnsWithData = (ruleResults, columns) =>
15
+ _.filter((column) =>
16
+ _.any(_.flow(columnDecorator(column), _.negate(_.isEmpty)))(ruleResults)
17
+ )(columns);
18
+
19
+ export const StructuresUploadEventsTable = ({
20
+ columns,
21
+ fetchStructuresUploadEvents,
22
+ structuresUploadEvents,
23
+ structuresUploadEventsLoading,
24
+ }) => {
25
+ const { formatMessage } = useIntl();
26
+ const [events, setEvents] = useState();
27
+
28
+ useEffect(() => {
29
+ fetchStructuresUploadEvents();
30
+ }, [fetchStructuresUploadEvents]);
31
+
32
+ const messagesImp = (data) => {
33
+ return _.has("errors")(data)
34
+ ? _.flow(
35
+ _.propOr([], "errors"),
36
+ _.map((error) => ({
37
+ id: `structures.bulkUpdateStructures.failed.note.${error.message}`,
38
+ fields: {
39
+ row: error.row,
40
+ id: error.id,
41
+ field: error.field,
42
+ externalId: error.external_id,
43
+ message: error.message,
44
+ },
45
+ defaultMessage: "structures.bulkUploadStructures.success.errors",
46
+ }))
47
+ )(data)
48
+ : [];
49
+ };
50
+
51
+ useEffect(() => {
52
+ const events = _.reduce(
53
+ (acc, event) => ({
54
+ ...acc,
55
+ [`${event.task_reference}_${event.status}`]: {
56
+ ...event,
57
+ response: {
58
+ ...event.response,
59
+ translatedHeader: !event.response
60
+ ? ""
61
+ : formatMessage(
62
+ { id: "structures.bulkUpdateStructures.success.header" },
63
+ {
64
+ count_ids: event.response.ids.length,
65
+ count_errors: event.response.errors.length,
66
+ }
67
+ ),
68
+ translatedErrors: _.map((error) => {
69
+ return formatMessage({ id: error.id }, error.fields);
70
+ })(messagesImp(event.response)),
71
+ },
72
+ expanded: false,
73
+ },
74
+ }),
75
+ {},
76
+ structuresUploadEvents
77
+ );
78
+
79
+ setEvents(events);
80
+ }, [structuresUploadEvents, formatMessage]);
81
+
82
+ const toggleExpanded = (eventKey) => {
83
+ setEvents({
84
+ ...events,
85
+ [eventKey]: {
86
+ ...events[eventKey],
87
+ expanded: !events[eventKey].expanded,
88
+ },
89
+ });
90
+ };
91
+
92
+ return (
93
+ <>
94
+ {!_.isEmpty(events) && (
95
+ <Table sortable selectable>
96
+ <Table.Header>
97
+ <Table.Row>
98
+ {columns.map((column, i) => {
99
+ return (
100
+ <Table.HeaderCell
101
+ key={i}
102
+ width={column.width}
103
+ content={formatMessage({
104
+ id: `structures.bulkUploadEvents.props.${column.name}`,
105
+ defaultMessage: column.name,
106
+ })}
107
+ />
108
+ );
109
+ })}
110
+ </Table.Row>
111
+ </Table.Header>
112
+ <Table.Body>
113
+ {_.map((eventKey) => {
114
+ return (
115
+ <Table.Row
116
+ className={
117
+ events[eventKey].expanded ? "expanded" : "contracted"
118
+ }
119
+ key={eventKey}
120
+ onClick={() => toggleExpanded(eventKey)}
121
+ >
122
+ {!events[eventKey].expanded ? (
123
+ columns &&
124
+ columns.map((column, key) => (
125
+ <Table.Cell
126
+ key={key}
127
+ textAlign={column.textAlign}
128
+ content={columnDecorator(column)({
129
+ ...events[eventKey],
130
+ formatMessage,
131
+ })}
132
+ />
133
+ ))
134
+ ) : (
135
+ <Table.Cell
136
+ colSpan={columns.length}
137
+ content={columns.map((column, key) => (
138
+ <div key={key}>
139
+ {columnDecorator(column)({
140
+ ...events[eventKey],
141
+ formatMessage,
142
+ })}
143
+ </div>
144
+ ))}
145
+ />
146
+ )}
147
+ </Table.Row>
148
+ );
149
+ }, Object.keys(events))}
150
+ </Table.Body>
151
+ </Table>
152
+ )}
153
+ {_.isEmpty(structuresUploadEvents) && !structuresUploadEventsLoading && (
154
+ <Header as="h4">
155
+ <Icon name="search" />
156
+ <Header.Content>
157
+ <FormattedMessage id="structures.bulkUploadEvents.results.empty" />
158
+ </Header.Content>
159
+ </Header>
160
+ )}
161
+ </>
162
+ );
163
+ };
164
+
165
+ StructuresUploadEventsTable.propTypes = {
166
+ columns: PropTypes.array,
167
+ structuresUploadEvents: PropTypes.array,
168
+ fetchStructuresUploadEvents: PropTypes.func,
169
+ structuresUploadEventsLoading: PropTypes.bool,
170
+ };
171
+
172
+ const mapStateToProps = (state) => ({
173
+ columns: getColumnsWithData(
174
+ state.structuresUploadEvents,
175
+ getStructuresUploadEventColumns(state)
176
+ ),
177
+ structuresUploadEvents: state.structuresUploadEvents,
178
+ structuresUploadEventsLoading: state.structuresUploadEventsLoading,
179
+ });
180
+
181
+ export default connect(mapStateToProps, { fetchStructuresUploadEvents })(
182
+ StructuresUploadEventsTable
183
+ );
@@ -0,0 +1,84 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { render } from "@truedat/test/render";
5
+ import en from "../../messages/en";
6
+ import StructuresUploadEventsTable from "../StructuresUploadEventsTable";
7
+
8
+
9
+
10
+ const events = [
11
+ {
12
+ "csv_hash": "99E400B91D164F8F0A39F400CA323C8F",
13
+ "inserted_at": "2022-04-23T15:53:24.638484Z",
14
+ "message": null,
15
+ "response": {
16
+ "errors": [],
17
+ "ids": [
18
+ 4825521,
19
+ 4825561,
20
+ 4825593
21
+ ]
22
+ },
23
+ "status": "COMPLETED",
24
+ "task_reference": "0.3225053307.1609564164.100549",
25
+ "user_id": 467
26
+ },
27
+ {
28
+ "csv_hash": "47D90FDF1AD967BD7DBBDAE28664278E",
29
+ "inserted_at": "2022-04-23T15:14:48.770275Z",
30
+ "message": null,
31
+ "response": {
32
+ "errors": [
33
+ {
34
+ "external_id": "file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_AC",
35
+ "field": "gdpr",
36
+ "message": "df_content.inclusion",
37
+ "row": 4
38
+ },
39
+ {
40
+ "external_id": "file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_FN",
41
+ "field": "business_description",
42
+ "message": "df_content.required",
43
+ "row": 2
44
+ },
45
+ {
46
+ "external_id": "file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_PC",
47
+ "field": "business_description",
48
+ "message": "df_content.required",
49
+ "row": 3
50
+ }
51
+ ],
52
+ "ids": []
53
+ },
54
+ "status": "COMPLETED",
55
+ "task_reference": "0.3225053307.1609564162.101463",
56
+ "user_id": 467
57
+ }
58
+ ];
59
+
60
+ const renderOpts = {
61
+ messages: { en },
62
+ state: { structuresUploadEvents: events },
63
+ };
64
+
65
+
66
+ describe("<StructuresUploadEventsTable />", () => {
67
+ it("matches the latest snapshot", () => {
68
+ const { container } = render(<StructuresUploadEventsTable />, renderOpts);
69
+ expect(container).toMatchSnapshot();
70
+ });
71
+
72
+ it("toggles row horizontal view when row is clicked", () => {
73
+ const { getAllByRole } = render(
74
+ <StructuresUploadEventsTable />,
75
+ renderOpts
76
+ );
77
+ const dataRow = getAllByRole("row")[1];
78
+ expect(dataRow).toHaveClass("contracted");
79
+ userEvent.click(dataRow);
80
+ expect(dataRow).toHaveClass("expanded");
81
+ });
82
+
83
+
84
+ });
@@ -21,6 +21,10 @@ exports[`<DictionaryRoutes /> matches the latest snapshot 1`] = `
21
21
  path="/structure_types"
22
22
  render={[Function]}
23
23
  />
24
+ <Route
25
+ path="/bulk_update_template_content_events"
26
+ render={[Function]}
27
+ />
24
28
  <Route
25
29
  path="/structure_notes"
26
30
  render={[Function]}
@@ -0,0 +1,130 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<StructuresUploadEventsTable /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <table
6
+ class="ui selectable sortable table"
7
+ >
8
+ <thead
9
+ class=""
10
+ >
11
+ <tr
12
+ class=""
13
+ >
14
+ <th
15
+ class="two wide"
16
+ >
17
+ CSV hash
18
+ </th>
19
+ <th
20
+ class="two wide"
21
+ >
22
+ Status
23
+ </th>
24
+ <th
25
+ class="two wide"
26
+ >
27
+ Response
28
+ </th>
29
+ <th
30
+ class="two wide"
31
+ >
32
+ Date
33
+ </th>
34
+ </tr>
35
+ </thead>
36
+ <tbody
37
+ class=""
38
+ >
39
+ <tr
40
+ class="contracted"
41
+ >
42
+ <td
43
+ class=""
44
+ >
45
+ 99E400B91D164F8F0A39F400CA323C8F
46
+ </td>
47
+ <td
48
+ class=""
49
+ >
50
+ Completed
51
+ </td>
52
+ <td
53
+ class=""
54
+ >
55
+ <p>
56
+ Bulk update completed. Loaded 3 errors 0
57
+ </p>
58
+ <ul
59
+ class="csv-bulk-upload-error"
60
+ />
61
+ </td>
62
+ <td
63
+ class="center aligned"
64
+ >
65
+ <time
66
+ datetime="1650729204638"
67
+ >
68
+ 2022-04-23 15:53
69
+ </time>
70
+ </td>
71
+ </tr>
72
+ <tr
73
+ class="contracted"
74
+ >
75
+ <td
76
+ class=""
77
+ >
78
+ 47D90FDF1AD967BD7DBBDAE28664278E
79
+ </td>
80
+ <td
81
+ class=""
82
+ >
83
+ Completed
84
+ </td>
85
+ <td
86
+ class=""
87
+ >
88
+ <p>
89
+ Bulk update completed. Loaded 0 errors 3
90
+ </p>
91
+ <ul
92
+ class="csv-bulk-upload-error"
93
+ >
94
+ <li>
95
+ External id: file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_AC The field &lt;gdpr&gt; does not have a permitted value
96
+ </li>
97
+ <li>
98
+ External id: file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_FN The field &lt;business_description&gt; is required
99
+ </li>
100
+ <li>
101
+ External id: file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_PC The field &lt;business_description&gt; is required
102
+ </li>
103
+ </ul>
104
+ <a
105
+ class="alert-exporter-in-table"
106
+ download="errors.csv"
107
+ href="data:text/csv;charset=utf-8,\\"RENDEREDMESSAGE\\",\\"EXTERNAL_ID\\",\\"FIELD\\",\\"MESSAGE\\",\\"ROW\\"
108
+ \\"External id: file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_AC The field <gdpr> does not have a permitted value\\",\\"file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_AC\\",\\"gdpr\\",\\"df_content.inclusion\\",\\"4\\"
109
+ \\"External id: file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_FN The field <business_description> is required\\",\\"file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_FN\\",\\"business_description\\",\\"df_content.required\\",\\"2\\"
110
+ \\"External id: file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_PC The field <business_description> is required\\",\\"file://corporate-landing-s3/cep-pro-ld-cor/batch/SAP/FSCM/vendor_issue/SAP_FSCM_CM_VENDOR_ISUE/ZFSCM_PC\\",\\"business_description\\",\\"df_content.required\\",\\"3\\""
111
+ style="float: right;"
112
+ target="_self"
113
+ >
114
+ Export to CSV
115
+ </a>
116
+ </td>
117
+ <td
118
+ class="center aligned"
119
+ >
120
+ <time
121
+ datetime="1650726888770"
122
+ >
123
+ 2022-04-23 15:14
124
+ </time>
125
+ </td>
126
+ </tr>
127
+ </tbody>
128
+ </table>
129
+ </div>
130
+ `;
@@ -395,8 +395,26 @@ export default {
395
395
  "structures.bulkUpdateStructures.no.structures.header": "Empty Search",
396
396
  "structures.bulkUpdateStructures.success.header":
397
397
  "Bulk update completed. Loaded {count_ids} errors {count_errors}",
398
+ "structures.bulkUpdateStructures.success.accepted.header":
399
+ "CSV accepted. Status can be checked at the link below:",
398
400
  "structures.bulkUpdateStructures.success.errors":
399
401
  "Error in external id {externalId}, message: {message}",
402
+ "structures.bulkUploadEvents.header": "Notes upload events",
403
+ "structures.bulkUploadEvents.subheader": "Structures notes CSV upload events",
404
+ "structures.bulkUploadEvents.props.csv_hash": "CSV hash",
405
+ "structures.bulkUploadEvents.props.status": "Status",
406
+ "structures.bulkUploadEvents.props.response": "Response",
407
+ "structures.bulkUploadEvents.props.message": "Message",
408
+ "structures.bulkUploadEvents.props.inserted_at": "Date",
409
+ "structures.bulkUploadEvents.results.empty":
410
+ "No structure notes CSV bulk upload events found",
411
+ "structures.bulkUploadEvents.status.already_calculated": "Already calculated",
412
+ "structures.bulkUploadEvents.status.already_started": "Already started",
413
+ "structures.bulkUploadEvents.status.completed": "Completed",
414
+ "structures.bulkUploadEvents.status.failed": "Failed",
415
+ "structures.bulkUploadEvents.status.just_started": "Just started",
416
+ "structures.bulkUploadEvents.status.started": "Started",
417
+ "structures.bulkUploadEvents.status.timed_out": "Timed out",
400
418
  "structures.bulkUploadStructuresDomains.success.errors.domain: not_exist":
401
419
  "Error in line {row}, in external id {externalId}, domain does not exist",
402
420
  "structures.bulkUploadStructuresDomains.success.errors.update_domain: forbbiden":
@@ -401,8 +401,29 @@ export default {
401
401
  "structures.bulkUpdateStructures.no.structures.header": "Búsqueda vacía",
402
402
  "structures.bulkUpdateStructures.success.header":
403
403
  "¡Actualización finalizada! subidos {count_ids} errores {count_errors}",
404
+ "structures.bulkUpdateStructures.success.accepted.header":
405
+ "CSV aceptado. Puede comprobar el estado en el siguiente enlace:",
404
406
  "structures.bulkUpdateStructures.success.errors":
405
407
  "Error en external id {externalId}, mensaje: {message}",
408
+ "structures.bulkUploadEvents.header": "Eventos de subida de notas",
409
+ "structures.bulkUploadEvents.subheader":
410
+ "Eventos de subida de notas de estructuras por CSV",
411
+ "structures.bulkUploadEvents.props.csv_hash": "Hash de CSV",
412
+ "structures.bulkUploadEvents.props.status": "Estado",
413
+ "structures.bulkUploadEvents.props.response": "Respuesta",
414
+ "structures.bulkUploadEvents.props.message": "Mensaje",
415
+ "structures.bulkUploadEvents.props.inserted_at": "Fecha",
416
+ "structures.bulkUploadEvents.results.empty":
417
+ "Ningún evento de subida de notas por CSV encontrado",
418
+ "structures.bulkUploadEvents.status.already_calculated":
419
+ "Previamente calculado",
420
+ "structures.bulkUploadEvents.status.already_started": "Previamente iniciado",
421
+ "structures.bulkUploadEvents.status.completed": "Finalizado",
422
+ "structures.bulkUploadEvents.status.failed": "Fallido",
423
+ "structures.bulkUploadEvents.status.just_started": "Acaba de iniciarse",
424
+ "structures.bulkUploadEvents.status.started": "Iniciado",
425
+ "structures.bulkUploadEvents.status.timed_out":
426
+ "Tiempo de espera máximo sobrepasado",
406
427
  "structures.bulkUploadStructuresDomains.success.errors.domain: not_exist":
407
428
  "Error en linea {row}, en external id {externalId}, el dominio no existe",
408
429
  "structures.bulkUploadStructuresDomains.success.errors.structure: not_exist":
@@ -0,0 +1,46 @@
1
+ import { fetchStructuresUploadEvents } from "../../routines";
2
+ import { structuresUploadEvents } from "..";
3
+
4
+ const fooState = { foo: "bar" };
5
+
6
+ describe("reducers: structuresUploadEvents", () => {
7
+ const initialState = [];
8
+
9
+ it("should provide the initial state", () => {
10
+ expect(structuresUploadEvents(undefined, {})).toEqual(initialState);
11
+ });
12
+
13
+ it("should handle the fetchStructuresUploadEvents.SUCCESS action", () => {
14
+ const someEvents = [
15
+ {
16
+ csv_hash: "99E400B91D164F8F0A39F400CA323C8F",
17
+ inserted_at: "2022-04-23T15:53:24.638484Z",
18
+ message: null,
19
+ response: {},
20
+ status: "COMPLETED",
21
+ task_reference: "0.3225053307.1609564164.100549",
22
+ user_id: 467,
23
+ },
24
+ {
25
+ csv_hash: "47D90FDF1AD967BD7DBBDAE28664278E",
26
+ inserted_at: "2022-04-23T15:14:48.770275Z",
27
+ message: null,
28
+ response: {},
29
+ status: "COMPLETED",
30
+ task_reference: "0.3225053307.1609564162.101463",
31
+ user_id: 467,
32
+ },
33
+ ];
34
+
35
+ expect(
36
+ structuresUploadEvents(fooState, {
37
+ type: fetchStructuresUploadEvents.SUCCESS,
38
+ payload: someEvents,
39
+ })
40
+ ).toMatchObject(someEvents);
41
+ });
42
+
43
+ it("should ignore unknown actions", () => {
44
+ expect(structuresUploadEvents(fooState, { type: "FOO" })).toBe(fooState);
45
+ });
46
+ });
@@ -0,0 +1,26 @@
1
+ import { fetchStructuresUploadEvents } from "../../routines";
2
+ import { structuresUploadEventsLoading } from "..";
3
+
4
+ const fooState = { foo: "bar" };
5
+
6
+ describe("reducers: structuresUploadEventsLoading", () => {
7
+ it("should provide the initial state", () => {
8
+ expect(structuresUploadEventsLoading(undefined, {})).toBe(false);
9
+ });
10
+
11
+ it("should be true after receiving the fetchStructuresUploadEvents.REQUEST action", () => {
12
+ expect(structuresUploadEventsLoading(fooState, { type: fetchStructuresUploadEvents.REQUEST })).toBe(true);
13
+ });
14
+
15
+ it("should be false after receiving the fetchStructuresUploadEvents.SUCCESS action", () => {
16
+ expect(structuresUploadEventsLoading(fooState, { type: fetchStructuresUploadEvents.SUCCESS })).toBe(false);
17
+ });
18
+
19
+ it("should be false after receiving the fetchStructuresUploadEvents.FAILURE action", () => {
20
+ expect(structuresUploadEventsLoading(fooState, { type: fetchStructuresUploadEvents.FAILURE })).toBe(false);
21
+ });
22
+
23
+ it("should ignore unhandled actions", () => {
24
+ expect(structuresUploadEventsLoading(fooState, { type: "FOO" })).toBe(fooState);
25
+ });
26
+ });
@@ -1,5 +1,6 @@
1
1
  import _ from "lodash/fp";
2
2
  import { dismissAlert } from "@truedat/core/routines";
3
+ import { STRUCTURES_UPLOAD_EVENTS } from "@truedat/core/routes";
3
4
  import {
4
5
  bulkUpdateStructures,
5
6
  uploadStructures,
@@ -9,42 +10,64 @@ import {
9
10
 
10
11
  const initialState = null;
11
12
 
13
+ const bulkUpdateErrors = (payload) => {
14
+ const messagesImp = _.has("errors")(payload.data)
15
+ ? _.flow(
16
+ _.propOr([], "errors"),
17
+ _.map((error) => ({
18
+ id: `structures.bulkUpdateStructures.failed.note.${error.message}`,
19
+ fields: {
20
+ row: error.row,
21
+ id: error.id,
22
+ field: error.field,
23
+ externalId: error.external_id,
24
+ message: error.message,
25
+ },
26
+ defaultMessage: "structures.bulkUploadStructures.success.errors",
27
+ }))
28
+ )(payload.data)
29
+ : "";
30
+ return {
31
+ error: false,
32
+ header: "structures.bulkUpdateStructures.success.header",
33
+ icon: "check",
34
+ color: payload.data.errors.length >= 1 ? "orange" : "green",
35
+ text: "",
36
+ messages: messagesImp,
37
+ fields: {
38
+ count_ids: payload.data.ids.length,
39
+ count_errors: payload.data.errors.length,
40
+ },
41
+ downloadable: true,
42
+ initialMaxListItems: 5,
43
+ };
44
+ };
45
+
46
+ const bulkUpdateAccepted = () => {
47
+ return {
48
+ error: false,
49
+ header: "structures.bulkUpdateStructures.success.accepted.header",
50
+ icon: "check",
51
+ color: "blue",
52
+ text: "",
53
+ anchor: {
54
+ reference: STRUCTURES_UPLOAD_EVENTS,
55
+ label: "sidemenu.structures_upload_events",
56
+ },
57
+ };
58
+ };
59
+
12
60
  const ddMessage = (state = initialState, { type, payload }) => {
13
61
  switch (type) {
14
62
  case dismissAlert.TRIGGER:
15
63
  return initialState;
16
64
  case uploadStructures.SUCCESS:
17
65
  case bulkUpdateStructures.SUCCESS: {
18
- const messagesImp = _.has("errors")(payload.data)
19
- ? _.flow(
20
- _.propOr([], "errors"),
21
- _.map((error) => ({
22
- id: `structures.bulkUpdateStructures.failed.note.${error.message}`,
23
- fields: {
24
- row: error.row,
25
- id: error.id,
26
- field: error.field,
27
- externalId: error.external_id,
28
- message: error.message,
29
- },
30
- defaultMessage: "structures.bulkUploadStructures.success.errors",
31
- }))
32
- )(payload.data)
33
- : "";
34
- return {
35
- error: false,
36
- header: "structures.bulkUpdateStructures.success.header",
37
- icon: "check",
38
- color: payload.data.errors.length >= 1 ? "orange" : "green",
39
- text: "",
40
- messages: messagesImp,
41
- fields: {
42
- count_ids: payload.data.ids.length,
43
- count_errors: payload.data.errors.length,
44
- },
45
- downloadable: true,
46
- initialMaxListItems: 5,
47
- };
66
+ if (payload.status === 202) {
67
+ return bulkUpdateAccepted(payload.data);
68
+ } else if (payload.status === 200) {
69
+ return bulkUpdateErrors(payload);
70
+ }
48
71
  }
49
72
 
50
73
  case uploadStructuresDomains.SUCCESS: {
@@ -83,6 +83,8 @@ import { structuresEditableDownloading } from "./structuresEditableDownloading";
83
83
  import { structuresLoading } from "./structuresLoading";
84
84
  import { structuresPageSize } from "./structuresPageSize";
85
85
  import { structuresSiblings } from "./structureSiblings";
86
+ import { structuresUploadEvents } from "./structuresUploadEvents";
87
+ import { structuresUploadEventsLoading } from "./structuresUploadEventsLoading";
86
88
  import { system } from "./system";
87
89
  import { systemId } from "./systemId";
88
90
  import { systemStructures } from "./systemStructures";
@@ -183,6 +185,8 @@ export {
183
185
  structuresLoading,
184
186
  structuresPageSize,
185
187
  structuresSiblings,
188
+ structuresUploadEvents,
189
+ structuresUploadEventsLoading,
186
190
  system,
187
191
  systemId,
188
192
  systemStructures,
@@ -0,0 +1,16 @@
1
+ import _ from "lodash/fp";
2
+ import { fetchStructuresUploadEvents } from "../routines";
3
+
4
+ export const initialState = [];
5
+
6
+ export const structuresUploadEvents = (
7
+ state = initialState,
8
+ { type, payload }
9
+ ) => {
10
+ switch (type) {
11
+ case fetchStructuresUploadEvents.SUCCESS:
12
+ return payload;
13
+ default:
14
+ return state;
15
+ }
16
+ };
@@ -0,0 +1,16 @@
1
+ import { fetchStructuresUploadEvents } from "../routines";
2
+
3
+ const structuresUploadEventsLoading = (state = false, { type }) => {
4
+ switch (type) {
5
+ case fetchStructuresUploadEvents.REQUEST:
6
+ return true;
7
+ case fetchStructuresUploadEvents.SUCCESS:
8
+ return false;
9
+ case fetchStructuresUploadEvents.FAILURE:
10
+ return false;
11
+ default:
12
+ return state;
13
+ }
14
+ };
15
+
16
+ export { structuresUploadEventsLoading };
package/src/routines.js CHANGED
@@ -86,6 +86,7 @@ export const fetchUserSearchFilters = createRoutine(
86
86
  "FETCH_USER_SEARCH_FILTERS"
87
87
  );
88
88
  export const fetchStructures = createRoutine("FETCH_STRUCTURES");
89
+ export const fetchStructuresUploadEvents = createRoutine("FETCH_STRUCTURES_UPLOAD_EVENTS");
89
90
  export const fetchSystem = createRoutine("FETCH_SYSTEM");
90
91
  export const fetchSystemStructures = createRoutine("FETCH_SYSTEM_STRUCTURES");
91
92
  export const fetchSystems = createRoutine("FETCH_SYSTEMS");
@@ -0,0 +1,79 @@
1
+ import { testSaga } from "redux-saga-test-plan";
2
+ import { apiJson, JSON_OPTS } from "@truedat/core/services/api";
3
+ import {
4
+ fetchStructuresUploadEventsRequestSaga,
5
+ fetchStructuresUploadEventsSaga,
6
+ } from "../fetchStructuresUploadEvents";
7
+ import { fetchStructuresUploadEvents } from "../../routines";
8
+ import { API_STRUCTURES_UPLOAD_EVENTS } from "../../api";
9
+
10
+ describe("sagas: fetchStructuresUploadEventsRequestSaga", () => {
11
+ it("should invoke fetchStructuresUploadEventsSaga on trigger", () => {
12
+ expect(() => {
13
+ testSaga(fetchStructuresUploadEventsRequestSaga)
14
+ .next()
15
+ .takeLatest(
16
+ fetchStructuresUploadEvents.TRIGGER,
17
+ fetchStructuresUploadEventsSaga
18
+ )
19
+ .finish()
20
+ .isDone();
21
+ }).not.toThrow();
22
+ });
23
+
24
+ it("should throw exception if an unhandled action is received", () => {
25
+ expect(() => {
26
+ testSaga(fetchStructuresUploadEventsRequestSaga)
27
+ .next()
28
+ .takeLatest("FOO", fetchStructuresUploadEventsSaga);
29
+ }).toThrow();
30
+ });
31
+ });
32
+
33
+ describe("sagas: fetchStructuresUploadEventsSaga", () => {
34
+ it("should put a success action when a response is returned", () => {
35
+ const url = API_STRUCTURES_UPLOAD_EVENTS;
36
+ const data = [
37
+ {
38
+ csv_hash: "99E400B91D164F8F0A39F400CA323C8F",
39
+ },
40
+ {
41
+ csv_hash: "47D90FDF1AD967BD7DBBDAE28664278E",
42
+ },
43
+ ];
44
+
45
+ expect(() => {
46
+ testSaga(fetchStructuresUploadEventsSaga)
47
+ .next()
48
+ .put(fetchStructuresUploadEvents.request())
49
+ .next()
50
+ .call(apiJson, url, JSON_OPTS)
51
+ .next({ data })
52
+ .put(fetchStructuresUploadEvents.success(data))
53
+ .next()
54
+ .put(fetchStructuresUploadEvents.fulfill())
55
+ .next()
56
+ .isDone();
57
+ }).not.toThrow();
58
+ });
59
+
60
+ it("should put a failure action when the call returns an error", () => {
61
+ const message = "Request failed";
62
+ const error = { message };
63
+ const url = API_STRUCTURES_UPLOAD_EVENTS;
64
+
65
+ expect(() => {
66
+ testSaga(fetchStructuresUploadEventsSaga)
67
+ .next()
68
+ .put(fetchStructuresUploadEvents.request())
69
+ .next()
70
+ .call(apiJson, url, JSON_OPTS)
71
+ .throw(error)
72
+ .put(fetchStructuresUploadEvents.failure(message))
73
+ .next()
74
+ .put(fetchStructuresUploadEvents.fulfill())
75
+ .next()
76
+ .isDone();
77
+ }).not.toThrow();
78
+ });
79
+ });
@@ -30,6 +30,7 @@ describe("sagas: uploadStructuresRequestSaga", () => {
30
30
  describe("sagas: uploadStructuresSaga", () => {
31
31
  const body = { structures: "content" };
32
32
  const payload = { data: body };
33
+ const status = "JUST_STARTED";
33
34
  const data = { message: [1, 2] };
34
35
  it("should put a success action when a response is returned", () => {
35
36
  expect(() => {
@@ -38,8 +39,8 @@ describe("sagas: uploadStructuresSaga", () => {
38
39
  .put(uploadStructures.request())
39
40
  .next()
40
41
  .call(apiJsonPost, API_STRUCTURES_UPLOAD, body, UPLOAD_JSON_OPTS)
41
- .next({ data })
42
- .put(uploadStructures.success({ data }))
42
+ .next({ status, data })
43
+ .put(uploadStructures.success({ status, data }))
43
44
  .next()
44
45
  .put(uploadStructures.fulfill())
45
46
  .next()
@@ -0,0 +1,29 @@
1
+ import { call, put, takeLatest } from "redux-saga/effects";
2
+ import { apiJson, JSON_OPTS } from "@truedat/core/services/api";
3
+ import { fetchStructuresUploadEvents as fetchStructuresUploadEvents } from "../routines";
4
+ import { API_STRUCTURES_UPLOAD_EVENTS } from "../api";
5
+
6
+ export function* fetchStructuresUploadEventsSaga() {
7
+ try {
8
+ const url = API_STRUCTURES_UPLOAD_EVENTS;
9
+ yield put(fetchStructuresUploadEvents.request());
10
+ const { data } = yield call(apiJson, url, JSON_OPTS);
11
+ yield put(fetchStructuresUploadEvents.success(data));
12
+ } catch (error) {
13
+ if (error.response) {
14
+ const { status, data } = error.response;
15
+ yield put(fetchStructuresUploadEvents.failure({ status, data }));
16
+ } else {
17
+ yield put(fetchStructuresUploadEvents.failure(error.message));
18
+ }
19
+ } finally {
20
+ yield put(fetchStructuresUploadEvents.fulfill());
21
+ }
22
+ }
23
+
24
+ export function* fetchStructuresUploadEventsRequestSaga() {
25
+ yield takeLatest(
26
+ fetchStructuresUploadEvents.TRIGGER,
27
+ fetchStructuresUploadEventsSaga
28
+ );
29
+ }
@@ -35,6 +35,7 @@ import { fetchStructureTypeRequestSaga } from "./fetchStructureType";
35
35
  import { fetchStructureTypesRequestSaga } from "./fetchStructureTypes";
36
36
  import { fetchUserSearchFiltersRequestSaga } from "./fetchUserSearchFilters";
37
37
  import { fetchStructuresRequestSaga } from "./fetchStructures";
38
+ import { fetchStructuresUploadEventsRequestSaga } from "./fetchStructuresUploadEvents";
38
39
  import { fetchSystemRequestSaga } from "./fetchSystem";
39
40
  import { fetchSystemStructuresRequestSaga } from "./fetchSystemStructures";
40
41
  import { fetchSystemsRequestSaga } from "./fetchSystems";
@@ -85,6 +86,7 @@ export {
85
86
  fetchStructureTagsRequestSaga,
86
87
  fetchStructureTypeRequestSaga,
87
88
  fetchStructureTypesRequestSaga,
89
+ fetchStructuresUploadEventsRequestSaga,
88
90
  fetchUserSearchFiltersRequestSaga,
89
91
  fetchStructuresRequestSaga,
90
92
  fetchSystemRequestSaga,
@@ -140,6 +142,7 @@ export default [
140
142
  fetchStructureTypesRequestSaga(),
141
143
  fetchUserSearchFiltersRequestSaga(),
142
144
  fetchStructuresRequestSaga(),
145
+ fetchStructuresUploadEventsRequestSaga(),
143
146
  fetchSystemRequestSaga(),
144
147
  fetchSystemStructuresRequestSaga(),
145
148
  fetchSystemsRequestSaga(),
@@ -7,14 +7,14 @@ export function* uploadStructuresSaga({ payload }) {
7
7
  try {
8
8
  const { data: body } = payload;
9
9
  yield put(uploadStructures.request());
10
- const { data } = yield call(
10
+ const { status, data } = yield call(
11
11
  apiJsonPost,
12
12
  API_STRUCTURES_UPLOAD,
13
13
  body,
14
14
  UPLOAD_JSON_OPTS
15
15
  );
16
16
 
17
- yield put(uploadStructures.success({ data }));
17
+ yield put(uploadStructures.success({ status, data }));
18
18
  } catch (error) {
19
19
  if (error.response) {
20
20
  const { status, data } = error.response;
@@ -0,0 +1,20 @@
1
+ import {
2
+ getStructuresUploadEventColumns,
3
+ defaultStructuresUploadEventColumns,
4
+ } from "..";
5
+
6
+ describe("selectors: getStructuresUploadEventColumns", () => {
7
+ it("should return custom structuresUploadEventColumns when present", () => {
8
+ const structuresUploadEventColumns = [{ name: "test" }];
9
+ const res = getStructuresUploadEventColumns({
10
+ structuresUploadEventColumns
11
+ });
12
+ expect(res).toHaveLength(1);
13
+ expect(res).toEqual(structuresUploadEventColumns);
14
+ });
15
+
16
+ it("should return default defaultRuleImplementationColumns when no customized", () => {
17
+ const res = getStructuresUploadEventColumns({});
18
+ expect(res).toHaveLength(defaultStructuresUploadEventColumns.length);
19
+ });
20
+ });
@@ -0,0 +1,74 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { createSelector } from "reselect";
4
+ import { FormattedMessage } from "react-intl";
5
+ import Moment from "react-moment";
6
+ import { AlertExporter } from "@truedat/core/components/AlertExporter";
7
+
8
+ const bulkUpdateErrors = (response) => {
9
+ return _.isEmpty(response) ? (
10
+ ""
11
+ ) : (
12
+ <>
13
+ <p>{response.translatedHeader}</p>
14
+ <ul className="csv-bulk-upload-error">
15
+ {_.map.convert({ cap: false })((translatedError, key) => {
16
+ return <li key={`error_${key}`}>{translatedError}</li>;
17
+ })(response.translatedErrors)}
18
+ </ul>
19
+ {_.isEmpty(response.errors) ? null : (
20
+ <AlertExporter
21
+ className="alert-exporter-in-table"
22
+ onClick={(e) => e.stopPropagation()}
23
+ {...{
24
+ list: response.translatedErrors,
25
+ messages: response.errors,
26
+ }}
27
+ />
28
+ )}
29
+ </>
30
+ );
31
+ };
32
+
33
+ const dateDecorator = (date) =>
34
+ date && <Moment locale="es" date={date} format="YYYY-MM-DD HH:mm" />;
35
+
36
+ const statusDecorator = (status) => (
37
+ <FormattedMessage
38
+ id={`structures.bulkUploadEvents.status.${_.toLower(status)}`}
39
+ />
40
+ );
41
+
42
+ export const defaultStructuresUploadEventColumns = [
43
+ {
44
+ name: "csv_hash",
45
+ width: 2,
46
+ },
47
+ {
48
+ name: "status",
49
+ fieldDecorator: statusDecorator,
50
+ width: 2,
51
+ },
52
+ {
53
+ name: "response",
54
+ fieldDecorator: bulkUpdateErrors,
55
+ width: 2,
56
+ },
57
+ {
58
+ name: "message",
59
+ width: 2,
60
+ },
61
+ {
62
+ name: "inserted_at",
63
+ fieldDecorator: dateDecorator,
64
+ textAlign: "center",
65
+ width: 2,
66
+ },
67
+ ];
68
+
69
+ const getColumns = (state) => state.structuresUploadEventColumns;
70
+
71
+ export const getStructuresUploadEventColumns = createSelector(
72
+ [getColumns],
73
+ (columns) => _.defaultTo(defaultStructuresUploadEventColumns)(columns)
74
+ );
@@ -39,6 +39,10 @@ export { getStructureSelectedFilterValues } from "./getStructureSelectedFilterVa
39
39
  export { getStructuresFields } from "./getStructuresFields";
40
40
  export { getStructureSortingCriteria } from "./getStructureSortingCriteria";
41
41
  export { getStructureUpdatedAt } from "./getStructureUpdatedAt";
42
+ export {
43
+ getStructuresUploadEventColumns,
44
+ defaultStructuresUploadEventColumns,
45
+ } from "./getStructuresUploadEventColumns";
42
46
  export { getSystem } from "./getSystem";
43
47
  export { getSystemTemplate } from "./getSystemTemplate";
44
48
  export { getTabVisibility } from "./getTabVisibility";
@@ -0,0 +1,24 @@
1
+ .contracted ul.csv-bulk-upload-error {
2
+ max-height: 100px;
3
+ overflow: hidden;
4
+ }
5
+
6
+ .contracted ul.csv-bulk-upload-error {
7
+ .fade-up();
8
+ }
9
+
10
+ .fade-up {
11
+ background: -webkit-linear-gradient(rgba(0,0,0,1), rgba(0,0,0,0));
12
+ background-clip: text;
13
+ -webkit-background-clip: text;
14
+ -webkit-text-fill-color: transparent;
15
+ }
16
+
17
+ .alert-exporter-in-table {
18
+ padding: 3px;
19
+ margin: 5px;
20
+ &:hover {
21
+ padding: 2px;
22
+ border: 1px solid blue;
23
+ }
24
+ }