Forge
csharp3407750f
1using AgentForge.Abstractions;
2using AgentForge.Plugin.Grouping.Core;
3using AgentForge.Plugin.View;
4
5namespace AgentForge.Plugin.Grouping.Repositories;
6
7internal 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
View only · write via MCP/CIDE