Skip to content

Commit 1cb3df2

Browse files
phmatrayclaude
andcommitted
feat(demo): add GitHub repository import UI with progress tracking
Integrate the VirtualFileSystem.GitHub package into the Blazor demo app, allowing users to load GitHub repositories directly from the UI. Features include URL auto-parsing, progress display, cancellation support, and advanced filtering options. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 96974ef commit 1cb3df2

File tree

10 files changed

+578
-6
lines changed

10 files changed

+578
-6
lines changed

src/Atypical.VirtualFileSystem.DemoBlazorApp/Atypical.VirtualFileSystem.DemoBlazorApp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<ProjectReference Include="../Atypical.VirtualFileSystem.Core/Atypical.VirtualFileSystem.Core.csproj" />
11+
<ProjectReference Include="../Atypical.VirtualFileSystem.GitHub/Atypical.VirtualFileSystem.GitHub.csproj" />
1112
</ItemGroup>
1213

1314
<!-- Tailwind CSS Build -->
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
@* GitHub Repository Import Dialog *@
2+
@inject IVirtualFileSystem VFS
3+
@inject GitHubImportService ImportService
4+
@inject VFSStateService StateService
5+
@inject ToastService Toast
6+
@inject NavigationManager Navigation
7+
@implements IDisposable
8+
9+
<Modal @bind-IsOpen="IsOpen" Title="Import from GitHub" Size="lg" CloseOnBackdropClick="@(!ImportService.State.IsImporting)">
10+
<ChildContent>
11+
@if (ImportService.State.IsImporting)
12+
{
13+
@* Loading State *@
14+
<div class="space-y-4">
15+
<div class="flex items-center gap-3">
16+
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-cloud-600"></div>
17+
<span class="text-gray-700">Importing repository...</span>
18+
</div>
19+
20+
@* Progress Bar *@
21+
<div class="space-y-2">
22+
<div class="flex justify-between text-sm text-gray-600">
23+
<span>@ImportService.State.ProgressText</span>
24+
<span>@ImportService.State.ProgressPercent%</span>
25+
</div>
26+
<div class="w-full bg-gray-200 rounded-full h-2">
27+
<div class="bg-cloud-600 h-2 rounded-full transition-all duration-300"
28+
style="width: @(ImportService.State.ProgressPercent)%"></div>
29+
</div>
30+
@if (!string.IsNullOrEmpty(ImportService.State.CurrentPath))
31+
{
32+
<p class="text-xs text-gray-500 truncate" title="@ImportService.State.CurrentPath">
33+
@ImportService.State.CurrentPath
34+
</p>
35+
}
36+
</div>
37+
</div>
38+
}
39+
else if (!string.IsNullOrEmpty(ImportService.State.ErrorMessage))
40+
{
41+
@* Error State *@
42+
<div class="space-y-4">
43+
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
44+
<div class="flex gap-3">
45+
<svg class="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
46+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
47+
</svg>
48+
<div>
49+
<h4 class="text-sm font-medium text-red-800">Import Failed</h4>
50+
<p class="text-sm text-red-700 mt-1">@ImportService.State.ErrorMessage</p>
51+
</div>
52+
</div>
53+
</div>
54+
<Button Variant="secondary" OnClick="ResetForm">Try Again</Button>
55+
</div>
56+
}
57+
else
58+
{
59+
@* Input Form *@
60+
<div class="space-y-4">
61+
@* URL Input *@
62+
<div>
63+
<label class="block text-sm font-medium text-gray-700 mb-1">GitHub URL</label>
64+
<input type="text"
65+
class="input"
66+
placeholder="https://github.com/owner/repository"
67+
@bind="_repositoryUrl"
68+
@bind:event="oninput"
69+
@onblur="ParseUrl" />
70+
<p class="text-xs text-gray-500 mt-1">Paste a GitHub repository URL to auto-fill owner and repository</p>
71+
</div>
72+
73+
<div class="flex items-center gap-4">
74+
<div class="flex-1 h-px bg-gray-200"></div>
75+
<span class="text-xs text-gray-500 uppercase">or enter manually</span>
76+
<div class="flex-1 h-px bg-gray-200"></div>
77+
</div>
78+
79+
@* Owner & Repository *@
80+
<div class="grid grid-cols-2 gap-4">
81+
<div>
82+
<label class="block text-sm font-medium text-gray-700 mb-1">Owner</label>
83+
<input type="text"
84+
class="input"
85+
placeholder="e.g., octocat"
86+
@bind="_owner" />
87+
</div>
88+
<div>
89+
<label class="block text-sm font-medium text-gray-700 mb-1">Repository</label>
90+
<input type="text"
91+
class="input"
92+
placeholder="e.g., Hello-World"
93+
@bind="_repository" />
94+
</div>
95+
</div>
96+
97+
@* Branch *@
98+
<div>
99+
<label class="block text-sm font-medium text-gray-700 mb-1">Branch</label>
100+
<input type="text"
101+
class="input"
102+
placeholder="main (default)"
103+
@bind="_branch" />
104+
</div>
105+
106+
@* SubPath *@
107+
<div>
108+
<label class="block text-sm font-medium text-gray-700 mb-1">Subdirectory (optional)</label>
109+
<input type="text"
110+
class="input"
111+
placeholder="e.g., src/components"
112+
@bind="_subPath" />
113+
<p class="text-xs text-gray-500 mt-1">Load only files from a specific directory</p>
114+
</div>
115+
116+
@* Advanced Options Toggle *@
117+
<div>
118+
<button type="button"
119+
class="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
120+
@onclick="() => _showAdvanced = !_showAdvanced">
121+
<svg class="w-4 h-4 transition-transform @(_showAdvanced ? "rotate-90" : "")"
122+
fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
123+
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
124+
</svg>
125+
Advanced Options
126+
</button>
127+
</div>
128+
129+
@if (_showAdvanced)
130+
{
131+
<div class="space-y-4 pl-4 border-l-2 border-gray-200">
132+
@* Target Path *@
133+
<div>
134+
<label class="block text-sm font-medium text-gray-700 mb-1">Target Path</label>
135+
<input type="text"
136+
class="input"
137+
placeholder="/github/{owner}/{repo}"
138+
@bind="_targetPath" />
139+
<p class="text-xs text-gray-500 mt-1">Where to load the repository in the VFS</p>
140+
</div>
141+
142+
@* Max File Size *@
143+
<div>
144+
<label class="block text-sm font-medium text-gray-700 mb-1">Max File Size (KB)</label>
145+
<input type="number"
146+
class="input"
147+
min="1"
148+
max="10240"
149+
@bind="_maxFileSizeKb" />
150+
<p class="text-xs text-gray-500 mt-1">Files larger than this will be skipped</p>
151+
</div>
152+
153+
@* Include Extensions *@
154+
<div>
155+
<label class="block text-sm font-medium text-gray-700 mb-1">Include Extensions (optional)</label>
156+
<input type="text"
157+
class="input"
158+
placeholder=".cs, .js, .json"
159+
@bind="_includeExtensions" />
160+
<p class="text-xs text-gray-500 mt-1">Comma-separated list of extensions to include</p>
161+
</div>
162+
163+
@* Exclude Binary Files *@
164+
<div class="flex items-center gap-2">
165+
<input type="checkbox"
166+
id="excludeBinary"
167+
class="w-4 h-4 text-cloud-600 rounded border-gray-300"
168+
@bind="_excludeBinaryFiles" />
169+
<label for="excludeBinary" class="text-sm text-gray-700">
170+
Exclude binary files (images, executables, etc.)
171+
</label>
172+
</div>
173+
</div>
174+
}
175+
</div>
176+
}
177+
</ChildContent>
178+
<Footer>
179+
@if (ImportService.State.IsImporting)
180+
{
181+
<Button Variant="danger" OnClick="CancelImport">Cancel</Button>
182+
}
183+
else if (!string.IsNullOrEmpty(ImportService.State.ErrorMessage))
184+
{
185+
<Button Variant="ghost" OnClick="Close">Close</Button>
186+
}
187+
else
188+
{
189+
<Button Variant="ghost" OnClick="Close">Cancel</Button>
190+
<Button Variant="primary"
191+
OnClick="StartImport"
192+
Disabled="@(!CanImport)"
193+
Icon="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5">
194+
Import
195+
</Button>
196+
}
197+
</Footer>
198+
</Modal>
199+
200+
@code {
201+
[Parameter] public bool IsOpen { get; set; }
202+
[Parameter] public EventCallback<bool> IsOpenChanged { get; set; }
203+
[Parameter] public EventCallback<string> OnImportComplete { get; set; }
204+
205+
private string _repositoryUrl = "";
206+
private string _owner = "";
207+
private string _repository = "";
208+
private string _branch = "";
209+
private string _subPath = "";
210+
private string _targetPath = "";
211+
private int _maxFileSizeKb = 1024; // 1 MB
212+
private string _includeExtensions = "";
213+
private bool _excludeBinaryFiles = false;
214+
private bool _showAdvanced = false;
215+
216+
private bool CanImport => !string.IsNullOrWhiteSpace(_owner) && !string.IsNullOrWhiteSpace(_repository);
217+
218+
protected override void OnInitialized()
219+
{
220+
ImportService.OnStateChanged += StateHasChanged;
221+
}
222+
223+
private void ParseUrl()
224+
{
225+
if (string.IsNullOrWhiteSpace(_repositoryUrl))
226+
return;
227+
228+
if (ImportService.TryParseUrl(_repositoryUrl, out var owner, out var repository))
229+
{
230+
_owner = owner;
231+
_repository = repository;
232+
}
233+
}
234+
235+
private async Task StartImport()
236+
{
237+
if (!CanImport) return;
238+
239+
// Parse include extensions
240+
HashSet<string>? includeExtensions = null;
241+
if (!string.IsNullOrWhiteSpace(_includeExtensions))
242+
{
243+
includeExtensions = _includeExtensions
244+
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
245+
.Select(ext => ext.StartsWith('.') ? ext : $".{ext}")
246+
.ToHashSet(StringComparer.OrdinalIgnoreCase);
247+
}
248+
249+
// Start import
250+
var result = await ImportService.ImportRepositoryAsync(
251+
VFS,
252+
_owner,
253+
_repository,
254+
string.IsNullOrWhiteSpace(_branch) ? null : _branch,
255+
string.IsNullOrWhiteSpace(_subPath) ? null : _subPath,
256+
string.IsNullOrWhiteSpace(_targetPath) ? null : _targetPath,
257+
_maxFileSizeKb * 1024,
258+
includeExtensions,
259+
_excludeBinaryFiles
260+
);
261+
262+
if (result != null)
263+
{
264+
// Success
265+
var message = result.HasSkippedFiles
266+
? $"Imported {result.FilesLoaded} files ({result.FilesSkipped} skipped)"
267+
: $"Imported {result.FilesLoaded} files successfully";
268+
269+
Toast.ShowSuccess(message);
270+
271+
// Navigate to the loaded path
272+
StateService.NavigateTo(result.TargetPath);
273+
StateService.NotifyDataChanged();
274+
275+
await OnImportComplete.InvokeAsync(result.TargetPath);
276+
await Close();
277+
}
278+
}
279+
280+
private void CancelImport()
281+
{
282+
ImportService.CancelImport();
283+
}
284+
285+
private void ResetForm()
286+
{
287+
ImportService.State.Reset();
288+
StateHasChanged();
289+
}
290+
291+
private async Task Close()
292+
{
293+
ResetForm();
294+
_repositoryUrl = "";
295+
_owner = "";
296+
_repository = "";
297+
_branch = "";
298+
_subPath = "";
299+
_targetPath = "";
300+
_maxFileSizeKb = 1024;
301+
_includeExtensions = "";
302+
_excludeBinaryFiles = false;
303+
_showAdvanced = false;
304+
await IsOpenChanged.InvokeAsync(false);
305+
}
306+
307+
public void Dispose()
308+
{
309+
ImportService.OnStateChanged -= StateHasChanged;
310+
}
311+
}

