Skip to content

Commit 779bba2

Browse files
committed
Add Import Files dialog to project page
1 parent bccda52 commit 779bba2

File tree

3 files changed

+293
-3
lines changed

3 files changed

+293
-3
lines changed

cloud/src/LrmCloud.Web/Components/ExportProjectDialog.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@
8989

9090
try
9191
{
92-
// Build export URL (relative - HttpClient handles base URL and auth)
93-
var url = $"api/projects/{ProjectId}/files/export?format={_selectedFormat}";
92+
// Build export URL (relative to HttpClient's base URL which is /api/)
93+
var url = $"projects/{ProjectId}/files/export?format={_selectedFormat}";
9494

9595
// Add selected languages if any
9696
if (_selectedLanguages.Any())
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
@using LrmCloud.Shared.DTOs.Files
2+
@using LrmCloud.Shared.DTOs.Sync
3+
@inject HttpClient Http
4+
@inject NotificationService NotificationService
5+
@inject Radzen.DialogService DialogService
6+
7+
<RadzenStack Gap="1rem">
8+
<RadzenText TextStyle="TextStyle.Body1" class="rz-color-secondary">
9+
Import localization files into this project. Existing keys will be updated, new keys will be added.
10+
</RadzenText>
11+
12+
<RadzenFormField Text="Format" Variant="Radzen.Variant.Outlined" Style="width: 100%;">
13+
<ChildContent>
14+
<RadzenDropDown @bind-Value="_selectedFormat" Style="width: 100%;"
15+
Data="@_formatOptions" TextProperty="Text" ValueProperty="Value"
16+
Disabled="@_isImporting" />
17+
</ChildContent>
18+
<Helper>
19+
<RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary">
20+
Format of the files you're uploading
21+
</RadzenText>
22+
</Helper>
23+
</RadzenFormField>
24+
25+
<RadzenCard Style="border: 2px dashed var(--rz-base-400); padding: 2rem; text-align: center; position: relative;">
26+
<InputFile OnChange="OnFilesChanged" multiple
27+
accept=".resx,.json,.xml,.strings,.stringsdict,.po,.pot,.xlf,.xliff"
28+
disabled="@_isImporting"
29+
style="position: absolute; top: 0; left: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer;" />
30+
<RadzenStack AlignItems="Radzen.AlignItems.Center" Gap="0.5rem">
31+
<RadzenIcon Icon="cloud_upload" Style="font-size: 3rem; color: var(--rz-secondary);" />
32+
<RadzenText TextStyle="TextStyle.Body1">Drop files here or click to browse</RadzenText>
33+
<RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary">
34+
Supported: .resx, .json, .xml, .strings, .po, .xlf (max 50 files)
35+
</RadzenText>
36+
</RadzenStack>
37+
</RadzenCard>
38+
39+
@if (_uploadedFiles.Any())
40+
{
41+
<RadzenText TextStyle="TextStyle.Subtitle2">Selected Files (@_uploadedFiles.Count)</RadzenText>
42+
<div style="max-height: 200px; overflow-y: auto;">
43+
<RadzenDataGrid TItem="IBrowserFile" Data="@_uploadedFiles" Density="Density.Compact" AllowSorting="false">
44+
<Columns>
45+
<RadzenDataGridColumn TItem="IBrowserFile" Title="File">
46+
<Template Context="file">
47+
<RadzenStack Orientation="Radzen.Orientation.Horizontal" AlignItems="Radzen.AlignItems.Center" Gap="0.5rem">
48+
<RadzenIcon Icon="@GetFileIcon(file.Name)" />
49+
<RadzenText TextStyle="TextStyle.Body2">@file.Name</RadzenText>
50+
</RadzenStack>
51+
</Template>
52+
</RadzenDataGridColumn>
53+
<RadzenDataGridColumn TItem="IBrowserFile" Title="Size" Width="80px">
54+
<Template Context="file">
55+
<RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary">@FormatFileSize(file.Size)</RadzenText>
56+
</Template>
57+
</RadzenDataGridColumn>
58+
<RadzenDataGridColumn TItem="IBrowserFile" Width="40px">
59+
<Template Context="file">
60+
<RadzenButton Icon="close" Variant="Radzen.Variant.Text" Size="ButtonSize.Small"
61+
Click="@(() => RemoveFile(file))" Disabled="@_isImporting" />
62+
</Template>
63+
</RadzenDataGridColumn>
64+
</Columns>
65+
</RadzenDataGrid>
66+
</div>
67+
}
68+
69+
@if (_importResult != null)
70+
{
71+
<RadzenAlert AlertStyle="@(_importResult.Success ? AlertStyle.Success : AlertStyle.Warning)"
72+
Shade="Shade.Lighter" AllowClose="false" Size="AlertSize.Small">
73+
<RadzenStack Gap="0.25rem">
74+
<RadzenText TextStyle="TextStyle.Body2">
75+
<strong>@(_importResult.Success ? "Import completed!" : "Import completed with warnings")</strong>
76+
</RadzenText>
77+
<RadzenText TextStyle="TextStyle.Caption">
78+
Applied: @_importResult.Applied entries
79+
</RadzenText>
80+
@if (_importResult.Errors.Any())
81+
{
82+
<ul style="margin: 0.5rem 0 0 0; padding-left: 1.5rem; font-size: 0.875rem;">
83+
@foreach (var error in _importResult.Errors.Take(5))
84+
{
85+
<li>@error</li>
86+
}
87+
@if (_importResult.Errors.Count > 5)
88+
{
89+
<li>...and @(_importResult.Errors.Count - 5) more</li>
90+
}
91+
</ul>
92+
}
93+
</RadzenStack>
94+
</RadzenAlert>
95+
}
96+
97+
@if (!string.IsNullOrEmpty(_errorMessage))
98+
{
99+
<RadzenAlert AlertStyle="AlertStyle.Danger" Shade="Shade.Lighter" AllowClose="false" Size="AlertSize.Small">
100+
@_errorMessage
101+
</RadzenAlert>
102+
}
103+
104+
<RadzenStack Orientation="Radzen.Orientation.Horizontal" JustifyContent="JustifyContent.End" Gap="0.5rem">
105+
<RadzenButton Variant="Radzen.Variant.Text" Text="@(_importResult != null ? "Close" : "Cancel")"
106+
Click="@Close" Disabled="@_isImporting" />
107+
@if (_importResult == null)
108+
{
109+
<RadzenButton ButtonStyle="ButtonStyle.Primary" Text="@(_isImporting ? "Importing..." : "Import")"
110+
Icon="upload" Click="@HandleImport"
111+
Disabled="@(_isImporting || !_uploadedFiles.Any())" IsBusy="@_isImporting" />
112+
}
113+
</RadzenStack>
114+
</RadzenStack>
115+
116+
@code {
117+
[Parameter]
118+
public int ProjectId { get; set; }
119+
120+
private string _selectedFormat = "resx";
121+
private List<IBrowserFile> _uploadedFiles = new();
122+
private bool _isImporting;
123+
private string? _errorMessage;
124+
private FileImportResponse? _importResult;
125+
126+
private readonly List<object> _formatOptions = new()
127+
{
128+
new { Value = "resx", Text = ".resx (C#/.NET)" },
129+
new { Value = "json", Text = "JSON Localization" },
130+
new { Value = "i18next", Text = "i18next (JS/React)" },
131+
new { Value = "android", Text = "Android strings.xml" },
132+
new { Value = "ios", Text = "iOS .strings/.stringsdict" },
133+
new { Value = "po", Text = "GNU gettext (.po/.pot)" },
134+
new { Value = "xliff", Text = "XLIFF (.xliff/.xlf)" }
135+
};
136+
137+
private void Close()
138+
{
139+
DialogService.Close(_importResult != null);
140+
}
141+
142+
private void OnFilesChanged(InputFileChangeEventArgs e)
143+
{
144+
_uploadedFiles = e.GetMultipleFiles(50).ToList();
145+
_errorMessage = null;
146+
_importResult = null;
147+
AutoDetectFormat();
148+
StateHasChanged();
149+
}
150+
151+
private void RemoveFile(IBrowserFile file)
152+
{
153+
_uploadedFiles.Remove(file);
154+
if (_uploadedFiles.Any())
155+
AutoDetectFormat();
156+
}
157+
158+
private void AutoDetectFormat()
159+
{
160+
if (!_uploadedFiles.Any()) return;
161+
162+
var firstFile = _uploadedFiles.First().Name.ToLowerInvariant();
163+
164+
if (firstFile.EndsWith(".resx"))
165+
_selectedFormat = "resx";
166+
else if (firstFile.EndsWith(".json"))
167+
_selectedFormat = "json";
168+
else if (firstFile.EndsWith(".xml"))
169+
_selectedFormat = "android";
170+
else if (firstFile.EndsWith(".strings") || firstFile.EndsWith(".stringsdict"))
171+
_selectedFormat = "ios";
172+
else if (firstFile.EndsWith(".po") || firstFile.EndsWith(".pot"))
173+
_selectedFormat = "po";
174+
else if (firstFile.EndsWith(".xlf") || firstFile.EndsWith(".xliff"))
175+
_selectedFormat = "xliff";
176+
}
177+
178+
private async Task HandleImport()
179+
{
180+
if (!_uploadedFiles.Any()) return;
181+
182+
_isImporting = true;
183+
_errorMessage = null;
184+
_importResult = null;
185+
186+
try
187+
{
188+
// Read all files
189+
var files = new List<FileDto>();
190+
foreach (var file in _uploadedFiles)
191+
{
192+
try
193+
{
194+
await using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
195+
using var reader = new StreamReader(stream);
196+
var content = await reader.ReadToEndAsync();
197+
198+
files.Add(new FileDto
199+
{
200+
Path = file.Name,
201+
Content = content
202+
});
203+
}
204+
catch (Exception ex)
205+
{
206+
_errorMessage = $"Failed to read {file.Name}: {ex.Message}";
207+
return;
208+
}
209+
}
210+
211+
// Call import API
212+
var request = new FileImportRequest
213+
{
214+
Format = _selectedFormat,
215+
Files = files
216+
};
217+
218+
var response = await Http.PostAsJsonAsync($"projects/{ProjectId}/files/import", request);
219+
220+
if (response.IsSuccessStatusCode)
221+
{
222+
var result = await response.Content.ReadFromJsonAsync<LrmCloud.Shared.Api.ApiResponse<FileImportResponse>>();
223+
_importResult = result?.Data;
224+
225+
if (_importResult?.Success == true)
226+
{
227+
NotificationService.Notify(NotificationSeverity.Success, "Import Complete",
228+
$"Imported {_importResult.Applied} entries");
229+
}
230+
}
231+
else
232+
{
233+
_errorMessage = $"Import failed: {response.StatusCode}";
234+
}
235+
}
236+
catch (Exception ex)
237+
{
238+
_errorMessage = $"Import failed: {ex.Message}";
239+
}
240+
finally
241+
{
242+
_isImporting = false;
243+
}
244+
}
245+
246+
private static string GetFileIcon(string fileName)
247+
{
248+
var lower = fileName.ToLowerInvariant();
249+
if (lower.EndsWith(".resx")) return "code";
250+
if (lower.EndsWith(".json")) return "data_object";
251+
if (lower.EndsWith(".xml")) return "android";
252+
if (lower.EndsWith(".strings") || lower.EndsWith(".stringsdict")) return "phone_iphone";
253+
if (lower.EndsWith(".po") || lower.EndsWith(".pot")) return "translate";
254+
if (lower.EndsWith(".xlf") || lower.EndsWith(".xliff")) return "swap_horiz";
255+
return "description";
256+
}
257+
258+
private static string FormatFileSize(long bytes)
259+
{
260+
if (bytes < 1024) return $"{bytes} B";
261+
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
262+
return $"{bytes / (1024.0 * 1024):F1} MB";
263+
}
264+
}

cloud/src/LrmCloud.Web/Pages/Projects/Detail.razor

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ else
4949
</RadzenStack>
5050

5151
<RadzenStack Orientation="Radzen.Orientation.Horizontal" Gap="0.5rem">
52+
<RadzenButton Variant="Radzen.Variant.Outlined" Icon="upload"
53+
Text="Import" Click="@OpenImportDialog" />
5254
<RadzenButton Variant="Radzen.Variant.Outlined" Icon="download"
5355
Text="Export" Click="@OpenExportDialog" />
5456
<RadzenButton Variant="Radzen.Variant.Outlined" ButtonStyle="ButtonStyle.Secondary" Icon="settings"
@@ -201,7 +203,8 @@ else
201203
private readonly List<ContextMenuItem> _actionMenuItems = new()
202204
{
203205
new ContextMenuItem { Text = "Translate Missing", Value = "translate", Icon = "translate" },
204-
new ContextMenuItem { Text = "Export", Value = "export", Icon = "file_download" }
206+
new ContextMenuItem { Text = "Import Files", Value = "import", Icon = "upload" },
207+
new ContextMenuItem { Text = "Export Files", Value = "export", Icon = "download" }
205208
};
206209

207210
private void OnActionMenuClick(MenuItemEventArgs args)
@@ -212,6 +215,9 @@ else
212215
case "translate":
213216
InvokeAsync(OpenTranslateDialog);
214217
break;
218+
case "import":
219+
InvokeAsync(OpenImportDialog);
220+
break;
215221
case "export":
216222
InvokeAsync(ExportProject);
217223
break;
@@ -324,6 +330,26 @@ else
324330
await OpenExportDialog();
325331
}
326332

333+
// =========================================================================
334+
// Import
335+
// =========================================================================
336+
337+
private async Task OpenImportDialog()
338+
{
339+
var result = await DialogService.OpenAsync<ImportFilesDialog>("Import Files",
340+
new Dictionary<string, object>
341+
{
342+
{ "ProjectId", ProjectId }
343+
},
344+
new Radzen.DialogOptions { Width = "500px" });
345+
346+
// Refresh data if import was successful
347+
if (result is true)
348+
{
349+
await LoadDataAsync();
350+
}
351+
}
352+
327353
private async Task OpenExportDialog()
328354
{
329355
if (_project == null || _stats == null)

0 commit comments

Comments
 (0)