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 ReceiverService ReceiverService
|
||||||
@inject EnvelopeService EnvelopeService
|
@inject EnvelopeService EnvelopeService
|
||||||
@inject EnvelopeTypeService EnvelopeTypeService
|
@inject EnvelopeTypeService EnvelopeTypeService
|
||||||
|
@inject CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
|
||||||
@inject AuthService AuthService
|
@inject AuthService AuthService
|
||||||
@inject IStringLocalizer<Resource> UiLocalizer
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
@inject IStringLocalizer<Model> ModelLocalizer
|
@inject IStringLocalizer<Model> ModelLocalizer
|
||||||
@@ -589,6 +590,9 @@
|
|||||||
[SupplyParameterFromQuery(Name = "envelopeId")]
|
[SupplyParameterFromQuery(Name = "envelopeId")]
|
||||||
public int? EnvelopeId { get; set; }
|
public int? EnvelopeId { get; set; }
|
||||||
|
|
||||||
|
[SupplyParameterFromQuery(Name = "bootstrapToken")]
|
||||||
|
public string? BootstrapToken { get; set; }
|
||||||
|
|
||||||
// ── Constants ──
|
// ── Constants ──
|
||||||
// Signature field size in PDF points (fixed): 1.77" × 1.96"
|
// Signature field size in PDF points (fixed): 1.77" × 1.96"
|
||||||
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
||||||
@@ -609,6 +613,8 @@
|
|||||||
int? _loadedEnvelopeId;
|
int? _loadedEnvelopeId;
|
||||||
int? _editingEnvelopeId;
|
int? _editingEnvelopeId;
|
||||||
bool _isLoadingEnvelopeForEdit;
|
bool _isLoadingEnvelopeForEdit;
|
||||||
|
bool _bootstrapApplied;
|
||||||
|
string? _pendingBootstrapToken;
|
||||||
|
|
||||||
List<SignatureFieldDraft> _signatureFields = [];
|
List<SignatureFieldDraft> _signatureFields = [];
|
||||||
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
||||||
@@ -629,6 +635,7 @@
|
|||||||
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
|
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
|
||||||
string _saveSuccessBody = string.Empty;
|
string _saveSuccessBody = string.Empty;
|
||||||
bool _saveNavigateToDashboard = false;
|
bool _saveNavigateToDashboard = false;
|
||||||
|
bool _saveDraftAfterBootstrap;
|
||||||
|
|
||||||
// ── Envelope metadata ──
|
// ── Envelope metadata ──
|
||||||
string _envelopeTitle = string.Empty;
|
string _envelopeTitle = string.Empty;
|
||||||
@@ -697,6 +704,9 @@
|
|||||||
if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
|
if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
|
||||||
_editingEnvelopeId = EnvelopeId.Value;
|
_editingEnvelopeId = EnvelopeId.Value;
|
||||||
|
|
||||||
|
if (!IsEditMode && !_bootstrapApplied && !string.IsNullOrWhiteSpace(BootstrapToken))
|
||||||
|
_pendingBootstrapToken = BootstrapToken;
|
||||||
|
|
||||||
if (IsEditMode && _loadedEnvelopeId != EnvelopeId)
|
if (IsEditMode && _loadedEnvelopeId != EnvelopeId)
|
||||||
{
|
{
|
||||||
await LoadEnvelopeForEditAsync(EnvelopeId!.Value);
|
await LoadEnvelopeForEditAsync(EnvelopeId!.Value);
|
||||||
@@ -707,15 +717,91 @@
|
|||||||
return;
|
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)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
_isPageReady = true;
|
_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)
|
async Task LoadEnvelopeForEditAsync(int envelopeId)
|
||||||
@@ -1108,12 +1194,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async Task SaveDraftAsync()
|
async Task SaveDraftAsync()
|
||||||
|
{
|
||||||
|
await SaveDraftCoreAsync(navigateToDashboard: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<int?> SaveDraftCoreAsync(bool navigateToDashboard)
|
||||||
{
|
{
|
||||||
if (!ValidateSettings(out var settingsError))
|
if (!ValidateSettings(out var settingsError))
|
||||||
{
|
{
|
||||||
_saveErrorMessage = settingsError;
|
_saveErrorMessage = settingsError;
|
||||||
_savePopupVisible = true;
|
_savePopupVisible = true;
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isSaving = true;
|
_isSaving = true;
|
||||||
@@ -1134,25 +1225,37 @@
|
|||||||
_loadedEnvelopeId = result.Id;
|
_loadedEnvelopeId = result.Id;
|
||||||
_editingEnvelopeId = 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/editor?envelopeId={result.Id}", forceLoad: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
return result.Id;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "[SenderEditor] Failed to save draft");
|
Logger.LogError(ex, "[SenderEditor] Failed to save draft");
|
||||||
_saveErrorMessage = ex.Message;
|
_saveErrorMessage = ex.Message;
|
||||||
_savePopupVisible = true;
|
_savePopupVisible = true;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_isSaving = false;
|
_isSaving = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task SaveDraftAndStayOnEditorAsync()
|
||||||
|
{
|
||||||
|
Logger.LogInformation("[SenderEditor] Trigger bootstrap draft save.");
|
||||||
|
await SaveDraftCoreAsync(navigateToDashboard: false);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Send envelope ──
|
// ── Send envelope ──
|
||||||
async Task SendAsync()
|
async Task SendAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user