Fix #5942: set the next execution time correctly.

This commit is contained in:
Ben
2026-01-08 11:39:32 +08:00
parent d4399955ac
commit 43ee682d1f
2 changed files with 228 additions and 151 deletions

View File

@@ -5,100 +5,108 @@
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> @if (_initialized)
<div class="container"> {
<div class="row mb-1 align-items-center"> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<Label Class="col-sm-3" For="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label> <div class="container">
<div class="col-sm-9"> @if (!_editable)
<input id="name" class="form-control" @bind="@_name" maxlength="200" required /> {
<div class="alert alert-warning alert-dismissible fade show mb-3 custom" role="alert">
@Localizer["JobNotEditable"]
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="200" required />
</div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label>
<Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="type" class="form-control" @bind="@_jobType" readonly />
<input id="type" class="form-control" @bind="@_jobType" readonly /> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label>
<Label Class="col-sm-3" For="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label> <div class="col-sm-9">
<div class="col-sm-9"> <select id="enabled" class="form-select" @bind="@_isEnabled" required>
<select id="enabled" class="form-select" @bind="@_isEnabled" required> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="True">@SharedLocalizer["Yes"]</option> <option value="False">@SharedLocalizer["No"]</option>
<option value="False">@SharedLocalizer["No"]</option> </select>
</select> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label>
<Label Class="col-sm-3" For="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="runs-every" class="form-control mb-1" @bind="@_interval" maxlength="4" required disabled="@(!_editable)" />
<input id="runs-every" class="form-control mb-1" @bind="@_interval" maxlength="4" required /> <select id="runs-every" class="form-select" @bind="@_frequency" required disabled="@(!_editable)">
<select id="runs-every" class="form-select" @bind="@_frequency" required> <option value="m">@Localizer["Minute(s)"]</option>
<option value="m">@Localizer["Minute(s)"]</option> <option value="H">@Localizer["Hour(s)"]</option>
<option value="H">@Localizer["Hour(s)"]</option> <option value="d">@Localizer["Day(s)"]</option>
<option value="d">@Localizer["Day(s)"]</option> <option value="w">@Localizer["Week(s)"]</option>
<option value="w">@Localizer["Week(s)"]</option> <option value="M">@Localizer["Month(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option> <option value="O">@Localizer["Once"]</option>
<option value="O">@Localizer["Once"]</option> </select>
</select> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="retention" type="number" min="0" step="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required disabled="@(!_editable)" />
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required /> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label>
<Label Class="col-sm-3" For="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <div class="row">
<div class="row"> <div class="col">
<div class="col"> <input id="starting" type="date" class="form-control" @bind="@_startDate" disabled="@(!_editable)" />
<input id="starting" type="date" class="form-control" @bind="@_startDate" /> </div>
</div> <div class="col">
<div class="col"> <input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" disabled="@(!_editable)" />
<input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" /> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label>
<Label Class="col-sm-3" For="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <div class="row">
<div class="row"> <div class="col">
<div class="col"> <input id="ending" type="date" class="form-control" @bind="@_endDate" disabled="@(!_editable)" />
<input id="ending" type="date" class="form-control" @bind="@_endDate" /> </div>
</div> <div class="col">
<div class="col"> <input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" disabled="@(!_editable)" />
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" /> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="next" HelpText="Optionally modify the date and time when this job should execute next" ResourceKey="NextExecution">Next Execution: </Label>
<Label Class="col-sm-3" For="next" HelpText="Optionally modify the date and time when this job should execute next" ResourceKey="NextExecution">Next Execution: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="next" class="form-control" @bind="@_nextDate" readonly />
<div class="row">
<div class="col">
<input id="next" type="date" class="form-control" @bind="@_nextDate" />
</div>
<div class="col">
<input id="next" type="time" class="form-control" placeholder="hh:mm" @bind="@_nextTime" required="@(_nextDate.HasValue)" />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <br />
<br /> <button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button> @if (!_editable)
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> {
<br /> <button type="button" class="btn btn-danger ms-1" @onclick="ExecuteJob">@SharedLocalizer["Execute"]</button>
<br /> }
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo> <NavLink class="btn btn-secondary ms-1" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</form> <br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
}
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private bool _initialized = false;
private bool _editable = true;
private int _jobId; private int _jobId;
private string _name = string.Empty; private string _name = string.Empty;
private string _jobType = string.Empty; private string _jobType = string.Empty;
@@ -113,26 +121,27 @@
private DateTime? _nextDate = null; private DateTime? _nextDate = null;
private DateTime? _nextTime = null; private DateTime? _nextTime = null;
private string createdby; private string createdby;
private DateTime createdon; private DateTime createdon;
private string modifiedby; private string modifiedby;
private DateTime modifiedon; private DateTime modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_jobId = Int32.Parse(PageState.QueryString["id"]); _jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId); Job job = await JobService.GetJobAsync(_jobId);
if (job != null) if (job != null)
{ {
_name = job.Name; _editable = !job.IsEnabled && !job.IsExecuting;
_jobType = job.JobType; _name = job.Name;
_isEnabled = job.IsEnabled.ToString(); _jobType = job.JobType;
_interval = job.Interval.ToString(); _isEnabled = job.IsEnabled.ToString();
_frequency = job.Frequency; _interval = job.Interval.ToString();
_startDate = UtcToLocal(job.StartDate); _frequency = job.Frequency;
_startDate = UtcToLocal(job.StartDate);
_startTime = UtcToLocal(job.StartDate); _startTime = UtcToLocal(job.StartDate);
_endDate = UtcToLocal(job.EndDate); _endDate = UtcToLocal(job.EndDate);
_endTime = UtcToLocal(job.EndDate); _endTime = UtcToLocal(job.EndDate);
@@ -140,70 +149,132 @@
_nextDate = UtcToLocal(job.NextExecution); _nextDate = UtcToLocal(job.NextExecution);
_nextTime = UtcToLocal(job.NextExecution); _nextTime = UtcToLocal(job.NextExecution);
createdby = job.CreatedBy; createdby = job.CreatedBy;
createdon = job.CreatedOn; createdon = job.CreatedOn;
modifiedby = job.ModifiedBy; modifiedby = job.ModifiedBy;
modifiedon = job.ModifiedOn; modifiedon = job.ModifiedOn;
} }
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
}
}
private async Task SaveJob() _initialized = true;
{ }
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
}
}
private async Task SaveJob()
{
if (!Utilities.ValidateEffectiveExpiryDates(_startDate, _endDate)) if (!Utilities.ValidateEffectiveExpiryDates(_startDate, _endDate))
{ {
AddModuleMessage(Localizer["Message.StartEndDateError"], MessageType.Warning); AddModuleMessage(Localizer["Message.StartEndDateError"], MessageType.Warning);
return; return;
} }
validated = true; validated = true;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
var job = await JobService.GetJobAsync(_jobId); var job = await JobService.GetJobAsync(_jobId);
job.Name = _name; job.Name = _name;
job.JobType = _jobType; job.JobType = _jobType;
job.IsEnabled = Boolean.Parse(_isEnabled); var enabledChanged = job.IsEnabled != Boolean.Parse(_isEnabled);
job.Frequency = _frequency; job.IsEnabled = Boolean.Parse(_isEnabled);
if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
job.StartDate = _startDate.HasValue && _startTime.HasValue
? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
job.EndDate = _endDate.HasValue && _endTime.HasValue if (_editable)
? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay)) {
: null; job.Frequency = _frequency;
if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
job.StartDate = _startDate.HasValue && _startTime.HasValue
? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
job.NextExecution = _nextDate.HasValue && _nextTime.HasValue job.EndDate = _endDate.HasValue && _endTime.HasValue
? LocalToUtc(_nextDate.GetValueOrDefault().Date.Add(_nextTime.GetValueOrDefault().TimeOfDay)) ? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay))
: null; : null;
job.RetentionHistory = int.Parse(_retentionHistory);
try job.RetentionHistory = int.Parse(_retentionHistory);
{ }
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job); if (!job.IsEnabled)
NavigationManager.NavigateTo(NavigateUrl()); {
} job.NextExecution = null;
catch (Exception ex) }
{ else if (enabledChanged)
await logger.LogError(ex, "Error Updating Job {Job} {Error}", job, ex.Message); {
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error); job.NextExecution = null;
} if(job.StartDate != null && job.StartDate < DateTime.UtcNow)
} {
else var startDate = DateTime.UtcNow;
{ if ((job.Frequency == "d" || job.Frequency == "w" || job.Frequency == "M") && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning); {
} // set the start time
} startDate = startDate.Date.Add(job.StartDate.Value.TimeOfDay);
if(startDate < DateTime.UtcNow)
{
switch (job.Frequency)
{
case "d":
startDate = startDate.AddDays(job.Interval);
break;
case "w":
startDate = startDate.AddDays(job.Interval * 7);
break;
case "M":
startDate = startDate.AddMonths(job.Interval);
break;
}
}
}
job.StartDate = startDate;
}
}
await PerformSaveJobAction(job);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
private async Task ExecuteJob()
{
var job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
job.NextExecution = null;
await PerformSaveJobAction(job);
if(!job.IsStarted)
{
await JobService.StartJobAsync(_jobId);
}
NavigationManager.NavigateTo(NavigateUrl());
}
}
private async Task PerformSaveJobAction(Job job)
{
try
{
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Updating Job {Job} {Error}", job, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
} }

View File

@@ -195,4 +195,10 @@
<data name="Message.StartEndDateError" xml:space="preserve"> <data name="Message.StartEndDateError" xml:space="preserve">
<value>Start Date cannot be after End Date.</value> <value>Start Date cannot be after End Date.</value>
</data> </data>
<data name="JobNotEditable" xml:space="preserve">
<value>The job properties is not able to be modified because it's enabled or running in progress.</value>
</data>
<data name="Execute" xml:space="preserve">
<value>Execute</value>
</data>
</root> </root>