@stackbit/cms-core 0.1.20-cross-references.0 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/content-store-types.d.ts +8 -34
  2. package/dist/content-store-types.d.ts.map +1 -1
  3. package/dist/content-store-utils.d.ts +1 -3
  4. package/dist/content-store-utils.d.ts.map +1 -1
  5. package/dist/content-store-utils.js +1 -8
  6. package/dist/content-store-utils.js.map +1 -1
  7. package/dist/content-store.d.ts +3 -8
  8. package/dist/content-store.d.ts.map +1 -1
  9. package/dist/content-store.js +45 -71
  10. package/dist/content-store.js.map +1 -1
  11. package/dist/utils/create-update-csi-docs.d.ts +10 -11
  12. package/dist/utils/create-update-csi-docs.d.ts.map +1 -1
  13. package/dist/utils/create-update-csi-docs.js +15 -94
  14. package/dist/utils/create-update-csi-docs.js.map +1 -1
  15. package/dist/utils/csi-to-store-docs-converter.js +1 -69
  16. package/dist/utils/csi-to-store-docs-converter.js.map +1 -1
  17. package/dist/utils/duplicate-document.js +0 -3
  18. package/dist/utils/duplicate-document.js.map +1 -1
  19. package/dist/utils/model-utils.d.ts +4 -4
  20. package/dist/utils/model-utils.d.ts.map +1 -1
  21. package/dist/utils/model-utils.js +2 -67
  22. package/dist/utils/model-utils.js.map +1 -1
  23. package/dist/utils/schema-utils.d.ts +87 -0
  24. package/dist/utils/schema-utils.d.ts.map +1 -0
  25. package/dist/utils/schema-utils.js +195 -0
  26. package/dist/utils/schema-utils.js.map +1 -0
  27. package/dist/utils/store-to-api-docs-converter.d.ts.map +1 -1
  28. package/dist/utils/store-to-api-docs-converter.js +1 -38
  29. package/dist/utils/store-to-api-docs-converter.js.map +1 -1
  30. package/dist/utils/store-to-csi-docs-converter.js +0 -30
  31. package/dist/utils/store-to-csi-docs-converter.js.map +1 -1
  32. package/package.json +4 -4
  33. package/src/content-store-types.ts +3 -41
  34. package/src/content-store-utils.ts +1 -9
  35. package/src/content-store.ts +49 -78
  36. package/src/utils/create-update-csi-docs.ts +19 -110
  37. package/src/utils/csi-to-store-docs-converter.ts +17 -91
  38. package/src/utils/duplicate-document.ts +0 -3
  39. package/src/utils/model-utils.ts +7 -95
  40. package/src/utils/schema-utils.js +212 -0
  41. package/src/utils/store-to-api-docs-converter.ts +1 -38
  42. package/src/utils/store-to-csi-docs-converter.ts +0 -30
