diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 000000000..8e3aae52b --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "meshweaver.azure.publish": { + "version": "1.0.0", + "commands": [ + "MeshWeaver.Azure.Publish" + ] + } + } +} \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index f4423de37..6a70d02f2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,6 +16,8 @@ + + @@ -32,11 +34,16 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + + diff --git a/MeshWeaver.sln b/MeshWeaver.sln index 43cbe1b2e..d2738355d 100644 --- a/MeshWeaver.sln +++ b/MeshWeaver.sln @@ -207,8 +207,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "monolith", "monolith", "{4E1A12BB-5348-4DA4-B05F-F831DC16F7DE}" ProjectSection(SolutionItems) = preProject monolith\Directory.Build.props = monolith\Directory.Build.props - Readme.md = Readme.md MIT License.md = MIT License.md + Readme.md = Readme.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshWeaver.Portal", "monolith\MeshWeaver.Portal\MeshWeaver.Portal.csproj", "{1471FB82-912E-411A-9141-096E0AECAAAE}" @@ -221,6 +221,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshWeaver.Search", "src\Me EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshWeaver.Search.Test", "test\MeshWeaver.Search.Test\MeshWeaver.Search.Test.csproj", "{BDEC6D93-F212-456F-BEEE-DE2BADAE2A56}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshWeaver.Azure.Publish", "src\MeshWeaver.Azure.Publish\MeshWeaver.Azure.Publish.csproj", "{844B4566-81A4-4CB2-9761-9B174FB7BCCB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -587,6 +589,10 @@ Global {BDEC6D93-F212-456F-BEEE-DE2BADAE2A56}.Debug|Any CPU.Build.0 = Debug|Any CPU {BDEC6D93-F212-456F-BEEE-DE2BADAE2A56}.Release|Any CPU.ActiveCfg = Release|Any CPU {BDEC6D93-F212-456F-BEEE-DE2BADAE2A56}.Release|Any CPU.Build.0 = Release|Any CPU + {844B4566-81A4-4CB2-9761-9B174FB7BCCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {844B4566-81A4-4CB2-9761-9B174FB7BCCB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {844B4566-81A4-4CB2-9761-9B174FB7BCCB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {844B4566-81A4-4CB2-9761-9B174FB7BCCB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/modules/Northwind/MeshWeaver.Northwind.ViewModel/AnnualReportExtensions.cs b/modules/Northwind/MeshWeaver.Northwind.ViewModel/AnnualReportExtensions.cs index ae649137d..4e420c5a1 100644 --- a/modules/Northwind/MeshWeaver.Northwind.ViewModel/AnnualReportExtensions.cs +++ b/modules/Northwind/MeshWeaver.Northwind.ViewModel/AnnualReportExtensions.cs @@ -1,5 +1,6 @@ using MeshWeaver.Layout; using MeshWeaver.Layout.Composition; +using System; namespace MeshWeaver.Northwind.ViewModel { diff --git a/modules/Northwind/MeshWeaver.Northwind.ViewModel/MeshWeaver.Northwind.ViewModel.csproj b/modules/Northwind/MeshWeaver.Northwind.ViewModel/MeshWeaver.Northwind.ViewModel.csproj index 1c2e39df5..928174865 100644 --- a/modules/Northwind/MeshWeaver.Northwind.ViewModel/MeshWeaver.Northwind.ViewModel.csproj +++ b/modules/Northwind/MeshWeaver.Northwind.ViewModel/MeshWeaver.Northwind.ViewModel.csproj @@ -1,32 +1,48 @@  + net8.0 true true - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + true bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml true + $(AssemblyName).xml @@ -34,5 +50,4 @@ true - - + \ No newline at end of file diff --git a/modules/Northwind/MeshWeaver.Northwind.ViewModel/OrdersSummaryArea.cs b/modules/Northwind/MeshWeaver.Northwind.ViewModel/OrdersSummaryArea.cs index 7fe72bfb1..deb73910a 100644 --- a/modules/Northwind/MeshWeaver.Northwind.ViewModel/OrdersSummaryArea.cs +++ b/modules/Northwind/MeshWeaver.Northwind.ViewModel/OrdersSummaryArea.cs @@ -1,7 +1,6 @@ using System.Reactive.Linq; using MeshWeaver.Application.Styles; using MeshWeaver.Data; -using MeshWeaver.Domain.Layout; using MeshWeaver.Domain.Layout.Documentation; using MeshWeaver.Layout; using MeshWeaver.Layout.Composition; @@ -48,7 +47,8 @@ RenderingContext ctx ) { var years = layoutArea - .Workspace.GetObservable() + .Workspace + .GetObservable() .DistinctUntilChanged() .Select(x => x.Select(y => y.OrderDate.Year) diff --git a/modules/Northwind/MeshWeaver.Northwind.ViewModel/ProductOverviewItem.cs b/modules/Northwind/MeshWeaver.Northwind.ViewModel/ProductOverviewItem.cs index 3dbee581f..8a43aac7e 100644 --- a/modules/Northwind/MeshWeaver.Northwind.ViewModel/ProductOverviewItem.cs +++ b/modules/Northwind/MeshWeaver.Northwind.ViewModel/ProductOverviewItem.cs @@ -44,5 +44,5 @@ public record ProductOverviewItem /// Gets the total amount. /// [DisplayFormat(DataFormatString = "N2")] - public double TotalAmount { get; init; } + public double TotalAmount { get; init; } } diff --git a/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.csproj b/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.csproj new file mode 100644 index 000000000..b1ae36a0f --- /dev/null +++ b/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.csproj @@ -0,0 +1,22 @@ + + + Exe + true + + + + + + + + + + + + + diff --git a/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.runtimeconfig.json b/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.runtimeconfig.json new file mode 100644 index 000000000..170449ccb --- /dev/null +++ b/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.runtimeconfig.json @@ -0,0 +1,9 @@ +{ + "runtimeOptions": { + "tfm": "net8.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "8.0.0" + } + } +} diff --git a/src/MeshWeaver.Azure.Publish/Program.cs b/src/MeshWeaver.Azure.Publish/Program.cs new file mode 100644 index 000000000..620551468 --- /dev/null +++ b/src/MeshWeaver.Azure.Publish/Program.cs @@ -0,0 +1,159 @@ +using System.CommandLine; +using System.CommandLine.NamingConventionBinder; +using System.Text.Json; +using Azure.Storage.Blobs; +using MeshWeaver.Search; +using Microsoft.Extensions.Logging; + +namespace MeshWeaver.Azure.Publish +{ + public static class Program + { + public static async Task Main(string[] args) + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }); + var logger = loggerFactory.CreateLogger(typeof(Program)); + + var rootCommand = new RootCommand + { + new Option( + "--path", + "The input files to process"), + new Option( + "--connection-string", + "The connection string for the blob storage"), + new Option( + "--container", + "The name of the blob container"), + new Option( + "--html-container", + "The name of the blob container containing pre-rendered html"), + new Option( + "--address", + "The address to use") + }; + + rootCommand.Description = "MeshWeaver Build Tasks"; + + rootCommand.Handler = CommandHandler.Create(async (path, connectionString, container, htmlContainer, address) => + { + if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(connectionString) || string.IsNullOrEmpty(container) || string.IsNullOrEmpty(address)) + { + logger.LogError("Error: All options (--path, --connection-string, --container, --address) must be provided and cannot be null or empty."); + return; + } + + // Call your custom task logic here + logger.LogInformation("Processing files from\npath: {path}\nstorage: {connectionString}\ncontainer: {container}\nhtmlContainer: {htmlContainer}\naddress: {address}", path, connectionString, container, htmlContainer, address); + await ExecuteAsync(connectionString, container, htmlContainer, path, address, logger); + }); + + await rootCommand.InvokeAsync(args); + } + + public static async Task ExecuteAsync(string connectionString, string container, string htmlContainer, string path, string address, ILogger logger) + { + try + { + var blobServiceClient = new BlobServiceClient(connectionString); + var blobContainer = blobServiceClient.GetBlobContainerClient(container); + var htmlBlobContainer = htmlContainer == null ? null : blobServiceClient.GetBlobContainerClient(container); + + // Create the container if it doesn't exist + await blobContainer.CreateIfNotExistsAsync(); + if(htmlContainer != null) + await htmlBlobContainer.CreateIfNotExistsAsync(); + + + var localFiles = Directory.GetFiles(path, "*", SearchOption.AllDirectories).Select(f => Path.Combine(address, Path.GetRelativePath(path, f))).ToHashSet(); + var blobs = blobContainer.GetBlobs(prefix:address).ToDictionary(b => b.Name); + + // Delete blobs that no longer exist in the local directory + foreach (var blob in blobs) + { + if (!localFiles.Contains(blob.Key)) + { + logger.LogInformation($"Deleting blob {blob.Key} as it no longer exists in the local directory."); + await blobContainer.DeleteBlobAsync(blob.Key); + } + } + + foreach (var inputFile in Directory.GetFiles(path)) + { + try + { + var filePath = Path.Combine(address, Path.GetRelativePath(path, inputFile)).Replace('\\', '/'); + var fileContent = await File.ReadAllTextAsync(inputFile); + var fileLastModified = File.GetLastWriteTimeUtc(inputFile); + + // Check if the blob exists and if the local file is newer + if (blobs.TryGetValue(filePath, out var blobItem) && blobItem.Properties.LastModified >= fileLastModified) + { + logger.LogInformation($"Skipping upload for {filePath} as the blob is up-to-date."); + continue; + } + + var extension = Path.GetExtension(filePath); + logger.LogInformation($"Uploading file {inputFile} to {filePath}. Parsing as {extension}"); + + var metadata = extension switch + { + ".md" => await ParseMarkdown(address, logger, filePath, fileContent, htmlBlobContainer), + _ => DefaultMetadata(filePath, extension.Trim('.')) + }; + + logger.LogInformation("Uploading {filePath} with {metadata}", filePath, JsonSerializer.Serialize(metadata)); + var blob = blobContainer.GetBlobClient(filePath); + await blob.UploadAsync(new BinaryData(fileContent), overwrite: true); + await blob.SetMetadataAsync(metadata); + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred during file upload."); + } + } + + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred during execution."); + return false; + } + } + + private static Dictionary DefaultMetadata(string filePath, string type) + { + return new Dictionary + { + { "type", type }, + { "path", filePath } + }; +} + + private static async Task> ParseMarkdown(string address, ILogger logger, string filePath, string fileContent, BlobContainerClient htmlContainer) + { + // Parse metadata and HTML + var (article, html) = MarkdownIndexer.ParseArticle(filePath, fileContent, address); + + // Set metadata on the HTML blob + var metadata = article?.ToMetadata(filePath, "md") ?? DefaultMetadata(filePath, "md"); + + if (htmlContainer != null) + { + // Store metadata and HTML in blob storage + var htmlFilePath = Path.ChangeExtension(filePath, ".html"); + logger.LogInformation("Uploading pre-rendered html {path}", htmlFilePath); + var htmlBlobClient = htmlContainer.GetBlobClient(htmlFilePath); + await htmlBlobClient.UploadAsync(new BinaryData(html), overwrite: true); + await htmlBlobClient.SetMetadataAsync(metadata); + + } + + return metadata; + } + } +} diff --git a/src/MeshWeaver.Blazor/BlazorClientRegistry.cs b/src/MeshWeaver.Blazor/BlazorClientRegistry.cs index cea78bf6c..666722f7d 100644 --- a/src/MeshWeaver.Blazor/BlazorClientRegistry.cs +++ b/src/MeshWeaver.Blazor/BlazorClientRegistry.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Runtime.CompilerServices; using System.Text.Json; +using MeshWeaver.Application; using MeshWeaver.Data; using Microsoft.DotNet.Interactive.Formatting; using MeshWeaver.Data.Serialization; @@ -23,7 +24,7 @@ internal static MessageHubConfiguration AddBlazor( ) => config .AddData() .AddLayoutClient(c => (configuration ?? (x => x)).Invoke(c.WithView(DefaultFormatting))) - + .WithTypes(typeof(ApplicationAddress)) ; #region Standard Formatting private static ViewDescriptor DefaultFormatting( diff --git a/src/MeshWeaver.Data.Contract/EntityStore.cs b/src/MeshWeaver.Data.Contract/EntityStore.cs index b0ee5e3c0..f39b855d0 100644 --- a/src/MeshWeaver.Data.Contract/EntityStore.cs +++ b/src/MeshWeaver.Data.Contract/EntityStore.cs @@ -147,8 +147,15 @@ public override int GetHashCode() => Collections.Values .Select(x => x.GetHashCode()) .Aggregate((x, y) => x ^ y); + +} + +public record EntityStoreAndUpdates(IEnumerable Changes, EntityStore Store) +{ + public EntityStoreAndUpdates(EntityStore Store) : this([], Store) + { + } } -public record EntityStoreAndUpdates(IEnumerable Changes, EntityStore Store); public record EntityStoreUpdate(string Collection, object Id, object Value) { diff --git a/src/MeshWeaver.Data/Serialization/SynchronizationStream.cs b/src/MeshWeaver.Data/Serialization/SynchronizationStream.cs index d1c0e73f3..f18cbad6f 100644 --- a/src/MeshWeaver.Data/Serialization/SynchronizationStream.cs +++ b/src/MeshWeaver.Data/Serialization/SynchronizationStream.cs @@ -146,6 +146,7 @@ public virtual void Initialize(ChangeItem initial) current = initial ?? throw new ArgumentNullException(nameof(initial)); Store.OnNext(initial); initialized.SetResult(current.Value); + deferral.Dispose(); } public void OnCompleted() @@ -181,8 +182,13 @@ IMessageDelivery delivery public void Update(Func> update) => InvokeAsync(() => SetCurrent(update.Invoke(Current is null ? default : Current.Value))); - public void OnNext(ChangeItem value) => - InvokeAsync(() => SetCurrent(value)); + public void OnNext(ChangeItem value) + { + if(!IsInitialized) + Initialize(value); + else + InvokeAsync(() => SetCurrent(value)); + } public virtual DataChangeResponse RequestChange(Func> update) { @@ -207,6 +213,7 @@ public SynchronizationStream(object Owner, this.Reference = Reference; this.InitializationMode = InitializationMode; synchronizationStreamHub = Hub.GetHostedHub(new SynchronizationStreamAddress(Hub.Address)); + deferral = synchronizationStreamHub.Defer(_ => true); } @@ -226,6 +233,7 @@ private record SynchronizationStreamAddress(object Host) : IHostedAddress } private readonly IMessageHub synchronizationStreamHub; + private readonly IDisposable deferral; //private void InvokeAsync(Func task) // => synchronizationStreamHub.InvokeAsync(task); diff --git a/src/MeshWeaver.Data/Workspace.cs b/src/MeshWeaver.Data/Workspace.cs index 9ccd9a1d8..76cf832ae 100644 --- a/src/MeshWeaver.Data/Workspace.cs +++ b/src/MeshWeaver.Data/Workspace.cs @@ -3,6 +3,7 @@ using System.Reactive.Linq; using System.Reflection; using System.Text.Json; +using System.Text.Json.Nodes; using Microsoft.Extensions.Logging; using MeshWeaver.Activities; using MeshWeaver.Data.Serialization; @@ -210,7 +211,8 @@ TReference reference where TReference : WorkspaceReference { // link to deserialized world. Will also potentially link to workspace. - + if(owner is JsonObject obj) + owner = obj.Deserialize(Hub.JsonSerializerOptions); var ret = new SynchronizationStream( owner, owner, diff --git a/src/MeshWeaver.Documentation/Documentation/DocumentationLayout.cs b/src/MeshWeaver.Documentation/Documentation/DocumentationLayout.cs index 618456504..1716a735a 100644 --- a/src/MeshWeaver.Documentation/Documentation/DocumentationLayout.cs +++ b/src/MeshWeaver.Documentation/Documentation/DocumentationLayout.cs @@ -106,8 +106,7 @@ public static LayoutAreaReference GetLayoutAreaReferenceForEmbeddedResource(this private const string ReadPattern = @"^(?[^@]+)/(?[^@]+)/(?[^@]+)$"; - public static async Task Doc(LayoutAreaHost area, RenderingContext context, - CancellationToken cancellationToken) + public static async Task Doc(LayoutAreaHost area, RenderingContext context, CancellationToken cancellationToken) { if (area.Stream.Reference.Id is not string path) throw new InvalidOperationException("No file name specified."); @@ -127,7 +126,7 @@ public static async Task Doc(LayoutAreaHost area, RenderingContext conte { var extension = documentId.Split('.').Last(); using var reader = new StreamReader(stream); - return new MarkdownControl(Format(await reader.ReadToEndAsync(cancellationToken), extension)); + return new MarkdownControl(Format(await reader.ReadToEndAsync(), extension)); } // Resource not found, return a warning control/message instead diff --git a/src/MeshWeaver.Documentation/DomainViews.cs b/src/MeshWeaver.Documentation/DomainViews.cs index 87a7c4251..8c7991428 100644 --- a/src/MeshWeaver.Documentation/DomainViews.cs +++ b/src/MeshWeaver.Documentation/DomainViews.cs @@ -40,7 +40,8 @@ public static object Details(LayoutAreaHost area, RenderingContext ctx) { var typeDefinition = typeSource.TypeDefinition; var idString = parts[1]; - var id = JsonSerializer.Deserialize(idString, typeDefinition.GetKeyType()); + var keyType = typeDefinition.GetKeyType(); + var id = keyType == typeof(string) ? idString : JsonSerializer.Deserialize(idString, keyType); return area.Hub.ServiceProvider.GetRequiredService().Render(new(area, typeDefinition, idString, id, ctx)); } catch (Exception e) diff --git a/src/MeshWeaver.Documentation/IDomainLayoutService.cs b/src/MeshWeaver.Documentation/IDomainLayoutService.cs index eb0d9b829..a46220515 100644 --- a/src/MeshWeaver.Documentation/IDomainLayoutService.cs +++ b/src/MeshWeaver.Documentation/IDomainLayoutService.cs @@ -97,11 +97,13 @@ public DomainViewConfiguration WithPropertyView(Func context.TypeDefinition.Type.GetProperties() .Aggregate(Controls.EditForm, (grid, property) => @@ -111,31 +113,7 @@ public object DetailsLayout(LayoutAreaHost host, RenderingContext ctx, EntityRen ) ); - var stream = host.Workspace - .GetStreamFor(new EntityReference(context.TypeDefinition.CollectionName, context.Id), host.Stream.Subscriber); - object current = null; - var forwardSubscription = stream.Subscribe(changeItem => - { - if (Equals(changeItem.Value, current)) - return; - current = changeItem.Value; - host.Stream.SetData(context.IdString, changeItem.SetValue(current)); - }); - //var subscription = host.Stream.Subscribe(changeItem => - //{ - // if(changeItem.Patch?.Value is null) - // return; - // if (changeItem.ChangedBy.Equals(host.Stream.Subscriber) &&changeItem.Patch.Value.Operations.Any(x => x.Path.ToString().StartsWith(ret.DataContext))) - // { - // var instance = changeItem.Value.GetCollection(LayoutAreaReference.Data)?.Instances - // .GetValueOrDefault(context.IdString); - // if(instance is not null) - // stream.Update(i => changeItem.SetValue(instance)); - // } - //}); - - host.AddDisposable(context.RenderingContext.Area, forwardSubscription); return ret; } diff --git a/src/MeshWeaver.Layout/Composition/LayoutAreaHost.cs b/src/MeshWeaver.Layout/Composition/LayoutAreaHost.cs index 949a51e0f..8b2cad450 100644 --- a/src/MeshWeaver.Layout/Composition/LayoutAreaHost.cs +++ b/src/MeshWeaver.Layout/Composition/LayoutAreaHost.cs @@ -254,8 +254,7 @@ private EntityStoreAndUpdates DisposeExistingAreas(EntityStore store, RenderingC } - internal EntityStoreAndUpdates - RenderArea(RenderingContext context, ViewStream generator, EntityStore store) + internal EntityStoreAndUpdates RenderArea(RenderingContext context, ViewStream generator, EntityStore store) { var ret = DisposeExistingAreas(store, context); AddDisposable(context.Parent?.Area ?? context.Area, @@ -281,8 +280,10 @@ internal EntityStoreAndUpdates RenderArea(RenderingContext context, ViewDefiniti }); return DisposeExistingAreas(store, context); } - internal EntityStoreAndUpdates RenderArea(RenderingContext context, - IObservable generator, EntityStore store) + internal EntityStoreAndUpdates RenderArea( + RenderingContext context, + IObservable generator, + EntityStore store) { AddDisposable(context.Area, generator.Subscribe(vd => InvokeAsync(async ct => diff --git a/src/MeshWeaver.Layout/LayoutDefinitionExtensions.cs b/src/MeshWeaver.Layout/LayoutDefinitionExtensions.cs index c21182a29..65cf745c8 100644 --- a/src/MeshWeaver.Layout/LayoutDefinitionExtensions.cs +++ b/src/MeshWeaver.Layout/LayoutDefinitionExtensions.cs @@ -148,7 +148,7 @@ Func view public static EntityStoreAndUpdates UpdateControl(this EntityStore store, string id, UiControl control) => new ([new EntityStoreUpdate(LayoutAreaReference.Areas, id, control)], store.Update(LayoutAreaReference.Areas, i => i.Update(id, control))); public static EntityStoreAndUpdates UpdateData(this EntityStore store, string id, object control) - => new ([new EntityStoreUpdate(LayoutAreaReference.Data, id, control)], store.Update(LayoutAreaReference.Data, i => i.Update(id, control))); + => new([new EntityStoreUpdate(LayoutAreaReference.Data, id, control)], store.Update(LayoutAreaReference.Data, i => i.Update(id, control))); public static LayoutDefinition WithRenderer( this LayoutDefinition layout, diff --git a/src/MeshWeaver.Layout/Template.cs b/src/MeshWeaver.Layout/Template.cs index f0602db47..5dd00499b 100644 --- a/src/MeshWeaver.Layout/Template.cs +++ b/src/MeshWeaver.Layout/Template.cs @@ -1,6 +1,7 @@ using System.Linq.Expressions; using System.Reflection; using MeshWeaver.Data; +using MeshWeaver.Data.Serialization; using MeshWeaver.Domain.Layout; using MeshWeaver.Layout.Composition; using MeshWeaver.Layout.DataBinding; @@ -9,16 +10,34 @@ namespace MeshWeaver.Layout; public static class Template{ - /// /// This is a generic template method which can be used if streams are connected to synchronize with Workspace. /// - /// + /// Type of the view + /// The type of the entity to be data bound /// Id to be referenced in the data binding /// View Template. - /// - public static TView Bind(string id, Expression> dataTemplate) - where TView : UiControl => BindObject(null, id, dataTemplate); + /// The view template + public static TView Bind(this ISynchronizationStream stream, string id, Expression> dataTemplate) + where TView : UiControl + { + object current = null; + + return (TView)GetTemplateControl(id, dataTemplate) + .WithBuildup((host, context, store) => + { + var forwardSubscription = stream.Subscribe(changeItem => + { + if (Equals(changeItem.Value, current)) + return; + current = changeItem.Value; + host.Stream.SetData(id, changeItem.SetValue(current)); + }); + host.AddDisposable(context.Area, forwardSubscription); + return new(store); + }); + + } /// @@ -52,6 +71,33 @@ Expression> dataTemplate return new ItemTemplateControl(view, data); } + /// + /// Takes expression tree of data template and replaces all property getters by binding instances and sets data context property + /// + [ReplaceBindMethod] + public static TView BindObservable( + this IObservable stream, + string id, + Expression> dataTemplate + ) + where TView : UiControl + { + object current = null; + return (TView)GetTemplateControl(id, dataTemplate) + .WithBuildup((host, context, store) => + { + var forwardSubscription = stream.Subscribe(val => + { + if (Equals(val, current)) + return; + current = val; + host.Stream.SetData(id, new ChangeItem(host.Stream.Owner, host.Stream.Reference, val, host.Stream.Owner, null, host.Hub.Version)); + }); + host.AddDisposable(context.Area, forwardSubscription); + return new(store); + }); + } + private static readonly MethodInfo ItemTemplateMethodNonGeneric = ReflectionHelper.GetStaticMethodGeneric( () => ItemTemplateNonGeneric(default(IEnumerable), null) ); @@ -99,12 +145,19 @@ Expression> dataTemplate ) where TView : UiControl { - var topLevel = UpdateData(data, id); + var view = GetTemplateControl(id, dataTemplate); + if(data != null) + view = (TView)view.WithBuildup((_,_,store) => store.UpdateData(id, data)); + return view; + } + + private static TView GetTemplateControl(string id, Expression> dataTemplate) + where TView : UiControl + { + var topLevel = LayoutAreaReference.GetDataPointer(id); var view = dataTemplate.Build(topLevel, out var _); if (view == null) throw new ArgumentException("Data template was not specified."); - if(data != null) - view = (TView)view.WithUpdates(store => store.UpdateData(id, data)); return view; } @@ -170,7 +223,7 @@ Expression> dataTemplate { DataContext = dataContext } - .WithUpdates(store => store.UpdateData(id, data)) + .WithBuildup((_,_,store) => store.UpdateData(id, data)) ; } diff --git a/src/MeshWeaver.Layout/UiControl.cs b/src/MeshWeaver.Layout/UiControl.cs index 91a2721bc..d19ac4d17 100644 --- a/src/MeshWeaver.Layout/UiControl.cs +++ b/src/MeshWeaver.Layout/UiControl.cs @@ -69,11 +69,10 @@ public UiControl AddSkin(Skin skin) => this with { Skins = (Skins ?? ImmutableList.Empty).Add(skin) }; - protected ImmutableList> Updates { get; init; } = - ImmutableList>.Empty; - public UiControl WithUpdates(Func update) + protected ImmutableList> Buildup { get; init; } = []; + public UiControl WithBuildup(Func buildup) { - return this with { Updates = Updates.Add(update) }; + return this with { Buildup = Buildup.Add(buildup) }; } @@ -181,10 +180,10 @@ protected override void Dispose() protected override EntityStoreAndUpdates Render (LayoutAreaHost host, RenderingContext context, EntityStore store) => - Updates + Buildup .Aggregate(RenderSelf(host, context, store), (r, u) => { - var updated = u.Invoke(r.Store); + var updated = u.Invoke(host, context, r.Store); return new(r.Changes.Concat(updated.Changes), updated.Store); }); protected virtual EntityStoreAndUpdates RenderSelf(LayoutAreaHost host, RenderingContext context, EntityStore store) diff --git a/src/MeshWeaver.Search/MarkdownIndexer.cs b/src/MeshWeaver.Search/MarkdownIndexer.cs index eed0723b4..7d9e7cd5a 100644 --- a/src/MeshWeaver.Search/MarkdownIndexer.cs +++ b/src/MeshWeaver.Search/MarkdownIndexer.cs @@ -3,10 +3,9 @@ using Markdig; using Markdig.Extensions.Yaml; using Markdig.Syntax; -using MeshWeaver.Search; using Markdown = Markdig.Markdown; -namespace MeshWeaver.Ai.Index; +namespace MeshWeaver.Search; public static class MarkdownIndexer { diff --git a/src/MeshWeaver.Search/MeshArticleIndex.cs b/src/MeshWeaver.Search/MeshArticleIndex.cs index c4c5198d8..4f5cca7d9 100644 --- a/src/MeshWeaver.Search/MeshArticleIndex.cs +++ b/src/MeshWeaver.Search/MeshArticleIndex.cs @@ -1,4 +1,6 @@ using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Text.Json; using Azure.Search.Documents.Indexes; using Azure.Search.Documents.Indexes.Models; @@ -25,4 +27,22 @@ public class MeshArticleIndex [VectorSearchField(VectorSearchDimensions = 1536, VectorSearchProfileName = "vector-profile")] public float[] VectorRepresentation { get; set; } + + public IDictionary ToMetadata(string path, string type) => + new Dictionary() + { + { nameof(Url), Url }, + { nameof(Name), Name }, + { nameof(Description), Description }, + { nameof(Thumbnail), Thumbnail }, + { nameof(Published), Published.ToString(CultureInfo.InvariantCulture) }, + { nameof(Authors), JsonSerializer.Serialize(Authors) }, + { nameof(Tags), JsonSerializer.Serialize(Tags) }, + { nameof(Path), path }, + { nameof(Type), type }, + }; + + + } + diff --git a/src/MeshWeaver.Search/MeshWeaver.Search.csproj b/src/MeshWeaver.Search/MeshWeaver.Search.csproj index 2c848d917..eb6b99c65 100644 --- a/src/MeshWeaver.Search/MeshWeaver.Search.csproj +++ b/src/MeshWeaver.Search/MeshWeaver.Search.csproj @@ -1,6 +1,6 @@  - + diff --git a/test/MeshWeaver.Data.Test/SynchronizationStreamTest.cs b/test/MeshWeaver.Data.Test/SynchronizationStreamTest.cs index 74a6b5aa4..fecd07ae9 100644 --- a/test/MeshWeaver.Data.Test/SynchronizationStreamTest.cs +++ b/test/MeshWeaver.Data.Test/SynchronizationStreamTest.cs @@ -40,14 +40,19 @@ public async Task ParallelUpdate() .Subscribe(tracker.Add); var count = 0; - Enumerable.Range(0, 10).AsParallel().ForEach(_ => stream.Update(state => + Enumerable.Range(0, 10).AsParallel().Select(_ => { - var instance = new MyData(Instance, (++count).ToString()); - var existingInstance = state.Collections.GetValueOrDefault(collectionName)?.Instances.GetValueOrDefault(Instance); - return stream.ApplyChanges( - (state ?? new()).Update(collectionName, i => i.Update(Instance, instance)), - [new(collectionName, Instance, instance){OldValue = existingInstance }]); - })); + stream.Update(state => + { + var instance = new MyData(Instance, (++count).ToString()); + var existingInstance = state.Collections.GetValueOrDefault(collectionName)?.Instances + .GetValueOrDefault(Instance); + return stream.ApplyChanges( + (state ?? new()).Update(collectionName, i => i.Update(Instance, instance)), + [new(collectionName, Instance, instance) { OldValue = existingInstance }]); + }); + return true; + }).ToArray(); await DisposeAsync(); tracker.Should().HaveCount(10) diff --git a/test/MeshWeaver.Layout.Test/LayoutTest.cs b/test/MeshWeaver.Layout.Test/LayoutTest.cs index d2d56e908..52adcbd0f 100644 --- a/test/MeshWeaver.Layout.Test/LayoutTest.cs +++ b/test/MeshWeaver.Layout.Test/LayoutTest.cs @@ -146,8 +146,7 @@ private static object UpdatingView() return Controls .Stack - .WithView( (_, _) => - Template.Bind(toolbar, nameof(toolbar), tb => Controls.Text(tb.Year)), "Toolbar") + .WithView(Template.Bind(toolbar, nameof(toolbar), tb => Controls.Text(tb.Year)), "Toolbar") .WithView((area, _) => area.GetDataStream(nameof(toolbar)) .Select(tb => Controls.Html($"Report for year {tb.Year}")), "Content"); @@ -166,7 +165,7 @@ public async Task TestUpdatingView() ); var reportArea = $"{reference.Area}/Content"; var content = await stream.GetControlStream(reportArea) - .Timeout(3.Seconds()) + // .Timeout(3.Seconds()) .FirstAsync(x => x is not null); content.Should().BeOfType().Which.Data.ToString().Should().Contain("2024"); diff --git a/test/MeshWeaver.Search.Test/ArticleParsingTest.cs b/test/MeshWeaver.Search.Test/ArticleParsingTest.cs index 0e0f5f65f..4c78dad0f 100644 --- a/test/MeshWeaver.Search.Test/ArticleParsingTest.cs +++ b/test/MeshWeaver.Search.Test/ArticleParsingTest.cs @@ -1,7 +1,6 @@ using System.Text; using Azure.Provisioning.Storage; using Azure.Storage.Blobs; -using MeshWeaver.Ai.Index; using MeshWeaver.Hub.Fixture; using Xunit.Abstractions;