@axinom/mosaic-ui 0.26.0-rc.9 → 0.26.1

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 (40) hide show
  1. package/dist/components/Buttons/Button/Button.d.ts +1 -1
  2. package/dist/components/Buttons/Button/Button.d.ts.map +1 -1
  3. package/dist/components/Buttons/CompositeButton/CompositeButton.d.ts +22 -0
  4. package/dist/components/Buttons/CompositeButton/CompositeButton.d.ts.map +1 -0
  5. package/dist/components/Buttons/index.d.ts +1 -0
  6. package/dist/components/Buttons/index.d.ts.map +1 -1
  7. package/dist/components/Explorer/Explorer.d.ts.map +1 -1
  8. package/dist/components/FormStation/SaveOnNavigate/SaveOnNavigate.d.ts.map +1 -1
  9. package/dist/components/Icons/Icons.d.ts.map +1 -1
  10. package/dist/components/Icons/Icons.models.d.ts +15 -12
  11. package/dist/components/Icons/Icons.models.d.ts.map +1 -1
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.es.js +4 -4
  15. package/dist/index.js +4 -4
  16. package/dist/initialize.d.ts +11 -0
  17. package/dist/initialize.d.ts.map +1 -0
  18. package/dist/types/index.d.ts +2 -0
  19. package/dist/types/index.d.ts.map +1 -0
  20. package/dist/types/ui-config.d.ts +20 -0
  21. package/dist/types/ui-config.d.ts.map +1 -0
  22. package/package.json +3 -3
  23. package/src/components/Buttons/Button/Button.tsx +54 -49
  24. package/src/components/Buttons/Buttons.stories.tsx +29 -7
  25. package/src/components/Buttons/CompositeButton/CompositeButton.scss +116 -0
  26. package/src/components/Buttons/CompositeButton/CompositeButton.spec.tsx +137 -0
  27. package/src/components/Buttons/CompositeButton/CompositeButton.tsx +70 -0
  28. package/src/components/Buttons/index.ts +4 -0
  29. package/src/components/Explorer/Explorer.tsx +4 -2
  30. package/src/components/FormElements/CustomTags/CustomTags.scss +1 -2
  31. package/src/components/FormStation/SaveOnNavigate/SaveOnNavigate.tsx +10 -2
  32. package/src/components/Icons/Icons.models.ts +3 -0
  33. package/src/components/Icons/Icons.spec.tsx +38 -29
  34. package/src/components/Icons/Icons.stories.tsx +30 -14
  35. package/src/components/Icons/Icons.tsx +129 -90
  36. package/src/index.ts +2 -0
  37. package/src/initialize.ts +25 -0
  38. package/src/styles/variables.scss +4 -0
  39. package/src/types/index.ts +1 -0
  40. package/src/types/ui-config.ts +30 -0
@@ -0,0 +1,11 @@
1
+ import { ShowNotification } from './types/ui-config';
2
+ export declare let showNotification: ShowNotification | (() => void);
3
+ /**
4
+ * Passes the PiralApi methods to the UI library.
5
+ * @param app {UiConfig} object containing PiralApi methods for use in UI library.
6
+ */
7
+ export declare function initializeUi(app: UiConfig): void;
8
+ export interface UiConfig {
9
+ showNotification: ShowNotification;
10
+ }
11
+ //# sourceMappingURL=initialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"initialize.d.ts","sourceRoot":"","sources":["../src/initialize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,eAAO,IAAI,gBAAgB,EAAE,gBAAgB,GAAG,CAAC,MAAM,IAAI,CAC7B,CAAC;AAE/B;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAEhD;AAED,MAAM,WAAW,QAAQ;IACvB,gBAAgB,EAAE,gBAAgB,CAAC;CACpC"}
@@ -0,0 +1,2 @@
1
+ export * from './ui-config';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { Component, ReactNode } from 'react';
2
+ export declare type ShowNotification = (notification: Notification) => NotificationId;
3
+ export declare type UpdateNotification = (id: NotificationId, notification: Notification) => void;
4
+ export declare type DismissNotification = (id: NotificationId) => void;
5
+ export interface Notification {
6
+ /** Title shown in toast messages header */
7
+ title: string;
8
+ /** Optional body of the notification */
9
+ body?: NotificationBody;
10
+ /** Additional options for the notification */
11
+ options?: NotificationOptions;
12
+ }
13
+ export interface NotificationOptions {
14
+ type?: NotificationType;
15
+ autoClose?: number | false;
16
+ }
17
+ export declare type NotificationId = string | number;
18
+ export declare type NotificationBody = string | ReactNode | Component;
19
+ export declare type NotificationType = 'success' | 'error' | 'info' | 'warning';
20
+ //# sourceMappingURL=ui-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-config.d.ts","sourceRoot":"","sources":["../../src/types/ui-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C,oBAAY,gBAAgB,GAAG,CAAC,YAAY,EAAE,YAAY,KAAK,cAAc,CAAC;AAE9E,oBAAY,kBAAkB,GAAG,CAC/B,EAAE,EAAE,cAAc,EAClB,YAAY,EAAE,YAAY,KACvB,IAAI,CAAC;AAEV,oBAAY,mBAAmB,GAAG,CAAC,EAAE,EAAE,cAAc,KAAK,IAAI,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC5B;AAED,oBAAY,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7C,oBAAY,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9D,oBAAY,gBAAgB,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.26.0-rc.9",
3
+ "version": "0.26.1",
4
4
  "description": "UI components for building Axinom Mosaic applications",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -32,7 +32,7 @@
