@truedat/bg 8.13.4 → 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.
Files changed (26) hide show
  1. package/package.json +3 -3
  2. package/src/concepts/api.js +5 -9
  3. package/src/concepts/components/ConceptEdit.js +23 -1
  4. package/src/concepts/components/ConceptForm.js +28 -1
  5. package/src/concepts/components/ConceptRoutes.js +10 -13
  6. package/src/concepts/components/ConceptsUploadButton.js +14 -5
  7. package/src/concepts/components/__tests__/ConceptEdit.dateRestrictions.spec.js +84 -0
  8. package/src/concepts/components/__tests__/ConceptEdit.spec.js +29 -0
  9. package/src/concepts/components/__tests__/ConceptEditLoader.spec.js +159 -0
  10. package/src/concepts/components/__tests__/ConceptForm.spec.js +97 -1
  11. package/src/concepts/components/__tests__/ConceptRoutes.spec.js +11 -6
  12. package/src/concepts/components/__tests__/__snapshots__/ConceptRoutes.spec.js.snap +8 -11
  13. package/src/concepts/hooks/useConcepts.js +9 -4
  14. package/src/concepts/sagas/__tests__/downloadConcepts.spec.js +3 -3
  15. package/src/concepts/sagas/downloadConcepts.js +2 -2
  16. package/src/concepts/selectors/__tests__/messages.spec.js +63 -0
  17. package/src/concepts/selectors/index.js +0 -4
  18. package/src/concepts/selectors/messages.js +20 -3
  19. package/src/concepts/components/ConceptsUploadEvents.js +0 -31
  20. package/src/concepts/components/ConceptsUploadEventsTable.js +0 -183
  21. package/src/concepts/components/__tests__/ConceptsUploadEventsTable.spec.js +0 -100
  22. package/src/concepts/components/__tests__/__snapshots__/ConceptsUploadEventsTable.spec.js.snap +0 -115
  23. package/src/concepts/hooks/useUploadEvents.js +0 -11
  24. package/src/concepts/selectors/__tests__/getConceptUploadEventColumns.spec.js +0 -20
  25. package/src/concepts/selectors/getConceptUploadEventColumns.js +0 -66
  26. package/src/concepts/styles/ConceptsUploadEventColumns.less +0 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/bg",
3
- "version": "8.13.4",
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.4",
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": "4ec142f1c856aa669b232b12250c800a19ab9ace"
93
+ "gitHead": "c0943c8cf31e82a81fdca24d68ef57406fe5bcbc"
94
94
  }
@@ -3,12 +3,11 @@ const API_BUSINESS_CONCEPT_VERSION =
3
3
  const API_BUSINESS_CONCEPT_VERSIONS_ACTIONS =
4
4
  "/api/business_concept_versions/actions";
5
5
  const API_BUSINESS_CONCEPT_VERSIONS_CSV = "/api/business_concept_versions/csv";
6
- const API_BUSINESS_CONCEPT_VERSIONS_XLSX =
7
- "/api/business_concept_versions/xlsx";
6
+ const API_BUSINESS_CONCEPTS_XLSX_DOWNLOAD =
7
+ "/api/business_concepts/xlsx/download";
8
8
  const API_BUSINESS_CONCEPT_VERSIONS_SEARCH =
9
9
  "/api/business_concept_versions/search";
10
- const API_BUSINESS_CONCEPT_VERSIONS_UPLOAD =
11
- "/api/business_concept_versions/upload";
10
+ const API_BUSINESS_CONCEPTS_XLSX_UPLOAD = "/api/business_concepts/xlsx/upload";
12
11
  const API_CONCEPT_ARCHIVE = "/api/business_concepts/:id/versions";
13
12
  const API_CONCEPT_LINKS_DOWNLOAD =
14
13
  "/api/business_concept_versions/links/download";
@@ -17,8 +16,6 @@ const API_CONCEPT_FILTERS = "/api/business_concept_filters/search";
17
16
  const API_CONCEPT_SHARED_DOMAINS = "/api/business_concepts/:id/shared_domains";
18
17
  const API_BUSINESS_CONCEPT_BULK_UPDATE =
19
18
  "/api/business_concept_versions/bulk_update";
20
- const API_BUSINESS_CONCEPT_BULK_UPLOAD =
21
- "/api/business_concepts/bulk_upload_event";
22
19
  const API_BUSINESS_CONCEPT_SET_CONFIDENTIAL =
23
20
  "/api/business_concept_versions/:id/set_confidential";
24
21
  const API_CONCEPT_SUGGESTIONS = "/api/business_concept_versions/suggestions";
@@ -28,15 +25,14 @@ export {
28
25
  API_BUSINESS_CONCEPT_VERSION,
29
26
  API_BUSINESS_CONCEPT_VERSIONS_ACTIONS,
30
27
  API_BUSINESS_CONCEPT_VERSIONS_CSV,
31
- API_BUSINESS_CONCEPT_VERSIONS_XLSX,
28
+ API_BUSINESS_CONCEPTS_XLSX_DOWNLOAD,
32
29
  API_BUSINESS_CONCEPT_VERSIONS_SEARCH,
33
- API_BUSINESS_CONCEPT_VERSIONS_UPLOAD,
30
+ API_BUSINESS_CONCEPTS_XLSX_UPLOAD,
34
31
  API_CONCEPT_ARCHIVE,
35
32
  API_CONCEPT_LINKS_DOWNLOAD,
36
33
  API_CONCEPT_LINKS_UPLOAD,
37
34
  API_CONCEPT_FILTERS,
38
35
  API_CONCEPT_SHARED_DOMAINS,
39
36
  API_BUSINESS_CONCEPT_BULK_UPDATE,
40
- API_BUSINESS_CONCEPT_BULK_UPLOAD,
41
37
  API_BUSINESS_CONCEPT_SET_CONFIDENTIAL,
42
38
  };
