@truedat/df 8.6.6 → 8.6.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/df",
3
- "version": "8.6.6",
3
+ "version": "8.6.7",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -51,14 +51,14 @@
51
51
  "@testing-library/jest-dom": "^6.6.3",
52
52
  "@testing-library/react": "^16.3.0",
53
53
  "@testing-library/user-event": "^14.6.1",
54
- "@truedat/test": "8.6.6",
54
+ "@truedat/test": "8.6.7",
55
55
  "identity-obj-proxy": "^3.0.0",
56
56
  "jest": "^29.7.0",
57
57
  "redux-saga-test-plan": "^4.0.6"
58
58
  },
59
59
  "dependencies": {
60
60
  "@apollo/client": "^3.13.8",
61
- "@truedat/core": "8.6.6",
61
+ "@truedat/core": "8.6.7",
62
62
  "axios": "^1.15.0",
63
63
  "graphql": "^16.11.0",
64
64
  "is-hotkey": "^0.2.0",
@@ -78,7 +78,7 @@
78
78
  "react-intl": "^7.1.11",
79
79
  "react-moment": "^1.1.3",
80
80
  "react-redux": "^9.2.0",
81
- "react-router": "^7.6.0",
81
+ "react-router": "^7.15.0",
82
82
  "react-sortable-hoc": "^2.0.0",
83
83
  "redux": "^5.0.1",
84
84
  "redux-saga": "^1.3.0",
@@ -87,5 +87,5 @@
87
87
  "semantic-ui-react": "^3.0.0-beta.2",
88
88
  "swr": "^2.3.3"
89
89
  },
90
- "gitHead": "8a7cc3d7598dde1a52abbb9933118f4a90155d42"
90
+ "gitHead": "17b10e045d4b90147c0f98ed5e781c20804024ba"
91
91
  }
@@ -5,7 +5,7 @@ import { Icon, List, Container } from "semantic-ui-react";
5
5
  import OriginLabel from "@truedat/core/components/OriginLabel";
6
6
  import DomainPreview from "./widgets/DomainPreview";
7
7
  import UserGroupPreview from "./widgets/UserGroupPreview";
8
- import { GroupPreview } from "./widgets/GroupPreview";
8
+ import GroupPreview from "./widgets/GroupPreview";
9
9
  import HierarchyPreview from "./widgets/HierarchyPreview";
10
10
  import SystemPreview from "./widgets/SystemPreview";
11
11
  import FieldViewerValue from "./FieldViewerValue";
