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 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
+ ![2022-07-13 12 53 29](https://user-images.githubusercontent.com/9684022/178729823-cbb1059f-4ae0-4ab0-900d-4f22b030c1d1.gif)
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
@@ -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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ //# sourceMappingURL=types.js.map
@@ -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,7 @@
1
+ {
2
+ "paths": {
3
+ "source": "./src",
4
+ "compiled": "./lib"
5
+ },
6
+ "parts": []
7
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react'
2
+ import {FormBuilderInput} from '@sanity/form-builder/lib/FormBuilderInput'
3
+
4
+ export default function ValueInput(props) {
5
+ return <FormBuilderInput {...props} />
6
+ }
@@ -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,3 @@
1
+ import {internationalizedArray} from './internationalizedArray'
2
+
3
+ export default internationalizedArray
@@ -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
+ }