diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/JSModelMapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/JSModelMapper.cs index 77fc0f6868..e555fe738b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/JSModelMapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/JSModelMapper.cs @@ -281,6 +281,15 @@ internal static class JSModelMapper item.Subtitle = subtitle; } + // Context items can carry their own nested context menu via "moreCommands". + // The wire omits the field entirely when empty, so only assign when the + // recursive parse yields children and otherwise leave the default. + var moreCommands = ParseContextItems(element, "moreCommands", "MoreCommands", connection); + if (moreCommands.Length > 0) + { + item.MoreCommands = moreCommands; + } + return item; } diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/JSAdapterProxyTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/JSAdapterProxyTests.cs index 0e32bfce29..1cad30c889 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/JSAdapterProxyTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/JSAdapterProxyTests.cs @@ -139,6 +139,55 @@ public class JSAdapterProxyTests Assert.AreEqual("Item B", items[2].Title); } + [TestMethod] + public void ContextItems_ParseNestedMoreCommandsRecursively() + { + using var fake = new JSFakeExtension(); + fake.OnResult("provider/getCommand", """{ "id": "nested-list", "pageType": "listPage", "name": "Nested" }"""); + var itemsJson = + """ + { + "items": [ + { + "title": "Root Item", + "moreCommands": [ + { + "command": { "id": "level1", "name": "Level 1" }, + "title": "Level 1", + "moreCommands": [ + { "command": { "id": "level2", "name": "Level 2" }, "title": "Level 2" } + ] + } + ] + }, + { "title": "Leaf Item" } + ] + } + """; + fake.OnResult("listPage/getItems", itemsJson); + + var provider = CreateProvider(fake); + var page = (IListPage)provider.GetCommand("nested-list")!; + var items = page.GetItems(); + + Assert.AreEqual(2, items.Length); + + // The root item carries a first-level nested command. + var firstLevel = items[0].MoreCommands; + Assert.AreEqual(1, firstLevel.Length); + var firstLevelCommand = (ICommandContextItem)firstLevel[0]; + Assert.AreEqual("Level 1", firstLevelCommand.Title); + + // That first-level command carries its own second-level nested command. + Assert.AreEqual(1, firstLevelCommand.MoreCommands.Length); + var secondLevelCommand = (ICommandContextItem)firstLevelCommand.MoreCommands[0]; + Assert.AreEqual("Level 2", secondLevelCommand.Title); + Assert.AreEqual(0, secondLevelCommand.MoreCommands.Length); + + // The leaf item with no moreCommands yields no children. + Assert.AreEqual(0, items[1].MoreCommands.Length); + } + [TestMethod] public async Task DynamicListPage_ForwardsSearchTextAndRaisesItemsChanged() {