@truedat/core 8.10.1 → 8.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "8.10.1",
3
+ "version": "8.10.3",
4
4
  "description": "Truedat Web Core",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -13,6 +13,9 @@
13
13
  ],
14
14
  "author": "Bluetab Solutions",
15
15
  "license": "GPL-3.0",
16
+ "engines": {
17
+ "node": ">=22.22.0"
18
+ },
16
19
  "publishConfig": {
17
20
  "access": "public"
18
21
  },
@@ -38,7 +41,7 @@
38
41
  ],
39
42
  "testEnvironment": "jsdom",
40
43
  "transform": {
41
- "\\.js$": [
44
+ "\\.m?js$": [
42
45
  "babel-jest",
43
46
  {
44
47
  "rootMode": "upward"
@@ -46,7 +49,7 @@
46
49
  ]
47
50
  },
48
51
  "transformIgnorePatterns": [
49
- "/node_modules/(?!marked|turndown|@tiptap)/"
52
+ "/node_modules/(?!marked|turndown|@tiptap|react-router|cookie-es)/"
50
53
  ]
51
54
  },
52
55
  "devDependencies": {
@@ -54,7 +57,7 @@
54
57
  "@testing-library/jest-dom": "^6.6.3",
55
58
  "@testing-library/react": "^16.3.0",
56
59
  "@testing-library/user-event": "^14.6.1",
57
- "@truedat/test": "8.10.1",
60
+ "@truedat/test": "8.10.3",
58
61
  "identity-obj-proxy": "^3.0.0",
59
62
  "jest": "^29.7.0",
60
63
  "redux-saga-test-plan": "^4.0.6"
@@ -78,15 +81,15 @@
78
81
  "path-to-regexp": "^8.2.0",
79
82
  "prop-types": "^15.8.1",
80
83
  "query-string": "^7.1.3",
81
- "react": "^19.1.0",
84
+ "react": "^19.2.7",
82
85
  "react-csv": "^2.2.2",
83
- "react-dom": "^19.1.0",
86
+ "react-dom": "^19.2.7",
84
87
  "react-dropzone": "^14.3.8",
85
88
  "react-hook-form": "^7.56.4",
86
89
  "react-intl": "^7.1.11",
87
90
  "react-moment": "^1.1.3",
88
91
  "react-redux": "^9.2.0",
89
- "react-router": "^7.15.0",
92
+ "react-router": "^8.3.0",
90
93
  "redux": "^5.0.1",
91
94
  "redux-saga": "^1.3.0",
92
95
  "redux-saga-routines": "^3.2.3",
@@ -95,5 +98,5 @@
95
98
  "swr": "^2.3.3",
96
99
  "turndown": "^7.2.2"
97
100
  },
98
- "gitHead": "77921b9429f2536dbc3c51e7e5feae55c0e5ebef"
101
+ "gitHead": "39f6a32de360216dbd10f5987e6a91f53bdd5a8e"
99
102
  }
