Skip to content
60 changes: 56 additions & 4 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -94,7 +95,7 @@
{
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);
Expand All @@ -117,15 +118,15 @@
};
}

public async Task<WritingSystem> GetWritingSystem(WritingSystemId id, WritingSystemType type)
public async Task<WritingSystem?> GetWritingSystem(WritingSystemId id, WritingSystemType type)
{
var writingSystems = await GetWritingSystems();
return type switch
{
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)
Expand All @@ -149,7 +150,7 @@
}
}

public async Task<WritingSystem> CreateWritingSystem(WritingSystem writingSystem)

Check warning on line 153 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 153 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 153 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 153 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / GHA integration tests / dotnet

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 153 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 153 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 153 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 153 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
var type = writingSystem.Type;
var exitingWs = type == WritingSystemType.Analysis ? Cache.ServiceLocator.WritingSystems.AnalysisWritingSystems : Cache.ServiceLocator.WritingSystems.VernacularWritingSystems;
Expand Down Expand Up @@ -205,7 +206,7 @@
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<WritingSystem> UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api = null)
Expand All @@ -219,6 +220,57 @@
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<WritingSystemId?> 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<CoreWritingSystemDefinition> 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<PartOfSpeech> GetPartsOfSpeech()
{
return PartOfSpeechRepository
Expand All @@ -236,7 +288,7 @@
? FromLcmPartOfSpeech(partOfSpeech) : null);
}

public async Task<PartOfSpeech> CreatePartOfSpeech(PartOfSpeech partOfSpeech)

Check warning on line 291 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 291 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 291 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 291 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / GHA integration tests / dotnet

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 291 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 291 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 291 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 291 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
IPartOfSpeech? lcmPartOfSpeech = null;
if (partOfSpeech.Id == default) partOfSpeech.Id = Guid.NewGuid();
Expand Down Expand Up @@ -438,7 +490,7 @@
return new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name) };
}

public async Task<ComplexFormType> CreateComplexFormType(ComplexFormType complexFormType)

Check warning on line 493 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 493 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 493 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 493 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / GHA integration tests / dotnet

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 493 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 493 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 493 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 493 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
if (complexFormType.Id == default) complexFormType.Id = Guid.NewGuid();
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create complex form type",
Expand Down Expand Up @@ -1299,7 +1351,7 @@
return Task.FromResult(lcmSense is null ? null : FromLexSense(lcmSense));
}

public async Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)

Check warning on line 1354 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1354 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1354 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1354 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / GHA integration tests / dotnet

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1354 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1354 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1354 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1354 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
if (sense.Id == default) sense.Id = Guid.NewGuid();
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
Expand Down Expand Up @@ -1444,7 +1496,7 @@
return CmTranslationFactory.Create(parent, freeTranslationType);
}

public async Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? between = null)

Check warning on line 1499 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1499 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1499 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1499 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / GHA integration tests / dotnet

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1499 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1499 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1499 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1499 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
if (exampleSentence.Id == default) exampleSentence.Id = Guid.NewGuid();
if (!SenseRepository.TryGetObject(senseId, out var lexSense))
Expand Down
31 changes: 31 additions & 0 deletions backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<CoreWritingSystemDefinition> allWritingSystems,
int desiredIndex,
CoreWritingSystemDefinition workingWs
)
{
// copy original contents into a list
var updatedList = new List<CoreWritingSystemDefinition>(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);
}
}
79 changes: 79 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/WritingSystemSyncTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using FwLiteProjectSync.Tests.Fixtures;
using MiniLcm.Models;

namespace FwLiteProjectSync.Tests;

public class WritingSystemSyncTests : IClassFixture<SyncFixture>, 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);
}
}
6 changes: 6 additions & 0 deletions backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public Task<WritingSystem> UpdateWritingSystem(WritingSystem before, WritingSyst
return Task.FromResult(after);
}

public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition<WritingSystemId?> between)
{
DryRunRecords.Add(new DryRunRecord(nameof(MoveWritingSystem), $"Move writing system {id} between {between.Previous} and {between.Next}"));
await Task.CompletedTask;
}