@@ -184,7 +184,7 @@ function mapCSIFieldToStoreField({
184
184
  context: MapContext;
185
185
  }): ContentStoreTypes.DocumentField {
186
186
  if (!csiDocumentField) {
187
- const isUnset = ['object', 'model', 'reference', 'cross-reference', 'richText', 'markdown', 'image', 'file', 'json'].includes(modelField.type);
187
+ const isUnset = ['object', 'model', 'reference', 'richText', 'markdown', 'image', 'file', 'json'].includes(modelField.type);
188
188
  return {
189
189
  type: modelField.type,
190
190
  ...(localized
@@ -223,8 +223,6 @@ function mapCSIFieldToStoreField({
223
223
  case 'file':
224
224
  case 'reference':
225
225
  return csiDocumentField as ContentStoreTypes.DocumentField;
226
- case 'cross-reference':
227
- return mapReferenceField(csiDocumentField)
228
226
  case 'object':
229
227
  return mapObjectField(csiDocumentField as CSITypes.DocumentObjectField, modelField, context);
230
228
  case 'model':
@@ -243,76 +241,6 @@ function mapCSIFieldToStoreField({
243
241
  }
244
242
  }
245
243
 
246
- function mapReferenceField(csiDocumentField: CSITypes.DocumentField): ContentStoreTypes.DocumentCrossReferenceField {
247
- const unlocalizedUnset = {
248
- type: 'cross-reference',
249
- refType: 'document',
250
- isUnset: true
251
- } as const;
252
- if (csiDocumentField.type !== 'string' && csiDocumentField.type !== 'text' && csiDocumentField.type !== 'json') {
253
- if (isLocalizedField(csiDocumentField)) {
254
- return {
255
- type: 'cross-reference',
256
- refType: 'document',
257
- localized: true,
258
- locales: {}
259
- };
260
- }
261
- return unlocalizedUnset;
262
- }
263
- const parseRefObject = (value: any): { refId: string; refSrcType: string; refProjectId: string } | null => {
264
- if (typeof value === 'string') {
265
- try {
266
- value = JSON.parse(value);
267
- } catch (error) {
268
- return null;
269
- }
270
- }
271
- if (_.isPlainObject(value) && 'refId' in value && 'refSrcType' in value && 'refProjectId' in value) {
272
- return {
273
- refId: value.refId,
274
- refSrcType: value.refSrcType,
275
- refProjectId: value.refProjectId
276
- };
277
- }
278
- return null;
279
- };
280
- if (isLocalizedField(csiDocumentField)) {
281
- csiDocumentField.locales;
282
- return {
283
- type: 'cross-reference',
284
- refType: 'document',
285
- localized: true,
286
- locales: _.reduce(
287
- csiDocumentField.locales,
288
- (accum: Record<string, { locale: string; refId: string; refSrcType: string; refProjectId: string }>, locale, localeKey) => {
289
- const refObject = parseRefObject(locale.value);
290
- if (refObject) {
291
- accum[localeKey] = {
292
- locale: locale.locale,
293
- ...refObject
294
- };
295
- }
296
- return accum;
297
- },
298
- {}
299
- )
300
- };
301
- }
302
- if (!('value' in csiDocumentField)) {
303
- return unlocalizedUnset;
304
- }
305
- const refObject = parseRefObject(csiDocumentField.value);
306
- if (!refObject) {
307
- return unlocalizedUnset;
308
- }
309
- return {
310
- type: 'cross-reference',
311
- refType: 'document',
312
- ...refObject
313
- };
314
- }
315
-
316
244
  function mapObjectField(
317
245
  csiDocumentField: CSITypes.DocumentObjectField,
318
246
  modelField: FieldObjectProps,
@@ -385,15 +313,14 @@ function mapListField(csiDocumentField: CSITypes.DocumentListField, modelField:
385
313
  if (!isLocalizedField(csiDocumentField)) {
386
314
  return {
387
315
  type: csiDocumentField.type,
388
- items: csiDocumentField.items.map(
389
- (item) =>
390
- mapCSIFieldToStoreField({
391
- csiDocumentField: item,
392
- modelField: modelField.items ?? { type: 'string' },
393
- // list items can not be localized, only the list itself can be localized
394
- localized: false,
395
- context
396
- }) as ContentStoreTypes.DocumentListFieldItems
316
+ items: csiDocumentField.items.map((item) =>
317
+ mapCSIFieldToStoreField({
318
+ csiDocumentField: item,
319
+ modelField: modelField.items ?? { type: 'string' },
320
+ // list items can not be localized, only the list itself can be localized
321
+ localized: false,
322
+ context
323
+ }) as ContentStoreTypes.DocumentListFieldItems
397
324
  )
398
325
  };
399
326
  }
@@ -403,15 +330,14 @@ function mapListField(csiDocumentField: CSITypes.DocumentListField, modelField:
403
330
  locales: _.mapValues(csiDocumentField.locales, (locale) => {
404
331
  return {
405
332
  locale: locale.locale,
406
- items: (locale.items ?? []).map(
407
- (item) =>
408
- mapCSIFieldToStoreField({
409
- csiDocumentField: item,
410
- modelField: modelField.items ?? { type: 'string' },
411
- // list items can not be localized, only the list itself can be localized
412
- localized: false,
413
- context
414
- }) as ContentStoreTypes.DocumentListFieldItems
333
+ items: (locale.items ?? []).map((item) =>
334
+ mapCSIFieldToStoreField({
335
+ csiDocumentField: item,
336
+ modelField: modelField.items ?? { type: 'string' },
337
+ // list items can not be localized, only the list itself can be localized
338
+ localized: false,
339
+ context
340
+ }) as ContentStoreTypes.DocumentListFieldItems
415
341
  )
416
342
  };
417
343
  })
@@ -202,9 +202,6 @@ function mergeObjectWithDocumentField({
202
202
  }
203
203
  break;
204
204
  }
205
- case 'cross-reference':
206
- // TODO: implement duplicating documents with cross-references
207
- break;
208
205
  case 'list': {
209
206
  const localizedField = getDocumentFieldForLocale(documentField, locale);
210
207
  if (value) {
@@ -1,18 +1,10 @@
1
1
  import _ from 'lodash';
2
2
 
3
- import { FieldCrossReferenceModel, Logger, ModelWithSource } from '@stackbit/types';
4
- import {
5
- Model,
6
- assignLabelFieldIfNeeded,
7
- isObjectField,
8
- isCrossReferenceField,
9
- isPageModel,
10
- mapModelFieldsRecursively,
11
- mapListItemsPropsOrSelfSpecificProps,
12
- validateConfig
13
- } from '@stackbit/sdk';
14
-
15
- export function normalizeModels({ models, logger }: { models: ModelWithSource[]; logger: Logger }): ModelWithSource[] {
3
+ import { Logger } from '@stackbit/types';
4
+ import { Model, assignLabelFieldIfNeeded, isObjectField, isPageModel, mapModelFieldsRecursively, mapListItemsPropsOrSelfSpecificProps } from '@stackbit/sdk';
5
+ import { validateConfig } from '@stackbit/sdk';
6
+
7
+ export function normalizeModels<T extends Model>({ models, logger }: { models: T[]; logger: Logger }): T[] {
16
8
  return models.map((model) => {
17
9
  model = { ...model };
18
10
 
@@ -53,98 +45,18 @@ export function normalizeModels({ models, logger }: { models: ModelWithSource[];
53
45
  mapListItemsPropsOrSelfSpecificProps(field, (listItemsPropsOrField) => {
54
46
  if (isObjectField(listItemsPropsOrField)) {
55
47
  assignLabelFieldIfNeeded(listItemsPropsOrField);
56
- } else if (isCrossReferenceField(listItemsPropsOrField)) {
57
- listItemsPropsOrField.models = validateAndNormalizeCrossReferenceModels({
58
- crossReferenceModels: listItemsPropsOrField.models,
59
- models,
60
- logger
61
- });
62
48
  }
63
49
  return listItemsPropsOrField;
64
50
  });
65
51
 
66
52
  return field;
67
- });
53
+ }) as T;
68
54
 
69
55
  return model;
70
56
  });
71
57
  }
72
58
 
73
- function validateAndNormalizeCrossReferenceModels({
74
- crossReferenceModels,
75
- models,
76
- logger
77
- }: {
78
- crossReferenceModels: FieldCrossReferenceModel[];
79
- models: ModelWithSource[];
80
- logger: Logger;
81
- }): FieldCrossReferenceModel[] {
82
- const modelGroupsByModelName = models.reduce((modelGroups: Record<string, ModelWithSource[]>, model) => {
83
- if (!(model.name in modelGroups)) {
84
- modelGroups[model.name] = [];
85
- }
86
- modelGroups[model.name]!.push(model);
87
- return modelGroups;
88
- }, {});
89
-
90
- // Match cross-reference models to the group of content source models with
91
- // the same name. Then, match the cross-reference model to content source
92
- // model by comparing srcType and srcProjectId. If after the comparison,
93
- // there are more than one model left, log a warning and filter out that
94
- // cross-reference model so it won't cause any model ambiguity.
95
- const nonMatchedCrossReferenceModels: {
96
- crossReferenceModel: FieldCrossReferenceModel;
97
- matchedModels: ModelWithSource[];
98
- }[] = [];
99
-
100
- const normalizedCrossReferenceModels = crossReferenceModels.reduce((matchedCrossReferenceModels: FieldCrossReferenceModel[], crossReferenceModel) => {
101
- const models = modelGroupsByModelName[crossReferenceModel.modelName];
102
- if (!models) {
103
- nonMatchedCrossReferenceModels.push({ crossReferenceModel, matchedModels: [] });
104
- return matchedCrossReferenceModels;
105
- }
106
- const matchedModels = models.filter((model) => {
107
- const matchesType = !crossReferenceModel.srcType || model.srcType === crossReferenceModel.srcType;
108
- const matchesId = !crossReferenceModel.srcProjectId || model.srcProjectId === crossReferenceModel.srcProjectId;
109
- return matchesType && matchesId;
110
- });
111
- if (matchedModels.length !== 1) {
112
- nonMatchedCrossReferenceModels.push({ crossReferenceModel, matchedModels });
113
- return matchedCrossReferenceModels;
114
- }
115
- const matchedModel = matchedModels[0]!;
116
- matchedCrossReferenceModels.push({
117
- modelName: crossReferenceModel.modelName,
118
- srcType: matchedModel.srcType,
119
- srcProjectId: matchedModel.srcProjectId
120
- });
121
- return matchedCrossReferenceModels;
122
- }, []);
123
-
124
- // Log model matching warnings using user logger
125
- for (const { crossReferenceModel, matchedModels } of nonMatchedCrossReferenceModels) {
126
- let message = `a model of cross-reference field: '${crossReferenceModel.modelName}'`;
127
- if (crossReferenceModel.srcType) {
128
- message += `, srcType: '${crossReferenceModel.srcType}'`;
129
- }
130
- if (crossReferenceModel.srcProjectId) {
131
- message += `, srcProjectId: '${crossReferenceModel.srcProjectId}'`;
132
- }
133
- message = message + ` defined in stackbit config`;
134
- let contentSourceModelsMessage;
135
- if (matchedModels.length) {
136
- const matchesModelsMessage = matchedModels.map((model) => `srcType: '${model.srcType}', srcProjectId: '${model.srcProjectId}'`).join('; ');
137
- contentSourceModelsMessage = ` matches more that 1 model in the following content sources: ${matchesModelsMessage}`;
138
- } else {
139
- contentSourceModelsMessage = ' does not match any content source model';
140
- }
141
- logger.warn(message + contentSourceModelsMessage);
142
- }
143
-
144
- return normalizedCrossReferenceModels;
145
- }
146
-
147
- export function validateModels<T extends Model>({ models, logger }: { models: T[]; logger: Logger }): T[] {
59
+ export function validateModels<T extends Model>({ models, logger }: { models: T[], logger: Logger }): T[] {
148
60
  const { config, errors } = validateConfig({
149
61
  stackbitVersion: '0.5.0',
150
62
  models: models,
@@ -0,0 +1,212 @@
1
+ const _ = require('lodash');
2
+
3
+ const FIELD_TYPES = [
4
+ 'string',
5
+ 'text',
6
+ 'markdown',
7
+ 'html',
8
+ 'url',
9
+ 'slug',
10
+ 'number',
11
+ 'boolean',
12
+ 'select',
13
+ 'enum',
14
+ 'date',
15
+ 'datetime',
16
+ 'color',
17
+ 'image',
18
+ 'file',
19
+ 'json',
20
+ 'reference',
21
+ 'model',
22
+ 'models',
23
+ 'object',
24
+ 'list',
25
+ 'array'
26
+ ];
27
+
28
+ module.exports = {
29
+ FIELD_TYPES,
30
+ isObjectField,
31
+ isModelField,
32
+ isModelsField,
33
+ isReferenceField,
34
+ isCustomModelField,
35
+ isListField,
36
+ isListOfObjectsField,
37
+ isListOfModelField,
38
+ isListOfModelsField,
39
+ isListOfCustomModelField,
40
+ isListOfReferenceField,
41
+ getListItemsField,
42
+ iterateModelFieldsRecursively,
43
+ resolveLabelFieldForModel,
44
+ resolveLabelFieldFromFields,
45
+ getUrlPath,
46
+ getNormalizedModelType
47
+ };
48
+
49
+
50
+ function resolveLabelFieldForModel(model, modelLabelFieldPath, fields) {
51
+ let labelField = _.get(model, modelLabelFieldPath, null);
52
+ if (labelField) {
53
+ return labelField;
54
+ }
55
+ return resolveLabelFieldFromFields(fields);
56
+ }
57
+
58
+ function resolveLabelFieldFromFields(fields) {
59
+ let labelField = null;
60
+ let titleField = _.find(fields, field => field.name === 'title' && _.includes(['string', 'text'], field.type));
61
+ if (!titleField) {
62
+ // get first string field
63
+ titleField = _.find(fields, {type: 'string'});
64
+ }
65
+ if (titleField) {
66
+ labelField = _.get(titleField, 'name');
67
+ }
68
+ return labelField;
69
+ }
70
+
71
+ function isObjectField(field) {
72
+ return field.type === 'object';
73
+ }
74
+
75
+ function isReferenceField(field) {
76
+ return field.type === 'reference';
77
+ }
78
+
79
+ function isCustomModelField(field, modelsByName) {
80
+ return !FIELD_TYPES.includes(field.type) && (!modelsByName || _.has(modelsByName, field.type));
81
+ }
82
+
83
+ function isModelField(field) {
84
+ return field.type === 'model';
85
+ }
86
+
87
+ function isModelsField(field) {
88
+ return field.type === 'models';
89
+ }
90
+
91
+ function isListField(field) {
92
+ return ['list', 'array'].includes(field.type);
93
+ }
94
+
95
+ function isListOfObjectsField(field) {
96
+ return isListField(field) && isObjectField(getListItemsField(field));
97
+ }
98
+
99
+ function isListOfModelField(field) {
100
+ return isListField(field) && isModelField(getListItemsField(field));
101
+ }
102
+
103
+ function isListOfModelsField(field) {
104
+ return isListField(field) && isModelsField(getListItemsField(field));
105
+ }
106
+
107
+ function isListOfCustomModelField(field) {
108
+ return isListField(field) && isCustomModelField(getListItemsField(field));
109
+ }
110
+
111
+ function isListOfReferenceField(field) {
112
+ return isListField(field) && isReferenceField(getListItemsField(field));
113
+ }
114
+
115
+ /**
116
+ * Gets a list field and returns its items field. If list field does not define
117
+ * items field, the default field is string:
118
+ *
119
+ * @example
120
+ * listItemField = getListItemsField({
121
+ * type: 'list',
122
+ * name: '...',
123
+ * items: { type: 'object', fields: [] }
124
+ * }
125
+ * listItemField => {
126
+ * type: 'object',
127
+ * name: '...',
128
+ * fields: []
129
+ * }
130
+ *
131
+ * // list field without `items`
132
+ * listItemField = getListItemsField({ type: 'list', name: '...' }
133
+ * listItemField => { type: 'string' }
134
+ *
135
+ * @param {Object} field
136
+ * @return {Object}
137
+ */
138
+ function getListItemsField(field) {
139
+ // items.type defaults to string
140
+ return _.defaults({}, _.get(field, 'items', {}), {type: 'string'});
141
+ }
142
+
143
+ /**
144
+ * This function invokes the `iterator` function for every field of the `model`.
145
+ * It recursively traverses through fields of type `object` and `list` with
146
+ * items of type `object` and invokes the `iterator` on their child fields,
147
+ * and so on. The traversal is a depth-first and the `iterator` is invoked
148
+ * before traversing the field's child fields.
149
+ *
150
+ * The iterator is invoked with two parameters, `field` and `fieldPath`. The
151
+ * `field` is the currently iterated field, and `fieldPath` is an array of
152
+ * strings indicating the path of the `field` relative to the model.
153
+ *
154
+ * @example
155
+ * model = {
156
+ * fields: [
157
+ * { name: "title", type: "string" },
158
+ * {
159
+ * name: "banner",
160
+ * type: "object",
161
+ * fields: [
162
+ * { name: "logo", type: "image" }
163
+ * ]}
164
+ * {
165
+ * name: "actions",
166
+ * type: "list",
167
+ * items: {
168
+ * type: "object",
169
+ * fields: [
170
+ * {name: "label", type: "string"}
171
+ * ]
172
+ * }
173
+ * }
174
+ * ]
175
+ * }
176
+ * iterateModelFieldsRecursively(model, iterator);
177
+ * // will call the iterator with following field.name and fieldPath
178
+ * - 'title', ['fields', 'title']
179
+ * - 'banner', ['fields', 'banner']
180
+ * - 'logo', ['fields', 'banner', 'fields', 'logo']
181
+ * - 'actions', ['fields', 'actions']
182
+ * - 'label', ['fields', 'actions', 'items', 'fields', 'label']
183
+ *
184
+ * @param {Object} model The root model to iterate fields
185
+ * @param {Function} iterator The iterator function
186
+ * @param {Array} [fieldPath]
187
+ */
188
+ function iterateModelFieldsRecursively(model, iterator, fieldPath = []) {
189
+ const fields = _.get(model, 'fields', []);
190
+ fieldPath = fieldPath.concat('fields');
191
+ _.forEach(fields, (field) => {
192
+ const childFieldPath = fieldPath.concat(field.name);
193
+ iterator(field, childFieldPath);
194
+ if (isObjectField(field)) {
195
+ iterateModelFieldsRecursively(field, iterator, childFieldPath);
196
+ } else if (isListOfObjectsField(field)) {
197
+ iterateModelFieldsRecursively(getListItemsField(field), iterator, childFieldPath.concat('items'));
198
+ }
199
+ });
200
+ }
201
+
202
+ function getUrlPath(stackbitModel) {
203
+ const modelType = getNormalizedModelType(stackbitModel);
204
+ if (modelType === 'page') {
205
+ return _.get(stackbitModel, 'urlPath', '/{slug}');
206
+ }
207
+ return null;
208
+ }
209
+
210
+ function getNormalizedModelType(stackbitModel) {
211
+ return stackbitModel ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : 'object';
212
+ }
@@ -144,7 +144,7 @@ function toLocalizedAPIField(docField: ContentStoreTypes.DocumentField, locale?:
144
144
  }),
145
145
  ...localeFields(docField.localized)
146
146
  };
147
- case 'reference': {
147
+ case 'reference':
148
148
  if (docField.localized) {
149
149
  const { type, refType, localized, locales, ...base } = docField;
150
150
  const localeProps = locales && locale ? locales[locale] : undefined;
@@ -179,43 +179,6 @@ function toLocalizedAPIField(docField: ContentStoreTypes.DocumentField, locale?:
179
179
  ...base,
180
180
  ...localeFields(docField.localized)
181
181
  };
182
- }
183
- case 'cross-reference': {
184
- if (docField.localized) {
185
- const { type, refType, localized, locales, ...base } = docField;
186
- const localeProps = locales && locale ? locales[locale] : undefined;
187
- // if reference field isUnset === true, it behaves like a regular object
188
- if (!localeProps) {
189
- return {
190
- type: 'object',
191
- isUnset: true,
192
- ...base,
193
- ...localeFields(localized)
194
- };
195
- }
196
- return {
197
- type: 'cross-reference',
198
- refType: refType,
199
- ...base,
200
- ...localeProps,
201
- ...localeFields(localized)
202
- };
203
- }
204
- const { type, refType, ...base } = docField;
205
- if (base.isUnset) {
206
- return {
207
- type: 'object',
208
- ...base,
209
- ...localeFields(docField.localized)
210
- };
211
- }
212
- return {
213
- type: 'cross-reference',
214
- refType: refType,
215
- ...base,
216
- ...localeFields(docField.localized)
217
- };
218
- }
219
182
  case 'list':
220
183
  if (docField.localized) {
221
184
  const { localized, locales, ...base } = docField;
@@ -179,36 +179,6 @@ function mapStoreFieldToCSIField(documentField: ContentStoreTypes.DocumentField)
179
179
  refId: documentField.refId
180
180
  };
181
181
  }
182
- case 'cross-reference': {
183
- if (documentField.localized) {
184
- if (_.isEmpty(documentField.locales)) {
185
- return;
186
- }
187
- return {
188
- type: 'json',
189
- localized: true,
190
- locales: _.mapValues(documentField.locales, (locale) => ({
191
- locale: locale.locale,
192
- value: {
193
- refId: locale.refId,
194
- refSrcType: locale.refSrcType,
195
- srcProjectId: locale.refProjectId
196
- }
197
- }))
198
- };
199
- }
200
- if (documentField.isUnset) {
201
- return;
202
- }
203
- return {
204
- type: 'json',
205
- value: {
206
- refId: documentField.refId,
207
- refSrcType: documentField.refSrcType,
208
- srcProjectId: documentField.refProjectId
209
- }
210
- };
211
- }
212
182
  case 'list': {
213
183
  if (documentField.localized) {
214
184
  if (_.isEmpty(documentField.locales)) {