@@ -26,8 +26,8 @@ const ToggleChildrenButton = ({ toggleChildrenOn, onClick }) => {
26
26
  return (
27
27
  <button type="button" className="toggle-children" onClick={onClick}>
28
28
  <span title={hoverText} className="toggle-children-content">
29
- {icon && <Icon name={icon} />}
30
- {label && <span>{label}</span>}
29
+ {icon ? <Icon name={icon} /> : null}
30
+ {label ? <span>{label}</span> : null}
31
31
  </span>
32
32
  </button>
33
33
  );
@@ -1,4 +1,5 @@
1
1
  import _ from "lodash/fp";
2
+ import { useEffect, useState } from "react";
2
3
  import PropTypes from "prop-types";
3
4
  import { useIntl } from "react-intl";
4
5
  import { useQuery } from "@apollo/client";
@@ -7,39 +8,91 @@ import { stratify, flatten } from "../services/tree";
7
8
  import { DOMAINS_QUERY } from "../api/queries";
8
9
  import TreeSelector from "./TreeSelector";
9
10
  import Loading from "./Loading";
11
+
12
+ const readCachedData = (cacheKey) => {
13
+ if (!cacheKey || typeof localStorage === "undefined") return null;
14
+
15
+ try {
16
+ const cached = JSON.parse(localStorage.getItem(cacheKey));
17
+
18
+ if (!cached?.expiresAt || cached.expiresAt <= Date.now()) {
19
+ localStorage.removeItem(cacheKey);
20
+ return null;
21
+ }
22
+
23
+ return cached.data || null;
24
+ } catch (_error) {
25
+ localStorage.removeItem(cacheKey);
26
+ return null;
27
+ }
28
+ };
29
+
30
+ const writeCachedData = (cacheKey, cacheTtl, data) => {
31
+ if (!cacheKey || !cacheTtl || typeof localStorage === "undefined") return;
32
+
33
+ localStorage.setItem(
34
+ cacheKey,
35
+ JSON.stringify({
36
+ data,
37
+ expiresAt: Date.now() + cacheTtl,
38
+ }),
39
+ );
40
+ };
41
+
10
42
  export const DomainSelector = ({
11
43
  allNodesOpen,
12
44
  action,
45
+ cacheKey,
46
+ cacheTtl,
13
47
  className,
48
+ loadingComponent,
14
49
  multiple,
15
50
  domainActions,
16
51
  domainIds,
17
52
  notDropdown,
18
53
  onNodeClick,
19
54
  onLoad,
55
+ optionDisabled,
56
+ query = DOMAINS_QUERY,
20
57
  value,
21
58
  ascendants,
22
59
  ...props
23
60
  }) => {
24
61
  const { formatMessage } = useIntl();
25
- const { loading, error, data } = useQuery(DOMAINS_QUERY, {
62
+ const [cachedData] = useState(() => readCachedData(cacheKey));
63
+ const { loading, error, data } = useQuery(query, {
26
64
  fetchPolicy: "cache-and-network",
65
+ skip: !!cachedData,
27
66
  variables: {
28
67
  action: action || "viewDomain",
29
68
  domainActions,
30
69
  ids: domainIds,
31
70
  },
32
- onCompleted: onLoad,
71
+ onCompleted: (loadedData) => {
72
+ writeCachedData(cacheKey, cacheTtl, loadedData);
73
+ onLoad && onLoad(loadedData);
74
+ },
33
75
  });
76
+
77
+ useEffect(() => {
78
+ if (cachedData) onLoad && onLoad(cachedData);
79
+ }, [cachedData, onLoad]);
80
+
34
81
  if (error) return null;
35
- if (loading) return <Loading />;
82
+ if (loading && !cachedData) return loadingComponent || <Loading />;
83
+
84
+ const resolvedData = cachedData || data;
36
85
 
37
86
  const options = _.flow(
38
87
  _.propOr([], "domains"),
88
+ _.map((domain) => ({
89
+ ...domain,
90
+ disabled: optionDisabled ? optionDisabled(domain) : false,
91
+ })),
39
92
  _.sortBy(accentInsensitivePathOrder("name")),
40
93
  stratify({}),
41
94
  flatten,
42
- )(data);
95
+ )(resolvedData);
43
96
 
44
97
  return (
45
98
  <TreeSelector
@@ -64,13 +117,18 @@ export const DomainSelector = ({
64
117
  DomainSelector.propTypes = {
65
118
  allNodesOpen: PropTypes.bool,
66
119
  action: PropTypes.string,
120
+ cacheKey: PropTypes.string,
121
+ cacheTtl: PropTypes.number,
67
122
  className: PropTypes.string,
123
+ loadingComponent: PropTypes.node,
68
124
  multiple: PropTypes.bool,
69
125
  domainActions: PropTypes.array,
70
126
  domainIds: PropTypes.array,
71
127
  notDropdown: PropTypes.bool,
72
128
  onNodeClick: PropTypes.func,
73
129
  onLoad: PropTypes.func,
130
+ optionDisabled: PropTypes.func,
131
+ query: PropTypes.object,
74
132
  ascendants: PropTypes.array,
75
133
  value: PropTypes.oneOfType([
76
134
  PropTypes.array,
@@ -23,19 +23,25 @@ export const DropdownMenuItem = ({
23
23
  const handleClick = (e) => {
24
24
  e && e.preventDefault();
25
25
  e && e.stopPropagation();
26
- onClick(e, id);
26
+ if (!disabled && onClick) {
27
+ onClick(e, id);
28
+ }
27
29
  };
28
30
 
29
31
  const itemStyle = {
30
32
  marginLeft: `${20 * level}px`,
31
33
  paddingLeft: !canOpen ? "25px" : "5px",
32
34
  };
35
+ const itemClassName = [className, disabled ? "disabled" : ""]
36
+ .filter(Boolean)
37
+ .join(" ");
33
38
 
34
39
  return (
35
40
  <Dropdown.Item
41
+ aria-disabled={disabled}
36
42
  onClick={handleClick}
37
43
  selected={!check && selected}
38
- className={className}
44
+ className={itemClassName}
39
45
  >
40
46
  <div className="item-content" style={itemStyle}>
41
47
  {canOpen ? (
@@ -1,12 +1,9 @@
1
1
  import { useIntl } from "react-intl";
2
2
  import { useAuthorized } from "../hooks";
3
- import { GRAPHS, LINEAGE_EVENTS } from "../routes";
3
+ import { GRAPHS } from "../routes";
4
4
  import Submenu from "./Submenu";
5
5
 
6
- const items = [
7
- { name: "lineage", routes: [GRAPHS] },
8
- { name: "lineage_events", routes: [LINEAGE_EVENTS] },
9
- ];
6
+ const items = [{ name: "lineage", routes: [GRAPHS] }];
10
7
 
11
8
  export const LineageMenu = () => {
12
9
  const { formatMessage } = useIntl();
@@ -68,6 +68,7 @@ export const TreeSelector = ({
68
68
  allNodesOpen,
69
69
  check = false,
70
70
  className = "",
71
+ closeOnSelect = false,
71
72
  disabled = false,
72
73
  error,
73
74
  label,
@@ -169,6 +170,9 @@ export const TreeSelector = ({
169
170
  setDropdownOpen(false);
170
171
  }
171
172
  onChange && onChange(e, { value: nextValue });
173
+ if (closeOnSelect && !multiple && !notDropdown) {
174
+ setDropdownOpen(false);
175
+ }
172
176
  };
173
177
 
174
178
  const filterSearch = query ? _.filter(matchAny(query)) : _.identity;
@@ -190,26 +194,36 @@ export const TreeSelector = ({
190
194
  <label>{placeholder}</label>
191
195
  );
192
196
 
193
- const isEnabled = ({ level }, minDepth) => level >= minDepth;
197
+ const isEnabled = ({ disabled, level }, minDepth) =>
198
+ !disabled && level >= minDepth;
194
199
 
195
200
  const items = _.flow(
196
201
  filterSearch,
197
202
  filterDisplayed,
198
- _.map((option) => (
199
- <DropdownMenuItem
200
- key={option?.id}
201
- check={check}
202
- onOpen={handleOpen}
203
- onClick={isEnabled(option, minDepth) ? handleClick : () => {}}
204
- open={_.contains(option.id)(open)}
205
- canOpen={!_.isEmpty(option.children)}
206
- level={option.level}
207
- disabled={!isEnabled(option, minDepth)}
208
- selected={selected(option.id)}
209
- className={ascendants?.includes(String(option?.id)) ? "ascendant" : ""}
210
- {...option}
211
- />
212
- )),
203
+ _.map((option) => {
204
+ const enabled = isEnabled(option, minDepth);
205
+ const optionClassName = [
206
+ ascendants?.includes(String(option?.id)) ? "ascendant" : "",
207
+ ]
208
+ .filter(Boolean)
209
+ .join(" ");
210
+
211
+ return (
212
+ <DropdownMenuItem
213
+ {...option}
214
+ key={option?.id}
215
+ check={check}
216
+ onOpen={handleOpen}
217
+ onClick={enabled ? handleClick : undefined}
218
+ open={_.contains(option.id)(open)}
219
+ canOpen={!_.isEmpty(option.children)}
220
+ level={option.level}
221
+ disabled={!enabled}
222
+ selected={selected(option.id)}
223
+ className={optionClassName}
224
+ />
225
+ );
226
+ }),
213
227
  )(options);
214
228
 
215
229
  const renderTreeSelector = () => {
@@ -273,6 +287,7 @@ TreeSelector.propTypes = {
273
287
  ascendants: PropTypes.array,
274
288
  check: PropTypes.bool,
275
289
  className: PropTypes.string,
290
+ closeOnSelect: PropTypes.bool,
276
291
  disabled: PropTypes.bool,
277
292
  error: PropTypes.bool,
278
293
  label: PropTypes.string,
@@ -11,6 +11,10 @@ const variables = { action: action };
11
11
  const renderOpts = { mocks: [domainsMock(variables)] };
12
12
 
13
13
  describe("<DomainSelector />", () => {
14
+ beforeEach(() => {
15
+ localStorage.clear();
16
+ });
17
+
14
18
  it("matches latest snapshot", async () => {
15
19
  const props = { action, onChange: jest.fn(), className: "test className" };
16
20
  const { container, queryByText } = render(
@@ -61,4 +65,72 @@ describe("<DomainSelector />", () => {
61
65
  expect(props.onChange.mock.calls.length).toBe(1);
62
66
  expect(props.onChange.mock.calls[0][1]).toEqual({ value: ["1"] });
63
67
  });
68
+
69
+ it("renders domains from a valid localStorage cache", async () => {
70
+ const onLoad = jest.fn();
71
+ localStorage.setItem(
72
+ "domain-selector-cache-test",
73
+ JSON.stringify({
74
+ expiresAt: Date.now() + 60000,
75
+ data: {
76
+ domains: [
77
+ {
78
+ __typename: "Domain",
79
+ name: "cachedDomain",
80
+ id: "cached",
81
+ parentId: null,
82
+ },
83
+ ],
84
+ },
85
+ }),
86
+ );
87
+
88
+ const { getByText } = render(
89
+ <DomainSelector
90
+ action={action}
91
+ cacheKey="domain-selector-cache-test"
92
+ cacheTtl={60000}
93
+ onLoad={onLoad}
94
+ />,
95
+ { mocks: [] },
96
+ );
97
+
98
+ await waitFor(() => {
99
+ expect(getByText(/cachedDomain/)).toBeInTheDocument();
100
+ });
101
+ expect(onLoad).toHaveBeenCalledWith({
102
+ domains: [
103
+ {
104
+ __typename: "Domain",
105
+ name: "cachedDomain",
106
+ id: "cached",
107
+ parentId: null,
108
+ },
109
+ ],
110
+ });
111
+ });
112
+
113
+ it("stores loaded domains in localStorage when cache options are provided", async () => {
114
+ const { queryByText } = render(
115
+ <DomainSelector
116
+ action={action}
117
+ cacheKey="domain-selector-cache-test"
118
+ cacheTtl={60000}
119
+ />,
120
+ renderOpts,
121
+ );
122
+
123
+ await waitFor(() => {
124
+ expect(queryByText(/fooDomain/)).toBeInTheDocument();
125
+ });
126
+
127
+ const cached = JSON.parse(
128
+ localStorage.getItem("domain-selector-cache-test"),
129
+ );
130
+
131
+ expect(cached.expiresAt).toBeGreaterThan(Date.now());
132
+ expect(cached.data.domains).toEqual(
133
+ expect.arrayContaining([expect.objectContaining({ name: "fooDomain" })]),
134
+ );
135
+ });
64
136
  });
@@ -88,6 +88,55 @@ describe("<TreeSelector />", () => {
88
88
  });
89
89
  });
90
90
 
91
+ it("closes dropdown after a single closeOnSelect selection", async () => {
92
+ const props = {
93
+ closeOnSelect: true,
94
+ onChange: jest.fn(),
95
+ options,
96
+ placeholder: "Select a domain",
97
+ };
98
+ const rendered = render(<TreeSelector {...props} />);
99
+ await waitForLoad(rendered);
100
+
101
+ const user = userEvent.setup({ delay: null });
102
+
103
+ await user.click(rendered.getByText(/select a domain/i));
104
+ await waitFor(() => {
105
+ expect(
106
+ rendered.getByRole("option", { name: /foo/i }),
107
+ ).toBeInTheDocument();
108
+ });
109
+
110
+ await user.click(rendered.getByRole("option", { name: /foo/i }));
111
+
112
+ await waitFor(() => {
113
+ expect(rendered.getByRole("listbox")).toHaveAttribute(
114
+ "aria-expanded",
115
+ "false",
116
+ );
117
+ });
118
+ });
119
+
120
+ it("does not call onChange for disabled options", async () => {
121
+ const props = {
122
+ onChange: jest.fn(),
123
+ options: [{ ...foo, disabled: true }],
124
+ placeholder: "Select a domain",
125
+ };
126
+ const rendered = render(<TreeSelector {...props} />);
127
+ await waitForLoad(rendered);
128
+
129
+ const user = userEvent.setup({ delay: null });
130
+
131
+ await user.click(rendered.getByText(/select a domain/i));
132
+ await user.click(rendered.getByRole("option", { name: /foo/i }));
133
+
134
+ expect(props.onChange).not.toHaveBeenCalled();
135
+ expect(rendered.getByRole("option", { name: /foo/i })).toHaveClass(
136
+ "disabled",
137
+ );
138
+ });
139
+
91
140
  it("select node with allowed depth (multiple)", async () => {
92
141
  const minDepth = 2;
93
142
 
@@ -24,14 +24,6 @@ exports[`<LineageMenu /> matches the latest snapshot 1`] = `
24
24
  <div
25
25
  class="menu transition"
26
26
  >
27
- <div
28
- class="header selectable"
29
- >
30
- lineage
31
- </div>
32
- <div
33
- class="divider"
34
- />
35
27
  <a
36
28
  aria-checked="false"
37
29
  class="item"
@@ -46,20 +38,6 @@ exports[`<LineageMenu /> matches the latest snapshot 1`] = `
46
38
  lineage
47
39
  </span>
48
40
  </a>
49
- <a
50
- aria-checked="false"
51
- class="item"
52
- data-discover="true"
53
- href="/lineageEvents"
54
- name="lineage_events"
55
- role="option"
56
- >
57
- <span
58
- class="text"
59
- >
60
- lineage_events
61
- </span>
62
- </a>
63
41
  </div>
64
42
  </div>
65
43
  </div>
@@ -412,24 +412,6 @@ exports[`<SideMenu /> matches the latest snapshot 1`] = `
412
412
  />
413
413
  lineage
414
414
  </a>
415
- <div
416
- class="menu"
417
- >
418
- <a
419
- class="link item"
420
- data-discover="true"
421
- href="/graphs"
422
- >
423
- lineage
424
- </a>
425
- <a
426
- class="link item"
427
- data-discover="true"
428
- href="/lineageEvents"
429
- >
430
- lineage_events
431
- </a>
432
- </div>
433
415
  </div>
434
416
  <div
435
417
  class="active item selectable"
package/src/routes.js CHANGED
@@ -165,7 +165,6 @@ export const INGEST_RELATIONS_STRUCTURES_NEW =
165
165
  "/ingests/:id/relations/structures/new";
166
166
  export const JOB = "/jobs/:id";
167
167
  export const JOBS = "/jobs";
168
- export const LINEAGE_EVENTS = "/lineageEvents";
169
168
  export const LOGIN = "/login";
170
169
  export const MY_GRANTS = "/myGrants";
171
170
  export const MY_GRANT_REQUESTS = "/myGrantRequests";
@@ -225,7 +224,6 @@ export const RULE_IMPLEMENTATIONS = "/rules/:id/implementations";
225
224
  export const RULE_IMPLEMENTATION_NEW = "/rules/:id/implementations/new";
226
225
  export const RULE_IMPLEMENTATION_NEW_RAW = "/rules/:id/implementations/new_raw";
227
226
  export const RULE_NEW = "/rules/new";
228
- export const SAMPLE = "/sample";
229
227
  export const MY_SCORE_GROUPS = "/myScoreGroups";
230
228
  export const SCORE_GROUPS = "/scoreGroups";
231
229
  export const SCORE_GROUP = "/scoreGroups/:id";
@@ -424,7 +422,6 @@ const routes = {
424
422
  INGEST_RELATIONS_STRUCTURES_NEW,
425
423
  JOB,
426
424
  JOBS,
427
- LINEAGE_EVENTS,
428
425
  LOGIN,
429
426
  MY_GRANTS,
430
427
  MY_GRANT_REQUESTS,
@@ -471,7 +468,6 @@ const routes = {
471
468
  RULE_IMPLEMENTATION_NEW,
472
469
  RULE_IMPLEMENTATION_NEW_RAW,
473
470
  RULE_NEW,
474
- SAMPLE,
475
471
  MY_SCORE_GROUPS,
476
472
  SCORE_GROUPS,
477
473
  SCORE_GROUP,