Skip to content
Draft
Show file tree
Hide file tree
Changes from 14 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
40 changes: 38 additions & 2 deletions backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ MiniLcmApiUserFacingWrappers userFacingWrappers
{
private readonly IMiniLcmApi _wrappedApi = userFacingWrappers.Apply(api, project, notificationWrapperFactory);

public record MiniLcmFeatures(bool? History, bool? Write, bool? OpenWithFlex, bool? Feedback, bool? Sync, bool? Audio, bool? CustomViews, bool? Comments);
public record MiniLcmFeatures(bool? History, bool? Write, bool? OpenWithFlex, bool? Feedback, bool? Sync, bool? Audio, bool? CustomViews, bool? Comments, bool? Plugins);
private bool SupportsSync => project.DataFormat == ProjectDataFormat.Harmony && api is CrdtMiniLcmApi;
[JSInvokable]
public MiniLcmFeatures SupportedFeatures()
{
var isCrdtProject = project.DataFormat == ProjectDataFormat.Harmony;
var isFwDataProject = project.DataFormat == ProjectDataFormat.FwData;
return new(History: isCrdtProject, Write: CanWrite, OpenWithFlex: isFwDataProject, Feedback: true, Sync: SupportsSync, Audio: true, CustomViews: isCrdtProject, Comments: isCrdtProject);
return new(History: isCrdtProject, Write: CanWrite, OpenWithFlex: isFwDataProject, Feedback: true, Sync: SupportsSync, Audio: true, CustomViews: isCrdtProject, Comments: isCrdtProject, Plugins: isCrdtProject);
}

private bool CanWrite =>
Expand Down Expand Up @@ -270,6 +270,42 @@ public async Task DeleteCustomView(Guid id)
OnDataChanged();
}

[JSInvokable]
public ValueTask<Plugin[]> GetPlugins()
{
return _wrappedApi.GetPlugins().ToArrayAsync();
}

[JSInvokable]
[TsFunction(Type = "Promise<IPlugin | null>")]
public Task<Plugin?> GetPlugin(Guid id)
{
return _wrappedApi.GetPlugin(id);
}

[JSInvokable]
public async Task<Plugin> CreatePlugin(Plugin plugin)
{
var createdPlugin = await _wrappedApi.CreatePlugin(plugin);
OnDataChanged();
return createdPlugin;
}

[JSInvokable]
public async Task<Plugin> UpdatePlugin(Plugin plugin)
{
var updatedPlugin = await _wrappedApi.UpdatePlugin(plugin);
OnDataChanged();
return updatedPlugin;
}

[JSInvokable]
public async Task DeletePlugin(Guid id)
{
await _wrappedApi.DeletePlugin(id);
OnDataChanged();
}

[JSInvokable]
public ValueTask<CommentThread[]> GetCommentThreads(SubjectType subjectType, Guid subjectId, bool includeComments = false)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,43 @@
{
"$type": "delete:UserComment",
"EntityId": "4be242d0-b1a6-986f-cc0d-b35860e69e60"
},
{
"$type": "CreatePluginChange",
"Name": "South Dakota",
"Description": "cultivate",
"FileUri": "sil-media://not-found/00000000-0000-0000-0000-000000000000",
"FileSize": 7006730054478530864,
"Permissions": [
"artificial intelligence"
],
"Contexts": [
"Developer"
],
"Requires": [
"interactive"
],
"EntityId": "0bb97e98-8ce9-e3a6-cdf1-1425a2780347"
},
{
"$type": "EditPluginChange",
"Name": "Investment Account",
"Description": "Gorgeous Granite Computer",
"FileUri": "sil-media://not-found/00000000-0000-0000-0000-000000000000",
"FileSize": 6231492617012874517,
"Permissions": [
"innovate"
],
"Contexts": [
"fresh-thinking"
],
"Requires": [
"maximized"
],
"EntityId": "d4d44566-77fa-3c2e-5cf0-7cd31b69b00f"
},
{
"$type": "delete:Plugin",
"EntityId": "c558fd49-9bd3-759c-d342-4e1c86a0d647"
}
]
23 changes: 23 additions & 0 deletions backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using LcmCrdt.Changes.CustomJsonPatches;
using LcmCrdt.Changes.Entries;
using LcmCrdt.Changes.ExampleSentences;
using MiniLcm.Media;
using MiniLcm.SyncHelpers;
using SIL.Harmony.Changes;
using SIL.Harmony.Resource;
Expand Down Expand Up @@ -318,6 +319,28 @@ customView with
});
yield return new ChangeWithDependencies(editCustomViewChange, [createCustomViewChange]);

var plugin = new Plugin
{
Id = Guid.NewGuid(),
Name = "Test Plugin",
FileUri = new MediaUri(Guid.NewGuid(), "test.lexbox.org"),
FileSize = 42,
Permissions = ["edit"],
Contexts = ["entry"],
Requires = ["history"],
};
var createPluginChange = new CreatePluginChange(plugin.Id, plugin);
yield return new ChangeWithDependencies(createPluginChange);
var editPluginChange = new EditPluginChange(
plugin.Id,
plugin with
{
Name = "Updated Plugin",
FileUri = new MediaUri(Guid.NewGuid(), "test.lexbox.org"),
FileSize = 43,
});
yield return new ChangeWithDependencies(editPluginChange, [createPluginChange]);

