@truedat/core 8.1.0 → 8.1.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.1.0",
3
+ "version": "8.1.3",
4
4
  "description": "Truedat Web Core",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -48,7 +48,7 @@
48
48
  "@testing-library/jest-dom": "^6.6.3",
49
49
  "@testing-library/react": "^16.3.0",
50
50
  "@testing-library/user-event": "^14.6.1",
51
- "@truedat/test": "8.1.0",
51
+ "@truedat/test": "8.1.3",
52
52
  "identity-obj-proxy": "^3.0.0",
53
53
  "jest": "^29.7.0",
54
54
  "redux-saga-test-plan": "^4.0.6"
@@ -85,5 +85,5 @@
85
85
  "slate-react": "^0.22.10",
86
86
  "swr": "^2.3.3"
87
87
  },
88
- "gitHead": "c954898f854501a4e91a10be48ea563e55967284"
88
+ "gitHead": "0eaf0c4b1342771cddb87d09ed78a9e4c65a5ca2"
89
89
  }
@@ -26,20 +26,17 @@ import {
26
26
  import Submenu from "./Submenu";
27
27
 
28
28
  function isMenuSubscope(pathname, subscope) {
29
- // const match = matchPath(
30
- // {
31
- // path: [
32
- // CONCEPTS_SUBSCOPE,
33
- // CONCEPTS_SIDEMENU_SUBSCOPE,
34
- // CONCEPTS_SIDEMENU_SUBSCOPE_DEPRECATED,
35
- // CONCEPTS_SIDEMENU_SUBSCOPE_PENDING,
36
- // ],
37
- // },
38
- // pathname
39
- // );
40
-
41
- // return !!match?.params?.subscope && subscope === match.params.subscope;
42
- return false;
29
+ const pathsToCheck = [
30
+ CONCEPTS_SUBSCOPE,
31
+ CONCEPTS_SIDEMENU_SUBSCOPE,
32
+ CONCEPTS_SIDEMENU_SUBSCOPE_DEPRECATED,
33
+ CONCEPTS_SIDEMENU_SUBSCOPE_PENDING,
34
+ ];
35
+
36
+ return pathsToCheck.some(path => {
37
+ const match = matchPath({ path }, pathname);
38
+ return match?.params?.subscope && subscope === decodeURIComponent(match.params.subscope);
39
+ });
43
40
  }
44
41
 
45
42
  function mainMenuSubscopes(allSubscopes, rootSubscopes) {
@@ -247,7 +244,6 @@ export const GlossaryMenu = ({ bgSubscopes, rootSubscopes }) => {
247
244
 
248
245
  const isActiveSubscope = () =>
249
246
  conceptSubscope === name || isMenuSubscope(location.pathname, name);
250
-
251
247
  return (
252
248
  <Submenu
253
249
  key={name}
@@ -6,16 +6,17 @@ import { useAuthorized } from "../hooks";
6
6
  import {
7
7
  EXECUTION_GROUPS,
8
8
  IMPLEMENTATIONS,
9
+ IMPLEMENTATIONS_BY_SUBSCOPE,
9
10
  IMPLEMENTATIONS_DEPRECATED,
10
11
  IMPLEMENTATIONS_PENDING,
11
12
  IMPLEMENTATIONS_UPLOAD_JOBS,
12
13
  QUALITY_DASHBOARD,
13
14
  RULES,
14
15
  } from "../routes";
15
- import { getQualityDashboardConfig } from "../selectors";
16
+ import { getQualityDashboardConfig, getRiSubscopes } from "../selectors";
16
17
  import Submenu from "./Submenu";
17
18
 
18
- export const ITEMS = [
19
+ export const BASE_ITEMS = [
19
20
  { name: "rules", routes: [RULES], groups: ["quality"] },
20
21
  { name: "implementations", routes: [IMPLEMENTATIONS], groups: ["quality"] },
21
22
  {
@@ -45,7 +46,7 @@ export const ITEMS = [
45
46
  },
46
47
  ];
47
48
 
48
- export const QualityMenu = ({ dashboardConfig }) => {
49
+ export const QualityMenu = ({ dashboardConfig, riSubscopes }) => {
49
50
  const { formatMessage } = useIntl();
50
51
  const iconQuality = formatMessage({
51
52
  id: "sidemenu.quality.icon",
@@ -57,21 +58,45 @@ export const QualityMenu = ({ dashboardConfig }) => {
57
58
  "quality_implementation_additional_actions"
58
59
  );
59
60
 
60
- const filteredItems = _.filter(
61
+ if (!authorized) return null;
62
+
63
+ const filteredBaseItems = _.filter(
61
64
  ({ name }) => name != "quality_dashboard" || !_.isEmpty(dashboardConfig)
62
- )(ITEMS);
65
+ )(BASE_ITEMS);
66
+
67
+ const extendedItems = filteredBaseItems.reduce((acc, item) => {
68
+ acc.push(item);
69
+
70
+ if (item.name === "implementations") {
71
+ riSubscopes.forEach(subscope => {
72
+ acc.push({
73
+ name: subscope,
74
+ routes: [IMPLEMENTATIONS_BY_SUBSCOPE.replace(':subscope', subscope)],
75
+ groups: ["quality"],
76
+ });
77
+ });
78
+ }
79
+
80
+ return acc;
81
+ }, []);
63
82
 
64
- return authorized ? (
65
- <Submenu items={filteredItems} icon={iconQuality} name="quality" />
66
- ) : null;
83
+ return (
84
+ <Submenu items={extendedItems} icon={iconQuality} name="quality" />
85
+ );
67
86
  };
68
87
 
69
88
  QualityMenu.propTypes = {
70
89
  dashboardConfig: PropTypes.object,
90
+ riSubscopes: PropTypes.array,
91
+ };
92
+
93
+ QualityMenu.defaultProps = {
94
+ riSubscopes: [],
71
95
  };
72
96
 
73
97
  export const mapStateToProps = (state) => ({
74
98
  dashboardConfig: getQualityDashboardConfig(state),
99
+ riSubscopes: getRiSubscopes(state),
75
100
  });
76
101
 
77
- export default connect(mapStateToProps)(QualityMenu);
102
+ export default connect(mapStateToProps)(QualityMenu);
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { useIntl, FormattedMessage } from "react-intl";
5
5
  import { connect } from "react-redux";
6
- import { useLocation, Link, useNavigate } from "react-router";
6
+ import { useLocation, Link, useNavigate, matchPath } from "react-router";
7
7
  import { Dropdown, Icon, Menu } from "semantic-ui-react";
8
8
  import { useAuthorizedItems, useActiveRoutes } from "@truedat/core/hooks";
9
9
  import { clearNavFilter as clearBucketFilterRoutine } from "@truedat/core/routines";
@@ -84,14 +84,49 @@ export const Submenu = ({
84
84
  }, [location]);
85
85
 
86
86
  const primaryRoute = _.flow(_.flatMap("routes"), _.head)(filteredItems);
87
+ const pathname = location.pathname;
88
+ const decodedPathname = decodeURIComponent(pathname);
89
+ const routes = _.flatMap("routes")(filteredItems);
90
+
91
+ const pathParts = pathname.split('/').filter(part => part !== '');
92
+ const hasSubscopeInPath = pathname.includes('/subscope/');
93
+ let subscopeName = null;
94
+ if (hasSubscopeInPath && pathParts.length >= 3 && pathParts[1] === 'subscope') {
95
+ subscopeName = decodeURIComponent(pathParts[2]).replace(/%20/g, ' ');
96
+ }
97
+
98
+ const isSubmenuActiveForSubscope = hasSubscopeInPath && subscopeName &&
99
+ _.some(item =>
100
+ item.name.toLowerCase() === subscopeName.toLowerCase()
101
+ )(filteredItems);
102
+
103
+ const isSubmenuActiveForRoutes = _.some(item =>
104
+ _.some(route => {
105
+ const decodedPathname = decodeURIComponent(pathname);
106
+ return decodedPathname === route ||
107
+ decodedPathname.startsWith(`${route}/`) ||
108
+ matchPath({ path: route }, decodedPathname);
109
+ })(item.routes)
110
+ )(filteredItems);
111
+
112
+ // Determine if submenu should be active, with special handling for management URLs
113
+ const submenuIsActive = (isActive || isSubmenuActiveForSubscope || isSubmenuActiveForRoutes) &&
114
+ !(name === 'glossary' && location.pathname.startsWith('/concepts/management/'));
115
+
87
116
 
88
- if (isActive && sidebarVisible) {
117
+ if (submenuIsActive && sidebarVisible) {
89
118
  const menuItems = filteredItems.map((item, i) => {
90
119
  const isItemActive =
91
- isActive &&
120
+ submenuIsActive &&
92
121
  (item.isActive
93
122
  ? item.isActive()
94
- : _.includes(activeRoute, item.routes));
123
+ : hasSubscopeInPath && subscopeName
124
+ ? item.name.toLowerCase() === subscopeName.toLowerCase()
125
+ : item.routes.some(route =>
126
+ route === activeRoute ||
127
+ route === pathname || route === decodedPathname ||
128
+ matchPath({ path: route }, pathname) || matchPath({ path: route }, decodedPathname) // Pattern match
129
+ ));
95
130
 
96
131
  return (
97
132
  <MenuItem
@@ -120,7 +155,7 @@ export const Submenu = ({
120
155
  </Menu.Item>
121
156
  );
122
157
  } else {
123
- const className = isActive ? "active" : null;
158
+ const className = submenuIsActive ? "active" : null;
124
159
 
125
160
  const trigger = (
126
161
  <Link to={primaryRoute} className="ui">
@@ -132,10 +167,16 @@ export const Submenu = ({
132
167
  );
133
168
  const dropdownItems = filteredItems.map((item, i) => {
134
169
  const isItemActive =
135
- isActive &&
170
+ submenuIsActive &&
136
171
  (item.isActive
137
172
  ? item.isActive()
138
- : _.includes(activeRoute, item.routes));
173
+ : hasSubscopeInPath && subscopeName
174
+ ? item.name.toLowerCase() === subscopeName.toLowerCase()
175
+ : item.routes.some(route =>
176
+ route === activeRoute ||
177
+ route === pathname || route === decodedPathname ||
178
+ matchPath({ path: route }, pathname) || matchPath({ path: route }, decodedPathname)
179
+ ));
139
180
 
140
181
  return (
141
182
  <DropdownItem
@@ -20,8 +20,10 @@ export const TemplateSelector = ({
20
20
  clearable,
21
21
  loading,
22
22
  name = "template",
23
+ onBlur,
23
24
  onChange,
24
25
  required,
26
+ requiredError = false,
25
27
  selectedValue,
26
28
  templates,
27
29
  label,
@@ -52,17 +54,19 @@ export const TemplateSelector = ({
52
54
  )}
53
55
  <Form.Dropdown
54
56
  disabled={disabled}
57
+ error={!!requiredError}
55
58
  clearable={clearable}
56
59
  loading={loading}
57
60
  name={name}
61
+ onBlur={onBlur}
58
62
  onChange={handleChange}
59
63
  options={options}
60
64
  placeholder={
61
65
  placeholder
62
66
  ? placeholder
63
67
  : formatMessage({
64
- id: loading ? "loading" : "template.selector.placeholder",
65
- })
68
+ id: loading ? "loading" : "template.selector.placeholder",
69
+ })
66
70
  }
67
71
  search
68
72
  selection
@@ -13,4 +13,17 @@ describe("<Submenu />", () => {
13
13
  const { container } = render(<Submenu {...props} />, renderOpts);
14
14
  expect(container).toMatchSnapshot();
15
15
  });
16
+
17
+ it("should handle subscope path detection correctly", () => {
18
+ const items = [
19
+ { name: "implementations", routes: ["/implementations"] },
20
+ { name: "subscope1", routes: ["/implementations/subscope/subscope1"] },
21
+ ];
22
+ const props = { name: "quality", items };
23
+ const { container } = render(<Submenu {...props} />, {
24
+ state: { sidebarVisible: true },
25
+ routes: ["/implementations/subscope/subscope1"]
26
+ });
27
+ expect(container).toMatchSnapshot();
28
+ });
16
29
  });
@@ -32,6 +32,7 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
32
32
  class="divider"
33
33
  />
34
34
  <a
35
+ aria-checked="false"
35
36
  class="item"
36
37
  data-discover="true"
37
38
  href="/templates"
@@ -45,6 +46,7 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
45
46
  </span>
46
47
  </a>
47
48
  <a
49
+ aria-checked="false"
48
50
  class="item"
49
51
  data-discover="true"
50
52
  href="/hierarchies"
@@ -58,6 +60,7 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
58
60
  </span>
59
61
  </a>
60
62
  <a
63
+ aria-checked="false"
61
64
  class="item"
62
65
  data-discover="true"
63
66
  href="/relationTags"
@@ -71,6 +74,7 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
71
74
  </span>
72
75
  </a>
73
76
  <a
77
+ aria-checked="false"
74
78
  class="item"
75
79
  data-discover="true"
76
80
  href="/subscriptions"
@@ -84,6 +88,7 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
84
88
  </span>
85
89
  </a>
86
90
  <a
91
+ aria-checked="false"
87
92
  class="item"
88
93
  data-discover="true"
89
94
  href="/sources"
@@ -97,6 +102,7 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
97
102
  </span>
98
103
  </a>
99
104
  <a
105
+ aria-checked="false"
100
106
  class="item"
101
107
  data-discover="true"
102
108
  href="/jobs"
@@ -110,6 +116,7 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
110
116
  </span>
111
117
  </a>
112
118
  <a
119
+ aria-checked="false"
113
120
  class="item"
114
121
  data-discover="true"
115
122
  href="/configurations"
@@ -123,6 +130,7 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
123
130
  </span>
124
131
  </a>
125
132
  <a
133
+ aria-checked="false"
126
134
  class="item"
127
135
  data-discover="true"
128
136
  href="/i18n/messages"
@@ -136,6 +144,7 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
136
144
  </span>
137
145
  </a>
138
146
  <a
147
+ aria-checked="false"
139
148
  class="item"
140
149
  data-discover="true"
141
150
  href="/reindex"
@@ -149,6 +158,7 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
149
158
  </span>
150
159
  </a>
151
160
  <a
161
+ aria-checked="false"
152
162
  class="item"
153
163
  data-discover="true"
154
164
  href="/search/elasticIndexes"
@@ -36,3 +36,40 @@ exports[`<Submenu /> matches the latest snapshot 1`] = `
36
36
  </div>
37
37
  </div>
38
38
  `;
39
+
40
+ exports[`<Submenu /> should handle subscope path detection correctly 1`] = `
41
+ <div>
42
+ <div
43
+ class="active item selectable"
44
+ >
45
+ <a
46
+ data-discover="true"
47
+ href="/implementations"
48
+ >
49
+ <i
50
+ aria-hidden="true"
51
+ class="large icon"
52
+ />
53
+ quality
54
+ </a>
55
+ <div
56
+ class="menu"
57
+ >
58
+ <a
59
+ class="link item"
60
+ data-discover="true"
61
+ href="/implementations"
62
+ >
63
+ implementations
64
+ </a>
65
+ <a
66
+ class="active link item"
67
+ data-discover="true"
68
+ href="/implementations/subscope/subscope1"
69
+ >
70
+ subscope1
71
+ </a>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ `;
@@ -0,0 +1,83 @@
1
+ import { useActiveRoutes } from "../useActiveRoutes";
2
+
3
+ // Mock the react-router and lodash functions that are used in the hook
4
+ jest.mock("react-router", () => ({
5
+ useLocation: () => ({ pathname: "/test" }),
6
+ }));
7
+
8
+ jest.mock("lodash/fp", () => ({
9
+ prop: jest.fn((key) => (obj) => obj[key]),
10
+ orderBy: jest.fn((iteratees, orders, collection) => collection.sort()),
11
+ castArray: jest.fn((val) => Array.isArray(val) ? val : [val]),
12
+ size: jest.fn((val) => typeof val === 'string' ? val.length : 0),
13
+ flow: jest.fn((...funcs) => (...args) => funcs.reduceRight((arg, fn) => fn(arg), args)),
14
+ map: jest.fn((fn) => (arr) => arr.map(fn)),
15
+ head: jest.fn((arr) => arr[0]),
16
+ }));
17
+
18
+ jest.mock("@truedat/core/routes", () => ({
19
+ BUCKETS_VIEW: "/buckets/:propertyPath",
20
+ }));
21
+
22
+ jest.mock("react-router", () => ({
23
+ matchPath: jest.fn(() => null),
24
+ }));
25
+
26
+ describe("hooks: useActiveRoutes", () => {
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+
30
+ // Setup default mocks
31
+ require("lodash/fp").prop.mockImplementation((key) => (obj) => obj[key]);
32
+ require("lodash/fp").orderBy.mockImplementation((iteratees, orders, collection) => collection);
33
+ require("lodash/fp").castArray.mockImplementation((val) => Array.isArray(val) ? val : [val]);
34
+ require("react-router").useLocation = () => ({ pathname: "/test" });
35
+ });
36
+
37
+ it("should return the most specific route when multiple routes match", () => {
38
+ const mockUseLocation = jest.spyOn(require("react-router"), "useLocation");
39
+ mockUseLocation.mockReturnValue({ pathname: "/implementations/subscope/test" });
40
+
41
+ const mockRoutes = ["/implementations", "/implementations/subscope/test"];
42
+
43
+ const result = useActiveRoutes(mockRoutes, null);
44
+
45
+ // Should return the more specific route
46
+ expect(result).toBe("/implementations/subscope/test");
47
+ });
48
+
49
+ it("should handle subscope path detection correctly", () => {
50
+ const mockUseLocation = jest.spyOn(require("react-router"), "useLocation");
51
+ mockUseLocation.mockReturnValue({ pathname: "/concepts/subscope/AI_Initiative" });
52
+
53
+ const mockRoutes = ["/concepts", "/concepts/subscope/AI_Initiative"];
54
+
55
+ const result = useActiveRoutes(mockRoutes, null);
56
+
57
+ // Should return the more specific route
58
+ expect(result).toBe("/concepts/subscope/AI_Initiative");
59
+ });
60
+
61
+ it("should return null when no routes match", () => {
62
+ const mockUseLocation = jest.spyOn(require("react-router"), "useLocation");
63
+ mockUseLocation.mockReturnValue({ pathname: "/other" });
64
+
65
+ const mockRoutes = ["/implementations", "/concepts"];
66
+
67
+ const result = useActiveRoutes(mockRoutes, null);
68
+
69
+ expect(result).toBeNull();
70
+ });
71
+
72
+ it("should handle URL decoding properly", () => {
73
+ const mockUseLocation = jest.spyOn(require("react-router"), "useLocation");
74
+ mockUseLocation.mockReturnValue({ pathname: "/concepts/subscope/AI%20Initiative" });
75
+
76
+ const mockRoutes = ["/concepts", "/concepts/subscope/AI Initiative"];
77
+
78
+ const result = useActiveRoutes(mockRoutes, null);
79
+
80
+ // Should match the decoded route
81
+ expect(result).toBe("/concepts/subscope/AI Initiative");
82
+ });
83
+ });
@@ -6,27 +6,33 @@ import { matchPath } from "react-router";
6
6
  export const useActiveRoutes = (route, navFilter) => {
7
7
  const location = useLocation();
8
8
  const pathname = _.prop("pathname")(location);
9
- const routes = _.castArray(route);
9
+ const decodedPathname = decodeURIComponent(pathname);
10
+ const routes = _.orderBy([_.size], ['desc'], _.castArray(route)); // Sort routes by length in descending order to prioritize more specific routes
10
11
 
11
- return _.flow(
12
- _.map((route) => ({
13
- route,
14
- filterMatch: matchPath({ path: BUCKETS_VIEW }, route),
15
- })),
16
- _.reduce(
17
- (acc, { route, filterMatch }) => {
18
- const { filterMatchRoutes, pathMatchRoutes } = acc;
12
+ for (const routeObj of _.map((route) => ({
13
+ route,
14
+ filterMatch: matchPath({ path: BUCKETS_VIEW }, route),
15
+ }))(routes)) {
16
+ const { route, filterMatch } = routeObj;
19
17
 
20
- return navFilter &&
21
- _.includes(filterMatch?.params?.propertyPath, Object.keys(navFilter))
22
- ? { ...acc, filterMatchRoutes: route }
23
- : route === pathname || _.startsWith(`${route}/`)(pathname)
24
- ? { ...acc, pathMatchRoutes: route }
25
- : { filterMatchRoutes, pathMatchRoutes };
26
- },
27
- { filterMatchRoutes: null, pathMatchRoutes: null }
28
- ),
29
- ({ filterMatchRoutes, pathMatchRoutes }) =>
30
- !_.isEmpty(filterMatchRoutes) ? filterMatchRoutes : pathMatchRoutes
31
- )(routes);
18
+ if (navFilter &&
19
+ filterMatch?.params?.propertyPath &&
20
+ _.includes(filterMatch?.params?.propertyPath, Object.keys(navFilter))) {
21
+ return route;
22
+ }
23
+
24
+ if (route === pathname || route === decodedPathname) {
25
+ return route;
26
+ }
27
+
28
+ if (_.startsWith(`${route}/`)(pathname) || _.startsWith(`${route}/`)(decodedPathname)) {
29
+ return route;
30
+ }
31
+
32
+ if (matchPath({ path: route }, pathname) || matchPath({ path: route }, decodedPathname)) {
33
+ return route;
34
+ }
35
+ }
36
+
37
+ return null;
32
38
  };
package/src/routes.js CHANGED
@@ -127,6 +127,7 @@ export const IMPLEMENTATION_MOVE = "/implementations/:implementation_id/move";
127
127
  export const IMPLEMENTATION_NEW = "/implementations/new";
128
128
  export const IMPLEMENTATION_NEW_BASIC = "/implementations/basic";
129
129
  export const IMPLEMENTATION_NEW_RAW = "/implementations/new_raw";
130
+ export const IMPLEMENTATIONS_BY_SUBSCOPE = "/implementations/subscope/:subscope";
130
131
  export const IMPLEMENTATION_RESULTS =
131
132
  "/implementations/:implementation_id/results";
132
133
  export const IMPLEMENTATION_RESULTS_DETAILS =
@@ -385,6 +386,7 @@ const routes = {
385
386
  IMPLEMENTATION_NEW,
386
387
  IMPLEMENTATION_NEW_BASIC,
387
388
  IMPLEMENTATION_NEW_RAW,
389
+ IMPLEMENTATIONS_BY_SUBSCOPE,
388
390
  IMPLEMENTATION_RESULTS,
389
391
  IMPLEMENTATION_RESULTS_DETAILS,
390
392
  IMPLEMENTATION_RESULT_DETAILS,
@@ -0,0 +1,53 @@
1
+ import { getRiSubscopes } from "../getRiSubscopes";
2
+
3
+ describe("selectors: getRiSubscopes", () => {
4
+ it("should return subscopes for ri scope", () => {
5
+ const allTemplates = [
6
+ { scope: "ri", subscope: "subscope1" },
7
+ { scope: "ri", subscope: "subscope2" },
8
+ { scope: "ri", subscope: "subscope1" }, // duplicate
9
+ { scope: "ri", subscope: null }, // should be filtered out
10
+ { scope: "bg", subscope: "other" }, // different scope
11
+ ];
12
+
13
+ const state = { allTemplates };
14
+
15
+ const result = getRiSubscopes(state);
16
+
17
+ expect(result).toEqual(["subscope1", "subscope2"]);
18
+ });
19
+
20
+ it("should return empty array when no templates match", () => {
21
+ const allTemplates = [
22
+ { scope: "bg", subscope: "subscope1" },
23
+ { scope: "dq", subscope: "subscope2" },
24
+ ];
25
+
26
+ const state = { allTemplates };
27
+
28
+ const result = getRiSubscopes(state);
29
+
30
+ expect(result).toEqual([]);
31
+ });
32
+
33
+ it("should return empty array when no subscopes exist", () => {
34
+ const allTemplates = [
35
+ { scope: "ri", subscope: null },
36
+ { scope: "ri", subscope: undefined },
37
+ ];
38
+
39
+ const state = { allTemplates };
40
+
41
+ const result = getRiSubscopes(state);
42
+
43
+ expect(result).toEqual([]);
44
+ });
45
+
46
+ it("should return empty array when allTemplates is empty", () => {
47
+ const state = { allTemplates: [] };
48
+
49
+ const result = getRiSubscopes(state);
50
+
51
+ expect(result).toEqual([]);
52
+ });
53
+ });
@@ -0,0 +1,8 @@
1
+ import _ from "lodash/fp";
2
+ import { createSelector } from "reselect";
3
+ import { getSubscopes } from "./subscopedTemplates";
4
+
5
+ export const getRiSubscopes = createSelector(
6
+ _.prop("allTemplates"),
7
+ (allTemplates) => getSubscopes(allTemplates, "ri")
8
+ );
@@ -5,6 +5,7 @@ export * from "./makeSearchQuerySelector";
5
5
  export * from "./getDashboardConfig";
6
6
  export * from "./getRecipients";
7
7
  export * from "./getConceptSubscope";
8
+ export * from "./getRiSubscopes";
8
9
  export * from "./getSidemenuGlossarySubscopes";
9
10
  export * from "./subscopedTemplates";
10
11
  export * from "./taxonomy";
@@ -5,27 +5,27 @@ export const numberRules = ({
5
5
  maxValue,
6
6
  }) => ({
7
7
  required: required
8
- ? formatMessage({ id: "form.validation.empty_required" })
8
+ ? { value: true, message: formatMessage({ id: "form.validation.empty_required" }) }
9
9
  : null,
10
10
  min:
11
11
  minValue || minValue === 0
12
12
  ? {
13
- value: minValue,
14
- message: formatMessage(
15
- { id: "form.validation.must_be_greater_than_or_equal" },
16
- { value: minValue }
17
- ),
18
- }
13
+ value: minValue,
14
+ message: formatMessage(
15
+ { id: "form.validation.must_be_greater_than_or_equal" },
16
+ { value: minValue }
17
+ ),
18
+ }
19
19
  : null,
20
20
  max:
21
21
  maxValue || maxValue === 0
22
22
  ? {
23
- value: maxValue,
24
- message: formatMessage(
25
- { id: "form.validation.must_be_less_than_or_equal" },
26
- { value: maxValue }
27
- ),
28
- }
23
+ value: maxValue,
24
+ message: formatMessage(
25
+ { id: "form.validation.must_be_less_than_or_equal" },
26
+ { value: maxValue }
27
+ ),
28
+ }
29
29
  : null,
30
30
  pattern: {
31
31
  value: /^(-)?\d+(\.\d+)?$/,