@truedat/bg 7.2.11 → 7.3.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.
@@ -1,169 +1,331 @@
1
1
  import _ from "lodash/fp";
2
- import React, { useState, useEffect } from "react";
2
+ import React, { useState, useEffect, useCallback, lazy } from "react";
3
3
  import PropTypes from "prop-types";
4
- import { Form, Header, Label } from "semantic-ui-react";
4
+ import {
5
+ Form,
6
+ Header,
7
+ Label,
8
+ Grid,
9
+ Tab,
10
+ Button,
11
+ Container,
12
+ Segment,
13
+ Icon,
14
+ } from "semantic-ui-react";
5
15
  import { useIntl, FormattedMessage } from "react-intl";
6
- import { apiJsonPost } from "@truedat/core/services/api";
7
- import { DomainSelector } from "@truedat/core/components";
8
16
  import { useAvailabilityCheck } from "@truedat/ai/hooks/useSuggestions";
17
+ import { apiJsonPost } from "@truedat/core/services/api";
9
18
  import { API_SUGGESTIONS_REQUEST } from "@truedat/ai/api";
19
+ import {
20
+ DomainSelector,
21
+ LanguagesTabs,
22
+ HistoryBackButton,
23
+ } from "@truedat/core/components";
24
+ import { useLanguage, I18nProvider } from "@truedat/core/i18n";
10
25
  import SuggestionsWidget from "@truedat/ai/components/suggestions/SuggestionsWidget";
11
26
 
12
- const SelectableDynamicForm = React.lazy(() =>
13
- import("@truedat/df/components/SelectableDynamicForm")
27
+ const SelectDynamicFormWithTranslations = lazy(() =>
28
+ import("@truedat/df/components/SelectDynamicFormWithTranslations")
14
29
  );
15
30
 
16
31
  const ConceptForm = ({
17
32
  actionKey,
18
33
  domainId,
19
- template,
20
- concept,
34
+ i18nConcept,
21
35
  loading,
22
- defaultContent,
23
- noTranslatableFields,
24
- onChangeDomainId,
25
- onChangeName,
26
- onChangeTemplate,
27
- onChangeConceptContent,
36
+ onSubmit,
37
+ setDomainId,
38
+ setI18nConcept,
39
+ setTemplate,
40
+ template,
28
41
  }) => {
29
42
  const { formatMessage } = useIntl();
43
+ const {
44
+ defaultLang,
45
+ altLangs,
46
+ requiredLangs,
47
+ altLang,
48
+ isMultilingual,
49
+ setAltLang,
50
+ } = useLanguage();
51
+ console.log("useLanguage", defaultLang);
30
52
  const [domains, setDomains] = useState();
31
53
  const [domainsLoading, setDomainsLoading] = useState(true);
32
54
  const [hasSuggestions, setHasSuggestions] = useState(false);
33
-
55
+ const isNonEmptyString = useCallback(_.flow(_.trim, _.negate(_.isEmpty)), []);
34
56
  const { trigger: checkSuggestionAvailability } = useAvailabilityCheck();
35
-
36
- const templateDisabled = !domainId || (!concept?.name && concept?.is_default);
37
57
  const templateId = template?.id;
38
58
 
59
+ const handleDomainChange = (_e, { value }) => setDomainId(_.toInteger(value));
60
+ const handleTemplateChange = (value) => setTemplate(value);
61
+ const handleI18nNameChange = (lang, name) =>
62
+ setI18nConcept(_.set(`${lang}.name`, name, i18nConcept));
63
+ const handleI18nConceptContentChange = (lang, { content }) => {
64
+ setI18nConcept({
65
+ ...i18nConcept,
66
+ [lang]: { ...i18nConcept[lang], content: { ...content } },
67
+ });
68
+ };
69
+
39
70
  useEffect(() => {
40
71
  if (templateId && domainId) {
41
72
  checkSuggestionAvailability({
42
73
  template_id: templateId,
43
74
  domain_ids: [domainId],
44
75
  resource_type: "business_concept",
45
- }).then((prop) =>
46
- setHasSuggestions(_.prop("data.data.status")(prop) == "ok")
76
+ }).then((res) =>
77
+ setHasSuggestions(_.get("data.data.status", res) === "ok")
47
78
  );
48
79
  }
49
- }, [templateId, domainId]);
80
+ }, [templateId, domainId, checkSuggestionAvailability]);
81
+
82
+ useEffect(() => {
83
+ if (domainsLoading && domainId) setDomainsLoading(false);
84
+ }, [domainsLoading, domainId]);
50
85
 
