Skip to content

Commit 56eeb94

Browse files
myieyehahn-kev
andauthored
Show total entry count in sidebar and filter (#1721)
* Add CountEntries API * Show entry count in sidebar * Show entry count in filter placeholder * Format numbers with lingui/core --------- Co-authored-by: Kevin Hahn <kevin_hahn@sil.org>
1 parent 9504970 commit 56eeb94

27 files changed

Lines changed: 509 additions & 157 deletions

File tree

backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ private RichMultiString FromLcmMultiString(IMultiString multiString)
690690

691691
internal RichString? ToRichString(ITsString? tsString)
692692
{
693-
if (tsString is null or {Length: 0}) return null;
693+
if (tsString is null or { Length: 0 }) return null;
694694
return RichTextMapping.FromTsString(tsString,
695695
h =>
696696
{
@@ -699,21 +699,28 @@ private RichMultiString FromLcmMultiString(IMultiString multiString)
699699
});
700700
}
701701

702+
public Task<int> CountEntries(string? query = null, FilterQueryOptions? options = null)
703+
{
704+
if (options?.HasFilter == true || query?.Length is > 0)
705+
return Task.FromResult(GetLexEntries(EntrySearchPredicate(query), options).Count());
706+
return Task.FromResult(EntriesRepository.Count);
707+
}
708+
702709
public IAsyncEnumerable<Entry> GetEntries(QueryOptions? options = null)
703710
{
704711
return GetEntries(null, options);
705712
}
706713

707-
public IAsyncEnumerable<Entry> GetEntries(
708-
Func<ILexEntry, bool>? predicate, QueryOptions? options = null)
714+
public IEnumerable<ILexEntry> GetLexEntries(
715+
Func<ILexEntry, bool>? predicate, FilterQueryOptions? options = null)
709716
{
710717
var entries = EntriesRepository.AllInstances();
711718

712-
options ??= QueryOptions.Default;
719+
options ??= FilterQueryOptions.Default;
713720
if (predicate is not null) entries = entries.Where(predicate);
714721
if (!string.IsNullOrEmpty(options.Filter?.GridifyFilter))
715722
{
716-
var query = new GridifyQuery(){Filter = options.Filter.GridifyFilter};
723+
var query = new GridifyQuery() { Filter = options.Filter.GridifyFilter };
717724
var filter = query.GetFilteringExpression(config.Value.Mapper).Compile();
718725
entries = entries.Where(filter);
719726
}
@@ -734,6 +741,14 @@ public IAsyncEnumerable<Entry> GetEntries(
734741
return CultureInfo.InvariantCulture.CompareInfo.IsPrefix(value, exemplar, CompareOptions.IgnoreCase);
735742
});
736743
}
744+
return entries;
745+
}
746+
747+
public IAsyncEnumerable<Entry> GetEntries(
748+
Func<ILexEntry, bool>? predicate, QueryOptions? options = null)
749+
{
750+
options ??= QueryOptions.Default;
751+
var entries = GetLexEntries(predicate, options);
737752

738753
var sortWs = GetWritingSystemHandle(options.Order.WritingSystem, WritingSystemType.Vernacular);
739754
string? order(ILexEntry e)
@@ -756,13 +771,18 @@ public IAsyncEnumerable<Entry> GetEntries(
756771

757772
public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options = null)
758773
{
759-
var entries = GetEntries(e =>
760-
e.CitationForm.SearchValue(query) ||
761-
e.LexemeFormOA.Form.SearchValue(query) ||
762-
e.AllSenses.Any(s => s.Gloss.SearchValue(query)), options);
774+
var entries = GetEntries(EntrySearchPredicate(query), options);
763775
return entries;
764776
}
765777

778+
private Func<ILexEntry, bool>? EntrySearchPredicate(string? query = null)
779+
{
780+
if (string.IsNullOrEmpty(query)) return null;
781+
return entry => entry.CitationForm.SearchValue(query) ||
782+
entry.LexemeFormOA.Form.SearchValue(query) ||
783+
entry.AllSenses.Any(s => s.Gloss.SearchValue(query));
784+
}
785+
766786
public Task<Entry?> GetEntry(Guid id)
767787
{
768788
return Task.FromResult<Entry?>(FromLexEntry(EntriesRepository.GetObject(id)));

backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ public ValueTask<ComplexFormType[]> GetComplexFormTypes()
7474
return _wrappedApi.GetComplexFormType(id);
7575
}
7676

77+
[JSInvokable]
78+
public Task<int> CountEntries(string? query, FilterQueryOptions? options)
79+
{
80+
return _wrappedApi.CountEntries(query, options);
81+
}
82+
7783
[JSInvokable]
7884
public ValueTask<Entry[]> GetEntries(QueryOptions? options = null)
7985
{

backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder)
100100
.WithPublicProperties()
101101
.WithPublicMethods(b => b.AlwaysReturnPromise().OnlyJsInvokable());
102102
builder.ExportAsEnum<SortField>().UseString();
103-
builder.ExportAsInterfaces([typeof(QueryOptions), typeof(SortOptions), typeof(ExemplarOptions), typeof(EntryFilter)],
103+
builder.ExportAsInterfaces([typeof(QueryOptions), typeof(FilterQueryOptions), typeof(SortOptions), typeof(ExemplarOptions), typeof(EntryFilter)],
104104
exportBuilder => exportBuilder.WithPublicNonStaticProperties());
105105
}
106106

backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,13 @@ public async Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId)
352352
await AddChange(new RemoveComplexFormTypeChange(entryId, complexFormTypeId));
353353
}
354354

