feat: edit print templates on the printer page and store them on presets
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,20 +2,19 @@
|
||||
@attribute [Authorize]
|
||||
@implements IAsyncDisposable
|
||||
@using Core.Printing
|
||||
@using Core.Services
|
||||
@inject INotesService NotesService
|
||||
@inject INoteNamingService NoteNamingService
|
||||
@inject INotePrintService NotePrintService
|
||||
@inject IPrintPresetService PrintPresetService
|
||||
@inject IConfiguration Configuration
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject ClipboardService ClipboardService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject MarkdownTablePasteService MarkdownTablePasteService
|
||||
|
||||
<div class="no-print">
|
||||
<PageHeader Title="Page printer"
|
||||
Description="Merge a markdown note onto students, teams, or events and print one page per match."
|
||||
Description="Write a markdown template, merge it onto students, teams, or events, and print."
|
||||
Icon="@Icons.Material.Filled.Print" />
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-3 pa-md-6 mb-4">
|
||||
@@ -25,7 +24,7 @@
|
||||
}
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudItem xs="12" md="5">
|
||||
<MudSelect T="int?"
|
||||
Label="Print preset"
|
||||
Value="_selectedPresetId"
|
||||
@@ -38,23 +37,17 @@
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudTextField @bind-Value="_saveAsName"
|
||||
Label="Save as name"
|
||||
Variant="Variant.Outlined"
|
||||
Immediate="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="5" Class="d-flex align-center gap-2 flex-wrap">
|
||||
<MudItem xs="12" md="7" Class="d-flex align-center gap-2 flex-wrap">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
StartIcon="@Icons.Material.Filled.Save"
|
||||
OnClick="SaveAsPreset"
|
||||
Disabled="@(_isBusy || !CanSaveAs)">
|
||||
Save as
|
||||
OnClick="NewTemplate"
|
||||
Disabled="@_isBusy">
|
||||
New
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
OnClick="UpdatePreset"
|
||||
Disabled="@(_isBusy || !_selectedPresetId.HasValue || !CanPreview)">
|
||||
Update
|
||||
StartIcon="@Icons.Material.Filled.Save"
|
||||
OnClick="SavePreset"
|
||||
Disabled="@_isBusy">
|
||||
Save
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Error"
|
||||
@@ -75,38 +68,10 @@
|
||||
<MudSelectItem Value="PrintEntityType.Event">Events</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="8">
|
||||
@if (_templateNotes.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info">
|
||||
Create a standalone note on <MudLink Href="/notes">Notes</MudLink> to use as a template.
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelect T="int?"
|
||||
Label="Template note"
|
||||
Value="_selectedNoteId"
|
||||
ValueChanged="OnNoteSelected"
|
||||
Variant="Variant.Outlined">
|
||||
@foreach (var note in _templateNotes)
|
||||
{
|
||||
<MudSelectItem T="int?" Value="@note.Id">@note.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
</MudItem>
|
||||
|
||||
@if (_templateWarning is not null)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Warning">@_templateWarning</MudAlert>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
@if (_entityType == PrintEntityType.Student)
|
||||
{
|
||||
<MudItem xs="12" sm="4" md="3">
|
||||
<MudItem xs="12" sm="4" md="2">
|
||||
<MudNumericField T="int?"
|
||||
Label="Grade"
|
||||
Value="_grade"
|
||||
@@ -116,7 +81,7 @@
|
||||
Max="12"
|
||||
Clearable="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4" md="3">
|
||||
<MudItem xs="12" sm="4" md="2">
|
||||
<MudNumericField T="int?"
|
||||
Label="TSA year"
|
||||
Value="_tsaYear"
|
||||
@@ -126,7 +91,7 @@
|
||||
Max="12"
|
||||
Clearable="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4" md="3">
|
||||
<MudItem xs="12" sm="4" md="4">
|
||||
<MudSelect T="string"
|
||||
Label="Officer"
|
||||
Value="@_officerChoice"
|
||||
@@ -140,7 +105,7 @@
|
||||
}
|
||||
else if (_entityType == PrintEntityType.Team)
|
||||
{
|
||||
<MudItem xs="12" md="4">
|
||||
<MudItem xs="12" md="8">
|
||||
<MudTextField T="string"
|
||||
Value="@_teamIdentifierContains"
|
||||
ValueChanged="OnTeamIdentifierChanged"
|
||||
@@ -186,12 +151,18 @@
|
||||
}
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="flex-wrap">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="flex-wrap mb-2">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
StartIcon="@Icons.Material.Filled.DataObject"
|
||||
OnClick="OpenTokenDialog"
|
||||
Disabled="@_isBusy">
|
||||
Insert token
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Visibility"
|
||||
OnClick="Preview"
|
||||
Disabled="@(_isBusy || !CanPreview)">
|
||||
Disabled="@_isBusy">
|
||||
Preview
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
@@ -223,6 +194,10 @@
|
||||
Min="PrintPresetFilters.MinAnswerSpaceLines"
|
||||
Max="PrintPresetFilters.MaxAnswerSpaceLines"
|
||||
Style="max-width: 8rem;" />
|
||||
@if (IsDirty())
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Warning">Unsaved changes</MudText>
|
||||
}
|
||||
@if (!_previewStale && _pages.Count > 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2">@_pages.Count page@(_pages.Count == 1 ? "" : "s")</MudText>
|
||||
@@ -235,60 +210,61 @@
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary mb-1">Tokens (click to copy)</MudText>
|
||||
@foreach (var group in TokenGroups)
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">Template (Markdown)</MudText>
|
||||
<div id="@EditorElementId" @key="_editorGeneration">
|
||||
<MarkdownEditor Value="@_templateMarkdown"
|
||||
ValueChanged="OnMarkdownChanged"
|
||||
Placeholder="Write the printable page. Insert tokens for student, team, or event values."
|
||||
AutoSaveEnabled="false"
|
||||
NativeSpellChecker="false"
|
||||
HideIcons="@HiddenEditorIcons" />
|
||||
</div>
|
||||
<MudText Typo="Typo.subtitle2" Class="mt-4 mb-1">Template preview</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary mb-2">Markdown only — tokens are not merged here.</MudText>
|
||||
@if (string.IsNullOrWhiteSpace(_templateMarkdown))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@group.Label</MudText>
|
||||
<div class="mb-2">
|
||||
@foreach (var token in group.Tokens)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" OnClick="() => CopyToken(token)" Class="ma-1">@FormatToken(token)</MudChip>
|
||||
}
|
||||
</div>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">The template will preview here as you type.</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Elevation="0" Class="pa-3 note-print-page" Style="@($"background-color: var(--mud-palette-background-grey);{PrintPageStyle}")">
|
||||
<div class="markdown-content">
|
||||
@((MarkupString)MarkdownHelper.ToHtml(_templateMarkdown))
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</div>
|
||||
|
||||
@if (_didPreview && _pages.Count == 0 && !_previewStale)
|
||||
{
|
||||
<MudText Class="no-print mud-text-secondary">No matching records for these filters.</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
@for (var i = 0; i < _pages.Count; i++)
|
||||
{
|
||||
var page = _pages[i];
|
||||
var isLast = i == _pages.Count - 1;
|
||||
var pageClass = _newPagePerRecord && !isLast
|
||||
? "note-print-page pagebreak"
|
||||
: "note-print-page";
|
||||
<MudContainer Class="@pageClass" Style="@PrintPageStyle">
|
||||
<div class="markdown-content">
|
||||
@((MarkupString)page.Html)
|
||||
</div>
|
||||
</MudContainer>
|
||||
}
|
||||
}
|
||||
<div class="print-only">
|
||||
<PrintPageStack Pages="_pages"
|
||||
NewPagePerRecord="_newPagePerRecord"
|
||||
PrintPageStyle="@PrintPageStyle" />
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private const string EditorElementId = "page-printer-editor";
|
||||
private static readonly string[] HiddenEditorIcons = ["preview", "side-by-side", "fullscreen"];
|
||||
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private bool _isDisposed;
|
||||
private bool _isLoading = true;
|
||||
private bool _isBusy;
|
||||
private bool _previewStale = true;
|
||||
private bool _didPreview;
|
||||
private bool _pasteInitialized;
|
||||
private int _editorGeneration;
|
||||
|
||||
private List<Note> _templateNotes = [];
|
||||
private List<PrintPreset> _presets = [];
|
||||
private List<string> _importedTokenNames = [];
|
||||
private IReadOnlyList<NotePrintPage> _pages = [];
|
||||
private IDialogReference? _previewDialog;
|
||||
private IDisposable? _navigationRegistration;
|
||||
|
||||
private int? _selectedPresetId;
|
||||
private int? _selectedNoteId;
|
||||
private string _saveAsName = string.Empty;
|
||||
private string? _templateWarning;
|
||||
private string _templateMarkdown = string.Empty;
|
||||
private PrintEntityType _entityType = PrintEntityType.Student;
|
||||
private int? _grade;
|
||||
private int? _tsaYear;
|
||||
@@ -301,41 +277,20 @@ else
|
||||
private int _fontSizePt = PrintPresetFilters.DefaultFontSizePt;
|
||||
private int _answerSpaceLines = PrintPresetFilters.DefaultAnswerSpaceLines;
|
||||
|
||||
private static string FormatToken(string token) => "{{" + token + "}}";
|
||||
private string _snapshotMarkdown = string.Empty;
|
||||
private PrintEntityType _snapshotEntityType = PrintEntityType.Student;
|
||||
private string _snapshotFiltersJson = string.Empty;
|
||||
|
||||
private string PrintPageStyle =>
|
||||
$"--print-font-size:{_fontSizePt}pt;--print-answer-lines:{_answerSpaceLines};";
|
||||
|
||||
private IEnumerable<(string Label, IReadOnlyList<string> Tokens)> TokenGroups
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return ("Layout", PrintFieldCatalog.Layout);
|
||||
yield return ("Chapter", PrintFieldCatalog.Chapter);
|
||||
yield return (_entityType.ToString(), PrintFieldCatalog.EntityTokens(_entityType));
|
||||
if (_entityType == PrintEntityType.Student)
|
||||
yield return ("Event ranks", PrintFieldCatalog.StudentRanks);
|
||||
if (_entityType == PrintEntityType.Student && _importedTokenNames.Count > 0)
|
||||
yield return ("Additional fields", _importedTokenNames);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanPreview =>
|
||||
_selectedNoteId.HasValue
|
||||
&& SelectedTemplateNote is { Content: not null } note
|
||||
&& !string.IsNullOrWhiteSpace(note.Content)
|
||||
&& !note.IsDeleted
|
||||
&& _templateWarning is null;
|
||||
|
||||
private bool CanSaveAs =>
|
||||
CanPreview && !string.IsNullOrWhiteSpace(_saveAsName);
|
||||
|
||||
private Note? SelectedTemplateNote =>
|
||||
_templateNotes.FirstOrDefault(n => n.Id == _selectedNoteId);
|
||||
private bool CanPreview => !string.IsNullOrWhiteSpace(_templateMarkdown);
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
CaptureSnapshot();
|
||||
_navigationRegistration = NavigationManager.RegisterLocationChangingHandler(OnLocationChanging);
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -343,6 +298,19 @@ else
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (_pasteInitialized || _isDisposed)
|
||||
return;
|
||||
|
||||
await Task.Delay(150);
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
await MarkdownTablePasteService.InitializeAsync(EditorElementId);
|
||||
_pasteInitialized = true;
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
if (_isDisposed)
|
||||
@@ -352,14 +320,6 @@ else
|
||||
try
|
||||
{
|
||||
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
|
||||
var notes = await NotesService.GetNotesAsync();
|
||||
_templateNotes =
|
||||
[
|
||||
.. notes.Where(n => !NoteNamingService.IsPageNote(n.Title))
|
||||
.OrderBy(n => n.Title)
|
||||
];
|
||||
|
||||
await RefreshImportedTokenNamesAsync(token);
|
||||
_presets = [.. await PrintPresetService.GetAllAsync(token)];
|
||||
}
|
||||
@@ -376,23 +336,40 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkStale()
|
||||
private async Task MarkStaleAsync()
|
||||
{
|
||||
_previewStale = true;
|
||||
_pages = [];
|
||||
await ClosePreviewDialogAsync();
|
||||
}
|
||||
|
||||
private void OnEntityTypeChanged(PrintEntityType value)
|
||||
private void MarkStale() => _ = MarkStaleAsync();
|
||||
|
||||
private async Task ClosePreviewDialogAsync()
|
||||
{
|
||||
var dialog = _previewDialog;
|
||||
_previewDialog = null;
|
||||
if (dialog is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
dialog.Close();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnEntityTypeChanged(PrintEntityType value)
|
||||
{
|
||||
_entityType = value;
|
||||
_templateWarning = null;
|
||||
MarkStale();
|
||||
await MarkStaleAsync();
|
||||
}
|
||||
|
||||
private void OnNoteSelected(int? value)
|
||||
private void OnMarkdownChanged(string? value)
|
||||
{
|
||||
_selectedNoteId = value;
|
||||
_templateWarning = null;
|
||||
_templateMarkdown = value ?? string.Empty;
|
||||
MarkStale();
|
||||
}
|
||||
|
||||
@@ -466,12 +443,66 @@ else
|
||||
];
|
||||
}
|
||||
|
||||
private async Task FlushEditorAsync()
|
||||
{
|
||||
var value = await MarkdownTablePasteService.GetValueAsync(EditorElementId);
|
||||
if (value is not null && value != _templateMarkdown)
|
||||
{
|
||||
_templateMarkdown = value;
|
||||
await MarkStaleAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsDirty()
|
||||
{
|
||||
return !string.Equals(_templateMarkdown, _snapshotMarkdown, StringComparison.Ordinal)
|
||||
|| _entityType != _snapshotEntityType
|
||||
|| !string.Equals(BuildFilters().ToJson(), _snapshotFiltersJson, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private void CaptureSnapshot()
|
||||
{
|
||||
_snapshotMarkdown = _templateMarkdown;
|
||||
_snapshotEntityType = _entityType;
|
||||
_snapshotFiltersJson = BuildFilters().ToJson();
|
||||
}
|
||||
|
||||
private async Task<bool> ConfirmDiscardIfDirtyAsync()
|
||||
{
|
||||
await FlushEditorAsync();
|
||||
if (!IsDirty())
|
||||
return true;
|
||||
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"Unsaved changes",
|
||||
"Discard unsaved template or filter changes?",
|
||||
yesText: "Discard",
|
||||
cancelText: "Stay");
|
||||
|
||||
return confirmed == true && !_isDisposed;
|
||||
}
|
||||
|
||||
private async ValueTask OnLocationChanging(LocationChangingContext context)
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
if (!await ConfirmDiscardIfDirtyAsync())
|
||||
context.PreventNavigation();
|
||||
}
|
||||
|
||||
private async Task OnPresetSelected(int? id)
|
||||
{
|
||||
if (id == _selectedPresetId)
|
||||
return;
|
||||
|
||||
if (!await ConfirmDiscardIfDirtyAsync())
|
||||
return;
|
||||
|
||||
_selectedPresetId = id;
|
||||
if (!id.HasValue)
|
||||
{
|
||||
MarkStale();
|
||||
await MarkStaleAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -480,14 +511,38 @@ else
|
||||
return;
|
||||
|
||||
ApplyPreset(preset);
|
||||
MarkStale();
|
||||
await Task.CompletedTask;
|
||||
CaptureSnapshot();
|
||||
await MarkStaleAsync();
|
||||
}
|
||||
|
||||
private async Task NewTemplate()
|
||||
{
|
||||
if (_isDisposed || !await ConfirmDiscardIfDirtyAsync())
|
||||
return;
|
||||
|
||||
_selectedPresetId = null;
|
||||
_templateMarkdown = string.Empty;
|
||||
_entityType = PrintEntityType.Student;
|
||||
_grade = null;
|
||||
_tsaYear = null;
|
||||
_officerChoice = "any";
|
||||
_teamIdentifierContains = null;
|
||||
_eventNameContains = null;
|
||||
_eventFormat = null;
|
||||
_regionalChoice = "any";
|
||||
_newPagePerRecord = true;
|
||||
_fontSizePt = PrintPresetFilters.DefaultFontSizePt;
|
||||
_answerSpaceLines = PrintPresetFilters.DefaultAnswerSpaceLines;
|
||||
_editorGeneration++;
|
||||
_pasteInitialized = false;
|
||||
CaptureSnapshot();
|
||||
await MarkStaleAsync();
|
||||
}
|
||||
|
||||
private void ApplyPreset(PrintPreset preset)
|
||||
{
|
||||
_saveAsName = preset.Name;
|
||||
_entityType = preset.EntityType;
|
||||
_templateMarkdown = preset.TemplateMarkdown ?? string.Empty;
|
||||
var filters = PrintPresetFilters.FromJson(preset.FiltersJson);
|
||||
_grade = filters.Grade;
|
||||
_tsaYear = filters.TsaYear;
|
||||
@@ -499,18 +554,8 @@ else
|
||||
_newPagePerRecord = filters.NewPagePerRecord;
|
||||
_fontSizePt = filters.FontSizePt;
|
||||
_answerSpaceLines = filters.AnswerSpaceLines;
|
||||
|
||||
var note = _templateNotes.FirstOrDefault(n => n.Id == preset.NoteId);
|
||||
if (note is null || note.IsDeleted || string.IsNullOrWhiteSpace(note.Content))
|
||||
{
|
||||
_selectedNoteId = null;
|
||||
_templateWarning = "This preset's template note is missing or empty. Choose another note.";
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedNoteId = note.Id;
|
||||
_templateWarning = null;
|
||||
}
|
||||
_editorGeneration++;
|
||||
_pasteInitialized = false;
|
||||
}
|
||||
|
||||
private PrintPresetFilters BuildFilters() =>
|
||||
@@ -528,23 +573,84 @@ else
|
||||
AnswerSpaceLines = _answerSpaceLines
|
||||
};
|
||||
|
||||
private async Task OpenTokenDialog()
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
await RefreshImportedTokenNamesAsync(token);
|
||||
|
||||
var parameters = new DialogParameters<PrintTokenInsertDialog>
|
||||
{
|
||||
{ x => x.EntityType, _entityType },
|
||||
{ x => x.ImportedFieldNames, _importedTokenNames },
|
||||
{ x => x.OnInsert, EventCallback.Factory.Create<string>(this, InsertToken) }
|
||||
};
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.Large,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
|
||||
await DialogService.ShowAsync<PrintTokenInsertDialog>("Insert token", parameters, options);
|
||||
}
|
||||
|
||||
private async Task InsertToken(string tokenName)
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
await FlushEditorAsync();
|
||||
var wrapped = "{{" + tokenName + "}}";
|
||||
var inserted = await MarkdownTablePasteService.InsertAtCursorAsync(EditorElementId, wrapped);
|
||||
if (!inserted)
|
||||
{
|
||||
_templateMarkdown += wrapped;
|
||||
await MarkdownTablePasteService.SetValueAsync(EditorElementId, _templateMarkdown);
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = await MarkdownTablePasteService.GetValueAsync(EditorElementId);
|
||||
if (value is not null)
|
||||
_templateMarkdown = value;
|
||||
}
|
||||
|
||||
await MarkStaleAsync();
|
||||
}
|
||||
|
||||
private async Task Preview()
|
||||
{
|
||||
if (_isDisposed || !CanPreview)
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
await FlushEditorAsync();
|
||||
if (!CanPreview)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
Snackbar.Add("Write a template before previewing.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_previewStale && _didPreview)
|
||||
{
|
||||
await OpenPreviewDialogAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_isBusy = true;
|
||||
try
|
||||
{
|
||||
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
await RefreshImportedTokenNamesAsync(token);
|
||||
|
||||
var note = SelectedTemplateNote!;
|
||||
_pages = await NotePrintService.PreviewAsync(
|
||||
new NotePrintRequest
|
||||
{
|
||||
EntityType = _entityType,
|
||||
TemplateMarkdown = note.Content ?? string.Empty,
|
||||
TemplateMarkdown = _templateMarkdown,
|
||||
Filters = BuildFilters(),
|
||||
ImportedFieldCatalog = _importedTokenNames
|
||||
},
|
||||
@@ -553,12 +659,8 @@ else
|
||||
_previewStale = false;
|
||||
_didPreview = true;
|
||||
|
||||
if (_pages.Count > 75 && !_isDisposed)
|
||||
{
|
||||
Snackbar.Add(
|
||||
$"This preview has {_pages.Count} pages. Printing a large set can be slow.",
|
||||
Severity.Warning);
|
||||
}
|
||||
if (!_isDisposed)
|
||||
await OpenPreviewDialogAsync();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
@@ -578,6 +680,46 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenPreviewDialogAsync()
|
||||
{
|
||||
await ClosePreviewDialogAsync();
|
||||
|
||||
var parameters = new DialogParameters<PrintPreviewDialog>
|
||||
{
|
||||
{ x => x.Pages, _pages },
|
||||
{ x => x.NewPagePerRecord, _newPagePerRecord },
|
||||
{ x => x.PrintPageStyle, PrintPageStyle },
|
||||
{ x => x.OnPrint, EventCallback.Factory.Create(this, Print) }
|
||||
};
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.ExtraLarge,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<PrintPreviewDialog>("Print preview", parameters, options);
|
||||
_previewDialog = dialog;
|
||||
_ = TrackPreviewDialog(dialog);
|
||||
}
|
||||
|
||||
private async Task TrackPreviewDialog(IDialogReference dialog)
|
||||
{
|
||||
try
|
||||
{
|
||||
await dialog.Result;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_previewDialog == dialog)
|
||||
_previewDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Print()
|
||||
{
|
||||
if (_isDisposed || _previewStale || _pages.Count == 0)
|
||||
@@ -595,40 +737,33 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CopyToken(string token)
|
||||
private async Task SavePreset()
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await ClipboardService.WriteTextAsync($"{{{{{token}}}}}");
|
||||
if (!_isDisposed)
|
||||
Snackbar.Add($"Copied {FormatToken(token)}", Severity.Info);
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
await FlushEditorAsync();
|
||||
if (!CanPreview)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
Snackbar.Add($"Could not copy: {ex.Message}", Severity.Error);
|
||||
Snackbar.Add("Write a template before saving.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveAsPreset()
|
||||
{
|
||||
if (_isDisposed || !CanSaveAs || !_selectedNoteId.HasValue)
|
||||
if (_selectedPresetId.HasValue)
|
||||
{
|
||||
await UpdateSelectedPresetAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var name = await PromptForPresetNameAsync();
|
||||
if (string.IsNullOrWhiteSpace(name) || _isDisposed)
|
||||
return;
|
||||
|
||||
_isBusy = true;
|
||||
try
|
||||
{
|
||||
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
var name = _saveAsName.Trim();
|
||||
if (await PrintPresetService.NameExistsAsync(name, null, token))
|
||||
{
|
||||
Snackbar.Add($"A print preset named '{name}' already exists.", Severity.Warning);
|
||||
@@ -639,7 +774,7 @@ else
|
||||
new PrintPreset
|
||||
{
|
||||
Name = name,
|
||||
NoteId = _selectedNoteId.Value,
|
||||
TemplateMarkdown = _templateMarkdown,
|
||||
EntityType = _entityType,
|
||||
FiltersJson = BuildFilters().ToJson()
|
||||
},
|
||||
@@ -647,6 +782,7 @@ else
|
||||
|
||||
_presets = [.. await PrintPresetService.GetAllAsync(token)];
|
||||
_selectedPresetId = created.Id;
|
||||
CaptureSnapshot();
|
||||
if (!_isDisposed)
|
||||
Snackbar.Add($"Saved print preset '{name}'.", Severity.Success);
|
||||
}
|
||||
@@ -668,9 +804,26 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdatePreset()
|
||||
private async Task<string?> PromptForPresetNameAsync()
|
||||
{
|
||||
if (_isDisposed || !_selectedPresetId.HasValue || !_selectedNoteId.HasValue || !CanPreview)
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<PrintPresetNameDialog>("Save print preset", options);
|
||||
var result = await dialog.Result;
|
||||
if (result is null || result.Canceled || result.Data is not string name)
|
||||
return null;
|
||||
|
||||
return name.Trim();
|
||||
}
|
||||
|
||||
private async Task UpdateSelectedPresetAsync()
|
||||
{
|
||||
if (_isDisposed || !_selectedPresetId.HasValue)
|
||||
return;
|
||||
|
||||
_isBusy = true;
|
||||
@@ -686,15 +839,16 @@ else
|
||||
{
|
||||
Id = existing.Id,
|
||||
Name = existing.Name,
|
||||
NoteId = _selectedNoteId.Value,
|
||||
TemplateMarkdown = _templateMarkdown,
|
||||
EntityType = _entityType,
|
||||
FiltersJson = BuildFilters().ToJson()
|
||||
},
|
||||
token);
|
||||
|
||||
_presets = [.. await PrintPresetService.GetAllAsync(token)];
|
||||
CaptureSnapshot();
|
||||
if (!_isDisposed)
|
||||
Snackbar.Add($"Updated print preset '{existing.Name}'.", Severity.Success);
|
||||
Snackbar.Add($"Saved print preset '{existing.Name}'.", Severity.Success);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
@@ -705,7 +859,7 @@ else
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
Snackbar.Add($"Could not update: {ex.Message}", Severity.Error);
|
||||
Snackbar.Add($"Could not save: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -739,6 +893,7 @@ else
|
||||
await PrintPresetService.DeleteAsync(existing.Id, token);
|
||||
_presets = [.. await PrintPresetService.GetAllAsync(token)];
|
||||
_selectedPresetId = null;
|
||||
CaptureSnapshot();
|
||||
if (!_isDisposed)
|
||||
Snackbar.Add($"Deleted print preset '{existing.Name}'.", Severity.Info);
|
||||
}
|
||||
@@ -765,6 +920,9 @@ else
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_isDisposed = true;
|
||||
_navigationRegistration?.Dispose();
|
||||
_navigationRegistration = null;
|
||||
await ClosePreviewDialogAsync();
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
|
||||
Reference in New Issue
Block a user