@@ -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,
@@ -10,7 +10,8 @@ import {
10
10
  CONCEPT_LINKS_MANAGEMENT,
11
11
  CONCEPT_VERSION,
12
12
  CONCEPTS_BULK_UPDATE,
13
- CONCEPTS_BULK_UPLOAD_EVENTS,
13
+ CONCEPTS_UPLOAD_JOBS,
14
+ CONCEPTS_UPLOAD_JOB,
14
15
  CONCEPTS_NEW,
15
16
  CONCEPTS_PENDING,
16
17
  CONCEPTS_DEPRECATED,
@@ -23,6 +24,8 @@ import {
23
24
  import { useIntl } from "react-intl";
24
25
  import { getSidemenuGlossarySubscopes } from "@truedat/core/selectors/getSidemenuGlossarySubscopes";
25
26
  import { SearchContextProvider } from "@truedat/core/search/SearchContext";
27
+ import UploadJobs from "@truedat/core/components/UploadJobs";
28
+ import UploadJob from "@truedat/core/components/UploadJob";
26
29
  import { useLinksSearch, useLinksFilters } from "../relations/hooks/useLinks";
27
30
  import ConceptLinksApprovals from "../relations/components/ConceptLinksApprovals";
28
31
  import ConceptLinksApprovalResults from "../relations/components/ConceptLinksApprovalResults";
@@ -36,7 +39,6 @@ import Concepts from "./Concepts";
36
39
  import ConceptsBulkUpdate from "./ConceptsBulkUpdate";
37
40
 
38
41
  import ConceptSubscriptionLoader from "./ConceptSubscriptionLoader";
39
- import ConceptsUploadEvents from "./ConceptsUploadEvents";
40
42
 
41
43
  const RelationTagsLoader = React.lazy(
42
44
  () => import("@truedat/lm/components/RelationTagsLoader"),
@@ -165,16 +167,12 @@ const ConceptRoutes = ({
165
167
  </>
166
168
  }
167
169
  />
168
- <Route
169
- // CONCEPTS_BULK_UPLOAD_EVENTS = "/concepts/management/bulk_upload_event";
170
- path="management"
171
- >
172
- <Route
173
- // CONCEPTS_BULK_UPLOAD_EVENTS = "/concepts/management/bulk_upload_event";
174
- path="bulk_upload_event"
175
- element={<ConceptsUploadEvents />}
176
- />
170
+ <Route path="uploadJobs">
171
+ <Route index element={<UploadJobs scope="concepts" />} />
172
+ <Route path=":id" element={<UploadJob scope="concepts" />} />
173
+ </Route>
177
174
 
175
+ <Route path="management">
178
176
  <Route
179
177
  path="approvals/results"
180
178
  element={<ConceptLinksApprovalResults />}
@@ -232,9 +230,8 @@ const ConceptRoutes = ({
232
230
  element={
233
231
  <>
234
232
  <ConceptCrumbs conceptAction="concepts.actions.edit" />
235
- <TemplatesLoader scope="bg" />
236
233
  <ConceptLoader />
237
- {conceptLoaded && templatesLoaded ? <ConceptEdit /> : null}
234
+ {conceptLoaded ? <ConceptEdit /> : null}
238
235
  </>
239
236
  }
240
237
  />
@@ -4,7 +4,7 @@ import { FormattedMessage, useIntl } from "react-intl";
4
4
  import UploadModal from "@truedat/core/components/UploadModal";
5
5
  import { useWebContext } from "@truedat/core/webContext";
6
6
  import { useConceptsUpload } from "../hooks/useConcepts";
7
- import { getUploadAlertMessage } from "../selectors/messages";
7
+ import { getConceptsUploadAlertMessage } from "../selectors/messages";
8
8
 
9
9
  export const ConceptsUploadButton = ({ canAutoPublish }) => {
10
10
  const { formatMessage, locale } = useIntl();
@@ -21,13 +21,16 @@ export const ConceptsUploadButton = ({ canAutoPublish }) => {
21
21
  data.append("lang", locale);
22
22
  triggerUpload(data)
23
23
  .then(({ status, data }) => {
24
- const alertMessage = getUploadAlertMessage({ status, data });
24
+ const alertMessage = getConceptsUploadAlertMessage({ status, data });
25
25
  setAlertMessage(alertMessage);
26
26
  })
27
27
  .catch((error) => {
28
28
  if (error.response) {
29
29
  const { status, data } = error.response;
30
- const alertMessage = getUploadAlertMessage({ status, data });
30
+ const alertMessage = getConceptsUploadAlertMessage({
31
+ status,
32
+ data,
33
+ });
31
34
  setAlertMessage(alertMessage);
32
35
  } else {
33
36
  setAlertMessage({ error: true, content: error.message });
@@ -60,13 +63,19 @@ export const ConceptsUploadButton = ({ canAutoPublish }) => {
60
63
  handleSubmit={(data) => {
61
64
  triggerUpload(data)
62
65
  .then(({ status, data }) => {
63
- const alertMessage = getUploadAlertMessage({ status, data });
66
+ const alertMessage = getConceptsUploadAlertMessage({
67
+ status,
68
+ data,
69
+ });
64
70
  setAlertMessage(alertMessage);
65
71
  })
66
72
  .catch((error) => {
67
73
  if (error.response) {
68
74
  const { status, data } = error.response;
69
- const alertMessage = getUploadAlertMessage({ status, data });
75
+ const alertMessage = getConceptsUploadAlertMessage({
76
+ status,
77
+ data,
78
+ });
70
79
  setAlertMessage(alertMessage);
71
80
  } else {
72
81
  setAlertMessage(error.message);
@@ -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
+ });