feat: separate create/edit form pages, Playwright test infrastructure, /auth/token endpoint
Move all CRUD create/edit forms from inline on list pages to dedicated form pages with back-button navigation and post-save redirect. Add Playwright Docker container (browser server on port 3000) with 25 passing E2E tests covering login, navigation, and site CRUD workflows. Add POST /auth/token endpoint for clean JWT retrieval.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
@page "/design/api-methods/create"
|
||||
@page "/design/api-methods/{Id:int}/edit"
|
||||
@using ScadaLink.Security
|
||||
@using ScadaLink.Commons.Entities.InboundApi
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject IInboundApiRepository InboundApiRepository
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<button class="btn btn-link text-decoration-none ps-0 mb-2" @onclick="GoBack">← Back</button>
|
||||
|
||||
<h4 class="mb-3">@(Id.HasValue ? "Edit API Method" : "Add API Method")</h4>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingSpinner IsLoading="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_name" disabled="@Id.HasValue" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Timeout (seconds)</label>
|
||||
<input type="number" class="form-control" @bind="_timeoutSeconds" min="1" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Params (JSON)</label>
|
||||
<input type="text" class="form-control" @bind="_params" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Returns (JSON)</label>
|
||||
<input type="text" class="form-control" @bind="_returns" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Script</label>
|
||||
<textarea class="form-control font-monospace" rows="5" @bind="_script" style="font-size: 0.85rem;"></textarea>
|
||||
</div>
|
||||
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mb-2">@_formError</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-success" @onclick="Save">Save</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public int? Id { get; set; }
|
||||
|
||||
private bool _loading = true;
|
||||
private string _name = "", _script = "";
|
||||
private int _timeoutSeconds = 30;
|
||||
private string? _params, _returns;
|
||||
private string? _formError;
|
||||
|
||||
private ApiMethod? _existing;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Id.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
_existing = await InboundApiRepository.GetApiMethodByIdAsync(Id.Value);
|
||||
if (_existing != null)
|
||||
{
|
||||
_name = _existing.Name;
|
||||
_script = _existing.Script;
|
||||
_timeoutSeconds = _existing.TimeoutSeconds;
|
||||
_params = _existing.ParameterDefinitions;
|
||||
_returns = _existing.ReturnDefinition;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
}
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
_formError = null;
|
||||
if (string.IsNullOrWhiteSpace(_name) || string.IsNullOrWhiteSpace(_script))
|
||||
{
|
||||
_formError = "Name and script required.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_existing != null)
|
||||
{
|
||||
_existing.Script = _script;
|
||||
_existing.TimeoutSeconds = _timeoutSeconds;
|
||||
_existing.ParameterDefinitions = _params?.Trim();
|
||||
_existing.ReturnDefinition = _returns?.Trim();
|
||||
await InboundApiRepository.UpdateApiMethodAsync(_existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var m = new ApiMethod(_name.Trim(), _script)
|
||||
{
|
||||
TimeoutSeconds = _timeoutSeconds,
|
||||
ParameterDefinitions = _params?.Trim(),
|
||||
ReturnDefinition = _returns?.Trim()
|
||||
};
|
||||
await InboundApiRepository.AddApiMethodAsync(m);
|
||||
}
|
||||
await InboundApiRepository.SaveChangesAsync();
|
||||
NavigationManager.NavigateTo("/design/external-systems");
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
}
|
||||
|
||||
private void GoBack() => NavigationManager.NavigateTo("/design/external-systems");
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
@page "/design/db-connections/create"
|
||||
@page "/design/db-connections/{Id:int}/edit"
|
||||
@using ScadaLink.Security
|
||||
@using ScadaLink.Commons.Entities.ExternalSystems
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject IExternalSystemRepository ExternalSystemRepository
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<button class="btn btn-link text-decoration-none ps-0 mb-2" @onclick="GoBack">← Back</button>
|
||||
|
||||
<h4 class="mb-3">@(Id.HasValue ? "Edit Database Connection" : "Add Database Connection")</h4>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingSpinner IsLoading="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_name" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Connection String</label>
|
||||
<input type="text" class="form-control" @bind="_connectionString" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Max Retries</label>
|
||||
<input type="number" class="form-control" @bind="_maxRetries" min="0" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Retry Delay (seconds)</label>
|
||||
<input type="number" class="form-control" @bind="_retryDelaySeconds" min="0" />
|
||||
</div>
|
||||
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mb-2">@_formError</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-success" @onclick="Save">Save</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public int? Id { get; set; }
|
||||
|
||||
private bool _loading = true;
|
||||
private string _name = "", _connectionString = "";
|
||||
private int _maxRetries = 3;
|
||||
private int _retryDelaySeconds = 5;
|
||||
private string? _formError;
|
||||
|
||||
private DatabaseConnectionDefinition? _existing;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Id.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
_existing = await ExternalSystemRepository.GetDatabaseConnectionByIdAsync(Id.Value);
|
||||
if (_existing != null)
|
||||
{
|
||||
_name = _existing.Name;
|
||||
_connectionString = _existing.ConnectionString;
|
||||
_maxRetries = _existing.MaxRetries;
|
||||
_retryDelaySeconds = (int)_existing.RetryDelay.TotalSeconds;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
}
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
_formError = null;
|
||||
if (string.IsNullOrWhiteSpace(_name) || string.IsNullOrWhiteSpace(_connectionString))
|
||||
{
|
||||
_formError = "Name and connection string required.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_existing != null)
|
||||
{
|
||||
_existing.Name = _name.Trim();
|
||||
_existing.ConnectionString = _connectionString.Trim();
|
||||
_existing.MaxRetries = _maxRetries;
|
||||
_existing.RetryDelay = TimeSpan.FromSeconds(_retryDelaySeconds);
|
||||
await ExternalSystemRepository.UpdateDatabaseConnectionAsync(_existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dc = new DatabaseConnectionDefinition(_name.Trim(), _connectionString.Trim())
|
||||
{
|
||||
MaxRetries = _maxRetries,
|
||||
RetryDelay = TimeSpan.FromSeconds(_retryDelaySeconds)
|
||||
};
|
||||
await ExternalSystemRepository.AddDatabaseConnectionAsync(dc);
|
||||
}
|
||||
await ExternalSystemRepository.SaveChangesAsync();
|
||||
NavigationManager.NavigateTo("/design/external-systems");
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
}
|
||||
|
||||
private void GoBack() => NavigationManager.NavigateTo("/design/external-systems");
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
@page "/design/external-systems/create"
|
||||
@page "/design/external-systems/{Id:int}/edit"
|
||||
@using ScadaLink.Security
|
||||
@using ScadaLink.Commons.Entities.ExternalSystems
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject IExternalSystemRepository ExternalSystemRepository
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<button class="btn btn-link text-decoration-none ps-0 mb-2" @onclick="GoBack">← Back</button>
|
||||
|
||||
<h4 class="mb-3">@(Id.HasValue ? "Edit External System" : "Add External System")</h4>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingSpinner IsLoading="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_name" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Endpoint URL</label>
|
||||
<input type="text" class="form-control" @bind="_endpointUrl" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Auth Type</label>
|
||||
<select class="form-select" @bind="_authType">
|
||||
<option>ApiKey</option>
|
||||
<option>BasicAuth</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Auth Config (JSON)</label>
|
||||
<input type="text" class="form-control" @bind="_authConfig" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Max Retries</label>
|
||||
<input type="number" class="form-control" @bind="_maxRetries" min="0" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Retry Delay (seconds)</label>
|
||||
<input type="number" class="form-control" @bind="_retryDelaySeconds" min="0" />
|
||||
</div>
|
||||
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mb-2">@_formError</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-success" @onclick="Save">Save</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public int? Id { get; set; }
|
||||
|
||||
private bool _loading = true;
|
||||
private string _name = "", _endpointUrl = "", _authType = "ApiKey";
|
||||
private string? _authConfig;
|
||||
private int _maxRetries = 3;
|
||||
private int _retryDelaySeconds = 5;
|
||||
private string? _formError;
|
||||
|
||||
private ExternalSystemDefinition? _existing;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Id.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
_existing = await ExternalSystemRepository.GetExternalSystemByIdAsync(Id.Value);
|
||||
if (_existing != null)
|
||||
{
|
||||
_name = _existing.Name;
|
||||
_endpointUrl = _existing.EndpointUrl;
|
||||
_authType = _existing.AuthType;
|
||||
_authConfig = _existing.AuthConfiguration;
|
||||
_maxRetries = _existing.MaxRetries;
|
||||
_retryDelaySeconds = (int)_existing.RetryDelay.TotalSeconds;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
}
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
_formError = null;
|
||||
if (string.IsNullOrWhiteSpace(_name) || string.IsNullOrWhiteSpace(_endpointUrl))
|
||||
{
|
||||
_formError = "Name and URL required.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_existing != null)
|
||||
{
|
||||
_existing.Name = _name.Trim();
|
||||
_existing.EndpointUrl = _endpointUrl.Trim();
|
||||
_existing.AuthType = _authType;
|
||||
_existing.AuthConfiguration = _authConfig?.Trim();
|
||||
_existing.MaxRetries = _maxRetries;
|
||||
_existing.RetryDelay = TimeSpan.FromSeconds(_retryDelaySeconds);
|
||||
await ExternalSystemRepository.UpdateExternalSystemAsync(_existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var es = new ExternalSystemDefinition(_name.Trim(), _endpointUrl.Trim(), _authType)
|
||||
{
|
||||
AuthConfiguration = _authConfig?.Trim(),
|
||||
MaxRetries = _maxRetries,
|
||||
RetryDelay = TimeSpan.FromSeconds(_retryDelaySeconds)
|
||||
};
|
||||
await ExternalSystemRepository.AddExternalSystemAsync(es);
|
||||
}
|
||||
await ExternalSystemRepository.SaveChangesAsync();
|
||||
NavigationManager.NavigateTo("/design/external-systems");
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
}
|
||||
|
||||
private void GoBack() => NavigationManager.NavigateTo("/design/external-systems");
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
@inject IExternalSystemRepository ExternalSystemRepository
|
||||
@inject INotificationRepository NotificationRepository
|
||||
@inject IInboundApiRepository InboundApiRepository
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<h4 class="mb-3">Integration Definitions</h4>
|
||||
@@ -62,22 +63,9 @@
|
||||
|
||||
// External Systems
|
||||
private List<ExternalSystemDefinition> _externalSystems = new();
|
||||
private bool _showExtSysForm;
|
||||
private ExternalSystemDefinition? _editingExtSys;
|
||||
private string _extSysName = "", _extSysUrl = "", _extSysAuth = "ApiKey";
|
||||
private string? _extSysAuthConfig;
|
||||
private int _extSysMaxRetries = 3;
|
||||
private int _extSysRetryDelaySeconds = 5;
|
||||
private string? _extSysFormError;
|
||||
|
||||
// Database Connections
|
||||
private List<DatabaseConnectionDefinition> _dbConnections = new();
|
||||
private bool _showDbConnForm;
|
||||
private DatabaseConnectionDefinition? _editingDbConn;
|
||||
private string _dbConnName = "", _dbConnString = "";
|
||||
private int _dbConnMaxRetries = 3;
|
||||
private int _dbConnRetryDelaySeconds = 5;
|
||||
private string? _dbConnFormError;
|
||||
|
||||
// SMTP Configuration
|
||||
private List<SmtpConfiguration> _smtpConfigs = new();
|
||||
@@ -92,26 +80,10 @@
|
||||
|
||||
// Notification Lists
|
||||
private List<NotificationList> _notificationLists = new();
|
||||
private bool _showNotifForm;
|
||||
private NotificationList? _editingNotifList;
|
||||
private string _notifName = "";
|
||||
private string? _notifFormError;
|
||||
|
||||
// Notification Recipients
|
||||
private Dictionary<int, List<NotificationRecipient>> _recipients = new();
|
||||
private bool _showRecipientForm;
|
||||
private int _recipientListId;
|
||||
private string _recipientName = "", _recipientEmail = "";
|
||||
private string? _recipientFormError;
|
||||
|
||||
// Inbound API Methods
|
||||
private List<ApiMethod> _apiMethods = new();
|
||||
private bool _showApiMethodForm;
|
||||
private ApiMethod? _editingApiMethod;
|
||||
private string _apiMethodName = "", _apiMethodScript = "";
|
||||
private int _apiMethodTimeout = 30;
|
||||
private string? _apiMethodParams, _apiMethodReturn;
|
||||
private string? _apiMethodFormError;
|
||||
|
||||
private ToastNotification _toast = default!;
|
||||
private ConfirmDialog _confirmDialog = default!;
|
||||
@@ -150,28 +122,9 @@
|
||||
{
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h6 class="mb-0">External Systems</h6>
|
||||
<button class="btn btn-primary btn-sm" @onclick="ShowExtSysAddForm">Add</button>
|
||||
<button class="btn btn-primary btn-sm" @onclick='() => NavigationManager.NavigateTo("/design/external-systems/create")'>Add</button>
|
||||
</div>
|
||||
|
||||
@if (_showExtSysForm)
|
||||
{
|
||||
<div class="card mb-2"><div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-2"><label class="form-label small">Name</label><input type="text" class="form-control form-control-sm" @bind="_extSysName" /></div>
|
||||
<div class="col-md-3"><label class="form-label small">Endpoint URL</label><input type="text" class="form-control form-control-sm" @bind="_extSysUrl" /></div>
|
||||
<div class="col-md-2"><label class="form-label small">Auth Type</label>
|
||||
<select class="form-select form-select-sm" @bind="_extSysAuth"><option>ApiKey</option><option>BasicAuth</option></select></div>
|
||||
<div class="col-md-2"><label class="form-label small">Auth Config (JSON)</label><input type="text" class="form-control form-control-sm" @bind="_extSysAuthConfig" /></div>
|
||||
<div class="col-md-1"><label class="form-label small">Max Retries</label><input type="number" class="form-control form-control-sm" @bind="_extSysMaxRetries" min="0" /></div>
|
||||
<div class="col-md-1"><label class="form-label small">Retry Delay (s)</label><input type="number" class="form-control form-control-sm" @bind="_extSysRetryDelaySeconds" min="0" /></div>
|
||||
<div class="col-md-1">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveExtSys">Save</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => _showExtSysForm = false">Cancel</button></div>
|
||||
</div>
|
||||
@if (_extSysFormError != null) { <div class="text-danger small mt-1">@_extSysFormError</div> }
|
||||
</div></div>
|
||||
}
|
||||
|
||||
<table class="table table-sm table-striped">
|
||||
<thead class="table-dark"><tr><th>Name</th><th>URL</th><th>Auth</th><th>Retries</th><th>Delay</th><th style="width:120px;">Actions</th></tr></thead>
|
||||
<tbody>
|
||||
@@ -181,7 +134,7 @@
|
||||
<td>@es.Name</td><td class="small">@es.EndpointUrl</td><td><span class="badge bg-secondary">@es.AuthType</span></td>
|
||||
<td class="small">@es.MaxRetries</td><td class="small">@es.RetryDelay.TotalSeconds s</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1" @onclick="() => { _editingExtSys = es; _extSysName = es.Name; _extSysUrl = es.EndpointUrl; _extSysAuth = es.AuthType; _extSysAuthConfig = es.AuthConfiguration; _extSysMaxRetries = es.MaxRetries; _extSysRetryDelaySeconds = (int)es.RetryDelay.TotalSeconds; _showExtSysForm = true; }">Edit</button>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1" @onclick='() => NavigationManager.NavigateTo($"/design/external-systems/{es.Id}/edit")'>Edit</button>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1" @onclick="() => DeleteExtSys(es)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -190,31 +143,6 @@
|
||||
</table>
|
||||
};
|
||||
|
||||
private void ShowExtSysAddForm()
|
||||
{
|
||||
_showExtSysForm = true;
|
||||
_editingExtSys = null;
|
||||
_extSysName = _extSysUrl = string.Empty;
|
||||
_extSysAuth = "ApiKey";
|
||||
_extSysAuthConfig = null;
|
||||
_extSysMaxRetries = 3;
|
||||
_extSysRetryDelaySeconds = 5;
|
||||
_extSysFormError = null;
|
||||
}
|
||||
|
||||
private async Task SaveExtSys()
|
||||
{
|
||||
_extSysFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_extSysName) || string.IsNullOrWhiteSpace(_extSysUrl)) { _extSysFormError = "Name and URL required."; return; }
|
||||
try
|
||||
{
|
||||
if (_editingExtSys != null) { _editingExtSys.Name = _extSysName.Trim(); _editingExtSys.EndpointUrl = _extSysUrl.Trim(); _editingExtSys.AuthType = _extSysAuth; _editingExtSys.AuthConfiguration = _extSysAuthConfig?.Trim(); _editingExtSys.MaxRetries = _extSysMaxRetries; _editingExtSys.RetryDelay = TimeSpan.FromSeconds(_extSysRetryDelaySeconds); await ExternalSystemRepository.UpdateExternalSystemAsync(_editingExtSys); }
|
||||
else { var es = new ExternalSystemDefinition(_extSysName.Trim(), _extSysUrl.Trim(), _extSysAuth) { AuthConfiguration = _extSysAuthConfig?.Trim(), MaxRetries = _extSysMaxRetries, RetryDelay = TimeSpan.FromSeconds(_extSysRetryDelaySeconds) }; await ExternalSystemRepository.AddExternalSystemAsync(es); }
|
||||
await ExternalSystemRepository.SaveChangesAsync(); _showExtSysForm = false; _toast.ShowSuccess("Saved."); await LoadAllAsync();
|
||||
}
|
||||
catch (Exception ex) { _extSysFormError = ex.Message; }
|
||||
}
|
||||
|
||||
private async Task DeleteExtSys(ExternalSystemDefinition es)
|
||||
{
|
||||
if (!await _confirmDialog.ShowAsync($"Delete '{es.Name}'?", "Delete External System")) return;
|
||||
@@ -227,25 +155,9 @@
|
||||
{
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h6 class="mb-0">Database Connections</h6>
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => { _showDbConnForm = true; _editingDbConn = null; _dbConnName = _dbConnString = string.Empty; _dbConnMaxRetries = 3; _dbConnRetryDelaySeconds = 5; _dbConnFormError = null; }">Add</button>
|
||||
<button class="btn btn-primary btn-sm" @onclick='() => NavigationManager.NavigateTo("/design/db-connections/create")'>Add</button>
|
||||
</div>
|
||||
|
||||
@if (_showDbConnForm)
|
||||
{
|
||||
<div class="card mb-2"><div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3"><label class="form-label small">Name</label><input type="text" class="form-control form-control-sm" @bind="_dbConnName" /></div>
|
||||
<div class="col-md-4"><label class="form-label small">Connection String</label><input type="text" class="form-control form-control-sm" @bind="_dbConnString" /></div>
|
||||
<div class="col-md-1"><label class="form-label small">Max Retries</label><input type="number" class="form-control form-control-sm" @bind="_dbConnMaxRetries" min="0" /></div>
|
||||
<div class="col-md-1"><label class="form-label small">Retry Delay (s)</label><input type="number" class="form-control form-control-sm" @bind="_dbConnRetryDelaySeconds" min="0" /></div>
|
||||
<div class="col-md-3">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveDbConn">Save</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => _showDbConnForm = false">Cancel</button></div>
|
||||
</div>
|
||||
@if (_dbConnFormError != null) { <div class="text-danger small mt-1">@_dbConnFormError</div> }
|
||||
</div></div>
|
||||
}
|
||||
|
||||
<table class="table table-sm table-striped">
|
||||
<thead class="table-dark"><tr><th>Name</th><th>Connection String</th><th>Retries</th><th>Delay</th><th style="width:120px;">Actions</th></tr></thead>
|
||||
<tbody>
|
||||
@@ -255,7 +167,7 @@
|
||||
<td>@dc.Name</td><td class="small text-muted text-truncate" style="max-width:400px;">@dc.ConnectionString</td>
|
||||
<td class="small">@dc.MaxRetries</td><td class="small">@dc.RetryDelay.TotalSeconds s</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1" @onclick="() => { _editingDbConn = dc; _dbConnName = dc.Name; _dbConnString = dc.ConnectionString; _dbConnMaxRetries = dc.MaxRetries; _dbConnRetryDelaySeconds = (int)dc.RetryDelay.TotalSeconds; _showDbConnForm = true; }">Edit</button>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1" @onclick='() => NavigationManager.NavigateTo($"/design/db-connections/{dc.Id}/edit")'>Edit</button>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1" @onclick="() => DeleteDbConn(dc)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -264,19 +176,6 @@
|
||||
</table>
|
||||
};
|
||||
|
||||
private async Task SaveDbConn()
|
||||
{
|
||||
_dbConnFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_dbConnName) || string.IsNullOrWhiteSpace(_dbConnString)) { _dbConnFormError = "Name and connection string required."; return; }
|
||||
try
|
||||
{
|
||||
if (_editingDbConn != null) { _editingDbConn.Name = _dbConnName.Trim(); _editingDbConn.ConnectionString = _dbConnString.Trim(); _editingDbConn.MaxRetries = _dbConnMaxRetries; _editingDbConn.RetryDelay = TimeSpan.FromSeconds(_dbConnRetryDelaySeconds); await ExternalSystemRepository.UpdateDatabaseConnectionAsync(_editingDbConn); }
|
||||
else { var dc = new DatabaseConnectionDefinition(_dbConnName.Trim(), _dbConnString.Trim()) { MaxRetries = _dbConnMaxRetries, RetryDelay = TimeSpan.FromSeconds(_dbConnRetryDelaySeconds) }; await ExternalSystemRepository.AddDatabaseConnectionAsync(dc); }
|
||||
await ExternalSystemRepository.SaveChangesAsync(); _showDbConnForm = false; _toast.ShowSuccess("Saved."); await LoadAllAsync();
|
||||
}
|
||||
catch (Exception ex) { _dbConnFormError = ex.Message; }
|
||||
}
|
||||
|
||||
private async Task DeleteDbConn(DatabaseConnectionDefinition dc)
|
||||
{
|
||||
if (!await _confirmDialog.ShowAsync($"Delete '{dc.Name}'?", "Delete DB Connection")) return;
|
||||
@@ -289,44 +188,16 @@
|
||||
{
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h6 class="mb-0">Notification Lists</h6>
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => { _showNotifForm = true; _editingNotifList = null; _notifName = string.Empty; _notifFormError = null; }">Add List</button>
|
||||
<button class="btn btn-primary btn-sm" @onclick='() => NavigationManager.NavigateTo("/design/notification-lists/create")'>Add List</button>
|
||||
</div>
|
||||
|
||||
@if (_showNotifForm)
|
||||
{
|
||||
<div class="card mb-2"><div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-4"><label class="form-label small">Name</label><input type="text" class="form-control form-control-sm" @bind="_notifName" /></div>
|
||||
<div class="col-md-4">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveNotifList">Save</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => _showNotifForm = false">Cancel</button></div>
|
||||
</div>
|
||||
@if (_notifFormError != null) { <div class="text-danger small mt-1">@_notifFormError</div> }
|
||||
</div></div>
|
||||
}
|
||||
|
||||
@if (_showRecipientForm)
|
||||
{
|
||||
<div class="card mb-2"><div class="card-body">
|
||||
<h6 class="card-title small">Add Recipient</h6>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3"><label class="form-label small">Name</label><input type="text" class="form-control form-control-sm" @bind="_recipientName" /></div>
|
||||
<div class="col-md-3"><label class="form-label small">Email</label><input type="email" class="form-control form-control-sm" @bind="_recipientEmail" /></div>
|
||||
<div class="col-md-3">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveRecipient">Add</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => _showRecipientForm = false">Cancel</button></div>
|
||||
</div>
|
||||
@if (_recipientFormError != null) { <div class="text-danger small mt-1">@_recipientFormError</div> }
|
||||
</div></div>
|
||||
}
|
||||
|
||||
@foreach (var list in _notificationLists)
|
||||
{
|
||||
<div class="card mb-2">
|
||||
<div class="card-header d-flex justify-content-between align-items-center py-2">
|
||||
<strong>@list.Name</strong>
|
||||
<div>
|
||||
<button class="btn btn-outline-info btn-sm py-0 px-1 me-1" @onclick="() => { _showRecipientForm = true; _recipientListId = list.Id; _recipientName = _recipientEmail = string.Empty; _recipientFormError = null; }">+ Recipient</button>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1" @onclick='() => NavigationManager.NavigateTo($"/design/notification-lists/{list.Id}/edit")'>Edit</button>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1" @onclick="() => DeleteNotifList(list)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -344,7 +215,6 @@
|
||||
{
|
||||
<span class="badge bg-light text-dark me-1 mb-1">
|
||||
@r.Name <@r.EmailAddress>
|
||||
<button type="button" class="btn-close ms-1" style="font-size: 0.5rem;" @onclick="() => DeleteRecipient(r)"></button>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
@@ -353,19 +223,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
private async Task SaveNotifList()
|
||||
{
|
||||
_notifFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_notifName)) { _notifFormError = "Name required."; return; }
|
||||
try
|
||||
{
|
||||
if (_editingNotifList != null) { _editingNotifList.Name = _notifName.Trim(); await NotificationRepository.UpdateNotificationListAsync(_editingNotifList); }
|
||||
else { var nl = new NotificationList(_notifName.Trim()); await NotificationRepository.AddNotificationListAsync(nl); }
|
||||
await NotificationRepository.SaveChangesAsync(); _showNotifForm = false; _toast.ShowSuccess("Saved."); await LoadAllAsync();
|
||||
}
|
||||
catch (Exception ex) { _notifFormError = ex.Message; }
|
||||
}
|
||||
|
||||
private async Task DeleteNotifList(NotificationList list)
|
||||
{
|
||||
if (!await _confirmDialog.ShowAsync($"Delete notification list '{list.Name}'?", "Delete")) return;
|
||||
@@ -373,54 +230,14 @@
|
||||
catch (Exception ex) { _toast.ShowError(ex.Message); }
|
||||
}
|
||||
|
||||
private async Task SaveRecipient()
|
||||
{
|
||||
_recipientFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_recipientName) || string.IsNullOrWhiteSpace(_recipientEmail)) { _recipientFormError = "Name and email required."; return; }
|
||||
try
|
||||
{
|
||||
var r = new NotificationRecipient(_recipientName.Trim(), _recipientEmail.Trim()) { NotificationListId = _recipientListId };
|
||||
await NotificationRepository.AddRecipientAsync(r); await NotificationRepository.SaveChangesAsync();
|
||||
_showRecipientForm = false; _toast.ShowSuccess("Recipient added."); await LoadAllAsync();
|
||||
}
|
||||
catch (Exception ex) { _recipientFormError = ex.Message; }
|
||||
}
|
||||
|
||||
private async Task DeleteRecipient(NotificationRecipient r)
|
||||
{
|
||||
try { await NotificationRepository.DeleteRecipientAsync(r.Id); await NotificationRepository.SaveChangesAsync(); _toast.ShowSuccess("Removed."); await LoadAllAsync(); }
|
||||
catch (Exception ex) { _toast.ShowError(ex.Message); }
|
||||
}
|
||||
|
||||
// ==== Inbound API Methods ====
|
||||
private RenderFragment RenderInboundApiMethods() => __builder =>
|
||||
{
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h6 class="mb-0">Inbound API Methods</h6>
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => { _showApiMethodForm = true; _editingApiMethod = null; _apiMethodName = _apiMethodScript = string.Empty; _apiMethodTimeout = 30; _apiMethodParams = _apiMethodReturn = null; _apiMethodFormError = null; }">Add Method</button>
|
||||
<button class="btn btn-primary btn-sm" @onclick='() => NavigationManager.NavigateTo("/design/api-methods/create")'>Add Method</button>
|
||||
</div>
|
||||
|
||||
@if (_showApiMethodForm)
|
||||
{
|
||||
<div class="card mb-2"><div class="card-body">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3"><label class="form-label small">Name</label><input type="text" class="form-control form-control-sm" @bind="_apiMethodName" disabled="@(_editingApiMethod != null)" /></div>
|
||||
<div class="col-md-2"><label class="form-label small">Timeout (s)</label><input type="number" class="form-control form-control-sm" @bind="_apiMethodTimeout" /></div>
|
||||
<div class="col-md-3"><label class="form-label small">Params (JSON)</label><input type="text" class="form-control form-control-sm" @bind="_apiMethodParams" /></div>
|
||||
<div class="col-md-3"><label class="form-label small">Returns (JSON)</label><input type="text" class="form-control form-control-sm" @bind="_apiMethodReturn" /></div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<label class="form-label small">Script</label>
|
||||
<textarea class="form-control form-control-sm font-monospace" rows="5" @bind="_apiMethodScript" style="font-size: 0.8rem;"></textarea>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveApiMethod">Save</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => _showApiMethodForm = false">Cancel</button>
|
||||
</div>
|
||||
@if (_apiMethodFormError != null) { <div class="text-danger small mt-1">@_apiMethodFormError</div> }
|
||||
</div></div>
|
||||
}
|
||||
|
||||
<table class="table table-sm table-striped">
|
||||
<thead class="table-dark"><tr><th>Name</th><th>Timeout</th><th>Script (preview)</th><th style="width:120px;">Actions</th></tr></thead>
|
||||
<tbody>
|
||||
@@ -431,7 +248,7 @@
|
||||
<td>@m.TimeoutSeconds s</td>
|
||||
<td class="small font-monospace text-truncate" style="max-width:300px;">@m.Script[..Math.Min(60, m.Script.Length)]</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1" @onclick="() => { _editingApiMethod = m; _apiMethodName = m.Name; _apiMethodScript = m.Script; _apiMethodTimeout = m.TimeoutSeconds; _apiMethodParams = m.ParameterDefinitions; _apiMethodReturn = m.ReturnDefinition; _showApiMethodForm = true; }">Edit</button>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1" @onclick='() => NavigationManager.NavigateTo($"/design/api-methods/{m.Id}/edit")'>Edit</button>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1" @onclick="() => DeleteApiMethod(m)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -440,19 +257,6 @@
|
||||
</table>
|
||||
};
|
||||
|
||||
private async Task SaveApiMethod()
|
||||
{
|
||||
_apiMethodFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_apiMethodName) || string.IsNullOrWhiteSpace(_apiMethodScript)) { _apiMethodFormError = "Name and script required."; return; }
|
||||
try
|
||||
{
|
||||
if (_editingApiMethod != null) { _editingApiMethod.Script = _apiMethodScript; _editingApiMethod.TimeoutSeconds = _apiMethodTimeout; _editingApiMethod.ParameterDefinitions = _apiMethodParams?.Trim(); _editingApiMethod.ReturnDefinition = _apiMethodReturn?.Trim(); await InboundApiRepository.UpdateApiMethodAsync(_editingApiMethod); }
|
||||
else { var m = new ApiMethod(_apiMethodName.Trim(), _apiMethodScript) { TimeoutSeconds = _apiMethodTimeout, ParameterDefinitions = _apiMethodParams?.Trim(), ReturnDefinition = _apiMethodReturn?.Trim() }; await InboundApiRepository.AddApiMethodAsync(m); }
|
||||
await InboundApiRepository.SaveChangesAsync(); _showApiMethodForm = false; _toast.ShowSuccess("Saved."); await LoadAllAsync();
|
||||
}
|
||||
catch (Exception ex) { _apiMethodFormError = ex.Message; }
|
||||
}
|
||||
|
||||
private async Task DeleteApiMethod(ApiMethod m)
|
||||
{
|
||||
if (!await _confirmDialog.ShowAsync($"Delete API method '{m.Name}'?", "Delete")) return;
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
@page "/design/notification-lists/create"
|
||||
@page "/design/notification-lists/{Id:int}/edit"
|
||||
@using ScadaLink.Security
|
||||
@using ScadaLink.Commons.Entities.Notifications
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject INotificationRepository NotificationRepository
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<button class="btn btn-link text-decoration-none ps-0 mb-2" @onclick="GoBack">← Back</button>
|
||||
|
||||
<h4 class="mb-3">@(Id.HasValue ? "Edit Notification List" : "Add Notification List")</h4>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingSpinner IsLoading="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_name" />
|
||||
</div>
|
||||
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mb-2">@_formError</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-success" @onclick="Save">Save</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Id.HasValue)
|
||||
{
|
||||
<h5 class="mt-4 mb-3">Recipients</h5>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_recipientName" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Email</label>
|
||||
<input type="email" class="form-control form-control-sm" @bind="_recipientEmail" />
|
||||
</div>
|
||||
@if (_recipientFormError != null)
|
||||
{
|
||||
<div class="text-danger small mt-2">@_recipientFormError</div>
|
||||
}
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-success btn-sm" @onclick="SaveRecipient">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm table-striped">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th style="width:80px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (_recipients.Count == 0)
|
||||
{
|
||||
<tr><td colspan="3" class="text-muted small">No recipients.</td></tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var r in _recipients)
|
||||
{
|
||||
<tr>
|
||||
<td>@r.Name</td>
|
||||
<td>@r.EmailAddress</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1" @onclick="() => DeleteRecipient(r)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public int? Id { get; set; }
|
||||
|
||||
private bool _loading = true;
|
||||
private string _name = "";
|
||||
private string? _formError;
|
||||
|
||||
private NotificationList? _existing;
|
||||
|
||||
// Recipients
|
||||
private List<NotificationRecipient> _recipients = new();
|
||||
private string _recipientName = "", _recipientEmail = "";
|
||||
private string? _recipientFormError;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Id.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
_existing = await NotificationRepository.GetNotificationListByIdAsync(Id.Value);
|
||||
if (_existing != null)
|
||||
{
|
||||
_name = _existing.Name;
|
||||
}
|
||||
_recipients = (await NotificationRepository.GetRecipientsByListIdAsync(Id.Value)).ToList();
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
}
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
_formError = null;
|
||||
if (string.IsNullOrWhiteSpace(_name))
|
||||
{
|
||||
_formError = "Name required.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_existing != null)
|
||||
{
|
||||
_existing.Name = _name.Trim();
|
||||
await NotificationRepository.UpdateNotificationListAsync(_existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var nl = new NotificationList(_name.Trim());
|
||||
await NotificationRepository.AddNotificationListAsync(nl);
|
||||
}
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
NavigationManager.NavigateTo("/design/external-systems");
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
}
|
||||
|
||||
private async Task SaveRecipient()
|
||||
{
|
||||
_recipientFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_recipientName) || string.IsNullOrWhiteSpace(_recipientEmail))
|
||||
{
|
||||
_recipientFormError = "Name and email required.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var r = new NotificationRecipient(_recipientName.Trim(), _recipientEmail.Trim())
|
||||
{
|
||||
NotificationListId = Id!.Value
|
||||
};
|
||||
await NotificationRepository.AddRecipientAsync(r);
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
_recipientName = _recipientEmail = string.Empty;
|
||||
_recipients = (await NotificationRepository.GetRecipientsByListIdAsync(Id.Value)).ToList();
|
||||
}
|
||||
catch (Exception ex) { _recipientFormError = ex.Message; }
|
||||
}
|
||||
|
||||
private async Task DeleteRecipient(NotificationRecipient r)
|
||||
{
|
||||
try
|
||||
{
|
||||
await NotificationRepository.DeleteRecipientAsync(r.Id);
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
_recipients = (await NotificationRepository.GetRecipientsByListIdAsync(Id!.Value)).ToList();
|
||||
}
|
||||
catch (Exception ex) { _recipientFormError = ex.Message; }
|
||||
}
|
||||
|
||||
private void GoBack() => NavigationManager.NavigateTo("/design/external-systems");
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
@page "/design/shared-scripts/create"
|
||||
@page "/design/shared-scripts/{Id:int}/edit"
|
||||
@using ScadaLink.Security
|
||||
@using ScadaLink.Commons.Entities.Scripts
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@using ScadaLink.TemplateEngine
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject ITemplateEngineRepository TemplateEngineRepository
|
||||
@inject SharedScriptService SharedScriptService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm me-3" @onclick="GoBack">← Back</button>
|
||||
<h4 class="mb-0">@(Id.HasValue ? $"Edit Shared Script: {_formName}" : "New Shared Script")</h4>
|
||||
</div>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingSpinner IsLoading="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_formName"
|
||||
disabled="@(Id.HasValue)" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Parameters (JSON)</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_formParameters"
|
||||
placeholder='e.g. [{"name":"x","type":"Int32"}]' />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Return Definition (JSON)</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_formReturn"
|
||||
placeholder='e.g. {"type":"Boolean"}' />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Code</label>
|
||||
<textarea class="form-control form-control-sm font-monospace" rows="10" @bind="_formCode"
|
||||
style="font-size: 0.8rem;"></textarea>
|
||||
</div>
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mt-2">@_formError</div>
|
||||
}
|
||||
@if (_syntaxCheckResult != null)
|
||||
{
|
||||
<div class="@(_syntaxCheckPassed ? "text-success" : "text-danger") small mt-1">@_syntaxCheckResult</div>
|
||||
}
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveScript">Save</button>
|
||||
<button class="btn btn-outline-info btn-sm me-1" @onclick="CheckCompilation">Check Syntax</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public int? Id { get; set; }
|
||||
|
||||
private bool _loading;
|
||||
private string _formName = string.Empty;
|
||||
private string _formCode = string.Empty;
|
||||
private string? _formParameters;
|
||||
private string? _formReturn;
|
||||
private string? _formError;
|
||||
private string? _syntaxCheckResult;
|
||||
private bool _syntaxCheckPassed;
|
||||
|
||||
private async Task<string> GetCurrentUserAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
return authState.User.FindFirst("Username")?.Value ?? "unknown";
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Id.HasValue)
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var scripts = await SharedScriptService.GetAllSharedScriptsAsync();
|
||||
var script = scripts.FirstOrDefault(s => s.Id == Id.Value);
|
||||
if (script != null)
|
||||
{
|
||||
_formName = script.Name;
|
||||
_formCode = script.Code;
|
||||
_formParameters = script.ParameterDefinitions;
|
||||
_formReturn = script.ReturnDefinition;
|
||||
}
|
||||
else
|
||||
{
|
||||
_formError = $"Shared script with ID {Id.Value} not found.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_formError = $"Failed to load script: {ex.Message}";
|
||||
}
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack()
|
||||
{
|
||||
NavigationManager.NavigateTo("/design/shared-scripts");
|
||||
}
|
||||
|
||||
private void CheckCompilation()
|
||||
{
|
||||
var syntaxError = ValidateSyntaxLocally(_formCode);
|
||||
if (syntaxError == null)
|
||||
{
|
||||
_syntaxCheckResult = "Syntax check passed.";
|
||||
_syntaxCheckPassed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_syntaxCheckResult = syntaxError;
|
||||
_syntaxCheckPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveScript()
|
||||
{
|
||||
_formError = null;
|
||||
_syntaxCheckResult = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (Id.HasValue)
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await SharedScriptService.UpdateSharedScriptAsync(
|
||||
Id.Value, _formCode, _formParameters?.Trim(), _formReturn?.Trim(), user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
NavigationManager.NavigateTo("/design/shared-scripts");
|
||||
}
|
||||
else
|
||||
{
|
||||
_formError = result.Error;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await SharedScriptService.CreateSharedScriptAsync(
|
||||
_formName.Trim(), _formCode, _formParameters?.Trim(), _formReturn?.Trim(), user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
NavigationManager.NavigateTo("/design/shared-scripts");
|
||||
}
|
||||
else
|
||||
{
|
||||
_formError = result.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_formError = $"Save failed: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Basic syntax check: balanced braces/brackets/parens.
|
||||
/// Mirrors the internal SharedScriptService.ValidateSyntax logic.
|
||||
/// </summary>
|
||||
private static string? ValidateSyntaxLocally(string code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code)) return "Script code cannot be empty.";
|
||||
int brace = 0, bracket = 0, paren = 0;
|
||||
foreach (var ch in code)
|
||||
{
|
||||
switch (ch) { case '{': brace++; break; case '}': brace--; break; case '[': bracket++; break; case ']': bracket--; break; case '(': paren++; break; case ')': paren--; break; }
|
||||
if (brace < 0) return "Syntax error: unmatched closing brace '}'.";
|
||||
if (bracket < 0) return "Syntax error: unmatched closing bracket ']'.";
|
||||
if (paren < 0) return "Syntax error: unmatched closing parenthesis ')'.";
|
||||
}
|
||||
if (brace != 0) return "Syntax error: unmatched opening brace '{'.";
|
||||
if (bracket != 0) return "Syntax error: unmatched opening bracket '['.";
|
||||
if (paren != 0) return "Syntax error: unmatched opening parenthesis '('.";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,12 @@
|
||||
@inject ITemplateEngineRepository TemplateEngineRepository
|
||||
@inject SharedScriptService SharedScriptService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0">Shared Scripts</h4>
|
||||
<button class="btn btn-primary btn-sm" @onclick="ShowAddForm">New Script</button>
|
||||
<button class="btn btn-primary btn-sm" @onclick='() => NavigationManager.NavigateTo("/design/shared-scripts/create")'>New Script</button>
|
||||
</div>
|
||||
|
||||
<ToastNotification @ref="_toast" />
|
||||
@@ -27,50 +28,6 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (_showForm)
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">@(_editingScript == null ? "New Shared Script" : $"Edit: {_editingScript.Name}")</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_formName"
|
||||
disabled="@(_editingScript != null)" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Parameters (JSON)</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_formParameters"
|
||||
placeholder='e.g. [{"name":"x","type":"Int32"}]' />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Return Definition (JSON)</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_formReturn"
|
||||
placeholder='e.g. {"type":"Boolean"}' />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<label class="form-label small">Code</label>
|
||||
<textarea class="form-control form-control-sm font-monospace" rows="10" @bind="_formCode"
|
||||
style="font-size: 0.8rem;"></textarea>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveScript">Save</button>
|
||||
<button class="btn btn-outline-info btn-sm me-1" @onclick="CheckCompilation">Check Syntax</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelForm">Cancel</button>
|
||||
</div>
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mt-1">@_formError</div>
|
||||
}
|
||||
@if (_syntaxCheckResult != null)
|
||||
{
|
||||
<div class="@(_syntaxCheckPassed ? "text-success" : "text-danger") small mt-1">@_syntaxCheckResult</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
@@ -101,7 +58,7 @@
|
||||
<td class="small text-muted">@(script.ReturnDefinition ?? "—")</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1"
|
||||
@onclick="() => EditScript(script)">Edit</button>
|
||||
@onclick='() => NavigationManager.NavigateTo($"/design/shared-scripts/{script.Id}/edit")'>Edit</button>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1"
|
||||
@onclick="() => DeleteScript(script)">Delete</button>
|
||||
</td>
|
||||
@@ -123,16 +80,6 @@
|
||||
private bool _loading = true;
|
||||
private string? _errorMessage;
|
||||
|
||||
private bool _showForm;
|
||||
private SharedScript? _editingScript;
|
||||
private string _formName = string.Empty;
|
||||
private string _formCode = string.Empty;
|
||||
private string? _formParameters;
|
||||
private string? _formReturn;
|
||||
private string? _formError;
|
||||
private string? _syntaxCheckResult;
|
||||
private bool _syntaxCheckPassed;
|
||||
|
||||
private ToastNotification _toast = default!;
|
||||
private ConfirmDialog _confirmDialog = default!;
|
||||
|
||||
@@ -156,118 +103,6 @@
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private void ShowAddForm()
|
||||
{
|
||||
_editingScript = null;
|
||||
_formName = string.Empty;
|
||||
_formCode = string.Empty;
|
||||
_formParameters = null;
|
||||
_formReturn = null;
|
||||
_formError = null;
|
||||
_syntaxCheckResult = null;
|
||||
_showForm = true;
|
||||
}
|
||||
|
||||
private void EditScript(SharedScript script)
|
||||
{
|
||||
_editingScript = script;
|
||||
_formName = script.Name;
|
||||
_formCode = script.Code;
|
||||
_formParameters = script.ParameterDefinitions;
|
||||
_formReturn = script.ReturnDefinition;
|
||||
_formError = null;
|
||||
_syntaxCheckResult = null;
|
||||
_showForm = true;
|
||||
}
|
||||
|
||||
private void CancelForm()
|
||||
{
|
||||
_showForm = false;
|
||||
_editingScript = null;
|
||||
}
|
||||
|
||||
private void CheckCompilation()
|
||||
{
|
||||
var syntaxError = ValidateSyntaxLocally(_formCode);
|
||||
if (syntaxError == null)
|
||||
{
|
||||
_syntaxCheckResult = "Syntax check passed.";
|
||||
_syntaxCheckPassed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_syntaxCheckResult = syntaxError;
|
||||
_syntaxCheckPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveScript()
|
||||
{
|
||||
_formError = null;
|
||||
_syntaxCheckResult = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (_editingScript != null)
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await SharedScriptService.UpdateSharedScriptAsync(
|
||||
_editingScript.Id, _formCode, _formParameters?.Trim(), _formReturn?.Trim(), user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showForm = false;
|
||||
_toast.ShowSuccess($"Script '{_editingScript.Name}' updated.");
|
||||
await LoadDataAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_formError = result.Error;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await SharedScriptService.CreateSharedScriptAsync(
|
||||
_formName.Trim(), _formCode, _formParameters?.Trim(), _formReturn?.Trim(), user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showForm = false;
|
||||
_toast.ShowSuccess($"Script '{_formName}' created.");
|
||||
await LoadDataAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_formError = result.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_formError = $"Save failed: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Basic syntax check: balanced braces/brackets/parens.
|
||||
/// Mirrors the internal SharedScriptService.ValidateSyntax logic.
|
||||
/// </summary>
|
||||
private static string? ValidateSyntaxLocally(string code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code)) return "Script code cannot be empty.";
|
||||
int brace = 0, bracket = 0, paren = 0;
|
||||
foreach (var ch in code)
|
||||
{
|
||||
switch (ch) { case '{': brace++; break; case '}': brace--; break; case '[': bracket++; break; case ']': bracket--; break; case '(': paren++; break; case ')': paren--; break; }
|
||||
if (brace < 0) return "Syntax error: unmatched closing brace '}'.";
|
||||
if (bracket < 0) return "Syntax error: unmatched closing bracket ']'.";
|
||||
if (paren < 0) return "Syntax error: unmatched closing parenthesis ')'.";
|
||||
}
|
||||
if (brace != 0) return "Syntax error: unmatched opening brace '{'.";
|
||||
if (bracket != 0) return "Syntax error: unmatched opening bracket '['.";
|
||||
if (paren != 0) return "Syntax error: unmatched opening parenthesis '('.";
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task DeleteScript(SharedScript script)
|
||||
{
|
||||
var confirmed = await _confirmDialog.ShowAsync(
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
@page "/design/templates/create"
|
||||
@using ScadaLink.Security
|
||||
@using ScadaLink.Commons.Entities.Templates
|
||||
@using ScadaLink.Commons.Interfaces.Repositories
|
||||
@using ScadaLink.TemplateEngine
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject ITemplateEngineRepository TemplateEngineRepository
|
||||
@inject TemplateService TemplateService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="GoBack">← Back</button>
|
||||
</div>
|
||||
|
||||
<h4 class="mb-3">Create Template</h4>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingSpinner IsLoading="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_createName" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Parent Template</label>
|
||||
<select class="form-select form-select-sm" @bind="_createParentId">
|
||||
<option value="0">(None - root template)</option>
|
||||
@foreach (var t in _templates)
|
||||
{
|
||||
<option value="@t.Id">@t.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Description</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_createDescription" />
|
||||
</div>
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mt-2">@_formError</div>
|
||||
}
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="CreateTemplate">Create</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<Template> _templates = new();
|
||||
private bool _loading = true;
|
||||
|
||||
private string _createName = string.Empty;
|
||||
private int _createParentId;
|
||||
private string? _createDescription;
|
||||
private string? _formError;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_templates = (await TemplateEngineRepository.GetAllTemplatesAsync()).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_formError = $"Failed to load templates: {ex.Message}";
|
||||
}
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task CreateTemplate()
|
||||
{
|
||||
_formError = null;
|
||||
if (string.IsNullOrWhiteSpace(_createName)) { _formError = "Name is required."; return; }
|
||||
|
||||
try
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.CreateTemplateAsync(
|
||||
_createName.Trim(), _createDescription?.Trim(),
|
||||
_createParentId == 0 ? null : _createParentId, user);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
NavigationManager.NavigateTo("/design/templates");
|
||||
}
|
||||
else
|
||||
{
|
||||
_formError = result.Error;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_formError = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack()
|
||||
{
|
||||
NavigationManager.NavigateTo("/design/templates");
|
||||
}
|
||||
|
||||
private async Task<string> GetCurrentUserAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
return authState.User.FindFirst("Username")?.Value ?? "unknown";
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
@inject ITemplateEngineRepository TemplateEngineRepository
|
||||
@inject TemplateService TemplateService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<ToastNotification @ref="_toast" />
|
||||
@@ -29,46 +30,9 @@
|
||||
@* Template list view *@
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0">Templates</h4>
|
||||
<button class="btn btn-primary btn-sm" @onclick="ShowCreateForm">New Template</button>
|
||||
<button class="btn btn-primary btn-sm" @onclick='() => NavigationManager.NavigateTo("/design/templates/create")'>New Template</button>
|
||||
</div>
|
||||
|
||||
@if (_showCreateForm)
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Create Template</h6>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_createName" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Parent Template</label>
|
||||
<select class="form-select form-select-sm" @bind="_createParentId">
|
||||
<option value="0">(None - root template)</option>
|
||||
@foreach (var t in _templates)
|
||||
{
|
||||
<option value="@t.Id">@t.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Description</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_createDescription" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="CreateTemplate">Create</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => _showCreateForm = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (_createError != null)
|
||||
{
|
||||
<div class="text-danger small mt-1">@_createError</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Inheritance tree visualization *@
|
||||
<div class="card">
|
||||
<div class="card-body p-2">
|
||||
@@ -254,13 +218,6 @@
|
||||
private string? _errorMessage;
|
||||
private string _activeTab = "attributes";
|
||||
|
||||
// Create form
|
||||
private bool _showCreateForm;
|
||||
private string _createName = string.Empty;
|
||||
private int _createParentId;
|
||||
private string? _createDescription;
|
||||
private string? _createError;
|
||||
|
||||
// Edit properties
|
||||
private string _editName = string.Empty;
|
||||
private string? _editDescription;
|
||||
@@ -377,44 +334,6 @@
|
||||
_validationResult = null;
|
||||
}
|
||||
|
||||
private void ShowCreateForm()
|
||||
{
|
||||
_createName = string.Empty;
|
||||
_createParentId = 0;
|
||||
_createDescription = null;
|
||||
_createError = null;
|
||||
_showCreateForm = true;
|
||||
}
|
||||
|
||||
private async Task CreateTemplate()
|
||||
{
|
||||
_createError = null;
|
||||
if (string.IsNullOrWhiteSpace(_createName)) { _createError = "Name is required."; return; }
|
||||
|
||||
try
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.CreateTemplateAsync(
|
||||
_createName.Trim(), _createDescription?.Trim(),
|
||||
_createParentId == 0 ? null : _createParentId, user);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showCreateForm = false;
|
||||
_toast.ShowSuccess($"Template '{_createName}' created.");
|
||||
await LoadTemplatesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_createError = result.Error;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_createError = $"Create failed: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteTemplate(Template template)
|
||||
{
|
||||
var confirmed = await _confirmDialog.ShowAsync(
|
||||
|
||||
Reference in New Issue
Block a user