Skip to content

Commit 6cf5455

Browse files
Merge branch 'dev' into LEGLINK-790
2 parents 6f07cea + 0aa808c commit 6cf5455

7 files changed

Lines changed: 104 additions & 14 deletions

File tree

DotNet/Automation.UI/Controllers/OrganizationResourceMapsController.cs

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Automation.UI.Models;
22
using Automation.UI.Services.Persistence;
33
using Microsoft.AspNetCore.Mvc;
4+
using MongoDB.Driver;
45

56
namespace Automation.UI.Controllers;
67

@@ -23,22 +24,40 @@ public async Task<IActionResult> GetJson(Guid id, CancellationToken ct)
2324

2425
[HttpPost]
2526
[ValidateAntiForgeryToken]
26-
public async Task<IActionResult> SaveInline([FromBody] OrganizationResourceMapTemplate model, CancellationToken ct)
27+
public async Task<IActionResult> SaveInline(
28+
[FromBody] OrganizationResourceMapTemplate model,
29+
CancellationToken ct)
2730
{
2831
if (string.IsNullOrWhiteSpace(model.Name))
2932
return BadRequest("Template name is required.");
33+
3034
if (model.Conditions == null || model.Conditions.Count == 0)
3135
return BadRequest("At least one mapping condition is required.");
36+
3237
if (model.Conditions.Any(c => string.IsNullOrWhiteSpace(c.FhirPath)))
3338
return BadRequest("All mapping conditions must include a FHIRPath.");
3439

40+
model.Name = model.Name.Trim();
41+
42+
var templates = await store.GetAllAsync(ct);
43+
44+
if (HasDuplicateName(templates, model.Name, model.Id))
45+
{
46+
return Conflict(
47+
$"An Organization Resource Map named '{model.Name}' already exists.");
48+
}
49+
3550
var existing = await store.GetByIdAsync(model.Id, ct);
51+
3652
if (existing is { IsSystem: true })
37-
return StatusCode(StatusCodes.Status403Forbidden, "System template cannot be modified.");
53+
return StatusCode(
54+
StatusCodes.Status403Forbidden,
55+
"System template cannot be modified.");
3856

3957
model.IsSystem = false;
4058
model.IsDefault = existing?.IsDefault ?? model.IsDefault;
4159
model.UpdatedAt = DateTimeOffset.UtcNow;
60+
4261
await store.UpsertAsync(model, ct);
4362
return Json(new { id = model.Id });
4463
}
@@ -71,10 +90,21 @@ public async Task<IActionResult> CloneInline([FromBody] IdRequest request, Cance
7190
var source = await store.GetByIdAsync(request.Id, ct);
7291
if (source == null) return NotFound();
7392

93+
var templates = await store.GetAllAsync(ct);
94+
95+
var cloneName = BuildCloneName(source.Name);
96+
var copyNumber = 2;
97+
98+
while (HasDuplicateName(templates, cloneName))
99+
{
100+
cloneName = BuildCloneName(source.Name, copyNumber);
101+
copyNumber++;
102+
}
103+
74104
var clone = new OrganizationResourceMapTemplate
75105
{
76106
Id = Guid.NewGuid(),
77-
Name = $"{source.Name} (Copy)",
107+
Name = cloneName,
78108
Description = source.Description,
79109
Conditions = source.Conditions.Select(c => new OrganizationResourceMapCondition
80110
{
@@ -87,7 +117,8 @@ public async Task<IActionResult> CloneInline([FromBody] IdRequest request, Cance
87117
};
88118

89119
await store.UpsertAsync(clone, ct);
90-
return Json(new { id = clone.Id });
120+
121+
return CreatedAtAction(nameof(GetJson), new { id = clone.Id }, new { id = clone.Id });
91122
}
92123

93124
[HttpPost]
@@ -104,4 +135,26 @@ public async Task<IActionResult> SetDefaultInline([FromBody] IdRequest request,
104135
return Ok();
105136
}
106137

138+
private static bool HasDuplicateName(
139+
IEnumerable<OrganizationResourceMapTemplate> templates,
140+
string name,
141+
Guid? excludeId = null)
142+
{
143+
return templates.Any(t =>
144+
(!excludeId.HasValue || t.Id != excludeId.Value) &&
145+
string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase));
146+
}
147+
148+
private static string BuildCloneName(
149+
string sourceName,
150+
int? copyNumber = null)
151+
{
152+
var suffix = copyNumber.HasValue
153+
? $" (Copy {copyNumber.Value})"
154+
: " (Copy)";
155+
156+
var baseName = sourceName.Trim();
157+
158+
return baseName + suffix;
159+
}
107160
}

DotNet/Automation.UI/Services/Persistence/MongoIndexManager.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,21 @@ public void EnsureAllIndexes()
4747

4848
private void EnsureOrganizationResourceMapTemplateIndexes()
4949
{
50-
var collection = _database.GetCollection<BsonDocument>("automation_org_resource_map_templates");
51-
CreateIndexSafe(collection, new BsonDocument { { "Name", 1 } }, unique: false, "idx_name_asc");
52-
CreateIndexSafe(collection, new BsonDocument { { "IsDefault", 1 } }, unique: false, "idx_isDefault");
50+
var collection = _database.GetCollection<BsonDocument>(
51+
"automation_org_resource_map_templates");
52+
53+
// Retain Name index because GetAllAsync sorts by display Name.
54+
CreateIndexSafe(
55+
collection,
56+
new BsonDocument { { "Name", 1 } },
57+
unique: false,
58+
"idx_name_asc");
59+
60+
CreateIndexSafe(
61+
collection,
62+
new BsonDocument { { "IsDefault", 1 } },
63+
unique: false,
64+
"idx_isDefault");
5365
}
5466

5567
// --- automation_runs ---

DotNet/Automation.UI/Services/Persistence/MongoOrganizationResourceMapTemplateStore.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
using System.Text.Json;
2-
using Automation.UI.Models;
1+
using Automation.UI.Models;
32
using MongoDB.Driver;
3+
using System.Text.Json;
44

55
namespace Automation.UI.Services.Persistence;
66

@@ -72,7 +72,7 @@ public async Task DeleteAsync(Guid id, CancellationToken ct = default)
7272
private static OrganizationResourceMapTemplateDocument ToDocument(OrganizationResourceMapTemplate model) => new()
7373
{
7474
Id = model.Id,
75-
Name = model.Name,
75+
Name = model.Name.Trim(),
7676
Description = model.Description,
7777
IsSystem = model.IsSystem,
7878
IsDefault = model.IsDefault,
@@ -87,7 +87,8 @@ public async Task DeleteAsync(Guid id, CancellationToken ct = default)
8787
Description = doc.Description,
8888
IsSystem = doc.IsSystem,
8989
IsDefault = doc.IsDefault,
90-
Conditions = Deserialize<List<OrganizationResourceMapCondition>>(doc.ConditionsJson) ?? [],
90+
Conditions = Deserialize<List<OrganizationResourceMapCondition>>(
91+
doc.ConditionsJson) ?? [],
9192
UpdatedAt = doc.UpdatedAt
9293
};
9394

DotNet/Automation.UI/Services/Persistence/OrganizationResourceMapTemplateDocument.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ internal sealed class OrganizationResourceMapTemplateDocument
88
[BsonId]
99
[BsonRepresentation(BsonType.String)]
1010
public Guid Id { get; set; }
11-
1211
public string Name { get; set; } = string.Empty;
1312
public string? Description { get; set; }
1413
public bool IsSystem { get; set; }

DotNet/Automation.UI/Views/OrganizationResourceMaps/Index.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@
462462
const conditions = collectConditions();
463463
const model={
464464
id: document.getElementById('MapId').value || crypto.randomUUID(),
465-
name: document.getElementById('MapName').value,
465+
name: document.getElementById('MapName').value.trim(),
466466
description: document.getElementById('MapDescription').value || null,
467467
conditions: conditions,
468468
isSystem:false,

DotNet/Automation.UI/Views/Runs/Index.cshtml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,10 @@
265265
</button>
266266
</div>
267267
<div class="col-md-2">
268-
<a asp-controller="Scenarios" asp-action="Index" class="btn btn-outline-secondary w-100">
268+
<a asp-controller="Scenarios"
269+
asp-action="Index"
270+
class="btn btn-outline-secondary w-100"
271+
id="btnManageScenario">
269272
<i class="bi bi-journal-text me-1"></i> Manage
270273
</a>
271274
</div>
@@ -302,6 +305,7 @@
302305
var recentRunsPartialUrl = '@Url.Action("RecentRunsPartial", "Runs")';
303306
var cancelUrl = '@Url.Action("CancelJson", "Runs")';
304307
var deleteUrl = '@Url.Action("DeleteJson", "Runs")';
308+
var scenariosUrl = '@Url.Action("Index", "Scenarios")';
305309
306310
// --- KPI elements ---
307311
var kpiActive = document.getElementById('kpiActive');
@@ -612,6 +616,7 @@
612616
var noResults = document.getElementById('quickLaunchNoResults');
613617
var quickTypeFilter = document.getElementById('quickLaunchTypeFilter');
614618
var quickSort = document.getElementById('quickLaunchSort');
619+
var manageLink = document.getElementById('btnManageScenario');
615620
616621
function getQuickLaunchOptions() {
617622
return Array.from(
@@ -689,6 +694,12 @@
689694
dropdownButton.textContent =
690695
label || '-- Select a scenario --';
691696
}
697+
698+
if (manageLink) {
699+
manageLink.href = id
700+
? scenariosUrl + '?editScenarioId=' + encodeURIComponent(id)
701+
: scenariosUrl;
702+
}
692703
}
693704
694705
if (quickSearch) {
@@ -1019,6 +1030,7 @@
10191030
refreshRecentRunsCard({ pageSize: sel.value, pageNumber: '1' });
10201031
});
10211032
})();
1033+
10221034
</script>
10231035

10241036
@await Html.PartialAsync("_ScenarioEditorModal")

DotNet/Automation.UI/Views/Scenarios/Index.cshtml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,5 +247,18 @@
247247
}
248248
249249
applyScenarioView();
250+
251+
// Open a scenario passed from Quick Launch directly in the editor.
252+
var editScenarioId = new URLSearchParams(window.location.search).get('editScenarioId');
253+
254+
if (editScenarioId && typeof window.openScenarioEditor === 'function') {
255+
window.openScenarioEditor(editScenarioId, 'edit');
256+
257+
// Remove the one-time navigation parameter so saving/reloading the
258+
// Scenarios page does not automatically reopen the editor.
259+
var url = new URL(window.location.href);
260+
url.searchParams.delete('editScenarioId');
261+
window.history.replaceState(null, '', url.toString());
262+
}
250263
})();
251264
</script>

0 commit comments

Comments
 (0)