Skip to content

Commit

Permalink
Allow reordering example sentences
Browse files Browse the repository at this point in the history
  • Loading branch information
myieye committed Jan 10, 2025
1 parent 2499cd6 commit 3c62972
Show file tree
Hide file tree
Showing 23 changed files with 229 additions and 70 deletions.
55 changes: 51 additions & 4 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,30 @@ internal void InsertSense(ILexEntry lexEntry, ILexSense lexSense, BetweenPositio
lexEntry.SensesOS.Add(lexSense);
}

internal void InsertExampleSentence(ILexSense lexSense, ILexExampleSentence lexExample, BetweenPosition? between = null)
{
var previousExampleId = between?.Previous;
var nextExampleId = between?.Next;

var previousExample = previousExampleId.HasValue ? lexSense.ExamplesOS.FirstOrDefault(s => s.Guid == previousExampleId) : null;
if (previousExample is not null)
{
var insertI = lexSense.ExamplesOS.IndexOf(previousExample) + 1;
lexSense.ExamplesOS.Insert(insertI, lexExample);
return;
}

var nextExample = nextExampleId.HasValue ? lexSense.ExamplesOS.FirstOrDefault(s => s.Guid == nextExampleId) : null;
if (nextExample is not null)
{
var insertI = lexSense.ExamplesOS.IndexOf(nextExample);
lexSense.ExamplesOS.Insert(insertI, lexExample);
return;
}

lexSense.ExamplesOS.Add(lexExample);
}

private void ApplySenseToLexSense(Sense sense, ILexSense lexSense)
{
if (lexSense.MorphoSyntaxAnalysisRA.GetPartOfSpeech()?.Guid != sense.PartOfSpeechId)
Expand Down Expand Up @@ -1056,9 +1080,10 @@ public Task DeleteSense(Guid entryId, Guid senseId)
return Task.FromResult(lcmExampleSentence is null ? null : FromLexExampleSentence(senseId, lcmExampleSentence));
}

internal void CreateExampleSentence(ILexSense lexSense, ExampleSentence exampleSentence)
internal void CreateExampleSentence(ILexSense lexSense, ExampleSentence exampleSentence, BetweenPosition? between = null)
{
var lexExampleSentence = LexExampleSentenceFactory.Create(exampleSentence.Id, lexSense);
var lexExampleSentence = LexExampleSentenceFactory.Create(exampleSentence.Id);
InsertExampleSentence(lexSense, lexExampleSentence, between);
UpdateLcmMultiString(lexExampleSentence.Example, exampleSentence.Sentence);
var freeTranslationType = CmPossibilityRepository.GetObject(CmPossibilityTags.kguidTranFreeTranslation);
var translation = CmTranslationFactory.Create(lexExampleSentence, freeTranslationType);
Expand All @@ -1067,15 +1092,15 @@ internal void CreateExampleSentence(ILexSense lexSense, ExampleSentence exampleS
lexExampleSentence.Reference.get_WritingSystem(0));
}

public Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence)
public Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? between = null)
{
if (exampleSentence.Id == default) exampleSentence.Id = Guid.NewGuid();
if (!SenseRepository.TryGetObject(senseId, out var lexSense))
throw new InvalidOperationException("Sense not found");
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Example Sentence",
"Remove example sentence",
Cache.ServiceLocator.ActionHandler,
() => CreateExampleSentence(lexSense, exampleSentence));
() => CreateExampleSentence(lexSense, exampleSentence, between));
return Task.FromResult(FromLexExampleSentence(senseId, ExampleSentenceRepository.GetObject(exampleSentence.Id)));
}

Expand Down Expand Up @@ -1111,6 +1136,28 @@ await Cache.DoUsingNewOrCurrentUOW("Update Example Sentence",
return await GetExampleSentence(entryId, senseId, after.Id) ?? throw new NullReferenceException("unable to find example sentence with id " + after.Id);
}

