@truedat/core 7.1.8 → 7.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "7.1.8",
3
+ "version": "7.2.1",
4
4
  "description": "Truedat Web Core",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -36,7 +36,7 @@
36
36
  "@testing-library/react": "^12.0.0",
37
37
  "@testing-library/react-hooks": "^8.0.1",
38
38
  "@testing-library/user-event": "^13.2.1",
39
- "@truedat/test": "7.1.8",
39
+ "@truedat/test": "7.2.1",
40
40
  "babel-jest": "^28.1.0",
41
41
  "babel-plugin-dynamic-import-node": "^2.3.3",
42
42
  "babel-plugin-lodash": "^3.3.4",
@@ -118,5 +118,5 @@
118
118
  "react-dom": ">= 16.8.6 < 17",
119
119
  "semantic-ui-react": ">= 2.0.3 < 2.2"
120
120
  },
121
- "gitHead": "eb92417df5ba0fbc5bc9f670d0d8540c39e3d712"
121
+ "gitHead": "10ba779646ef549e6db1235b3acd6d430526f487"
122
122
  }
@@ -1,39 +1,201 @@
1
1
  import _ from "lodash/fp";
2
- import React, { useEffect, useState } from "react";
2
+ import React, {
3
+ useCallback,
4
+ useContext,
5
+ createContext,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ } from "react";
3
10
  import { IntlProvider } from "react-intl";
11
+ import PropTypes from "prop-types";
4
12
  import { Loading } from "@truedat/core/components";
5
13
  import { useLocales } from "../../hooks/useLocales";
6
- import { useMessages } from "../../hooks/useMessages";
7
14
  import { getNavigatorLanguage } from "../../services/i18n";
8
15
 
