Skip to content

Commit 7882f12

Browse files
authored
Fix various android issues (#1886)
* create sync repo and write a test to ensure getting latest works * ensure there's always a username when commiting files * enable exporting projects for troubleshooting * add right click to home view to allow troubleshooting projects which won't open * disable test case because it's so slow
1 parent 2b02568 commit 7882f12

File tree

18 files changed

+457
-340
lines changed

18 files changed

+457
-340
lines changed

backend/FwHeadless/Services/SendReceiveHelpers.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace FwHeadless.Services;
66

77
public static class SendReceiveHelpers
88
{
9+
private const string HgUsername = "FieldWorks Lite";
910
public record ProjectPath(string Code, string Dir)
1011
{
1112
public string FwDataFile { get; } = Path.Join(Dir, $"{Code}.fwdata");
@@ -91,8 +92,8 @@ public static async Task CommitFile(string filePath, string commitMessage, IProg
9192
ArgumentNullException.ThrowIfNull(fileDir);
9293

9394
//we need to track the file, otherwise hg will not commit it
94-
await ExecuteHgSuccess($"hg add {EscapeShellArg(filePath)}", fileDir, progress);
95-
await ExecuteHgSuccess($"hg commit --message {EscapeShellArg(commitMessage)}", fileDir, progress);
95+
await ExecuteHgSuccess($"hg add --config ui.username={EscapeShellArg(HgUsername)} {EscapeShellArg(filePath)}", fileDir, progress);
96+
await ExecuteHgSuccess($"hg commit --config ui.username={EscapeShellArg(HgUsername)} --message {EscapeShellArg(commitMessage)}", fileDir, progress);
9697
}
9798

9899
private static string EscapeShellArg(string arg)
@@ -143,7 +144,7 @@ public static async Task<LfMergeBridgeResult> SendReceive(FwDataProject project,
143144
{ "languageDepotRepoName", "LexBox" },
144145
{ "languageDepotRepoUri", repoUrl.AbsoluteUri },
145146
{ "deleteRepoIfNoSuchBranch", "false" },
146-
{ "user", "FieldWorks Lite" }, // Not necessary if username was set at clone time, but why not
147+
{ "user", HgUsername }, // Not necessary if username was set at clone time, but why not
147148
};
148149
if (commitMessage is not null) flexBridgeOptions["commitMessage"] = commitMessage;
149150
return await CallLfMergeBridge("Language_Forge_Send_Receive", flexBridgeOptions, progress);
@@ -166,7 +167,7 @@ public static async Task<LfMergeBridgeResult> CloneProject(FwDataProject project
166167
{ "languageDepotRepoName", "LexBox" },
167168
{ "languageDepotRepoUri", repoUrl.ToString() },
168169
{ "deleteRepoIfNoSuchBranch", "false" },
169-
{ "user", "FieldWorks Lite" }
170+
{ "user", HgUsername }
170171
};
171172
return await CallLfMergeBridge("Language_Forge_Clone", flexBridgeOptions, progress);
172173
}

backend/FwLite/FwLiteMaui/Services/MauiTroubleshootingService.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
using FwLiteShared.Services;
2+
using LcmCrdt;
23
using Microsoft.Extensions.Logging;
34
using Microsoft.Extensions.Options;
45
using Microsoft.JSInterop;
56

67
namespace FwLiteMaui.Services;
78

8-
public class MauiTroubleshootingService(IOptions<FwLiteMauiConfig> config, ILogger<MauiTroubleshootingService> logger) : ITroubleshootingService
9+
public class MauiTroubleshootingService(
10+
IOptions<FwLiteMauiConfig> config,
11+
ILogger<MauiTroubleshootingService> logger,
12+
CrdtProjectsService projectsService) : ITroubleshootingService
913
{
1014
private readonly ILauncher _launcher = Launcher.Default;
1115
private readonly IBrowser _browser = Browser.Default;
@@ -57,4 +61,14 @@ public async Task ShareLogFile()
5761
await _share.RequestAsync(shareRequest);
5862
}
5963
}
64+
65+
[JSInvokable]
66+
public async Task ShareCrdtProject(string projectCode)
67+
{
68+
var crdtProject = projectsService.GetProject(projectCode);
69+
if (crdtProject is null) throw new ArgumentException($"Project {projectCode} not found");
70+
var filePath = crdtProject.DbPath;
71+
var shareTitle = $"FieldWorks Lite project {projectCode}";
72+
await _share.RequestAsync(new ShareFileRequest(shareTitle, new ShareFile(filePath, "application/x-sqlite3")));
73+
}
6074
}