51
86
  const handleDomainsLoaded = ({ domains }) => {
52
87
  setDomains(domains);
53
88
  setDomainsLoading(false);
54
89
  };
55
90
 
56
- if (domainsLoading && domainId) {
57
- setDomainsLoading(false);
58
- }
59
-
60
91
  const requestAiSuggestion = (callback) => {
61
- const domainName = _.flow(
62
- _.find({ id: domainId }),
63
- _.prop("name")
64
- )(domains);
65
- const body = {
92
+ const domainName = _.get("name", _.find({ id: domainId }, domains));
93
+ apiJsonPost(API_SUGGESTIONS_REQUEST, {
66
94
  resource_type: "business_concept",
67
95
  resource_body: {
68
- name: concept.name,
96
+ name: i18nConcept?.[defaultLang]?.name || "",
69
97
  domain: domainName,
70
98
  type: template?.name,
71
99
  },
72
100
  domain_ids: [domainId],
73
101
  template_id: templateId,
74
- };
75
- apiJsonPost(API_SUGGESTIONS_REQUEST, body).then(callback);
102
+ }).then(callback);
76
103
  };
77
104
 
78
- const handleOnChangeConceptContent = (content) =>
79
- onChangeConceptContent(concept.lang, content);
80
-
81
105
  const applySuggestions = (suggestions) => {
82
- onChangeConceptContent(concept.lang, {
83
- content: { ...concept?.content, ...suggestions },
106
+ handleI18nConceptContentChange(defaultLang, {
107
+ content: { ...i18nConcept[defaultLang]?.content, ...suggestions },
84
108
  });
85
109
  };
86
110
 
87
- return (
111
+ const renderNameField = useCallback(
112
+ (lang, value, onChange, showValidation = false) => (
113
+ <I18nProvider lang={lang}>
114
+ <Form.Field required={requiredLangs.includes(lang)}>
115
+ <label>
116
+ <FormattedMessage id="concepts.props.name" />
117
+ {showValidation &&
118
+ requiredLangs.includes(lang) &&
119
+ _.isEmpty(value) && (
120
+ <Label pointing="left">
121
+ <FormattedMessage id="template.form.validation.empty_required" />
122
+ </Label>
123
+ )}
124
+ </label>
125
+ <Form.Input
126
+ name="name"
127
+ value={value || ""}
128
+ onChange={(_e, { value }) => onChange(lang, value)}
129
+ />
130
+ </Form.Field>
131
+ </I18nProvider>
132
+ ),
133
+ [requiredLangs, i18nConcept]
134
+ );
135
+
136
+ const renderFormHeader = () => (
88
137
  <>
89
- <Form loading={loading || domainsLoading}>
90
- {actionKey == "create" ? (
138
+ {!isMultilingual ? (
139
+ renderNameField(
140
+ defaultLang,
141
+ i18nConcept[defaultLang]?.name,
142
+ handleI18nNameChange,
143
+ true
144
+ )
145
+ ) : (
146
+ <>
147
+ <Grid style={{ background: "white" }}>
148
+ <Grid.Row columns={2}>
149
+ <Grid.Column>
150
+ <Tab
151
+ attached="top"
152
+ panes={[
153
+ {
154
+ menuItem: formatMessage({
155
+ id: `i18n.messages.locale.${defaultLang}`,
156
+ defaultMessage: defaultLang,
157
+ }),
158
+ },
159
+ ]}
160
+ activeIndex={0}
161
+ />
162
+ </Grid.Column>
163
+ <Grid.Column>
164
+ <LanguagesTabs
165
+ altLangs={altLangs}
166
+ requiredLangs={requiredLangs}
167
+ selectedLang={altLang}
168
+ onLangChange={setAltLang}
169
+ />
170
+ </Grid.Column>
171
+ </Grid.Row>
172
+ </Grid>
173
+ <Segment attached="bottom" className="attached-segment">
174
+ <Grid className="translation-tabs-grid">
175
+ <Grid.Row columns={2}>
176
+ <Grid.Column>
177
+ {renderNameField(
178
+ defaultLang,
179
+ i18nConcept[defaultLang]?.name,
180
+ handleI18nNameChange
181
+ )}
182
+ </Grid.Column>
183
+ <Grid.Column>
184
+ {renderNameField(
185
+ altLang,
186
+ i18nConcept[altLang]?.name,
187
+ handleI18nNameChange
188
+ )}
189
+ </Grid.Column>
190
+ </Grid.Row>
191
+ </Grid>
192
+ </Segment>
193
+ </>
194
+ )}
195
+ {hasSuggestions && (
196
+ <Grid>
197
+ <Grid.Row columns={isMultilingual ? 2 : 1}>
198
+ <Grid.Column>
199
+ <SuggestionsWidget
200
+ requestAiSuggestion={requestAiSuggestion}
201
+ template={template}
202
+ applySuggestions={applySuggestions}
203
+ isModification
204
+ enableRequestButton={!_.isEmpty(i18nConcept[defaultLang]?.name)}
205
+ disabledButtonMessage={formatMessage({
206
+ id: "template.form.validation.name_required",
207
+ })}
208
+ />
209
+ </Grid.Column>
210
+ </Grid.Row>
211
+ </Grid>
212
+ )}
213
+ </>
214
+ );
215
+
216
+ const validDomain = () => _.isFinite(domainId);
217
+ const validNames = () =>
218
+ _.flow(
219
+ _.filter("is_required"),
220
+ _.map("name"),
221
+ _.every(isNonEmptyString)
222
+ )(i18nConcept);
223
+ const validTemplate = () =>
224
+ _.isObject(template) && isNonEmptyString(template?.name);
225
+ const isInvalid = !validDomain() || !validNames() || !validTemplate();
226
+
227
+ const renderDomainSelector = () =>
228
+ actionKey === "create" &&
229
+ (isMultilingual ? (
230
+ <Grid.Row columns={2}>
231
+ <Grid.Column>
91
232
  <DomainSelector
92
233
  action="createBusinessConcept"
93
234
  label={formatMessage({ id: "domain.selector.label" })}
94
235
  labels
95
236
  required
96
- disabled={!concept?.is_default}
97
- onChange={onChangeDomainId}
237
+ disabled={!(i18nConcept[defaultLang]?.is_default ?? true)}
238
+ onChange={handleDomainChange}
98
239
  onLoad={handleDomainsLoaded}
99
240
  value={domainId}
100
241
  />
101
- ) : null}
242
+ </Grid.Column>
243
+ <Grid.Column />
244
+ </Grid.Row>
245
+ ) : (
246
+ <DomainSelector
247
+ action="createBusinessConcept"
248
+ label={formatMessage({ id: "domain.selector.label" })}
249
+ labels
250
+ required
251
+ disabled={!(i18nConcept[defaultLang]?.is_default ?? true)}
252
+ onChange={handleDomainChange}
253
+ onLoad={handleDomainsLoaded}
254
+ value={domainId}
255
+ />
256
+ ));
102
257
 
103
- <Form.Field required>
104
- <label>
105
- <FormattedMessage id="concepts.props.name" />
106
- {_.isEmpty(concept?.name) ? (
107
- <Label pointing="left">
108
- <FormattedMessage id="template.form.validation.empty_required" />
109
- </Label>
110
- ) : null}
111
- </label>
112
- <Form.Input
113
- name="name"
114
- value={concept?.name}
115
- onChange={(_e, { value }) => onChangeName(concept?.lang, value)}
116
- />
117
- </Form.Field>
118
- <SelectableDynamicForm
119
- key={concept?.lang}
120
- disableSelector={!concept?.is_default}
121
- disabled={templateDisabled}
122
- scope="bg"
123
- domainIds={_.isNil(domainId) ? null : [domainId]}
124
- required
125
- header={
126
- <>
127
- <Header
128
- as="h3"
129
- content={formatMessage({
130
- id: "concepts.form.aditional_fields",
131
- })}
132
- />
133
- {hasSuggestions && concept?.is_default ? (
134
- <SuggestionsWidget
135
- requestAiSuggestion={requestAiSuggestion}
136
- template={template}
137
- applySuggestions={applySuggestions}
138
- isModification={true}
139
- />
140
- ) : null}
141
- </>
258
+ const commonForm = (
259
+ <>
260
+ {isMultilingual ? (
261
+ <Grid>{renderDomainSelector()}</Grid>
262
+ ) : (
263
+ renderDomainSelector()
264
+ )}
265
+ <SelectDynamicFormWithTranslations
266
+ scope="bg"
267
+ domainIds={domainId ? [domainId] : null}
268
+ required
269
+ header={renderFormHeader()}
270
+ i18nContent={_.mapValues("content", i18nConcept)}
271
+ name={template?.name}
272
+ actionKey={actionKey}
273
+ onChangeContent={handleI18nConceptContentChange}
274
+ onChangeTemplate={handleTemplateChange}
275
+ selectedTemplate={template}
276
+ />
277
+ <div className="concept-forms-actions actions">
278
+ <HistoryBackButton content={formatMessage({ id: "actions.cancel" })} />
279
+ <Button
280
+ primary
281
+ disabled={isInvalid}
282
+ onClick={onSubmit}
283
+ content={formatMessage({ id: `actions.${actionKey}` })}
284
+ />
285
+ </div>
286
+ </>
287
+ );
288
+
289
+ const renderHeader = () => (
290
+ <Header as="h2">
291
+ <Icon name="book" />
292
+ <Header.Content>
293
+ <FormattedMessage
294
+ id={
295
+ actionKey === "create"
296
+ ? "concepts.actions.create"
297
+ : "concepts.header.edit"
142
298
  }
143
- content={{ ...defaultContent, ...concept?.content }}
144
- name={template?.name}
145
- noTranslatableFields={noTranslatableFields}
146
- actionKey={actionKey}
147
- onChange={handleOnChangeConceptContent}
148
- onNameChange={onChangeName}
149
- onTemplateChange={onChangeTemplate}
150
- selectedTemplate={template}
151
299
  />
300
+ </Header.Content>
301
+ </Header>
302
+ );
303
+
304
+ return isMultilingual ? (
305
+ <>
306
+ {renderHeader()}
307
+ <Form loading={Boolean(loading) || Boolean(domainsLoading)}>
308
+ {commonForm}
152
309
  </Form>
153
310
  </>
311
+ ) : (
312
+ <Container as={Segment} text>
313
+ {renderHeader()}
314
+ <Form loading={Boolean(loading) || Boolean(domainsLoading)}>
315
+ {commonForm}
316
+ </Form>
317
+ </Container>
154
318
  );
155
319
  };
156
320
 
157
321
  ConceptForm.propTypes = {
158
322
  domainId: PropTypes.number,
159
323
  template: PropTypes.object,
160
- concept: PropTypes.object,
161
- defaultContent: PropTypes.object,
162
- noTranslatableFields: PropTypes.array,
163
- onChangeDomainId: PropTypes.func,
164
- onChangeName: PropTypes.func,
165
- onChangeTemplate: PropTypes.func,
166
- onChangeConceptContent: PropTypes.func,
324
+ i18nConcept: PropTypes.object,
325
+ onSubmit: PropTypes.func,
326
+ setDomainId: PropTypes.func,
327
+ setI18nConcept: PropTypes.func,
328
+ setTemplate: PropTypes.func,
167
329
  actionKey: PropTypes.string,
168
330
  loading: PropTypes.bool,
169
331
  };
@@ -1,12 +1,7 @@
1
1
  import React, { Suspense } from "react";
2
2
  import { waitFor } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
3
  import { render } from "@truedat/test/render";
5
- import {
6
- domainsMock,
7
- multipleTemplatesMock,
8
- singleTemplateMock,
9
- } from "@truedat/test/mocks";
4
+ import { domainsMock, multipleTemplatesMock } from "@truedat/test/mocks";
10
5
  import ConceptForm from "../ConceptForm";
11
6
 
12
7
  jest.mock("@truedat/ai/hooks/useSuggestions", () => {
@@ -38,10 +33,13 @@ const props = {
38
33
  loading: false,
39
34
  defaultContent: {},
40
35
  noTranslatableFields: [],
36
+ i18nConcept: { en: { is_default: true } },
37
+ defaultLang: "en",
41
38
  onChangeDomainId: jest.fn(),
42
39
  onChangeName: jest.fn(),
43
40
  onChangeTemplate: jest.fn(),
44
41
  onChangeConceptContent: jest.fn(),
42
+ setAltLang: jest.fn(),
45
43
  };
46
44
 
47
45
  const state = {
@@ -42,131 +42,138 @@ exports[`<ConceptEdit /> matches the latest snapshot 1`] = `
42
42
  </div>
43
43
  </div>
44
44
  </div>
45
- <h3
46
- class="ui header"
47
- >
48
- concepts.form.aditional_fields
49
- </h3>
50
45
  <div
51
- class="ui segment"
46
+ class="ui grid"
52
47
  >
53
48
  <div
54
- class="field"
55
- data-testid="form-field"
49
+ class="one column row"
56
50
  >
57
- <label>
58
- user
59
- </label>
60
51
  <div
61
- class="dimmable"
52
+ class="column"
62
53
  >
63
54
  <div
64
- class="field"
55
+ class="ui segment"
65
56
  >
66
57
  <div
67
- aria-expanded="false"
68
- class="ui fluid search selection dropdown"
69
- name="user"
70
- role="combobox"
58
+ class="field"
59
+ data-testid="form-field"
71
60
  >
72
- <input
73
- aria-autocomplete="list"
74
- autocomplete="off"
75
- class="search"
76
- tabindex="0"
77
- type="text"
78
- value=""
79
- />
80
- <div
81
- aria-atomic="true"
82
- aria-live="polite"
83
- class="divider default text"
84
- role="alert"
85
- >
86
- fields.dropdown.placeholder
87
- </div>
88
- <i
89
- aria-hidden="true"
90
- class="dropdown icon"
91
- />
61
+ <label>
62
+ user
63
+ </label>
92
64
  <div
93
- aria-multiselectable="false"
94
- class="menu transition"
95
- role="listbox"
65
+ class="dimmable"
96
66
  >
97
67
  <div
98
- aria-checked="false"
99
- aria-selected="true"
100
- class="selected item"
101
- role="option"
102
- style="pointer-events: all;"
103
- >
104
- <span
105
- class="text"
106
- >
107
- selector.no.selection
108
- </span>
109
- </div>
110
- <div
111
- aria-checked="false"
112
- aria-selected="false"
113
- class="item"
114
- role="option"
115
- style="pointer-events: all;"
68
+ class="field"
116
69
  >
117
- <span
118
- class="text"
70
+ <div
71
+ aria-expanded="false"
72
+ class="ui fluid search selection dropdown"
73
+ name="user"
74
+ role="combobox"
119
75
  >
120
- foo_user
121
- </span>
76
+ <input
77
+ aria-autocomplete="list"
78
+ autocomplete="off"
79
+ class="search"
80
+ tabindex="0"
81
+ type="text"
82
+ value=""
83
+ />
84
+ <div
85
+ aria-atomic="true"
86
+ aria-live="polite"
87
+ class="divider default text"
88
+ role="alert"
89
+ >
90
+ fields.dropdown.placeholder
91
+ </div>
92
+ <i
93
+ aria-hidden="true"
94
+ class="dropdown icon"
95
+ />
96
+ <div
97
+ aria-multiselectable="false"
98
+ class="menu transition"
99
+ role="listbox"
100
+ >
101
+ <div
102
+ aria-checked="false"
103
+ aria-selected="true"
104
+ class="selected item"
105
+ role="option"
106
+ style="pointer-events: all;"
107
+ >
108
+ <span
109
+ class="text"
110
+ >
111
+ selector.no.selection
112
+ </span>
113
+ </div>
114
+ <div
115
+ aria-checked="false"
116
+ aria-selected="false"
117
+ class="item"
118
+ role="option"
119
+ style="pointer-events: all;"
120
+ >
121
+ <span
122
+ class="text"
123
+ >
124
+ foo_user
125
+ </span>
126
+ </div>
127
+ <div
128
+ aria-checked="false"
129
+ aria-selected="false"
130
+ class="item"
131
+ role="option"
132
+ style="pointer-events: all;"
133
+ >
134
+ <span
135
+ class="text"
136
+ >
137
+ xyz_user
138
+ </span>
139
+ </div>
140
+ </div>
141
+ </div>
122
142
  </div>
123
143
  <div
124
- aria-checked="false"
125
- aria-selected="false"
126
- class="item"
127
- role="option"
128
- style="pointer-events: all;"
144
+ class="ui inverted dimmer"
129
145
  >
130
- <span
131
- class="text"
146
+ <div
147
+ class="content"
132
148
  >
133
- xyz_user
134
- </span>
149
+ <div
150
+ class="ui loader"
151
+ />
152
+ </div>
135
153
  </div>
136
154
  </div>
137
155
  </div>
138
156
  </div>
139
- <div
140
- class="ui inverted dimmer"
141
- >
142
- <div
143
- class="content"
144
- >
145
- <div
146
- class="ui loader"
147
- />
148
- </div>
149
- </div>
150
157
  </div>
151
158
  </div>
152
159
  </div>
153
- </form>
154
- <div
155
- class="concept-forms-actions actions"
156
- >
157
- <a
158
- class="ui secondary button"
159
- href="/"
160
- role="button"
161
- >
162
- actions.cancel
163
- </a>
164
- <button
165
- class="ui primary button"
160
+ <div
161
+ class="concept-forms-actions actions"
166
162
  >
167
- actions.update
168
- </button>
169
- </div>
163
+ <a
164
+ class="ui secondary button"
165
+ href="/"
166
+ role="button"
167
+ >
168
+ actions.cancel
169
+ </a>
170
+ <button
171
+ class="ui primary button"
172
+ >
173
+ actions.update
174
+ </button>
175
+ </div>
176
+ </form>
170
177
  </div>
171
178
  </div>
172
179
  `;