Skip to content

Commit 565be8d

Browse files
enkodellcmckaragoz
andauthored
MudCsvMapper #69 (#70)
* MudCSVFieldMapper * Fix where Header Names are very close Address, Address2 caused header issue, only allow 1 match per csv header * Replace Exact Csv HeaderField Resolves header replacement where fields are substrings of another field * MudCsvMapper - refactor name * Fixed Api Page Bug * Add Localization, Fix CSS * Indendation Co-authored-by: mckaragoz <[email protected]>
1 parent 439dc71 commit 565be8d

File tree

9 files changed

+505
-82
lines changed

9 files changed

+505
-82
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
@namespace MudExtensions
2+
@inherits MudComponentBase
3+
4+
@using Microsoft.AspNetCore.Components
5+
@using System.ComponentModel.DataAnnotations
6+
@using Microsoft.AspNetCore.Components.Forms
7+
8+
<div class="@Classname" style="@Style">
9+
10+
<style>
11+
.mud-csv-mapper .mud-drop-item {
12+
flex-shrink: 0 !important;
13+
display: inline-block !important;
14+
margin: 0 4px;
15+
}
16+
</style>
17+
18+
19+
<MudStack Style="width: 100%">
20+
<MudFileUpload T="IBrowserFile" Accept=".csv" OnFilesChanged="OnInputFileChanged" Hidden="false" Class="flex-1 d-flex justify-center align-content-center my-2"
21+
InputClass="absolute mud-width-full mud-height-full d-flex justify-center align-content-center overflow-hidden z-20" InputStyle="opacity:0"
22+
@ondragenter="@SetDragClass" @ondragleave="@ClearDragClass" @ondragend="@ClearDragClass">
23+
<ButtonTemplate>
24+
<MudPaper Outlined="true" Class="@("d-flex flex-column justify-center align-content-center mud-background-gray p-2 " + DragClass)">
25+
<MudPaper Elevation="0" class="mud-transparent d-flex justify-center align-content-center">
26+
<svg class="mud-icon-root" viewBox="0 0 24 24" height="75" width="75" style=" height:75px; width:175px;" aria-hidden="true">
27+
<path d="M0 0h24v24H0V0z" fill="none"></path>
28+
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3zm-5.55-8h-2.9v3H8l4 4 4-4h-2.55z"></path>
29+
</svg>
30+
</MudPaper>
31+
<MudPaper Elevation="0" class="d-flex justify-center align-content-center mud-transparent">
32+
<h3>
33+
<MudFab HtmlTag="Button"
34+
Color="Color.Secondary"
35+
Label="@LocalizedStrings.ChooseFile"
36+
Icon="@Icons.Outlined.CloudDownload" Size="Size.Large"
37+
for="@context" />@LocalizedStrings.OrDragAndDrop
38+
</h3>
39+
</MudPaper>
40+
<MudPaper Elevation="0" class="d-flex justify-center align-content-center mud-transparent m2">
41+
@foreach (var file in FileNames)
42+
{
43+
<MudChip Color="Color.Dark" Text="@file" />
44+
}
45+
</MudPaper>
46+
</MudPaper>
47+
</ButtonTemplate>
48+
</MudFileUpload>
49+
</MudStack>
50+
51+
<MudDropContainer T="MudCsvHeader" @ref="DropContainer" Items="@MudCsvHeaders" ItemsSelector="@((item,column) => item.MappedField == column)" ItemDropped="TaskUpdated">
52+
<ChildContent>
53+
@if (MudCsvHeaders.Count > 0)
54+
{
55+
<h2 class="d-flex" style="width:100%">CSV File Headers</h2>
56+
<div class="d-flex flex-column flex-grow-1 my-2">
57+
<MudDropZone T="MudCsvHeader" Identifier="File" DraggingClass="mud-alert-text-info"
58+
ItemDraggingClass="mud-alert-text-info" style="min-height:40px"
59+
Class="rounded-lg border-2 border-dashed mud-border-lines-default pa-2 flex-row">
60+
</MudDropZone>
61+
</div>
62+
}
63+
<div class="d-flex flex-wrap justify-space-between">
64+
@if (MudFieldHeaders.Count == 0)
65+
{
66+
<MudText Typo="Typo.overline" class="--mud-palette-error">@LocalizedStrings.DefineHeaders</MudText>
67+
}
68+
<h2 class="d-flex" style="width:100%">@LocalizedStrings.ExpectedHeaders</h2>
69+
70+
@foreach (var item in MudFieldHeaders)
71+
{
72+
<MudDropZone T="MudCsvHeader" Identifier="@item.Name" DraggingClass="mud-alert-text-info"
73+
CanDrop="@((x) => (item.FieldCount == 0))"
74+
ItemDraggingClass="mud-alert-text-error" Class="rounded-lg border-2 border-dashed mud-border-lines-default pa-2 my-1">
75+
<MudText Typo="Typo.subtitle1">
76+
<b>@item.Name</b>
77+
@if (item.Required)
78+
{
79+
<b>*</b>
80+
}
81+
</MudText>
82+
@if (item.FieldCount == 0)
83+
{
84+
<MudPaper Elevation="0" Class="pa-2 ma-2 d-flex flex-column mud-background-gray rounded-lg">
85+
<MudText Typo="Typo.overline">@LocalizedStrings.DragHere</MudText>
86+
</MudPaper>
87+
}
88+
</MudDropZone>
89+
}
90+
</div>
91+
92+
</ChildContent>
93+
<ItemRenderer>
94+
<MudPaper Elevation="5" Class="pa-4 rounded-lg my-3 flex-shrink-0">@context.Name</MudPaper>
95+
</ItemRenderer>
96+
97+
</MudDropContainer>
98+
99+
<MudButton Class="my-2" Color="Color.Primary" StartIcon="@Icons.Filled.CloudDownload" Variant="Variant.Filled" OnClick="@(() => Upload())" Disabled="!_valid">@LocalizedStrings.Import</MudButton>
100+
</div>
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
using Microsoft.AspNetCore.Components;
2+
using Microsoft.AspNetCore.Components.Forms;
3+
using MudBlazor;
4+
using MudBlazor.Utilities;
5+
using MudExtensions.Utilities;
6+
using System.Text;
7+
using System.Text.RegularExpressions;
8+
9+
namespace MudExtensions
10+
{
11+
//Default fields in your database
12+
public class MudFieldHeader
13+
{
14+
public string Name { get; set; } = "";
15+
public bool Required { get; set; } = false;
16+
public int FieldCount { get; set; } = 0;
17+
18+
public MudFieldHeader(string name)
19+
{
20+
Name = name;
21+
Required = false;
22+
}
23+
24+
public MudFieldHeader(string name, bool required = false)
25+
{
26+
Name = name;
27+
Required = required;
28+
}
29+
}
30+
31+
//Header fields in your CSV File
32+
public class MudCsvHeader
33+
{
34+
public string Name { get; set; } = "";
35+
public string MappedField { get; set; } = "File";
36+
37+
public MudCsvHeader(string name, string mappedField = "File")
38+
{
39+
Name = name;
40+
MappedField = mappedField;
41+
}
42+
}
43+
44+
public partial class MudCsvMapper : MudComponentBase
45+
{
46+
protected string Classname =>
47+
new CssBuilder("mud-csv-mapper")
48+
.AddClass(Class)
49+
.Build();
50+
51+
/// <summary>
52+
/// A class for provide all local strings at once.
53+
/// </summary>
54+
[Parameter]
55+
public CsvMapperLocalizedStrings LocalizedStrings { get; set; } = new();
56+
57+
/// <summary>
58+
/// Choose Table Column Headers
59+
/// </summary>
60+
[Parameter]
61+
public List<MudFieldHeader> MudFieldHeaders { get; set; } = new();
62+
63+
private bool _valid = false;
64+
65+
[Parameter]
66+
public IBrowserFile CsvFile { get; set; } = null;
67+
68+
[Parameter]
69+
public byte[] FileContentByte { get; set; }
70+
71+
//if you want to see what was mapped use this dictionary
72+
[Parameter]
73+
public Dictionary<string, string> CsvMapping { get; set; } = new();
74+
75+
[Parameter]
76+
public EventCallback<bool> OnUpload { get; set; }
77+
78+
private static string DefaultDragClass = "relative rounded-lg border-2 border-dashed pa-4 mt-4 mud-width-full mud-height-full z-10";
79+
private string DragClass = DefaultDragClass;
80+
private MudDropContainer<MudCsvHeader> DropContainer;
81+
private List<string> FileNames = new List<string>();
82+
private string HeaderLine = "";
83+
List<MudCsvHeader> MudCsvHeaders = new();
84+
string FileContentStr;
85+
86+
protected override void OnInitialized()
87+
{
88+
base.OnInitialized();
89+
}
90+
91+
//TODO SearchFunc
92+
/// <summary>
93+
/// The SearchFunc returns a list of items matching the typed text
94+
/// </summary>
95+
//[Parameter]
96+
//public Func<string, string> SearchFunc { get; set; }
97+
98+
private async Task OnInputFileChanged(InputFileChangeEventArgs args)
99+
{
100+
ClearDragClass();
101+
var files = args.GetMultipleFiles();
102+
foreach (var file in files)
103+
{
104+
FileNames.Add(file.Name);
105+
}
106+
if (files.Count > 0)
107+
{
108+
109+
long maxFileSize = 1024 * 1024 * 15;
110+
using var stream = new MemoryStream();
111+
var buffer = new byte[files[0].Size];
112+
113+
using var newFileStream = files[0].OpenReadStream(maxFileSize);
114+
115+
int bytesRead;
116+
double totalRead = 0;
117+
118+
while ((bytesRead = await newFileStream.ReadAsync(buffer)) != 0)
119+
{
120+
totalRead += bytesRead;
121+
await stream.WriteAsync(buffer, 0, bytesRead);
122+
}
123+
124+
FileContentByte = stream.GetBuffer();
125+
var reader = new StreamReader(new MemoryStream(FileContentByte), Encoding.Default);
126+
HeaderLine = reader.ReadLine();
127+
ReadCSVHeaders(HeaderLine);
128+
CsvFile = files[0];
129+
FileContentStr = reader.ReadToEnd();
130+
}
131+
}
132+
133+
public async Task Upload()
134+
{
135+
string NewHeader = HeaderLine;
136+
for (int i = 0; i < MudCsvHeaders.Count; i++)
137+
{
138+
if (MudCsvHeaders[i].MappedField != "File")
139+
{
140+
NewHeader = Regex.Replace(NewHeader, String.Format(@"\b{0}\b", MudCsvHeaders[i].Name), MudCsvHeaders[i].MappedField);
141+
CsvMapping.Add(MudCsvHeaders[i].MappedField, MudCsvHeaders[i].Name);
142+
}
143+
}
144+
145+
FileContentStr = NewHeader + "\r\n" + FileContentStr;
146+
FileContentByte = System.Text.Encoding.UTF8.GetBytes(FileContentStr);
147+
148+
await OnUpload.InvokeAsync();
149+
}
150+
151+
public void ReadCSVHeaders(string input)
152+
{
153+
Regex csvSplit = new Regex("(?:^|,)(\"(?:[^\"]+|\"\")*\"|[^,]*)", RegexOptions.Compiled);
154+
155+
foreach (Match match in csvSplit.Matches(input))
156+
{
157+
string csvField = match.Value.TrimStart(',');
158+
bool matchedField = false;
159+
for (int i = 0; i < MudFieldHeaders.Count; i++)
160+
{
161+
//Do an exact match on the fields first
162+
if (String.Compare(MudFieldHeaders[i].Name, csvField, StringComparison.CurrentCultureIgnoreCase) == 0)
163+
{
164+
if (MudFieldHeaders[i].FieldCount == 0) //only match if it hasn't already been matched
165+
{
166+
MudCsvHeaders.Add(new MudCsvHeader(csvField, MudFieldHeaders[i].Name));
167+
MudFieldHeaders[i].FieldCount++;
168+
matchedField = true;
169+
break;
170+
}
171+
}
172+
173+
//Then do a Fuzzy match if possible. This works best because sometimes you have fields that are substrings of another field
174+
//Todo Create an optional Parent Method for Comparison so someone could use a fuzzy name matcher: https://github.com/JakeBayer/FuzzySharp
175+
//if (FuzzySharp.Fuzz.Ratio(MudFieldHeaders[i].Name.ToLower(), csvField.ToLower()) > 90)
176+
}
177+
178+
if (matchedField) continue;
179+
MudCsvHeaders.Add(new MudCsvHeader(csvField));
180+
}
181+
182+
IsValid();
183+
}
184+
185+
private void SetDragClass()
186+
{
187+
DragClass = $"{DefaultDragClass} mud-border-primary";
188+
}
189+
190+
private void ClearDragClass()
191+
{
192+
DragClass = DefaultDragClass;
193+
}
194+
195+
/* handling board events */
196+
private void TaskUpdated(MudItemDropInfo<MudCsvHeader> mudCSVField)
197+
{
198+
string oldMappedField = mudCSVField.Item.MappedField;
199+
mudCSVField.Item.MappedField = mudCSVField.DropzoneIdentifier;
200+
201+
for (int i = 0; i < MudFieldHeaders.Count; i++)
202+
{
203+
if (MudFieldHeaders[i].Name == oldMappedField)
204+
{
205+
MudFieldHeaders[i].FieldCount--;
206+
}
207+
}
208+
209+
for (int i = 0; i < MudFieldHeaders.Count; i++)
210+
{
211+
if (MudFieldHeaders[i].Name == mudCSVField.DropzoneIdentifier)
212+
{
213+
MudFieldHeaders[i].FieldCount++;
214+
}
215+
}
216+
IsValid();
217+
}
218+
219+
private void IsValid()
220+
{
221+
foreach (MudFieldHeader mudFieldHeader in MudFieldHeaders.Where(i => i.Required))
222+
{
223+
if (!MudCsvHeaders.Where(i => i.MappedField == mudFieldHeader.Name).Any())
224+
{
225+
_valid = false;
226+
return;
227+
}
228+
}
229+
_valid = true;
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)