Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/FwHeadless/Services/SyncHostedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ static async Task<CrdtProject> SetupCrdtProject(string crdtFile,
Id: projectId,
Path: projectFolder,
FwProjectId: fwProjectId,
Role: UserProjectRole.Editor,
Domain: new Uri(lexboxUrl)));
}

Expand Down
37 changes: 36 additions & 1 deletion backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using FwLiteShared.Auth;
using FwLiteShared.Sync;
using LcmCrdt;
using LexCore.Entities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using MiniLcm.Models;
Expand All @@ -14,6 +15,7 @@ public record ProjectModel(
bool Crdt,
bool Fwdata,
bool Lexbox = false,
ProjectRole Role = ProjectRole.Editor,
LexboxServer? Server = null,
Guid? Id = null)
{
Expand Down Expand Up @@ -54,18 +56,31 @@ private async Task<ProjectModel[]> ServerProjects(LexboxServer server, bool forc
{
if (forceRefresh) lexboxProjectService.InvalidateProjectsCache(server);
var lexboxProjects = await lexboxProjectService.GetLexboxProjects(server);
await UpdateProjectServerInfo(lexboxProjects, await lexboxProjectService.GetLexboxUser(server));
var projectModels = lexboxProjects.Select(p => new ProjectModel(
p.Name,
p.Code,
Crdt: p.IsCrdtProject,
Fwdata: false,
Lexbox: true,
Role: p.Role,
server,
p.Id))
.ToArray();
return projectModels;
}

private async Task UpdateProjectServerInfo(FieldWorksLiteProject[] lexboxProjects, LexboxUser? lexboxUser)
{
foreach (var serverProject in lexboxProjects)
{
var localProject = crdtProjectsService.GetProject(serverProject.Id);
if (localProject?.Data is null) continue;
await crdtProjectsService.UpdateProjectServerInfo(localProject, lexboxUser?.Name, lexboxUser?.Id, ToRole(serverProject.Role));
}
}


[JSInvokable]
public async Task<ProjectModel[]> ServerProjects(string serverId, bool forceRefresh)
{
Expand All @@ -87,6 +102,7 @@ public async ValueTask<IReadOnlyCollection<ProjectModel>> LocalProjects()
true,
false,
p.Data?.OriginDomain is not null,
p.Data is null ? ProjectRole.Unknown : FromRole(p.Data.Role),
lexboxProjectService.GetServer(p.Data),
p.Data?.Id));
//basically populate projects and indicate if they are lexbox or fwdata
Expand All @@ -109,6 +125,24 @@ public async ValueTask<IReadOnlyCollection<ProjectModel>> LocalProjects()
return projects.Values;
}

private UserProjectRole ToRole(ProjectRole role) =>
role switch
{
ProjectRole.Observer => UserProjectRole.Observer,
ProjectRole.Editor => UserProjectRole.Editor,
ProjectRole.Manager => UserProjectRole.Manager,
_ => UserProjectRole.Unknown
};

private ProjectRole FromRole(UserProjectRole role) =>
role switch
{
UserProjectRole.Observer => ProjectRole.Observer,
UserProjectRole.Editor => ProjectRole.Editor,
UserProjectRole.Manager => ProjectRole.Manager,
_ => ProjectRole.Unknown
};

public async Task DownloadProject(string code, LexboxServer server)
{
var serverProjects = await ServerProjects(server, false);
Expand All @@ -133,7 +167,8 @@ await crdtProjectsService.CreateProject(new(project.Name,
},
SeedNewProjectData: false,
AuthenticatedUser: currentUser?.Name,
AuthenticatedUserId: currentUser?.Id));
AuthenticatedUserId: currentUser?.Id,
Role: ToRole(project.Role)));
}

