@focus-reactive/payload-plugin-translator 0.5.0 → 0.6.0
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/README.md +46 -8
- package/dist/client/entities/translation/api/mutations/useTranslateField.d.ts +22 -0
- package/dist/client/entities/translation/api/mutations/useTranslateField.js +31 -0
- package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.d.ts +3 -3
- package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.js +15 -13
- package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.d.ts +2 -2
- package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.js +10 -10
- package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.d.ts +1 -1
- package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.js +9 -9
- package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.d.ts +2 -2
- package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.js +10 -8
- package/dist/client/shared/lib/assets/icons/SendIcon.d.ts +1 -0
- package/dist/client/shared/lib/assets/icons/SendIcon.js +17 -0
- package/dist/client/shared/ui/Button/Button.d.ts +5 -4
- package/dist/client/shared/ui/Button/Button.js +19 -19
- package/dist/client/shared/ui/Button/styles.module.scss +27 -4
- 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/client/widgets/translate-field-control/index.d.ts +1 -0
- package/dist/client/widgets/translate-field-control/index.js +3 -0
- package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.d.ts +26 -0
- package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.export.d.ts +13 -0
- package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.export.js +14 -0
- package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.js +204 -0
- package/dist/client/widgets/translate-field-control/ui/styles.module.scss +48 -0
- package/dist/field-actions.d.ts +25 -0
- package/dist/field-actions.js +23 -0
- package/dist/field-config.d.ts +26 -26
- package/dist/field-config.js +14 -37
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +3 -1
- package/dist/server/features/translate-field/handler.d.ts +17 -0
- package/dist/server/features/translate-field/handler.js +85 -0
- package/dist/server/features/translate-field/index.d.ts +6 -0
- package/dist/server/features/translate-field/index.js +5 -0
- package/dist/server/features/translate-field/model.d.ts +62 -0
- package/dist/server/features/translate-field/model.js +26 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +39 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.js +51 -0
- package/dist/server/features/translate-field/route.d.ts +12 -0
- package/dist/server/features/translate-field/route.js +18 -0
- package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +6 -0
- package/dist/server/modules/translation-levels/PluginConfigBuilder.js +4 -0
- package/dist/server/modules/translation-levels/fieldLevel.d.ts +23 -0
- package/dist/server/modules/translation-levels/fieldLevel.js +37 -0
- package/dist/server/modules/translation-levels/index.d.ts +1 -0
- package/dist/server/modules/translation-levels/index.js +1 -0
- package/dist/server/modules/translation-levels/types.d.ts +6 -1
- package/dist/server/modules/translation-levels/types.js +0 -1
- 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 +76 -77
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.stage.d.ts +1 -1
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.stage.js +1 -1
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +15 -21
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +93 -105
- package/dist/server/modules/translation-providers/OpenAITranslation.provider.d.ts +17 -0
- package/dist/server/modules/translation-providers/OpenAITranslation.provider.js +8 -2
- package/dist/server/shared/field-config/types.js +1 -1
- package/dist/server/shared/field-traversal/findFieldByPath.d.ts +55 -0
- package/dist/server/shared/field-traversal/findFieldByPath.js +133 -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 +107 -0
- package/dist/server/shared/field-traversal/kernel.js +172 -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 +3 -3
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { classifyField, resolveBlockFields, tabScopes } from "./kernel";
|
|
2
|
+
const isIndexSegment = (segment)=>/^\d+$/u.test(segment);
|
|
3
|
+
/** Safely read `key` off an object/array; `undefined` for non-objects (arrays index by string key). */ const childData = (data, key)=>data != null && typeof data === "object" ? data[key] : undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Navigate a field schema by a path of segment NAMES, descending one matching branch at a time
|
|
6
|
+
* with early-exit (targeted navigation, not an exhaustive walk). Presentational containers
|
|
7
|
+
* (`row`/`collapsible`/unnamed `group`) and unnamed tabs are transparent — searched in the same
|
|
8
|
+
* path scope. Built on {@link classifyField} / {@link tabScopes} / {@link resolveBlockFields} so
|
|
9
|
+
* the structural dispatch lives in one place.
|
|
10
|
+
*
|
|
11
|
+
* **Array indices and `blocks`.** Array element configs are shared, so a numeric segment after an
|
|
12
|
+
* array name selects the data item but not the schema (the schema continues into `array.fields`).
|
|
13
|
+
* A `blocks` field is polymorphic — which block (and thus which fields) sits at an index lives in
|
|
14
|
+
* the *data*, not the schema. So to descend through `blocks` the caller must pass the document
|
|
15
|
+
* `data` and keep the element index in the path: this function reads `data[name][index].blockType`
|
|
16
|
+
* and resolves the matching block via {@link resolveBlockFields}. Without `data` (or with a
|
|
17
|
+
* non-indexed path), descending through `blocks` returns `inside-blocks`.
|
|
18
|
+
*
|
|
19
|
+
* @param fields - The schema level to search.
|
|
20
|
+
* @param segments - Remaining path segments to match, head-first. Keep array/block element indices
|
|
21
|
+
* in the path when navigating with `data`.
|
|
22
|
+
* @param data - The document (sub)tree aligned with `fields`, used only to disambiguate `blocks`.
|
|
23
|
+
* Omit for schema-only navigation.
|
|
24
|
+
* @returns A {@link FieldPathResult}.
|
|
25
|
+
* @public
|
|
26
|
+
*/ export function findFieldByPath(fields, segments, data) {
|
|
27
|
+
const [head, ...rest] = segments;
|
|
28
|
+
if (head === undefined) return {
|
|
29
|
+
status: "not-found"
|
|
30
|
+
};
|
|
31
|
+
for (const field of fields){
|
|
32
|
+
const structure = classifyField(field);
|
|
33
|
+
switch(structure.kind){
|
|
34
|
+
case "tabs":
|
|
35
|
+
{
|
|
36
|
+
for (const scope of tabScopes(structure.field)){
|
|
37
|
+
if (scope.named) {
|
|
38
|
+
if (scope.tab.name === head) {
|
|
39
|
+
return rest.length === 0 ? {
|
|
40
|
+
status: "container"
|
|
41
|
+
} : findFieldByPath(scope.tab.fields, rest, childData(data, head));
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
const found = findFieldByPath(scope.fields, segments, data); // unnamed tab → same path + data scope
|
|
45
|
+
if (found.status !== "not-found") return found;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case "transparent":
|
|
51
|
+
{
|
|
52
|
+
const found = findFieldByPath(structure.fields, segments, data); // row/collapsible/unnamed group → same scope
|
|
53
|
+
if (found.status !== "not-found") return found;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case "presentational":
|
|
57
|
+
break;
|
|
58
|
+
case "group":
|
|
59
|
+
{
|
|
60
|
+
if (structure.name !== head) break;
|
|
61
|
+
return rest.length === 0 ? {
|
|
62
|
+
status: "container"
|
|
63
|
+
} : findFieldByPath(structure.fields, rest, childData(data, head));
|
|
64
|
+
}
|
|
65
|
+
case "array":
|
|
66
|
+
{
|
|
67
|
+
if (structure.name !== head) break;
|
|
68
|
+
if (rest.length === 0) return {
|
|
69
|
+
status: "container"
|
|
70
|
+
};
|
|
71
|
+
// A localized array is an independent per-locale structure — its element index is not a
|
|
72
|
+
// stable identity across locales, so refuse to navigate THROUGH it by path.
|
|
73
|
+
if (structure.field.localized) return {
|
|
74
|
+
status: "localized-list-ancestor"
|
|
75
|
+
};
|
|
76
|
+
// A numeric segment selects the data element; the schema is shared across elements.
|
|
77
|
+
const [next, ...tail] = rest;
|
|
78
|
+
if (isIndexSegment(next)) {
|
|
79
|
+
return tail.length === 0 ? {
|
|
80
|
+
status: "container"
|
|
81
|
+
} : findFieldByPath(structure.fields, tail, childData(childData(data, head), next));
|
|
82
|
+
}
|
|
83
|
+
return findFieldByPath(structure.fields, rest, childData(data, head));
|
|
84
|
+
}
|
|
85
|
+
case "blocks":
|
|
86
|
+
{
|
|
87
|
+
if (structure.name !== head) break;
|
|
88
|
+
if (rest.length === 0) return {
|
|
89
|
+
status: "container"
|
|
90
|
+
};
|
|
91
|
+
// A localized blocks field is an independent per-locale structure — its element index is
|
|
92
|
+
// not a stable identity across locales, so refuse to navigate THROUGH it by path.
|
|
93
|
+
if (structure.field.localized) return {
|
|
94
|
+
status: "localized-list-ancestor"
|
|
95
|
+
};
|
|
96
|
+
// Polymorphic: need the element index + its data to pick the block schema by `blockType`.
|
|
97
|
+
const [next, ...tail] = rest;
|
|
98
|
+
if (!isIndexSegment(next)) return {
|
|
99
|
+
status: "inside-blocks"
|
|
100
|
+
};
|
|
101
|
+
const item = childData(childData(data, head), next);
|
|
102
|
+
const blockFields = resolveBlockFields(structure.field, item);
|
|
103
|
+
if (!blockFields) return {
|
|
104
|
+
status: "inside-blocks"
|
|
105
|
+
};
|
|
106
|
+
return tail.length === 0 ? {
|
|
107
|
+
status: "container"
|
|
108
|
+
} : findFieldByPath(blockFields, tail, item);
|
|
109
|
+
}
|
|
110
|
+
case "leaf":
|
|
111
|
+
{
|
|
112
|
+
if (structure.name !== head) break;
|
|
113
|
+
return rest.length === 0 ? {
|
|
114
|
+
status: "leaf",
|
|
115
|
+
field: structure.field
|
|
116
|
+
} : {
|
|
117
|
+
status: "not-found"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
default:
|
|
121
|
+
{
|
|
122
|
+
// Exhaustiveness guard: a new FieldStructure kind would error here at compile time.
|
|
123
|
+
const exhaustive = structure;
|
|
124
|
+
throw new Error(`unhandled field structure: ${String(exhaustive)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
status: "not-found"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=findFieldByPath.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { findFieldByPath } from "./findFieldByPath";
|
|
2
|
+
export type { FieldPathResult } from "./findFieldByPath";
|
|
3
|
+
export { classifyField, matchElementById, 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,107 @@
|
|
|
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;
|
|
86
|
+
/**
|
|
87
|
+
* Find the element of a parallel data array that corresponds to `refItem` by `id` — not by
|
|
88
|
+
* position.
|
|
89
|
+
*
|
|
90
|
+
* Localized `blocks`/`array` fields are stored independently per locale, so the locales can be
|
|
91
|
+
* reordered or differ entirely; pairing two parallel trees by position then cross-contaminates
|
|
92
|
+
* unrelated elements ("blocks don't line up across locales"). Matching by `id` is correct in both
|
|
93
|
+
* regimes: a non-localized field is one shared row with the same `id` across locales (so id-match
|
|
94
|
+
* equals the old positional match), while independent localized content has diverging ids (no
|
|
95
|
+
* match → the caller falls back to `refItem`'s own values, never another element's). For blocks the
|
|
96
|
+
* `blockType` must also match, so an id collision across types can't graft mismatched fields.
|
|
97
|
+
*
|
|
98
|
+
* Shared by the translation pipeline's two multi-tree walkers — reconcile and collect — to pair
|
|
99
|
+
* their `target` array against the source/reference element currently being visited.
|
|
100
|
+
*
|
|
101
|
+
* @param arr - The array to search (e.g. the target-locale elements).
|
|
102
|
+
* @param refItem - The element being matched, carrying the canonical `id` (and `blockType`).
|
|
103
|
+
* @param isBlocks - Whether the field is a `blocks` field (then `blockType` must also match).
|
|
104
|
+
* @returns The matched element, or `{}` when there is no counterpart (or `refItem` carries no `id`).
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
export declare function matchElementById(arr: unknown[], refItem: Record<string, unknown>, isBlocks: boolean): Record<string, unknown>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from "payload/shared";
|
|
2
|
+
import { hasFields, isBlockItem, isTabsField } from "../guards";
|
|
3
|
+
import { isObject } from "../utils";
|
|
4
|
+
/**
|
|
5
|
+
* Classify a single Payload {@link Field} into a discriminated {@link FieldStructure} — the
|
|
6
|
+
* one place that encodes how Payload field types map onto data boundaries.
|
|
7
|
+
*
|
|
8
|
+
* Traversals switch on the result instead of re-deriving the predicate chain
|
|
9
|
+
* (`fieldAffectsData` / `fieldIsGroupType` / …), so the dispatch — and any future Payload
|
|
10
|
+
* field type — lives in exactly one place.
|
|
11
|
+
*
|
|
12
|
+
* @param field - Any field from a collection/global `fields` array (original, un-sanitized
|
|
13
|
+
* config).
|
|
14
|
+
* @returns The field's {@link FieldStructure}:
|
|
15
|
+
* - `tabs` — a `tabs` field (expand with {@link tabScopes}).
|
|
16
|
+
* - `transparent` — a presentational container that opens **no** data boundary (`row`,
|
|
17
|
+
* `collapsible`, unnamed `group`); its `fields` live in the parent's data scope.
|
|
18
|
+
* - `presentational` — a `ui` field: no data, no subfields.
|
|
19
|
+
* - `group` / `array` / `blocks` — named data boundaries (carry `name` + child `fields`,
|
|
20
|
+
* except `blocks` whose child fields are per-element via {@link resolveBlockFields}).
|
|
21
|
+
* - `leaf` — a data-affecting field with no subfields.
|
|
22
|
+
*
|
|
23
|
+
* Dispatch order is significant: some fields satisfy more than one predicate and only the
|
|
24
|
+
* order disambiguates them.
|
|
25
|
+
* - `tabs` is checked first — it carries `.tabs` (not `.fields`) and is non-data-affecting,
|
|
26
|
+
* so a later check would mislabel it `presentational`.
|
|
27
|
+
* - `fieldAffectsData` is resolved before the `group` check, so an unnamed group resolves to
|
|
28
|
+
* `transparent`, never a `group` with `name: undefined`.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const structure = classifyField(field);
|
|
33
|
+
* switch (structure.kind) {
|
|
34
|
+
* case "group": descend(structure.fields, data[structure.name]); break;
|
|
35
|
+
* case "leaf": translate(structure.field, structure.name); break;
|
|
36
|
+
* // ...handle the remaining kinds
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @see {@link walkFields} — the recursive engine built on this classification.
|
|
41
|
+
* @public
|
|
42
|
+
*/ export function classifyField(field) {
|
|
43
|
+
if (isTabsField(field)) {
|
|
44
|
+
return {
|
|
45
|
+
kind: "tabs",
|
|
46
|
+
field
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (!fieldAffectsData(field)) {
|
|
50
|
+
return hasFields(field) ? {
|
|
51
|
+
kind: "transparent",
|
|
52
|
+
fields: field.fields
|
|
53
|
+
} : {
|
|
54
|
+
kind: "presentational"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (fieldIsGroupType(field)) {
|
|
58
|
+
return {
|
|
59
|
+
kind: "group",
|
|
60
|
+
name: field.name,
|
|
61
|
+
fields: field.fields,
|
|
62
|
+
field
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (fieldIsArrayType(field)) {
|
|
66
|
+
return {
|
|
67
|
+
kind: "array",
|
|
68
|
+
name: field.name,
|
|
69
|
+
fields: field.fields,
|
|
70
|
+
field
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (fieldIsBlockType(field)) {
|
|
74
|
+
return {
|
|
75
|
+
kind: "blocks",
|
|
76
|
+
name: field.name,
|
|
77
|
+
field
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const leaf = field;
|
|
81
|
+
return {
|
|
82
|
+
kind: "leaf",
|
|
83
|
+
name: leaf.name,
|
|
84
|
+
field: leaf
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Flatten a `tabs` field's tabs into {@link TabScope}s, absorbing the named/unnamed +
|
|
89
|
+
* `hasFields` handling that field traversals would otherwise each repeat.
|
|
90
|
+
*
|
|
91
|
+
* @param field - A Payload `tabs` field.
|
|
92
|
+
* @returns One {@link TabScope} per tab that has fields, in declaration order:
|
|
93
|
+
* - a **named** tab → `{ named: true, tab }` — it opens a data boundary at `tab.name`, and
|
|
94
|
+
* the `NamedTab` is surfaced so callers can read its `name`/`fields`/config;
|
|
95
|
+
* - an **unnamed** tab → `{ named: false, fields }` — its fields live in the parent's data
|
|
96
|
+
* scope, so only the `fields` are needed.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* for (const scope of tabScopes(tabsField)) {
|
|
101
|
+
* if (scope.named) descendInto(data[scope.tab.name], scope.tab.fields);
|
|
102
|
+
* else descendInto(data, scope.fields); // unnamed tab — same data scope as the parent
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
* @public
|
|
106
|
+
*/ export function tabScopes(field) {
|
|
107
|
+
const scopes = [];
|
|
108
|
+
for (const tab of field.tabs){
|
|
109
|
+
if (tabHasName(tab)) scopes.push({
|
|
110
|
+
named: true,
|
|
111
|
+
tab
|
|
112
|
+
});
|
|
113
|
+
else if (hasFields(tab)) scopes.push({
|
|
114
|
+
named: false,
|
|
115
|
+
fields: tab.fields
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return scopes;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the child `fields` for a single `blocks` element by matching its `blockType`
|
|
122
|
+
* against the field's block definitions.
|
|
123
|
+
*
|
|
124
|
+
* @param field - A Payload `blocks` field carrying inline block definitions.
|
|
125
|
+
* @param item - The data for one block element (expected to carry a `blockType` string).
|
|
126
|
+
* @returns The matched block's `fields`, or `null` when `item` is not a block object or its
|
|
127
|
+
* `blockType` matches no defined block.
|
|
128
|
+
*
|
|
129
|
+
* Assumes the **original, un-sanitized** schema, where blocks are inline `Block` objects on
|
|
130
|
+
* `field.blocks`. Payload's sanitized config may instead carry `blockReferences` resolved
|
|
131
|
+
* against the top-level config — not handled here by design, since this package traverses
|
|
132
|
+
* the original schema to preserve nested `localized` flags.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const fields = resolveBlockFields(blocksField, item);
|
|
137
|
+
* if (fields) descendInto(item, fields); // null → unknown blockType / not a block → skip
|
|
138
|
+
* ```
|
|
139
|
+
* @public
|
|
140
|
+
*/ export function resolveBlockFields(field, item) {
|
|
141
|
+
if (!isBlockItem(item)) return null;
|
|
142
|
+
const block = field.blocks.find((candidate)=>candidate.slug === item.blockType);
|
|
143
|
+
return block ? block.fields : null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Find the element of a parallel data array that corresponds to `refItem` by `id` — not by
|
|
147
|
+
* position.
|
|
148
|
+
*
|
|
149
|
+
* Localized `blocks`/`array` fields are stored independently per locale, so the locales can be
|
|
150
|
+
* reordered or differ entirely; pairing two parallel trees by position then cross-contaminates
|
|
151
|
+
* unrelated elements ("blocks don't line up across locales"). Matching by `id` is correct in both
|
|
152
|
+
* regimes: a non-localized field is one shared row with the same `id` across locales (so id-match
|
|
153
|
+
* equals the old positional match), while independent localized content has diverging ids (no
|
|
154
|
+
* match → the caller falls back to `refItem`'s own values, never another element's). For blocks the
|
|
155
|
+
* `blockType` must also match, so an id collision across types can't graft mismatched fields.
|
|
156
|
+
*
|
|
157
|
+
* Shared by the translation pipeline's two multi-tree walkers — reconcile and collect — to pair
|
|
158
|
+
* their `target` array against the source/reference element currently being visited.
|
|
159
|
+
*
|
|
160
|
+
* @param arr - The array to search (e.g. the target-locale elements).
|
|
161
|
+
* @param refItem - The element being matched, carrying the canonical `id` (and `blockType`).
|
|
162
|
+
* @param isBlocks - Whether the field is a `blocks` field (then `blockType` must also match).
|
|
163
|
+
* @returns The matched element, or `{}` when there is no counterpart (or `refItem` carries no `id`).
|
|
164
|
+
* @public
|
|
165
|
+
*/ export function matchElementById(arr, refItem, isBlocks) {
|
|
166
|
+
const id = refItem.id;
|
|
167
|
+
if (id === undefined || id === null) return {};
|
|
168
|
+
const match = arr.find((candidate)=>isObject(candidate) && candidate.id === id && (!isBlocks || candidate.blockType === refItem.blockType));
|
|
169
|
+
return isObject(match) ? match : {};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//# sourceMappingURL=kernel.js.map
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ArrayField, BlocksField, Field, FieldAffectingData, NamedGroupField, NamedTab, TabAsField, TabsField } from "payload";
|
|
2
|
+
/**
|
|
3
|
+
* A data-affecting field the engine routes to `leaf`: a scalar/relational leaf, never a
|
|
4
|
+
* container (`group`/`array`/`blocks`) and never a `TabAsField`. Narrowing `FieldAffectingData`
|
|
5
|
+
* down to these members is what guarantees `name: string` — the raw union does NOT, because
|
|
6
|
+
* an unnamed `TabAsField` carries `name?: string`.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export type LeafField = Exclude<FieldAffectingData, ArrayField | BlocksField | NamedGroupField | TabAsField>;
|
|
11
|
+
/**
|
|
12
|
+
* Structural classification of a single Payload field — the one place that encodes
|
|
13
|
+
* how Payload field types map onto data boundaries (the dispatch order
|
|
14
|
+
* `tabs → transparent → group → array → blocks → leaf`).
|
|
15
|
+
*
|
|
16
|
+
* Consumed by {@link FieldWalker}/`walkFields` (the exhaustive, data-parallel walks)
|
|
17
|
+
* and by path-navigation; the four hand-rolled traversals collapse onto it so the
|
|
18
|
+
* dispatch lives in exactly one place.
|
|
19
|
+
*
|
|
20
|
+
* - `transparent` — presentational container that does NOT open a data boundary
|
|
21
|
+
* (row, collapsible, unnamed group). Its `fields` live in the SAME data scope as
|
|
22
|
+
* the parent.
|
|
23
|
+
* - `presentational` — a `ui` leaf: no data, no subfields.
|
|
24
|
+
* - `group`/`array`/`blocks` — open a data boundary at `name`.
|
|
25
|
+
* - `leaf` — a data-affecting field with no subfields.
|
|
26
|
+
*
|
|
27
|
+
* @see {@link classifyField} — produces this; {@link walkFields} — consumes it.
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export type FieldStructure = {
|
|
31
|
+
kind: "tabs";
|
|
32
|
+
field: TabsField;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "transparent";
|
|
35
|
+
fields: Field[];
|
|
36
|
+
} | {
|
|
37
|
+
kind: "presentational";
|
|
38
|
+
} | {
|
|
39
|
+
kind: "group";
|
|
40
|
+
name: string;
|
|
41
|
+
fields: Field[];
|
|
42
|
+
field: NamedGroupField;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "array";
|
|
45
|
+
name: string;
|
|
46
|
+
fields: Field[];
|
|
47
|
+
field: ArrayField;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "blocks";
|
|
50
|
+
name: string;
|
|
51
|
+
field: BlocksField;
|
|
52
|
+
} | {
|
|
53
|
+
kind: "leaf";
|
|
54
|
+
name: string;
|
|
55
|
+
field: LeafField;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* A tab flattened by `tabScopes`. A named tab opens a data boundary, so its `NamedTab`
|
|
59
|
+
* is surfaced (the walker passes it to `enterObject`, and `tab.name` is the data key).
|
|
60
|
+
* An unnamed tab flattens into the parent data scope, so only its `fields` are needed.
|
|
61
|
+
*
|
|
62
|
+
* @see {@link tabScopes}
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export type TabScope = {
|
|
66
|
+
named: true;
|
|
67
|
+
tab: NamedTab;
|
|
68
|
+
} | {
|
|
69
|
+
named: false;
|
|
70
|
+
fields: Field[];
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Control signal a walker callback can return instead of descending.
|
|
74
|
+
* - `skip` — do not descend this branch; continue with siblings.
|
|
75
|
+
* - `stop` — halt the whole walk (used for path-style navigation / early-exit).
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
export type WalkSignal = "skip" | "stop";
|
|
80
|
+
/**
|
|
81
|
+
* One child to recurse under an array/blocks boundary. The caller derives these from its own
|
|
82
|
+
* data (it alone knows how many elements there are and their shape) and returns them from
|
|
83
|
+
* {@link FieldWalker.enterList}.
|
|
84
|
+
*
|
|
85
|
+
* @template Cursor - The walker's data-position type (see {@link FieldWalker}).
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
export interface ChildCursor<Cursor extends object> {
|
|
89
|
+
cursor: Cursor;
|
|
90
|
+
/** Child fields to walk under this item. For `blocks`, the resolved `block.fields`. */
|
|
91
|
+
fields: Field[];
|
|
92
|
+
/** Index/key this child sits at; surfaced back to `combine`. */
|
|
93
|
+
key: string | number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The container the engine asks the walker to assemble, bottom-up.
|
|
97
|
+
*
|
|
98
|
+
* - `root` — the top-level field list (`field` is `null`).
|
|
99
|
+
* - `object` — a `group` or a named tab.
|
|
100
|
+
* - `list` — an `array` or `blocks` collection (children are its elements, keyed by index).
|
|
101
|
+
* - `element` — a single element of a list (children are its fields). `field` is the
|
|
102
|
+
* parent list field; distinguish array vs blocks via `field.type`.
|
|
103
|
+
*
|
|
104
|
+
* @see {@link FieldWalker.combine}
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
export type ContainerInfo = {
|
|
108
|
+
kind: "root";
|
|
109
|
+
field: null;
|
|
110
|
+
} | {
|
|
111
|
+
kind: "object";
|
|
112
|
+
field: NamedGroupField | NamedTab;
|
|
113
|
+
key: string;
|
|
114
|
+
} | {
|
|
115
|
+
kind: "list";
|
|
116
|
+
field: ArrayField | BlocksField;
|
|
117
|
+
key: string;
|
|
118
|
+
} | {
|
|
119
|
+
kind: "element";
|
|
120
|
+
field: ArrayField | BlocksField;
|
|
121
|
+
key: string | number;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* A child's assembled output, tagged with the key it sat at within its parent. Passed to
|
|
125
|
+
* {@link FieldWalker.combine} as the `children` list.
|
|
126
|
+
*
|
|
127
|
+
* @template Out - The walker's per-node output type (see {@link FieldWalker}).
|
|
128
|
+
* @public
|
|
129
|
+
*/
|
|
130
|
+
export interface ChildOutput<Out> {
|
|
131
|
+
key: string | number;
|
|
132
|
+
out: Out;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Behaviour a caller plugs into {@link walkFields}. The engine owns structural dispatch and
|
|
136
|
+
* recursion over the **schema**; the caller owns the **data** via the opaque `Cursor`
|
|
137
|
+
* (1..n parallel data trees plus any path live there) and decides what to produce.
|
|
138
|
+
*
|
|
139
|
+
* Shapes this is designed to subsume (one engine, many callers):
|
|
140
|
+
* - build-tree (filter / reconcile) — reconstruct values in `combine`.
|
|
141
|
+
* - collect + mutate (field-chunk collector) — push/mutate in `leaf`, `combine` is a no-op.
|
|
142
|
+
* - navigate + early-exit (path resolver) — `enter*` returns `'skip'`/`'stop'`.
|
|
143
|
+
*
|
|
144
|
+
* @template Cursor - The data position threaded through the walk. The engine never reads it;
|
|
145
|
+
* it only passes it to your callbacks and stores the cursors you return. Constrained to
|
|
146
|
+
* `object` so a cursor can never collide with a {@link WalkSignal} string (`'skip'`/`'stop'`).
|
|
147
|
+
* @template Out - What you produce per leaf and assemble per container. Use `void` for a
|
|
148
|
+
* collect-only walk (accumulate in a closure, return `undefined` from `combine`).
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* A collect-only walker that records the path of every localized leaf:
|
|
152
|
+
* ```ts
|
|
153
|
+
* type Cursor = { data: Record<string, unknown>; path: string[] };
|
|
154
|
+
* const found: string[] = [];
|
|
155
|
+
*
|
|
156
|
+
* const walker: FieldWalker<Cursor, void> = {
|
|
157
|
+
* enterObject: (field, c) =>
|
|
158
|
+
* isRecord(c.data[field.name])
|
|
159
|
+
* ? { data: c.data[field.name], path: [...c.path, field.name] }
|
|
160
|
+
* : "skip",
|
|
161
|
+
* enterList: (field, c) => toItemCursors(field, c),
|
|
162
|
+
* leaf: (field, c) => {
|
|
163
|
+
* if (field.localized) found.push([...c.path, field.name].join("."));
|
|
164
|
+
* return undefined;
|
|
165
|
+
* },
|
|
166
|
+
* combine: () => undefined, // collect-only: nothing to assemble
|
|
167
|
+
* };
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @see {@link walkFields}
|
|
171
|
+
* @public
|
|
172
|
+
*/
|
|
173
|
+
export interface FieldWalker<Cursor extends object, Out> {
|
|
174
|
+
/** Enter a single-object boundary (named `group` or named tab). Return the child cursor, or a signal. */
|
|
175
|
+
enterObject(field: NamedGroupField | NamedTab, cursor: Cursor): Cursor | WalkSignal;
|
|
176
|
+
/** Enter an `array`/`blocks` boundary. Return one {@link ChildCursor} per element, or a signal. */
|
|
177
|
+
enterList(field: ArrayField | BlocksField, cursor: Cursor): ChildCursor<Cursor>[] | WalkSignal;
|
|
178
|
+
/**
|
|
179
|
+
* Visit a data-affecting leaf. `field` is a {@link LeafField} — the engine has already
|
|
180
|
+
* resolved it via the `fieldAffectsData` guard and excluded containers/tabs, so `field.name`
|
|
181
|
+
* is always present and callers never touch the raw `Field` union. Return its output, or
|
|
182
|
+
* `undefined` to drop it.
|
|
183
|
+
*/
|
|
184
|
+
leaf(field: LeafField, cursor: Cursor): Out | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* Assemble a container from its children's outputs (called bottom-up). Build-tree
|
|
187
|
+
* callers reconstruct here; collectors return `undefined`. Return `undefined` to drop
|
|
188
|
+
* the container from its parent (e.g. an empty object after filtering).
|
|
189
|
+
*/
|
|
190
|
+
combine(container: ContainerInfo, children: ChildOutput<Out>[], cursor: Cursor): Out | undefined;
|
|
191
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behaviour a caller plugs into {@link walkFields}. The engine owns structural dispatch and
|
|
3
|
+
* recursion over the **schema**; the caller owns the **data** via the opaque `Cursor`
|
|
4
|
+
* (1..n parallel data trees plus any path live there) and decides what to produce.
|
|
5
|
+
*
|
|
6
|
+
* Shapes this is designed to subsume (one engine, many callers):
|
|
7
|
+
* - build-tree (filter / reconcile) — reconstruct values in `combine`.
|
|
8
|
+
* - collect + mutate (field-chunk collector) — push/mutate in `leaf`, `combine` is a no-op.
|
|
9
|
+
* - navigate + early-exit (path resolver) — `enter*` returns `'skip'`/`'stop'`.
|
|
10
|
+
*
|
|
11
|
+
* @template Cursor - The data position threaded through the walk. The engine never reads it;
|
|
12
|
+
* it only passes it to your callbacks and stores the cursors you return. Constrained to
|
|
13
|
+
* `object` so a cursor can never collide with a {@link WalkSignal} string (`'skip'`/`'stop'`).
|
|
14
|
+
* @template Out - What you produce per leaf and assemble per container. Use `void` for a
|
|
15
|
+
* collect-only walk (accumulate in a closure, return `undefined` from `combine`).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* A collect-only walker that records the path of every localized leaf:
|
|
19
|
+
* ```ts
|
|
20
|
+
* type Cursor = { data: Record<string, unknown>; path: string[] };
|
|
21
|
+
* const found: string[] = [];
|
|
22
|
+
*
|
|
23
|
+
* const walker: FieldWalker<Cursor, void> = {
|
|
24
|
+
* enterObject: (field, c) =>
|
|
25
|
+
* isRecord(c.data[field.name])
|
|
26
|
+
* ? { data: c.data[field.name], path: [...c.path, field.name] }
|
|
27
|
+
* : "skip",
|
|
28
|
+
* enterList: (field, c) => toItemCursors(field, c),
|
|
29
|
+
* leaf: (field, c) => {
|
|
30
|
+
* if (field.localized) found.push([...c.path, field.name].join("."));
|
|
31
|
+
* return undefined;
|
|
32
|
+
* },
|
|
33
|
+
* combine: () => undefined, // collect-only: nothing to assemble
|
|
34
|
+
* };
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @see {@link walkFields}
|
|
38
|
+
* @public
|
|
39
|
+
*/ export { };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=types.js.map
|