@@ -23,6 +23,7 @@ export const DynamicFieldValue = ({
23
23
  value: null,
24
24
  origin: "default",
25
25
  };
26
+
26
27
  const { formatMessage } = useIntl();
27
28
  return (
28
29
  <List.Item>
@@ -55,7 +56,7 @@ export const DynamicFieldValue = ({
55
56
  ) : type == "domain" ? (
56
57
  <DomainPreview domainIds={_.castArray(value)} />
57
58
  ) : type == "group" ? (
58
- <GroupPreview groups={_.castArray(value)} />
59
+ <GroupPreview groupId={value} />
59
60
  ) : type == "user_group" ? (
60
61
  <UserGroupPreview userGroupId={value} />
61
62
  ) : type == "hierarchy" ? (
@@ -37,21 +37,26 @@ const DynamicTableField = (field) => {
37
37
  );
38
38
  };
39
39
 
40
- const groupNameOrAlias = (group) =>
41
- _.isString(group) ? group : group?.alias || group?.name;
42
-
43
40
  const stripGroupPrefix = (value) =>
44
41
  _.isString(value) && value.startsWith("group:")
45
42
  ? value.slice("group:".length)
46
43
  : value;
47
44
 
45
+ const groupLabel = (group) =>
46
+ _.isString(group) ? group : group?.alias || group?.name;
47
+
48
+ const groupMatchesName = (group, name) => {
49
+ if (_.isString(group)) return group === name;
50
+ return [group?.name, group?.alias].includes(name);
51
+ };
52
+
48
53
  const groupDisplayName = (rawName, values) => {
49
54
  const name = stripGroupPrefix(rawName);
50
- const group = _.find(values?.processed_groups, (group) =>
51
- [groupNameOrAlias(group), _.get(group, "name")].includes(name)
52
- );
55
+ const groups =
56
+ values?.processed_groups_details || values?.processed_groups || [];
57
+ const group = _.find(groups, (group) => groupMatchesName(group, name));
53
58
 
54
- return group ? groupNameOrAlias(group) : name;
59
+ return group ? groupLabel(group) : name;
55
60
  };
56
61
 
57
62
  const userGroupDisplayName = (value, values) => {
@@ -1,7 +1,22 @@
1
+ import { useQuery } from "@apollo/client";
1
2
  import { render } from "@truedat/test/render";
2
3
  import { DynamicFieldValue } from "../DynamicFieldValue";
3
4
 
5
+ jest.mock("@apollo/client", () => {
6
+ const originalModule = jest.requireActual("@apollo/client");
7
+
8
+ return {
9
+ __esModule: true,
10
+ ...originalModule,
11
+ useQuery: jest.fn(),
12
+ };
13
+ });
14
+
4
15
  describe("<DynamicFieldValue />", () => {
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ });
19
+
5
20
  it("matches the latest snapshot", () => {
6
21
  const props = {
7
22
  label: "label",
@@ -28,31 +43,50 @@ describe("<DynamicFieldValue />", () => {
28
43
  });
29
44
 
30
45
  it("renders a single group value with a group icon", () => {
46
+ useQuery.mockReturnValue({
47
+ loading: false,
48
+ error: undefined,
49
+ data: {
50
+ groupDetails: [{ id: 1, name: "group:foo bar" }],
51
+ },
52
+ });
53
+
31
54
  const rendered = render(
32
55
  <DynamicFieldValue
33
56
  label="label"
34
- value={{ value: "Data Owners", origin: "user" }}
57
+ value={{ value: "foo bar", origin: "user" }}
35
58
  type="group"
36
59
  widget="dropdown"
37
60
  />
38
61
  );
39
62
 
40
- expect(rendered.getByText(/data owners/i)).toBeInTheDocument();
63
+ expect(rendered.getByText(/foo bar/i)).toBeInTheDocument();
41
64
  expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
42
65
  });
43
66
 
44
67
  it("renders multiple group values with a group icon each", () => {
68
+ useQuery.mockReturnValue({
69
+ loading: false,
70
+ error: undefined,
71
+ data: {
72
+ groupDetails: [
73
+ { id: 1, name: "group:foo bar" },
74
+ { id: 2, name: "group:bar baz" },
75
+ ],
76
+ },
77
+ });
78
+
45
79
  const rendered = render(
46
80
  <DynamicFieldValue
47
81
  label="label"
48
- value={{ value: ["Data Owners", "Quality"], origin: "user" }}
82
+ value={{ value: ["foo bar", "bar baz"], origin: "user" }}
49
83
  type="group"
50
84
  widget="checkbox"
51
85
  />
52
86
  );
53
87
 
54
- expect(rendered.getByText(/data owners/i)).toBeInTheDocument();
55
- expect(rendered.getByText(/quality/i)).toBeInTheDocument();
88
+ expect(rendered.getByText(/foo bar/i)).toBeInTheDocument();
89
+ expect(rendered.getByText(/bar baz/i)).toBeInTheDocument();
56
90
  expect(rendered.container.querySelectorAll(".group.icon")).toHaveLength(2);
57
91
  });
58
92
  });
@@ -1,3 +1,4 @@
1
+ import { useQuery } from "@apollo/client";
1
2
  import { render } from "@truedat/test/render";
2
3
  import { useTemplate } from "@truedat/core/hooks";
3
4
  import { markdownToHtml } from "@truedat/core/components/Markdown";
@@ -5,6 +6,16 @@ import DynamicFormViewerFetcher, {
5
6
  DynamicFormViewer,
6
7
  } from "../DynamicFormViewer";
7
8
 
9
+ jest.mock("@apollo/client", () => {
10
+ const originalModule = jest.requireActual("@apollo/client");
11
+
12
+ return {
13
+ __esModule: true,
14
+ ...originalModule,
15
+ useQuery: jest.fn(),
16
+ };
17
+ });
18
+
8
19
  jest.mock("@truedat/core/components/Loading", () => ({
9
20
  __esModule: true,
10
21
  default: () => <div data-testid="loading" />,
@@ -20,16 +31,6 @@ jest.mock("@truedat/core/hooks", () => {
20
31
  };
21
32
  });
22
33
 
23
- jest.mock("../widgets/GroupPreview", () => ({
24
- __esModule: true,
25
- GroupPreview: ({ groups }) => (
26
- <div data-testid="group-preview">{[].concat(groups).join(",")}</div>
27
- ),
28
- default: ({ groupId }) => (
29
- <div data-testid="group-preview">{[].concat(groupId).join(",")}</div>
30
- ),
31
- }));
32
-
33
34
  const groupTemplate = {
34
35
  scope: "bg",
35
36
  content: [
@@ -50,7 +51,7 @@ const groupTemplate = {
50
51
 
51
52
  const groupContent = {
52
53
  "group-field": {
53
- value: ["Group Alias"],
54
+ value: ["foo bar"],
54
55
  origin: "user",
55
56
  },
56
57
  };
@@ -75,6 +76,13 @@ const unnamedGroupTemplate = {
75
76
  describe("<DynamicFormViewer />", () => {
76
77
  beforeEach(() => {
77
78
  jest.clearAllMocks();
79
+ useQuery.mockReturnValue({
80
+ loading: false,
81
+ error: undefined,
82
+ data: {
83
+ groupDetails: [{ id: 1, name: "group:foo bar" }],
84
+ },
85
+ });
78
86
  useTemplate.mockReturnValue({
79
87
  data: {},
80
88
  loading: false,
@@ -121,9 +129,8 @@ describe("<DynamicFormViewer />", () => {
121
129
  <DynamicFormViewer template={groupTemplate} content={groupContent} />
122
130
  );
123
131
 
124
- expect(rendered.getByTestId("group-preview")).toHaveTextContent(
125
- "Group Alias"
126
- );
132
+ expect(rendered.getByText(/foo bar/i)).toBeInTheDocument();
133
+ expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
127
134
  });
128
135
 
129
136
  it("renders groups without heading when the group name is missing", () => {
@@ -134,9 +141,8 @@ describe("<DynamicFormViewer />", () => {
134
141
  />
135
142
  );
136
143
 
137
- expect(rendered.getByTestId("group-preview")).toHaveTextContent(
138
- "Group Alias"
139
- );
144
+ expect(rendered.getByText(/foo bar/i)).toBeInTheDocument();
145
+ expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
140
146
  expect(rendered.container.querySelector("h3, h4")).not.toBeInTheDocument();
141
147
  });
142
148
 
@@ -157,9 +163,8 @@ describe("<DynamicFormViewer />", () => {
157
163
  );
158
164
 
159
165
  expect(useTemplate).not.toHaveBeenCalled();
160
- expect(rendered.getByTestId("group-preview")).toHaveTextContent(
161
- "Group Alias"
162
- );
166
+ expect(rendered.getByText(/foo bar/i)).toBeInTheDocument();
167
+ expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
163
168
  });
164
169
 
165
170
  it("renders a loader while fetching string templates", () => {
@@ -197,8 +202,7 @@ describe("<DynamicFormViewer />", () => {
197
202
  name: "group-template",
198
203
  domainIds: [1],
199
204
  });
200
- expect(rendered.getByTestId("group-preview")).toHaveTextContent(
201
- "Group Alias"
202
- );
205
+ expect(rendered.getByText(/foo bar/i)).toBeInTheDocument();
206
+ expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
203
207
  });
204
208
  });
@@ -17,6 +17,9 @@ const groupDisplayName = (group) => stripGroupPrefix(groupName(group));
17
17
  const isUserValue = (group) =>
18
18
  _.isString(groupName(group)) && groupName(group).startsWith("user:");
19
19
 
20
+ const normalizeGroupId = (value) =>
21
+ value.startsWith("group:") ? value : `group:${value}`;
22
+
20
23
  export const GroupLabel = ({ value }) => (
21
24
  <Label>
22
25
  <Icon name="group" />
@@ -30,7 +33,7 @@ GroupLabel.propTypes = {
30
33
 
31
34
  export const LinkedGroupLabel = ({ group }) => {
32
35
  const id = groupId(group);
33
- const label = <GroupLabel value={groupName(group)} />;
36
+ const label = <GroupLabel value={groupDisplayName(group)} />;
34
37
 
35
38
  return id ? <Link to={linkTo.GROUP({ id })}>{label}</Link> : label;
36
39
  };
@@ -53,7 +56,7 @@ export const GroupPreview = ({ groups }) =>
53
56
  key={groupId(group) || groupDisplayName(group)}
54
57
  group={group}
55
58
  />
56
- )),
59
+ ))
57
60
  )(groups);
58
61
 
59
62
  GroupPreview.propTypes = {
@@ -65,13 +68,13 @@ GroupPreview.propTypes = {
65
68
  name: PropTypes.string.isRequired,
66
69
  }),
67
70
  PropTypes.string,
68
- ]),
71
+ ])
69
72
  ).isRequired,
70
73
  };
71
74
 
72
75
  export const GroupPreviewLoader = ({ groupId }) => {
73
76
  const { loading, error, data } = useQuery(GROUP_PREVIEW_QUERY, {
74
- variables: { ids: _.castArray(groupId) },
77
+ variables: { ids: _.map(normalizeGroupId, _.castArray(groupId)) },
75
78
  });
76
79
  if (error) return null;
77
80
  return loading ? (
@@ -17,11 +17,19 @@ const userGroupName = (userGroup) =>
17
17
  const userGroupId = (userGroup) =>
18
18
  _.isString(userGroup) ? null : userGroup.id;
19
19
 
20
+ const userGroupDisplayName = (userGroup) =>
21
+ typeAndName(userGroupName(userGroup)).name;
22
+
20
23
  const isGroup = (userGroup) => userGroupName(userGroup)?.startsWith("group:");
21
24
 
25
+ const normalizeUserGroupId = (value) => {
26
+ if (!_.isString(value)) return value;
27
+ if (value.startsWith("user:") || value.startsWith("group:")) return value;
28
+ return `group:${value}`;
29
+ };
30
+
22
31
  export const UserGroupLabel = ({ value }) => {
23
32
  const { type, name } = typeAndName(value);
24
-
25
33
  return (
26
34
  <Label>
27
35
  <Icon name={type} />
@@ -58,7 +66,7 @@ LinkedUserGroupLabel.propTypes = {
58
66
  export const UserGroupPreview = ({ userGroups }) =>
59
67
  _.map((userGroup) => (
60
68
  <LinkedUserGroupLabel
61
- key={userGroupId(userGroup) || userGroupName(userGroup)}
69
+ key={userGroupId(userGroup) || userGroupDisplayName(userGroup)}
62
70
  userGroup={userGroup}
63
71
  />
64
72
  ))(userGroups);
@@ -71,13 +79,13 @@ UserGroupPreview.propTypes = {
71
79
  name: PropTypes.string.isRequired,
72
80
  }),
73
81
  PropTypes.string,
74
- ]),
82
+ ])
75
83
  ).isRequired,
76
84
  };
77
85
 
78
86
  export const UserGroupPreviewLoader = ({ userGroupId }) => {
79
87
  const { loading, error, data } = useQuery(USER_GROUP_PREVIEW_QUERY, {
80
- variables: { ids: _.castArray(userGroupId) },
88
+ variables: { ids: _.map(normalizeUserGroupId, _.castArray(userGroupId)) },
81
89
  });
82
90
  if (error) return null;
83
91
  return loading ? (
@@ -20,36 +20,36 @@ describe("<GroupPreview />", () => {
20
20
 
21
21
  it("renders group values with a group icon", () => {
22
22
  const rendered = render(
23
- <GroupPreview groups={[{ id: 123, name: "Group Alias" }]} />
23
+ <GroupPreview groups={[{ id: 123, name: "foo bar" }]} />
24
24
  );
25
25
 
26
- expect(rendered.container.textContent).toBe("Group Alias");
26
+ expect(rendered.container.textContent).toBe("foo bar");
27
27
  expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
28
- expect(
29
- rendered.getByRole("link", { name: /group alias/i })
30
- ).toHaveAttribute("href", "/groups/123");
28
+ expect(rendered.getByRole("link", { name: /foo bar/i })).toHaveAttribute(
29
+ "href",
30
+ "/groups/123"
31
+ );
31
32
  });
32
33
 
33
34
  it("strips the legacy group: prefix when rendering names", () => {
34
35
  const rendered = render(
35
- <GroupPreview groups={[{ id: 123, name: "group:Group Alias" }]} />
36
+ <GroupPreview groups={[{ id: 123, name: "group:foo bar" }]} />
36
37
  );
37
38
 
38
- expect(rendered.container.textContent).toBe("Group Alias");
39
+ expect(rendered.container.textContent).toBe("foo bar");
39
40
  expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
40
- expect(
41
- rendered.getByRole("link", { name: /group alias/i })
42
- ).toHaveAttribute("href", "/groups/123");
41
+ expect(rendered.getByRole("link", { name: /foo bar/i })).toHaveAttribute(
42
+ "href",
43
+ "/groups/123"
44
+ );
43
45
  });
44
46
 
45
47
  it("does not render user values", () => {
46
48
  const rendered = render(
47
- <GroupPreview
48
- groups={["user:John Doe", { id: 123, name: "Group Alias" }]}
49
- />
49
+ <GroupPreview groups={["user:baz qux", { id: 123, name: "foo bar" }]} />
50
50
  );
51
51
 
52
- expect(rendered.container.textContent).toBe("Group Alias");
52
+ expect(rendered.container.textContent).toBe("foo bar");
53
53
  expect(
54
54
  rendered.container.querySelector(".user.icon")
55
55
  ).not.toBeInTheDocument();
@@ -57,14 +57,22 @@ describe("<GroupPreview />", () => {
57
57
  });
58
58
 
59
59
  it("renders string groups without a link when there is no group id", () => {
60
- const rendered = render(
61
- <GroupPreview groups={["Standalone Group"]} />
62
- );
60
+ const rendered = render(<GroupPreview groups={["lone foo bar"]} />);
61
+
62
+ expect(rendered.container.textContent).toBe("lone foo bar");
63
+ expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
64
+ expect(
65
+ rendered.queryByRole("link", { name: /lone foo bar/i })
66
+ ).not.toBeInTheDocument();
67
+ });
63
68
 
64
- expect(rendered.container.textContent).toBe("Standalone Group");
69
+ it("strips the legacy group: prefix from string values stored in the database", () => {
70
+ const rendered = render(<GroupPreview groups={["group:foo bar"]} />);
71
+
72
+ expect(rendered.container.textContent).toBe("foo bar");
65
73
  expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
66
74
  expect(
67
- rendered.queryByRole("link", { name: /standalone group/i })
75
+ rendered.queryByRole("link", { name: /foo bar/i })
68
76
  ).not.toBeInTheDocument();
69
77
  });
70
78
 
@@ -75,10 +83,10 @@ describe("<GroupPreview />", () => {
75
83
  data: undefined,
76
84
  });
77
85
 
78
- const rendered = render(<GroupPreviewLoader groupId="123" />);
86
+ const rendered = render(<GroupPreviewLoader groupId="foo bar" />);
79
87
 
80
88
  expect(useQuery).toHaveBeenCalledWith(GROUP_PREVIEW_QUERY, {
81
- variables: { ids: ["123"] },
89
+ variables: { ids: ["group:foo bar"] },
82
90
  });
83
91
  expect(rendered.container.firstChild).toBeNull();
84
92
  });
@@ -90,28 +98,73 @@ describe("<GroupPreview />", () => {
90
98
  data: undefined,
91
99
  });
92
100
 
93
- const rendered = render(<GroupPreviewLoader groupId={["123"]} />);
101
+ const rendered = render(<GroupPreviewLoader groupId={["bar baz"]} />);
94
102
 
95
103
  expect(useQuery).toHaveBeenCalledWith(GROUP_PREVIEW_QUERY, {
96
- variables: { ids: ["123"] },
104
+ variables: { ids: ["group:bar baz"] },
97
105
  });
98
106
  expect(rendered.getByText(/\.\.\./i)).toBeInTheDocument();
99
107
  });
100
108
 
109
+ it("normalizes unprefixed group ids before querying group details", () => {
110
+ useQuery.mockReturnValue({
111
+ loading: false,
112
+ error: undefined,
113
+ data: {
114
+ groupDetails: [{ id: 123, name: "group:foo bar" }],
115
+ },
116
+ });
117
+
118
+ const rendered = render(<GroupPreviewLoader groupId="foo bar" />);
119
+
120
+ expect(useQuery).toHaveBeenCalledWith(GROUP_PREVIEW_QUERY, {
121
+ variables: { ids: ["group:foo bar"] },
122
+ });
123
+ expect(rendered.getByRole("link", { name: /foo bar/i })).toHaveAttribute(
124
+ "href",
125
+ "/groups/123"
126
+ );
127
+ });
128
+
129
+ it("keeps legacy group: prefixed values when querying group details", () => {
130
+ useQuery.mockReturnValue({
131
+ loading: false,
132
+ error: undefined,
133
+ data: {
134
+ groupDetails: [{ id: 123, name: "group:foo bar" }],
135
+ },
136
+ });
137
+
138
+ const rendered = render(<GroupPreviewLoader groupId="group:foo bar" />);
139
+
140
+ expect(useQuery).toHaveBeenCalledWith(GROUP_PREVIEW_QUERY, {
141
+ variables: { ids: ["group:foo bar"] },
142
+ });
143
+ expect(rendered.getByRole("link", { name: /foo bar/i })).toHaveAttribute(
144
+ "href",
145
+ "/groups/123"
146
+ );
147
+ });
148
+
101
149
  it("renders the fetched group preview when loading finishes", () => {
102
150
  useQuery.mockReturnValue({
103
151
  loading: false,
104
152
  error: undefined,
105
153
  data: {
106
- groupDetails: [{ id: 123, name: "Group Alias" }],
154
+ groupDetails: [{ id: 123, name: "foo bar" }],
107
155
  },
108
156
  });
109
157
 
110
- const rendered = render(<GroupPreviewLoader groupId="123" />);
158
+ const rendered = render(<GroupPreviewLoader groupId="foo bar" />);
111
159
 
112
- expect(
113
- rendered.getByRole("link", { name: /group alias/i })
114
- ).toHaveAttribute("href", "/groups/123");
160
+ expect(useQuery).toHaveBeenCalledWith(GROUP_PREVIEW_QUERY, {
161
+ variables: { ids: ["group:foo bar"] },
162
+ });
163
+
164
+ expect(rendered.getByRole("link", { name: /foo bar/i })).toHaveAttribute(
165
+ "href",
166
+ "/groups/123"
167
+ );
115
168
  expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
116
169
  });
117
170
  });
@@ -20,10 +20,10 @@ describe("<UserGroupPreview />", () => {
20
20
 
21
21
  it("renders users and groups with icons instead of prefixes", () => {
22
22
  const rendered = render(
23
- <UserGroupPreview userGroups={["user:John Doe", "group:Group Alias"]} />,
23
+ <UserGroupPreview userGroups={["user:baz qux", "group:foo bar"]} />
24
24
  );
25
25
 
26
- expect(rendered.container.textContent).toBe("John DoeGroup Alias");
26
+ expect(rendered.container.textContent).toBe("baz quxfoo bar");
27
27
  expect(rendered.container.querySelector(".user.icon")).toBeInTheDocument();
28
28
  expect(rendered.container.querySelector(".group.icon")).toBeInTheDocument();
29
29
  });
@@ -32,16 +32,16 @@ describe("<UserGroupPreview />", () => {
32
32
  const rendered = render(
33
33
  <UserGroupPreview
34
34
  userGroups={[
35
- { name: "user:John Doe" },
36
- { id: 123, name: "group:Group Alias" },
35
+ { name: "user:baz qux" },
36
+ { id: 123, name: "group:foo bar" },
37
37
  ]}
38
- />,
38
+ />
39
39
  );
40
40
 
41
- expect(rendered.getByText(/john doe/i).closest("a")).toBeNull();
42
- expect(rendered.getByRole("link", { name: /group alias/i })).toHaveAttribute(
41
+ expect(rendered.getByText(/baz qux/i).closest("a")).toBeNull();
42
+ expect(rendered.getByRole("link", { name: /foo bar/i })).toHaveAttribute(
43
43
  "href",
44
- "/groups/123",
44
+ "/groups/123"
45
45
  );
46
46
  });
47
47
 
@@ -52,10 +52,12 @@ describe("<UserGroupPreview />", () => {
52
52
  data: undefined,
53
53
  });
54
54
 
55
- const rendered = render(<UserGroupPreviewLoader userGroupId="123" />);
55
+ const rendered = render(
56
+ <UserGroupPreviewLoader userGroupId="group:foo bar" />
57
+ );
56
58
 
57
59
  expect(useQuery).toHaveBeenCalledWith(USER_GROUP_PREVIEW_QUERY, {
58
- variables: { ids: ["123"] },
60
+ variables: { ids: ["group:foo bar"] },
59
61
  });
60
62
  expect(rendered.container.firstChild).toBeNull();
61
63
  });
@@ -67,32 +69,66 @@ describe("<UserGroupPreview />", () => {
67
69
  data: undefined,
68
70
  });
69
71
 
70
- const rendered = render(<UserGroupPreviewLoader userGroupId={["123"]} />);
72
+ const rendered = render(
73
+ <UserGroupPreviewLoader userGroupId={["user:baz qux", "bar baz"]} />
74
+ );
71
75
 
72
76
  expect(useQuery).toHaveBeenCalledWith(USER_GROUP_PREVIEW_QUERY, {
73
- variables: { ids: ["123"] },
77
+ variables: { ids: ["user:baz qux", "group:bar baz"] },
74
78
  });
75
79
  expect(rendered.getByText(/\.\.\./i)).toBeInTheDocument();
76
80
  });
77
81
 
82
+ it("keeps user: and group: prefixes when querying user group details", () => {
83
+ useQuery.mockReturnValue({
84
+ loading: true,
85
+ error: undefined,
86
+ data: undefined,
87
+ });
88
+
89
+ render(
90
+ <UserGroupPreviewLoader userGroupId={["user:baz qux", "group:foo bar"]} />
91
+ );
92
+
93
+ expect(useQuery).toHaveBeenCalledWith(USER_GROUP_PREVIEW_QUERY, {
94
+ variables: { ids: ["user:baz qux", "group:foo bar"] },
95
+ });
96
+ });
97
+
98
+ it("renders group alias from userGroupDetails API response", () => {
99
+ const rendered = render(
100
+ <UserGroupPreview
101
+ userGroups={[{ id: "9", name: "group:foo bar alias" }]}
102
+ />
103
+ );
104
+
105
+ expect(rendered.getByText(/foo bar alias/i)).toBeInTheDocument();
106
+ expect(
107
+ rendered.getByRole("link", { name: /foo bar alias/i })
108
+ ).toHaveAttribute("href", "/groups/9");
109
+ expect(rendered.container.textContent).not.toContain("group:");
110
+ });
111
+
78
112
  it("renders the fetched user groups when loading finishes", () => {
79
113
  useQuery.mockReturnValue({
80
114
  loading: false,
81
115
  error: undefined,
82
116
  data: {
83
117
  userGroupDetails: [
84
- { name: "user:John Doe" },
85
- { id: 123, name: "group:Group Alias" },
118
+ { name: "user:baz qux" },
119
+ { id: 123, name: "group:foo bar" },
86
120
  ],
87
121
  },
88
122
  });
89
123
 
90
- const rendered = render(<UserGroupPreviewLoader userGroupId="123" />);
124
+ const rendered = render(
125
+ <UserGroupPreviewLoader userGroupId={["user:baz qux", "group:foo bar"]} />
126
+ );
91
127
 
92
- expect(rendered.getByText(/john doe/i).closest("a")).toBeNull();
93
- expect(rendered.getByRole("link", { name: /group alias/i })).toHaveAttribute(
128
+ expect(rendered.getByText(/baz qux/i).closest("a")).toBeNull();
129
+ expect(rendered.getByRole("link", { name: /foo bar/i })).toHaveAttribute(
94
130
  "href",
95
- "/groups/123",
131
+ "/groups/123"
96
132
  );
97
133
  });
98
134
  });
@@ -44,7 +44,7 @@ describe("utils: validValues", () => {
44
44
  });
45
45
  });
