orc-shared 5.4.0-dev.0 → 5.5.1-dev.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 (66) hide show
  1. package/dist/actions/navigation.js +14 -1
  2. package/dist/actions/tasks.js +170 -0
  3. package/dist/buildStore.js +3 -1
  4. package/dist/components/AppFrame/Sidebar.js +4 -8
  5. package/dist/components/MaterialUI/DataDisplay/PredefinedElements/InformationItem.js +13 -5
  6. package/dist/components/MaterialUI/DataDisplay/TooltippedElements/MultipleLinesText.js +4 -2
  7. package/dist/components/MaterialUI/Inputs/PredefinedElements/SearchControl.js +43 -46
  8. package/dist/components/MaterialUI/hocs/withDeferredTooltip.js +3 -1
  9. package/dist/components/Routing/Page.js +4 -1
  10. package/dist/components/Routing/SegmentPage.js +4 -1
  11. package/dist/components/Routing/SubPage.js +21 -8
  12. package/dist/components/TaskDetailsModal.js +191 -0
  13. package/dist/constants.js +18 -2
  14. package/dist/content/iconsSheet.svg +22 -0
  15. package/dist/content/orckestra-logo-white.png +0 -0
  16. package/dist/hooks/useEditState.js +4 -2
  17. package/dist/reducers/navigation.js +16 -0
  18. package/dist/reducers/request.js +4 -0
  19. package/dist/reducers/tasks.js +98 -0
  20. package/dist/selectors/authentication.js +15 -1
  21. package/dist/selectors/tasks.js +64 -0
  22. package/dist/sharedMessages.js +17 -1
  23. package/dist/utils/propertyHelper.js +33 -0
  24. package/package.json +1 -1
  25. package/src/actions/navigation.js +7 -0
  26. package/src/actions/navigation.test.js +12 -0
  27. package/src/actions/tasks.js +77 -0
  28. package/src/actions/tasks.test.js +169 -0
  29. package/src/buildStore.js +2 -0
  30. package/src/components/AppFrame/About.test.js +3 -3
  31. package/src/components/AppFrame/Sidebar.js +4 -3
  32. package/src/components/MaterialUI/DataDisplay/PredefinedElements/InformationItem.js +15 -3
  33. package/src/components/MaterialUI/DataDisplay/TooltippedElements/MultipleLinesText.js +2 -1
  34. package/src/components/MaterialUI/Inputs/PredefinedElements/SearchControl.js +39 -27
  35. package/src/components/MaterialUI/Inputs/PredefinedElements/SearchControl.test.js +39 -34
  36. package/src/components/MaterialUI/hocs/withDeferredTooltip.js +2 -1
  37. package/src/components/MaterialUI/hocs/withDeferredTooltip.test.js +52 -0
  38. package/src/components/Routing/Page.js +12 -1
  39. package/src/components/Routing/SegmentPage.js +12 -1
  40. package/src/components/Routing/SubPage.js +21 -9
  41. package/src/components/Routing/SubPage.test.js +213 -0
  42. package/src/components/TaskDetailsModal.js +132 -0
  43. package/src/components/TaskDetailsModal.test.js +317 -0
  44. package/src/components/Text.test.js +44 -59
  45. package/src/constants.js +15 -0
  46. package/src/content/iconsSheet.svg +22 -0
  47. package/src/content/orckestra-logo-white.png +0 -0
  48. package/src/hooks/useEditState.js +12 -2
  49. package/src/hooks/useEditState.test.js +1 -1
  50. package/src/hooks/useLabelMessage.test.js +16 -10
  51. package/src/reducers/navigation.js +24 -0
  52. package/src/reducers/navigation.test.js +38 -0
  53. package/src/reducers/request.js +4 -0
  54. package/src/reducers/request.test.js +11 -0
  55. package/src/reducers/tasks.js +56 -0
  56. package/src/reducers/tasks.test.js +404 -0
  57. package/src/selectors/authentication.js +13 -0
  58. package/src/selectors/authentication.test.js +322 -0
  59. package/src/selectors/tasks.js +16 -0
  60. package/src/selectors/tasks.test.js +60 -0
  61. package/src/sharedMessages.js +17 -1
  62. package/src/translations/en-US.json +16 -12
  63. package/src/translations/fr-CA.json +16 -12
  64. package/src/utils/propertyHelper.js +38 -0
  65. package/src/utils/propertyHelper.test.js +160 -0
  66. package/src/utils/timezoneHelper.test.js +4 -2
@@ -25,6 +25,13 @@ export const removeTab = (module, path) => ({
25
25
  payload: { module, path },
26
26
  });
27
27
 