public Task<PartOfSpeech> CreatePartOfSpeech(PartOfSpeech partOfSpeech)
{
DryRunRecords.Add(new DryRunRecord(nameof(CreatePartOfSpeech), $"Create part of speech {partOfSpeech.Name}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ private static IEnumerable<ChangeWithDependencies> GetAllChanges()
var setComplexFormComponentOrderChange = new LcmCrdt.Changes.SetOrderChange<ComplexFormComponent>(complexFormComponent.Id, 10);
yield return new ChangeWithDependencies(setComplexFormComponentOrderChange, [createComplexFormComponentChange]);

var setWritingSystemOrderChange = new LcmCrdt.Changes.SetOrderChange<WritingSystem>(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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@
{
DerivedType: SetOrderChange<ComplexFormComponent>,
TypeDiscriminator: SetOrderChange:ComplexFormComponent
},
{
DerivedType: SetOrderChange<WritingSystem>,
TypeDiscriminator: SetOrderChange:WritingSystem
}
],
IgnoreUnrecognizedTypeDiscriminators: false,
Expand Down
2 changes: 1 addition & 1 deletion backend/FwLite/LcmCrdt/Changes/SetOrderChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace LcmCrdt.Changes;

public class SetOrderChange<T>(Guid entityId, double order) : EditChange<T>(entityId), IPolyType
where T : class, IOrderable
where T : class, IOrderableNoId
{
public static string TypeName => $"{nameof(SetOrderChange<T>)}:" + typeof(T).Name;

Expand Down
12 changes: 11 additions & 1 deletion backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,17 @@ public async Task<WritingSystem> 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<WritingSystem?> GetWritingSystem(WritingSystemId id, WritingSystemType type)
public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition<WritingSystemId?> 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 order = await OrderPicker.PickOrder(repo.WritingSystems.Where(s => s.Type == type), betweenIds);
await AddChange(new Changes.SetOrderChange<WritingSystem>(ws.Id, order));
}

public async Task<WritingSystem?> GetWritingSystem(WritingSystemId id, WritingSystemType type)
{
await using var repo = await repoFactory.CreateRepoAsync();
return await repo.GetWritingSystem(id, type);
Expand Down
1 change: 1 addition & 0 deletions backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ public static void ConfigureCrdt(CrdtConfig config)
.Add<Changes.SetOrderChange<Sense>>()
.Add<Changes.SetOrderChange<ExampleSentence>>()
.Add<Changes.SetOrderChange<ComplexFormComponent>>()
.Add<Changes.SetOrderChange<WritingSystem>>()
// When adding anything other than a Delete or JsonPatch change,
// you must add an instance of it to UseChangesTests.GetAllChanges()
;
Expand Down
3 changes: 2 additions & 1 deletion backend/FwLite/LcmCrdt/OrderPicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ namespace LcmCrdt;

public static class OrderPicker
{
public static async Task<double> PickOrder<T>(IQueryable<T> siblings, BetweenPosition? between = null) where T : class, IOrderable
public static async Task<double> PickOrder<T>(IQueryable<T> 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 })
Expand Down
33 changes: 33 additions & 0 deletions backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions backend/FwLite/MiniLcm/IMiniLcmReadApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace MiniLcm;
public interface IMiniLcmReadApi
{
Task<WritingSystems> GetWritingSystems();
Task<WritingSystem?> GetWritingSystem(WritingSystemId id, WritingSystemType type);
IAsyncEnumerable<PartOfSpeech> GetPartsOfSpeech();
IAsyncEnumerable<Publication> GetPublications();
IAsyncEnumerable<SemanticDomain> GetSemanticDomains();
Expand Down
1 change: 1 addition & 0 deletions backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Task<WritingSystem> UpdateWritingSystem(WritingSystemId id,
UpdateObjectInput<WritingSystem> update);
Task<WritingSystem> 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<WritingSystemId?> between);

#region PartOfSpeech
Task<PartOfSpeech> CreatePartOfSpeech(PartOfSpeech partOfSpeech);
Expand Down
8 changes: 6 additions & 2 deletions backend/FwLite/MiniLcm/Models/IOrderable.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
Loading
Loading