From 86475d77afade8ccb21a556065621836a3370d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schultz=20Madsen?= Date: Sun, 27 Sep 2026 09:37:33 +0200 Subject: [PATCH] fix(tags): list planning tags in Danish alphabetical order (#2126) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetItemsPlanningTags sorted in SQL, so the order came from the column collation (utf8mb4_general_ci), which folds Å to A. Materialise the tags and sort them in memory with TagNameComparer.Danish (da-DK, case-insensitive, always Danish regardless of UI language), the same rule backend-configuration uses for task tags. Adds a Testcontainers MariaDB integration test: tags created as Øko, Beta, Åben, Alfa, Æble come back as Alfa, Beta, Æble, Øko, Åben. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017kbiaVNigNkVARtq94xkDa --- .../ItemsPlanningTagsServiceOrderTests.cs | 96 +++++++++++++++++++ .../Helpers/TagNameComparer.cs | 41 ++++++++ .../ItemsPlanningTagsService.cs | 6 +- 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn.Test/ItemsPlanningTagsServiceOrderTests.cs create mode 100644 eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn/Helpers/TagNameComparer.cs diff --git a/eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn.Test/ItemsPlanningTagsServiceOrderTests.cs b/eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn.Test/ItemsPlanningTagsServiceOrderTests.cs new file mode 100644 index 00000000..3dce9c0f --- /dev/null +++ b/eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn.Test/ItemsPlanningTagsServiceOrderTests.cs @@ -0,0 +1,96 @@ +/* +The MIT License (MIT) + +Copyright (c) 2007 - 2026 Microting A/S + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +namespace ItemsPlanning.Pn.Test; + +using System.Linq; +using System.Threading.Tasks; +using ItemsPlanning.Pn.Services.ItemsPlanningLocalizationService; +using ItemsPlanning.Pn.Services.ItemsPlanningTagsService; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Microting.eFormApi.BasePn.Abstractions; +using Microting.ItemsPlanningBase.Infrastructure.Data; +using Microting.ItemsPlanningBase.Infrastructure.Data.Entities; +using NSubstitute; +using NUnit.Framework; +using Testcontainers.MariaDb; + +/// +/// GetItemsPlanningTags must list tags in Danish order (#2126). Runs against a real +/// MariaDB so the test also fails if the sort ever moves back into SQL, where the +/// column collation (utf8mb4_general_ci) folds Å to A. +/// +[TestFixture] +public class ItemsPlanningTagsServiceOrderTests +{ + private readonly MariaDbContainer _mariaDbContainer = new MariaDbBuilder("mariadb:11.2") + .WithDatabase("items-planning-tags-order") + .WithPassword("secretpassword") + .Build(); + + private ItemsPlanningPnDbContext _dbContext; + + [OneTimeSetUp] + public async Task OneTimeSetUp() + { + await _mariaDbContainer.StartAsync(); + + var connectionString = _mariaDbContainer.GetConnectionString(); + var options = new DbContextOptionsBuilder() + .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)) + .Options; + + _dbContext = new ItemsPlanningPnDbContext(options); + await _dbContext.Database.EnsureCreatedAsync(); + } + + [OneTimeTearDown] + public async Task OneTimeTearDown() + { + await _dbContext.DisposeAsync(); + await _mariaDbContainer.DisposeAsync(); + } + + [Test] + public async Task GetItemsPlanningTags_ReturnsTagsInDanishOrder() + { + foreach (var name in new[] { "Øko", "Beta", "Åben", "Alfa", "Æble" }) + { + await new PlanningTag { Name = name }.Create(_dbContext); + } + + var service = new ItemsPlanningTagsService( + Substitute.For(), + NullLogger.Instance, + _dbContext, + Substitute.For()); + + var result = await service.GetItemsPlanningTags(); + + Assert.That(result.Success, Is.True); + Assert.That(result.Model.Select(x => x.Name), + Is.EqualTo(new[] { "Alfa", "Beta", "Æble", "Øko", "Åben" })); + } +} diff --git a/eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn/Helpers/TagNameComparer.cs b/eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn/Helpers/TagNameComparer.cs new file mode 100644 index 00000000..2aaa1758 --- /dev/null +++ b/eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn/Helpers/TagNameComparer.cs @@ -0,0 +1,41 @@ +/* +The MIT License (MIT) + +Copyright (c) 2007 - 2026 Microting A/S + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +namespace ItemsPlanning.Pn.Helpers; + +using System; +using System.Globalization; + +/// +/// The one order for planning tag names (#2126). Always Danish (a…z, æ, ø, å), +/// case-insensitive, regardless of the user's UI language, so every tag picker +/// shows the same order. The database collation (utf8mb4_general_ci) folds Å to A, +/// so tags are sorted in memory with this comparer instead of in SQL. +/// Needs ICU at runtime (InvariantGlobalization must stay off in the host). +/// +public static class TagNameComparer +{ + public static readonly StringComparer Danish = + StringComparer.Create(CultureInfo.GetCultureInfo("da-DK"), ignoreCase: true); +} diff --git a/eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn/Services/ItemsPlanningTagsService/ItemsPlanningTagsService.cs b/eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn/Services/ItemsPlanningTagsService/ItemsPlanningTagsService.cs index 7ce5fc26..ea4f6d18 100644 --- a/eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn/Services/ItemsPlanningTagsService/ItemsPlanningTagsService.cs +++ b/eFormAPI/Plugins/ItemsPlanning.Pn/ItemsPlanning.Pn/Services/ItemsPlanningTagsService/ItemsPlanningTagsService.cs @@ -38,6 +38,7 @@ namespace ItemsPlanning.Pn.Services.ItemsPlanningTagsService; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Helpers; using Infrastructure.Models.Planning; using Microting.eForm.Infrastructure.Constants; @@ -60,11 +61,12 @@ public async Task>> GetItemsPlanningT Id = x.Id, Name = x.Name, IsLocked = x.IsLocked - }).OrderBy(x => x.Name).ToListAsync(); + }).ToListAsync(); + // Sorted in memory: the column collation folds Å to A (#2126). return new OperationDataResult>( true, - itemsPlanningTags); + itemsPlanningTags.OrderBy(x => x.Name, TagNameComparer.Danish).ToList()); } catch (Exception e) {