public Task MoveExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, BetweenPosition between)
{
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
throw new InvalidOperationException("Entry not found");
if (!SenseRepository.TryGetObject(senseId, out var lexSense))
throw new InvalidOperationException("Sense not found");
if (!ExampleSentenceRepository.TryGetObject(exampleSentenceId, out var lexExample))
throw new InvalidOperationException("Example sentence not found");

ValidateOwnership(lexExample, entryId, senseId);

UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Move Example sentence",
"Move Example sentence back",
Cache.ServiceLocator.ActionHandler,
() =>
{
// LibLCM treats an insert as a move if the example sentence is already on the sense
InsertExampleSentence(lexSense, lexExample, between);
});
return Task.CompletedTask;
}

public Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId)
{
var lexExampleSentence = ExampleSentenceRepository.GetObject(exampleSentenceId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,9 @@ public override required Guid Id
}
}

public override IList<ExampleSentence> ExampleSentences
public override List<ExampleSentence> ExampleSentences
{
get =>
new UpdateListProxy<ExampleSentence>(
sentence => lexboxLcmApi.CreateExampleSentence(sense, sentence),
sentence => lexboxLcmApi.DeleteExampleSentence(sense.Owner.Guid, Id, sentence.Id),
i => new UpdateExampleSentenceProxy(sense.ExamplesOS[i], lexboxLcmApi),
sense.ExamplesOS.Count
);
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
}
4 changes: 3 additions & 1 deletion backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ public async Task CanSyncRandomEntries()
var actual = await _fixture.CrdtApi.GetEntry(after.Id);
actual.Should().NotBeNull();
actual.Should().BeEquivalentTo(after, options => options
.For(e => e.Senses).Exclude(s => s.Order));
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order)
);
}

[Fact]
Expand Down
3 changes: 2 additions & 1 deletion backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ private void ShouldAllBeEquivalentTo(Dictionary<Guid, Entry> crdtEntries, Dictio
options => options
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id)
.For(e => e.Senses).Exclude(s => s.Order),
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order),
$"CRDT entry {crdtEntry.Id} was synced with FwData");
}
}
Expand Down
7 changes: 7 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public async Task FirstSyncJustDoesAnImport()
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}
Expand Down Expand Up @@ -149,6 +150,7 @@ await crdtApi.CreateEntry(new Entry()
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}
Expand Down Expand Up @@ -229,6 +231,7 @@ public async Task CreatingAComplexEntryInFwDataSyncsWithoutIssue()
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));

Expand Down Expand Up @@ -313,6 +316,7 @@ await crdtApi.CreateEntry(new Entry()
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}
Expand Down Expand Up @@ -394,6 +398,7 @@ await crdtApi.CreateEntry(new Entry()
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}
Expand All @@ -419,6 +424,7 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth()
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
//todo the headword should be changed
.For(e => e.Components).Exclude(c => c.ComponentHeadword)
Expand Down Expand Up @@ -488,6 +494,7 @@ public async Task AddingASenseToAnEntryInEachProjectSyncsAcrossBoth()
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}
Expand Down
10 changes: 8 additions & 2 deletions backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,9 @@ public Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId)
return api.GetExampleSentence(entryId, senseId, id);
}

public Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence)
public Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null)
{
DryRunRecords.Add(new DryRunRecord(nameof(CreateExampleSentence), $"Create example sentence {exampleSentence.Sentence}"));
DryRunRecords.Add(new DryRunRecord(nameof(CreateExampleSentence), $"Create example sentence {exampleSentence.Sentence} between {position?.Previous} and {position?.Next}"));
return Task.FromResult(exampleSentence);
}

Expand All @@ -278,6 +278,12 @@ public Task<ExampleSentence> UpdateExampleSentence(Guid entryId,
return Task.FromResult(after);
}

public Task MoveExampleSentence(Guid entryId, Guid senseId, Guid exampleId, BetweenPosition between)
{
DryRunRecords.Add(new DryRunRecord(nameof(MoveExampleSentence), $"Move example sentence {exampleId} between {between.Previous} and {between.Next}"));
return Task.CompletedTask;
}

public Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId)
{
DryRunRecords.Add(new DryRunRecord(nameof(DeleteExampleSentence), $"Delete example sentence {exampleSentenceId}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using LcmCrdt;
using Microsoft.JSInterop;
using MiniLcm;
using MiniLcm.Attributes;
using MiniLcm.Models;
using Reinforced.Typings;
using Reinforced.Typings.Ast.Dependency;
Expand Down Expand Up @@ -45,26 +46,25 @@ public static void Configure(ConfigurationBuilder builder)
From = "@microsoft/dotnet-js-interop",
Target = "type {DotNet}"
}]));
builder.ExportAsInterface<Sense>().WithPublicNonStaticProperties(exportBuilder =>
{
if (exportBuilder.Member.Name == nameof(Sense.Order))
{
exportBuilder.Ignore();
}
});
builder.ExportAsInterfaces([
typeof(Entry),
typeof(Sense),
typeof(ExampleSentence),
typeof(WritingSystem),
typeof(WritingSystems),
typeof(PartOfSpeech),
typeof(SemanticDomain),
typeof(ComplexFormType),
typeof(ComplexFormComponent),

typeof(MiniLcmJsInvokable.MiniLcmFeatures),
],
exportBuilder => exportBuilder.WithPublicNonStaticProperties());
exportBuilder => exportBuilder.WithPublicNonStaticProperties(exportBuilder =>
{
if (exportBuilder.Member.GetCustomAttribute<MiniLcmInternalAttribute>() is not null)
{
exportBuilder.Ignore();
}
}));
builder.ExportAsEnum<WritingSystemType>().UseString();
builder.ExportAsInterface<MiniLcmJsInvokable>()
.FlattenHierarchy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@
{
DerivedType: SetOrderChange<Sense>,
TypeDiscriminator: SetOrderChange:Sense
},
{
DerivedType: SetOrderChange<ExampleSentence>,
TypeDiscriminator: SetOrderChange:ExampleSentence
}
],
IgnoreUnrecognizedTypeDiscriminators: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
Properties:
Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
DeletedAt (DateTimeOffset?)
Order (double) Required
Reference (string)
SenseId (Guid) Required FK Index
Sentence (MultiString) Required
Expand Down Expand Up @@ -208,7 +209,7 @@
Relational:ColumnType: jsonb
SnapshotId (no field, Guid?) Shadow FK Index
Navigations:
ExampleSentences (IList<ExampleSentence>) Collection ToDependent ExampleSentence
ExampleSentences (List<ExampleSentence>) Collection ToDependent ExampleSentence
Keys:
Id PK
Foreign keys:
Expand Down
3 changes: 3 additions & 0 deletions backend/FwLite/LcmCrdt/Changes/CreateExampleSentenceChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public CreateExampleSentenceChange(ExampleSentence exampleSentence, Guid senseId
{
exampleSentence.Id = EntityId;
SenseId = senseId;
Order = exampleSentence.Order;
Sentence = exampleSentence.Sentence;
Translation = exampleSentence.Translation;
Reference = exampleSentence.Reference;
Expand All @@ -24,6 +25,7 @@ private CreateExampleSentenceChange(Guid entityId, Guid senseId) : base(entityId
}

public Guid SenseId { get; init; }
public double Order { get; set; }
public MultiString? Sentence { get; set; }
public MultiString? Translation { get; set; }
public string? Reference { get; set; }
Expand All @@ -34,6 +36,7 @@ public override async ValueTask<ExampleSentence> NewEntity(Commit commit, Change
{
Id = EntityId,
SenseId = SenseId,
Order = Order,
Sentence = Sentence ?? new MultiString(),
Translation = Translation ?? new MultiString(),
Reference = Reference,
Expand Down
30 changes: 24 additions & 6 deletions backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ private IEnumerable<IChange> CreateEntryChanges(Entry entry, Dictionary<Guid, Se
{
yield return addComplexFormTypeChange;
}
var i = 1;
var senseOrder = 1;
foreach (var sense in entry.Senses)
{
sense.SemanticDomains = sense.SemanticDomains
Expand All @@ -350,10 +350,14 @@ private IEnumerable<IChange> CreateEntryChanges(Entry entry, Dictionary<Guid, Se
}
if (sense.Order != default) // we don't anticipate this being necessary, so we'll be strict for now
throw new InvalidOperationException("Order should not be provided when creating a sense");
sense.Order = i++;
sense.Order = senseOrder++;
yield return new CreateSenseChange(sense, entry.Id);
var exampleOrder = 1;
foreach (var exampleSentence in sense.ExampleSentences)
{
if (exampleSentence.Order != default) // we don't anticipate this being necessary, so we'll be strict for now
throw new InvalidOperationException("Order should not be provided when creating an example sentence");
exampleSentence.Order = exampleOrder++;
yield return new CreateExampleSentenceChange(exampleSentence, sense.Id);
}
}
Expand Down Expand Up @@ -470,10 +474,13 @@ private async IAsyncEnumerable<IChange> CreateSenseChanges(Guid entryId, Sense s
}

yield return new CreateSenseChange(sense, entryId);
foreach (var change in sense.ExampleSentences.Select(sentence =>
new CreateExampleSentenceChange(sentence, sense.Id)))
var exampleOrder = 1;
foreach (var exampleSentence in sense.ExampleSentences)
{
yield return change;
if (exampleSentence.Order != default) // we don't anticipate this being necessary, so we'll be strict for now
throw new InvalidOperationException("Order should not be provided when creating an example sentence");
exampleSentence.Order = exampleOrder++;
yield return new CreateExampleSentenceChange(exampleSentence, sense.Id);
}
}

Expand Down Expand Up @@ -536,8 +543,13 @@ public async Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomai

public async Task<ExampleSentence> CreateExampleSentence(Guid entryId,
Guid senseId,
ExampleSentence exampleSentence)
ExampleSentence exampleSentence,
BetweenPosition? between = null)
{
if (exampleSentence.Order != default) // we don't anticipate this being necessary, so we'll be strict for now
throw new InvalidOperationException("Order should not be provided when creating an example sentence");

exampleSentence.Order = await OrderPicker.PickOrder(ExampleSentences.Where(s => s.SenseId == senseId), between);
await dataModel.AddChange(ClientId, new CreateExampleSentenceChange(exampleSentence, senseId));
return await dataModel.GetLatest<ExampleSentence>(exampleSentence.Id) ?? throw new NullReferenceException();
}
Expand Down Expand Up @@ -570,6 +582,12 @@ public async Task<ExampleSentence> UpdateExampleSentence(Guid entryId,
return await GetExampleSentence(entryId, senseId, after.Id) ?? throw new NullReferenceException();
}

public async Task MoveExampleSentence(Guid entryId, Guid senseId, Guid exampleId, BetweenPosition between)
{
var order = await OrderPicker.PickOrder(ExampleSentences.Where(s => s.SenseId == senseId), between);
await dataModel.AddChange(ClientId, new Changes.SetOrderChange<ExampleSentence>(exampleId, order));
}

public async Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId)
{
await dataModel.AddChange(ClientId, new DeleteChange<ExampleSentence>(exampleSentenceId));
Expand Down
4 changes: 3 additions & 1 deletion backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ public static void ConfigureCrdt(CrdtConfig config)
.Add<RemoveComplexFormTypeChange>()
.Add<SetComplexFormComponentChange>()
.Add<CreateComplexFormType>()
.Add<Changes.SetOrderChange<Sense>>();
.Add<Changes.SetOrderChange<Sense>>()
.Add<Changes.SetOrderChange<ExampleSentence>>()
;
}

public static Type[] AllChangeTypes()
Expand Down
4 changes: 4 additions & 0 deletions backend/FwLite/LcmCrdt/QueryHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ public static class QueryHelpers
public static void ApplySortOrder(this Entry entry)
{
entry.Senses.ApplySortOrder();
foreach (var sense in entry.Senses)
{
sense.ExampleSentences.ApplySortOrder();
}
}

public static void ApplySortOrder<T>(this List<T> items) where T : IOrderable
Expand Down
Loading

0 comments on commit 3c62972

Please sign in to comment.