28
+ export const REMOVE_MODULE_TABS = "REMOVE_MODULE_TABS";
29
+
30
+ export const removeModuleTabs = module => ({
31
+ type: REMOVE_MODULE_TABS,
32
+ payload: { module },
33
+ });
34
+
28
35
  export const SET_HREF_CONFIG = "SET_HREF_CONFIG";
29
36
 
30
37
  export const setHrefConfig = (prependPath, prependHref, otherConfigs = {}) => ({
@@ -5,6 +5,8 @@ import {
5
5
  SET_HREF_CONFIG,
6
6
  setClosingTabHandlerActions,
7
7
  SET_CLOSING_TAB_HANDLER_ACTIONS,
8
+ removeModuleTabs,
9
+ REMOVE_MODULE_TABS,
8
10
  } from "./navigation";
9
11
 
10
12
  describe("removeTab", () => {
@@ -18,6 +20,16 @@ describe("removeTab", () => {
18
20
  }));
19
21
  });
20
22
 
23
+ describe("removeModuleTabs", () => {
24
+ it("creates a remove module tabs action", () =>
25
+ expect(removeModuleTabs, "when called with", ["module"], "to equal", {
26
+ type: REMOVE_MODULE_TABS,
27
+ payload: {
28
+ module: "module",
29
+ },
30
+ }));
31
+ });
32
+
21
33
  describe("setHrefConfig", () => {
22
34
  it("set href config action", () =>
23
35
  expect(setHrefConfig, "when called with", ["/:scope/", "/scope/"], "to equal", {
@@ -0,0 +1,77 @@
1
+ import { makeActionTypes } from "./makeApiAction";
2
+ import makeOrcApiAction from "./makeOrcApiAction";
3
+ import {
4
+ deleteTaskInfoRequest,
5
+ getRequesterTasksInfoRequest,
6
+ getTaskExecutionLogsRequest,
7
+ getTaskInfoRequest,
8
+ } from "./requestsApi";
9
+
10
+ const GET_TASKINFO = "GET_TASKINFO";
11
+
12
+ export const [GET_TASKINFO_REQUEST, GET_TASKINFO_SUCCESS, GET_TASKINFO_FAILURE] = makeActionTypes(GET_TASKINFO);
13
+
14
+ export const getTaskInfo = taskId => makeOrcApiAction(GET_TASKINFO, getTaskInfoRequest.buildUrl(taskId));
15
+
16
+ export const GET_TASK_LIST = "GET_TASK_LIST";
17
+
18
+ export const ssrsDownloadFilterTaskNames = [
19
+ "Orckestra.Overture.Providers.CommerceEngine.Profiles.ProfileSchemaExportTask, Orckestra.Overture.Providers.CommerceEngine",
20
+ "Orckestra.Overture.Providers.CommerceEngine.Profiles.ProfileSchemaImportTask, Orckestra.Overture.Providers.CommerceEngine",
21
+ "Orckestra.Overture.Providers.CommerceEngine.Orders.ExportOrderSchemaTask, Orckestra.Overture.Providers.CommerceEngine",
22
+ "Orckestra.Overture.Providers.CommerceEngine.Orders.ImportOrderSchemaTask, Orckestra.Overture.Providers.CommerceEngine",
23
+ "Orckestra.Overture.Providers.CommerceEngine.Products.ImportExport.ExportProductsTask, Orckestra.Overture.Providers.CommerceEngine",
24
+ "Orckestra.Overture.Providers.CommerceEngine.Products.ImportExport.ProductSchemaExportTask, Orckestra.Overture.Providers.CommerceEngine",
25
+ "Orckestra.Overture.Providers.CommerceEngine.Products.ImportExport.ImportProductsTask, Orckestra.Overture.Providers.CommerceEngine",
26
+ "OrckestraCommerce.DataExchange.Product.Tasks.ProductExportTask, OrckestraCommerce.DataExchange",
27
+ "Orckestra.Overture.Providers.CommerceEngine.Marketing.ExportCouponCodesTask, Orckestra.Overture.Providers.CommerceEngine",
28
+ "Orckestra.Overture.Providers.CommerceEngine.Marketing.ImportCouponCodesTask, Orckestra.Overture.Providers.CommerceEngine",
29
+ "Orckestra.Overture.Providers.CommerceEngine.Marketing.GenerateCouponTask, Orckestra.Overture.Providers.CommerceEngine",
30
+ "Orckestra.Overture.Providers.CommerceEngine.Reporting.ReportExportTask, Orckestra.Overture.Providers.CommerceEngine",
31
+ ];
32
+
33
+ export const [GET_TASK_LIST_REQUEST, GET_TASK_LIST_SUCCESS, GET_TASK_LIST_FAILURE] = makeActionTypes(GET_TASK_LIST);
34
+
35
+ export const getTaskList = (requester, filterTaskNames, lastModified = null, addToActiveRequests = true) =>
36
+ makeOrcApiAction(
37
+ GET_TASK_LIST,
38
+ getRequesterTasksInfoRequest.buildUrl({
39
+ filterTaskNames: filterTaskNames,
40
+ requester: requester,
41
+ lastModified: lastModified,
42
+ }),
43
+ getRequesterTasksInfoRequest.verb,
44
+ {
45
+ meta: {
46
+ addToActiveRequests,
47
+ },
48
+ },
49
+ );
50
+
51
+ const DELETE_TASK = "DELETE_TASK";
52
+
53
+ export const [DELETE_TASK_REQUEST, DELETE_TASK_SUCCESS, DELETE_TASK_FAILURE] = makeActionTypes(DELETE_TASK);
54
+
55
+ export const deleteTask = taskId =>
56
+ makeOrcApiAction(DELETE_TASK, deleteTaskInfoRequest.buildUrl(taskId), deleteTaskInfoRequest.verb, {
57
+ meta: { taskId },
58
+ });
59
+
60
+ const GET_TASK_LOG = "GET_TASK_LOG";
61
+
62
+ export const [GET_TASK_LOG_REQUEST, GET_TASK_LOG_SUCCESS, GET_TASK_LOG_FAILURE] = makeActionTypes(GET_TASK_LOG);
63
+
64
+ export const getTaskLog = (taskId, addToActiveRequests = true) =>
65
+ makeOrcApiAction(GET_TASK_LOG, getTaskExecutionLogsRequest.buildUrl(taskId), getTaskExecutionLogsRequest.verb, {
66
+ meta: {
67
+ taskId,
68
+ addToActiveRequests,
69
+ },
70
+ });
71
+
72
+ export const CLEAR_TASK_LOG = "CLEAR_TASK_LOG";
73
+
74
+ export const clearTaskLog = taskId => ({
75
+ type: CLEAR_TASK_LOG,
76
+ meta: { taskId },
77
+ });
@@ -0,0 +1,169 @@
1
+ import { RSAA } from "redux-api-middleware";
2
+ import {
3
+ GET_TASKINFO_REQUEST,
4
+ GET_TASKINFO_SUCCESS,
5
+ GET_TASKINFO_FAILURE,
6
+ getTaskInfo,
7
+ getTaskList,
8
+ getTaskLog,
9
+ ssrsDownloadFilterTaskNames,
10
+ deleteTask,
11
+ } from "./tasks";
12
+
13
+ jest.mock("../utils/buildUrl", () => {
14
+ const modExport = {};
15
+ modExport.loadConfig = () => Promise.resolve({});
16
+ modExport.buildUrl = (path = [], params = "") => "URL: " + path.join("/") + " " + JSON.stringify(params);
17
+ return modExport;
18
+ });
19
+
20
+ describe("getTaskInfo", () => {
21
+ it("creates a RSAA to get task info", () =>
22
+ expect(getTaskInfo, "when called with", ["1234"], "to exhaustively satisfy", {
23
+ [RSAA]: {
24
+ types: [GET_TASKINFO_REQUEST, GET_TASKINFO_SUCCESS, GET_TASKINFO_FAILURE],
25
+ endpoint: 'URL: tasks/1234 ""',
26
+ method: "GET",
27
+ body: undefined,
28
+ credentials: "include",
29
+ bailout: expect.it("to be a function"),
30
+ headers: {
31
+ Accept: "application/json; charset=utf-8",
32
+ "Content-Type": "application/json",
33
+ },
34
+ options: { redirect: "follow" },
35
+ },
36
+ }));
37
+ });
38
+
39
+ describe("getTaskLog", () => {
40
+ it("creates a RSAA to get task info", () =>
41
+ expect(getTaskLog, "when called with", ["1234"], "to exhaustively satisfy", {
42
+ [RSAA]: {
43
+ types: [
44
+ { type: "GET_TASK_LOG_REQUEST", meta: { taskId: "1234", addToActiveRequests: true } },
45
+ { type: "GET_TASK_LOG_SUCCESS", meta: { taskId: "1234", addToActiveRequests: true } },
46
+ { type: "GET_TASK_LOG_FAILURE", meta: { taskId: "1234", addToActiveRequests: true } },
47
+ ],
48
+ endpoint: 'URL: tasks/1234/logs ""',
49
+ method: "GET",
50
+ body: undefined,
51
+ credentials: "include",
52
+ bailout: expect.it("to be a function"),
53
+ headers: {
54
+ Accept: "application/json; charset=utf-8",
55
+ "Content-Type": "application/json",
56
+ },
57
+ options: { redirect: "follow" },
58
+ },
59
+ }));
60
+
61
+ it("creates a RSAA to get task info with addToActiveRequests false", () =>
62
+ expect(getTaskLog, "when called with", ["1234", false], "to exhaustively satisfy", {
63
+ [RSAA]: {
64
+ types: [
65
+ { type: "GET_TASK_LOG_REQUEST", meta: { taskId: "1234", addToActiveRequests: false } },
66
+ { type: "GET_TASK_LOG_SUCCESS", meta: { taskId: "1234", addToActiveRequests: false } },
67
+ { type: "GET_TASK_LOG_FAILURE", meta: { taskId: "1234", addToActiveRequests: false } },
68
+ ],
69
+ endpoint: 'URL: tasks/1234/logs ""',
70
+ method: "GET",
71
+ body: undefined,
72
+ credentials: "include",
73
+ bailout: expect.it("to be a function"),
74
+ headers: {
75
+ Accept: "application/json; charset=utf-8",
76
+ "Content-Type": "application/json",
77
+ },
78
+ options: { redirect: "follow" },
79
+ },
80
+ }));
81
+ });
82
+
83
+ describe("getTaskList", () => {
84
+ it("creates a RSAA to get task info", () => {
85
+ const expectedParameters = {
86
+ filterTaskNames: ssrsDownloadFilterTaskNames,
87
+ requester: "freddie",
88
+ lastModified: "date value",
89
+ };
90
+
91
+ expect(
92
+ getTaskList,
93
+ "when called with",
94
+ ["freddie", ssrsDownloadFilterTaskNames, "date value", false],
95
+ "to exhaustively satisfy",
96
+ {
97
+ [RSAA]: {
98
+ types: [
99
+ { type: "GET_TASK_LIST_REQUEST", meta: { addToActiveRequests: false } },
100
+ { type: "GET_TASK_LIST_SUCCESS", meta: { addToActiveRequests: false } },
101
+ { type: "GET_TASK_LIST_FAILURE", meta: { addToActiveRequests: false } },
102
+ ],
103
+ endpoint: "URL: tasks " + JSON.stringify(expectedParameters),
104
+ method: "GET",
105
+ body: undefined,
106
+ credentials: "include",
107
+ bailout: expect.it("to be a function"),
108
+ headers: {
109
+ Accept: "application/json; charset=utf-8",
110
+ "Content-Type": "application/json",
111
+ },
112
+ options: { redirect: "follow" },
113
+ },
114
+ },
115
+ );
116
+ });
117
+
118
+ it("creates a RSAA to get task info with null lastModified and true addToActiveRequests", () => {
119
+ const expectedParameters = {
120
+ filterTaskNames: ssrsDownloadFilterTaskNames,
121
+ requester: "freddie",
122
+ lastModified: null,
123
+ };
124
+
125
+ expect(getTaskList, "when called with", ["freddie", ssrsDownloadFilterTaskNames], "to exhaustively satisfy", {
126
+ [RSAA]: {
127
+ types: [
128
+ { type: "GET_TASK_LIST_REQUEST", meta: { addToActiveRequests: true } },
129
+ { type: "GET_TASK_LIST_SUCCESS", meta: { addToActiveRequests: true } },
130
+ { type: "GET_TASK_LIST_FAILURE", meta: { addToActiveRequests: true } },
131
+ ],
132
+ endpoint: "URL: tasks " + JSON.stringify(expectedParameters),
133
+ method: "GET",
134
+ body: undefined,
135
+ credentials: "include",
136
+ bailout: expect.it("to be a function"),
137
+ headers: {
138
+ Accept: "application/json; charset=utf-8",
139
+ "Content-Type": "application/json",
140
+ },
141
+ options: { redirect: "follow" },
142
+ },
143
+ });
144
+ });
145
+ });
146
+
147
+ describe("deleteTask", () => {
148
+ it("creates a RSAA to delete a task", () => {
149
+ expect(deleteTask, "when called with", ["1234"], "to exhaustively satisfy", {
150
+ [RSAA]: {
151
+ types: [
152
+ { type: "DELETE_TASK_REQUEST", meta: { taskId: "1234" } },
153
+ { type: "DELETE_TASK_SUCCESS", meta: { taskId: "1234" } },
154
+ { type: "DELETE_TASK_FAILURE", meta: { taskId: "1234" } },
155
+ ],
156
+ endpoint: 'URL: tasks/1234 ""',
157
+ method: "DELETE",
158
+ body: undefined,
159
+ credentials: "include",
160
+ bailout: expect.it("to be a function"),
161
+ headers: {
162
+ Accept: "application/json; charset=utf-8",
163
+ "Content-Type": "application/json",
164
+ },
165
+ options: { redirect: "follow" },
166
+ },
167
+ });
168
+ });
169
+ });
package/src/buildStore.js CHANGED
@@ -20,6 +20,7 @@ import timezonesReducer from "./reducers/timezones";
20
20
  import modulesReducer from "./reducers/modules";
21
21
  import metadataReducer from "./reducers/metadata";
22
22
  import requestStatesReducer from "./reducers/requestStates";
23
+ import tasksReducer from "./reducers/tasks";
23
24
 
24
25
  window.BUILD_ID = BUILD_ID;
25
26
  window.BUILD_NUMBER = BUILD_NUMBER;
@@ -65,6 +66,7 @@ const buildStore = (reducers, devOptions = {}) => {
65
66
  modules: modulesReducer,
66
67
  metadata: metadataReducer,
67
68
  requestStates: requestStatesReducer,
69
+ tasks: tasksReducer,
68
70
  });
69
71
  const rootReducer = buildReducer(reducers);
70
72
 
@@ -79,7 +79,7 @@ describe("About", () => {
79
79
  </AboutLink>
80
80
  </AboutParagraph>
81
81
  <AboutParagraph>
82
- {stringifyWithoutQuotes(messages["orc-shared.copyright"])}
82
+ {stringifyWithoutQuotes(messages["orc-shared.copyright"]).replace("{year}", new Date().getFullYear())}
83
83
  <br />
84
84
  {stringifyWithoutQuotes(messages["orc-shared.allRightsReserved"])}
85
85
  </AboutParagraph>
@@ -130,7 +130,7 @@ describe("About", () => {
130
130
  </AboutLink>
131
131
  </AboutParagraph>
132
132
  <AboutParagraph>
133
- {stringifyWithoutQuotes(messages["orc-shared.copyright"])}
133
+ {stringifyWithoutQuotes(messages["orc-shared.copyright"]).replace("{year}", new Date().getFullYear())}
134
134
  <br />
135
135
  {stringifyWithoutQuotes(messages["orc-shared.allRightsReserved"])}
136
136
  </AboutParagraph>
@@ -165,7 +165,7 @@ describe("About", () => {
165
165
  </AboutLink>
166
166
  </AboutParagraph>
167
167
  <AboutParagraph>
168
- {stringifyWithoutQuotes(messages["orc-shared.copyright"])}
168
+ {stringifyWithoutQuotes(messages["orc-shared.copyright"]).replace("{year}", new Date().getFullYear())}
169
169
  <br />
170
170
  {stringifyWithoutQuotes(messages["orc-shared.allRightsReserved"])}
171
171
  </AboutParagraph>
@@ -60,9 +60,10 @@ const LogoSvg = styled.svg`
60
60
  `;
61
61
 
62
62
  export const Logo = () => (
63
- <LogoSvg viewBox="0 0 260 260">
64
- <path d="M1.11,125.62C1.11,74.94,40.22,39,93.72,39S186,74.94,186,125.62s-38.79,86.66-92.29,86.66S1.11,176.3,1.11,125.62Zm135.47,0c0-29.1-18.46-46.62-42.86-46.62S50.54,96.52,50.54,125.62s18.77,46.62,43.18,46.62S136.58,154.72,136.58,125.62Z" />
65
- <circle cx="227.6" cy="181.13" r="31.29" />
63
+ <LogoSvg viewBox="0 0 354 354">
64
+ <g>
65
+ <path d="M0 241.41c0-65.49 50.53-111.98 119.66-111.98s119.25 46.49 119.25 111.98c0 65.49-50.13 111.97-119.25 111.97S0 306.9 0 241.41Zm175.04 0c0-37.6-23.85-60.23-55.38-60.23s-55.79 22.64-55.79 60.23c0 37.59 24.26 60.23 55.79 60.23 31.53 0 55.38-22.64 55.38-60.23Zm178.1-76.35h-77.48c0-48.29-39.29-87.58-87.58-87.58V0c91.02 0 165.07 74.05 165.07 165.06h-.01Z" />
66
+ </g>
66
67
  </LogoSvg>
67
68
  );
68
69
 
@@ -7,6 +7,7 @@ import TextProps from "../../textProps";
7
7
  import { isReactComponent } from "../../../../utils/propertyValidator";
8
8
  import sharedMessages from "../../../../sharedMessages";
9
9
  import { useIntl } from "react-intl";
10
+ import classNames from "classnames";
10
11
 
11
12
  const useStyles = makeStyles(theme => ({
12
13
  title: {
@@ -53,7 +54,14 @@ const useStyles = makeStyles(theme => ({
53
54
  },
54
55
  }));
55
56
 
56
- const InformationItemChildren = ({ classes, children, showNotAvailable, isMaxLineCountEnabled = true }) => {
57
+ const InformationItemChildren = ({
58
+ classes,
59
+ children,
60
+ showNotAvailable,
61
+ isMaxLineCountEnabled = true,
62
+ tooltipClasses,
63
+ valueClasses,
64
+ }) => {
57
65
  const { formatMessage } = useIntl();
58
66
 
59
67
  if (isReactComponent(children)) {
@@ -62,11 +70,11 @@ const InformationItemChildren = ({ classes, children, showNotAvailable, isMaxLin
62
70
 
63
71
  const multipleLinesTextProps = new TextProps();
64
72
  if (isMaxLineCountEnabled) multipleLinesTextProps.set(TextProps.propNames.lineCount, 2);
65
- multipleLinesTextProps.set(TextProps.propNames.classes, classes.value);
73
+ multipleLinesTextProps.set(TextProps.propNames.classes, classNames(classes.value, valueClasses));
66
74
 
67
75
  const value = children ?? (showNotAvailable ? formatMessage(sharedMessages.notAvailable) : "");
68
76
 
69
- return <MultipleLinesText textProps={multipleLinesTextProps} children={value} />;
77
+ return <MultipleLinesText textProps={multipleLinesTextProps} children={value} tooltipClasses={tooltipClasses} />;
70
78
  };
71
79
 
72
80
  const InformationItemHeader = ({ classes, label, headerIcon }) => {
@@ -94,6 +102,8 @@ const InformationItem = ({
94
102
  showNotAvailable = false,
95
103
  marginTop = 0,
96
104
  isMaxLineCountEnabled,
105
+ tooltipClasses,
106
+ valueClasses,
97
107
  }) => {
98
108
  const classes = useStyles({ required, error, marginTop });
99
109
 
@@ -105,6 +115,8 @@ const InformationItem = ({
105
115
  children={children}
106
116
  showNotAvailable={showNotAvailable}
107
117
  isMaxLineCountEnabled={isMaxLineCountEnabled}
118
+ tooltipClasses={tooltipClasses}
119
+ valueClasses={valueClasses}
108
120
  />
109
121
  </div>
110
122
  );
@@ -11,7 +11,7 @@ const useStyles = makeStyles(theme => ({
11
11
  },
12
12
  }));
13
13
 
14
- const MultipleLinesText = ({ children, titleValue, textProps }) => {
14
+ const MultipleLinesText = ({ children, titleValue, textProps, tooltipClasses }) => {
15
15
  const classes = useStyles();
16
16
 
17
17
  const [isClamped, setIsClamped] = useState(false);
@@ -48,6 +48,7 @@ const MultipleLinesText = ({ children, titleValue, textProps }) => {
48
48
  titleValue={title}
49
49
  alwaysDisplay={isClamped}
50
50
  onClampStart={event => clampHandler(event)}
51
+ tooltipClasses={tooltipClasses}
51
52
  />
52
53
  );
53
54
  };
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef } from "react";
1
+ import React, { useRef, useEffect } from "react";
2
2
  import { makeStyles } from "@material-ui/core/styles";
3
3
  import SelectProps from "../SelectProps";
4
4
  import Icon from "../../DataDisplay/Icon";
@@ -80,12 +80,9 @@ export const useStyles = makeStyles(theme => ({
80
80
  },
81
81
  parentInput: {
82
82
  flex: "13 0 0",
83
- zIndex: props => (props.focused ? 99 : 1),
84
- border: props =>
85
- props.focused
86
- ? `${theme.spacing(0.1)} solid ${theme.palette.focus}`
87
- : `${theme.spacing(0.1)} solid ${theme.palette.grey.borders}`,
88
- boxShadow: props => (props.focused ? `0 0 4px ${theme.palette.focus}` : "none"),
83
+ zIndex: 1,
84
+ border: `${theme.spacing(0.1)} solid ${theme.palette.grey.borders}`,
85
+ boxShadow: "none",
89
86
  width: "100%",
90
87
  display: "inherit",
91
88
  marginLeft: theme.spacing(-0.1),
@@ -104,6 +101,11 @@ export const useStyles = makeStyles(theme => ({
104
101
  borderBottomLeftRadius: 0,
105
102
  },
106
103
  },
104
+ "&:focus-within": {
105
+ zIndex: 99,
106
+ border: `${theme.spacing(0.1)} solid ${theme.palette.focus}`,
107
+ boxShadow: `0 0 4px ${theme.palette.focus}`,
108
+ },
107
109
  },
108
110
  selectRoot: {
109
111
  zIndex: 10,
@@ -149,17 +151,30 @@ const SearchControl = ({
149
151
  searchOption,
150
152
  onSearch = () => {},
151
153
  disabled,
154
+ focusAndSelectSearchFieldOnLoad = true,
155
+ focusSearchOnSearchOptionChange = false,
152
156
  }) => {
153
157
  searchOptions = !searchOptions?.length ? null : searchOptions;
154
158
  searchOption = getSearchOptionValue(searchOptions, searchOption);
155
- const [inputFocused, setInputFocused] = useState(false);
156
159
 
157
- const classes = useStyles({ focused: inputFocused });
160
+ const classes = useStyles();
158
161
 
159
162
  const inputRef = useRef();
160
163
 
161
164
  const update = value => {
162
- onSearch(value, defaultValue);
165
+ if (focusSearchOnSearchOptionChange && inputRef.current) {
166
+ onSearch(value, "");
167
+ setTimeout(() => {
168
+ /* istanbul ignore next */
169
+ if (inputRef.current) {
170
+ inputRef.current.value = "";
171
+ inputRef.current.focus();
172
+ inputRef.current.select();
173
+ }
174
+ }, 0);
175
+ } else {
176
+ onSearch(value, defaultValue);
177
+ }
163
178
  };
164
179
 
165
180
  const selectProps = new SelectProps();
@@ -178,28 +193,28 @@ const SearchControl = ({
178
193
  selectProps.setStyle(SelectProps.ruleNames.root, classes.selectRoot);
179
194
  selectProps.setStyle(SelectProps.ruleNames.paper, classes.selectPaper);
180
195
 
181
- const handleKeyDown = e => {
182
- if (e.key === "Enter") {
183
- onSearch(searchOption, e.target.value);
184
- e.preventDefault();
185
- e.stopPropagation();
196
+ useEffect(() => {
197
+ /* istanbul ignore next */
198
+ if (focusAndSelectSearchFieldOnLoad && inputRef.current) {
199
+ inputRef.current.focus();
200
+ inputRef.current.select();
186
201
  }
187
- };
188
-
189
- const onFocusedEvent = (event, focused) => {
190
- setInputFocused(focused);
191
- event.preventDefault();
192
- event.stopPropagation();
193
- };
202
+ }, [focusAndSelectSearchFieldOnLoad]);
194
203
 
195
204
  const SelectSection = () => {
196
205
  if (searchOptions === null) return null;
197
206
  else return <Select className={classes.selectInput} options={searchOptions} selectProps={selectProps} />;
198
207
  };
199
208
 
209
+ const onSubmit = event => {
210
+ // using form submit instead of a keydown (with key=enter) to allow the 'enter key' event to be canceled elsewhere to avoid the submit event
211
+ onSearch(searchOption, inputRef.current?.value);
212
+ event.preventDefault();
213
+ };
214
+
200
215
  const inputSection = (
201
- <div data-qa="searchInput" data-qa-is-focused={inputFocused} className={classes.parentInput}>
202
- <form data-qa="searchForm" className={classes.fullWidth}>
216
+ <div data-qa="searchInput" className={classes.parentInput}>
217
+ <form data-qa="searchForm" className={classes.fullWidth} onSubmitCapture={onSubmit}>
203
218
  <Input
204
219
  placeholder={placeholder}
205
220
  defaultValue={defaultValue}
@@ -207,10 +222,7 @@ const SearchControl = ({
207
222
  type="text"
208
223
  disabled={disabled}
209
224
  classes={{ input: classes.controlInput }}
210
- onKeyDown={handleKeyDown}
211
225
  disableUnderline={true}
212
- onFocus={e => onFocusedEvent(e, true)}
213
- onBlur={e => onFocusedEvent(e, false)}
214
226
  endAdornment={
215
227
  <InputAdornment position="start">
216
228
  <IconButton
@@ -53,10 +53,11 @@ describe("useStyles", () => {
53
53
  });
54
54
 
55
55
  describe("SearchControl Component", () => {
56
- const stateSetter = sinon.spy().named("focus");
57
- const useStateMock = initState => [initState, stateSetter];
58
-
56
+ beforeEach(() => {
57
+ jest.useFakeTimers();
58
+ });
59
59
  afterEach(() => {
60
+ jest.useRealTimers();
60
61
  jest.clearAllMocks();
61
62
  });
62
63
 
@@ -232,8 +233,7 @@ describe("SearchControl Component", () => {
232
233
  expect(searchInput.length, "to be", 1);
233
234
 
234
235
  searchInput.instance().value = "abc";
235
- searchInput.simulate("keydown", { key: "Tab" });
236
- searchInput.simulate("keydown", { key: "Enter" });
236
+ mountedComponent.find("form").simulate("submit", { preventDefault: () => {} });
237
237
 
238
238
  expect(onSearchEvent, "to have calls satisfying", [{ args: ["aValue", "abc"] }]);
239
239
  });
@@ -371,7 +371,7 @@ describe("SearchControl Component", () => {
371
371
  expect(onSearchEvent, "to have calls satisfying", [{ args: ["anotherValue", "abcdef123"] }]);
372
372
  });
373
373
 
374
- it("Search Control should render with the 2nd value", () => {
374
+ it("Search Control should clear the search when changing the option", () => {
375
375
  const options = [
376
376
  { value: "aValue", label: "aLabel" },
377
377
  { value: "anotherValue", label: "anotherLabel" },
@@ -385,8 +385,8 @@ describe("SearchControl Component", () => {
385
385
  placeholder="placeHolderTest"
386
386
  defaultValue={"abcdef123"}
387
387
  searchOptions={options}
388
- searchOption={"anotherValue"}
389
388
  onSearch={onSearchEvent}
389
+ focusSearchOnSearchOptionChange={true}
390
390
  />
391
391
  </TestWrapper>
392
392
  );
@@ -395,48 +395,53 @@ describe("SearchControl Component", () => {
395
395
 
396
396
  const selectMui = mountedComponent.find(SelectMUI);
397
397
 
398
- expect(selectMui.props().value, "to equal", "anotherValue");
399
- });
398
+ const event = {
399
+ target: {
400
+ value: "anotherValue",
401
+ },
402
+ };
400
403
 
401
- it("focusing text input should set focus on container", () => {
402
- jest.spyOn(React, "useState").mockImplementation(useStateMock);
404
+ onSearchEvent.resetHistory();
405
+
406
+ selectMui.invoke("onChange")(event);
407
+
408
+ expect(onSearchEvent.callCount, "to equal", 1);
409
+ expect(onSearchEvent, "to have calls satisfying", [{ args: ["anotherValue", ""] }]);
410
+
411
+ const allInputs = mountedComponent.find("input");
412
+ const searchInput = allInputs.find("[placeholder='placeHolderTest']");
413
+ expect(searchInput.length, "to be", 1);
403
414
 
415
+ expect(searchInput.instance().value, "to be", "abcdef123");
416
+ jest.runOnlyPendingTimers();
417
+ expect(searchInput.instance().value, "to be", "");
418
+ });
419
+
420
+ it("Search Control should render with the 2nd value", () => {
404
421
  const options = [
405
422
  { value: "aValue", label: "aLabel" },
406
423
  { value: "anotherValue", label: "anotherLabel" },
407
424
  ];
408
425
 
426
+ const onSearchEvent = sinon.spy().named("search");
427
+
409
428
  const component = (
410
429
  <TestWrapper stylesProvider muiThemeProvider={{ theme }}>
411
- <SearchControl placeholder="placeHolderTest" searchOptions={options} />
430
+ <SearchControl
431
+ placeholder="placeHolderTest"
432
+ defaultValue={"abcdef123"}
433
+ searchOptions={options}
434
+ searchOption={"anotherValue"}
435
+ onSearch={onSearchEvent}
436
+ />
412
437
  </TestWrapper>
413
438
  );
414
439
 
415
440
  const mountedComponent = mount(component);
416
441
 
417
- const allInputs = mountedComponent.find("input");
418
- const searchInput = allInputs.find("[placeholder='placeHolderTest']");
419
- expect(searchInput.length, "to be", 1);
420
-
421
- let searchEditParent = mountedComponent.find('[data-qa="searchInput"]');
422
- expect(searchEditParent.length, "to be", 1);
423
-
424
- expect(searchEditParent.props()["data-qa-is-focused"], "to be", false);
425
-
426
- const event = {
427
- preventDefault: () => {},
428
- stopPropagation: () => {},
429
- };
430
-
431
- searchInput.invoke("onFocus")(event);
432
-
433
- searchEditParent = mountedComponent.find('[data-qa="searchInput"]');
434
- expect(searchEditParent.props()["data-qa-is-focused"], "to be", true);
435
-
436
- searchInput.invoke("onBlur")(event);
442
+ const selectMui = mountedComponent.find(SelectMUI);
437
443
 
438
- searchEditParent = mountedComponent.find('[data-qa="searchInput"]');
439
- expect(searchEditParent.props()["data-qa-is-focused"], "to be", false);
444
+ expect(selectMui.props().value, "to equal", "anotherValue");
440
445
  });
441
446
 
442
447
  it("Renders Search Control component without errors when disabled", () => {
@@ -8,7 +8,7 @@ import { isString, isObject, isStringNullOrWhitespace, isReactComponent } from "
8
8
 
9
9
  const withDeferredTooltip =
10
10
  Comp =>
11
- ({ titleValue, alwaysDisplay, ...props }) => {
11
+ ({ titleValue, alwaysDisplay, tooltipClasses, ...props }) => {
12
12
  const [shouldBeTooltipped, setShouldBeTooltipped] = useState(false);
13
13
 
14
14
  const defaultComponent = <Comp onMouseEnter={event => makeComponentTooltipped(event)} {...props} />;
@@ -32,6 +32,7 @@ const withDeferredTooltip =
32
32
  return (
33
33
  <MuiTooltip
34
34
  arrow
35
+ classes={tooltipClasses ?? null}
35
36
  title={titleValue}
36
37
  disableHoverListener={false}
37
38
  disableFocusListener={true}