feat(transport-ui): import Map step + per-line diff view (M8 E2)

This commit is contained in:
Joseph Doherty
2026-06-18 07:21:23 -04:00
parent e67587ec93
commit c8211f6363
5 changed files with 733 additions and 4 deletions
@@ -206,6 +206,8 @@
var hasBlockers = _preview.Items.Any(i => i.Kind == ConflictKind.Blocker);
<div>
@RenderMapSection();
<p class="text-body-secondary">
Review each artifact in the bundle and choose how it should be applied
to this environment. Identical items are skipped automatically; new
@@ -245,11 +247,22 @@
</tr>
@if (item.Kind == ConflictKind.Modified && !string.IsNullOrEmpty(item.FieldDiffJson))
{
var lineDiff = TryExtractLineDiff(item.FieldDiffJson);
<tr>
<td colspan="6">
<details>
<summary class="small">Field diff</summary>
<pre class="small mb-0"><code>@item.FieldDiffJson</code></pre>
@if (lineDiff is not null)
{
<div class="mt-2" data-testid="code-line-diff">
<div class="small text-body-secondary mb-1">Code changes</div>
<LineDiffView LineDiff="lineDiff" />
</div>
}
else
{
<pre class="small mb-0"><code>@item.FieldDiffJson</code></pre>
}
</details>
</td>
</tr>
@@ -283,6 +296,123 @@
</div>
};
// ============================================================
// Step 3 — Map sub-section (M8 E2)
// ============================================================
// Shown only when the preview references source-environment sites/connections
// the operator must resolve before import. For central-config-only bundles the
// preview carries no required mappings and this renders nothing.
private RenderFragment RenderMapSection() => __builder =>
{
if (_preview is null) return;
var hasSiteMappings = _preview.RequiredSiteMappings.Count > 0;
var hasConnMappings = _preview.RequiredConnectionMappings.Count > 0;
if (!hasSiteMappings && !hasConnMappings)
{
return;
}
<div class="card mb-4" data-testid="map-section">
<div class="card-header bg-body-tertiary">
<strong>Resolve site &amp; connection references</strong>
<span class="small text-body-secondary ms-2">
This bundle references sites/connections from its source environment.
Map each to an existing target, or create a new one.
</span>
</div>
<div class="card-body">
@if (hasSiteMappings)
{
<h6 class="small text-uppercase text-body-secondary">Sites</h6>
<table class="table table-sm align-middle mb-4" data-testid="map-sites-table">
<thead>
<tr>
<th>Source identifier</th>
<th>Source name</th>
<th>Map to target</th>
</tr>
</thead>
<tbody>
@foreach (var rsm in _preview.RequiredSiteMappings)
{
var chosen = _siteChoices.TryGetValue(rsm.SourceSiteIdentifier, out var c) ? c : CreateNewValue;
<tr data-testid="map-site-row">
<td><code>@rsm.SourceSiteIdentifier</code></td>
<td>@rsm.SourceSiteName</td>
<td>
<select class="form-select form-select-sm"
style="max-width: 22rem;"
data-testid="@($"map-site-select-{rsm.SourceSiteIdentifier}")"
value="@chosen"
@onchange="e => OnSiteChoiceChangedAsync(rsm.SourceSiteIdentifier, e.Value?.ToString())">
<option value="@CreateNewValue" selected="@(string.IsNullOrEmpty(chosen))">Create new</option>
@foreach (var site in _targetSites)
{
<option value="@site.SiteIdentifier" selected="@(chosen == site.SiteIdentifier)">
@site.Name (@site.SiteIdentifier)
</option>
}
</select>
</td>
</tr>
}
</tbody>
</table>
}
@if (hasConnMappings)
{
<h6 class="small text-uppercase text-body-secondary">Connections</h6>
@foreach (var grp in _preview.RequiredConnectionMappings.GroupBy(m => m.SourceSiteIdentifier))
{
var siteTarget = _siteChoices.TryGetValue(grp.Key, out var st) ? st : CreateNewValue;
var targetConns = ConnectionsForChosenTarget(grp.Key);
<div class="mb-3" data-testid="map-conn-group">
<div class="small text-body-secondary mb-1">
Site <code>@grp.Key</code>
@if (string.IsNullOrEmpty(siteTarget))
{
<span class="badge bg-success ms-1">new site</span>
}
</div>
<table class="table table-sm align-middle mb-0" data-testid="map-conns-table">
<thead>
<tr>
<th>Source connection</th>
<th>Map to target</th>
</tr>
</thead>
<tbody>
@foreach (var rcm in grp)
{
var key = (rcm.SourceSiteIdentifier, rcm.SourceConnectionName);
var chosenConn = _connectionChoices.TryGetValue(key, out var cc) ? cc : CreateNewValue;
<tr data-testid="map-conn-row">
<td>@rcm.SourceConnectionName</td>
<td>
<select class="form-select form-select-sm"
style="max-width: 22rem;"
data-testid="@($"map-conn-select-{rcm.SourceSiteIdentifier}-{rcm.SourceConnectionName}")"
value="@chosenConn"
@onchange="e => OnConnectionChoiceChanged(rcm.SourceSiteIdentifier, rcm.SourceConnectionName, e.Value?.ToString())">
<option value="@CreateNewValue" selected="@(string.IsNullOrEmpty(chosenConn))">Create new</option>
@foreach (var conn in targetConns)
{
<option value="@conn.Name" selected="@(chosenConn == conn.Name)">@conn.Name</option>
}
</select>
</td>
</tr>
}
</tbody>
</table>
</div>
}
}
</div>
</div>
};
private RenderFragment RenderKindBadge(ImportPreviewItem item) => __builder =>
{
var (cls, label) = item.Kind switch