Skip to content

Commit f26e6d1

Browse files
authored
Full virtual scrolling for entry list (#2152)
* Do morph type filtering server-side * Add Delayed component to wrap VList items * Move SortMenu and extract options * Add entry loader service * Make agent instructions leaner * Add virtual scrolling tests * Add entry-loader-service tests * Prepare backend API for querying entry index * v2 test improvements with added event handling * Add more failing event handling test cases * Frontend: Add entry index lookup, versions and invalidation * Use quiet reset for all events * Debounce quiet resets * Fix sqlite contains function not always registered * Handle restoring deleted entries
1 parent d6f0576 commit f26e6d1

File tree

65 files changed

+3010
-461
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+3010
-461
lines changed

.github/copilot-instructions.md

Lines changed: 0 additions & 60 deletions
This file was deleted.

AGENTS.md

Lines changed: 38 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,14 @@ This is a monorepo containing:
77
- **FwLite** - A lightweight FieldWorks application (MAUI desktop app)
88
- **FwHeadless** - A headless service for FieldWorks data processing
99

10-
## Tech Stack
10+
### Tech Stack
1111

1212
- **Backend**: .NET 9, C#, Entity Framework Core, GraphQL (Hot Chocolate)
1313
- **Frontend**: SvelteKit, TypeScript
1414
- **Database**: PostgreSQL
1515
- **Infrastructure**: Docker, Kubernetes, Skaffold, Tilt
1616

17-
## Development Commands
18-
19-
```bash
20-
# Backend
21-
dotnet build backend/LexBoxApi/LexBoxApi.csproj
22-
dotnet test
23-
24-
# FwLite (Windows)
25-
dotnet build backend/FwLite/FwLiteMaui/FwLiteMaui.csproj --framework net9.0-windows10.0.19041.0
26-
27-
# Frontend
28-
cd frontend && pnpm dev
29-
```
30-
31-
## Project Structure
17+
### Structure
3218

3319
```text
3420
languageforge-lexbox/
@@ -39,68 +25,67 @@ languageforge-lexbox/
3925
│ ├── FwLite/ # FwLite MAUI app
4026
│ ├── FwHeadless/ # Headless FW service
4127
│ └── Testing/ # Test projects
42-
├── frontend/ # SvelteKit web app
28+
├── frontend/ # Lexbox SvelteKit web app
29+
├── frontend/viewer/ # FieldWorks Lite frontend Svelte code
4330
└── deployment/ # K8s/Docker configs
4431
```
4532

46-
**IMPORTANT: Testing Policy**
47-
-**Do NOT run integration tests** (`dotnet test`) unless the user explicitly asks
48-
- Integration tests require full test infrastructure (database, services) and take significant time
49-
- Only run unit tests locally when verifying critical business logic
50-
- User must explicitly request test runs before executing them
51-
52-
### Checking GitHub Issues and PRs
53-
54-
When asked to check GitHub issues or PRs, use the `gh` CLI instead of browser tools:
55-
56-
```bash
57-
# List open issues
58-
gh issue list --limit 30
59-
60-
# List open PRs
61-
gh pr list --limit 30
62-
63-
# View specific issue or PR
64-
gh issue view <number>
65-
gh pr view <number>
66-
```
67-
68-
- If the user asks about "the" PR, but does not explicitly name a PR or branch, assume they mean the PR associated with the current branch.
69-
70-
Provide an in-conversation summary highlighting:
71-
- Urgent/critical issues (regressions, bugs, broken builds)
72-
- Common themes or patterns
73-
- Items needing immediate attention
74-
75-
**Why CLI over browser**: Faster, less tokens, easier to scan and discuss.
76-
7733
### Important Files
7834

7935
Key documentation for this project:
8036
- `README.md` - Project overview and setup
8137
- `AGENTS.md` - You are here! Agent instructions
82-
- `.github/copilot-instructions.md` - GitHub Copilot auto-loaded instructions
8338
- `.github/AGENTS.md` - **CI/CD and deployment guide** (workflows, K8s, Docker)
8439
- `docs/DEVELOPER-win.md` - Windows development setup
8540
- `docs/DEVELOPER-linux.md` - Linux development setup
8641
- `docs/DEVELOPER-osx.md` - macOS development setup
8742
- `backend/README.md` - Backend architecture
88-
- `backend/FwLite/AGENTS.md` - **FwLite/CRDT critical code guide** (data loss risks!)
89-
- `backend/FwHeadless/AGENTS.md` - **FwHeadless sync guide**
43+
- `backend/AGENTS.md` - General backend guidelines
44+
- `backend/LexBoxApi/AGENTS.md` - API & GraphQL specific rules
45+
- `backend/FwLite/AGENTS.md` - **FwLite/CRDT** (Critical code! Data loss risks!)
46+
- `backend/FwHeadless/AGENTS.md` - **FwHeadless guide** (Critical code! Data loss risks! Mercurial sync, FwData processing)
47+
- `frontend/AGENTS.md` - General frontend/SvelteKit rules
48+
- `frontend/viewer/AGENTS.md` - **FwLite Viewer** (Specific frontend rules)
9049
- `deployment/README.md` - Deployment and infrastructure
9150

51+
## Guidelines
52+
53+
### Testing
54+
55+
-**Do NOT run dotnet INTEGRATION tests** unless the user explicitly asks. They require full test infrastructure (database, services) which usually isn't available.
56+
-**DO run unit tests locally** and filter to the tests that are relevant to the changes you are making. Use IDE testing tools over the cli.
57+
9258
### Questions?
9359

9460
- Check existing issues: `gh issue list --limit 30`
9561
- Look at recent commits: `git log --oneline -20`
9662
- Read the docs in `docs/` directory
9763
- Create a GitHub issue if unsure
64+
- Ask the user to clarify
65+
66+
### Pre-Flight Check
67+
68+
Before implementing any change that will touch many files or is in a 🔴 **Critical** area (FwLite sync, FwHeadless) do a "Pre-Flight Check" and list every component in the chain that will be touched (e.g., MiniLcm -> LcmCrdt -> FwDataBridge -> SyncHelper).
9869

9970
### Important Rules
10071

101-
- ✅ Use GitHub Issues for task tracking
72+
-**ALWAYS read local `AGENTS.md` files** in the directories you are working in (and their parents) before starting.
73+
-**ALWAYS review relevant code paths** before asking clarification questions.
74+
- ✅ New instructions in AGENTS.md files should be SUCCINCT.
10275
- ✅ Use `gh` CLI for GitHub issues/PRs, not browser tools
76+
- ✅ When pulling PR comments with `gh` use `api`. It's the only thing that returns review comments.
77+
- ✅ If the user asks about "the" PR, but does not explicitly name a PR or branch, assume they mean the PR associated with the current branch.
10378
- ✅ Use **Mermaid diagrams** for flowcharts and architecture (not ASCII art)
79+
- ✅ Prefer IDE diagnostics (compiler/lint errors) over CLI tools for identifying issues. Fixing these diagnostics is part of completing any instruction.
10480
- ✅ Do NOT run integration tests unless user explicitly requests
105-
- ❌ Do NOT use ASCII art for diagrams (use Mermaid instead)
81+
- ✅ When handling a user prompt ALWAYS ask for clarification if there are details to clarify, important decisions that must be made first or the plan sounds unwise
10682
- ❌ Do NOT git commit or git push without explicit user approval
83+
84+
### 🛡️ VIGILANCE
85+
86+
-**NEVER "fix" a failure** by removing assertions, commenting out code, or changing data to match a broken implementation.
87+
-**ALWAYS fix the root cause** when a test or check fails.
88+
-**ALWAYS double-check** that your "fix" hasn't made a check or test meaningless (e.g., asserting `expect(true).toBe(true)`).
89+
-**Assert that E2E test user actions** e.g. (scroll, click, etc.) actually have the expected effect before proceeding further.
90+
91+
If you are struggling, explain the difficulty to the user instead of cheating. **Integrity is non-negotiable.**

backend/FwLite/AGENTS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ dotnet test FwLiteOnly.slnf
3030
dotnet build FwLiteMaui/FwLiteMaui.csproj --framework net9.0-windows10.0.19041.0
3131
```
3232

33+
## Generated Types (TypeScript)
34+
35+
The frontend viewer uses TypeScript types and API interfaces generated from .NET using **Reinforced.Typings**. These are automatically updated when you build the **FwLiteShared** project (or any project that depends on it like `FwLiteMaui` or `FwLiteWeb`).
36+
37+
```bash
38+
# To manually update generated types:
39+
dotnet build backend/FwLite/FwLiteShared/FwLiteShared.csproj
40+
```
41+
42+
The configuration for this lives in `FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs` and `FwLiteShared/Reinforced.Typings.settings.xml`.
43+
3344
## Project Structure
3445

3546
| Directory | Priority | Purpose |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using FwDataMiniLcmBridge.Tests.Fixtures;
2+
3+
namespace FwDataMiniLcmBridge.Tests.MiniLcmTests;
4+
5+
[Collection(ProjectLoaderFixture.Name)]
6+
public class EntryIndexTests(ProjectLoaderFixture fixture) : EntryIndexTestsBase
7+
{
8+
protected override Task<IMiniLcmApi> NewApi()
9+
{
10+
return Task.FromResult<IMiniLcmApi>(fixture.NewProjectApi("entry-index-test", "en", "en"));
11+
}
12+
}

backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -916,23 +916,27 @@ public IAsyncEnumerable<Entry> GetEntries(
916916
Func<ILexEntry, bool>? predicate, QueryOptions? options = null, string? query = null)
917917
{
918918
options ??= QueryOptions.Default;
919-
var entries = GetLexEntries(predicate, options);
920-
921-
entries = ApplySorting(options, entries, query);
919+
var entries = GetFilteredAndSortedEntries(predicate, options, options.Order, query);
922920
entries = options.ApplyPaging(entries);
923921

924922
return entries.ToAsyncEnumerable().Select(FromLexEntry);
925923
}
926924

927-
private IEnumerable<ILexEntry> ApplySorting(QueryOptions options, IEnumerable<ILexEntry> entries, string? query)
925+
private IEnumerable<ILexEntry> GetFilteredAndSortedEntries(Func<ILexEntry, bool>? predicate, FilterQueryOptions? filterOptions, SortOptions order, string? query)
926+
{
927+
var entries = GetLexEntries(predicate, filterOptions);
928+
return ApplySorting(order, entries, query);
929+
}
930+
931+
private IEnumerable<ILexEntry> ApplySorting(SortOptions order, IEnumerable<ILexEntry> entries, string? query)
928932
{
929-
var sortWs = GetWritingSystemHandle(options.Order.WritingSystem, WritingSystemType.Vernacular);
930-
if (options.Order.Field == SortField.SearchRelevance)
933+
var sortWs = GetWritingSystemHandle(order.WritingSystem, WritingSystemType.Vernacular);
934+
if (order.Field == SortField.SearchRelevance)
931935
{
932-
return entries.ApplyRoughBestMatchOrder(options.Order, sortWs, query);
936+
return entries.ApplyRoughBestMatchOrder(order, sortWs, query);
933937
}
934938

935-
return options.ApplyOrder(entries, e => e.LexEntryHeadword(sortWs));
939+
return order.ApplyOrder(entries, e => e.LexEntryHeadword(sortWs));
936940
}
937941

938942
public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options = null)
@@ -955,6 +959,24 @@ public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options
955959
return Task.FromResult(lexEntry is null ? null : FromLexEntry(lexEntry));
956960
}
957961

962+
public Task<int> GetEntryIndex(Guid entryId, string? query = null, IndexQueryOptions? options = null)
963+
{
964+
var predicate = EntrySearchPredicate(query);
965+
var entries = GetFilteredAndSortedEntries(predicate, options, options?.Order ?? SortOptions.Default, query);
966+
967+
var rowIndex = 0;
968+
foreach (var entry in entries)
969+
{
970+
if (entry.Guid == entryId)
971+
{
972+
return Task.FromResult(rowIndex);
973+
}
974+
rowIndex++;
975+
}
976+
977+
return Task.FromResult(-1);
978+
}
979+
958980
public async Task<Entry> CreateEntry(Entry entry, CreateEntryOptions? options = null)
959981
{
960982
options ??= CreateEntryOptions.Everything;

backend/FwLite/FwDataMiniLcmBridge/LexEntryFilterMapProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class LexEntryFilterMapProvider : EntryFilterMapProvider<ILexEntry>
3232

3333
public override Expression<Func<ILexEntry, string, object>> EntryCitationForm => (entry, ws) => entry.PickText(entry.CitationForm, ws);
3434
public override Expression<Func<ILexEntry, string, object>> EntryLiteralMeaning => (entry, ws) => entry.PickText(entry.LiteralMeaning, ws);
35+
public override Expression<Func<ILexEntry, object?>> EntryMorphType => e => LcmHelpers.FromLcmMorphType(e.PrimaryMorphType);
3536
public override Expression<Func<ILexEntry, object?>> EntryComplexFormTypes => e => EmptyToNull(e.ComplexFormEntryRefs.SelectMany(r => r.ComplexEntryTypesRS));
3637
public override Func<string, object>? EntryComplexFormTypesConverter => EntryFilter.NormalizeEmptyToNull<ILexEntryType>;
3738
}

backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ public Task<int> CountEntries(string? query, FilterQueryOptions? options)
9595
return Task.Run(() => _wrappedApi.CountEntries(query, options));
9696
}
9797

98+
[JSInvokable]
99+
public Task<int> GetEntryIndex(Guid id, string? query, IndexQueryOptions? options)
100+
{
101+
return Task.Run(() => _wrappedApi.GetEntryIndex(id, query, options));
102+
}
103+
98104
[JSInvokable]
99105
public Task<Entry[]> GetEntries(QueryOptions? options = null)
100106
{

backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder)
119119
builder.ExportAsInterfaces([
120120
typeof(QueryOptions),
121121
typeof(FilterQueryOptions),
122+
typeof(IndexQueryOptions),
122123
typeof(SortOptions),
123124
typeof(ExemplarOptions),
124125
typeof(EntryFilter),

backend/FwLite/FwLiteWeb/Routes/MiniLcmRoutes.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ await projectProvider.OpenProject(project, context.HttpContext.RequestServices)
9999
api.MapGet("/entries", MiniLcm.GetEntries);
100100
api.MapGet("/entries/{search}", MiniLcm.SearchEntries);
101101
api.MapGet("/entry/{id:Guid}", MiniLcm.GetEntry);
102+
api.MapGet("/entry/{id:Guid}/index", MiniLcm.GetEntryIndex);
102103
api.MapGet("/parts-of-speech", MiniLcm.GetPartsOfSpeech);
103104
api.MapGet("/semantic-domains", MiniLcm.GetSemanticDomains);
104105
api.MapGet("/publications", MiniLcm.GetPublications);
@@ -137,6 +138,16 @@ public static IAsyncEnumerable<Entry> SearchEntries([FromServices] MiniLcmHolder
137138
return api.GetEntry(id);
138139
}
139140

141+
public static Task<int> GetEntryIndex(
142+
Guid id,
143+
[FromQuery] string? search,
144+
[AsParameters] MiniLcmQueryOptions options,
145+
[FromServices] MiniLcmHolder holder)
146+
{
147+
var api = holder.MiniLcmApi;
148+
return api.GetEntryIndex(id, search, options.ToIndexQueryOptions());
149+
}
150+
140151
public static IAsyncEnumerable<PartOfSpeech> GetPartsOfSpeech([FromServices] MiniLcmHolder holder)
141152
{
142153
var api = holder.MiniLcmApi;
@@ -185,6 +196,19 @@ public QueryOptions ToQueryOptions()
185196
string.IsNullOrEmpty(GridifyFilter) ? null : new EntryFilter {GridifyFilter = GridifyFilter});
186197
}
187198

199+
public IndexQueryOptions ToIndexQueryOptions()
200+
{
201+
ExemplarOptions? exemplarOptions = string.IsNullOrEmpty(ExemplarValue) || ExemplarWritingSystem is null
202+
? null
203+
: new(ExemplarValue, ExemplarWritingSystem);
204+
var sortField = SortField ?? SortOptions.Default.Field;
205+
return new IndexQueryOptions(new SortOptions(sortField,
206+
SortWritingSystem ?? SortOptions.Default.WritingSystem,
207+
Ascending ?? SortOptions.Default.Ascending),
208+
exemplarOptions,
209+
string.IsNullOrEmpty(GridifyFilter) ? null : new EntryFilter {GridifyFilter = GridifyFilter});
210+
}
211+
188212
public SortField? SortField { get; set; } = SortOptions.Default.Field;
189213

190214
[DefaultValue(SortOptions.DefaultWritingSystem)]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Xunit.Abstractions;
2+
3+
namespace LcmCrdt.Tests.MiniLcmTests;
4+
5+
public class EntryIndexTests(ITestOutputHelper output) : EntryIndexTestsBase
6+
{
7+
private readonly MiniLcmApiFixture _fixture = new();
8+
9+
public override async Task InitializeAsync()
10+
{
11+
_fixture.LogTo(output);
12+
await _fixture.InitializeAsync();
13+
await base.InitializeAsync();
14+
}
15+
16+
protected override Task<IMiniLcmApi> NewApi()
17+
{
18+
return Task.FromResult<IMiniLcmApi>(_fixture.Api);
19+
}
20+
21+
public override async Task DisposeAsync()
22+
{
23+
await base.DisposeAsync();
24+
await _fixture.DisposeAsync();
25+
}
26+
}

0 commit comments

Comments
 (0)