@truedat/bg 8.13.5 → 8.13.6

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/bg",
3
- "version": "8.13.5",
3
+ "version": "8.13.6",
4
4
  "description": "Truedat Web Business Glossary",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -57,7 +57,7 @@
57
57
  "@testing-library/jest-dom": "^6.6.3",
58
58
  "@testing-library/react": "^16.3.0",
59
59
  "@testing-library/user-event": "^14.6.1",
60
- "@truedat/test": "8.13.5",
60
+ "@truedat/test": "8.13.6",
61
61
  "identity-obj-proxy": "^3.0.0",
62
62
  "jest": "^29.7.0",
63
63
  "redux-saga-test-plan": "^4.0.6"
@@ -90,5 +90,5 @@
90
90
  "semantic-ui-react": "^3.0.0-beta.2",
91
91
  "swr": "^2.3.3"
92
92
  },
93
- "gitHead": "9b87cbb7143f20b305b84077c1fcf004e5b27726"
93
+ "gitHead": "c0943c8cf31e82a81fdca24d68ef57406fe5bcbc"
94
94
  }
@@ -7,6 +7,7 @@ import { applyTemplate } from "@truedat/df/utils";
7
7
  import { useLanguage } from "@truedat/core/i18n";
8
8
  import { splitTranslatableFields } from "@truedat/core/services/i18nContent";
9
9
  import { useTemplate } from "@truedat/core/hooks";
10
+ import { Loading, TemplateLoadError } from "@truedat/core/components";
10
11
  import { conceptAction } from "../routines";
11
12
  import ConceptForm from "./ConceptForm";
12
13
 
