@truedat/core 8.1.1 → 8.1.4

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 (28) hide show
  1. package/package.json +3 -3
  2. package/src/api.js +2 -0
  3. package/src/components/CatalogMenu.js +2 -2
  4. package/src/components/GlossaryMenu.js +11 -15
  5. package/src/components/QualityMenu.js +34 -9
  6. package/src/components/Submenu.js +48 -7
  7. package/src/components/UploadJob.js +217 -0
  8. package/src/components/UploadJobBreadcrumbs.js +38 -0
  9. package/src/components/UploadJobParser.js +370 -0
  10. package/src/components/UploadJobs.js +136 -0
  11. package/src/components/__tests__/Submenu.spec.js +13 -0
  12. package/src/components/__tests__/UploadJob.spec.js +112 -0
  13. package/src/components/__tests__/UploadJobBreadcrumbs.spec.js +37 -0
  14. package/src/components/__tests__/UploadJobParser.spec.js +103 -0
  15. package/src/components/__tests__/UploadJobs.spec.js +60 -0
  16. package/src/components/__tests__/__snapshots__/AdminMenu.spec.js.snap +10 -0
  17. package/src/components/__tests__/__snapshots__/CatalogMenu.spec.js.snap +1 -1
  18. package/src/components/__tests__/__snapshots__/SideMenu.spec.js.snap +1 -1
  19. package/src/components/__tests__/__snapshots__/Submenu.spec.js.snap +37 -0
  20. package/src/components/__tests__/__snapshots__/UploadJobBreadcrumbs.spec.js.snap +42 -0
  21. package/src/hooks/__tests__/useActiveRoutes.spec.js +83 -0
  22. package/src/hooks/index.js +1 -0
  23. package/src/hooks/useActiveRoutes.js +27 -21
  24. package/src/hooks/useUploadJobs.js +24 -0
  25. package/src/routes.js +6 -2
  26. package/src/selectors/__tests__/getRiSubscopes.spec.js +53 -0
  27. package/src/selectors/getRiSubscopes.js +8 -0
  28. package/src/selectors/index.js +1 -0
