@truedat/core 4.49.0 → 4.49.3
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/CHANGELOG.md +12 -0
- package/package.json +3 -3
- package/src/components/DomainSelector.js +16 -3
- package/src/components/DropdownMenuItem.js +10 -10
- package/src/components/FilterMultilevelDropdown.js +2 -2
- package/src/components/TreeSelector.js +60 -31
- package/src/components/__tests__/DomainSelector.spec.js +1 -1
- package/src/components/__tests__/DropdownMenuItem.spec.js +7 -7
- package/src/components/__tests__/TreeSelector.spec.js +31 -5
- package/src/components/__tests__/__snapshots__/TreeSelector.spec.js.snap +2 -2
- package/src/services/__tests__/api.spec.js +2 -2
- package/src/services/api.js +1 -1
- package/src/services/axios.js +22 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.49.3] 2022-07-29
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- [TD-5094] Federated authentication fails if token is present in local storage
|
|
8
|
+
|
|
9
|
+
## [4.49.1] 2022-07-28
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- [TD-5090] Support for single selection on `DomainSelector`
|
|
14
|
+
|
|
3
15
|
## [4.49.0] 2022-07-27
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/core",
|
|
3
|
-
"version": "4.49.
|
|
3
|
+
"version": "4.49.3",
|
|
4
4
|
"description": "Truedat Web Core",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"jsnext:main": "src/index.js",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@apollo/client": "^3.6.4",
|
|
92
|
-
"axios": "^0.
|
|
92
|
+
"axios": "^0.27.2",
|
|
93
93
|
"immutable": "^4.0.0-rc.12",
|
|
94
94
|
"is-hotkey": "^0.1.6",
|
|
95
95
|
"is-url": "^1.2.4",
|
|
@@ -112,5 +112,5 @@
|
|
|
112
112
|
"react-dom": ">= 16.8.6 < 17",
|
|
113
113
|
"semantic-ui-react": ">= 0.88.2 < 2.1"
|
|
114
114
|
},
|
|
115
|
-
"gitHead": "
|
|
115
|
+
"gitHead": "85dd5f00cb57a32c3707ecd126f8ac238ad8a71d"
|
|
116
116
|
}
|
|
@@ -8,9 +8,16 @@ import { stratify, flatten } from "../services/tree";
|
|
|
8
8
|
import { DOMAINS_QUERY } from "../api/queries";
|
|
9
9
|
import TreeSelector from "./TreeSelector";
|
|
10
10
|
|
|
11
|
-
export const DomainSelector = ({
|
|
11
|
+
export const DomainSelector = ({
|
|
12
|
+
action,
|
|
13
|
+
multiple,
|
|
14
|
+
onLoad,
|
|
15
|
+
value,
|
|
16
|
+
...props
|
|
17
|
+
}) => {
|
|
12
18
|
const { formatMessage } = useIntl();
|
|
13
19
|
const { loading, error, data } = useQuery(DOMAINS_QUERY, {
|
|
20
|
+
fetchPolicy: "cache-and-network",
|
|
14
21
|
variables: { action },
|
|
15
22
|
onCompleted: onLoad,
|
|
16
23
|
});
|
|
@@ -26,8 +33,13 @@ export const DomainSelector = ({ action, onLoad, value, ...props }) => {
|
|
|
26
33
|
return (
|
|
27
34
|
<TreeSelector
|
|
28
35
|
options={options}
|
|
29
|
-
placeholder={formatMessage({
|
|
30
|
-
|
|
36
|
+
placeholder={formatMessage({
|
|
37
|
+
id: multiple
|
|
38
|
+
? "domain.multiple.placeholder"
|
|
39
|
+
: "domain.selector.placeholder",
|
|
40
|
+
})}
|
|
41
|
+
value={multiple ? _.map(_.toString)(value) : _.toString(value)}
|
|
42
|
+
multiple={multiple}
|
|
31
43
|
{...props}
|
|
32
44
|
/>
|
|
33
45
|
);
|
|
@@ -35,6 +47,7 @@ export const DomainSelector = ({ action, onLoad, value, ...props }) => {
|
|
|
35
47
|
|
|
36
48
|
DomainSelector.propTypes = {
|
|
37
49
|
action: PropTypes.string,
|
|
50
|
+
multiple: PropTypes.bool,
|
|
38
51
|
onLoad: PropTypes.func,
|
|
39
52
|
value: PropTypes.array,
|
|
40
53
|
};
|
|
@@ -6,30 +6,30 @@ export const DropdownMenuItem = ({
|
|
|
6
6
|
id,
|
|
7
7
|
canOpen,
|
|
8
8
|
check = true,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
onOpen,
|
|
10
|
+
onClick,
|
|
11
11
|
selected,
|
|
12
12
|
open,
|
|
13
13
|
name,
|
|
14
14
|
level,
|
|
15
15
|
}) => {
|
|
16
|
-
const
|
|
16
|
+
const handleOpen = (e) => {
|
|
17
17
|
e && e.preventDefault();
|
|
18
18
|
e && e.stopPropagation();
|
|
19
|
-
|
|
19
|
+
onOpen(id);
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const handleClick = (e) => {
|
|
23
23
|
e && e.preventDefault();
|
|
24
24
|
e && e.stopPropagation();
|
|
25
|
-
|
|
25
|
+
onClick(e, id);
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
|
-
<Dropdown.Item onClick={
|
|
29
|
+
<Dropdown.Item onClick={handleClick} selected={!check && selected}>
|
|
30
30
|
<div style={{ marginLeft: `${8 * level}px` }}>
|
|
31
31
|
{canOpen ? (
|
|
32
|
-
<Icon name={open ? "minus" : "plus"} onClick={
|
|
32
|
+
<Icon name={open ? "minus" : "plus"} onClick={handleOpen} />
|
|
33
33
|
) : (
|
|
34
34
|
<Icon />
|
|
35
35
|
)}
|
|
@@ -46,8 +46,8 @@ DropdownMenuItem.propTypes = {
|
|
|
46
46
|
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
47
47
|
canOpen: PropTypes.bool,
|
|
48
48
|
check: PropTypes.bool,
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
onOpen: PropTypes.func,
|
|
50
|
+
onClick: PropTypes.func,
|
|
51
51
|
selected: PropTypes.bool,
|
|
52
52
|
open: PropTypes.bool,
|
|
53
53
|
name: PropTypes.string,
|
|
@@ -172,8 +172,8 @@ export const FilterMultilevelDropdown = ({
|
|
|
172
172
|
{_.map.convert({ cap: false })((option, i) => (
|
|
173
173
|
<DropdownMenuItem
|
|
174
174
|
key={i}
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
onOpen={handleOpen}
|
|
176
|
+
onClick={handleClick}
|
|
177
177
|
open={_.contains(option.id)(open)}
|
|
178
178
|
canOpen={_.negate(_.isEmpty)(option.children)}
|
|
179
179
|
selected={_.contains(option.id)(selected)}
|
|
@@ -12,18 +12,57 @@ export const match =
|
|
|
12
12
|
_.contains(query)(lowerDeburr(name));
|
|
13
13
|
export const matchAny = recursiveMatch(match);
|
|
14
14
|
|
|
15
|
+
export const SelectedLabels = ({ onClick, options, placeholder, value }) =>
|
|
16
|
+
_.isEmpty(value) ? (
|
|
17
|
+
<label>{placeholder}</label>
|
|
18
|
+
) : (
|
|
19
|
+
_.map((id) => (
|
|
20
|
+
<Label key={id}>
|
|
21
|
+
{_.flow(_.find({ id }), _.prop("name"))(options)}
|
|
22
|
+
<Icon
|
|
23
|
+
name="delete"
|
|
24
|
+
onClick={(e) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
onClick(e, id);
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
</Label>
|
|
31
|
+
))(value)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
SelectedLabels.propTypes = {
|
|
35
|
+
onClick: PropTypes.func,
|
|
36
|
+
options: PropTypes.array,
|
|
37
|
+
placeholder: PropTypes.string,
|
|
38
|
+
value: PropTypes.array,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const toggle = (id) => (value) =>
|
|
42
|
+
_.includes(id)(value) ? _.without([id])(value) : _.union([id])(value);
|
|
43
|
+
|
|
44
|
+
const labelValues = _.cond([
|
|
45
|
+
[_.isArray, _.identity],
|
|
46
|
+
[_.isEmpty, _.constant([])],
|
|
47
|
+
[_.isString, _.castArray],
|
|
48
|
+
[_.stubTrue, _.constant([])],
|
|
49
|
+
]);
|
|
50
|
+
|
|
15
51
|
export const TreeSelector = ({
|
|
16
|
-
options,
|
|
17
52
|
error,
|
|
18
53
|
label,
|
|
54
|
+
multiple = false,
|
|
19
55
|
name,
|
|
20
56
|
onBlur,
|
|
21
57
|
onChange,
|
|
22
|
-
|
|
58
|
+
options,
|
|
23
59
|
placeholder,
|
|
24
|
-
|
|
60
|
+
required = false,
|
|
61
|
+
value: initialValue,
|
|
25
62
|
}) => {
|
|
26
|
-
const [value, setValue] = useState(
|
|
63
|
+
const [value, setValue] = useState(
|
|
64
|
+
_.defaultTo(multiple ? [] : null)(initialValue)
|
|
65
|
+
);
|
|
27
66
|
const [query, setQuery] = useState();
|
|
28
67
|
const [open, setOpen] = useState([]);
|
|
29
68
|
const [displayed, setDisplayed] = useState([]);
|
|
@@ -63,11 +102,9 @@ export const TreeSelector = ({
|
|
|
63
102
|
};
|
|
64
103
|
|
|
65
104
|
const handleClick = (e, id) => {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
setValue(ids);
|
|
70
|
-
onChange && onChange(e, { value: ids });
|
|
105
|
+
const nextValue = multiple ? toggle(id)(value) : value === id ? null : id;
|
|
106
|
+
setValue(nextValue);
|
|
107
|
+
onChange && onChange(e, { value: nextValue });
|
|
71
108
|
};
|
|
72
109
|
|
|
73
110
|
const filterSearch = query ? _.filter(matchAny(query)) : _.identity;
|
|
@@ -76,22 +113,13 @@ export const TreeSelector = ({
|
|
|
76
113
|
({ id, level }) => level === 0 || _.includes(id)(displayed)
|
|
77
114
|
);
|
|
78
115
|
|
|
79
|
-
const trigger =
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
name="delete"
|
|
87
|
-
onClick={(e) => {
|
|
88
|
-
e.preventDefault();
|
|
89
|
-
e.stopPropagation();
|
|
90
|
-
handleClick(e, id);
|
|
91
|
-
}}
|
|
92
|
-
/>
|
|
93
|
-
</Label>
|
|
94
|
-
))(value)
|
|
116
|
+
const trigger = (
|
|
117
|
+
<SelectedLabels
|
|
118
|
+
placeholder={placeholder}
|
|
119
|
+
options={options}
|
|
120
|
+
onClick={handleClick}
|
|
121
|
+
value={labelValues(value)}
|
|
122
|
+
/>
|
|
95
123
|
);
|
|
96
124
|
|
|
97
125
|
const items = _.flow(
|
|
@@ -101,11 +129,11 @@ export const TreeSelector = ({
|
|
|
101
129
|
<DropdownMenuItem
|
|
102
130
|
key={option?.id}
|
|
103
131
|
check={false}
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
onOpen={handleOpen}
|
|
133
|
+
onClick={handleClick}
|
|
106
134
|
open={_.contains(option.id)(open)}
|
|
107
135
|
canOpen={!_.isEmpty(option.children)}
|
|
108
|
-
selected={_.contains(option.id)(value)}
|
|
136
|
+
selected={multiple ? _.contains(option.id)(value) : value === option.id}
|
|
109
137
|
{...option}
|
|
110
138
|
/>
|
|
111
139
|
))
|
|
@@ -116,12 +144,12 @@ export const TreeSelector = ({
|
|
|
116
144
|
error={error}
|
|
117
145
|
floating
|
|
118
146
|
label={label}
|
|
147
|
+
multiple={multiple}
|
|
119
148
|
name={name}
|
|
120
149
|
onBlur={onBlur}
|
|
121
|
-
upward={false}
|
|
122
150
|
required={required}
|
|
123
151
|
trigger={trigger}
|
|
124
|
-
|
|
152
|
+
upward={false}
|
|
125
153
|
value={value}
|
|
126
154
|
>
|
|
127
155
|
<Dropdown.Menu>
|
|
@@ -149,13 +177,14 @@ export const TreeSelector = ({
|
|
|
149
177
|
TreeSelector.propTypes = {
|
|
150
178
|
error: PropTypes.bool,
|
|
151
179
|
label: PropTypes.string,
|
|
180
|
+
multiple: PropTypes.bool,
|
|
152
181
|
name: PropTypes.string,
|
|
153
182
|
onBlur: PropTypes.func,
|
|
154
183
|
onChange: PropTypes.func,
|
|
155
184
|
options: PropTypes.array,
|
|
156
185
|
placeholder: PropTypes.string,
|
|
157
186
|
required: PropTypes.bool,
|
|
158
|
-
value: PropTypes.array,
|
|
187
|
+
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
|
|
159
188
|
};
|
|
160
189
|
|
|
161
190
|
export default TreeSelector;
|
|
@@ -36,7 +36,7 @@ describe("<DomainSelector />", () => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it("calls onChange with selected values", async () => {
|
|
39
|
-
const props = { action, onChange: jest.fn() };
|
|
39
|
+
const props = { action, onChange: jest.fn(), multiple: true };
|
|
40
40
|
const { getByText, getByRole } = render(
|
|
41
41
|
<DomainSelector {...props} />,
|
|
42
42
|
renderOpts
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { waitFor } from "@testing-library/react";
|
|
3
|
-
import userEvent
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
4
|
import { render } from "@truedat/test/render";
|
|
5
5
|
import { DropdownMenuItem } from "../DropdownMenuItem";
|
|
6
6
|
|
|
7
7
|
describe("<DropdownMenuItem />", () => {
|
|
8
8
|
const canOpen = true;
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const onOpen = jest.fn();
|
|
10
|
+
const onClick = jest.fn();
|
|
11
11
|
const id = 1;
|
|
12
12
|
const open = false;
|
|
13
13
|
const name = "foo";
|
|
14
14
|
|
|
15
15
|
const props = {
|
|
16
16
|
canOpen,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
onOpen,
|
|
18
|
+
onClick,
|
|
19
19
|
id,
|
|
20
20
|
open,
|
|
21
21
|
name,
|
|
@@ -35,7 +35,7 @@ describe("<DropdownMenuItem />", () => {
|
|
|
35
35
|
|
|
36
36
|
userEvent.click(container.querySelector('[class="plus icon"]'));
|
|
37
37
|
await waitFor(() => {
|
|
38
|
-
expect(
|
|
38
|
+
expect(onOpen).toHaveBeenCalledWith(id);
|
|
39
39
|
});
|
|
40
40
|
rerender(<DropdownMenuItem {...{ ...props, open: true }} />);
|
|
41
41
|
|
|
@@ -55,7 +55,7 @@ describe("<DropdownMenuItem />", () => {
|
|
|
55
55
|
userEvent.click(getByRole("option"));
|
|
56
56
|
|
|
57
57
|
await waitFor(() => {
|
|
58
|
-
expect(
|
|
58
|
+
expect(onClick.mock.calls[0][1]).toBe(id);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
rerender(<DropdownMenuItem {...{ ...props, selected: true }} />);
|
|
@@ -10,16 +10,42 @@ const foo = { id: "1", level: 0, name: "foo", children: [bar] };
|
|
|
10
10
|
const options = [foo, bar, baz];
|
|
11
11
|
|
|
12
12
|
const renderOpts = {};
|
|
13
|
-
const onChange = jest.fn();
|
|
14
|
-
const props = { options, placeholder: "Select a domain", onChange };
|
|
15
13
|
|
|
16
14
|
describe("<TreeSelector />", () => {
|
|
17
15
|
it("matches latest snapshot", () => {
|
|
16
|
+
const props = { options, placeholder: "Select a domain" };
|
|
18
17
|
const { container } = render(<TreeSelector {...props} />, renderOpts);
|
|
19
18
|
expect(container).toMatchSnapshot();
|
|
20
19
|
});
|
|
21
20
|
|
|
22
|
-
it("calls onChange with selected values", async () => {
|
|
21
|
+
it("calls onChange with selected values (multiple)", async () => {
|
|
22
|
+
const props = {
|
|
23
|
+
onChange: jest.fn(),
|
|
24
|
+
options,
|
|
25
|
+
placeholder: "Select a domain",
|
|
26
|
+
};
|
|
27
|
+
const { getByText, getByRole } = render(
|
|
28
|
+
<TreeSelector multiple {...props} />,
|
|
29
|
+
renderOpts
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
userEvent.click(getByText("Select a domain"));
|
|
33
|
+
|
|
34
|
+
await waitFor(() => {
|
|
35
|
+
expect(getByRole("option", { name: /foo/i })).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
userEvent.click(getByRole("option", { name: /foo/i }));
|
|
39
|
+
expect(props.onChange.mock.calls.length).toBe(1);
|
|
40
|
+
expect(props.onChange.mock.calls[0][1]).toEqual({ value: ["1"] });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("calls onChange with selected values (single)", async () => {
|
|
44
|
+
const props = {
|
|
45
|
+
onChange: jest.fn(),
|
|
46
|
+
options,
|
|
47
|
+
placeholder: "Select a domain",
|
|
48
|
+
};
|
|
23
49
|
const { getByText, getByRole } = render(
|
|
24
50
|
<TreeSelector {...props} />,
|
|
25
51
|
renderOpts
|
|
@@ -32,7 +58,7 @@ describe("<TreeSelector />", () => {
|
|
|
32
58
|
});
|
|
33
59
|
|
|
34
60
|
userEvent.click(getByRole("option", { name: /foo/i }));
|
|
35
|
-
expect(onChange.mock.calls.length).toBe(1);
|
|
36
|
-
expect(onChange.mock.calls[0][1]).toEqual({ value:
|
|
61
|
+
expect(props.onChange.mock.calls.length).toBe(1);
|
|
62
|
+
expect(props.onChange.mock.calls[0][1]).toEqual({ value: "1" });
|
|
37
63
|
});
|
|
38
64
|
});
|
|
@@ -7,8 +7,8 @@ exports[`<TreeSelector /> matches latest snapshot 1`] = `
|
|
|
7
7
|
>
|
|
8
8
|
<div
|
|
9
9
|
aria-expanded="false"
|
|
10
|
-
aria-multiselectable="
|
|
11
|
-
class="ui floating
|
|
10
|
+
aria-multiselectable="false"
|
|
11
|
+
class="ui floating dropdown"
|
|
12
12
|
role="listbox"
|
|
13
13
|
tabindex="0"
|
|
14
14
|
>
|
|
@@ -7,7 +7,7 @@ describe("authJsonOpts", () => {
|
|
|
7
7
|
|
|
8
8
|
it("should include an authorization header with the bearer token", () => {
|
|
9
9
|
const token = "foo";
|
|
10
|
-
const
|
|
11
|
-
expect(authJsonOpts(token)).toMatchObject({ headers: {
|
|
10
|
+
const Authorization = `Bearer ${token}`;
|
|
11
|
+
expect(authJsonOpts(token)).toMatchObject({ headers: { Authorization } });
|
|
12
12
|
});
|
|
13
13
|
});
|
package/src/services/api.js
CHANGED
|
@@ -15,7 +15,7 @@ const UPLOAD_JSON_OPTS = {
|
|
|
15
15
|
|
|
16
16
|
const authJsonOpts = (token) =>
|
|
17
17
|
token
|
|
18
|
-
? { headers: { ...JSON_HEADERS,
|
|
18
|
+
? { headers: { ...JSON_HEADERS, Authorization: `Bearer ${token}` } }
|
|
19
19
|
: JSON_OPTS;
|
|
20
20
|
|
|
21
21
|
const apiJson = (url, opts) =>
|
package/src/services/axios.js
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
1
2
|
import axios from "axios";
|
|
2
|
-
import { readToken } from "./storage";
|
|
3
|
+
import { clearToken, readToken } from "./storage";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
// If no authorization header is present and token exists in local storage, put
|
|
6
|
+
// the bearer token authorization header
|
|
7
|
+
const maybePutAuthorization = (config) => {
|
|
8
|
+
if (config.headers["Authorization"] || config.headers["authorization"]) {
|
|
9
|
+
return config;
|
|
10
|
+
} else {
|
|
6
11
|
const token = readToken();
|
|
7
12
|
return token === null
|
|
8
|
-
?
|
|
13
|
+
? token
|
|
9
14
|
: {
|
|
10
15
|
...config,
|
|
11
16
|
headers: {
|
|
12
17
|
...config.headers,
|
|
13
|
-
|
|
18
|
+
authorization: `Bearer ${token}`,
|
|
14
19
|
},
|
|
15
20
|
};
|
|
16
|
-
},
|
|
17
|
-
function (err) {
|
|
18
|
-
return Promise.reject(err);
|
|
19
21
|
}
|
|
20
|
-
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// If an unauthorized response is received, remove token from local storage
|
|
25
|
+
const maybeClearToken = (error) => {
|
|
26
|
+
if (error?.response?.status === 401) {
|
|
27
|
+
clearToken();
|
|
28
|
+
}
|
|
29
|
+
return Promise.reject(error);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
axios.interceptors.request.use(maybePutAuthorization, Promise.reject);
|
|
33
|
+
axios.interceptors.response.use(_.identity, maybeClearToken);
|
|
21
34
|
|
|
22
35
|
export default axios;
|