Skip to content

Commit 1f49d83

Browse files
committed
Add GitHubKanbanBoard component and StatusMapper service (#74, #75)
- Add GitHubKanbanBoard component with dynamic columns from project status fields - Add BoardStateService for caching and state management - Add StatusMapper service with status inference rules and custom mappings - Add tab UI to TaskBoard for switching between Local Tasks and GitHub board
1 parent 6063312 commit 1f49d83

7 files changed

Lines changed: 1122 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
@using GexVisor.UI.Models
2+
@using GexVisor.UI.Services
3+
@inject GitHubProjectService ProjectService
4+
@inject GitHubAuthService AuthService
5+
@inject BoardStateService BoardState
6+
@inject IJSRuntime JS
7+
@implements IDisposable
8+
9+
<div class="github-kanban-board">
10+
@if (!AuthService.IsAuthenticated)
11+
{
12+
<div class="board-message">
13+
<i class="fab fa-github"></i>
14+
<p>Connect to GitHub to view your project board</p>
15+
</div>
16+
}
17+
else if (ProjectService.SelectedProject == null)
18+
{
19+
<div class="board-message">
20+
<i class="fas fa-project-diagram"></i>
21+
<p>Select a GitHub project to view its board</p>
22+
</div>
23+
}
24+
else
25+
{
26+
<div class="board-header">
27+
<div class="board-info">
28+
<h3>@ProjectService.SelectedProject.Title</h3>
29+
<span class="item-count">@_stats.TotalItems items</span>
30+
@if (_stats.LastRefresh.HasValue)
31+
{
32+
<span class="last-refresh" title="Last refreshed">
33+
<i class="fas fa-clock"></i> @FormatLastRefresh(_stats.LastRefresh.Value)
34+
</span>
35+
}
36+
</div>
37+
<div class="board-actions">
38+
<button class="btn-refresh" @onclick="RefreshBoard" disabled="@BoardState.IsLoading">
39+
@if (BoardState.IsLoading)
40+
{
41+
<span class="spinner-small"></span>
42+
}
43+
else
44+
{
45+
<i class="fas fa-sync-alt"></i>
46+
}
47+
Refresh
48+
</button>
49+
<a href="@ProjectService.SelectedProject.Url" target="_blank" class="btn-github-link">
50+
<i class="fab fa-github"></i> Open in GitHub
51+
</a>
52+
</div>
53+
</div>
54+
55+
@if (!string.IsNullOrEmpty(BoardState.LastError))
56+
{
57+
<div class="board-error">
58+
<i class="fas fa-exclamation-triangle"></i>
59+
<span>@BoardState.LastError</span>
60+
<button @onclick="RefreshBoard">Retry</button>
61+
</div>
62+
}
63+
64+
@if (BoardState.IsLoading && _items.Count == 0)
65+
{
66+
<div class="board-loading">
67+
<div class="spinner"></div>
68+
<p>Loading board...</p>
69+
</div>
70+
}
71+
else
72+
{
73+
<div class="kanban-columns" style="--column-count: @_columnData.Count">
74+
@foreach (var column in _columnData)
75+
{
76+
<div class="kanban-column">
77+
<div class="column-header">
78+
<span class="column-title">@column.Key</span>
79+
<span class="column-count">@column.Value.Count</span>
80+
</div>
81+
<div class="column-content">
82+
@foreach (var item in column.Value)
83+
{
84+
<div class="github-card @GetStateClass(item.State)" @onclick="@(() => OpenItem(item))">
85+
<div class="card-header">
86+
<span class="state-indicator" title="@item.State"></span>
87+
<span class="card-title">@item.Title</span>
88+
</div>
89+
@if (item.Labels.Count > 0)
90+
{
91+
<div class="card-labels">
92+
@foreach (var label in item.Labels.Take(4))
93+
{
94+
<span class="label">@label</span>
95+
}
96+
@if (item.Labels.Count > 4)
97+
{
98+
<span class="label more">+@(item.Labels.Count - 4)</span>
99+
}
100+
</div>
101+
}
102+
</div>
103+
}
104+
@if (column.Value.Count == 0)
105+
{
106+
<div class="empty-column">
107+
<i class="fas fa-inbox"></i>
108+
<span>No items</span>
109+
</div>
110+
}
111+
</div>
112+
</div>
113+
}
114+
</div>
115+
}
116+
}
117+
</div>
118+
119+
@code {
120+
private List<GitHubProjectItem> _items = new();
121+
private Dictionary<string, List<GitHubProjectItem>> _columnData = new();
122+
private BoardStats _stats = new();
123+
124+
protected override async Task OnInitializedAsync()
125+
{
126+
AuthService.OnAuthStateChanged += OnAuthChanged;
127+
ProjectService.OnProjectsChanged += OnProjectChanged;
128+
BoardState.OnBoardStateChanged += OnBoardStateChanged;
129+
130+
if (AuthService.IsAuthenticated && ProjectService.SelectedProject != null)
131+
{
132+
await LoadBoard();
133+
}
134+
}
135+
136+
private async void OnAuthChanged()
137+
{
138+
await InvokeAsync(async () =>
139+
{
140+
if (AuthService.IsAuthenticated && ProjectService.SelectedProject != null)
141+
{
142+
await LoadBoard();
143+
}
144+
StateHasChanged();
145+
});
146+
}
147+
148+
private async void OnProjectChanged()
149+
{
150+
await InvokeAsync(async () =>
151+
{
152+
if (ProjectService.SelectedProject != null)
153+
{
154+
await LoadBoard();
155+
}
156+
else
157+
{
158+
_items.Clear();
159+
_columnData.Clear();
160+
_stats = new();
161+
}
162+
StateHasChanged();
163+
});
164+
}
165+
166+
private void OnBoardStateChanged()
167+
{
168+
InvokeAsync(StateHasChanged);
169+
}
170+
171+
private async Task LoadBoard()
172+
{
173+
_items = await BoardState.GetItemsAsync();
174+
_columnData = BoardState.GetItemsByStatus(_items);
175+
_stats = BoardState.GetStats(_items);
176+
}
177+
178+
private async Task RefreshBoard()
179+
{
180+
_items = await BoardState.RefreshItemsAsync();
181+
_columnData = BoardState.GetItemsByStatus(_items);
182+
_stats = BoardState.GetStats(_items);
183+
}
184+
185+
private async Task OpenItem(GitHubProjectItem item)
186+
{
187+
if (!string.IsNullOrEmpty(item.Url))
188+
{
189+
await JS.InvokeVoidAsync("open", item.Url, "_blank");
190+
}
191+
}
192+
193+
private string GetStateClass(string? state)
194+
{
195+
return state?.ToUpper() switch
196+
{
197+
"OPEN" => "state-open",
198+
"CLOSED" => "state-closed",
199+
_ => ""
200+
};
201+
}
202+
203+
private string FormatLastRefresh(DateTime timestamp)
204+
{
205+
var ago = DateTime.UtcNow - timestamp;
206+
if (ago.TotalSeconds < 60) return "just now";
207+
if (ago.TotalMinutes < 60) return $"{(int)ago.TotalMinutes}m ago";
208+
return timestamp.ToLocalTime().ToString("h:mm tt");
209+
}
210+
211+
public void Dispose()
212+
{
213+
AuthService.OnAuthStateChanged -= OnAuthChanged;
214+
ProjectService.OnProjectsChanged -= OnProjectChanged;
215+
BoardState.OnBoardStateChanged -= OnBoardStateChanged;
216+
}
217+
}

0 commit comments

Comments
 (0)