orc-shared 1.6.0-dev.1 → 1.6.0-dev.10

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 (57) hide show
  1. package/dist/actions/globalErrorMessages.js +67 -0
  2. package/dist/actions/makeApiAction.js +1 -1
  3. package/dist/buildStore.js +3 -1
  4. package/dist/components/MaterialUI/DataDisplay/PredefinedElements/GlobalErrorMessages.js +134 -0
  5. package/dist/components/MaterialUI/DataDisplay/PredefinedElements/LookupDisplayValue.js +84 -0
  6. package/dist/components/MaterialUI/DataDisplay/PredefinedElements/StepperModal.js +2 -1
  7. package/dist/components/MaterialUI/DataDisplay/Table.js +2 -2
  8. package/dist/components/MaterialUI/Inputs/InputBase.js +97 -15
  9. package/dist/components/MaterialUI/Inputs/InputBaseProps.js +3 -1
  10. package/dist/components/MaterialUI/hocs/withDeferredPopper.js +1 -1
  11. package/dist/components/Routing/Page.js +4 -1
  12. package/dist/components/Routing/SegmentPage.js +4 -1
  13. package/dist/components/Routing/SubPage.js +11 -13
  14. package/dist/components/Scope/useScopeConfirmationModalState.js +1 -1
  15. package/dist/hooks/useDispatchWithErrorHandling.js +106 -0
  16. package/dist/hooks/useNotificationRequestState.js +2 -2
  17. package/dist/reducers/globalErrorMessages.js +79 -0
  18. package/dist/reducers/request.js +2 -1
  19. package/dist/reducers/scopes.js +3 -0
  20. package/dist/selectors/authentication.js +2 -2
  21. package/dist/selectors/globalErrorMessages.js +58 -0
  22. package/dist/selectors/metadata.js +2 -2
  23. package/dist/utils/buildUrl.js +1 -1
  24. package/dist/utils/responseProcessingHelper.js +87 -0
  25. package/package.json +7 -4
  26. package/src/actions/globalErrorMessages.js +12 -0
  27. package/src/actions/globalErrorMessages.test.js +21 -0
  28. package/src/buildStore.js +2 -0
  29. package/src/components/MaterialUI/DataDisplay/PredefinedElements/GlobalErrorMessages.js +93 -0
  30. package/src/components/MaterialUI/DataDisplay/PredefinedElements/GlobalErrorMessages.test.js +472 -0
  31. package/src/components/MaterialUI/DataDisplay/PredefinedElements/LookupDisplayValue.js +18 -0
  32. package/src/components/MaterialUI/DataDisplay/PredefinedElements/LookupDisplayValue.test.js +121 -0
  33. package/src/components/MaterialUI/DataDisplay/PredefinedElements/StepperModal.js +2 -1
  34. package/src/components/MaterialUI/DataDisplay/PredefinedElements/StepperModal.test.js +20 -0
  35. package/src/components/MaterialUI/Inputs/InputBase.js +97 -15
  36. package/src/components/MaterialUI/Inputs/InputBase.test.js +339 -3
  37. package/src/components/MaterialUI/Inputs/InputBaseProps.js +2 -0
  38. package/src/components/MaterialUI/Inputs/InputBaseProps.test.js +2 -0
  39. package/src/components/Routing/Page.js +12 -1
  40. package/src/components/Routing/SegmentPage.js +12 -1
  41. package/src/components/Routing/SubPage.js +4 -8
  42. package/src/components/Routing/SubPage.test.js +46 -0
  43. package/src/hooks/useDispatchWithErrorHandling.js +57 -0
  44. package/src/hooks/useDispatchWithErrorHandling.test.js +230 -0
  45. package/src/reducers/globalErrorMessages.js +25 -0
  46. package/src/reducers/globalErrorMessages.test.js +66 -0
  47. package/src/reducers/request.js +2 -1
  48. package/src/reducers/request.test.js +23 -0
  49. package/src/reducers/scopes.js +3 -0
  50. package/src/reducers/scopes.test.js +47 -0
  51. package/src/selectors/globalErrorMessages.js +11 -0
  52. package/src/selectors/globalErrorMessages.test.js +25 -0
  53. package/src/selectors/metadata.js +1 -1
  54. package/src/selectors/metadata.test.js +12 -0
  55. package/src/utils/buildUrl.js +1 -1
  56. package/src/utils/responseProcessingHelper.js +42 -0
  57. package/src/utils/responseProcessingHelper.test.js +218 -0
