sanity-plugin-internationalized-array 0.0.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/LICENSE +21 -0
- package/README.md +79 -0
- package/lib/LanguageArray/ValueInput.js +17 -0
- package/lib/LanguageArray/ValueInput.js.map +1 -0
- package/lib/LanguageArray/index.js +243 -0
- package/lib/LanguageArray/index.js.map +1 -0
- package/lib/index.js +12 -0
- package/lib/index.js.map +1 -0
- package/lib/internationalizedArray.js +98 -0
- package/lib/internationalizedArray.js.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +60 -0
- package/sanity.json +7 -0
- package/src/LanguageArray/ValueInput.tsx +6 -0
- package/src/LanguageArray/index.tsx +291 -0
- package/src/index.ts +3 -0
- package/src/internationalizedArray.ts +75 -0
- package/src/types.ts +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Simeon Griggs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# sanity-plugin-internationalized-array
|
|
2
|
+
|
|
3
|
+
A helper function that renders a custom input component for writing localised fields of content into an array.
|
|
4
|
+
|
|
5
|
+
**This an early proof-of-concept and should not yet be used without thorough testing.**
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
sanity install internationalized-array
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Add an array to your schema by importing the helper function.
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import {internationalizedArray} from 'sanity-plugin-internationalized-array'
|
|
19
|
+
|
|
20
|
+
// ./src/schema/person.js
|
|
21
|
+
export default {
|
|
22
|
+
name: 'person',
|
|
23
|
+
title: 'Person',
|
|
24
|
+
type: 'document',
|
|
25
|
+
fields: [
|
|
26
|
+
// ...all your other fields
|
|
27
|
+
internationalizedArray({
|
|
28
|
+
name: 'greeting' // required
|
|
29
|
+
type: 'string' // required: string | text | number | boolean
|
|
30
|
+
languages: [
|
|
31
|
+
{id: 'en', title: 'English'},
|
|
32
|
+
{id: 'fr', title: 'French'},
|
|
33
|
+
] // required, must be an array of objects
|
|
34
|
+
showNativeInput: false // optional: just for debugging
|
|
35
|
+
})
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This will create an Array field where `string` fields can be added with the name `title`. The custom input contains buttons which will add new array items with the language as the `_key` value. Data returned from this array will look like this:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
"greeting": [
|
|
44
|
+
{ "_key": "en", "value": "hello" },
|
|
45
|
+
{ "_key": "fr", "value": "bonjour" },
|
|
46
|
+
]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Using GROQ filters you can query for a specific language key like so:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
*[_type == "person"] {
|
|
53
|
+
"greeting": greeting[_key == "en"][0].value
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Why store localised field data like this?
|
|
58
|
+
|
|
59
|
+
The most popular way to store translated content is in an object using the method prescribed in [@sanity/language-filter](https://www.npmjs.com/package/@sanity/language-filter). This works well and creates tidy object structures, but also create a unique field path for every unique field name, multiplied by the number of languages in your dataset.
|
|
60
|
+
|
|
61
|
+
For most people, this won't become an issue. On a very large dataset with a lot of languages, the [Attribute Limit](https://www.sanity.io/docs/attribute-limit) can become a concern.
|
|
62
|
+
|
|
63
|
+
An object with the same content as above would look like this:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
"greeting" {
|
|
67
|
+
"en": "hello",
|
|
68
|
+
"fr": "bonjour"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This creates three unique query paths. The array created by this plugin creates four.
|
|
73
|
+
|
|
74
|
+
However, every language added to the object increases the number of attributes. Where the array method is limited only by the amount of data you can store in your dataset (heaps!).
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT © Simeon Griggs
|
|
79
|
+
See LICENSE
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = ValueInput;
|
|
7
|
+
|
|
8
|
+
var _react = _interopRequireDefault(require("react"));
|
|
9
|
+
|
|
10
|
+
var _FormBuilderInput = require("@sanity/form-builder/lib/FormBuilderInput");
|
|
11
|
+
|
|
12
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
13
|
+
|
|
14
|
+
function ValueInput(props) {
|
|
15
|
+
return /*#__PURE__*/_react.default.createElement(_FormBuilderInput.FormBuilderInput, props);
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=ValueInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ValueInput.js","names":["ValueInput","props"],"sources":["../../src/LanguageArray/ValueInput.tsx"],"sourcesContent":["import React from 'react'\nimport {FormBuilderInput} from '@sanity/form-builder/lib/FormBuilderInput'\n\nexport default function ValueInput(props) {\n return <FormBuilderInput {...props} />\n}\n"],"mappings":";;;;;;;AAAA;;AACA;;;;AAEe,SAASA,UAAT,CAAoBC,KAApB,EAA2B;EACxC,oBAAO,6BAAC,kCAAD,EAAsBA,KAAtB,CAAP;AACD"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
|
|
8
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
9
|
+
|
|
10
|
+
var _sanityPluginNrknoOddUtils = require("@nrk/sanity-plugin-nrkno-odd-utils");
|
|
11
|
+
|
|
12
|
+
var _ui = require("@sanity/ui");
|
|
13
|
+
|
|
14
|
+
var _formBuilder = require("part:@sanity/form-builder");
|
|
15
|
+
|
|
16
|
+
var _PatchEvent = require("@sanity/form-builder/PatchEvent");
|
|
17
|
+
|
|
18
|
+
var _icons = require("@sanity/icons");
|
|
19
|
+
|
|
20
|
+
var _components = require("@sanity/base/components");
|
|
21
|
+
|
|
22
|
+
var _presence = require("@sanity/base/presence");
|
|
23
|
+
|
|
24
|
+
var _ValueInput = _interopRequireDefault(require("./ValueInput"));
|
|
25
|
+
|
|
26
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
27
|
+
|
|
28
|
+
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
|
29
|
+
|
|
30
|
+
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
31
|
+
|
|
32
|
+
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
33
|
+
|
|
34
|
+
var schemaExample = {
|
|
35
|
+
name: 'title',
|
|
36
|
+
type: 'localisedArray',
|
|
37
|
+
options: {
|
|
38
|
+
languages: [{
|
|
39
|
+
id: 'en',
|
|
40
|
+
title: 'English'
|
|
41
|
+
}, {
|
|
42
|
+
id: 'no',
|
|
43
|
+
title: 'Norsk'
|
|
44
|
+
}]
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var DEFAULT_OPTIONS = {
|
|
48
|
+
languages: [],
|
|
49
|
+
showNativeInput: false
|
|
50
|
+
};
|
|
51
|
+
var LanguageArrayWrapper = /*#__PURE__*/(0, _react.forwardRef)(function CustomComponent(props, ref) {
|
|
52
|
+
var _type$options;
|
|
53
|
+
|
|
54
|
+
var onChange = props.onChange,
|
|
55
|
+
onBlur = props.onBlur,
|
|
56
|
+
readOnly = props.readOnly,
|
|
57
|
+
presence = props.presence,
|
|
58
|
+
markers = props.markers;
|
|
59
|
+
var value = props === null || props === void 0 ? void 0 : props.value; // IMPORTANT: leaving out will cause the browser to lock up in an infinite loop
|
|
60
|
+
|
|
61
|
+
var type = (0, _sanityPluginNrknoOddUtils.useUnsetInputComponent)(props.type);
|
|
62
|
+
var options = (_type$options = type === null || type === void 0 ? void 0 : type.options) !== null && _type$options !== void 0 ? _type$options : DEFAULT_OPTIONS;
|
|
63
|
+
var languages = options.languages,
|
|
64
|
+
showNativeInput = options.showNativeInput;
|
|
65
|
+
var handleAddLanguage = (0, _react.useCallback)(languageId => {
|
|
66
|
+
// Create new items
|
|
67
|
+
var newItems = languageId ? // Just one for this language
|
|
68
|
+
[{
|
|
69
|
+
_key: languageId
|
|
70
|
+
}] : // Or one for every missing language
|
|
71
|
+
languages.filter(language => value !== null && value !== void 0 && value.length ? !value.find(v => v._key === language.id) : true).map(language => ({
|
|
72
|
+
_key: language.id
|
|
73
|
+
})); // Insert new items in the correct order
|
|
74
|
+
|
|
75
|
+
var languagesInUse = value !== null && value !== void 0 && value.length ? value.map(v => v) : [];
|
|
76
|
+
var insertions = newItems.map(item => {
|
|
77
|
+
// What's the original index of this language?
|
|
78
|
+
var languageIndex = languages.findIndex(l => item._key === l.id); // What languages are there beyond that index?
|
|
79
|
+
|
|
80
|
+
var remainingLanguages = languages.slice(languageIndex + 1); // So what is the index in the current value array of the next language in the language array?
|
|
81
|
+
|
|
82
|
+
var nextLanguageIndex = languagesInUse.findIndex(l => remainingLanguages.find(r => r.id === l._key)); // Keep local state up to date incase multiple insertions are being made
|
|
83
|
+
|
|
84
|
+
if (nextLanguageIndex < 0) {
|
|
85
|
+
languagesInUse.push(item);
|
|
86
|
+
} else {
|
|
87
|
+
languagesInUse.splice(nextLanguageIndex, 0, item);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return nextLanguageIndex < 0 ? // No next language (-1), add to end of array
|
|
91
|
+
(0, _PatchEvent.insert)([item], 'after', [nextLanguageIndex]) : // Next language found, insert before that
|
|
92
|
+
(0, _PatchEvent.insert)([item], 'before', [nextLanguageIndex]);
|
|
93
|
+
});
|
|
94
|
+
onChange(_PatchEvent.PatchEvent.from((0, _PatchEvent.setIfMissing)([]), ...insertions));
|
|
95
|
+
}, [languages, onChange, value]);
|
|
96
|
+
var handleUnsetByKey = (0, _react.useCallback)(_key => {
|
|
97
|
+
onChange(_PatchEvent.PatchEvent.from((0, _PatchEvent.unset)([{
|
|
98
|
+
_key
|
|
99
|
+
}])));
|
|
100
|
+
}, [onChange]);
|
|
101
|
+
var handleInnerValueChange = (0, _react.useCallback)((patchEvent, _key) => {
|
|
102
|
+
var _patchEvent$patches$;
|
|
103
|
+
|
|
104
|
+
var inputValue = (_patchEvent$patches$ = patchEvent.patches[0]) === null || _patchEvent$patches$ === void 0 ? void 0 : _patchEvent$patches$.value;
|
|
105
|
+
var inputPath = [{
|
|
106
|
+
_key
|
|
107
|
+
}, "value"];
|
|
108
|
+
onChange(_PatchEvent.PatchEvent.from(inputValue ? (0, _PatchEvent.set)(inputValue, inputPath) : (0, _PatchEvent.unset)(inputPath)));
|
|
109
|
+
}, [onChange]); // TODO: This is lazy, reordering and re-setting the whole array – it should be surgical
|
|
110
|
+
|
|
111
|
+
var handleRestoreOrder = (0, _react.useCallback)(() => {
|
|
112
|
+
// Create a new value array in the correct order
|
|
113
|
+
var updatedValue = value.reduce((acc, v) => {
|
|
114
|
+
var newIndex = languages.findIndex(l => l.id === v._key);
|
|
115
|
+
acc[newIndex] = v;
|
|
116
|
+
return acc;
|
|
117
|
+
}, []);
|
|
118
|
+
onChange(_PatchEvent.PatchEvent.from((0, _PatchEvent.unset)(), (0, _PatchEvent.set)(updatedValue)));
|
|
119
|
+
}, [languages, onChange, value]); // Check languages are in the correct order
|
|
120
|
+
|
|
121
|
+
var languagesOutOfOrder = (0, _react.useMemo)(() => {
|
|
122
|
+
if (!(value !== null && value !== void 0 && value.length)) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var languagesInUse = languages.filter(l => value.find(v => v._key === l.id));
|
|
127
|
+
return value.map((v, vIndex) => vIndex === languagesInUse.findIndex(l => l.id === v._key) ? null : v).filter(Boolean);
|
|
128
|
+
}, [value, languages]); // Check options are supplied and valid
|
|
129
|
+
|
|
130
|
+
var languagesAreValid = (0, _react.useMemo)(() => (languages === null || languages === void 0 ? void 0 : languages.length) && languages.every(item => item.id && item.title), [languages]);
|
|
131
|
+
|
|
132
|
+
if (!languagesAreValid) {
|
|
133
|
+
return /*#__PURE__*/_react.default.createElement(_ui.Card, {
|
|
134
|
+
tone: "caution",
|
|
135
|
+
border: true,
|
|
136
|
+
radius: 2,
|
|
137
|
+
padding: 3
|
|
138
|
+
}, /*#__PURE__*/_react.default.createElement(_ui.Stack, {
|
|
139
|
+
space: 4
|
|
140
|
+
}, /*#__PURE__*/_react.default.createElement(_ui.Text, null, "An array of language objects must be passed into the ", /*#__PURE__*/_react.default.createElement("code", null, type.name), " field as options, each with an ", /*#__PURE__*/_react.default.createElement("code", null, "id"), " and ", /*#__PURE__*/_react.default.createElement("code", null, "title"), " field. Example:"), /*#__PURE__*/_react.default.createElement(_ui.Card, {
|
|
141
|
+
padding: 2,
|
|
142
|
+
border: true,
|
|
143
|
+
radius: 2
|
|
144
|
+
}, /*#__PURE__*/_react.default.createElement(_ui.Code, {
|
|
145
|
+
size: 1,
|
|
146
|
+
language: "javascript"
|
|
147
|
+
}, JSON.stringify(schemaExample, null, 2)))));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
var validationMarkers = markers !== null && markers !== void 0 && markers.length ? markers.filter(mark => mark.type === "validation") : [];
|
|
151
|
+
var invalidKeys = validationMarkers.map(mark => mark.path).flat().map(item => item._key);
|
|
152
|
+
return /*#__PURE__*/_react.default.createElement(_ui.Stack, {
|
|
153
|
+
space: 2
|
|
154
|
+
}, (value === null || value === void 0 ? void 0 : value.length) > 0 ? /*#__PURE__*/_react.default.createElement(_ui.Card, {
|
|
155
|
+
padding: 1,
|
|
156
|
+
border: true,
|
|
157
|
+
radius: 1
|
|
158
|
+
}, /*#__PURE__*/_react.default.createElement(_ui.Stack, {
|
|
159
|
+
space: 1
|
|
160
|
+
}, value.map(item => {
|
|
161
|
+
var _type$of;
|
|
162
|
+
|
|
163
|
+
return /*#__PURE__*/_react.default.createElement(_ui.Card, {
|
|
164
|
+
paddingY: 1,
|
|
165
|
+
paddingX: 2,
|
|
166
|
+
key: item._key,
|
|
167
|
+
tone: // TODO: Move this logic somewhere else
|
|
168
|
+
invalidKeys.includes(item._key) ? "critical" : undefined || languagesOutOfOrder.find(l => l._key === item._key) ? "caution" : undefined
|
|
169
|
+
}, /*#__PURE__*/_react.default.createElement(_ui.Flex, {
|
|
170
|
+
gap: 3,
|
|
171
|
+
align: "center"
|
|
172
|
+
}, (type === null || type === void 0 ? void 0 : (_type$of = type.of) === null || _type$of === void 0 ? void 0 : _type$of.length) > 0 && (type === null || type === void 0 ? void 0 : type.of.map(subType => {
|
|
173
|
+
var _subType$fields;
|
|
174
|
+
|
|
175
|
+
return /*#__PURE__*/_react.default.createElement(_ui.Flex, {
|
|
176
|
+
key: subType.name,
|
|
177
|
+
flex: 1,
|
|
178
|
+
align: "center",
|
|
179
|
+
gap: 2
|
|
180
|
+
}, (subType === null || subType === void 0 ? void 0 : (_subType$fields = subType.fields) === null || _subType$fields === void 0 ? void 0 : _subType$fields.length) > 0 ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_ui.Box, null, /*#__PURE__*/_react.default.createElement(_ui.Label, null, item._key)), /*#__PURE__*/_react.default.createElement(_ui.Box, {
|
|
181
|
+
flex: 1
|
|
182
|
+
}, subType.fields.map(subTypeField => /*#__PURE__*/_react.default.createElement(_ValueInput.default, {
|
|
183
|
+
key: subTypeField.name,
|
|
184
|
+
onChange: patchEvent => handleInnerValueChange(patchEvent, item._key),
|
|
185
|
+
onBlur: onBlur // We don't want the array item to open onFocus
|
|
186
|
+
,
|
|
187
|
+
onFocus: () => null,
|
|
188
|
+
path: [{
|
|
189
|
+
_key: item._key
|
|
190
|
+
}, subTypeField.name],
|
|
191
|
+
parent: item,
|
|
192
|
+
readOnly: readOnly,
|
|
193
|
+
type: subTypeField,
|
|
194
|
+
value: item.value,
|
|
195
|
+
level: props.level + 1,
|
|
196
|
+
markers: []
|
|
197
|
+
})))) : null);
|
|
198
|
+
})), (presence === null || presence === void 0 ? void 0 : presence.length) > 0 ? /*#__PURE__*/_react.default.createElement(_presence.FieldPresence, {
|
|
199
|
+
maxAvatars: 1,
|
|
200
|
+
presence: presence
|
|
201
|
+
}) : null, invalidKeys.includes(item._key) ? /*#__PURE__*/_react.default.createElement(_components.FormFieldValidationStatus, {
|
|
202
|
+
__unstable_markers: validationMarkers
|
|
203
|
+
}) : null, /*#__PURE__*/_react.default.createElement(_ui.Button, {
|
|
204
|
+
mode: "ghost",
|
|
205
|
+
icon: _icons.RemoveIcon,
|
|
206
|
+
tone: "critical",
|
|
207
|
+
onClick: () => handleUnsetByKey(item._key)
|
|
208
|
+
})));
|
|
209
|
+
}))) : null, languagesOutOfOrder.length > 0 ? /*#__PURE__*/_react.default.createElement(_ui.Button, {
|
|
210
|
+
tone: "caution",
|
|
211
|
+
disabled: languagesOutOfOrder.length > languages.length,
|
|
212
|
+
icon: _icons.RestoreIcon,
|
|
213
|
+
onClick: () => handleRestoreOrder(),
|
|
214
|
+
text: "Restore order of languages"
|
|
215
|
+
}) : null, languages.length > 0 ? /*#__PURE__*/_react.default.createElement(_ui.Stack, {
|
|
216
|
+
space: 2
|
|
217
|
+
}, /*#__PURE__*/_react.default.createElement(_ui.Grid, {
|
|
218
|
+
columns: Math.min(languages.length, 5),
|
|
219
|
+
gap: 2
|
|
220
|
+
}, languages.map(language => /*#__PURE__*/_react.default.createElement(_ui.Button, {
|
|
221
|
+
key: language.id,
|
|
222
|
+
tone: "primary",
|
|
223
|
+
mode: "ghost",
|
|
224
|
+
fontSize: 1,
|
|
225
|
+
disabled: readOnly || (value === null || value === void 0 ? void 0 : value.find(item => item._key === language.id)),
|
|
226
|
+
text: language.id.toUpperCase(),
|
|
227
|
+
icon: _icons.AddIcon,
|
|
228
|
+
onClick: () => handleAddLanguage(language.id)
|
|
229
|
+
}))), /*#__PURE__*/_react.default.createElement(_ui.Button, {
|
|
230
|
+
tone: "primary",
|
|
231
|
+
mode: "ghost",
|
|
232
|
+
disabled: readOnly || (value === null || value === void 0 ? void 0 : value.length) >= (languages === null || languages === void 0 ? void 0 : languages.length),
|
|
233
|
+
text: value !== null && value !== void 0 && value.length ? "Add missing languages" : "Add all languages",
|
|
234
|
+
onClick: () => handleAddLanguage()
|
|
235
|
+
})) : null, showNativeInput ? /*#__PURE__*/_react.default.createElement(_sanityPluginNrknoOddUtils.NestedFormBuilder, _extends({}, props, {
|
|
236
|
+
type: type
|
|
237
|
+
})) : null);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
var _default = (0, _formBuilder.withDocument)(LanguageArrayWrapper);
|
|
241
|
+
|
|
242
|
+
exports.default = _default;
|
|
243
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["schemaExample","name","type","options","languages","id","title","DEFAULT_OPTIONS","showNativeInput","LanguageArrayWrapper","forwardRef","CustomComponent","props","ref","onChange","onBlur","readOnly","presence","markers","value","useUnsetInputComponent","handleAddLanguage","useCallback","languageId","newItems","_key","filter","language","length","find","v","map","languagesInUse","insertions","item","languageIndex","findIndex","l","remainingLanguages","slice","nextLanguageIndex","r","push","splice","insert","PatchEvent","from","setIfMissing","handleUnsetByKey","unset","handleInnerValueChange","patchEvent","inputValue","patches","inputPath","set","handleRestoreOrder","updatedValue","reduce","acc","newIndex","languagesOutOfOrder","useMemo","vIndex","Boolean","languagesAreValid","every","JSON","stringify","validationMarkers","mark","invalidKeys","path","flat","includes","undefined","of","subType","fields","subTypeField","level","RemoveIcon","RestoreIcon","Math","min","toUpperCase","AddIcon","withDocument"],"sources":["../../src/LanguageArray/index.tsx"],"sourcesContent":["import React, {forwardRef, useCallback, useMemo} from 'react'\nimport {useUnsetInputComponent, NestedFormBuilder} from '@nrk/sanity-plugin-nrkno-odd-utils'\nimport {Code, Text, Card, Label, Flex, Box, Stack, Button, Grid} from '@sanity/ui'\nimport {withDocument} from 'part:@sanity/form-builder'\nimport {PatchEvent, setIfMissing, insert, unset, set} from '@sanity/form-builder/PatchEvent'\nimport {AddIcon, RemoveIcon, RestoreIcon} from '@sanity/icons'\nimport {FormFieldValidationStatus} from '@sanity/base/components'\nimport {FieldPresence} from '@sanity/base/presence'\n\nimport ValueInput from './ValueInput'\n\nconst schemaExample = {\n name: 'title',\n type: 'localisedArray',\n options: {\n languages: [\n {id: 'en', title: 'English'},\n {id: 'no', title: 'Norsk'},\n ],\n },\n}\n\ntype Value = {\n _key: string\n value?: string\n}\n\ntype Language = {\n id: string\n title: string\n}\n\ntype Options = {\n languages: Language[]\n showNativeInput: boolean\n}\n\nconst DEFAULT_OPTIONS = {\n languages: [],\n showNativeInput: false,\n}\n\nconst LanguageArrayWrapper = forwardRef(function CustomComponent(props, ref) {\n const {onChange, onBlur, readOnly, presence, markers} = props\n const value: Value[] = props?.value\n\n // IMPORTANT: leaving out will cause the browser to lock up in an infinite loop\n const type = useUnsetInputComponent(props.type)\n const options: Options = type?.options ?? DEFAULT_OPTIONS\n const {languages, showNativeInput} = options\n\n const handleAddLanguage = useCallback(\n (languageId?: string) => {\n // Create new items\n const newItems = languageId\n ? // Just one for this language\n [{_key: languageId}]\n : // Or one for every missing language\n languages\n .filter((language) =>\n value?.length ? !value.find((v) => v._key === language.id) : true\n )\n .map((language) => ({_key: language.id}))\n\n // Insert new items in the correct order\n const languagesInUse = value?.length ? value.map((v) => v) : []\n\n const insertions = newItems.map((item) => {\n // What's the original index of this language?\n const languageIndex = languages.findIndex((l) => item._key === l.id)\n\n // What languages are there beyond that index?\n const remainingLanguages = languages.slice(languageIndex + 1)\n\n // So what is the index in the current value array of the next language in the language array?\n const nextLanguageIndex = languagesInUse.findIndex((l) =>\n remainingLanguages.find((r) => r.id === l._key)\n )\n\n // Keep local state up to date incase multiple insertions are being made\n if (nextLanguageIndex < 0) {\n languagesInUse.push(item)\n } else {\n languagesInUse.splice(nextLanguageIndex, 0, item)\n }\n\n return nextLanguageIndex < 0\n ? // No next language (-1), add to end of array\n insert([item], 'after', [nextLanguageIndex])\n : // Next language found, insert before that\n insert([item], 'before', [nextLanguageIndex])\n })\n\n onChange(PatchEvent.from(setIfMissing([]), ...insertions))\n },\n [languages, onChange, value]\n )\n\n const handleUnsetByKey = useCallback(\n (_key) => {\n onChange(PatchEvent.from(unset([{_key}])))\n },\n [onChange]\n )\n\n const handleInnerValueChange = useCallback(\n (patchEvent: PatchEvent, _key: string) => {\n const inputValue = patchEvent.patches[0]?.value\n const inputPath = [{_key}, `value`]\n\n onChange(PatchEvent.from(inputValue ? set(inputValue, inputPath) : unset(inputPath)))\n },\n [onChange]\n )\n\n // TODO: This is lazy, reordering and re-setting the whole array – it should be surgical\n const handleRestoreOrder = useCallback(() => {\n // Create a new value array in the correct order\n const updatedValue = value.reduce((acc, v) => {\n const newIndex = languages.findIndex((l) => l.id === v._key)\n\n acc[newIndex] = v\n\n return acc\n }, [])\n\n onChange(PatchEvent.from(unset(), set(updatedValue)))\n }, [languages, onChange, value])\n\n // Check languages are in the correct order\n const languagesOutOfOrder = useMemo(() => {\n if (!value?.length) {\n return []\n }\n\n const languagesInUse = languages.filter((l) => value.find((v) => v._key === l.id))\n\n return value\n .map((v, vIndex) => (vIndex === languagesInUse.findIndex((l) => l.id === v._key) ? null : v))\n .filter(Boolean)\n }, [value, languages])\n\n // Check options are supplied and valid\n const languagesAreValid = useMemo(\n () => languages?.length && languages.every((item) => item.id && item.title),\n [languages]\n )\n\n if (!languagesAreValid) {\n return (\n <Card tone=\"caution\" border radius={2} padding={3}>\n <Stack space={4}>\n <Text>\n An array of language objects must be passed into the <code>{type.name}</code> field as\n options, each with an <code>id</code> and <code>title</code> field. Example:\n </Text>\n <Card padding={2} border radius={2}>\n <Code size={1} language=\"javascript\">\n {JSON.stringify(schemaExample, null, 2)}\n </Code>\n </Card>\n </Stack>\n </Card>\n )\n }\n\n const validationMarkers = markers?.length\n ? markers.filter((mark) => mark.type === `validation`)\n : []\n const invalidKeys = validationMarkers\n .map((mark) => mark.path)\n .flat()\n .map((item) => item._key)\n\n return (\n <Stack space={2}>\n {/* Loop over the values */}\n {value?.length > 0 ? (\n <Card padding={1} border radius={1}>\n <Stack space={1}>\n {value.map((item) => (\n <Card\n paddingY={1}\n paddingX={2}\n key={item._key}\n tone={\n // TODO: Move this logic somewhere else\n invalidKeys.includes(item._key)\n ? `critical`\n : undefined || languagesOutOfOrder.find((l) => l._key === item._key)\n ? `caution`\n : undefined\n }\n >\n <Flex gap={3} align=\"center\">\n {/* To render each individual field in this type */}\n {type?.of?.length > 0 &&\n type?.of.map((subType) => (\n <Flex key={subType.name} flex={1} align=\"center\" gap={2}>\n {subType?.fields?.length > 0 ? (\n <>\n <Box>\n <Label>{item._key}</Label>\n </Box>\n <Box flex={1}>\n {/* There _should_ only be one field */}\n {subType.fields.map((subTypeField) => (\n <ValueInput\n key={subTypeField.name}\n onChange={(patchEvent) =>\n handleInnerValueChange(patchEvent, item._key)\n }\n onBlur={onBlur}\n // We don't want the array item to open onFocus\n onFocus={() => null}\n path={[{_key: item._key}, subTypeField.name]}\n parent={item}\n readOnly={readOnly}\n type={subTypeField}\n value={item.value}\n level={props.level + 1}\n markers={[]}\n />\n ))}\n </Box>\n </>\n ) : null}\n </Flex>\n ))}\n {presence?.length > 0 ? (\n <FieldPresence maxAvatars={1} presence={presence} />\n ) : null}\n {invalidKeys.includes(item._key) ? (\n <FormFieldValidationStatus __unstable_markers={validationMarkers} />\n ) : null}\n <Button\n mode=\"ghost\"\n icon={RemoveIcon}\n tone=\"critical\"\n onClick={() => handleUnsetByKey(item._key)}\n />\n </Flex>\n </Card>\n ))}\n </Stack>\n </Card>\n ) : null}\n\n {languagesOutOfOrder.length > 0 ? (\n <Button\n tone=\"caution\"\n disabled={languagesOutOfOrder.length > languages.length}\n icon={RestoreIcon}\n onClick={() => handleRestoreOrder()}\n text=\"Restore order of languages\"\n />\n ) : null}\n\n {languages.length > 0 ? (\n <Stack space={2}>\n {/* No more than 5 columns */}\n <Grid columns={Math.min(languages.length, 5)} gap={2}>\n {languages.map((language) => (\n <Button\n key={language.id}\n tone=\"primary\"\n mode=\"ghost\"\n fontSize={1}\n disabled={readOnly || value?.find((item) => item._key === language.id)}\n text={language.id.toUpperCase()}\n icon={AddIcon}\n onClick={() => handleAddLanguage(language.id)}\n />\n ))}\n </Grid>\n <Button\n tone=\"primary\"\n mode=\"ghost\"\n disabled={readOnly || value?.length >= languages?.length}\n text={value?.length ? `Add missing languages` : `Add all languages`}\n onClick={() => handleAddLanguage()}\n />\n </Stack>\n ) : null}\n\n {showNativeInput ? <NestedFormBuilder {...props} type={type} /> : null}\n </Stack>\n )\n})\n\nexport default withDocument(LanguageArrayWrapper)\n"],"mappings":"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"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
|
|
8
|
+
var _internationalizedArray = require("./internationalizedArray");
|
|
9
|
+
|
|
10
|
+
var _default = _internationalizedArray.internationalizedArray;
|
|
11
|
+
exports.default = _default;
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["internationalizedArray"],"sources":["../src/index.ts"],"sourcesContent":["import {internationalizedArray} from './internationalizedArray'\n\nexport default internationalizedArray\n"],"mappings":";;;;;;;AAAA;;eAEeA,8C"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.internationalizedArray = internationalizedArray;
|
|
7
|
+
|
|
8
|
+
var _LanguageArray = _interopRequireDefault(require("./LanguageArray"));
|
|
9
|
+
|
|
10
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
11
|
+
|
|
12
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
13
|
+
|
|
14
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
15
|
+
|
|
16
|
+
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
17
|
+
|
|
18
|
+
function internationalizedArray(config) {
|
|
19
|
+
var _config$name = config.name,
|
|
20
|
+
name = _config$name === void 0 ? "title" : _config$name,
|
|
21
|
+
_config$type = config.type,
|
|
22
|
+
type = _config$type === void 0 ? "string" : _config$type,
|
|
23
|
+
_config$languages = config.languages,
|
|
24
|
+
languages = _config$languages === void 0 ? [] : _config$languages,
|
|
25
|
+
_config$showNativeInp = config.showNativeInput,
|
|
26
|
+
showNativeInput = _config$showNativeInp === void 0 ? false : _config$showNativeInp;
|
|
27
|
+
return {
|
|
28
|
+
name,
|
|
29
|
+
type: 'array',
|
|
30
|
+
inputComponent: _LanguageArray.default,
|
|
31
|
+
options: {
|
|
32
|
+
languages,
|
|
33
|
+
showNativeInput
|
|
34
|
+
},
|
|
35
|
+
of: [{
|
|
36
|
+
type: 'object',
|
|
37
|
+
fields: [{
|
|
38
|
+
name: 'value',
|
|
39
|
+
type
|
|
40
|
+
}],
|
|
41
|
+
preview: {
|
|
42
|
+
select: {
|
|
43
|
+
title: 'value',
|
|
44
|
+
key: '_key'
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
prepare(_ref) {
|
|
48
|
+
var title = _ref.title,
|
|
49
|
+
key = _ref.key;
|
|
50
|
+
return {
|
|
51
|
+
title,
|
|
52
|
+
subtitle: key.toUpperCase()
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
}],
|
|
58
|
+
validation: Rule => Rule.max(languages.length).custom((value, context) => {
|
|
59
|
+
var languages = context.type.options.languages;
|
|
60
|
+
var nonLanguageKeys = value !== null && value !== void 0 && value.length ? value.filter(item => !languages.find(language => item._key === language.id)) : [];
|
|
61
|
+
|
|
62
|
+
if (nonLanguageKeys.length) {
|
|
63
|
+
return {
|
|
64
|
+
message: "Array item keys must be valid languages registered to the field type",
|
|
65
|
+
paths: nonLanguageKeys.map(item => ({
|
|
66
|
+
_key: item._key
|
|
67
|
+
}))
|
|
68
|
+
};
|
|
69
|
+
} // Ensure there's no duplicate `language` fields
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
var valuesByLanguage = value !== null && value !== void 0 && value.length ? value.filter(item => Boolean(item === null || item === void 0 ? void 0 : item._key)).reduce((acc, cur) => {
|
|
73
|
+
if (acc[cur._key]) {
|
|
74
|
+
return _objectSpread(_objectSpread({}, acc), {}, {
|
|
75
|
+
[cur._key]: [...acc[cur._key], cur]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return _objectSpread(_objectSpread({}, acc), {}, {
|
|
80
|
+
[cur._key]: [cur]
|
|
81
|
+
});
|
|
82
|
+
}, {}) : {};
|
|
83
|
+
var duplicateValues = Object.values(valuesByLanguage).filter(item => (item === null || item === void 0 ? void 0 : item.length) > 1).flat();
|
|
84
|
+
|
|
85
|
+
if (duplicateValues.length) {
|
|
86
|
+
return {
|
|
87
|
+
message: 'There can only be one field per language',
|
|
88
|
+
paths: duplicateValues.map(item => ({
|
|
89
|
+
_key: item._key
|
|
90
|
+
}))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=internationalizedArray.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internationalizedArray.js","names":["internationalizedArray","config","name","type","languages","showNativeInput","inputComponent","LanguageArray","options","of","fields","preview","select","title","key","prepare","subtitle","toUpperCase","validation","Rule","max","length","custom","value","context","nonLanguageKeys","filter","item","find","language","_key","id","message","paths","map","valuesByLanguage","Boolean","reduce","acc","cur","duplicateValues","Object","values","flat"],"sources":["../src/internationalizedArray.ts"],"sourcesContent":["import {ArrayConfig, Value} from './types'\nimport LanguageArray from './LanguageArray'\n\nexport function internationalizedArray(config: ArrayConfig) {\n const {name = `title`, type = `string`, languages = [], showNativeInput = false} = config\n\n return {\n name,\n type: 'array',\n inputComponent: LanguageArray,\n options: {\n languages,\n showNativeInput,\n },\n of: [\n {\n type: 'object',\n fields: [{name: 'value', type}],\n preview: {\n select: {title: 'value', key: '_key'},\n prepare({title, key}) {\n return {\n title,\n subtitle: key.toUpperCase(),\n }\n },\n },\n },\n ],\n validation: (Rule) =>\n Rule.max(languages.length).custom((value: Value[], context) => {\n const {languages} = context.type.options\n\n const nonLanguageKeys = value?.length\n ? value.filter((item) => !languages.find((language) => item._key === language.id))\n : []\n\n if (nonLanguageKeys.length) {\n return {\n message: `Array item keys must be valid languages registered to the field type`,\n paths: nonLanguageKeys.map((item) => ({_key: item._key})),\n }\n }\n\n // Ensure there's no duplicate `language` fields\n const valuesByLanguage = value?.length\n ? value\n .filter((item) => Boolean(item?._key))\n .reduce((acc, cur) => {\n if (acc[cur._key]) {\n return {...acc, [cur._key]: [...acc[cur._key], cur]}\n }\n\n return {\n ...acc,\n [cur._key]: [cur],\n }\n }, {})\n : {}\n\n const duplicateValues = Object.values(valuesByLanguage)\n .filter((item) => item?.length > 1)\n .flat()\n\n if (duplicateValues.length) {\n return {\n message: 'There can only be one field per language',\n paths: duplicateValues.map((item) => ({_key: item._key})),\n }\n }\n\n return true\n }),\n }\n}\n"],"mappings":"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"}
|
package/lib/types.js
ADDED
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../src/types.ts"],"sourcesContent":["export type ArrayConfig = Options & {\n name: string\n type: 'string' | 'number' | 'boolean' | 'text'\n}\n\nexport type Value = {\n _key: string\n value?: string\n}\n\nexport type Language = {\n id: string\n title: string\n}\n\nexport type Options = {\n languages: Language[]\n showNativeInput: boolean\n}\n"],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sanity-plugin-internationalized-array",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Store localised fields in an array to save on attributes",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "sanipack build",
|
|
8
|
+
"verify": "sanipack verify",
|
|
9
|
+
"watch": "sanipack build --watch",
|
|
10
|
+
"_postinstall": "husky install",
|
|
11
|
+
"prepublishOnly": "pinst --disable && sanipack build && sanipack verify",
|
|
12
|
+
"postpublish": "pinst --enable",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint . --fix"
|
|
15
|
+
},
|
|
16
|
+
"husky": {
|
|
17
|
+
"hooks": {
|
|
18
|
+
"pre-commit": "npm run lint:fix"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+ssh://git@github.com/sanity-io/sanity-plugin-internationalized-array.git"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"sanity",
|
|
27
|
+
"sanity-plugin"
|
|
28
|
+
],
|
|
29
|
+
"author": "Sanity.io <hello@sanity.io>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@nrk/sanity-plugin-nrkno-odd-utils": "^1.0.11",
|
|
33
|
+
"@sanity/icons": "^1.3.1",
|
|
34
|
+
"@sanity/ui": "^0.37.12"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@sanity/base": "^2.30.1",
|
|
38
|
+
"@sanity/desk-tool": "^2.30.1",
|
|
39
|
+
"@sanity/form-builder": "^2.30.1",
|
|
40
|
+
"@sanity/util": "^2.29.5",
|
|
41
|
+
"react": "^16.0.0 || ^17.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@sanity/eslint-config-studio": "^2.0.0",
|
|
45
|
+
"eslint": "8.19.0",
|
|
46
|
+
"eslint-config-prettier": "^8.5.0",
|
|
47
|
+
"eslint-config-sanity": "6.0.0",
|
|
48
|
+
"eslint-plugin-prettier": "^4.2.1",
|
|
49
|
+
"eslint-plugin-react": "^7.30.1",
|
|
50
|
+
"husky": "^8.0.1",
|
|
51
|
+
"pinst": "^3.0.0",
|
|
52
|
+
"prettier": "^2.7.1",
|
|
53
|
+
"sanipack": "^2.1.0",
|
|
54
|
+
"typescript": "^4.7.4"
|
|
55
|
+
},
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/sanity-io/sanity-plugin-internationalized-array/issues"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/sanity-io/sanity-plugin-internationalized-array#readme"
|
|
60
|
+
}
|
package/sanity.json
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import React, {forwardRef, useCallback, useMemo} from 'react'
|
|
2
|
+
import {useUnsetInputComponent, NestedFormBuilder} from '@nrk/sanity-plugin-nrkno-odd-utils'
|
|
3
|
+
import {Code, Text, Card, Label, Flex, Box, Stack, Button, Grid} from '@sanity/ui'
|
|
4
|
+
import {withDocument} from 'part:@sanity/form-builder'
|
|
5
|
+
import {PatchEvent, setIfMissing, insert, unset, set} from '@sanity/form-builder/PatchEvent'
|
|
6
|
+
import {AddIcon, RemoveIcon, RestoreIcon} from '@sanity/icons'
|
|
7
|
+
import {FormFieldValidationStatus} from '@sanity/base/components'
|
|
8
|
+
import {FieldPresence} from '@sanity/base/presence'
|
|
9
|
+
|
|
10
|
+
import ValueInput from './ValueInput'
|
|
11
|
+
|
|
12
|
+
const schemaExample = {
|
|
13
|
+
name: 'title',
|
|
14
|
+
type: 'localisedArray',
|
|
15
|
+
options: {
|
|
16
|
+
languages: [
|
|
17
|
+
{id: 'en', title: 'English'},
|
|
18
|
+
{id: 'no', title: 'Norsk'},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Value = {
|
|
24
|
+
_key: string
|
|
25
|
+
value?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Language = {
|
|
29
|
+
id: string
|
|
30
|
+
title: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Options = {
|
|
34
|
+
languages: Language[]
|
|
35
|
+
showNativeInput: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_OPTIONS = {
|
|
39
|
+
languages: [],
|
|
40
|
+
showNativeInput: false,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const LanguageArrayWrapper = forwardRef(function CustomComponent(props, ref) {
|
|
44
|
+
const {onChange, onBlur, readOnly, presence, markers} = props
|
|
45
|
+
const value: Value[] = props?.value
|
|
46
|
+
|
|
47
|
+
// IMPORTANT: leaving out will cause the browser to lock up in an infinite loop
|
|
48
|
+
const type = useUnsetInputComponent(props.type)
|
|
49
|
+
const options: Options = type?.options ?? DEFAULT_OPTIONS
|
|
50
|
+
const {languages, showNativeInput} = options
|
|
51
|
+
|
|
52
|
+
const handleAddLanguage = useCallback(
|
|
53
|
+
(languageId?: string) => {
|
|
54
|
+
// Create new items
|
|
55
|
+
const newItems = languageId
|
|
56
|
+
? // Just one for this language
|
|
57
|
+
[{_key: languageId}]
|
|
58
|
+
: // Or one for every missing language
|
|
59
|
+
languages
|
|
60
|
+
.filter((language) =>
|
|
61
|
+
value?.length ? !value.find((v) => v._key === language.id) : true
|
|
62
|
+
)
|
|
63
|
+
.map((language) => ({_key: language.id}))
|
|
64
|
+
|
|
65
|
+
// Insert new items in the correct order
|
|
66
|
+
const languagesInUse = value?.length ? value.map((v) => v) : []
|
|
67
|
+
|
|
68
|
+
const insertions = newItems.map((item) => {
|
|
69
|
+
// What's the original index of this language?
|
|
70
|
+
const languageIndex = languages.findIndex((l) => item._key === l.id)
|
|
71
|
+
|
|
72
|
+
// What languages are there beyond that index?
|
|
73
|
+
const remainingLanguages = languages.slice(languageIndex + 1)
|
|
74
|
+
|
|
75
|
+
// So what is the index in the current value array of the next language in the language array?
|
|
76
|
+
const nextLanguageIndex = languagesInUse.findIndex((l) =>
|
|
77
|
+
remainingLanguages.find((r) => r.id === l._key)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// Keep local state up to date incase multiple insertions are being made
|
|
81
|
+
if (nextLanguageIndex < 0) {
|
|
82
|
+
languagesInUse.push(item)
|
|
83
|
+
} else {
|
|
84
|
+
languagesInUse.splice(nextLanguageIndex, 0, item)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return nextLanguageIndex < 0
|
|
88
|
+
? // No next language (-1), add to end of array
|
|
89
|
+
insert([item], 'after', [nextLanguageIndex])
|
|
90
|
+
: // Next language found, insert before that
|
|
91
|
+
insert([item], 'before', [nextLanguageIndex])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
onChange(PatchEvent.from(setIfMissing([]), ...insertions))
|
|
95
|
+
},
|
|
96
|
+
[languages, onChange, value]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const handleUnsetByKey = useCallback(
|
|
100
|
+
(_key) => {
|
|
101
|
+
onChange(PatchEvent.from(unset([{_key}])))
|
|
102
|
+
},
|
|
103
|
+
[onChange]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const handleInnerValueChange = useCallback(
|
|
107
|
+
(patchEvent: PatchEvent, _key: string) => {
|
|
108
|
+
const inputValue = patchEvent.patches[0]?.value
|
|
109
|
+
const inputPath = [{_key}, `value`]
|
|
110
|
+
|
|
111
|
+
onChange(PatchEvent.from(inputValue ? set(inputValue, inputPath) : unset(inputPath)))
|
|
112
|
+
},
|
|
113
|
+
[onChange]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// TODO: This is lazy, reordering and re-setting the whole array – it should be surgical
|
|
117
|
+
const handleRestoreOrder = useCallback(() => {
|
|
118
|
+
// Create a new value array in the correct order
|
|
119
|
+
const updatedValue = value.reduce((acc, v) => {
|
|
120
|
+
const newIndex = languages.findIndex((l) => l.id === v._key)
|
|
121
|
+
|
|
122
|
+
acc[newIndex] = v
|
|
123
|
+
|
|
124
|
+
return acc
|
|
125
|
+
}, [])
|
|
126
|
+
|
|
127
|
+
onChange(PatchEvent.from(unset(), set(updatedValue)))
|
|
128
|
+
}, [languages, onChange, value])
|
|
129
|
+
|
|
130
|
+
// Check languages are in the correct order
|
|
131
|
+
const languagesOutOfOrder = useMemo(() => {
|
|
132
|
+
if (!value?.length) {
|
|
133
|
+
return []
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const languagesInUse = languages.filter((l) => value.find((v) => v._key === l.id))
|
|
137
|
+
|
|
138
|
+
return value
|
|
139
|
+
.map((v, vIndex) => (vIndex === languagesInUse.findIndex((l) => l.id === v._key) ? null : v))
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
}, [value, languages])
|
|
142
|
+
|
|
143
|
+
// Check options are supplied and valid
|
|
144
|
+
const languagesAreValid = useMemo(
|
|
145
|
+
() => languages?.length && languages.every((item) => item.id && item.title),
|
|
146
|
+
[languages]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if (!languagesAreValid) {
|
|
150
|
+
return (
|
|
151
|
+
<Card tone="caution" border radius={2} padding={3}>
|
|
152
|
+
<Stack space={4}>
|
|
153
|
+
<Text>
|
|
154
|
+
An array of language objects must be passed into the <code>{type.name}</code> field as
|
|
155
|
+
options, each with an <code>id</code> and <code>title</code> field. Example:
|
|
156
|
+
</Text>
|
|
157
|
+
<Card padding={2} border radius={2}>
|
|
158
|
+
<Code size={1} language="javascript">
|
|
159
|
+
{JSON.stringify(schemaExample, null, 2)}
|
|
160
|
+
</Code>
|
|
161
|
+
</Card>
|
|
162
|
+
</Stack>
|
|
163
|
+
</Card>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const validationMarkers = markers?.length
|
|
168
|
+
? markers.filter((mark) => mark.type === `validation`)
|
|
169
|
+
: []
|
|
170
|
+
const invalidKeys = validationMarkers
|
|
171
|
+
.map((mark) => mark.path)
|
|
172
|
+
.flat()
|
|
173
|
+
.map((item) => item._key)
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<Stack space={2}>
|
|
177
|
+
{/* Loop over the values */}
|
|
178
|
+
{value?.length > 0 ? (
|
|
179
|
+
<Card padding={1} border radius={1}>
|
|
180
|
+
<Stack space={1}>
|
|
181
|
+
{value.map((item) => (
|
|
182
|
+
<Card
|
|
183
|
+
paddingY={1}
|
|
184
|
+
paddingX={2}
|
|
185
|
+
key={item._key}
|
|
186
|
+
tone={
|
|
187
|
+
// TODO: Move this logic somewhere else
|
|
188
|
+
invalidKeys.includes(item._key)
|
|
189
|
+
? `critical`
|
|
190
|
+
: undefined || languagesOutOfOrder.find((l) => l._key === item._key)
|
|
191
|
+
? `caution`
|
|
192
|
+
: undefined
|
|
193
|
+
}
|
|
194
|
+
>
|
|
195
|
+
<Flex gap={3} align="center">
|
|
196
|
+
{/* To render each individual field in this type */}
|
|
197
|
+
{type?.of?.length > 0 &&
|
|
198
|
+
type?.of.map((subType) => (
|
|
199
|
+
<Flex key={subType.name} flex={1} align="center" gap={2}>
|
|
200
|
+
{subType?.fields?.length > 0 ? (
|
|
201
|
+
<>
|
|
202
|
+
<Box>
|
|
203
|
+
<Label>{item._key}</Label>
|
|
204
|
+
</Box>
|
|
205
|
+
<Box flex={1}>
|
|
206
|
+
{/* There _should_ only be one field */}
|
|
207
|
+
{subType.fields.map((subTypeField) => (
|
|
208
|
+
<ValueInput
|
|
209
|
+
key={subTypeField.name}
|
|
210
|
+
onChange={(patchEvent) =>
|
|
211
|
+
handleInnerValueChange(patchEvent, item._key)
|
|
212
|
+
}
|
|
213
|
+
onBlur={onBlur}
|
|
214
|
+
// We don't want the array item to open onFocus
|
|
215
|
+
onFocus={() => null}
|
|
216
|
+
path={[{_key: item._key}, subTypeField.name]}
|
|
217
|
+
parent={item}
|
|
218
|
+
readOnly={readOnly}
|
|
219
|
+
type={subTypeField}
|
|
220
|
+
value={item.value}
|
|
221
|
+
level={props.level + 1}
|
|
222
|
+
markers={[]}
|
|
223
|
+
/>
|
|
224
|
+
))}
|
|
225
|
+
</Box>
|
|
226
|
+
</>
|
|
227
|
+
) : null}
|
|
228
|
+
</Flex>
|
|
229
|
+
))}
|
|
230
|
+
{presence?.length > 0 ? (
|
|
231
|
+
<FieldPresence maxAvatars={1} presence={presence} />
|
|
232
|
+
) : null}
|
|
233
|
+
{invalidKeys.includes(item._key) ? (
|
|
234
|
+
<FormFieldValidationStatus __unstable_markers={validationMarkers} />
|
|
235
|
+
) : null}
|
|
236
|
+
<Button
|
|
237
|
+
mode="ghost"
|
|
238
|
+
icon={RemoveIcon}
|
|
239
|
+
tone="critical"
|
|
240
|
+
onClick={() => handleUnsetByKey(item._key)}
|
|
241
|
+
/>
|
|
242
|
+
</Flex>
|
|
243
|
+
</Card>
|
|
244
|
+
))}
|
|
245
|
+
</Stack>
|
|
246
|
+
</Card>
|
|
247
|
+
) : null}
|
|
248
|
+
|
|
249
|
+
{languagesOutOfOrder.length > 0 ? (
|
|
250
|
+
<Button
|
|
251
|
+
tone="caution"
|
|
252
|
+
disabled={languagesOutOfOrder.length > languages.length}
|
|
253
|
+
icon={RestoreIcon}
|
|
254
|
+
onClick={() => handleRestoreOrder()}
|
|
255
|
+
text="Restore order of languages"
|
|
256
|
+
/>
|
|
257
|
+
) : null}
|
|
258
|
+
|
|
259
|
+
{languages.length > 0 ? (
|
|
260
|
+
<Stack space={2}>
|
|
261
|
+
{/* No more than 5 columns */}
|
|
262
|
+
<Grid columns={Math.min(languages.length, 5)} gap={2}>
|
|
263
|
+
{languages.map((language) => (
|
|
264
|
+
<Button
|
|
265
|
+
key={language.id}
|
|
266
|
+
tone="primary"
|
|
267
|
+
mode="ghost"
|
|
268
|
+
fontSize={1}
|
|
269
|
+
disabled={readOnly || value?.find((item) => item._key === language.id)}
|
|
270
|
+
text={language.id.toUpperCase()}
|
|
271
|
+
icon={AddIcon}
|
|
272
|
+
onClick={() => handleAddLanguage(language.id)}
|
|
273
|
+
/>
|
|
274
|
+
))}
|
|
275
|
+
</Grid>
|
|
276
|
+
<Button
|
|
277
|
+
tone="primary"
|
|
278
|
+
mode="ghost"
|
|
279
|
+
disabled={readOnly || value?.length >= languages?.length}
|
|
280
|
+
text={value?.length ? `Add missing languages` : `Add all languages`}
|
|
281
|
+
onClick={() => handleAddLanguage()}
|
|
282
|
+
/>
|
|
283
|
+
</Stack>
|
|
284
|
+
) : null}
|
|
285
|
+
|
|
286
|
+
{showNativeInput ? <NestedFormBuilder {...props} type={type} /> : null}
|
|
287
|
+
</Stack>
|
|
288
|
+
)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
export default withDocument(LanguageArrayWrapper)
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {ArrayConfig, Value} from './types'
|
|
2
|
+
import LanguageArray from './LanguageArray'
|
|
3
|
+
|
|
4
|
+
export function internationalizedArray(config: ArrayConfig) {
|
|
5
|
+
const {name = `title`, type = `string`, languages = [], showNativeInput = false} = config
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
name,
|
|
9
|
+
type: 'array',
|
|
10
|
+
inputComponent: LanguageArray,
|
|
11
|
+
options: {
|
|
12
|
+
languages,
|
|
13
|
+
showNativeInput,
|
|
14
|
+
},
|
|
15
|
+
of: [
|
|
16
|
+
{
|
|
17
|
+
type: 'object',
|
|
18
|
+
fields: [{name: 'value', type}],
|
|
19
|
+
preview: {
|
|
20
|
+
select: {title: 'value', key: '_key'},
|
|
21
|
+
prepare({title, key}) {
|
|
22
|
+
return {
|
|
23
|
+
title,
|
|
24
|
+
subtitle: key.toUpperCase(),
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
validation: (Rule) =>
|
|
31
|
+
Rule.max(languages.length).custom((value: Value[], context) => {
|
|
32
|
+
const {languages} = context.type.options
|
|
33
|
+
|
|
34
|
+
const nonLanguageKeys = value?.length
|
|
35
|
+
? value.filter((item) => !languages.find((language) => item._key === language.id))
|
|
36
|
+
: []
|
|
37
|
+
|
|
38
|
+
if (nonLanguageKeys.length) {
|
|
39
|
+
return {
|
|
40
|
+
message: `Array item keys must be valid languages registered to the field type`,
|
|
41
|
+
paths: nonLanguageKeys.map((item) => ({_key: item._key})),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Ensure there's no duplicate `language` fields
|
|
46
|
+
const valuesByLanguage = value?.length
|
|
47
|
+
? value
|
|
48
|
+
.filter((item) => Boolean(item?._key))
|
|
49
|
+
.reduce((acc, cur) => {
|
|
50
|
+
if (acc[cur._key]) {
|
|
51
|
+
return {...acc, [cur._key]: [...acc[cur._key], cur]}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
...acc,
|
|
56
|
+
[cur._key]: [cur],
|
|
57
|
+
}
|
|
58
|
+
}, {})
|
|
59
|
+
: {}
|
|
60
|
+
|
|
61
|
+
const duplicateValues = Object.values(valuesByLanguage)
|
|
62
|
+
.filter((item) => item?.length > 1)
|
|
63
|
+
.flat()
|
|
64
|
+
|
|
65
|
+
if (duplicateValues.length) {
|
|
66
|
+
return {
|
|
67
|
+
message: 'There can only be one field per language',
|
|
68
|
+
paths: duplicateValues.map((item) => ({_key: item._key})),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return true
|
|
73
|
+
}),
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ArrayConfig = Options & {
|
|
2
|
+
name: string
|
|
3
|
+
type: 'string' | 'number' | 'boolean' | 'text'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type Value = {
|
|
7
|
+
_key: string
|
|
8
|
+
value?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Language = {
|
|
12
|
+
id: string
|
|
13
|
+
title: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Options = {
|
|
17
|
+
languages: Language[]
|
|
18
|
+
showNativeInput: boolean
|
|
19
|
+
}
|