Skip to content

Commit 3f8a338

Browse files
authored
Add custom views (#2215)
* Implement custom views backend * Enhance example project with custom view and manager role * Implement custom views frontend * Avoid references in view objects * Show examples even if no sense fields * Update verified regression data * Use project-storage not localStorage for current view * Move frontend custom-view code into sub-folder * init project storage in demo api * Apply cursor-pointer to view labels * Reset validation error when submitting * Make custom-views lite-only feature in UI * Select view after saving
1 parent d882d99 commit 3f8a338

109 files changed

Lines changed: 4478 additions & 857 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ MiniLcmApiStringNormalizationWrapperFactory normalizationWrapperFactory
2424
{
2525
private readonly IMiniLcmApi _wrappedApi = api.WrapWith([normalizationWrapperFactory, validationWrapperFactory, notificationWrapperFactory], project);
2626

27-
public record MiniLcmFeatures(bool? History, bool? Write, bool? OpenWithFlex, bool? Feedback, bool? Sync, bool? Audio);
27+
public record MiniLcmFeatures(bool? History, bool? Write, bool? OpenWithFlex, bool? Feedback, bool? Sync, bool? Audio, bool? CustomViews);
2828
private bool SupportsSync => project.DataFormat == ProjectDataFormat.Harmony && api is CrdtMiniLcmApi;
2929
[JSInvokable]
3030
public MiniLcmFeatures SupportedFeatures()
3131
{
3232
var isCrdtProject = project.DataFormat == ProjectDataFormat.Harmony;
3333
var isFwDataProject = project.DataFormat == ProjectDataFormat.FwData;
34-
return new(History: isCrdtProject, Write: CanWrite, OpenWithFlex: isFwDataProject, Feedback: true, Sync: SupportsSync, Audio: true);
34+
return new(History: isCrdtProject, Write: CanWrite, OpenWithFlex: isFwDataProject, Feedback: true, Sync: SupportsSync, Audio: true, CustomViews: isCrdtProject);
3535
}
3636

3737
private bool CanWrite =>
@@ -82,6 +82,19 @@ public ValueTask<ComplexFormType[]> GetComplexFormTypes()
8282
return _wrappedApi.GetComplexFormTypes().ToArrayAsync();
8383
}
8484

85+
[JSInvokable]
86+
public ValueTask<CustomView[]> GetCustomViews()
87+
{
88+
return _wrappedApi.GetCustomViews().ToArrayAsync();
89+
}
90+
91+
[JSInvokable]
92+
[TsFunction(Type = "Promise<ICustomView | null>")]
93+
public Task<CustomView?> GetCustomView(Guid id)
94+
{
95+
return _wrappedApi.GetCustomView(id);
96+
}
97+
8598
[JSInvokable]
8699
[TsFunction(Type = "Promise<IComplexFormType | null>")]
87100
public Task<ComplexFormType?> GetComplexFormType(Guid id)
@@ -231,6 +244,29 @@ public async Task DeleteComplexFormType(Guid id)
231244
OnDataChanged();
232245
}
233246

247+
[JSInvokable]
248+
public async Task<CustomView> CreateCustomView(CustomView customView)
249+
{
250+
var createdCustomView = await _wrappedApi.CreateCustomView(customView);
251+
OnDataChanged();
252+
return createdCustomView;
253+
}
254+
255+
[JSInvokable]
256+
public async Task<CustomView> UpdateCustomView(CustomView customView)
257+
{
258+
var updatedCustomView = await _wrappedApi.UpdateCustomView(customView);
259+
OnDataChanged();
260+
return updatedCustomView;
261+
}
262+
263+
[JSInvokable]
264+
public async Task DeleteCustomView(Guid id)
265+
{
266+
await _wrappedApi.DeleteCustomView(id);
267+
OnDataChanged();
268+
}
269+
234270
[JSInvokable]
235271
public async Task<Entry> CreateEntry(Entry entry)
236272
{

backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder)
8787
typeof(Translation),
8888