32
32
  "build-storybook": "build-storybook -s ./.storybook/static"
33
33
  },
34
34
  "dependencies": {
35
- "@axinom/mosaic-core": "^0.3.0-rc.9",
35
+ "@axinom/mosaic-core": "^0.3.0",
36
36
  "@faker-js/faker": "^7.4.0",
37
37
  "@popperjs/core": "^2.9.2",
38
38
  "clsx": "^1.1.0",
@@ -92,5 +92,5 @@
92
92
  "publishConfig": {
93
93
  "access": "public"
94
94
  },
95
- "gitHead": "9d74a14d2f82f48bfdf530cf3e9164ee9192e02b"
95
+ "gitHead": "90efdcc98420ade859329a8f89863ab64ed4354a"
96
96
  }
@@ -14,59 +14,64 @@ export interface ButtonProps extends BaseButtonOptions {
14
14
  icon?: IconName;
15
15
  }
16
16
 
17
+ const ButtonElement: React.ForwardRefRenderFunction<
18
+ HTMLButtonElement,
19
+ ButtonProps
20
+ > = (
21
+ {
22
+ type = 'button',
23
+ height,
24
+ width,
25
+ buttonContext = ButtonContext.Navigation,
26
+ disabled = false,
27
+ icon,
28
+ className = '',
29
+ dataTestId = undefined,
30
+ onButtonClicked = noop,
31
+ onBlur,
32
+ },
33
+ ref,
34
+ ) => {
35
+ const customStyles = {
36
+ height: height,
37
+ width: width,
38
+ } as React.CSSProperties;
39
+
40
+ return (
41
+ <button
42
+ ref={ref}
43
+ className={clsx(
44
+ classes.container,
45
+ { [classes.navigation]: buttonContext === ButtonContext.Navigation },
46
+ {
47
+ [classes.context]: buttonContext === ButtonContext.Context,
48
+ },
49
+ {
50
+ [classes.icon]: buttonContext === ButtonContext.Icon,
51
+ },
52
+ {
53
+ [classes.active]: buttonContext === ButtonContext.Active,
54
+ },
55
+ 'button-container',
56
+ className,
57
+ )}
58
+ type={type}
59
+ style={customStyles}
60
+ onClick={onButtonClicked}
61
+ disabled={disabled}
62
+ onBlur={onBlur}
63
+ data-test-id={dataTestId ?? 'button'}
64
+ >
65
+ <Icons icon={icon} />
66
+ </button>
67
+ );
68
+ };
69
+
17
70
  /**
18
71
  * Button which can render an optional icon
19
72
  * @example
20
73
  * <Button type="button" icon={IconName.X} />
21
74
  */
22
75
  export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
