feat(ui/templates): new-folder, new-template, move-template dialogs
This commit is contained in:
@@ -41,6 +41,84 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (_showNewFolderDialog)
|
||||||
|
{
|
||||||
|
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.4);">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h6 class="modal-title">New Folder</h6>
|
||||||
|
<button type="button" class="btn-close" @onclick="() => _showNewFolderDialog = false"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input class="form-control form-control-sm" placeholder="Folder name" @bind="_newFolderName" />
|
||||||
|
@if (_newFolderError != null) { <div class="text-danger small mt-1">@_newFolderError</div> }
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" @onclick="() => _showNewFolderDialog = false">Cancel</button>
|
||||||
|
<button class="btn btn-primary btn-sm" @onclick="SubmitNewFolder">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (_showNewTemplateDialog)
|
||||||
|
{
|
||||||
|
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.4);">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h6 class="modal-title">New Template</h6>
|
||||||
|
<button type="button" class="btn-close" @onclick="() => _showNewTemplateDialog = false"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small">Name</label>
|
||||||
|
<input class="form-control form-control-sm" @bind="_newTemplateName" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small">Description</label>
|
||||||
|
<input class="form-control form-control-sm" @bind="_newTemplateDescription" />
|
||||||
|
</div>
|
||||||
|
@if (_newTemplateError != null) { <div class="text-danger small mt-1">@_newTemplateError</div> }
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" @onclick="() => _showNewTemplateDialog = false">Cancel</button>
|
||||||
|
<button class="btn btn-primary btn-sm" @onclick="SubmitNewTemplate">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (_showMoveTemplateDialog)
|
||||||
|
{
|
||||||
|
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.4);">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h6 class="modal-title">Move '@_moveTemplateName' to…</h6>
|
||||||
|
<button type="button" class="btn-close" @onclick="() => _showMoveTemplateDialog = false"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<select class="form-select form-select-sm" @bind="_moveTemplateTargetFolderId">
|
||||||
|
@foreach (var opt in EnumerateFolderOptions())
|
||||||
|
{
|
||||||
|
<option value="@opt.Id">@opt.Label</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
@if (_moveTemplateError != null) { <div class="text-danger small mt-1">@_moveTemplateError</div> }
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" @onclick="() => _showMoveTemplateDialog = false">Cancel</button>
|
||||||
|
<button class="btn btn-primary btn-sm" @onclick="SubmitMoveTemplate">Move</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (_loading)
|
@if (_loading)
|
||||||
{
|
{
|
||||||
<LoadingSpinner IsLoading="true" />
|
<LoadingSpinner IsLoading="true" />
|
||||||
@@ -518,9 +596,122 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private void OpenNewFolderDialog(int? parentFolderId) { /* Task 17 */ }
|
// New-folder dialog state
|
||||||
private void OpenNewTemplateDialog(int? parentFolderId) { /* Task 17 */ }
|
private bool _showNewFolderDialog;
|
||||||
private void OpenMoveTemplateDialog(int templateId, string label) { /* Task 17 */ }
|
private int? _newFolderParentId;
|
||||||
|
private string _newFolderName = string.Empty;
|
||||||
|
private string? _newFolderError;
|
||||||
|
|
||||||
|
private void OpenNewFolderDialog(int? parentFolderId)
|
||||||
|
{
|
||||||
|
_newFolderParentId = parentFolderId;
|
||||||
|
_newFolderName = string.Empty;
|
||||||
|
_newFolderError = null;
|
||||||
|
_showNewFolderDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubmitNewFolder()
|
||||||
|
{
|
||||||
|
_newFolderError = null;
|
||||||
|
var user = await GetCurrentUserAsync();
|
||||||
|
var result = await TemplateFolderService.CreateFolderAsync(_newFolderName.Trim(), _newFolderParentId, user);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
_showNewFolderDialog = false;
|
||||||
|
_toast.ShowSuccess($"Folder '{result.Value.Name}' created.");
|
||||||
|
await LoadTemplatesAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_newFolderError = result.Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New-template dialog state
|
||||||
|
private bool _showNewTemplateDialog;
|
||||||
|
private int? _newTemplateFolderId;
|
||||||
|
private string _newTemplateName = string.Empty;
|
||||||
|
private string? _newTemplateDescription;
|
||||||
|
private string? _newTemplateError;
|
||||||
|
|
||||||
|
private void OpenNewTemplateDialog(int? folderId)
|
||||||
|
{
|
||||||
|
_newTemplateFolderId = folderId;
|
||||||
|
_newTemplateName = string.Empty;
|
||||||
|
_newTemplateDescription = null;
|
||||||
|
_newTemplateError = null;
|
||||||
|
_showNewTemplateDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubmitNewTemplate()
|
||||||
|
{
|
||||||
|
_newTemplateError = null;
|
||||||
|
var user = await GetCurrentUserAsync();
|
||||||
|
var result = await TemplateService.CreateTemplateAsync(
|
||||||
|
_newTemplateName.Trim(), _newTemplateDescription?.Trim(), null, user, folderId: _newTemplateFolderId);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
_showNewTemplateDialog = false;
|
||||||
|
_toast.ShowSuccess($"Template '{result.Value.Name}' created.");
|
||||||
|
await LoadTemplatesAsync();
|
||||||
|
await SelectTemplate(result.Value.Id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_newTemplateError = result.Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move-template dialog state
|
||||||
|
private bool _showMoveTemplateDialog;
|
||||||
|
private int _moveTemplateId;
|
||||||
|
private string _moveTemplateName = string.Empty;
|
||||||
|
private int? _moveTemplateTargetFolderId;
|
||||||
|
private string? _moveTemplateError;
|
||||||
|
|
||||||
|
private void OpenMoveTemplateDialog(int templateId, string label)
|
||||||
|
{
|
||||||
|
_moveTemplateId = templateId;
|
||||||
|
_moveTemplateName = label;
|
||||||
|
_moveTemplateTargetFolderId = null;
|
||||||
|
_moveTemplateError = null;
|
||||||
|
_showMoveTemplateDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubmitMoveTemplate()
|
||||||
|
{
|
||||||
|
_moveTemplateError = null;
|
||||||
|
var user = await GetCurrentUserAsync();
|
||||||
|
var result = await TemplateService.MoveTemplateAsync(_moveTemplateId, _moveTemplateTargetFolderId, user);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
_showMoveTemplateDialog = false;
|
||||||
|
_toast.ShowSuccess($"Template '{_moveTemplateName}' moved.");
|
||||||
|
await LoadTemplatesAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_moveTemplateError = result.Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flat list of folders with indentation labels, for the picker.
|
||||||
|
private IEnumerable<(int? Id, string Label)> EnumerateFolderOptions()
|
||||||
|
{
|
||||||
|
yield return (null, "(Root)");
|
||||||
|
foreach (var f in WalkFolderHierarchy(_folders.Where(f => f.ParentFolderId == null), 0))
|
||||||
|
yield return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<(int? Id, string Label)> WalkFolderHierarchy(IEnumerable<TemplateFolder> level, int depth)
|
||||||
|
{
|
||||||
|
foreach (var f in level.OrderBy(f => f.Name, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
yield return ((int?)f.Id, new string(' ', depth * 2) + f.Name);
|
||||||
|
foreach (var sub in WalkFolderHierarchy(_folders.Where(c => c.ParentFolderId == f.Id), depth + 1))
|
||||||
|
yield return sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Rename folder dialog state
|
// Rename folder dialog state
|
||||||
private bool _showRenameFolderDialog;
|
private bool _showRenameFolderDialog;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public class TemplateService
|
|||||||
string? description,
|
string? description,
|
||||||
int? parentTemplateId,
|
int? parentTemplateId,
|
||||||
string user,
|
string user,
|
||||||
|
int? folderId = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
@@ -46,7 +47,8 @@ public class TemplateService
|
|||||||
var template = new Template(name)
|
var template = new Template(name)
|
||||||
{
|
{
|
||||||
Description = description,
|
Description = description,
|
||||||
ParentTemplateId = parentTemplateId
|
ParentTemplateId = parentTemplateId,
|
||||||
|
FolderId = folderId
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check acyclicity (inheritance) — for new templates this is mostly a parent-exists check,
|
// Check acyclicity (inheritance) — for new templates this is mostly a parent-exists check,
|
||||||
|
|||||||
@@ -70,6 +70,18 @@ public class TemplateServiceTests
|
|||||||
Assert.Contains("not found", result.Error);
|
Assert.Contains("not found", result.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateTemplate_WithFolderId_SetsFolderId()
|
||||||
|
{
|
||||||
|
var folder = new TemplateFolder("Dev") { Id = 7 };
|
||||||
|
_repoMock.Setup(r => r.GetFolderByIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>())).ReturnsAsync(folder);
|
||||||
|
|
||||||
|
var result = await _service.CreateTemplateAsync("X", null, null, "admin", folderId: 7);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(7, result.Value.FolderId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DeleteTemplate_Success()
|
public async Task DeleteTemplate_Success()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user