var commentThread = new CommentThread
{
Id = Guid.NewGuid(),
Expand Down
1 change: 1 addition & 0 deletions backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class ConfigRegistrationTests
typeof(JsonPatchChange<RemoteResource>), //not supported
typeof(JsonPatchChange<ExampleSentence>), //replaced by JsonPatchExampleSentenceChange
typeof(JsonPatchChange<CustomView>), //not supported. Use EditCustomViewChange
typeof(JsonPatchChange<Plugin>), //not supported. Use EditPluginChange
typeof(JsonPatchChange<CommentThread>), //not supported. Use SetCommentThreadStatusChange
typeof(JsonPatchChange<UserComment>), //not supported. Use EditUserCommentChange
typeof(DeleteChange<MorphType>), //MorphTypes cannot be deleted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4089,5 +4089,28 @@
},
"Id": "4f5808a8-bbf1-e23d-e5fe-5a997f13bf3a",
"DeletedAt": null
},
{
"$type": "MiniLcmCrdtAdapter",
"Obj": {
"$type": "Plugin",
"Id": "d7147f14-258d-ccda-9165-a12e38c1e65e",
"DeletedAt": null,
"Name": "hard drive",
"Description": "Director",
"FileUri": "sil-media://not-found/00000000-0000-0000-0000-000000000000",
"FileSize": 1690216227413767371,
"Permissions": [
"array"
],
"Contexts": [
"View"
],
"Requires": [
"convergence"
]
},
"Id": "d7147f14-258d-ccda-9165-a12e38c1e65e",
"DeletedAt": null
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,18 @@
DerivedType: DeleteChange<CustomView>,
TypeDiscriminator: delete:CustomView
},
{
DerivedType: CreatePluginChange,
TypeDiscriminator: CreatePluginChange
},
{
DerivedType: EditPluginChange,
TypeDiscriminator: EditPluginChange
},
{
DerivedType: DeleteChange<Plugin>,
TypeDiscriminator: delete:Plugin
},
{
DerivedType: CreateCommentThreadChange,
TypeDiscriminator: CreateCommentThreadChange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,37 @@
Relational:TableName: PartOfSpeech
Relational:ViewName:
Relational:ViewSchema:
EntityType: Plugin
Properties:
Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
Contexts (string[]) Required
Annotations:
Relational:ColumnType: jsonb
DeletedAt (DateTimeOffset?)
Description (string)
FileSize (long) Required
FileUri (MediaUri) Required
Name (string) Required
Permissions (string[]) Required
Annotations:
Relational:ColumnType: jsonb
Requires (string[]) Required
Annotations:
Relational:ColumnType: jsonb
SnapshotId (no field, Guid?) Shadow FK Index
Keys:
Id PK
Foreign keys:
Plugin {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull
Indexes:
SnapshotId Unique
Annotations:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
Relational:TableName: Plugin
Relational:ViewName:
Relational:ViewSchema:
EntityType: Publication
Properties:
Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
DerivedType: CustomView,
TypeDiscriminator: CustomView
},
{
DerivedType: Plugin,
TypeDiscriminator: Plugin
},
{
DerivedType: CommentThread,
TypeDiscriminator: CommentThread
Expand Down
47 changes: 47 additions & 0 deletions backend/FwLite/LcmCrdt.Tests/MiniLcmTests/MediaSubfolderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Text;
using LcmCrdt.MediaServer;
using Microsoft.Extensions.DependencyInjection;
using MiniLcm.Media;

namespace LcmCrdt.Tests.MiniLcmTests;

public class MediaSubfolderTests(MiniLcmApiFixture fixture) : IClassFixture<MiniLcmApiFixture>
{
private LcmMediaService MediaService => fixture.GetService<LcmMediaService>();

private static MemoryStream Bytes(string content) => new(Encoding.UTF8.GetBytes(content));

[Fact]
public async Task SaveFile_PlacesTheLocalCopyInTheRequestedSubfolder()
{
var (resource, newResource) = await MediaService.SaveFile(Bytes("<html></html>"),
new LcmFileMetadata("plugin-abc123.html", "text/html", LinkedFilesSubfolder: "Plugins"));

newResource.Should().BeTrue();
resource.LocalPath.Should().Be(
Path.Combine(MediaService.ProjectResourceCachePath, "Plugins", "plugin-abc123.html"));
File.Exists(resource.LocalPath).Should().BeTrue();
}

[Fact]
public async Task SaveFile_WithoutSubfolderStaysAtTheCacheRoot()
{
var (resource, _) = await MediaService.SaveFile(Bytes("audio"),
new LcmFileMetadata("word.wav", "audio/wav"));

resource.LocalPath.Should().Be(Path.Combine(MediaService.ProjectResourceCachePath, "word.wav"));
}

[Theory]
[InlineData("../escape")]
[InlineData("a/b")]
[InlineData("a\\b")]
[InlineData("spaced folder")]
public async Task SaveFile_RejectsSubfoldersThatAreNotASingleSafeSegment(string subfolder)
{
var act = () => MediaService.SaveFile(Bytes("x"),
new LcmFileMetadata("file.html", "text/html", LinkedFilesSubfolder: subfolder));

await act.Should().ThrowAsync<ArgumentException>();
}
}
Loading
Loading