9
- const LangProvider = ({ children, defaultMessages }) => {
10
- const { locales } = useLocales(false);
11
- const [lang, setLang] = useState("en");
12
- const [enabledLocales, setEnabledLocales] = useState([]);
13
- const { messages, loading } = useMessages(lang);
16
+ const defaultContext = {
17
+ defaultLang: "en",
18
+ altLangs: ["es"],
19
+ requiredLangs: ["en"],
20
+ enabledLangs: ["en", "es"],
21
+ isMultilingual: true,
22
+ getMessagesForLang: () => ({}),
23
+ lang: "en",
24
+ loading: false,
25
+ locales: [{ lang: "en", messages: {} }],
26
+ localesError: null,
27
+ mutate: () => {},
28
+ };
29
+
30
+ export const LanguageContext = createContext(defaultContext);
31
+ export const useLanguage = () => useContext(LanguageContext);
32
+
33
+ const LanguageContextProvider = ({ children, defaultMessages }) => {
34
+ const {
35
+ locales,
36
+ mutate,
37
+ error: localesError,
38
+ isValidating: loading,
39
+ } = useLocales();
40
+
41
+ const langsOf = _.map(({ lang }) => lang);
42
+ const enabledLangs = useMemo(
43
+ () =>
44
+ _.flow(
45
+ _.filter(({ is_enabled }) => is_enabled),
46
+ langsOf
47
+ )(locales),
48
+ [locales]
49
+ );
50
+
51
+ const altLangs = useMemo(
52
+ () =>
53
+ _.flow(
54
+ _.filter(({ is_enabled, is_default }) => is_enabled && !is_default),
55
+ langsOf
56
+ )(locales),
57
+ [locales]
58
+ );
59
+
60
+ const requiredLangs = useMemo(
61
+ () =>
62
+ _.flow(
63
+ _.filter(({ is_required }) => is_required),
64
+ langsOf
65
+ )(locales),
66
+ [locales]
67
+ );
68
+
69
+ const defaultLang = useMemo(
70
+ () =>
71
+ _.flow(
72
+ _.find(({ is_default }) => is_default),
73
+ _.prop("lang")
74
+ )(locales),
75
+ [locales]
76
+ );
77
+
78
+ const mapMessages = _.flow(
79
+ _.map(({ message_id, definition }) => [message_id, definition]),
80
+ _.fromPairs
81
+ );
82
+
83
+ const messages = useMemo(
84
+ () =>
85
+ _.flow(
86
+ _.map(({ lang, messages }) => [lang, mapMessages(messages)]),
87
+ _.fromPairs
88
+ )(locales),
89
+ [locales]
90
+ );
91
+
92
+ const [lang, setLang] = useState(defaultLang);
93
+ const [altLang, setAltLang] = useState();
94
+
95
+ const getMessagesForLang = useCallback(
96
+ (requestedLang) => {
97
+ if (loading || !requestedLang) return null;
98
+ return messages[requestedLang] || defaultMessages[requestedLang];
99
+ },
100
+ [loading, messages, defaultMessages]
101
+ );
102
+
103
+ const context = useMemo(
104
+ () => ({
105
+ defaultLang,
106
+ altLangs,
107
+ requiredLangs,
108
+ enabledLangs,
109
+ isMultilingual: enabledLangs.length > 1,
110
+ setAltLang,
111
+ altLang,
112
+ getMessagesForLang,
113
+ lang,
114
+ loading,
115
+ locales,
116
+ localesError,
117
+ mutate,
118
+ }),
119
+ [
120
+ defaultLang,
121
+ altLangs,
122
+ enabledLangs,
123
+ locales,
124
+ enabledLangs,
125
+ altLang,
126
+ requiredLangs,
127
+ lang,
128
+ loading,
129
+ getMessagesForLang,
130
+ localesError,
131
+ ]
132
+ );
133
+
14
134
  useEffect(() => {
15
- _.flow(
16
- _.filter("is_enabled"),
17
- _.map("lang"),
18
- setEnabledLocales
19
- )(locales || []);
20
- }, [locales, setEnabledLocales]);
135
+ if (enabledLangs.length > 0) {
136
+ const navigatorLang = getNavigatorLanguage(enabledLangs);
137
+ if (navigatorLang && navigatorLang !== lang) {
138
+ setLang(navigatorLang);
139
+ }
140
+ }
141
+ }, [enabledLangs, lang]);
142
+
21
143
  useEffect(() => {
22
- const navigatorLang = getNavigatorLanguage(enabledLocales);
23
- if (navigatorLang !== lang) setLang(navigatorLang);
24
- }, [enabledLocales, setLang, lang]);
144
+ if (enabledLangs.length > 1) {
145
+ const firstNonDefaultLang = _.first(altLangs);
146
+ if (firstNonDefaultLang && !altLang) {
147
+ setAltLang(firstNonDefaultLang);
148
+ }
149
+ }
150
+ }, [enabledLangs, altLang]);
151
+
152
+ if (loading || !lang) return <Loading />;
153
+
154
+ if (localesError) return <div>Error loading language data</div>;
25
155
 
26
- return loading ? (
27
- <Loading />
28
- ) : (
156
+ return (
157
+ <LanguageContext.Provider value={context}>
158
+ {children}
159
+ </LanguageContext.Provider>
160
+ );
161
+ };
162
+
163
+ LanguageContextProvider.propTypes = {
164
+ children: PropTypes.node.isRequired,
165
+ defaultMessages: PropTypes.object.isRequired,
166
+ };
167
+
168
+ export const I18nProvider = ({ children, lang }) => {
169
+ const { lang: contextLang, getMessagesForLang } = useLanguage();
170
+ const currentLang = lang || contextLang;
171
+
172
+ return (
29
173
  <IntlProvider
30
- locale={lang}
31
- defaultLocale={lang}
32
- messages={messages || defaultMessages[lang]}
174
+ locale={currentLang}
175
+ defaultLocale={currentLang}
176
+ messages={getMessagesForLang(currentLang)}
33
177
  >
34
178
  {children}
35
179
  </IntlProvider>
36
180
  );
37
181
  };
38
182
 
183
+ I18nProvider.propTypes = {
184
+ children: PropTypes.node.isRequired,
185
+ lang: PropTypes.string,
186
+ };
187
+
188
+ export const LangProvider = ({ children, defaultMessages }) => {
189
+ return (
190
+ <LanguageContextProvider defaultMessages={defaultMessages}>
191
+ <I18nProvider>{children}</I18nProvider>
192
+ </LanguageContextProvider>
193
+ );
194
+ };
195
+
196
+ LangProvider.propTypes = {
197
+ children: PropTypes.node.isRequired,
198
+ defaultMessages: PropTypes.object.isRequired,
199
+ };
200
+
39
201
  export default LangProvider;