[JSInvokable]
Expand Down
14 changes: 9 additions & 5 deletions backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using System.Net.Http.Json;
using System.Net.Http.Json;
using FwLiteShared.Auth;
using FwLiteShared.Events;
using FwLiteShared.Sync;
using LcmCrdt;
using LexCore.Entities;
using LexCore.Sync;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Caching.Memory;
Expand Down Expand Up @@ -52,8 +53,6 @@ public void Dispose()
onAuthChangedSubscription.Dispose();
}

public record LexboxProject(Guid Id, string Code, string Name, bool IsFwDataProject, bool IsCrdtProject);

public LexboxServer[] Servers()
{
return options.Value.LexboxServers;
Expand All @@ -65,7 +64,7 @@ public LexboxServer[] Servers()
return Servers().FirstOrDefault(s => s.Id == projectData.ServerId);
}

public async Task<LexboxProject[]> GetLexboxProjects(LexboxServer server)
public async Task<FieldWorksLiteProject[]> GetLexboxProjects(LexboxServer server)
{
return await cache.GetOrCreateAsync(CacheKey(server),
async entry =>
Expand All @@ -75,7 +74,7 @@ public async Task<LexboxProject[]> GetLexboxProjects(LexboxServer server)
if (httpClient is null) return [];
try
{
return await httpClient.GetFromJsonAsync<LexboxProject[]>("api/crdt/listProjects") ?? [];
return await httpClient.GetFromJsonAsync<FieldWorksLiteProject[]>("api/crdt/listProjects") ?? [];
}
catch (Exception e)
{
Expand All @@ -85,6 +84,11 @@ public async Task<LexboxProject[]> GetLexboxProjects(LexboxServer server)
}) ?? [];
}

public async Task<LexboxUser?> GetLexboxUser(LexboxServer server)
{
return await clientFactory.GetClient(server).GetCurrentUser();
}

private static string CacheKey(LexboxServer server)
{
return $"Projects|{server.Authority.Authority}";
Expand Down
6 changes: 5 additions & 1 deletion backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ public MiniLcmFeatures SupportedFeatures()
{
var isCrdtProject = project.DataFormat == ProjectDataFormat.Harmony;
var isFwDataProject = project.DataFormat == ProjectDataFormat.FwData;
return new(History: isCrdtProject, Write: true, OpenWithFlex: isFwDataProject, Feedback: true, Sync: SupportsSync);
return new(History: isCrdtProject, Write: CanWrite, OpenWithFlex: isFwDataProject, Feedback: true, Sync: SupportsSync);
}

private bool CanWrite =>
project is not CrdtProject crdt ||
(crdt.Data?.Role ?? UserProjectRole.Editor) is UserProjectRole.Editor or UserProjectRole.Manager;

//todo move info notify wrapper factory
private void OnDataChanged()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Drawing;
using System.Reflection;
using System.Text.Json.Serialization;
using System.Reflection;

Check warning on line 4 in backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

The using directive for 'System.Reflection' appeared previously in this namespace

Check warning on line 4 in backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

The using directive for 'System.Reflection' appeared previously in this namespace

Check warning on line 4 in backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

The using directive for 'System.Reflection' appeared previously in this namespace

Check warning on line 4 in backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

The using directive for 'System.Reflection' appeared previously in this namespace

Check warning on line 4 in backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

The using directive for 'System.Reflection' appeared previously in this namespace

Check warning on line 4 in backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

The using directive for 'System.Reflection' appeared previously in this namespace

Check warning on line 4 in backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Android

The using directive for 'System.Reflection' appeared previously in this namespace

Check warning on line 4 in backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

The using directive for 'System.Reflection' appeared previously in this namespace
using FwLiteShared.Auth;
using FwLiteShared.Events;
using FwLiteShared.Projects;
using FwLiteShared.Services;
using LcmCrdt;
using LexCore.Entities;
using LexCore.Sync;
using Microsoft.JSInterop;
using MiniLcm;
Expand Down Expand Up @@ -111,6 +112,8 @@
builder.ExportAsEnum<FwLitePlatform>().UseString();
builder.ExportAsEnum<ProjectSyncStatusEnum>().UseString();
builder.ExportAsEnum<ProjectDataFormat>();
builder.ExportAsEnum<UserProjectRole>().UseString();
builder.ExportAsEnum<ProjectRole>().UseString();
var serviceTypes = Enum.GetValues<DotnetService>()
//lcm has it's own dedicated export, config is not a service just a object, and testing needs a custom export below
.Where(s => s is not (DotnetService.MiniLcmApi or DotnetService.FwLiteConfig or DotnetService.TroubleshootingService))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
LastUserName (string)
Name (string) Required
OriginDomain (string)
Role (UserProjectRole) Required ValueGenerated.OnAdd
Annotations:
Relational:DefaultValue: Editor
Keys:
Id PK
Annotations:
Expand Down
15 changes: 12 additions & 3 deletions backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Data;
using System.Linq.Expressions;
using FluentValidation;
using Gridify;
Expand Down Expand Up @@ -57,6 +58,7 @@ private CommitMetadata NewMetadata()
}
private async Task<Commit> AddChange(IChange change)
{
AssertWritable();
var commit = await dataModel.AddChange(ClientId, change, commitMetadata: NewMetadata());
return commit;
}
Expand All @@ -66,12 +68,19 @@ private async Task AddChanges(IEnumerable<IChange> changes)
await AddChanges(changes.Chunk(100));
}

