@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 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.0",
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.19.2",
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": "7260126df6067fe59b11c5dc2b94dbdd881fed7b"
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 = ({ action, onLoad, value, ...props }) => {
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({ id: "domain.multiple.placeholder" })}
30
- value={_.map(_.toString)(value)}
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
- handleOpen,
10
- handleClick,
9
+ onOpen,
10
+ onClick,
11
11
  selected,
12
12
  open,
13
13
  name,
14
14
  level,
15
15
  }) => {
16
- const doHandleOpen = (e) => {
16
+ const handleOpen = (e) => {
17
17
  e && e.preventDefault();
18
18
  e && e.stopPropagation();
19
- handleOpen(id);
19
+ onOpen(id);
20
20
  };
21
21
 
22
- const doHandleClick = (e) => {
22
+ const handleClick = (e) => {
23
23
  e && e.preventDefault();
24
24
  e && e.stopPropagation();
25
- handleClick(e, id);
25
+ onClick(e, id);
26
26
  };
27
27
 
28
28
  return (
29
- <Dropdown.Item onClick={doHandleClick} selected={!check && selected}>
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={doHandleOpen} />
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
- handleOpen: PropTypes.func,
50
- handleClick: PropTypes.func,
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
- handleOpen={handleOpen}
176
- handleClick={handleClick}
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
- required = false,
58
+ options,
23
59
  placeholder,
24
- value: initialValue = [],
60
+ required = false,
61
+ value: initialValue,
25
62
  }) => {
26
- const [value, setValue] = useState(initialValue);
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 ids = _.includes(id)(value)
67
- ? _.without([id])(value)
68
- : _.union([id])(value);
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 = _.isEmpty(value) ? (
80
- <label>{placeholder}</label>
81
- ) : (
82
- _.map((id) => (
83
- <Label key={id}>
84
- {_.flow(_.find({ id }), _.prop("name"))(options)}
85
- <Icon
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
- handleOpen={handleOpen}
105
- handleClick={handleClick}
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
- multiple
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, { specialChars } from "@testing-library/user-event";
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 handleOpen = jest.fn();
10
- const handleClick = jest.fn();
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
- handleOpen,
18
- handleClick,
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(handleOpen).toHaveBeenCalledWith(id);
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(handleClick.mock.calls[0][1]).toBe(id);
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: ["1"] });
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="true"
11
- class="ui floating multiple dropdown"
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 authorization = `Bearer ${token}`;
11
- expect(authJsonOpts(token)).toMatchObject({ headers: { authorization } });
10
+ const Authorization = `Bearer ${token}`;
11
+ expect(authJsonOpts(token)).toMatchObject({ headers: { Authorization } });
12
12
  });
13
13
  });
@@ -15,7 +15,7 @@ const UPLOAD_JSON_OPTS = {
15
15
 
16
16
  const authJsonOpts = (token) =>
17
17
  token
18
- ? { headers: { ...JSON_HEADERS, authorization: `Bearer ${token}` } }
18
+ ? { headers: { ...JSON_HEADERS, Authorization: `Bearer ${token}` } }
19
19
  : JSON_OPTS;
20
20
 
21
21
  const apiJson = (url, opts) =>
@@ -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
- axios.interceptors.request.use(
5
- function (config) {
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
- ? config
13
+ ? token
9
14
  : {
10
15
  ...config,
11
16
  headers: {
12
17
  ...config.headers,
13
- Authorization: `Bearer ${token}`,
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;