@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 +3 -3
- package/src/components/GlossaryMenu.js +11 -15
- package/src/components/QualityMenu.js +34 -9
- package/src/components/Submenu.js +48 -7
- package/src/components/TemplateSelector.js +6 -2
- package/src/components/__tests__/Submenu.spec.js +13 -0
- package/src/components/__tests__/__snapshots__/AdminMenu.spec.js.snap +10 -0
- package/src/components/__tests__/__snapshots__/Submenu.spec.js.snap +37 -0
- package/src/hooks/__tests__/useActiveRoutes.spec.js +83 -0
- package/src/hooks/useActiveRoutes.js +27 -21
- package/src/routes.js +2 -0
- package/src/selectors/__tests__/getRiSubscopes.spec.js +53 -0
- package/src/selectors/getRiSubscopes.js +8 -0
- package/src/selectors/index.js +1 -0
- package/src/services/formRules.js +13 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/core",
|
|
3
|
-
"version": "8.1.
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
61
|
+
if (!authorized) return null;
|
|
62
|
+
|
|
63
|
+
const filteredBaseItems = _.filter(
|
|
61
64
|
({ name }) => name != "quality_dashboard" || !_.isEmpty(dashboardConfig)
|
|
62
|
-
)(
|
|
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
|
|
65
|
-
<Submenu items={
|
|
66
|
-
)
|
|
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 (
|
|
117
|
+
if (submenuIsActive && sidebarVisible) {
|
|
89
118
|
const menuItems = filteredItems.map((item, i) => {
|
|
90
119
|
const isItemActive =
|
|
91
|
-
|
|
120
|
+
submenuIsActive &&
|
|
92
121
|
(item.isActive
|
|
93
122
|
? item.isActive()
|
|
94
|
-
:
|
|
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 =
|
|
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
|
-
|
|
170
|
+
submenuIsActive &&
|
|
136
171
|
(item.isActive
|
|
137
172
|
? item.isActive()
|
|
138
|
-
:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
});
|
package/src/selectors/index.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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+)?$/,
|