@@ -1,11 +1,12 @@
1
1
  import React from "react";
2
2
  import { mount } from "enzyme";
3
- import InputBase from "./InputBase";
3
+ import InputBase, { AdvancedNumericInput } from "./InputBase";
4
4
  import InputBaseMUI from "@material-ui/core/InputBase";
5
5
  import sinon from "sinon";
6
6
  import { ignoreConsoleError } from "../../../utils/testUtils";
7
7
  import InputBaseProps from "./InputBaseProps";
8
8
  import { act } from "unexpected-reaction";
9
+ import { fireEvent, render, getByDisplayValue } from "@testing-library/react";
9
10
 
10
11
  describe("InputBase Component", () => {
11
12
  let update, container;
@@ -71,7 +72,7 @@ describe("InputBase Component", () => {
71
72
  const component = <InputBase inputProps={inputProps} />;
72
73
 
73
74
  const mountedComponent = mount(component);
74
- const expected = <InputBaseMUI value={aValue} title={aValue} rows={defaultRows} />;
75
+ const expected = <InputBaseMUI value={aValue} title={aValue} minRows={defaultRows} />;
75
76
 
76
77
  expect(mountedComponent.containsMatchingElement(expected), "to be truthy");
77
78
  });
@@ -91,7 +92,7 @@ describe("InputBase Component", () => {
91
92
  const component = <InputBase inputProps={inputProps} />;
92
93
 
93
94
  const mountedComponent = mount(component);
94
- const expected = <InputBaseMUI value={aValue} title={aValue} rows={desiredNumberOfRows} />;
95
+ const expected = <InputBaseMUI value={aValue} title={aValue} minRows={desiredNumberOfRows} />;
95
96
 
96
97
  expect(mountedComponent.containsMatchingElement(expected), "to be truthy");
97
98
  });
@@ -285,6 +286,35 @@ describe("InputBase Component", () => {
285
286
 
286
287
  expect(mountedComponent.containsMatchingElement(expected), "to be truthy");
287
288
  });
289
+
290
+ it("Change value on blur with pending timer", () => {
291
+ window.bypassDebounce = false;
292
+ const inputProps = new InputBaseProps();
293
+ const aLabel = "aLabel";
294
+ const aValue = "12.2";
295
+ const metadata = {
296
+ test: "value",
297
+ };
298
+ const onBlur = sinon.spy().named("blur");
299
+
300
+ inputProps.set(InputBaseProps.propNames.update, update);
301
+ inputProps.set(InputBaseProps.propNames.value, aValue);
302
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
303
+ inputProps.set(InputBaseProps.propNames.metadata, metadata);
304
+ inputProps.set(InputBaseProps.propNames.onBlur, onBlur);
305
+ inputProps.set(InputBaseProps.propNames.timeoutDelay, 1000);
306
+
307
+ const component = <InputBase inputProps={inputProps} />;
308
+ const mountedComponent = mount(component);
309
+ const input = mountedComponent.find("input");
310
+
311
+ input.simulate("change", { target: { value: "13", focus: () => {} } });
312
+ input.simulate("blur", {});
313
+
314
+ expect(onBlur, "was called once");
315
+ expect(update, "not to have calls satisfying", [{ args: [aValue, metadata] }]);
316
+ expect(update, "to have calls satisfying", [{ args: ["13", metadata] }]);
317
+ });
288
318
  });
289
319
 
290
320
  describe("InputBase component debouce", () => {
@@ -350,3 +380,309 @@ describe("InputBase component debouce", () => {
350
380
  expect(input.get(0).props.value, "to equal", "");
351
381
  });
352
382
  });