355+
public async Task<int> CountEntries(string? query = null, FilterQueryOptions? options = null)
356+
{
357+
var predicate = string.IsNullOrEmpty(query) ? null : Filtering.SearchFilter(query);
358+
var queryable = await FilterEntries(predicate, options);
359+
return await queryable.CountAsync();
360+
}
361+
355362
public IAsyncEnumerable<Entry> GetEntries(QueryOptions? options = null)
356363
{
357364
return GetEntriesAsyncEnum(predicate: null, options);
@@ -376,20 +383,7 @@ private async IAsyncEnumerable<Entry> GetEntries(
376383
QueryOptions? options = null)
377384
{
378385
options ??= QueryOptions.Default;
379-
var queryable = Entries;
380-
if (predicate is not null) queryable = queryable.Where(predicate);
381-
if (options.Exemplar is not null)
382-
{
383-
var ws = (await GetWritingSystem(options.Exemplar.WritingSystem, WritingSystemType.Vernacular))?.WsId;
384-
if (ws is null)
385-
throw new NullReferenceException($"writing system {options.Exemplar.WritingSystem} not found");
386-
queryable = queryable.WhereExemplar(ws.Value, options.Exemplar.Value);
387-
}
388-
389-
if (options.Filter?.GridifyFilter != null)
390-
{
391-
queryable = queryable.ApplyFiltering(options.Filter.GridifyFilter, LcmConfig.Mapper);
392-
}
386+
var queryable = await FilterEntries(predicate, options);
393387

394388
var sortWs = (await GetWritingSystem(options.Order.WritingSystem, WritingSystemType.Vernacular));
395389
if (sortWs is null)
@@ -421,6 +415,28 @@ private async IAsyncEnumerable<Entry> GetEntries(
421415
}
422416
}
423417

418+
private async Task<IQueryable<Entry>> FilterEntries(
419+
Expression<Func<Entry, bool>>? predicate = null,
420+
FilterQueryOptions? options = null)
421+
{
422+
options ??= FilterQueryOptions.Default;
423+
var queryable = Entries;
424+
if (predicate is not null) queryable = queryable.Where(predicate);
425+
if (options.Exemplar is not null)
426+
{
427+
var ws = (await GetWritingSystem(options.Exemplar.WritingSystem, WritingSystemType.Vernacular))?.WsId;
428+
if (ws is null)
429+
throw new NullReferenceException($"writing system {options.Exemplar.WritingSystem} not found");
430+
queryable = queryable.WhereExemplar(ws.Value, options.Exemplar.Value);
431+
}
432+
433+
if (options.Filter?.GridifyFilter != null)
434+
{
435+
queryable = queryable.ApplyFiltering(options.Filter.GridifyFilter, LcmConfig.Mapper);
436+
}
437+
return queryable;
438+
}
439+
424440
public async Task<Entry?> GetEntry(Guid id)
425441
{
426442
var entry = await Entries

backend/FwLite/MiniLcm/IMiniLcmReadApi.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public interface IMiniLcmReadApi
1212
IAsyncEnumerable<SemanticDomain> GetSemanticDomains();
1313
IAsyncEnumerable<ComplexFormType> GetComplexFormTypes();
1414
Task<ComplexFormType?> GetComplexFormType(Guid id);
15+
Task<int> CountEntries(string? query = null, FilterQueryOptions? options = null);
1516
IAsyncEnumerable<Entry> GetEntries(QueryOptions? options = null);
1617
IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options = null);
1718
Task<Entry?> GetEntry(Guid id);
@@ -22,14 +23,22 @@ public interface IMiniLcmReadApi
2223
Task<ExampleSentence?> GetExampleSentence(Guid entryId, Guid senseId, Guid id);
2324
}
2425

26+
public record FilterQueryOptions(
27+
ExemplarOptions? Exemplar = null,
28+
EntryFilter? Filter = null)
29+
{
30+
public static FilterQueryOptions Default { get; } = new();
31+
public bool HasFilter => Filter is {GridifyFilter.Length: > 0 } || Exemplar is {Value.Length: > 0};
32+
}
33+
2534
public record QueryOptions(
2635
SortOptions? Order = null,
2736
ExemplarOptions? Exemplar = null,
2837
int Count = QueryOptions.DefaultCount,
2938
int Offset = 0,
30-
EntryFilter? Filter = null)
39+
EntryFilter? Filter = null) : FilterQueryOptions(Exemplar, Filter)
3140
{
32-
public static QueryOptions Default { get; } = new();
41+
public static new QueryOptions Default { get; } = new();
3342
public const int QueryAll = -1;
3443
public const int DefaultCount = 1000;
3544
public SortOptions Order { get; init; } = Order ?? SortOptions.Default;

backend/LfClassicData/LfClassicMiniLcmApi.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,18 @@ public async IAsyncEnumerable<SemanticDomain> GetSemanticDomains()
133133
return await GetSemanticDomains().FirstOrDefaultAsync(semdom => semdom.Id == id);
134134
}
135135

