| 1 | using AgentForge.Abstractions; |
| 2 | using AgentForge.Plugin.Grouping.Core; |
| 3 | using AgentForge.Plugin.View; |
| 4 | |
| 5 | namespace AgentForge.Plugin.Grouping.Repositories; |
| 6 | |
| 7 | internal static class ForgeViewCatalogGroupingToolbar |
| 8 | { |
| 9 | internal static string Render( |
| 10 | IForgeOrgCatalogGrouping grouping, |
| 11 | ForgeGroupingAxisCatalog<IForgeCatalogGroupingAxis, ForgeCatalogGroupingRepoRow> axes, |
| 12 | ForgeOrgCatalogViewContext context) |
| 13 | { |
| 14 | var rowCount = ResolveRowCount(context); |
| 15 | var parts = new List<string> |
| 16 | { |
| 17 | RenderHeader(grouping, context, rowCount), |
| 18 | }; |
| 19 | |
| 20 | if (context.GroupStack.Count == 0) |
| 21 | { |
| 22 | parts.Add(ForgeHtml.P( |
| 23 | "meta org-catalog-grouping-hint", |
| 24 | ForgeHtml.Text("Add one or more grouping levels to build a tree."))); |
| 25 | } |
| 26 | |
| 27 | for (var levelIndex = 0; levelIndex < rowCount; levelIndex++) |
| 28 | parts.Add(RenderRow(grouping, axes, context, levelIndex, rowCount)); |
| 29 | |
| 30 | if (context.GroupStack.Count > 0) |
| 31 | { |
| 32 | parts.Add(ForgeHtml.P( |
| 33 | "meta org-catalog-grouping-clear", |
| 34 | ForgeHtml.A( |
| 35 | grouping.BuildCatalogUrl(context, groupStack: [], editorRowCount: 1), |
| 36 | "Clear grouping"))); |
| 37 | } |
| 38 | |
| 39 | return ForgeHtml.Div("meta org-catalog-toolbar org-catalog-grouping", ForgeHtml.Fragment([.. parts])); |
| 40 | } |
| 41 | |
| 42 | private static int ResolveRowCount(ForgeOrgCatalogViewContext context) => |
| 43 | context.EditorRowCount > 0 |
| 44 | ? context.EditorRowCount |
| 45 | : ForgeGroupingStackOptions.DefaultEditorRowCount(context.GroupStack.Count); |
| 46 | |
| 47 | private static string RenderHeader( |
| 48 | IForgeOrgCatalogGrouping grouping, |
| 49 | ForgeOrgCatalogViewContext context, |
| 50 | int rowCount) |
| 51 | { |
| 52 | var canAdd = rowCount < ForgeGroupingStackOptions.MaxStackDepth; |
| 53 | var addLink = canAdd |
| 54 | ? ForgeHtml.A( |
| 55 | grouping.BuildCatalogUrl( |
| 56 | context, |
| 57 | groupStack: context.GroupStack, |
| 58 | editorRowCount: rowCount + 1), |
| 59 | "Add level", |
| 60 | "view-group-add") |
| 61 | : ForgeHtml.Span("view-group-add disabled", ForgeHtml.Text("Add level")); |
| 62 | |
| 63 | return ForgeHtml.Div( |
| 64 | "org-catalog-grouping-header", |
| 65 | ForgeHtml.Strong("Grouping"), |
| 66 | addLink); |
| 67 | } |
| 68 | |
| 69 | private static string RenderRow( |
| 70 | IForgeOrgCatalogGrouping grouping, |
| 71 | ForgeGroupingAxisCatalog<IForgeCatalogGroupingAxis, ForgeCatalogGroupingRepoRow> axes, |
| 72 | ForgeOrgCatalogViewContext context, |
| 73 | int levelIndex, |
| 74 | int rowCount) |
| 75 | { |
| 76 | var label = levelIndex == 0 ? "Group by" : "Then by"; |
| 77 | var currentAxis = levelIndex < context.GroupStack.Count ? context.GroupStack[levelIndex] : null; |
| 78 | |
| 79 | return ForgeHtml.Div( |
| 80 | "org-catalog-grouping-row", |
| 81 | ForgeHtml.Span("org-catalog-grouping-label", label), |
| 82 | RenderAxisSelect(grouping, axes, context, levelIndex, rowCount, currentAxis), |
| 83 | RenderMoveControl( |
| 84 | grouping, |
| 85 | context, |
| 86 | levelIndex, |
| 87 | rowCount, |
| 88 | direction: -1, |
| 89 | label: "↑", |
| 90 | title: "Move up", |
| 91 | disabled: levelIndex == 0 || levelIndex >= context.GroupStack.Count), |
| 92 | RenderMoveControl( |
| 93 | grouping, |
| 94 | context, |
| 95 | levelIndex, |
| 96 | rowCount, |
| 97 | direction: 1, |
| 98 | label: "↓", |
| 99 | title: "Move down", |
| 100 | disabled: levelIndex >= context.GroupStack.Count - 1 || context.GroupStack.Count <= 1), |
| 101 | RenderRemoveControl(grouping, context, levelIndex, rowCount)); |
| 102 | } |
| 103 | |
| 104 | private static string RenderAxisSelect( |
| 105 | IForgeOrgCatalogGrouping grouping, |
| 106 | ForgeGroupingAxisCatalog<IForgeCatalogGroupingAxis, ForgeCatalogGroupingRepoRow> axes, |
| 107 | ForgeOrgCatalogViewContext context, |
| 108 | int levelIndex, |
| 109 | int rowCount, |
| 110 | string? currentAxis) |
| 111 | { |
| 112 | var options = new List<string> |
| 113 | { |
| 114 | ForgeHtml.Option( |
| 115 | grouping.BuildCatalogUrl( |
| 116 | context, |
| 117 | groupStack: ClearStackAtLevel(context.GroupStack, levelIndex), |
| 118 | editorRowCount: rowCount), |
| 119 | "— choose axis —", |
| 120 | string.IsNullOrEmpty(currentAxis)), |
| 121 | }; |
| 122 | |
| 123 | foreach (var axis in axes.Axes) |
| 124 | { |
| 125 | options.Add(ForgeHtml.Option( |
| 126 | grouping.BuildCatalogUrl( |
| 127 | context, |
| 128 | groupStack: SetStackAtLevel(axes, context.GroupStack, levelIndex, axis.Id), |
| 129 | editorRowCount: rowCount), |
| 130 | axis.Label, |
| 131 | string.Equals(axis.Id, currentAxis, StringComparison.OrdinalIgnoreCase))); |
| 132 | } |
| 133 | |
| 134 | return ForgeHtml.SelectNav( |
| 135 | $"grouping-axis-{levelIndex}", |
| 136 | "org-catalog-grouping-axis", |
| 137 | levelIndex == 0 ? "Group by" : "Then by", |
| 138 | [.. options]); |
| 139 | } |
| 140 | |
| 141 | private static string RenderMoveControl( |
| 142 | IForgeOrgCatalogGrouping grouping, |
| 143 | ForgeOrgCatalogViewContext context, |
| 144 | int levelIndex, |
| 145 | int rowCount, |
| 146 | int direction, |
| 147 | string label, |
| 148 | string title, |
| 149 | bool disabled) |
| 150 | { |
| 151 | if (disabled) |
| 152 | return ForgeHtml.Span("view-group-move disabled", ForgeHtml.Text(label)); |
| 153 | |
| 154 | var targetIndex = levelIndex + direction; |
| 155 | var stack = SwapStackAt(context.GroupStack, levelIndex, targetIndex); |
| 156 | return ForgeHtml.ARaw( |
| 157 | grouping.BuildCatalogUrl(context, groupStack: stack, editorRowCount: rowCount), |
| 158 | "view-group-move", |
| 159 | ariaLabel: null, |
| 160 | title, |
| 161 | ForgeHtml.Text(label)); |
| 162 | } |
| 163 | |
| 164 | private static string RenderRemoveControl( |
| 165 | IForgeOrgCatalogGrouping grouping, |
| 166 | ForgeOrgCatalogViewContext context, |
| 167 | int levelIndex, |
| 168 | int rowCount) |
| 169 | { |
| 170 | var nextRowCount = Math.Max(1, rowCount - 1); |
| 171 | var stack = levelIndex < context.GroupStack.Count |
| 172 | ? RemoveStackAt(context.GroupStack, levelIndex) |
| 173 | : context.GroupStack; |
| 174 | |
| 175 | return ForgeHtml.ARaw( |
| 176 | grouping.BuildCatalogUrl(context, groupStack: stack, editorRowCount: nextRowCount), |
| 177 | "view-group-remove", |
| 178 | ariaLabel: "Remove level", |
| 179 | title: "Remove level", |
| 180 | ForgeHtml.Text("×")); |
| 181 | } |
| 182 | |
| 183 | private static IReadOnlyList<string> SetStackAtLevel( |
| 184 | ForgeGroupingAxisCatalog<IForgeCatalogGroupingAxis, ForgeCatalogGroupingRepoRow> axes, |
| 185 | IReadOnlyList<string> stack, |
| 186 | int levelIndex, |
| 187 | string axisId) |
| 188 | { |
| 189 | var result = new List<string>(); |
| 190 | for (var i = 0; i < levelIndex; i++) |
| 191 | { |
| 192 | if (i < stack.Count) |
| 193 | result.Add(stack[i]); |
| 194 | } |
| 195 | |
| 196 | result.Add(axisId); |
| 197 | |
| 198 | for (var i = levelIndex + 1; i < stack.Count; i++) |
| 199 | { |
| 200 | var axis = stack[i]; |
| 201 | if (!string.Equals(axis, axisId, StringComparison.OrdinalIgnoreCase)) |
| 202 | result.Add(axis); |
| 203 | } |
| 204 | |
| 205 | return axes.ParseStack(string.Join(',', result)); |
| 206 | } |
| 207 | |
| 208 | private static IReadOnlyList<string> ClearStackAtLevel(IReadOnlyList<string> stack, int levelIndex) |
| 209 | { |
| 210 | if (levelIndex < 0 || levelIndex >= stack.Count) |
| 211 | return stack; |
| 212 | |
| 213 | return stack.Where((_, index) => index != levelIndex).ToList(); |
| 214 | } |
| 215 | |
| 216 | private static IReadOnlyList<string> RemoveStackAt(IReadOnlyList<string> stack, int levelIndex) |
| 217 | { |
| 218 | if (levelIndex < 0 || levelIndex >= stack.Count) |
| 219 | return stack; |
| 220 | |
| 221 | return stack.Where((_, index) => index != levelIndex).ToList(); |
| 222 | } |
| 223 | |
| 224 | private static IReadOnlyList<string> SwapStackAt(IReadOnlyList<string> stack, int leftIndex, int rightIndex) |
| 225 | { |
| 226 | if (leftIndex < 0 || rightIndex < 0 || leftIndex >= stack.Count || rightIndex >= stack.Count) |
| 227 | return stack; |
| 228 | |
| 229 | var list = stack.ToList(); |
| 230 | (list[leftIndex], list[rightIndex]) = (list[rightIndex], list[leftIndex]); |
| 231 | return list; |
| 232 | } |
| 233 | } |
| 234 | |