mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-13 14:14:29 +08:00
feat(config-tool): 支持对象数组内嵌对象数组编辑
- 新增对象数组编辑器对数组项内嵌对象数组的递归渲染与保存能力 - 补充嵌套对象数组表单模型与 YAML 写回回归测试 - 更新配置系统文档中的 raw YAML 回退边界说明
This commit is contained in:
parent
d6a154726c
commit
fad391e8cf
@ -6,6 +6,13 @@
|
|||||||
|
|
||||||
当前阶段不再把 VS Code 工具能力当作阻塞项;工具链只要不拖累 C# 首发可用版本即可。
|
当前阶段不再把 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()`
|
- [x] 单表注册辅助:`Register{Entity}Table()`
|
||||||
@ -60,6 +67,24 @@
|
|||||||
- [ ] 继续扩插件的复杂表单能力
|
- [ ] 继续扩插件的复杂表单能力
|
||||||
- 说明:这是可选项,不阻塞 C# 主线
|
- 说明:这是可选项,不阻塞 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 全量支持
|
- [ ] 不追求完整 JSON Schema 全量支持
|
||||||
@ -75,7 +100,7 @@
|
|||||||
|
|
||||||
1. 用 `GeneratedConfigCatalog` 继续补齐启动与诊断辅助
|
1. 用 `GeneratedConfigCatalog` 继续补齐启动与诊断辅助
|
||||||
2. 补一条比 `Architecture.OnInitialize()` 更正式的模块化接入建议
|
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/configValidation.js`
|
||||||
- `tools/gframework-config-tool/src/extension.js`
|
- `tools/gframework-config-tool/src/extension.js`
|
||||||
- `docs/zh-CN/game/config-system.md`
|
- `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` 中当前已支持的关键字列表
|
1. 检查 `YamlConfigSchemaValidator.cs`、`SchemaConfigGenerator.cs`、`configValidation.js` 中当前已支持的关键字列表
|
||||||
2. 跳过 `oneOf` / `anyOf`,选择下一批仍不改变生成类型形状的共享关键字
|
2. 跳过 `oneOf` / `anyOf`,选择下一批仍不改变生成类型形状的共享关键字
|
||||||
3. 优先找不需要属性合并、联合分支生成或额外 UI 形状解释的关键字,而不是先回工具 UI
|
3. 优先找不需要属性合并、联合分支生成或额外 UI 形状解释的关键字,而不是先回工具 UI
|
||||||
|
4. 若主线批次暂不动代码,可并行开启 Tooling / Docs lane,但不要让其反向改写主线恢复点定义
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
- 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移
|
- 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移
|
||||||
- 已完成 PR #262 的 CodeRabbit follow-up,补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests
|
- 已完成 PR #262 的 CodeRabbit follow-up,补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests
|
||||||
- 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema
|
- 先以 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 数量以便快速识别解析缺口
|
- 缓解措施:`gframework-pr-review` 现已同时解析 latest review body,并输出 declared / parsed 数量以便快速识别解析缺口
|
||||||
- PR follow-up 残留风险:PR `#262` 最新 review thread 仍有少量 open comments,且 nitpick 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 解析,剩余项只需等待本地修复推送后再复抓确认
|
- 缓解措施:先以 latest unresolved thread 为准逐条本地核验;已确认并补齐运行时诊断路径与 `else without if` 回归测试,skill 现已补齐 `.py` nitpick 与 outside-diff comment 解析,剩余项只需等待本地修复推送后再复抓确认
|
||||||
- 非阻塞项回退风险:将 VS Code 功能标为非阻塞但导致主线回退的风险
|
- 并行 lane 漂移风险:Tooling / Docs 作为并行项后,后续 batch 可能重新把治理说明写回 active 入口或 public docs
|
||||||
- 缓解措施:C# 主线补齐新关键字时仍需在 `configValidation.js` 与 `extension.js` 中同步落地,只是不让复杂表单控件阻塞发布
|
- 缓解措施:active tracking / trace 只保留恢复点、验证和 lane 指针;reader-facing 文档只写接入信息,治理说明继续留在 `ai-plan/**`
|
||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
@ -73,9 +73,7 @@
|
|||||||
|
|
||||||
- 继续扩展“不会改变生成类型形状”的共享关键字支持
|
- 继续扩展“不会改变生成类型形状”的共享关键字支持
|
||||||
- 继续降低复杂 schema 与多配置域项目的接入成本
|
- 继续降低复杂 schema 与多配置域项目的接入成本
|
||||||
- 让 VS Code 表单支持更深层对象数组嵌套,减少 raw YAML 回退
|
- Tooling / Docs 并行 lane 仍需推进复杂表单、交互式宿主验证和后续接入文档,但这些事项不再阻塞当前恢复点
|
||||||
- 为复杂结构提供比“顶层标量 / 标量数组”更强的批量编辑能力
|
|
||||||
- 在真实 VS Code 宿主中完成对象数组编辑与复杂 schema 的交互式手工验证
|
|
||||||
|
|
||||||
## 活跃文档
|
## 活跃文档
|
||||||
|
|
||||||
@ -93,9 +91,11 @@
|
|||||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"`:通过
|
- `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 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,无新增错误)
|
- `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 是否都已收口
|
1. 主线继续回到 `YamlConfigSchemaValidator.cs`、`SchemaConfigGenerator.cs` 与 `configValidation.js` 的共享关键字盘点,默认跳过 `oneOf` / `anyOf`
|
||||||
2. 若 PR review 已收口,再回到 `GFramework.Game/Config/YamlConfigSchemaValidator.cs`、`GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`、`tools/gframework-config-tool/src/configValidation.js` 盘点下一批候选关键字
|
2. Tooling / Docs 若要并发推进,直接从 `ai-first-config-system-csharp-experience-next.md` 的并行 lane 开新 batch,不再扩写 active 入口
|
||||||
3. 跳过 `oneOf` / `anyOf`,优先筛选下一个仍不改变生成类型形状、且不需要属性合并或联合分支生成的共享关键字
|
3. 保持 active tracking / trace 精简,只记录当前恢复点、最近验证和下一步恢复指针
|
||||||
|
|||||||
@ -135,3 +135,32 @@
|
|||||||
1. 若本轮定向验证通过,继续盘点下一批真正低风险、且不改变生成类型形状的共享关键字
|
1. 若本轮定向验证通过,继续盘点下一批真正低风险、且不改变生成类型形状的共享关键字
|
||||||
2. 不再重复评估 `oneOf` / `anyOf` 的 object-focused 子集,除非未来主线明确接受联合形状生成
|
2. 不再重复评估 `oneOf` / `anyOf` 的 object-focused 子集,除非未来主线明确接受联合形状生成
|
||||||
3. 若后续关键字需要新诊断编号或文档边界说明,继续保持 Runtime / Generator / Tooling 同步收口
|
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 排版。
|
当前批量编辑入口仍刻意限制在“同域文件统一改动顶层标量字段和顶层标量数组”,避免复杂结构批量写回时破坏人工维护的 YAML 排版。
|
||||||
|
|
||||||
@ -1017,7 +1018,6 @@ var hotReload = loader.EnableHotReload(
|
|||||||
以下场景目前仍建议保留 raw YAML 编辑,或由项目补充专用工具:
|
以下场景目前仍建议保留 raw YAML 编辑,或由项目补充专用工具:
|
||||||
|
|
||||||
- 需要更完整的 JSON Schema 支持
|
- 需要更完整的 JSON Schema 支持
|
||||||
- 需要在 VS Code 中安全编辑更深层对象数组嵌套
|
|
||||||
- 需要覆盖更复杂的数组结构和更深层 schema 关键字
|
- 需要覆盖更复杂的数组结构和更深层 schema 关键字
|
||||||
|
|
||||||
## 工具形态建议
|
## 工具形态建议
|
||||||
|
|||||||
@ -1,6 +1,43 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
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 {
|
const {
|
||||||
applyFormUpdates,
|
applyFormUpdates,
|
||||||
createSampleConfigYaml,
|
createSampleConfigYaml,
|
||||||
@ -973,7 +1010,7 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function renumberObjectArrayItems(editor) {
|
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) => {
|
items.forEach((item, index) => {
|
||||||
const title = item.querySelector(".object-array-item-title");
|
const title = item.querySelector(".object-array-item-title");
|
||||||
if (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) => {
|
document.addEventListener("click", (event) => {
|
||||||
const schemaButton = event.target.closest("[data-open-ref-schema]");
|
const schemaButton = event.target.closest("[data-open-ref-schema]");
|
||||||
if (schemaButton) {
|
if (schemaButton) {
|
||||||
@ -1048,23 +1131,9 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
|
|||||||
for (const textarea of document.querySelectorAll("textarea[data-comment-path]")) {
|
for (const textarea of document.querySelectorAll("textarea[data-comment-path]")) {
|
||||||
comments[textarea.dataset.commentPath] = textarea.value;
|
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 path = editor.dataset.objectArrayPath;
|
||||||
const items = [];
|
objectArrays[path] = collectObjectArrayEditorItems(editor);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
vscode.postMessage({ type: "save", scalars, arrays, objectArrays, comments });
|
vscode.postMessage({ type: "save", scalars, arrays, objectArrays, comments });
|
||||||
});
|
});
|
||||||
@ -1110,8 +1179,11 @@ function renderFormField(field) {
|
|||||||
title: localizer.t("webview.objectArray.item"),
|
title: localizer.t("webview.objectArray.item"),
|
||||||
fields: field.templateFields
|
fields: field.templateFields
|
||||||
});
|
});
|
||||||
|
const pathAttribute = field.itemMode
|
||||||
|
? `data-item-object-array-path="${escapeHtml(field.path)}"`
|
||||||
|
: `data-object-array-path="${escapeHtml(field.path)}"`;
|
||||||
return `
|
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="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>
|
<div class="meta-key">${escapeHtml(field.displayPath || field.path)}</div>
|
||||||
${renderYamlCommentBlock(field)}
|
${renderYamlCommentBlock(field)}
|
||||||
@ -1507,6 +1579,41 @@ function collectObjectArrayItemFields(schemaNode, yamlNode, localPath, displayPa
|
|||||||
continue;
|
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)) {
|
if (["string", "integer", "number", "boolean"].includes(propertySchema.type)) {
|
||||||
fields.push({
|
fields.push({
|
||||||
kind: "scalar",
|
kind: "scalar",
|
||||||
@ -2096,5 +2203,8 @@ function parseArrayFieldPayload(arrays) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
activate,
|
activate,
|
||||||
deactivate
|
deactivate,
|
||||||
|
__test: {
|
||||||
|
buildFormModel
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
const test = require("node:test");
|
const test = require("node:test");
|
||||||
const assert = require("node:assert/strict");
|
const assert = require("node:assert/strict");
|
||||||
|
const {__test: extensionTest} = require("../src/extension");
|
||||||
const {
|
const {
|
||||||
applyFormUpdates,
|
applyFormUpdates,
|
||||||
applyScalarUpdates,
|
applyScalarUpdates,
|
||||||
@ -2525,6 +2526,123 @@ test("applyFormUpdates should rewrite object-array items from structured form pa
|
|||||||
assert.match(updated, /^ monsterId: goblin$/mu);
|
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", () => {
|
test("applyFormUpdates should clear object arrays when the form removes all items", () => {
|
||||||
const updated = applyFormUpdates(
|
const updated = applyFormUpdates(
|
||||||
[
|
[
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user