backend/FwLite/FwLiteShared/Services/ITroubleshootingService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ public interface ITroubleshootingService
66
Task<string> GetDataDirectory();
77
Task OpenLogFile();
88
Task ShareLogFile();
9+
Task ShareCrdtProject(string projectCode);
910
}

backend/FwLite/FwLiteShared/Services/SyncServiceJsInvokable.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
using FwLiteShared.Auth;
22
using FwLiteShared.Sync;
3+
using LcmCrdt.Data;
34
using LexCore.Sync;
45
using SIL.Harmony;
56
using Microsoft.JSInterop;
67

78
namespace FwLiteShared.Services;
89

9-
public class SyncServiceJsInvokable(SyncService syncService)
10+
public class SyncServiceJsInvokable(SyncService syncService, SyncRepository syncRepository)
1011
{
1112
[JSInvokable]
1213
public Task<ProjectSyncStatus> GetSyncStatus()
@@ -30,7 +31,7 @@ public async Task<SyncJobResult> TriggerFwHeadlessSync()
3031
[JSInvokable]
3132
public Task<DateTimeOffset?> GetLatestCommitDate()
3233
{
33-
return syncService.GetLatestCommitDate();
34+
return syncRepository.GetLatestCommitDate();
3435
}
3536

3637
[JSInvokable]

backend/FwLite/FwLiteShared/Sync/SyncService.cs

Lines changed: 4 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using FwLiteShared.Projects;
55
using LexCore.Sync;
66
using LcmCrdt;
7+
using LcmCrdt.Data;
78
using LcmCrdt.MediaServer;
89
using LcmCrdt.RemoteSync;
910
using LcmCrdt.Utils;
@@ -30,7 +31,7 @@ public class SyncService(
3031
LcmMediaService lcmMediaService,
3132
IOptions<AuthConfig> authOptions,
3233
ILogger<SyncService> logger,
33-
IDbContextFactory<LcmCrdtDbContext> dbContextFactory)
34+
SyncRepository syncRepository)
3435
{
3536
public async Task<SyncResults> SafeExecuteSync(bool skipNotifications = false)
3637
{
@@ -94,7 +95,7 @@ public async Task<SyncResults> ExecuteSync(bool skipNotifications = false)
9495
}
9596
logger.LogInformation("Synced project {ProjectName} with server", project.Name);
9697
UpdateSyncStatus(SyncStatus.Success);
97-
await UpdateSyncDate(syncDate);
98+
await syncRepository.UpdateSyncDate(syncDate);
9899
//need to await this, otherwise the database connection will be closed before the notifications are sent
99100
if (!skipNotifications) await SendNotifications(syncResults);
100101
return syncResults;
@@ -137,7 +138,7 @@ public async Task<SyncJobResult> AwaitSyncFinished()
137138
var project = await currentProjectService.GetProjectData();
138139
var localSyncState = await dataModel.GetSyncState();
139140
if (!authOptions.Value.TryGetServer(project, out var server)) return null;
140-
var localChangesPending = CountPendingCommits(); // Not awaited yet
141+
var localChangesPending = syncRepository.CountPendingCommits(); // Not awaited yet
141142
var remoteChangesPending = lexboxProjectService.CountPendingCrdtCommits(server, project.Id, localSyncState); // Not awaited yet
142143
await Task.WhenAll(localChangesPending, remoteChangesPending);
143144
var localChanges = await localChangesPending;
@@ -220,70 +221,6 @@ private async Task SendNotifications(SyncResults syncResults)
220221
}
221222
}
222223

