mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(config-tool): 支持对象数组内嵌对象数组编辑
- 新增对象数组编辑器对数组项内嵌对象数组的递归渲染与保存能力 - 补充嵌套对象数组表单模型与 YAML 写回回归测试 - 更新配置系统文档中的 raw YAML 回退边界说明
This commit is contained in:
parent
d6a154726c
commit
fad391e8cf
@ -6,6 +6,13 @@
|
||||
|
||||
当前阶段不再把 VS Code 工具能力当作阻塞项;工具链只要不拖累 C# 首发可用版本即可。
|
||||
|
||||
## 并行 Lane 约束
|
||||
|
||||
- `C# Runtime + Source Generator + Consumer DX` 仍是当前主线恢复点
|
||||
- Tooling / Docs 作为非阻塞并行 lane 单独推进,但每一批仍要和 Runtime / Generator 的共享关键字边界保持一致
|
||||
- active tracking / trace 只保留恢复点、验证与 lane 指针;复杂编辑器细节、宿主手工验证和文档批次安排统一写在本文件
|
||||
- public docs 只写消费者接入、限制和迁移边界;治理噪音、批次编排和 recovery 元数据继续留在 `ai-plan/**`
|
||||
|
||||
## 当前状态
|
||||
|
||||
- [x] 单表注册辅助:`Register{Entity}Table()`
|
||||
@ -60,6 +67,24 @@
|
||||
- [ ] 继续扩插件的复杂表单能力
|
||||
- 说明:这是可选项,不阻塞 C# 主线
|
||||
|
||||
## Tooling / Docs 并行 Lane
|
||||
|
||||
- [ ] Tooling:让 VS Code 表单支持更深层对象数组嵌套,减少 raw YAML 回退
|
||||
- 边界:不改变 Runtime / Generator 已定义的 schema 形状契约
|
||||
- 验证:优先补 JS 测试,其次再做真实 VS Code 宿主手工验证
|
||||
|
||||
- [ ] Tooling:为复杂结构提供比“顶层标量 / 标量数组”更强的批量编辑能力
|
||||
- 边界:只增强编辑体验,不反向要求 schema 扩展或新的生成类型形状
|
||||
- 验证:记录可观察的编辑路径和回退路径,而不是在 active 入口堆叠 UI 细节
|
||||
|
||||
- [ ] Tooling:在真实 VS Code 宿主中完成对象数组编辑与复杂 schema 的交互式手工验证
|
||||
- 边界:作为发布前增强项,不阻塞共享关键字主线
|
||||
- 验证:后续 batch 直接补记宿主验证结论与未覆盖场景
|
||||
|
||||
- [ ] Docs:在相关接入文档里补齐“工具能力是辅助层,不定义 Runtime 契约”的读者提示
|
||||
- 边界:只写 reader-facing 接入 guidance,不写批次、治理、风险台账
|
||||
- 验证:确认文档用语聚焦接入路径、能力边界和回退方案
|
||||
|
||||
## 暂缓
|
||||
|
||||
- [ ] 不追求完整 JSON Schema 全量支持
|
||||
@ -75,7 +100,7 @@
|
||||
|
||||
1. 用 `GeneratedConfigCatalog` 继续补齐启动与诊断辅助
|
||||
2. 补一条比 `Architecture.OnInitialize()` 更正式的模块化接入建议
|
||||
当前状态:第 1 项和第 2 项已完成,`allOf` 与 object-focused `if` / `then` / `else` 也已补齐;下一步转到下一批仍不改变生成形状的组合关键字评估,或继续推进 VS Code 复杂编辑体验
|
||||
当前状态:第 1 项和第 2 项已完成,`allOf` 与 object-focused `if` / `then` / `else` 也已补齐;下一步默认转到下一批仍不改变生成形状的组合关键字评估。若另开并行 batch,再从本文件的 Tooling / Docs lane 接手
|
||||
|
||||
## 完成标准
|
||||
|
||||
@ -94,6 +119,9 @@
|
||||
- `tools/gframework-config-tool/src/configValidation.js`
|
||||
- `tools/gframework-config-tool/src/extension.js`
|
||||
- `docs/zh-CN/game/config-system.md`
|
||||
- 若恢复的是 Tooling / Docs 并行 lane:
|
||||
- 先回看本文件的 `Tooling / Docs 并行 Lane`
|
||||
- 只把结果摘要回填到 active tracking / trace,避免把编辑器批次细节重新塞回默认入口
|
||||
|
||||
### 恢复块
|
||||
|
||||
@ -111,3 +139,4 @@
|
||||
1. 检查 `YamlConfigSchemaValidator.cs`、`SchemaConfigGenerator.cs`、`configValidation.js` 中当前已支持的关键字列表
|
||||
2. 跳过 `oneOf` / `anyOf`,选择下一批仍不改变生成类型形状的共享关键字
|
||||
3. 优先找不需要属性合并、联合分支生成或额外 UI 形状解释的关键字,而不是先回工具 UI
|
||||
4. 若主线批次暂不动代码,可并行开启 Tooling / Docs lane,但不要让其反向改写主线恢复点定义
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
- 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移
|
||||
- 已完成 PR #262 的 CodeRabbit follow-up,补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests
|
||||
- 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema
|
||||
- 继续把 VS Code 工具能力视为非阻塞项,不让复杂 UI 编辑器需求反过来拖慢 C# 主线
|
||||
- Tooling / Docs 后续改为非阻塞并行 lane;active 入口只保留主线恢复点,把批处理细节下沉到 backlog 文件
|
||||
|
||||
### 已知风险
|
||||
|
||||
@ -26,8 +26,8 @@
|
||||
- 缓解措施:`gframework-pr-review` 现已同时解析 latest review body,并输出 declared / parsed 数量以便快速识别解析缺口
|
||||
- PR follow-up 残留风险:PR `#262` 最新 review thread 仍有少量 open comments,且 nitpick body 解析仍存在 declared / parsed 缺口
|
||||
- 缓解措施:先以 latest unresolved thread 为准逐条本地核验;已确认并补齐运行时诊断路径与 `else without if` 回归测试,skill 现已补齐 `.py` nitpick 与 outside-diff comment 解析,剩余项只需等待本地修复推送后再复抓确认
|
||||
- 非阻塞项回退风险:将 VS Code 功能标为非阻塞但导致主线回退的风险
|
||||
- 缓解措施:C# 主线补齐新关键字时仍需在 `configValidation.js` 与 `extension.js` 中同步落地,只是不让复杂表单控件阻塞发布
|
||||
- 并行 lane 漂移风险:Tooling / Docs 作为并行项后,后续 batch 可能重新把治理说明写回 active 入口或 public docs
|
||||
- 缓解措施:active tracking / trace 只保留恢复点、验证和 lane 指针;reader-facing 文档只写接入信息,治理说明继续留在 `ai-plan/**`
|
||||
|
||||
## 当前状态
|
||||
|
||||
@ -73,9 +73,7 @@
|
||||
|
||||
- 继续扩展“不会改变生成类型形状”的共享关键字支持
|
||||
- 继续降低复杂 schema 与多配置域项目的接入成本
|
||||
- 让 VS Code 表单支持更深层对象数组嵌套,减少 raw YAML 回退
|
||||
- 为复杂结构提供比“顶层标量 / 标量数组”更强的批量编辑能力
|
||||
- 在真实 VS Code 宿主中完成对象数组编辑与复杂 schema 的交互式手工验证
|
||||
- Tooling / Docs 并行 lane 仍需推进复杂表单、交互式宿主验证和后续接入文档,但这些事项不再阻塞当前恢复点
|
||||
|
||||
## 活跃文档
|
||||
|
||||
@ -93,9 +91,11 @@
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"`:通过
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderIfThenElseTests"`:通过(8 tests;新增 `else without if` 运行时回归)
|
||||
- `dotnet build GFramework.sln -c Release`:通过(存在仓库既有 analyzer warning,无新增错误)
|
||||
- `2026-04-30` Tooling lane 收口验证:
|
||||
- `dotnet build GFramework.sln -c Release`:待本轮 ai-plan 收口后补记结果;仅用来确认计划文件改动没有伴随未验证的代码漂移
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 提交并推送当前 PR `#262` follow-up 修复后,重新抓取一次 PR review,确认 outside-diff comment 与 open thread 是否都已收口
|
||||
2. 若 PR review 已收口,再回到 `GFramework.Game/Config/YamlConfigSchemaValidator.cs`、`GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`、`tools/gframework-config-tool/src/configValidation.js` 盘点下一批候选关键字
|
||||
3. 跳过 `oneOf` / `anyOf`,优先筛选下一个仍不改变生成类型形状、且不需要属性合并或联合分支生成的共享关键字
|
||||
1. 主线继续回到 `YamlConfigSchemaValidator.cs`、`SchemaConfigGenerator.cs` 与 `configValidation.js` 的共享关键字盘点,默认跳过 `oneOf` / `anyOf`
|
||||
2. Tooling / Docs 若要并发推进,直接从 `ai-first-config-system-csharp-experience-next.md` 的并行 lane 开新 batch,不再扩写 active 入口
|
||||
3. 保持 active tracking / trace 精简,只记录当前恢复点、最近验证和下一步恢复指针
|
||||
|
||||
@ -135,3 +135,32 @@
|
||||
1. 若本轮定向验证通过,继续盘点下一批真正低风险、且不改变生成类型形状的共享关键字
|
||||
2. 不再重复评估 `oneOf` / `anyOf` 的 object-focused 子集,除非未来主线明确接受联合形状生成
|
||||
3. 若后续关键字需要新诊断编号或文档边界说明,继续保持 Runtime / Generator / Tooling 同步收口
|
||||
|
||||
## 2026-04-30
|
||||
|
||||
### 阶段:Tooling lane 收口整理(AI-FIRST-CONFIG-RP-003)
|
||||
|
||||
- 已把 Tooling / Docs 后续动作从 active 入口的主线叙述中剥离,改成 backlog 文件里的非阻塞并行 lane
|
||||
- 当前 active tracking / trace 只继续承担三件事:
|
||||
- 给 `boot` 提供当前恢复点
|
||||
- 记录最近一次验证或计划性验证占位
|
||||
- 指向真正承载并行批次细节的 backlog 文件
|
||||
- 本轮不新增代码范围、测试范围或文档范围,只整理 public `ai-plan/**` 的恢复入口表达,避免把治理噪音带回 reader-facing docs
|
||||
|
||||
### 关键决定
|
||||
|
||||
- `C# Runtime + Source Generator + Consumer DX` 仍是默认恢复主线
|
||||
- Tooling / Docs 可以并发推进,但后续 batch 应直接以 `ai-first-config-system-csharp-experience-next.md` 为入口,而不是继续扩写 active tracking / trace
|
||||
- public docs 后续只承接接入 guidance、能力边界和回退方式;批次编排、lane 风险和治理说明继续留在 `ai-plan/**`
|
||||
|
||||
### 验证
|
||||
|
||||
- 2026-04-30:`wc -l ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md ai-plan/public/ai-first-config-system/todos/ai-first-config-system-csharp-experience-next.md ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md`
|
||||
- 结果:通过
|
||||
- 备注:确认本轮仍把 active 入口控制在精简范围,并把 lane 细节下沉到 backlog 文件
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 若继续做主线代码批次,直接回到共享关键字盘点,不让 Tooling / Docs 成为阻塞条件
|
||||
2. 若另开 Tooling / Docs batch,先读取 `ai-first-config-system-csharp-experience-next.md` 的并行 lane,再把结果摘要写回 active tracking / trace
|
||||
3. 继续保持 active 入口精简,不在默认恢复文件中追加 UI 细节、治理台账或面向读者的文档草稿
|
||||
|
||||
@ -1005,8 +1005,9 @@ var hotReload = loader.EnableHotReload(
|
||||
- 编辑对象项中的标量字段
|
||||
- 编辑对象项中的标量数组
|
||||
- 编辑对象项中的嵌套对象字段
|
||||
- 编辑对象项内部继续嵌套的对象数组,只要这些内层对象数组项仍然由对象、标量字段、标量数组和嵌套对象组成
|
||||
|
||||
如果对象数组项内部继续包含对象数组,当前仍建议回退到 raw YAML 完成。
|
||||
如果对象数组中混入了标量项,或者更深层结构超出当前 schema 子集,表单入口会明确提示该路径需要回退到 raw YAML。
|
||||
|
||||
当前批量编辑入口仍刻意限制在“同域文件统一改动顶层标量字段和顶层标量数组”,避免复杂结构批量写回时破坏人工维护的 YAML 排版。
|
||||
|
||||
@ -1017,7 +1018,6 @@ var hotReload = loader.EnableHotReload(
|
||||
以下场景目前仍建议保留 raw YAML 编辑,或由项目补充专用工具:
|
||||
|
||||
- 需要更完整的 JSON Schema 支持
|
||||
- 需要在 VS Code 中安全编辑更深层对象数组嵌套
|
||||
- 需要覆盖更复杂的数组结构和更深层 schema 关键字
|
||||
|
||||
## 工具形态建议
|
||||
|
||||
@ -1,6 +1,43 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const vscode = require("vscode");
|
||||
let vscode;
|
||||
try {
|
||||
vscode = require("vscode");
|
||||
} catch {
|
||||
// Tests load pure helpers from this module without the VS Code host.
|
||||
vscode = {
|
||||
env: {
|
||||
language: "en"
|
||||
},
|
||||
EventEmitter: class EventEmitter {
|
||||
constructor() {
|
||||
this.event = () => undefined;
|
||||
}
|
||||
|
||||
fire() {
|
||||
}
|
||||
},
|
||||
TreeItem: class TreeItem {
|
||||
constructor(label, collapsibleState) {
|
||||
this.label = label;
|
||||
this.collapsibleState = collapsibleState;
|
||||
}
|
||||
},
|
||||
TreeItemCollapsibleState: {
|
||||
None: 0,
|
||||
Collapsed: 1,
|
||||
Expanded: 2
|
||||
},
|
||||
Uri: {
|
||||
joinPath() {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
window: {},
|
||||
workspace: {},
|
||||
languages: {}
|
||||
};
|
||||
}
|
||||
const {
|
||||
applyFormUpdates,
|
||||
createSampleConfigYaml,
|
||||
@ -973,7 +1010,7 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
|
||||
}
|
||||
}
|
||||
function renumberObjectArrayItems(editor) {
|
||||
const items = editor.querySelectorAll("[data-object-array-item]");
|
||||
const items = editor.querySelectorAll("[data-object-array-items] > [data-object-array-item]");
|
||||
items.forEach((item, index) => {
|
||||
const title = item.querySelector(".object-array-item-title");
|
||||
if (title) {
|
||||
@ -981,6 +1018,52 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function shouldIncludeNestedControl(control, ownerItem) {
|
||||
return control.closest("[data-object-array-item]") === ownerItem;
|
||||
}
|
||||
function collectObjectArrayEditorItems(editor) {
|
||||
const items = [];
|
||||
for (const item of editor.querySelectorAll("[data-object-array-items] > [data-object-array-item]")) {
|
||||
items.push(collectObjectArrayItemValue(item));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
function collectObjectArrayItemValue(item) {
|
||||
const itemValue = {};
|
||||
|
||||
for (const control of item.querySelectorAll("[data-item-local-path]")) {
|
||||
if (!shouldIncludeNestedControl(control, item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setNestedObjectValue(itemValue, control.dataset.itemLocalPath, control.value);
|
||||
}
|
||||
|
||||
for (const textarea of item.querySelectorAll("textarea[data-item-array-path]")) {
|
||||
if (!shouldIncludeNestedControl(textarea, item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setNestedObjectValue(
|
||||
itemValue,
|
||||
textarea.dataset.itemArrayPath,
|
||||
parseArrayEditorValue(textarea.value));
|
||||
}
|
||||
|
||||
for (const nestedEditor of item.querySelectorAll("[data-item-object-array-path]")) {
|
||||
if (!shouldIncludeNestedControl(nestedEditor, item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setNestedObjectValue(
|
||||
itemValue,
|
||||
nestedEditor.dataset.itemObjectArrayPath,
|
||||
collectObjectArrayEditorItems(nestedEditor));
|
||||
}
|
||||
|
||||
return itemValue;
|
||||
}
|
||||
document.addEventListener("click", (event) => {
|
||||
const schemaButton = event.target.closest("[data-open-ref-schema]");
|
||||
if (schemaButton) {
|
||||
@ -1048,23 +1131,9 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
|
||||
for (const textarea of document.querySelectorAll("textarea[data-comment-path]")) {
|
||||
comments[textarea.dataset.commentPath] = textarea.value;
|
||||
}
|
||||
for (const editor of document.querySelectorAll("[data-object-array-editor]")) {
|
||||
for (const editor of document.querySelectorAll("[data-object-array-editor][data-object-array-path]")) {
|
||||
const path = editor.dataset.objectArrayPath;
|
||||
const items = [];
|
||||
for (const item of editor.querySelectorAll("[data-object-array-items] > [data-object-array-item]")) {
|
||||
const itemValue = {};
|
||||
for (const control of item.querySelectorAll("[data-item-local-path]")) {
|
||||
setNestedObjectValue(itemValue, control.dataset.itemLocalPath, control.value);
|
||||
}
|
||||
for (const textarea of item.querySelectorAll("textarea[data-item-array-path]")) {
|
||||
setNestedObjectValue(
|
||||
itemValue,
|
||||
textarea.dataset.itemArrayPath,
|
||||
parseArrayEditorValue(textarea.value));
|
||||
}
|
||||
items.push(itemValue);
|
||||
}
|
||||
objectArrays[path] = items;
|
||||
objectArrays[path] = collectObjectArrayEditorItems(editor);
|
||||
}
|
||||
vscode.postMessage({ type: "save", scalars, arrays, objectArrays, comments });
|
||||
});
|
||||
@ -1110,8 +1179,11 @@ function renderFormField(field) {
|
||||
title: localizer.t("webview.objectArray.item"),
|
||||
fields: field.templateFields
|
||||
});
|
||||
const pathAttribute = field.itemMode
|
||||
? `data-item-object-array-path="${escapeHtml(field.path)}"`
|
||||
: `data-object-array-path="${escapeHtml(field.path)}"`;
|
||||
return `
|
||||
<div class="object-array depth-${field.depth}" data-object-array-editor data-object-array-path="${escapeHtml(field.path)}">
|
||||
<div class="object-array depth-${field.depth}" data-object-array-editor ${pathAttribute}>
|
||||
<div class="label">${escapeHtml(field.label)} ${field.required ? `<span class="badge">${escapeHtml(localizer.t("webview.badge.required"))}</span>` : ""}</div>
|
||||
<div class="meta-key">${escapeHtml(field.displayPath || field.path)}</div>
|
||||
${renderYamlCommentBlock(field)}
|
||||
@ -1507,6 +1579,41 @@ function collectObjectArrayItemFields(schemaNode, yamlNode, localPath, displayPa
|
||||
continue;
|
||||
}
|
||||
|
||||
if (propertySchema.type === "array" &&
|
||||
propertySchema.items &&
|
||||
propertySchema.items.type === "object") {
|
||||
const templateFields = [];
|
||||
collectObjectArrayItemFields(
|
||||
propertySchema.items,
|
||||
undefined,
|
||||
"",
|
||||
joinArrayTemplatePath(itemDisplayPath),
|
||||
depth + 1,
|
||||
templateFields,
|
||||
unsupported,
|
||||
commentLookup);
|
||||
fields.push({
|
||||
kind: "objectArray",
|
||||
path: itemLocalPath,
|
||||
displayPath: itemDisplayPath,
|
||||
label,
|
||||
required: requiredSet.has(key),
|
||||
depth,
|
||||
schema: propertySchema,
|
||||
itemMode: true,
|
||||
comment: commentLookup[itemDisplayPath] || "",
|
||||
items: buildObjectArrayItemModels(
|
||||
propertySchema.items,
|
||||
propertyValue,
|
||||
itemDisplayPath,
|
||||
depth + 1,
|
||||
unsupported,
|
||||
commentLookup),
|
||||
templateFields
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (["string", "integer", "number", "boolean"].includes(propertySchema.type)) {
|
||||
fields.push({
|
||||
kind: "scalar",
|
||||
@ -2096,5 +2203,8 @@ function parseArrayFieldPayload(arrays) {
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate
|
||||
deactivate,
|
||||
__test: {
|
||||
buildFormModel
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const {__test: extensionTest} = require("../src/extension");
|
||||
const {
|
||||
applyFormUpdates,
|
||||
applyScalarUpdates,
|
||||
@ -2525,6 +2526,123 @@ test("applyFormUpdates should rewrite object-array items from structured form pa
|
||||
assert.match(updated, /^ monsterId: goblin$/mu);
|
||||
});
|
||||
|
||||
test("buildFormModel should expose nested object-array editors inside object-array items", () => {
|
||||
const schema = parseSchemaContent(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"phases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wave": { "type": "integer" },
|
||||
"spawns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"monsterId": { "type": "string" },
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
const yaml = parseTopLevelYaml(`
|
||||
phases:
|
||||
-
|
||||
wave: 1
|
||||
spawns:
|
||||
-
|
||||
monsterId: slime
|
||||
tags:
|
||||
- starter
|
||||
`);
|
||||
|
||||
const formModel = extensionTest.buildFormModel(schema, yaml, {});
|
||||
const phasesField = formModel.fields.find((field) => field.path === "phases");
|
||||
|
||||
assert.ok(phasesField);
|
||||
assert.equal(phasesField.kind, "objectArray");
|
||||
assert.equal(phasesField.items.length, 1);
|
||||
assert.deepEqual(formModel.unsupported, []);
|
||||
|
||||
const nestedSpawnField = phasesField.items[0].fields.find((field) => field.path === "spawns");
|
||||
assert.ok(nestedSpawnField);
|
||||
assert.equal(nestedSpawnField.kind, "objectArray");
|
||||
assert.equal(nestedSpawnField.itemMode, true);
|
||||
assert.equal(nestedSpawnField.items.length, 1);
|
||||
|
||||
const spawnMonsterField = nestedSpawnField.items[0].fields.find((field) => field.path === "monsterId");
|
||||
assert.ok(spawnMonsterField);
|
||||
assert.equal(spawnMonsterField.kind, "scalar");
|
||||
|
||||
const spawnTagsField = nestedSpawnField.items[0].fields.find((field) => field.path === "tags");
|
||||
assert.ok(spawnTagsField);
|
||||
assert.equal(spawnTagsField.kind, "array");
|
||||
});
|
||||
|
||||
test("applyFormUpdates should rewrite nested object arrays from structured form payloads", () => {
|
||||
const updated = applyFormUpdates(
|
||||
[
|
||||
"phases:",
|
||||
" -",
|
||||
" wave: 1"
|
||||
].join("\n"),
|
||||
{
|
||||
objectArrays: {
|
||||
phases: [
|
||||
{
|
||||
wave: "1",
|
||||
spawns: [
|
||||
{
|
||||
monsterId: "slime",
|
||||
tags: ["starter", "melee"],
|
||||
reward: {
|
||||
gold: "10"
|
||||
}
|
||||
},
|
||||
{
|
||||
monsterId: "goblin",
|
||||
conditions: [
|
||||
{
|
||||
type: "night",
|
||||
value: "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
assert.match(updated, /^phases:$/mu);
|
||||
assert.match(updated, /^ -$/mu);
|
||||
assert.match(updated, /^ wave: 1$/mu);
|
||||
assert.match(updated, /^ spawns:$/mu);
|
||||
assert.match(updated, /^ -$/mu);
|
||||
assert.match(updated, /^ monsterId: slime$/mu);
|
||||
assert.match(updated, /^ tags:$/mu);
|
||||
assert.match(updated, /^ - starter$/mu);
|
||||
assert.match(updated, /^ - melee$/mu);
|
||||
assert.match(updated, /^ reward:$/mu);
|
||||
assert.match(updated, /^ gold: 10$/mu);
|
||||
assert.match(updated, /^ monsterId: goblin$/mu);
|
||||
assert.match(updated, /^ conditions:$/mu);
|
||||
assert.match(updated, /^ -$/mu);
|
||||
assert.match(updated, /^ type: night$/mu);
|
||||
assert.match(updated, /^ value: true$/mu);
|
||||
});
|
||||
|
||||
test("applyFormUpdates should clear object arrays when the form removes all items", () => {
|
||||
const updated = applyFormUpdates(
|
||||
[
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user