383
+
384
+ describe("AdvancedNumericInput", () => {
385
+ const noop = function () {};
386
+ let update, container;
387
+ beforeEach(() => {
388
+ window.bypassDebounce = true;
389
+ jest.useFakeTimers();
390
+
391
+ container = document.createElement("div");
392
+ document.body.appendChild(container);
393
+ update = sinon.spy().named("update");
394
+ });
395
+ afterEach(() => {
396
+ jest.useRealTimers();
397
+ delete window.bypassDebounce;
398
+ document.body.removeChild(container);
399
+ container = null;
400
+ });
401
+
402
+ it("Renders InputBase component as advanced numeric input", () => {
403
+ const inputProps = new InputBaseProps();
404
+ const aLabel = "aLabel";
405
+ const aValue = "value";
406
+
407
+ inputProps.set(InputBaseProps.propNames.update, update);
408
+ inputProps.set(InputBaseProps.propNames.value, aValue);
409
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
410
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
411
+
412
+ const component = <InputBase inputProps={inputProps} />;
413
+
414
+ const mountedComponent = mount(component);
415
+ const expected = <InputBaseMUI value={aValue} title="" inputComponent={AdvancedNumericInput} />;
416
+
417
+ expect(mountedComponent.containsMatchingElement(expected), "to be truthy");
418
+ });
419
+
420
+ it("Renders InputBase component as advanced numeric input with custom numeric props", () => {
421
+ const inputProps = new InputBaseProps();
422
+ const aLabel = "aLabel";
423
+ const aValue = "value";
424
+
425
+ inputProps.set(InputBaseProps.propNames.update, update);
426
+ inputProps.set(InputBaseProps.propNames.value, aValue);
427
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
428
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
429
+ inputProps.set(InputBaseProps.propNames.numericInputProps, { decimalScale: 2 });
430
+
431
+ const component = <InputBase inputProps={inputProps} />;
432
+
433
+ const mountedComponent = mount(component);
434
+
435
+ const advInput = mountedComponent.find("AdvancedNumericInput");
436
+ expect(advInput.props().decimalScale, "to be", 2);
437
+ });
438
+
439
+ it("Change advanced numeric input value", () => {
440
+ const inputProps = new InputBaseProps();
441
+ const aLabel = "aLabel";
442
+ const aValue = "12.2";
443
+ const metadata = {
444
+ test: "value",
445
+ };
446
+
447
+ inputProps.set(InputBaseProps.propNames.update, update);
448
+ inputProps.set(InputBaseProps.propNames.value, aValue);
449
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
450
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
451
+ inputProps.set(InputBaseProps.propNames.numericInputProps, { decimalScale: 2 });
452
+ inputProps.set(InputBaseProps.propNames.metadata, metadata);
453
+
454
+ const component = <InputBase inputProps={inputProps} />;
455
+ const mountedComponent = mount(component);
456
+ const input = mountedComponent.find("input");
457
+ input.simulate("change", { target: { value: "13", focus: noop } });
458
+
459
+ expect(update, "to have calls satisfying", [{ args: ["13", metadata] }]);
460
+ });
461
+
462
+ it("Change advanced numeric input on blur without custom blur method", () => {
463
+ const inputProps = new InputBaseProps();
464
+ const aLabel = "aLabel";
465
+ const aValue = "12.2";
466
+ const metadata = {
467
+ test: "value",
468
+ };
469
+
470
+ inputProps.set(InputBaseProps.propNames.update, update);
471
+ inputProps.set(InputBaseProps.propNames.value, aValue);
472
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
473
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
474
+ inputProps.set(InputBaseProps.propNames.numericInputProps, { decimalScale: 2 });
475
+ inputProps.set(InputBaseProps.propNames.metadata, metadata);
476
+
477
+ const component = <InputBase inputProps={inputProps} />;
478
+ const mountedComponent = mount(component);
479
+ const input = mountedComponent.find("input");
480
+
481
+ input.simulate("blur", {});
482
+
483
+ expect(update, "to have calls satisfying", [{ args: ["12.20", metadata] }]);
484
+ });
485
+
486
+ it("Onblur send a formatted string instead of a number", () => {
487
+ const inputProps = new InputBaseProps();
488
+ const aLabel = "aLabel";
489
+ const aValue = 12.2;
490
+ const metadata = {
491
+ test: "value",
492
+ };
493
+
494
+ inputProps.set(InputBaseProps.propNames.update, update);
495
+ inputProps.set(InputBaseProps.propNames.value, aValue);
496
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
497
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
498
+ inputProps.set(InputBaseProps.propNames.numericInputProps, { decimalScale: 2 });
499
+ inputProps.set(InputBaseProps.propNames.metadata, metadata);
500
+
501
+ const component = <InputBase inputProps={inputProps} />;
502
+ const mountedComponent = mount(component);
503
+ const input = mountedComponent.find("input");
504
+
505
+ input.simulate("blur", {});
506
+
507
+ expect(update, "to have calls satisfying", [{ args: ["12.20", metadata] }]);
508
+ });
509
+
510
+ it("Onblur send a formatted string with default numeric input props", () => {
511
+ const inputProps = new InputBaseProps();
512
+ const aLabel = "aLabel";
513
+ const aValue = 12.2;
514
+ const metadata = {
515
+ test: "value",
516
+ };
517
+
518
+ inputProps.set(InputBaseProps.propNames.update, update);
519
+ inputProps.set(InputBaseProps.propNames.value, aValue);
520
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
521
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
522
+ inputProps.set(InputBaseProps.propNames.metadata, metadata);
523
+
524
+ const component = <InputBase inputProps={inputProps} />;
525
+ const mountedComponent = mount(component);
526
+ const input = mountedComponent.find("input");
527
+
528
+ input.simulate("blur", {});
529
+
530
+ expect(update, "to have calls satisfying", [{ args: ["12.2", metadata] }]);
531
+ });
532
+
533
+ it("Onblur send an unformatted string when blurFormattingSkipFixedDecimalScale is true", () => {
534
+ const inputProps = new InputBaseProps();
535
+ const aLabel = "aLabel";
536
+ const aValue = 12.2;
537
+ const metadata = {
538
+ test: "value",
539
+ };
540
+
541
+ inputProps.set(InputBaseProps.propNames.update, update);
542
+ inputProps.set(InputBaseProps.propNames.value, aValue);
543
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
544
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
545
+ inputProps.set(InputBaseProps.propNames.numericInputProps, {
546
+ decimalScale: 2,
547
+ blurFormattingSkipFixedDecimalScale: true,
548
+ });
549
+ inputProps.set(InputBaseProps.propNames.metadata, metadata);
550
+
551
+ const component = <InputBase inputProps={inputProps} />;
552
+ const mountedComponent = mount(component);
553
+ const input = mountedComponent.find("input");
554
+
555
+ input.simulate("blur", {});
556
+
557
+ expect(update, "to have calls satisfying", [{ args: ["12.2", metadata] }]);
558
+ });
559
+
560
+ it("Onblur send an empty string when the initial value is empty", () => {
561
+ const inputProps = new InputBaseProps();
562
+ const aLabel = "aLabel";
563
+ const aValue = null;
564
+ const metadata = {
565
+ test: "value",
566
+ };
567
+
568
+ inputProps.set(InputBaseProps.propNames.update, update);
569
+ inputProps.set(InputBaseProps.propNames.value, aValue);
570
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
571
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
572
+ inputProps.set(InputBaseProps.propNames.numericInputProps, { decimalScale: 2 });
573
+ inputProps.set(InputBaseProps.propNames.metadata, metadata);
574
+
575
+ const component = <InputBase inputProps={inputProps} />;
576
+ const mountedComponent = mount(component);
577
+ const input = mountedComponent.find("input");
578
+
579
+ input.simulate("blur", {});
580
+
581
+ expect(update, "to have calls satisfying", [{ args: ["", metadata] }]);
582
+ });
583
+
584
+ it("Change advanced numeric input on blur with no pending timer", () => {
585
+ const inputProps = new InputBaseProps();
586
+ const aLabel = "aLabel";
587
+ const aValue = "12.2";
588
+ const metadata = {
589
+ test: "value",
590
+ };
591
+ const onBlur = sinon.spy().named("blur");
592
+
593
+ inputProps.set(InputBaseProps.propNames.update, update);
594
+ inputProps.set(InputBaseProps.propNames.value, aValue);
595
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
596
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
597
+ inputProps.set(InputBaseProps.propNames.numericInputProps, { decimalScale: 2 });
598
+ inputProps.set(InputBaseProps.propNames.metadata, metadata);
599
+ inputProps.set(InputBaseProps.propNames.onBlur, onBlur);
600
+
601
+ const component = <InputBase inputProps={inputProps} />;
602
+ const mountedComponent = mount(component);
603
+ const input = mountedComponent.find("input");
604
+
605
+ input.simulate("blur", {});
606
+
607
+ expect(onBlur, "was called once");
608
+ expect(update, "to have calls satisfying", [{ args: ["12.20", metadata] }]);
609
+ });
610
+
611
+ it("Change advanced numeric input on blur with pending timer", () => {
612
+ window.bypassDebounce = false;
613
+ const inputProps = new InputBaseProps();
614
+ const aLabel = "aLabel";
615
+ const aValue = "12.2";
616
+ const metadata = {
617
+ test: "value",
618
+ };
619
+ const onBlur = sinon.spy().named("blur");
620
+
621
+ inputProps.set(InputBaseProps.propNames.update, update);
622
+ inputProps.set(InputBaseProps.propNames.value, aValue);
623
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
624
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
625
+ inputProps.set(InputBaseProps.propNames.numericInputProps, { decimalScale: 2 });
626
+ inputProps.set(InputBaseProps.propNames.metadata, metadata);
627
+ inputProps.set(InputBaseProps.propNames.onBlur, onBlur);
628
+ inputProps.set(InputBaseProps.propNames.timeoutDelay, 1000);
629
+
630
+ const component = <InputBase inputProps={inputProps} />;
631
+ const mountedComponent = mount(component);
632
+ const input = mountedComponent.find("input");
633
+
634
+ input.simulate("change", { target: { value: "13", focus: noop } });
635
+ input.simulate("blur", {});
636
+
637
+ expect(onBlur, "was called once");
638
+ expect(update, "not to have calls satisfying", [{ args: [aValue, metadata] }]);
639
+ expect(update, "to have calls satisfying", [{ args: ["13.00", metadata] }]);
640
+ });
641
+
642
+ it("Local state is reset when the value props changes for the same value as inputText", () => {
643
+ window.bypassDebounce = false;
644
+ const inputProps = new InputBaseProps();
645
+ const aLabel = "aLabel";
646
+ const aValue = "12.2";
647
+ const metadata = {
648
+ test: "value",
649
+ };
650
+ const onBlur = sinon.spy().named("blur");
651
+
652
+ inputProps.set(InputBaseProps.propNames.update, update);
653
+ inputProps.set(InputBaseProps.propNames.value, aValue);
654
+ inputProps.set(InputBaseProps.propNames.label, aLabel);
655
+ inputProps.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
656
+ inputProps.set(InputBaseProps.propNames.numericInputProps, { decimalScale: 2 });
657
+ inputProps.set(InputBaseProps.propNames.metadata, metadata);
658
+ inputProps.set(InputBaseProps.propNames.onBlur, onBlur);
659
+ inputProps.set(InputBaseProps.propNames.timeoutDelay, 1000);
660
+
661
+ const component = <InputBase inputProps={inputProps} />;
662
+ const { container, rerender } = render(component);
663
+
664
+ const input = getByDisplayValue(container, "12.2");
665
+
666
+ fireEvent.change(input, {
667
+ target: {
668
+ value: "13",
669
+ },
670
+ });
671
+
672
+ const inputProps2 = new InputBaseProps();
673
+ inputProps2.set(InputBaseProps.propNames.update, update);
674
+ inputProps2.set(InputBaseProps.propNames.value, "13");
675
+ inputProps2.set(InputBaseProps.propNames.label, aLabel);
676
+ inputProps2.set(InputBaseProps.propNames.type, "AdvancedNumericInput");
677
+ inputProps2.set(InputBaseProps.propNames.numericInputProps, { decimalScale: 2 });
678
+ inputProps2.set(InputBaseProps.propNames.metadata, metadata);
679
+ inputProps2.set(InputBaseProps.propNames.onBlur, onBlur);
680
+ inputProps2.set(InputBaseProps.propNames.timeoutDelay, 1000);
681
+
682
+ act(() => {
683
+ rerender(<InputBase inputProps={inputProps2} />);
684
+ });
685
+
686
+ // no idea what to assert here, this test is mostly for code coverage
687
+ });
688
+ });
@@ -19,6 +19,7 @@ class InputBaseProps extends ComponentProps {
19
19
  autoComplete: "autoComplete",
20
20
  timeoutDelay: "timeoutDelay",
21
21
  rows: "rows",
22
+ numericInputProps: "numericInputProps",
22
23
  };
