From 1264808a3acc1ee16d92b97cdeceb0dacf74aad8 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Sun, 8 Sep 2024 03:28:14 +0700 Subject: [PATCH 01/26] enable dev mode in fw lite from c# --- .../FwLiteDesktop/FwLiteDesktopKernel.cs | 6 ++++ backend/FwLite/FwLiteDesktop/MainPage.xaml | 2 +- backend/FwLite/FwLiteDesktop/MainPage.xaml.cs | 30 +++++++++++++++++-- .../ServerBridge/ServerManager.cs | 1 + 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/backend/FwLite/FwLiteDesktop/FwLiteDesktopKernel.cs b/backend/FwLite/FwLiteDesktop/FwLiteDesktopKernel.cs index 921c28a32..fbf0aef82 100644 --- a/backend/FwLite/FwLiteDesktop/FwLiteDesktopKernel.cs +++ b/backend/FwLite/FwLiteDesktop/FwLiteDesktopKernel.cs @@ -2,6 +2,7 @@ using LcmCrdt; using LocalWebApp.Auth; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NReco.Logging.File; @@ -17,6 +18,9 @@ public static void AddFwLiteDesktopServices(this IServiceCollection services, var serverManager = new ServerManager(webAppBuilder => { + #if DEBUG + webAppBuilder.Environment.EnvironmentName = "Development"; + #endif webAppBuilder.Logging.AddFile(Path.Combine(FileSystem.AppDataDirectory, "web-app.log")); webAppBuilder.Services.Configure(config => { @@ -31,9 +35,11 @@ public static void AddFwLiteDesktopServices(this IServiceCollection services, //using a lambda here means that the serverManager will be disposed when the app is disposed services.AddSingleton(_ => serverManager); services.AddSingleton(_ => _.GetRequiredService()); + services.AddSingleton(_ => _.GetRequiredService().WebServices.GetRequiredService()); configuration.Add(source => source.ServerManager = serverManager); services.AddOptions().BindConfiguration("LocalWebApp"); logging.AddFile(Path.Combine(FileSystem.AppDataDirectory, "app.log")); + logging.AddConsole(); #if DEBUG logging.AddDebug(); #endif diff --git a/backend/FwLite/FwLiteDesktop/MainPage.xaml b/backend/FwLite/FwLiteDesktop/MainPage.xaml index dca7fed91..37d1dd4d0 100644 --- a/backend/FwLite/FwLiteDesktop/MainPage.xaml +++ b/backend/FwLite/FwLiteDesktop/MainPage.xaml @@ -3,6 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="FwLiteDesktop.MainPage"> - + diff --git a/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs b/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs index 317a41583..000d2cf41 100644 --- a/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs +++ b/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs @@ -1,12 +1,18 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace FwLiteDesktop; public partial class MainPage : ContentPage { - public MainPage(IOptionsMonitor options, ILogger logger) + private readonly ILogger _logger; + private readonly IHostEnvironment _environment; + + public MainPage(IOptionsMonitor options, ILogger logger, IHostEnvironment environment) { + _logger = logger; + _environment = environment; InitializeComponent(); options.OnChange(o => { @@ -14,6 +20,7 @@ public MainPage(IOptionsMonitor options, ILogger lo webView.Dispatcher.Dispatch(() => webView.Source = url); logger.LogInformation("Url updated: {Url}", url); }); + webView.IsVisible = false; var url = options.CurrentValue.Url; webView.Source = url; logger.LogInformation("Main page initialized, url: {Url}", url); @@ -39,7 +46,24 @@ public MainPage(IOptionsMonitor options, ILogger lo logger.LogWarning("Too many navigations, stopping"); } } + else if (!args.Url.StartsWith("https://appdir")) + { + NavigationSuccess(); + } }; } -} + private void NavigationSuccess() + { + webView.IsVisible = true; + //not currently working + // if (_environment.IsDevelopment()) + { + //lang=js + webView.Eval(""" + localStorage.setItem('devMode', 'true'); + if (enableDevMode) enableDevMode(); + """); + } + } +} diff --git a/backend/FwLite/FwLiteDesktop/ServerBridge/ServerManager.cs b/backend/FwLite/FwLiteDesktop/ServerBridge/ServerManager.cs index 3893bf0e0..0e507152a 100644 --- a/backend/FwLite/FwLiteDesktop/ServerBridge/ServerManager.cs +++ b/backend/FwLite/FwLiteDesktop/ServerBridge/ServerManager.cs @@ -14,6 +14,7 @@ public class ServerManager(Action? configure = null) : IM private ILogger? _logger; private Thread? _thread; private readonly ManualResetEvent _stop = new(false); + public IServiceProvider WebServices => _webApp?.Services ?? throw new ApplicationException("initlize not yet called"); public void Initialize(IServiceProvider services) { From d0d68519e24e6a6fcfb58728dccc1dfe6a04fd2f Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Sun, 8 Sep 2024 04:26:18 +0700 Subject: [PATCH 02/26] add upload button to project config, display configured server when there is one. --- frontend/viewer/src/HomeView.svelte | 69 ++++--------------- .../src/lib/layout/ViewOptionsDrawer.svelte | 62 ++++++++++++++--- .../src/lib/services/projects-service.ts | 63 +++++++++++++++++ 3 files changed, 131 insertions(+), 63 deletions(-) create mode 100644 frontend/viewer/src/lib/services/projects-service.ts diff --git a/frontend/viewer/src/HomeView.svelte b/frontend/viewer/src/HomeView.svelte index 3f01d8032..9e4dee600 100644 --- a/frontend/viewer/src/HomeView.svelte +++ b/frontend/viewer/src/HomeView.svelte @@ -11,37 +11,20 @@ import {Button, Card, type ColumnDef, Table, TextField, tableCell, Icon, ProgressCircle} from 'svelte-ux'; import flexLogo from './lib/assets/flex-logo.png'; import DevContent, {isDev} from './lib/layout/DevContent.svelte'; + import {useProjectsService, type Project, type ServerStatus} from './lib/services/projects-service'; + import {onMount} from 'svelte'; - type Project = { - name: string; - crdt: boolean; - fwdata: boolean; - lexbox: boolean, - server: string | null, - id: string | null - }; + const projectsService = useProjectsService(); let newProjectName = ''; let createError: string; async function createProject() { - createError = ''; - if (!newProjectName) { - createError = 'Project name is required.'; - return; - } - const response = await fetch(`/api/project?name=${newProjectName}`, { - method: 'POST', - }); - - if (!response.ok) { - createError = await response.json(); - return; - } - - createError = ''; + const response = await projectsService.createProject(newProjectName); + createError = response.error ?? ''; + if (createError) return; newProjectName = ''; void refreshProjects(); } @@ -51,9 +34,7 @@ async function importFwDataProject(name: string) { importing = name; - await fetch(`/api/import/fwdata/${name}`, { - method: 'POST', - }); + await projectsService.importFwDataProject(name); await refreshProjects(); importing = ''; } @@ -62,32 +43,17 @@ async function downloadCrdtProject(project: Project) { downloading = project.name; - await fetch(`/api/download/crdt/${project.server}/${project.name}`, {method: 'POST'}); + await projectsService.downloadCrdtProject(project); await refreshProjects(); downloading = ''; } - let uploading = ''; - - async function uploadCrdtProject(project: Project) { - uploading = project.name; - await fetch(`/api/upload/crdt/${project.server}/${project.name}`, {method: 'POST'}); - await refreshProjects(); - uploading = ''; - } - - let projectsPromise = fetchProjects(); + let projectsPromise = projectsService.fetchProjects(); let projects: Project[] = []; - async function fetchProjects() { - let r = await fetch('/api/localProjects'); - projects = (await r.json()) as Project[]; - return projects; - } - async function refreshProjects() { - let promise = fetchProjects(); - await promise;//avoids clearing out the list until the new list is fetched + let promise = projectsService.fetchProjects(); + projects = await promise;//avoids clearing out the list until the new list is fetched projectsPromise = promise; } @@ -95,21 +61,14 @@ let loadingRemoteProjects = false; async function fetchRemoteProjects() { loadingRemoteProjects = true; - let r = await fetch('/api/remoteProjects'); - remoteProjects = (await r.json()) as { [server: string]: Project[] }; + remoteProjects = await projectsService.fetchRemoteProjects(); loadingRemoteProjects = false; } fetchRemoteProjects(); - type ServerStatus = { displayName: string; loggedIn: boolean; loggedInAs: string | null }; - let servers: ServerStatus[] = []; - async function fetchServers() { - let r = await fetch('/api/auth/servers'); - servers = await r.json(); - } - - fetchServers(); + let servers: ServerStatus[] = []; + onMount(async () => servers = await projectsService.fetchServers()); $: columns = [ { diff --git a/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte b/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte index 765ead594..5e04e9054 100644 --- a/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte +++ b/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte @@ -1,15 +1,40 @@ @@ -28,27 +53,48 @@ + {#if $servers.length > 1} + ({ value: server, label: server.displayName, group: server.displayName }))} + bind:value={$projectServer} + classes={{root: 'view-select w-auto', options: 'view-select-options'}} + clearable={false} + labelPlacement="top" + clearSearchOnOpen={false} + fieldActions={(elem) => /* a hack to disable typing/filtering */ {elem.readOnly = true; return [];}} + search={() => /* a hack to always show all options */ Promise.resolve()}> + + {:else if isUploaded} +
Syncing with {$projectServer}
+ {/if} + {#if $projectServer && !isUploaded} + + {/if} +
Debug
diff --git a/frontend/viewer/src/lib/services/projects-service.ts b/frontend/viewer/src/lib/services/projects-service.ts new file mode 100644 index 000000000..4582584ca --- /dev/null +++ b/frontend/viewer/src/lib/services/projects-service.ts @@ -0,0 +1,63 @@ +export type Project = { + name: string; + crdt: boolean; + fwdata: boolean; + lexbox: boolean, + server: string | null, + id: string | null +}; +export type ServerStatus = { displayName: string; loggedIn: boolean; loggedInAs: string | null }; +export function useProjectsService() { + return projectService; +} +export class ProjectService { + async createProject(newProjectName: string): Promise<{error: string|undefined}> { + + if (!newProjectName) { + return {error: 'Project name is required'}; + } + const response = await fetch(`/api/project?name=${newProjectName}`, { + method: 'POST', + }); + + if (!response.ok) { + return {error: await response.json()}; + } + return {error: undefined}; + } + + async importFwDataProject(name: string) { + await fetch(`/api/import/fwdata/${name}`, { + method: 'POST', + }); + } + + async downloadCrdtProject(project: Project) { + await fetch(`/api/download/crdt/${project.server}/${project.name}`, {method: 'POST'}); + } + + async uploadCrdtProject(server: string, projectName: string) { + await fetch(`/api/upload/crdt/${server}/${projectName}`, {method: 'POST'}); + } + async getProjectServer(projectName: string): Promise { + const projects = await this.fetchProjects(); + return projects.find(p => p.name === projectName)?.server ?? null; + } + + async fetchProjects() { + let r = await fetch('/api/localProjects'); + return (await r.json()) as Project[]; + } + + async fetchRemoteProjects() { + + let r = await fetch('/api/remoteProjects'); + return (await r.json()) as { [server: string]: Project[] }; + } + + async fetchServers() { + let r = await fetch('/api/servers'); + return (await r.json()) as ServerStatus[]; + } +} +const projectService = new ProjectService(); From e493b212f5904a73e868dff50cebc99f5f88b284 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Sun, 8 Sep 2024 22:09:19 +0700 Subject: [PATCH 03/26] only push changes to the front end if they match the current filter. If the front end gets an entry not in the list, then add it to the end. --- backend/FwLite/LcmCrdt/CrdtLexboxApi.cs | 9 ++-- backend/FwLite/LcmCrdt/Data/Filtering.cs | 39 +++++++++++++++ backend/FwLite/LcmCrdt/SqlHelpers.cs | 4 +- .../LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs | 48 +++++++++++++++++-- .../LocalWebApp/Hubs/MiniLcmApiHubBase.cs | 4 +- .../FwLite/LocalWebApp/Routes/TestRoutes.cs | 7 +++ .../LocalWebApp/Services/ChangeEventBus.cs | 26 ++++++---- frontend/viewer/src/ProjectView.svelte | 19 +++++++- 8 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 backend/FwLite/LcmCrdt/Data/Filtering.cs diff --git a/backend/FwLite/LcmCrdt/CrdtLexboxApi.cs b/backend/FwLite/LcmCrdt/CrdtLexboxApi.cs index 750f086d2..6338466ff 100644 --- a/backend/FwLite/LcmCrdt/CrdtLexboxApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtLexboxApi.cs @@ -4,6 +4,7 @@ using SIL.Harmony; using SIL.Harmony.Changes; using LcmCrdt.Changes; +using LcmCrdt.Data; using MiniLcm; using LinqToDB; using LinqToDB.EntityFrameworkCore; @@ -102,11 +103,7 @@ public async Task BulkImportSemanticDomains(IEnumerable { if (string.IsNullOrEmpty(query)) return GetEntriesAsyncEnum(null, options); - return GetEntriesAsyncEnum(e => e.LexemeForm.SearchValue(query) - || e.CitationForm.SearchValue(query) - || e.Senses.Any(s => s.Gloss.SearchValue(query)) - - , options); + return GetEntriesAsyncEnum(Filtering.SearchFilter(query), options); } private async IAsyncEnumerable GetEntriesAsyncEnum( @@ -133,7 +130,7 @@ public async Task BulkImportSemanticDomains(IEnumerable var ws = (await GetWritingSystem(options.Exemplar.WritingSystem, WritingSystemType.Vernacular))?.WsId; if (ws is null) throw new NullReferenceException($"writing system {options.Exemplar.WritingSystem} not found"); - queryable = queryable.Where(e => e.Headword(ws.Value).StartsWith(options.Exemplar.Value)); + queryable = queryable.WhereExemplar(ws.Value, options.Exemplar.Value); } var sortWs = (await GetWritingSystem(options.Order.WritingSystem, WritingSystemType.Vernacular))?.WsId; diff --git a/backend/FwLite/LcmCrdt/Data/Filtering.cs b/backend/FwLite/LcmCrdt/Data/Filtering.cs new file mode 100644 index 000000000..98950e56e --- /dev/null +++ b/backend/FwLite/LcmCrdt/Data/Filtering.cs @@ -0,0 +1,39 @@ +using System.Linq.Expressions; +using MiniLcm; + +namespace LcmCrdt.Data; + +public static class Filtering +{ + public static IQueryable WhereExemplar( + this IQueryable query, + WritingSystemId ws, + string exemplar) + { + return query.Where(e => e.Headword(ws).StartsWith(exemplar)); + } + + public static Expression> SearchFilter(string query) + { + return e => e.LexemeForm.SearchValue(query) + || e.CitationForm.SearchValue(query) + || e.Senses.Any(s => s.Gloss.SearchValue(query)); + } + + public static Func CompiledFilter(string? query, WritingSystemId ws, string? exemplar) + { + query = string.IsNullOrEmpty(query) ? null : query; + return (query, exemplar) switch + { + (null, null) => _ => true, + (not null, null) => e => e.LexemeForm.SearchValue(query) + || e.CitationForm.SearchValue(query) + || e.Senses.Any(s => s.Gloss.SearchValue(query)), + (null, not null) => e => e.Headword(ws).StartsWith(exemplar), + (_, _) => e => e.Headword(ws).StartsWith(exemplar) + && (e.LexemeForm.SearchValue(query) + || e.CitationForm.SearchValue(query) + || e.Senses.Any(s => s.Gloss.SearchValue(query))) + }; + } +} diff --git a/backend/FwLite/LcmCrdt/SqlHelpers.cs b/backend/FwLite/LcmCrdt/SqlHelpers.cs index 1ef1894d6..1099c381d 100644 --- a/backend/FwLite/LcmCrdt/SqlHelpers.cs +++ b/backend/FwLite/LcmCrdt/SqlHelpers.cs @@ -16,8 +16,8 @@ public static bool HasValue(this MultiString ms, string value) [Sql.Expression("exists(select 1 from json_each({0}, '$') where value like '%' || {1} || '%')", PreferServerSide = true, IsPredicate = true)] - public static bool SearchValue(this MultiString ms, string value) + public static bool SearchValue(this MultiString ms, string search) { - return ms.Values.Values.Contains(value); + return ms.Values.Any(pair => pair.Value.Contains(search)); } } diff --git a/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs b/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs index bc186c8d2..f56db263e 100644 --- a/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs +++ b/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs @@ -1,6 +1,7 @@ using LcmCrdt; +using LcmCrdt.Data; using LocalWebApp.Services; -using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Caching.Memory; using MiniLcm; using SystemTextJsonPatch; @@ -12,7 +13,8 @@ public class CrdtMiniLcmApiHub( SyncService syncService, ChangeEventBus changeEventBus, CurrentProjectService projectContext, - LexboxProjectService lexboxProjectService) : MiniLcmApiHubBase(lexboxApi) + LexboxProjectService lexboxProjectService, + IMemoryCache memoryCache) : MiniLcmApiHubBase(lexboxApi) { public const string ProjectRouteKey = "project"; public static string ProjectGroup(string projectName) => "crdt-" + projectName; @@ -21,11 +23,51 @@ public override async Task OnConnectedAsync() { await Groups.AddToGroupAsync(Context.ConnectionId, ProjectGroup(projectContext.Project.Name)); await syncService.ExecuteSync(); - changeEventBus.SetupGlobalSignalRSubscription(); + IDisposable[] cleanup = + [ + //todo this results in a memory leak, due to holding on to the hub instance, it will be disposed even if the context items are not. + changeEventBus.ListenForEntryChanges(projectContext.Project.Name, Context.ConnectionId) + ]; + Context.Items["clanup"] = cleanup; await lexboxProjectService.ListenForProjectChanges(projectContext.ProjectData, Context.ConnectionAborted); } + public override async Task OnDisconnectedAsync(Exception? exception) + { + await base.OnDisconnectedAsync(exception); + foreach (var disposable in Context.Items["cleanup"] as IDisposable[] ?? []) + { + disposable.Dispose(); + } + } + + private Func CurrentFilter + { + set => memoryCache.Set($"CurrentFilter|HubConnectionId={Context.ConnectionId}", value); + } + + public static Func CurrentProjectFilter(IMemoryCache memoryCache, string connectionId) + { + return memoryCache.Get>( + $"CurrentFilter|HubConnectionId={connectionId}") ?? (_ => true); + } + + public override IAsyncEnumerable GetEntries(QueryOptions? options = null) + { + CurrentFilter = + Filtering.CompiledFilter(null, options?.Exemplar?.WritingSystem ?? "default", options?.Exemplar?.Value); + return base.GetEntries(options); + } + + public override IAsyncEnumerable SearchEntries(string query, QueryOptions? options = null) + { + CurrentFilter = Filtering.CompiledFilter(query, + options?.Exemplar?.WritingSystem ?? "default", + options?.Exemplar?.Value); + return base.SearchEntries(query, options); + } + public override async Task CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem) { var newWritingSystem = await base.CreateWritingSystem(type, writingSystem); diff --git a/backend/FwLite/LocalWebApp/Hubs/MiniLcmApiHubBase.cs b/backend/FwLite/LocalWebApp/Hubs/MiniLcmApiHubBase.cs index 361283314..a6460e617 100644 --- a/backend/FwLite/LocalWebApp/Hubs/MiniLcmApiHubBase.cs +++ b/backend/FwLite/LocalWebApp/Hubs/MiniLcmApiHubBase.cs @@ -37,12 +37,12 @@ public IAsyncEnumerable GetSemanticDomains() return lexboxApi.GetSemanticDomains(); } - public IAsyncEnumerable GetEntries(QueryOptions? options = null) + public virtual IAsyncEnumerable GetEntries(QueryOptions? options = null) { return lexboxApi.GetEntries(options); } - public IAsyncEnumerable SearchEntries(string query, QueryOptions? options = null) + public virtual IAsyncEnumerable SearchEntries(string query, QueryOptions? options = null) { return lexboxApi.SearchEntries(query, options); } diff --git a/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs b/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs index a6558629c..9b7e8d4d6 100644 --- a/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs @@ -31,6 +31,13 @@ public static IEndpointConventionBuilder MapTest(this WebApplication app) if (entry is Entry crdtEntry) eventBus.NotifyEntryUpdated(crdtEntry); }); + group.MapPost("/add-new-entry", + async (ILexboxApi api, ChangeEventBus eventBus, MiniLcm.Entry entry) => + { + var createdEntry = await api.CreateEntry(entry); + if (createdEntry is Entry crdtEntry) + eventBus.NotifyEntryUpdated(crdtEntry); + }); return group; } } diff --git a/backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs b/backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs index d82fa9e4a..97954f5c1 100644 --- a/backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs +++ b/backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs @@ -4,22 +4,29 @@ using LcmCrdt.Objects; using LocalWebApp.Hubs; using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Caching.Memory; namespace LocalWebApp.Services; -public class ChangeEventBus(ProjectContext projectContext, IHubContext hubContext, ILogger logger) +public class ChangeEventBus( + ProjectContext projectContext, + IHubContext hubContext, + ILogger logger, + IMemoryCache cache) : IDisposable { - private IDisposable? _subscription; + public IDisposable ListenForEntryChanges(string projectName, string connectionId) => + _entryUpdated + .Where(n => n.ProjectName == projectName) + .Subscribe(n => OnEntryChangedExternal(n.Entry, connectionId)); - public void SetupGlobalSignalRSubscription() + private void OnEntryChangedExternal(Entry e, string connectionId) { - if (_subscription is not null) return; - _subscription = _entryUpdated.Subscribe(notification => + var currentFilter = CrdtMiniLcmApiHub.CurrentProjectFilter(cache, connectionId); + if (currentFilter.Invoke(e)) { - logger.LogInformation("Sending notification for {EntryId} to {ProjectName}", notification.Entry.Id, notification.ProjectName); - _ = hubContext.Clients.Group(CrdtMiniLcmApiHub.ProjectGroup(notification.ProjectName)).OnEntryUpdated(notification.Entry); - }); + _ = hubContext.Clients.Client(connectionId).OnEntryUpdated(e); + } } private record struct ChangeNotification(Entry Entry, string ProjectName); @@ -45,6 +52,7 @@ public void NotifyEntryUpdated(Entry entry) public void Dispose() { - _subscription?.Dispose(); + _entryUpdated.OnCompleted(); + _entryUpdated.Dispose(); } } diff --git a/frontend/viewer/src/ProjectView.svelte b/frontend/viewer/src/ProjectView.svelte index eb92fc6b4..30ebad5fc 100644 --- a/frontend/viewer/src/ProjectView.svelte +++ b/frontend/viewer/src/ProjectView.svelte @@ -40,8 +40,23 @@ export let loading = false; const changeEventBus = useEventBus(); - onDestroy(changeEventBus.onEntryUpdated(entry => { - entries.update(list => list?.map(e => e.id === entry.id ? entry : e)); + onDestroy(changeEventBus.onEntryUpdated(updatedEntry => { + entries.update(list => { + let updated = false; + let updatedList = list?.map(e => { + if (e.id === updatedEntry.id) { + updated = true; + return updatedEntry; + } else { + return e; + } + }) ?? []; + //entry not found, add it to the end + //todo this does not handle sorting, should we bother? + if (!updated) updatedList.push(updatedEntry); + + return updatedList; + }); })); const lexboxApi = useLexboxApi(); From b138e5275d650145aec614ae0f75e715893153b9 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 11 Sep 2024 09:11:46 -1000 Subject: [PATCH 04/26] use staging as default crdt server --- backend/FwLite/LocalWebApp/LocalWebAppServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwLite/LocalWebApp/LocalWebAppServer.cs b/backend/FwLite/LocalWebApp/LocalWebAppServer.cs index 88bab2760..c7ddbb036 100644 --- a/backend/FwLite/LocalWebApp/LocalWebAppServer.cs +++ b/backend/FwLite/LocalWebApp/LocalWebAppServer.cs @@ -32,7 +32,7 @@ public static WebApplication SetupAppServer(string[] args, Action(config => - config.LexboxServers = [new(new("https://lexbox.dev.languagetechnology.org"), "Lexbox Dev")]); + config.LexboxServers = [new(new("https://staging.languagedepot.org"), "Lexbox Staging")]); builder.Services.Configure(c => c.ClientId = "becf2856-0690-434b-b192-a4032b72067f"); builder.Services.AddLocalAppServices(builder.Environment); From 4a37fe492f4bd198d332846f3a704bbd1d07abf5 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 11 Sep 2024 11:59:57 -1000 Subject: [PATCH 05/26] correct servers fetch path --- frontend/viewer/src/lib/services/projects-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/viewer/src/lib/services/projects-service.ts b/frontend/viewer/src/lib/services/projects-service.ts index 4582584ca..4a32e227b 100644 --- a/frontend/viewer/src/lib/services/projects-service.ts +++ b/frontend/viewer/src/lib/services/projects-service.ts @@ -56,7 +56,7 @@ export class ProjectService { } async fetchServers() { - let r = await fetch('/api/servers'); + let r = await fetch('/api/auth/servers'); return (await r.json()) as ServerStatus[]; } } From ac5f5007b177beb00e66a5782c01dcf6e8a5643e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 12 Sep 2024 07:10:04 -1000 Subject: [PATCH 06/26] remove admin required from crdt controller --- backend/LexBoxApi/Controllers/CrdtController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/LexBoxApi/Controllers/CrdtController.cs b/backend/LexBoxApi/Controllers/CrdtController.cs index bbc5d4c98..c982eb99f 100644 --- a/backend/LexBoxApi/Controllers/CrdtController.cs +++ b/backend/LexBoxApi/Controllers/CrdtController.cs @@ -14,7 +14,6 @@ namespace LexBoxApi.Controllers; [ApiController] [Route("/api/crdt")] -[AdminRequired] [ApiExplorerSettings(GroupName = LexBoxKernel.OpenApiPublicDocumentName)] public class CrdtController( LexBoxDbContext dbContext, From 79123fb63651ba9524579be72169e7d4475819b7 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 12 Sep 2024 07:10:28 -1000 Subject: [PATCH 07/26] enable oauth in staging --- deployment/staging/app-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deployment/staging/app-config.yaml b/deployment/staging/app-config.yaml index 57a4435ea..cb2b2308a 100644 --- a/deployment/staging/app-config.yaml +++ b/deployment/staging/app-config.yaml @@ -5,3 +5,4 @@ metadata: data: environment-name: "Staging" hg-domain: "hg-staging.languageforge.org" + enable-oauth: "true" From 07d8e2a25e36896bf1f808e4d46f3bcd0004f216 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 12 Sep 2024 07:39:13 -1000 Subject: [PATCH 08/26] prevent crash when there's a duplicate ws between vernacular and analysis --- backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 22f0529af..5c4d94129 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -122,7 +122,7 @@ private WritingSystem FromLcmWritingSystem(CoreWritingSystemDefinition ws) internal void CompleteExemplars(WritingSystems writingSystems) { var wsExemplars = writingSystems.Vernacular.Concat(writingSystems.Analysis) - .Distinct() + .DistinctBy(ws => ws.Id) .ToDictionary(ws => ws, ws => ws.Exemplars.Select(s => s[0]).ToHashSet()); var wsExemplarsByHandle = wsExemplars.ToFrozenDictionary(kv => GetWritingSystemHandle(kv.Key.Id), kv => kv.Value); From 7ec86dff0957825e08898fead2e750766e38614c Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 12 Sep 2024 07:39:49 -1000 Subject: [PATCH 09/26] ensure the first server in the list is picked, add some todos for errors to fix --- backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs | 2 +- .../viewer/src/lib/layout/ViewOptionsDrawer.svelte | 10 ++++++++-- frontend/viewer/src/lib/services/projects-service.ts | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs index 6dba7c4bf..785f8c9ad 100644 --- a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs @@ -34,7 +34,7 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap FieldWorksProjectList fieldWorksProjectList) => { var crdtProjects = await projectService.ListProjects(); - //todo get project Id and use that to specify the Id in the model + //todo get project Id and use that to specify the Id in the model. Also pull out server var projects = crdtProjects.ToDictionary(p => p.Name, p => new ProjectModel(p.Name, true, false)); //basically populate projects and indicate if they are lexbox or fwdata foreach (var p in fieldWorksProjectList.EnumerateProjects()) diff --git a/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte b/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte index 5e04e9054..bae27f64f 100644 --- a/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte +++ b/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte @@ -8,6 +8,7 @@ import {writable} from 'svelte/store'; import {type ServerStatus, useProjectsService} from '../services/projects-service'; import {getContext} from 'svelte'; + import {mdiBookArrowUpOutline, mdiBookSyncOutline} from '@mdi/js'; const projectsService = useProjectsService(); let projectName = getContext('project-name'); @@ -25,6 +26,9 @@ return server; }).then(set); }); + $: if (!$projectServer && $servers.length > 0) { + $projectServer = $servers[0].displayName; + } let uploading = false; async function upload() { @@ -70,10 +74,12 @@ search={() => /* a hack to always show all options */ Promise.resolve()}> {:else if isUploaded} -
Syncing with {$projectServer}
+ {/if} {#if $projectServer && !isUploaded} - {/if} diff --git a/frontend/viewer/src/lib/services/projects-service.ts b/frontend/viewer/src/lib/services/projects-service.ts index 4a32e227b..7cfb0e396 100644 --- a/frontend/viewer/src/lib/services/projects-service.ts +++ b/frontend/viewer/src/lib/services/projects-service.ts @@ -41,6 +41,7 @@ export class ProjectService { } async getProjectServer(projectName: string): Promise { const projects = await this.fetchProjects(); + //todo project server is always null from local projects` return projects.find(p => p.name === projectName)?.server ?? null; } From cbfe923ef5982600291f1439be3a6790902012b5 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 13 Sep 2024 08:56:36 -1000 Subject: [PATCH 10/26] allow overriding the project paths in appsettings.json, add comments to explain some properties --- .../FwDataMiniLcmBridge/FwDataBridgeKernel.cs | 2 +- .../FwDataMiniLcmBridge/FwDataMiniLcmBridge.csproj | 1 + backend/FwLite/LcmCrdt/LcmCrdt.csproj | 1 + backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 2 +- backend/FwLite/LocalWebApp/appsettings.json | 14 +++++++++++++- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/FwDataBridgeKernel.cs b/backend/FwLite/FwDataMiniLcmBridge/FwDataBridgeKernel.cs index 436948e4c..522ad1e90 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/FwDataBridgeKernel.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/FwDataBridgeKernel.cs @@ -11,7 +11,7 @@ public static IServiceCollection AddFwDataBridge(this IServiceCollection service { services.AddMemoryCache(); services.AddLogging(); - services.AddOptions(); + services.AddOptions().BindConfiguration("FwDataBridge"); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/backend/FwLite/FwDataMiniLcmBridge/FwDataMiniLcmBridge.csproj b/backend/FwLite/FwDataMiniLcmBridge/FwDataMiniLcmBridge.csproj index 606eb038f..43a840e1c 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/FwDataMiniLcmBridge.csproj +++ b/backend/FwLite/FwDataMiniLcmBridge/FwDataMiniLcmBridge.csproj @@ -11,6 +11,7 @@ + diff --git a/backend/FwLite/LcmCrdt/LcmCrdt.csproj b/backend/FwLite/LcmCrdt/LcmCrdt.csproj index de3206339..68176ba8f 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdt.csproj +++ b/backend/FwLite/LcmCrdt/LcmCrdt.csproj @@ -15,6 +15,7 @@ + diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 496be2c45..60d7a7b68 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -24,7 +24,7 @@ public static IServiceCollection AddLcmCrdtClient(this IServiceCollection servic LinqToDBForEFTools.Initialize(); services.AddMemoryCache(); services.AddDbContext(ConfigureDbOptions); - services.AddOptions(); + services.AddOptions().BindConfiguration("LcmCrdt"); services.AddCrdtData( ConfigureCrdt diff --git a/backend/FwLite/LocalWebApp/appsettings.json b/backend/FwLite/LocalWebApp/appsettings.json index 356485704..6c9c73606 100644 --- a/backend/FwLite/LocalWebApp/appsettings.json +++ b/backend/FwLite/LocalWebApp/appsettings.json @@ -7,5 +7,17 @@ "Microsoft.EntityFrameworkCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "LcmCrdt": { + // this is a comment because it starts with // + + // uncomment the following line to set the path where sqlite files are loaded and saved. + // by default windows uses AppData\Local\SIL\FwLiteDesktop + //"ProjectPath": "" + }, + "FwDataBridge": { + // uncomment the following line to set the path fwdata projects are loaded + // by default we load them from the ProgramData folder where FW stores it's projects +// "ProjectsFolder": "" + } } From bb9c1dc59f33ff6661b8db04285f4b6d48fc4dbf Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 20 Sep 2024 13:21:15 +0700 Subject: [PATCH 11/26] update the splash screen to use the lexbox logo, it's not shown on windows currently though --- .../FwLite/FwLiteDesktop/FwLiteDesktop.csproj | 2 +- .../FwLiteDesktop/Resources/Splash/splash.svg | 139 +++++++++++++++++- 2 files changed, 134 insertions(+), 7 deletions(-) diff --git a/backend/FwLite/FwLiteDesktop/FwLiteDesktop.csproj b/backend/FwLite/FwLiteDesktop/FwLiteDesktop.csproj index fecb71fee..2c5b24013 100644 --- a/backend/FwLite/FwLiteDesktop/FwLiteDesktop.csproj +++ b/backend/FwLite/FwLiteDesktop/FwLiteDesktop.csproj @@ -65,7 +65,7 @@ - + diff --git a/backend/FwLite/FwLiteDesktop/Resources/Splash/splash.svg b/backend/FwLite/FwLiteDesktop/Resources/Splash/splash.svg index 21dfb25f1..fb45d5837 100644 --- a/backend/FwLite/FwLiteDesktop/Resources/Splash/splash.svg +++ b/backend/FwLite/FwLiteDesktop/Resources/Splash/splash.svg @@ -1,8 +1,135 @@ - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 65a48a9bf2493c3ef75fa9f2b733bc3181b576d6 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 20 Sep 2024 13:21:41 +0700 Subject: [PATCH 12/26] update harmony to fix bug updating fields --- backend/harmony | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/harmony b/backend/harmony index 5508102db..0063f050e 160000 --- a/backend/harmony +++ b/backend/harmony @@ -1 +1 @@ -Subproject commit 5508102db01a3e9b28f26fc024c86f5667212de0 +Subproject commit 0063f050e8ef3d79f64c81d89f36c8a5dceff489 From c8fb5588ca38dad9bc5fb2b3db2e578e29f9dc08 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 20 Sep 2024 14:00:34 +0700 Subject: [PATCH 13/26] use server authority for matching projects with servers, try to return project authority from local projects route --- backend/FwLite/LcmCrdt/CrdtProject.cs | 1 + .../FwLite/LcmCrdt/CurrentProjectService.cs | 5 ++ backend/FwLite/LcmCrdt/ProjectsService.cs | 8 ++- backend/FwLite/LocalWebApp/Auth/AuthConfig.cs | 4 ++ .../FwLite/LocalWebApp/Routes/AuthRoutes.cs | 10 ++-- .../LocalWebApp/Routes/ProjectRoutes.cs | 27 ++++++--- frontend/viewer/src/HomeView.svelte | 57 +++++++++++-------- .../src/lib/services/projects-service.ts | 8 +-- 8 files changed, 77 insertions(+), 43 deletions(-) diff --git a/backend/FwLite/LcmCrdt/CrdtProject.cs b/backend/FwLite/LcmCrdt/CrdtProject.cs index 2c5020e9e..8e394304e 100644 --- a/backend/FwLite/LcmCrdt/CrdtProject.cs +++ b/backend/FwLite/LcmCrdt/CrdtProject.cs @@ -8,6 +8,7 @@ public class CrdtProject(string name, string dbPath) : IProjectIdentifier public string Name { get; } = name; public string Origin { get; } = "CRDT"; public string DbPath { get; } = dbPath; + public ProjectData? Data { get; set; } } public record ProjectData(string Name, Guid Id, string? OriginDomain, Guid ClientId) diff --git a/backend/FwLite/LcmCrdt/CurrentProjectService.cs b/backend/FwLite/LcmCrdt/CurrentProjectService.cs index 62c460848..da3202174 100644 --- a/backend/FwLite/LcmCrdt/CurrentProjectService.cs +++ b/backend/FwLite/LcmCrdt/CurrentProjectService.cs @@ -36,6 +36,11 @@ private static string CacheKey(Guid projectId) return $"ProjectData|{projectId}"; } + public static ProjectData? LookupProjectData(IMemoryCache memoryCache, string projectName) + { + return memoryCache.Get(projectName + "|ProjectData"); + } + public static ProjectData? LookupProjectById(IMemoryCache memoryCache, Guid projectId) { return memoryCache.Get(CacheKey(projectId)); diff --git a/backend/FwLite/LcmCrdt/ProjectsService.cs b/backend/FwLite/LcmCrdt/ProjectsService.cs index aaa2d9f07..71f5eb9dc 100644 --- a/backend/FwLite/LcmCrdt/ProjectsService.cs +++ b/backend/FwLite/LcmCrdt/ProjectsService.cs @@ -1,6 +1,7 @@ using SIL.Harmony; using SIL.Harmony.Db; using LcmCrdt.Utils; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -9,14 +10,17 @@ namespace LcmCrdt; -public class ProjectsService(IServiceProvider provider, ProjectContext projectContext, ILogger logger, IOptions config) +public class ProjectsService(IServiceProvider provider, ProjectContext projectContext, ILogger logger, IOptions config, IMemoryCache memoryCache) { public Task ListProjects() { return Task.FromResult(Directory.EnumerateFiles(config.Value.ProjectPath, "*.sqlite").Select(file => { var name = Path.GetFileNameWithoutExtension(file); - return new CrdtProject(name, file); + return new CrdtProject(name, file) + { + Data = CurrentProjectService.LookupProjectData(memoryCache, name) + }; }).ToArray()); } diff --git a/backend/FwLite/LocalWebApp/Auth/AuthConfig.cs b/backend/FwLite/LocalWebApp/Auth/AuthConfig.cs index 5b5f05a41..4e11f8706 100644 --- a/backend/FwLite/LocalWebApp/Auth/AuthConfig.cs +++ b/backend/FwLite/LocalWebApp/Auth/AuthConfig.cs @@ -11,6 +11,10 @@ public class AuthConfig public bool SystemWebViewLogin { get; set; } = false; public LexboxServer DefaultServer => LexboxServers.First(); + public LexboxServer GetServerByAuthority(string authority) + { + return LexboxServers.FirstOrDefault(s => s.Authority.Authority == authority) ?? throw new ArgumentException($"Server {authority} not found"); + } public LexboxServer GetServer(string serverName) { return LexboxServers.FirstOrDefault(s => s.DisplayName == serverName) ?? throw new ArgumentException($"Server {serverName} not found"); diff --git a/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs b/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs index 8578502f4..a4431e057 100644 --- a/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs @@ -8,7 +8,7 @@ namespace LocalWebApp.Routes; public static class AuthRoutes { public const string CallbackRoute = "AuthRoutes_Callback"; - public record ServerStatus(string DisplayName, bool LoggedIn, string? LoggedInAs); + public record ServerStatus(string DisplayName, bool LoggedIn, string? LoggedInAs, string? Authority); public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app) { var group = app.MapGroup("/api/auth").WithOpenApi(); @@ -19,13 +19,13 @@ public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app) var currentName = await factory.GetHelper(s).GetCurrentName(); return new ServerStatus(s.DisplayName, !string.IsNullOrEmpty(currentName), - currentName); + currentName, s.Authority.Authority); }); }); group.MapGet("/login/{server}", async (AuthHelpersFactory factory, string server, IOptions options) => { - var result = await factory.GetHelper(options.Value.GetServer(server)).SignIn(); + var result = await factory.GetHelper(options.Value.GetServerByAuthority(server)).SignIn(); if (result.HandledBySystemWebView) { return Results.Redirect("/"); @@ -49,12 +49,12 @@ public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app) group.MapGet("/me/{server}", async (AuthHelpersFactory factory, string server, IOptions options) => { - return new { name = await factory.GetHelper(options.Value.GetServer(server)).GetCurrentName() }; + return new { name = await factory.GetHelper(options.Value.GetServerByAuthority(server)).GetCurrentName() }; }); group.MapGet("/logout/{server}", async (AuthHelpersFactory factory, string server, IOptions options) => { - await factory.GetHelper(options.Value.GetServer(server)).Logout(); + await factory.GetHelper(options.Value.GetServerByAuthority(server)).Logout(); return Results.Redirect("/"); }); return group; diff --git a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs index 785f8c9ad..93baab5ff 100644 --- a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs @@ -23,7 +23,7 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap foreach (var server in options.Value.LexboxServers) { var lexboxProjects = await lexboxProjectService.GetLexboxProjects(server); - serversProjects.Add(server.DisplayName, lexboxProjects.Select(p => new ProjectModel(p.Name, false, false, true, server.DisplayName, p.Id)).ToArray()); + serversProjects.Add(server.Authority.Authority, lexboxProjects.Select(p => new ProjectModel(p.Name, false, false, true, server.Authority.Authority, p.Id)).ToArray()); } return serversProjects; @@ -35,7 +35,16 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap { var crdtProjects = await projectService.ListProjects(); //todo get project Id and use that to specify the Id in the model. Also pull out server - var projects = crdtProjects.ToDictionary(p => p.Name, p => new ProjectModel(p.Name, true, false)); + var projects = crdtProjects.ToDictionary(p => p.Name, p => + { + var uri = p.Data?.OriginDomain is not null ? new Uri(p.Data.OriginDomain) : null; + return new ProjectModel(p.Name, + true, + false, + p.Data?.OriginDomain is not null, + uri?.Authority, + p.Data?.Id); + }); //basically populate projects and indicate if they are lexbox or fwdata foreach (var p in fieldWorksProjectList.EnumerateProjects()) { @@ -62,14 +71,14 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap await projectService.CreateProject(new(name, AfterCreate: AfterCreate)); return TypedResults.Ok(); }); - group.MapPost($"/upload/crdt/{{serverName}}/{{{CrdtMiniLcmApiHub.ProjectRouteKey}}}", + group.MapPost($"/upload/crdt/{{serverAuthority}}/{{{CrdtMiniLcmApiHub.ProjectRouteKey}}}", async (LexboxProjectService lexboxProjectService, SyncService syncService, IOptions options, CurrentProjectService currentProjectService, - string serverName) => + string serverAuthority) => { - var server = options.Value.GetServer(serverName); + var server = options.Value.GetServerByAuthority(serverAuthority); var foundProjectGuid = await lexboxProjectService.GetLexboxProjectId(server, currentProjectService.ProjectData.Name); if (foundProjectGuid is null) @@ -79,17 +88,17 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap await syncService.ExecuteSync(); return TypedResults.Ok(); }); - group.MapPost("/download/crdt/{serverName}/{newProjectName}", + group.MapPost("/download/crdt/{serverAuthority}/{newProjectName}", async (LexboxProjectService lexboxProjectService, IOptions options, ProjectsService projectService, string newProjectName, - string serverName + string serverAuthority ) => { if (!ProjectName().IsMatch(newProjectName)) return Results.BadRequest("Project name is invalid"); - var server = options.Value.GetServer(serverName); + var server = options.Value.GetServerByAuthority(serverAuthority); var foundProjectGuid = await lexboxProjectService.GetLexboxProjectId(server,newProjectName); if (foundProjectGuid is null) return Results.BadRequest($"Project code {newProjectName} not found on lexbox"); @@ -106,7 +115,7 @@ await projectService.CreateProject(new(newProjectName, return group; } - public record ProjectModel(string Name, bool Crdt, bool Fwdata, bool Lexbox = false, string? Server = null, Guid? Id = null); + public record ProjectModel(string Name, bool Crdt, bool Fwdata, bool Lexbox = false, string? ServerAuthority = null, Guid? Id = null); private static async Task AfterCreate(IServiceProvider provider, CrdtProject project) { diff --git a/frontend/viewer/src/HomeView.svelte b/frontend/viewer/src/HomeView.svelte index 9e4dee600..daf344546 100644 --- a/frontend/viewer/src/HomeView.svelte +++ b/frontend/viewer/src/HomeView.svelte @@ -48,7 +48,7 @@ downloading = ''; } - let projectsPromise = projectsService.fetchProjects(); + let projectsPromise = projectsService.fetchProjects().then(p => projects = p); let projects: Project[] = []; async function refreshProjects() { @@ -100,18 +100,28 @@ function matchesProject(projects: Project[], project: Project) { let matches: Project | undefined = undefined; if (project.id) { - matches = projects.find(p => p.id == project.id); + matches = projects.find(p => p.id == project.id && p.serverAuthority == project.serverAuthority); } //for now the local project list does not include the id, so fallback to the name if (!matches) { - matches = projects.find(p => p.name === project.name); + matches = projects.find(p => p.name === project.name && p.serverAuthority == project.serverAuthority); } return matches; } - function syncedServer(serversProjects: { [server: string]: Project[] }, project: Project): string | null { - return Object.entries(serversProjects) - .find(([server, projects]) => matchesProject(projects, project))?.[0] ?? null; + function syncedServer(serversProjects: { [server: string]: Project[] }, project: Project): ServerStatus | undefined { + //this may be null, even if the project is synced, when the project info isn't cached on the server yet. + if (project.serverAuthority) { + return servers.find(s => s.authority == project.serverAuthority) ?? { + displayName: 'Unknown server ' + project.serverAuthority, + loggedIn: false, + loggedInAs: null, + authority: project.serverAuthority + }; + } + let authority = Object.entries(serversProjects) + .find(([server, projects]) => matchesProject(projects, project))?.[0]; + return authority ? servers.find(s => s.authority == authority) : undefined; } @@ -143,43 +153,43 @@ data={projects.filter((p) => $isDev || p.fwdata).sort((p1, p2) => p1.name.localeCompare(p2.name))} classes={{ th: 'p-4' }}> - {#each data ?? [] as rowData, rowIndex} + {#each data ?? [] as project, rowIndex} {#each columns as column (column.name)} - + {#if column.name === 'fwdata'} - {#if rowData.fwdata} - {/if} {:else if column.name === 'lexbox'} - {@const server = syncedServer(remoteProjects, rowData)} - {#if rowData.crdt && server} - + {@const server = syncedServer(remoteProjects, project)} + {#if project.crdt && server} + {/if} {:else if column.name === 'crdt'} - {#if rowData.crdt} + {#if project.crdt} - {:else if rowData.fwdata} + {:else if project.fwdata} {/if} {:else} - {getCellContent(column, rowData, rowIndex)} + {getCellContent(column, project, rowIndex)} {/if} {/each} @@ -218,17 +228,18 @@

{server.loggedInAs}

{/if} {#if server.loggedIn} - + {:else} - + {/if} - {@const serverProjects = remoteProjects[server.displayName] ?? []} + {@const serverProjects = remoteProjects[server.authority] ?? []} {#each serverProjects as project} + {@const localProject = matchesProject(projects, project)}

{project.name}

- {#if matchesProject(projects, project)?.crdt} + {#if localProject?.crdt} {:else} + {:else} - + {/if}
{@const serverProjects = remoteProjects[server.authority] ?? []} diff --git a/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte b/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte index bae27f64f..c8a42f99b 100644 --- a/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte +++ b/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte @@ -27,8 +27,9 @@ }).then(set); }); $: if (!$projectServer && $servers.length > 0) { - $projectServer = $servers[0].displayName; + $projectServer = $servers[0].authority; } + $: server = $servers.find((server) => server.authority === $projectServer) ?? {displayName: 'Unknown', authority: '', loggedIn: false}; let uploading = false; async function upload() { @@ -60,11 +61,11 @@ color="neutral"/> Hide empty fields - {#if $servers.length > 1} + {#if $servers.length > 1 && !isUploaded} ({ value: server, label: server.displayName, group: server.displayName }))} + options={($servers).map((server) => ({ value: server.authority, label: server.displayName, group: server.displayName }))} bind:value={$projectServer} classes={{root: 'view-select w-auto', options: 'view-select-options'}} clearable={false} @@ -75,12 +76,16 @@ {:else if isUploaded} {/if} - {#if $projectServer && !isUploaded} - + {#if $projectServer && !isUploaded && server.loggedIn} + + {:else if !isUploaded && !server.loggedIn} + + {/if}
From 7de1cf76919cb07c3787240238da9ec257e40abd Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 23 Sep 2024 09:50:43 +0700 Subject: [PATCH 16/26] only show expand button in the entry list on the lg breakpoint, it doesn't do anything at md --- frontend/viewer/src/lib/layout/EntryList.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/viewer/src/lib/layout/EntryList.svelte b/frontend/viewer/src/lib/layout/EntryList.svelte index e453ecf99..d6bf68569 100644 --- a/frontend/viewer/src/lib/layout/EntryList.svelte +++ b/frontend/viewer/src/lib/layout/EntryList.svelte @@ -72,7 +72,7 @@ rounded on:click={() => dictionaryMode = !dictionaryMode}> - -
+
diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte index d6728d998..7d7821196 100644 --- a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte +++ b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte @@ -147,7 +147,7 @@ {/if}
- dispatch('change', {entry, sense})}/> + dispatch('change', {entry, sense})}/>
{#each sense.exampleSentences as example, j (example.id)} From 9fefc49a88f5ad9eabd99a1e8b1214e588b6b17a Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 27 Sep 2024 10:09:10 +0700 Subject: [PATCH 26/26] apply some minor fixes --- backend/FwLite/FwLiteDesktop/MainPage.xaml.cs | 1 - backend/FwLite/LocalWebApp/appsettings.json | 2 -- frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs b/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs index 62fdae7a6..c4e45f04d 100644 --- a/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs +++ b/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs @@ -55,7 +55,6 @@ public MainPage(IOptionsMonitor options, ILogger lo private void NavigationSuccess() { webView.IsVisible = true; - //not currently working if (_environment.IsDevelopment()) { _logger.LogInformation("Enabling dev mode in browser"); diff --git a/backend/FwLite/LocalWebApp/appsettings.json b/backend/FwLite/LocalWebApp/appsettings.json index 6c9c73606..8171300d2 100644 --- a/backend/FwLite/LocalWebApp/appsettings.json +++ b/backend/FwLite/LocalWebApp/appsettings.json @@ -9,8 +9,6 @@ }, "AllowedHosts": "*", "LcmCrdt": { - // this is a comment because it starts with // - // uncomment the following line to set the path where sqlite files are loaded and saved. // by default windows uses AppData\Local\SIL\FwLiteDesktop //"ProjectPath": "" diff --git a/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte b/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte index c8a42f99b..d10824421 100644 --- a/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte +++ b/frontend/viewer/src/lib/layout/ViewOptionsDrawer.svelte @@ -83,7 +83,7 @@ - {:else if !isUploaded && !server.loggedIn} + {:else if $projectServer && !isUploaded && !server.loggedIn} {/if}