8989
typeof(MediaFile),
90-
typeof(LcmFileMetadata)
90+
typeof(LcmFileMetadata),
91+
typeof(ViewField),
92+
typeof(ViewWritingSystem),
9193
],
9294
exportBuilder => exportBuilder.WithPublicNonStaticProperties(exportBuilder =>
9395
{
@@ -148,6 +150,7 @@ private static void ConfigureFwLiteSharedTypes(ConfigurationBuilder builder)
148150
builder.ExportAsEnum<SyncStatus>().UseString();
149151
builder.ExportAsEnum<DownloadProjectByCodeResult>().UseString();
150152
builder.ExportAsEnum<SyncJobStatusEnum>().UseString();
153+
builder.ExportAsEnum<ViewBase>().UseString();
151154
var serviceTypes = Enum.GetValues<DotnetService>()
152155
//lcm has it's own dedicated export, config is not a service just a object, and testing needs a custom export below
153156
.Where(s => s is not (DotnetService.MiniLcmApi or DotnetService.FwLiteConfig or DotnetService.TroubleshootingService))

backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.latest.verified.txt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,5 +698,55 @@
698698
"$type": "SetFirstTranslationIdChange",
699699
"TranslationId": "349419f8-65f4-1a81-42f4-b641837fa203",
700700
"EntityId": "d5ddbafb-d417-5df1-f4a4-d1224bdfac42"
701+
},
702+
{
703+
"$type": "CreateCustomViewChange",
704+
"Name": "Afghani",
705+
"Base": "FieldWorks",
706+
"EntryFields": [
707+
{
708+
"FieldId": "middleware"
709+
}
710+
],
711+
"SenseFields": [
712+
{
713+
"FieldId": "SMS"
714+
}
715+
],
716+
"ExampleFields": [],
717+
"Vernacular": [
718+
{
719+
"WsId": "txq"
720+
}
721+
],
722+
"Analysis": null,
723+
"EntityId": "935d8100-6254-f587-c712-65469362105f"
724+
},
725+
{
726+
"$type": "EditCustomViewChange",
727+
"Name": "Rial Omani",
728+
"Base": "FieldWorks",
729+
"EntryFields": [
730+
{
731+
"FieldId": "fresh-thinking"
732+
}
733+
],
734+
"SenseFields": [
735+
{
736+
"FieldId": "Auto Loan Account"
737+
}
738+
],
739+
"ExampleFields": [],
740+
"Vernacular": [
741+
{
742+
"WsId": "nrk"
743+
}
744+
],
745+
"Analysis": null,
746+
"EntityId": "99082da6-745e-9aaa-5231-3b83bd84e0e7"
747+
},
748+
{
749+
"$type": "delete:CustomView",
750+
"EntityId": "c954bd38-e4b5-ce57-0069-2a6edc91e629"
701751
}
702752
]

backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,5 +259,35 @@ private static IEnumerable<ChangeWithDependencies> GetAllChanges()
259259
yield return new ChangeWithDependencies(
260260
new RemoteResourceUploadedChange(createRemoteResourcePendingUploadChange.EntityId, "test-remote-id"),
261261
[createRemoteResourcePendingUploadChange]);
262+
263+
var customView = new CustomView
264+
{
265+
Id = Guid.NewGuid(),
266+
Name = "Test View",
267+
Base = ViewBase.FwLite,
268+
EntryFields = [new ViewField { FieldId = "lexemeForm" }],
269+
SenseFields = [],
270+
ExampleFields = [new ViewField { FieldId = "sentence" }],
271+
Vernacular = null,
272+
Analysis = [new ViewWritingSystem { WsId = "en" }],
273+
};
274+
var createCustomViewChange = new CreateCustomViewChange(
275+
customView.Id,
276+
customView
277+
);
278+
yield return new ChangeWithDependencies(createCustomViewChange);
279+
var editCustomViewChange = new EditCustomViewChange(
280+
customView.Id,
281+
customView with
282+
{
283+
Name = "Updated View",
284+
Base = ViewBase.FieldWorks,
285+
EntryFields = [new ViewField { FieldId = "citationForm" }],
286+
SenseFields = [new ViewField { FieldId = "definition" }],
287+
ExampleFields = [],
288+
Vernacular = [new ViewWritingSystem { WsId = "fr" }],
289+
Analysis = null
290+
});
291+
yield return new ChangeWithDependencies(editCustomViewChange, [createCustomViewChange]);
262292
}
263293
}

backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public class ConfigRegistrationTests
1515
typeof(ReplaceComplexFormTypeChange), //not currently in use
1616
typeof(JsonPatchChange<ComplexFormComponent>), //not supported
1717
typeof(JsonPatchChange<RemoteResource>), //not supported
18-
typeof(JsonPatchChange<ExampleSentence>)//replaced by JsonPatchExampleSentenceChange
18+
typeof(JsonPatchChange<ExampleSentence>), //replaced by JsonPatchExampleSentenceChange
19+
typeof(JsonPatchChange<CustomView>), //not supported. Use EditCustomViewChange
1920
];
2021

2122
private readonly CrdtConfig _config;

backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.latest.verified.txt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[
1+
[
22
{
33
"$type": "MiniLcmCrdtAdapter",
44
"Obj": {
@@ -1585,5 +1585,34 @@
15851585
"Id": "fce4cb31-93bd-f380-fd73-3fb0d09c0fa5",
15861586
"DeletedAt": "2025-09-17T23:53:03.6147633+02:00",
15871587
"RemoteId": "end-to-end"
1588+
},
1589+
{
1590+
"$type": "MiniLcmCrdtAdapter",
1591+
"Obj": {
1592+
"$type": "CustomView",
1593+
"Id": "72bcb2dd-4d73-486c-d5df-e5d7b9a6a3eb",
1594+
"DeletedAt": null,
1595+
"Name": "Mountain",
1596+
"Base": "FieldWorks",
1597+
"EntryFields": [
1598+
{
1599+
"FieldId": "spidy"
1600+
}
1601+
],
1602+
"SenseFields": [
1603+
{
1604+
"FieldId": "Division"
1605+
}
1606+
],
1607+
"ExampleFields": [],
1608+
"Vernacular": [
1609+
{
1610+
"WsId": "kup"
1611+
}
1612+
],
1613+
"Analysis": null
1614+
},
1615+
"Id": "72bcb2dd-4d73-486c-d5df-e5d7b9a6a3eb",
1616+
"DeletedAt": null
15881617
}
15891618
]

backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,18 @@
180180
DerivedType: CreateComplexFormType,
181181
TypeDiscriminator: CreateComplexFormType
182182
},
183+
{
184+
DerivedType: CreateCustomViewChange,
185+
TypeDiscriminator: CreateCustomViewChange
186+
},
187+
{
188+
DerivedType: EditCustomViewChange,
189+
TypeDiscriminator: EditCustomViewChange
190+
},
191+
{
192+
DerivedType: DeleteChange<CustomView>,
193+
TypeDiscriminator: delete:CustomView
194+
},
183195
{
184196
DerivedType: SetOrderChange<Sense>,
185197
TypeDiscriminator: SetOrderChange:Sense

backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,42 @@
100100
Relational:TableName: ComplexFormType
101101
Relational:ViewName:
102102
Relational:ViewSchema:
103+
EntityType: CustomView
104+
Properties:
105+
Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
106+
Analysis (ViewWritingSystem[])
107+
Annotations:
108+
Relational:ColumnType: jsonb
109+
Base (ViewBase) Required
110+
DeletedAt (DateTimeOffset?)
111+
EntryFields (ViewField[]) Required
112+
Annotations:
113+
Relational:ColumnType: jsonb
114+
ExampleFields (ViewField[]) Required
115+
Annotations:
116+
Relational:ColumnType: jsonb
117+
Name (string) Required
118+
SenseFields (ViewField[]) Required
119+
Annotations:
120+
Relational:ColumnType: jsonb
121+
SnapshotId (no field, Guid?) Shadow FK Index
122+
Vernacular (ViewWritingSystem[])
123+
Annotations:
124+
Relational:ColumnType: jsonb
125+
Keys:
126+
Id PK
127+
Foreign keys:
128+
CustomView {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull
129+
Indexes:
130+
SnapshotId Unique
131+
Annotations:
132+
DiscriminatorProperty:
133+
Relational:FunctionName:
134+
Relational:Schema:
135+
Relational:SqlQuery:
136+
Relational:TableName: CustomView
137+
Relational:ViewName:
138+
Relational:ViewSchema:
103139
EntityType: Entry
104140
Properties:
105141
Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd

backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectWithIdModels.verified.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
{
3636
DerivedType: ComplexFormComponent,
3737
TypeDiscriminator: ComplexFormComponent
38+
},
39+
{
40+
DerivedType: CustomView,
41+
TypeDiscriminator: CustomView
3842
}
3943
],
4044
IgnoreUnrecognizedTypeDiscriminators: false,

0 commit comments

Comments
 (0)