46
46
 
47
- it("should validate processed_groups values", () => {
47
+ it("should validate user_group values by canonical group name", () => {
48
48
  const template = [
49
49
  {
50
50
  name: "role",
@@ -52,21 +52,80 @@ describe("utils: validValues", () => {
52
52
  values: {
53
53
  role_groups: "some_role",
54
54
  processed_users: ["u1"],
55
- processed_groups: ["alias1", "group2"],
55
+ processed_groups_details: [
56
+ { id: 1, name: "group one", alias: "alias1" },
57
+ { id: 2, name: "group2", alias: null },
58
+ ],
56
59
  },
57
60
  },
58
61
  ];
59
62
 
60
63
  const result = filterValues(template)({
61
- role: { value: "group:alias1", origin: "default" },
64
+ role: { value: "group:group one", origin: "default" },
62
65
  });
63
66
 
64
67
  expect(result).toEqual({
65
- role: { value: "group:alias1", origin: "default" },
68
+ role: { value: "group:group one", origin: "default" },
66
69
  });
67
70
  });
68
71
 
69
- it("should validate group values without allowing users", () => {
72
+ it("should validate legacy names when processed_groups_details are present", () => {
73
+ const template = [
74
+ {
75
+ name: "role",
76
+ type: "user_group",
77
+ values: {
78
+ role_groups: "some_role",
79
+ processed_users: ["u1"],
80
+ processed_groups: ["alias1"],
81
+ processed_groups_details: [
82
+ {
83
+ id: 1,
84
+ alias: "alias1",
85
+ name: "group1",
86
+ },
87
+ ],
88
+ },
89
+ },
90
+ ];
91
+
92
+ const result = filterValues(template)({
93
+ role: { value: "group:group1", origin: "default" },
94
+ });
95
+
96
+ expect(result).toEqual({
97
+ role: { value: "group:group1", origin: "default" },
98
+ });
99
+ });
100
+
101
+ it("should drop stale alias values from user_group fields on save", () => {
102
+ const template = [
103
+ {
104
+ name: "role",
105
+ type: "user_group",
106
+ values: {
107
+ role_groups: "some_role",
108
+ processed_users: ["u1"],
109
+ processed_groups_details: [
110
+ { id: 1, name: "bar baz", alias: "foo bar alias" },
111
+ ],
112
+ },
113
+ },
114
+ ];
115
+
116
+ const result = filterValues(template)({
117
+ role: {
118
+ value: ["group:bar baz", "group:foo bar alias", "group:qux invalid"],
119
+ origin: "default",
120
+ },
121
+ });
122
+
123
+ expect(result).toEqual({
124
+ role: { value: ["group:bar baz"], origin: "default" },
125
+ });
126
+ });
127
+
128
+ it("should validate group values by canonical name without allowing users", () => {
70
129
  const template = [
71
130
  {
72
131
  name: "role",
@@ -74,31 +133,42 @@ describe("utils: validValues", () => {
74
133
  values: {
75
134
  role_groups: "some_role",
76
135
  processed_users: ["u1"],
77
- processed_groups: ["alias1", "group2"],
136
+ processed_groups_details: [
137
+ { id: 1, name: "foo bar", alias: "alias1" },
138
+ { id: 2, name: "group2", alias: null },
139
+ ],
78
140
  },
79
141
  },
80
142
  ];
81
143
 
82
144
  expect(
83
145
  filterValues(template)({
84
- role: { value: "alias1", origin: "default" },
85
- }),
146
+ role: { value: "foo bar", origin: "default" },
147
+ })
86
148
  ).toEqual({
87
- role: { value: "alias1", origin: "default" },
149
+ role: { value: "foo bar", origin: "default" },
88
150
  });
89
151
 
90
152
  expect(
91
153
  filterValues(template)({
92
154
  role: { value: "user:u1", origin: "default" },
93
- }),
155
+ })
156
+ ).toEqual({
157
+ role: { value: null, origin: "default" },
158
+ });
159
+
160
+ expect(
161
+ filterValues(template)({
162
+ role: { value: "alias1", origin: "default" },
163
+ })
94
164
  ).toEqual({
95
165
  role: { value: null, origin: "default" },
96
166
  });
97
167
 
98
168
  expect(
99
169
  filterValues(template)({
100
- role: { value: "group:alias1", origin: "default" },
101
- }),
170
+ role: { value: "group:foo bar", origin: "default" },
171
+ })
102
172
  ).toEqual({
103
173
  role: { value: null, origin: "default" },
104
174
  });
@@ -150,6 +150,108 @@ describe("utils: parseFieldOptions", () => {
150
150
  });
151
151
  });
152
152
 
153
+ it("normalizes legacy prefixed values for group fields", () => {
154
+ const field = {
155
+ name: "field",
156
+ type: "group",
157
+ values: {
158
+ role_groups: "Some Role",
159
+ processed_groups: [{ alias: "alias1", name: "group1" }],
160
+ },
161
+ };
162
+ const content = { field: { value: "group:group1", origin: "default" } };
163
+ const result = parseFormatFieldOptions(content)(field);
164
+ expect(result).toEqual({
165
+ ...field,
166
+ value: { value: "group1", origin: "default" },
167
+ parsedValues: [{ value: "group1", text: "alias1", icon: "group" }],
168
+ });
169
+ });
170
+
171
+ it("normalizes legacy group names to canonical names for user_group fields", () => {
172
+ const field = {
173
+ name: "field",
174
+ type: "user_group",
175
+ values: {
176
+ role_groups: "Some Role",
177
+ processed_users: ["x"],
178
+ processed_groups: [{ alias: "alias1", name: "group1" }],
179
+ },
180
+ };
181
+ const content = { field: { value: "group:group1", origin: "default" } };
182
+ const result = parseFormatFieldOptions(content)(field);
183
+ expect(result).toEqual({
184
+ ...field,
185
+ value: { value: "group:group1", origin: "default" },
186
+ parsedValues: [
187
+ { value: "user:x", text: "x", icon: "user" },
188
+ { value: "group:group1", text: "alias1", icon: "group" },
189
+ ],
190
+ });
191
+ });
192
+
193
+ it("maps legacy group values to canonical names without extra options", () => {
194
+ const field = {
195
+ name: "field",
196
+ type: "user_group",
197
+ values: {
198
+ role_groups: "Some Role",
199
+ processed_users: ["x"],
200
+ processed_groups: ["alias1", "foo bar alias"],
201
+ processed_groups_details: [
202
+ {
203
+ id: 1,
204
+ alias: "alias1",
205
+ name: "group1",
206
+ },
207
+ {
208
+ id: 2,
209
+ alias: "foo bar alias",
210
+ name: "bar baz",
211
+ },
212
+ ],
213
+ },
214
+ };
215
+ const content = {
216
+ field: { value: "group:bar baz", origin: "default" },
217
+ };
218
+ const result = parseFormatFieldOptions(content)(field);
219
+ expect(result).toEqual({
220
+ ...field,
221
+ value: { value: "group:bar baz", origin: "default" },
222
+ parsedValues: [
223
+ { value: "user:x", text: "x", icon: "user" },
224
+ { value: "group:group1", text: "alias1", icon: "group" },
225
+ {
226
+ value: "group:bar baz",
227
+ text: "foo bar alias",
228
+ icon: "group",
229
+ },
230
+ ],
231
+ });
232
+ });
233
+
234
+ it("normalizes legacy user prefixed values for user fields", () => {
235
+ const field = {
236
+ name: "field",
237
+ type: "user",
238
+ values: {
239
+ role_users: "Some Role",
240
+ processed_users: ["user1", "user2"],
241
+ },
242
+ };
243
+ const content = { field: { value: "user:user2", origin: "default" } };
244
+ const result = parseFormatFieldOptions(content)(field);
245
+ expect(result).toEqual({
246
+ ...field,
247
+ value: { value: "user2", origin: "default" },
248
+ parsedValues: [
249
+ { text: "user1", value: "user1" },
250
+ { text: "user2", value: "user2" },
251
+ ],
252
+ });
253
+ });
254
+
153
255
  it("handles values domain", () => {
154
256
  const selectedDomain = { id: 1 };
155
257
  const field = {
@@ -1,27 +1,35 @@
1
1
  import _ from "lodash/fp";
2
2
 
3
- const groupNameOrAlias = (group) =>
4
- _.isString(group) ? group : group?.alias || group?.name;
3
+ const groupCanonicalName = (group) =>
4
+ _.isString(group) ? group : group?.name;
5
5
 
6
- const userGroupValues = (field) =>
6
+ const processedGroups = (values = {}) => {
7
+ const details = _.get("processed_groups_details")(values);
8
+ return _.isArray(details) && !_.isEmpty(details)
9
+ ? details
10
+ : _.get("processed_groups")(values);
11
+ };
12
+
13
+ const userGroupValues = (values) =>
7
14
  _.concat(
8
15
  _.flow(
9
16
  _.get("processed_users"),
10
17
  _.map((name) => `user:${name}`),
11
- )(field),
18
+ )(values),
12
19
  _.flow(
13
- _.get("processed_groups"),
14
- _.map((group) => {
15
- const name = groupNameOrAlias(group);
16
- return `group:${name}`;
17
- }),
18
- )(field),
20
+ () => processedGroups(values),
21
+ _.map((group) => `group:${groupCanonicalName(group)}`),
22
+ _.compact,
23
+ )(),
19
24
  );
20
25
 
21
- const groupValues = _.flow(
22
- _.get("processed_groups"),
23
- _.map(groupNameOrAlias),
24
- );
26
+ const groupValues = (values) =>
27
+ _.flow(
28
+ () => processedGroups(values),
29
+ _.map(groupCanonicalName),
30
+ _.compact,
31
+ _.uniq,
32
+ )();
25
33
 
26
34
  export const validValues = _.flow(
27
35
  _.filter(
@@ -29,6 +37,7 @@ export const validValues = _.flow(
29
37
  _.has("values.fixed"),
30
38
  _.has("values.processed_users"),
31
39
  _.has("values.processed_groups"),
40
+ _.has("values.processed_groups_details"),
32
41
  ]),
33
42
  ),
34
43
  _.map(_.pick(["name", "type", "values"])),
@@ -37,10 +46,18 @@ export const validValues = _.flow(
37
46
  _.cond([
38
47
  [_.has("fixed"), _.pathOr([], "fixed")],
39
48
  [
40
- () => type === "group" && _.has("processed_groups")(values),
41
- groupValues,
49
+ () =>
50
+ type === "group" &&
51
+ (_.has("processed_groups", values) ||
52
+ _.has("processed_groups_details", values)),
53
+ () => groupValues(values),
54
+ ],
55
+ [
56
+ () =>
57
+ _.has("processed_groups", values) ||
58
+ _.has("processed_groups_details", values),
59
+ () => userGroupValues(values),
42
60
  ],
43
- [_.has("processed_groups"), userGroupValues],
44
61
  [_.has("processed_users"), _.pathOr([], "processed_users")],
45
62
  ])(values),
46
63
  ]),
@@ -49,14 +66,22 @@ export const validValues = _.flow(
49
66
 
50
67
  export const filterValues = (templateContent) => {
51
68
  const vv = validValues(templateContent);
69
+ const fieldTypeByName = _.flow(
70
+ _.map(({ name, type }) => [name, type]),
71
+ _.fromPairs,
72
+ )(templateContent);
73
+
74
+ const isValidFieldValue = (fieldName, candidate) =>
75
+ _.includes(candidate)(_.prop(fieldName)(vv));
76
+
52
77
  return _.mapValues.convert({ cap: false })((value, key) =>
53
78
  _.has(key)(vv)
54
79
  ? _.isArray(value.value)
55
80
  ? _.set(
56
81
  "value",
57
- _.filter((v) => _.includes(v)(_.prop(key)(vv)))(value.value),
82
+ _.filter((v) => isValidFieldValue(key, v))(value.value),
58
83
  )(value)
59
- : _.includes(value.value)(_.prop(key)(vv))
84
+ : isValidFieldValue(key, value.value)
60
85
  ? value
61
86
  : { ...value, value: null }
62
87
  : value,
@@ -7,6 +7,99 @@ const makeOptions = (formatMessage, label) =>
7
7
  text: formatMessage({ id: `fields.${label}.${v}`, defaultMessage: v }),
8
8
  }));
9
9
 
10
+ const stripUserPrefix = (value) =>
11
+ typeof value === "string" && value.startsWith("user:")
12
+ ? value.slice("user:".length)
13
+ : value;
14
+
15
+ const stripGroupPrefix = (value) =>
16
+ typeof value === "string" && value.startsWith("group:")
17
+ ? value.slice("group:".length)
18
+ : value;
19
+
20
+ const groupCanonicalName = (group) =>
21
+ typeof group === "string" ? group : group.name;
22
+
23
+ const groupDisplayName = (group) =>
24
+ typeof group === "string" ? group : group.alias || group.name;
25
+
26
+ const groupNameCandidates = (group) =>
27
+ typeof group === "string"
28
+ ? [group]
29
+ : _.flow(_.at(["alias", "name"]), _.compact, _.uniq)(group);
30
+
31
+ const groupEntries = (values = {}) => {
32
+ const details = _.get("processed_groups_details")(values);
33
+ if (_.isArray(details) && !_.isEmpty(details)) return details;
34
+ return _.getOr([], "processed_groups")(values);
35
+ };
36
+
37
+ const buildGroupNameMap = (groups = []) =>
38
+ groups.reduce((acc, group) => {
39
+ const canonicalName = groupCanonicalName(group);
40
+ groupNameCandidates(group).forEach((candidate) => {
41
+ acc[candidate] = canonicalName;
42
+ });
43
+ return acc;
44
+ }, {});
45
+
46
+ const normalizeRoleUsersValue = (fieldValue, processedUsers = []) => {
47
+ const normalizeOne = (value) => {
48
+ if (typeof value !== "string") return value;
49
+ const normalizedUser = stripUserPrefix(value);
50
+ return processedUsers.includes(normalizedUser) ? normalizedUser : value;
51
+ };
52
+
53
+ return _.isArray(fieldValue)
54
+ ? fieldValue.map(normalizeOne)
55
+ : normalizeOne(fieldValue);
56
+ };
57
+
58
+ const normalizeRoleGroupsValue = (fieldValue, fieldType, values = {}) => {
59
+ const processedUsers = _.getOr([], "processed_users")(values);
60
+ const groupNameMap = buildGroupNameMap(groupEntries(values));
61
+ const isGroupOnly = fieldType === "group";
62
+
63
+ const normalizeOne = (value) => {
64
+ if (typeof value !== "string") return value;
65
+ if (isGroupOnly) {
66
+ const normalizedGroup = stripGroupPrefix(value);
67
+ return groupNameMap[normalizedGroup] || normalizedGroup;
68
+ }
69
+
70
+ if (value.startsWith("user:")) {
71
+ const normalizedUser = stripUserPrefix(value);
72
+ return processedUsers.includes(normalizedUser)
73
+ ? `user:${normalizedUser}`
74
+ : value;
75
+ }
76
+
77
+ const normalizedGroup = stripGroupPrefix(value);
78
+ const resolvedGroup = groupNameMap[normalizedGroup];
79
+ if (resolvedGroup) return `group:${resolvedGroup}`;
80
+ if (processedUsers.includes(value)) {
81
+ return `user:${value}`;
82
+ }
83
+
84
+ return value;
85
+ };
86
+
87
+ return _.isArray(fieldValue)
88
+ ? fieldValue.map(normalizeOne)
89
+ : normalizeOne(fieldValue);
90
+ };
91
+
92
+ const normalizeFieldValue = (fieldValue, normalizer) => {
93
+ if (_.isNil(fieldValue)) return fieldValue;
94
+ if (_.isObject(fieldValue) && _.has("value")(fieldValue)) {
95
+ return {
96
+ ...fieldValue,
97
+ value: normalizer(fieldValue.value),
98
+ };
99
+ }
100
+ return normalizer(fieldValue);
101
+ };
102
+
10
103
  const doParseFieldOptions = (formatMessage, content, selectedDomain, field) => {
11
104
  const value = _.prop(field?.name)(content);
12
105
 
@@ -15,8 +108,8 @@ const doParseFieldOptions = (formatMessage, content, selectedDomain, field) => {
15
108
  const tableColumns = _.flow(
16
109
  _.propOr([], "values.table_columns"),
17
110
  _.map((field) =>
18
- doParseFieldOptions(formatMessage, content, selectedDomain, field),
19
- ),
111
+ doParseFieldOptions(formatMessage, content, selectedDomain, field)
112
+ )
20
113
  )(field);
21
114
  return {
22
115
  ...field,
@@ -26,12 +119,12 @@ const doParseFieldOptions = (formatMessage, content, selectedDomain, field) => {
26
119
  }
27
120
  const translateValues = _.flow(
28
121
  makeOptions(formatMessage, field?.label),
29
- _.sortBy(accentInsensitivePathOrder("text")),
122
+ _.sortBy(accentInsensitivePathOrder("text"))
30
123
  );
31
124
  const parseSwitch = (values) => {
32
125
  const switchOn = _.prop("switch.on")(values);
33
126
  const switchedValues = _.prop(`switch.values.${content[switchOn].value}`)(
34
- values,
127
+ values
35
128
  );
36
129
  return translateValues(switchedValues);
37
130
  };
@@ -42,24 +135,21 @@ const doParseFieldOptions = (formatMessage, content, selectedDomain, field) => {
42
135
  [_.isArray, _.identity],
43
136
  [_.stubTrue, _.constant([])],
44
137
  ]),
45
- translateValues,
138
+ translateValues
46
139
  );
47
- const groupNameOrAlias = (group) =>
48
- typeof group === "string" ? group : group.alias || group.name;
49
140
  const parseRoleGroups = (values) => {
50
141
  const isGroupOnly = field?.type === "group";
51
142
  const groups = _.flow(
52
- _.get("processed_groups"),
143
+ () => groupEntries(values),
53
144
  _.map((group) => {
54
- const name = groupNameOrAlias(group);
145
+ const name = groupCanonicalName(group);
55
146
  return {
56
147
  value: isGroupOnly ? name : `group:${name}`,
57
- text: name,
148
+ text: groupDisplayName(group),
58
149
  icon: "group",
59
150
  };
60
- }),
151
+ })
61
152
  )(values);
62
-
63
153
  if (isGroupOnly) return groups;
64
154
 
65
155
  const users = _.flow(
@@ -68,7 +158,7 @@ const doParseFieldOptions = (formatMessage, content, selectedDomain, field) => {
68
158
  value: `user:${name}`,
69
159
  text: name,
70
160
  icon: "user",
71
- })),
161
+ }))
72
162
  )(values);