@@ -0,0 +1,49 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useMemo, useCallback } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { LanguageContext } from "./LangProvider";
5
+
6
+ const LangProviderWrapper = ({ children, langs }) => {
7
+ const getMessagesForLang = useCallback(() => ({}));
8
+
9
+ const locales = useMemo(() => {
10
+ return langs.map((lang, idx) => ({
11
+ lang,
12
+ messages: {},
13
+ id: idx + 1,
14
+ is_default: idx === 0,
15
+ is_required: idx < 2,
16
+ is_enabled: true,
17
+ }));
18
+ }, [langs]);
19
+
20
+ const context = useMemo(
21
+ () => ({
22
+ defaultLang: _.first(langs),
23
+ altLangs: langs,
24
+ requiredLangs: langs,
25
+ enabledLangs: langs,
26
+ isMultilingual: langs.length > 1,
27
+ getMessagesForLang,
28
+ lang: _.first(langs),
29
+ loading: false,
30
+ locales,
31
+ localesError: null,
32
+ mutate: () => {},
33
+ }),
34
+ [langs]
35
+ );
36
+
37
+ return (
38
+ <LanguageContext.Provider value={context}>
39
+ {children}
40
+ </LanguageContext.Provider>
41
+ );
42
+ };
43
+
44
+ LangProviderWrapper.propTypes = {
45
+ children: PropTypes.node.isRequired,
46
+ langs: PropTypes.arrayOf(PropTypes.string).isRequired,
47
+ };
48
+
49
+ export default LangProviderWrapper;
@@ -5,7 +5,7 @@ import { useForm, Controller } from "react-hook-form";
5
5
  import { useIntl } from "react-intl";
6
6
  import { Button, Form, Segment, Header, Loader } from "semantic-ui-react";
7
7
  import { HistoryBackButton } from "@truedat/core/components";
8
- import { useLocales } from "@truedat/core/hooks";
8
+ import { useLanguage } from "./LangProvider";
9
9
 