23
- (
24
- {
25
- type = 'button',
26
- height,
27
- width,
28
- buttonContext = ButtonContext.Navigation,
29
- disabled = false,
30
- icon,
31
- className = '',
32
- dataTestId = undefined,
33
- onButtonClicked = noop,
34
- onBlur,
35
- },
36
- ref,
37
- ) => {
38
- const customStyles = {
39
- height: height,
40
- width: width,
41
- } as React.CSSProperties;
42
-
43
- return (
44
- <button
45
- ref={ref}
46
- className={clsx(
47
- classes.container,
48
- { [classes.navigation]: buttonContext === ButtonContext.Navigation },
49
- {
50
- [classes.context]: buttonContext === ButtonContext.Context,
51
- },
52
- {
53
- [classes.icon]: buttonContext === ButtonContext.Icon,
54
- },
55
- {
56
- [classes.active]: buttonContext === ButtonContext.Active,
57
- },
58
- 'button-container',
59
- className,
60
- )}
61
- type={type}
62
- style={customStyles}
63
- onClick={onButtonClicked}
64
- disabled={disabled}
65
- onBlur={onBlur}
66
- data-test-id={dataTestId ?? 'button'}
67
- >
68
- <Icons icon={icon} />
69
- </button>
70
- );
71
- },
76
+ ButtonElement,
72
77
  );
@@ -5,6 +5,7 @@ import { Story } from '../../helpers/storybook';
5
5
  import { IconName } from '../Icons';
6
6
  import { ButtonContext } from './Button.model';
7
7
  import { Button } from './Button/Button';
8
+ import { CompositeButton } from './CompositeButton/CompositeButton';
8
9
  import { TextButton } from './TextButton/TextButton';
9
10
 
