From a0280060bccfff6113e2c6a6f41fcfdaa16be69f Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 8 Aug 2025 11:17:49 +0700 Subject: [PATCH 01/12] create move WritingSystem api with test --- .../Api/FwDataMiniLcmApi.cs | 10 ++++-- .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 6 ++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 7 +++- .../MiniLcm.Tests/WritingSystemTestsBase.cs | 33 +++++++++++++++++++ backend/FwLite/MiniLcm/IMiniLcmReadApi.cs | 1 + backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 1 + backend/LfClassicData/LfClassicMiniLcmApi.cs | 11 +++++++ 7 files changed, 65 insertions(+), 4 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 94fedc809f..062f4b9727 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -117,7 +117,7 @@ private WritingSystem FromLcmWritingSystem(CoreWritingSystemDefinition ws, int i }; } - public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) + public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) { var writingSystems = await GetWritingSystems(); return type switch @@ -125,7 +125,7 @@ public async Task GetWritingSystem(WritingSystemId id, WritingSys WritingSystemType.Vernacular => writingSystems.Vernacular.FirstOrDefault(ws => ws.WsId == id), WritingSystemType.Analysis => writingSystems.Analysis.FirstOrDefault(ws => ws.WsId == id), _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - } ?? throw new NullReferenceException($"unable to find writing system with id {id}"); + }; } internal void CompleteExemplars(WritingSystems writingSystems) @@ -205,7 +205,7 @@ await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem", update.Apply(updateProxy); return ValueTask.CompletedTask; }); - return await GetWritingSystem(id, type); + return await GetWritingSystem(id, type) ?? throw new NullReferenceException($"unable to find writing system with id {id}"); } public async Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api = null) @@ -219,6 +219,10 @@ await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem", return await GetWritingSystem(after.WsId, after.Type) ?? throw new NullReferenceException($"unable to find {after.Type} writing system with id {after.WsId}"); } + public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) + { + } + public IAsyncEnumerable GetPartsOfSpeech() { return PartOfSpeechRepository diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index 87c5d28549..6dd2ee0426 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -45,6 +45,12 @@ public Task UpdateWritingSystem(WritingSystem before, WritingSyst return Task.FromResult(after); } + public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) + { + DryRunRecords.Add(new DryRunRecord(nameof(MoveWritingSystem), $"Move writing system {id} between {between.Previous} and {between.Next}")); + await Task.CompletedTask; + } + public Task CreatePartOfSpeech(PartOfSpeech partOfSpeech) { DryRunRecords.Add(new DryRunRecord(nameof(CreatePartOfSpeech), $"Create part of speech {partOfSpeech.Name}")); diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index e5b18cd204..42a1a59caa 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -105,7 +105,12 @@ public async Task UpdateWritingSystem(WritingSystem before, Writi return await GetWritingSystem(after.WsId, after.Type) ?? throw new NullReferenceException("unable to find writing system with id " + after.WsId); } - private async ValueTask GetWritingSystem(WritingSystemId id, WritingSystemType type) + public Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) + { + throw new NotSupportedException("MoveWritingSystem is not supported"); + } + + public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) { await using var repo = await repoFactory.CreateRepoAsync(); return await repo.GetWritingSystem(id, type); diff --git a/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs b/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs index f7f359aae8..0ade096733 100644 --- a/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs @@ -72,4 +72,37 @@ public async Task UpdateExistingWritingSystem_Works() var updatedWritingSystem = await Api.UpdateWritingSystem(original, writingSystem); updatedWritingSystem.Abbreviation.Should().Be("New Abbreviation"); } + + [Fact] + public async Task MoveWritingSystem_Works() + { + var ws1 = await Api.CreateWritingSystem(new() + { + Id = Guid.NewGuid(), + WsId = "es", + Type = WritingSystemType.Vernacular, + Name = "Spanish", + Abbreviation = "Es", + Font = "Arial" + }); + var ws2 = await Api.CreateWritingSystem(new() + { + Id = Guid.NewGuid(), + WsId = "fr", + Type = WritingSystemType.Vernacular, + Name = "French", + Abbreviation = "Fr", + Font = "Arial" + }); + ws2.Order.Should().BeGreaterThan(ws1.Order); + + //act + await Api.MoveWritingSystem(ws2.WsId, WritingSystemType.Vernacular, new(null, ws1.WsId)); + + ws1 = await Api.GetWritingSystem(ws1.WsId, WritingSystemType.Vernacular); + ws1.Should().NotBeNull(); + ws2 = await Api.GetWritingSystem(ws2.WsId, WritingSystemType.Vernacular); + ws2.Should().NotBeNull(); + ws2.Order.Should().BeLessThan(ws1.Order); + } } diff --git a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs index a6c4865271..3607a295db 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs @@ -9,6 +9,7 @@ namespace MiniLcm; public interface IMiniLcmReadApi { Task GetWritingSystems(); + Task GetWritingSystem(WritingSystemId id, WritingSystemType type); IAsyncEnumerable GetPartsOfSpeech(); IAsyncEnumerable GetPublications(); IAsyncEnumerable GetSemanticDomains(); diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index dd33a7afbb..91fb56c1fd 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -15,6 +15,7 @@ Task UpdateWritingSystem(WritingSystemId id, UpdateObjectInput update); Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api = null); // Note there's no Task DeleteWritingSystem(Guid id) because deleting writing systems needs careful consideration, as it can cause a massive cascade of data deletion + Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between); #region PartOfSpeech Task CreatePartOfSpeech(PartOfSpeech partOfSpeech); diff --git a/backend/LfClassicData/LfClassicMiniLcmApi.cs b/backend/LfClassicData/LfClassicMiniLcmApi.cs index 23b59e5191..bfe215438e 100644 --- a/backend/LfClassicData/LfClassicMiniLcmApi.cs +++ b/backend/LfClassicData/LfClassicMiniLcmApi.cs @@ -64,6 +64,17 @@ public async Task GetWritingSystems() }; } + public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) + { + var ws = await GetWritingSystems(); + return type switch + { + WritingSystemType.Vernacular => ws.Vernacular.FirstOrDefault(w => w.WsId == id), + WritingSystemType.Analysis => ws.Analysis.FirstOrDefault(w => w.WsId == id), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } + public async Task PickDefaultVernacularWritingSystem() { var cacheKey = $"LfClassic|DefaultVernacular|{projectCode}"; From d266d27233f3192f8115eba0f79e0d6a10e2fc41 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 8 Aug 2025 11:45:21 +0700 Subject: [PATCH 02/12] implement move writing system for FwData --- .../Api/FwDataMiniLcmApi.cs | 50 ++++++++++++++++++- .../FwDataMiniLcmBridge/Api/LcmHelpers.cs | 31 ++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 062f4b9727..1265fc9ef8 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -19,6 +19,7 @@ using SIL.LCModel.Core.WritingSystems; using SIL.LCModel.DomainServices; using SIL.LCModel.Infrastructure; +using CollectionExtensions = SIL.Extensions.CollectionExtensions; namespace FwDataMiniLcmBridge.Api; @@ -94,7 +95,7 @@ public Task GetWritingSystems() { Vernacular = WritingSystemContainer.CurrentVernacularWritingSystems.Select((definition, index) => FromLcmWritingSystem(definition, index, WritingSystemType.Vernacular)).ToArray(), - Analysis = Cache.ServiceLocator.WritingSystems.CurrentAnalysisWritingSystems.Select((definition, index) => + Analysis = WritingSystemContainer.CurrentAnalysisWritingSystems.Select((definition, index) => FromLcmWritingSystem(definition, index, WritingSystemType.Analysis)).ToArray() }; CompleteExemplars(writingSystems); @@ -221,6 +222,53 @@ await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem", public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) { + var wsToUpdate = GetLexWritingSystem(id, type); + if (wsToUpdate is null) throw new NullReferenceException($"unable to find writing system with id {id}"); + var previousWs = between.Previous is null ? null : GetLexWritingSystem(between.Previous.Value, type); + var nextWs = between.Next is null ? null : GetLexWritingSystem(between.Next.Value, type); + if (nextWs is null && previousWs is null) throw new NullReferenceException($"unable to find writing system with id {between.Previous} or {between.Next}"); + await Cache.DoUsingNewOrCurrentUOW("Move WritingSystem", + "Revert Move WritingSystem", + () => + { + var exitingWs = type == WritingSystemType.Analysis + ? WritingSystemContainer.AnalysisWritingSystems + : WritingSystemContainer.VernacularWritingSystems; + var currentExistingWs = type == WritingSystemType.Analysis + ? WritingSystemContainer.CurrentAnalysisWritingSystems + : WritingSystemContainer.CurrentVernacularWritingSystems; + MoveWs(wsToUpdate, previousWs, nextWs, exitingWs); + MoveWs(wsToUpdate, previousWs, nextWs, currentExistingWs); + + void MoveWs(CoreWritingSystemDefinition ws, + CoreWritingSystemDefinition? previous, + CoreWritingSystemDefinition? next, + ICollection list) + { + int index; + if (previous is not null) + { + index = CollectionExtensions.IndexOf(list, previous) + 1; + } else if (next is not null) + { + index = CollectionExtensions.IndexOf(list, next); + } else + { + throw new InvalidOperationException("unable to find writing system with id " + between.Previous + " or " + between.Next); + } + LcmHelpers.AddOrMoveInList(list, index, ws); + } + + return ValueTask.CompletedTask; + }); + } + + private CoreWritingSystemDefinition? GetLexWritingSystem(WritingSystemId id, WritingSystemType type) + { + var exitingWs = type == WritingSystemType.Analysis + ? WritingSystemContainer.AnalysisWritingSystems + : WritingSystemContainer.VernacularWritingSystems; + return exitingWs.FirstOrDefault(ws => ws.Id == id); } public IAsyncEnumerable GetPartsOfSpeech() diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs index 3812af59e3..74d6e898c6 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs @@ -1,9 +1,11 @@ using System.Globalization; using MiniLcm.Culture; using MiniLcm.Models; +using SIL.Extensions; using SIL.LCModel; using SIL.LCModel.Core.KernelInterfaces; using SIL.LCModel.Core.Text; +using SIL.LCModel.Core.WritingSystems; namespace FwDataMiniLcmBridge.Api; @@ -199,4 +201,33 @@ internal static void SetString(this ITsMultiString multiString, FwDataMiniLcmApi multiString.set_String(writingSystemHandle, tsString); } } + + //copy of method in SIL.FieldWorks.FwCoreDlgs.FwWritingSystemSetupModel + internal static void AddOrMoveInList( + ICollection allWritingSystems, + int desiredIndex, + CoreWritingSystemDefinition workingWs + ) + { + // copy original contents into a list + var updatedList = new List(allWritingSystems); + var ws = updatedList.Find(listItem => + listItem.Id == (string.IsNullOrEmpty(workingWs.Id) ? workingWs.LanguageTag : workingWs.Id)); + if (ws != null) + { + updatedList.Remove(ws); + } + + if (desiredIndex > updatedList.Count) + { + updatedList.Add(workingWs); + } + else + { + updatedList.Insert(desiredIndex, workingWs); + } + + allWritingSystems.Clear(); + allWritingSystems.AddRange(updatedList); + } } From 2983af6ffbe8190fae52fb73d823f66524155e97 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 8 Aug 2025 12:00:38 +0700 Subject: [PATCH 03/12] implement moving ws in LcmCrdt --- .../Changes/RegressionDeserializationData.json | 5 +++++ .../FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs | 3 +++ ...aModelSnapshotTests.VerifyChangeModels.verified.txt | 4 ++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 9 +++++++-- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 1 + backend/FwLite/MiniLcm/Models/WritingSystem.cs | 2 +- backend/FwLite/MiniLcm/SyncHelpers/DiffCollection.cs | 10 +++++++++- 7 files changed, 30 insertions(+), 4 deletions(-) diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/RegressionDeserializationData.json b/backend/FwLite/LcmCrdt.Tests/Changes/RegressionDeserializationData.json index cc4acedfff..7d74dfec83 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/RegressionDeserializationData.json +++ b/backend/FwLite/LcmCrdt.Tests/Changes/RegressionDeserializationData.json @@ -277,6 +277,11 @@ }, "EntityId": "e03826fb-e6d2-6e3c-1f43-9fc6a0f6c4c6" }, + { + "$type": "SetOrderChange:WritingSystem", + "Order": 0.4870418422144669, + "EntityId": "9d3e9006-c4b4-4316-6628-faeede75af40" + }, { "$type": "SetOrderChange:Sense", "Order": 0.4870418422144669, diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs b/backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs index 83ce47e7ed..04e1543132 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs @@ -199,6 +199,9 @@ private static IEnumerable GetAllChanges() var setComplexFormComponentOrderChange = new LcmCrdt.Changes.SetOrderChange(complexFormComponent.Id, 10); yield return new ChangeWithDependencies(setComplexFormComponentOrderChange, [createComplexFormComponentChange]); + var setWritingSystemOrderChange = new LcmCrdt.Changes.SetOrderChange(writingSystem.Id, 10); + yield return new ChangeWithDependencies(setWritingSystemOrderChange, [createWritingSystemChange]); + var publication = new Publication { Id = Guid.NewGuid(), Name = { { "en", "Main" } } }; var createPublicationChange = new CreatePublicationChange(publication.Id, publication.Name); yield return new ChangeWithDependencies(createPublicationChange); diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt index 388fb1480e..2b712953af 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt @@ -171,6 +171,10 @@ { DerivedType: SetOrderChange, TypeDiscriminator: SetOrderChange:ComplexFormComponent + }, + { + DerivedType: SetOrderChange, + TypeDiscriminator: SetOrderChange:WritingSystem } ], IgnoreUnrecognizedTypeDiscriminators: false, diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 42a1a59caa..707f0025c3 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -105,9 +105,14 @@ public async Task UpdateWritingSystem(WritingSystem before, Writi return await GetWritingSystem(after.WsId, after.Type) ?? throw new NullReferenceException("unable to find writing system with id " + after.WsId); } - public Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) + public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) { - throw new NotSupportedException("MoveWritingSystem is not supported"); + var ws = await GetWritingSystem(id, type); + if (ws is null) throw new NullReferenceException($"unable to find writing system with id {id}"); + await using var repo = await repoFactory.CreateRepoAsync(); + var betweenIds = await between.MapAsync(async wsId => wsId is null ? null : (await GetWritingSystem(wsId.Value, type))?.Id); + var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(s => s.Type == type), betweenIds); + await AddChange(new Changes.SetOrderChange(ws.Id, order)); } public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 7b8535f8af..2a7c75af85 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -259,6 +259,7 @@ public static void ConfigureCrdt(CrdtConfig config) .Add>() .Add>() .Add>() + .Add>() // When adding anything other than a Delete or JsonPatch change, // you must add an instance of it to UseChangesTests.GetAllChanges() ; diff --git a/backend/FwLite/MiniLcm/Models/WritingSystem.cs b/backend/FwLite/MiniLcm/Models/WritingSystem.cs index 68c163855f..8aca39a4c7 100644 --- a/backend/FwLite/MiniLcm/Models/WritingSystem.cs +++ b/backend/FwLite/MiniLcm/Models/WritingSystem.cs @@ -2,7 +2,7 @@ namespace MiniLcm.Models; -public record WritingSystem: IObjectWithId +public record WritingSystem: IObjectWithId, IOrderable { public required Guid Id { get; set; } public virtual required WritingSystemId WsId { get; set; } diff --git a/backend/FwLite/MiniLcm/SyncHelpers/DiffCollection.cs b/backend/FwLite/MiniLcm/SyncHelpers/DiffCollection.cs index aecad8ea03..7d8376ae71 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/DiffCollection.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/DiffCollection.cs @@ -245,5 +245,13 @@ public record PositionDiff(int Index, PositionDiffKind Kind) public int SortIndex => Kind == PositionDiffKind.Remove ? -Index - 1 : Index; } -public record BetweenPosition(T? Previous, T? Next); +public record BetweenPosition(T? Previous, T? Next) +{ + public async Task MapAsync(Func> map) + { + return new BetweenPosition( + Previous is null ? null : await map(Previous), + Next is null ? null : await map(Next)); + } +} public record BetweenPosition(Guid? Previous, Guid? Next) : BetweenPosition(Previous, Next); From 500ad5c7260a2e1456f8e415397b28c0b951155d Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 8 Aug 2025 15:02:55 +0700 Subject: [PATCH 04/12] handle diff orderable writing systems --- .../WritingSystemSyncTests.cs | 79 +++++++++++++++++++ .../FwLite/LcmCrdt/Changes/SetOrderChange.cs | 2 +- backend/FwLite/LcmCrdt/OrderPicker.cs | 3 +- backend/FwLite/MiniLcm/Models/IOrderable.cs | 8 +- .../FwLite/MiniLcm/Models/WritingSystem.cs | 5 +- .../FwLite/MiniLcm/Models/WritingSystemId.cs | 1 + .../MiniLcm/SyncHelpers/WritingSystemSync.cs | 75 +++++++++++++++--- 7 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 backend/FwLite/FwLiteProjectSync.Tests/WritingSystemSyncTests.cs diff --git a/backend/FwLite/FwLiteProjectSync.Tests/WritingSystemSyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/WritingSystemSyncTests.cs new file mode 100644 index 0000000000..9d36afe5e1 --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync.Tests/WritingSystemSyncTests.cs @@ -0,0 +1,79 @@ +using FwLiteProjectSync.Tests.Fixtures; +using MiniLcm.Models; + +namespace FwLiteProjectSync.Tests; + +public class WritingSystemSyncTests : IClassFixture, IAsyncLifetime +{ + + private readonly SyncFixture _fixture; + private readonly CrdtFwdataProjectSyncService _syncService; + + public WritingSystemSyncTests(SyncFixture fixture) + { + _fixture = fixture; + _syncService = _fixture.SyncService; + } + + public async Task InitializeAsync() + { + await _fixture.FwDataApi.CreateEntry(new Entry() + { + Id = Guid.NewGuid(), + LexemeForm = { { "en", "Pineapple" } }, + }); + await _fixture.FwDataApi.CreateWritingSystem(new WritingSystem() + { + Id = Guid.NewGuid(), + Type = WritingSystemType.Vernacular, + WsId = new WritingSystemId("fr"), + Name = "French", + Abbreviation = "fr", + Font = "Arial" + }); + await _syncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi); + } + + public async Task DisposeAsync() + { + await foreach (var entry in _fixture.FwDataApi.GetAllEntries()) + { + await _fixture.FwDataApi.DeleteEntry(entry.Id); + } + + foreach (var entry in await _fixture.CrdtApi.GetAllEntries().ToArrayAsync()) + { + await _fixture.CrdtApi.DeleteEntry(entry.Id); + } + } + + [Fact] + public async Task SyncWs_UpdatesOrder() + { + var en = await _fixture.FwDataApi.GetWritingSystem("en", WritingSystemType.Vernacular); + en.Should().NotBeNull(); + en.Order.Should().Be(0); + var fr = await _fixture.FwDataApi.GetWritingSystem("fr", WritingSystemType.Vernacular); + fr.Should().NotBeNull(); + fr.Order.Should().Be(1); + await _fixture.FwDataApi.MoveWritingSystem("fr", WritingSystemType.Vernacular, new(null, "en")); + fr = await _fixture.FwDataApi.GetWritingSystem("fr", WritingSystemType.Vernacular); + fr.Should().NotBeNull(); + fr.Order.Should().Be(0); + var crdtFr = await _fixture.CrdtApi.GetWritingSystem("fr", WritingSystemType.Vernacular); + crdtFr.Should().NotBeNull(); + crdtFr.Order.Should().Be(1); + + await _syncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi); + + fr = await _fixture.FwDataApi.GetWritingSystem("fr", WritingSystemType.Vernacular); + fr.Should().NotBeNull(); + fr.Order.Should().Be(0); + + var crdtEn = await _fixture.CrdtApi.GetWritingSystem("en", WritingSystemType.Vernacular); + crdtEn.Should().NotBeNull(); + var actualFr = await _fixture.CrdtApi.GetWritingSystem("fr", WritingSystemType.Vernacular); + actualFr.Should().NotBeNull(); + actualFr.Order.Should().BeLessThan(crdtEn.Order); + } +} diff --git a/backend/FwLite/LcmCrdt/Changes/SetOrderChange.cs b/backend/FwLite/LcmCrdt/Changes/SetOrderChange.cs index fa97d50a02..97b6a1008a 100644 --- a/backend/FwLite/LcmCrdt/Changes/SetOrderChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/SetOrderChange.cs @@ -5,7 +5,7 @@ namespace LcmCrdt.Changes; public class SetOrderChange(Guid entityId, double order) : EditChange(entityId), IPolyType - where T : class, IOrderable + where T : class, IOrderableNoId { public static string TypeName => $"{nameof(SetOrderChange)}:" + typeof(T).Name; diff --git a/backend/FwLite/LcmCrdt/OrderPicker.cs b/backend/FwLite/LcmCrdt/OrderPicker.cs index e044ee110a..14eb7746e7 100644 --- a/backend/FwLite/LcmCrdt/OrderPicker.cs +++ b/backend/FwLite/LcmCrdt/OrderPicker.cs @@ -5,7 +5,8 @@ namespace LcmCrdt; public static class OrderPicker { - public static async Task PickOrder(IQueryable siblings, BetweenPosition? between = null) where T : class, IOrderable + public static async Task PickOrder(IQueryable siblings, BetweenPosition? between = null) + where T : class, IOrderableNoId, IObjectWithId//this is weird, but WritingSystems should not be IOrderable, because that won't work with FW data, but they have Ids when working with CRDTs { // a common case that we can optimize by not querying whole objects if (between is null or { Previous: null, Next: null }) diff --git a/backend/FwLite/MiniLcm/Models/IOrderable.cs b/backend/FwLite/MiniLcm/Models/IOrderable.cs index 994e2e31ce..2d22155605 100644 --- a/backend/FwLite/MiniLcm/Models/IOrderable.cs +++ b/backend/FwLite/MiniLcm/Models/IOrderable.cs @@ -1,7 +1,11 @@ namespace MiniLcm.Models; -public interface IOrderable +public interface IOrderableNoId { - Guid Id { get; } double Order { get; set; } } + +public interface IOrderable: IOrderableNoId +{ + Guid Id { get; } +} diff --git a/backend/FwLite/MiniLcm/Models/WritingSystem.cs b/backend/FwLite/MiniLcm/Models/WritingSystem.cs index 8aca39a4c7..f4cfd5df8c 100644 --- a/backend/FwLite/MiniLcm/Models/WritingSystem.cs +++ b/backend/FwLite/MiniLcm/Models/WritingSystem.cs @@ -2,8 +2,11 @@ namespace MiniLcm.Models; -public record WritingSystem: IObjectWithId, IOrderable +public record WritingSystem: IObjectWithId, IOrderableNoId { + /// + /// this ID is always empty when working with FW data, it is only used when working with CRDTs + /// public required Guid Id { get; set; } public virtual required WritingSystemId WsId { get; set; } public bool IsAudio => WsId.IsAudio; diff --git a/backend/FwLite/MiniLcm/Models/WritingSystemId.cs b/backend/FwLite/MiniLcm/Models/WritingSystemId.cs index 607ef97ae4..79b8e65a2c 100644 --- a/backend/FwLite/MiniLcm/Models/WritingSystemId.cs +++ b/backend/FwLite/MiniLcm/Models/WritingSystemId.cs @@ -47,6 +47,7 @@ public static WritingSystemId FromUnknown(object? obj) public WritingSystemId(string code) { + ArgumentException.ThrowIfNullOrEmpty(code); //__key is used by the LfClassicMiniLcmApi to smuggle non guid ids with possibilitie lists if (code == "default" || code == "__key") { diff --git a/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs b/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs index 114bd2ea26..db1bef6e3e 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs @@ -1,4 +1,5 @@ using MiniLcm.Models; +using SIL.Extensions; using SystemTextJsonPatch; namespace MiniLcm.SyncHelpers; @@ -16,10 +17,8 @@ public static async Task Sync(WritingSystem[] beforeWritingSystems, WritingSystem[] afterWritingSystems, IMiniLcmApi api) { - return await DiffCollection.Diff( - beforeWritingSystems, - afterWritingSystems, - new WritingSystemsDiffApi(api)); + var writingSystemsDiffApi = new WritingSystemsDiffApi(api); + return await writingSystemsDiffApi.Diff(beforeWritingSystems, afterWritingSystems); } public static async Task Sync(WritingSystem beforeWs, WritingSystem afterWs, IMiniLcmApi api) @@ -51,29 +50,81 @@ public static async Task Sync(WritingSystem beforeWs, WritingSystem afterWs return new UpdateObjectInput(patchDocument); } - private class WritingSystemsDiffApi(IMiniLcmApi api) : CollectionDiffApi + private class WritingSystemsDiffApi(IMiniLcmApi api) : IOrderableCollectionDiffApi { - public override (WritingSystemId, WritingSystemType) GetId(WritingSystem value) + private Dictionary Mapping { get; } = new(); + public async Task Diff(WritingSystem[] beforeWritingSystems, WritingSystem[] afterWritingSystems) { - return (value.WsId, value.Type); + return await DiffCollection.DiffOrderable( + //diff collection must work with a Guid, and the id's must match between the two lists + //so we just generate the guids on the fly and make sure they're the same for the same wsId + beforeWritingSystems.Select(ws => new OrderableWs(ws, GetOrderableId(ws.WsId))).OrderBy(o => o.Order).ToList(), + afterWritingSystems.Select(ws => new OrderableWs(ws, GetOrderableId(ws.WsId))).OrderBy(o => o.Order).ToList(), + this + ); } - public override async Task Add(WritingSystem currentWs) + private Guid GetOrderableId(WritingSystemId wsId) { - await api.CreateWritingSystem(currentWs); + foreach (var kvp in Mapping) + { + if (kvp.Value == wsId) + { + return kvp.Key; + } + } + var newId = Guid.NewGuid(); + Mapping[newId] = wsId; + return newId; + } + + private class OrderableWs : IOrderable + { + public WritingSystem Ws { get; } + //can't use Ws Guid because it is not set for FwData + public Guid Id { get; } + + public double Order + { + get => Ws.Order; + set => Ws.Order = value; + } + + public OrderableWs(WritingSystem ws, Guid id) + { + this.Ws = ws; + this.Id = id; + } + } + + async Task IOrderableCollectionDiffApi.Add(OrderableWs value, BetweenPosition between) + { + //todo set order? + await api.CreateWritingSystem(value.Ws); return 1; } - public override Task Remove(WritingSystem beforeWs) + Task IOrderableCollectionDiffApi.Remove(OrderableWs value) { // await api.DeleteWritingSystem(beforeWs.Id); // Deleting writing systems is dangerous as it causes cascading data deletion. Needs careful thought. // TODO: should we throw an exception? return Task.FromResult(0); } - public override Task Replace(WritingSystem beforeWs, WritingSystem afterWs) + async Task IOrderableCollectionDiffApi.Move(OrderableWs value, BetweenPosition between) + { + var betweenWsId = new BetweenPosition( + //we can't use `null` and must use new WritingSystemId?() to set a nullable value to null + between.Previous is null ? new WritingSystemId?() : Mapping[between.Previous.Value], + between.Next is null ? new WritingSystemId?() : Mapping[between.Next.Value] + ); + await api.MoveWritingSystem(value.Ws.WsId, value.Ws.Type, betweenWsId); + return 1; + } + + Task IOrderableCollectionDiffApi.Replace(OrderableWs before, OrderableWs after) { - return Sync(beforeWs, afterWs, api); + return Sync(before.Ws, after.Ws, api); } } } From 771a1a9e11052d1848fd5ad4f8dcdf6d4fed3754 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 12 Aug 2025 14:21:10 +0200 Subject: [PATCH 05/12] Mark WritingSystem.Order as MiniLcmInternal --- backend/FwLite/MiniLcm/Models/WritingSystem.cs | 2 ++ .../generated-types/MiniLcm/Models/IWritingSystem.ts | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/FwLite/MiniLcm/Models/WritingSystem.cs b/backend/FwLite/MiniLcm/Models/WritingSystem.cs index f4cfd5df8c..c3c4971f6f 100644 --- a/backend/FwLite/MiniLcm/Models/WritingSystem.cs +++ b/backend/FwLite/MiniLcm/Models/WritingSystem.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using MiniLcm.Attributes; namespace MiniLcm.Models; @@ -21,6 +22,7 @@ public record WritingSystem: IObjectWithId, IOrderableNoId public static string[] LatinExemplars => Enumerable.Range('A', 'Z' - 'A' + 1).Select(c => ((char)c).ToString()).ToArray(); + [MiniLcmInternal] public double Order { get; set; } public Guid[] GetReferences() diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IWritingSystem.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IWritingSystem.ts index 9e6ccf42e3..735af80459 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IWritingSystem.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IWritingSystem.ts @@ -17,6 +17,5 @@ export interface IWritingSystem extends IObjectWithId deletedAt?: string; type: WritingSystemType; exemplars: string[]; - order: number; } /* eslint-enable */ From 3b577ee186e1f5165ab8962c4d0e63ec90472421 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 12 Aug 2025 15:15:52 +0200 Subject: [PATCH 06/12] Implement inserting WritingSystems --- .../Api/FwDataMiniLcmApi.cs | 10 +++-- .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 5 ++- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 12 +++--- .../MiniLcm.Tests/WritingSystemTestsBase.cs | 42 ++++++++++++++++++- backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 2 +- .../MiniLcm/SyncHelpers/WritingSystemSync.cs | 8 +++- .../Validators/MiniLcmApiValidationWrapper.cs | 4 +- 7 files changed, 66 insertions(+), 17 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 1265fc9ef8..41498273ef 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -150,7 +150,7 @@ internal void CompleteExemplars(WritingSystems writingSystems) } } - public async Task CreateWritingSystem(WritingSystem writingSystem) + public async Task CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between = null) { var type = writingSystem.Type; var exitingWs = type == WritingSystemType.Analysis ? Cache.ServiceLocator.WritingSystems.AnalysisWritingSystems : Cache.ServiceLocator.WritingSystems.VernacularWritingSystems; @@ -159,10 +159,9 @@ public async Task CreateWritingSystem(WritingSystem writingSystem throw new DuplicateObjectException($"Writing system {writingSystem.WsId.Code} already exists"); } CoreWritingSystemDefinition? ws = null; - UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Writing System", + await Cache.DoUsingNewOrCurrentUOW("Create Writing System", "Remove writing system", - Cache.ServiceLocator.ActionHandler, - () => + async () => { Cache.ServiceLocator.WritingSystemManager.GetOrSet(writingSystem.WsId.Code, out ws); ws.Abbreviation = writingSystem.Abbreviation; @@ -177,6 +176,9 @@ public async Task CreateWritingSystem(WritingSystem writingSystem default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } + + if (between is not null && (between.Previous is not null || between.Next is not null)) + await MoveWritingSystem(writingSystem.WsId, type, between); }); if (ws is null) throw new InvalidOperationException("Writing system not found"); var index = type switch diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index 6dd2ee0426..3cee26fbc1 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -18,9 +18,10 @@ public void Dispose() public record DryRunRecord(string Method, string Description); - public Task CreateWritingSystem(WritingSystem writingSystem) + public Task CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? position = null) { - DryRunRecords.Add(new DryRunRecord(nameof(CreateWritingSystem), $"Create writing system {writingSystem.Type}")); + DryRunRecords.Add(new DryRunRecord(nameof(CreateWritingSystem), + $"Create writing system {writingSystem.Type} between {position?.Previous} and {position?.Next}")); return Task.FromResult(writingSystem); } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 707f0025c3..bded64ebad 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -78,15 +78,17 @@ public async Task GetWritingSystems() }; } - public async Task CreateWritingSystem(WritingSystem writingSystem) + public async Task CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between = null) { await using var repo = await repoFactory.CreateRepoAsync(); var entityId = Guid.NewGuid(); - var exists = await repo.WritingSystems.AnyAsync(ws => ws.WsId == writingSystem.WsId && ws.Type == writingSystem.Type); + var wsType = writingSystem.Type; + var exists = await repo.WritingSystems.AnyAsync(ws => ws.WsId == writingSystem.WsId && ws.Type == wsType); if (exists) throw new DuplicateObjectException($"Writing system {writingSystem.WsId.Code} already exists"); - var wsCount = await repo.WritingSystems.CountAsync(ws => ws.Type == writingSystem.Type); - await AddChange(new CreateWritingSystemChange(writingSystem, entityId, wsCount)); - return await repo.GetWritingSystem(writingSystem.WsId, writingSystem.Type) ?? throw new NullReferenceException(); + var betweenIds = between is null ? null : await between.MapAsync(async wsId => wsId is null ? null : (await GetWritingSystem(wsId.Value, wsType))?.Id); + var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(ws => ws.Type == wsType), betweenIds); + await AddChange(new CreateWritingSystemChange(writingSystem, entityId, order)); + return await repo.GetWritingSystem(writingSystem.WsId, wsType) ?? throw new NullReferenceException(); } public async Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update) diff --git a/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs b/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs index 0ade096733..eef8c431ba 100644 --- a/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs @@ -1,5 +1,5 @@ using MiniLcm.Exceptions; -using MiniLcm.Models; +using MiniLcm.SyncHelpers; namespace MiniLcm.Tests; @@ -99,10 +99,50 @@ public async Task MoveWritingSystem_Works() //act await Api.MoveWritingSystem(ws2.WsId, WritingSystemType.Vernacular, new(null, ws1.WsId)); + //assert ws1 = await Api.GetWritingSystem(ws1.WsId, WritingSystemType.Vernacular); ws1.Should().NotBeNull(); ws2 = await Api.GetWritingSystem(ws2.WsId, WritingSystemType.Vernacular); ws2.Should().NotBeNull(); ws2.Order.Should().BeLessThan(ws1.Order); + + var writingSystems = await Api.GetWritingSystems(); + var en = writingSystems.Vernacular.Single(ws => ws.WsId.Code == "en"); + writingSystems.Vernacular.Should().BeEquivalentTo([en, ws2, ws1], + // we care about the order of return, not the internal Order property + options => options.WithStrictOrdering().Excluding(ws => ws.Order)); + } + + [Fact] + public async Task InsertWritingSystem_Works() + { + var writingSystems = await Api.GetWritingSystems(); + var en = writingSystems.Vernacular.Single(ws => ws.WsId.Code == "en"); + + //act + var ws1 = await Api.CreateWritingSystem(new() + { + Id = Guid.NewGuid(), + WsId = "es", + Type = WritingSystemType.Vernacular, + Name = "Spanish", + Abbreviation = "Es", + Font = "Arial" + }, new BetweenPosition(null, en.WsId)); + var ws2 = await Api.CreateWritingSystem(new() + { + Id = Guid.NewGuid(), + WsId = "fr", + Type = WritingSystemType.Vernacular, + Name = "French", + Abbreviation = "Fr", + Font = "Arial" + }, new BetweenPosition(ws1.WsId, en.WsId)); + + // assert + writingSystems = await Api.GetWritingSystems(); + writingSystems.Vernacular.Should().BeEquivalentTo([ws1, ws2, en], + // we care about the order of return, not the internal Order property + options => options.WithStrictOrdering().Excluding(ws => ws.Order)); } } diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index 91fb56c1fd..0002ab05fd 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -8,7 +8,7 @@ namespace MiniLcm; public interface IMiniLcmWriteApi { - Task CreateWritingSystem(WritingSystem writingSystem); + Task CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between = null); Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, diff --git a/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs b/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs index db1bef6e3e..11cf303543 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs @@ -99,8 +99,12 @@ public OrderableWs(WritingSystem ws, Guid id) async Task IOrderableCollectionDiffApi.Add(OrderableWs value, BetweenPosition between) { - //todo set order? - await api.CreateWritingSystem(value.Ws); + var betweenWsId = new BetweenPosition( + //we can't use `null` and must use new WritingSystemId?() to set a nullable value to null + between.Previous is null ? new WritingSystemId?() : Mapping[between.Previous.Value], + between.Next is null ? new WritingSystemId?() : Mapping[between.Next.Value] + ); + await api.CreateWritingSystem(value.Ws, betweenWsId); return 1; } diff --git a/backend/FwLite/MiniLcm/Validators/MiniLcmApiValidationWrapper.cs b/backend/FwLite/MiniLcm/Validators/MiniLcmApiValidationWrapper.cs index b360fdc73a..077873b5d1 100644 --- a/backend/FwLite/MiniLcm/Validators/MiniLcmApiValidationWrapper.cs +++ b/backend/FwLite/MiniLcm/Validators/MiniLcmApiValidationWrapper.cs @@ -21,10 +21,10 @@ public partial class MiniLcmApiValidationWrapper( // ********** Overrides go here ********** - public async Task CreateWritingSystem(WritingSystem writingSystem) + public async Task CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between = null) { await validators.ValidateAndThrow(writingSystem); - return await _api.CreateWritingSystem(writingSystem); + return await _api.CreateWritingSystem(writingSystem, between); } public async Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update) From 2935308604572ea508fe21b27cb4cc3e51cea90d Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 12 Aug 2025 15:30:34 +0200 Subject: [PATCH 07/12] AI feedback --- .../FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | 16 ++++++++++------ .../FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs | 12 ++++++++++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 41498273ef..9e9a2037a0 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -247,17 +247,21 @@ void MoveWs(CoreWritingSystemDefinition ws, CoreWritingSystemDefinition? next, ICollection list) { - int index; + var index = -1; if (previous is not null) { - index = CollectionExtensions.IndexOf(list, previous) + 1; - } else if (next is not null) + index = CollectionExtensions.IndexOf(list, previous); + if (index >= 0) index++; + } + + if (index < 0 && next is not null) { index = CollectionExtensions.IndexOf(list, next); - } else - { - throw new InvalidOperationException("unable to find writing system with id " + between.Previous + " or " + between.Next); } + + if (index < 0) + throw new InvalidOperationException("unable to find writing system with id " + between.Previous + " or " + between.Next); + LcmHelpers.AddOrMoveInList(list, index, ws); } diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs index 74d6e898c6..4b660254cb 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs @@ -202,17 +202,25 @@ internal static void SetString(this ITsMultiString multiString, FwDataMiniLcmApi } } - //copy of method in SIL.FieldWorks.FwCoreDlgs.FwWritingSystemSetupModel + //mostly a copy of method in SIL.FieldWorks.FwCoreDlgs.FwWritingSystemSetupModel internal static void AddOrMoveInList( ICollection allWritingSystems, int desiredIndex, CoreWritingSystemDefinition workingWs ) { + if (desiredIndex < 0) throw new ArgumentOutOfRangeException(nameof(desiredIndex), desiredIndex, "desiredIndex must be >= 0"); + // copy original contents into a list var updatedList = new List(allWritingSystems); var ws = updatedList.Find(listItem => - listItem.Id == (string.IsNullOrEmpty(workingWs.Id) ? workingWs.LanguageTag : workingWs.Id)); + { + if (ReferenceEquals(listItem, workingWs)) return true; + var workingTag = string.IsNullOrEmpty(workingWs.Id) ? workingWs.LanguageTag : workingWs.Id; + var listItemTag = string.IsNullOrEmpty(listItem.Id) ? listItem.LanguageTag : listItem.Id; + return string.Equals(listItemTag, workingTag); + }); + if (ws != null) { updatedList.Remove(ws); From 0776aad1a3c53e7703ffa1893c55066ccdc14498 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 12 Aug 2025 15:36:27 +0200 Subject: [PATCH 08/12] Remove internal order value from demo ts data --- frontend/viewer/src/lib/demo-entry-data.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/viewer/src/lib/demo-entry-data.ts b/frontend/viewer/src/lib/demo-entry-data.ts index 08ce32835b..1cf2c89efa 100644 --- a/frontend/viewer/src/lib/demo-entry-data.ts +++ b/frontend/viewer/src/lib/demo-entry-data.ts @@ -40,7 +40,6 @@ export const writingSystems: IWritingSystems = { 'font': '???', 'exemplars': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'], 'type': WritingSystemType.Analysis, - 'order': 0, isAudio: false, }, { @@ -51,7 +50,6 @@ export const writingSystems: IWritingSystems = { 'font': '???', 'exemplars': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'], 'type': WritingSystemType.Analysis, - 'order': 1, isAudio: false, } ], @@ -64,7 +62,6 @@ export const writingSystems: IWritingSystems = { 'font': '???', 'exemplars': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'], 'type': WritingSystemType.Vernacular, - 'order': 0, isAudio: false, }, { @@ -75,7 +72,6 @@ export const writingSystems: IWritingSystems = { 'font': '???', 'exemplars': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'], 'type': WritingSystemType.Vernacular, - 'order': 1, isAudio: false, } ] From c8713f094f781b57448ce76b824f87eae949636c Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 12 Aug 2025 15:40:27 +0200 Subject: [PATCH 09/12] Fix method signature --- .../FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs b/backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs index bf63243712..00bf736ab1 100644 --- a/backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs +++ b/backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; using MiniLcm; using MiniLcm.Models; +using MiniLcm.SyncHelpers; namespace FwLiteProjectSync.Import; @@ -65,9 +66,9 @@ async Task IMiniLcmWriteApi.CreatePublication(Publication publicati { return await HasCreated(publication, _api.GetPublications(), () => _api.CreatePublication(publication)); } - async Task IMiniLcmWriteApi.CreateWritingSystem(WritingSystem writingSystem) + async Task IMiniLcmWriteApi.CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between = null) { - return await HasCreated(writingSystem, AsyncWs(), () => _api.CreateWritingSystem(writingSystem), ws => ws.Type + ws.WsId.Code); + return await HasCreated(writingSystem, AsyncWs(), () => _api.CreateWritingSystem(writingSystem, between), ws => ws.Type + ws.WsId.Code); } private async IAsyncEnumerable AsyncWs() From d47573d47bbd961482c1e88a341359db4f092f4d Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 13 Aug 2025 11:46:35 +0700 Subject: [PATCH 10/12] reuse exiting repo when possible --- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index bded64ebad..c633eb5d71 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -85,7 +85,7 @@ public async Task CreateWritingSystem(WritingSystem writingSystem var wsType = writingSystem.Type; var exists = await repo.WritingSystems.AnyAsync(ws => ws.WsId == writingSystem.WsId && ws.Type == wsType); if (exists) throw new DuplicateObjectException($"Writing system {writingSystem.WsId.Code} already exists"); - var betweenIds = between is null ? null : await between.MapAsync(async wsId => wsId is null ? null : (await GetWritingSystem(wsId.Value, wsType))?.Id); + var betweenIds = between is null ? null : await between.MapAsync(async wsId => wsId is null ? null : (await repo.GetWritingSystem(wsId.Value, wsType))?.Id); var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(ws => ws.Type == wsType), betweenIds); await AddChange(new CreateWritingSystemChange(writingSystem, entityId, order)); return await repo.GetWritingSystem(writingSystem.WsId, wsType) ?? throw new NullReferenceException(); @@ -109,10 +109,10 @@ public async Task UpdateWritingSystem(WritingSystem before, Writi public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) { - var ws = await GetWritingSystem(id, type); - if (ws is null) throw new NullReferenceException($"unable to find writing system with id {id}"); await using var repo = await repoFactory.CreateRepoAsync(); - var betweenIds = await between.MapAsync(async wsId => wsId is null ? null : (await GetWritingSystem(wsId.Value, type))?.Id); + var ws = await repo.GetWritingSystem(id, type); + if (ws is null) throw new NullReferenceException($"unable to find writing system with id {id}"); + var betweenIds = await between.MapAsync(async wsId => wsId is null ? null : (await repo.GetWritingSystem(wsId.Value, type))?.Id); var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(s => s.Type == type), betweenIds); await AddChange(new Changes.SetOrderChange(ws.Id, order)); } From 8d4a3d4ebdc78993a15f024d87f2b968dfe65266 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 13 Aug 2025 11:57:38 +0700 Subject: [PATCH 11/12] update unreliable api so it compiles --- .../FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs index d280802f6f..e53c7aa158 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs @@ -203,9 +203,9 @@ Task IMiniLcmWriteApi.CreatePublication(Publication publication) ResumableTests.MaybeThrowRandom(random, 0.2); return _api.CreatePublication(publication); } - Task IMiniLcmWriteApi.CreateWritingSystem(WritingSystem writingSystems) + Task IMiniLcmWriteApi.CreateWritingSystem(WritingSystem writingSystems, BetweenPosition? between) { ResumableTests.MaybeThrowRandom(random, 0.2); - return _api.CreateWritingSystem(writingSystems); + return _api.CreateWritingSystem(writingSystems, between); } } From 364e5c0a56d40302bb322fe4966ef05dbc114cb5 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Thu, 14 Aug 2025 10:53:17 +0200 Subject: [PATCH 12/12] Fix lint --- frontend/viewer/src/lib/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/viewer/src/lib/utils.ts b/frontend/viewer/src/lib/utils.ts index b18b94982e..bd6ff14a13 100644 --- a/frontend/viewer/src/lib/utils.ts +++ b/frontend/viewer/src/lib/utils.ts @@ -73,7 +73,6 @@ export function defaultWritingSystem(type: WritingSystemType): IWritingSystem { abbreviation: 'en', font: 'Arial', exemplars: [], - order: 0, deletedAt: undefined, type };