@truedat/df 8.13.5 → 8.13.7

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 (39) hide show
  1. package/package.json +4 -4
  2. package/src/components/DynamicForm.js +3 -0
  3. package/src/components/DynamicFormWithTranslations.js +3 -0
  4. package/src/components/FieldGroupSegment.js +3 -0
  5. package/src/components/FieldGroupWithTranslations.js +13 -0
  6. package/src/components/SelectDynamicFormWithTranslations.js +7 -1
  7. package/src/components/SelectableDynamicForm.js +7 -1
  8. package/src/components/__tests__/__snapshots__/SelectableDynamicForm.spec.js.snap +18 -1
  9. package/src/components/__tests__/originalContentFlow.spec.js +131 -0
  10. package/src/components/widgets/DateField.js +18 -9
  11. package/src/components/widgets/DateTimeField.js +26 -9
  12. package/src/components/widgets/DynamicField.js +54 -1
  13. package/src/components/widgets/DynamicTableField.js +41 -7
  14. package/src/components/widgets/FieldByWidget.js +36 -6
  15. package/src/components/widgets/__tests__/dateBoundsByWidget.spec.js +133 -0
  16. package/src/components/widgets/__tests__/dateBoundsEndToEnd.spec.js +83 -0
  17. package/src/components/widgets/__tests__/datePickerBoundaries.spec.js +124 -0
  18. package/src/components/widgets/__tests__/dateValueValidation.spec.js +215 -0
  19. package/src/components/widgets/__tests__/existingDateRestrictions.spec.js +354 -0
  20. package/src/templates/components/templateForm/DateRestrictionsForm.js +217 -0
  21. package/src/templates/components/templateForm/DefaultValue.js +79 -11
  22. package/src/templates/components/templateForm/FieldDefinition.js +15 -1
  23. package/src/templates/components/templateForm/TemplateForm.js +5 -0
  24. package/src/templates/components/templateForm/ValuesField.js +7 -0
  25. package/src/templates/components/templateForm/__tests__/FieldDefinition.spec.js +48 -0
  26. package/src/templates/components/templateForm/__tests__/TemplateForm.spec.js +107 -0
  27. package/src/templates/components/templateForm/__tests__/dateRestrictionsForm.spec.js +429 -0
  28. package/src/templates/components/templateForm/__tests__/dateRestrictionsValidation.spec.js +166 -0
  29. package/src/templates/components/templateForm/__tests__/defaultValueBounds.spec.js +191 -0
  30. package/src/templates/components/templateForm/__tests__/defaultValueRestrictions.spec.js +175 -0
  31. package/src/templates/components/templateForm/contentValidation.js +184 -0
  32. package/src/templates/utils/__tests__/dateRestrictions.spec.js +578 -0
  33. package/src/templates/utils/__tests__/originalTableRows.spec.js +129 -0
  34. package/src/templates/utils/__tests__/validateContentDateRestrictions.spec.js +519 -0
  35. package/src/templates/utils/applyTemplate.js +27 -20
  36. package/src/templates/utils/dateRestrictions.js +497 -0
  37. package/src/templates/utils/dateValueUnchanged.js +12 -0
  38. package/src/templates/utils/originalTableRows.js +41 -0
  39. package/src/templates/utils/validateContent.js +75 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/df",
3
- "version": "8.13.5",
3
+ "version": "8.13.7",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -54,14 +54,14 @@
54
54
  "@testing-library/jest-dom": "^6.6.3",
55
55
  "@testing-library/react": "^16.3.0",
56
56
  "@testing-library/user-event": "^14.6.1",
57
- "@truedat/test": "8.13.5",
57
+ "@truedat/test": "8.13.7",
58
58
  "identity-obj-proxy": "^3.0.0",
59
59
  "jest": "^29.7.0",
60
60
  "redux-saga-test-plan": "^4.0.6"
61
61
  },
62
62
  "dependencies": {
63
63
  "@apollo/client": "^3.13.8",
64
- "@truedat/core": "8.13.5",
64
+ "@truedat/core": "8.13.7",
65
65
  "axios": "^1.20.0",
66
66
  "graphql": "^16.11.0",
67
67
  "is-hotkey": "^0.2.0",
@@ -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": "704dcfcba34b6094a368a93059de7f4937502f32"
94
94
  }
