Refactor receiver input and improve UI/UX
Replaced receiver list with a tag-based `DxTagBox` for better flexibility and dynamic updates. Updated receiver management logic to synchronize `_receivers` and `_receiverTags`. Enhanced accessibility with ARIA attributes for the suggestion list. Redesigned the "Add Receiver" button with custom styles, hover effects, and an inline SVG icon. Improved the suggestion list with horizontal scrolling and nowrap styling. Styled tags and added a remove button for each tag. Increased recent receiver suggestions fetch limit to 20. Added validation to ensure default envelope type selection. Refactored empty state handling and cleaned up redundant code for better maintainability.
This commit is contained in:
@@ -114,7 +114,7 @@
|
|||||||
}
|
}
|
||||||
else if (_recentReceiverSuggestions.Count > 0)
|
else if (_recentReceiverSuggestions.Count > 0)
|
||||||
{
|
{
|
||||||
<div class="sender-create-wizard__suggestion-list">
|
<div class="sender-create-wizard__suggestion-list" role="listbox" aria-label="Recent receivers">
|
||||||
@foreach (var suggestion in _recentReceiverSuggestions)
|
@foreach (var suggestion in _recentReceiverSuggestions)
|
||||||
{
|
{
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@@ -154,11 +154,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sender-create-wizard__receiver-actions">
|
<div class="sender-create-wizard__receiver-actions">
|
||||||
<DxButton Text='@L("AddReceiver", "Empfänger hinzufügen")'
|
<button type="button"
|
||||||
RenderStyle="ButtonRenderStyle.Primary"
|
class="sender-create-wizard__add-receiver-btn"
|
||||||
CssClass="sender-create-wizard__add-receiver-btn"
|
@onclick="AddReceiver"
|
||||||
Click="AddReceiver"
|
disabled="@IsActionLocked">
|
||||||
Enabled="@(!IsActionLocked)" />
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M8 4a.5.5 0 0 1 .5.5V7.5H11.5a.5.5 0 0 1 0 1H8.5v3a.5.5 0 0 1-1 0v-3H4.5a.5.5 0 0 1 0-1H7.5v-3A.5.5 0 0 1 8 4z" />
|
||||||
|
</svg>
|
||||||
|
<span>@L("AddReceiver", "Empfänger hinzufügen")</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (_receivers.Count == 0)
|
@if (_receivers.Count == 0)
|
||||||
@@ -169,22 +173,47 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="sender-create-wizard__receiver-list">
|
<div class="sender-create-wizard__receiver-tagbox-wrap">
|
||||||
@foreach (var receiver in _receivers)
|
<label class="sender-create-wizard__label" for="wizardReceiversTagBox">
|
||||||
|
@L("Receiver", "Empfänger")
|
||||||
|
</label>
|
||||||
|
<DxTagBox Data="@_receivers"
|
||||||
|
TData="CreateEnvelopeWizardReceiver"
|
||||||
|
TValue="CreateEnvelopeWizardReceiver"
|
||||||
|
AllowCustomTags="true"
|
||||||
|
@bind-Tags="@_receiverTags"
|
||||||
|
@bind-Tags:after="OnReceiverTagsChanged"
|
||||||
|
HideSelectedItems="false"
|
||||||
|
TextFieldName="@nameof(CreateEnvelopeWizardReceiver.EmailAddress)"
|
||||||
|
ListRenderMode="ListRenderMode.Virtual"
|
||||||
|
SearchMode="ListSearchMode.AutoSearch"
|
||||||
|
SearchFilterCondition="ListSearchFilterCondition.Contains"
|
||||||
|
SearchTextParseMode="ListSearchTextParseMode.GroupWordsByAnd"
|
||||||
|
CssClass="sender-create-wizard__receiver-tagbox"
|
||||||
|
InputId="wizardReceiversTagBox"
|
||||||
|
Enabled="@(!IsActionLocked)">
|
||||||
|
<TagDisplayTemplate Context="tagContext">
|
||||||
|
<div class="sender-create-wizard__receiver-tag">
|
||||||
|
@if (tagContext.DataItem is CreateEnvelopeWizardReceiver receiver)
|
||||||
{
|
{
|
||||||
<div class="sender-create-wizard__receiver-item">
|
<span class="sender-create-wizard__receiver-tag-name">@receiver.FullName</span>
|
||||||
<div>
|
<span class="sender-create-wizard__receiver-tag-email">@receiver.EmailAddress</span>
|
||||||
<div class="sender-create-wizard__receiver-name">@receiver.FullName</div>
|
|
||||||
<div class="sender-create-wizard__receiver-email">@receiver.EmailAddress</div>
|
|
||||||
</div>
|
|
||||||
<button type="button"
|
|
||||||
class="sender-create-wizard__receiver-remove"
|
|
||||||
@onclick="() => RemoveReceiver(receiver)"
|
|
||||||
disabled="@IsActionLocked">
|
|
||||||
@L("Delete", "Löschen")
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="sender-create-wizard__receiver-tag-email">@tagContext.DisplayText</span>
|
||||||
|
}
|
||||||
|
<button type="button"
|
||||||
|
class="sender-create-wizard__receiver-tag-remove"
|
||||||
|
@onclick="tagContext.RemoveTagAction"
|
||||||
|
aria-label="Remove receiver"
|
||||||
|
disabled="@IsActionLocked">x</button>
|
||||||
|
</div>
|
||||||
|
</TagDisplayTemplate>
|
||||||
|
<EmptyDataAreaTemplate Context="emptyContext">
|
||||||
|
<div class="sender-create-wizard__tagbox-empty"></div>
|
||||||
|
</EmptyDataAreaTemplate>
|
||||||
|
</DxTagBox>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,6 +318,7 @@
|
|||||||
private string _receiverName = string.Empty;
|
private string _receiverName = string.Empty;
|
||||||
private string _receiverEmail = string.Empty;
|
private string _receiverEmail = string.Empty;
|
||||||
private readonly List<CreateEnvelopeWizardReceiver> _receivers = [];
|
private readonly List<CreateEnvelopeWizardReceiver> _receivers = [];
|
||||||
|
private IEnumerable<string> _receiverTags = [];
|
||||||
private readonly List<RecentReceiverSuggestionDto> _recentReceiverSuggestions = [];
|
private readonly List<RecentReceiverSuggestionDto> _recentReceiverSuggestions = [];
|
||||||
private bool _isLoadingRecentSuggestions;
|
private bool _isLoadingRecentSuggestions;
|
||||||
private bool _recentSuggestionsLoaded;
|
private bool _recentSuggestionsLoaded;
|
||||||
@@ -330,6 +360,9 @@
|
|||||||
{
|
{
|
||||||
_isVisible = Visible;
|
_isVisible = Visible;
|
||||||
|
|
||||||
|
if (_selectedEnvelopeTypeId is null && EnvelopeTypes.Count > 0)
|
||||||
|
_selectedEnvelopeTypeId = GetDefaultEnvelopeTypeId();
|
||||||
|
|
||||||
if (Visible && !_previousVisible)
|
if (Visible && !_previousVisible)
|
||||||
ResetWizardState();
|
ResetWizardState();
|
||||||
|
|
||||||
@@ -411,6 +444,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
_receivers.Add(new CreateEnvelopeWizardReceiver(fullName, email));
|
_receivers.Add(new CreateEnvelopeWizardReceiver(fullName, email));
|
||||||
|
_receiverTags = _receivers.Select(x => x.EmailAddress).ToList();
|
||||||
_receiverName = string.Empty;
|
_receiverName = string.Empty;
|
||||||
_receiverEmail = string.Empty;
|
_receiverEmail = string.Empty;
|
||||||
_validationMessage = null;
|
_validationMessage = null;
|
||||||
@@ -423,6 +457,17 @@
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
_receivers.Remove(receiver);
|
_receivers.Remove(receiver);
|
||||||
|
_receiverTags = _receivers.Select(x => x.EmailAddress).ToList();
|
||||||
|
_validationMessage = null;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task OnReceiverTagsChanged()
|
||||||
|
{
|
||||||
|
var selectedEmails = _receiverTags.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_receivers.RemoveAll(x => !selectedEmails.Contains(x.EmailAddress));
|
||||||
|
_receiverTags = _receivers.Select(x => x.EmailAddress).ToList();
|
||||||
_validationMessage = null;
|
_validationMessage = null;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@@ -613,7 +658,7 @@
|
|||||||
_isLoadingRecentSuggestions = true;
|
_isLoadingRecentSuggestions = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var suggestions = await ReceiverService.GetRecentReceiverSuggestionsAsync();
|
var suggestions = await ReceiverService.GetRecentReceiverSuggestionsAsync(20);
|
||||||
_recentReceiverSuggestions.Clear();
|
_recentReceiverSuggestions.Clear();
|
||||||
_recentReceiverSuggestions.AddRange(suggestions);
|
_recentReceiverSuggestions.AddRange(suggestions);
|
||||||
_recentSuggestionsLoaded = true;
|
_recentSuggestionsLoaded = true;
|
||||||
@@ -644,6 +689,9 @@
|
|||||||
{
|
{
|
||||||
if (_currentStep == 0)
|
if (_currentStep == 0)
|
||||||
{
|
{
|
||||||
|
if (_selectedEnvelopeTypeId is null && EnvelopeTypes.Count > 0)
|
||||||
|
_selectedEnvelopeTypeId = GetDefaultEnvelopeTypeId();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_title))
|
if (string.IsNullOrWhiteSpace(_title))
|
||||||
{
|
{
|
||||||
_validationMessage = L("SenderEditor_Validation_TitleRequired", "Bitte geben Sie einen Titel ein.");
|
_validationMessage = L("SenderEditor_Validation_TitleRequired", "Bitte geben Sie einen Titel ein.");
|
||||||
@@ -711,6 +759,7 @@
|
|||||||
_receiverName = string.Empty;
|
_receiverName = string.Empty;
|
||||||
_receiverEmail = string.Empty;
|
_receiverEmail = string.Empty;
|
||||||
_receivers.Clear();
|
_receivers.Clear();
|
||||||
|
_receiverTags = [];
|
||||||
_recentReceiverSuggestions.Clear();
|
_recentReceiverSuggestions.Clear();
|
||||||
_isLoadingRecentSuggestions = false;
|
_isLoadingRecentSuggestions = false;
|
||||||
_recentSuggestionsLoaded = false;
|
_recentSuggestionsLoaded = false;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public class ReceiverService(IHttpClientFactory httpClientFactory)
|
|||||||
/// Fetches recent receiver suggestions for the authenticated sender.
|
/// Fetches recent receiver suggestions for the authenticated sender.
|
||||||
/// Calls GET /api/EnvelopeReceiver/recent-suggestions?take={take}.
|
/// Calls GET /api/EnvelopeReceiver/recent-suggestions?take={take}.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> GetRecentReceiverSuggestionsAsync(int take = 5, CancellationToken cancel = default)
|
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> GetRecentReceiverSuggestionsAsync(int take = 20, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
var url = $"/api/EnvelopeReceiver/recent-suggestions?take={take}";
|
var url = $"/api/EnvelopeReceiver/recent-suggestions?take={take}";
|
||||||
|
|||||||
@@ -1271,10 +1271,31 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-create-wizard__add-receiver-btn.dxbl-btn {
|
.sender-create-wizard__add-receiver-btn {
|
||||||
min-width: 210px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
min-height: 36px;
|
||||||
|
min-width: 220px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
box-shadow: 0 8px 18px rgba(14, 116, 144, 0.22);
|
font-size: 0.84rem;
|
||||||
|
border: 1px solid rgba(14, 116, 144, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(236, 254, 255, 0.95);
|
||||||
|
color: #0f766e;
|
||||||
|
box-shadow: 0 6px 14px rgba(14, 116, 144, 0.15);
|
||||||
|
padding: 0.45rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__add-receiver-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(207, 250, 254, 1);
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__add-receiver-btn:disabled {
|
||||||
|
opacity: 0.58;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-create-wizard__suggestions {
|
.sender-create-wizard__suggestions {
|
||||||
@@ -1291,12 +1312,17 @@
|
|||||||
|
|
||||||
.sender-create-wizard__suggestion-list {
|
.sender-create-wizard__suggestion-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-create-wizard__suggestion-item {
|
.sender-create-wizard__suggestion-item {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.12rem;
|
gap: 0.12rem;
|
||||||
@@ -1308,6 +1334,54 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tagbox-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tagbox {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.18rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(236, 254, 255, 0.95);
|
||||||
|
border: 1px solid rgba(14, 116, 144, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tag-name {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tag-email {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tag-remove {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #0f766e;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__tagbox-empty {
|
||||||
|
height: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.sender-create-wizard__suggestion-item:disabled {
|
.sender-create-wizard__suggestion-item:disabled {
|
||||||
opacity: 0.58;
|
opacity: 0.58;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user