fix: replace event ranking drag-and-drop with add/reorder buttons
SortableJS duplicated and mis-ordered events; buttons keep rank in C# and drop the unused BlazorSortableList package. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,7 +22,6 @@
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||
<script src="_content/PSC.Blazor.Components.MarkdownEditor/js/easymde.min.js"></script>
|
||||
<script src="_content/PSC.Blazor.Components.MarkdownEditor/js/markdownEditor.js"></script>
|
||||
<script src="js/markdownTablePaste.js"></script>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
@page "/students/event-ranking-edit/{StudentId:int}"
|
||||
@attribute [Authorize]
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using BlazorSortableList
|
||||
@using WebApp.Models
|
||||
@using WebApp.Components.Shared.Components
|
||||
@inject AppDbContext Context
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ISnackbar Snackbar
|
||||
@@ -33,7 +31,6 @@ else
|
||||
</ActionButtons>
|
||||
</PageHeader>
|
||||
|
||||
@* https://github.com/AlexNek/BlazorSortableList *@
|
||||
<MudGrid Class="mt-2">
|
||||
<MudItem xs="12" md="6" xl="4">
|
||||
<MudPaper Class="pa-3 mb-3" Elevation="2">
|
||||
@@ -41,29 +38,52 @@ else
|
||||
<MudIcon Icon="@Icons.Material.Filled.FormatListNumbered" Class="mr-2" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">Ranked Events</MudText>
|
||||
</div>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">Drag events here in order of preference</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
Use the arrows to set rank. Use the X to remove an event. Up to @StudentEventRanking.MaxRank events.
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
<SortableList
|
||||
Group="GroupId" Id="ListId1" Context="item"
|
||||
Items="_rankedEvents" OnRemove="RankedEventsRemove" OnUpdate="Update">
|
||||
<SortableItemTemplate>
|
||||
<MudCard Outlined="true" Class="mb-2">
|
||||
<MudCardContent Class="pa-2">
|
||||
<div class="d-flex align-center">
|
||||
<MudBadge Content="@(_rankedEvents.IndexOf(item) + 1)" Color="Color.Primary" Overlap="true" Class="mr-3">
|
||||
<MudIcon Icon="@Icons.Material.Filled.DragIndicator" />
|
||||
</MudBadge>
|
||||
<div class="flex-grow-1">
|
||||
<MudText Typo="Typo.body2"><strong>@item.Name</strong></MudText>
|
||||
<MudText Typo="Typo.caption">
|
||||
@AppIcons.EventAttributes(item) @AppIcons.EventEffort(item)
|
||||
</MudText>
|
||||
</div>
|
||||
</div>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</SortableItemTemplate>
|
||||
</SortableList>
|
||||
@if (_rankedEvents.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Class="mb-2">
|
||||
No ranked events yet. Add events from the list on the right.
|
||||
</MudAlert>
|
||||
}
|
||||
@for (var i = 0; i < _rankedEvents.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
var item = _rankedEvents[index];
|
||||
var rank = index + 1;
|
||||
<MudCard @key="item.Id" Outlined="true" Class="mb-2">
|
||||
<MudCardContent Class="pa-2">
|
||||
<div class="d-flex align-center">
|
||||
<MudBadge Content="@rank" Color="Color.Primary" Overlap="true" Class="mr-3">
|
||||
<MudIcon Icon="@Icons.Material.Filled.FormatListNumbered" />
|
||||
</MudBadge>
|
||||
<div class="flex-grow-1">
|
||||
<MudText Typo="Typo.body2"><strong>@item.Name</strong></MudText>
|
||||
<MudText Typo="Typo.caption">
|
||||
@AppIcons.EventAttributes(item) @AppIcons.EventEffort(item)
|
||||
</MudText>
|
||||
</div>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.KeyboardArrowUp"
|
||||
Size="Size.Small"
|
||||
Disabled="@(index == 0)"
|
||||
aria-label="Move up"
|
||||
OnClick="@(() => MoveRanked(item, -1))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.KeyboardArrowDown"
|
||||
Size="Size.Small"
|
||||
Disabled="@(index == _rankedEvents.Count - 1)"
|
||||
aria-label="Move down"
|
||||
OnClick="@(() => MoveRanked(item, 1))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Close"
|
||||
Size="Size.Small"
|
||||
Color="Color.Default"
|
||||
aria-label="Remove from ranked events"
|
||||
OnClick="@(() => RemoveRanked(item))" />
|
||||
</div>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6" xl="4">
|
||||
<MudPaper Class="pa-3 mb-3" Elevation="2">
|
||||
@@ -71,99 +91,117 @@ else
|
||||
<MudIcon Icon="@AppIcons.Events" Class="mr-2" />
|
||||
<MudText Typo="Typo.h6">Available Events</MudText>
|
||||
</div>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">Drag events to rank them</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
@if (AtMaxRank)
|
||||
{
|
||||
<text>Maximum of @StudentEventRanking.MaxRank ranked events reached. Remove one to add another.</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Use Add to place an event at the end of the ranked list.</text>
|
||||
}
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
<SortableList
|
||||
Group="GroupId" Id="ListId2" Context="item"
|
||||
Items="_availableEvents" OnRemove="AvailableEventsRemove" Sort="false">
|
||||
<SortableItemTemplate>
|
||||
<MudCard Outlined="true" Class="mb-2">
|
||||
<MudCardContent Class="pa-2">
|
||||
@if (_availableEvents.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true">
|
||||
@(_rankedEvents.Count == 0 ? "No events are available to rank." : "All events are already ranked.")
|
||||
</MudAlert>
|
||||
}
|
||||
@foreach (var item in _availableEvents)
|
||||
{
|
||||
<MudCard @key="item.Id" Outlined="true" Class="mb-2">
|
||||
<MudCardContent Class="pa-2">
|
||||
<div class="d-flex align-center">
|
||||
<div class="flex-grow-1">
|
||||
<MudText Typo="Typo.body2"><strong>@item.Name</strong></MudText>
|
||||
<MudText Typo="Typo.caption">
|
||||
@AppIcons.EventAttributes(item) @AppIcons.EventEffort(item)
|
||||
</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</SortableItemTemplate>
|
||||
</SortableList>
|
||||
</div>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Primary"
|
||||
Disabled="@AtMaxRank"
|
||||
aria-label="@($"Add {item.Name} to ranked events")"
|
||||
OnClick="@(() => AddRanked(item))" />
|
||||
</div>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
private const string ListId1 = "SharedListId1";
|
||||
private const string ListId2 = "SharedListId2";
|
||||
private const string GroupId = "CommonGroup";
|
||||
|
||||
[Parameter] public int? StudentId { get; set; }
|
||||
|
||||
private Student? _student;
|
||||
private List<EventDefinition>? _events;
|
||||
|
||||
public List<EventDefinition> _rankedEvents = [];
|
||||
public List<EventDefinition> _availableEvents = [];
|
||||
private List<EventDefinition> _rankedEvents = [];
|
||||
private List<EventDefinition> _availableEvents = [];
|
||||
|
||||
private void RankedEventsRemove((int oldIndex, int newIndex) indices)
|
||||
{
|
||||
// get the item at the old index in list 1
|
||||
var item = _rankedEvents[indices.oldIndex];
|
||||
|
||||
// add it to the new index in list 2
|
||||
_availableEvents.Insert(indices.newIndex, item);
|
||||
|
||||
// remove the item from the old index in list 1
|
||||
_rankedEvents.Remove(_rankedEvents[indices.oldIndex]);
|
||||
}
|
||||
|
||||
private void AvailableEventsRemove((int oldIndex, int newIndex) indices)
|
||||
{
|
||||
// get the item at the old index in list 2
|
||||
var item = _availableEvents[indices.oldIndex];
|
||||
|
||||
// add it to the new index in list 1
|
||||
_rankedEvents.Insert(indices.newIndex, item);
|
||||
|
||||
// remove the item from the old index in list 2
|
||||
_availableEvents.Remove(_availableEvents[indices.oldIndex]);
|
||||
}
|
||||
private bool AtMaxRank => _rankedEvents.Count >= StudentEventRanking.MaxRank;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_student =
|
||||
await Context.Students
|
||||
.Include(e => e.EventRankings)
|
||||
.Where(e => e.Id == StudentId).FirstAsync();
|
||||
_events =
|
||||
.ThenInclude(r => r.EventDefinition)
|
||||
.Where(e => e.Id == StudentId)
|
||||
.FirstAsync();
|
||||
|
||||
var events =
|
||||
await Context.Events
|
||||
.OrderBy(e => e.Name)
|
||||
.ToListAsync();
|
||||
.ToListAsync();
|
||||
|
||||
_rankedEvents = _student.EventRankings.OrderBy(e => e.Rank).Select(e => e.EventDefinition).ToList();
|
||||
_availableEvents = _events.Where(e => !_rankedEvents.Contains(e)).ToList();
|
||||
_rankedEvents = _student.EventRankings
|
||||
.Where(e => e.EventDefinition != null)
|
||||
.OrderBy(e => e.Rank)
|
||||
.Select(e => e.EventDefinition)
|
||||
.DistinctBy(e => e.Id)
|
||||
.Take(StudentEventRanking.MaxRank)
|
||||
.ToList();
|
||||
|
||||
var rankedIds = _rankedEvents.Select(e => e.Id).ToHashSet();
|
||||
_availableEvents = events.Where(e => !rankedIds.Contains(e.Id)).ToList();
|
||||
}
|
||||
|
||||
private void Update((int oldIndex, int newIndex) indices)
|
||||
private void AddRanked(EventDefinition item)
|
||||
{
|
||||
var (oldIndex, newIndex) = indices;
|
||||
|
||||
var items = _rankedEvents;
|
||||
var itemToMove = items[oldIndex];
|
||||
items.RemoveAt(oldIndex);
|
||||
|
||||
if (newIndex < items.Count)
|
||||
if (AtMaxRank || _rankedEvents.Any(e => e.Id == item.Id))
|
||||
{
|
||||
items.Insert(newIndex, itemToMove);
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(itemToMove);
|
||||
return;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
_availableEvents.RemoveAll(e => e.Id == item.Id);
|
||||
_rankedEvents.Add(item);
|
||||
}
|
||||
|
||||
private void RemoveRanked(EventDefinition item)
|
||||
{
|
||||
_rankedEvents.RemoveAll(e => e.Id == item.Id);
|
||||
if (_availableEvents.All(e => e.Id != item.Id))
|
||||
{
|
||||
_availableEvents.Add(item);
|
||||
_availableEvents.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveRanked(EventDefinition item, int offset)
|
||||
{
|
||||
var index = _rankedEvents.FindIndex(e => e.Id == item.Id);
|
||||
var newIndex = index + offset;
|
||||
if (index < 0 || newIndex < 0 || newIndex >= _rankedEvents.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_rankedEvents.RemoveAt(index);
|
||||
_rankedEvents.Insert(newIndex, item);
|
||||
}
|
||||
|
||||
async Task Save()
|
||||
{
|
||||
@@ -172,10 +210,12 @@ else
|
||||
|
||||
try
|
||||
{
|
||||
var uniqueRanked = _rankedEvents.DistinctBy(e => e.Id).Take(StudentEventRanking.MaxRank).ToList();
|
||||
|
||||
_student.EventRankings.Clear();
|
||||
for (var index = 0; index < _rankedEvents.Count; index++)
|
||||
for (var index = 0; index < uniqueRanked.Count; index++)
|
||||
{
|
||||
var evt = _rankedEvents[index];
|
||||
var evt = uniqueRanked[index];
|
||||
_student.EventRankings.Add(new StudentEventRanking
|
||||
{
|
||||
EventDefinition = evt,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using BlazorSortableList;
|
||||
using Core.Entities;
|
||||
|
||||
namespace WebApp.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Class SharedSortableListGroup.
|
||||
/// Used for BlazorSortableList
|
||||
/// </summary>
|
||||
internal class SharedSortableListGroup : MultiSortableListGroup<EventDefinition>
|
||||
{
|
||||
public SharedSortableListGroup(Action refreshComponent)
|
||||
: base(refreshComponent)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="BlazorSortableList" Version="2.1.0" />
|
||||
<PackageReference Include="Heron.MudCalendar" Version="3.4.0" />
|
||||
<PackageReference Include="VisNetwork.Blazor" Version="3.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" />
|
||||
|
||||
Reference in New Issue
Block a user