10
11
  const typeOptions = {
@@ -14,14 +15,15 @@ const typeOptions = {
14
15
  Active: ButtonContext.Active,
15
16
  };
16
17
 
17
- export const button: Story = () => {
18
- const iconOptions = {
19
- ChevronRight: IconName.ChevronRight,
20
- Archive: IconName.Archive,
21
- Undo: IconName.Undo,
22
- X: IconName.X,
23
- };
18
+ const iconOptions = {
19
+ ChevronRight: IconName.ChevronRight,
20
+ ChevronLeft: IconName.ChevronLeft,
21
+ Archive: IconName.Archive,
22
+ Undo: IconName.Undo,
23
+ X: IconName.X,
24
+ };
24
25
 
26
+ export const button: Story = () => {
25
27
  return (
26
28
  <Button
27
29
  type="button"
@@ -57,6 +59,26 @@ export const textButton: Story = () => {
57
59
  );
58
60
  };
59
61
 
62
+ export const compositeButton: Story = () => {
63
+ return (
64
+ <CompositeButton
65
+ type="button"
66
+ height={text('height', '50px')}
67
+ width={text('width', '')}
68
+ text={text('text', 'Placeholder')}
69
+ icon={select<IconName>('Icon Name', iconOptions, IconName.ChevronRight)}
70
+ iconPosition={select('Icon Position', ['left', 'right'], 'right')}
71
+ buttonContext={select<ButtonContext>(
72
+ 'Button Type',
73
+ typeOptions,
74
+ ButtonContext.Context,
75
+ )}
76
+ disabled={boolean('Disabled?', false)}
77
+ onButtonClicked={action('buttonClicked')}
78
+ />
79
+ );
80
+ };
81
+
60
82
  export default {
61
83
  title: `Other Components/Buttons`,
62
84
  };
@@ -0,0 +1,116 @@
1
+ @import '../../../styles/common.scss';
2
+
3
+ .container {
4
+ @include boxSizing;
5
+
6
+ height: 50px;
7
+
8
+ display: grid;
9
+ grid-auto-flow: column;
10
+ gap: 5px;
11
+ place-items: center;
12
+ width: max-content;
13
+
14
+ padding: 5px 15px;
15
+
16
+ font-weight: bold;
17
+
18
+ cursor: pointer;
19
+
20
+ div {
21
+ direction: ltr;
22
+ }
23
+
24
+ svg {
25
+ height: 60%;
26
+ }
27
+
28
+ &.navigation {
29
+ color: white;
30
+ background-color: $light-blue;
31
+
32
+ svg * {
33
+ stroke: white;
34
+ }
35
+
36
+ &:hover {
37
+ background-color: lighten($light-blue, 10%);
38
+ }
39
+
40
+ &:active {
41
+ background-color: white;
42
+ color: $light-blue;
43
+
44
+ svg * {
45
+ stroke: $light-blue;
46
+ }
47
+ }
48
+
49
+ &:disabled {
50
+ background-color: $gray;
51
+ color: rgba(white, 0.5);
52
+
53
+ svg * {
54
+ opacity: 0.5;
55
+ }
56
+ }
57
+ }
58
+
59
+ &.context {
60
+ color: $blue;
61
+ border: 1px solid $blue;
62
+ background-color: white;
63
+
64
+ transition: box-shadow 0.15s ease-in-out 0s;
65
+
66
+ svg * {
67
+ stroke: $blue;
68
+ }
69
+
70
+ &:hover {
71
+ border: 1px solid $blue;
72
+ box-shadow: 0 0 0 2px $blue;
73
+ }
74
+
75
+ &:active {
76
+ background-color: $blue;
77
+ color: white;
78
+
79
+ svg * {
80
+ stroke: white;
81
+ }
82
+ }
83
+
84
+ &:disabled {
85
+ border: 1px solid $gray;
86
+ color: $gray;
87
+
88
+ svg * {
89
+ stroke: $gray;
90
+ }
91
+ }
92
+ }
93
+
94
+ &.active {
95
+ color: white;
96
+ background-color: $blue;
97
+
98
+ &:hover {
99
+ background-color: lighten($blue, 10%);
100
+ }
101
+
102
+ &:active {
103
+ background-color: white;
104
+ color: $blue;
105
+ }
106
+
107
+ &:disabled {
108
+ background-color: $gray;
109
+ color: rgba(white, 0.5);
110
+ }
111
+ }
112
+
113
+ &:disabled {
114
+ cursor: not-allowed;
115
+ }
116
+ }
@@ -0,0 +1,137 @@
1
+ import { mount, shallow } from 'enzyme';
2
+ import React from 'react';
3
+ import { ButtonContext } from '../Button.model';
4
+ import { CompositeButton } from './CompositeButton';
5
+
6
+ describe('Button', () => {
7
+ it('renders the component without crashing', () => {
8
+ const wrapper = shallow(<CompositeButton />);
9
+
10
+ expect(wrapper).toBeTruthy();
11
+ });
12
+
13
+ it('raises the onButtonClicked event', () => {
14
+ const spy = jest.fn();
15
+ const wrapper = shallow(<CompositeButton onButtonClicked={spy} />);
16
+
17
+ wrapper.simulate('click');
18
+
19
+ expect(spy).toHaveBeenCalledTimes(1);
20
+ });
21
+
22
+ it('creates a class based off of the className prop', () => {
23
+ const mockClassName = 'test-class';
24
+ const wrapper = shallow(<CompositeButton className={mockClassName} />);
25
+
26
+ const button = wrapper.find('button');
27
+
28
+ expect(button.hasClass(mockClassName)).toBe(true);
29
+ });
30
+
31
+ it(`button 'type' must be 'button' by default`, () => {
32
+ const wrapper = shallow(<CompositeButton />);
33
+
34
+ const buttonType = wrapper.find('button').prop('type');
35
+
36
+ expect(buttonType).toBe('button');
37
+ });
38
+
39
+ it('accepts type html attribute and width/height styles', () => {
40
+ const mockType = 'submit';
41
+ const mockHeight = '75px';
42
+ const mockWidth = '80px';
43
+
44
+ const wrapper = shallow(<CompositeButton />);
45
+
46
+ let buttonType = wrapper.find('button').prop('type');
47
+ let buttonStyles = wrapper
48
+ .find('button')
49
+ .prop('style') as React.CSSProperties;
50
+
51
+ expect(buttonType).toBe('button');
52
+ expect(buttonStyles.height).toBeUndefined();
53
+ expect(buttonStyles.width).toBeUndefined();
54
+
55
+ wrapper.setProps({ type: mockType, height: mockHeight, width: mockWidth });
56
+ wrapper.update();
57
+
58
+ buttonType = wrapper.find('button').prop('type');
59
+ buttonStyles = wrapper.find('button').prop('style') as React.CSSProperties;
60
+
61
+ expect(buttonType).toBe(mockType);
62
+ expect(buttonStyles.height).toBe(mockHeight);
63
+ expect(buttonStyles.width).toBe(mockWidth);
64
+ });
65
+
66
+ it('allows the button to be enabled by default', () => {
67
+ const spy = jest.fn();
68
+ const wrapper = mount(<CompositeButton onButtonClicked={spy} />);
69
+
70
+ const button = wrapper.find('button');
71
+
72
+ wrapper.simulate('click');
73
+
74
+ expect(button.prop('disabled')).toBe(false);
75
+ expect(spy).toHaveBeenCalledTimes(1);
76
+ });
77
+
78
+ it('allows button to be disabled', () => {
79
+ const spy = jest.fn();
80
+ const wrapper = mount(
81
+ <CompositeButton disabled={true} onButtonClicked={spy} />,
82
+ );
83
+
84
+ const button = wrapper.find('button');
85
+
86
+ wrapper.simulate('click');
87
+
88
+ expect(button.prop('disabled')).toBe(true);
89
+ expect(spy).not.toHaveBeenCalled();
90
+ });
91
+
92
+ it('default should have a context class', () => {
93
+ const wrapper = shallow(<CompositeButton />);
94
+
95
+ const button = wrapper.find('button');
96
+
97
+ expect(button.hasClass('context')).toBe(true);
98
+ });
99
+
100
+ it('context button type should have a context class', () => {
101
+ const wrapper = shallow(
102
+ <CompositeButton buttonContext={ButtonContext.Context} />,
103
+ );
104
+
105
+ const button = wrapper.find('button');
106
+
107
+ expect(button.hasClass('context')).toBe(true);
108
+ });
109
+
110
+ it('active button type should have a active class', () => {
111
+ const wrapper = shallow(
112
+ <CompositeButton buttonContext={ButtonContext.Active} />,
113
+ );
114
+
115
+ const button = wrapper.find('button');
116
+
117
+ expect(button.hasClass('active')).toBe(true);
118
+ });
119
+
120
+ it(`renders value from 'text' prop`, () => {
121
+ const mockText = 'mock-text';
122
+
123
+ const wrapper = shallow(<CompositeButton text={mockText} />);
124
+
125
+ const button = wrapper.find('button');
126
+
127
+ expect(button.text()).toBe(mockText);
128
+ });
129
+
130
+ it(`defaults to an empty string is 'text' prop is not set`, () => {
131
+ const wrapper = shallow(<CompositeButton />);
132
+
133
+ const button = wrapper.find('button');
134
+
135
+ expect(button.text()).toBe('');
136
+ });
137
+ });
@@ -0,0 +1,70 @@
1
+ import clsx from 'clsx';
2
+ import React from 'react';
3
+ import { noop } from '../../../helpers/utils';
4
+ import { IconName, Icons } from '../../Icons';
5
+ import { BaseButtonOptions, ButtonContext } from '../Button.model';
6
+ import classes from './CompositeButton.scss';
7
+
8
+ // TODO: Refactor buttons. Use a composition model.
9
+ export interface CompositeButtonProps extends BaseButtonOptions {
10
+ /** Button's height (default: '30px') */
11
+ height?: string | number;
12
+ /** Button's width (default: undefined) */
13
+ width?: string | number;
14
+ /** Button text */
15
+ text?: string;
16
+ /** Optional icon */
17
+ icon?: IconName;
18
+ /** Optional Icon Position */
19
+ iconPosition?: 'left' | 'right';
20
+ }
21
+
22
+ /**
23
+ * Button which can render an optional text string
24
+ * @example
25
+ * <CompositeButton type="button" text="Click me!" />
26
+ */
27
+ export const CompositeButton: React.FC<CompositeButtonProps> = ({
28
+ type = 'button',
29
+ height,
30
+ width,
31
+ buttonContext = ButtonContext.Context,
32
+ disabled = false,
33
+ text = '',
34
+ icon,
35
+ iconPosition,
36
+ className,
37
+ dataTestId = undefined,
38
+ onButtonClicked = noop,
39
+ }: CompositeButtonProps) => {
40
+ const customStyles = {
41
+ height: height,
42
+ width: width,
43
+ direction: iconPosition === 'left' ? 'rtl' : 'ltr',
44
+ } as React.CSSProperties;
45
+
46
+ return (
47
+ <button
48
+ className={clsx(
49
+ classes.container,
50
+ { [classes.navigation]: buttonContext === ButtonContext.Navigation },
51
+ {
52
+ [classes.context]: buttonContext === ButtonContext.Context,
53
+ },
54
+ {
55
+ [classes.active]: buttonContext === ButtonContext.Active,
56
+ },
57
+ 'composite-button-container',
58
+ className,
59
+ )}
60
+ type={type}
61
+ style={customStyles}
62
+ onClick={onButtonClicked}
63
+ disabled={disabled}
64
+ data-test-id={dataTestId ?? 'text-button'}
65
+ >
66
+ <div>{text}</div>
67
+ {icon !== null && icon !== undefined && <Icons icon={icon} />}
68
+ </button>
69
+ );
70
+ };
@@ -1,3 +1,7 @@
1
1
  export { ButtonContext } from './Button.model';
2
2
  export { Button, ButtonProps } from './Button/Button';
3
+ export {
4
+ CompositeButton,
5
+ CompositeButtonProps,
6
+ } from './CompositeButton/CompositeButton';
3
7
  export { TextButton, TextButtonProps } from './TextButton/TextButton';
@@ -9,6 +9,7 @@ import {
9
9
  } from 'react';
10
10
  import { ActionData } from '..';
11
11
  import { noop } from '../../helpers/utils';
12
+ import { showNotification } from '../../initialize';
12
13
  import { ErrorTypeToStationError } from '../../utils/ErrorTypeToStationError';
13
14
  import { Filters } from '../Filters/Filters';
14
15
  import { FilterType, FilterValues } from '../Filters/Filters.model';
@@ -361,8 +362,9 @@ export const Explorer = React.forwardRef(function Explorer<T>(
361
362
  if (result) {
362
363
  setStationMessage(errMsg(result));
363
364
  } else {
364
- // eslint-disable-next-line no-console
365
- // console.log(`Bulk Action ${action.label} Started`);
365
+ showNotification({
366
+ title: `Bulk Action ${action.label} Started`,
367
+ });
366
368
 
367
369
  if (action.reloadData) {
368
370
  onReloadData();
@@ -62,8 +62,6 @@
62
62
  input,
63
63
  .autoComplete > li {
64
64
  padding: 0 60px 0 12px;
65
- z-index: 5;
66
-
67
65
  color: var(--input-color, $input-color);
68
66
  }
69
67
 
@@ -82,6 +80,7 @@
82
80
  right: -25px;
83
81
 
84
82
  list-style-type: none;
83
+ z-index: 10;
85
84
 
86
85
  // -webkit-box-shadow: 0px 0px 9px -2px rgba(0, 0, 0, 0.87);
87
86
 
@@ -1,5 +1,5 @@
1
1
  import { useFormikContext } from 'formik';
2
- import React from 'react';
2
+ import React, { useState } from 'react';
3
3
  import { NavigationAPI, useReactRouterPause } from '../../../hooks';
4
4
 
5
5
  interface SaveOnNavigateProps {
@@ -14,6 +14,8 @@ export const SaveOnNavigate: React.FC<SaveOnNavigateProps> = ({
14
14
  isSubmitting = false,
15
15
  }) => {
16
16
  const { dirty, isValid, submitForm } = useFormikContext();
17
+ const [canSubmit, setCanSubmit] = useState<boolean>(true);
18
+
17
19
  async function handleNavigationAttempt(
18
20
  navigation: NavigationAPI,
19
21
  ): Promise<void> {
@@ -32,7 +34,9 @@ export const SaveOnNavigate: React.FC<SaveOnNavigateProps> = ({
32
34
  navigation.pause();
33
35
  try {
34
36
  // only attempt to submit the form if there is no current submission in progress
35
- if (!isSubmitting) {
37
+ if (!isSubmitting && canSubmit) {
38
+ // Block additional submit requests before continuing navigation or double clicking can cause the form to be submitted again.
39
+ setCanSubmit(false);
36
40
  // Wait for success response
37
41
  await submitForm();
38
42
  // Successfully saved, continue navigation
@@ -42,8 +46,12 @@ export const SaveOnNavigate: React.FC<SaveOnNavigateProps> = ({
42
46
  // TODO: Proper Error handling
43
47
  // An error occurred while attempting to save, cancel navigation
44
48
  navigation.cancel();
49
+ // Allow submits again if error
50
+ setCanSubmit(true);
45
51
  }
46
52
  }
53
+ // After a successful navigation, this component is destroyed and 'canSubmit' is no longer false.
54
+
47
55
  useReactRouterPause({ handler: handleNavigationAttempt }, dirty);
48
56
  return null;
49
57
  };
@@ -24,12 +24,15 @@ export enum IconName {
24
24
  MidlineEllipsis,
25
25
  Minus,
26
26
  NavigateRight,
27
+ Pause,
28
+ Play,
27
29
  Plus,
28
30
  Publish,
29
31
  RemoveFilter,
30
32
  Retry,
31
33
  Replace,
32
34
  Snapshot,
35
+ Stop,
33
36
  Success,
34
37
  Undo,
35
38
  Unpublish,