73
163
 
74
164
  return _.concat(users, groups);
@@ -88,7 +178,7 @@ const doParseFieldOptions = (formatMessage, content, selectedDomain, field) => {
88
178
  }) || [];
89
179
  return _.uniq(allValues);
90
180
  },
91
- translateValues,
181
+ translateValues
92
182
  );
93
183
 
94
184
  const parsedValues = _.flow(
@@ -100,9 +190,37 @@ const doParseFieldOptions = (formatMessage, content, selectedDomain, field) => {
100
190
  [_.has("role_users"), parseRoleUsers],
101
191
  [_.has("role_groups"), parseRoleGroups],
102
192
  [_.has("domain"), parseDomain],
103
- ]),
193
+ ])
104
194
  )(field);
105
- return { ...field, value, parsedValues };
195
+ const normalizedValue = _.flow(
196
+ _.get("values"),
197
+ _.cond([
198
+ [
199
+ _.has("role_users"),
200
+ () =>
201
+ normalizeFieldValue(value, (rawValue) =>
202
+ normalizeRoleUsersValue(
203
+ rawValue,
204
+ _.getOr([], "values.processed_users")(field)
205
+ )
206
+ ),
207
+ ],
208
+ [
209
+ _.has("role_groups"),
210
+ () =>
211
+ normalizeFieldValue(value, (rawValue) =>
212
+ normalizeRoleGroupsValue(
213
+ rawValue,
214
+ field?.type,
215
+ _.get("values")(field)
216
+ )
217
+ ),
218
+ ],
219
+ [_.stubTrue, () => value],
220
+ ])
221
+ )(field);
222
+
223
+ return { ...field, value: normalizedValue, parsedValues };
106
224
  };
107
225
  export const parseFieldOptions =
108
226
  (formatMessage) => (content, selectedDomain) => (field) =>