@@ -0,0 +1,103 @@
1
+ import { render } from "@truedat/test/render";
2
+ import { StatusPill, ResponseCell } from "../UploadJobParser";
3
+
4
+ describe("<StatusPill />", () => {
5
+ it("renders with correct color based on status", async () => {
6
+ const statuses = ["COMPLETED", "FAILED", "ERROR", "INFO", "OTHER"];
7
+ const expectedColors = ["green", "red", "red", "blue", "grey"];
8
+
9
+ statuses.forEach((status, index) => {
10
+ const rendered = render(<StatusPill status={status} />);
11
+ const label = rendered.container.querySelector(".ui.label");
12
+ expect(label).toHaveClass(expectedColors[index]);
13
+ expect(
14
+ rendered.getByText(
15
+ new RegExp(`uploadJob.parser.event.status.${status}`, "i")
16
+ )
17
+ ).toBeInTheDocument();
18
+ });
19
+ });
20
+ });
21
+
22
+ describe("<ResponseCell />", () => {
23
+ it("returns null when response is empty", () => {
24
+ const rendered = render(
25
+ <ResponseCell response={null} status="COMPLETED" />
26
+ );
27
+ expect(rendered.container.firstChild).toBeNull();
28
+
29
+ const rendered2 = render(<ResponseCell response={{}} status="COMPLETED" />);
30
+ expect(rendered2.container.firstChild).toBeNull();
31
+ });
32
+
33
+ it("renders FAILED status with message", () => {
34
+ const response = { message: "missing_required_headers" };
35
+ const rendered = render(
36
+ <ResponseCell response={response} status="FAILED" />
37
+ );
38
+ expect(
39
+ rendered.getByText(/missing_required_headers/i)
40
+ ).toBeInTheDocument();
41
+ });
42
+
43
+ it("renders ERROR status with details", () => {
44
+ const response = {
45
+ type: "missing_required_headers",
46
+ details: {
47
+ missing_headers: ["header1", "header2"],
48
+ },
49
+ };
50
+ const rendered = render(
51
+ <ResponseCell response={response} status="ERROR" />
52
+ );
53
+ expect(
54
+ rendered.getByText(
55
+ /uploadJob.parser.error.missing_required_headers/i
56
+ )
57
+ ).toBeInTheDocument();
58
+ expect(rendered.getByText(/header1, header2/i)).toBeInTheDocument();
59
+ });
60
+
61
+ it("renders COMPLETED status with summary", () => {
62
+ const response = {
63
+ insert_count: 2,
64
+ update_count: 1,
65
+ error_count: 0,
66
+ };
67
+ const rendered = render(
68
+ <ResponseCell response={response} status="COMPLETED" />
69
+ );
70
+ expect(
71
+ rendered.getByText(/uploadJob.parser.result.summary.created/i)
72
+ ).toBeInTheDocument();
73
+ expect(
74
+ rendered.getByText(/uploadJob.parser.result.summary.updated/i)
75
+ ).toBeInTheDocument();
76
+ });
77
+
78
+ it("renders INFO status with implementation details", () => {
79
+ const response = {
80
+ type: "implementation_updated",
81
+ details: {
82
+ id: 123,
83
+ changes: {
84
+ name: "new name",
85
+ },
86
+ },
87
+ };
88
+ const rendered = render(<ResponseCell response={response} status="INFO" />);
89
+ expect(
90
+ rendered.getByText(
91
+ /uploadJob.parser.info.implementation_updated/i
92
+ )
93
+ ).toBeInTheDocument();
94
+ });
95
+
96
+ it("renders default case for unknown status", () => {
97
+ const response = "some text";
98
+ const rendered = render(
99
+ <ResponseCell response={response} status="UNKNOWN" />
100
+ );
101
+ expect(rendered.getByText(/some text/i)).toBeInTheDocument();
102
+ });
103
+ });
@@ -0,0 +1,60 @@
1
+ import { render, waitForLoad } from "@truedat/test/render";
2
+ import { useUploadJobs } from "../../hooks/useUploadJobs";
3
+ import UploadJobs from "../UploadJobs";
4
+
5
+ jest.mock("../../hooks/useUploadJobs", () => ({
6
+ useUploadJobs: jest.fn(),
7
+ }));
8
+
9
+ describe("<UploadJobs />", () => {
10
+ it("renders loading state", async () => {
11
+ useUploadJobs.mockReturnValue({
12
+ loading: true,
13
+ data: null,
14
+ });
15
+
16
+ const rendered = render(<UploadJobs />);
17
+
18
+ expect(rendered.container.querySelector(".ui.loader")).toBeInTheDocument();
19
+ });
20
+
21
+ it("renders empty state", async () => {
22
+ useUploadJobs.mockReturnValue({
23
+ loading: false,
24
+ data: {
25
+ data: [],
26
+ },
27
+ });
28
+
29
+ const rendered = render(<UploadJobs />);
30
+ await waitForLoad(rendered);
31
+
32
+ expect(rendered.getByText(/No jobs found./i)).toBeInTheDocument();
33
+ });
34
+
35
+ it("renders jobs list", async () => {
36
+ const mockJobs = [
37
+ {
38
+ id: 1,
39
+ filename: "test.csv",
40
+ latest_status: "completed",
41
+ latest_event_response: "Success",
42
+ latest_event_at: "2023-01-01T00:00:00Z",
43
+ },
44
+ ];
45
+
46
+ useUploadJobs.mockReturnValue({
47
+ loading: false,
48
+ data: {
49
+ data: mockJobs,
50
+ },
51
+ });
52
+
53
+ const rendered = render(<UploadJobs />);
54
+ await waitForLoad(rendered);
55
+
56
+ expect(rendered.getByText(/test.csv/i)).toBeInTheDocument();
57
+ expect(rendered.getByText(/completed/i)).toBeInTheDocument();
58
+ expect(rendered.getByText(/success/i)).toBeInTheDocument();
59
+ });
60
+ });
@@ -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"
@@ -56,7 +56,7 @@ exports[`<CatalogMenu /> matches the latest snapshot 1`] = `
56
56
  <a
57
57
  class="link item"
58
58
  data-discover="true"
59
- href="/bulkUpdateTemplateContentEvents"
59
+ href="/structureNotes/uploadJobs"
60
60
  >
61
61
  structures_upload_events
62
62
  </a>
@@ -192,7 +192,7 @@ exports[`<SideMenu /> matches the latest snapshot 1`] = `
192
192
  <a
193
193
  class="link item"
194
194
  data-discover="true"
195
- href="/bulkUpdateTemplateContentEvents"
195
+ href="/structureNotes/uploadJobs"
196
196
  >
197
197
  structures_upload_events
198
198
  </a>
@@ -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,42 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`ImplementationUploadJobBreadcrumbs renders breadcrumbs with filename 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui breadcrumb"
7
+ >
8
+ <a
9
+ class="section"
10
+ data-discover="true"
11
+ href="/implementations/uploadJobs"
12
+ >
13
+ uploadJobs.implementations.header
14
+ </a>
15
+ <i
16
+ aria-hidden="true"
17
+ class="right angle icon divider"
18
+ />
19
+ <div
20
+ class="active section"
21
+ >
22
+ test.csv
23
+ </div>
24
+ </div>
25
+ </div>
26
+ `;
27
+
28
+ exports[`ImplementationUploadJobBreadcrumbs renders breadcrumbs without filename 1`] = `
29
+ <div>
30
+ <div
31
+ class="ui breadcrumb"
32
+ >
33
+ <a
34
+ class="section"
35
+ data-discover="true"
36
+ href="/implementations/uploadJobs"
37
+ >
38
+ uploadJobs.implementations.header
39
+ </a>
40
+ </div>
41
+ </div>
42
+ `;
@@ -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
+ });
@@ -7,3 +7,4 @@ export * from "./usePath";
7
7
  export * from "./useOnScreen";
