@truedat/dd 7.1.4 → 7.1.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/package.json +6 -6
- package/src/api/queries.js +59 -0
- package/src/components/CursorPagination.js +73 -0
- package/src/components/StructureFieldRow.js +19 -2
- package/src/components/StructureFields.js +270 -19
- package/src/components/StructureGrantListButton.js +2 -2
- package/src/components/StructureSummary.js +0 -5
- package/src/components/StructureTabPane.js +8 -3
- package/src/components/__tests__/StructureFields.spec.js +72 -26
- package/src/components/__tests__/__snapshots__/StructureFields.spec.js.snap +88 -2
- package/src/components/index.js +2 -0
- package/src/reducers/__tests__/structureFields.spec.js +2 -2
- package/src/selectors/__tests__/getLinkedImplementationsToStructuresColumns.spec.js +20 -0
- package/src/selectors/getLinkedImplementationsToStructuresColumns.js +95 -0
- package/src/selectors/index.js +4 -2
- package/src/components/StructuresProfileButton.js +0 -28
- package/src/components/StructuresProfileForm.js +0 -89
- package/src/components/__tests__/StructuresProfileForm.spec.js +0 -56
- package/src/components/__tests__/__snapshots__/StructuresProfileForm.spec.js.snap +0 -54
- package/src/selectors/__tests__/getSortedFields.spec.js +0 -39
- package/src/selectors/__tests__/getSortedStructureChildren.spec.js +0 -36
- package/src/selectors/getSortedFields.js +0 -9
- package/src/selectors/getSortedStructureChildren.js +0 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/dd",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.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.16.5",
|
|
35
35
|
"@testing-library/react": "^12.0.0",
|
|
36
36
|
"@testing-library/user-event": "^13.2.1",
|
|
37
|
-
"@truedat/test": "7.1.
|
|
37
|
+
"@truedat/test": "7.1.6",
|
|
38
38
|
"babel-jest": "^28.1.0",
|
|
39
39
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
|
40
40
|
"babel-plugin-lodash": "^3.3.4",
|
|
@@ -88,9 +88,9 @@
|
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
90
|
"@apollo/client": "^3.7.1",
|
|
91
|
-
"@truedat/auth": "7.1.
|
|
92
|
-
"@truedat/core": "7.1.
|
|
93
|
-
"@truedat/df": "7.1.
|
|
91
|
+
"@truedat/auth": "7.1.6",
|
|
92
|
+
"@truedat/core": "7.1.6",
|
|
93
|
+
"@truedat/df": "7.1.6",
|
|
94
94
|
"lodash": "^4.17.21",
|
|
95
95
|
"moment": "^2.29.4",
|
|
96
96
|
"path-to-regexp": "^1.7.0",
|
|
@@ -115,5 +115,5 @@
|
|
|
115
115
|
"react-dom": ">= 16.8.6 < 17",
|
|
116
116
|
"semantic-ui-react": ">= 2.0.3 < 2.2"
|
|
117
117
|
},
|
|
118
|
-
"gitHead": "
|
|
118
|
+
"gitHead": "0d15ef9da4d597618a5e3543ad2173e3d76f258b"
|
|
119
119
|
}
|
package/src/api/queries.js
CHANGED
|
@@ -535,3 +535,62 @@ export const CATALOG_VIEW_CONFIG_QUERY = gql`
|
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
`;
|
|
538
|
+
|
|
539
|
+
export const DATA_FIELDS_QUERY = gql`
|
|
540
|
+
query DataFields(
|
|
541
|
+
$dataStructureId: ID!
|
|
542
|
+
$version: String!
|
|
543
|
+
$note_fields: [String]
|
|
544
|
+
$first: Int
|
|
545
|
+
$last: Int
|
|
546
|
+
$search: String
|
|
547
|
+
$before: Cursor
|
|
548
|
+
$after: Cursor
|
|
549
|
+
$filters: DataFieldsFilter
|
|
550
|
+
) {
|
|
551
|
+
dataFields(
|
|
552
|
+
dataStructureId: $dataStructureId
|
|
553
|
+
version: $version
|
|
554
|
+
first: $first
|
|
555
|
+
last: $last
|
|
556
|
+
before: $before
|
|
557
|
+
after: $after
|
|
558
|
+
search: $search
|
|
559
|
+
filters: $filters
|
|
560
|
+
) {
|
|
561
|
+
page {
|
|
562
|
+
alias
|
|
563
|
+
classes
|
|
564
|
+
data_structure_id
|
|
565
|
+
deleted_at
|
|
566
|
+
description
|
|
567
|
+
degree {
|
|
568
|
+
in
|
|
569
|
+
out
|
|
570
|
+
}
|
|
571
|
+
links
|
|
572
|
+
id
|
|
573
|
+
metadata
|
|
574
|
+
name
|
|
575
|
+
type
|
|
576
|
+
profile {
|
|
577
|
+
max
|
|
578
|
+
min
|
|
579
|
+
most_frequent
|
|
580
|
+
null_count
|
|
581
|
+
patterns
|
|
582
|
+
total_count
|
|
583
|
+
unique_count
|
|
584
|
+
}
|
|
585
|
+
has_note
|
|
586
|
+
note(select_fields: $note_fields)
|
|
587
|
+
}
|
|
588
|
+
pageInfo {
|
|
589
|
+
startCursor
|
|
590
|
+
endCursor
|
|
591
|
+
hasNextPage
|
|
592
|
+
hasPreviousPage
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
`;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import PropTypes from "prop-types";
|
|
4
|
+
import queryString from "query-string";
|
|
5
|
+
import { useLocation, Link } from "react-router-dom";
|
|
6
|
+
import { Menu } from "semantic-ui-react";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
This component is duplicated from packages/core/src/components/CursorPagination.js
|
|
10
|
+
to accommodate poorly implemented pagination logic on the back end.
|
|
11
|
+
We have created a task to improve the pagination system and streamline GraphQL query handling in the future.
|
|
12
|
+
*/
|
|
13
|
+
const CursorPagination = ({ pageInfo, className }) => {
|
|
14
|
+
const { search, ...location } = useLocation();
|
|
15
|
+
const { q } = queryString.parse(search);
|
|
16
|
+
const { endCursor, startCursor, hasPreviousPage, hasNextPage } =
|
|
17
|
+
pageInfo || {};
|
|
18
|
+
|
|
19
|
+
const next =
|
|
20
|
+
hasNextPage && endCursor
|
|
21
|
+
? {
|
|
22
|
+
...location,
|
|
23
|
+
search: "?" + queryString.stringify({ after: endCursor, q }),
|
|
24
|
+
}
|
|
25
|
+
: null;
|
|
26
|
+
const prev =
|
|
27
|
+
hasPreviousPage && startCursor
|
|
28
|
+
? {
|
|
29
|
+
...location,
|
|
30
|
+
search: "?" + queryString.stringify({ before: startCursor, q }),
|
|
31
|
+
}
|
|
32
|
+
: null;
|
|
33
|
+
const first = { ...location, search: "?" + queryString.stringify({ q }) };
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Menu className={className} pagination role="navigation">
|
|
37
|
+
<Menu.Item
|
|
38
|
+
as={first ? Link : null}
|
|
39
|
+
link={!!first}
|
|
40
|
+
content="«"
|
|
41
|
+
disabled={!hasPreviousPage}
|
|
42
|
+
to={first}
|
|
43
|
+
replace={first ? true : null}
|
|
44
|
+
aria-label="First"
|
|
45
|
+
/>
|
|
46
|
+
<Menu.Item
|
|
47
|
+
as={prev ? Link : null}
|
|
48
|
+
link={!!prev}
|
|
49
|
+
content="⟨"
|
|
50
|
+
disabled={!hasPreviousPage}
|
|
51
|
+
to={prev}
|
|
52
|
+
replace={prev ? true : null}
|
|
53
|
+
aria-label="Later"
|
|
54
|
+
/>
|
|
55
|
+
<Menu.Item
|
|
56
|
+
as={next ? Link : null}
|
|
57
|
+
link={!!next}
|
|
58
|
+
content="⟩"
|
|
59
|
+
disabled={!hasNextPage}
|
|
60
|
+
to={next}
|
|
61
|
+
replace={next ? true : null}
|
|
62
|
+
aria-label="Earlier"
|
|
63
|
+
/>
|
|
64
|
+
</Menu>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
CursorPagination.propTypes = {
|
|
69
|
+
className: PropTypes.string,
|
|
70
|
+
pageInfo: PropTypes.object,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default CursorPagination;
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import PropTypes from "prop-types";
|
|
3
|
-
import { Table } from "semantic-ui-react";
|
|
3
|
+
import { Checkbox, Table } from "semantic-ui-react";
|
|
4
4
|
import { columnDecorator } from "@truedat/core/services";
|
|
5
5
|
|
|
6
|
-
export const StructureFieldRow = ({
|
|
6
|
+
export const StructureFieldRow = ({
|
|
7
|
+
field,
|
|
8
|
+
columns,
|
|
9
|
+
profilingMode,
|
|
10
|
+
onCheckedRow,
|
|
11
|
+
checked,
|
|
12
|
+
}) => (
|
|
7
13
|
<Table.Row key={field.data_structure_id} warning={!!field.deleted_at}>
|
|
14
|
+
{profilingMode && (
|
|
15
|
+
<Table.HeaderCell textAlign="center" width={1}>
|
|
16
|
+
<Checkbox
|
|
17
|
+
onChange={() => onCheckedRow(field.data_structure_id)}
|
|
18
|
+
checked={checked}
|
|
19
|
+
/>
|
|
20
|
+
</Table.HeaderCell>
|
|
21
|
+
)}
|
|
8
22
|
{columns
|
|
9
23
|
? columns.map((column, key) => (
|
|
10
24
|
<Table.Cell
|
|
@@ -20,6 +34,9 @@ export const StructureFieldRow = ({ field, columns }) => (
|
|
|
20
34
|
StructureFieldRow.propTypes = {
|
|
21
35
|
field: PropTypes.object.isRequired,
|
|
22
36
|
columns: PropTypes.array,
|
|
37
|
+
profilingMode: PropTypes.bool,
|
|
38
|
+
checked: PropTypes.bool,
|
|
39
|
+
onCheckedRow: PropTypes.func,
|
|
23
40
|
};
|
|
24
41
|
|
|
25
42
|
export default StructureFieldRow;
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
|
-
import React from "react";
|
|
2
|
+
import React, { useState, useCallback } from "react";
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import queryString from "query-string";
|
|
5
|
+
import { useLocation, useParams, useHistory } from "react-router-dom";
|
|
6
|
+
import { useQuery } from "@apollo/client";
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
Checkbox,
|
|
10
|
+
Grid,
|
|
11
|
+
Icon,
|
|
12
|
+
Input,
|
|
13
|
+
Label,
|
|
14
|
+
Message,
|
|
15
|
+
Table,
|
|
16
|
+
} from "semantic-ui-react";
|
|
17
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
7
18
|
import { columnPredicate } from "@truedat/core/services";
|
|
8
|
-
import {
|
|
19
|
+
import { Loading } from "@truedat/core/components";
|
|
20
|
+
import { connect, useSelector } from "react-redux";
|
|
21
|
+
import { DATA_FIELDS_QUERY } from "../api/queries";
|
|
22
|
+
import { getStructureFieldColumns } from "../selectors";
|
|
23
|
+
import { createProfileGroup } from "../routines";
|
|
9
24
|
import StructureFieldRow from "./StructureFieldRow";
|
|
25
|
+
import CursorPagination from "./CursorPagination";
|
|
10
26
|
|
|
11
27
|
const columnPred = (c) =>
|
|
12
28
|
c?.name === "degree" ? _.prop("degree") : columnPredicate(c);
|
|
@@ -14,10 +30,71 @@ const columnPred = (c) =>
|
|
|
14
30
|
const getColumns = (columns, fields) =>
|
|
15
31
|
_.filter((c) => _.any(columnPred(c))(fields))(columns);
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
const PAGE_SIZE = 50;
|
|
34
|
+
|
|
35
|
+
const ProfileActions = ({
|
|
36
|
+
emptyFields,
|
|
37
|
+
handleSubmit,
|
|
38
|
+
onProfileCheck,
|
|
39
|
+
profilingMode,
|
|
40
|
+
}) => {
|
|
41
|
+
return (
|
|
42
|
+
<Grid.Column
|
|
43
|
+
textAlign="right"
|
|
44
|
+
style={{
|
|
45
|
+
display: "flex",
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
justifyContent: "flex-end",
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<Checkbox
|
|
51
|
+
disabled={emptyFields}
|
|
52
|
+
checked={profilingMode}
|
|
53
|
+
className="bgOrange"
|
|
54
|
+
toggle
|
|
55
|
+
onChange={() => onProfileCheck(!profilingMode)}
|
|
56
|
+
/>
|
|
57
|
+
<Button
|
|
58
|
+
style={{ marginLeft: "10px" }}
|
|
59
|
+
icon
|
|
60
|
+
labelPosition="left"
|
|
61
|
+
disabled={!profilingMode || emptyFields}
|
|
62
|
+
onClick={handleSubmit}
|
|
63
|
+
>
|
|
64
|
+
<Icon name="tachometer alternate" />
|
|
65
|
+
<FormattedMessage id="structure.execute_profile" />
|
|
66
|
+
</Button>
|
|
67
|
+
</Grid.Column>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
ProfileActions.propTypes = {
|
|
72
|
+
emptyFields: PropTypes.bool,
|
|
73
|
+
handleSubmit: PropTypes.func,
|
|
74
|
+
onProfileCheck: PropTypes.func,
|
|
75
|
+
profilingMode: PropTypes.bool,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const StructureFieldsTable = ({
|
|
79
|
+
allCheckedInPage,
|
|
80
|
+
profilingMode,
|
|
81
|
+
columns,
|
|
82
|
+
fields,
|
|
83
|
+
onCheckedRow,
|
|
84
|
+
onPageCheck,
|
|
85
|
+
checkedFields,
|
|
86
|
+
}) => (
|
|
18
87
|
<Table>
|
|
19
88
|
<Table.Header>
|
|
20
89
|
<Table.Row>
|
|
90
|
+
{profilingMode && (
|
|
91
|
+
<Table.HeaderCell textAlign="center" width={1}>
|
|
92
|
+
<Checkbox
|
|
93
|
+
checked={allCheckedInPage}
|
|
94
|
+
onChange={() => onPageCheck()}
|
|
95
|
+
/>
|
|
96
|
+
</Table.HeaderCell>
|
|
97
|
+
)}
|
|
21
98
|
{columns.map((column, i) => (
|
|
22
99
|
<Table.HeaderCell
|
|
23
100
|
key={i}
|
|
@@ -29,26 +106,200 @@ export const StructureFields = ({ columns, fields, ...rest }) => (
|
|
|
29
106
|
</Table.Header>
|
|
30
107
|
<Table.Body>
|
|
31
108
|
{fields.map((f, i) => (
|
|
32
|
-
<StructureFieldRow
|
|
109
|
+
<StructureFieldRow
|
|
110
|
+
key={i}
|
|
111
|
+
columns={columns}
|
|
112
|
+
field={f}
|
|
113
|
+
profilingMode={profilingMode}
|
|
114
|
+
onCheckedRow={onCheckedRow}
|
|
115
|
+
checked={_.contains(f?.data_structure_id)(checkedFields)}
|
|
116
|
+
/>
|
|
33
117
|
))}
|
|
34
118
|
</Table.Body>
|
|
35
119
|
</Table>
|
|
36
120
|
);
|
|
37
121
|
|
|
122
|
+
StructureFieldsTable.propTypes = {
|
|
123
|
+
allCheckedInPage: PropTypes.bool,
|
|
124
|
+
profilingMode: PropTypes.bool,
|
|
125
|
+
fields: PropTypes.arrayOf(PropTypes.object),
|
|
126
|
+
columns: PropTypes.arrayOf(PropTypes.object),
|
|
127
|
+
onCheckedRow: PropTypes.func,
|
|
128
|
+
onPageCheck: PropTypes.func,
|
|
129
|
+
checkedFields: PropTypes.arrayOf(PropTypes.object),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const StructureFields = ({
|
|
133
|
+
createProfileGroup,
|
|
134
|
+
canExecuteProfiling,
|
|
135
|
+
}) => {
|
|
136
|
+
const { id: dataStructureId, version: version } = useParams();
|
|
137
|
+
const { search, pathname } = useLocation();
|
|
138
|
+
const { before, after, q } = queryString.parse(search);
|
|
139
|
+
const history = useHistory();
|
|
140
|
+
const { formatMessage } = useIntl();
|
|
141
|
+
const [profilingMode, setProfilingMode] = useState(false);
|
|
142
|
+
const [checkedFields, setCheckedFields] = useState([]);
|
|
143
|
+
const [searchQuery, setSearchQuery] = useState(q || "");
|
|
144
|
+
|
|
145
|
+
const structureVariables = _.isNil(version)
|
|
146
|
+
? { dataStructureId, version: "latest" }
|
|
147
|
+
: { dataStructureId, version };
|
|
148
|
+
const searchVariables = before
|
|
149
|
+
? { after, before, last: PAGE_SIZE, search: q }
|
|
150
|
+
: { after, before, first: PAGE_SIZE, search: q };
|
|
151
|
+
const variables = { ...structureVariables, ...searchVariables };
|
|
152
|
+
const { data, loading } = useQuery(DATA_FIELDS_QUERY, { variables });
|
|
153
|
+
const fields = data?.dataFields?.page || [];
|
|
154
|
+
const pageInfo = data?.dataFields?.pageInfo || {};
|
|
155
|
+
const pageStructureIds = _.map("data_structure_id")(fields);
|
|
156
|
+
const emptyFields = _.isEmpty(fields);
|
|
157
|
+
|
|
158
|
+
const columns = useSelector((state) =>
|
|
159
|
+
getColumns(getStructureFieldColumns(state), fields)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const allCheckedInPage = _.every((structureId) =>
|
|
163
|
+
_.contains(structureId)(checkedFields)
|
|
164
|
+
)(pageStructureIds);
|
|
165
|
+
|
|
166
|
+
const debounceSearch = useCallback(
|
|
167
|
+
_.debounce(300)((value) => {
|
|
168
|
+
history.replace({
|
|
169
|
+
pathname: pathname,
|
|
170
|
+
search: queryString.stringify({ q: value }),
|
|
171
|
+
});
|
|
172
|
+
}),
|
|
173
|
+
[]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const handleInputChange = (e) => {
|
|
177
|
+
const value = e.target.value;
|
|
178
|
+
setCheckedFields([]);
|
|
179
|
+
setSearchQuery(value);
|
|
180
|
+
debounceSearch(value);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const handleCheckedRow = (id) => {
|
|
184
|
+
const updatedSelection = _.includes(id)(checkedFields)
|
|
185
|
+
? _.without([id])(checkedFields)
|
|
186
|
+
: [id, ...checkedFields];
|
|
187
|
+
setCheckedFields(updatedSelection);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleCheckedPage = () => {
|
|
191
|
+
const updatedSelection = allCheckedInPage
|
|
192
|
+
? _.without(pageStructureIds)(checkedFields)
|
|
193
|
+
: _.union(checkedFields)(pageStructureIds);
|
|
194
|
+
setCheckedFields(updatedSelection);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleCheckedProfileExecution = (checked) => {
|
|
198
|
+
!checked && !_.isEmpty(checkedFields) && setCheckedFields([]);
|
|
199
|
+
setProfilingMode(checked);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleSubmit = (e) => {
|
|
203
|
+
_.isEmpty(checkedFields)
|
|
204
|
+
? createProfileGroup({ parent_structure_id: dataStructureId })
|
|
205
|
+
: createProfileGroup({ data_structure_ids: checkedFields });
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<>
|
|
210
|
+
{loading ? <Loading /> : null}
|
|
211
|
+
<>
|
|
212
|
+
<>
|
|
213
|
+
<Grid>
|
|
214
|
+
<Grid.Row columns={2}>
|
|
215
|
+
<Grid.Column>
|
|
216
|
+
<Input
|
|
217
|
+
icon={{ name: "search", link: true }}
|
|
218
|
+
iconPosition="left"
|
|
219
|
+
loading={loading}
|
|
220
|
+
placeholder={formatMessage({
|
|
221
|
+
id: "structure.search.placeholder",
|
|
222
|
+
})}
|
|
223
|
+
onChange={handleInputChange}
|
|
224
|
+
value={searchQuery}
|
|
225
|
+
/>
|
|
226
|
+
</Grid.Column>
|
|
227
|
+
{canExecuteProfiling ? (
|
|
228
|
+
<ProfileActions
|
|
229
|
+
emptyFields={emptyFields}
|
|
230
|
+
profilingMode={profilingMode}
|
|
231
|
+
onProfileCheck={handleCheckedProfileExecution}
|
|
232
|
+
handleSubmit={handleSubmit}
|
|
233
|
+
/>
|
|
234
|
+
) : null}
|
|
235
|
+
</Grid.Row>
|
|
236
|
+
<Grid.Row
|
|
237
|
+
style={{
|
|
238
|
+
minHeight: "40px",
|
|
239
|
+
paddingTop: "2px",
|
|
240
|
+
paddingBottom: "2px",
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
<Grid.Column textAlign="right">
|
|
244
|
+
{!loading &&
|
|
245
|
+
profilingMode &&
|
|
246
|
+
!emptyFields &&
|
|
247
|
+
_.isEmpty(checkedFields) && (
|
|
248
|
+
<Label>
|
|
249
|
+
<FormattedMessage id="structure.profile.tab.execute_all" />
|
|
250
|
+
</Label>
|
|
251
|
+
)}
|
|
252
|
+
{!loading && profilingMode && !_.isEmpty(checkedFields) && (
|
|
253
|
+
<Label>
|
|
254
|
+
<FormattedMessage
|
|
255
|
+
id="structure.profile.tab.execute"
|
|
256
|
+
values={{ count: checkedFields.length }}
|
|
257
|
+
/>
|
|
258
|
+
</Label>
|
|
259
|
+
)}
|
|
260
|
+
</Grid.Column>
|
|
261
|
+
</Grid.Row>
|
|
262
|
+
</Grid>
|
|
263
|
+
</>
|
|
264
|
+
{!_.isEmpty(fields) && !loading && (
|
|
265
|
+
<>
|
|
266
|
+
<StructureFieldsTable
|
|
267
|
+
allCheckedInPage={allCheckedInPage}
|
|
268
|
+
onPageCheck={handleCheckedPage}
|
|
269
|
+
profilingMode={profilingMode}
|
|
270
|
+
fields={fields}
|
|
271
|
+
columns={columns}
|
|
272
|
+
onCheckedRow={handleCheckedRow}
|
|
273
|
+
checkedFields={checkedFields}
|
|
274
|
+
/>
|
|
275
|
+
<CursorPagination pageInfo={pageInfo} />
|
|
276
|
+
</>
|
|
277
|
+
)}
|
|
278
|
+
{_.isEmpty(fields) && !loading && (
|
|
279
|
+
<Message icon info>
|
|
280
|
+
<Icon name="search" />
|
|
281
|
+
<Message.Content>
|
|
282
|
+
<Message.Header>
|
|
283
|
+
<FormattedMessage id={`structures.not_found.header`} />
|
|
284
|
+
</Message.Header>
|
|
285
|
+
<FormattedMessage id="structures.not_found.body" />
|
|
286
|
+
</Message.Content>
|
|
287
|
+
</Message>
|
|
288
|
+
)}
|
|
289
|
+
</>
|
|
290
|
+
</>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
38
294
|
StructureFields.propTypes = {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
version: PropTypes.string,
|
|
295
|
+
canExecuteProfiling: PropTypes.bool,
|
|
296
|
+
createProfileGroup: PropTypes.func,
|
|
42
297
|
};
|
|
43
298
|
|
|
44
|
-
const mapStateToProps = (
|
|
45
|
-
|
|
46
|
-
version: state.structureVersion,
|
|
47
|
-
fields: getSortedFields(state),
|
|
48
|
-
columns: getColumns(
|
|
49
|
-
_.propOr(getStructureFieldColumns(state), "columns")(ownProps),
|
|
50
|
-
state.structureFields
|
|
51
|
-
),
|
|
299
|
+
const mapStateToProps = ({ userPermissions }) => ({
|
|
300
|
+
canExecuteProfiling: _.propOr(false, "profile_permission")(userPermissions),
|
|
52
301
|
});
|
|
53
302
|
|
|
54
|
-
|
|
303
|
+
const mapDispatchToProps = { createProfileGroup };
|
|
304
|
+
|
|
305
|
+
export default connect(mapStateToProps, mapDispatchToProps)(StructureFields);
|
|
@@ -74,9 +74,9 @@ const mapStateToProps = (
|
|
|
74
74
|
structure: {
|
|
75
75
|
...structure,
|
|
76
76
|
id: structure.id + "",
|
|
77
|
-
hasDataFields: _.negate(_.isEmpty)(structure
|
|
77
|
+
hasDataFields: _.negate(_.isEmpty)(structure?.data_fields),
|
|
78
78
|
structureFields: _.flow(
|
|
79
|
-
_.
|
|
79
|
+
_.get("data_fields"),
|
|
80
80
|
_.map((dataField) => ({ ...dataField, data_structure_id: dataField.id }))
|
|
81
81
|
)(structure),
|
|
82
82
|
},
|
|
@@ -12,7 +12,6 @@ import StructureConfidentialButton from "./StructureConfidentialButton";
|
|
|
12
12
|
import StructureDeleteButton from "./StructureDeleteButton";
|
|
13
13
|
import StructureGrantSummaryButton from "./StructureGrantSummaryButton";
|
|
14
14
|
import StructureProfileButton from "./StructureProfileButton";
|
|
15
|
-
import StructuresProfileButton from "./StructuresProfileButton";
|
|
16
15
|
import StructureTags from "./StructureTags";
|
|
17
16
|
|
|
18
17
|
const Date = ({ className, icon, label, date }) => {
|
|
@@ -100,10 +99,6 @@ export const StructureSummary = ({
|
|
|
100
99
|
structureClass === "field" ? (
|
|
101
100
|
<StructureProfileButton />
|
|
102
101
|
) : null}
|
|
103
|
-
{userPermissions.profile_permission &&
|
|
104
|
-
structureClass !== "field" ? (
|
|
105
|
-
<StructuresProfileButton />
|
|
106
|
-
) : null}
|
|
107
102
|
{userPermissions.confidential ? (
|
|
108
103
|
<StructureConfidentialButton />
|
|
109
104
|
) : null}
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
STRUCTURE_NOTES_EDIT,
|
|
10
10
|
} from "@truedat/core/routes";
|
|
11
11
|
import { getActiveTab } from "../selectors/getActiveTab";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getTabVisibility,
|
|
14
|
+
getLinkedImplementationsToStructuresColumns,
|
|
15
|
+
} from "../selectors";
|
|
13
16
|
import StructureChildrenRelations from "./StructureChildrenRelations";
|
|
14
17
|
import StructureEvents from "./StructureEvents";
|
|
15
18
|
import StructureGrants from "./StructureGrants";
|
|
@@ -40,7 +43,7 @@ const RuleImplementationsTable = React.lazy(() =>
|
|
|
40
43
|
import("@truedat/dq/components/RuleImplementationsTable")
|
|
41
44
|
);
|
|
42
45
|
|
|
43
|
-
export const StructureTabPane = ({ activeTab, tabVisibility }) => {
|
|
46
|
+
export const StructureTabPane = ({ activeTab, tabVisibility, columns }) => {
|
|
44
47
|
return (
|
|
45
48
|
<ErrorBoundary>
|
|
46
49
|
{activeTab === "fields" && <StructureFields />}
|
|
@@ -82,7 +85,7 @@ export const StructureTabPane = ({ activeTab, tabVisibility }) => {
|
|
|
82
85
|
{activeTab === "events" && <StructureEvents />}
|
|
83
86
|
{activeTab === "roles" && <StructureRoles />}
|
|
84
87
|
{activeTab === "rules" && tabVisibility.rules && (
|
|
85
|
-
<RuleImplementationsTable
|
|
88
|
+
<RuleImplementationsTable columns={columns} />
|
|
86
89
|
)}
|
|
87
90
|
{activeTab === "metadata" && tabVisibility.metadata && (
|
|
88
91
|
<StructureMetadata />
|
|
@@ -96,6 +99,7 @@ export const StructureTabPane = ({ activeTab, tabVisibility }) => {
|
|
|
96
99
|
StructureTabPane.propTypes = {
|
|
97
100
|
activeTab: PropTypes.string,
|
|
98
101
|
tabVisibility: PropTypes.object,
|
|
102
|
+
columns: PropTypes.array,
|
|
99
103
|
};
|
|
100
104
|
|
|
101
105
|
const mapStateToProps = (
|
|
@@ -107,6 +111,7 @@ const mapStateToProps = (
|
|
|
107
111
|
return {
|
|
108
112
|
tabVisibility,
|
|
109
113
|
activeTab,
|
|
114
|
+
columns: getLinkedImplementationsToStructuresColumns(state),
|
|
110
115
|
};
|
|
111
116
|
};
|
|
112
117
|
|
|
@@ -1,41 +1,87 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { render } from "@truedat/test/render";
|
|
3
|
+
import { useQuery } from "@apollo/client";
|
|
3
4
|
import StructureFields from "../StructureFields";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
6
|
+
jest.mock("@apollo/client", () => ({
|
|
7
|
+
...jest.requireActual("@apollo/client"),
|
|
8
|
+
useQuery: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const page = [
|
|
12
|
+
{
|
|
13
|
+
id: 123,
|
|
14
|
+
data_structure_id: 1,
|
|
15
|
+
name: "foo",
|
|
16
|
+
description: "desc1",
|
|
17
|
+
links: [{ resource_id: 123 }],
|
|
18
|
+
metadata: {
|
|
19
|
+
nullable: true,
|
|
20
|
+
precision: "22",
|
|
21
|
+
type: "varchar",
|
|
18
22
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 122,
|
|
26
|
+
data_structure_id: 2,
|
|
27
|
+
name: "bar",
|
|
28
|
+
description: "desc2",
|
|
29
|
+
external_id: "foo",
|
|
30
|
+
metadata: {
|
|
31
|
+
nullable: true,
|
|
32
|
+
precision: "22",
|
|
33
|
+
type: "varchar",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const mockData = {
|
|
39
|
+
data: {
|
|
40
|
+
dataFields: {
|
|
41
|
+
page,
|
|
42
|
+
pageInfo: {
|
|
43
|
+
hasNextPage: true,
|
|
44
|
+
hasPreviousPage: false,
|
|
29
45
|
},
|
|
30
46
|
},
|
|
31
|
-
|
|
47
|
+
},
|
|
48
|
+
loading: false,
|
|
49
|
+
error: null,
|
|
32
50
|
};
|
|
33
51
|
|
|
34
|
-
const
|
|
52
|
+
const renderProps = {
|
|
53
|
+
messages: {
|
|
54
|
+
en: {
|
|
55
|
+
"structure.field.metadata.nullable.true": "Yes",
|
|
56
|
+
"structure.field.links": "Concepts",
|
|
57
|
+
"structure.field.description": "Description",
|
|
58
|
+
"structure.field.metadata.nullable": "Nullable",
|
|
59
|
+
"structure.field.metadata.precision": "Precision",
|
|
60
|
+
"structure.field.metadata.type": "Type",
|
|
61
|
+
"structure.field.name": "Field",
|
|
62
|
+
"structure.execute_profile": "Execute profile",
|
|
63
|
+
"structure.search.placeholder": "Enter a search...",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
state: {
|
|
67
|
+
userPermissions: { profile_permission: true },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
35
70
|
|
|
36
71
|
describe("<StructureFields />", () => {
|
|
37
72
|
it("matches the latest snapshot", () => {
|
|
38
|
-
|
|
73
|
+
useQuery.mockImplementation(() => mockData);
|
|
74
|
+
const { container } = render(<StructureFields />, renderProps);
|
|
39
75
|
expect(container).toMatchSnapshot();
|
|
40
76
|
});
|
|
77
|
+
|
|
78
|
+
it("no render profile button if dont have permissions", () => {
|
|
79
|
+
useQuery.mockImplementation(() => mockData);
|
|
80
|
+
const newRenderProps = {
|
|
81
|
+
...renderProps,
|
|
82
|
+
state: {},
|
|
83
|
+
};
|
|
84
|
+
const { queryByText } = render(<StructureFields />, newRenderProps);
|
|
85
|
+
expect(queryByText("Execute profile")).not.toBeInTheDocument();
|
|
86
|
+
});
|
|
41
87
|
});
|
|
@@ -2,6 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`<StructureFields /> matches the latest snapshot 1`] = `
|
|
4
4
|
<div>
|
|
5
|
+
<div
|
|
6
|
+
class="ui grid"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
class="two column row"
|
|
10
|
+
>
|
|
11
|
+
<div
|
|
12
|
+
class="column"
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
class="ui left icon input"
|
|
16
|
+
>
|
|
17
|
+
<input
|
|
18
|
+
placeholder="Enter a search..."
|
|
19
|
+
type="text"
|
|
20
|
+
value=""
|
|
21
|
+
/>
|
|
22
|
+
<i
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
class="search link icon"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div
|
|
29
|
+
class="right aligned column"
|
|
30
|
+
style="display: flex; align-items: center; justify-content: flex-end;"
|
|
31
|
+
>
|
|
32
|
+
<div
|
|
33
|
+
class="ui fitted toggle checkbox bgOrange"
|
|
34
|
+
>
|
|
35
|
+
<input
|
|
36
|
+
class="hidden"
|
|
37
|
+
readonly=""
|
|
38
|
+
tabindex="0"
|
|
39
|
+
type="checkbox"
|
|
40
|
+
value=""
|
|
41
|
+
/>
|
|
42
|
+
<label />
|
|
43
|
+
</div>
|
|
44
|
+
<button
|
|
45
|
+
class="ui icon disabled left labeled button"
|
|
46
|
+
disabled=""
|
|
47
|
+
style="margin-left: 10px;"
|
|
48
|
+
tabindex="-1"
|
|
49
|
+
>
|
|
50
|
+
<i
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
class="tachometer alternate icon"
|
|
53
|
+
/>
|
|
54
|
+
Execute profile
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div
|
|
59
|
+
class="row"
|
|
60
|
+
style="min-height: 40px; padding-top: 2px; padding-bottom: 2px;"
|
|
61
|
+
>
|
|
62
|
+
<div
|
|
63
|
+
class="right aligned column"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
5
67
|
<table
|
|
6
68
|
class="ui table"
|
|
7
69
|
>
|
|
@@ -55,7 +117,7 @@ exports[`<StructureFields /> matches the latest snapshot 1`] = `
|
|
|
55
117
|
<a
|
|
56
118
|
href="/structures/1"
|
|
57
119
|
>
|
|
58
|
-
|
|
120
|
+
foo
|
|
59
121
|
</a>
|
|
60
122
|
</td>
|
|
61
123
|
<td
|
|
@@ -105,7 +167,7 @@ exports[`<StructureFields /> matches the latest snapshot 1`] = `
|
|
|
105
167
|
<a
|
|
106
168
|
href="/structures/2"
|
|
107
169
|
>
|
|
108
|
-
|
|
170
|
+
bar
|
|
109
171
|
</a>
|
|
110
172
|
</td>
|
|
111
173
|
<td
|
|
@@ -134,5 +196,29 @@ exports[`<StructureFields /> matches the latest snapshot 1`] = `
|
|
|
134
196
|
</tr>
|
|
135
197
|
</tbody>
|
|
136
198
|
</table>
|
|
199
|
+
<div
|
|
200
|
+
class="ui pagination menu"
|
|
201
|
+
role="navigation"
|
|
202
|
+
>
|
|
203
|
+
<a
|
|
204
|
+
aria-label="First"
|
|
205
|
+
class="disabled link item"
|
|
206
|
+
href="/"
|
|
207
|
+
>
|
|
208
|
+
«
|
|
209
|
+
</a>
|
|
210
|
+
<div
|
|
211
|
+
aria-label="Later"
|
|
212
|
+
class="disabled item"
|
|
213
|
+
>
|
|
214
|
+
⟨
|
|
215
|
+
</div>
|
|
216
|
+
<div
|
|
217
|
+
aria-label="Earlier"
|
|
218
|
+
class="item"
|
|
219
|
+
>
|
|
220
|
+
⟩
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
137
223
|
</div>
|
|
138
224
|
`;
|
package/src/components/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import CatalogCustomViewCards from "./CatalogCustomViewCards";
|
|
2
|
+
import CursorPagination from "./CursorPagination";
|
|
2
3
|
import DictionaryRoutes from "./DictionaryRoutes";
|
|
3
4
|
import FilteredNav from "./FilteredNav";
|
|
4
5
|
import GrantRoutes from "./GrantRoutes";
|
|
@@ -53,6 +54,7 @@ import ValueConditionStructure from "./ValueConditionStructure";
|
|
|
53
54
|
|
|
54
55
|
export {
|
|
55
56
|
CatalogCustomViewCards,
|
|
57
|
+
CursorPagination,
|
|
56
58
|
DictionaryRoutes,
|
|
57
59
|
FilteredNav,
|
|
58
60
|
GrantRequestBulkActions,
|
|
@@ -29,7 +29,7 @@ describe("reducers: structureFields", () => {
|
|
|
29
29
|
expect(
|
|
30
30
|
structureFields(fooState, {
|
|
31
31
|
type: fetchStructure.SUCCESS,
|
|
32
|
-
payload: { data }
|
|
32
|
+
payload: { data },
|
|
33
33
|
})
|
|
34
34
|
).toMatchObject(data_fields);
|
|
35
35
|
});
|
|
@@ -60,7 +60,7 @@ describe("reducers: structuresFields", () => {
|
|
|
60
60
|
expect(
|
|
61
61
|
structuresFields(fooState, {
|
|
62
62
|
type: fetchStructure.SUCCESS,
|
|
63
|
-
payload: { data }
|
|
63
|
+
payload: { data },
|
|
64
64
|
})
|
|
65
65
|
).toMatchObject({ 2: data_fields });
|
|
66
66
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getLinkedImplementationsToStructuresColumns,
|
|
3
|
+
defaultImplementationToStructuresColumns,
|
|
4
|
+
} from "..";
|
|
5
|
+
|
|
6
|
+
describe("selectors: getLinkedImplementationsToStructuresColumns", () => {
|
|
7
|
+
it("should return custom ruleColumns when present", () => {
|
|
8
|
+
const ruleImplementationToStructuresColumns = [{ name: "test" }];
|
|
9
|
+
const res = getLinkedImplementationsToStructuresColumns({
|
|
10
|
+
ruleImplementationToStructuresColumns,
|
|
11
|
+
});
|
|
12
|
+
expect(res).toHaveLength(1);
|
|
13
|
+
expect(res).toEqual(ruleImplementationToStructuresColumns);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should return default defaultImplementationToStructuresColumns when no customized", () => {
|
|
17
|
+
const res = getLinkedImplementationsToStructuresColumns({});
|
|
18
|
+
expect(res).toHaveLength(defaultImplementationToStructuresColumns.length);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { createSelector } from "reselect";
|
|
4
|
+
import { FormattedMessage } from "react-intl";
|
|
5
|
+
import DateTime from "@truedat/core/components/DateTime";
|
|
6
|
+
import { formatNumber } from "@truedat/core/services/format";
|
|
7
|
+
import ConceptsLinkDecorator from "@truedat/bg/concepts/components/ConceptsLinkDecorator";
|
|
8
|
+
import RuleResultDecorator from "@truedat/dq/components/RuleResultDecorator";
|
|
9
|
+
import RuleImplementationLink from "@truedat/dq/components/RuleImplementationLink";
|
|
10
|
+
import RuleLink from "@truedat/dq/components/RuleLink";
|
|
11
|
+
|
|
12
|
+
const translateDecorator = (id) =>
|
|
13
|
+
id ? <FormattedMessage id={id} defaultMessage={id} /> : null;
|
|
14
|
+
|
|
15
|
+
const resultTypeDecorator = (result, result_type, resultType) =>
|
|
16
|
+
_.defaultTo(result_type)(resultType) === "errors_number"
|
|
17
|
+
? formatNumber(result)
|
|
18
|
+
: `${result}%`;
|
|
19
|
+
|
|
20
|
+
export const defaultImplementationToStructuresColumns = [
|
|
21
|
+
{
|
|
22
|
+
name: "implementation_key",
|
|
23
|
+
fieldSelector: _.pick(["id", "implementation_key", "rule_id"]),
|
|
24
|
+
fieldDecorator: RuleImplementationLink,
|
|
25
|
+
sort: { name: "implementation_key.raw" },
|
|
26
|
+
width: 2,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "rule",
|
|
30
|
+
fieldSelector: ({ rule, rule_id: id }) => ({ id, name: rule?.name }),
|
|
31
|
+
sort: { name: "rule.name.raw" },
|
|
32
|
+
fieldDecorator: RuleLink,
|
|
33
|
+
width: 2,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "business_concepts",
|
|
37
|
+
fieldSelector: _.path("concepts"),
|
|
38
|
+
fieldDecorator: ConceptsLinkDecorator,
|
|
39
|
+
width: 2,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "last_execution_at",
|
|
43
|
+
fieldSelector: ({ execution_result_info }) => ({
|
|
44
|
+
value: execution_result_info?.date,
|
|
45
|
+
}),
|
|
46
|
+
fieldDecorator: DateTime,
|
|
47
|
+
sort: { name: "execution_result_info.date" },
|
|
48
|
+
width: 2,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "result_type",
|
|
52
|
+
fieldDecorator: (value) =>
|
|
53
|
+
_.isNil(value)
|
|
54
|
+
? null
|
|
55
|
+
: translateDecorator(`ruleImplementations.props.result_type.${value}`),
|
|
56
|
+
sort: { name: "result_type.raw" },
|
|
57
|
+
width: 2,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "minimum",
|
|
61
|
+
fieldSelector: _.pick(["minimum", "result_type"]),
|
|
62
|
+
fieldDecorator: (field) =>
|
|
63
|
+
resultTypeDecorator(field.minimum, field.result_type),
|
|
64
|
+
sort: { name: "minimum" },
|
|
65
|
+
textAlign: "right",
|
|
66
|
+
width: 1,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "goal",
|
|
70
|
+
fieldSelector: _.pick(["goal", "result_type"]),
|
|
71
|
+
fieldDecorator: (field) =>
|
|
72
|
+
resultTypeDecorator(field.goal, field.result_type),
|
|
73
|
+
sort: { name: "goal" },
|
|
74
|
+
textAlign: "right",
|
|
75
|
+
width: 1,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "result",
|
|
79
|
+
fieldSelector: (ruleImplementation) => ({
|
|
80
|
+
ruleResult: ruleImplementation?.execution_result_info,
|
|
81
|
+
ruleImplementation,
|
|
82
|
+
}),
|
|
83
|
+
fieldDecorator: RuleResultDecorator,
|
|
84
|
+
textAlign: "center",
|
|
85
|
+
sort: { name: "execution_result_info.result.sort" },
|
|
86
|
+
width: 2,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const getColumns = (state) => state.ruleImplementationToStructuresColumns;
|
|
91
|
+
|
|
92
|
+
export const getLinkedImplementationsToStructuresColumns = createSelector(
|
|
93
|
+
[getColumns],
|
|
94
|
+
_.defaultTo(defaultImplementationToStructuresColumns)
|
|
95
|
+
);
|
package/src/selectors/index.js
CHANGED
|
@@ -31,8 +31,6 @@ export {
|
|
|
31
31
|
getSortedChildrenRelations,
|
|
32
32
|
getSortedParentRelations,
|
|
33
33
|
} from "./getSortedRelations";
|
|
34
|
-
export { getSortedFields } from "./getSortedFields";
|
|
35
|
-
export { getSortedStructureChildren } from "./getSortedStructureChildren";
|
|
36
34
|
export { getStructureDeletedAt } from "./getStructureDeletedAt";
|
|
37
35
|
export {
|
|
38
36
|
getStructureFieldColumns,
|
|
@@ -55,6 +53,10 @@ export { getSystemTemplate } from "./getSystemTemplate";
|
|
|
55
53
|
export { getTabVisibility } from "./getTabVisibility";
|
|
56
54
|
export { groupOptionsSelector } from "./groupOptionsSelector";
|
|
57
55
|
export { resourceOptionsSelector } from "./resourceOptionsSelector";
|
|
56
|
+
export {
|
|
57
|
+
getLinkedImplementationsToStructuresColumns,
|
|
58
|
+
defaultImplementationToStructuresColumns,
|
|
59
|
+
} from "./getLinkedImplementationsToStructuresColumns";
|
|
58
60
|
export * from "./templateNamesSelector";
|
|
59
61
|
export * from "./getGrantsColumns";
|
|
60
62
|
export * from "./getLineageLevels";
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { useIntl } from "react-intl";
|
|
3
|
-
import { Button, Popup, Icon } from "semantic-ui-react";
|
|
4
|
-
import StructuresProfileForm from "./StructuresProfileForm";
|
|
5
|
-
|
|
6
|
-
export const StructuresProfileButton = () => {
|
|
7
|
-
const { formatMessage } = useIntl();
|
|
8
|
-
const trigger = (
|
|
9
|
-
<Button
|
|
10
|
-
icon={<Icon name="tachometer alternate" />}
|
|
11
|
-
className="button basic icon group-actions profile structureButton"
|
|
12
|
-
data-tooltip={formatMessage({ id: "structure.execute_profile" })}
|
|
13
|
-
/>
|
|
14
|
-
);
|
|
15
|
-
return (
|
|
16
|
-
<Popup
|
|
17
|
-
className="StructuresProfileFrom-popup"
|
|
18
|
-
on="click"
|
|
19
|
-
position="bottom right"
|
|
20
|
-
flowing
|
|
21
|
-
trigger={trigger}
|
|
22
|
-
>
|
|
23
|
-
<StructuresProfileForm />
|
|
24
|
-
</Popup>
|
|
25
|
-
);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export default StructuresProfileButton;
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { includes, map, without } from "lodash/fp";
|
|
2
|
-
import React, { useState } from "react";
|
|
3
|
-
import PropTypes from "prop-types";
|
|
4
|
-
import { useIntl } from "react-intl";
|
|
5
|
-
import { connect } from "react-redux";
|
|
6
|
-
import { Form, List, Header } from "semantic-ui-react";
|
|
7
|
-
import { getSortedFields } from "../selectors";
|
|
8
|
-
import { createProfileGroup } from "../routines";
|
|
9
|
-
|
|
10
|
-
export const StructuresProfileForm = ({ fields, createProfileGroup }) => {
|
|
11
|
-
const [selected, setSelected] = useState([]);
|
|
12
|
-
const { formatMessage } = useIntl();
|
|
13
|
-
const disabled = selected?.length === 0;
|
|
14
|
-
|
|
15
|
-
const handleSubmit = e => {
|
|
16
|
-
const payload = {
|
|
17
|
-
data_structure_ids: selected
|
|
18
|
-
};
|
|
19
|
-
createProfileGroup(payload);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const handleCheckAll = () => setSelected(map("data_structure_id")(fields));
|
|
23
|
-
|
|
24
|
-
const handleClearAll = () => setSelected([]);
|
|
25
|
-
|
|
26
|
-
const handleChange = (e, { checked, value }) => {
|
|
27
|
-
e && e.preventDefault() && e.stopPropagation();
|
|
28
|
-
setSelected(checked ? [...selected, value] : without([value])(selected));
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
return fields ? (
|
|
32
|
-
<Form onSubmit={handleSubmit} className="StructuresProfileForm">
|
|
33
|
-
<List horizontal link>
|
|
34
|
-
<List.Item
|
|
35
|
-
as="a"
|
|
36
|
-
onClick={handleCheckAll}
|
|
37
|
-
content={formatMessage({
|
|
38
|
-
id: "selectAll",
|
|
39
|
-
defaultMessage: "Select All"
|
|
40
|
-
})}
|
|
41
|
-
/>
|
|
42
|
-
<List.Item
|
|
43
|
-
as="a"
|
|
44
|
-
onClick={handleClearAll}
|
|
45
|
-
content={formatMessage({ id: "clearAll", defaultMessage: "Clear" })}
|
|
46
|
-
/>
|
|
47
|
-
</List>
|
|
48
|
-
<Header
|
|
49
|
-
as="h5"
|
|
50
|
-
content={formatMessage({
|
|
51
|
-
id: "tabs.dd.fields",
|
|
52
|
-
defaultMessage: "Fields"
|
|
53
|
-
})}
|
|
54
|
-
/>
|
|
55
|
-
<div className="scrollable">
|
|
56
|
-
{fields.map(({ data_structure_id: id, name }) => (
|
|
57
|
-
<Form.Checkbox
|
|
58
|
-
key={id}
|
|
59
|
-
name={name}
|
|
60
|
-
label={<label>{name}</label>}
|
|
61
|
-
value={id}
|
|
62
|
-
checked={includes(id)(selected)}
|
|
63
|
-
onChange={handleChange}
|
|
64
|
-
/>
|
|
65
|
-
))}
|
|
66
|
-
</div>
|
|
67
|
-
<Form.Button
|
|
68
|
-
type="submit"
|
|
69
|
-
content={formatMessage({ id: "structure.execute_profile" })}
|
|
70
|
-
disabled={disabled}
|
|
71
|
-
/>
|
|
72
|
-
</Form>
|
|
73
|
-
) : null;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
StructuresProfileForm.propTypes = {
|
|
77
|
-
createProfileGroup: PropTypes.func,
|
|
78
|
-
fields: PropTypes.array
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const mapStateToProps = state => ({
|
|
82
|
-
fields: getSortedFields(state)
|
|
83
|
-
});
|
|
84
|
-
const mapDispatchToProps = { createProfileGroup };
|
|
85
|
-
|
|
86
|
-
export default connect(
|
|
87
|
-
mapStateToProps,
|
|
88
|
-
mapDispatchToProps
|
|
89
|
-
)(StructuresProfileForm);
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { shallow } from "enzyme";
|
|
3
|
-
import { render, waitFor } from "@testing-library/react";
|
|
4
|
-
import userEvent from "@testing-library/user-event";
|
|
5
|
-
import { intl } from "@truedat/test/intl-stub";
|
|
6
|
-
import { StructuresProfileForm } from "../StructuresProfileForm";
|
|
7
|
-
|
|
8
|
-
// workaround for enzyme issue with React.useContext
|
|
9
|
-
// see https://github.com/airbnb/enzyme/issues/2176#issuecomment-532361526
|
|
10
|
-
jest.spyOn(React, "useContext").mockImplementation(() => intl);
|
|
11
|
-
|
|
12
|
-
describe("<StructuresProfileForm />", () => {
|
|
13
|
-
const createProfileGroup = jest.fn();
|
|
14
|
-
const fields = [{ data_structure_id: 1, name: "bar" }];
|
|
15
|
-
const structure = {
|
|
16
|
-
id: 1,
|
|
17
|
-
external_id: 1,
|
|
18
|
-
source: { external_id: 1 },
|
|
19
|
-
};
|
|
20
|
-
const props = {
|
|
21
|
-
createProfileGroup,
|
|
22
|
-
fields,
|
|
23
|
-
structure,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
it("matches the latest snapshot", () => {
|
|
27
|
-
const wrapper = shallow(<StructuresProfileForm {...props} />);
|
|
28
|
-
expect(wrapper).toMatchSnapshot();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("submit button is disable when no fields are selected", async () => {
|
|
32
|
-
const { getByText } = render(<StructuresProfileForm {...props} />);
|
|
33
|
-
|
|
34
|
-
await waitFor(() => getByText("structure.execute_profile"));
|
|
35
|
-
|
|
36
|
-
expect(getByText("structure.execute_profile")).toHaveAttribute("disabled");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("calls create job when button handleSubmit click", async () => {
|
|
40
|
-
const { getByText } = render(<StructuresProfileForm {...props} />);
|
|
41
|
-
|
|
42
|
-
await waitFor(() => userEvent.click(getByText("bar")));
|
|
43
|
-
|
|
44
|
-
expect(getByText("structure.execute_profile")).not.toHaveAttribute(
|
|
45
|
-
"disabled"
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
await waitFor(() =>
|
|
49
|
-
userEvent.click(getByText("structure.execute_profile"))
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
expect(createProfileGroup).toHaveBeenCalledWith({
|
|
53
|
-
data_structure_ids: [structure.id],
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
-
|
|
3
|
-
exports[`<StructuresProfileForm /> matches the latest snapshot 1`] = `
|
|
4
|
-
<Form
|
|
5
|
-
as="form"
|
|
6
|
-
className="StructuresProfileForm"
|
|
7
|
-
onSubmit={[Function]}
|
|
8
|
-
>
|
|
9
|
-
<List
|
|
10
|
-
horizontal={true}
|
|
11
|
-
link={true}
|
|
12
|
-
>
|
|
13
|
-
<ListItem
|
|
14
|
-
as="a"
|
|
15
|
-
content="Select All"
|
|
16
|
-
onClick={[Function]}
|
|
17
|
-
/>
|
|
18
|
-
<ListItem
|
|
19
|
-
as="a"
|
|
20
|
-
content="Clear"
|
|
21
|
-
onClick={[Function]}
|
|
22
|
-
/>
|
|
23
|
-
</List>
|
|
24
|
-
<Header
|
|
25
|
-
as="h5"
|
|
26
|
-
content="tabs.dd.fields"
|
|
27
|
-
/>
|
|
28
|
-
<div
|
|
29
|
-
className="scrollable"
|
|
30
|
-
>
|
|
31
|
-
<FormCheckbox
|
|
32
|
-
as={[Function]}
|
|
33
|
-
checked={false}
|
|
34
|
-
control={[Function]}
|
|
35
|
-
key="1"
|
|
36
|
-
label={
|
|
37
|
-
<label>
|
|
38
|
-
bar
|
|
39
|
-
</label>
|
|
40
|
-
}
|
|
41
|
-
name="bar"
|
|
42
|
-
onChange={[Function]}
|
|
43
|
-
value={1}
|
|
44
|
-
/>
|
|
45
|
-
</div>
|
|
46
|
-
<FormButton
|
|
47
|
-
as={[Function]}
|
|
48
|
-
content="structure.execute_profile"
|
|
49
|
-
control={[Function]}
|
|
50
|
-
disabled={true}
|
|
51
|
-
type="submit"
|
|
52
|
-
/>
|
|
53
|
-
</Form>
|
|
54
|
-
`;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { getSortedFields } from "..";
|
|
2
|
-
|
|
3
|
-
const projects = [
|
|
4
|
-
{ name: "foo2", type: "project" },
|
|
5
|
-
{ name: "foo1", type: "project" },
|
|
6
|
-
];
|
|
7
|
-
const schemas = [
|
|
8
|
-
{ name: "bar2", type: "schema", metadata: { order: 1 } },
|
|
9
|
-
{ name: "bar1", type: "schema", metadata: { order: 1 } },
|
|
10
|
-
];
|
|
11
|
-
const documents = [
|
|
12
|
-
{ name: "baz1", type: "document" },
|
|
13
|
-
{ name: "baz2", type: "document" },
|
|
14
|
-
];
|
|
15
|
-
const others = [
|
|
16
|
-
{ name: "bay1", type: "other", metadata: { order: "0" } },
|
|
17
|
-
{ name: "bay2", type: "other", metadata: { order: "0" } },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
describe("selectors: getSortedFields", () => {
|
|
21
|
-
const state = {
|
|
22
|
-
structureFields: [...documents, ...projects, ...schemas, ...others],
|
|
23
|
-
};
|
|
24
|
-
const foo = [
|
|
25
|
-
...others,
|
|
26
|
-
...[
|
|
27
|
-
{ name: "bar1", type: "schema", metadata: { order: 1 } },
|
|
28
|
-
{ name: "bar2", type: "schema", metadata: { order: 1 } },
|
|
29
|
-
],
|
|
30
|
-
...[
|
|
31
|
-
{ name: "foo1", type: "project" },
|
|
32
|
-
{ name: "foo2", type: "project" },
|
|
33
|
-
],
|
|
34
|
-
...documents,
|
|
35
|
-
];
|
|
36
|
-
it("get fields sorted by type criteria and name", () => {
|
|
37
|
-
expect(getSortedFields(state)).toEqual(foo);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { getSortedStructureChildren } from "..";
|
|
2
|
-
|
|
3
|
-
const projects = [
|
|
4
|
-
{ name: "foo2", type: "project" },
|
|
5
|
-
{ name: "foo1", type: "project" }
|
|
6
|
-
];
|
|
7
|
-
const schemas = [
|
|
8
|
-
{ name: "bar1", type: "schema" },
|
|
9
|
-
{ name: "bar2", type: "schema" }
|
|
10
|
-
];
|
|
11
|
-
const documents = [
|
|
12
|
-
{ name: "baz1", type: "document" },
|
|
13
|
-
{ name: "baz2", type: "document" }
|
|
14
|
-
];
|
|
15
|
-
const others = [
|
|
16
|
-
{ name: "bay1", type: "other" },
|
|
17
|
-
{ name: "bay2", type: "other" }
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
describe("selectors: getSortedStructureChildren", () => {
|
|
21
|
-
const state = {
|
|
22
|
-
structureChildren: [...others, ...documents, ...schemas, ...projects]
|
|
23
|
-
};
|
|
24
|
-
const foo = [
|
|
25
|
-
...[
|
|
26
|
-
{ name: "foo1", type: "project" },
|
|
27
|
-
{ name: "foo2", type: "project" }
|
|
28
|
-
],
|
|
29
|
-
...schemas,
|
|
30
|
-
...documents,
|
|
31
|
-
...others
|
|
32
|
-
];
|
|
33
|
-
it("get structure children sorted by type criteria and name", () => {
|
|
34
|
-
expect(getSortedStructureChildren(state)).toEqual(foo);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import _ from "lodash/fp";
|
|
2
|
-
import { createSelector } from "reselect";
|
|
3
|
-
import { getStructureSortingCriteria } from "./getStructureSortingCriteria";
|
|
4
|
-
|
|
5
|
-
export const getSortedFields = createSelector(
|
|
6
|
-
[_.propOr([], "structureFields"), getStructureSortingCriteria],
|
|
7
|
-
(structureFields, structureSortingCriteria) =>
|
|
8
|
-
_.sortBy(structureSortingCriteria)(structureFields)
|
|
9
|
-
);
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import _ from "lodash/fp";
|
|
2
|
-
import { createSelector } from "reselect";
|
|
3
|
-
import { getStructureSortingCriteria } from "./getStructureSortingCriteria";
|
|
4
|
-
|
|
5
|
-
export const getSortedStructureChildren = createSelector(
|
|
6
|
-
[
|
|
7
|
-
_.propOr([], "structureChildren"),
|
|
8
|
-
_.propOr([], "structureSiblings"),
|
|
9
|
-
_.propOr([], "systemStructures"),
|
|
10
|
-
getStructureSortingCriteria,
|
|
11
|
-
],
|
|
12
|
-
(
|
|
13
|
-
structureChildren,
|
|
14
|
-
structureSiblings,
|
|
15
|
-
systemStructures,
|
|
16
|
-
structureSortingCriteria
|
|
17
|
-
) =>
|
|
18
|
-
_.size(structureChildren)
|
|
19
|
-
? _.sortBy(structureSortingCriteria)(structureChildren)
|
|
20
|
-
: _.size(structureSiblings)
|
|
21
|
-
? _.sortBy(structureSortingCriteria)(structureSiblings)
|
|
22
|
-
: _.sortBy(structureSortingCriteria)(systemStructures)
|
|
23
|
-
);
|