@truedat/lm 8.7.4 → 8.8.0

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.
Files changed (26) hide show
  1. package/package.json +3 -3
  2. package/src/components/ConfirmDeleteRelation.js +14 -2
  3. package/src/components/LinksPane.js +116 -88
  4. package/src/components/LinksPaneDataCell.js +72 -0
  5. package/src/components/LinksPaneRow.js +18 -0
  6. package/src/components/LinksPaneTypeFilter.js +82 -0
  7. package/src/components/__tests__/ConfirmDeleteRelation.spec.js +9 -0
  8. package/src/components/__tests__/LinksPane.spec.js +60 -95
  9. package/src/components/__tests__/LinksPaneDataCell.spec.js +192 -0
  10. package/src/components/__tests__/LinksPaneRow.spec.js +94 -0
  11. package/src/components/__tests__/LinksPaneTypeFilter.spec.js +164 -0
  12. package/src/components/__tests__/__snapshots__/ConfirmDeleteRelation.spec.js.snap +2 -2
  13. package/src/components/__tests__/__snapshots__/ImplementationLinks.spec.js.snap +4 -4
  14. package/src/components/__tests__/__snapshots__/LinksPane.spec.js.snap +98 -40
  15. package/src/hooks/__tests__/useFilteredLinks.spec.js +116 -0
  16. package/src/hooks/__tests__/useSortedLinks.spec.js +129 -0
  17. package/src/hooks/__tests__/useStructureNotes.spec.js +138 -0
  18. package/src/hooks/__tests__/useTruncatedPopup.spec.js +109 -0
  19. package/src/hooks/useFilteredLinks.js +98 -0
  20. package/src/hooks/useSortedLinks.js +8 -0
  21. package/src/hooks/useStructureNotes.js +52 -0
  22. package/src/hooks/useTruncatedPopup.js +30 -0
  23. package/src/styles/LinksPane.less +117 -0
  24. package/src/styles/RelationActions.less +7 -0
  25. package/src/utils/__tests__/linksPaneHelpers.spec.js +316 -0
  26. package/src/utils/linksPaneHelpers.js +164 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/lm",
3
- "version": "8.7.4",
3
+ "version": "8.8.0",
4
4
  "description": "Truedat Link Manager",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -54,7 +54,7 @@
54
54
  "@testing-library/jest-dom": "^6.6.3",
55
55
  "@testing-library/react": "^16.3.0",
56
56
  "@testing-library/user-event": "^14.6.1",
57
- "@truedat/test": "8.7.4",
57
+ "@truedat/test": "8.8.0",
58
58
  "identity-obj-proxy": "^3.0.0",
59
59
  "jest": "^29.7.0",
60
60
  "redux-saga-test-plan": "^4.0.6"
@@ -86,5 +86,5 @@
86
86
  "semantic-ui-react": "^3.0.0-beta.2",
87
87
  "swr": "^2.3.3"
88
88
  },
89
- "gitHead": "74bf59ddf29e05b518dc206271c4f1215d66a942"
89
+ "gitHead": "006b9cd6486cd37976d4dc06ede1a48e37175f30"
90
90
  }
