feat(sender-editor): consume bootstrap payload and auto-save draft
Accept bootstrapToken query parameter and consume one-time payload from sessionStorage-backed service. Hydrate editor state with title, message, envelope type, receivers, and PDF document from bootstrap data. Refactor draft save into reusable core flow and trigger non-navigating auto-save after bootstrap is applied. Prevent stale token replay by clearing query token from URL and handling invalid tokens gracefully.
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
@inject ReceiverService ReceiverService
|
||||
@inject EnvelopeService EnvelopeService
|
||||
@inject EnvelopeTypeService EnvelopeTypeService
|
||||
@inject CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
|
||||
@inject AuthService AuthService
|
||||
@inject IStringLocalizer<Resource> UiLocalizer
|
||||
@inject IStringLocalizer<Model> ModelLocalizer
|
||||
@@ -589,6 +590,9 @@
|
||||
[SupplyParameterFromQuery(Name = "envelopeId")]
|
||||
public int? EnvelopeId { get; set; }
|
||||
|
||||
[SupplyParameterFromQuery(Name = "bootstrapToken")]
|
||||
public string? BootstrapToken { get; set; }
|
||||
|
||||
// ── Constants ──
|
||||
// Signature field size in PDF points (fixed): 1.77" × 1.96"
|
||||
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
||||
@@ -609,6 +613,8 @@
|
||||
int? _loadedEnvelopeId;
|
||||
int? _editingEnvelopeId;
|
||||
bool _isLoadingEnvelopeForEdit;
|
||||
bool _bootstrapApplied;
|
||||
string? _pendingBootstrapToken;
|
||||
|
||||
List<SignatureFieldDraft> _signatureFields = [];
|
||||
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
||||
@@ -629,6 +635,7 @@
|
||||
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
|
||||
string _saveSuccessBody = string.Empty;
|
||||
bool _saveNavigateToDashboard = false;
|
||||
bool _saveDraftAfterBootstrap;
|
||||
|
||||
// ── Envelope metadata ──
|
||||
string _envelopeTitle = string.Empty;
|
||||
@@ -697,6 +704,9 @@
|
||||
if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
|
||||
_editingEnvelopeId = EnvelopeId.Value;
|
||||
|
||||
if (!IsEditMode && !_bootstrapApplied && !string.IsNullOrWhiteSpace(BootstrapToken))
|
||||
_pendingBootstrapToken = BootstrapToken;
|
||||
|
||||
if (IsEditMode && _loadedEnvelopeId != EnvelopeId)
|
||||
{
|
||||
await LoadEnvelopeForEditAsync(EnvelopeId!.Value);
|
||||
@@ -707,15 +717,91 @@
|
||||
return;
|
||||
}
|
||||
|
||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
||||
async Task TryApplyBootstrapAsync(string token)
|
||||
{
|
||||
var payload = await CreateEnvelopeBootstrapStorageService.ConsumeAsync(token);
|
||||
_bootstrapApplied = true;
|
||||
|
||||
if (payload is null)
|
||||
{
|
||||
NavigationManager.NavigateTo("/sender/editor", replace: true);
|
||||
return;
|
||||
}
|
||||
|
||||
_envelopeTitle = payload.Title ?? string.Empty;
|
||||
_envelopeMessage = payload.Message ?? string.Empty;
|
||||
_selectedEnvelopeTypeId = payload.EnvelopeTypeId;
|
||||
_language = string.IsNullOrWhiteSpace(payload.Language) ? "de-DE" : payload.Language;
|
||||
|
||||
_receivers = payload.Receivers
|
||||
.Select((receiver, index) => new ReceiverDraft(
|
||||
Guid.NewGuid(),
|
||||
receiver.FullName,
|
||||
receiver.EmailAddress,
|
||||
string.Empty,
|
||||
ReceiverPalette[index % ReceiverPalette.Length]))
|
||||
.ToList();
|
||||
|
||||
_originalPdfBytes = payload.DocumentBytes;
|
||||
_fileName = string.IsNullOrWhiteSpace(payload.DocumentFileName) ? "document.pdf" : payload.DocumentFileName;
|
||||
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
|
||||
|
||||
_signatureFields.Clear();
|
||||
_pendingReceiverForPlacement = null;
|
||||
|
||||
_saveDraftAfterBootstrap = true;
|
||||
|
||||
if (_pdfLoaded && _originalPdfBytes is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_pagePreviews = (await PdfRenderService.RenderPagesAsync(_originalPdfBytes))
|
||||
.OrderBy(p => p.PageNumber)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to render bootstrap PDF previews.");
|
||||
_pagePreviews = [];
|
||||
}
|
||||
|
||||
_pageCount = _pagePreviews.Count > 0
|
||||
? _pagePreviews.Count
|
||||
: EstimatePdfPageCount(_originalPdfBytes) ?? 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
_pagePreviews = [];
|
||||
_pageCount = 0;
|
||||
}
|
||||
|
||||
NavigationManager.NavigateTo("/sender/editor", replace: true);
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_isPageReady = true;
|
||||
StateHasChanged();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
if (!OperatingSystem.IsBrowser())
|
||||
return;
|
||||
|
||||
if (_saveDraftAfterBootstrap && !_isSaving)
|
||||
{
|
||||
_saveDraftAfterBootstrap = false;
|
||||
await SaveDraftAndStayOnEditorAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsEditMode && !_bootstrapApplied && !string.IsNullOrWhiteSpace(_pendingBootstrapToken))
|
||||
{
|
||||
var token = _pendingBootstrapToken;
|
||||
_pendingBootstrapToken = null;
|
||||
await TryApplyBootstrapAsync(token);
|
||||
}
|
||||
}
|
||||
|
||||
async Task LoadEnvelopeForEditAsync(int envelopeId)
|
||||
@@ -1108,12 +1194,17 @@
|
||||
}
|
||||
|
||||
async Task SaveDraftAsync()
|
||||
{
|
||||
await SaveDraftCoreAsync(navigateToDashboard: true);
|
||||
}
|
||||
|
||||
async Task<int?> SaveDraftCoreAsync(bool navigateToDashboard)
|
||||
{
|
||||
if (!ValidateSettings(out var settingsError))
|
||||
{
|
||||
_saveErrorMessage = settingsError;
|
||||
_savePopupVisible = true;
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
_isSaving = true;
|
||||
@@ -1134,25 +1225,37 @@
|
||||
_loadedEnvelopeId = result.Id;
|
||||
_editingEnvelopeId = result.Id;
|
||||
|
||||
if (previousEnvelopeId != result.Id)
|
||||
if (navigateToDashboard)
|
||||
{
|
||||
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
||||
}
|
||||
else if (previousEnvelopeId != result.Id)
|
||||
{
|
||||
NavigationManager.NavigateTo($"/sender/editor?envelopeId={result.Id}", forceLoad: false);
|
||||
}
|
||||
|
||||
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
||||
return result.Id;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "[SenderEditor] Failed to save draft");
|
||||
_saveErrorMessage = ex.Message;
|
||||
_savePopupVisible = true;
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSaving = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
async Task SaveDraftAndStayOnEditorAsync()
|
||||
{
|
||||
Logger.LogInformation("[SenderEditor] Trigger bootstrap draft save.");
|
||||
await SaveDraftCoreAsync(navigateToDashboard: false);
|
||||
}
|
||||
|
||||
// ── Send envelope ──
|
||||
async Task SendAsync()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user