@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.
- package/dist/components/Buttons/Button/Button.d.ts +1 -1
- package/dist/components/Buttons/Button/Button.d.ts.map +1 -1
- package/dist/components/Buttons/CompositeButton/CompositeButton.d.ts +22 -0
- package/dist/components/Buttons/CompositeButton/CompositeButton.d.ts.map +1 -0
- package/dist/components/Buttons/index.d.ts +1 -0
- package/dist/components/Buttons/index.d.ts.map +1 -1
- package/dist/components/Explorer/Explorer.d.ts.map +1 -1
- package/dist/components/FormStation/SaveOnNavigate/SaveOnNavigate.d.ts.map +1 -1
- package/dist/components/Icons/Icons.d.ts.map +1 -1
- package/dist/components/Icons/Icons.models.d.ts +15 -12
- package/dist/components/Icons/Icons.models.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.js +4 -4
- package/dist/index.js +4 -4
- package/dist/initialize.d.ts +11 -0
- package/dist/initialize.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/ui-config.d.ts +20 -0
- package/dist/types/ui-config.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/components/Buttons/Button/Button.tsx +54 -49
- package/src/components/Buttons/Buttons.stories.tsx +29 -7
- package/src/components/Buttons/CompositeButton/CompositeButton.scss +116 -0
- package/src/components/Buttons/CompositeButton/CompositeButton.spec.tsx +137 -0
- package/src/components/Buttons/CompositeButton/CompositeButton.tsx +70 -0
- package/src/components/Buttons/index.ts +4 -0
- package/src/components/Explorer/Explorer.tsx +4 -2
- package/src/components/FormElements/CustomTags/CustomTags.scss +1 -2
- package/src/components/FormStation/SaveOnNavigate/SaveOnNavigate.tsx +10 -2
- package/src/components/Icons/Icons.models.ts +3 -0
- package/src/components/Icons/Icons.spec.tsx +38 -29
- package/src/components/Icons/Icons.stories.tsx +30 -14
- package/src/components/Icons/Icons.tsx +129 -90
- package/src/index.ts +2 -0
- package/src/initialize.ts +25 -0
- package/src/styles/variables.scss +4 -0
- package/src/types/index.ts +1 -0
- 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 @@
|
|
|
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.
|
|
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
|
|
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": "
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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
|
};
|