@@ -10,8 +10,20 @@ export const ConfirmDeleteRelation = ({ id, deleteRelation, resourceType }) => (
10
10
  icon="trash"
11
11
  color="red"
12
12
  trigger={
13
- <Button basic color="red" icon size="mini">
14
- <Icon name="trash alternate outline" color="red" />
13
+ <Button
14
+ basic
15
+ className="relation-actions__delete-button"
16
+ color="red"
17
+ compact
18
+ icon
19
+ size="mini"
20
+ >
21
+ <Icon
22
+ className="relation-actions__delete-icon"
23
+ name="trash alternate outline"
24
+ color="red"
25
+ size="small"
26
+ />
15
27
  </Button>
16
28
  }
17
29
  header={
@@ -1,95 +1,107 @@
1
1
  import _ from "lodash/fp";
2
2
  import PropTypes from "prop-types";
3
- import { useEffect, useState } from "react";
4
- import { Table, Header, Grid } from "semantic-ui-react";
3
+ import { useMemo, useState } from "react";
5
4
  import { FormattedMessage } from "react-intl";
6
- import { columnDecorator, columnPredicate } from "@truedat/core/services";
7
- import { useDataStructureSearch } from "@truedat/dd/hooks/useStructures";
8
-
9
- const Row = ({ columns, link }) => (
10
- <Table.Row>
11
- {columns.map((column, i) => (
12
- <Table.Cell
13
- key={i}
14
- className={_.prop("className")(column)}
15
- textAlign={column.textAlign}
16
- content={columnDecorator(column)(link)}
17
- />
18
- ))}
19
- </Table.Row>
20
- );
21
-
22
- Row.propTypes = {
23
- columns: PropTypes.array,
24
- link: PropTypes.object,
5
+ import { Grid, Header, Table } from "semantic-ui-react";
6
+ import { useStructureNotes } from "../hooks/useStructureNotes";
7
+ import { useFilteredLinks } from "../hooks/useFilteredLinks";
8
+ import { useSortedLinks } from "../hooks/useSortedLinks";
9
+ import {
10
+ isSortableColumn,
11
+ mergeClassNames,
12
+ columnClassName,
13
+ columnWidthStyle,
14
+ tableWidthStyle,
15
+ filterColumns,
16
+ } from "../utils/linksPaneHelpers";
17
+ import { LinksPaneTypeFilter } from "./LinksPaneTypeFilter";
18
+ import { LinksPaneRow } from "./LinksPaneRow";
19
+
20
+ const onSort = (column, setSort, sort) => {
21
+ if (!isSortableColumn(column)) return;
22
+
23
+ const name = _.prop("name")(column);
24
+ setSort((currentSort) =>
25
+ currentSort.column === name
26
+ ? {
27
+ column: name,
28
+ direction:
29
+ currentSort.direction === "ascending" ? "descending" : "ascending",
30
+ }
31
+ : { column: name, direction: "ascending" },
32
+ );
25
33
  };
26
34
 
27
35
  export const Links = ({ tag, sourceType, targetType, columns, links }) => {
28
- const [enrichedLinks, setEnrichedLinks] = useState(links);
29
- const { trigger } = useDataStructureSearch();
30
-
31
- useEffect(() => {
32
- const fetchStructureNotes = async () => {
33
- const structureIds = _.flow(
34
- _.filter(_.propEq("resource_type", "data_structure")),
35
- _.map(({ resource_id }) => parseInt(resource_id)),
36
- _.uniq,
37
- )(links);
38
-
39
- if (_.isEmpty(structureIds)) {
40
- setEnrichedLinks(links);
41
- return;
42
- }
43
-
44
- try {
45
- const response = await trigger({
46
- must: { ids: structureIds },
47
- page: 0,
48
- size: structureIds.length,
49
- });
50
-
51
- const structuresMap = _.flow(
52
- _.getOr([], "data.data"),
53
- _.keyBy("id"),
54
- )(response);
55
-
56
- const enriched = _.map((link) => {
57
- if (link.resource_type === "data_structure") {
58
- const structure = structuresMap[link.resource_id];
59
- return {
60
- ...link,
61
- note: structure?.note || null,
62
- };
63
- }
64
- return link;
65
- })(links);
36
+ const enrichedLinks = useStructureNotes(links);
37
+ const [selectedTypes, setSelectedTypes] = useState([]);
38
+ const [selectedRelationTypes, setSelectedRelationTypes] = useState([]);
39
+ const [sort, setSort] = useState({ column: null, direction: "ascending" });
40
+
41
+ const filteredColumns = useMemo(
42
+ () => filterColumns(columns, enrichedLinks, tag),
43
+ [columns, enrichedLinks, tag],
44
+ );
66
45
 
67
- setEnrichedLinks(enriched);
68
- } catch (error) {
69
- setEnrichedLinks(links);
70
- }
71
- };
46
+ const { filteredLinks, typeOptions, relationTypeOptions } = useFilteredLinks({
47
+ enrichedLinks,
48
+ selectedTypes,
49
+ selectedRelationTypes,
50
+ });
72
51
 
73
- fetchStructureNotes();
74
- }, [links, trigger]);
52
+ const linksData = useSortedLinks(filteredLinks, filteredColumns, sort);
75
53
 
76
- const linksData = _.orderBy(["business_concept_id", "name"])(["desc", "asc"])(
77
- enrichedLinks,
78
- );
54
+ const handleSort = (column) => onSort(column, setSort, sort);
79
55
 
80
- const filteredColumns = filterColumns(columns, enrichedLinks);
56
+ const headerRow = filteredColumns.map(({ name }, key) => {
57
+ const column = filteredColumns[key];
58
+ const sortable = isSortableColumn(column);
81
59
 
82
- const headerRow = filteredColumns.map(({ name }, key) => (
83
- <Table.HeaderCell
84
- key={key}
85
- content={
86
- <FormattedMessage id={`${targetType}.${name}`} defaultMessage={name} />
87
- }
88
- />
89
- ));
60
+ return (
61
+ <Table.HeaderCell
62
+ className={mergeClassNames(
63
+ sortable ? undefined : "disabled",
64
+ columnClassName(column, true),
65
+ )}
66
+ key={key}
67
+ onClick={sortable ? () => handleSort(column) : undefined}
68
+ sorted={sortable && sort.column === name ? sort.direction : null}
69
+ style={columnWidthStyle(column)}
70
+ content={
71
+ <span className="links-pane__filter-label">
72
+ <span className="links-pane__filter-label-text">
73
+ <FormattedMessage
74
+ id={`${targetType}.${name}`}
75
+ defaultMessage={name}
76
+ />
77
+ </span>
78
+ {name === "type" && (
79
+ <LinksPaneTypeFilter
80
+ onChange={setSelectedTypes}
81
+ options={typeOptions}
82
+ selectedTypes={selectedTypes}
83
+ />
84
+ )}
85
+ {name === "relation.relation_type_name" && (
86
+ <LinksPaneTypeFilter
87
+ onChange={setSelectedRelationTypes}
88
+ messageId={(option) =>
89
+ option === ""
90
+ ? "filter.empty"
91
+ : `conceptRelations.relationType.${option}`
92
+ }
93
+ options={relationTypeOptions}
94
+ selectedTypes={selectedRelationTypes}
95
+ />
96
+ )}
97
+ </span>
98
+ }
99
+ />
100
+ );
101
+ });
90
102
 
91
103
  const renderBodyRow = (link, i) => (
92
- <Row key={i} columns={filteredColumns} link={link} />
104
+ <LinksPaneRow key={i} columns={filteredColumns} link={link} />
93
105
  );
94
106
 
95
107
  return (
@@ -97,17 +109,36 @@ export const Links = ({ tag, sourceType, targetType, columns, links }) => {
97
109
  <Header as="h3">
98
110
  {tag === "disabled" || tag === "deleted" ? (
99
111
  <FormattedMessage id={`${sourceType}DisabledRelation.table.title`} />
100
- ) : tag === "\uffee" ? (
112
+ ) : tag === "" ? (
101
113
  <FormattedMessage id={`${sourceType}Relation.table.title`} />
102
114
  ) : (
103
115
  <FormattedMessage id={`${sourceType}Relation.master.table.title`} />
104
116
  )}
105
117
  </Header>
106
- <Table
107
- headerRow={headerRow}
108
- renderBodyRow={renderBodyRow}
109
- tableData={linksData}
110
- />
118
+ <div className="links-pane__table-scroll">
119
+ <Table
120
+ celled
121
+ className="links-pane__table"
122
+ compact
123
+ sortable
124
+ style={tableWidthStyle(filteredColumns)}
125
+ unstackable
126
+ >
127
+ <colgroup>
128
+ {filteredColumns.map((column) => (
129
+ <col
130
+ className="links-pane__column"
131
+ key={_.prop("name")(column)}
132
+ style={columnWidthStyle(column)}
133
+ />
134
+ ))}
135
+ </colgroup>
136
+ <Table.Header>
137
+ <Table.Row>{headerRow}</Table.Row>
138
+ </Table.Header>
139
+ <Table.Body>{linksData.map(renderBodyRow)}</Table.Body>
140
+ </Table>
141
+ </div>
111
142
  </>
112
143
  );
113
144
  };
@@ -120,9 +151,6 @@ Links.propTypes = {
120
151
  links: PropTypes.array,
121
152
  };
122
153
 
123
- const filterColumns = (columns, links) =>
124
- _.filter((c) => _.any(columnPredicate(c))(links))(columns);
125
-
126
154
  export const LinksPane = ({ groups, sourceType, targetType, linksActions }) => (
127
155
  <Grid>
128
156
  <Grid.Row>
@@ -0,0 +1,72 @@
1
+ import _ from "lodash/fp";
2
+ import PropTypes from "prop-types";
3
+ import { Popup, Table } from "semantic-ui-react";
4
+ import { useTruncatedPopup } from "../hooks/useTruncatedPopup";
5
+ import {
6
+ getColumnValue,
7
+ normalizeValue,
8
+ mergeClassNames,
9
+ columnClassName,
10
+ columnWidthStyle,
11
+ POPUP_EXCLUDED_COLUMNS,
12
+ NOTE_COLUMN,
13
+ cellContent,
14
+ } from "../utils/linksPaneHelpers";
15
+
16
+ export const LinksPaneDataCell = ({ column, link }) => {
17
+ const {
18
+ cellRef,
19
+ popupOpen,
20
+ popupContent,
21
+ handleMouseEnter,
22
+ handleMouseLeave,
23
+ setPopupOpen,
24
+ } = useTruncatedPopup();
25
+ const rawValue = getColumnValue(column, link);
26
+ const normalizedValue = normalizeValue(rawValue);
27
+ const popupEnabled =
28
+ !_.includes(_.prop("name")(column))(POPUP_EXCLUDED_COLUMNS) &&
29
+ !_.isEmpty(normalizedValue);
30
+
31
+ const renderCell = (props = {}) => (
32
+ <Table.Cell
33
+ {...props}
34
+ className={mergeClassNames(
35
+ _.prop("className")(column),
36
+ columnClassName(column),
37
+ )}
38
+ textAlign={column.textAlign}
39
+ style={columnWidthStyle(column)}
40
+ content={cellContent(column, link, rawValue)}
41
+ />
42
+ );
43
+
44
+ if (!popupEnabled) return renderCell();
45
+
46
+ return (
47
+ <>
48
+ {renderCell({
49
+ onMouseEnter: handleMouseEnter,
50
+ onMouseLeave: handleMouseLeave,
51
+ ref: cellRef,
52
+ })}
53
+ <Popup
54
+ className="links-pane__truncated-popup"
55
+ closeOnDocumentClick={false}
56
+ content={popupContent}
57
+ context={cellRef}
58
+ onClose={() => setPopupOpen(false)}
59
+ open={popupOpen}
60
+ position="top center"
61
+ positionFixed
62
+ />
63
+ </>
64
+ );
65
+ };
66
+
67
+ LinksPaneDataCell.propTypes = {
68
+ column: PropTypes.object,
69
+ link: PropTypes.object,
70
+ };
71
+
72
+ export default LinksPaneDataCell;
@@ -0,0 +1,18 @@
1
+ import PropTypes from "prop-types";
2
+ import { Table } from "semantic-ui-react";
3
+ import { LinksPaneDataCell } from "./LinksPaneDataCell";
4
+
5
+ export const LinksPaneRow = ({ columns, link }) => (
6
+ <Table.Row>
7
+ {columns.map((column, i) => (
8
+ <LinksPaneDataCell key={i} column={column} link={link} />
9
+ ))}
10
+ </Table.Row>
11
+ );
12
+
13
+ LinksPaneRow.propTypes = {
14
+ columns: PropTypes.array,
15
+ link: PropTypes.object,
16
+ };
17
+
18
+ export default LinksPaneRow;
@@ -0,0 +1,82 @@
1
+ import _ from "lodash/fp";
2
+ import PropTypes from "prop-types";
3
+ import { Checkbox, Icon, Popup } from "semantic-ui-react";
4
+ import { useIntl } from "react-intl";
5
+
6
+ export const LinksPaneTypeFilter = ({
7
+ onChange,
8
+ options,
9
+ selectedTypes,
10
+ messageId = (option) => `structure.type.${option}.text`,
11
+ }) => {
12
+ const { formatMessage } = useIntl();
13
+
14
+ if (_.isEmpty(options)) return null;
15
+
16
+ const activeSelectedTypes = _.filter((type) => options.includes(type))(
17
+ selectedTypes,
18
+ );
19
+ const selected = new Set(activeSelectedTypes);
20
+ const allSelected = _.isEmpty(activeSelectedTypes);
21
+
22
+ const toggleType = (option) => {
23
+ const current = allSelected ? new Set(options) : new Set(selected);
24
+ current.has(option) ? current.delete(option) : current.add(option);
25
+ onChange(current.size === options.length ? [] : Array.from(current));
26
+ };
27
+
28
+ const handleOptionClick = (event, option) => {
29
+ event.stopPropagation();
30
+ toggleType(option);
31
+ };
32
+
33
+ return (
34
+ <span onClick={(event) => event.stopPropagation()}>
35
+ <Popup
36
+ basic
37
+ flowing
38
+ hoverable
39
+ on="click"
40
+ position="bottom left"
41
+ positionFixed
42
+ className="links-pane__filter-menu"
43
+ trigger={
44
+ <Icon
45
+ className="links-pane__filter-icon"
46
+ color={allSelected ? undefined : "blue"}
47
+ name="filter"
48
+ />
49
+ }
50
+ >
51
+ <div onClick={(event) => event.stopPropagation()}>
52
+ {options.map((option) => (
53
+ <div
54
+ key={option}
55
+ onClick={(event) => handleOptionClick(event, option)}
56
+ className="links-pane__filter-menu-item"
57
+ >
58
+ <Checkbox
59
+ checked={allSelected || selected.has(option)}
60
+ label={formatMessage({
61
+ id: messageId(option),
62
+ defaultMessage: option === "" ? "(Empty)" : option,
63
+ })}
64
+ onChange={(event) => handleOptionClick(event, option)}
65
+ onClick={(event) => event.stopPropagation()}
66
+ />
67
+ </div>
68
+ ))}
69
+ </div>
70
+ </Popup>
71
+ </span>
72
+ );
73
+ };
74
+
75
+ LinksPaneTypeFilter.propTypes = {
76
+ onChange: PropTypes.func.isRequired,
77
+ messageId: PropTypes.func,
78
+ options: PropTypes.array,
79
+ selectedTypes: PropTypes.array,
80
+ };
81
+
82
+ export default LinksPaneTypeFilter;
@@ -21,6 +21,15 @@ describe("<ConfirmDeleteRelation />", () => {
21
21
  <ConfirmDeleteRelation {...props} />,
22
22
  renderOpts,
23
23
  );
24
+
25
+ expect(container.querySelector("button")).toHaveClass(
26
+ "relation-actions__delete-button",
27
+ );
28
+ expect(container.querySelector("button")).not.toHaveAttribute("style");
29
+ expect(container.querySelector("i")).toHaveClass(
30
+ "relation-actions__delete-icon",
31
+ );
32
+ expect(container.querySelector("i")).not.toHaveAttribute("style");
24
33
  expect(container).toMatchSnapshot();
25
34
  });
26
35
  });