@focus-reactive/payload-plugin-translator 0.5.0 → 0.5.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/dist/client/shared/ui/Button/Button.d.ts +4 -4
- package/dist/client/shared/ui/Button/Button.js +18 -18
- package/dist/client/shared/ui/Button/styles.module.scss +1 -1
- package/dist/client/shared/ui/Popup/Popup.d.ts +10 -4
- package/dist/client/shared/ui/Popup/Popup.js +4 -3
- package/dist/client/shared/ui/Select/Select.d.ts +9 -7
- package/dist/client/shared/ui/Select/Select.js +18 -17
- package/dist/client/shared/ui/Select/styles.module.scss +5 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +31 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.js +45 -0
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.d.ts +3 -9
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.js +71 -78
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +11 -22
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +85 -106
- package/dist/server/shared/field-traversal/findFieldByPath.d.ts +40 -0
- package/dist/server/shared/field-traversal/findFieldByPath.js +88 -0
- package/dist/server/shared/field-traversal/index.d.ts +5 -0
- package/dist/server/shared/field-traversal/index.js +5 -0
- package/dist/server/shared/field-traversal/kernel.d.ts +85 -0
- package/dist/server/shared/field-traversal/kernel.js +145 -0
- package/dist/server/shared/field-traversal/types.d.ts +191 -0
- package/dist/server/shared/field-traversal/types.js +41 -0
- package/dist/server/shared/field-traversal/walkFields.d.ts +51 -0
- package/dist/server/shared/field-traversal/walkFields.js +164 -0
- package/dist/server/shared/utils/filterLocalizedFields.d.ts +3 -3
- package/dist/server/shared/utils/filterLocalizedFields.js +52 -63
- package/package.json +2 -2
package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js
CHANGED
|
@@ -1,128 +1,107 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isFieldExcludedFromTranslation, isLocalizedField, isObject, isTranslatableField } from "../../../../shared";
|
|
2
|
+
import { resolveBlockFields, walkFields } from "../../../../shared/field-traversal";
|
|
3
|
+
const asObject = (value)=>isObject(value) ? value : {};
|
|
3
4
|
/**
|
|
4
|
-
* Collects FieldChunks by
|
|
5
|
-
*
|
|
5
|
+
* Collects FieldChunks by walking schema + data with the shared {@link walkFields} engine.
|
|
6
|
+
* Iteration is driven by `filteredData`; each translatable, localized, non-excluded leaf that
|
|
7
|
+
* the strategy approves is written with its source value and pushed as a chunk carrying a
|
|
8
|
+
* mutable reference to its parent object (for later write-back). Collect-only — `combine` is a
|
|
9
|
+
* no-op; results accumulate in a closure.
|
|
6
10
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - isLocalizedField
|
|
10
|
-
* - strategy.shouldTranslate(sourceValue, targetValue)
|
|
11
|
-
* - !isFieldExcludedFromTranslation
|
|
12
|
-
*
|
|
13
|
-
* IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization).
|
|
14
|
-
* Original schemas preserve `localized: true` on nested fields.
|
|
11
|
+
* IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization). Original
|
|
12
|
+
* schemas preserve `localized: true` on nested fields.
|
|
15
13
|
*/ export class FieldChunkCollector {
|
|
16
14
|
schema;
|
|
17
15
|
filteredData;
|
|
18
16
|
sourceData;
|
|
19
17
|
targetData;
|
|
20
18
|
strategy;
|
|
21
|
-
chunks;
|
|
22
19
|
constructor(schema, filteredData, sourceData, targetData, strategy){
|
|
23
20
|
this.schema = schema;
|
|
24
21
|
this.filteredData = filteredData;
|
|
25
22
|
this.sourceData = sourceData;
|
|
26
23
|
this.targetData = targetData;
|
|
27
24
|
this.strategy = strategy;
|
|
28
|
-
this.chunks = [];
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Collects translatable field chunks that need translation.
|
|
32
|
-
*/ collect() {
|
|
33
|
-
this.chunks = [];
|
|
34
|
-
this.traverseFields(this.schema, this.filteredData, this.sourceData, this.targetData, []);
|
|
35
|
-
return this.chunks;
|
|
36
25
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!fieldAffectsData(field)) {
|
|
63
|
-
if (hasFields(field)) this.traverseFields(field.fields, data, source, target, path);
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
const value = data[field.name];
|
|
67
|
-
const sourceValue = source[field.name];
|
|
68
|
-
const targetValue = target[field.name];
|
|
69
|
-
if (value === undefined || value === null) continue;
|
|
70
|
-
const currentPath = [
|
|
71
|
-
...path,
|
|
72
|
-
field.name
|
|
73
|
-
];
|
|
74
|
-
if (fieldIsGroupType(field) && isObject(value)) {
|
|
75
|
-
this.traverseFields(field.fields, value, isObject(sourceValue) ? sourceValue : {}, isObject(targetValue) ? targetValue : {}, currentPath);
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (fieldIsArrayType(field) && Array.isArray(value)) {
|
|
79
|
-
const sourceArray = Array.isArray(sourceValue) ? sourceValue : [];
|
|
80
|
-
const targetArray = Array.isArray(targetValue) ? targetValue : [];
|
|
26
|
+
/** Collects translatable field chunks that need translation. */ collect() {
|
|
27
|
+
const chunks = [];
|
|
28
|
+
const { strategy } = this;
|
|
29
|
+
const walker = {
|
|
30
|
+
enterObject (field, cursor) {
|
|
31
|
+
const value = cursor.data[field.name];
|
|
32
|
+
if (!isObject(value)) return "skip";
|
|
33
|
+
return {
|
|
34
|
+
data: value,
|
|
35
|
+
source: asObject(cursor.source[field.name]),
|
|
36
|
+
target: asObject(cursor.target[field.name]),
|
|
37
|
+
path: [
|
|
38
|
+
...cursor.path,
|
|
39
|
+
field.name
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
enterList (field, cursor) {
|
|
44
|
+
const value = cursor.data[field.name];
|
|
45
|
+
if (!Array.isArray(value)) return "skip";
|
|
46
|
+
const sourceValue = cursor.source[field.name];
|
|
47
|
+
const targetValue = cursor.target[field.name];
|
|
48
|
+
const sourceArr = Array.isArray(sourceValue) ? sourceValue : [];
|
|
49
|
+
const targetArr = Array.isArray(targetValue) ? targetValue : [];
|
|
50
|
+
const children = [];
|
|
81
51
|
value.forEach((item, index)=>{
|
|
82
|
-
if (isObject(item))
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (fieldIsBlockType(field) && Array.isArray(value)) {
|
|
94
|
-
const sourceArray = Array.isArray(sourceValue) ? sourceValue : [];
|
|
95
|
-
const targetArray = Array.isArray(targetValue) ? targetValue : [];
|
|
96
|
-
value.forEach((item, index)=>{
|
|
97
|
-
if (isBlockItem(item)) {
|
|
98
|
-
const block = field.blocks.find((b)=>b.slug === item.blockType);
|
|
99
|
-
if (block) {
|
|
100
|
-
const sourceItem = isObject(sourceArray[index]) ? sourceArray[index] : {};
|
|
101
|
-
const targetItem = isObject(targetArray[index]) ? targetArray[index] : {};
|
|
102
|
-
this.traverseFields(block.fields, item, sourceItem, targetItem, [
|
|
103
|
-
...currentPath,
|
|
52
|
+
if (!isObject(item)) return;
|
|
53
|
+
const fields = field.type === "blocks" ? resolveBlockFields(field, item) : field.fields;
|
|
54
|
+
if (!fields) return; // unknown blockType → skip element
|
|
55
|
+
children.push({
|
|
56
|
+
cursor: {
|
|
57
|
+
data: item,
|
|
58
|
+
source: asObject(sourceArr[index]),
|
|
59
|
+
target: asObject(targetArr[index]),
|
|
60
|
+
path: [
|
|
61
|
+
...cursor.path,
|
|
62
|
+
field.name,
|
|
104
63
|
String(index)
|
|
105
|
-
]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
// Collect if: translatable, localized, not excluded, and strategy says to translate
|
|
112
|
-
if (isTranslatableField(field) && isLocalizedField(field) && !isFieldExcludedFromTranslation(field) && this.strategy.shouldTranslate({
|
|
113
|
-
sourceValue,
|
|
114
|
-
targetValue
|
|
115
|
-
})) {
|
|
116
|
-
// Write sourceValue to filteredData — this is what will be translated
|
|
117
|
-
data[field.name] = sourceValue;
|
|
118
|
-
this.chunks.push({
|
|
119
|
-
schema: field,
|
|
120
|
-
dataRef: data,
|
|
121
|
-
key: field.name,
|
|
122
|
-
path: currentPath
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
fields,
|
|
67
|
+
key: index
|
|
68
|
+
});
|
|
123
69
|
});
|
|
70
|
+
return children;
|
|
71
|
+
},
|
|
72
|
+
leaf (field, cursor) {
|
|
73
|
+
const value = cursor.data[field.name];
|
|
74
|
+
if (value === undefined || value === null) return undefined;
|
|
75
|
+
const sourceValue = cursor.source[field.name];
|
|
76
|
+
const targetValue = cursor.target[field.name];
|
|
77
|
+
if (isTranslatableField(field) && isLocalizedField(field) && !isFieldExcludedFromTranslation(field) && strategy.shouldTranslate({
|
|
78
|
+
sourceValue,
|
|
79
|
+
targetValue
|
|
80
|
+
})) {
|
|
81
|
+
cursor.data[field.name] = sourceValue; // write source value into filteredData — this is what gets translated
|
|
82
|
+
chunks.push({
|
|
83
|
+
schema: field,
|
|
84
|
+
dataRef: cursor.data,
|
|
85
|
+
key: field.name,
|
|
86
|
+
path: [
|
|
87
|
+
...cursor.path,
|
|
88
|
+
field.name
|
|
89
|
+
]
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
},
|
|
94
|
+
combine () {
|
|
95
|
+
return undefined; // collect-only — nothing to assemble
|
|
124
96
|
}
|
|
125
|
-
}
|
|
97
|
+
};
|
|
98
|
+
walkFields(this.schema, {
|
|
99
|
+
data: this.filteredData,
|
|
100
|
+
source: this.sourceData,
|
|
101
|
+
target: this.targetData,
|
|
102
|
+
path: []
|
|
103
|
+
}, walker);
|
|
104
|
+
return chunks;
|
|
126
105
|
}
|
|
127
106
|
}
|
|
128
107
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Field } from "payload";
|
|
2
|
+
import type { LeafField } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Outcome of navigating a field schema by a path of segment names (see {@link findFieldByPath}).
|
|
5
|
+
*
|
|
6
|
+
* - `leaf` — the path lands on a data-affecting leaf field (carries it).
|
|
7
|
+
* - `container` — the path lands on a `group`/`array`/`blocks` field or a named tab (a container,
|
|
8
|
+
* not a leaf).
|
|
9
|
+
* - `inside-blocks` — the path descends THROUGH a `blocks` field, which can't be resolved from the
|
|
10
|
+
* schema alone (the block's `blockType` lives in the data, not the schema).
|
|
11
|
+
* - `not-found` — no field matches a segment, or the path continues past a leaf.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export type FieldPathResult = {
|
|
16
|
+
status: "leaf";
|
|
17
|
+
field: LeafField;
|
|
18
|
+
} | {
|
|
19
|
+
status: "container";
|
|
20
|
+
} | {
|
|
21
|
+
status: "inside-blocks";
|
|
22
|
+
} | {
|
|
23
|
+
status: "not-found";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Navigate a field schema by a path of segment NAMES, descending one matching branch at a time
|
|
27
|
+
* with early-exit (targeted navigation, not an exhaustive walk). Presentational containers
|
|
28
|
+
* (`row`/`collapsible`/unnamed `group`) and unnamed tabs are transparent — searched in the same
|
|
29
|
+
* path scope. Built on {@link classifyField} / {@link tabScopes} so the structural dispatch lives
|
|
30
|
+
* in one place.
|
|
31
|
+
*
|
|
32
|
+
* The caller supplies already-prepared name segments (split, trimmed, with array indices dropped —
|
|
33
|
+
* a field's config is shared across array items, so indices never appear in the schema).
|
|
34
|
+
*
|
|
35
|
+
* @param fields - The schema level to search.
|
|
36
|
+
* @param segments - Remaining path segments (names) to match, head-first.
|
|
37
|
+
* @returns A {@link FieldPathResult}.
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
export declare function findFieldByPath(fields: Field[], segments: string[]): FieldPathResult;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { classifyField, tabScopes } from "./kernel";
|
|
2
|
+
/**
|
|
3
|
+
* Navigate a field schema by a path of segment NAMES, descending one matching branch at a time
|
|
4
|
+
* with early-exit (targeted navigation, not an exhaustive walk). Presentational containers
|
|
5
|
+
* (`row`/`collapsible`/unnamed `group`) and unnamed tabs are transparent — searched in the same
|
|
6
|
+
* path scope. Built on {@link classifyField} / {@link tabScopes} so the structural dispatch lives
|
|
7
|
+
* in one place.
|
|
8
|
+
*
|
|
9
|
+
* The caller supplies already-prepared name segments (split, trimmed, with array indices dropped —
|
|
10
|
+
* a field's config is shared across array items, so indices never appear in the schema).
|
|
11
|
+
*
|
|
12
|
+
* @param fields - The schema level to search.
|
|
13
|
+
* @param segments - Remaining path segments (names) to match, head-first.
|
|
14
|
+
* @returns A {@link FieldPathResult}.
|
|
15
|
+
* @public
|
|
16
|
+
*/ export function findFieldByPath(fields, segments) {
|
|
17
|
+
const [head, ...rest] = segments;
|
|
18
|
+
if (head === undefined) return {
|
|
19
|
+
status: "not-found"
|
|
20
|
+
};
|
|
21
|
+
for (const field of fields){
|
|
22
|
+
const structure = classifyField(field);
|
|
23
|
+
switch(structure.kind){
|
|
24
|
+
case "tabs":
|
|
25
|
+
{
|
|
26
|
+
for (const scope of tabScopes(structure.field)){
|
|
27
|
+
if (scope.named) {
|
|
28
|
+
if (scope.tab.name === head) {
|
|
29
|
+
return rest.length === 0 ? {
|
|
30
|
+
status: "container"
|
|
31
|
+
} : findFieldByPath(scope.tab.fields, rest);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
const found = findFieldByPath(scope.fields, segments); // unnamed tab → same path scope
|
|
35
|
+
if (found.status !== "not-found") return found;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case "transparent":
|
|
41
|
+
{
|
|
42
|
+
const found = findFieldByPath(structure.fields, segments); // row/collapsible/unnamed group → same scope
|
|
43
|
+
if (found.status !== "not-found") return found;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "presentational":
|
|
47
|
+
break;
|
|
48
|
+
case "group":
|
|
49
|
+
case "array":
|
|
50
|
+
{
|
|
51
|
+
if (structure.name !== head) break;
|
|
52
|
+
return rest.length === 0 ? {
|
|
53
|
+
status: "container"
|
|
54
|
+
} : findFieldByPath(structure.fields, rest);
|
|
55
|
+
}
|
|
56
|
+
case "blocks":
|
|
57
|
+
{
|
|
58
|
+
if (structure.name !== head) break;
|
|
59
|
+
return rest.length === 0 ? {
|
|
60
|
+
status: "container"
|
|
61
|
+
} : {
|
|
62
|
+
status: "inside-blocks"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
case "leaf":
|
|
66
|
+
{
|
|
67
|
+
if (structure.name !== head) break;
|
|
68
|
+
return rest.length === 0 ? {
|
|
69
|
+
status: "leaf",
|
|
70
|
+
field: structure.field
|
|
71
|
+
} : {
|
|
72
|
+
status: "not-found"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
default:
|
|
76
|
+
{
|
|
77
|
+
// Exhaustiveness guard: a new FieldStructure kind would error here at compile time.
|
|
78
|
+
const exhaustive = structure;
|
|
79
|
+
throw new Error(`unhandled field structure: ${String(exhaustive)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
status: "not-found"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=findFieldByPath.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { findFieldByPath } from "./findFieldByPath";
|
|
2
|
+
export type { FieldPathResult } from "./findFieldByPath";
|
|
3
|
+
export { classifyField, resolveBlockFields, tabScopes } from "./kernel";
|
|
4
|
+
export type { ChildCursor, ChildOutput, ContainerInfo, FieldStructure, FieldWalker, LeafField, TabScope, WalkSignal } from "./types";
|
|
5
|
+
export { walkFields } from "./walkFields";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { BlocksField, Field, TabsField } from "payload";
|
|
2
|
+
import type { FieldStructure, TabScope } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Classify a single Payload {@link Field} into a discriminated {@link FieldStructure} — the
|
|
5
|
+
* one place that encodes how Payload field types map onto data boundaries.
|
|
6
|
+
*
|
|
7
|
+
* Traversals switch on the result instead of re-deriving the predicate chain
|
|
8
|
+
* (`fieldAffectsData` / `fieldIsGroupType` / …), so the dispatch — and any future Payload
|
|
9
|
+
* field type — lives in exactly one place.
|
|
10
|
+
*
|
|
11
|
+
* @param field - Any field from a collection/global `fields` array (original, un-sanitized
|
|
12
|
+
* config).
|
|
13
|
+
* @returns The field's {@link FieldStructure}:
|
|
14
|
+
* - `tabs` — a `tabs` field (expand with {@link tabScopes}).
|
|
15
|
+
* - `transparent` — a presentational container that opens **no** data boundary (`row`,
|
|
16
|
+
* `collapsible`, unnamed `group`); its `fields` live in the parent's data scope.
|
|
17
|
+
* - `presentational` — a `ui` field: no data, no subfields.
|
|
18
|
+
* - `group` / `array` / `blocks` — named data boundaries (carry `name` + child `fields`,
|
|
19
|
+
* except `blocks` whose child fields are per-element via {@link resolveBlockFields}).
|
|
20
|
+
* - `leaf` — a data-affecting field with no subfields.
|
|
21
|
+
*
|
|
22
|
+
* Dispatch order is significant: some fields satisfy more than one predicate and only the
|
|
23
|
+
* order disambiguates them.
|
|
24
|
+
* - `tabs` is checked first — it carries `.tabs` (not `.fields`) and is non-data-affecting,
|
|
25
|
+
* so a later check would mislabel it `presentational`.
|
|
26
|
+
* - `fieldAffectsData` is resolved before the `group` check, so an unnamed group resolves to
|
|
27
|
+
* `transparent`, never a `group` with `name: undefined`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const structure = classifyField(field);
|
|
32
|
+
* switch (structure.kind) {
|
|
33
|
+
* case "group": descend(structure.fields, data[structure.name]); break;
|
|
34
|
+
* case "leaf": translate(structure.field, structure.name); break;
|
|
35
|
+
* // ...handle the remaining kinds
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @see {@link walkFields} — the recursive engine built on this classification.
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
export declare function classifyField(field: Field): FieldStructure;
|
|
43
|
+
/**
|
|
44
|
+
* Flatten a `tabs` field's tabs into {@link TabScope}s, absorbing the named/unnamed +
|
|
45
|
+
* `hasFields` handling that field traversals would otherwise each repeat.
|
|
46
|
+
*
|
|
47
|
+
* @param field - A Payload `tabs` field.
|
|
48
|
+
* @returns One {@link TabScope} per tab that has fields, in declaration order:
|
|
49
|
+
* - a **named** tab → `{ named: true, tab }` — it opens a data boundary at `tab.name`, and
|
|
50
|
+
* the `NamedTab` is surfaced so callers can read its `name`/`fields`/config;
|
|
51
|
+
* - an **unnamed** tab → `{ named: false, fields }` — its fields live in the parent's data
|
|
52
|
+
* scope, so only the `fields` are needed.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* for (const scope of tabScopes(tabsField)) {
|
|
57
|
+
* if (scope.named) descendInto(data[scope.tab.name], scope.tab.fields);
|
|
58
|
+
* else descendInto(data, scope.fields); // unnamed tab — same data scope as the parent
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
export declare function tabScopes(field: TabsField): TabScope[];
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the child `fields` for a single `blocks` element by matching its `blockType`
|
|
66
|
+
* against the field's block definitions.
|
|
67
|
+
*
|
|
68
|
+
* @param field - A Payload `blocks` field carrying inline block definitions.
|
|
69
|
+
* @param item - The data for one block element (expected to carry a `blockType` string).
|
|
70
|
+
* @returns The matched block's `fields`, or `null` when `item` is not a block object or its
|
|
71
|
+
* `blockType` matches no defined block.
|
|
72
|
+
*
|
|
73
|
+
* Assumes the **original, un-sanitized** schema, where blocks are inline `Block` objects on
|
|
74
|
+
* `field.blocks`. Payload's sanitized config may instead carry `blockReferences` resolved
|
|
75
|
+
* against the top-level config — not handled here by design, since this package traverses
|
|
76
|
+
* the original schema to preserve nested `localized` flags.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const fields = resolveBlockFields(blocksField, item);
|
|
81
|
+
* if (fields) descendInto(item, fields); // null → unknown blockType / not a block → skip
|
|
82
|
+
* ```
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveBlockFields(field: BlocksField, item: unknown): Field[] | null;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from "payload/shared";
|
|
2
|
+
import { hasFields, isBlockItem, isTabsField } from "../guards";
|
|
3
|
+
/**
|
|
4
|
+
* Classify a single Payload {@link Field} into a discriminated {@link FieldStructure} — the
|
|
5
|
+
* one place that encodes how Payload field types map onto data boundaries.
|
|
6
|
+
*
|
|
7
|
+
* Traversals switch on the result instead of re-deriving the predicate chain
|
|
8
|
+
* (`fieldAffectsData` / `fieldIsGroupType` / …), so the dispatch — and any future Payload
|
|
9
|
+
* field type — lives in exactly one place.
|
|
10
|
+
*
|
|
11
|
+
* @param field - Any field from a collection/global `fields` array (original, un-sanitized
|
|
12
|
+
* config).
|
|
13
|
+
* @returns The field's {@link FieldStructure}:
|
|
14
|
+
* - `tabs` — a `tabs` field (expand with {@link tabScopes}).
|
|
15
|
+
* - `transparent` — a presentational container that opens **no** data boundary (`row`,
|
|
16
|
+
* `collapsible`, unnamed `group`); its `fields` live in the parent's data scope.
|
|
17
|
+
* - `presentational` — a `ui` field: no data, no subfields.
|
|
18
|
+
* - `group` / `array` / `blocks` — named data boundaries (carry `name` + child `fields`,
|
|
19
|
+
* except `blocks` whose child fields are per-element via {@link resolveBlockFields}).
|
|
20
|
+
* - `leaf` — a data-affecting field with no subfields.
|
|
21
|
+
*
|
|
22
|
+
* Dispatch order is significant: some fields satisfy more than one predicate and only the
|
|
23
|
+
* order disambiguates them.
|
|
24
|
+
* - `tabs` is checked first — it carries `.tabs` (not `.fields`) and is non-data-affecting,
|
|
25
|
+
* so a later check would mislabel it `presentational`.
|
|
26
|
+
* - `fieldAffectsData` is resolved before the `group` check, so an unnamed group resolves to
|
|
27
|
+
* `transparent`, never a `group` with `name: undefined`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const structure = classifyField(field);
|
|
32
|
+
* switch (structure.kind) {
|
|
33
|
+
* case "group": descend(structure.fields, data[structure.name]); break;
|
|
34
|
+
* case "leaf": translate(structure.field, structure.name); break;
|
|
35
|
+
* // ...handle the remaining kinds
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @see {@link walkFields} — the recursive engine built on this classification.
|
|
40
|
+
* @public
|
|
41
|
+
*/ export function classifyField(field) {
|
|
42
|
+
if (isTabsField(field)) {
|
|
43
|
+
return {
|
|
44
|
+
kind: "tabs",
|
|
45
|
+
field
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (!fieldAffectsData(field)) {
|
|
49
|
+
return hasFields(field) ? {
|
|
50
|
+
kind: "transparent",
|
|
51
|
+
fields: field.fields
|
|
52
|
+
} : {
|
|
53
|
+
kind: "presentational"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (fieldIsGroupType(field)) {
|
|
57
|
+
return {
|
|
58
|
+
kind: "group",
|
|
59
|
+
name: field.name,
|
|
60
|
+
fields: field.fields,
|
|
61
|
+
field
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (fieldIsArrayType(field)) {
|
|
65
|
+
return {
|
|
66
|
+
kind: "array",
|
|
67
|
+
name: field.name,
|
|
68
|
+
fields: field.fields,
|
|
69
|
+
field
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (fieldIsBlockType(field)) {
|
|
73
|
+
return {
|
|
74
|
+
kind: "blocks",
|
|
75
|
+
name: field.name,
|
|
76
|
+
field
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const leaf = field;
|
|
80
|
+
return {
|
|
81
|
+
kind: "leaf",
|
|
82
|
+
name: leaf.name,
|
|
83
|
+
field: leaf
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Flatten a `tabs` field's tabs into {@link TabScope}s, absorbing the named/unnamed +
|
|
88
|
+
* `hasFields` handling that field traversals would otherwise each repeat.
|
|
89
|
+
*
|
|
90
|
+
* @param field - A Payload `tabs` field.
|
|
91
|
+
* @returns One {@link TabScope} per tab that has fields, in declaration order:
|
|
92
|
+
* - a **named** tab → `{ named: true, tab }` — it opens a data boundary at `tab.name`, and
|
|
93
|
+
* the `NamedTab` is surfaced so callers can read its `name`/`fields`/config;
|
|
94
|
+
* - an **unnamed** tab → `{ named: false, fields }` — its fields live in the parent's data
|
|
95
|
+
* scope, so only the `fields` are needed.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* for (const scope of tabScopes(tabsField)) {
|
|
100
|
+
* if (scope.named) descendInto(data[scope.tab.name], scope.tab.fields);
|
|
101
|
+
* else descendInto(data, scope.fields); // unnamed tab — same data scope as the parent
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
* @public
|
|
105
|
+
*/ export function tabScopes(field) {
|
|
106
|
+
const scopes = [];
|
|
107
|
+
for (const tab of field.tabs){
|
|
108
|
+
if (tabHasName(tab)) scopes.push({
|
|
109
|
+
named: true,
|
|
110
|
+
tab
|
|
111
|
+
});
|
|
112
|
+
else if (hasFields(tab)) scopes.push({
|
|
113
|
+
named: false,
|
|
114
|
+
fields: tab.fields
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return scopes;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolve the child `fields` for a single `blocks` element by matching its `blockType`
|
|
121
|
+
* against the field's block definitions.
|
|
122
|
+
*
|
|
123
|
+
* @param field - A Payload `blocks` field carrying inline block definitions.
|
|
124
|
+
* @param item - The data for one block element (expected to carry a `blockType` string).
|
|
125
|
+
* @returns The matched block's `fields`, or `null` when `item` is not a block object or its
|
|
126
|
+
* `blockType` matches no defined block.
|
|
127
|
+
*
|
|
128
|
+
* Assumes the **original, un-sanitized** schema, where blocks are inline `Block` objects on
|
|
129
|
+
* `field.blocks`. Payload's sanitized config may instead carry `blockReferences` resolved
|
|
130
|
+
* against the top-level config — not handled here by design, since this package traverses
|
|
131
|
+
* the original schema to preserve nested `localized` flags.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* const fields = resolveBlockFields(blocksField, item);
|
|
136
|
+
* if (fields) descendInto(item, fields); // null → unknown blockType / not a block → skip
|
|
137
|
+
* ```
|
|
138
|
+
* @public
|
|
139
|
+
*/ export function resolveBlockFields(field, item) {
|
|
140
|
+
if (!isBlockItem(item)) return null;
|
|
141
|
+
const block = field.blocks.find((candidate)=>candidate.slug === item.blockType);
|
|
142
|
+
return block ? block.fields : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
//# sourceMappingURL=kernel.js.map
|