223-
/// <summary>
224-
/// Note this will update any commits, not just the ones that were synced. This includes ours which we just sent
225-
/// </summary>
226-
private async Task UpdateSyncDate(DateTimeOffset syncDate)
227-
{
228-
try
229-
{
230-
//the prop name is hardcoded into the sql so we just want to assert it's what we expect
231-
Debug.Assert(CommitHelpers.SyncDateProp == "SyncDate");
232-
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
233-
await dbContext.Database.ExecuteSqlAsync(
234-
$"""
235-
UPDATE Commits
236-
SET metadata = json_set(metadata, '$.ExtraMetadata.SyncDate', {syncDate.ToString("u")})
237-
WHERE json_extract(Metadata, '$.ExtraMetadata.SyncDate') IS NULL;
238-
""");
239-
}
240-
catch (Exception e)
241-
{
242-
logger.LogError(e, "Failed to update sync date");
243-
}
244-
}
245-
246-
public async Task<int?> CountPendingCommits()
247-
{
248-
try
249-
{
250-
// Assert sync date prop for same reason as in UpdateSyncDate
251-
Debug.Assert(CommitHelpers.SyncDateProp == "SyncDate");
252-
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
253-
int count = await dbContext.Database.SqlQuery<int>(
254-
$"""
255-
SELECT COUNT(*) AS Value FROM Commits
256-
WHERE json_extract(Metadata, '$.ExtraMetadata.SyncDate') IS NULL
257-
""").SingleAsync();
258-
return count;
259-
}
260-
catch (Exception e)
261-
{
262-
logger.LogError(e, "Failed to count pending commits");
263-
return null;
264-
}
265-
}
266-
267-
public async Task<DateTimeOffset?> GetLatestCommitDate()
268-
{
269-
try
270-
{
271-
// Assert sync date prop for same reason as in UpdateSyncDate
272-
Debug.Assert(CommitHelpers.SyncDateProp == "SyncDate");
273-
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
274-
var date = await dbContext.Database.SqlQuery<DateTimeOffset>(
275-
$"""
276-
SELECT MAX(json_extract(Metadata, '$.ExtraMetadata.SyncDate')) AS Value FROM Commits
277-
""").SingleAsync();
278-
return date;
279-
}
280-
catch (Exception e)
281-
{
282-
logger.LogError(e, "Failed to find most recent commit date");
283-
return null;
284-
}
285-
}
286-
287224
public async Task UploadProject(Guid lexboxProjectId, LexboxServer server)
288225
{
289226
await currentProjectService.SetProjectSyncOrigin(server.Authority, lexboxProjectId);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using LcmCrdt.Data;
2+
3+
namespace LcmCrdt.Tests.Data;
4+
5+
public class SyncRepositoryTests: IAsyncLifetime
6+
{
7+
private readonly MiniLcmApiFixture _apiFixture;
8+
private SyncRepository _syncRepository = null!;
9+
10+
public SyncRepositoryTests()
11+
{
12+
_apiFixture = MiniLcmApiFixture.Create(false);
13+
}
14+
15+
public async Task InitializeAsync()
16+
{
17+
await _apiFixture.InitializeAsync();
18+
_syncRepository = _apiFixture.GetService<SyncRepository>();
19+
}
20+
21+
public async Task DisposeAsync()
22+
{
23+
await _apiFixture.DisposeAsync();
24+
}
25+
26+
[Fact]
27+
public async Task GetLatestCommitDate_WhenNoCommits_ReturnsNull()
28+
{
29+
var latestCommitDate = await _syncRepository.GetLatestCommitDate();
30+
latestCommitDate.Should().BeNull();
31+
}
32+
}

backend/FwLite/LcmCrdt.Tests/MiniLcmTests/QueryEntryTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ private async Task DeleteAllEntries()
3030

3131
[Theory]
3232
[InlineData(50_000)]
33-
[InlineData(100_000)]
33+
//disabled because it takes too long to run
34+
// [InlineData(100_000)]
3435
public async Task QueryPerformanceTesting(int count)
3536
{
3637
await DeleteAllEntries();
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.Diagnostics;
2+
using LcmCrdt.Utils;
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace LcmCrdt.Data;
7+
8+
public class SyncRepository(IDbContextFactory<LcmCrdtDbContext> dbContextFactory, ILogger<SyncRepository> logger)
9+
{
10+
/// <summary>
11+
/// Note this will update any commits, not just the ones that were synced. This includes ours which we just sent
12+
/// </summary>
13+
public async Task UpdateSyncDate(DateTimeOffset syncDate)
14+
{
15+
try
16+
{
17+
//the prop name is hardcoded into the sql so we just want to assert it's what we expect
18+
Debug.Assert(CommitHelpers.SyncDateProp == "SyncDate");
19+
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
20+
await dbContext.Database.ExecuteSqlAsync(
21+
$"""
22+
UPDATE Commits
23+
SET metadata = json_set(metadata, '$.ExtraMetadata.SyncDate', {syncDate.ToString("u")})
24+
WHERE json_extract(Metadata, '$.ExtraMetadata.SyncDate') IS NULL;
25+
""");
26+
}
27+
catch (Exception e)
28+
{
29+
logger.LogError(e, "Failed to update sync date");
30+
}
31+
}
32+
33+
public async Task<int?> CountPendingCommits()
34+
{
35+
try
36+
{
37+
// Assert sync date prop for same reason as in UpdateSyncDate
38+
Debug.Assert(CommitHelpers.SyncDateProp == "SyncDate");
39+
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
40+
int count = await dbContext.Database.SqlQuery<int>(
41+
$"""
42+
SELECT COUNT(*) AS Value FROM Commits
43+
WHERE json_extract(Metadata, '$.ExtraMetadata.SyncDate') IS NULL
44+
""").SingleAsync();
45+
return count;
46+
}
47+
catch (Exception e)
48+
{
49+
logger.LogError(e, "Failed to count pending commits");
50+
return null;
51+
}
52+
}
53+
54+
public async Task<DateTimeOffset?> GetLatestCommitDate()
55+
{
56+
try
57+
{
58+
// Assert sync date prop for same reason as in UpdateSyncDate
59+
Debug.Assert(CommitHelpers.SyncDateProp == "SyncDate");
60+
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
61+
var date = await dbContext.Database.SqlQuery<DateTimeOffset?>(
62+
$"""
63+
SELECT MAX(json_extract(Metadata, '$.ExtraMetadata.SyncDate')) AS Value FROM Commits
64+
""").SingleAsync();
65+
return date;
66+
}
67+
catch (Exception e)
68+
{
69+
logger.LogError(e, "Failed to find most recent commit date");
70+
return null;
71+
}
72+
}
73+
}

backend/FwLite/LcmCrdt/LcmCrdtKernel.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public static IServiceCollection AddLcmCrdtClientCore(this IServiceCollection se
7070
services.AddScoped<CurrentProjectService>();
7171
services.AddScoped<HistoryService>();
7272
services.AddScoped<LcmMediaService>();
73+
services.AddScoped<SyncRepository>();
7374
services.AddSingleton<CrdtProjectsService>();
7475
services.AddSingleton<IProjectProvider>(s => s.GetRequiredService<CrdtProjectsService>());
7576

frontend/viewer/src/home/HomeView.svelte

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -192,31 +192,37 @@
192192
{@const server = project.server}
193193
{@const loading = deletingProject === project.id}
194194
<div out:send={{key: 'project-' + project.code}} in:receive={{key: 'project-' + project.code}}>
195-
<Anchor href={`/project/${project.code}`}>
196-
<ProjectListItem icon="i-mdi-book-edit-outline"
197-
{project}
198-
{loading}
199-
subtitle={!server ? $t`Local only` : $t`Synced with ${server.displayName}`}
200-
>
201-
{#snippet actions()}
202-
<div class="flex items-center">
203-
{#if $isDev}
204-
<Button
205-
icon="i-mdi-delete"
206-
variant="ghost"
207-
title={$t`Delete`}
208-
class="p-2 hover:bg-primary/20"
209-
onclick={(e) => {
210-
e.preventDefault();
211-
void deleteProject(project);
212-
}}
213-
/>
214-
{/if}
215-
<Icon icon="i-mdi-chevron-right" class="p-2"/>
216-
</div>
195+
<ResponsiveMenu.Root contextMenu>
196+
<ResponsiveMenu.Trigger>
197+
{#snippet child({props})}
198+
<Anchor {...props} href={`/project/${project.code}`}>
199+
<ProjectListItem icon="i-mdi-book-edit-outline"
200+
{project}
201+
{loading}
202+
subtitle={!server ? $t`Local only` : $t`Synced with ${server.displayName}`}
203+
>
204+
{#snippet actions()}
205+
<div class="flex items-center">
206+
<Icon icon="i-mdi-chevron-right" class="p-2"/>
207+
</div>
208+
{/snippet}
209+
</ProjectListItem>
210+
</Anchor>
217211
{/snippet}
218-
</ProjectListItem>
219-
</Anchor>
212+
</ResponsiveMenu.Trigger>
213+
<ResponsiveMenu.Content>
214+
{#if supportsTroubleshooting}
215+
<ResponsiveMenu.Item icon="i-mdi-bug" onSelect={() => troubleshootDialog?.open(project.code)}>
216+
{$t`Troubleshoot`}
217+
</ResponsiveMenu.Item>
218+
{/if}
219+
{#if $isDev}
220+
<ResponsiveMenu.Item icon="i-mdi-delete" onSelect={() => void deleteProject(project)}>
221+
{$t`Delete`}
222+
</ResponsiveMenu.Item>
223+
{/if}
224+
</ResponsiveMenu.Content>
225+
</ResponsiveMenu.Root>
220226
</div>
221227
{/each}
222228
<DevContent>

0 commit comments

Comments
 (0)