@* Bulk equipment import modal wired straight into IUnsTreeService. Paste a CSV (header + rows), the modal parses it into EquipmentInput rows and calls ImportEquipmentAsync, then shows the Inserted / Skipped / Errors summary in place. The host owns visibility; "Close" raises OnImported so the page can reload the whole tree (an import can add equipment across many lines/clusters). Required header columns (in order): Name, MachineCode, UnsLineId, DriverInstanceId. Optional: ZTag, SAPID, Manufacturer, Model. Existing rows are detected by MachineCode and skipped (additive-only — no updates), matching the retired /clusters/{id}/equipment/import page. *@ @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns @inject IUnsTreeService Svc @if (Visible) { } @code { // Required, in order; DriverInstanceId may be left blank per row (driver-less equipment). private static readonly string[] RequiredColumns = ["Name", "MachineCode", "UnsLineId", "DriverInstanceId"]; /// Whether the modal is shown. The host owns this flag. [Parameter] public bool Visible { get; set; } /// /// Raised when the user closes the modal after an import so the host can reload the tree and /// dismiss the modal. Fired on close regardless of whether anything was inserted — closing always /// returns control to the host. /// [Parameter] public EventCallback OnImported { get; set; } /// Raised when the user cancels before importing so the host can close. [Parameter] public EventCallback OnCancel { get; set; } private string _csv = ""; private bool _busy; private string? _parseError; private EquipmentImportResult? _result; private bool _wasVisible; protected override void OnParametersSet() { // Reset only on the false→true transition so that re-renders while open don't wipe state. if (Visible && !_wasVisible) { _csv = ""; _parseError = null; _result = null; } _wasVisible = Visible; } private async Task ImportAsync() { _busy = true; _parseError = null; _result = null; try { var rows = ParseCsv(_csv); if (rows is null) { return; } _result = await Svc.ImportEquipmentAsync(rows); } finally { _busy = false; } } /// /// Closes the modal. Once an import has run (the user has seen the summary), closing raises /// OnImported so the host reloads the tree; before any import it raises OnCancel. /// private Task CloseAsync() => _result is not null ? OnImported.InvokeAsync() : OnCancel.InvokeAsync(); /// /// Parses the pasted CSV into rows. Requires a header row whose first /// four columns are Name, MachineCode, UnsLineId, DriverInstanceId (case-insensitive); the optional /// ZTag/SAPID/Manufacturer/Model follow. Blank optional cells parse as null. Returns /// null (and sets ) when the CSV is empty or the header is wrong. /// private List? ParseCsv(string csv) { if (string.IsNullOrWhiteSpace(csv)) { _parseError = "CSV is empty."; return null; } var lines = csv.Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries); if (lines.Length < 2) { _parseError = "Need a header row and at least one data row."; return null; } var header = lines[0].Split(',').Select(c => c.Trim()).ToArray(); for (var i = 0; i < RequiredColumns.Length; i++) { if (i >= header.Length || !string.Equals(header[i], RequiredColumns[i], StringComparison.OrdinalIgnoreCase)) { _parseError = $"Header column #{i + 1} must be '{RequiredColumns[i]}' (got '{(i < header.Length ? header[i] : "")}')."; return null; } } var rows = new List(); for (var lineIdx = 1; lineIdx < lines.Length; lineIdx++) { // NOTE: simple comma split — no RFC4180 quoting; values with commas are not supported. var parts = lines[lineIdx].Split(',').Select(c => c.Trim()).ToArray(); if (parts.Length < RequiredColumns.Length) { _parseError = $"Row {lineIdx}: too few columns (got {parts.Length}, need {RequiredColumns.Length})."; return null; } rows.Add(new EquipmentInput( Name: parts[0], MachineCode: parts[1], UnsLineId: parts[2], DriverInstanceId: NullIfEmpty(parts, 3), ZTag: NullIfEmpty(parts, 4), SAPID: NullIfEmpty(parts, 5), Manufacturer: NullIfEmpty(parts, 6), Model: NullIfEmpty(parts, 7), SerialNumber: null, HardwareRevision: null, SoftwareRevision: null, YearOfConstruction: null, AssetLocation: null, ManufacturerUri: null, DeviceManualUri: null, Enabled: true)); } return rows; } private static string? NullIfEmpty(string[] parts, int idx) => idx < parts.Length && !string.IsNullOrWhiteSpace(parts[idx]) ? parts[idx] : null; }