private void AssertWritable()
{
if (ProjectData.IsReadonly)
throw new ReadOnlyException($"project is readonly because you are logged in with the {ProjectData.Role} role. If your role recently changed, try refreshing the server project list on the home page.");
}

/// <summary>
/// use when making a large number of changes at once
/// </summary>
/// <param name="changeChunks"></param>
private async Task AddChanges(IEnumerable<IChange[]> changeChunks)
{
AssertWritable();
await using var transaction = await dbContext.Database.BeginTransactionAsync();
foreach (var chunk in changeChunks)
{
Expand Down Expand Up @@ -334,7 +343,7 @@ public async Task MoveComplexFormComponent(ComplexFormComponent component, Betwe
{
var betweenIds = new BetweenPosition(between.Previous?.Id, between.Next?.Id);
var order = await OrderPicker.PickOrder(ComplexFormComponents.Where(s => s.ComplexFormEntryId == component.ComplexFormEntryId), betweenIds);
await dataModel.AddChange(ClientId, new Changes.SetOrderChange<ComplexFormComponent>(component.Id, order));
await AddChange(new Changes.SetOrderChange<ComplexFormComponent>(component.Id, order));
}

public async Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent)
Expand Down Expand Up @@ -620,7 +629,7 @@ public async Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition?
throw new InvalidOperationException($"Part of speech must exist when creating a sense (could not find GUID {sense.PartOfSpeechId.Value})");

sense.Order = await OrderPicker.PickOrder(Senses.Where(s => s.EntryId == entryId), between);
await dataModel.AddChanges(ClientId, await CreateSenseChanges(entryId, sense).ToArrayAsync());
await AddChanges(await CreateSenseChanges(entryId, sense).ToArrayAsync());
return await GetSense(entryId, sense.Id) ?? throw new NullReferenceException("unable to find sense " + sense.Id);
}

Expand All @@ -630,7 +639,7 @@ public async Task<Sense> UpdateSense(Guid entryId,
{
var sense = await GetSense(entryId, senseId);
if (sense is null) throw new NullReferenceException($"unable to find sense with id {senseId}");
await dataModel.AddChanges(ClientId, [..sense.ToChanges(update.Patch)]);
await AddChanges([..sense.ToChanges(update.Patch)]);
return await GetSense(entryId, senseId) ?? throw new NullReferenceException("unable to find sense with id " + senseId);
}

Expand Down
12 changes: 11 additions & 1 deletion backend/FwLite/LcmCrdt/CrdtProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,22 @@ public CrdtProject(string code, string dbPath, IMemoryCache memoryCache) : this(
/// <param name="OriginDomain">Server to sync with, null if not synced</param>
/// <param name="ClientId">Unique id for this client machine</param>
/// <param name="FwProjectId">FieldWorks project id, aka LangProjectId</param>
public record ProjectData(string Name, string Code, Guid Id, string? OriginDomain, Guid ClientId, Guid? FwProjectId = null, string? LastUserName = null, string? LastUserId = null)
public record ProjectData(string Name, string Code, Guid Id, string? OriginDomain, Guid ClientId, Guid? FwProjectId = null, string? LastUserName = null, string? LastUserId = null,
UserProjectRole Role = UserProjectRole.Unknown)
{
public static string? GetOriginDomain(Uri? uri)
{
return uri?.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
}

public string? ServerId => OriginDomain is not null ? new Uri(OriginDomain).Authority : null;
public bool IsReadonly => Role is not UserProjectRole.Editor and not UserProjectRole.Manager;
}

public enum UserProjectRole
{
Unknown,
Manager,
Editor,
Observer
}
20 changes: 18 additions & 2 deletions backend/FwLite/LcmCrdt/CrdtProjectsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ public async ValueTask EnsureProjectDataCacheIsLoaded()
await Task.WhenAll(tasks);
}

public async ValueTask UpdateProjectServerInfo(CrdtProject project,
string? userName,
string? userId,
UserProjectRole role)
{
if (project.Data?.LastUserName == userName && project.Data?.LastUserId == userId && project.Data?.Role == role) return;
await using var scope = provider.CreateAsyncScope();
var scopedServices = scope.ServiceProvider;
var currentProjectService = scopedServices.GetRequiredService<CurrentProjectService>();
await currentProjectService.SetupProjectContext(project);

await currentProjectService.UpdateLastUser(userName, userId);
await currentProjectService.UpdateUserRole(role);
}

public IEnumerable<CrdtProject> ListProjects()
{
return Directory.EnumerateFiles(config.Value.ProjectPath, "*.sqlite").Select(file =>
Expand Down Expand Up @@ -82,7 +97,8 @@ public record CreateProjectRequest(
string? Path = null,
Guid? FwProjectId = null,
string? AuthenticatedUser = null,
string? AuthenticatedUserId = null);
string? AuthenticatedUserId = null,
UserProjectRole? Role = null);

public async Task<CrdtProject> CreateExampleProject(string name)
{
Expand Down Expand Up @@ -121,7 +137,7 @@ public async Task<CrdtProject> CreateProject(CreateProjectRequest request)
code,
request.Id ?? Guid.NewGuid(),
ProjectData.GetOriginDomain(request.Domain),
Guid.NewGuid(), request.FwProjectId, request.AuthenticatedUser, request.AuthenticatedUserId);
Guid.NewGuid(), request.FwProjectId, request.AuthenticatedUser, request.AuthenticatedUserId, request.Role ?? UserProjectRole.Editor);
crdtProject.Data = projectData;
await InitProjectDb(db, projectData);
await currentProjectService.RefreshProjectData();
Expand Down
8 changes: 8 additions & 0 deletions backend/FwLite/LcmCrdt/CurrentProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,12 @@ await DbContext.ProjectData.ExecuteUpdateAsync(calls => calls
await RefreshProjectData();
}
}

public async Task UpdateUserRole(UserProjectRole role)
{
if (ProjectData.Role == role) return;
await DbContext.ProjectData.ExecuteUpdateAsync(calls => calls
.SetProperty(p => p.Role, role));
await RefreshProjectData();
}
}
2 changes: 2 additions & 0 deletions backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
var projectDataModel = modelBuilder.Entity<ProjectData>();
projectDataModel.HasKey(p => p.Id);
projectDataModel.Ignore(p => p.ServerId);
//setting default value to handle migration
projectDataModel.Property(p => p.Role).HasConversion<EnumToStringConverter<UserProjectRole>>().HasDefaultValue(UserProjectRole.Editor);
}

protected override void ConfigureConventions(ModelConfigurationBuilder builder)
Expand Down
Loading
Loading