@sequoialabs/payload-plugin-reversia 0.1.4 → 0.1.6

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.
@@ -179,16 +179,23 @@ function describeTopLevelField(field) {
179
179
  }
180
180
  // Structured wrapper: group / array / blocks. Walk inside to collect
181
181
  // localized descendant leaves; emit a container only if any were found.
182
+ //
183
+ // When the top-level field itself is `localized: true`, ALL its inner
184
+ // translatable fields are inherently locale-specific (the whole container
185
+ // is stored per-locale). Payload may strip `localized: true` from inner
186
+ // fields in this case, so we pass `parentIsLocalized` to treat them as
187
+ // localized regardless.
188
+ const fieldIsLocalized = isLocalized(field);
182
189
  const innerLeaves = [];
183
190
  if (isGroupLikeField(field)) {
184
- collectLeaves(field.fields, [], innerLeaves);
191
+ collectLeaves(field.fields, [], innerLeaves, fieldIsLocalized);
185
192
  }
186
193
  else if (isArrayField(field)) {
187
- collectLeaves(field.fields, [{ kind: 'iterate' }], innerLeaves);
194
+ collectLeaves(field.fields, [{ kind: 'iterate' }], innerLeaves, fieldIsLocalized);
188
195
  }
189
196
  else if (isBlocksField(field)) {
190
197
  for (const block of field.blocks) {
191
- collectLeaves(block.fields, [{ kind: 'iterateBlock', blockSlug: block.slug }], innerLeaves);
198
+ collectLeaves(block.fields, [{ kind: 'iterateBlock', blockSlug: block.slug }], innerLeaves, fieldIsLocalized);
192
199
  }
193
200
  }
194
201
  else {
@@ -211,7 +218,7 @@ function describeTopLevelField(field) {
211
218
  hasRequiredLeaf: true,
212
219
  };
213
220
  }