23
24
 
24
25
  static ruleNames = {
@@ -45,6 +46,7 @@ class InputBaseProps extends ComponentProps {
45
46
  this.componentProps.set(this.constructor.propNames.autoComplete, null);
46
47
  this.componentProps.set(this.constructor.propNames.timeoutDelay, null);
47
48
  this.componentProps.set(this.constructor.propNames.rows, null);
49
+ this.componentProps.set(this.constructor.propNames.numericInputProps, null);
48
50
 
49
51
  this.componentClasses.set(this.constructor.ruleNames.input, null);
50
52
  this.componentClasses.set(this.constructor.ruleNames.errorText, null);
@@ -20,6 +20,7 @@ describe("InputBase Props", () => {
20
20
  "autoComplete",
21
21
  "timeoutDelay",
22
22
  "rows",
23
+ "numericInputProps",
23
24
  ];
24
25
 
25
26
  expect(InputBaseProps.propNames, "to have keys", propNames);
@@ -44,6 +45,7 @@ describe("InputBase Props", () => {
44
45
  "autoComplete",
45
46
  "timeoutDelay",
46
47
  "rows",
48
+ "numericInputProps",
47
49
  ];
48
50
 
49
51
  const ruleNames = ["input", "errorText"];
@@ -4,9 +4,12 @@ import withErrorBoundary from "../../hocs/withErrorBoundary";
4
4
  import FullPage from "./FullPage";
5
5
  import SubPage from "./SubPage";
6
6
  import withWaypointing from "./withWaypointing";
7
+ import UrlPattern from "url-pattern";
7
8
 
8
9
  const Page = ({ component: View, path, pages = {}, subpages = {}, modulePrependPath, isVisible = true }) => {
9
10
  const WrappedView = useMemo(() => withErrorBoundary(path)(withWaypointing(View, isVisible)), [path, View, isVisible]);
11
+ const parentUrlPattern = new UrlPattern(path);
12
+
10
13
  return (
11
14
  <React.Fragment>
12
15
  <Switch>
@@ -38,7 +41,15 @@ const Page = ({ component: View, path, pages = {}, subpages = {}, modulePrependP
38
41
  <Route
39
42
  key={subpath}
40
43
  path={path + subpath}
41
- render={route => <SubPage root={path} config={config} {...route} modulePrependPath={modulePrependPath} />}
44
+ render={route => (
45
+ <SubPage
46
+ root={path}
47
+ config={config}
48
+ parentUrlPattern={parentUrlPattern}
49
+ {...route}
50
+ modulePrependPath={modulePrependPath}
51
+ />
52
+ )}
42
53
  />
43
54
  ))}
44
55
  </Switch>
@@ -197,6 +197,9 @@ const SegmentPage = ({
197
197
  );
198
198
  }
199
199
  if (config.subpages) {
200
+ const parentUrl = path + segpath;
201
+ const parentUrlPattern = new UrlPattern(parentUrl);
202
+
200
203
  subpages.push(
201
204
  ...Object.entries(config.subpages).map(([subpath, config]) => {
202
205
  const pagePath = segpath + subpath;
@@ -204,7 +207,15 @@ const SegmentPage = ({
204
207
  <Route
205
208
  key={pagePath}
206
209
  path={path + pagePath}
207
- render={route => <SubPage root={path} config={config} {...route} modulePrependPath={modulePrependPath} />}
210
+ render={route => (
211
+ <SubPage
212
+ root={path}
213
+ config={config}
214
+ parentUrlPattern={parentUrlPattern}
215
+ {...route}
216
+ modulePrependPath={modulePrependPath}
217
+ />
218
+ )}
208
219
  />
209
220
  );
210
221
  }),
@@ -20,22 +20,19 @@ const useStyles = makeStyles(theme => ({
20
20
  },
21
21
  }));
22
22
 
23
- export const SubPage = ({ config, match, location, history, root, modulePrependPath }) => {
23
+ export const SubPage = ({ config, match, location, history, root, modulePrependPath, parentUrlPattern }) => {
24
24
  const classes = useStyles();
25
25
  const dispatch = useDispatch();
26
26
  let { component: View, ...props } = config;
27
27
  const pattern = new UrlPattern(root);
28
28
  const baseHref = pattern.stringify(match.params);
29
-
30
29
  const path = location.pathname;
31
30
 
32
- const basePathArr = path.split("/");
33
- basePathArr.pop();
34
- const basePath = basePathArr.join("/");
35
31
  const WrappedView = useMemo(() => withErrorBoundary(path)(withWaypointing(View)), [path, View]);
36
32
  const closeSubPage = () => {
37
- history.push(basePath);
38
- dispatch(mapHref(basePath, basePath));
33
+ const parentHref = parentUrlPattern.stringify(match.params);
34
+ history.push(parentHref);
35
+ dispatch(mapHref(parentHref, parentHref));
39
36
  };
40
37
 
41
38
  const message = (
@@ -59,7 +56,6 @@ export const SubPage = ({ config, match, location, history, root, modulePrependP
59
56
  modalProps.set(ModalProps.propNames.title, titleComponent);
60
57
  modalProps.set(ModalProps.propNames.open, true);
61
58
  modalProps.set(ModalProps.propNames.type, "fullwidth");
62
- modalProps.set(ModalProps.propNames.backdropClickCallback, closeSubPage);
63
59
 
64
60
  let actionPanel = (
65
61
  <div className={classes.actionPanel}>
@@ -13,6 +13,7 @@ import Button from "@material-ui/core/Button";
13
13
  import translations from "~/translations/en-US.json";
14
14
  import { TestWrapper, createMuiTheme } from "../../utils/testUtils";
15
15
  import sharedMessages from "../../sharedMessages";
16
+ import UrlPattern from "url-pattern";
16
17
 
17
18
  const InnerView = ({ theme, pathname, search, mapFrom, match, location, routeIsAligned, set }) => (
18
19
  <PropStruct
@@ -92,6 +93,7 @@ describe("SubPage", () => {
92
93
  config={{ component: InnerView, set: true, title: "Item Details" }}
93
94
  root="/foo"
94
95
  path="/foo/bar"
96
+ parentUrlPattern={new UrlPattern("/foo")}
95
97
  {...route}
96
98
  />
97
99
  )}
@@ -140,6 +142,7 @@ describe("SubPage", () => {
140
142
  config={{ component: InnerView, set: true, title: sharedMessages.confirmation }}
141
143
  root="/foo"
142
144
  path="/foo/bar"
145
+ parentUrlPattern={new UrlPattern("/foo")}
143
146
  {...route}
144
147
  />
145
148
  )}
@@ -164,6 +167,7 @@ describe("SubPage", () => {
164
167
  config={{ component: InnerView, set: true, title: "Item Details" }}
165
168
  root="/foo"
166
169
  path="/foo/bar"
170
+ parentUrlPattern={new UrlPattern("/foo")}
167
171
  {...route}
168
172
  />
169
173
  )}
@@ -180,6 +184,42 @@ describe("SubPage", () => {
180
184
  expect(history.push, "to have calls satisfying", [{ args: ["/foo"] }]);
181
185
  expect(dispatch, "to have calls satisfying", [{ args: [mapHref("/foo", "/foo")] }]);
182
186
  });
187
+ it("closing the dialog navigate to the parentUrlPattern with parameters", () => {
188
+ history = createMemoryHistory({ initialEntries: ["/foo/bar/123/456"] });
189
+ sinon.spy(history, "push");
190
+ history.push.named("history.push");
191
+ dispatch.resetHistory();
192
+
193
+ const component = (
194
+ <TestWrapper provider={{ store }} intlProvider={intlProvider} stylesProvider muiThemeProvider={{ theme }}>
195
+ <div>
196
+ <div id="outer" />
197
+ <Router history={history}>
198
+ <Route
199
+ path="/foo/bar/:parentId/:id"
200
+ render={route => (
201
+ <SubPage
202
+ config={{ component: InnerView, set: true, title: "Item Details" }}
203
+ root="/foo/bar/:parentId/:id"
204
+ path="/foo/bar/123/456"
205
+ parentUrlPattern={new UrlPattern("/foo/bar/:parentId")}
206
+ {...route}
207
+ />
208
+ )}
209
+ />
210
+ </Router>
211
+ </div>
212
+ </TestWrapper>
213
+ );
214
+ const mountedComponent = mount(component);
215
+
216
+ const closeButton = mountedComponent.find("button").at(0);
217
+
218
+ closeButton.invoke("onClick")();
219
+ expect(history.push, "to have calls satisfying", [{ args: ["/foo/bar/123"] }]);
220
+ expect(dispatch, "to have a call satisfying", { args: [mapHref("/foo/bar/123", "/foo/bar/123")] });
221
+ });
222
+
183
223
  it("renders action panel passed from props", () => {
184
224
  const actions = () => [{ label: sharedMessages.cancel }, { label: sharedMessages.applyChanges }];
185
225
 
@@ -200,6 +240,7 @@ describe("SubPage", () => {
200
240
  }}
201
241
  root="/foo"
202
242
  path="/foo/bar"
243
+ parentUrlPattern={new UrlPattern("/foo")}
203
244
  {...route}
204
245
  />
205
246
  )}
@@ -242,6 +283,7 @@ describe("SubPage", () => {
242
283
  }}
243
284
  root="/foo"
244
285
  path="/foo/bar"
286
+ parentUrlPattern={new UrlPattern("/foo")}
245
287
  {...route}
246
288
  />
247
289
  )}
@@ -286,6 +328,7 @@ describe("SubPage", () => {
286
328
  }}
287
329
  root="/foo"
288
330
  path="/foo/bar"
331
+ parentUrlPattern={new UrlPattern("/foo")}
289
332
  {...route}
290
333
  />
291
334
  )}
@@ -327,6 +370,7 @@ describe("SubPage", () => {
327
370
  }}
328
371
  root="/foo"
329
372
  path="/foo/bar"
373
+ parentUrlPattern={new UrlPattern("/foo")}
330
374
  {...route}
331
375
  />
332
376
  )}
@@ -368,6 +412,7 @@ describe("SubPage", () => {
368
412
  }}
369
413
  root="/foo"
370
414
  path="/foo/bar"
415
+ parentUrlPattern={new UrlPattern("/foo")}
371
416
  {...route}
372
417
  />
373
418
  )}
@@ -411,6 +456,7 @@ describe("SubPage", () => {
411
456
  }}
412
457
  root="/foo"
413
458
  path="/foo/bar"
459
+ parentUrlPattern={new UrlPattern("/foo")}
414
460
  {...route}
415
461
  />
416
462
  )}
@@ -0,0 +1,57 @@
1
+ import { useDispatch } from "react-redux";
2
+ import { useCallback } from "react";
3
+ import { extractStandardErrorMessagesFromResponse } from "../utils/responseProcessingHelper";
4
+ import { pushGlobalErrorMessage } from "../actions/globalErrorMessages";
5
+
6
+ export const executeDispatchWithErrorHandling = ({
7
+ dispatch,
8
+ action,
9
+ errorTitle,
10
+ errorDescription,
11
+ validationLookupModule,
12
+ validationLookupName,
13
+ }) => {
14
+ return dispatch(action).then(data => {
15
+ const extractedMessages = extractStandardErrorMessagesFromResponse(
16
+ data,
17
+ validationLookupModule,
18
+ validationLookupName,
19
+ );
20
+ if (extractedMessages.hasErrors) {
21
+ const newMsg = {
22
+ messages: extractedMessages.messages,
23
+ };
24
+
25
+ if (extractedMessages.messages.length > 0) {
26
+ newMsg.title = errorDescription;
27
+ } else {
28
+ newMsg.title = errorTitle;
29
+ newMsg.description = errorDescription;
30
+ }
31
+
32
+ dispatch(pushGlobalErrorMessage(newMsg));
33
+ }
34
+
35
+ return data;
36
+ });
37
+ };
38
+
39
+ export const useDispatchWithErrorHandling = () => {
40
+ const dispatch = useDispatch();
41
+
42
+ return useCallback(
43
+ ({ action, errorTitle, errorDescription, validationLookupModule, validationLookupName }) => {
44
+ return executeDispatchWithErrorHandling({
45
+ dispatch,
46
+ action,
47
+ errorTitle,
48
+ errorDescription,
49
+ validationLookupModule,
50
+ validationLookupName,
51
+ });
52
+ },
53
+ [dispatch],
54
+ );
55
+ };
56
+
57
+ export default useDispatchWithErrorHandling;