@truedat/df 8.3.5 → 8.4.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/df",
3
- "version": "8.3.5",
3
+ "version": "8.4.1",
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.3.5",
54
+ "@truedat/test": "8.4.1",
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.3.5",
61
+ "@truedat/core": "8.4.1",
62
62
  "axios": "^1.13.5",
63
63
  "graphql": "^16.11.0",
64
64
  "is-hotkey": "^0.2.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": "12d1969c3403861d360fbdc24fd270ab1fece070"
90
+ "gitHead": "fc56d67c36dfe2d5db692b26ebdd2071b7518e1f"
91
91
  }
@@ -2,7 +2,7 @@ import _ from "lodash";
2
2
  import PropTypes from "prop-types";
3
3
  import { useIntl } from "react-intl";
4
4
  import { Icon, Label, Table } from "semantic-ui-react";
5
- import { SafeLink, RichTextEditor } from "@truedat/core/components";
5
+ import { MarkdownReader, SafeLink, RichTextEditor } from "@truedat/core/components";
6
6
  import ImagePreview from "./widgets/ImagePreview";
7
7
  import SystemPreview from "./widgets/SystemPreview";
8
8
  import "../styles/fieldGroupDetail.less";
@@ -56,6 +56,8 @@ export const FieldViewerValue = ({
56
56
  );
57
57
  case "enriched_text":
58
58
  return <RichTextEditor readOnly value={value} />;
59
+ case "markdown":
60
+ return <MarkdownReader content={value} />;
59
61
  case "table":
60
62
  return (
61
63
  <Table style={{ marginTop: "10px" }} celled striped compact fixed>
@@ -1,6 +1,31 @@
1
+ import { fireEvent, waitFor } from "@testing-library/react";
1
2
  import { render, waitForLoad } from "@truedat/test/render";
2
3
  import { DynamicForm } from "../DynamicForm";
3
4
 
5
+ jest.mock("../widgets/FieldByWidget", () => ({
6
+ __esModule: true,
7
+ default: ({ field, onChange }) => {
8
+ if (field?.widget !== "markdown") {
9
+ const actual = jest.requireActual("../widgets/FieldByWidget");
10
+ return actual.default({ field, onChange });
11
+ }
12
+
13
+ return (
14
+ <button
15
+ type="button"
16
+ onClick={() =>
17
+ onChange?.(null, {
18
+ name: field?.name,
19
+ value: `${field?.value || ""} updated`,
20
+ })
21
+ }
22
+ >
23
+ Edit markdown
24
+ </button>
25
+ );
26
+ },
27
+ }));
28
+
4
29
  describe("<DynamicForm />", () => {
5
30
  const applyTemplate = jest.fn(),
6
31
  onChange = jest.fn(),
@@ -162,4 +187,68 @@ describe("<DynamicForm />", () => {
162
187
  await waitForLoad(rendered);
163
188
  expect(rendered.container.querySelector(".field-group-segment")).toBeNull();
164
189
  });
190
+
191
+ it("sends updated markdown string to onChange payload", async () => {
192
+ const applyTemplate = jest.fn((content) => content);
193
+ const onChange = jest.fn();
194
+
195
+ const template = {
196
+ scope: "bg",
197
+ content: [
198
+ {
199
+ name: "g1",
200
+ fields: [
201
+ {
202
+ name: "md-field",
203
+ label: "md-label",
204
+ type: "markdown",
205
+ widget: "markdown",
206
+ cardinality: "1",
207
+ },
208
+ ],
209
+ },
210
+ ],
211
+ };
212
+
213
+ const initialMarkdown = `## Initial
214
+
215
+ _Now you can use:_
216
+
217
+ * Lists
218
+
219
+ [Links](https://example.com)
220
+
221
+ **Bold**
222
+
223
+ _Italic_`;
224
+
225
+ const content = {
226
+ "md-field": { value: initialMarkdown, origin: "user" },
227
+ };
228
+
229
+ const rendered = render(
230
+ <DynamicForm
231
+ applyTemplate={applyTemplate}
232
+ content={content}
233
+ onChange={onChange}
234
+ template={template}
235
+ />,
236
+ );
237
+
238
+ await waitForLoad(rendered);
239
+ onChange.mockClear();
240
+
241
+ fireEvent.click(rendered.getByText(/edit markdown/i));
242
+
243
+ await waitFor(() => {
244
+ expect(onChange).toHaveBeenCalled();
245
+ });
246
+
247
+ expect(onChange).toHaveBeenLastCalledWith({
248
+ "md-field": {
249
+ value: `${initialMarkdown} updated`,
250
+ origin: "user",
251
+ },
252
+ });
253
+ });
165
254
  });
@@ -0,0 +1,40 @@
1
+ import { render } from "@truedat/test/render";
2
+ import { DynamicFormViewer } from "../DynamicFormViewer";
3
+ import { markdownToHtml } from "@truedat/core/components/Markdown";
4
+
5
+ describe("<DynamicFormViewer />", () => {
6
+ it("renders markdown content through MarkdownReader", () => {
7
+ const markdown = "## Title\n\n**Hello** world\n\n[link](https://example.com)";
8
+
9
+ const template = {
10
+ scope: "bg",
11
+ content: [
12
+ {
13
+ name: "g1",
14
+ fields: [
15
+ {
16
+ name: "md-field",
17
+ label: "md-label",
18
+ type: "markdown",
19
+ widget: "markdown",
20
+ cardinality: "1",
21
+ },
22
+ ],
23
+ },
24
+ ],
25
+ };
26
+
27
+ const content = {
28
+ "md-field": { value: markdown, origin: "user" },
29
+ };
30
+
31
+ const { container } = render(
32
+ <DynamicFormViewer template={template} content={content} />
33
+ );
34
+
35
+ const reader = container.querySelector(".markdown-reader");
36
+ expect(reader).toBeInTheDocument();
37
+ expect(reader.innerHTML).toBe(markdownToHtml(markdown));
38
+ });
39
+ });
40
+
@@ -0,0 +1,122 @@
1
+ import { fireEvent, waitFor } from "@testing-library/react";
2
+ import { render } from "@truedat/test/render";
3
+ import { DynamicFormWithTranslations } from "../DynamicFormWithTranslations";
4
+
5
+ jest.mock("@truedat/core/i18n", () => {
6
+ const originalModule = jest.requireActual("@truedat/core/i18n");
7
+ return {
8
+ __esModule: true,
9
+ ...originalModule,
10
+ useLanguage: () => ({
11
+ defaultLang: "es",
12
+ altLangs: ["es", "en"],
13
+ requiredLangs: ["es", "en"],
14
+ enabledLangs: ["es", "en"],
15
+ isMultilingual: true,
16
+ setAltLang: jest.fn(),
17
+ altLang: "en",
18
+ getMessagesForLang: () => ({}),
19
+ lang: "es",
20
+ loading: false,
21
+ locales: [],
22
+ localesError: null,
23
+ mutate: jest.fn(),
24
+ onIntlError: () => {},
25
+ }),
26
+ };
27
+ });
28
+
29
+ jest.mock("../widgets/FieldByWidget", () => ({
30
+ __esModule: true,
31
+ default: ({ field, onChange }) => (
32
+ <button
33
+ type="button"
34
+ onClick={() =>
35
+ onChange?.(null, {
36
+ name: field?.name,
37
+ value: `${field?.value || ""} updated`,
38
+ })
39
+ }
40
+ >
41
+ Edit markdown
42
+ </button>
43
+ ),
44
+ }));
45
+
46
+ jest.mock("@truedat/core/components", () => {
47
+ const originalModule = jest.requireActual("@truedat/core/components");
48
+ return {
49
+ __esModule: true,
50
+ ...originalModule,
51
+ LanguagesTabs: () => null,
52
+ };
53
+ });
54
+
55
+ describe("<DynamicFormWithTranslations /> markdown", () => {
56
+ it("calls onChange with the updated markdown per language", async () => {
57
+ const applyTemplate = jest.fn((content) => content);
58
+ const onChange = jest.fn();
59
+
60
+ const template = {
61
+ scope: "bg",
62
+ name: "tmpl",
63
+ content: [
64
+ {
65
+ name: "g1",
66
+ fields: [
67
+ {
68
+ name: "md-field",
69
+ label: "md-label",
70
+ type: "markdown",
71
+ widget: "markdown",
72
+ cardinality: "1",
73
+ },
74
+ ],
75
+ },
76
+ ],
77
+ };
78
+
79
+ const i18nContent = {
80
+ es: { "md-field": { value: "## ES", origin: "user" } },
81
+ en: { "md-field": { value: "## EN", origin: "user" } },
82
+ };
83
+
84
+ const rendered = render(
85
+ <DynamicFormWithTranslations
86
+ applyTemplate={applyTemplate}
87
+ i18nContent={i18nContent}
88
+ onChange={onChange}
89
+ template={template}
90
+ />,
91
+ );
92
+
93
+ const buttons = rendered.getAllByText(/edit markdown/i);
94
+ expect(buttons.length).toBe(2);
95
+
96
+ fireEvent.click(buttons[0]);
97
+ fireEvent.click(buttons[1]);
98
+
99
+ await waitFor(() => {
100
+ expect(onChange).toHaveBeenCalled();
101
+ });
102
+
103
+ const calls = onChange.mock.calls;
104
+ const esCall = calls.find(([lang]) => lang === "es");
105
+ const enCall = calls.find(([lang]) => lang === "en");
106
+
107
+ expect(esCall).toBeTruthy();
108
+ expect(esCall[1]).toEqual({
109
+ content: {
110
+ "md-field": { value: "## ES updated", origin: "user" },
111
+ },
112
+ });
113
+
114
+ expect(enCall).toBeTruthy();
115
+ expect(enCall[1]).toEqual({
116
+ content: {
117
+ "md-field": { value: "## EN updated", origin: "user" },
118
+ },
119
+ });
120
+ });
121
+ });
122
+
@@ -1,4 +1,6 @@
1
+ import { waitFor } from "@testing-library/react";
1
2
  import { render } from "@truedat/test/render";
3
+ import { markdownToHtml } from "@truedat/core/components/Markdown";
2
4
  import { EditableDynamicFieldValue } from "../EditableDynamicFieldValue";
3
5
 
4
6
  describe("<EditableDynamicFieldValue />", () => {
@@ -33,4 +35,75 @@ describe("<EditableDynamicFieldValue />", () => {
33
35
  );
34
36
  expect(wrapper).toMatchSnapshot();
35
37
  });
38
+
39
+ it("renders MarkdownReader when not editing", () => {
40
+ const markdown = "## Title\n\n**Hello** world";
41
+
42
+ const getEditFunctions = ({
43
+ editingField,
44
+ setEditingField = jest.fn(),
45
+ onChange = jest.fn(),
46
+ }) => ({
47
+ onChange,
48
+ onCancel: jest.fn(),
49
+ onSubmit: jest.fn(),
50
+ editingField,
51
+ setEditingField,
52
+ updateStatus: "none",
53
+ });
54
+
55
+ const props = {
56
+ label: "md-label",
57
+ name: "md-field",
58
+ value: { value: markdown, origin: "user" },
59
+ type: "markdown",
60
+ values: null,
61
+ widget: "markdown",
62
+ isSecret: false,
63
+ isChanged: false,
64
+ editFunctions: getEditFunctions({ editingField: "other" }),
65
+ };
66
+
67
+ const { container } = render(<EditableDynamicFieldValue {...props} />);
68
+ const reader = container.querySelector(".markdown-reader");
69
+ expect(reader).toBeInTheDocument();
70
+ expect(reader.innerHTML).toBe(markdownToHtml(markdown));
71
+ });
72
+
73
+ it("renders MarkdownEditor when editing", async () => {
74
+ const markdown = "## Title\n\n**Hello** world";
75
+
76
+ const getEditFunctions = ({
77
+ editingField,
78
+ setEditingField = jest.fn(),
79
+ onChange = jest.fn(),
80
+ }) => ({
81
+ onChange,
82
+ onCancel: jest.fn(),
83
+ onSubmit: jest.fn(),
84
+ editingField,
85
+ setEditingField,
86
+ updateStatus: "none",
87
+ });
88
+
89
+ const props = {
90
+ label: "md-label",
91
+ name: "md-field",
92
+ value: { value: markdown, origin: "user" },
93
+ type: "markdown",
94
+ values: null,
95
+ widget: "markdown",
96
+ isSecret: false,
97
+ isChanged: false,
98
+ editFunctions: getEditFunctions({ editingField: "md-field" }),
99
+ };
100
+
101
+ const { container } = render(<EditableDynamicFieldValue {...props} />);
102
+
103
+ await waitFor(() => {
104
+ expect(container.querySelector(".markdown-editor")).toBeInTheDocument();
105
+ });
106
+
107
+ expect(container.querySelector(".tiptap")).toBeInTheDocument();
108
+ });
36
109
  });
@@ -1,5 +1,6 @@
1
1
  import { render } from "@truedat/test/render";
2
2
  import { FieldViewerValue } from "../FieldViewerValue";
3
+ import { markdownToHtml } from "@truedat/core/components/Markdown";
3
4
 
4
5
  describe("<FieldViewerValue />", () => {
5
6
  const testTypes = [
@@ -78,4 +79,22 @@ describe("<FieldViewerValue />", () => {
78
79
  const { container } = render(<FieldViewerValue {...props} />);
79
80
  expect(container).toMatchSnapshot();
80
81
  });
82
+
83
+ it("renders MarkdownReader for type=markdown", () => {
84
+ const markdown = "## Title\n\n**Hello** world\n\n[link](https://example.com)";
85
+ const { container } = render(
86
+ <FieldViewerValue type="markdown" value={markdown} label="md" widget="markdown" />,
87
+ );
88
+
89
+ const reader = container.querySelector(".markdown-reader");
90
+ expect(reader).toBeInTheDocument();
91
+ expect(reader.innerHTML).toBe(markdownToHtml(markdown));
92
+ });
93
+
94
+ it("returns null when markdown content is empty", () => {
95
+ const { container } = render(
96
+ <FieldViewerValue type="markdown" value=" " label="md" widget="markdown" />,
97
+ );
98
+ expect(container.firstChild).toBeNull();
99
+ });
81
100
  });
@@ -6,6 +6,7 @@ import DropdownField from "./DropdownField";
6
6
  import EnrichedTextField from "./EnrichedTextField";
7
7
  import IdentifierField from "./IdentifierField";
8
8
  import ImageField from "./ImageField";
9
+ import MarkdownField from "./MarkdownField";
9
10
  import PairListField from "./PairListField";
10
11
  import PasswordField from "./PasswordField";
11
12
  import RadioField from "./RadioField";
@@ -34,6 +35,8 @@ const FieldByWidget = ({ scope, widget, onChange, field }) => {
34
35
  return <PairListField field={field} onChange={onChange} />;
35
36
  case "enriched_text":
36
37
  return <EnrichedTextField field={field} onChange={onChange} />;
38
+ case "markdown":
39
+ return <MarkdownField field={field} onChange={onChange} />;
37
40
  case "table":
38
41
  return <TableField field={field} onChange={onChange} />;
39
42
  case "dynamic_table":
@@ -0,0 +1,14 @@
1
+ import PropTypes from "prop-types";
2
+ import { MarkdownEditor } from "@truedat/core/components";
3
+
4
+ export const MarkdownField = ({ field: { name, value }, onChange }) => (
5
+ <MarkdownEditor name={name} value={value || ""} onChange={onChange} />
6
+ );
7
+
8
+ MarkdownField.propTypes = {
9
+ field: PropTypes.object,
10
+ onChange: PropTypes.func,
11
+ };
12
+
13
+ export default MarkdownField;
14
+
@@ -1,5 +1,5 @@
1
1
  import { render, waitForLoad } from "@truedat/test/render";
2
- import { within } from "@testing-library/react";
2
+ import { within, waitFor } from "@testing-library/react";
3
3
  import { DynamicField } from "../DynamicField";
4
4
 
5
5
  const onChange = jest.fn();
@@ -155,4 +155,29 @@ describe("<DynamicField />", () => {
155
155
  await waitForLoad(rendered);
156
156
  expect(rendered.container).toMatchSnapshot();
157
157
  });
158
+
159
+ it("renders MarkdownEditor for widget=markdown", async () => {
160
+ const props = {
161
+ field: {
162
+ widget: "markdown",
163
+ type: "markdown",
164
+ name: "md-field",
165
+ label: "Markdown field",
166
+ description: null,
167
+ cardinality: "1",
168
+ value: { value: "**Bold** text", origin: "user" },
169
+ editable: true,
170
+ disabled: false,
171
+ },
172
+ onChange: jest.fn(),
173
+ };
174
+
175
+ const { container } = render(<DynamicField {...props} />);
176
+
177
+ await waitFor(() => {
178
+ expect(container.querySelector(".markdown-editor")).toBeInTheDocument();
179
+ });
180
+
181
+ expect(container.querySelector(".tiptap")).toBeInTheDocument();
182
+ });
158
183
  });
@@ -426,6 +426,23 @@ exports[`<ActiveGroupForm /> matches the latest snapshot 1`] = `
426
426
  Enrichedtext
427
427
  </span>
428
428
  </div>
429
+ <div
430
+ aria-checked="false"
431
+ aria-selected="false"
432
+ class="item"
433
+ role="option"
434
+ style="pointer-events: all;"
435
+ >
436
+ <i
437
+ aria-hidden="true"
438
+ class="text cursor icon"
439
+ />
440
+ <span
441
+ class="text"
442
+ >
443
+ Markdown
444
+ </span>
445
+ </div>
429
446
  <div
430
447
  aria-checked="false"
431
448
  aria-selected="false"
@@ -187,6 +187,23 @@ exports[`FieldDefinition renders dropdowns with initial values 1`] = `
187
187
  Enrichedtext
188
188
  </span>
189
189
  </div>
190
+ <div
191
+ aria-checked="false"
192
+ aria-selected="false"
193
+ class="item"
194
+ role="option"
195
+ style="pointer-events: all;"
196
+ >
197
+ <i
198
+ aria-hidden="true"
199
+ class="text cursor icon"
200
+ />
201
+ <span
202
+ class="text"
203
+ >
204
+ Markdown
205
+ </span>
206
+ </div>
190
207
  <div
191
208
  aria-checked="false"
192
209
  aria-selected="false"
@@ -353,6 +353,23 @@ exports[`<FieldForm /> matches the latest snapshot 1`] = `
353
353
  Enrichedtext
354
354
  </span>
355
355
  </div>
356
+ <div
357
+ aria-checked="false"
358
+ aria-selected="false"
359
+ class="item"
360
+ role="option"
361
+ style="pointer-events: all;"
362
+ >
363
+ <i
364
+ aria-hidden="true"
365
+ class="text cursor icon"
366
+ />
367
+ <span
368
+ class="text"
369
+ >
370
+ Markdown
371
+ </span>
372
+ </div>
356
373
  <div
357
374
  aria-checked="false"
358
375
  aria-selected="false"
@@ -1049,6 +1066,23 @@ exports[`<FieldForm /> renders MandatoryConditional 1`] = `
1049
1066
  Enrichedtext
1050
1067
  </span>
1051
1068
  </div>
1069
+ <div
1070
+ aria-checked="false"
1071
+ aria-selected="false"
1072
+ class="item"
1073
+ role="option"
1074
+ style="pointer-events: all;"
1075
+ >
1076
+ <i
1077
+ aria-hidden="true"
1078
+ class="text cursor icon"
1079
+ />
1080
+ <span
1081
+ class="text"
1082
+ >
1083
+ Markdown
1084
+ </span>
1085
+ </div>
1052
1086
  <div
1053
1087
  aria-checked="false"
1054
1088
  aria-selected="false"
@@ -1899,6 +1933,23 @@ exports[`<FieldForm /> renders ValuesField and manages onChange 1`] = `
1899
1933
  Enrichedtext
1900
1934
  </span>
1901
1935
  </div>
1936
+ <div
1937
+ aria-checked="false"
1938
+ aria-selected="false"
1939
+ class="item"
1940
+ role="option"
1941
+ style="pointer-events: all;"
1942
+ >
1943
+ <i
1944
+ aria-hidden="true"
1945
+ class="text cursor icon"
1946
+ />
1947
+ <span
1948
+ class="text"
1949
+ >
1950
+ Markdown
1951
+ </span>
1952
+ </div>
1902
1953
  <div
1903
1954
  aria-checked="false"
1904
1955
  aria-selected="false"
@@ -697,6 +697,23 @@ exports[`<TemplateForm /> matches snapshot when scope has no relations 1`] = `
697
697
  Enrichedtext
698
698
  </span>
699
699
  </div>
700
+ <div
701
+ aria-checked="false"
702
+ aria-selected="false"
703
+ class="item"
704
+ role="option"
705
+ style="pointer-events: all;"
706
+ >
707
+ <i
708
+ aria-hidden="true"
709
+ class="text cursor icon"
710
+ />
711
+ <span
712
+ class="text"
713
+ >
714
+ Markdown
715
+ </span>
716
+ </div>
700
717
  <div
701
718
  aria-checked="false"
702
719
  aria-selected="false"
@@ -1792,6 +1809,23 @@ exports[`<TemplateForm /> matches snapshot when scope has relations 1`] = `
1792
1809
  Enrichedtext
1793
1810
  </span>
1794
1811
  </div>
1812
+ <div
1813
+ aria-checked="false"
1814
+ aria-selected="false"
1815
+ class="item"
1816
+ role="option"
1817
+ style="pointer-events: all;"
1818
+ >
1819
+ <i
1820
+ aria-hidden="true"
1821
+ class="text cursor icon"
1822
+ />
1823
+ <span
1824
+ class="text"
1825
+ >
1826
+ Markdown
1827
+ </span>
1828
+ </div>
1795
1829
  <div
1796
1830
  aria-checked="false"
1797
1831
  aria-selected="false"
@@ -28,6 +28,7 @@ const eligibleValues = {
28
28
  pair_list: { url: [null] }, // Links
29
29
  color_picker: { string: [null] },
30
30
  enriched_text: { enriched_text: [null] },
31
+ markdown: { markdown: [null] },
31
32
  table: { table: ["table_columns"] },
32
33
  dynamic_table: { dynamic_table: ["table_columns"] },
33
34
  password: { string: [null] },
@@ -55,7 +56,7 @@ export const valuesSelector = (type) =>
55
56
  export const valueSegment = (values, type, fieldType) =>
56
57
  _.size(values) > 1 || valuesSelector(type) || defaultValue(type, fieldType);
57
58
 
58
- export const hasAiSuggestions = (fieldType, keyType) => _.includes(fieldType)(["string", "enriched_text"]) &&
59
+ export const hasAiSuggestions = (fieldType, keyType) => _.includes(fieldType)(["string", "enriched_text", "markdown"]) &&
59
60
  _.includes(keyType)(["fixed", "fixed_tuple", undefined]);
60
61
 
61
62
  const typeFromKey = (keys) =>
@@ -108,6 +108,14 @@ export const WIDGETS = [
108
108
  cardinalities: ["?", "1"],
109
109
  types: ["enriched_text"],
110
110
  },
111
+ {
112
+ key: "markdown",
113
+ value: "markdown",
114
+ text: "Markdown",
115
+ icon: "text cursor",
116
+ cardinalities: ["?", "1"],
117
+ types: ["markdown"],
118
+ },
111
119
  {
112
120
  key: "table",
113
121
  value: "table",