214
- function collectLeaves(fields, parentSegments, out) {
221
+ function collectLeaves(fields, parentSegments, out, parentIsLocalized = false) {
215
222
  for (const field of fields) {
216
223
  if (!isNamed(field)) {
217
224
  if (isTabsField(field)) {
@@ -219,19 +226,23 @@ function collectLeaves(fields, parentSegments, out) {
219
226
  const tabSegments = 'name' in tab && typeof tab.name === 'string' && tab.name.length > 0
220
227
  ? [...parentSegments, { kind: 'key', name: tab.name }]
221
228
  : parentSegments;
222
- collectLeaves(tab.fields, tabSegments, out);
229
+ collectLeaves(tab.fields, tabSegments, out, parentIsLocalized);
223
230
  }
224
231
  }
225
232
  if ((field.type === 'row' || field.type === 'collapsible') &&
226
233
  'fields' in field &&
227
234
  Array.isArray(field.fields)) {
228
- collectLeaves(field.fields, parentSegments, out);
235
+ collectLeaves(field.fields, parentSegments, out, parentIsLocalized);
229
236
  }
230
237
  continue;
231
238
  }
232
239
  const segments = [...parentSegments, { kind: 'key', name: field.name }];
233
240
  const reversia = getReversiaCustom(field);
234
241
  const localized = isLocalized(field);
242
+ // Scalars (text, textarea, etc.) must be explicitly `localized: true` to
243
+ // be treated as translatable. Scalars inside a localized parent that
244
+ // aren't marked localized are structural (keys, enums) — translating
245
+ // "who" to "qui" would break app logic.
235
246
  if (localized && isScalarPayloadType(field.type)) {
236
247
  out.push({
237
248
  segments,
@@ -241,7 +252,12 @@ function collectLeaves(fields, parentSegments, out) {
241
252
  });
242
253
  continue;
243
254
  }
244
- if (localized && (field.type === 'richText' || field.type === 'json')) {
255
+ // RichText/json fields inherit localized status from their parent. Payload
256
+ // may strip `localized: true` from inner fields when the parent container
257
+ // is already localized (the whole container is per-locale, so inner
258
+ // localization is redundant). These are always translatable content.
259
+ const effectivelyLocalized = localized || parentIsLocalized;
260
+ if (effectivelyLocalized && (field.type === 'richText' || field.type === 'json')) {
245
261
  out.push({
246
262
  segments,
247
263
  kind: 'json',
@@ -251,16 +267,17 @@ function collectLeaves(fields, parentSegments, out) {
251
267
  continue;
252
268
  }
253
269
  if (isGroupLikeField(field)) {
254
- collectLeaves(field.fields, segments, out);
270
+ collectLeaves(field.fields, segments, out, parentIsLocalized);
255
271
  continue;
256
272
  }
257
273
  if (isArrayField(field)) {
258
- collectLeaves(field.fields, [...segments, { kind: 'iterate' }], out);
274
+ collectLeaves(field.fields, [...segments, { kind: 'iterate' }], out, parentIsLocalized || isLocalized(field));
259
275
  continue;
260
276
  }
261
277
  if (isBlocksField(field)) {
278
+ const blocksLocalized = parentIsLocalized || isLocalized(field);
262
279
  for (const block of field.blocks) {
263
- collectLeaves(block.fields, [...segments, { kind: 'iterateBlock', blockSlug: block.slug }], out);
280
+ collectLeaves(block.fields, [...segments, { kind: 'iterateBlock', blockSlug: block.slug }], out, blocksLocalized);
264
281
  }
265
282
  }
266
283
  }
@@ -855,9 +872,13 @@ export function deflatePopulatedRelationships(value) {
855
872
  return out;
856
873
  }
857
874
  /**
858
- * Deflate a single value: if it's a populated doc, return its id. If it's a
859
- * polymorphic relationship `{ relationTo, value: <populated> }`, deflate the
860
- * inner value. Otherwise recurse.
875
+ * Deflate a single value: if it's a populated doc, return its id. If it's an
876
+ * object with a `relationTo` + populated `value` (polymorphic relationship OR
877
+ * Lexical upload/relationship node), deflate only the `value` property while
878
+ * preserving all other keys (`type`, `children`, `version`, `format`, …).
879
+ *
880
+ * Previous implementation returned `{ relationTo, value: id }` which stripped
881
+ * Lexical node metadata and corrupted richText trees.
861
882
  */
862
883
  function deflateValue(val) {
863
884
  if (val === null || val === undefined || typeof val !== 'object') {
@@ -866,30 +887,51 @@ function deflateValue(val) {
866
887
  if (Array.isArray(val)) {
867
888
  return val.map(deflateValue);
868
889
  }
869
- // Direct populated doc → raw id (e.g. upload field: { id, filename, ... })
870
- if (isPopulatedDoc(val)) {
871
- return val.id;
872
- }
873
- // Polymorphic relationship: { relationTo: 'x', value: <populated> }
874
890
  const obj = val;
875
- if (typeof obj.relationTo === 'string' && 'value' in obj && isPopulatedDoc(obj.value)) {
876
- return { relationTo: obj.relationTo, value: obj.value.id };
877
- }
878
- // hasMany polymorphic: { relationTo: 'x', value: [<populated>, ...] }
879
- if (typeof obj.relationTo === 'string' && Array.isArray(obj.value)) {
880
- return {
881
- relationTo: obj.relationTo,
882
- value: obj.value.map((item) => isPopulatedDoc(item) ? item.id : item),
883
- };
891
+ // Direct populated doc → raw id (e.g. upload field stored as { id, filename, … })
892
+ // Only match when the object has NO other "structural" keys that indicate
893
+ // it's a Lexical node or rich object we shouldn't collapse.
894
+ if (isPopulatedDoc(obj) && !isStructuralNode(obj)) {
895
+ return obj.id;
896
+ }
897
+ // Object with `relationTo` + populated `value` — this matches both Payload
898
+ // polymorphic relationships AND Lexical upload/relationship nodes.
899
+ // Deflate only the `value` key; preserve everything else.
900
+ if (typeof obj.relationTo === 'string' && 'value' in obj) {
901
+ if (isPopulatedDoc(obj.value)) {
902
+ const out = {};
903
+ for (const [k, v] of Object.entries(obj)) {
904
+ out[k] = k === 'value' ? obj.value.id : deflateValue(v);
905
+ }
906
+ return out;
907
+ }
908
+ if (Array.isArray(obj.value)) {
909
+ const out = {};
910
+ for (const [k, v] of Object.entries(obj)) {
911
+ out[k] =
912
+ k === 'value'
913
+ ? v.map((item) => isPopulatedDoc(item) ? item.id : item)
914
+ : deflateValue(v);
915
+ }
916
+ return out;
917
+ }
884
918
  }
885
919
  // Recurse into nested structure
886
920
  return deflatePopulatedRelationships(val);
887
921
  }
922
+ /**
923
+ * A "structural node" is an object that carries its own semantics beyond being
924
+ * a populated relationship doc — e.g. a Lexical node with `type`, `children`,
925
+ * `version`. Collapsing it to just its `id` would destroy the tree.
926
+ */
927
+ function isStructuralNode(obj) {
928
+ return typeof obj.type === 'string' || 'children' in obj || 'version' in obj;
929
+ }
888
930
  function isPopulatedDoc(val) {
889
931
  if (!val || typeof val !== 'object' || Array.isArray(val)) {
890
932
  return false;
891
933
  }
892
934
  const obj = val;
893
- return (typeof obj.id === 'string' &&
894
- ('createdAt' in obj || 'updatedAt' in obj || 'filename' in obj || 'mimeType' in obj));
935
+ const hasId = typeof obj.id === 'string' || typeof obj.id === 'number';
936
+ return (hasId && ('createdAt' in obj || 'updatedAt' in obj || 'filename' in obj || 'mimeType' in obj));
895
937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequoialabs/payload-plugin-reversia",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "author": {
5
5
  "name": "Jean Walrave",
6
6
  "email": "contact@reversia.tech",