10
10
  export const LangForm = ({
11
11
  control,
@@ -74,7 +74,7 @@ LangForm.propTypes = {
74
74
  };
75
75
 
76
76
  export const MessageForm = ({ onSubmit, isSubmitting }) => {
77
- const { locales, loading } = useLocales();
77
+ const { locales } = useLanguage();
78
78
  const { formatMessage } = useIntl();
79
79
  const { handleSubmit, control, formState } = useForm({
80
80
  mode: "all",
@@ -85,9 +85,7 @@ export const MessageForm = ({ onSubmit, isSubmitting }) => {
85
85
  });
86
86
  const { errors, isDirty, isValid } = formState;
87
87
 
88
- return loading ? (
89
- <Loader />
90
- ) : (
88
+ return (
91
89
  <Form onSubmit={handleSubmit(onSubmit)}>
92
90
  <Controller
93
91
  control={control}
@@ -8,22 +8,20 @@ import {
8
8
  Divider,
9
9
  Icon,
10
10
  Segment,
11
- Dimmer,
12
- Loader,
13
11
  Menu,
14
12
  } from "semantic-ui-react";
15
13
  import { Link } from "react-router-dom";
16
14
  import { SearchInput } from "@truedat/core/components";
17
15
  import { lowerDeburrTrim } from "@truedat/core/services/sort";
18
- import { useLocales } from "@truedat/core/hooks";
19
16
  import { I18N_MESSAGES_NEW } from "@truedat/core/routes";
20
17
  import { Pagination } from "@truedat/core/components";
18
+ import { useLanguage } from "./LangProvider";
21
19
  import MessagesTable from "./MessagesTable";
22
20
  import Languages from "./Languages";
23
21
 
24
22
  const ITEMS_PER_PAGE = 30;
25
23
 
26
- export function MessagesContent({ locales, loading, mutate, isValidating }) {
24
+ export function MessagesContent({ locales, mutate }) {
27
25
  const [selectedLang, setSelectedLang] = useState(
28
26
  _.flow(
29
27
  _.find(({ is_default }) => is_default),
@@ -105,11 +103,7 @@ export function MessagesContent({ locales, loading, mutate, isValidating }) {
105
103
  })}
106
104
  value={filter}
107
105
  />
108
- <MessagesTable
109
- messages={paginatedMessages}
110
- locales={locales}
111
- loading={loading}
112
- />
106
+ <MessagesTable messages={paginatedMessages} locales={locales} />
113
107
  <Pagination
114
108
  totalPages={totalPages}
115
109
  activePage={page}
@@ -117,11 +111,7 @@ export function MessagesContent({ locales, loading, mutate, isValidating }) {
117
111
  />
118
112
  </>
119
113
  ) : (
120
- <Languages
121
- locales={_.keyBy("lang")(locales)}
122
- mutate={mutate}
123
- isValidating={isValidating}
124
- />
114
+ <Languages locales={_.keyBy("lang")(locales)} mutate={mutate} />
125
115
  )}
126
116
  </>
127
117
  );
@@ -129,15 +119,13 @@ export function MessagesContent({ locales, loading, mutate, isValidating }) {
129
119
 
130
120
  MessagesContent.propTypes = {
131
121
  locales: PropTypes.array,
132
- loading: PropTypes.bool,
133
122
  mutate: PropTypes.func,
134
- isValidating: PropTypes.bool,
135
123
  };
136
124
 
137
125
  export default function Messages() {
138
126
  const { formatMessage } = useIntl();
139
127
 
140
- const { locales, loading, mutate, isValidating } = useLocales();
128
+ const { locales, mutate } = useLanguage();
141
129
 
142
130
  return (
143
131
  <Segment>
@@ -157,11 +145,6 @@ export default function Messages() {
157
145
  </Header.Content>
158
146
  </Header>
159
147
  <Segment attached="bottom">
160
- <Dimmer.Dimmable dimmed={loading}>
161
- <Dimmer active={loading} inverted>
162
- <Loader />
163
- </Dimmer>
164
- </Dimmer.Dimmable>
165
148
  <Button
166
149
  floated="right"
167
150
  primary
@@ -170,14 +153,7 @@ export default function Messages() {
170
153
  as={Link}
171
154
  to={I18N_MESSAGES_NEW}
172
155
  />
173
- {!loading ? (
174
- <MessagesContent
175
- locales={locales}
176
- loading={loading}
177
- mutate={mutate}
178
- isValidating={isValidating}
179
- />
180
- ) : null}
156
+ <MessagesContent locales={locales} mutate={mutate} />
181
157
  </Segment>
182
158
  </Segment>
183
159
  );
@@ -1,36 +1,27 @@
1
1
  import React from "react";
2
2
  import { render } from "@truedat/test/render";
3
- import en from "@truedat/core/messages/en";
3
+ import { LangProviderWrapper } from "@truedat/core/i18n";
4
4
  import Messages from "../Messages";
5
5
 
6
6
  const renderOpts = {
7
- messages: { en: { ...en, "i18n.messages.locale.manage": "manage" } },
7
+ messages: {},
8
8
  };
9
9
 
10
10
  jest.mock("@truedat/core/hooks", () => ({
11
- useLocales: jest.fn(() => ({
12
- loading: false,
13
- locales: [
14
- {
15
- lang: "es",
16
- id: 1,
17
- messages: [
18
- {
19
- id: 1,
20
- message_id: "message_id",
21
- definition: "definition",
22
- description: "description",
23
- },
24
- ],
25
- },
26
- ],
27
- })),
28
11
  useMessagePatch: jest.fn(() => ({ trigger: jest.fn() })),
29
12
  }));
30
13
 
14
+ const renderComponent = () =>
15
+ render(
16
+ <LangProviderWrapper langs={["es"]}>
17
+ <Messages />
18
+ </LangProviderWrapper>,
19
+ renderOpts
20
+ );
21
+
31
22
  describe("<Messages />", () => {
32
23
  it("matches the latest snapshot", () => {
33
- const { container } = render(<Messages />, renderOpts);
24
+ const { container } = renderComponent();
34
25
  expect(container).toMatchSnapshot();
35
26
  });
36
27
  });
@@ -15,32 +15,17 @@ exports[`<Messages /> matches the latest snapshot 1`] = `
15
15
  <div
16
16
  class="content"
17
17
  >
18
- Translations
18
+ i18n.messages.header
19
19
  <div
20
20
  class="sub header"
21
21
  >
22
- Messages used in Truedat
22
+ i18n.messages.subheader
23
23
  </div>
24
24
  </div>
25
25
  </h2>
26
26
  <div
27
27
  class="ui bottom attached segment"
28
28
  >
29
- <div
30
- class="dimmable"
31
- >
32
- <div
33
- class="ui inverted dimmer"
34
- >
35
- <div
36
- class="content"
37
- >
38
- <div
39
- class="ui loader"
40
- />
41
- </div>
42
- </div>
43
- </div>
44
29
  <a
45
30
  class="ui primary right floated button"
46
31
  href="/i18n/messages/new"
@@ -50,7 +35,7 @@ exports[`<Messages /> matches the latest snapshot 1`] = `
50
35
  aria-hidden="true"
51
36
  class="add circle icon"
52
37
  />
53
- Create message
38
+ i18n.actions.createMessage
54
39
  </a>
55
40
  <div
56
41
  class="ui pointing secondary top attached tabular menu"
@@ -58,7 +43,12 @@ exports[`<Messages /> matches the latest snapshot 1`] = `
58
43
  <a
59
44
  class="item"
60
45
  >
61
- manage
46
+ i18n.messages.locale.manage
47
+ </a>
48
+ <a
49
+ class="active item"
50
+ >
51
+ undefined ( undefined )
62
52
  </a>
63
53
  </div>
64
54
  <div
@@ -68,7 +58,7 @@ exports[`<Messages /> matches the latest snapshot 1`] = `
68
58
  class="ui icon input"
69
59
  >
70
60
  <input
71
- placeholder="Search messages"
61
+ placeholder="i18n.messages.search.placeholder"
72
62
  type="text"
73
63
  value=""
74
64
  />
@@ -87,7 +77,7 @@ exports[`<Messages /> matches the latest snapshot 1`] = `
87
77
  <div
88
78
  class="content"
89
79
  >
90
- No messages
80
+ i18n.messages.empty
91
81
  </div>
92
82
  </h4>
93
83
  </div>
@@ -0,0 +1,7 @@
1
+ export {
2
+ LanguageContext,
3
+ useLanguage,
4
+ I18nProvider,
5
+ LangProvider,
6
+ } from "./components/LangProvider";
7
+ export { default as LangProviderWrapper } from "./components/LangProviderWrapper";
@@ -0,0 +1,164 @@
1
+ import {
2
+ splitTranslatableFields,
3
+ formatLocales,
4
+ hasTranslatableFields,
5
+ isTranslatetableField,
6
+ } from "../i18nContent";
7
+
8
+ describe("services: i18nContent", () => {
9
+ describe("splitTranslatableFields", () => {
10
+ it("should split fields into translatable and non-translatable", () => {
11
+ const template = {
12
+ content: [
13
+ {
14
+ fields: [
15
+ { name: "title", widget: "string" },
16
+ { name: "description", widget: "enriched_text" },
17
+ { name: "date", widget: "date" },
18
+ { name: "notes", widget: "textarea" },
19
+ ],
20
+ },
21
+ ],
22
+ };
23
+
24
+ const result = splitTranslatableFields(template);
25
+
26
+ expect(result).toEqual({
27
+ translatable: {
28
+ title: { value: "", origin: "user" },
29
+ description: { value: {}, origin: "user" },
30
+ notes: { value: null, origin: "user" },
31
+ },
32
+ noTranslatable: {
33
+ date: { value: null, origin: "user" },
34
+ },
35
+ });
36
+ });
37
+
38
+ it("should handle empty template", () => {
39
+ const template = { content: [] };
40
+ const result = splitTranslatableFields(template);
41
+
42
+ expect(result).toEqual({
43
+ translatable: {},
44
+ noTranslatable: {},
45
+ });
46
+ });
47
+
48
+ it("should handle multiple groups", () => {
49
+ const template = {
50
+ content: [
51
+ {
52
+ fields: [{ name: "title", widget: "string" }],
53
+ },
54
+ {
55
+ fields: [
56
+ { name: "subtitle", widget: "string" },
57
+ { name: "count", widget: "number" },
58
+ ],
59
+ },
60
+ ],
61
+ };
62
+
63
+ const result = splitTranslatableFields(template);
64
+
65
+ expect(result).toEqual({
66
+ translatable: {
67
+ title: { value: "", origin: "user" },
68
+ subtitle: { value: "", origin: "user" },
69
+ },
70
+ noTranslatable: {
71
+ count: { value: null, origin: "user" },
72
+ },
73
+ });
74
+ });
75
+ });
76
+
77
+ describe("formatLocales", () => {
78
+ it("should format locales into default, required and enabled categories", () => {
79
+ const locales = [
80
+ { lang: "en", is_enabled: true, is_required: true, is_default: true },
81
+ { lang: "es", is_enabled: true, is_required: true, is_default: false },
82
+ { lang: "fr", is_enabled: true, is_required: false, is_default: false },
83
+ {
84
+ lang: "de",
85
+ is_enabled: false,
86
+ is_required: false,
87
+ is_default: false,
88
+ },
89
+ ];
90
+
91
+ const result = formatLocales(locales);
92
+
93
+ expect(result).toEqual({
94
+ default: [
95
+ { lang: "en", is_enabled: true, is_required: true, is_default: true },
96
+ ],
97
+ required: [
98
+ {
99
+ lang: "es",
100
+ is_enabled: true,
101
+ is_required: true,
102
+ is_default: false,
103
+ },
104
+ ],
105
+ enabled: [
106
+ {
107
+ lang: "fr",
108
+ is_enabled: true,
109
+ is_required: false,
110
+ is_default: false,
111
+ },
112
+ ],
113
+ });
114
+ });
115
+
116
+ it("should sort locales by language code", () => {
117
+ const locales = [
118
+ { lang: "zh", is_enabled: true, is_required: false, is_default: false },
119
+ { lang: "en", is_enabled: true, is_required: false, is_default: false },
120
+ { lang: "ar", is_enabled: true, is_required: false, is_default: false },
121
+ ];
122
+
123
+ const result = formatLocales(locales);
124
+
125
+ expect(result.enabled.map((l) => l.lang)).toEqual(["ar", "en", "zh"]);
126
+ });
127
+ });
128
+
129
+ describe("hasTranslatableFields", () => {
130
+ it("should return true if any field is translatable", () => {
131
+ const fields = [
132
+ { widget: "number" },
133
+ { widget: "string" },
134
+ { widget: "date" },
135
+ ];
136
+
137
+ expect(hasTranslatableFields(fields)).toBe(true);
138
+ });
139
+
140
+ it("should return false if no fields are translatable", () => {
141
+ const fields = [
142
+ { widget: "number" },
143
+ { widget: "date" },
144
+ { widget: "boolean" },
145
+ ];
146
+
147
+ expect(hasTranslatableFields(fields)).toBe(false);
148
+ });
149
+ });
150
+
151
+ describe("isTranslatetableField", () => {
152
+ it("should identify translatable fields", () => {
153
+ expect(isTranslatetableField({ widget: "string" })).toBe(true);
154
+ expect(isTranslatetableField({ widget: "enriched_text" })).toBe(true);
155
+ expect(isTranslatetableField({ widget: "textarea" })).toBe(true);
156
+ });
157
+
158
+ it("should identify non-translatable fields", () => {
159
+ expect(isTranslatetableField({ widget: "number" })).toBe(false);
160
+ expect(isTranslatetableField({ widget: "date" })).toBe(false);
161
+ expect(isTranslatetableField({ widget: "boolean" })).toBe(false);
162
+ });
163
+ });
164
+ });
@@ -62,3 +62,13 @@ export const formatLocales = (locales) =>
62
62
  is_default: false,
63
63
  })(localeList),
64
64
  }))(locales);
65
+
66
+ export const hasTranslatableFields = (fields) => {
67
+ return _.some((field) => translatableFieldTypes.includes(field.widget))(
68
+ fields
69
+ );
70
+ };
71
+
72
+ export const isTranslatetableField = (field) => {
73
+ return translatableFieldTypes.includes(field.widget);
74
+ };