@@ -25,6 +26,10 @@ export const ConceptEdit = ({
25
26
  const { domain } = concept;
26
27
  const [i18nConcept, setI18nConcept] = useState(content);
27
28
  const { defaultLang, altLangs } = useLanguage();
29
+ const [originalI18nConcept] = useState(() => ({
30
+ ...concept.i18n_content,
31
+ [defaultLang]: { content: concept.dynamic_content },
32
+ }));
28
33
 
29
34
  const templateFields = splitTranslatableFields(template);
30
35
  const translatableKeys = _.keys(templateFields.translatable);
@@ -66,6 +71,7 @@ export const ConceptEdit = ({
66
71
  resourceId={concept.business_concept_id}
67
72
  template={template}
68
73
  i18nConcept={i18nConcept}
74
+ originalI18nConcept={originalI18nConcept}
69
75
  loading={templateLoading || localesLoading}
70
76
  setI18nConcept={setI18nConcept}
71
77
  onSubmit={handleSubmit}
@@ -91,12 +97,28 @@ export const ConceptEditLoader = (props) => {
91
97
  const { dynamic_content: propContent, name: propName, domain } = concept;
92
98
 
93
99
  const { locales } = useLanguage();
94
- const { data, loading: templateLoading } = useTemplate({
100
+ const {
101
+ data,
102
+ error: templateError,
103
+ loading: templateLoading,
104
+ refetch: refetchTemplate,
105
+ } = useTemplate({
95
106
  name: templateName,
96
107
  domainIds: [domain?.id],
97
108
  });
98
109
 
99
110
  const template = _.prop("template")(data);
111
+
112
+ if (templateError && !templateLoading) {
113
+ return <TemplateLoadError onRetry={() => refetchTemplate()} />;
114
+ }
115
+
116
+ if (templateLoading && !template) return <Loading />;
117
+
118
+ if (!template) {
119
+ return <TemplateLoadError onRetry={() => refetchTemplate()} />;
120
+ }
121
+
100
122
  const processedContent = applyTemplate(template);
101
123
 
102
124
  const translatableKeys = _.keys(
@@ -22,15 +22,29 @@ import { useAvailabilityCheck as useTranslatableCheck } from "@truedat/ai/hooks/
22
22
  import { splitTranslatableFields } from "@truedat/core/services/i18nContent";
23
23
  import { TranslationModal } from "@truedat/ai/components";
24
24
  import { useWebContext } from "@truedat/core/webContext";
25
+ import { validateContent } from "@truedat/df/utils";
25
26
 
26
27
  const SelectDynamicFormWithTranslations = lazy(
27
28
  () => import("@truedat/df/components/SelectDynamicFormWithTranslations"),
28
29
  );
29
30
 
31
+ export const conceptHasDateRestrictionErrors = (
32
+ template,
33
+ i18nConcept,
34
+ originalI18nConcept,
35
+ ) =>
36
+ Object.entries(i18nConcept || {}).some(([lang, { content } = {}]) =>
37
+ validateContent(
38
+ template,
39
+ originalI18nConcept?.[lang]?.content,
40
+ )(content || {}).some(({ error }) => error === "date out of range"),
41
+ );
42
+
30
43
  const ConceptForm = ({
31
44
  actionKey,
32
45
  domainId,
33
46
  i18nConcept,
47
+ originalI18nConcept,
34
48
  loading,
35
49
  onSubmit,
36
50
  resourceId,
@@ -240,7 +254,16 @@ const ConceptForm = ({
240
254
  )(i18nConcept);
241
255
  const validTemplate = () =>
242
256
  _.isObject(template) && isNonEmptyString(template?.name);
243
- const isInvalid = !validDomain() || !validNames() || !validTemplate();
257
+ const hasDateRestrictionErrors = conceptHasDateRestrictionErrors(
258
+ template,
259
+ i18nConcept,
260
+ originalI18nConcept,
261
+ );
262
+ const isInvalid =
263
+ !validDomain() ||
264
+ !validNames() ||
265
+ !validTemplate() ||
266
+ hasDateRestrictionErrors;
244
267
 
245
268
  const renderDomainSelector = () =>
246
269
  actionKey === "create" ? (
@@ -274,6 +297,9 @@ const ConceptForm = ({
274
297
  required
275
298
  header={renderFormHeader()}
276
299
  i18nContent={_.mapValues("content", i18nConcept)}
300
+ originalI18nContent={
301
+ originalI18nConcept && _.mapValues("content", originalI18nConcept)
302
+ }
277
303
  name={template?.name}
278
304
  actionKey={actionKey}
279
305
  onChangeContent={handleI18nConceptContentChange}
@@ -354,6 +380,7 @@ ConceptForm.propTypes = {
354
380
  domainId: PropTypes.number,
355
381
  template: PropTypes.object,
356
382
  i18nConcept: PropTypes.object,
383
+ originalI18nConcept: PropTypes.object,
357
384
  onSubmit: PropTypes.func,
358
385
  resourceId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
359
386
  setDomainId: PropTypes.func,
@@ -230,9 +230,8 @@ const ConceptRoutes = ({
230
230
  element={
231
231
  <>
232
232
  <ConceptCrumbs conceptAction="concepts.actions.edit" />
233
- <TemplatesLoader scope="bg" />
234
233
  <ConceptLoader />
235
- {conceptLoaded && templatesLoaded ? <ConceptEdit /> : null}
234
+ {conceptLoaded ? <ConceptEdit /> : null}
236
235
  </>
237
236
  }
238
237
  />
@@ -0,0 +1,84 @@
1
+ import { act, render } from "@testing-library/react";
2
+ import ConceptForm from "../ConceptForm";
3
+ import { ConceptEdit } from "../ConceptEdit";
4
+
5
+ jest.mock("../ConceptForm", () => ({
6
+ __esModule: true,
7
+ default: jest.fn(() => null),
8
+ }));
9
+
10
+ jest.mock("@truedat/core/i18n", () => ({
11
+ useLanguage: () => ({ defaultLang: "en", altLangs: ["es"] }),
12
+ }));
13
+
14
+ const savedDate = { value: "2027-01-01", origin: "user" };
15
+ const concept = {
16
+ domain: { id: 1 },
17
+ dynamic_content: { fecha: savedDate },
18
+ i18n_content: { es: { content: { fecha: savedDate } } },
19
+ };
20
+ const props = {
21
+ concept,
22
+ content: {
23
+ en: { content: concept.dynamic_content },
24
+ es: {
25
+ content: {
26
+ ...concept.i18n_content.es.content,
27
+ newDate: { value: "2027-01-01", origin: "default" },
28
+ },
29
+ },
30
+ },
31
+ template: {},
32
+ conceptAction: jest.fn(),
33
+ processedContent: (content) => content,
34
+ };
35
+
36
+ const formProps = () => ConceptForm.mock.calls.at(-1)[0];
37
+
38
+ describe("ConceptEdit date restriction originals", () => {
39
+ beforeEach(() => jest.clearAllMocks());
40
+
41
+ it("uses persisted default-language dates as the baseline", () => {
42
+ render(<ConceptEdit {...props} />);
43
+
44
+ expect(formProps().originalI18nConcept.en.content).toEqual(
45
+ concept.dynamic_content,
46
+ );
47
+ });
48
+
49
+ it("does not exempt defaults inserted into translated content", () => {
50
+ render(<ConceptEdit {...props} />);
51
+
52
+ expect(formProps().originalI18nConcept.es.content).not.toHaveProperty(
53
+ "newDate",
54
+ );
55
+ });
56
+
57
+ it("keeps the baseline when editable content changes", () => {
58
+ render(<ConceptEdit {...props} />);
59
+
60
+ act(() => {
61
+ formProps().setI18nConcept({
62
+ en: { content: { fecha: { value: "2027-02-01", origin: "user" } } },
63
+ });
64
+ });
65
+
66
+ expect(formProps().originalI18nConcept.en.content.fecha).toEqual(savedDate);
67
+ });
68
+
69
+ it("keeps the edit-session baseline across refreshed source props", () => {
70
+ const { rerender } = render(<ConceptEdit {...props} />);
71
+
72
+ rerender(
73
+ <ConceptEdit
74
+ {...props}
75
+ concept={{
76
+ ...concept,
77
+ dynamic_content: { fecha: { value: "2027-02-01", origin: "user" } },
78
+ }}
79
+ />,
80
+ );
81
+
82
+ expect(formProps().originalI18nConcept.en.content.fecha).toEqual(savedDate);
83
+ });
84
+ });
@@ -1,6 +1,8 @@
1
+ import { fireEvent } from "@testing-library/react";
1
2
  import { render } from "@truedat/test/render";
2
3
  import { waitFor } from "@testing-library/react";
3
4
  import { LangProviderWrapper } from "@truedat/core/i18n";
5
+ import { useTemplate } from "@truedat/core/hooks";
4
6
  import ConceptEdit from "../ConceptEdit";
5
7
 
6
8
  jest.mock("@truedat/ai/hooks/useTranslations", () => {
@@ -58,6 +60,7 @@ jest.mock("@truedat/core/hooks", () => ({
58
60
  useTemplate: jest.fn(() => ({
59
61
  loading: false,
60
62
  data: { template },
63
+ refetch: jest.fn(),
61
64
  })),
62
65
  }));
63
66
 
@@ -126,10 +129,36 @@ describe("<ConceptEdit />", () => {
126
129
  const props = {
127
130
  concept,
128
131
  };
132
+
133
+ beforeEach(() => {
134
+ useTemplate.mockReturnValue({
135
+ loading: false,
136
+ data: { template },
137
+ refetch: jest.fn(),
138
+ });
139
+ });
140
+
129
141
  it("matches the latest snapshot", async () => {
130
142
  const { container, queryByText } = renderComponent(props);
131
143
 
132
144
  await waitFor(() => expect(queryByText(/foo_user/i)).toBeInTheDocument());
133
145
  expect(container).toMatchSnapshot();
134
146
  });
147
+
148
+ it("shows a visible retry message when its template fails to load", () => {
149
+ const refetch = jest.fn();
150
+ useTemplate.mockReturnValue({
151
+ loading: false,
152
+ error: new Error("template request failed"),
153
+ refetch,
154
+ });
155
+
156
+ const { getByText, getByRole } = renderComponent({
157
+ concept: { ...concept, type: "template1" },
158
+ });
159
+
160
+ expect(getByText(/templates could not be loaded/i)).toBeInTheDocument();
161
+ fireEvent.click(getByRole("button", { name: /retry/i }));
162
+ expect(refetch).toHaveBeenCalledTimes(1);
163
+ });
135
164
  });
@@ -0,0 +1,159 @@
1
+ import { fireEvent, render } from "@testing-library/react";
2
+ import { render as renderWithProviders } from "@truedat/test/render";
3
+ import { LangProviderWrapper } from "@truedat/core/i18n";
4
+ import { useTemplate } from "@truedat/core/hooks";
5
+ import ConceptForm from "../ConceptForm";
6
+ import { ConceptEditLoader } from "../ConceptEdit";
7
+
8
+ jest.mock("@truedat/core/hooks", () => ({
9
+ useTemplate: jest.fn(),
10
+ }));
11
+
12
+ jest.mock("../ConceptForm", () => ({
13
+ __esModule: true,
14
+ default: jest.fn(() => null),
15
+ }));
16
+
17
+ const template = {
18
+ name: "concept-template",
19
+ content: [
20
+ {
21
+ fields: [
22
+ { name: "description", widget: "string" },
23
+ { name: "owner", widget: "dropdown" },
24
+ ],
25
+ },
26
+ ],
27
+ };
28
+
29
+ const concept = {
30
+ business_concept_id: 42,
31
+ type: template.name,
32
+ name: "BCV name",
33
+ domain: { id: 7 },
34
+ dynamic_content: {
35
+ description: { value: "BCV description", origin: "user" },
36
+ owner: { value: "alice", origin: "user" },
37
+ },
38
+ i18n_content: {
39
+ es: {
40
+ name: "Nombre traducido",
41
+ content: {
42
+ description: { value: "Descripción BCV", origin: "user" },
43
+ },
44
+ },
45
+ },
46
+ };
47
+
48
+ describe("ConceptEditLoader", () => {
49
+ const props = {
50
+ concept,
51
+ templateName: template.name,
52
+ conceptAction: jest.fn(),
53
+ action: {},
54
+ };
55
+
56
+ beforeEach(() => {
57
+ jest.clearAllMocks();
58
+ });
59
+
60
+ it("waits for the BCV template before initializing the form with BCV content", () => {
61
+ useTemplate.mockReturnValueOnce({ loading: true }).mockReturnValueOnce({
62
+ loading: false,
63
+ data: { template },
64
+ refetch: jest.fn(),
65
+ });
66
+
67
+ const { rerender } = render(
68
+ <LangProviderWrapper langs={["en", "es"]}>
69
+ <ConceptEditLoader {...props} />
70
+ </LangProviderWrapper>,
71
+ );
72
+
73
+ expect(ConceptForm).not.toHaveBeenCalled();
74
+
75
+ rerender(
76
+ <LangProviderWrapper langs={["en", "es"]}>
77
+ <ConceptEditLoader {...props} />
78
+ </LangProviderWrapper>,
79
+ );
80
+
81
+ expect(ConceptForm).toHaveBeenCalledTimes(1);
82
+ const formProps = ConceptForm.mock.calls[0][0];
83
+ expect(formProps.i18nConcept.en).toMatchObject({
84
+ name: "BCV name",
85
+ content: concept.dynamic_content,
86
+ });
87
+ expect(formProps.i18nConcept.es).toMatchObject({
88
+ name: "Nombre traducido",
89
+ content: {
90
+ description: { value: "Descripción BCV", origin: "user" },
91
+ },
92
+ });
93
+ expect(formProps.template).toEqual(template);
94
+ });
95
+
96
+ it("shows a retry message when the completed query has no template", () => {
97
+ useTemplate.mockReturnValue({
98
+ loading: false,
99
+ data: { template: null },
100
+ refetch: jest.fn(),
101
+ });
102
+
103
+ const rendered = renderWithProviders(
104
+ <LangProviderWrapper langs={["en", "es"]}>
105
+ <ConceptEditLoader {...props} />
106
+ </LangProviderWrapper>,
107
+ );
108
+
109
+ expect(
110
+ rendered.getByText(/templates could not be loaded/i),
111
+ ).toBeInTheDocument();
112
+ expect(
113
+ rendered.getByRole("button", { name: /retry/i }),
114
+ ).toBeInTheDocument();
115
+ expect(ConceptForm).not.toHaveBeenCalled();
116
+ });
117
+
118
+ it("loads the form after retrying a failed template query", () => {
119
+ const refetch = jest.fn(() => {
120
+ useTemplate.mockReturnValue({
121
+ loading: true,
122
+ refetch,
123
+ });
124
+ });
125
+ useTemplate.mockReturnValue({
126
+ loading: false,
127
+ error: new Error("template request failed"),
128
+ refetch,
129
+ });
130
+ const component = () => (
131
+ <LangProviderWrapper langs={["en", "es"]}>
132
+ <ConceptEditLoader {...props} />
133
+ </LangProviderWrapper>
134
+ );
135
+ const rendered = renderWithProviders(component());
136
+
137
+ fireEvent.click(rendered.getByRole("button", { name: /retry/i }));
138
+ expect(refetch).toHaveBeenCalledTimes(1);
139
+
140
+ rendered.rerender(component());
141
+ expect(ConceptForm).not.toHaveBeenCalled();
142
+ expect(
143
+ rendered.queryByText(/templates could not be loaded/i),
144
+ ).not.toBeInTheDocument();
145
+
146
+ useTemplate.mockReturnValue({
147
+ loading: false,
148
+ data: { template },
149
+ refetch,
150
+ });
151
+ rendered.rerender(component());
152
+
153
+ expect(ConceptForm).toHaveBeenCalled();
154
+ expect(ConceptForm.mock.calls[0][0].i18nConcept.en).toMatchObject({
155
+ name: concept.name,
156
+ content: concept.dynamic_content,
157
+ });
158
+ });
159
+ });
@@ -1,7 +1,7 @@
1
1
  import { waitFor } from "@testing-library/react";
2
2
  import { render, waitForLoad } from "@truedat/test/render";
3
3
  import { domainsMock, multipleTemplatesMock } from "@truedat/test/mocks";
4
- import ConceptForm from "../ConceptForm";
4
+ import ConceptForm, { conceptHasDateRestrictionErrors } from "../ConceptForm";
5
5
 
6
6
  jest.mock("@truedat/ai/hooks/useSuggestions", () => {
7
7
  const originalModule = jest.requireActual("@truedat/ai/hooks/useSuggestions");
@@ -76,7 +76,103 @@ const props = {
76
76
  const templateVariables = { scope: "bg", domainIds: [1] };
77
77
  const domainVariables = { action: "createBusinessConcept" };
78
78
 
79
+ const dateTemplate = {
80
+ content: [
81
+ {
82
+ name: "dates",
83
+ fields: [
84
+ {
85
+ name: "fecha",
86
+ type: "date",
87
+ restrictions: { resolved: { min: null, max: "2026-10-15" } },
88
+ },
89
+ ],
90
+ },
91
+ ],
92
+ };
93
+
94
+ const savedConcept = {
95
+ en: { content: { fecha: { value: "2026-10-16", origin: "user" } } },
96
+ };
97
+
79
98
  describe("<ConceptForm />", () => {
99
+ it("allows an unchanged existing date outside the latest restriction", () => {
100
+ expect(
101
+ conceptHasDateRestrictionErrors(
102
+ dateTemplate,
103
+ {
104
+ en: {
105
+ name: "Updated name",
106
+ content: { fecha: { value: "2026-10-16", origin: "user" } },
107
+ },
108
+ },
109
+ savedConcept,
110
+ ),
111
+ ).toBe(false);
112
+ });
113
+
114
+ it("rejects a modified date outside the latest restriction", () => {
115
+ expect(
116
+ conceptHasDateRestrictionErrors(
117
+ dateTemplate,
118
+ { en: { content: { fecha: { value: "2026-10-17", origin: "user" } } } },
119
+ savedConcept,
120
+ ),
121
+ ).toBe(true);
122
+ });
123
+
124
+ it("does not exempt a new language using another language's saved date", () => {
125
+ expect(
126
+ conceptHasDateRestrictionErrors(
127
+ dateTemplate,
128
+ {
129
+ ...savedConcept,
130
+ es: { content: { fecha: { value: "2026-10-16", origin: "user" } } },
131
+ },
132
+ savedConcept,
133
+ ),
134
+ ).toBe(true);
135
+ });
136
+
137
+ it("detects an invalid date so Save can be disabled", () => {
138
+ const template = {
139
+ content: [
140
+ {
141
+ name: "dates",
142
+ fields: [
143
+ {
144
+ name: "fecha",
145
+ type: "date",
146
+ restrictions: {
147
+ resolved: { min: null, max: "2026-10-15" },
148
+ },
149
+ },
150
+ ],
151
+ },
152
+ ],
153
+ };
154
+
155
+ expect(
156
+ conceptHasDateRestrictionErrors(template, {
157
+ en: {
158
+ content: {
159
+ fecha: { value: "2026-10-16", origin: "user" },
160
+ },
161
+ },
162
+ }),
163
+ ).toBe(true);
164
+
165
+ expect(
166
+ conceptHasDateRestrictionErrors(template, {
167
+ en: {
168
+ content: {
169
+ fecha: { value: "2026-10-15", origin: "user" },
170
+ },
171
+ },
172
+ }),
173
+ ).toBe(false);
174
+ });
175
+
80
176
  describe("with multiple templates", () => {
81
177
  const renderOpts = {
82
178
  mocks: [
@@ -152,10 +152,12 @@ describe("<ConceptRoutes />", () => {
152
152
  state: {
153
153
  concept: { business_concept_id: "123" },
154
154
  templatesLoading: false,
155
- templates: { some: "template" },
155
+ templates: [],
156
156
  },
157
157
  });
158
158
  await waitForLoad(rendered);
159
+ expect(rendered.getByText("ConceptEdit")).toBeInTheDocument();
160
+ expect(rendered.queryByText("TemplatesLoader")).not.toBeInTheDocument();
159
161
  expect(rendered.container).toMatchSnapshot();
160
162
  });
161
163
 
@@ -19,9 +19,6 @@ exports[`<ConceptRoutes /> renders correctly with concept edit route 1`] = `
19
19
  <div>
20
20
  ConceptCrumbs
21
21
  </div>
22
- <div>
23
- TemplatesLoader
24
- </div>
25
22
  <div>
26
23
  ConceptLoader
27
24
  </div>