mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(extension): 添加GFramework配置工具扩展功能
- 实现配置文件浏览器树视图,支持工作区配置目录导航 - 集成轻量级验证系统,支持YAML配置文件语法检查 - 添加模式感知表单预览功能,支持结构化配置编辑 - 实现批量编辑功能,支持跨多个配置文件统一修改字段值 - 集成国际化支持,提供中英文本地化界面 - 添加实时配置文件保存验证,在文件保存时自动校验 - 实现引用导航功能,支持跳转到关联配置表和文件 - 添加工作区变更响应,支持动态刷新配置树视图
This commit is contained in:
parent
4ff5189da4
commit
039ef9817a
@ -1211,6 +1211,170 @@ public class YamlConfigLoaderTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证匹配数量刚好等于 <c>minContains</c> / <c>maxContains</c> 时会被视为合法边界。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task LoadAsync_Should_Accept_Array_When_Contains_Match_Count_Equals_Min_And_Max_Bounds()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
dropRates:
|
||||
- 5
|
||||
- 7
|
||||
- 5
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "dropRates"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"dropRates": {
|
||||
"type": "array",
|
||||
"minContains": 2,
|
||||
"maxContains": 2,
|
||||
"contains": {
|
||||
"type": "integer",
|
||||
"const": 5
|
||||
},
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<int, MonsterConfigIntegerArrayStub>("monster", "monster", "schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
var registry = new ConfigRegistry();
|
||||
|
||||
await loader.LoadAsync(registry);
|
||||
|
||||
var table = registry.GetTable<int, MonsterConfigIntegerArrayStub>("monster");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(table.Count, Is.EqualTo(1));
|
||||
Assert.That(table.Get(1).DropRates, Is.EqualTo(new[] { 5, 7, 5 }));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证数组字段将 <c>contains</c> 声明为非对象 schema 时,会在 schema 解析阶段被拒绝。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Contains_Is_Not_Object_Schema()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
dropRates:
|
||||
- 5
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "dropRates"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"dropRates": {
|
||||
"type": "array",
|
||||
"contains": 5,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<int, MonsterConfigIntegerArrayStub>("monster", "monster", "schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
var registry = new ConfigRegistry();
|
||||
|
||||
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
|
||||
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("dropRates"));
|
||||
Assert.That(exception.Message, Does.Contain("'contains' as an object-valued schema"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证数组字段将 <c>contains</c> 声明为嵌套数组 schema 时,会在 schema 解析阶段被拒绝。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Contains_Uses_Nested_Array_Schema()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
dropRates:
|
||||
- 5
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "dropRates"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"dropRates": {
|
||||
"type": "array",
|
||||
"contains": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<int, MonsterConfigIntegerArrayStub>("monster", "monster", "schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
var registry = new ConfigRegistry();
|
||||
|
||||
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
|
||||
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("dropRates"));
|
||||
Assert.That(exception.Message, Does.Contain("unsupported nested array 'contains' schemas"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证数组在未声明 <c>contains</c> 时不能单独使用 <c>minContains</c>。
|
||||
/// </summary>
|
||||
@ -2345,6 +2509,190 @@ public class YamlConfigLoaderTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证仅声明在 <c>contains</c> 子 schema 里的跨表引用也会参与整批加载校验。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Contains_Matched_Reference_Target_Is_Missing()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"item/potion.yaml",
|
||||
"""
|
||||
id: potion
|
||||
name: Potion
|
||||
""");
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
dropItemIds:
|
||||
- potion
|
||||
- missing_item
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/item.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "dropItemIds"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"dropItemIds": {
|
||||
"type": "array",
|
||||
"minContains": 1,
|
||||
"contains": {
|
||||
"type": "string",
|
||||
"x-gframework-ref-table": "item"
|
||||
},
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
|
||||
static config => config.Id)
|
||||
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
var registry = new ConfigRegistry();
|
||||
|
||||
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.ReferencedKeyNotFound));
|
||||
Assert.That(exception.Diagnostic.TableName, Is.EqualTo("monster"));
|
||||
Assert.That(exception.Diagnostic.ReferencedTableName, Is.EqualTo("item"));
|
||||
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("dropItemIds[1]"));
|
||||
Assert.That(exception.Diagnostic.RawValue, Is.EqualTo("missing_item"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证依赖关系仅来自 <c>contains</c> 子 schema 时,热重载仍会追踪该依赖并在目标表破坏引用后回滚。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task EnableHotReload_Should_Keep_Previous_State_When_Contains_Reference_Dependency_Breaks()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"item/potion.yaml",
|
||||
"""
|
||||
id: potion
|
||||
name: Potion
|
||||
""");
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
dropItemIds:
|
||||
- potion
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/item.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "dropItemIds"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"dropItemIds": {
|
||||
"type": "array",
|
||||
"minContains": 1,
|
||||
"contains": {
|
||||
"type": "string",
|
||||
"x-gframework-ref-table": "item"
|
||||
},
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
|
||||
static config => config.Id)
|
||||
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
var registry = new ConfigRegistry();
|
||||
await loader.LoadAsync(registry);
|
||||
|
||||
var reloadFailureTaskSource =
|
||||
new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions
|
||||
.RunContinuationsAsynchronously);
|
||||
var hotReload = loader.EnableHotReload(
|
||||
registry,
|
||||
onTableReloadFailed: (tableName, exception) =>
|
||||
reloadFailureTaskSource.TrySetResult((tableName, exception)),
|
||||
debounceDelay: TimeSpan.FromMilliseconds(150));
|
||||
|
||||
try
|
||||
{
|
||||
CreateConfigFile(
|
||||
"item/potion.yaml",
|
||||
"""
|
||||
id: elixir
|
||||
name: Elixir
|
||||
""");
|
||||
|
||||
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
|
||||
var diagnosticException = failure.Exception as ConfigLoadException;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(failure.TableName, Is.EqualTo("item"));
|
||||
Assert.That(diagnosticException, Is.Not.Null);
|
||||
Assert.That(diagnosticException!.Diagnostic.FailureKind,
|
||||
Is.EqualTo(ConfigLoadFailureKind.ReferencedKeyNotFound));
|
||||
Assert.That(diagnosticException.Diagnostic.TableName, Is.EqualTo("monster"));
|
||||
Assert.That(diagnosticException.Diagnostic.ReferencedTableName, Is.EqualTo("item"));
|
||||
Assert.That(diagnosticException.Diagnostic.DisplayPath, Is.EqualTo("dropItemIds[0]"));
|
||||
Assert.That(diagnosticException.Diagnostic.RawValue, Is.EqualTo("potion"));
|
||||
Assert.That(registry.GetTable<string, ItemConfigStub>("item").ContainsKey("potion"), Is.True);
|
||||
Assert.That(registry.GetTable<int, MonsterDropArrayConfigStub>("monster").Get(1).DropItemIds,
|
||||
Is.EqualTo(new[] { "potion" }));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
hotReload.UnRegister();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证启用热重载后,配置文件内容变更会刷新已注册配置表。
|
||||
/// </summary>
|
||||
@ -2779,7 +3127,7 @@ public class YamlConfigLoaderTests
|
||||
/// <summary>
|
||||
/// 获取或设置掉落率列表。
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> DropRates { get; set; } = Array.Empty<int>();
|
||||
public List<int> DropRates { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -687,7 +687,7 @@ internal static class YamlConfigSchemaValidator
|
||||
}
|
||||
|
||||
ValidateArrayUniqueItemsConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
||||
ValidateArrayContainsConstraints(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
||||
ValidateArrayContainsConstraints(tableName, yamlPath, displayPath, sequenceNode, schemaNode, references);
|
||||
ValidateConstantValue(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
||||
}
|
||||
|
||||
@ -2153,12 +2153,14 @@ internal static class YamlConfigSchemaValidator
|
||||
/// <param name="displayPath">字段路径。</param>
|
||||
/// <param name="sequenceNode">实际数组节点。</param>
|
||||
/// <param name="schemaNode">数组 schema 节点。</param>
|
||||
/// <param name="references">匹配成功的 <c>contains</c> 子树所声明的跨表引用收集器。</param>
|
||||
private static void ValidateArrayContainsConstraints(
|
||||
string tableName,
|
||||
string yamlPath,
|
||||
string displayPath,
|
||||
YamlSequenceNode sequenceNode,
|
||||
YamlConfigSchemaNode schemaNode)
|
||||
YamlConfigSchemaNode schemaNode,
|
||||
ICollection<YamlConfigReferenceUsage>? references)
|
||||
{
|
||||
var containsConstraints = schemaNode.ArrayConstraints?.ContainsConstraints;
|
||||
if (containsConstraints is null)
|
||||
@ -2171,7 +2173,8 @@ internal static class YamlConfigSchemaValidator
|
||||
yamlPath,
|
||||
displayPath,
|
||||
sequenceNode,
|
||||
containsConstraints.ContainsNode);
|
||||
containsConstraints.ContainsNode,
|
||||
references);
|
||||
var rawValue = matchingCount.ToString(CultureInfo.InvariantCulture);
|
||||
var requiredMinContains = containsConstraints.MinContains ?? 1;
|
||||
if (matchingCount < requiredMinContains)
|
||||
@ -2211,13 +2214,15 @@ internal static class YamlConfigSchemaValidator
|
||||
/// <param name="displayPath">数组字段路径。</param>
|
||||
/// <param name="sequenceNode">实际数组节点。</param>
|
||||
/// <param name="containsNode">contains 子 schema。</param>
|
||||
/// <param name="references">匹配成功元素的可选跨表引用收集器。</param>
|
||||
/// <returns>匹配 <c>contains</c> 子 schema 的元素数量。</returns>
|
||||
private static int CountMatchingContainsItems(
|
||||
string tableName,
|
||||
string yamlPath,
|
||||
string displayPath,
|
||||
YamlSequenceNode sequenceNode,
|
||||
YamlConfigSchemaNode containsNode)
|
||||
YamlConfigSchemaNode containsNode,
|
||||
ICollection<YamlConfigReferenceUsage>? references)
|
||||
{
|
||||
var matchingCount = 0;
|
||||
for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
|
||||
@ -2227,7 +2232,8 @@ internal static class YamlConfigSchemaValidator
|
||||
yamlPath,
|
||||
$"{displayPath}[{itemIndex}]",
|
||||
sequenceNode.Children[itemIndex],
|
||||
containsNode))
|
||||
containsNode,
|
||||
references))
|
||||
{
|
||||
matchingCount++;
|
||||
}
|
||||
@ -2245,17 +2251,33 @@ internal static class YamlConfigSchemaValidator
|
||||
/// <param name="displayPath">当前数组元素路径。</param>
|
||||
/// <param name="itemNode">实际 YAML 元素。</param>
|
||||
/// <param name="containsNode">contains 子 schema。</param>
|
||||
/// <param name="references">当前元素匹配成功后要写回的可选跨表引用收集器。</param>
|
||||
/// <returns>当前元素是否匹配 contains 子 schema。</returns>
|
||||
private static bool IsArrayItemMatchingContains(
|
||||
string tableName,
|
||||
string yamlPath,
|
||||
string displayPath,
|
||||
YamlNode itemNode,
|
||||
YamlConfigSchemaNode containsNode)
|
||||
YamlConfigSchemaNode containsNode,
|
||||
ICollection<YamlConfigReferenceUsage>? references)
|
||||
{
|
||||
// contains 的“试匹配”不能把失败元素的引用泄漏给外层,但匹配成功的元素仍需要参与
|
||||
// 跨表引用收集,否则仅声明在 contains 子 schema 里的 ref-table 会被运行时遗漏。
|
||||
List<YamlConfigReferenceUsage>? matchedReferences = references is null ? null : new();
|
||||
|
||||
try
|
||||
{
|
||||
ValidateNode(tableName, yamlPath, displayPath, itemNode, containsNode, references: null);
|
||||
ValidateNode(tableName, yamlPath, displayPath, itemNode, containsNode, matchedReferences);
|
||||
|
||||
if (references is not null &&
|
||||
matchedReferences is not null)
|
||||
{
|
||||
foreach (var referenceUsage in matchedReferences)
|
||||
{
|
||||
references.Add(referenceUsage);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (ConfigLoadException exception) when (exception.Diagnostic.FailureKind != ConfigLoadFailureKind.UnexpectedFailure)
|
||||
@ -2627,6 +2649,12 @@ internal static class YamlConfigSchemaValidator
|
||||
{
|
||||
CollectReferencedTableNames(node.ItemNode, referencedTableNames);
|
||||
}
|
||||
|
||||
var containsNode = node.ArrayConstraints?.ContainsConstraints?.ContainsNode;
|
||||
if (containsNode is not null)
|
||||
{
|
||||
CollectReferencedTableNames(containsNode, referencedTableNames);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
41
tools/gframework-config-tool/src/containsSummary.js
Normal file
41
tools/gframework-config-tool/src/containsSummary.js
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Build a compact contains-schema summary for array field hints.
|
||||
* The summary reuses existing localized hint strings so Chinese UI surfaces
|
||||
* do not fall back to mixed English tokens such as const/enum/pattern/ref.
|
||||
*
|
||||
* @param {{type?: string, enumValues?: string[], constValue?: string, constDisplayValue?: string, pattern?: string, refTable?: string}} containsSchema Parsed contains schema metadata.
|
||||
* @param {{t: (key: string, params?: Record<string, string | number>) => string}} localizer Runtime localizer.
|
||||
* @returns {string} Human-facing summary.
|
||||
*/
|
||||
function describeContainsSchema(containsSchema, localizer) {
|
||||
const parts = [];
|
||||
if (containsSchema.type) {
|
||||
parts.push(containsSchema.type);
|
||||
}
|
||||
|
||||
if (containsSchema.constValue !== undefined) {
|
||||
parts.push(localizer.t("webview.hint.const", {
|
||||
value: containsSchema.constDisplayValue ?? containsSchema.constValue
|
||||
}));
|
||||
} else if (Array.isArray(containsSchema.enumValues) && containsSchema.enumValues.length > 0) {
|
||||
parts.push(localizer.t("webview.hint.allowed", {
|
||||
values: containsSchema.enumValues.join(", ")
|
||||
}));
|
||||
} else if (containsSchema.pattern) {
|
||||
parts.push(localizer.t("webview.hint.pattern", {
|
||||
value: containsSchema.pattern
|
||||
}));
|
||||
}
|
||||
|
||||
if (containsSchema.refTable) {
|
||||
parts.push(localizer.t("webview.hint.refTable", {
|
||||
refTable: containsSchema.refTable
|
||||
}));
|
||||
}
|
||||
|
||||
return parts.join(", ") || localizer.t("webview.objectArray.item");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
describeContainsSchema
|
||||
};
|
||||
@ -18,6 +18,7 @@ const {
|
||||
joinArrayTemplatePath,
|
||||
joinPropertyPath
|
||||
} = require("./configPath");
|
||||
const {describeContainsSchema} = require("./containsSummary");
|
||||
const {createLocalizer} = require("./localization");
|
||||
|
||||
const localizer = createLocalizer(vscode.env.language);
|
||||
@ -1658,7 +1659,7 @@ function renderFieldHint(propertySchema, isArrayField, includeDescription = true
|
||||
|
||||
if (isArrayField && propertySchema.contains) {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.contains", {
|
||||
summary: describeContainsSchema(propertySchema.contains)
|
||||
summary: describeContainsSchema(propertySchema.contains, localizer)
|
||||
})));
|
||||
}
|
||||
|
||||
@ -1723,35 +1724,6 @@ function renderFieldHint(propertySchema, isArrayField, includeDescription = true
|
||||
return `<span class="hint">${hints.join(" · ")}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a compact contains-schema summary for array field hints.
|
||||
* The hint intentionally stays short so the form preview can expose the rule
|
||||
* without inlining a second full schema tree beside the field controls.
|
||||
*
|
||||
* @param {{type?: string, enumValues?: string[], constValue?: string, constDisplayValue?: string, pattern?: string, refTable?: string}} containsSchema Parsed contains schema metadata.
|
||||
* @returns {string} Human-facing summary.
|
||||
*/
|
||||
function describeContainsSchema(containsSchema) {
|
||||
const parts = [];
|
||||
if (containsSchema.type) {
|
||||
parts.push(containsSchema.type);
|
||||
}
|
||||
|
||||
if (containsSchema.constValue !== undefined) {
|
||||
parts.push(`const = ${containsSchema.constDisplayValue ?? containsSchema.constValue}`);
|
||||
} else if (Array.isArray(containsSchema.enumValues) && containsSchema.enumValues.length > 0) {
|
||||
parts.push(`enum = ${containsSchema.enumValues.join(", ")}`);
|
||||
} else if (containsSchema.pattern) {
|
||||
parts.push(`pattern = ${containsSchema.pattern}`);
|
||||
}
|
||||
|
||||
if (containsSchema.refTable) {
|
||||
parts.push(`ref = ${containsSchema.refTable}`);
|
||||
}
|
||||
|
||||
return parts.join(", ") || "item";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for one batch-edit field value.
|
||||
*
|
||||
|
||||
27
tools/gframework-config-tool/test/containsSummary.test.js
Normal file
27
tools/gframework-config-tool/test/containsSummary.test.js
Normal file
@ -0,0 +1,27 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const {describeContainsSchema} = require("../src/containsSummary");
|
||||
const {createLocalizer} = require("../src/localization");
|
||||
|
||||
test("describeContainsSchema should reuse localized Chinese hint strings", () => {
|
||||
const localizer = createLocalizer("zh-cn");
|
||||
|
||||
const summary = describeContainsSchema(
|
||||
{
|
||||
type: "string",
|
||||
constValue: "\"potion\"",
|
||||
constDisplayValue: "\"potion\"",
|
||||
refTable: "item"
|
||||
},
|
||||
localizer);
|
||||
|
||||
assert.equal(summary, "string, 固定值:\"potion\", 引用表:item");
|
||||
});
|
||||
|
||||
test("describeContainsSchema should fall back to localized item label", () => {
|
||||
const localizer = createLocalizer("en");
|
||||
|
||||
const summary = describeContainsSchema({}, localizer);
|
||||
|
||||
assert.equal(summary, "Item");
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user