src/Atypical.VirtualFileSystem.DemoBlazorApp/Components/Layout/MainLayout.razor

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<div class="h-screen flex flex-col bg-gray-50">
88
@* Top Bar *@
9-
<TopBar OnToggleSidebar="ToggleSidebar" OnToggleHistory="ToggleHistory" />
9+
<TopBar OnToggleSidebar="ToggleSidebar" OnToggleHistory="ToggleHistory" OnToggleGitHubImport="ToggleGitHubImport" />
1010

1111
@* Main Content Area *@
1212
<div class="flex-1 flex overflow-hidden">
@@ -39,6 +39,9 @@
3939
<HistoryDrawer OnClose="() => _showHistory = false" />
4040
}
4141

42+
@* GitHub Import Dialog *@
43+
<GitHubImportDialog @bind-IsOpen="_showGitHubImport" />
44+
4245
@* Mobile Sidebar Overlay *@
4346
@if (_sidebarOpen)
4447
{
@@ -59,6 +62,7 @@
5962
@code {
6063
private bool _sidebarOpen = false;
6164
private bool _showHistory = false;
65+
private bool _showGitHubImport = false;
6266
private DotNetObjectReference<MainLayout>? _jsRef;
6367

6468
protected override void OnInitialized()
@@ -85,6 +89,11 @@
8589
_showHistory = !_showHistory;
8690
}
8791

92+
private void ToggleGitHubImport()
93+
{
94+
_showGitHubImport = !_showGitHubImport;
95+
}
96+
8897
[JSInvokable]
8998
public void HandleUndo()
9099
{

src/Atypical.VirtualFileSystem.DemoBlazorApp/Components/Layout/TopBar.razor

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@
3939

4040
@* Right Actions *@
4141
<div class="flex items-center gap-2">
42+
@* GitHub Import Button *@
43+
<button class="btn-icon hidden md:flex"
44+
@onclick="() => OnToggleGitHubImport.InvokeAsync()"
45+
title="Import from GitHub">
46+
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
47+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5a17.92 17.92 0 0 1-8.716-4.247m0 0A8.966 8.966 0 0 1 3 12c0-1.97.633-3.791 1.708-5.273" />
48+
</svg>
49+
</button>
50+
4251
@* View Toggle *@
4352
<div class="hidden md:flex items-center border border-gray-200 rounded-lg p-0.5">
4453
<button class="@(StateService.ViewMode == ViewMode.Grid ? "bg-gray-100" : "") p-1.5 rounded-md transition-colors"
@@ -94,6 +103,7 @@
94103
@code {
95104
[Parameter] public EventCallback OnToggleSidebar { get; set; }
96105
[Parameter] public EventCallback OnToggleHistory { get; set; }
106+
[Parameter] public EventCallback OnToggleGitHubImport { get; set; }
97107

98108
private string SearchQuery { get; set; } = "";
99109

0 commit comments

Comments
 (0)