@contentful/field-editor-validation-errors 1.1.11 → 1.3.0
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/dist/cjs/ValidationErrors.js +162 -0
- package/dist/cjs/ValidationErrors.test.js +164 -0
- package/dist/cjs/index.js +11 -0
- package/dist/cjs/styles.js +50 -0
- package/dist/esm/ValidationErrors.js +113 -0
- package/dist/esm/ValidationErrors.test.js +121 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/styles.js +21 -0
- package/dist/{ValidationErrors.d.ts → types/ValidationErrors.d.ts} +9 -9
- package/dist/types/ValidationErrors.test.d.ts +1 -0
- package/dist/{index.d.ts → types/index.d.ts} +1 -1
- package/dist/{styles.d.ts → types/styles.d.ts} +4 -4
- package/package.json +25 -11
- package/CHANGELOG.md +0 -198
- package/dist/field-editor-validation-errors.cjs.development.js +0 -181
- package/dist/field-editor-validation-errors.cjs.development.js.map +0 -1
- package/dist/field-editor-validation-errors.cjs.production.min.js +0 -2
- package/dist/field-editor-validation-errors.cjs.production.min.js.map +0 -1
- package/dist/field-editor-validation-errors.esm.js +0 -175
- package/dist/field-editor-validation-errors.esm.js.map +0 -1
- package/dist/index.js +0 -8
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "ValidationErrors", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return ValidationErrors;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _react = _interop_require_wildcard(require("react"));
|
|
12
|
+
const _f36components = require("@contentful/f36-components");
|
|
13
|
+
const _f36icons = require("@contentful/f36-icons");
|
|
14
|
+
const _fieldeditorshared = require("@contentful/field-editor-shared");
|
|
15
|
+
const _styles = _interop_require_wildcard(require("./styles"));
|
|
16
|
+
function _getRequireWildcardCache(nodeInterop) {
|
|
17
|
+
if (typeof WeakMap !== "function") return null;
|
|
18
|
+
var cacheBabelInterop = new WeakMap();
|
|
19
|
+
var cacheNodeInterop = new WeakMap();
|
|
20
|
+
return (_getRequireWildcardCache = function(nodeInterop) {
|
|
21
|
+
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
22
|
+
})(nodeInterop);
|
|
23
|
+
}
|
|
24
|
+
function _interop_require_wildcard(obj, nodeInterop) {
|
|
25
|
+
if (!nodeInterop && obj && obj.__esModule) {
|
|
26
|
+
return obj;
|
|
27
|
+
}
|
|
28
|
+
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
|
|
29
|
+
return {
|
|
30
|
+
default: obj
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
var cache = _getRequireWildcardCache(nodeInterop);
|
|
34
|
+
if (cache && cache.has(obj)) {
|
|
35
|
+
return cache.get(obj);
|
|
36
|
+
}
|
|
37
|
+
var newObj = {};
|
|
38
|
+
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
39
|
+
for(var key in obj){
|
|
40
|
+
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
41
|
+
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
|
|
42
|
+
if (desc && (desc.get || desc.set)) {
|
|
43
|
+
Object.defineProperty(newObj, key, desc);
|
|
44
|
+
} else {
|
|
45
|
+
newObj[key] = obj[key];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
newObj.default = obj;
|
|
50
|
+
if (cache) {
|
|
51
|
+
cache.set(obj, newObj);
|
|
52
|
+
}
|
|
53
|
+
return newObj;
|
|
54
|
+
}
|
|
55
|
+
function UniquenessError(props) {
|
|
56
|
+
const [state, setState] = _react.useState({
|
|
57
|
+
loading: true,
|
|
58
|
+
entries: []
|
|
59
|
+
});
|
|
60
|
+
const contentTypesById = _react.useMemo(()=>props.space.getCachedContentTypes().reduce((prev, ct)=>({
|
|
61
|
+
...prev,
|
|
62
|
+
[ct.sys.id]: ct
|
|
63
|
+
}), {}), [
|
|
64
|
+
props.space
|
|
65
|
+
]);
|
|
66
|
+
const getTitle = _react.useCallback((entry)=>_fieldeditorshared.entityHelpers.getEntryTitle({
|
|
67
|
+
entry,
|
|
68
|
+
defaultTitle: 'Untitled',
|
|
69
|
+
localeCode: props.localeCode,
|
|
70
|
+
defaultLocaleCode: props.defaultLocaleCode,
|
|
71
|
+
contentType: contentTypesById[entry.sys.contentType.sys.id]
|
|
72
|
+
}), [
|
|
73
|
+
props.localeCode,
|
|
74
|
+
props.defaultLocaleCode,
|
|
75
|
+
contentTypesById
|
|
76
|
+
]);
|
|
77
|
+
let conflicting = [];
|
|
78
|
+
if ('conflicting' in props.error) {
|
|
79
|
+
conflicting = props.error.conflicting;
|
|
80
|
+
}
|
|
81
|
+
_react.useEffect(()=>{
|
|
82
|
+
const entryIds = state.entries.map((entry)=>entry.id);
|
|
83
|
+
const conflictIds = conflicting.map((entry)=>entry.sys.id);
|
|
84
|
+
if (conflictIds.every((id)=>entryIds.includes(id))) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
setState((state)=>({
|
|
88
|
+
...state,
|
|
89
|
+
loading: true
|
|
90
|
+
}));
|
|
91
|
+
const query = {
|
|
92
|
+
'sys.id[in]': conflictIds.join(',')
|
|
93
|
+
};
|
|
94
|
+
props.space.getEntries(query).then(({ items })=>{
|
|
95
|
+
const entries = items.map((entry)=>({
|
|
96
|
+
id: entry.sys.id,
|
|
97
|
+
title: getTitle(entry),
|
|
98
|
+
href: props.getEntryURL(entry)
|
|
99
|
+
}));
|
|
100
|
+
setState({
|
|
101
|
+
loading: false,
|
|
102
|
+
entries
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}, [
|
|
106
|
+
getTitle,
|
|
107
|
+
state.entries,
|
|
108
|
+
conflicting,
|
|
109
|
+
props.space.getEntries,
|
|
110
|
+
props.getEntryURL
|
|
111
|
+
]);
|
|
112
|
+
return _react.createElement(_f36components.List, {
|
|
113
|
+
className: _styles.errorList,
|
|
114
|
+
testId: "validation-errors-uniqueness"
|
|
115
|
+
}, _react.createElement(_f36components.ListItem, {
|
|
116
|
+
className: _styles.entryLink
|
|
117
|
+
}, state.loading ? _react.createElement("div", null, "Loading title for conflicting entry…") : state.entries.map((entry)=>_react.createElement(_f36components.TextLink, {
|
|
118
|
+
key: entry.id,
|
|
119
|
+
href: entry.href,
|
|
120
|
+
icon: _react.createElement(_f36icons.ExternalLinkIcon, null),
|
|
121
|
+
alignIcon: "end",
|
|
122
|
+
variant: "negative",
|
|
123
|
+
target: "_blank",
|
|
124
|
+
rel: "noopener noreferrer"
|
|
125
|
+
}, entry.title))));
|
|
126
|
+
}
|
|
127
|
+
function ValidationErrors(props) {
|
|
128
|
+
const [errors, setErrors] = _react.useState([]);
|
|
129
|
+
_react.useEffect(()=>{
|
|
130
|
+
const onErrors = (errors)=>{
|
|
131
|
+
setErrors(errors || []);
|
|
132
|
+
};
|
|
133
|
+
return props.field.onSchemaErrorsChanged(onErrors);
|
|
134
|
+
}, [
|
|
135
|
+
props.field
|
|
136
|
+
]);
|
|
137
|
+
if (errors.length === 0) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return _react.createElement(_f36components.List, {
|
|
141
|
+
className: _styles.errorList,
|
|
142
|
+
testId: "validation-errors"
|
|
143
|
+
}, errors.map((error, index)=>{
|
|
144
|
+
return _react.createElement("li", {
|
|
145
|
+
key: index,
|
|
146
|
+
role: "status",
|
|
147
|
+
"aria-roledescription": "field-locale-schema",
|
|
148
|
+
"data-error-code": `entry.schema.${error.name}`,
|
|
149
|
+
className: _styles.errorItem
|
|
150
|
+
}, _react.createElement(_f36icons.InfoCircleIcon, {
|
|
151
|
+
variant: "negative"
|
|
152
|
+
}), _react.createElement("div", {
|
|
153
|
+
className: _styles.errorMessage
|
|
154
|
+
}, error.message, error.name === 'unique' && _react.createElement(UniquenessError, {
|
|
155
|
+
error: error,
|
|
156
|
+
space: props.space,
|
|
157
|
+
localeCode: props.field.locale,
|
|
158
|
+
defaultLocaleCode: props.locales.default,
|
|
159
|
+
getEntryURL: props.getEntryURL
|
|
160
|
+
})));
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _react = _interop_require_wildcard(require("react"));
|
|
6
|
+
require("@testing-library/jest-dom/extend-expect");
|
|
7
|
+
const _fieldeditortestutils = _interop_require_wildcard(require("@contentful/field-editor-test-utils"));
|
|
8
|
+
const _react1 = require("@testing-library/react");
|
|
9
|
+
const _ValidationErrors = require("./ValidationErrors");
|
|
10
|
+
function _getRequireWildcardCache(nodeInterop) {
|
|
11
|
+
if (typeof WeakMap !== "function") return null;
|
|
12
|
+
var cacheBabelInterop = new WeakMap();
|
|
13
|
+
var cacheNodeInterop = new WeakMap();
|
|
14
|
+
return (_getRequireWildcardCache = function(nodeInterop) {
|
|
15
|
+
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
16
|
+
})(nodeInterop);
|
|
17
|
+
}
|
|
18
|
+
function _interop_require_wildcard(obj, nodeInterop) {
|
|
19
|
+
if (!nodeInterop && obj && obj.__esModule) {
|
|
20
|
+
return obj;
|
|
21
|
+
}
|
|
22
|
+
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
|
|
23
|
+
return {
|
|
24
|
+
default: obj
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
var cache = _getRequireWildcardCache(nodeInterop);
|
|
28
|
+
if (cache && cache.has(obj)) {
|
|
29
|
+
return cache.get(obj);
|
|
30
|
+
}
|
|
31
|
+
var newObj = {};
|
|
32
|
+
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
33
|
+
for(var key in obj){
|
|
34
|
+
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
35
|
+
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
|
|
36
|
+
if (desc && (desc.get || desc.set)) {
|
|
37
|
+
Object.defineProperty(newObj, key, desc);
|
|
38
|
+
} else {
|
|
39
|
+
newObj[key] = obj[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
newObj.default = obj;
|
|
44
|
+
if (cache) {
|
|
45
|
+
cache.set(obj, newObj);
|
|
46
|
+
}
|
|
47
|
+
return newObj;
|
|
48
|
+
}
|
|
49
|
+
(0, _react1.configure)({
|
|
50
|
+
testIdAttribute: 'data-test-id'
|
|
51
|
+
});
|
|
52
|
+
const displayField = 'my-title';
|
|
53
|
+
const contentTypeId = 'my-content-type';
|
|
54
|
+
const getCachedContentTypes = ()=>[
|
|
55
|
+
{
|
|
56
|
+
displayField,
|
|
57
|
+
fields: [
|
|
58
|
+
{
|
|
59
|
+
id: displayField
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
sys: {
|
|
63
|
+
id: 'my-content-type'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
const createEntry = (id)=>({
|
|
68
|
+
fields: {
|
|
69
|
+
[displayField]: {
|
|
70
|
+
'en-US': 'entry-title-for-' + id
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
sys: {
|
|
74
|
+
id,
|
|
75
|
+
contentType: {
|
|
76
|
+
sys: {
|
|
77
|
+
id: contentTypeId
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
describe('ValidationErrors', ()=>{
|
|
83
|
+
afterEach(_react1.cleanup);
|
|
84
|
+
it('renders without crashing', ()=>{
|
|
85
|
+
const [field] = _fieldeditortestutils.createFakeFieldAPI();
|
|
86
|
+
const { container } = (0, _react1.render)(_react.createElement(_ValidationErrors.ValidationErrors, {
|
|
87
|
+
field: field,
|
|
88
|
+
space: _fieldeditortestutils.createFakeSpaceAPI(),
|
|
89
|
+
locales: _fieldeditortestutils.createFakeLocalesAPI(),
|
|
90
|
+
getEntryURL: (entry)=>`url.${entry.sys.id}`
|
|
91
|
+
}));
|
|
92
|
+
expect(container).toBeEmptyDOMElement();
|
|
93
|
+
});
|
|
94
|
+
it('should render the list of error messages', async ()=>{
|
|
95
|
+
const errors = [
|
|
96
|
+
{
|
|
97
|
+
name: 'test-error',
|
|
98
|
+
message: 'The input is invalid',
|
|
99
|
+
path: []
|
|
100
|
+
}
|
|
101
|
+
];
|
|
102
|
+
const [field, emitter] = _fieldeditortestutils.createFakeFieldAPI();
|
|
103
|
+
const { findByText } = (0, _react1.render)(_react.createElement(_ValidationErrors.ValidationErrors, {
|
|
104
|
+
field: field,
|
|
105
|
+
space: _fieldeditortestutils.createFakeSpaceAPI(),
|
|
106
|
+
locales: _fieldeditortestutils.createFakeLocalesAPI(),
|
|
107
|
+
getEntryURL: (entry)=>`url.${entry.sys.id}`
|
|
108
|
+
}));
|
|
109
|
+
(0, _react1.act)(()=>{
|
|
110
|
+
emitter.emit('onSchemaErrorsChanged', errors);
|
|
111
|
+
});
|
|
112
|
+
await Promise.all(errors.map((e)=>{
|
|
113
|
+
if (e.message) {
|
|
114
|
+
return findByText(e.message);
|
|
115
|
+
} else {
|
|
116
|
+
return Promise.reject();
|
|
117
|
+
}
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
it('should fetch & render links to duplicated entries', async ()=>{
|
|
121
|
+
const ids = [
|
|
122
|
+
'id-0',
|
|
123
|
+
'id-1',
|
|
124
|
+
'id-2'
|
|
125
|
+
];
|
|
126
|
+
const errors = [
|
|
127
|
+
{
|
|
128
|
+
name: 'unique',
|
|
129
|
+
message: 'entry is duplicated',
|
|
130
|
+
conflicting: ids.map((id)=>({
|
|
131
|
+
sys: {
|
|
132
|
+
id,
|
|
133
|
+
type: 'Link',
|
|
134
|
+
linkType: 'Entry'
|
|
135
|
+
}
|
|
136
|
+
})),
|
|
137
|
+
path: []
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
const [field, emitter] = _fieldeditortestutils.createFakeFieldAPI();
|
|
141
|
+
const space = _fieldeditortestutils.createFakeSpaceAPI((api)=>({
|
|
142
|
+
...api,
|
|
143
|
+
getCachedContentTypes,
|
|
144
|
+
getEntries: jest.fn().mockResolvedValue({
|
|
145
|
+
items: ids.map(createEntry)
|
|
146
|
+
})
|
|
147
|
+
}));
|
|
148
|
+
const { findByText , findAllByTestId } = (0, _react1.render)(_react.createElement(_ValidationErrors.ValidationErrors, {
|
|
149
|
+
field: field,
|
|
150
|
+
space: space,
|
|
151
|
+
locales: _fieldeditortestutils.createFakeLocalesAPI(),
|
|
152
|
+
getEntryURL: (entry)=>`url.${entry.sys.id}`
|
|
153
|
+
}));
|
|
154
|
+
(0, _react1.act)(()=>{
|
|
155
|
+
emitter.emit('onSchemaErrorsChanged', errors);
|
|
156
|
+
});
|
|
157
|
+
await findByText(/Loading title for conflicting entry/);
|
|
158
|
+
const links = await findAllByTestId('cf-ui-text-link');
|
|
159
|
+
links.forEach((link, index)=>{
|
|
160
|
+
expect(link).toHaveAttribute('href', 'url.id-' + index);
|
|
161
|
+
expect(link).toHaveTextContent('entry-title-for-id-' + index);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "ValidationErrors", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return _ValidationErrors.ValidationErrors;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _ValidationErrors = require("./ValidationErrors");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: all[name]
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
errorList: function() {
|
|
13
|
+
return errorList;
|
|
14
|
+
},
|
|
15
|
+
errorMessage: function() {
|
|
16
|
+
return errorMessage;
|
|
17
|
+
},
|
|
18
|
+
errorItem: function() {
|
|
19
|
+
return errorItem;
|
|
20
|
+
},
|
|
21
|
+
entryLink: function() {
|
|
22
|
+
return entryLink;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const _f36tokens = _interop_require_default(require("@contentful/f36-tokens"));
|
|
26
|
+
const _emotion = require("emotion");
|
|
27
|
+
function _interop_require_default(obj) {
|
|
28
|
+
return obj && obj.__esModule ? obj : {
|
|
29
|
+
default: obj
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const errorList = (0, _emotion.css)({
|
|
33
|
+
padding: 0,
|
|
34
|
+
wordWrap: 'break-word',
|
|
35
|
+
marginTop: _f36tokens.default.spacingS,
|
|
36
|
+
color: _f36tokens.default.red500,
|
|
37
|
+
listStyleType: 'none'
|
|
38
|
+
});
|
|
39
|
+
const errorMessage = (0, _emotion.css)({
|
|
40
|
+
display: 'inline-flex',
|
|
41
|
+
flexDirection: 'column',
|
|
42
|
+
marginLeft: _f36tokens.default.spacingXs
|
|
43
|
+
});
|
|
44
|
+
const errorItem = (0, _emotion.css)({
|
|
45
|
+
display: 'flex',
|
|
46
|
+
alignItems: 'center'
|
|
47
|
+
});
|
|
48
|
+
const entryLink = (0, _emotion.css)({
|
|
49
|
+
fontWeight: Number(_f36tokens.default.fontWeightDemiBold)
|
|
50
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { List, ListItem, TextLink } from '@contentful/f36-components';
|
|
3
|
+
import { ExternalLinkIcon, InfoCircleIcon } from '@contentful/f36-icons';
|
|
4
|
+
import { entityHelpers } from '@contentful/field-editor-shared';
|
|
5
|
+
import * as styles from './styles';
|
|
6
|
+
function UniquenessError(props) {
|
|
7
|
+
const [state, setState] = React.useState({
|
|
8
|
+
loading: true,
|
|
9
|
+
entries: []
|
|
10
|
+
});
|
|
11
|
+
const contentTypesById = React.useMemo(()=>props.space.getCachedContentTypes().reduce((prev, ct)=>({
|
|
12
|
+
...prev,
|
|
13
|
+
[ct.sys.id]: ct
|
|
14
|
+
}), {}), [
|
|
15
|
+
props.space
|
|
16
|
+
]);
|
|
17
|
+
const getTitle = React.useCallback((entry)=>entityHelpers.getEntryTitle({
|
|
18
|
+
entry,
|
|
19
|
+
defaultTitle: 'Untitled',
|
|
20
|
+
localeCode: props.localeCode,
|
|
21
|
+
defaultLocaleCode: props.defaultLocaleCode,
|
|
22
|
+
contentType: contentTypesById[entry.sys.contentType.sys.id]
|
|
23
|
+
}), [
|
|
24
|
+
props.localeCode,
|
|
25
|
+
props.defaultLocaleCode,
|
|
26
|
+
contentTypesById
|
|
27
|
+
]);
|
|
28
|
+
let conflicting = [];
|
|
29
|
+
if ('conflicting' in props.error) {
|
|
30
|
+
conflicting = props.error.conflicting;
|
|
31
|
+
}
|
|
32
|
+
React.useEffect(()=>{
|
|
33
|
+
const entryIds = state.entries.map((entry)=>entry.id);
|
|
34
|
+
const conflictIds = conflicting.map((entry)=>entry.sys.id);
|
|
35
|
+
if (conflictIds.every((id)=>entryIds.includes(id))) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setState((state)=>({
|
|
39
|
+
...state,
|
|
40
|
+
loading: true
|
|
41
|
+
}));
|
|
42
|
+
const query = {
|
|
43
|
+
'sys.id[in]': conflictIds.join(',')
|
|
44
|
+
};
|
|
45
|
+
props.space.getEntries(query).then(({ items })=>{
|
|
46
|
+
const entries = items.map((entry)=>({
|
|
47
|
+
id: entry.sys.id,
|
|
48
|
+
title: getTitle(entry),
|
|
49
|
+
href: props.getEntryURL(entry)
|
|
50
|
+
}));
|
|
51
|
+
setState({
|
|
52
|
+
loading: false,
|
|
53
|
+
entries
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}, [
|
|
57
|
+
getTitle,
|
|
58
|
+
state.entries,
|
|
59
|
+
conflicting,
|
|
60
|
+
props.space.getEntries,
|
|
61
|
+
props.getEntryURL
|
|
62
|
+
]);
|
|
63
|
+
return React.createElement(List, {
|
|
64
|
+
className: styles.errorList,
|
|
65
|
+
testId: "validation-errors-uniqueness"
|
|
66
|
+
}, React.createElement(ListItem, {
|
|
67
|
+
className: styles.entryLink
|
|
68
|
+
}, state.loading ? React.createElement("div", null, "Loading title for conflicting entry…") : state.entries.map((entry)=>React.createElement(TextLink, {
|
|
69
|
+
key: entry.id,
|
|
70
|
+
href: entry.href,
|
|
71
|
+
icon: React.createElement(ExternalLinkIcon, null),
|
|
72
|
+
alignIcon: "end",
|
|
73
|
+
variant: "negative",
|
|
74
|
+
target: "_blank",
|
|
75
|
+
rel: "noopener noreferrer"
|
|
76
|
+
}, entry.title))));
|
|
77
|
+
}
|
|
78
|
+
export function ValidationErrors(props) {
|
|
79
|
+
const [errors, setErrors] = React.useState([]);
|
|
80
|
+
React.useEffect(()=>{
|
|
81
|
+
const onErrors = (errors)=>{
|
|
82
|
+
setErrors(errors || []);
|
|
83
|
+
};
|
|
84
|
+
return props.field.onSchemaErrorsChanged(onErrors);
|
|
85
|
+
}, [
|
|
86
|
+
props.field
|
|
87
|
+
]);
|
|
88
|
+
if (errors.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return React.createElement(List, {
|
|
92
|
+
className: styles.errorList,
|
|
93
|
+
testId: "validation-errors"
|
|
94
|
+
}, errors.map((error, index)=>{
|
|
95
|
+
return React.createElement("li", {
|
|
96
|
+
key: index,
|
|
97
|
+
role: "status",
|
|
98
|
+
"aria-roledescription": "field-locale-schema",
|
|
99
|
+
"data-error-code": `entry.schema.${error.name}`,
|
|
100
|
+
className: styles.errorItem
|
|
101
|
+
}, React.createElement(InfoCircleIcon, {
|
|
102
|
+
variant: "negative"
|
|
103
|
+
}), React.createElement("div", {
|
|
104
|
+
className: styles.errorMessage
|
|
105
|
+
}, error.message, error.name === 'unique' && React.createElement(UniquenessError, {
|
|
106
|
+
error: error,
|
|
107
|
+
space: props.space,
|
|
108
|
+
localeCode: props.field.locale,
|
|
109
|
+
defaultLocaleCode: props.locales.default,
|
|
110
|
+
getEntryURL: props.getEntryURL
|
|
111
|
+
})));
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
3
|
+
import * as utils from '@contentful/field-editor-test-utils';
|
|
4
|
+
import { render, configure, cleanup, act } from '@testing-library/react';
|
|
5
|
+
import { ValidationErrors } from './ValidationErrors';
|
|
6
|
+
configure({
|
|
7
|
+
testIdAttribute: 'data-test-id'
|
|
8
|
+
});
|
|
9
|
+
const displayField = 'my-title';
|
|
10
|
+
const contentTypeId = 'my-content-type';
|
|
11
|
+
const getCachedContentTypes = ()=>[
|
|
12
|
+
{
|
|
13
|
+
displayField,
|
|
14
|
+
fields: [
|
|
15
|
+
{
|
|
16
|
+
id: displayField
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
sys: {
|
|
20
|
+
id: 'my-content-type'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
const createEntry = (id)=>({
|
|
25
|
+
fields: {
|
|
26
|
+
[displayField]: {
|
|
27
|
+
'en-US': 'entry-title-for-' + id
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
sys: {
|
|
31
|
+
id,
|
|
32
|
+
contentType: {
|
|
33
|
+
sys: {
|
|
34
|
+
id: contentTypeId
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
describe('ValidationErrors', ()=>{
|
|
40
|
+
afterEach(cleanup);
|
|
41
|
+
it('renders without crashing', ()=>{
|
|
42
|
+
const [field] = utils.createFakeFieldAPI();
|
|
43
|
+
const { container } = render(React.createElement(ValidationErrors, {
|
|
44
|
+
field: field,
|
|
45
|
+
space: utils.createFakeSpaceAPI(),
|
|
46
|
+
locales: utils.createFakeLocalesAPI(),
|
|
47
|
+
getEntryURL: (entry)=>`url.${entry.sys.id}`
|
|
48
|
+
}));
|
|
49
|
+
expect(container).toBeEmptyDOMElement();
|
|
50
|
+
});
|
|
51
|
+
it('should render the list of error messages', async ()=>{
|
|
52
|
+
const errors = [
|
|
53
|
+
{
|
|
54
|
+
name: 'test-error',
|
|
55
|
+
message: 'The input is invalid',
|
|
56
|
+
path: []
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
const [field, emitter] = utils.createFakeFieldAPI();
|
|
60
|
+
const { findByText } = render(React.createElement(ValidationErrors, {
|
|
61
|
+
field: field,
|
|
62
|
+
space: utils.createFakeSpaceAPI(),
|
|
63
|
+
locales: utils.createFakeLocalesAPI(),
|
|
64
|
+
getEntryURL: (entry)=>`url.${entry.sys.id}`
|
|
65
|
+
}));
|
|
66
|
+
act(()=>{
|
|
67
|
+
emitter.emit('onSchemaErrorsChanged', errors);
|
|
68
|
+
});
|
|
69
|
+
await Promise.all(errors.map((e)=>{
|
|
70
|
+
if (e.message) {
|
|
71
|
+
return findByText(e.message);
|
|
72
|
+
} else {
|
|
73
|
+
return Promise.reject();
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
it('should fetch & render links to duplicated entries', async ()=>{
|
|
78
|
+
const ids = [
|
|
79
|
+
'id-0',
|
|
80
|
+
'id-1',
|
|
81
|
+
'id-2'
|
|
82
|
+
];
|
|
83
|
+
const errors = [
|
|
84
|
+
{
|
|
85
|
+
name: 'unique',
|
|
86
|
+
message: 'entry is duplicated',
|
|
87
|
+
conflicting: ids.map((id)=>({
|
|
88
|
+
sys: {
|
|
89
|
+
id,
|
|
90
|
+
type: 'Link',
|
|
91
|
+
linkType: 'Entry'
|
|
92
|
+
}
|
|
93
|
+
})),
|
|
94
|
+
path: []
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
const [field, emitter] = utils.createFakeFieldAPI();
|
|
98
|
+
const space = utils.createFakeSpaceAPI((api)=>({
|
|
99
|
+
...api,
|
|
100
|
+
getCachedContentTypes,
|
|
101
|
+
getEntries: jest.fn().mockResolvedValue({
|
|
102
|
+
items: ids.map(createEntry)
|
|
103
|
+
})
|
|
104
|
+
}));
|
|
105
|
+
const { findByText , findAllByTestId } = render(React.createElement(ValidationErrors, {
|
|
106
|
+
field: field,
|
|
107
|
+
space: space,
|
|
108
|
+
locales: utils.createFakeLocalesAPI(),
|
|
109
|
+
getEntryURL: (entry)=>`url.${entry.sys.id}`
|
|
110
|
+
}));
|
|
111
|
+
act(()=>{
|
|
112
|
+
emitter.emit('onSchemaErrorsChanged', errors);
|
|
113
|
+
});
|
|
114
|
+
await findByText(/Loading title for conflicting entry/);
|
|
115
|
+
const links = await findAllByTestId('cf-ui-text-link');
|
|
116
|
+
links.forEach((link, index)=>{
|
|
117
|
+
expect(link).toHaveAttribute('href', 'url.id-' + index);
|
|
118
|
+
expect(link).toHaveTextContent('entry-title-for-id-' + index);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ValidationErrors } from './ValidationErrors';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import tokens from '@contentful/f36-tokens';
|
|
2
|
+
import { css } from 'emotion';
|
|
3
|
+
export const errorList = css({
|
|
4
|
+
padding: 0,
|
|
5
|
+
wordWrap: 'break-word',
|
|
6
|
+
marginTop: tokens.spacingS,
|
|
7
|
+
color: tokens.red500,
|
|
8
|
+
listStyleType: 'none'
|
|
9
|
+
});
|
|
10
|
+
export const errorMessage = css({
|
|
11
|
+
display: 'inline-flex',
|
|
12
|
+
flexDirection: 'column',
|
|
13
|
+
marginLeft: tokens.spacingXs
|
|
14
|
+
});
|
|
15
|
+
export const errorItem = css({
|
|
16
|
+
display: 'flex',
|
|
17
|
+
alignItems: 'center'
|
|
18
|
+
});
|
|
19
|
+
export const entryLink = css({
|
|
20
|
+
fontWeight: Number(tokens.fontWeightDemiBold)
|
|
21
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import type {
|
|
3
|
-
export interface ValidationErrorsProps {
|
|
4
|
-
field: FieldAPI;
|
|
5
|
-
space: SpaceAPI;
|
|
6
|
-
locales: LocalesAPI;
|
|
7
|
-
getEntryURL: (entry: Entry) => string;
|
|
8
|
-
}
|
|
9
|
-
export declare function ValidationErrors(props: ValidationErrorsProps): JSX.Element | null;
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Entry, FieldAPI, LocalesAPI, SpaceAPI } from '@contentful/field-editor-shared';
|
|
3
|
+
export interface ValidationErrorsProps {
|
|
4
|
+
field: FieldAPI;
|
|
5
|
+
space: SpaceAPI;
|
|
6
|
+
locales: LocalesAPI;
|
|
7
|
+
getEntryURL: (entry: Entry) => string;
|
|
8
|
+
}
|
|
9
|
+
export declare function ValidationErrors(props: ValidationErrorsProps): React.JSX.Element | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ValidationErrors } from './ValidationErrors';
|
|
1
|
+
export { ValidationErrors } from './ValidationErrors';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const errorList: string;
|
|
2
|
-
export declare const errorMessage: string;
|
|
3
|
-
export declare const errorItem: string;
|
|
4
|
-
export declare const entryLink: string;
|
|
1
|
+
export declare const errorList: string;
|
|
2
|
+
export declare const errorMessage: string;
|
|
3
|
+
export declare const errorItem: string;
|
|
4
|
+
export declare const entryLink: string;
|