@@ -10,6 +10,7 @@ import FieldGroupSegment from "./FieldGroupSegment";
10
10
  export const DynamicForm = ({
11
11
  applyTemplate,
12
12
  content,
13
+ originalContent,
13
14
  fieldsToOmit,
14
15
  instanceId,
15
16
  isModification,
@@ -98,6 +99,7 @@ export const DynamicForm = ({
98
99
  noTranslatableFields={noTranslatableFields}
99
100
  scope={template?.scope}
100
101
  fields={fields}
102
+ originalContent={originalContent}
101
103
  onFieldChange={handleChange}
102
104
  isModification={isModification}
103
105
  />
@@ -107,6 +109,7 @@ export const DynamicForm = ({
107
109
  DynamicForm.propTypes = {
108
110
  applyTemplate: PropTypes.func,
109
111
  content: PropTypes.object,
112
+ originalContent: PropTypes.object,
110
113
  fieldsToOmit: PropTypes.array,
111
114
  instanceId: PropTypes.string,
112
115
  isModification: PropTypes.bool,
@@ -11,6 +11,7 @@ import FieldGroupWithTranslations from "./FieldGroupWithTranslations";
11
11
  export const DynamicFormWithTranslations = ({
12
12
  applyTemplate,
13
13
  i18nContent,
14
+ originalI18nContent,
14
15
  fieldsToOmit,
15
16
  isModification,
16
17
  loading,
@@ -76,6 +77,7 @@ export const DynamicFormWithTranslations = ({
76
77
  }
77
78
  scope={template?.scope}
78
79
  fieldsByLang={langs}
80
+ originalI18nContent={originalI18nContent}
79
81
  onFieldChange={handleChange}
80
82
  isModification={isModification}
81
83
  />
@@ -85,6 +87,7 @@ export const DynamicFormWithTranslations = ({
85
87
  DynamicFormWithTranslations.propTypes = {
86
88
  applyTemplate: PropTypes.func,
87
89
  i18nContent: PropTypes.object,
90
+ originalI18nContent: PropTypes.object,
88
91
  fieldsToOmit: PropTypes.array,
89
92
  isModification: PropTypes.bool,
90
93
  loading: PropTypes.bool,
@@ -7,6 +7,7 @@ export const FieldGroupSegment = ({
7
7
  instanceId,
8
8
  scope,
9
9
  fields,
10
+ originalContent,
10
11
  isModification,
11
12
  name,
12
13
  noTranslatableFields,
@@ -24,6 +25,7 @@ export const FieldGroupSegment = ({
24
25
  disabled: _.includes(field.name)(noTranslatableFields),
25
26
  }}
26
27
  fields={fields}
28
+ originalField={originalContent?.[field.name]}
27
29
  isModification={isModification}
28
30
  key={i}
29
31
  onChange={
@@ -38,6 +40,7 @@ export const FieldGroupSegment = ({
38
40
 
39
41
  FieldGroupSegment.propTypes = {
40
42
  fields: PropTypes.array,
43
+ originalContent: PropTypes.object,
41
44
  instanceId: PropTypes.string,
42
45
  isModification: PropTypes.bool,
43
46
  name: PropTypes.string,
@@ -13,6 +13,7 @@ export const FieldGroupWithTranslations = ({
13
13
  scope,
14
14
  name: groupName,
15
15
  fieldsByLang,
16
+ originalI18nContent,
16
17
  isModification,
17
18
  onFieldChange,
18
19
  }) => {
@@ -101,6 +102,9 @@ export const FieldGroupWithTranslations = ({
101
102
  disabled: false,
102
103
  }}
103
104
  fields={fieldsByLang[defaultLang]}
105
+ originalField={
106
+ originalI18nContent?.[defaultLang]?.[field.name]
107
+ }
104
108
  isModification={isModification}
105
109
  key={`${field.name}-${defaultLang}`}
106
110
  lang={defaultLang}
@@ -122,6 +126,11 @@ export const FieldGroupWithTranslations = ({
122
126
  scope={scope}
123
127
  field={getFieldForLang(field, selectedAltLang)}
124
128
  fields={fieldsByLang[selectedAltLang] || []}
129
+ originalField={
130
+ originalI18nContent?.[selectedAltLang]?.[
131
+ field.name
132
+ ]
133
+ }
125
134
  isModification={isModification}
126
135
  key={`${field.name}-${selectedAltLang}`}
127
136
  onChange={(e, { name, value }) =>
@@ -155,6 +164,9 @@ export const FieldGroupWithTranslations = ({
155
164
  disabled: false,
156
165
  }}
157
166
  fields={fieldsByLang[defaultLang]}
167
+ originalField={
168
+ originalI18nContent?.[defaultLang]?.[field.name]
169
+ }
158
170
  isModification={isModification}
159
171
  lang={defaultLang}
160
172
  onChange={(e, { name, value }) =>
@@ -173,6 +185,7 @@ export const FieldGroupWithTranslations = ({
173
185
 
174
186
  FieldGroupWithTranslations.propTypes = {
175
187
  fieldsByLang: PropTypes.objectOf(PropTypes.array),
188
+ originalI18nContent: PropTypes.object,
176
189
  isModification: PropTypes.bool,
177
190
  name: PropTypes.string,
178
191
  onFieldChange: PropTypes.func,
@@ -11,6 +11,7 @@ import DynamicFormWithTranslations from "./DynamicFormWithTranslations";
11
11
  export default function SelectDynamicFormWithTranslations({
12
12
  beforeSelector,
13
13
  i18nContent,
14
+ originalI18nContent,
14
15
  onChangeContent,
15
16
  domainIds,
16
17
  header,
@@ -48,7 +49,10 @@ export default function SelectDynamicFormWithTranslations({
48
49
  const handleChange = (content, currentTemplate = thisTemplate, lang) => {
49
50
  onChangeContent(lang, {
50
51
  content,
51
- valid: validateContent(currentTemplate)(content),
52
+ valid: validateContent(
53
+ currentTemplate,
54
+ originalI18nContent?.[lang],
55
+ )(content),
52
56
  });
53
57
  };
54
58
 
@@ -124,6 +128,7 @@ export default function SelectDynamicFormWithTranslations({
124
128
  <DynamicFormWithTranslations
125
129
  template={template}
126
130
  i18nContent={i18nContent}
131
+ originalI18nContent={originalI18nContent}
127
132
  onChange={onChangeContent}
128
133
  selectedDomain={domain}
129
134
  isModification={isModification}
@@ -137,6 +142,7 @@ export default function SelectDynamicFormWithTranslations({
137
142
  SelectDynamicFormWithTranslations.propTypes = {
138
143
  beforeSelector: PropTypes.node,
139
144
  i18nContent: PropTypes.object,
145
+ originalI18nContent: PropTypes.object,
140
146
  onChangeContent: PropTypes.func,
141
147
  domainIds: PropTypes.array,
142
148
  header: PropTypes.node,
@@ -23,6 +23,7 @@ const TemplateQueryFetcher = ({ scope, domainIds, onLoad }) => {
23
23
 
24
24
  export default function SelectableDynamicForm({
25
25
  content,
26
+ originalContent,
26
27
  domainIds,
27
28
  header,
28
29
  hideLabel,
@@ -59,7 +60,10 @@ export default function SelectableDynamicForm({
59
60
  : null;
60
61
 
61
62
  const handleChange = (content, currentTemplate = template) => {
62
- onChange({ content, valid: validateContent(currentTemplate)(content) });
63
+ onChange({
64
+ content,
65
+ valid: validateContent(currentTemplate, originalContent)(content),
66
+ });
63
67
  };
64
68
 
65
69
  const handleLoad = ({ templates }) => {
@@ -112,6 +116,7 @@ export default function SelectableDynamicForm({
112
116
  {header}
113
117
  <DynamicForm
114
118
  content={content}
119
+ originalContent={originalContent}
115
120
  instanceId={instanceId}
116
121
  selectedDomain={domain}
117
122
  template={template}
@@ -127,6 +132,7 @@ export default function SelectableDynamicForm({
127
132
 
128
133
  SelectableDynamicForm.propTypes = {
129
134
  content: PropTypes.object,
135
+ originalContent: PropTypes.object,
130
136
  domainIds: PropTypes.array,
131
137
  header: PropTypes.node,
132
138
  hideLabel: PropTypes.bool,
@@ -35,7 +35,24 @@ exports[`<SelectableDynamicForm /> with disableSelector and selectedTemplate mat
35
35
  </div>
36
36
  `;
37
37
 
38
- exports[`<SelectableDynamicForm /> with multiple templates matches error snapshot 1`] = `<div />`;
38
+ exports[`<SelectableDynamicForm /> with multiple templates matches error snapshot 1`] = `
39
+ <div>
40
+ <div
41
+ class="ui compact negative message"
42
+ >
43
+ <div
44
+ class="content"
45
+ >
46
+ Templates could not be loaded. Check your access and try again.
47
+ <button
48
+ class="ui small button"
49
+ >
50
+ Retry
51
+ </button>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ `;
39
56
 
40
57
  exports[`<SelectableDynamicForm /> with multiple templates matches the latest snapshot 1`] = `
41
58
  <div>
@@ -0,0 +1,131 @@
1
+ import { render } from "@truedat/test/render";
2
+ import { DynamicForm } from "../DynamicForm";
3
+ import { DynamicFormWithTranslations } from "../DynamicFormWithTranslations";
4
+ import DynamicField from "../widgets/DynamicField";
5
+ import { parseGroups, parseGroupsWithLangs } from "../../utils";
6
+
7
+ jest.mock("../widgets/DynamicField", () => ({
8
+ __esModule: true,
9
+ default: jest.fn(() => null),
10
+ }));
11
+
12
+ jest.mock("../../utils", () => ({
13
+ ...jest.requireActual("../../utils"),
14
+ parseGroups: jest.fn(),
15
+ parseGroupsWithLangs: jest.fn(),
16
+ }));
17
+
18
+ jest.mock("@truedat/core/i18n", () => ({
19
+ useLanguage: () => ({
20
+ defaultLang: "en",
21
+ altLang: "es",
22
+ altLangs: ["es"],
23
+ requiredLangs: ["en"],
24
+ isMultilingual: true,
25
+ setAltLang: jest.fn(),
26
+ }),
27
+ I18nProvider: ({ children }) => children,
28
+ }));
29
+
30
+ const template = { name: "template", content: [] };
31
+ const originalDate = { value: "2025-01-01", origin: "user" };
32
+ const translatedOriginalDate = { value: "2024-01-01", origin: "user" };
33
+ const field = {
34
+ name: "date",
35
+ label: "Date",
36
+ type: "date",
37
+ widget: "date",
38
+ value: originalDate,
39
+ };
40
+
41
+ beforeEach(() => {
42
+ jest.clearAllMocks();
43
+ parseGroups.mockReturnValue(() => [{ fields: [field] }]);
44
+ parseGroupsWithLangs.mockReturnValue(() => [
45
+ {
46
+ langs: {
47
+ en: [{ ...field, widget: "string" }],
48
+ es: [{ ...field, widget: "string" }],
49
+ },
50
+ },
51
+ ]);
52
+ });
53
+
54
+ describe("DynamicForm original content", () => {
55
+ it("forwards the original field object through its group", () => {
56
+ render(
57
+ <DynamicForm
58
+ template={template}
59
+ content={{ date: originalDate }}
60
+ originalContent={{ date: originalDate }}
61
+ applyTemplate={(content) => content}
62
+ onChange={jest.fn()}
63
+ />,
64
+ );
65
+
66
+ expect(DynamicField.mock.calls[0][0].originalField).toBe(originalDate);
67
+ });
68
+
69
+ it("does not invent a baseline when original content is absent", () => {
70
+ render(
71
+ <DynamicForm
72
+ template={template}
73
+ content={{ date: originalDate }}
74
+ applyTemplate={(content) => content}
75
+ onChange={jest.fn()}
76
+ />,
77
+ );
78
+
79
+ expect(DynamicField.mock.calls[0][0].originalField).toBeUndefined();
80
+ });
81
+ });
82
+
83
+ describe("DynamicFormWithTranslations original content", () => {
84
+ it("selects each language baseline directly from its content map", () => {
85
+ render(
86
+ <DynamicFormWithTranslations
87
+ template={template}
88
+ i18nContent={{ en: { date: originalDate }, es: {} }}
89
+ originalI18nContent={{
90
+ en: { date: originalDate },
91
+ es: { date: translatedOriginalDate },
92
+ }}
93
+ onChange={jest.fn()}
94
+ />,
95
+ );
96
+
97
+ expect(DynamicField.mock.calls[0][0].originalField).toBe(originalDate);
98
+ expect(DynamicField.mock.calls[1][0].originalField).toBe(
99
+ translatedOriginalDate,
100
+ );
101
+ });
102
+
103
+ it("does not use the default language baseline for a new translation", () => {
104
+ render(
105
+ <DynamicFormWithTranslations
106
+ template={template}
107
+ i18nContent={{ en: { date: originalDate }, es: {} }}
108
+ originalI18nContent={{ en: { date: originalDate } }}
109
+ onChange={jest.fn()}
110
+ />,
111
+ );
112
+
113
+ expect(DynamicField.mock.calls[1][0].originalField).toBeUndefined();
114
+ });
115
+
116
+ it("forwards the baseline for groups without translatable fields", () => {
117
+ parseGroupsWithLangs.mockReturnValue(() => [
118
+ { langs: { en: [{ ...field, type: "number", widget: "number" }] } },
119
+ ]);
120
+ render(
121
+ <DynamicFormWithTranslations
122
+ template={template}
123
+ i18nContent={{ en: { date: originalDate } }}
124
+ originalI18nContent={{ en: { date: originalDate } }}
125
+ onChange={jest.fn()}
126
+ />,
127
+ );
128
+
129
+ expect(DynamicField.mock.calls[0][0].originalField).toBe(originalDate);
130
+ });
131
+ });
@@ -1,19 +1,28 @@
1
- import _ from "lodash/fp";
2
1
  import PropTypes from "prop-types";
3
2
  import { Form } from "semantic-ui-react";
3
+ import { checkValuesAgainstBounds } from "../../templates/utils/dateRestrictions";
4
4
 
5
- export const DateField = ({ field: { name, value }, onChange }) => (
6
- <Form.Input
7
- placeholder="Date"
8
- onChange={(e, { value }) => onChange(e, { name: name, value: value })}
9
- value={value || ""}
10
- type="date"
11
- />
12
- );
5
+ export const DateField = ({ field: { name, value }, onChange, min, max }) => {
6
+ const outOfRange = checkValuesAgainstBounds(value, { min, max }) !== "ok";
7
+
8
+ return (
9
+ <Form.Input
10
+ error={outOfRange}
11
+ placeholder="Date"
12
+ onChange={(e, { value }) => onChange(e, { name: name, value: value })}
13
+ value={value || ""}
14
+ min={min}
15
+ max={max}
16
+ type="date"
17
+ />
18
+ );
19
+ };
13
20
 
14
21
  DateField.propTypes = {
15
22
  field: PropTypes.object,
16
23
  onChange: PropTypes.func,
24
+ min: PropTypes.string,
25
+ max: PropTypes.string,
17
26
  };
18
27
 
19
28
  export default DateField;
@@ -1,19 +1,36 @@
1
- import _ from "lodash/fp";
2
1
  import PropTypes from "prop-types";
3
2
  import { Form } from "semantic-ui-react";
3
+ import {
4
+ checkValuesAgainstBounds,
5
+ toInputBound,
6
+ } from "../../templates/utils/dateRestrictions";
4
7
 
5
- export const DateTimeField = ({ field: { name, value } = {}, onChange }) => (
6
- <Form.Input
7
- placeholder="Date Time"
8
- onChange={(e, { value }) => onChange(e, { name: name, value: value })}
9
- value={value || ""}
10
- type="datetime-local"
11
- />
12
- );
8
+ export const DateTimeField = ({
9
+ field: { name, value } = {},
10
+ onChange,
11
+ min,
12
+ max,
13
+ }) => {
14
+ const outOfRange = checkValuesAgainstBounds(value, { min, max }) !== "ok";
15
+
16
+ return (
17
+ <Form.Input
18
+ error={outOfRange}
19
+ placeholder="Date Time"
20
+ onChange={(e, { value }) => onChange(e, { name: name, value: value })}
21
+ value={value || ""}
22
+ min={toInputBound(min, "min")}
23
+ max={toInputBound(max, "max")}
24
+ type="datetime-local"
25
+ />
26
+ );
27
+ };
13
28
 
14
29
  DateTimeField.propTypes = {
15
30
  field: PropTypes.object,
16
31
  onChange: PropTypes.func,
32
+ min: PropTypes.string,
33
+ max: PropTypes.string,
17
34
  };
18
35
 
19
36
  export default DateTimeField;
@@ -1,14 +1,28 @@
1
1
  import _ from "lodash/fp";
2
2
  import PropTypes from "prop-types";
3
3
  import { Form, Icon, Popup, Label } from "semantic-ui-react";
4
- import { FormattedMessage } from "react-intl";
4
+ import { FormattedMessage, useIntl } from "react-intl";
5
5
  import { MarkdownReader } from "@truedat/core/components/MarkdownReader";
6
6
  import OriginLabel from "@truedat/core/components/OriginLabel";
7
+ import { dateValueUnchanged } from "../../templates/utils/dateValueUnchanged";
8
+ import {
9
+ publishedBounds,
10
+ checkValuesAgainstBounds,
11
+ resolveRestrictions,
12
+ } from "../../templates/utils/dateRestrictions";
7
13
  import FieldByWidget from "./FieldByWidget";
8
14
 
9
15
  const isNullOrEmpty = (value) =>
10
16
  !_.isNumber(value) && (!value || _.isEmpty(value));
11
17
 
18
+ const dateBounds = (field, type) => {
19
+ if (type !== "date" && type !== "datetime") return {};
20
+ return (
21
+ publishedBounds(field?.restrictions) ||
22
+ resolveRestrictions(field?.restrictions, type)
23
+ );
24
+ };
25
+
12
26
  export const DynamicField = ({
13
27
  field: {
14
28
  label,
@@ -21,6 +35,7 @@ export const DynamicField = ({
21
35
  },
22
36
  instanceId,
23
37
  isModification,
38
+ originalField,
24
39
  onChange,
25
40
  scope,
26
41
  }) => {
@@ -28,8 +43,21 @@ export const DynamicField = ({
28
43
  value: null,
29
44
  origin: "default",
30
45
  };
46
+
47
+ const { formatMessage } = useIntl();
48
+
49
+ const unchanged = dateValueUnchanged(value, originalField);
50
+ const bounds = unchanged ? {} : dateBounds(fieldProps, fieldProps.type);
51
+ const verdict =
52
+ fieldProps.type === "date" || fieldProps.type === "datetime"
53
+ ? checkValuesAgainstBounds(value, bounds)
54
+ : "ok";
55
+ const outOfRange = verdict !== "ok";
56
+ const breachedBound = verdict === "too_early" ? bounds.min : bounds.max;
57
+
31
58
  return (
32
59
  <Form.Field
60
+ error={outOfRange}
33
61
  disabled={(!editable && isModification) || disabled}
34
62
  data-testid="form-field"
35
63
  >
@@ -89,10 +117,34 @@ export const DynamicField = ({
89
117
  </label>
90
118
  <FieldByWidget
91
119
  field={{ ...fieldProps, value, cardinality, instanceId }}
120
+ originalField={originalField}
92
121
  onChange={onChange}
93
122
  scope={scope}
94
123
  widget={_.prop("widget")(fieldProps)}
95
124
  />
125
+ {outOfRange ? (
126
+ <Label
127
+ basic
128
+ color="red"
129
+ pointing="above"
130
+ style={{ marginTop: "0.5em" }}
131
+ >
132
+ {verdict === "invalid_date"
133
+ ? formatMessage({
134
+ id: "template.field.restrictions.validation.invalid_date",
135
+ defaultMessage: "Enter a valid date or date-time.",
136
+ })
137
+ : formatMessage(
138
+ {
139
+ id:
140
+ verdict === "too_early"
141
+ ? "template.field.values.default_after_min"
142
+ : "template.field.values.default_before_max",
143
+ },
144
+ { limit: breachedBound },
145
+ )}
146
+ </Label>
147
+ ) : null}
96
148
  </Form.Field>
97
149
  );
98
150
  };
@@ -101,6 +153,7 @@ DynamicField.propTypes = {
101
153
  field: PropTypes.object.isRequired,
102
154
  instanceId: PropTypes.string,
103
155
  isModification: PropTypes.bool,
156
+ originalField: PropTypes.object,
104
157
  onChange: PropTypes.func.isRequired,
105
158
  scope: PropTypes.string,
106
159
  };
@@ -3,39 +3,53 @@ import { lazy } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { Button, Icon, Label, Table } from "semantic-ui-react";
5
5
  import { useIntl } from "react-intl";
6
+ import {
7
+ copyRow,
8
+ getOriginalRow,
9
+ markNewRow,
10
+ } from "../../templates/utils/originalTableRows";
11
+ import { dateValueUnchanged } from "../../templates/utils/dateValueUnchanged";
12
+ import {
13
+ publishedBounds,
14
+ checkValuesAgainstBounds,
15
+ resolveRestrictions,
16
+ } from "../../templates/utils/dateRestrictions";
6
17
 
7
18
  const FieldByWidget = lazy(() => import("./FieldByWidget"));
8
19
 
9
20
  export const DynamicTableField = ({
10
21
  field: { name, value: fieldValue = [], values },
22
+ originalField,
11
23
  onChange,
12
24
  scope,
13
25
  }) => {
14
26
  const value = fieldValue || [];
15
27
  const table_columns = _.get("table_columns")(values) || [];
16
28
  const { formatMessage } = useIntl();
29
+ const originalRows = value.map((row, index) =>
30
+ getOriginalRow(row, index, originalField?.value),
31
+ );
17
32
 
18
33
  const handleAddRow = (e) => {
19
34
  e && e.preventDefault();
20
35
  onChange(e, {
21
36
  name,
22
- value: [...value, {}],
37
+ value: [...value, markNewRow({})],
23
38
  });
24
39
  };
25
40
 
26
41
  const handleRemoveRow = (e, id) => {
27
42
  e && e.preventDefault();
28
- const newValue = [...value];
29
- newValue.splice(id, 1);
43
+ const newValue = value.filter((_row, index) => index !== id);
30
44
  onChange(e, { name, value: newValue });
31
45
  };
32
46
 
33
47
  const handleChange = (idx, e, data) => {
34
48
  const currentValue = removeDeletedColumns(table_columns, [...value]);
35
- const input = {
49
+ const input = copyRow(currentValue[idx], {
36
50
  ...currentValue[idx],
37
51
  [data.name]: { origin: "user", value: data.value },
38
- };
52
+ });
39
53
  onChange(e, {
40
54
  name,
41
55
  value: currentValue.map((newElem, i) => (i == idx ? input : newElem)),
@@ -48,7 +62,7 @@ export const DynamicTableField = ({
48
62
  const field_deleted = _.keys(row).filter(
49
63
  (x) => !columns_name.includes(x),
50
64
  );
51
- return _.omit(field_deleted)(row);
65
+ return copyRow(row, _.omit(field_deleted)(row));
52
66
  });
53
67
  };
54
68
 
@@ -70,6 +84,20 @@ export const DynamicTableField = ({
70
84
  _.fromPairs,
71
85
  )(rows);
72
86
 
87
+ const originalCell = (rowIndex, fieldName) =>
88
+ originalRows[rowIndex]?.[fieldName];
89
+
90
+ const dateError = (field, originalCell) => {
91
+ const unchanged = dateValueUnchanged(field.value, originalCell);
92
+ if (unchanged || !["date", "datetime"].includes(field?.type)) return false;
93
+ const bounds =
94
+ publishedBounds(field?.restrictions) ||
95
+ resolveRestrictions(field?.restrictions, field?.type);
96
+ return bounds
97
+ ? checkValuesAgainstBounds(field?.value, bounds) !== "ok"
98
+ : false;
99
+ };
100
+
73
101
  return (
74
102
  <Table celled className="no_padding">
75
103
  <Table.Header>
@@ -96,9 +124,14 @@ export const DynamicTableField = ({
96
124
  {rows.map((row, i) => (
97
125
  <Table.Row key={i}>
98
126
  {row.map((field, k) => (
99
- <Table.Cell key={k} width={5}>
127
+ <Table.Cell
128
+ key={k}
129
+ width={5}
130
+ error={dateError(field, originalCell(i, field.name))}
131
+ >
100
132
  <FieldByWidget
101
133
  field={field}
134
+ originalField={originalCell(i, field.name)}
102
135
  widget={field.widget}
103
136
  onChange={(e, data) => handleChange(i, e, data)}
104
137
  scope={scope}
@@ -147,6 +180,7 @@ export const DynamicTableField = ({
147
180
 
148
181
  DynamicTableField.propTypes = {
149
182
  field: PropTypes.object,
183
+ originalField: PropTypes.object,
150
184
  onChange: PropTypes.func,
151
185
  scope: PropTypes.string,
152
186
  };