From 4a2b026f56a93e22281b4cd96a72c4077d3225ed Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 30 Jun 2026 16:23:54 +0200 Subject: [PATCH 1/8] Attribute template-import commits to a System author A scoped CommitMetadataInterceptor stamps only the template-import commits as the System author (and flags them Template); later user edits keep their real author. The activity view shows System and stops pinning special authors. Co-Authored-By: Claude Opus 4.8 --- .../FwLite/LcmCrdt.Tests/OpenProjectTests.cs | 138 ++++++++++++++---- .../LcmCrdt/CommitMetadataInterceptor.cs | 27 ++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 2 + backend/FwLite/LcmCrdt/CrdtProjectsService.cs | 11 +- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 1 + backend/FwLite/LcmCrdt/Utils/CommitHelpers.cs | 11 ++ .../src/home/CreateProjectDialog.svelte | 1 - frontend/viewer/src/home/HomeView.svelte | 2 +- .../src/lib/activity/ActivityFilter.svelte | 8 +- frontend/viewer/src/lib/activity/utils.ts | 20 ++- 10 files changed, 179 insertions(+), 42 deletions(-) create mode 100644 backend/FwLite/LcmCrdt/CommitMetadataInterceptor.cs diff --git a/backend/FwLite/LcmCrdt.Tests/OpenProjectTests.cs b/backend/FwLite/LcmCrdt.Tests/OpenProjectTests.cs index a650a5f01a..7ce2d17008 100644 --- a/backend/FwLite/LcmCrdt.Tests/OpenProjectTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/OpenProjectTests.cs @@ -1,7 +1,7 @@ +using LcmCrdt.Utils; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using SIL.Harmony; using static LcmCrdt.CrdtProjectsService; namespace LcmCrdt.Tests; @@ -76,46 +76,122 @@ public async Task ProjectDbIsDeletedIfCreateFails() public async Task CreateProjectFromTemplateAppliesRequestedIdentity() { var code = $"blank-from-template-{Guid.NewGuid():N}"; - var sqliteFile = $"{code}.sqlite"; - if (File.Exists(sqliteFile)) File.Delete(sqliteFile); - var builder = Host.CreateEmptyApplicationBuilder(null); - builder.Services.AddTestLcmCrdtClient(); - using var host = builder.Build(); - var asyncScope = host.Services.CreateAsyncScope(); - var crdtProjectsService = asyncScope.ServiceProvider.GetRequiredService(); - - var crdtProject = await crdtProjectsService.CreateProjectFromTemplate(new( + var request = new CreateProjectRequest( Name: "Blank From Template", Code: code, Path: "", - Role: UserProjectRole.Manager), - vernacularWs: "fr"); + Role: UserProjectRole.Manager); - var miniLcmApi = (CrdtMiniLcmApi)await asyncScope.ServiceProvider.OpenCrdtProject(crdtProject); - miniLcmApi.ProjectData.Name.Should().Be("Blank From Template"); - miniLcmApi.ProjectData.Code.Should().Be(code); - miniLcmApi.ProjectData.Role.Should().Be(UserProjectRole.Manager); - miniLcmApi.ProjectData.ClientId.Should().NotBe(Guid.Empty); + await WithProjectFromTemplate(request, "fr", async (services, api) => + { + var miniLcmApi = (CrdtMiniLcmApi)api; + miniLcmApi.ProjectData.Name.Should().Be("Blank From Template"); + miniLcmApi.ProjectData.Code.Should().Be(code); + miniLcmApi.ProjectData.Role.Should().Be(UserProjectRole.Manager); + miniLcmApi.ProjectData.ClientId.Should().NotBe(Guid.Empty); + + var morphTypes = await miniLcmApi.GetMorphTypes().ToArrayAsync(); + morphTypes.Should().HaveCount(CanonicalMorphTypes.All.Count); + + var writingSystems = await miniLcmApi.GetWritingSystems(); + writingSystems.Analysis.Should().ContainSingle().Which.WsId.Should().Be((WritingSystemId)"en"); + writingSystems.Vernacular.Should().ContainSingle().Which.WsId.Should().Be((WritingSystemId)"fr"); + + var entry = await miniLcmApi.CreateEntry(new Entry + { + LexemeForm = { { "fr", "post-template" } }, + }); + (await miniLcmApi.GetEntry(entry.Id)).Should().NotBeNull(); + + await using var dbContext = await services.GetRequiredService>().CreateDbContextAsync(); + // The template is imported through the normal MiniLcm write path, so every commit — the imported + // system data and the post-template writes — is authored under this project's own ClientId. + var commitClientIds = await dbContext.Set().AsNoTracking().Select(c => c.ClientId).Distinct().ToArrayAsync(); + commitClientIds.Should().ContainSingle().Which.Should().Be(miniLcmApi.ProjectData.ClientId); + }); + } - var morphTypes = await miniLcmApi.GetMorphTypes().ToArrayAsync(); - morphTypes.Should().HaveCount(CanonicalMorphTypes.All.Count); + [Fact] + public async Task TemplateCommitsUseTheSystemAuthorAndAreStampedAsTemplateCommits() + { + // Create as a signed-in user so the template stamp has a real author to override. + var request = new CreateProjectRequest("Template Author", $"template-author-{Guid.NewGuid():N}", + AuthenticatedUser: "Test User", AuthenticatedUserId: "test-user-id", Role: UserProjectRole.Manager); + + await WithProjectFromTemplate(request, "fr", async (services, api) => + { + var historyService = services.GetRequiredService(); + + // The imported template data is re-attributed to System and tagged, overriding the signed-in user. + var templateCommits = await historyService.ProjectActivity(0, 1000, new ActivityQuery()).ToArrayAsync(); + templateCommits.Should().NotBeEmpty(); + templateCommits.Should().AllSatisfy(activity => + { + activity.Metadata.AuthorId.Should().Be(CommitHelpers.SystemAuthorId); + activity.Metadata.AuthorName.Should().BeNull("the UI owns and translates the System display name"); + activity.Metadata[CommitHelpers.TemplateProp].Should().Be("true"); + }); + + // A later edit keeps the signed-in user (not System) — the stamp is scoped to the template import. + await api.CreateEntry(new() { LexemeForm = { ["fr"] = "post-template" } }); + var latest = (await historyService.ProjectActivity(0, 1, new ActivityQuery()).ToArrayAsync()).Single(); + latest.Metadata.AuthorId.Should().Be("test-user-id"); + latest.Metadata.AuthorName.Should().Be("Test User"); + latest.Metadata[CommitHelpers.TemplateProp].Should().BeNull(); + }); + } - var writingSystems = await miniLcmApi.GetWritingSystems(); - writingSystems.Analysis.Should().ContainSingle().Which.WsId.Should().Be((WritingSystemId)"en"); - writingSystems.Vernacular.Should().ContainSingle().Which.WsId.Should().Be((WritingSystemId)"fr"); + [Fact] + public async Task CommitMetadataOverrideIsScopedAndRestored() + { + var request = new CreateProjectRequest("Commit Metadata Scope", $"commit-metadata-scope-{Guid.NewGuid():N}", + Role: UserProjectRole.Manager); - var entry = await miniLcmApi.CreateEntry(new Entry + await WithProjectFromTemplate(request, "fr", async (services, api) => { - LexemeForm = { { "fr", "post-template" } }, + var historyService = services.GetRequiredService(); + var interceptor = services.GetRequiredService(); + + // A commit made inside the interceptor scope carries the overridden author. + using (interceptor.Intercept(metadata => metadata.AuthorId = "override-id")) + { + await api.CreateEntry(new() { LexemeForm = { ["fr"] = "in scope" } }); + } + (await LatestCommitAuthorId(historyService)).Should().Be("override-id"); + + // Once the scope ends the override is gone. + await api.CreateEntry(new() { LexemeForm = { ["fr"] = "after scope" } }); + (await LatestCommitAuthorId(historyService)).Should().BeNull(); }); - (await miniLcmApi.GetEntry(entry.Id)).Should().NotBeNull(); + } - await using var dbContext = await asyncScope.ServiceProvider.GetRequiredService>().CreateDbContextAsync(); - // The template is imported through the normal MiniLcm write path, so every commit — the imported - // system data and the post-template writes — is authored under this project's own ClientId. - var commitClientIds = await dbContext.Set().AsNoTracking().Select(c => c.ClientId).Distinct().ToArrayAsync(); - commitClientIds.Should().ContainSingle().Which.Should().Be(miniLcmApi.ProjectData.ClientId); - await dbContext.Database.EnsureDeletedAsync(); + private static async Task LatestCommitAuthorId(HistoryService historyService) + { + var latest = await historyService.ProjectActivity(skip: 0, take: 1).ToArrayAsync(); + return latest.Single().Metadata.AuthorId; + } + + // Spins up a fresh CRDT host, creates a project from the template, opens it, runs the test, and deletes the db. + private static async Task WithProjectFromTemplate( + CreateProjectRequest request, + WritingSystemId vernacularWs, + Func test) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Services.AddTestLcmCrdtClient(); + using var host = builder.Build(); + await using var scope = host.Services.CreateAsyncScope(); + var services = scope.ServiceProvider; + var project = await services.GetRequiredService().CreateProjectFromTemplate(request, vernacularWs); + try + { + await test(services, await services.OpenCrdtProject(project)); + } + finally + { + await using var dbContext = await services.GetRequiredService>().CreateDbContextAsync(); + await dbContext.Database.EnsureDeletedAsync(); + } } [Theory] diff --git a/backend/FwLite/LcmCrdt/CommitMetadataInterceptor.cs b/backend/FwLite/LcmCrdt/CommitMetadataInterceptor.cs new file mode 100644 index 0000000000..68f7cd7145 --- /dev/null +++ b/backend/FwLite/LcmCrdt/CommitMetadataInterceptor.cs @@ -0,0 +1,27 @@ +using SIL.Harmony.Core; + +namespace LcmCrdt; + +/// +/// Scoped hook for shaping the of commits written by +/// for the duration of a scope — e.g. attributing template-imported +/// system data to the System author. applies it when building metadata. +/// +public class CommitMetadataInterceptor +{ + private Action? _interceptor; + + public IDisposable Intercept(Action interceptor) + { + var previous = _interceptor; + _interceptor = interceptor; + return new DisposableAction(() => _interceptor = previous); + } + + public void Apply(CommitMetadata metadata) => _interceptor?.Invoke(metadata); + + private sealed class DisposableAction(Action onDispose) : IDisposable + { + public void Dispose() => onDispose(); + } +} diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index e1bfb02511..1aa48ada4a 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -30,6 +30,7 @@ public class CrdtMiniLcmApi( IOptions config, ILogger logger, LcmMediaService lcmMediaService, + CommitMetadataInterceptor commitMetadataInterceptor, EntrySearchService? entrySearchService = null) : IMiniLcmApi { private Guid ClientId { get; } = projectService.ProjectData.ClientId; @@ -45,6 +46,7 @@ private CommitMetadata NewMetadata() AuthorName = ProjectData.LastUserName ?? config.Value.DefaultAuthorForCommits, AuthorId = ProjectData.LastUserId }; + commitMetadataInterceptor.Apply(metadata); return metadata; } private async Task AddChange(IChange change) diff --git a/backend/FwLite/LcmCrdt/CrdtProjectsService.cs b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs index c4811e202e..c06a2e03f1 100644 --- a/backend/FwLite/LcmCrdt/CrdtProjectsService.cs +++ b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs @@ -2,14 +2,13 @@ using System.Text; using System.Text.RegularExpressions; using LcmCrdt.MediaServer; +using LcmCrdt.Utils; using SIL.Harmony; -using MiniLcm; using MiniLcm.Import; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using LcmCrdt.Objects; using LcmCrdt.Project; using Microsoft.EntityFrameworkCore; using MiniLcm.Project; @@ -149,9 +148,11 @@ public virtual async Task CreateProjectFromTemplate( // Templated projects are created locally; this path has no way to associate one with a server // at creation time, so reject a Domain up front — the invariant holds where the project is born. if (request.Domain is not null) + { throw new ArgumentException( "Templated projects can't be associated with a server at creation time — they're local-only.", nameof(request)); + } var callerAfterCreate = request.AfterCreate; return await CreateProject(request with @@ -161,7 +162,11 @@ public virtual async Task CreateProjectFromTemplate( var api = provider.GetRequiredService(); var jsonOptions = provider.GetRequiredService>().Value.JsonSerializerOptions; var snapshot = ProjectTemplate.CreateNewSnapshot(jsonOptions, vernacularWs, analysisWs); - await projectImporter.ImportProject(api, snapshot); + var commitMetadataInterceptor = provider.GetRequiredService(); + using (commitMetadataInterceptor.Intercept(CommitHelpers.StampAsTemplate)) + { + await projectImporter.ImportProject(api, snapshot); + } if (callerAfterCreate is not null) await callerAfterCreate(provider, project); } }); diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 4aaa97f3b7..d6282371ed 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -69,6 +69,7 @@ public static IServiceCollection AddLcmCrdtClientCore(this IServiceCollection se crdtConfig.LocalResourceCachePath = Path.Combine(lcmConfig.Value.ProjectPath, "localResourcesCache"); }); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddMiniLcmValidators(); services.AddSingleton(); diff --git a/backend/FwLite/LcmCrdt/Utils/CommitHelpers.cs b/backend/FwLite/LcmCrdt/Utils/CommitHelpers.cs index 119eb9d7b2..9294c1f9be 100644 --- a/backend/FwLite/LcmCrdt/Utils/CommitHelpers.cs +++ b/backend/FwLite/LcmCrdt/Utils/CommitHelpers.cs @@ -5,6 +5,10 @@ namespace LcmCrdt.Utils; public static class CommitHelpers { public const string SyncDateProp = "SyncDate"; + // Mirrored by SYSTEM_AUTHOR_KEY in frontend/viewer/src/lib/activity/utils.ts + public const string SystemAuthorId = "00000000-0000-0000-0000-000000000001"; + public const string TemplateProp = "Template"; + public static DateTimeOffset? SyncDate(this CommitBase commit) { var dateAsString = commit.Metadata[SyncDateProp]; @@ -21,4 +25,11 @@ public static void SetSyncDate(this CommitBase commit, DateTimeOffset? syncDate) { commit.Metadata[SyncDateProp] = syncDate?.ToString("u"); } + + public static void StampAsTemplate(CommitMetadata metadata) + { + metadata.AuthorId = SystemAuthorId; + metadata.AuthorName = null; + metadata[TemplateProp] = "true"; + } } diff --git a/frontend/viewer/src/home/CreateProjectDialog.svelte b/frontend/viewer/src/home/CreateProjectDialog.svelte index 99fa5540a1..448bf93f85 100644 --- a/frontend/viewer/src/home/CreateProjectDialog.svelte +++ b/frontend/viewer/src/home/CreateProjectDialog.svelte @@ -37,7 +37,6 @@ error = undefined; submitted = false; name = ''; - code = ''; vernacularWs = ''; analysisWs = ''; loading = false; diff --git a/frontend/viewer/src/home/HomeView.svelte b/frontend/viewer/src/home/HomeView.svelte index 7cc6dd7da0..06e9c53117 100644 --- a/frontend/viewer/src/home/HomeView.svelte +++ b/frontend/viewer/src/home/HomeView.svelte @@ -219,7 +219,7 @@ icon="i-mdi-book-plus-outline" class="mb-2 bg-transparent shadow-none hover:shadow-none border-2 border-dashed border-muted-foreground/40" onclick={() => createProjectDialog?.openDialog()}> - {$t`New Project`} ({$t`Local only`}) + {$t`New Project (Local only)`} {$t`Create a new FieldWorks Lite project`} diff --git a/frontend/viewer/src/lib/activity/ActivityFilter.svelte b/frontend/viewer/src/lib/activity/ActivityFilter.svelte index a9a84f3e68..089f1dadfc 100644 --- a/frontend/viewer/src/lib/activity/ActivityFilter.svelte +++ b/frontend/viewer/src/lib/activity/ActivityFilter.svelte @@ -16,6 +16,7 @@ ALL_AUTHORS, ALL_CHANGE_TYPES, FIELDWORKS_AUTHOR_KEY, + SYSTEM_AUTHOR_KEY, UNKNOWN_AUTHOR_KEY, applyMultiSelectValue, authorFilterKey, @@ -23,6 +24,7 @@ createDefaultActivityFilters, isAllFilterSelection, resolveFilterKeys, + wellKnownAuthorKeyToLabel, type ActivityFilters, type MultiFilterSelection, } from './utils'; @@ -79,7 +81,8 @@ }; function authorKeyToLabel(key: string): string { - if (key === UNKNOWN_AUTHOR_KEY) return $t`Unknown`; + const wellKnownLabel = wellKnownAuthorKeyToLabel(key); + if (wellKnownLabel) return wellKnownLabel; const author = authors.current.find(a => authorFilterKey(a) === key); return author?.authorName ?? key; } @@ -110,6 +113,9 @@ {#if key === FIELDWORKS_AUTHOR_KEY} {/if} + {#if key === SYSTEM_AUTHOR_KEY} + + {/if} {/snippet} diff --git a/frontend/viewer/src/lib/activity/utils.ts b/frontend/viewer/src/lib/activity/utils.ts index 6740dc5713..d27f8fc5fc 100644 --- a/frontend/viewer/src/lib/activity/utils.ts +++ b/frontend/viewer/src/lib/activity/utils.ts @@ -1,8 +1,11 @@ import {ActivitySort, type IActivityAuthor, type IActivityQuery, type IProjectActivity} from '$lib/dotnet-types'; +import {gt} from 'svelte-i18n-lingui'; export const ALL_AUTHORS = '__all__'; export const UNKNOWN_AUTHOR_KEY = '__unknown__'; export const FIELDWORKS_AUTHOR_KEY = authorFilterKey({authorName: 'FieldWorks'}); +// Mirrors SystemAuthorId in backend/FwLite/LcmCrdt/Utils/CommitHelpers.cs +export const SYSTEM_AUTHOR_KEY = '00000000-0000-0000-0000-000000000001'; export const ALL_CHANGE_TYPES = '__all__'; export const MIN_VISIBLE_FILTERED = 20; @@ -68,17 +71,24 @@ export function authorFilterKey(author: Omit): s return `name:${author.authorName}`; } +export function wellKnownAuthorKeyToLabel(key: string | undefined): string | undefined { + if (key === UNKNOWN_AUTHOR_KEY) return gt`Unknown`; + if (key === SYSTEM_AUTHOR_KEY) return gt`System`; + return undefined; +} + function authorSortRank(author: IActivityAuthor): number { - const key = authorFilterKey(author); - if (key === UNKNOWN_AUTHOR_KEY) return 0; // 1st - if (key === FIELDWORKS_AUTHOR_KEY) return 1; // 2nd - return 2; + // Unknown is pinned to the top (it's the catch-all, not a real author); FieldWorks, System + // and people all sort alphabetically together. + return authorFilterKey(author) === UNKNOWN_AUTHOR_KEY ? 0 : 1; } export function compareActivityAuthors(a: IActivityAuthor, b: IActivityAuthor): number { const rankDiff = authorSortRank(a) - authorSortRank(b); if (rankDiff !== 0) return rankDiff; - return (a.authorName || '').localeCompare(b.authorName || ''); + const aName = wellKnownAuthorKeyToLabel(a.authorId) || a.authorName || a.authorId || ''; + const bName = wellKnownAuthorKeyToLabel(b.authorId) || b.authorName || b.authorId || ''; + return aName.localeCompare(bName); } export function applyMultiSelectValue( From 92b444f5a7276214761efd2e3d518426b7ba607a Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 30 Jun 2026 16:24:53 +0200 Subject: [PATCH 2/8] Re-extract i18n catalogs for New Project (Local only) Folds the runtime-composed "New Project" + "(Local only)" label (HomeView) into one translatable msgid across all locales. Co-Authored-By: Claude Opus 4.8 --- frontend/viewer/src/locales/en.po | 9 ++++++--- frontend/viewer/src/locales/es.po | 9 ++++++--- frontend/viewer/src/locales/fr.po | 9 ++++++--- frontend/viewer/src/locales/id.po | 9 ++++++--- frontend/viewer/src/locales/ko.po | 9 ++++++--- frontend/viewer/src/locales/ms.po | 9 ++++++--- frontend/viewer/src/locales/sw.po | 9 ++++++--- frontend/viewer/src/locales/vi.po | 9 ++++++--- 8 files changed, 48 insertions(+), 24 deletions(-) diff --git a/frontend/viewer/src/locales/en.po b/frontend/viewer/src/locales/en.po index 3e1c92a748..6b84bbad2d 100644 --- a/frontend/viewer/src/locales/en.po +++ b/frontend/viewer/src/locales/en.po @@ -1174,7 +1174,6 @@ msgstr "Local" #. Subtitle shown on a project card when the project has no server configured (no sync partner). #: src/home/HomeView.svelte -#: src/home/HomeView.svelte msgid "Local only" msgstr "Local only" @@ -1328,10 +1327,13 @@ msgid "New Entry" msgstr "New Entry" #: src/home/CreateProjectDialog.svelte -#: src/home/HomeView.svelte msgid "New Project" msgstr "New Project" +#: src/home/HomeView.svelte +msgid "New Project (Local only)" +msgstr "New Project (Local only)" + #. Relevant view: Lite #. Classic view equivalent: "New Entry" #. Dialog title for creating new word @@ -1940,6 +1942,7 @@ msgid "Syncing..." msgstr "Syncing..." #. Theme option +#: src/lib/activity/utils.ts #: src/lib/components/ThemePicker.svelte msgid "System" msgstr "System" @@ -2090,9 +2093,9 @@ msgstr "Unable to open in FieldWorks" #. Fallback value shown when author name or last-change date is unavailable (e.g., in activity history or the sync panel). #: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityFilter.svelte -#: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityItem.svelte #: src/lib/activity/ActivityView.svelte +#: src/lib/activity/utils.ts #: src/project/sync/FwLiteToFwMergeDetails.svelte #: src/project/sync/FwLiteToFwMergeDetails.svelte msgid "Unknown" diff --git a/frontend/viewer/src/locales/es.po b/frontend/viewer/src/locales/es.po index 23be159e98..4466e176aa 100644 --- a/frontend/viewer/src/locales/es.po +++ b/frontend/viewer/src/locales/es.po @@ -1179,7 +1179,6 @@ msgstr "Local" #. Subtitle shown on a project card when the project has no server configured (no sync partner). #: src/home/HomeView.svelte -#: src/home/HomeView.svelte msgid "Local only" msgstr "Sólo local" @@ -1333,10 +1332,13 @@ msgid "New Entry" msgstr "Nueva entrada" #: src/home/CreateProjectDialog.svelte -#: src/home/HomeView.svelte msgid "New Project" msgstr "" +#: src/home/HomeView.svelte +msgid "New Project (Local only)" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "New Entry" #. Dialog title for creating new word @@ -1945,6 +1947,7 @@ msgid "Syncing..." msgstr "Sincronizando..." #. Theme option +#: src/lib/activity/utils.ts #: src/lib/components/ThemePicker.svelte msgid "System" msgstr "Sistema" @@ -2095,9 +2098,9 @@ msgstr "No se puede abrir en FieldWorks" #. Fallback value shown when author name or last-change date is unavailable (e.g., in activity history or the sync panel). #: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityFilter.svelte -#: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityItem.svelte #: src/lib/activity/ActivityView.svelte +#: src/lib/activity/utils.ts #: src/project/sync/FwLiteToFwMergeDetails.svelte #: src/project/sync/FwLiteToFwMergeDetails.svelte msgid "Unknown" diff --git a/frontend/viewer/src/locales/fr.po b/frontend/viewer/src/locales/fr.po index cb7f3351cc..11ac66807a 100644 --- a/frontend/viewer/src/locales/fr.po +++ b/frontend/viewer/src/locales/fr.po @@ -1179,7 +1179,6 @@ msgstr "Local" #. Subtitle shown on a project card when the project has no server configured (no sync partner). #: src/home/HomeView.svelte -#: src/home/HomeView.svelte msgid "Local only" msgstr "Uniquement au niveau local" @@ -1333,10 +1332,13 @@ msgid "New Entry" msgstr "Nouvelle entrée" #: src/home/CreateProjectDialog.svelte -#: src/home/HomeView.svelte msgid "New Project" msgstr "" +#: src/home/HomeView.svelte +msgid "New Project (Local only)" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "New Entry" #. Dialog title for creating new word @@ -1945,6 +1947,7 @@ msgid "Syncing..." msgstr "Synchronisation en cours..." #. Theme option +#: src/lib/activity/utils.ts #: src/lib/components/ThemePicker.svelte msgid "System" msgstr "Système" @@ -2095,9 +2098,9 @@ msgstr "Impossible d'ouvrir FieldWorks" #. Fallback value shown when author name or last-change date is unavailable (e.g., in activity history or the sync panel). #: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityFilter.svelte -#: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityItem.svelte #: src/lib/activity/ActivityView.svelte +#: src/lib/activity/utils.ts #: src/project/sync/FwLiteToFwMergeDetails.svelte #: src/project/sync/FwLiteToFwMergeDetails.svelte msgid "Unknown" diff --git a/frontend/viewer/src/locales/id.po b/frontend/viewer/src/locales/id.po index 22a8ccbb86..cc4db4c822 100644 --- a/frontend/viewer/src/locales/id.po +++ b/frontend/viewer/src/locales/id.po @@ -1179,7 +1179,6 @@ msgstr "Lokal" #. Subtitle shown on a project card when the project has no server configured (no sync partner). #: src/home/HomeView.svelte -#: src/home/HomeView.svelte msgid "Local only" msgstr "Hanya lokal" @@ -1333,10 +1332,13 @@ msgid "New Entry" msgstr "Entri Baru" #: src/home/CreateProjectDialog.svelte -#: src/home/HomeView.svelte msgid "New Project" msgstr "" +#: src/home/HomeView.svelte +msgid "New Project (Local only)" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "New Entry" #. Dialog title for creating new word @@ -1945,6 +1947,7 @@ msgid "Syncing..." msgstr "Menyinkronkan..." #. Theme option +#: src/lib/activity/utils.ts #: src/lib/components/ThemePicker.svelte msgid "System" msgstr "Sistem" @@ -2095,9 +2098,9 @@ msgstr "Tidak dapat dibuka di FieldWorks" #. Fallback value shown when author name or last-change date is unavailable (e.g., in activity history or the sync panel). #: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityFilter.svelte -#: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityItem.svelte #: src/lib/activity/ActivityView.svelte +#: src/lib/activity/utils.ts #: src/project/sync/FwLiteToFwMergeDetails.svelte #: src/project/sync/FwLiteToFwMergeDetails.svelte msgid "Unknown" diff --git a/frontend/viewer/src/locales/ko.po b/frontend/viewer/src/locales/ko.po index 5570a00efb..463bc45c15 100644 --- a/frontend/viewer/src/locales/ko.po +++ b/frontend/viewer/src/locales/ko.po @@ -1179,7 +1179,6 @@ msgstr "로컬" #. Subtitle shown on a project card when the project has no server configured (no sync partner). #: src/home/HomeView.svelte -#: src/home/HomeView.svelte msgid "Local only" msgstr "로컬 전용" @@ -1333,10 +1332,13 @@ msgid "New Entry" msgstr "새 항목" #: src/home/CreateProjectDialog.svelte -#: src/home/HomeView.svelte msgid "New Project" msgstr "" +#: src/home/HomeView.svelte +msgid "New Project (Local only)" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "New Entry" #. Dialog title for creating new word @@ -1945,6 +1947,7 @@ msgid "Syncing..." msgstr "동기화..." #. Theme option +#: src/lib/activity/utils.ts #: src/lib/components/ThemePicker.svelte msgid "System" msgstr "시스템" @@ -2095,9 +2098,9 @@ msgstr "FieldWorks에서 열 수 없음" #. Fallback value shown when author name or last-change date is unavailable (e.g., in activity history or the sync panel). #: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityFilter.svelte -#: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityItem.svelte #: src/lib/activity/ActivityView.svelte +#: src/lib/activity/utils.ts #: src/project/sync/FwLiteToFwMergeDetails.svelte #: src/project/sync/FwLiteToFwMergeDetails.svelte msgid "Unknown" diff --git a/frontend/viewer/src/locales/ms.po b/frontend/viewer/src/locales/ms.po index 882f4fea77..99a2ba01ca 100644 --- a/frontend/viewer/src/locales/ms.po +++ b/frontend/viewer/src/locales/ms.po @@ -1179,7 +1179,6 @@ msgstr "Tempatan" #. Subtitle shown on a project card when the project has no server configured (no sync partner). #: src/home/HomeView.svelte -#: src/home/HomeView.svelte msgid "Local only" msgstr "Tempatan sahaja" @@ -1333,10 +1332,13 @@ msgid "New Entry" msgstr "Entri Baru" #: src/home/CreateProjectDialog.svelte -#: src/home/HomeView.svelte msgid "New Project" msgstr "" +#: src/home/HomeView.svelte +msgid "New Project (Local only)" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "New Entry" #. Dialog title for creating new word @@ -1945,6 +1947,7 @@ msgid "Syncing..." msgstr "Sedang disegerakkan..." #. Theme option +#: src/lib/activity/utils.ts #: src/lib/components/ThemePicker.svelte msgid "System" msgstr "Sistem" @@ -2095,9 +2098,9 @@ msgstr "Tidak dapat membuka dalam FieldWorks" #. Fallback value shown when author name or last-change date is unavailable (e.g., in activity history or the sync panel). #: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityFilter.svelte -#: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityItem.svelte #: src/lib/activity/ActivityView.svelte +#: src/lib/activity/utils.ts #: src/project/sync/FwLiteToFwMergeDetails.svelte #: src/project/sync/FwLiteToFwMergeDetails.svelte msgid "Unknown" diff --git a/frontend/viewer/src/locales/sw.po b/frontend/viewer/src/locales/sw.po index a942c1bd89..2dd287849e 100644 --- a/frontend/viewer/src/locales/sw.po +++ b/frontend/viewer/src/locales/sw.po @@ -1179,7 +1179,6 @@ msgstr "Mitaa" #. Subtitle shown on a project card when the project has no server configured (no sync partner). #: src/home/HomeView.svelte -#: src/home/HomeView.svelte msgid "Local only" msgstr "Mitaa tu" @@ -1333,10 +1332,13 @@ msgid "New Entry" msgstr "Ingizo Mpya" #: src/home/CreateProjectDialog.svelte -#: src/home/HomeView.svelte msgid "New Project" msgstr "" +#: src/home/HomeView.svelte +msgid "New Project (Local only)" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "New Entry" #. Dialog title for creating new word @@ -1945,6 +1947,7 @@ msgid "Syncing..." msgstr "Inaoanisha..." #. Theme option +#: src/lib/activity/utils.ts #: src/lib/components/ThemePicker.svelte msgid "System" msgstr "Mfumo" @@ -2095,9 +2098,9 @@ msgstr "Haiwezi kufungua katika FieldWorks" #. Fallback value shown when author name or last-change date is unavailable (e.g., in activity history or the sync panel). #: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityFilter.svelte -#: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityItem.svelte #: src/lib/activity/ActivityView.svelte +#: src/lib/activity/utils.ts #: src/project/sync/FwLiteToFwMergeDetails.svelte #: src/project/sync/FwLiteToFwMergeDetails.svelte msgid "Unknown" diff --git a/frontend/viewer/src/locales/vi.po b/frontend/viewer/src/locales/vi.po index 0b88e18e74..8a88c9fac6 100644 --- a/frontend/viewer/src/locales/vi.po +++ b/frontend/viewer/src/locales/vi.po @@ -1179,7 +1179,6 @@ msgstr "Cục bộ" #. Subtitle shown on a project card when the project has no server configured (no sync partner). #: src/home/HomeView.svelte -#: src/home/HomeView.svelte msgid "Local only" msgstr "Chỉ cục bộ" @@ -1333,10 +1332,13 @@ msgid "New Entry" msgstr "Mục mới" #: src/home/CreateProjectDialog.svelte -#: src/home/HomeView.svelte msgid "New Project" msgstr "" +#: src/home/HomeView.svelte +msgid "New Project (Local only)" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "New Entry" #. Dialog title for creating new word @@ -1945,6 +1947,7 @@ msgid "Syncing..." msgstr "Đang đồng bộ..." #. Theme option +#: src/lib/activity/utils.ts #: src/lib/components/ThemePicker.svelte msgid "System" msgstr "Hệ thống" @@ -2095,9 +2098,9 @@ msgstr "Không thể mở trong FieldWorks" #. Fallback value shown when author name or last-change date is unavailable (e.g., in activity history or the sync panel). #: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityFilter.svelte -#: src/lib/activity/ActivityFilter.svelte #: src/lib/activity/ActivityItem.svelte #: src/lib/activity/ActivityView.svelte +#: src/lib/activity/utils.ts #: src/project/sync/FwLiteToFwMergeDetails.svelte #: src/project/sync/FwLiteToFwMergeDetails.svelte msgid "Unknown" From f76605d3032ad4136a7eb87e5cd1e7258f1d23d1 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 1 Jul 2026 09:40:03 +0200 Subject: [PATCH 3/8] Isolate CommitMetadataInterceptor overrides per async flow Store the interceptor in an AsyncLocal instead of a shared field so a template-import override can't leak into a concurrent commit made through the same DI-scoped CrdtMiniLcmApi. Co-Authored-By: Claude Opus 4.8 --- .../LcmCrdt/CommitMetadataInterceptor.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/FwLite/LcmCrdt/CommitMetadataInterceptor.cs b/backend/FwLite/LcmCrdt/CommitMetadataInterceptor.cs index 68f7cd7145..3182eb7da1 100644 --- a/backend/FwLite/LcmCrdt/CommitMetadataInterceptor.cs +++ b/backend/FwLite/LcmCrdt/CommitMetadataInterceptor.cs @@ -3,22 +3,24 @@ namespace LcmCrdt; /// -/// Scoped hook for shaping the of commits written by -/// for the duration of a scope — e.g. attributing template-imported -/// system data to the System author. applies it when building metadata. +/// Async-flow-scoped hook for shaping the of commits written by +/// for the duration of an scope — e.g. attributing +/// template-imported system data to the System author. The override lives in an , +/// so it applies only to commits made within the awaited call tree of the scope and can't leak into other +/// operations that share the same DI scope. applies it when building metadata. /// public class CommitMetadataInterceptor { - private Action? _interceptor; + private readonly AsyncLocal?> _interceptor = new(); public IDisposable Intercept(Action interceptor) { - var previous = _interceptor; - _interceptor = interceptor; - return new DisposableAction(() => _interceptor = previous); + var previous = _interceptor.Value; + _interceptor.Value = interceptor; + return new DisposableAction(() => _interceptor.Value = previous); } - public void Apply(CommitMetadata metadata) => _interceptor?.Invoke(metadata); + public void Apply(CommitMetadata metadata) => _interceptor.Value?.Invoke(metadata); private sealed class DisposableAction(Action onDispose) : IDisposable { From a88fe7f259832593077ab6e93ab3b22ad089a53c Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 1 Jul 2026 15:03:43 +0200 Subject: [PATCH 4/8] Fix System author typeahead label in activity filter The Select.Item label prop fell back to "Unknown" for the System author (authorName is null by design), so typeahead couldn't jump to it. Use authorKeyToLabel(key), which resolves the System/Unknown display names. Co-Authored-By: Claude Opus 4.8 --- frontend/viewer/src/lib/activity/ActivityFilter.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/viewer/src/lib/activity/ActivityFilter.svelte b/frontend/viewer/src/lib/activity/ActivityFilter.svelte index 089f1dadfc..2923416987 100644 --- a/frontend/viewer/src/lib/activity/ActivityFilter.svelte +++ b/frontend/viewer/src/lib/activity/ActivityFilter.svelte @@ -147,7 +147,7 @@ {#each authors.current as author (authorFilterKey(author))} {@const key = authorFilterKey(author)} - + {@render authorLabel(key)} ({author.commitCount}) From 511f5ff94514de5e87e5d68b9896f0c5704eab32 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 1 Jul 2026 15:14:37 +0200 Subject: [PATCH 5/8] Show System author in activity list and detail views ActivityView and ActivityItem rendered author from the raw authorName, which is null for template (System) commits, so they showed "Unknown". Resolve the well-known label from authorId (as the filter already does) before falling back to authorName, then "Unknown". Co-Authored-By: Claude Opus 4.8 --- frontend/viewer/src/lib/activity/ActivityItem.svelte | 8 +++++--- frontend/viewer/src/lib/activity/ActivityView.svelte | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/viewer/src/lib/activity/ActivityItem.svelte b/frontend/viewer/src/lib/activity/ActivityItem.svelte index 2bc998e217..00de2ba419 100644 --- a/frontend/viewer/src/lib/activity/ActivityItem.svelte +++ b/frontend/viewer/src/lib/activity/ActivityItem.svelte @@ -25,7 +25,7 @@ import {T, t} from 'svelte-i18n-lingui'; import {VList} from 'virtua/svelte'; import ActivityItemChangePreview from './ActivityItemChangePreview.svelte'; - import {formatJsonForUi} from './utils'; + import {formatJsonForUi, wellKnownAuthorKeyToLabel} from './utils'; import type {HTMLAttributes} from 'svelte/elements'; import {cn} from '$lib/utils'; import * as Popover from '$lib/components/ui/popover'; @@ -51,6 +51,8 @@ const changes = $derived(!historyService.loaded ? undefined : activity.changes.map(change => { return new ChangeWithLazyContext(change, activity, () => historyService.loadChangeContext(activity.commitId, change.index)); })); + + const authorLabel = $derived(wellKnownAuthorKeyToLabel(activity.metadata.authorId) ?? activity.metadata.authorName);
@@ -59,8 +61,8 @@ {$t`Author:`} - {#if activity.metadata.authorName} - {activity.metadata.authorName} + {#if authorLabel} + {authorLabel} {:else} {$t`Unknown`} {/if} diff --git a/frontend/viewer/src/lib/activity/ActivityView.svelte b/frontend/viewer/src/lib/activity/ActivityView.svelte index 39cf17cfdb..4185d8401d 100644 --- a/frontend/viewer/src/lib/activity/ActivityView.svelte +++ b/frontend/viewer/src/lib/activity/ActivityView.svelte @@ -18,6 +18,7 @@ MIN_VISIBLE_FILTERED, serverQueryKey, toServerQuery, + wellKnownAuthorKeyToLabel, type ActivityFilters, type ActivityLoad, } from './utils'; @@ -164,7 +165,7 @@ actualDateOptions={{ dateStyle: 'medium', timeStyle: 'short' }}/> - {row.metadata.authorName ?? $t`Unknown`} + {wellKnownAuthorKeyToLabel(row.metadata.authorId) ?? row.metadata.authorName ?? $t`Unknown`}
From f9a08dbe12e26fc65c5e9f8cc99db5cb1a2ffa79 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 1 Jul 2026 15:35:35 +0200 Subject: [PATCH 6/8] Show well-known author icons consistently via shared AuthorLabel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the author name + well-known icon (FieldWorks logo, System cog) into a single AuthorLabel component and use it in the filter dropdown/trigger and the activity list and detail views, so the icons show everywhere a well-known author is displayed — not just in the filter. Co-Authored-By: Claude Opus 4.8 --- .../src/lib/activity/ActivityFilter.svelte | 27 +++------------- .../src/lib/activity/ActivityItem.svelte | 11 ++----- .../src/lib/activity/ActivityView.svelte | 6 ++-- .../src/lib/activity/AuthorLabel.svelte | 31 +++++++++++++++++++ 4 files changed, 40 insertions(+), 35 deletions(-) create mode 100644 frontend/viewer/src/lib/activity/AuthorLabel.svelte diff --git a/frontend/viewer/src/lib/activity/ActivityFilter.svelte b/frontend/viewer/src/lib/activity/ActivityFilter.svelte index 2923416987..5c2698c5f6 100644 --- a/frontend/viewer/src/lib/activity/ActivityFilter.svelte +++ b/frontend/viewer/src/lib/activity/ActivityFilter.svelte @@ -1,5 +1,4 @@ -{#snippet authorLabel(key: string)} - {@const label = authorKeyToLabel(key)} - - {#if key === UNKNOWN_AUTHOR_KEY} - {$t`Unknown`} - {:else} - {label} - {/if} - {#if key === FIELDWORKS_AUTHOR_KEY} - - {/if} - {#if key === SYSTEM_AUTHOR_KEY} - - {/if} - -{/snippet} -
@@ -130,7 +110,8 @@ {:else if filters.authorFilterKeys.length === 0} {$t`No authors`} {:else if filters.authorFilterKeys.length === 1} - {@render authorLabel(filters.authorFilterKeys[0])} + {@const selectedAuthor = authors.current.find(a => authorFilterKey(a) === filters.authorFilterKeys[0])} + {:else} {$t`${filters.authorFilterKeys.length} authors`} {/if} @@ -148,7 +129,7 @@ {#each authors.current as author (authorFilterKey(author))} {@const key = authorFilterKey(author)} - {@render authorLabel(key)} + ({author.commitCount}) {/each} diff --git a/frontend/viewer/src/lib/activity/ActivityItem.svelte b/frontend/viewer/src/lib/activity/ActivityItem.svelte index 00de2ba419..3dc4673c04 100644 --- a/frontend/viewer/src/lib/activity/ActivityItem.svelte +++ b/frontend/viewer/src/lib/activity/ActivityItem.svelte @@ -25,7 +25,8 @@ import {T, t} from 'svelte-i18n-lingui'; import {VList} from 'virtua/svelte'; import ActivityItemChangePreview from './ActivityItemChangePreview.svelte'; - import {formatJsonForUi, wellKnownAuthorKeyToLabel} from './utils'; + import {formatJsonForUi} from './utils'; + import AuthorLabel from './AuthorLabel.svelte'; import type {HTMLAttributes} from 'svelte/elements'; import {cn} from '$lib/utils'; import * as Popover from '$lib/components/ui/popover'; @@ -51,8 +52,6 @@ const changes = $derived(!historyService.loaded ? undefined : activity.changes.map(change => { return new ChangeWithLazyContext(change, activity, () => historyService.loadChangeContext(activity.commitId, change.index)); })); - - const authorLabel = $derived(wellKnownAuthorKeyToLabel(activity.metadata.authorId) ?? activity.metadata.authorName);
@@ -61,11 +60,7 @@ {$t`Author:`} - {#if authorLabel} - {authorLabel} - {:else} - {$t`Unknown`} - {/if} + {#if activity.changes.length > 1} {$t`– (${activity.changes.length} changes)`} diff --git a/frontend/viewer/src/lib/activity/ActivityView.svelte b/frontend/viewer/src/lib/activity/ActivityView.svelte index 4185d8401d..862c87cbe9 100644 --- a/frontend/viewer/src/lib/activity/ActivityView.svelte +++ b/frontend/viewer/src/lib/activity/ActivityView.svelte @@ -18,10 +18,10 @@ MIN_VISIBLE_FILTERED, serverQueryKey, toServerQuery, - wellKnownAuthorKeyToLabel, type ActivityFilters, type ActivityLoad, } from './utils'; + import AuthorLabel from './AuthorLabel.svelte'; const historyService = useHistoryService(); @@ -164,9 +164,7 @@ - - {wellKnownAuthorKeyToLabel(row.metadata.authorId) ?? row.metadata.authorName ?? $t`Unknown`} - +
{/snippet} diff --git a/frontend/viewer/src/lib/activity/AuthorLabel.svelte b/frontend/viewer/src/lib/activity/AuthorLabel.svelte new file mode 100644 index 0000000000..776c77db6a --- /dev/null +++ b/frontend/viewer/src/lib/activity/AuthorLabel.svelte @@ -0,0 +1,31 @@ + + + + {#if key === UNKNOWN_AUTHOR_KEY} + {$t`Unknown`} + {:else} + {label} + {/if} + {#if key === FIELDWORKS_AUTHOR_KEY} + + {:else if key === SYSTEM_AUTHOR_KEY} + + {/if} + From d45a9d5f8cf9f474ab41702c66431bd484ccfee3 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 1 Jul 2026 16:15:56 +0200 Subject: [PATCH 7/8] Tune AuthorLabel icon sizes per context Default the icon to size-4 (matches the text-sm activity list and detail views) and have the author filter opt into size-5 explicitly, restoring the filter's original icon size. Co-Authored-By: Claude Opus 4.8 --- frontend/viewer/src/lib/activity/ActivityFilter.svelte | 4 ++-- frontend/viewer/src/lib/activity/ActivityView.svelte | 2 +- frontend/viewer/src/lib/activity/AuthorLabel.svelte | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/viewer/src/lib/activity/ActivityFilter.svelte b/frontend/viewer/src/lib/activity/ActivityFilter.svelte index 5c2698c5f6..b5445394a3 100644 --- a/frontend/viewer/src/lib/activity/ActivityFilter.svelte +++ b/frontend/viewer/src/lib/activity/ActivityFilter.svelte @@ -111,7 +111,7 @@ {$t`No authors`} {:else if filters.authorFilterKeys.length === 1} {@const selectedAuthor = authors.current.find(a => authorFilterKey(a) === filters.authorFilterKeys[0])} - + {:else} {$t`${filters.authorFilterKeys.length} authors`} {/if} @@ -129,7 +129,7 @@ {#each authors.current as author (authorFilterKey(author))} {@const key = authorFilterKey(author)} - + ({author.commitCount}) {/each} diff --git a/frontend/viewer/src/lib/activity/ActivityView.svelte b/frontend/viewer/src/lib/activity/ActivityView.svelte index 862c87cbe9..9fb0a255e8 100644 --- a/frontend/viewer/src/lib/activity/ActivityView.svelte +++ b/frontend/viewer/src/lib/activity/ActivityView.svelte @@ -164,7 +164,7 @@ - +
{/snippet} diff --git a/frontend/viewer/src/lib/activity/AuthorLabel.svelte b/frontend/viewer/src/lib/activity/AuthorLabel.svelte index 776c77db6a..73be811797 100644 --- a/frontend/viewer/src/lib/activity/AuthorLabel.svelte +++ b/frontend/viewer/src/lib/activity/AuthorLabel.svelte @@ -5,7 +5,7 @@ import flexLogo from '$lib/assets/flex-logo.png'; import {FIELDWORKS_AUTHOR_KEY, SYSTEM_AUTHOR_KEY, UNKNOWN_AUTHOR_KEY, authorFilterKey, wellKnownAuthorKeyToLabel} from './utils'; - let {authorId, authorName, iconClass = 'size-5', class: className}: { + let {authorId, authorName, iconClass = 'size-4', class: className}: { authorId?: string; authorName?: string; iconClass?: string; From 297d6943f03702bb25f8108de8f80feb79239753 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 1 Jul 2026 16:19:21 +0200 Subject: [PATCH 8/8] Fix AuthorLabel icon sizes per icon, not per context The FieldWorks logo reads best at size-5 and the System cog at size-4 in every context, so hardcode each and drop the iconClass prop. Co-Authored-By: Claude Opus 4.8 --- frontend/viewer/src/lib/activity/ActivityFilter.svelte | 4 ++-- frontend/viewer/src/lib/activity/AuthorLabel.svelte | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/viewer/src/lib/activity/ActivityFilter.svelte b/frontend/viewer/src/lib/activity/ActivityFilter.svelte index b5445394a3..5c2698c5f6 100644 --- a/frontend/viewer/src/lib/activity/ActivityFilter.svelte +++ b/frontend/viewer/src/lib/activity/ActivityFilter.svelte @@ -111,7 +111,7 @@ {$t`No authors`} {:else if filters.authorFilterKeys.length === 1} {@const selectedAuthor = authors.current.find(a => authorFilterKey(a) === filters.authorFilterKeys[0])} - + {:else} {$t`${filters.authorFilterKeys.length} authors`} {/if} @@ -129,7 +129,7 @@ {#each authors.current as author (authorFilterKey(author))} {@const key = authorFilterKey(author)} - + ({author.commitCount}) {/each} diff --git a/frontend/viewer/src/lib/activity/AuthorLabel.svelte b/frontend/viewer/src/lib/activity/AuthorLabel.svelte index 73be811797..9e6eb43638 100644 --- a/frontend/viewer/src/lib/activity/AuthorLabel.svelte +++ b/frontend/viewer/src/lib/activity/AuthorLabel.svelte @@ -5,10 +5,9 @@ import flexLogo from '$lib/assets/flex-logo.png'; import {FIELDWORKS_AUTHOR_KEY, SYSTEM_AUTHOR_KEY, UNKNOWN_AUTHOR_KEY, authorFilterKey, wellKnownAuthorKeyToLabel} from './utils'; - let {authorId, authorName, iconClass = 'size-4', class: className}: { + let {authorId, authorName, class: className}: { authorId?: string; authorName?: string; - iconClass?: string; class?: string; } = $props(); @@ -24,8 +23,8 @@ {label} {/if} {#if key === FIELDWORKS_AUTHOR_KEY} - + {:else if key === SYSTEM_AUTHOR_KEY} - + {/if}