8
8
  export * from "./useOperators";
9
9
  export * from "./useTemplate";
10
+ export * from "./useUploadJobs";
@@ -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
  };
@@ -0,0 +1,24 @@
1
+ import { compile } from "path-to-regexp";
2
+ import useSWR from "swr";
3
+ import { apiJson } from "@truedat/core/services/api";
4
+ import {
5
+ API_UPLOAD_JOBS,
6
+ API_UPLOAD_JOB,
7
+ } from "../api";
8
+
9
+ export const useUploadJobs = (scope) => {
10
+ const url = scope ? `${API_UPLOAD_JOBS}?scope=${scope}` : API_UPLOAD_JOBS;
11
+ const { data, error, mutate } = useSWR(
12
+ url,
13
+ apiJson
14
+ );
15
+ return { data: data?.data, error, loading: !error && !data, mutate };
16
+ };
17
+
18
+ export const useUploadJob = (id) => {
19
+ const url = compile(API_UPLOAD_JOB)({
20
+ id: `${id}`,
21
+ });
22
+ const { data, error, mutate } = useSWR(url, apiJson);
23
+ return { data: data?.data, error, loading: !error && !data, mutate };
24
+ };
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 =
@@ -233,7 +234,6 @@ export const SOURCE_JOBS_NEW = "/sources/:sourceId/jobs/new";
233
234
  export const STRUCTURE = "/structures/:id";
234
235
  export const STRUCTURES = "/structures";
235
236
  export const STRUCTURES_BULK_UPDATE = "/structures/bulkUpdate";
236
- export const STRUCTURES_UPLOAD_EVENTS = "/bulkUpdateTemplateContentEvents";
237
237
  export const STRUCTURE_CHILDREN = "/structures/:id/children";
238
238
  export const STRUCTURE_FIELDS = "/structures/:id/fields";
239
239
  export const STRUCTURE_VERSION_FIELDS =
@@ -253,6 +253,8 @@ export const STRUCTURE_MEMBERS_NEW = "/structures/:id/members/new";
253
253
  export const STRUCTURE_METADATA = "/structures/:id/metadata";
254
254
  export const STRUCTURE_NOTES = "/structures/:id/notes";
255
255
  export const STRUCTURE_NOTES_EDIT = "/structures/:id/notes/edit";
256
+ export const STRUCTURE_NOTES_UPLOAD_JOBS = "/structureNotes/uploadJobs";
257
+ export const STRUCTURE_NOTES_UPLOAD_JOB = "/structureNotes/uploadJobs/:id";
256
258
  export const STRUCTURE_PARENTS = "/structures/:id/parents";
257
259
  export const STRUCTURE_PROFILE = "/structures/:id/profile";
258
260
  export const STRUCTURE_RULES = "/structures/:id/rules";
@@ -385,6 +387,7 @@ const routes = {
385
387
  IMPLEMENTATION_NEW,
386
388
  IMPLEMENTATION_NEW_BASIC,
387
389
  IMPLEMENTATION_NEW_RAW,
390
+ IMPLEMENTATIONS_BY_SUBSCOPE,
388
391
  IMPLEMENTATION_RESULTS,
389
392
  IMPLEMENTATION_RESULTS_DETAILS,
390
393
  IMPLEMENTATION_RESULT_DETAILS,
@@ -471,7 +474,6 @@ const routes = {
471
474
  STRUCTURE,
472
475
  STRUCTURES,
473
476
  STRUCTURES_BULK_UPDATE,
474
- STRUCTURES_UPLOAD_EVENTS,
475
477
  STRUCTURE_CHILDREN,
476
478
  STRUCTURE_FIELDS,
477
479
  STRUCTURE_VERSION_FIELDS,
@@ -489,6 +491,8 @@ const routes = {
489
491
  STRUCTURE_METADATA,
490
492
  STRUCTURE_NOTES_EDIT,
491
493
  STRUCTURE_NOTES,
494
+ STRUCTURE_NOTES_UPLOAD_JOBS,
495
+ STRUCTURE_NOTES_UPLOAD_JOB,
492
496
  STRUCTURE_PARENTS,
493
497
  STRUCTURE_PROFILE,
494
498
  STRUCTURE_RULES,
@@ -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";