136+
public async Task<int> CountEntries(string? query = null, FilterQueryOptions? options = null)
137+
{
138+
// not efficient, but this will likely never get used
139+
var entries = Query(new QueryOptions
140+
{
141+
Count = QueryOptions.QueryAll,
142+
Exemplar = options?.Exemplar,
143+
Filter = options?.Filter
144+
}, query);
145+
return await entries.CountAsync();
146+
}
147+
136148
public IAsyncEnumerable<Entry> GetEntries(QueryOptions? options = null)
137149
{
138150
return Query(options);

frontend/src/routes/(authenticated)/project/[project_code]/viewer/lfClassicLexboxApi.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
/* eslint-disable @typescript-eslint/naming-convention,@typescript-eslint/no-unused-vars */
22

3-
import type {
4-
IComplexFormType,
5-
IEntry,
6-
IExampleSentence,
7-
ISense,
8-
IMiniLcmJsInvokable,
9-
IPartOfSpeech,
10-
IQueryOptions,
11-
ISemanticDomain,
12-
IWritingSystem,
13-
WritingSystemType,
14-
IWritingSystems,
15-
IComplexFormComponent,
16-
IMiniLcmFeatures,
3+
import {
4+
SortField,
5+
type IComplexFormComponent,
6+
type IComplexFormType,
7+
type IEntry,
8+
type IExampleSentence,
9+
type IMiniLcmFeatures,
10+
type IMiniLcmJsInvokable,
11+
type IPartOfSpeech,
12+
type IQueryOptions,
13+
type ISemanticDomain,
14+
type ISense,
15+
type IWritingSystem,
16+
type IWritingSystems,
17+
type WritingSystemType,
1718
} from 'viewer/mini-lcm-api';
1819

19-
import {SEMANTIC_DOMAINS_EN} from './semantic-domains.en.generated-data';
20+
import type {IFilterQueryOptions} from '$lib/dotnet-types/generated-types/MiniLcm/IFilterQueryOptions';
2021
import type {IPublication} from '$lib/dotnet-types/generated-types/MiniLcm/Models/IPublication';
22+
import {SEMANTIC_DOMAINS_EN} from './semantic-domains.en.generated-data';
2123

2224
function prepareEntriesForUi(entries: IEntry[]): void {
2325
for (const entry of entries) {
@@ -46,6 +48,21 @@ export class LfClassicLexboxApi implements IMiniLcmJsInvokable {
4648
return (await result.json()) as IWritingSystems;
4749
}
4850

51+
async countEntries(query?: string, options?: IFilterQueryOptions): Promise<number> {
52+
const queryOptions: IQueryOptions = {
53+
...options,
54+
count: -1,
55+
offset: 0,
56+
order: {
57+
field: SortField.Headword,
58+
writingSystem: 'default',
59+
ascending: true,
60+
},
61+
};
62+
const entries = await (query ? this.searchEntries(query, queryOptions) : this.getEntries(queryOptions));
63+
return entries.length;
64+
}
65+
4966
async getEntries(_options: IQueryOptions | undefined): Promise<IEntry[]> {
5067
//todo pass query options into query
5168
const result = await fetch(`/api/lfclassic/${this.projectCode}/entries${this.toQueryParams(_options)}`);

frontend/viewer/src/lib/components/ui/format-date/index.ts

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

frontend/viewer/src/lib/components/ui/format-date/format-date.svelte renamed to frontend/viewer/src/lib/components/ui/format/format-date.svelte

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
<script lang="ts">
22
import { DateFormatter } from '@internationalized/date';
33
import {locale} from 'svelte-i18n-lingui';
4+
import type {HTMLAttributes} from 'svelte/elements';
45
5-
let { date, ...restProps }: { date: Date | undefined } = $props();
6+
type Props = HTMLAttributes<HTMLSpanElement> & {
7+
date: Date | undefined | null;
8+
defaultValue?: string;
9+
};
10+
11+
const {
12+
date,
13+
defaultValue = '',
14+
...restProps
15+
}: Props = $props();
616
717
const formatter = $derived(new DateFormatter($locale, {
818
dateStyle: 'medium',
919
timeStyle: 'short',
1020
}));
1121
12-
const formattedDate = $derived(date ? formatter.format(date) : '');
22+
const formattedDate = $derived(date ? formatter.format(date) : defaultValue);
1323
</script>
1424

1525
<span {...restProps}>{formattedDate}</span>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {i18n} from '@lingui/core';
2+
3+
export function formatNumber(value: number | undefined, options?: Intl.NumberFormatOptions, defaultValue = ''): string {
4+
if (value === undefined) return defaultValue;
5+
6+
return i18n.number(value, options);
7+
}

0 commit comments

Comments
 (0)