diff --git a/.github/workflows/npm-build-test.yml b/.github/workflows/npm-build-test.yml new file mode 100644 index 0000000000..a65c009705 --- /dev/null +++ b/.github/workflows/npm-build-test.yml @@ -0,0 +1,37 @@ +name: shesha-reactjs-build +description: Attempt to build the shesha-reactjs project to ensure the branch is not broken + +on: + pull_request: + branches: + - main + +permissions: + pull-requests: write + contents: read + +jobs: + build-attempt: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./shesha-reactjs + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build + id: build + run: npm run build + \ No newline at end of file diff --git a/ChangeLog.md b/ChangeLog.md index 223724d9a6..c1a1f80052 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,29 +1,3 @@ # Release Notes ## Enhancements -- **Ehanced Form Settings**: Introduced configurable form lifecycle management, providing more flexibility in form handling. - - **Appearance**: Contains all settings related to form view (layout, size etc). - - **Data**: Contains settings related to data processing and form lifecylce. - - **Security**: Security related settings. -- **Enhanced App Settings Page**: Expanded the App Settings page to manage front-end specific settings in addition to general settings, including Default URL, Theme, and Main Menu configurations. - - **Default URL**: This is the url the user should be redirected to if the user is not authenticated and does not specify a specific page. - - **Main Menu Settings**: Here you can configure the sidebar menu items by adjusting their settings and ordering. - - **Theme Settings**: Here you customise the theme color of the application. -- **Enhanced Button Component**: Added additional styling properties to the Button Component for improved customization. -- **Exposed getAnonymousForms Endpoint**: Made the getAnonymousForms endpoint accessible for easier integration. -- **Improved JavaScript Settings Component**: Updated the JS settings component for better performance and user experience. -- **Configurable User Profile**: Allowed additional configurations to be configured below the user logged information. -- Added `addDelayedUpdateData` variable which is required for files when using a script method. -- **Style Section Hidden in Table**: The style section is now hidden for Textfield and Autocomplete components when rendered within a table. -- **Font Weight on Table Column Names**: Increased the font weight to 600 for table column names to enhance visibility and readability. -- **Reset Wizard State Action**: Exposed an action on Wizard actions that allows resetting the wizard state. - -## Bug Fixes -- **Component Label Issue**: Fixed an issue where components (Address, Entity Reference, Subform, ChildEntitiesTag Group) did not append the label by default when a property name was selected. -- **Base64 Image Rendering**: Resolved a problem with Base64 images not rendering in the Image component and container background property. -- **CRUD Operations Column Expansion**: Corrected the unexpected expansion of CRUD operations columns when using the toogle columns selector option. -- **Table View Component Issue**: Fixed the "add item" modal in the newly created Table view component, which was not closeable by default. -- **Permission Configurations Import**: Addressed a bug causing the application to break when importing permission configurations with ConfigMigrations. -- **setFormData Initialization**: Resolved an issue where setFormData used on Initialization did not append values to the specified field in forms. -- **Backend Endpoint Management**: Fixed the issue where the backend did not properly remove or reinstate endpoints when disabled or enabled. -- **Duplicate Records from Button Clicks**: Fixed a bug causing duplicate records due to multiple clicks on the default, custom buttons and datalist during network delays. -- **Notes Component Overflow Issue**: Fixed an issue where clicking 'More' on long notes caused them to overlap outside the line section on the Notes component. +- Component property updates diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000000..2bd90f40f8 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,68 @@ +variables: + - name: 'BuildParameters.configuration' + value: release + +# trigger: +# - main + +pool: + name: Azure Pipelines +stages: + - stage: build + jobs: + - job: UnitTests_1 + displayName: 'Unit Tests' + pool: + name: Azure Pipelines + vmImage: ubuntu-22.04 + steps: + - checkout: self + - task: NuGetToolInstaller@1 + inputs: + versionSpec: '6.x' + checkLatest: true + - task: NuGetCommand@2 + condition: ne(variables.CACHE_RESTORED, true) + displayName: Nuget Restore + inputs: + command: 'restore' + feedsToUse: 'config' + restoreSolution: 'shesha-core/Shesha.sln' + nugetConfigPath: 'shesha-core/.nuget/NuGet.Azure.Config' + - task: UseDotNet@2 + displayName: Use .NET Core sdk 8.x + retryCountOnTaskFailure: 3 + inputs: + version: 8.x + - task: DotNetCoreCLI@2 + displayName: dotnet build + inputs: + projects: shesha-core/Shesha.sln + arguments: --configuration $(BuildParameters.configuration) + - task: DockerInstaller@0 + inputs: + dockerVersion: '20.10.11' + - script: | + docker --version + sudo systemctl start docker + displayName: 'Verify Docker' + - task: Docker@2 + inputs: + containerRegistry: 'docker-images' + command: 'login' + - script: dotnet test Shesha.Tests.csproj --configuration $(buildConfiguration) --no-build --logger "trx;LogFileName=testresults.trx" + workingDirectory: 'shesha-core/test/Shesha.Tests' + displayName: 'Run tests with Testcontainers' + env: + DOCKER_HOST: 'unix:///var/run/docker.sock' + TESTCONTAINERS_RYUK_DISABLED: 'true' + - task: PublishTestResults@2 + displayName: 'Publish test results' + inputs: + testResultsFormat: 'VSTest' + testResultsFiles: '**/*.trx' + failTaskOnFailedTests: true + failTaskOnFailureToPublishResults: true + failTaskOnMissingResultsFile: true + testRunTitle: 'Unit Tests' + condition: succeededOrFailed() \ No newline at end of file diff --git a/shesha-core/.nuget/NuGet.Azure.Config b/shesha-core/.nuget/NuGet.Azure.Config new file mode 100644 index 0000000000..e6bf919ab9 --- /dev/null +++ b/shesha-core/.nuget/NuGet.Azure.Config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/shesha-core/.nuget/NuGet.Config b/shesha-core/.nuget/NuGet.Config index b192b019bc..e56dc7d6ca 100644 --- a/shesha-core/.nuget/NuGet.Config +++ b/shesha-core/.nuget/NuGet.Config @@ -5,10 +5,9 @@ + - - diff --git a/shesha-core/README.md b/shesha-core/README.md index f01e544764..5c9386551e 100644 --- a/shesha-core/README.md +++ b/shesha-core/README.md @@ -6,7 +6,7 @@ shesha-core ## License -Shesha and the various shesha community components and services are available under the [GPL-3.0 license](https://opensource.org/licenses/GPL-3.0). Shesha also includes external libraries that are available under a variety of licenses. See [LICENSE](https://github.com/shesha-io/shesha-framework/blob/main/shesha-reactjs/LICENCE.md) for the full license text +Shesha and the various shesha community components and services are available under the [Apache License 2.0](../LICENSE.md). Shesha also includes external libraries that are available under a variety of licenses. See [LICENSE.md](../LICENSE.md) for the full license text. ## Running locally from CLI: diff --git a/shesha-core/Shesha.sln b/shesha-core/Shesha.sln index 9f98b1a654..0931ecbab5 100644 --- a/shesha-core/Shesha.sln +++ b/shesha-core/Shesha.sln @@ -50,6 +50,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shesha.NHibernate.PostGis", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shesha.Common", "src\Shesha.Common\Shesha.Common.csproj", "{97D1FF0C-72EF-4C45-93B3-C169BCD654FA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "analyzers", "analyzers", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SheshaCodeAnalyzers", "analyzers\SheshaCodeAnalyzers\SheshaCodeAnalyzers.csproj", "{588EA4DF-22E2-64C6-2DD1-6E8ADEDCBF1B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SheshaCodeAnalyzers.CodeFixes", "analyzers\SheshaCodeAnalyzers.CodeFixes\SheshaCodeAnalyzers.CodeFixes.csproj", "{05DC5A74-CCC2-3A28-008A-F69B34CF66E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SheshaCodeAnalyzers.Package", "analyzers\SheshaCodeAnalyzers.Package\SheshaCodeAnalyzers.Package.csproj", "{18145AAA-0E16-6212-F535-4A82F5069A17}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SheshaCodeAnalyzers.Test", "analyzers\SheshaCodeAnalyzers.Test\SheshaCodeAnalyzers.Test.csproj", "{C2196593-1C13-50DC-5C68-2C3B3193BB38}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -128,6 +138,22 @@ Global {97D1FF0C-72EF-4C45-93B3-C169BCD654FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {97D1FF0C-72EF-4C45-93B3-C169BCD654FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {97D1FF0C-72EF-4C45-93B3-C169BCD654FA}.Release|Any CPU.Build.0 = Release|Any CPU + {588EA4DF-22E2-64C6-2DD1-6E8ADEDCBF1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {588EA4DF-22E2-64C6-2DD1-6E8ADEDCBF1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {588EA4DF-22E2-64C6-2DD1-6E8ADEDCBF1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {588EA4DF-22E2-64C6-2DD1-6E8ADEDCBF1B}.Release|Any CPU.Build.0 = Release|Any CPU + {05DC5A74-CCC2-3A28-008A-F69B34CF66E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05DC5A74-CCC2-3A28-008A-F69B34CF66E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05DC5A74-CCC2-3A28-008A-F69B34CF66E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05DC5A74-CCC2-3A28-008A-F69B34CF66E5}.Release|Any CPU.Build.0 = Release|Any CPU + {18145AAA-0E16-6212-F535-4A82F5069A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18145AAA-0E16-6212-F535-4A82F5069A17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18145AAA-0E16-6212-F535-4A82F5069A17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18145AAA-0E16-6212-F535-4A82F5069A17}.Release|Any CPU.Build.0 = Release|Any CPU + {C2196593-1C13-50DC-5C68-2C3B3193BB38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2196593-1C13-50DC-5C68-2C3B3193BB38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2196593-1C13-50DC-5C68-2C3B3193BB38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2196593-1C13-50DC-5C68-2C3B3193BB38}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -152,6 +178,10 @@ Global {2A550F70-7843-4188-BD57-C994E0D27DA2} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC} {ADC60E62-AAD0-44A0-87DC-126170557958} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC} {97D1FF0C-72EF-4C45-93B3-C169BCD654FA} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC} + {588EA4DF-22E2-64C6-2DD1-6E8ADEDCBF1B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {05DC5A74-CCC2-3A28-008A-F69B34CF66E5} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {18145AAA-0E16-6212-F535-4A82F5069A17} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {C2196593-1C13-50DC-5C68-2C3B3193BB38} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {95ED23F3-8DEA-456E-A44B-87A161CB696A} diff --git a/shesha-core/analyzers/Directory.Build.props b/shesha-core/analyzers/Directory.Build.props new file mode 100644 index 0000000000..65b8ccf793 --- /dev/null +++ b/shesha-core/analyzers/Directory.Build.props @@ -0,0 +1,6 @@ + + + true + $(NoWarn);CS1591;612;618;809;AsyncFixer01;8618; + + diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/CodeFixResources.Designer.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/CodeFixResources.Designer.cs new file mode 100644 index 0000000000..b3b4b96051 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/CodeFixResources.Designer.cs @@ -0,0 +1,83 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +namespace SheshaCodeAnalyzers +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class CodeFixResources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal CodeFixResources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SheshaCodeAnalyzers.CodeFixResources", typeof(CodeFixResources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Make uppercase. + /// + internal static string CodeFixTitle + { + get + { + return ResourceManager.GetString("CodeFixTitle", resourceCulture); + } + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/CodeFixResources.resx b/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/CodeFixResources.resx new file mode 100644 index 0000000000..97abe68945 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/CodeFixResources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Make uppercase + The title of the code fix. + + \ No newline at end of file diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/RestrictedStaticPropertyCodeFixProvider.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/RestrictedStaticPropertyCodeFixProvider.cs new file mode 100644 index 0000000000..51a7c33320 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/RestrictedStaticPropertyCodeFixProvider.cs @@ -0,0 +1,98 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace SheshaCodeAnalyzers +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RestrictedStaticPropertyCodeFixProvider)), Shared] + public class RestrictedStaticPropertyCodeFixProvider : CodeFixProvider + { + public sealed override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create(RestrictedStaticPropertyAnalyzer.DiagnosticId); + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + var diagnostic = context.Diagnostics[0]; + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var memberAccess = root.FindNode(diagnosticSpan) as MemberAccessExpressionSyntax; + if (memberAccess == null) + return; + + context.RegisterCodeFix( + new MyCodeAction( + "Replace with StaticContext usage", + c => RemoveRestrictedPropertyAccessAsync(context.Document, memberAccess, c)), + diagnostic); + } + + private async Task RemoveRestrictedPropertyAccessAsync( + Document document, + MemberAccessExpressionSyntax memberAccess, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + // Replace the entire expression + var newExpression = SyntaxFactory.ParseExpression("StaticContext.IocManager"); + var newRoot = root.ReplaceNode(memberAccess, newExpression); + + // Add both namespaces if needed + newRoot = AddUsingIfMissing(newRoot, "Shesha.Services"); + + return document.WithSyntaxRoot(newRoot); + } + + private SyntaxNode AddUsingIfMissing(SyntaxNode root, string namespaceToAdd) + { + if (root is CompilationUnitSyntax compilationUnit) + { + if (!compilationUnit.Usings.Any(u => u.Name.ToString() == namespaceToAdd)) + { + var newUsing = SyntaxFactory.UsingDirective( + SyntaxFactory.ParseName(namespaceToAdd)); + + // Add to existing usings, keeping them sorted + var newUsings = compilationUnit.Usings.Add(newUsing) + .OrderBy(u => u.Name.ToString()) + .ToList(); + + return compilationUnit.WithUsings(SyntaxFactory.List(newUsings)); + } + } + return root; + } + + private class MyCodeAction : CodeAction + { + private readonly Func> _createChangedDocument; + + public MyCodeAction(string title, Func> createChangedDocument) + { + Title = title; + _createChangedDocument = createChangedDocument; + } + + public override string Title { get; } + + public override string EquivalenceKey => Title; + + protected override Task GetChangedDocumentAsync(CancellationToken cancellationToken) + { + return _createChangedDocument(cancellationToken); + } + } + } +} \ No newline at end of file diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/SheshaCodeAnalyzers.CodeFixes.csproj b/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/SheshaCodeAnalyzers.CodeFixes.csproj new file mode 100644 index 0000000000..d1d348e8b9 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.CodeFixes/SheshaCodeAnalyzers.CodeFixes.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + false + SheshaCodeAnalyzers + + + + + + + + + + + + + + + + diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Package/SheshaCodeAnalyzers.Package.csproj b/shesha-core/analyzers/SheshaCodeAnalyzers.Package/SheshaCodeAnalyzers.Package.csproj new file mode 100644 index 0000000000..b623f0915a --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Package/SheshaCodeAnalyzers.Package.csproj @@ -0,0 +1,45 @@ + + + + netstandard2.0 + false + true + true + + + + SheshaCodeAnalyzers + 1.0.0.0 + ivan + http://LICENSE_URL_HERE_OR_DELETE_THIS_LINE + http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE + http://ICON_URL_HERE_OR_DELETE_THIS_LINE + http://REPOSITORY_URL_HERE_OR_DELETE_THIS_LINE + false + SheshaCodeAnalyzers + Summary of changes made in this release of the package. + Copyright + SheshaCodeAnalyzers, analyzers + true + true + + $(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput + + + + + + + + + + + + + + + + + + + diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Package/tools/install.ps1 b/shesha-core/analyzers/SheshaCodeAnalyzers.Package/tools/install.ps1 new file mode 100644 index 0000000000..53136a238f --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Package/tools/install.ps1 @@ -0,0 +1,275 @@ +param($installPath, $toolsPath, $package, $project) + +if($project.Object.SupportsPackageDependencyResolution) +{ + if($project.Object.SupportsPackageDependencyResolution()) + { + # Do not install analyzers via install.ps1, instead let the project system handle it. + return + } +} + +$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve + +foreach($analyzersPath in $analyzersPaths) +{ + if (Test-Path $analyzersPath) + { + # Install the language agnostic analyzers. + foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } + } + } +} + +# $project.Type gives the language name like (C# or VB.NET) +$languageFolder = "" +if($project.Type -eq "C#") +{ + $languageFolder = "cs" +} +if($project.Type -eq "VB.NET") +{ + $languageFolder = "vb" +} +if($languageFolder -eq "") +{ + return +} + +foreach($analyzersPath in $analyzersPaths) +{ + # Install language specific analyzers. + $languageAnalyzersPath = join-path $analyzersPath $languageFolder + if (Test-Path $languageAnalyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } + } + } +} +# SIG # Begin signature block +# MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor +# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA/i+qRUHsWzI0s +# FVk99zLgt/HOEQ33uvkFsWtHTHZgf6CCDXYwggX0MIID3KADAgECAhMzAAADTrU8 +# esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p +# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw +# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u +# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +# AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU +# p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1 +# 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm +# WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa +# +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq +# jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE +# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw +# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW +# MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci +# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j +# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG +# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu +# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 +# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk +# mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31 +# TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2 +# kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d +# hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM +# pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh +# JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX +# UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir +# IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8 +# 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A +# Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H +# tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq +# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x +# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv +# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG +# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG +# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg +# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 +# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr +# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg +# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy +# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 +# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh +# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k +# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB +# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn +# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 +# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w +# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o +# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD +# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa +# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny +# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG +# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t +# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV +# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 +# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG +# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl +# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb +# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l +# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 +# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 +# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 +# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam +# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa +# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah +# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA +# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt +# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr +# /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw +# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN +# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp +# Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB +# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO +# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIEY4Ow3COroWH11sAEOoStJj +# 1u4sq9rcx0dAx0gyZLHCMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A +# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB +# BQAEggEAFlSLlk17KQp2AwLbr5e4T5zyE44MGZOdNejIip+Mg8co8qho16qdNSvg +# AhvoJYxPJJr70DSCU9BIUoWY7hXoi9S4P08YlAid7BUT5OciIgHlrb8I900LaE+S +# 83rSvfVU1CZCjiSwcgng5DD2VPRo0Lu4G9p2Ky14dyOPwtPvrpsib5s9kewZLdiy +# /KxEDLKX8P+cHat1xH7RaZLDNxweRS6GSomjE2vjOlQHNSW879XR8bSoAt/m4uR1 +# WyrAxTGZb4miEYX+I5HsrWvbZLw9NSCJ/crbbap3LIobfQtK5binjY7v4MQp/5Oq +# y4S/4FAfwhWDXfaQfq6YTeOjHRVQbKGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC +# F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq +# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl +# AwQCAQUABCCV3irbWIoYz2Llfx9YQhUNtcP6GOrL7+YTUXQ4y5qzWAIGZNTJrsAW +# GBMyMDIzMDgzMTAwMTI1OC45ODNaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV +# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE +# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l +# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTIwMC0w +# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg +# ghHqMIIHIDCCBQigAwIBAgITMwAAAc9SNr5xS81IygABAAABzzANBgkqhkiG9w0B +# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE +# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD +# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzA1MjUxOTEy +# MTFaFw0yNDAyMDExOTEyMTFaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz +# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv +# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z +# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTIwMC0wNUUwLUQ5NDcxJTAjBgNV +# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB +# AQUAA4ICDwAwggIKAoICAQC4Pct+15TYyrUje553lzBQodgmd5Bz7WuH8SdHpAoW +# z+01TrHExBSuaMKnxvVMsyYtas5h6aopUGAS5WKVLZAvUtH62TKmAE0JK+i1hafi +# CSXLZPcRexxeRkOqeZefLBzXp0nudMOXUUab333Ss8LkoK4l3LYxm1Ebsr3b2OTo +# 2ebsAoNJ4kSxmVuPM7C+RDhGtVKR/EmHsQ9GcwGmluu54bqiVFd0oAFBbw4txTU1 +# mruIGWP/i+sgiNqvdV/wah/QcrKiGlpWiOr9a5aGrJaPSQD2xgEDdPbrSflYxsRM +# dZCJI8vzvOv6BluPcPPGGVLEaU7OszdYjK5f4Z5Su/lPK1eST5PC4RFsVcOiS4L0 +# sI4IFZywIdDJHoKgdqWRp6Q5vEDk8kvZz6HWFnYLOlHuqMEYvQLr6OgooYU9z0A5 +# cMLHEIHYV1xiaBzx2ERiRY9MUPWohh+TpZWEUZlUm/q9anXVRN0ujejm6OsUVFDs +# sIMszRNCqEotJGwtHHm5xrCKuJkFr8GfwNelFl+XDoHXrQYL9zY7Np+frsTXQpKR +# NnmI1ashcn5EC+wxUt/EZIskWzewEft0/+/0g3+8YtMkUdaQE5+8e7C8UMiXOHkM +# K25jNNQqLCedlJwFIf9ir9SpMc72NR+1j6Uebiz/ZPV74do3jdVvq7DiPFlTb92U +# KwIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFDaeKPtp0eTSVdG+gZc5BDkabTg4MB8G +# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG +# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy +# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w +# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy +# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG +# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD +# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQBQgm4pnA0xkd/9uKXJMzdMYyxUfUm/ZusU +# Ba32MEZXQuMGp20pSuX2VW9/tpTMo5bkaJdBVoUyd2DbDsNb1kjr/36ntT0jvL3A +# oWStAFhZBypmpPbx+BPK49ZlejlM4d5epX668tRRGfFip9Til9yKRfXBrXnM/q64 +# IinN7zXEQ3FFQhdJMzt8ibXClO7eFA+1HiwZPWysYWPb/ZOFobPEMvXie+GmEbTK +# bhE5tze6RrA9aejjP+v1ouFoD5bMj5Qg+wfZXqe+hfYKpMd8QOnQyez+Nlj1ityn +# OZWfwHVR7dVwV0yLSlPT+yHIO8g+3fWiAwpoO17bDcntSZ7YOBljXrIgad4W4gX+ +# 4tp1eBsc6XWIITPBNzxQDZZRxD4rXzOB6XRlEVJdYZQ8gbXOirg/dNvS2GxcR50Q +# dOXDAumdEHaGNHb6y2InJadCPp2iT5QLC4MnzR+YZno1b8mWpCdOdRs9g21QbbrI +# 06iLk9KD61nx7K5ReSucuS5Z9nbkIBaLUxDesFhr1wmd1ynf0HQ51Swryh7YI7TX +# T0jr81mbvvI9xtoqjFvIhNBsICdCfTR91ylJTH8WtUlpDhEgSqWt3gzNLPTSvXAx +# XTpIM583sZdd+/2YGADMeWmt8PuMce6GsIcLCOF2NiYZ10SXHZS5HRrLrChuzedD +# RisWpIu5uTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI +# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw +# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x +# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy +# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC +# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV +# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp +# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg +# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF +# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6 +# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp +# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu +# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E +# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0 +# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q +# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ +# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA +# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw +# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG +# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV +# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj +# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK +# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC +# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX +# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v +# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI +# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j +# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG +# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x +# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC +# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449 +# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM +# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS +# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d +# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn +# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs +# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL +# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL +# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN +# MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp +# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw +# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn +# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjkyMDAtMDVFMC1EOTQ3MSUwIwYDVQQD +# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQDq +# 8xzVXwLguauAQj1rrJ4/TyEMm6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w +# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6JpNsjAiGA8yMDIzMDgzMDIzMjIy +# NloYDzIwMjMwODMxMjMyMjI2WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDomk2y +# AgEAMAcCAQACAi7oMAcCAQACAhLnMAoCBQDom58yAgEAMDYGCisGAQQBhFkKBAIx +# KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI +# hvcNAQELBQADggEBADE16CHIs3WeXYKQG9djja+StAB7gsNJxV9p+CETL3847UL3 +# +DIeoj6p5g0FkS8PJK2xc+UbcNx6XJO+WUXEbU9GL4mrOCQjYOM+i3r8FEU+l3Gh +# 6ZG/9ygsIYRXEfDKK4lsbrrUkFQs9nDISHT3f8JZEJJXSsGmwcHWlNC0LC8bv0Jp +# e2Bw2+SNc6SlGD8Vv45r4WFPHhfSioCz4HSsF1He3/2Wku7OH85FKvugBlsca7+F +# bpGsDSL4LO9bv60DxuD+8xBZuyTB8s64ifCGlOXCNpK5VaHND48PhoJbuD0COwlM +# Rn5NlT6T4hhtkPOqNscMlzYHmTOKc5NhWK8PyrIxggQNMIIECQIBATCBkzB8MQsw +# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u +# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy +# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAc9SNr5xS81IygABAAABzzAN +# BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G +# CSqGSIb3DQEJBDEiBCAjGiC3/PscCMBvPZjpZqbYcL2WRZ+Ecf74oiIPQKkjSzCB +# +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EILPpsLqeNS4NuYXE2VJlMuvQeWVA +# 80ZDFhpOPjSzhPa/MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh +# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD +# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw +# MTACEzMAAAHPUja+cUvNSMoAAQAAAc8wIgQgWdbLA3Co0zf3nE5086HU0tdn0vK7 +# yXyU9aAFpszvWFIwDQYJKoZIhvcNAQELBQAEggIANrHN06oaP0N1lUSxxoJteJgI +# fQjN82xV5VSE0cwNCBy05fg1VydPERF59+MIwIlJHhikSo3YOj5tt3AohC78U46P +# 2IdrLHKlA3rjiLUGwQvGUKJUSsEH4uueA1mmh9jwUBJhY3NjTcMQaQqp/oxZTaya +# l7NqbubBIZvsDD126SUr8jtVWtZZzw8pnWCFb4Rijii4fY1UiQzfQLFwqQuid6tE +# I0AaY3IoTlp7U9K2wfAPWcP1G7n3qv+990GEiQlGJlCfIJSSJQodzL2QZF5HCn/K +# SfgkRPn3y/Aax8683mWCT8zricYzO3MZ9j0T7tAcqOiWb7PFCsk5Va44lq4Gv0u+ +# +60FYCAA/Qn6eMuNkqIpBeIK0+NYUcMSwPdY/riKXdgkVLwChEJC5WWznD/Iqsil +# Jj9XYailhXzYx7Pa2MLays62LPwCnUxRQBTD9/rL3XQMj69iA4lisb51dKAtrqAU +# 53aRXFn+twYYFTAUQ/oNK14nZEQE5H53xAfhphMokJOu+CnQQKeCMeYlex6Q4zfw +# TQxP/xXZ+QW2cSZTwh1d5iE0XMhKCZxhxIOF/rRA+75L5GUz60reRZPeH/7USYZL +# VPc0+kxIdSbNFJAhAp0u59wSMQdBofor3+HfDfmxmoSjfCSH4TvOkNIulX1PPJPX +# UB7H4n7XHWJPkUU0cUw= +# SIG # End signature block diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Package/tools/uninstall.ps1 b/shesha-core/analyzers/SheshaCodeAnalyzers.Package/tools/uninstall.ps1 new file mode 100644 index 0000000000..1f6b6d4f0e --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Package/tools/uninstall.ps1 @@ -0,0 +1,282 @@ +param($installPath, $toolsPath, $package, $project) + +if($project.Object.SupportsPackageDependencyResolution) +{ + if($project.Object.SupportsPackageDependencyResolution()) + { + # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. + return + } +} + +$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve + +foreach($analyzersPath in $analyzersPaths) +{ + # Uninstall the language agnostic analyzers. + if (Test-Path $analyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } + } + } +} + +# $project.Type gives the language name like (C# or VB.NET) +$languageFolder = "" +if($project.Type -eq "C#") +{ + $languageFolder = "cs" +} +if($project.Type -eq "VB.NET") +{ + $languageFolder = "vb" +} +if($languageFolder -eq "") +{ + return +} + +foreach($analyzersPath in $analyzersPaths) +{ + # Uninstall language specific analyzers. + $languageAnalyzersPath = join-path $analyzersPath $languageFolder + if (Test-Path $languageAnalyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + try + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } + catch + { + + } + } + } + } +} +# SIG # Begin signature block +# MIIoLQYJKoZIhvcNAQcCoIIoHjCCKBoCAQExDzANBglghkgBZQMEAgEFADB5Bgor +# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDC68wb97fg0QGL +# yXrxJhYfmibzcOh8caqC0uZprfczDaCCDXYwggX0MIID3KADAgECAhMzAAADTrU8 +# esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p +# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw +# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u +# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +# AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU +# p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1 +# 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm +# WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa +# +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq +# jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE +# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw +# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW +# MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci +# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j +# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG +# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu +# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 +# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk +# mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31 +# TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2 +# kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d +# hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM +# pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh +# JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX +# UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir +# IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8 +# 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A +# Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H +# tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq +# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x +# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv +# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG +# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG +# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg +# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 +# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr +# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg +# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy +# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 +# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh +# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k +# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB +# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn +# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 +# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w +# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o +# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD +# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa +# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny +# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG +# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t +# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV +# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 +# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG +# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl +# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb +# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l +# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 +# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 +# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 +# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam +# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa +# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah +# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA +# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt +# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr +# /Xmfwb1tbWrJUnMTDXpQzTGCGg0wghoJAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw +# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN +# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp +# Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB +# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO +# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIBdcqRcs5QL71hlQnl3M636V +# 5iTZvb6co3MHeMuIr36qMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A +# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB +# BQAEggEARbhO/zqm6SWBf6DSJ3P1r82VjSyMGXaLtMfTOq/bqHOXPOqC25R1v5uO +# zu4ri+UOS8dU6EfW6C9Xf/Z1Ue/oxrvxn5j8mPvsmcs5OyDO0hW0Wv6pYEy5Z5up +# mfcqvfUfl3+ir29lgPuz0f1mLpz0XxqhjqElEi5RfZD1k1YVg65f0qHroP2txql5 +# TfC77DXJ8verzVm1wqXBHTAERQD94TJobahYTCmyaudMjLUVFakv2lTMv0YTnrQR +# So006ZQg3i1jcVCJt/bRDGKh3xUo1IHgoh3NjMEkxT3iWt8rnX8Us6T6Zg8B2OxC +# 0EnuIu/eYYUlYTLQxO9eks7w1kVcUqGCF5cwgheTBgorBgEEAYI3AwMBMYIXgzCC +# F38GCSqGSIb3DQEHAqCCF3AwghdsAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq +# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl +# AwQCAQUABCBQ1HvJFFsT0ICl6oEZlSNN8DlUeQobBHt/oTUGN6iKBgIGZNTKS0Bc +# GBMyMDIzMDgzMTAwMTI1OC4zODVaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV +# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE +# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l +# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046N0YwMC0w +# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg +# ghHtMIIHIDCCBQigAwIBAgITMwAAAdWpAs/Fp8npWgABAAAB1TANBgkqhkiG9w0B +# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE +# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD +# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzA1MjUxOTEy +# MzBaFw0yNDAyMDExOTEyMzBaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz +# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv +# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z +# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046N0YwMC0wNUUwLUQ5NDcxJTAjBgNV +# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB +# AQUAA4ICDwAwggIKAoICAQDFfak57Oph9vuxtloABiLc6enT+yKH619b+OhGdkyh +# gNzkX80KUGI/jEqOVMV4Sqt/UPFFidx2t7v2SETj2tAzuVKtDfq2HBpu80vZ0vyQ +# DydVt4MDL4tJSKqgYofCxDIBrWzJJjgBolKdOJx1ut2TyOc+UOm7e92tVPHpjdg+ +# Omf31TLUf/oouyAOJ/Inn2ih3ASP0QYm+AFQjhYDNDu8uzMdwHF5QdwsscNa9PVS +# GedLdDLo9jL6DoPF4NYo06lvvEQuSJ9ImwZfBGLy/8hpE7RD4ewvJKmM1+t6eQuE +# sTXjrGM2WjkW18SgUZ8n+VpL2uk6AhDkCa355I531p0Jkqpoon7dHuLUdZSQO40q +# mVIQ6qQCanvImTqmNgE/rPJ0rgr0hMPI/uR1T/iaL0mEq4bqak+3sa8I+FAYOI/P +# C7V+zEek+sdyWtaX+ndbGlv/RJb5mQaGn8NunbkfvHD1Qt5D0rmtMOekYMq7QjYq +# E3FEP/wAY4TDuJxstjsa2HXi2yUDEg4MJL6/JvsQXToOZ+IxR6KT5t5fB5FpZYBp +# VLMma3pm5z6VXvkXrYs33NXJqVWLwiswa7NUFV87Es2sou9Idw3yAZmHIYWgOQ+D +# IY1nY3aG5DODiwN1rJyEb+mbWDagrdVxcncr6UKKO49eoNTXEW+scUf6GwXG0KEy +# mQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFK/QXKNO35bBMOz3R5giX7Ala2OaMB8G +# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG +# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy +# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w +# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy +# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG +# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD +# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQBmRddqvQuyjRpx0HGxvOqffFrbgFAg0j82 +# v0v7R+/8a70S2V4t7yKYKSsQGI6pvt1A8JGmuZyjmIXmw23AkI5bZkxvSgws8rrB +# tJw9vakEckcWFQb7JG6b618x0s9Q3DL0dRq46QZRnm7U6234lecvjstAow30dP0T +# nIacPWKpPc3QgB+WDnglN2fdT1ruQ6WIVBenmpjpG9ypRANKUx5NRcpdJAQW2FqE +# HTS3Ntb+0tCqIkNHJ5aFsF6ehRovWZp0MYIz9bpJHix0VrjdLVMOpe7wv62t90E3 +# UrE2KmVwpQ5wsMD6YUscoCsSRQZrA5AbwTOCZJpeG2z3vDo/huvPK8TeTJ2Ltu/I +# tXgxIlIOQp/tbHAiN8Xptw/JmIZg9edQ/FiDaIIwG5YHsfm2u7TwOFyd6OqLw18Z +# 5j/IvDPzlkwWJxk6RHJF5dS4s3fnyLw3DHBe5Dav6KYB4n8x/cEmD/R44/8gS5Pf +# uG1srjLdyyGtyh0KiRDSmjw+fa7i1VPoemidDWNZ7ksNadMad4ZoDvgkqOV4A6a+ +# N8HIc/P6g0irrezLWUgbKXSN8iH9RP+WJFx5fBHE4AFxrbAUQ2Zn5jDmHAI3wYcQ +# DnnEYP51A75WFwPsvBrfrb1+6a1fuTEH1AYdOOMy8fX8xKo0E0Ys+7bxIvFPsUpS +# zfFjBolmhzCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI +# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw +# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x +# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy +# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC +# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV +# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp +# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg +# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF +# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6 +# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp +# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu +# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E +# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0 +# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q +# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ +# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA +# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw +# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG +# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV +# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj +# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK +# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC +# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX +# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v +# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI +# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j +# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG +# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x +# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC +# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449 +# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM +# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS +# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d +# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn +# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs +# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL +# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL +# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNQ +# MIICOAIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp +# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw +# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn +# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjdGMDAtMDVFMC1EOTQ3MSUwIwYDVQQD +# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQBO +# Ei+S/ZVFe6w1Id31m6Kge26lNKCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w +# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6JpN3jAiGA8yMDIzMDgzMDIzMjMx +# MFoYDzIwMjMwODMxMjMyMzEwWjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDomk3e +# AgEAMAoCAQACAiLLAgH/MAcCAQACAhLTMAoCBQDom59eAgEAMDYGCisGAQQBhFkK +# BAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJ +# KoZIhvcNAQELBQADggEBAC/zAZ9IEqD3nHBUWwxFTfnDSRErnCJ6XGxH5jOVtR0t +# 9pi7yCpApLpA2D12g1lcv4ugnwGpsbwmFjrcF4WlHemRa77qv409xNhNKrnh3H+U +# X2hvy9Utp9LiJiqS7lOW5VN1Uv+LbnA+FWt//4J+YLv44D/dliUGjYX623X7KiEX +# dbdXPR/Sn+W2YVQ19O8liKaFDnDnIAz+WLCfL6EaoGu4Te/Mr65Khy3YWTwQfXxr +# gR/JMDzLzWossnGszYCN8S8d9X6mfzWuYv4JHLEiThW++WbMLeT2hhKPomcbvqU4 +# wPb/ylDrTrWuAr/fVndECXVjCIzYJiFwOWn/ZfN9FpQxggQNMIIECQIBATCBkzB8 +# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk +# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N +# aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAdWpAs/Fp8npWgABAAAB +# 1TANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEE +# MC8GCSqGSIb3DQEJBDEiBCAHS+EjBkJ/YPugQQv1D7eXVQOI4DWPIYpUxpssheDN +# lTCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EINm/I4YM166JMM7EKIcYvlcb +# r2CHjKC0LUOmpZIbBsH/MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m +# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB +# IDIwMTACEzMAAAHVqQLPxafJ6VoAAQAAAdUwIgQgBVj4mNvMkXn0xEeEe3i6urr/ +# L5Eh2p4uPk3zOcR/jHIwDQYJKoZIhvcNAQELBQAEggIAiKbnM5VQAwi5gWDevpEJ +# oHiZRrb0HApEUkca54Oye2+5tyRfWo3ruZvLZueHWsbEzYXclOscHVPjyR1t6s+p +# W13utx4mDab1BaJCBq1q9IH1HDfCf/kSMb0H02UDNBumrt1s5/oaiiY75S3kigpt +# XHCKV/AKbi+4vORYdBwJhWvy4kug+zZHWAp7+K4rVeAaMbWOitriFPQaSdZlOkNT +# VKMbYUtX6VEnhyfr5O390TxPJCdkDznT6rAf5FlQEkim7wb2gb/Osi73KF0dtnRd +# 0ePNT4GxulYTrAhHsZmyuinh/FIyqJDtW3C+2eVt/lx6GJfAtDA/gVCe9mAL4bYv +# 7vjocHeLvWJ8bc0PYMCRZXMsU2zkJqVJ7pZWq+z5hGQAHemrQj9hUZBXCEFVZ8dd +# jEZbIsg2nwZOFakhvdAvvGlTXPbRMOCHblToXAKA8ksRbLob8CDL6Cstoy5VL3al +# fOAWLj3FYITlvGvAYCtJzHrHdqyphL805Co1syR6YDopR8tDrxgWzJKAby5fIolP +# 7SfunXsCa0n3xx80aaxR0apljIXZWBSMGZdJmVzASQFxlexNsU1PmaLmmpn0fih/ +# 567e+kyzFG1wiw5btwttSW9hKgCX+yze1B4IK4yquTQSfPikrCpbBaFZ0OlWmOnL +# r0eKAKY3WDyxYgNyeg6KeCc= +# SIG # End signature block diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/SheshaCodeAnalyzers.Test.csproj b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/SheshaCodeAnalyzers.Test.csproj new file mode 100644 index 0000000000..f0c15f7fd4 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/SheshaCodeAnalyzers.Test.csproj @@ -0,0 +1,28 @@ + + + + netcoreapp3.1 + + true + true + + + + + + + + + + + + + + + + + + + + + diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/SheshaCodeAnalyzersUnitTests.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/SheshaCodeAnalyzersUnitTests.cs new file mode 100644 index 0000000000..ea8bcdebc8 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/SheshaCodeAnalyzersUnitTests.cs @@ -0,0 +1,187 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; +using System.Threading.Tasks; +using VerifyCS = SheshaCodeAnalyzers.Test.CSharpCodeFixVerifier< + SheshaCodeAnalyzers.RestrictedStaticPropertyAnalyzer, + SheshaCodeAnalyzers.RestrictedStaticPropertyCodeFixProvider>; + +namespace SheshaCodeAnalyzers.Test +{ + [TestClass] + public class SheshaCodeAnalyzersUnitTest + { + //No diagnostics expected to show up + [TestMethod] + public async Task TestMethod1() + { + var test = @""; + + await VerifyCS.VerifyAnalyzerAsync(test); + } + + //Diagnostic and CodeFix both triggered and checked for + [TestMethod] + public async Task TestMethod2() + { + const string testCode = @" +using System; +using Abp.Dependency; + +class TestClass +{ + void TestMethod() + { + var repository = {|#0:IocManager.Instance|}.Resolve(); + var time = {|#1:DateTime.Now|}; + } +}"; + var expectedValidation = VerifyCS.Diagnostic(RestrictedStaticPropertyAnalyzer.DiagnosticId) + .WithLocation(0) + .WithArguments("Abp.Dependency.IocManager", "Instance"); + await VerifyCS.VerifyAnalyzerAsync(testCode, expectedValidation); + + + var test = @" + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using System.Diagnostics; + + namespace ConsoleApplication1 + { + class {|#0:TypeName|} + { + } + }"; + + var fixtest = @" + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using System.Diagnostics; + + namespace ConsoleApplication1 + { + class TYPENAME + { + } + }"; + + var expected = VerifyCS.Diagnostic(RestrictedStaticPropertyAnalyzer.DiagnosticId).WithLocation(0).WithArguments("TypeName"); + + await VerifyCS.VerifyCodeFixAsync(test, expected, fixtest); + } + + [TestMethod] + public async Task TestAnalyzer() + { + const string testCode = @" +using System; +using Abp.Dependency; + +public interface IRepository +{ +} + +class TestClass +{ + void TestMethod() + { + var repository = {|#0:IocManager.Instance|}.Resolve(); + } +}"; + + const string fixedCode = @"using Abp.Dependency; +using Shesha.Services; +using System; + +public interface IRepository +{ +} + +class TestClass +{ + void TestMethod() + { + var repository = StaticContext.IocManager.Resolve(); + } +}"; + + const string abpCode = @" +using System; + +namespace Abp.Dependency { + public class IocManager { + public static IocManager Instance; + public T Resolve() + { + throw new NotImplementedException(); + } + } +} +"; + const string shaCode = @" +using System; +using Abp.Dependency; + +namespace Shesha.Services { + public static class StaticContext { + public static IocManager IocManager; + } +} +"; + + var expected = VerifyCS.Diagnostic(RestrictedStaticPropertyAnalyzer.DiagnosticId).WithLocation(0).WithArguments("Abp.Dependency.IocManager", "Instance"); + + var testAnalyze = new VerifyCS.Test + { + TestState = { + Sources = { testCode, abpCode, shaCode }, + }, + FixedState = { + Sources = { fixedCode, abpCode, shaCode } + }, + // Disable automatic verification + TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck | TestBehaviors.SkipSuppressionCheck, + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipFixAllCheck + + }; + testAnalyze.TestState.ExpectedDiagnostics.Add(expected); + + // Need to store the solution outside the event handler + Solution fixedSolution = null; + + // Register a callback for when fixes are computed + testAnalyze.SolutionTransforms.Add((solution, projectId) => + { + fixedSolution = solution; + return solution; // Return unchanged + }); + + try + { + await testAnalyze.RunAsync(); + } + catch + { + throw; + } + + // Assert + Assert.IsNotNull(fixedSolution); + + var doc = testAnalyze.TestState.Sources.First(); + var fixedDocument = fixedSolution.GetDocumentIdsWithFilePath(doc.filename); + + var docId = fixedDocument.First(); + var fixedDoc = fixedSolution.GetDocument(docId); + var fixedText = await fixedDoc.GetTextAsync(); + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpAnalyzerVerifier`1+Test.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpAnalyzerVerifier`1+Test.cs new file mode 100644 index 0000000000..672fd9b293 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpAnalyzerVerifier`1+Test.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class CSharpAnalyzerVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + { + public class Test : CSharpAnalyzerTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + var compilationOptions = solution.GetProject(projectId).CompilationOptions; + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + + return solution; + }); + } + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs new file mode 100644 index 0000000000..a0117591cc --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs @@ -0,0 +1,38 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using System.Threading; +using System.Threading.Tasks; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class CSharpAnalyzerVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + { + /// + public static DiagnosticResult Diagnostic() + => CSharpAnalyzerVerifier.Diagnostic(); + + /// + public static DiagnosticResult Diagnostic(string diagnosticId) + => CSharpAnalyzerVerifier.Diagnostic(diagnosticId); + + /// + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => CSharpAnalyzerVerifier.Diagnostic(descriptor); + + /// + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new Test + { + TestCode = source, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeFixVerifier`2+Test.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeFixVerifier`2+Test.cs new file mode 100644 index 0000000000..b8205edde2 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeFixVerifier`2+Test.cs @@ -0,0 +1,28 @@ +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class CSharpCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() + { + public class Test : CSharpCodeFixTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + var compilationOptions = solution.GetProject(projectId).CompilationOptions; + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + + return solution; + }); + } + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeFixVerifier`2.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeFixVerifier`2.cs new file mode 100644 index 0000000000..5753001fd4 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeFixVerifier`2.cs @@ -0,0 +1,61 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using System.Threading; +using System.Threading.Tasks; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class CSharpCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() + { + /// + public static DiagnosticResult Diagnostic() + => CSharpCodeFixVerifier.Diagnostic(); + + /// + public static DiagnosticResult Diagnostic(string diagnosticId) + => CSharpCodeFixVerifier.Diagnostic(diagnosticId); + + /// + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => CSharpCodeFixVerifier.Diagnostic(descriptor); + + /// + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new Test + { + TestCode = source, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + /// + public static async Task VerifyCodeFixAsync(string source, string fixedSource) + => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) + => await VerifyCodeFixAsync(source, new[] { expected }, fixedSource); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource) + { + var test = new Test + { + TestCode = source, + FixedCode = fixedSource, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1+Test.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1+Test.cs new file mode 100644 index 0000000000..2be88732be --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1+Test.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class CSharpCodeRefactoringVerifier + where TCodeRefactoring : CodeRefactoringProvider, new() + { + public class Test : CSharpCodeRefactoringTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + var compilationOptions = solution.GetProject(projectId).CompilationOptions; + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + + return solution; + }); + } + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1.cs new file mode 100644 index 0000000000..2be77a9ffd --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1.cs @@ -0,0 +1,36 @@ +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Testing; +using System.Threading; +using System.Threading.Tasks; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class CSharpCodeRefactoringVerifier + where TCodeRefactoring : CodeRefactoringProvider, new() + { + /// + public static async Task VerifyRefactoringAsync(string source, string fixedSource) + { + await VerifyRefactoringAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + } + + /// + public static async Task VerifyRefactoringAsync(string source, DiagnosticResult expected, string fixedSource) + { + await VerifyRefactoringAsync(source, new[] { expected }, fixedSource); + } + + /// + public static async Task VerifyRefactoringAsync(string source, DiagnosticResult[] expected, string fixedSource) + { + var test = new Test + { + TestCode = source, + FixedCode = fixedSource, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpVerifierHelper.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpVerifierHelper.cs new file mode 100644 index 0000000000..5afe9f7328 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/CSharpVerifierHelper.cs @@ -0,0 +1,33 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System; +using System.Collections.Immutable; + +namespace SheshaCodeAnalyzers.Test +{ + internal static class CSharpVerifierHelper + { + /// + /// By default, the compiler reports diagnostics for nullable reference types at + /// , and the analyzer test framework defaults to only validating + /// diagnostics at . This map contains all compiler diagnostic IDs + /// related to nullability mapped to , which is then used to enable all + /// of these warnings for default validation during analyzer and code fix tests. + /// + internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); + + private static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = { "/warnaserror:nullable" }; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + // Workaround for https://github.com/dotnet/roslyn/issues/41610 + nullableWarnings = nullableWarnings + .SetItem("CS8632", ReportDiagnostic.Error) + .SetItem("CS8669", ReportDiagnostic.Error); + + return nullableWarnings; + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicAnalyzerVerifier`1+Test.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicAnalyzerVerifier`1+Test.cs new file mode 100644 index 0000000000..d7106ecc97 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicAnalyzerVerifier`1+Test.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.VisualBasic.Testing; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class VisualBasicAnalyzerVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + { + public class Test : VisualBasicAnalyzerTest + { + public Test() + { + } + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicAnalyzerVerifier`1.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicAnalyzerVerifier`1.cs new file mode 100644 index 0000000000..74f5a83984 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicAnalyzerVerifier`1.cs @@ -0,0 +1,38 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.VisualBasic.Testing; +using System.Threading; +using System.Threading.Tasks; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class VisualBasicAnalyzerVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + { + /// + public static DiagnosticResult Diagnostic() + => VisualBasicAnalyzerVerifier.Diagnostic(); + + /// + public static DiagnosticResult Diagnostic(string diagnosticId) + => VisualBasicAnalyzerVerifier.Diagnostic(diagnosticId); + + /// + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => VisualBasicAnalyzerVerifier.Diagnostic(descriptor); + + /// + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new Test + { + TestCode = source, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeFixVerifier`2+Test.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeFixVerifier`2+Test.cs new file mode 100644 index 0000000000..6909947892 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeFixVerifier`2+Test.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.VisualBasic.Testing; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class VisualBasicCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() + { + public class Test : VisualBasicCodeFixTest + { + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeFixVerifier`2.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeFixVerifier`2.cs new file mode 100644 index 0000000000..8691e3d070 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeFixVerifier`2.cs @@ -0,0 +1,61 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.VisualBasic.Testing; +using System.Threading; +using System.Threading.Tasks; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class VisualBasicCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() + { + /// + public static DiagnosticResult Diagnostic() + => VisualBasicCodeFixVerifier.Diagnostic(); + + /// + public static DiagnosticResult Diagnostic(string diagnosticId) + => VisualBasicCodeFixVerifier.Diagnostic(diagnosticId); + + /// + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => VisualBasicCodeFixVerifier.Diagnostic(descriptor); + + /// + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new Test + { + TestCode = source, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + /// + public static async Task VerifyCodeFixAsync(string source, string fixedSource) + => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) + => await VerifyCodeFixAsync(source, new[] { expected }, fixedSource); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource) + { + var test = new Test + { + TestCode = source, + FixedCode = fixedSource, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeRefactoringVerifier`1+Test.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeRefactoringVerifier`1+Test.cs new file mode 100644 index 0000000000..114864f01a --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeRefactoringVerifier`1+Test.cs @@ -0,0 +1,14 @@ +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.VisualBasic.Testing; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class VisualBasicCodeRefactoringVerifier + where TCodeRefactoring : CodeRefactoringProvider, new() + { + public class Test : VisualBasicCodeRefactoringTest + { + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeRefactoringVerifier`1.cs b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeRefactoringVerifier`1.cs new file mode 100644 index 0000000000..f78362f926 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Test/Verifiers/VisualBasicCodeRefactoringVerifier`1.cs @@ -0,0 +1,36 @@ +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Testing; +using System.Threading; +using System.Threading.Tasks; + +namespace SheshaCodeAnalyzers.Test +{ + public static partial class VisualBasicCodeRefactoringVerifier + where TCodeRefactoring : CodeRefactoringProvider, new() + { + /// + public static async Task VerifyRefactoringAsync(string source, string fixedSource) + { + await VerifyRefactoringAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + } + + /// + public static async Task VerifyRefactoringAsync(string source, DiagnosticResult expected, string fixedSource) + { + await VerifyRefactoringAsync(source, new[] { expected }, fixedSource); + } + + /// + public static async Task VerifyRefactoringAsync(string source, DiagnosticResult[] expected, string fixedSource) + { + var test = new Test + { + TestCode = source, + FixedCode = fixedSource, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Vsix/SheshaCodeAnalyzers.Vsix.csproj b/shesha-core/analyzers/SheshaCodeAnalyzers.Vsix/SheshaCodeAnalyzers.Vsix.csproj new file mode 100644 index 0000000000..3a25f14d5d --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Vsix/SheshaCodeAnalyzers.Vsix.csproj @@ -0,0 +1,48 @@ + + + + + + net472 + SheshaCodeAnalyzers.Vsix + SheshaCodeAnalyzers.Vsix + + + + false + false + false + false + false + false + Roslyn + + + + + + + + Program + $(DevEnvDir)devenv.exe + /rootsuffix $(VSSDKTargetPlatformRegRootSuffix) + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers.Vsix/source.extension.vsixmanifest b/shesha-core/analyzers/SheshaCodeAnalyzers.Vsix/source.extension.vsixmanifest new file mode 100644 index 0000000000..3e357977b2 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers.Vsix/source.extension.vsixmanifest @@ -0,0 +1,24 @@ + + + + + SheshaCodeAnalyzers + This is a sample diagnostic extension for the .NET Compiler Platform ("Roslyn"). + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers/Resources.Designer.cs b/shesha-core/analyzers/SheshaCodeAnalyzers/Resources.Designer.cs new file mode 100644 index 0000000000..d7f04b38d3 --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers/Resources.Designer.cs @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +namespace SheshaCodeAnalyzers +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SheshaCodeAnalyzers.Resources", typeof(Resources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Type names should be all uppercase.. + /// + internal static string AnalyzerDescription + { + get + { + return ResourceManager.GetString("AnalyzerDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type name '{0}' contains lowercase letters. + /// + internal static string AnalyzerMessageFormat + { + get + { + return ResourceManager.GetString("AnalyzerMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type name contains lowercase letters. + /// + internal static string AnalyzerTitle + { + get + { + return ResourceManager.GetString("AnalyzerTitle", resourceCulture); + } + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers/Resources.resx b/shesha-core/analyzers/SheshaCodeAnalyzers/Resources.resx new file mode 100644 index 0000000000..410edccd7c --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers/Resources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Type names should be all uppercase. + An optional longer localizable description of the diagnostic. + + + Type name '{0}' contains lowercase letters + The format-able message the diagnostic displays. + + + Type name contains lowercase letters + The title of the diagnostic. + + \ No newline at end of file diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers/RestrictedStaticPropertyAnalyzer.cs b/shesha-core/analyzers/SheshaCodeAnalyzers/RestrictedStaticPropertyAnalyzer.cs new file mode 100644 index 0000000000..4103acf6fe --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers/RestrictedStaticPropertyAnalyzer.cs @@ -0,0 +1,75 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace SheshaCodeAnalyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class RestrictedStaticPropertyAnalyzer : DiagnosticAnalyzer + { + private static readonly ImmutableDictionary RestrictedProperties = + ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("Abp.Dependency.IocManager", new[] { "Instance" }), + //new KeyValuePair("System.Environment", new[] { "CurrentDirectory", "ExitCode" }), + //new KeyValuePair("System.DateTime", new[] { "Now", "UtcNow" }) + }); + + public const string DiagnosticId = "SHA001"; + private const string Category = "Usage"; + + private static readonly LocalizableString Title = "Restricted static property access"; + private static readonly LocalizableString MessageFormat = "Usage of property '{1}' from static class '{0}' is not allowed"; + private static readonly LocalizableString Description = "Certain static properties are restricted from usage in this codebase."; + + private static readonly DiagnosticDescriptor Rule = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, + DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); + } + + private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) + { + var memberAccess = (MemberAccessExpressionSyntax)context.Node; + + if (!(memberAccess.Name is IdentifierNameSyntax propertyNameSyntax)) + return; + + var propertyName = propertyNameSyntax.Identifier.Text; + var expression = memberAccess.Expression; + var typeInfo = context.SemanticModel.GetTypeInfo(expression); + + if (typeInfo.Type == null) + return; + + var fullTypeName = typeInfo.Type.ToDisplayString(); + + if (!RestrictedProperties.TryGetValue(fullTypeName, out var restrictedProps)) + return; + + if (!restrictedProps.Contains(propertyName)) + return; + + var diagnostic = Diagnostic.Create( + Rule, + memberAccess.GetLocation(), + fullTypeName, + propertyName); + + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/shesha-core/analyzers/SheshaCodeAnalyzers/SheshaCodeAnalyzers.csproj b/shesha-core/analyzers/SheshaCodeAnalyzers/SheshaCodeAnalyzers.csproj new file mode 100644 index 0000000000..7176c81d7f --- /dev/null +++ b/shesha-core/analyzers/SheshaCodeAnalyzers/SheshaCodeAnalyzers.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + false + + + *$(MSBuildProjectFile)* + + + + + + + + + + + + + diff --git a/shesha-core/local-packages/SheshaCodeAnalyzers.1.0.0.nupkg b/shesha-core/local-packages/SheshaCodeAnalyzers.1.0.0.nupkg new file mode 100644 index 0000000000..1fd9e4794f Binary files /dev/null and b/shesha-core/local-packages/SheshaCodeAnalyzers.1.0.0.nupkg differ diff --git a/shesha-core/Directory.Build.props b/shesha-core/src/Directory.Build.props similarity index 50% rename from shesha-core/Directory.Build.props rename to shesha-core/src/Directory.Build.props index aa7ca60e8b..1ad5c39ca8 100644 --- a/shesha-core/Directory.Build.props +++ b/shesha-core/src/Directory.Build.props @@ -1,8 +1,9 @@ - - true - $(NoWarn);CS1591;612;618;809 - + + enable + true + $(NoWarn);CS1591;612;618;809;AsyncFixer01;8618; + True @@ -10,4 +11,10 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/shesha-core/src/Shesha.Application/AbpAsyncCrudAppService.cs b/shesha-core/src/Shesha.Application/AbpAsyncCrudAppService.cs index 85751527c2..be088a3d6e 100644 --- a/shesha-core/src/Shesha.Application/AbpAsyncCrudAppService.cs +++ b/shesha-core/src/Shesha.Application/AbpAsyncCrudAppService.cs @@ -2,11 +2,9 @@ using Abp.Application.Services.Dto; using Abp.Domain.Entities; using Abp.Domain.Repositories; -using Abp.Linq; -using System; -using System.Collections.Generic; +using Abp.Linq.Extensions; +using Shesha.Application.Services.Dto; using System.Linq; -using System.Text; using System.Threading.Tasks; namespace Shesha @@ -29,7 +27,7 @@ protected AbpAsyncCrudAppService(IRepository repository) } public abstract class AbpAsyncCrudAppService - : AbpAsyncCrudAppService + : AbpAsyncCrudAppService where TEntity : class, IEntity where TEntityDto : IEntityDto { @@ -169,6 +167,22 @@ protected virtual Task GetEntityByIdAsync(TPrimaryKey id) { return Repository.GetAsync(id); } - } + protected override IQueryable ApplyPaging(IQueryable query, TGetAllInput input) + { + if (input is IPagedResultRequest pagedInput) + { + if (pagedInput.SkipCount > 0) + query = query.Skip(pagedInput.SkipCount); + } + + if (input is ILimitedResultRequest limitedInput) + { + if (limitedInput.MaxResultCount > 0 && limitedInput.MaxResultCount < int.MaxValue) + query = query.Take(limitedInput.MaxResultCount); + } + + return query; + } + } } diff --git a/shesha-core/src/Shesha.Application/AbpCrudAppService.cs b/shesha-core/src/Shesha.Application/AbpCrudAppService.cs index 0ce0b11814..d797494bec 100644 --- a/shesha-core/src/Shesha.Application/AbpCrudAppService.cs +++ b/shesha-core/src/Shesha.Application/AbpCrudAppService.cs @@ -22,15 +22,15 @@ public abstract class AbpCrudAppService Repository; - protected virtual string GetPermissionName { get; set; } + protected virtual string? GetPermissionName { get; set; } - protected virtual string GetAllPermissionName { get; set; } + protected virtual string? GetAllPermissionName { get; set; } - protected virtual string CreatePermissionName { get; set; } + protected virtual string? CreatePermissionName { get; set; } - protected virtual string UpdatePermissionName { get; set; } + protected virtual string? UpdatePermissionName { get; set; } - protected virtual string DeletePermissionName { get; set; } + protected virtual string? DeletePermissionName { get; set; } protected AbpCrudAppService(IRepository repository) { @@ -131,7 +131,7 @@ protected virtual void MapToEntity(TUpdateInput updateInput, TEntity entity) ObjectMapper.Map(updateInput, entity); } - protected virtual void CheckPermission(string permissionName) + protected virtual void CheckPermission(string? permissionName) { if (!string.IsNullOrEmpty(permissionName)) { diff --git a/shesha-core/src/Shesha.Application/Api/ApiAppService.cs b/shesha-core/src/Shesha.Application/Api/ApiAppService.cs index c9a773756a..d5478c2925 100644 --- a/shesha-core/src/Shesha.Application/Api/ApiAppService.cs +++ b/shesha-core/src/Shesha.Application/Api/ApiAppService.cs @@ -16,7 +16,7 @@ namespace Shesha.Api /// /// API application service /// - public class ApiAppService: SheshaAppServiceBase//, IApiAppService + public class ApiAppService : SheshaAppServiceBase//, IApiAppService { private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider; private readonly IPermissionedObjectManager _permissionedObjectManager; @@ -31,7 +31,7 @@ IPermissionedObjectManager permissionedObjectManager } [HttpGet] - public async Task> EndpointsAsync(string term, string verb, int maxResultCount = 10) + public async Task> EndpointsAsync(string? term, string? verb, int maxResultCount = 10) { var actionDescriptors = _apiDescriptionsProvider.ApiDescriptionGroups.Items.SelectMany(g => g.Items.Select(gi => gi.ActionDescriptor)).ToList(); @@ -42,19 +42,20 @@ public async Task> EndpointsAsync(string term, string if (await _permissionedObjectManager.IsActionDescriptorEnabledAsync(actionDescriptor)) permissioned.Add(actionDescriptor); - var allEndpoints = permissioned.SelectMany(desc => { + var allEndpoints = permissioned.SelectMany(desc => + { var verbs = desc.ActionConstraints?.OfType().SelectMany(c => c.HttpMethods).Distinct().ToList() ?? new List { "" }; - var actionDescriptor = desc as ControllerActionDescriptor; + return desc is ControllerActionDescriptor actionDescriptor + ? verbs.Select(v => new ApiEndpointInfo + { + HttpVerb = v.ToLower(), + Url = "/" + desc.AttributeRouteInfo?.Template?.TrimStart('/'), - return verbs.Select(v => new ApiEndpointInfo - { - HttpVerb = v.ToLower(), - Url = "/" + desc.AttributeRouteInfo?.Template.TrimStart('/'), - - ActionName = actionDescriptor?.ActionName, - ControllerName = actionDescriptor?.ControllerName, - }); + ActionName = actionDescriptor.ActionName, + ControllerName = actionDescriptor.ControllerName, + }) + : new List(); }) .Where(i => !string.IsNullOrWhiteSpace(i.Url)) .ToList(); diff --git a/shesha-core/src/Shesha.Application/Api/ApiQuery.cs b/shesha-core/src/Shesha.Application/Api/ApiQuery.cs deleted file mode 100644 index cfaad8ee8c..0000000000 --- a/shesha-core/src/Shesha.Application/Api/ApiQuery.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Abp.Application.Services.Dto; -using Abp.Dependency; -using GraphQL; -using GraphQL.Types; -using Microsoft.AspNetCore.Mvc.ActionConstraints; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Linq; -using Shesha.Api.Dto; -using Shesha.Application.Services.Dto; -using Shesha.GraphQL.Provider.GraphTypes; -using Shesha.JsonLogic; -using Shesha.QuickSearch; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Shesha.Api -{ - public class ApiQuery: ObjectGraphType, ITransientDependency - { - private readonly IJsonLogic2LinqConverter _jsonLogicConverter; - - public ApiQuery(IServiceProvider serviceProvider) - { - _jsonLogicConverter = serviceProvider.GetRequiredService(); - - FieldAsync>( - "endpoints", - arguments: new QueryArguments( - new QueryArgument> { Name = "input", DefaultValue = new FilteredPagedAndSortedResultRequestDto() } - ), - resolve: context => { - var input = context.GetArgument("input"); - - var apiDescriptionsProvider = serviceProvider.GetService(); - var quickSearcher = serviceProvider.GetRequiredService(); - - var actionDescriptors = apiDescriptionsProvider.ApiDescriptionGroups.Items.SelectMany(g => g.Items.Select(gi => gi.ActionDescriptor)).ToList(); - - var allEndpoints = actionDescriptors.SelectMany(desc => { - var verbs = desc.ActionConstraints.OfType().SelectMany(c => c.HttpMethods).Distinct().ToList(); - - var actionDescriptor = desc as ControllerActionDescriptor; - - /* - if (actionDescriptor != null && actionDescriptor.ActionName.Contains("Query")) - { - XmlCommentsNodeNameHelper.GetMemberNameForType(); - } - */ - - return verbs.Select(v => new ApiEndpointInfo - { - HttpVerb = v, - Url = desc.AttributeRouteInfo?.Template, - - ActionName = actionDescriptor?.ActionName, - ControllerName = actionDescriptor?.ControllerName, - }); - }) - .Where(i => !string.IsNullOrWhiteSpace(i.Url)) - .AsQueryable(); - - var endpoints = AddFilter(allEndpoints, input.Filter); - - // add quick search - if (!string.IsNullOrWhiteSpace(input.QuickSearch)) - endpoints = endpoints.Where(ep => ep.Url.Contains(input.QuickSearch, StringComparison.InvariantCultureIgnoreCase) || (ep.Description ?? "").Contains(input.QuickSearch, StringComparison.InvariantCultureIgnoreCase)); - - // apply paging - var pageQuery = endpoints.Skip(input.SkipCount); - if (input.MaxResultCount > 0) - pageQuery = pageQuery.Take(input.MaxResultCount); - - var result = new PagedResultDto - { - Items = pageQuery.ToList(), - TotalCount = allEndpoints.Count() - }; - - return Task.FromResult(result); - } - ); - } - - /// - /// Add filter to - /// - /// Queryable to be filtered - /// String representation of JsonLogic filter - /// - private IQueryable AddFilter(IQueryable query, string filter) - { - if (string.IsNullOrWhiteSpace(filter)) - return query; - - var jsonLogic = JObject.Parse(filter); - - var expression = _jsonLogicConverter.ParseExpressionOf(jsonLogic); - - return expression != null - ? query.Where(expression) - : query; - } - } -} diff --git a/shesha-core/src/Shesha.Application/Api/ApiSchema.cs b/shesha-core/src/Shesha.Application/Api/ApiSchema.cs deleted file mode 100644 index b9070ab46e..0000000000 --- a/shesha-core/src/Shesha.Application/Api/ApiSchema.cs +++ /dev/null @@ -1,25 +0,0 @@ -using GraphQL; -using GraphQL.Types; -using Shesha.Api.Dto; -using Shesha.GraphQL.Provider; -using Shesha.GraphQL.Provider.GraphTypes; -using System; - -namespace Shesha.Api -{ - /// - /// API schema - /// - public class ApiSchema: Schema - { - public ApiSchema(IServiceProvider serviceProvider) : base(serviceProvider) - { - Query = new ApiQuery(serviceProvider); - - this.RegisterTypeMapping>(); - this.RegisterTypeMapping, GraphQLGenericType>>(); - - this.NameConverter = ShaCamelCaseNameConverter.Instance; - } - } -} diff --git a/shesha-core/src/Shesha.Application/Api/Dto/ApiEndpointInfo.cs b/shesha-core/src/Shesha.Application/Api/Dto/ApiEndpointInfo.cs index 993cf105f8..a6137f4aa5 100644 --- a/shesha-core/src/Shesha.Application/Api/Dto/ApiEndpointInfo.cs +++ b/shesha-core/src/Shesha.Application/Api/Dto/ApiEndpointInfo.cs @@ -1,19 +1,14 @@ -using System.Collections.Generic; - -namespace Shesha.Api.Dto +namespace Shesha.Api.Dto { /// /// API endpoint info /// public class ApiEndpointInfo { - //public List HttpVerbs { get; set; } = new List(); - public string HttpVerb { get; set; } - public string Url { get; set; } - public string Description { get; set; } - - - public string ActionName { get; set; } - public string ControllerName { get; set; } + public required string HttpVerb { get; set; } + public required string Url { get; set; } + public string? Description { get; set; } + public required string ActionName { get; set; } + public required string ControllerName { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Areas/AreaAppService.cs b/shesha-core/src/Shesha.Application/Areas/AreaAppService.cs index 26ce0b3463..36563a3e94 100644 --- a/shesha-core/src/Shesha.Application/Areas/AreaAppService.cs +++ b/shesha-core/src/Shesha.Application/Areas/AreaAppService.cs @@ -26,7 +26,7 @@ public AreaAppService(IRepository repository, IRepository> AutocompleteAsync(string term, RefListAreaType? areaType, Guid? parentAreaId) + public async Task> AutocompleteAsync(string? term, RefListAreaType? areaType, Guid? parentAreaId) { term = (term ?? "").ToLower(); @@ -51,7 +51,7 @@ public async Task> AutocompleteAsync(string term, RefL else { var areas = await Repository.GetAll() - .Where(p => (p.Name ?? "").ToLower().Contains(term) && (areaType == null || p.AreaType == areaType) && (parentAreaId == null || p.ParentArea.Id == parentAreaId)) + .Where(p => (p.Name ?? "").ToLower().Contains(term) && (areaType == null || p.AreaType == areaType) && (parentAreaId == null || p.ParentArea != null && p.ParentArea.Id == parentAreaId)) .OrderBy(p => p.Name) .Take(10) .Select(p => new AutocompleteItemDto @@ -88,7 +88,7 @@ public async Task> CascadeSelectAsync(RefListAreaType? else { var areas = await Repository.GetAll() - .Where(p => (areaType == null || p.AreaType == areaType) && (parentAreaId == null || p.ParentArea.Id == parentAreaId)) + .Where(p => (areaType == null || p.AreaType == areaType) && (parentAreaId == null || p.ParentArea != null && p.ParentArea.Id == parentAreaId)) .OrderBy(p => p.Name) .Select(p => new AutocompleteItemDto { diff --git a/shesha-core/src/Shesha.Application/Areas/Dto/AreaCreateDto.cs b/shesha-core/src/Shesha.Application/Areas/Dto/AreaCreateDto.cs index 0be30e6b88..7f02f45d59 100644 --- a/shesha-core/src/Shesha.Application/Areas/Dto/AreaCreateDto.cs +++ b/shesha-core/src/Shesha.Application/Areas/Dto/AreaCreateDto.cs @@ -8,10 +8,10 @@ namespace Shesha.Areas.Dto [AutoMapTo(typeof(Area))] public class AreaCreateDto { - public string Name { get; set; } - public string ShortName { get; set; } - public EntityReferenceDto ParentArea { get; set; } - public string Comments { get; set; } - public ReferenceListItemValueDto AreaType { get; set; } + public required string Name { get; set; } + public string? ShortName { get; set; } + public EntityReferenceDto? ParentArea { get; set; } + public string? Comments { get; set; } + public ReferenceListItemValueDto? AreaType { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/Areas/Dto/AreaDto.cs b/shesha-core/src/Shesha.Application/Areas/Dto/AreaDto.cs index 547e478c5f..9bc7ccbebf 100644 --- a/shesha-core/src/Shesha.Application/Areas/Dto/AreaDto.cs +++ b/shesha-core/src/Shesha.Application/Areas/Dto/AreaDto.cs @@ -1,18 +1,18 @@ -using System; -using Abp.Application.Services.Dto; +using Abp.Application.Services.Dto; using Abp.AutoMapper; using Shesha.AutoMapper.Dto; using Shesha.Domain; +using System; namespace Shesha.Areas.Dto { [AutoMap(typeof(Area))] public class AreaDto: EntityDto { - public string Name { get; set; } - public string ShortName { get; set; } - public EntityReferenceDto ParentArea { get; set; } - public string Comments { get; set; } - public ReferenceListItemValueDto AreaType { get; set; } + public required string Name { get; set; } + public string? ShortName { get; set; } + public EntityReferenceDto? ParentArea { get; set; } + public string? Comments { get; set; } + public ReferenceListItemValueDto? AreaType { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/Areas/Dto/AreaMapProfile.cs b/shesha-core/src/Shesha.Application/Areas/Dto/AreaMapProfile.cs index 12d8c8d370..52e567deed 100644 --- a/shesha-core/src/Shesha.Application/Areas/Dto/AreaMapProfile.cs +++ b/shesha-core/src/Shesha.Application/Areas/Dto/AreaMapProfile.cs @@ -1,13 +1,11 @@ -using System; -using Abp.Application.Services.Dto; using Abp.Domain.Entities; using Abp.Domain.Repositories; using AutoMapper; using Shesha.AutoMapper; using Shesha.AutoMapper.Dto; using Shesha.Domain; -using Shesha.Extensions; using Shesha.Services; +using System; namespace Shesha.Areas.Dto { @@ -20,7 +18,7 @@ public AreaMapProfile() CreateMap() .ForMember(u => u.ParentArea, options => options.MapFrom(e => e.ParentArea != null - ? new EntityReferenceDto(e.ParentArea.Id, e.ParentArea.Name, e.ParentArea.GetClassName()) + ? new EntityReferenceDto(e.ParentArea) : null)) .MapReferenceListValuesToDto(); @@ -46,7 +44,7 @@ public AreaMapProfile() } // todo: implement automapper convention for nested entities - private static T GetEntity(TId id) where T : class, IEntity + private static T? GetEntity(TId id) where T : class, IEntity { if (id == null || id is Guid guid && guid == Guid.Empty) return null; diff --git a/shesha-core/src/Shesha.Application/Areas/Dto/AreaUpdateDto.cs b/shesha-core/src/Shesha.Application/Areas/Dto/AreaUpdateDto.cs index 1176c0ce2e..f3ec4f9362 100644 --- a/shesha-core/src/Shesha.Application/Areas/Dto/AreaUpdateDto.cs +++ b/shesha-core/src/Shesha.Application/Areas/Dto/AreaUpdateDto.cs @@ -6,9 +6,9 @@ namespace Shesha.Areas.Dto { public class AreaUpdateDto : EntityDto { - public string Name { get; set; } - public string ShortName { get; set; } - public string Comments { get; set; } - public ReferenceListItemValueDto AreaType { get; set; } + public required string Name { get; set; } + public string? ShortName { get; set; } + public string? Comments { get; set; } + public ReferenceListItemValueDto? AreaType { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/Authorization/Accounts/AccountAppService.cs b/shesha-core/src/Shesha.Application/Authorization/Accounts/AccountAppService.cs deleted file mode 100644 index 3018980f64..0000000000 --- a/shesha-core/src/Shesha.Application/Authorization/Accounts/AccountAppService.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Threading.Tasks; -using Abp.Configuration; -using Abp.Zero.Configuration; -using Microsoft.AspNetCore.Mvc; -using Shesha.Authorization.Accounts.Dto; -using Shesha.Authorization.Users; -using Shesha.Services; - -namespace Shesha.Authorization.Accounts -{ - /* - public class AccountAppService : SheshaAppServiceBase, IAccountAppService - { - private readonly UserRegistrationManager _userRegistrationManager; - - public AccountAppService(UserRegistrationManager userRegistrationManager) - { - _userRegistrationManager = userRegistrationManager; - } - - public async Task IsTenantAvailable(IsTenantAvailableInput input) - { - var tenant = await TenantManager.FindByTenancyNameAsync(input.TenancyName); - if (tenant == null) - { - return new IsTenantAvailableOutput(TenantAvailabilityState.NotFound); - } - - if (!tenant.IsActive) - { - return new IsTenantAvailableOutput(TenantAvailabilityState.InActive); - } - - return new IsTenantAvailableOutput(TenantAvailabilityState.Available, tenant.Id); - } - - public async Task Register(RegisterInput input) - { - var user = await _userRegistrationManager.RegisterAsync( - input.Name, - input.Surname, - input.EmailAddress, - input.UserName, - input.Password, - true // Assumed email address is always confirmed. Change this if you want to implement email confirmation. - ); - - var isEmailConfirmationRequiredForLogin = await SettingManager.GetSettingValueAsync(AbpZeroSettingNames.UserManagement.IsEmailConfirmationRequiredForLogin); - - return new RegisterOutput - { - CanLogin = user.IsActive && (user.IsEmailConfirmed || !isEmailConfirmationRequiredForLogin) - }; - } - } - */ -} diff --git a/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/IsTenantAvailableInput.cs b/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/IsTenantAvailableInput.cs deleted file mode 100644 index 5d2448e99d..0000000000 --- a/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/IsTenantAvailableInput.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Abp.MultiTenancy; - -namespace Shesha.Authorization.Accounts.Dto -{ - public class IsTenantAvailableInput - { - [Required] - [StringLength(AbpTenantBase.MaxTenancyNameLength)] - public string TenancyName { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/IsTenantAvailableOutput.cs b/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/IsTenantAvailableOutput.cs deleted file mode 100644 index 1ee0194f6a..0000000000 --- a/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/IsTenantAvailableOutput.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Shesha.Authorization.Accounts.Dto -{ - public class IsTenantAvailableOutput - { - public TenantAvailabilityState State { get; set; } - - public int? TenantId { get; set; } - - public IsTenantAvailableOutput() - { - } - - public IsTenantAvailableOutput(TenantAvailabilityState state, int? tenantId = null) - { - State = state; - TenantId = tenantId; - } - } -} diff --git a/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/RegisterInput.cs b/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/RegisterInput.cs deleted file mode 100644 index 27d4e83aa3..0000000000 --- a/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/RegisterInput.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using Abp.Auditing; -using Abp.Authorization.Users; -using Abp.Extensions; -using Shesha.Validation; - -namespace Shesha.Authorization.Accounts.Dto -{ - public class RegisterInput : IValidatableObject - { - [Required] - [StringLength(AbpUserBase.MaxNameLength)] - public string Name { get; set; } - - [Required] - [StringLength(AbpUserBase.MaxSurnameLength)] - public string Surname { get; set; } - - [Required] - [StringLength(AbpUserBase.MaxUserNameLength)] - public string UserName { get; set; } - - [Required] - [EmailAddress] - [StringLength(AbpUserBase.MaxEmailAddressLength)] - public string EmailAddress { get; set; } - - [Required] - [StringLength(AbpUserBase.MaxPlainPasswordLength)] - [DisableAuditing] - public string Password { get; set; } - - [DisableAuditing] - public string CaptchaResponse { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - if (!UserName.IsNullOrEmpty()) - { - if (!UserName.Equals(EmailAddress) && ValidationHelper.IsEmail(UserName)) - { - yield return new ValidationResult("UserName cannot be an email address unless it's the same as your email address!"); - } - } - } - } -} diff --git a/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/RegisterOutput.cs b/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/RegisterOutput.cs deleted file mode 100644 index bed607555d..0000000000 --- a/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/RegisterOutput.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Shesha.Authorization.Accounts.Dto -{ - public class RegisterOutput - { - public bool CanLogin { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/TenantAvailabilityState.cs b/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/TenantAvailabilityState.cs deleted file mode 100644 index ee5723aa5e..0000000000 --- a/shesha-core/src/Shesha.Application/Authorization/Accounts/Dto/TenantAvailabilityState.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Shesha.Authorization.Accounts.Dto -{ - public enum TenantAvailabilityState - { - Available = 1, - InActive, - NotFound - } -} diff --git a/shesha-core/src/Shesha.Application/Authorization/Accounts/IAccountAppService.cs b/shesha-core/src/Shesha.Application/Authorization/Accounts/IAccountAppService.cs deleted file mode 100644 index a849bc17a7..0000000000 --- a/shesha-core/src/Shesha.Application/Authorization/Accounts/IAccountAppService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Abp.Application.Services; -using Shesha.Authorization.Accounts.Dto; -using System.Threading.Tasks; - -namespace Shesha.Authorization.Accounts -{ - public interface IAccountAppService : IApplicationService - { - Task IsTenantAvailableAsync(IsTenantAvailableInput input); - - Task RegisterAsync(RegisterInput input); - } -} diff --git a/shesha-core/src/Shesha.Application/Authorization/ApiAuthorizationHelper.cs b/shesha-core/src/Shesha.Application/Authorization/ApiAuthorizationHelper.cs index cd191eccab..a668ab66f7 100644 --- a/shesha-core/src/Shesha.Application/Authorization/ApiAuthorizationHelper.cs +++ b/shesha-core/src/Shesha.Application/Authorization/ApiAuthorizationHelper.cs @@ -37,30 +37,29 @@ ISecuritySettings securitySettings _securitySettings = securitySettings; } - public override async Task AuthorizeAsync(MethodInfo methodInfo, Type type) + public override async Task AuthorizeAsync(MethodInfo methodInfo, Type? type) { if (!_authConfiguration.IsEnabled) - { return; - } - if (type.HasAttribute() || methodInfo.HasAttribute() - || type.HasAttribute() || methodInfo.HasAttribute()) + if (type == null || + type.HasAttribute() || methodInfo.HasAttribute() || + type.HasAttribute() || methodInfo.HasAttribute()) return; var shaServiceType = typeof(ApplicationService); var controllerType = typeof(ControllerBase); - if (type == null || !shaServiceType.IsAssignableFrom(type) && !controllerType.IsAssignableFrom(type)) + if (!shaServiceType.IsAssignableFrom(type) && !controllerType.IsAssignableFrom(type)) return; - var typeName = type.FullName; + var typeName = type.GetRequiredFullName(); var methodName = methodInfo.Name.RemovePostfix("Async"); var isCrud = type.FindBaseGenericType(typeof(AbpCrudAppService<,,,,,>)) != null; if (isCrud && PermissionedObjectManager.CrudMethods.ContainsKey(methodName)) return; - var securitySettings = await _securitySettings?.SecuritySettings?.GetValueAsync(); + var securitySettings = await _securitySettings.SecuritySettings.GetValueOrNullAsync(); var settings = securitySettings?.DefaultEndpointAccess; if (settings == null) diff --git a/shesha-core/src/Shesha.Application/Authorization/EntityCrudAuthorizationHelper.cs b/shesha-core/src/Shesha.Application/Authorization/EntityCrudAuthorizationHelper.cs index 48402dacf7..844b718121 100644 --- a/shesha-core/src/Shesha.Application/Authorization/EntityCrudAuthorizationHelper.cs +++ b/shesha-core/src/Shesha.Application/Authorization/EntityCrudAuthorizationHelper.cs @@ -10,7 +10,6 @@ using Shesha.Permissions; using Shesha.Utilities; using System; -using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; @@ -36,16 +35,14 @@ ILocalizationManager localizationManager _objectPermissionChecker = objectPermissionChecker; } - public override async Task AuthorizeAsync(MethodInfo methodInfo, Type type) + public override async Task AuthorizeAsync(MethodInfo methodInfo, Type? type) { if (!_authConfiguration.IsEnabled) { return; } - var method = PermissionedObjectManager.CrudMethods.ContainsKey(methodInfo.Name.RemovePostfix("Async")) - ? PermissionedObjectManager.CrudMethods[methodInfo.Name.RemovePostfix("Async")] - : null; + var method = PermissionedObjectManager.GetCrudMethod(methodInfo.Name); // It is not a Crud method if (method == null) return; @@ -59,19 +56,10 @@ public override async Task AuthorizeAsync(MethodInfo methodInfo, Type type) // It is not an entity Crud Api if (entityType == null || !Abp.Domain.Entities.EntityHelper.IsEntity(entityType)) return; - /*var isDynamic = type.GetInterfaces().Any(x => - x.IsGenericType && - x.GetGenericTypeDefinition() == typeof(IDynamicCrudAppService<,,>));*/ - var config = AsyncHelper.RunSync(() => _entityConfigCache.GetEntityConfigAsync(entityType)); if (config == null) return; - //if (!config.GenerateAppService) - //{ - // throw new AbpAuthorizationException($"Service for entity type `{config.FullClassName}` is not configured"); - //} - // ToDo: add RequireAll flag await _objectPermissionChecker.AuthorizeAsync(false, config.FullClassName, method, ShaPermissionedObjectsTypes.EntityAction, AbpSession.UserId.HasValue); } diff --git a/shesha-core/src/Shesha.Application/Authorization/ISheshaAuthorizationHelper.cs b/shesha-core/src/Shesha.Application/Authorization/ISheshaAuthorizationHelper.cs index 34d81cb34c..67b9ed511a 100644 --- a/shesha-core/src/Shesha.Application/Authorization/ISheshaAuthorizationHelper.cs +++ b/shesha-core/src/Shesha.Application/Authorization/ISheshaAuthorizationHelper.cs @@ -6,6 +6,6 @@ namespace Shesha.Authorization { public interface ISheshaAuthorizationHelper { - Task AuthorizeAsync(MethodInfo methodInfo, Type type); + Task AuthorizeAsync(MethodInfo methodInfo, Type? type); } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Authorization/JwtTokenHelper.cs b/shesha-core/src/Shesha.Application/Authorization/JwtTokenHelper.cs index 0a1b75a77c..6acd0f2f93 100644 --- a/shesha-core/src/Shesha.Application/Authorization/JwtTokenHelper.cs +++ b/shesha-core/src/Shesha.Application/Authorization/JwtTokenHelper.cs @@ -7,7 +7,7 @@ namespace Shesha.Authorization { public static class JwtTokenHelper { - public static JwtSecurityToken GetSecurityToken(this HttpContext httpContext) + public static JwtSecurityToken? GetSecurityToken(this HttpContext httpContext) { var cookies = httpContext?.Request.Cookies; var jwtToken = cookies?["Abp.AuthToken"]; @@ -18,7 +18,7 @@ public static JwtSecurityToken GetSecurityToken(this HttpContext httpContext) return handler.ReadToken(jwtToken) as JwtSecurityToken; } - public static string GetUsernameFromJwtToken(this HttpContext httpContext) + public static string? GetUsernameFromJwtToken(this HttpContext httpContext) { var token = httpContext.GetSecurityToken(); return token?.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Name)?.Value; diff --git a/shesha-core/src/Shesha.Application/Authorization/Settings/AuthorizationSettingsAppService.cs b/shesha-core/src/Shesha.Application/Authorization/Settings/AuthorizationSettingsAppService.cs index 52c911f802..4f8eb8b377 100644 --- a/shesha-core/src/Shesha.Application/Authorization/Settings/AuthorizationSettingsAppService.cs +++ b/shesha-core/src/Shesha.Application/Authorization/Settings/AuthorizationSettingsAppService.cs @@ -54,17 +54,17 @@ public async Task GetSettingsAsync() var dto = new AuthorizationSettingsDto(); //Lockout - dto.IsLockoutEnabled = await _securitySettings.UserLockOutEnabled.GetValueAsync(); - dto.DefaultAccountLockoutSeconds = await _securitySettings.DefaultAccountLockoutSeconds.GetValueAsync(); - dto.MaxFailedAccessAttemptsBeforeLockout = await _securitySettings.MaxFailedAccessAttemptsBeforeLockout.GetValueAsync(); + dto.IsLockoutEnabled = await _securitySettings.UserLockOutEnabled.GetValueOrNullAsync(); + dto.DefaultAccountLockoutSeconds = await _securitySettings.DefaultAccountLockoutSeconds.GetValueOrNullAsync(); + dto.MaxFailedAccessAttemptsBeforeLockout = await _securitySettings.MaxFailedAccessAttemptsBeforeLockout.GetValueOrNullAsync(); dto.AutoLogoffTimeout = settings.AutoLogoffTimeout; //Password complexity - dto.RequireDigit = await _passwordComplexitySettings.RequireDigit.GetValueAsync(); - dto.RequireLowercase = await _passwordComplexitySettings.RequireLowercase.GetValueAsync(); - dto.RequireNonAlphanumeric = await _passwordComplexitySettings.RequireNonAlphanumeric.GetValueAsync(); - dto.RequireUppercase = await _passwordComplexitySettings.RequireUppercase.GetValueAsync(); - dto.RequiredLength = await _passwordComplexitySettings.RequiredLength.GetValueAsync(); + dto.RequireDigit = await _passwordComplexitySettings.RequireDigit.GetValueOrNullAsync(); + dto.RequireLowercase = await _passwordComplexitySettings.RequireLowercase.GetValueOrNullAsync(); + dto.RequireNonAlphanumeric = await _passwordComplexitySettings.RequireNonAlphanumeric.GetValueOrNullAsync(); + dto.RequireUppercase = await _passwordComplexitySettings.RequireUppercase.GetValueOrNullAsync(); + dto.RequiredLength = await _passwordComplexitySettings.RequiredLength.GetValueOrNullAsync(); // Password reset dto.ResetPasswordWithEmailLinkIsSupported = settings.UseResetPasswordViaEmailLink; diff --git a/shesha-core/src/Shesha.Application/Authorization/SheshaAuthorizationFilter.cs b/shesha-core/src/Shesha.Application/Authorization/SheshaAuthorizationFilter.cs index f21fc0c4e5..ea53ba2b4e 100644 --- a/shesha-core/src/Shesha.Application/Authorization/SheshaAuthorizationFilter.cs +++ b/shesha-core/src/Shesha.Application/Authorization/SheshaAuthorizationFilter.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Abp.AspNetCore.Mvc.Authorization; -using Abp.AspNetCore.Mvc.Extensions; +using Abp.AspNetCore.Mvc.Extensions; using Abp.AspNetCore.Mvc.Results; using Abp.Authorization; using Abp.Dependency; @@ -14,6 +10,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Threading.Tasks; namespace Shesha.Authorization { @@ -39,7 +37,7 @@ public SheshaAuthorizationFilter( public virtual async Task OnAuthorizationAsync(AuthorizationFilterContext context) { - var endpoint = context?.HttpContext?.GetEndpoint(); + var endpoint = context.HttpContext?.GetEndpoint(); // Allow Anonymous skips all authorization if (endpoint?.Metadata.GetMetadata() != null) { @@ -71,9 +69,10 @@ await authorization.AuthorizeAsync( if (ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType)) { + var identity = context.HttpContext?.User.Identity; context.Result = new ObjectResult(new AjaxResponse(_errorInfoBuilder.BuildForException(ex), true)) { - StatusCode = context.HttpContext.User.Identity.IsAuthenticated + StatusCode = identity != null && identity.IsAuthenticated ? (int)System.Net.HttpStatusCode.Forbidden : (int)System.Net.HttpStatusCode.Unauthorized }; diff --git a/shesha-core/src/Shesha.Application/Authorization/TokenAuthController.cs b/shesha-core/src/Shesha.Application/Authorization/TokenAuthController.cs index 59b27818d6..27f2d24609 100644 --- a/shesha-core/src/Shesha.Application/Authorization/TokenAuthController.cs +++ b/shesha-core/src/Shesha.Application/Authorization/TokenAuthController.cs @@ -71,7 +71,7 @@ public async Task AuthenticateAsync([FromBody] Authenti // Check for user registration status var registration = await _userRegistration.FirstOrDefaultAsync(e => e.UserNameOrEmailAddress == model.UserNameOrEmailAddress); - if (registration != null && !registration.IsComplete) + if (registration != null && !registration.IsComplete && registration.AdditionalRegistrationInfoForm != null) { // Return a result indicating a client-side redirect return new AuthenticateResultModel @@ -96,17 +96,21 @@ public async Task AuthenticateAsync([FromBody] Authenti return authenticateResult; } - private async Task GetAuthenticateResultAsync(ShaLoginResult loginResult, string imei) + private async Task GetAuthenticateResultAsync(ShaLoginResult loginResult, string? imei) { + if (!loginResult.IsSuccess) + throw new ArgumentException("Can't create token for invalid login result", nameof(loginResult)); + var accessToken = CreateAccessToken(CreateJwtClaims(loginResult.Identity)); var expireInSeconds = (int)_configuration.Expiration.TotalSeconds; - var personId = loginResult?.User != null - ? _personRepository.GetAll() + var personId = loginResult.User != null + ? await _personRepository.GetAll() .Where(p => p.User == loginResult.User) .OrderBy(p => p.CreationTime) - .Select(p => p.Id).FirstOrDefault() + .Select(p => p.Id) + .FirstOrDefaultAsync() : (Guid?)null; var device = !string.IsNullOrWhiteSpace(imei) ? await _mobileDeviceRepository.FirstOrDefaultAsync(e => e.IMEI == imei.Trim()) @@ -118,7 +122,7 @@ private async Task GetAuthenticateResultAsync(ShaLoginR EncryptedAccessToken = GetEncryptedAccessToken(accessToken), ExpireInSeconds = expireInSeconds, ExpireOn = DateTime.Now.AddSeconds(expireInSeconds), - UserId = loginResult.User.Id, + UserId = loginResult.User?.Id, PersonId = personId, DeviceName = device?.Name, ResultType = AuthenticateResultType.Success @@ -140,7 +144,7 @@ public bool SignOff() [HttpPost] public async Task OtpAuthenticateSendPinAsync(string userNameOrMobileNo) { - var persons = await _personRepository.GetAll().Where(u => u.MobileNumber1 == userNameOrMobileNo || u.User.UserName == userNameOrMobileNo).ToListAsync(); + var persons = await _personRepository.GetAll().Where(u => u.MobileNumber1 == userNameOrMobileNo || u.User != null && u.User.UserName == userNameOrMobileNo).ToListAsync(); if (!persons.Any()) throw new UserFriendlyException("User with the specified mobile number not found"); if (persons.Count() > 1) @@ -151,6 +155,9 @@ public async Task OtpAuthenticateSendPinAsync(st if (person.User == null) throw new UserFriendlyException("User with the specified mobile has no internal account"); + if (string.IsNullOrWhiteSpace(person.MobileNumber1)) + throw new UserFriendlyException("User has no mobile no"); + var sendPinResponse = await _logInManager.SendLoginOtpAsync(person.User, person.MobileNumber1); return new OtpAuthenticateSendPinResponse @@ -193,6 +200,9 @@ public async Task ExternalAuthenticateAsync([Fr { case ShaLoginResultType.Success: { + if (!loginResult.IsSuccess) + throw new ArgumentException("Can't create token for invalid login result", nameof(loginResult)); + var accessToken = CreateAccessToken(CreateJwtClaims(loginResult.Identity)); return new ExternalAuthenticateResultModel { @@ -214,7 +224,7 @@ public async Task ExternalAuthenticateAsync([Fr // Try to login again with newly registered user! loginResult = await _logInManager.LoginAsync(new UserLoginInfo(model.AuthProvider, model.ProviderKey, model.AuthProvider), GetTenancyNameOrNull()); - if (loginResult.Result != ShaLoginResultType.Success) + if (!loginResult.IsSuccess) { throw _shaLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt( loginResult.Result, @@ -278,17 +288,15 @@ private async Task GetExternalUserInfoAsync(ExternalAuthen } [DebuggerStepThrough] - private string GetTenancyNameOrNull() + private string? GetTenancyNameOrNull() { if (!AbpSession.TenantId.HasValue) - { return null; - } return _tenantCache.GetOrNull(AbpSession.TenantId.Value)?.TenancyName; } - private async Task> GetLoginResultAsync(string usernameOrEmailAddress, string password, string imei, string tenancyName) + private async Task> GetLoginResultAsync(string usernameOrEmailAddress, string password, string? imei, string? tenancyName) { var loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, imei, tenancyName); diff --git a/shesha-core/src/Shesha.Application/ConfigMigrations/package20250228_1711.shaconfig b/shesha-core/src/Shesha.Application/ConfigMigrations/package20250228_1711.shaconfig new file mode 100644 index 0000000000..7771268567 Binary files /dev/null and b/shesha-core/src/Shesha.Application/ConfigMigrations/package20250228_1711.shaconfig differ diff --git a/shesha-core/src/Shesha.Application/ConfigMigrations/package20250618_1441.shaconfig b/shesha-core/src/Shesha.Application/ConfigMigrations/package20250618_1441.shaconfig new file mode 100644 index 0000000000..9a5f8598eb Binary files /dev/null and b/shesha-core/src/Shesha.Application/ConfigMigrations/package20250618_1441.shaconfig differ diff --git a/shesha-core/src/Shesha.Application/ConfigurationItems/ConfigurationItemAppService.cs b/shesha-core/src/Shesha.Application/ConfigurationItems/ConfigurationItemAppService.cs index 0d6072847d..beb00505dc 100644 --- a/shesha-core/src/Shesha.Application/ConfigurationItems/ConfigurationItemAppService.cs +++ b/shesha-core/src/Shesha.Application/ConfigurationItems/ConfigurationItemAppService.cs @@ -3,6 +3,7 @@ using Abp.Domain.Entities; using Abp.Domain.Repositories; using Abp.Runtime.Validation; +using Abp.UI; using Microsoft.AspNetCore.Mvc; using Shesha.Application.Services.Dto; using Shesha.ConfigurationItems.Cache; @@ -16,6 +17,7 @@ using Shesha.Dto.Interfaces; using Shesha.Extensions; using Shesha.Mvc; +using Shesha.Validations; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -57,7 +59,10 @@ public async Task ExportPackageAsync(PackageExportInput input) var pack = await _packageManager.PreparePackageAsync(exportСontext); +#pragma warning disable IDISP001 // Dispose created var zipStream = new MemoryStream(); +#pragma warning restore IDISP001 // Dispose created + await _packageManager.PackAsync(pack, zipStream); var fileName = $"package{DateTime.Now:yyyyMMdd_HHmm}.shaconfig"; @@ -71,12 +76,15 @@ public async Task ExportPackageAsync(PackageExportInput input) [Consumes("multipart/form-data")] public async Task AnalyzePackageAsync([FromForm] AnalyzePackageInput input) { + if (input.File == null) + throw new UserFriendlyException("Please upload a package"); + using (var zipStream = input.File.OpenReadStream()) { var context = new ReadPackageContext { SkipUnsupportedItems = true }; - var package = await _packageManager.ReadPackageAsync(zipStream, context); + using var package = await _packageManager.ReadPackageAsync(zipStream, context); var result = new AnalyzePackageResult(); @@ -137,7 +145,7 @@ public async Task ImportPackageAsync([FromForm] PackageImpo using (var zipStream = input.File.OpenReadStream()) { var context = new ReadPackageContext(); - var package = await _packageManager.ReadPackageAsync(zipStream, context); + using var package = await _packageManager.ReadPackageAsync(zipStream, context); var importContext = new PackageImportContext { CreateModules = true, @@ -173,8 +181,8 @@ public async Task UpdateStatusAsync(UpdateItemStatusInput input) var validationResults = new List(); if (string.IsNullOrWhiteSpace(input.Filter)) validationResults.Add(new ValidationResult("Filter is mandatory", new string[] { nameof(input.Filter) })); - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + + validationResults.ThrowValidationExceptionIfAny(L); var items = await _itemsRepository.GetAllFiltered(input.Filter).ToListAsync(); diff --git a/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/AnalyzePackageInput.cs b/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/AnalyzePackageInput.cs index 369862b676..21148ab35f 100644 --- a/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/AnalyzePackageInput.cs +++ b/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/AnalyzePackageInput.cs @@ -14,6 +14,6 @@ public class AnalyzePackageInput /// [Required] [BindProperty(Name = "file")] - public IFormFile File { get; set; } + public IFormFile? File { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/AnalyzePackageResult.cs b/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/AnalyzePackageResult.cs index bb5d433c94..850e8dcc5f 100644 --- a/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/AnalyzePackageResult.cs +++ b/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/AnalyzePackageResult.cs @@ -17,21 +17,21 @@ public class AnalyzePackageResult public class PackageItemDto { public Guid? Id { get; set; } - public string Name { get; set; } - public string Label { get; set; } - public string Description { get; set; } - public string FrontEndApplication { get; set; } + public required string Name { get; set; } + public string? Label { get; set; } + public string? Description { get; set; } + public string? FrontEndApplication { get; set; } } public class PackageItemTypeDto { - public string Name { get; set; } + public required string Name { get; set; } public List Items { get; set; } = new List(); } public class PackageModuleDto { - public string Name { get; set; } + public required string Name { get; set; } public List ItemTypes { get; set; } = new List(); } } diff --git a/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/ItemsToImportBinder.cs b/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/ItemsToImportBinder.cs index 435a8adf1e..3bdc33924c 100644 --- a/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/ItemsToImportBinder.cs +++ b/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/ItemsToImportBinder.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; +using Shesha.Extensions; +using Shesha.Reflection; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -15,7 +17,7 @@ public Task BindModelAsync(ModelBindingContext bindingContext) { var model = bindingContext.Model as ICollection; if (model == null) - model = Activator.CreateInstance(targetType) as ICollection; + model = ActivatorHelper.CreateNotNullObject(targetType).ForceCastAs>(); var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); @@ -23,9 +25,12 @@ public Task BindModelAsync(ModelBindingContext bindingContext) { foreach (var value in valueProviderResult.Values) { - var items = JsonConvert.DeserializeObject>(value); - foreach (var item in items) - model.Add(item); + if (!string.IsNullOrWhiteSpace(value)) + { + var items = JsonConvert.DeserializeObject>(value).NotNull(); + foreach (var item in items) + model.Add(item); + } } bindingContext.Result = ModelBindingResult.Success(model); diff --git a/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/PackageImportInput.cs b/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/PackageImportInput.cs index 8787d0e19f..9160992f63 100644 --- a/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/PackageImportInput.cs +++ b/shesha-core/src/Shesha.Application/ConfigurationItems/Dtos/PackageImportInput.cs @@ -23,6 +23,6 @@ public class PackageImportInput /// List of items to import /// [BindProperty(BinderType = typeof(ItemsToImportBinder))] - public List ItemsToImport { get; set; } = new List(); + public List ItemsToImport { get; set; } = new(); } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/DeviceForceUpdate/DeviceForceUpdateAppService.cs b/shesha-core/src/Shesha.Application/DeviceForceUpdate/DeviceForceUpdateAppService.cs index c05602802b..b74fdef426 100644 --- a/shesha-core/src/Shesha.Application/DeviceForceUpdate/DeviceForceUpdateAppService.cs +++ b/shesha-core/src/Shesha.Application/DeviceForceUpdate/DeviceForceUpdateAppService.cs @@ -27,7 +27,7 @@ public async override Task CreateAsync(DeviceForceUpdateDt return input; } - public async Task GetForceUpdateByOSTypeAsync(int osType) + public async Task GetForceUpdateByOSTypeAsync(int osType) { var items = await Repository.GetAll().Where(r => r.OSType == osType && !r.IsDeleted).ToListAsync(); var currentForceUpdate = items.FirstOrDefault(); diff --git a/shesha-core/src/Shesha.Application/DeviceForceUpdate/Dto/DeviceForceUpdateDto.cs b/shesha-core/src/Shesha.Application/DeviceForceUpdate/Dto/DeviceForceUpdateDto.cs index 7f7f021f85..8c1ec02bff 100644 --- a/shesha-core/src/Shesha.Application/DeviceForceUpdate/Dto/DeviceForceUpdateDto.cs +++ b/shesha-core/src/Shesha.Application/DeviceForceUpdate/Dto/DeviceForceUpdateDto.cs @@ -1,9 +1,6 @@ using Abp.Application.Services.Dto; using Abp.AutoMapper; using System; -using System.Collections.Generic; -using System.Text; -using Shesha.Domain; namespace Shesha.DeviceForceUpdate.Dto { @@ -18,6 +15,5 @@ public class DeviceForceUpdateDto : EntityDto public string AppStoreUrl { get; set; } public int? OSType { get; set; } public double? VersionNo { get; set; } - } } diff --git a/shesha-core/src/Shesha.Application/DynamicCrudAppService.cs b/shesha-core/src/Shesha.Application/DynamicCrudAppService.cs index af404250ac..1f19338d63 100644 --- a/shesha-core/src/Shesha.Application/DynamicCrudAppService.cs +++ b/shesha-core/src/Shesha.Application/DynamicCrudAppService.cs @@ -2,18 +2,15 @@ using Abp.Dependency; using Abp.Domain.Entities; using Abp.Domain.Repositories; -using Abp.Runtime.Validation; using Microsoft.AspNetCore.Mvc; using Shesha.Application.Services.Dto; using Shesha.Attributes; -using Shesha.DelayedUpdate; using Shesha.DynamicEntities; using Shesha.DynamicEntities.Dtos; using Shesha.GraphQL.Mvc; +using Shesha.Validations; using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Threading.Tasks; namespace Shesha @@ -25,6 +22,7 @@ public class DynamicCrudAppService where TCreateDynamicDto: class, TDynamicDto where TUpdateDynamicDto : class, TDynamicDto + where TPrimaryKey : notnull { public DynamicCrudAppService( @@ -102,15 +100,14 @@ private async Task InternalUpdateAsync(TDynamicDto input) var entity = await Repository.GetAsync(input.Id); var result = await MapDynamicDtoToEntityAsync(input, entity); - if (result.HasValidationError) - throw new AbpValidationException("Please correct the errors and try again", result.ValidationResults); + + result.ValidationResults.ThrowValidationExceptionIfAny(L); await Repository.UpdateAsync(entity); await UnitOfWorkManager.Current.SaveChangesAsync(); result = await DelayedUpdateAsync(input, entity); - if (result.HasValidationError) - throw new AbpValidationException("Please correct the errors and try again", result.ValidationResults); + result.ValidationResults.ThrowValidationExceptionIfAny(L); return entity; } @@ -140,12 +137,10 @@ private async Task InternalCreateAsync(TCreateDynamicDto input) await Repository.InsertAsync(entity); await UnitOfWorkManager.Current.SaveChangesAsync(); }); - if (result.HasValidationError) - throw new AbpValidationException("Please correct the errors and try again", result.ValidationResults); + result.ValidationResults.ThrowValidationExceptionIfAny(L); result = await DelayedUpdateAsync(input, entity); - if (result.HasValidationError) - throw new AbpValidationException("Please correct the errors and try again", result.ValidationResults); + result.ValidationResults.ThrowValidationExceptionIfAny(L); await UnitOfWorkManager.Current.SaveChangesAsync(); return entity; @@ -170,8 +165,9 @@ public virtual async Task> CreateGqlAsync(string prop [DynamicControllerNameConvention] public class DynamicCrudAppService : DynamicCrudAppService, ITransientDependency - where TEntity : class, IEntity - where TDynamicDto : class, IDynamicDto + where TEntity : class, IEntity + where TDynamicDto : class, IDynamicDto + where TPrimaryKey : notnull { public DynamicCrudAppService( IRepository repository diff --git a/shesha-core/src/Shesha.Application/DynamicDtoMapingResult.cs b/shesha-core/src/Shesha.Application/DynamicDtoMapingResult.cs index 508ac3e5d6..f28c61ed3b 100644 --- a/shesha-core/src/Shesha.Application/DynamicDtoMapingResult.cs +++ b/shesha-core/src/Shesha.Application/DynamicDtoMapingResult.cs @@ -1,10 +1,6 @@ -using Shesha.DelayedUpdate; -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha { diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/DefaultEntityActionRouteProvider.cs b/shesha-core/src/Shesha.Application/DynamicEntities/DefaultEntityActionRouteProvider.cs index fc247bcc64..6b80101213 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/DefaultEntityActionRouteProvider.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/DefaultEntityActionRouteProvider.cs @@ -16,7 +16,7 @@ public string GetDynamicAppServiceRoute(Type entityType, ActionModel action) return routeTemplate; } - private string GetConfigurableModuleName(Type entityType) + private string? GetConfigurableModuleName(Type entityType) { return entityType.GetConfigurableModuleName(); } diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/BaseEntitySyncResponse.cs b/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/BaseEntitySyncResponse.cs index aa1297ca18..4c65527999 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/BaseEntitySyncResponse.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/BaseEntitySyncResponse.cs @@ -8,7 +8,7 @@ namespace Shesha.DynamicEntities.Dtos /// public class BaseEntitySyncResponse { - public string Accessor { get; set; } + public string? Accessor { get; set; } [JsonConverter(typeof(StringEnumConverter))] public SyncStatus Status { get; set; } } diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/EntityConfigurationDto.cs b/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/EntityConfigurationDto.cs index 6cebab4dcb..beb4884081 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/EntityConfigurationDto.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/EntityConfigurationDto.cs @@ -11,7 +11,7 @@ public class EntityConfigurationDto : EntityApiItemBase public class EntityApiItemBase { public string Name { get; set; } - public string Description { get; set; } + public string? Description { get; set; } public string Accessor { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ModuleSyncRequest.cs b/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ModuleSyncRequest.cs index 7d1a8ed3eb..1a33301769 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ModuleSyncRequest.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ModuleSyncRequest.cs @@ -8,7 +8,7 @@ namespace Shesha.DynamicEntities.Dtos /// public class ModuleSyncRequest { - public string Accessor { get; set; } + public string? Accessor { get; set; } public List Entities { get; set; } = new List(); } } diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ModuleSyncResponse.cs b/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ModuleSyncResponse.cs index 5bf28b84f9..ae36c84b89 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ModuleSyncResponse.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ModuleSyncResponse.cs @@ -9,7 +9,7 @@ namespace Shesha.DynamicEntities.Dtos /// public class ModuleSyncResponse { - public string Accessor { get; set; } + public string? Accessor { get; set; } [JsonConverter(typeof(StringEnumConverter))] public SyncStatus Status { get; set; } public List Entities { get; set; } = new List(); diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ReorderResponse.cs b/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ReorderResponse.cs index 9493d2cbe7..70317eeffe 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ReorderResponse.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/Dtos/ReorderResponse.cs @@ -7,16 +7,10 @@ namespace Shesha.DynamicEntities.Dtos /// /// Entities reordering response /// - public class ReorderResponse: IReorderResponse + public class ReorderResponse: IReorderResponse where TId : notnull { - /* - /// - /// Reordered items - /// - public List> Items { get; set; } = new List>(); - */ [JsonIgnore] - public Dictionary ReorderedItems { get; } = new Dictionary(); + public Dictionary ReorderedItems { get; } = []; public IDictionary Items => ReorderedItems; } diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/DynamicAppServiceHelper.cs b/shesha-core/src/Shesha.Application/DynamicEntities/DynamicAppServiceHelper.cs index f37e98a985..22323273dd 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/DynamicAppServiceHelper.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/DynamicAppServiceHelper.cs @@ -8,7 +8,7 @@ namespace Shesha.DynamicEntities /// public static class DynamicAppServiceHelper { - public static Type MakeApplicationServiceType(Type entityType) + public static Type? MakeApplicationServiceType(Type entityType) { var idType = entityType.GetProperty("Id")?.PropertyType; if (idType == null) diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/DynamicControllerNameConvention.cs b/shesha-core/src/Shesha.Application/DynamicEntities/DynamicControllerNameConvention.cs index 91433b01e9..b25295779a 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/DynamicControllerNameConvention.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/DynamicControllerNameConvention.cs @@ -23,7 +23,7 @@ public void Apply(ControllerModel controller) var entityType = controller.ControllerType.GenericTypeArguments[0]; - var entityAttribute = entityType.GetAttribute(); + var entityAttribute = entityType.GetAttributeOrNull(); controller.ControllerName = !string.IsNullOrWhiteSpace(entityAttribute?.ApplicationServiceName) ? entityAttribute.ApplicationServiceName : entityType.Name + "Crud"; diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoBinderProvider.cs b/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoBinderProvider.cs index 748870cc41..52c6d1d8ff 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoBinderProvider.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoBinderProvider.cs @@ -52,7 +52,7 @@ public DynamicDtoBinderProvider( } /// - public IModelBinder GetBinder(ModelBinderProviderContext context) + public IModelBinder? GetBinder(ModelBinderProviderContext context) { if (context == null) { diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoInterceptor.cs b/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoInterceptor.cs index c138075059..1c98ecf917 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoInterceptor.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoInterceptor.cs @@ -1,19 +1,16 @@ using Castle.DynamicProxy; -using System; +using Shesha.Extensions; +using Shesha.Reflection; using System.Collections.Generic; -using System.Linq; namespace Shesha.DynamicEntities { - - // ToDo AS: Don't use. Is not completed! - //[Obsolete("Don't use. Is not completed!")] public class DynamicDtoInterceptor : IInterceptor { private string _name; private DynamicDtoInterceptor _parent; - private Dictionary _changes = new Dictionary(); + private Dictionary _changes = new (); private bool _isChanged = false; @@ -33,7 +30,7 @@ public void ResetState() _changes.Clear(); } - public void Change(string name, object value) + public void Change(string name, object? value) { if (_changes.ContainsKey(name)) _changes[name] = value; else _changes.Add(name, value); @@ -59,11 +56,6 @@ public void Intercept(IInvocation invocation) invocation.ReturnValue = _isChanged; return; } - /*if (invocation.Method.Name == $"set_{nameof(IDynamicDtoInputProxy.IsChanged)}") - { - _isChanged = (bool)invocation.Arguments[0]; - return; - }*/ if (invocation.Method.Name.StartsWith("set_")) { @@ -72,7 +64,7 @@ public void Intercept(IInvocation invocation) if (prop != null) { var val = prop.GetValue(invocation.InvocationTarget, null); - if (!val.Equals(invocation.Arguments[0])) + if (!val.NullEquals(invocation.Arguments[0])) { Change(prop.Name, val); } diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoModelBinder.cs b/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoModelBinder.cs index 874a5a1009..a429144b9e 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoModelBinder.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/DynamicDtoModelBinder.cs @@ -12,9 +12,6 @@ using Newtonsoft.Json.Linq; using Shesha.DynamicEntities.Dtos; using Shesha.Extensions; -using Shesha.JsonEntities; -using Shesha.Reflection; -using Shesha.Services; using System; using System.Collections.Generic; using System.IO; @@ -34,8 +31,9 @@ public class DynamicDtoModelBinder : IModelBinder private readonly IList _formatters; private readonly Func _readerFactory; private readonly ILogger _logger; - private readonly MvcOptions _options; + private readonly MvcOptions? _options; private readonly IDynamicDtoTypeBuilder _dtoBuilder; + private IDynamicMappingSettings _defaultSettings = new DynamicMappingSettings(); /// /// Creates a new . @@ -56,6 +54,16 @@ public DynamicDtoModelBinder( { } + /// + /// Set default binding settings. + /// Note: is used for unit tests only + /// + /// + public void SetDefaultSettings(IDynamicMappingSettings defaultSettings) + { + _defaultSettings = defaultSettings; + } + /// /// Creates a new . /// @@ -71,7 +79,7 @@ public DynamicDtoModelBinder( IList formatters, IHttpRequestStreamReaderFactory readerFactory, ILoggerFactory loggerFactory, - MvcOptions options, + MvcOptions? options, IDynamicDtoTypeBuilder dynamicDtoTypeBuilder) { if (formatters == null) @@ -107,7 +115,7 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) var bindingSettings = (defaultMetadata != null ? defaultMetadata.Attributes.ParameterAttributes?.OfType().FirstOrDefault() : null) - ?? new DynamicMappingSettings(); + ?? _defaultSettings; // Special logic for body, treat the model name as string.Empty for the top level // object, but allow an override via BinderModelName. The purpose of this is to try @@ -159,7 +167,7 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) _readerFactory, AllowEmptyBody); - var formatter = (IInputFormatter)null; + IInputFormatter? formatter = null; for (var i = 0; i < _formatters.Count; i++) { if (_formatters[i].CanRead(formatterContext)) @@ -229,26 +237,16 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) AddFormFieldsProperty = true, }; var effectiveModelType = await _dtoBuilder.BuildDtoProxyTypeAsync(buildContext); - var mapper = GetMapper(result.Model.GetType(), effectiveModelType, fullDtoBuildContext.Classes); - model = mapper.Map(result.Model, result.Model.GetType(), effectiveModelType); + var mapper = GetMapper(modelWithFormFields.GetType(), effectiveModelType, fullDtoBuildContext.Classes); + model = mapper.Map(result.Model, modelWithFormFields.GetType(), effectiveModelType); } if (model is IHasJObjectField modelDynamicDto) { // Add JObject only if not a DtoProxy - modelDynamicDto._jObject = !bindingSettings.UseDynamicDtoProxy - ? Newtonsoft.Json.Linq.JObject.Parse(body) + modelDynamicDto._jObject = !bindingSettings.UseDynamicDtoProxy && !string.IsNullOrWhiteSpace(body) + ? JObject.Parse(body) : null; - - // Attempt to proxy DTo to determine changed properties - /*if (!modelType.IsGenericType) - { - var interceptor = new DynamicDtoInterceptor(); - MakeProxy(model, interceptor); - model = new ProxyGenerator().CreateClassProxyWithTarget( - modelDynamicDto.GetType(), new[] { typeof(IDynamicDtoInputProxy) }, modelDynamicDto, interceptor); - (model as IDynamicDtoInputProxy).ResetState(); - }*/ } bindingContext.Result = ModelBindingResult.Success(model); @@ -342,7 +340,7 @@ private async Task GetRequestBodyAsync(HttpContext httpContext) } } - private void FillPropertyNamesRecursive(JObject jsonObject, List propertyNames, string prefix = "") + private void FillPropertyNamesRecursive(JObject? jsonObject, List propertyNames, string prefix = "") { if (jsonObject == null) return; @@ -359,7 +357,9 @@ private List GetFormFieldsFromBody(string body) { var propertyNames = new List(); - var jsonObject = JObject.Parse(body); + var jsonObject = !string.IsNullOrWhiteSpace(body) + ? JObject.Parse(body) + : null; FillPropertyNamesRecursive(jsonObject, propertyNames); return propertyNames; @@ -393,9 +393,9 @@ private IMapper GetMapper(Type sourceType, Type destinationType, Dictionary parts, ControllerFeatur var entityControllers = feature.Controllers.Where(c => c.AsType().ImplementsGenericInterface(typeof(IEntityAppService<,>))).ToList(); foreach (var controller in entityControllers) { - var genericInterface = controller.AsType().GetGenericInterfaces(typeof(IEntityAppService<,>)).FirstOrDefault(); + var genericInterface = controller.AsType().GetGenericInterfaces(typeof(IEntityAppService<,>)).First(); - var entityType = genericInterface.GenericTypeArguments.FirstOrDefault(); + var entityType = genericInterface.GenericTypeArguments.First(); entityConfigurationStore.SetDefaultAppService(entityType, controller); } diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/EntitiesAppService.cs b/shesha-core/src/Shesha.Application/DynamicEntities/EntitiesAppService.cs index 9fb77d481f..580a351612 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/EntitiesAppService.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/EntitiesAppService.cs @@ -11,6 +11,7 @@ using Shesha.Configuration.Runtime.Exceptions; using Shesha.DynamicEntities.Dtos; using Shesha.Excel; +using Shesha.Extensions; using Shesha.Metadata.Dtos; using Shesha.Permissions; using Shesha.Reflection; @@ -49,7 +50,8 @@ ISpecificationsFinder specificationsFinder protected async Task CheckPermissionAsync(EntityConfiguration entityConfig, string method) { - await _objectPermissionChecker.AuthorizeAsync(false, entityConfig.EntityType.FullName, method, ShaPermissionedObjectsTypes.EntityAction, AbpSession.UserId != null); + var crudMethod = PermissionedObjectManager.GetCrudMethod(method, method); + await _objectPermissionChecker.AuthorizeAsync(false, entityConfig.EntityType.GetRequiredFullName(), crudMethod.NotNull(), ShaPermissionedObjectsTypes.EntityAction, AbpSession.UserId != null); } [HttpGet] @@ -63,10 +65,6 @@ public virtual async Task GetAsync(string entityType, GetDyn var typeName = entityConfig.EntityType.FullName; - //var config = _entityConfigRepository.GetAll().FirstOrDefault(x => (x.Namespace + "." + x.ClassName) == typeName || x.TypeShortAlias == typeName); - //if (!(config?.GenerateAppService ?? true)) - // throw new NotSupportedException($"Application service is not configured for entity of type {typeName}"); - var appServiceType = entityConfig.ApplicationServiceType; if (entityConfig.ApplicationServiceType == null) @@ -87,11 +85,11 @@ public virtual async Task GetAsync(string entityType, GetDyn await CheckPermissionAsync(entityConfig, methodName); // invoke query - var convertedInputType = typeof(GetDynamicEntityInput<>).MakeGenericType(entityConfig.IdType); - var convertedInput = Activator.CreateInstance(convertedInputType); + var convertedInputType = typeof(GetDynamicEntityInput<>).MakeGenericType(entityConfig.IdType) ?? throw new Exception($"Failed to create generic type '{typeof(GetDynamicEntityInput<>).FullName}', id type: '{entityConfig.IdType.FullName}'"); + var convertedInput = Activator.CreateInstance(convertedInputType) ?? throw new Exception($"Failed to create instance of type '{convertedInputType.FullName}'"); AutoMapper.Map(input, convertedInput); - var task = (Task)method.Invoke(appService, new object[] { convertedInput }); + var task = method.Invoke(appService, [convertedInput]).NotNull(); await task.ConfigureAwait(false); var resultProperty = task.GetType().GetProperty("Result"); @@ -120,12 +118,6 @@ public virtual async Task GetAllAsync(string entityType, Pro if (entityConfig == null) throw new EntityTypeNotFoundException(entityType); - /* we MUST NOT disable it here - var config = _entityConfigRepository.GetAll().FirstOrDefault(x => (x.Namespace + "." + x.ClassName) == entityConfig.EntityType.FullName); - if (!(config?.GenerateAppService ?? true)) - throw new NotSupportedException($"Application service is not configured for entity of type {entityConfig.EntityType.FullName}"); - */ - var appServiceType = entityConfig.ApplicationServiceType; if (entityConfig.ApplicationServiceType == null) @@ -156,12 +148,6 @@ public async Task ExportToExcelAsync(ExportToExcelInput input, var typeName = entityConfig.EntityType.FullName; - /* we MUST NOT disable it here - var config = _entityConfigRepository.GetAll().FirstOrDefault(x => (x.Namespace + "." + x.ClassName) == typeName || x.TypeShortAlias == typeName); - if (!(config?.GenerateAppService ?? true)) - throw new NotSupportedException($"Application service is not configured for entity of type {typeName}"); - */ - var appServiceType = entityConfig.ApplicationServiceType; if (entityConfig.ApplicationServiceType == null) @@ -186,9 +172,9 @@ public async Task ExportToExcelAsync(ExportToExcelInput input, } } - private IEnumerable> ExtractGqlListData(IDynamicDataResult dataResult) + private IEnumerable> ExtractGqlListData(IDynamicDataResult dataResult) { - var jsonData = (Microsoft.AspNetCore.Mvc.JsonResult)dataResult; + var jsonData = (JsonResult)dataResult; if (jsonData.Value is ExecutionResult executionResult && executionResult.Data is ExecutionNode executionNode) { var value = executionNode.ToValue(); @@ -197,18 +183,25 @@ private IEnumerable> ExtractGqlListData(IDynamicDataR if (objectExecutionNode.SubFields != null) { var root = objectExecutionNode.SubFields.FirstOrDefault(); // field itself e.g. '*List' - var listResponse = (root as ObjectExecutionNode).SubFields; - var itemsFieldName = StringHelper.ToCamelCase(nameof(PagedResultDto>.Items)); - var itemsArray = listResponse.OfType().FirstOrDefault(f => f.Name == itemsFieldName); - if (itemsArray != null) - { - var rows = itemsArray.Items.OfType(); - return rows.Select(row => row.ToValue() as Dictionary); - } + var listResponse = root is ObjectExecutionNode rootExecutionNode + ? rootExecutionNode.SubFields + : null; + if (listResponse != null) + { + var itemsFieldName = StringHelper.ToCamelCase(nameof(PagedResultDto>.Items)); + var itemsArray = listResponse.OfType().Single(f => f.Name == itemsFieldName); + if (itemsArray != null && itemsArray.Items != null) + { + var rows = itemsArray.Items.OfType(); + return rows != null + ? rows.Select(row => row.ToValue() as Dictionary).WhereNotNull() + : []; + } + } } } } - return null; + return []; } /// @@ -251,7 +244,7 @@ public async Task ReorderAsync(ReorderRequest input) throw new ArgumentException($"Property `{input.PropertyName}` not found in the type `{input.EntityType}`"); var reordererType = typeof(IEntityReorderer<,,>).MakeGenericType(entityConfig.EntityType, entityConfig.IdType, property.PropertyType.GetUnderlyingTypeIfNullable()); - var reorderer = IocManager.Resolve(reordererType) as IEntityReorderer; + var reorderer = IocManager.Resolve(reordererType).ForceCastAs(); return await reorderer.ReorderAsync(input, property); } diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/EntityConfigAppService.cs b/shesha-core/src/Shesha.Application/DynamicEntities/EntityConfigAppService.cs index 731a90f414..acff6429ae 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/EntityConfigAppService.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/EntityConfigAppService.cs @@ -46,7 +46,7 @@ IRepository propertyRepository _propertyRepository = propertyRepository; } - public async Task GetEntityConfigFormAsync(string entityConfigName, string typeName) + public async Task GetEntityConfigFormAsync(string entityConfigName, string typeName) { var entityConfig = await AsyncQueryableExecuter.FirstOrDefaultAsync(Repository.GetAll().Where(x => x.Name == entityConfigName || x.TypeShortAlias == entityConfigName)); if (entityConfig == null) @@ -78,17 +78,17 @@ public async Task> GetMainDataListAsync(Filtered } [HttpGet] - public async Task> EntityConfigAutocompleteAsync(bool? implemented, string term, string selectedValue) + public async Task> EntityConfigAutocompleteAsync(bool? implemented, string? term, string? selectedValue) { var isPreselection = string.IsNullOrWhiteSpace(term) && !string.IsNullOrWhiteSpace(selectedValue); var models = await _entityConfigManager.GetMainDataListAsync(implemented: implemented); var entities = isPreselection - ? models.Where(e => e.Id == selectedValue.ToGuid()).ToList() + ? models.Where(e => e.Id == selectedValue.NotNull().ToGuid()).ToList() : models .Where(e => string.IsNullOrWhiteSpace(term) || e.FullClassName.Contains(term, StringComparison.InvariantCultureIgnoreCase) - || e.Label.Contains(term, StringComparison.InvariantCultureIgnoreCase)) + || e.Label != null && e.Label.Contains(term, StringComparison.InvariantCultureIgnoreCase)) .OrderBy(e => e.FullClassName) .Take(10) .ToList(); diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/EntityReorderer.cs b/shesha-core/src/Shesha.Application/DynamicEntities/EntityReorderer.cs index 49f54fab9e..d79acff218 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/EntityReorderer.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/EntityReorderer.cs @@ -7,6 +7,7 @@ using Shesha.Configuration.Runtime; using Shesha.DynamicEntities.Dtos; using Shesha.Extensions; +using Shesha.Reflection; using Shesha.Utilities; using System; using System.Collections.Generic; @@ -51,10 +52,16 @@ public async Task ReorderAsync(ReorderRequest input, PropertyI if (!idConverter.CanConvertFrom(typeof(string))) throw new NotSupportedException($"Conversion of string to type `{typeof(TId).FullName}` is not supported"); - var passedItems = input.Items.Select(item => new ReorderingItem { - OrderIndex = convertOrderIndex(item.OrderIndex), - Id = (TId)idConverter.ConvertFrom(item.Id) } - ) + var passedItems = input.Items.Select(item => + { + var id = idConverter.ConvertFrom(item.Id); + + return new ReorderingItem + { + OrderIndex = convertOrderIndex(item.OrderIndex), + Id = id != null ? (TId)id : throw new Exception($"Failed to convert id = '{item.Id}' to type '{typeof(TId).FullName}'") + }; + }) .ToList(); var ids = passedItems.Select(item => item.Id).ToList(); @@ -123,7 +130,7 @@ private Expression> GetUpdateExpression(Type partialType, string // new statement "new {}" var constructor = partialType.GetConstructors().Where(c => c.GetParameters().Any()).Single(); - var orderIndexProperty = partialType.GetProperty(orderIndexPropertyName); + var orderIndexProperty = partialType.GetRequiredProperty(orderIndexPropertyName); var newExpression = Expression.New(constructor, new Expression[] { Expression.Constant(orderIndex) }, orderIndexProperty); var lambda = Expression.Lambda>(newExpression, entParam); @@ -139,8 +146,8 @@ private Expression>> CreateSelectExpres var newExpression = Expression.New(typeof(ReorderingItem)); var bindings = new MemberBinding[]{ - Expression.Bind(typeof(ReorderingItem).GetProperty(nameof(ReorderingItem.Id)), Expression.Property(entParam, typeof(T).GetProperty("Id"))), - Expression.Bind(typeof(ReorderingItem).GetProperty(nameof(ReorderingItem.OrderIndex)), Expression.Property(entParam, orderIndexProperty)), + Expression.Bind(typeof(ReorderingItem).GetRequiredProperty(nameof(ReorderingItem.Id)), Expression.Property(entParam, typeof(T).GetRequiredProperty("Id"))), + Expression.Bind(typeof(ReorderingItem).GetRequiredProperty(nameof(ReorderingItem.OrderIndex)), Expression.Property(entParam, orderIndexProperty)), }; var initExpression = Expression.MemberInit(newExpression, bindings); diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/ModelConfigurationsAppService.cs b/shesha-core/src/Shesha.Application/DynamicEntities/ModelConfigurationsAppService.cs index 83e9b06bc4..2c216855a5 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/ModelConfigurationsAppService.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/ModelConfigurationsAppService.cs @@ -1,16 +1,13 @@ using Abp.Application.Services; using Abp.Domain.Entities; using Abp.Domain.Repositories; -using Abp.Domain.Uow; using Microsoft.AspNetCore.Mvc; using Shesha.Configuration.Runtime; using Shesha.Domain; -using Shesha.Domain.ConfigurationItems; using Shesha.Domain.Enums; using Shesha.DynamicEntities.Dtos; using Shesha.Elmah; using Shesha.Extensions; -using Shesha.Permissions; using Shesha.Swagger; using Shesha.Utilities; using Swashbuckle.AspNetCore.Swagger; @@ -27,31 +24,22 @@ namespace Shesha.DynamicEntities public class ModelConfigurationsAppService : SheshaAppServiceBase, IApplicationService { private readonly IRepository _entityConfigRepository; - private readonly IRepository _moduleRepository; private readonly IRepository _entityPropertyRepository; private readonly IModelConfigurationManager _modelConfigurationManager; - private readonly IPermissionedObjectManager _permissionedObjectManager; - private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly ISwaggerProvider _swaggerProvider; private readonly IEntityConfigurationStore _entityConfigurationStore; public ModelConfigurationsAppService( IRepository entityConfigRepository, - IRepository moduleRepository, IRepository entityPropertyRepository, IModelConfigurationManager modelConfigurationProvider, - IPermissionedObjectManager permissionedObjectManager, - IUnitOfWorkManager unitOfWorkManager, ISwaggerProvider swaggerProvider, IEntityConfigurationStore entityConfigurationStore) { _entityConfigRepository = entityConfigRepository; - _moduleRepository = moduleRepository; _entityPropertyRepository = entityPropertyRepository; _modelConfigurationManager = modelConfigurationProvider; - _permissionedObjectManager = permissionedObjectManager; - _unitOfWorkManager = unitOfWorkManager; _swaggerProvider = swaggerProvider; _entityConfigurationStore = entityConfigurationStore; } @@ -101,10 +89,10 @@ public async Task MergeAsync(MergeConfigurationDto input) { var source = await AsyncQueryableExecuter.FirstOrDefaultAsync(_entityConfigRepository.GetAll().Where(x => x.Id == input.SourceId.ToGuid())); if (source == null) - new EntityNotFoundException("Source configuration not found"); + throw new EntityNotFoundException("Source configuration not found"); var destination = await AsyncQueryableExecuter.FirstOrDefaultAsync(_entityConfigRepository.GetAll().Where(x => x.Id == input.DestinationId.ToGuid())); - if (source == null) - new EntityNotFoundException("Destination configuration not found"); + if (destination == null) + throw new EntityNotFoundException("Destination configuration not found"); using (var uow = UnitOfWorkManager.Begin()) { @@ -127,23 +115,12 @@ await _modelConfigurationManager.MergeConfigurationsAsync(source, destination, i var tokenSource = SheshaActionDescriptorChangeProvider.Instance.TokenSource; if (tokenSource != null) await tokenSource.CancelAsync(); - (_swaggerProvider as CachingSwaggerProvider)?.ClearCache(); + + if (_swaggerProvider is CachingSwaggerProvider cachedProvider) + await cachedProvider.ClearCacheAsync(); } return await _modelConfigurationManager.GetModelConfigurationAsync(destination); } - - private async Task GetAsync(EntityConfig modelConfig) - { - var dto = ObjectMapper.Map(modelConfig); - - var properties = await _entityPropertyRepository.GetAll().Where(p => p.EntityConfig == modelConfig && p.ParentProperty == null) - .OrderBy(p => p.SortOrder) - .ToListAsync(); - - dto.Properties = properties.Select(p => ObjectMapper.Map(p)).ToList(); - - return dto; - } } } diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/MvcCoreLoggerExtensions.cs b/shesha-core/src/Shesha.Application/DynamicEntities/MvcCoreLoggerExtensions.cs index 3d749c67a7..b9652f6395 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/MvcCoreLoggerExtensions.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/MvcCoreLoggerExtensions.cs @@ -3,10 +3,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.DynamicEntities { @@ -15,29 +11,29 @@ namespace Shesha.DynamicEntities /// internal static class MvcCoreLoggerExtensions { - private static readonly Action _attemptingToBindParameterModel; - private static readonly Action _attemptingToBindPropertyModel; - private static readonly Action _attemptingToBindModel; + private static readonly Action _attemptingToBindParameterModel; + private static readonly Action _attemptingToBindPropertyModel; + private static readonly Action _attemptingToBindModel; - private static readonly Action _doneAttemptingToBindParameterModel; - private static readonly Action _doneAttemptingToBindPropertyModel; - private static readonly Action _doneAttemptingToBindModel; + private static readonly Action _doneAttemptingToBindParameterModel; + private static readonly Action _doneAttemptingToBindPropertyModel; + private static readonly Action _doneAttemptingToBindModel; - private static readonly Action _inputFormatterSelected; + private static readonly Action _inputFormatterSelected; - private static readonly Action _inputFormatterRejected; + private static readonly Action _inputFormatterRejected; - private static readonly Action _noInputFormatterSelected; - private static readonly Action _removeFromBodyAttribute; + private static readonly Action _noInputFormatterSelected; + private static readonly Action _removeFromBodyAttribute; static MvcCoreLoggerExtensions () { - _attemptingToBindParameterModel = LoggerMessage.Define( + _attemptingToBindParameterModel = LoggerMessage.Define( LogLevel.Debug, new EventId(44, "AttemptingToBindParameterModel"), "Attempting to bind parameter '{ParameterName}' of type '{ModelType}' using the name '{ModelName}' in request data ..."); - _attemptingToBindPropertyModel = LoggerMessage.Define( + _attemptingToBindPropertyModel = LoggerMessage.Define( LogLevel.Debug, new EventId(13, "AttemptingToBindPropertyModel"), "Attempting to bind property '{PropertyContainerType}.{PropertyName}' of type '{ModelType}' using the name '{ModelName}' in request data ..."); @@ -47,12 +43,12 @@ static MvcCoreLoggerExtensions () new EventId(24, "AttemptingToBindModel"), "Attempting to bind model of type '{ModelType}' using the name '{ModelName}' in request data ..."); - _doneAttemptingToBindParameterModel = LoggerMessage.Define( + _doneAttemptingToBindParameterModel = LoggerMessage.Define( LogLevel.Debug, new EventId(45, "DoneAttemptingToBindParameterModel"), "Done attempting to bind parameter '{ParameterName}' of type '{ModelType}'."); - _doneAttemptingToBindPropertyModel = LoggerMessage.Define( + _doneAttemptingToBindPropertyModel = LoggerMessage.Define( LogLevel.Debug, new EventId(14, "DoneAttemptingToBindPropertyModel"), "Done attempting to bind property '{PropertyContainerType}.{PropertyName}' of type '{ModelType}'."); @@ -62,23 +58,23 @@ static MvcCoreLoggerExtensions () new EventId(25, "DoneAttemptingToBindModel"), "Done attempting to bind model of type '{ModelType}' using the name '{ModelName}'."); - _inputFormatterSelected = LoggerMessage.Define( + _inputFormatterSelected = LoggerMessage.Define( LogLevel.Debug, new EventId(1, "InputFormatterSelected"), "Selected input formatter '{InputFormatter}' for content type '{ContentType}'."); - _inputFormatterRejected = LoggerMessage.Define( + _inputFormatterRejected = LoggerMessage.Define( LogLevel.Debug, new EventId(2, "InputFormatterRejected"), "Rejected input formatter '{InputFormatter}' for content type '{ContentType}'."); - _noInputFormatterSelected = LoggerMessage.Define( + _noInputFormatterSelected = LoggerMessage.Define( LogLevel.Debug, new EventId(3, "NoInputFormatterSelected"), "No input formatter was found to support the content type '{ContentType}' for use with the [FromBody] attribute."/*, SkipEnabledCheckLogOptions*/); - _removeFromBodyAttribute = LoggerMessage.Define( + _removeFromBodyAttribute = LoggerMessage.Define( LogLevel.Debug, new EventId(4, "RemoveFromBodyAttribute"), "To use model binding, remove the [FromBody] attribute from the property or parameter named '{ModelName}' with model type '{ModelType}'."); @@ -190,4 +186,4 @@ public static void NoInputFormatterSelected( } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/DynamicEntities/SheshaActionDescriptorChangeProvider.cs b/shesha-core/src/Shesha.Application/DynamicEntities/SheshaActionDescriptorChangeProvider.cs index ed79dd4a81..4da6f78bcb 100644 --- a/shesha-core/src/Shesha.Application/DynamicEntities/SheshaActionDescriptorChangeProvider.cs +++ b/shesha-core/src/Shesha.Application/DynamicEntities/SheshaActionDescriptorChangeProvider.cs @@ -1,29 +1,39 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Primitives; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; -using System.Threading.Tasks; namespace Shesha.DynamicEntities { /// /// ASP.Net Core register Controller at runtime /// https://stackoverflow.com/questions/46156649/asp-net-core-register-controller-at-runtime /// - public class SheshaActionDescriptorChangeProvider : IActionDescriptorChangeProvider - { + public sealed class SheshaActionDescriptorChangeProvider : IActionDescriptorChangeProvider, IDisposable + { + private bool _disposed; + public static SheshaActionDescriptorChangeProvider Instance { get; } = new SheshaActionDescriptorChangeProvider(); - public CancellationTokenSource TokenSource { get; private set; } + public CancellationTokenSource? TokenSource { get; private set; } public bool HasChanged { get; set; } public IChangeToken GetChangeToken() { + TokenSource?.Dispose(); TokenSource = new CancellationTokenSource(); return new CancellationChangeToken(TokenSource.Token); - } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + TokenSource?.Dispose(); + } } } diff --git a/shesha-core/src/Shesha.Application/Email/Dtos/EmailAttachment.cs b/shesha-core/src/Shesha.Application/Email/Dtos/EmailAttachment.cs index c312a9a35c..68618ffc02 100644 --- a/shesha-core/src/Shesha.Application/Email/Dtos/EmailAttachment.cs +++ b/shesha-core/src/Shesha.Application/Email/Dtos/EmailAttachment.cs @@ -24,8 +24,11 @@ public void Dispose() } protected virtual void Dispose(bool disposing) - { - Stream?.Dispose(); + { +#pragma warning disable IDISP007 // Don't dispose injected + if (disposing) + Stream?.Dispose(); +#pragma warning restore IDISP007 // Don't dispose injected } } } diff --git a/shesha-core/src/Shesha.Application/Email/Dtos/SmtpSettingsDto.cs b/shesha-core/src/Shesha.Application/Email/Dtos/SmtpSettingsDto.cs index b112663153..93847cf6a6 100644 --- a/shesha-core/src/Shesha.Application/Email/Dtos/SmtpSettingsDto.cs +++ b/shesha-core/src/Shesha.Application/Email/Dtos/SmtpSettingsDto.cs @@ -5,7 +5,7 @@ public class SmtpSettingsDto /// /// SMTP Host name/IP. /// - public string Host { get; set; } + public string? Host { get; set; } /// /// SMTP Port. @@ -15,17 +15,17 @@ public class SmtpSettingsDto /// /// User name to login to SMTP server. /// - public string UserName { get; set; } + public string? UserName { get; set; } /// /// Password to login to SMTP server. /// - public string Password { get; set; } + public string? Password { get; set; } /// /// Domain name to login to SMTP server. /// - public string Domain { get; set; } + public string? Domain { get; set; } /// /// Is SSL enabled? @@ -35,12 +35,12 @@ public class SmtpSettingsDto /// /// Default from address. /// - public string DefaultFromAddress { get; set; } + public string? DefaultFromAddress { get; set; } /// /// Default display name. /// - public string DefaultFromDisplayName { get; set; } + public string? DefaultFromDisplayName { get; set; } /// /// If true, indicate that SMTP relay service will be used where it's needed (e.g. if the application needs to notify one person about the action that was performed by another person then real person's email address will be used for the "from" address, otherwise "Site Email" will be used) @@ -50,7 +50,7 @@ public class SmtpSettingsDto /// /// If not null or empty the all outgoing emails will be sent to this email address, is used for testing /// - public string RedirectAllMessagesTo { get; set; } + public string? RedirectAllMessagesTo { get; set; } /// /// If true, all emails will be disabled. Is used only for testing diff --git a/shesha-core/src/Shesha.Application/Email/EmailSenderAppService.cs b/shesha-core/src/Shesha.Application/Email/EmailSenderAppService.cs index 114af57d5b..93c45f44ed 100644 --- a/shesha-core/src/Shesha.Application/Email/EmailSenderAppService.cs +++ b/shesha-core/src/Shesha.Application/Email/EmailSenderAppService.cs @@ -83,13 +83,6 @@ public async Task GetSmtpSettingsAsync() [EnableValidation] public async Task SendEmailAsync(SendTestEmailInput input) { - /* - // note: validation doesn't work for ControllerBase (looks like not supported by Abp) - // todo: check support by the `Controller` and try it in the `ApplicationService` - if (!ModelState.IsValid) - throw new ValidationException(); - */ - await _emailSender.SendAsync( input.To, input.Subject, diff --git a/shesha-core/src/Shesha.Application/Email/ISheshaEmailSender.cs b/shesha-core/src/Shesha.Application/Email/ISheshaEmailSender.cs index 59630d7be1..8deaab64b6 100644 --- a/shesha-core/src/Shesha.Application/Email/ISheshaEmailSender.cs +++ b/shesha-core/src/Shesha.Application/Email/ISheshaEmailSender.cs @@ -12,6 +12,6 @@ public interface ISheshaEmailSender: ISmtpEmailSender /// /// Compose and send email with attachments /// - bool SendMail(string fromAddress, string toAddress, string subject, string body, bool isBodyHtml, List attachments = null, string cc = "", bool throwException = false); + bool SendMail(string fromAddress, string toAddress, string subject, string body, bool isBodyHtml, List? attachments = null, string cc = "", bool throwException = false); } } diff --git a/shesha-core/src/Shesha.Application/Email/SheshaEmailSender.cs b/shesha-core/src/Shesha.Application/Email/SheshaEmailSender.cs index a2d77a07fb..a740d35419 100644 --- a/shesha-core/src/Shesha.Application/Email/SheshaEmailSender.cs +++ b/shesha-core/src/Shesha.Application/Email/SheshaEmailSender.cs @@ -11,7 +11,6 @@ using System.Net; using System.Net.Mail; using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Shesha.Email @@ -37,7 +36,7 @@ private bool EmailsEnabled() return enabled; } - public bool SendMail(string fromAddress, string toAddress, string subject, string body, bool isBodyHtml, List attachments = null, + public bool SendMail(string fromAddress, string toAddress, string subject, string body, bool isBodyHtml, List? attachments = null, string cc = "", bool throwException = false) { if (!EmailsEnabled()) @@ -115,6 +114,13 @@ private bool PrepareAndCheckMail(MailMessage mail, SmtpSettings smtpSettings) NormalizeMail(mail); NormalizeMailSender(mail, smtpSettings); + if (mail.From == null || string.IsNullOrWhiteSpace(mail.From.Address)) + { + Logger.Error("Attempt to send email using empty from address. Subject: " + mail.Subject); + return false; + + } + // check recipient before redirect if (!mail.To.Any() || mail.To[0].Address == "") { @@ -181,11 +187,12 @@ private void NormalizeMailSender(MailMessage mail, SmtpSettings smtpSettings) } else { - mail.From = new MailAddress( - smtpSettings.UserName, - null, - Encoding.UTF8 - ); + if (!string.IsNullOrWhiteSpace(smtpSettings.UserName)) + mail.From = new MailAddress( + smtpSettings.UserName, + null, + Encoding.UTF8 + ); } } } diff --git a/shesha-core/src/Shesha.Application/Email/SmtpEmailSenderSettings.cs b/shesha-core/src/Shesha.Application/Email/SmtpEmailSenderSettings.cs index a7003b94da..6328df4541 100644 --- a/shesha-core/src/Shesha.Application/Email/SmtpEmailSenderSettings.cs +++ b/shesha-core/src/Shesha.Application/Email/SmtpEmailSenderSettings.cs @@ -1,11 +1,8 @@ -using System; -using System.Globalization; -using Abp.Configuration; -using Abp.Dependency; -using Abp.Net.Mail; +using Abp.Dependency; using Abp.Net.Mail.Smtp; using Abp.Runtime.Session; using Shesha.Configuration.Email; +using System; namespace Shesha.Email { @@ -26,12 +23,12 @@ public SmtpEmailSenderSettings(IEmailSettings emailSettings) public virtual string DefaultFromAddress { - get => _emailSettings.SmtpSettings.GetValue()?.DefaultFromAddress; + get => _emailSettings.SmtpSettings.GetValueOrNull()?.DefaultFromAddress ?? string.Empty; } public virtual string DefaultFromDisplayName { - get => _emailSettings.SmtpSettings.GetValue()?.DefaultFromDisplayName; + get => _emailSettings.SmtpSettings.GetValueOrNull()?.DefaultFromDisplayName ?? string.Empty; } /// @@ -39,7 +36,7 @@ public virtual string DefaultFromDisplayName /// public virtual string Host { - get => _emailSettings.SmtpSettings.GetValue()?.Host; + get => _emailSettings.SmtpSettings.GetValueOrNull()?.Host ?? string.Empty; } /// @@ -47,7 +44,7 @@ public virtual string Host /// public virtual int Port { - get => _emailSettings.SmtpSettings.GetValue()?.Port ?? 0; + get => _emailSettings.SmtpSettings.GetValueOrNull()?.Port ?? 0; } /// @@ -55,7 +52,7 @@ public virtual int Port /// public virtual string UserName { - get => _emailSettings.SmtpSettings.GetValue()?.UserName; + get => _emailSettings.SmtpSettings.GetValueOrNull()?.UserName ?? string.Empty; } /// @@ -63,7 +60,7 @@ public virtual string UserName /// public virtual string Password { - get => _emailSettings.SmtpSettings.GetValue()?.Password; + get => _emailSettings.SmtpSettings.GetValueOrNull()?.Password ?? string.Empty; } /// @@ -71,15 +68,7 @@ public virtual string Password /// public virtual string Domain { - get => _emailSettings.SmtpSettings.GetValue()?.Domain; - } - - /// - /// Domain name to login to incoming server. - /// - public virtual string IncomingServer - { - get => _emailSettings.SmtpSettings.GetValue()?.IncomingServer; + get => _emailSettings.SmtpSettings.GetValueOrNull()?.Domain ?? string.Empty; } /// @@ -87,7 +76,7 @@ public virtual string IncomingServer /// public virtual bool EnableSsl { - get => _emailSettings.SmtpSettings.GetValue()?.EnableSsl ?? false; + get => _emailSettings.SmtpSettings.GetValueOrNull()?.EnableSsl ?? false; } /// diff --git a/shesha-core/src/Shesha.Application/EntityHistory/EntityHistoryAppService.cs b/shesha-core/src/Shesha.Application/EntityHistory/EntityHistoryAppService.cs index 7b0334656d..b59ddfb378 100644 --- a/shesha-core/src/Shesha.Application/EntityHistory/EntityHistoryAppService.cs +++ b/shesha-core/src/Shesha.Application/EntityHistory/EntityHistoryAppService.cs @@ -2,6 +2,7 @@ using Abp.Application.Services.Dto; using Shesha.Authorization; using Shesha.Extensions; +using Shesha.QuickSearch; using System; using System.Collections.Generic; using System.Linq; @@ -16,12 +17,15 @@ namespace Shesha.EntityHistory public class EntityHistoryAppService : ApplicationService, IEntityHistoryAppService { private readonly IEntityHistoryProvider _entityHistoryProvider; - + private readonly IQuickSearcher _quickSearcher; + public EntityHistoryAppService( - IEntityHistoryProvider entityHistoryProvider + IEntityHistoryProvider entityHistoryProvider, + IQuickSearcher quickSearcher ) { _entityHistoryProvider = entityHistoryProvider; + _quickSearcher = quickSearcher; } public async Task> GetAuditTrailAsync(EntityHistoryResultRequestDto input, string entityId, string entityTypeFullName, bool includeEventsOnChildEntities) @@ -31,10 +35,8 @@ public async Task> GetAuditTrailAsync(Entit var totalRowsBeforeFilter = history.Count(); // Dynamic filter - if (!string.IsNullOrEmpty(input.QuickSearch)) - { - history = history.LikeDynamic(input.QuickSearch).ToList(); - } + if (!string.IsNullOrWhiteSpace(input.QuickSearch)) + history = await _quickSearcher.ApplyQuickSearch(history.AsQueryable(), input.QuickSearch).ToListAsync(); var totalRows = history.Count(); var totalPages = (int)Math.Ceiling((double)history.Count() / input.MaxResultCount); @@ -48,6 +50,8 @@ public async Task> GetAuditTrailAsync(Entit var sorting = input.Sorting.Split(","); var sort = sorting[0].Split(" "); var sortOrder = sort.Length > 1 ? sort[1] : "asc"; + + // TODO: review and replace with code from AbpCrudAppService.ApplySorting history = history.OrderByDynamic(sort[0], sortOrder.ToLower()).ToList(); } @@ -57,22 +61,6 @@ public async Task> GetAuditTrailAsync(Entit var dataRows = new List>(); - /*var authorizedColumns = - tableConfig.Columns //.Where(c => c.AuthorizationRules == null || c.AuthorizationRules.IsAuthorized()) - .ToList(); - - foreach (var item in history) - { - var row = new Dictionary(); - foreach (var col in authorizedColumns) - { - var value = item.GetType().GetProperty(col.PropertyName)?.GetValue(item); - row.Add(col.PropertyName, value); - } - - dataRows.Add(row); - }*/ - var result = new PagedResultDto { TotalCount= totalRows, diff --git a/shesha-core/src/Shesha.Application/EntityHistory/EntityHistoryResultRequestDto.cs b/shesha-core/src/Shesha.Application/EntityHistory/EntityHistoryResultRequestDto.cs index 40271afefe..52bf0db649 100644 --- a/shesha-core/src/Shesha.Application/EntityHistory/EntityHistoryResultRequestDto.cs +++ b/shesha-core/src/Shesha.Application/EntityHistory/EntityHistoryResultRequestDto.cs @@ -1,17 +1,16 @@ using Abp.Application.Services.Dto; using Shesha.Application.Services.Dto; -using System.Collections.Generic; namespace Shesha.EntityHistory { /// /// Filtered, pages and sorted request DTO /// - public class EntityHistoryResultRequestDto : PagedAndSortedResultRequestDto, IPagedAndSortedResultRequest + public class EntityHistoryResultRequestDto : ShaFilteredPagedAndSortedResultRequestDto, IPagedAndSortedResultRequest { /// /// Quick search string. Is used to search entities by text /// - public string QuickSearch { get; set; } + public string? QuickSearch { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/Excel/ExcelUtility.cs b/shesha-core/src/Shesha.Application/Excel/ExcelUtility.cs index 247dd3c85d..9e03440eee 100644 --- a/shesha-core/src/Shesha.Application/Excel/ExcelUtility.cs +++ b/shesha-core/src/Shesha.Application/Excel/ExcelUtility.cs @@ -37,23 +37,45 @@ public ExcelUtility(IReferenceListHelper refListHelper) /// private static void SetSheetName(string excelSpreadSheetName, SpreadsheetDocument spreadSheet) { - var ss = spreadSheet.WorkbookPart.Workbook.Descendants().FirstOrDefault(s => s.Name == DefaultSheetName); + var ss = spreadSheet.WorkbookPart?.Workbook.Descendants().FirstOrDefault(s => s.Name == DefaultSheetName); if (ss != null) ss.Name = excelSpreadSheetName; } + + private static WorkbookPart GetWorkbookPart(SpreadsheetDocument spreadSheet) + { + if (spreadSheet.WorkbookPart == null) + { + return spreadSheet.AddWorkbookPart(); + } else + return spreadSheet.WorkbookPart; + } + + private static Stylesheet GetStyleSheet(SpreadsheetDocument spreadSheet) + { + var workbookPart = GetWorkbookPart(spreadSheet); + + var stylePart = workbookPart.WorkbookStylesPart ?? workbookPart.AddNewPart(); + var styleSheet = stylePart.Stylesheet ?? (stylePart.Stylesheet = new Stylesheet()); + return styleSheet; + } + /// /// Set the style sheet /// private static void SetStyleSheet(SpreadsheetDocument spreadSheet) { - var styleSheet = spreadSheet.WorkbookPart.WorkbookStylesPart.Stylesheet; + var styleSheet = GetStyleSheet(spreadSheet); + + styleSheet.Fonts ??= new(); styleSheet.Fonts.Append(new Font( new FontSize { Val = 11 }, new Color { Rgb = "FFFFFF" }, new Bold(), new FontName { Val = "Calibri" })); + styleSheet.Fills ??= new(); styleSheet.Fills.AppendChild(new Fill { PatternFill = new PatternFill @@ -72,7 +94,7 @@ private static void SetStyleSheet(SpreadsheetDocument spreadSheet) styleSheet.CellFormats.AppendChild(new CellFormat { FormatId = 1, FontId = 0, BorderId = 0, FillId = 0, ApplyFill = false, ApplyBorder = false, ApplyFont = false }).AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Top, WrapText = true }); styleSheet.CellFormats.Count = 2; - spreadSheet.WorkbookPart.WorkbookStylesPart.Stylesheet.Save(); + styleSheet.Save(); } /// @@ -91,7 +113,7 @@ private static void AddCellWidthStyles(uint minCol, uint maxCol, int maxWidth, S private static readonly Dictionary LettersCache = new Dictionary(); - private static string ReplaceSpecialCharacters(string value) + private static string ReplaceSpecialCharacters(string? value) { return (value ?? "").Replace("’", "'") .Replace("“", "\"") @@ -102,15 +124,19 @@ private static string ReplaceSpecialCharacters(string value) private static void SetHeaderStyle(SpreadsheetDocument spreadSheet, Cell cell) { - Stylesheet styleSheet = spreadSheet.WorkbookPart.WorkbookStylesPart.Stylesheet; + var styleSheet = GetStyleSheet(spreadSheet); cell.SetAttribute(new OpenXmlAttribute("", "s", "", "1")); OpenXmlAttribute cellStyleAttribute = cell.GetAttribute("s", ""); - CellFormats cellFormats = spreadSheet.WorkbookPart.WorkbookStylesPart.Stylesheet.CellFormats; + var cellFormats = styleSheet.CellFormats.NotNull(); // pick the first cell format. CellFormat cellFormat = (CellFormat)cellFormats.ElementAt(0); CellFormat cf = new CellFormat(cellFormat.OuterXml); + + styleSheet.Fonts ??= new(); + styleSheet.Fills ??= new(); + cf.FontId = styleSheet.Fonts.Count; cf.FillId = styleSheet.Fills.Count; @@ -123,13 +149,13 @@ private static void SetHeaderStyle(SpreadsheetDocument spreadSheet, Cell cell) #endregion - public static async Task DataToExcelStreamAsync(WriteRowsDelegate writeRows, IList headers, string sheetName, List columnWidths = null) + public static async Task DataToExcelStreamAsync(WriteRowsDelegate writeRows, IList headers, string sheetName, List? columnWidths = null) { var xmlStream = ReportingHelper.GetResourceStream("Shesha.Excel.template.xlsx", typeof(ExcelUtility).Assembly); using (var document = SpreadsheetDocument.Open(xmlStream, true)) { - var workbookPart = document.WorkbookPart; + var workbookPart = GetWorkbookPart(document); var worksheetPart = workbookPart.WorksheetParts.First(); var originalSheetId = workbookPart.GetIdOfPart(worksheetPart); @@ -147,6 +173,8 @@ public static async Task DataToExcelStreamAsync(WriteRowsDelegate ws.SheetProperties = sp; // Set the FitToPage property to true + ws.SheetProperties ??= new(); + ws.SheetProperties.PageSetupProperties ??= new(); ws.SheetProperties.PageSetupProperties.FitToPage = BooleanValue.FromBoolean(true); var pgOr = new PageSetup @@ -187,7 +215,7 @@ public static async Task DataToExcelStreamAsync(WriteRowsDelegate } worksheetPart.Worksheet.Save(); - document.WorkbookPart.Workbook.Save(); + workbookPart.Workbook.Save(); using (var xmlReader = OpenXmlReader.Create(worksheetPart)) { @@ -209,7 +237,7 @@ public static async Task DataToExcelStreamAsync(WriteRowsDelegate SetHeaderStyle(document, headerCell); foreach (var header in headers) { - headerCell.CellValue.Text = header; + headerCell.CellValue.NotNull().Text = header; xmlWriter.WriteElement(headerCell); } xmlWriter.WriteEndElement(); @@ -233,16 +261,16 @@ public static async Task DataToExcelStreamAsync(WriteRowsDelegate } } - var sheet = workbookPart.Workbook.Descendants().First(s => s.Id.Value.Equals(originalSheetId)); + var sheet = workbookPart.Workbook.Descendants().First(s => s.Id != null && s.Id.Value != null && s.Id.Value.Equals(originalSheetId)); - sheet.Id.Value = replacementPartId; + sheet.Id.NotNull().Value = replacementPartId; workbookPart.DeletePart(worksheetPart); } return xmlStream; } - public async Task ReadToExcelStreamAsync(Type rowType, IEnumerable> list, IList columns, string sheetName) + public async Task ReadToExcelStreamAsync(Type rowType, IEnumerable> list, IList columns, string sheetName) { var headers = columns .Select(t => ReplaceSpecialCharacters(t.Label)) @@ -287,7 +315,7 @@ public async Task ReadToExcelStreamAsync(Type rowType, IEnumerable return result; } - private Func CreateGetAsString(Type rowType, ExcelColumn c) + private Func CreateGetAsString(Type rowType, ExcelColumn c) { var property = ReflectionHelper.GetProperty(rowType, c.PropertyName, useCamelCase: true); @@ -297,8 +325,8 @@ private Func CreateGetAsString(Type rowType, ExcelColumn c) var underlyingType = ReflectionHelper.GetUnderlyingTypeIfNullable(property.PropertyType); - var dataTypeAttr = property.GetAttribute(); - var formatAttr = property.GetAttribute(); + var dataTypeAttr = property.GetAttributeOrNull(); + var formatAttr = property.GetAttributeOrNull(); if (underlyingType == typeof(DateTime)) { @@ -307,7 +335,7 @@ private Func CreateGetAsString(Type rowType, ExcelColumn c) : dataTypeAttr != null && dataTypeAttr.DataType == DataType.Date ? "dd/MM/yyyy" : "dd/MM/yyyy HH:mm"; - return new Func(val => + return new Func(val => { if (val == null) return null; @@ -325,7 +353,7 @@ private Func CreateGetAsString(Type rowType, ExcelColumn c) var format = !string.IsNullOrWhiteSpace(formatAttr?.DataFormatString) ? formatAttr.DataFormatString : @"hh\:mm"; - return new Func(val => + return new Func(val => { if (val == null) return null; @@ -340,13 +368,13 @@ private Func CreateGetAsString(Type rowType, ExcelColumn c) else if (ReflectionHelper.IsReferenceListProperty(property)) { - var refListIdentifier = ReflectionHelper.GetReferenceListIdentifierOrNull(property); + var refListIdentifier = ReflectionHelper.GetReferenceListIdentifierOrNull(property).NotNull(); var list = _refListHelper.GetItems(refListIdentifier); var isMultiValue = ReflectionHelper.IsMultiValueReferenceListProperty(property); if (isMultiValue) { - return new Func(val => + return new Func(val => { if (val == null) return null; @@ -369,7 +397,7 @@ private Func CreateGetAsString(Type rowType, ExcelColumn c) }); } else - return new Func(val => + return new Func(val => { if (val == null) return null; @@ -379,7 +407,7 @@ private Func CreateGetAsString(Type rowType, ExcelColumn c) } else if (property.PropertyType.IsEntityType()) { - return new Func(val => { + return new Func(val => { if (val == null) return null; @@ -388,14 +416,14 @@ private Func CreateGetAsString(Type rowType, ExcelColumn c) return dict[EntityConstants.DisplayNameField]?.ToString(); }); } else - return new Func(val => val?.ToString()); + return new Func(val => val?.ToString()); } private static GetValueDelegate CalculateColumnValueGetter(ExcelColumn column) { var parts = column.PropertyName.Split(".").Select(p => p.ToCamelCase()).Reverse().ToList(); - var getterFactory = new Func((name, getter) => { + var getterFactory = new Func((name, getter) => { if (getter == null) { return new GetValueDelegate(row => row == null @@ -412,23 +440,24 @@ private static GetValueDelegate CalculateColumnValueGetter(ExcelColumn column) : row.TryGetValue(name, out var partValue) ? partValue : null; - return container != null && container is Dictionary dict + return container != null && container is Dictionary dict ? getter.Invoke(dict) : null; }); } }); - GetValueDelegate result = null; + GetValueDelegate? result = null; foreach (var part in parts) { result = getterFactory(part, result); } - return result; + return result ?? throw new Exception("Failed to get column valu getter"); } private static void FillCellValue(Cell cell, string value, Type type) { + cell.CellValue ??= new(); cell.CellValue.Text = ReplaceSpecialCharacters(ReplaceSpecialCharacters(value ?? "")); switch (Type.GetTypeCode(type)) @@ -457,14 +486,14 @@ private static void FillCellValue(Cell cell, string value, Type type) } public delegate Task WriteRowsDelegate(OpenXmlWriter xmlWriter); - public delegate object GetValueDelegate(Dictionary container); + public delegate object? GetValueDelegate(Dictionary container); public class ExcelColumnProcessing { public string PropertyName { get; set; } public string Label { get; set; } public GetValueDelegate Getter { get; set; } - public Func GetAsString { get; set; } + public Func GetAsString { get; set; } } } } diff --git a/shesha-core/src/Shesha.Application/Excel/IExcelUtility.cs b/shesha-core/src/Shesha.Application/Excel/IExcelUtility.cs index 8b13d94b1e..d9539dda04 100644 --- a/shesha-core/src/Shesha.Application/Excel/IExcelUtility.cs +++ b/shesha-core/src/Shesha.Application/Excel/IExcelUtility.cs @@ -18,6 +18,6 @@ public interface IExcelUtility /// /// /// - Task ReadToExcelStreamAsync(Type rowType, IEnumerable> list, IList columns, string sheetName); + Task ReadToExcelStreamAsync(Type rowType, IEnumerable> list, IList columns, string sheetName); } } diff --git a/shesha-core/src/Shesha.Application/Metadata/IMetadataAppService.cs b/shesha-core/src/Shesha.Application/Metadata/IMetadataAppService.cs index b36ff443f1..c19c36afc0 100644 --- a/shesha-core/src/Shesha.Application/Metadata/IMetadataAppService.cs +++ b/shesha-core/src/Shesha.Application/Metadata/IMetadataAppService.cs @@ -17,7 +17,7 @@ public interface IMetadataAppService: IApplicationService /// Term to search /// Selected value, is used to fetch user-friendly name of selected item /// - Task> EntityTypeAutocompleteAsync(string term, string selectedValue); + Task> EntityTypeAutocompleteAsync(string? term, string? selectedValue); /// /// Autocomplete of container's properties @@ -26,7 +26,7 @@ public interface IMetadataAppService: IApplicationService /// Container (e.g. entity type) /// Selected value, is used to fetch user-friendly name of selected item /// - Task> PropertyAutocompleteAsync(string term, string container, string selectedValue); + Task> PropertyAutocompleteAsync(string? term, string container, string? selectedValue); /// /// Get properties of the specified container diff --git a/shesha-core/src/Shesha.Application/Metadata/MetadataAppService.cs b/shesha-core/src/Shesha.Application/Metadata/MetadataAppService.cs index 3f4e9e62ce..2ddfce50f4 100644 --- a/shesha-core/src/Shesha.Application/Metadata/MetadataAppService.cs +++ b/shesha-core/src/Shesha.Application/Metadata/MetadataAppService.cs @@ -8,6 +8,7 @@ using Shesha.Metadata.Dtos; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -45,7 +46,7 @@ private async Task> GetAllModelsAsync() return models.Distinct(new ModelDtoTypeComparer()).Where(x => !x.Suppress).ToList(); } - private async Task GetContainerTypeAsync(string container) + private async Task GetContainerTypeOrNullAsync(string container) { var allModels = await GetAllModelsAsync(); var models = allModels.Where(m => m.Alias == container || m.ClassName == container).ToList(); @@ -56,8 +57,14 @@ private async Task GetContainerTypeAsync(string container) return models.FirstOrDefault()?.Type; } + private async Task GetContainerTypeAsync(string container) + { + return await GetContainerTypeOrNullAsync(container) ?? throw new MetadataOfTypeNotFoundException(container); + } + + [HttpGet] - public Task> TypeAutocompleteAsync(string term, string selectedValue) + public Task> TypeAutocompleteAsync(string? term, string? selectedValue) { // note: temporary return only entities return EntityTypeAutocompleteAsync(term, selectedValue); @@ -65,7 +72,7 @@ public Task> TypeAutocompleteAsync(string term, string /// inheritedDoc [HttpGet] - public async Task> EntityTypeAutocompleteAsync(string term, string selectedValue) + public async Task> EntityTypeAutocompleteAsync(string? term, string? selectedValue) { var isPreselection = string.IsNullOrWhiteSpace(term) && !string.IsNullOrWhiteSpace(selectedValue); var models = await GetAllModelsAsync(); @@ -95,12 +102,12 @@ public async Task> EntityTypeAutocompleteAsync(string /// inheritedDoc [HttpGet] - public async Task> PropertyAutocompleteAsync(string term, string container, string selectedValue) + public async Task> PropertyAutocompleteAsync(string? term, string container, string? selectedValue) { if (string.IsNullOrWhiteSpace(container)) throw new AbpValidationException($"'{nameof(container)}' is mandatory"); - var containerType = await GetContainerTypeAsync(container); + var containerType = await GetContainerTypeOrNullAsync(container); if (containerType == null) return new List(); @@ -138,10 +145,7 @@ public async Task GetAsync(string container) if (string.IsNullOrWhiteSpace(container)) throw new AbpValidationException($"'{nameof(container)}' is mandatory"); - // ToDo: show Dynamic entities var containerType = await GetContainerTypeAsync(container); - if (containerType == null) - throw new MetadataOfTypeNotFoundException(container); return await _metadataProvider.GetAsync(containerType, container); } @@ -161,9 +165,9 @@ public async Task> SpecificationsAsync(string entityType) private class ModelDtoTypeComparer : IEqualityComparer { - bool IEqualityComparer.Equals(ModelDto x, ModelDto y) + bool IEqualityComparer.Equals(ModelDto? x, ModelDto? y) { - return x.ClassName == y.ClassName; + return x != null && y != null && x.ClassName == y.ClassName || x == null && y == null; } int IEqualityComparer.GetHashCode(ModelDto obj) diff --git a/shesha-core/src/Shesha.Application/MobileDevices/MobileDeviceAppService.cs b/shesha-core/src/Shesha.Application/MobileDevices/MobileDeviceAppService.cs index a63563faf7..8107a16ddc 100644 --- a/shesha-core/src/Shesha.Application/MobileDevices/MobileDeviceAppService.cs +++ b/shesha-core/src/Shesha.Application/MobileDevices/MobileDeviceAppService.cs @@ -12,7 +12,7 @@ public MobileDeviceAppService(IRepository repository) : base { } - public async Task GetDeviceByEmeiAsync(string imei) + public async Task GetDeviceByEmeiAsync(string imei) { var device = await Repository.FirstOrDefaultAsync(r => r.IMEI == imei); return device != null diff --git a/shesha-core/src/Shesha.Application/MultiTenancy/TenantAppService.cs b/shesha-core/src/Shesha.Application/MultiTenancy/TenantAppService.cs index 1a5919019f..82f70c265a 100644 --- a/shesha-core/src/Shesha.Application/MultiTenancy/TenantAppService.cs +++ b/shesha-core/src/Shesha.Application/MultiTenancy/TenantAppService.cs @@ -48,7 +48,7 @@ public override async Task CreateAsync(CreateTenantDto input) // Create tenant var tenant = ObjectMapper.Map(input); - tenant.ConnectionString = input.ConnectionString.IsNullOrEmpty() + tenant.ConnectionString = string.IsNullOrEmpty(input.ConnectionString) ? null : SimpleStringCipher.Instance.Encrypt(input.ConnectionString); diff --git a/shesha-core/src/Shesha.Application/Notes/Dto/CreateNoteDto.cs b/shesha-core/src/Shesha.Application/Notes/Dto/CreateNoteDto.cs index a015e059d8..dc825bc232 100644 --- a/shesha-core/src/Shesha.Application/Notes/Dto/CreateNoteDto.cs +++ b/shesha-core/src/Shesha.Application/Notes/Dto/CreateNoteDto.cs @@ -21,7 +21,7 @@ public class CreateNoteDto : EntityDto /// /// Category of the note. Is used to split notes into groups /// - public int? Category { get; set; } + public string? Category { get; set; } /// /// Note importance (priority) @@ -38,5 +38,6 @@ public class CreateNoteDto : EntityDto /// [Required] public string NoteText { get; set; } + } } diff --git a/shesha-core/src/Shesha.Application/Notes/Dto/GetListInput.cs b/shesha-core/src/Shesha.Application/Notes/Dto/GetListInput.cs index 370b91b9cb..b3177e4387 100644 --- a/shesha-core/src/Shesha.Application/Notes/Dto/GetListInput.cs +++ b/shesha-core/src/Shesha.Application/Notes/Dto/GetListInput.cs @@ -19,7 +19,7 @@ public class GetListInput /// /// Category of the note. Is used to split notes into groups /// - public int? Category { get; set; } + public string? Category { get; set; } /// /// Set to true to get notes of all categories diff --git a/shesha-core/src/Shesha.Application/Notes/Dto/NoteDto.cs b/shesha-core/src/Shesha.Application/Notes/Dto/NoteDto.cs index 916da48190..cff9a547ab 100644 --- a/shesha-core/src/Shesha.Application/Notes/Dto/NoteDto.cs +++ b/shesha-core/src/Shesha.Application/Notes/Dto/NoteDto.cs @@ -27,7 +27,7 @@ public class NoteDto: EntityDto /// /// Category of the note. Is used to split notes into groups /// - public int? Category { get; set; } + public string? Category { get; set; } /// /// Note importance (priority) diff --git a/shesha-core/src/Shesha.Application/Notes/Dto/NoteProfile.cs b/shesha-core/src/Shesha.Application/Notes/Dto/NoteProfile.cs index 23df2dbb4b..f5e51cded0 100644 --- a/shesha-core/src/Shesha.Application/Notes/Dto/NoteProfile.cs +++ b/shesha-core/src/Shesha.Application/Notes/Dto/NoteProfile.cs @@ -1,8 +1,7 @@ -using System; -using Shesha.AutoMapper; +using Shesha.AutoMapper; using Shesha.AutoMapper.Dto; using Shesha.Domain; -using Shesha.Extensions; +using System; namespace Shesha.Notes.Dto { @@ -15,7 +14,7 @@ public NoteProfile() CreateMap() .ForMember(u => u.Author, options => options.MapFrom(e => e.Author != null - ? new EntityReferenceDto(e.Author.Id, e.Author.FullName, e.Author.GetClassName()) + ? new EntityReferenceDto(e.Author) : null)); CreateMap(); diff --git a/shesha-core/src/Shesha.Application/Notes/Dto/UpdateNoteDto.cs b/shesha-core/src/Shesha.Application/Notes/Dto/UpdateNoteDto.cs index 60ac68fbc4..69b452c998 100644 --- a/shesha-core/src/Shesha.Application/Notes/Dto/UpdateNoteDto.cs +++ b/shesha-core/src/Shesha.Application/Notes/Dto/UpdateNoteDto.cs @@ -9,7 +9,7 @@ public class UpdateNoteDto : EntityDto /// /// Category of the note. Is used to split notes into groups /// - public int? Category { get; set; } + public string? Category { get; set; } /// /// Note importance (priority) diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/Dto/DistributedNotificationChannel.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/Dto/DistributedNotificationChannel.cs index e15c41876f..9d986c9ee6 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/Dto/DistributedNotificationChannel.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/Dto/DistributedNotificationChannel.cs @@ -1,7 +1,5 @@ using Shesha.ConfigurationItems.Distribution; -using Shesha.Domain.Attributes; using Shesha.Domain.Enums; -using System; namespace Shesha.Notifications.Distribution.NotificationChannels.Dto { diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelImport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelImport.cs index 7de3aa8a45..eba6bff40f 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelImport.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelImport.cs @@ -1,15 +1,11 @@ using Abp.Dependency; using Abp.Domain.Repositories; -using Newtonsoft.Json; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Notifications.Distribution.NotificationChannels.Dto; -using Shesha.EntityReferences; -using Shesha.Services; using Shesha.Services.ConfigurationItems; using System; -using System.IO; using System.Threading.Tasks; namespace Shesha.Notifications.Distribution.NotificationChannels @@ -17,7 +13,7 @@ namespace Shesha.Notifications.Distribution.NotificationChannels /// /// file template import /// - public class NotificationChannelImport : ConfigurationItemImportBase, INotificationChannelImport, ITransientDependency + public class NotificationChannelImport : ConfigurationItemImportBase, INotificationChannelImport, ITransientDependency { public readonly IRepository _configurationRepo; @@ -105,22 +101,5 @@ protected async Task MapConfigAsync(DistributedNotifi return dbItem; } - - public async Task ReadFromJsonAsync(Stream jsonStream) - { - using (var reader = new StreamReader(jsonStream)) - { - var json = await reader.ReadToEndAsync(); - - var result = !string.IsNullOrWhiteSpace(json) - ? JsonConvert.DeserializeObject(json) - : null; - - if (result == null) - throw new Exception($"Failed to read {nameof(NotificationChannelConfig)} from json"); - - return result; - } - } } } diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationTemplateDto.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationTemplateDto.cs new file mode 100644 index 0000000000..28a02293b4 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationTemplateDto.cs @@ -0,0 +1,17 @@ +using Abp.Application.Services.Dto; +using Shesha.Domain; +using Shesha.Domain.Enums; +using System; + +namespace Shesha.Notifications.Distribution.NotificationTypes.Dto +{ + /// + /// Distributed DTO of the + /// + public class DistributedNotificationTemplateDto: EntityDto, INotificationTemplateProps + { + public RefListNotificationMessageFormat? MessageFormat { get; set; } + public string TitleTemplate { get; set; } + public string BodyTemplate { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationType.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationType.cs index f089f677f0..2296cbcbc1 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationType.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationType.cs @@ -1,7 +1,6 @@ using Shesha.ConfigurationItems.Distribution; -using Shesha.Domain.Attributes; -using Shesha.Domain.Enums; -using System; +using Shesha.Domain; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Shesha.Notifications.Distribution.NotificationTypes.Dto @@ -9,7 +8,7 @@ namespace Shesha.Notifications.Distribution.NotificationTypes.Dto /// /// Distributed file template /// - public class DistributedNotificationTypes: DistributedConfigurableItemBase + public class DistributedNotificationType: DistributedConfigurableItemBase, INotificationTypeSpecificProps { /// /// @@ -22,7 +21,7 @@ public class DistributedNotificationTypes: DistributedConfigurableItemBase /// /// If true indicates that users may opt out of this notification /// - public bool CanOtpOut { get; set; } + public bool CanOptOut { get; set; } /// /// /// @@ -35,6 +34,15 @@ public class DistributedNotificationTypes: DistributedConfigurableItemBase /// List of NotificationChannelConfigs /// [StringLength(int.MaxValue)] - public string OverrideChannels { get; set; } + public string? OverrideChannels { get; set; } + /// + /// messages without which the user should not proceed in any case e.g. OTP + /// + public bool IsTimeSensitive { get; set; } + + /// + /// Templates + /// + public List Templates { get; set; } = new(); } } diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeExport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeExport.cs index 2e9d666339..dd3bb7d06e 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeExport.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeExport.cs @@ -3,11 +3,12 @@ using Newtonsoft.Json; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; +using Shesha.Extensions; using Shesha.Notifications.Distribution.NotificationTypes.Dto; -using Shesha.Services; -using Shesha.StoredFiles; using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; namespace Shesha.Notifications.Distribution.NotificationTypes @@ -18,10 +19,12 @@ namespace Shesha.Notifications.Distribution.NotificationTypes public class NotificationTypeExport : INotificationTypeExport, ITransientDependency { private readonly IRepository _configurationRepo; + private readonly IRepository _templateRepo; - public NotificationTypeExport(IRepository configurationRepo) + public NotificationTypeExport(IRepository configurationRepo, IRepository templateRepo) { _configurationRepo = configurationRepo; + _templateRepo = templateRepo; } public string ItemType => NotificationTypeConfig.ItemTypeName; @@ -37,7 +40,7 @@ public async Task ExportItemAsync(Configuration if (!(item is NotificationTypeConfig itemConfig)) throw new ArgumentException($"Wrong type of argument {item}. Expected {nameof(NotificationTypeConfig)}, actual: {item.GetType().FullName}", nameof(item)); - var result = new DistributedNotificationTypes + var result = new DistributedNotificationType { Id = itemConfig.Id, Name = itemConfig.Name, @@ -53,19 +56,19 @@ public async Task ExportItemAsync(Configuration VersionStatus = itemConfig.VersionStatus, ParentVersionId = itemConfig.ParentVersion?.Id, Suppress = itemConfig.Suppress, - - // specific properties - AllowAttachments = itemConfig.AllowAttachments, - Disable = itemConfig.Disable, - CanOtpOut = itemConfig.CanOptOut, - Category = itemConfig.Category, - OrderIndex = itemConfig.OrderIndex, - OverrideChannels = itemConfig.OverrideChannels, }; + result.CopyNotificationSpecificPropsFrom(itemConfig); + result.Templates = await ExportTemplatesAsync(itemConfig); return await Task.FromResult(result); } + private async Task> ExportTemplatesAsync(NotificationTypeConfig notification) + { + var templates = await _templateRepo.GetAll().Where(t => t.PartOf == notification).ToListAsync(); + return templates.Select(e => new DistributedNotificationTemplateDto { Id = e.Id }.CopyTemplatePropsFrom(e)).ToList(); + } + /// inheritedDoc public async Task WriteToJsonAsync(DistributedConfigurableItemBase item, Stream jsonStream) { diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeImport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeImport.cs index 950bff5858..764b4b29fb 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeImport.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeImport.cs @@ -1,33 +1,43 @@ using Abp.Dependency; using Abp.Domain.Repositories; -using Newtonsoft.Json; +using Abp.Domain.Uow; +using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; -using Shesha.EntityReferences; -using Shesha.Services; +using Shesha.Extensions; +using Shesha.Notifications.Distribution.NotificationTypes.Dto; using Shesha.Services.ConfigurationItems; using System; -using System.IO; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; -using Shesha.Notifications.Distribution.NotificationTypes.Dto; namespace Shesha.Notifications.Distribution.NotificationTypes { /// /// file template import /// - public class NotificationTypeImport : ConfigurationItemImportBase, INotificationTypeImport, ITransientDependency + public class NotificationTypeImport : ConfigurationItemImportBase, INotificationTypeImport, ITransientDependency { - public readonly IRepository _configurationRepo; + private readonly IRepository _repository; + private readonly INotificationManager _manager; + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly IRepository _templateRepo; public NotificationTypeImport( IRepository moduleRepo, IRepository frontEndAppRepo, - IRepository configurationRepo + IRepository repository, + IRepository templateRepo, + INotificationManager manager, + IUnitOfWorkManager unitOfWorkManager ) : base (moduleRepo, frontEndAppRepo) { - _configurationRepo = configurationRepo; + _repository = repository; + _templateRepo = templateRepo; + _manager = manager; + _unitOfWorkManager = unitOfWorkManager; } public string ItemType => NotificationTypeConfig.ItemTypeName; @@ -37,52 +47,92 @@ public async Task ImportItemAsync(DistributedConfigurable if (item == null) throw new ArgumentNullException(nameof(item)); - if (!(item is DistributedNotificationTypes itemConfig)) + if (!(item is DistributedNotificationType itemConfig)) throw new NotSupportedException($"{this.GetType().FullName} supports only items of type {nameof(NotificationTypeConfig)}. Actual type is {item.GetType().FullName}"); return await ImportAsync(itemConfig, context); } - protected async Task ImportAsync(DistributedNotificationTypes item, IConfigurationItemsImportContext context) + protected async Task ImportAsync(DistributedNotificationType item, IConfigurationItemsImportContext context) { // use status specified in the context with fallback to imported value var statusToImport = context.ImportStatusAs ?? item.VersionStatus; - // get DB config - var dbItem = await _configurationRepo.FirstOrDefaultAsync(x => x.Name == item.Name && x.Description == item.Description - && (x.Module == null && item.ModuleName == null || x.Module != null && x.Module.Name == item.ModuleName) - && x.IsLast); + var existingNotification = await _repository.GetByByFullName(item.ModuleName, item.Name).FirstOrDefaultAsync(e => e.IsLast); - if (dbItem != null) + if (existingNotification != null) { + switch (existingNotification.VersionStatus) + { + case ConfigurationItemVersionStatus.Draft: + case ConfigurationItemVersionStatus.Ready: + { + // cancel existing version + await _manager.CancelVersionAsync(existingNotification); + break; + } + } + // mark existing live form as retired if we import new form as live + if (statusToImport == ConfigurationItemVersionStatus.Live) + { + var liveVersion = existingNotification.VersionStatus == ConfigurationItemVersionStatus.Live + ? existingNotification + : await _repository.GetByByFullName(item.ModuleName, item.Name).FirstOrDefaultAsync(e => e.VersionStatus == ConfigurationItemVersionStatus.Live); + if (liveVersion != null) + { + await _manager.UpdateStatusAsync(liveVersion, ConfigurationItemVersionStatus.Retired); + await _unitOfWorkManager.Current.SaveChangesAsync(); // save changes to guarantee sequence of update + } + } + + // Create new version. Note: it copies all nested items + var newVersion = await _manager.CreateNewVersionWithoutDetailsAsync(existingNotification); + + // important: set status according to the context + newVersion.VersionStatus = statusToImport; + newVersion.CreatedByImport = context.ImportResult; + newVersion.Normalize(); + + // todo: save external Id + // how to handle origin? - // ToDo: Temporary update the current version. - // Need to update the rest of the other code to work with versioning first + await _repository.UpdateAsync(newVersion); - await MapConfigAsync(item, dbItem, context); - await _configurationRepo.UpdateAsync(dbItem); + await ImportTemplatesAsync(newVersion, item.Templates); + + return newVersion; } else { - dbItem = new NotificationTypeConfig(); - await MapConfigAsync(item, dbItem, context); + var newNotification = new NotificationTypeConfig(); + await MapConfigAsync(item, newNotification, context); - // fill audit? - dbItem.VersionNo = 1; - dbItem.Module = await GetModuleAsync(item.ModuleName, context); + newNotification.VersionNo = 1; // important: set status according to the context - dbItem.VersionStatus = statusToImport; - dbItem.CreatedByImport = context.ImportResult; + newNotification.VersionStatus = statusToImport; + newNotification.CreatedByImport = context.ImportResult; + + newNotification.Normalize(); + + await _repository.InsertAsync(newNotification); - dbItem.Normalize(); - await _configurationRepo.InsertAsync(dbItem); + await ImportTemplatesAsync(newNotification, item.Templates); + + return newNotification; } - return dbItem; } + private async Task ImportTemplatesAsync(NotificationTypeConfig newVersion, List templates) + { + foreach (var templateDto in templates) + { + var template = new NotificationTemplate { PartOf = newVersion }.CopyTemplatePropsFrom(templateDto); + await _templateRepo.InsertAsync(template); + } + } - protected async Task MapConfigAsync(DistributedNotificationTypes item, NotificationTypeConfig dbItem, IConfigurationItemsImportContext context) + protected async Task MapConfigAsync(DistributedNotificationType item, NotificationTypeConfig dbItem, IConfigurationItemsImportContext context) { dbItem.Name = item.Name; dbItem.Module = await GetModuleAsync(item.ModuleName, context); @@ -96,31 +146,9 @@ protected async Task MapConfigAsync(DistributedNotificat dbItem.Suppress = item.Suppress; // entity specific properties - dbItem.AllowAttachments = item.AllowAttachments; - dbItem.Disable = item.Disable; - dbItem.CanOptOut = item.CanOtpOut; - dbItem.Category = item.Category; - dbItem.OrderIndex = item.OrderIndex; - dbItem.OverrideChannels = item.OverrideChannels; + dbItem.CopyNotificationSpecificPropsFrom(item); return dbItem; } - - public async Task ReadFromJsonAsync(Stream jsonStream) - { - using (var reader = new StreamReader(jsonStream)) - { - var json = await reader.ReadToEndAsync(); - - var result = !string.IsNullOrWhiteSpace(json) - ? JsonConvert.DeserializeObject(json) - : null; - - if (result == null) - throw new Exception($"Failed to read {nameof(NotificationTypeConfig)} from json"); - - return result; - } - } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationMessageDto.cs b/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationMessageDto.cs index e85464d581..ffe2d00bc1 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationMessageDto.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationMessageDto.cs @@ -1,9 +1,5 @@ using Shesha.Email.Dtos; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Notifications.Dto { diff --git a/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationProfile.cs b/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationProfile.cs new file mode 100644 index 0000000000..969826e925 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationProfile.cs @@ -0,0 +1,23 @@ +using Shesha.AutoMapper; +using Shesha.Domain; +using System; + +namespace Shesha.Notifications.Dto +{ + public class NotificationProfile : ShaProfile + { + public NotificationProfile() + { + CreateMap() + .ForMember(e => e.ModuleId, m => m.MapFrom(e => e.Module != null ? e.Module.Id : (Guid?)null)) + .ForMember(e => e.OriginId, m => m.MapFrom(e => e.Origin != null ? e.Origin.Id : (Guid?)null)) + .ForMember(e => e.Module, m => m.MapFrom(e => e.Module != null ? e.Module.Name : null)) + .ForMember(e => e.IsLastVersion, m => m.MapFrom(e => e.IsLast)) + .ForMember(e => e.Name, m => m.MapFrom(e => e.Name)) + .ForMember(e => e.Label, m => m.MapFrom(e => e.Label)) + .ForMember(e => e.Description, m => m.MapFrom(e => e.Description)) + .ForMember(e => e.VersionNo, m => m.MapFrom(e => e.VersionNo)) + .ForMember(e => e.VersionStatus, m => m.MapFrom(e => e.VersionStatus)); + } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationTypeConfigDto.cs b/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationTypeConfigDto.cs new file mode 100644 index 0000000000..d84b80e589 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationTypeConfigDto.cs @@ -0,0 +1,12 @@ +using Shesha.Domain; +using Shesha.Dto; + +namespace Shesha.Notifications.Dto +{ + /// + /// Dto of the + /// + public class NotificationTypeConfigDto : ConfigurationItemDto + { + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Dto/SendStatus.cs b/shesha-core/src/Shesha.Application/Notifications/Dto/SendStatus.cs index e9cd37faa7..f8c201c9eb 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Dto/SendStatus.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Dto/SendStatus.cs @@ -1,11 +1,9 @@ -using Hangfire.Storage.Monitoring; - -namespace Shesha.Notifications.Dto +namespace Shesha.Notifications.Dto { public class SendStatus { public bool IsSuccess { get; set; } - public string Message { get; set; } + public string? Message { get; set; } public static SendStatus Failed(string message) { @@ -15,7 +13,7 @@ public static SendStatus Failed(string message) }; } - public static SendStatus Success(string message = null) + public static SendStatus Success(string? message = null) { return new SendStatus { diff --git a/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs index 7df9314653..b6c1f489c6 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs @@ -1,5 +1,4 @@ -using Abp.Extensions; -using Abp.UI; +using Abp.UI; using Castle.Core.Logging; using Shesha.Configuration; using Shesha.Configuration.Email; @@ -26,7 +25,7 @@ public EmailChannelSender(IEmailSettings emailSettings) _emailSettings = emailSettings; } - public string GetRecipientId(Person person) + public string? GetRecipientId(Person person) { return person?.EmailAddress1; } @@ -36,28 +35,26 @@ private async Task GetSettingsAsync() return await _emailSettings.EmailSettings.GetValueAsync(); } - public async Task SendAsync(IMessageSender fromPerson, IMessageReceiver toPerson, NotificationMessage message, string cc = "", List attachments = null) + public async Task SendAsync(IMessageSender? sender, IMessageReceiver receiver, NotificationMessage message, List? attachments = null) { var settings = await GetSettingsAsync(); if (settings == null) - return new SendStatus() - { - IsSuccess = false, - Message = "Email settings are not configured" - }; + return SendStatus.Failed("Email settings are not configured"); if (!settings.EmailsEnabled) { Logger.Warn("Emails are disabled"); - return new SendStatus() - { - IsSuccess = false, - Message = "Emails are disabled." - }; + return SendStatus.Failed("Emails are disabled"); } - using (var mail = BuildMessageWith(fromPerson.GetAddress(this), !settings.RedirectAllMessagesTo.IsNullOrWhiteSpace() ? settings.RedirectAllMessagesTo : toPerson.GetAddress(this), message.Subject, message.Message, cc)) + var toAddress = !string.IsNullOrWhiteSpace(settings.RedirectAllMessagesTo) + ? settings.RedirectAllMessagesTo + : receiver.GetAddress(this); + if (string.IsNullOrWhiteSpace(toAddress)) + return SendStatus.Failed("Recipient address is empty"); + + using (var mail = BuildMessageWith(sender?.GetAddress(this), toAddress, message.Subject, message.Message, message.Cc)) { if (attachments != null) { @@ -69,20 +66,12 @@ public async Task SendAsync(IMessageSender fromPerson, IMessageRecei try { await SendEmailAsync(mail); - return new SendStatus() - { - IsSuccess = true, - Message = "Successfully Sent!" - }; + return SendStatus.Success(); } catch (Exception e) { Logger.Error("Failed to send email", e); - return new SendStatus() - { - IsSuccess = false, - Message = e.Message - }; + return SendStatus.Failed(e.Message); } }; } @@ -107,10 +96,6 @@ private async Task SendEmailAsync(MailMessage mail) Logger.Error($"Error sending email: {ex.Message}", ex); throw new InvalidOperationException($"An error occurred while sending the email. Message: {ex.Message} ", ex); } - finally - { - mail.Dispose(); - } } private async Task GetSmtpClientAsync() @@ -144,7 +129,7 @@ private SmtpClient GetSmtpClient(SmtpSettings smtpSettings) /// /// /// - private MailMessage BuildMessageWith(string fromAddress, string toAddress, string subject, string body, string cc = "") + private MailMessage BuildMessageWith(string? fromAddress, string toAddress, string subject, string body, string? cc = null) { var smtpSettings = _emailSettings.SmtpSettings.GetValue(); var message = new MailMessage diff --git a/shesha-core/src/Shesha.Application/Notifications/Helpers/TemplateHelper.cs b/shesha-core/src/Shesha.Application/Notifications/Helpers/TemplateHelper.cs deleted file mode 100644 index 2d73a572a5..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/Helpers/TemplateHelper.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; - -namespace Shesha.Notifications.Helpers -{ - public static class TemplateHelper - { - // TODO: review and this implementation - public static string ReplacePlaceholders(string template, TData data) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - // Use regex to find placeholders in the form {{propertyName}} - return Regex.Replace(template, @"\{\{(\w+)\}\}", match => - { - var propertyName = match.Groups[1].Value; - - // Use the runtime type of the object for property lookup - var propertyInfo = data.GetType() - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .FirstOrDefault(prop => string.Equals(prop.Name, propertyName, StringComparison.OrdinalIgnoreCase)); - - return propertyInfo != null - ? propertyInfo.GetValue(data)?.ToString() ?? string.Empty - : string.Empty; - }); - } - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs index 05ff0c57ad..35dd24c56c 100644 --- a/shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs @@ -9,7 +9,7 @@ namespace Shesha.Notifications { public interface INotificationChannelSender { - string GetRecipientId(Person person); - Task SendAsync(IMessageSender sender, IMessageReceiver receiver, NotificationMessage message, string cc, List attachments = null); + string? GetRecipientId(Person person); + Task SendAsync(IMessageSender? sender, IMessageReceiver receiver, NotificationMessage message, List? attachments = null); } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/INotificationManager.cs b/shesha-core/src/Shesha.Application/Notifications/INotificationManager.cs index fd348ec69d..42d970133d 100644 --- a/shesha-core/src/Shesha.Application/Notifications/INotificationManager.cs +++ b/shesha-core/src/Shesha.Application/Notifications/INotificationManager.cs @@ -1,4 +1,5 @@ -using Shesha.Domain; +using Shesha.ConfigurationItems; +using Shesha.Domain; using Shesha.Domain.Enums; using Shesha.Notifications.MessageParticipants; using System.Collections.Generic; @@ -9,7 +10,7 @@ namespace Shesha.Notifications /// /// Notification manager /// - public interface INotificationManager + public interface INotificationManager : IConfigurationItemManager { /// /// Get list of channels for the specified notification type, receiver and priority @@ -19,5 +20,10 @@ public interface INotificationManager /// /// Task> GetChannelsAsync(NotificationTypeConfig type, IMessageReceiver receiver, RefListNotificationPriority priority); + + /// + /// Create new version of the specified excluding detailing items + /// + Task CreateNewVersionWithoutDetailsAsync(NotificationTypeConfig src); } } diff --git a/shesha-core/src/Shesha.Application/Notifications/INotificationSender.cs b/shesha-core/src/Shesha.Application/Notifications/INotificationSender.cs index 45d733571d..d0487fa8b8 100644 --- a/shesha-core/src/Shesha.Application/Notifications/INotificationSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/INotificationSender.cs @@ -12,21 +12,25 @@ namespace Shesha.Notifications public interface INotificationSender { Task SendNotificationAsync(NotificationTypeConfig type, - IMessageSender sender, + IMessageSender? sender, IMessageReceiver receiver, TData data, RefListNotificationPriority priority, - List attachments = null, - GenericEntityReference triggeringEntity = null, - NotificationChannelConfig channel = null) where TData : NotificationData; + List? attachments = null, + string? cc = null, + GenericEntityReference? triggeringEntity = null, + NotificationChannelConfig? channel = null, + string? category = null) where TData : NotificationData; Task SendNotificationAsync(NotificationTypeConfig type, - Person sender, + Person? sender, Person receiver, TData data, RefListNotificationPriority priority, - List attachments = null, - GenericEntityReference triggeringEntity = null, - NotificationChannelConfig channel = null) where TData : NotificationData; + List? attachments = null, + string? cc = null, + GenericEntityReference? triggeringEntity = null, + NotificationChannelConfig? channel = null, + string? category = null) where TData : NotificationData; } } diff --git a/shesha-core/src/Shesha.Application/Notifications/INotificationTemplateProcessor.cs b/shesha-core/src/Shesha.Application/Notifications/INotificationTemplateProcessor.cs new file mode 100644 index 0000000000..9a471c3fcf --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/INotificationTemplateProcessor.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace Shesha.Notifications +{ + /// + /// Notification template processor + /// + public interface INotificationTemplateProcessor + { + /// + /// Generate notification text using specified and + /// + /// Template with mustache syntax + /// Model to use for replacements in the template + /// + Task GenerateAsync(string template, object model); + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/IMessageParticipant.cs b/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/IMessageParticipant.cs index 85b448a189..6adee1ab18 100644 --- a/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/IMessageParticipant.cs +++ b/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/IMessageParticipant.cs @@ -2,11 +2,9 @@ namespace Shesha.Notifications.MessageParticipants { -#nullable enable public interface IMessageParticipant { string? GetAddress(INotificationChannelSender notificationChannelSender); Person? GetPerson(); } -#nullable restore } diff --git a/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/PersonMessageParticipant.cs b/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/PersonMessageParticipant.cs index 6b6b2fd9ca..07cdc1b65d 100644 --- a/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/PersonMessageParticipant.cs +++ b/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/PersonMessageParticipant.cs @@ -1,14 +1,13 @@ -using Abp.Dependency; -using Abp.Domain.Repositories; +using Abp.Domain.Repositories; using Shesha.Domain; using Shesha.Extensions; +using Shesha.Services; using System; using System.Threading.Tasks; namespace Shesha.Notifications.MessageParticipants { -#nullable enable - public class PersonMessageParticipant : IMessageSender, IMessageReceiver + public class PersonMessageParticipant : IMessageSender, IMessageReceiver, IEquatable { private readonly Person _person; public PersonMessageParticipant(Person person) @@ -27,9 +26,36 @@ public PersonMessageParticipant(Person person) public async Task IsNotificationOptedOutAsync(NotificationTypeConfig type) { - var userNotificationPreferenceRepository = IocManager.Instance.Resolve>(); + var userNotificationPreferenceRepository = StaticContext.IocManager.Resolve>(); return await userNotificationPreferenceRepository.GetAll().AnyAsync(x => x.User.Id == _person.Id && x.NotificationType.Id == type.Id && x.OptOut); } + + public override int GetHashCode() + { + return _person != null + ? _person.Id.GetHashCode() + : base.GetHashCode(); + } + + public bool Equals(IMessageReceiver? other) + { + return other is PersonMessageParticipant participant && participant.GetPerson() == this.GetPerson(); + } + + public override bool Equals(object? obj) => this.Equals(obj as PersonMessageParticipant); + + public static bool operator ==(PersonMessageParticipant? l, PersonMessageParticipant? r) + { + if (l is null) + { + if (r is null) + return true; + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return l.Equals(r); + } + public static bool operator !=(PersonMessageParticipant? l, PersonMessageParticipant? r) => !(l == r); } -#nullable restore } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/RawPersonMessageParticipant.cs b/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/RawPersonMessageParticipant.cs index 2391c7c7b5..d6b33d84dd 100644 --- a/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/RawPersonMessageParticipant.cs +++ b/shesha-core/src/Shesha.Application/Notifications/MessageParticipants/RawPersonMessageParticipant.cs @@ -1,10 +1,10 @@ using Shesha.Domain; +using System; using System.Threading.Tasks; namespace Shesha.Notifications.MessageParticipants { -#nullable enable - public class RawAddressMessageParticipant : IMessageSender, IMessageReceiver + public class RawAddressMessageParticipant : IMessageSender, IMessageReceiver, IEquatable { private readonly string _address; public RawAddressMessageParticipant(string address) @@ -25,6 +25,33 @@ public Task IsNotificationOptedOutAsync(NotificationTypeConfig type) { return Task.FromResult(false); } + + public override int GetHashCode() + { + return _address != null + ? _address.GetHashCode() + : base.GetHashCode(); + } + + public bool Equals(IMessageReceiver? other) + { + return other is RawAddressMessageParticipant participant && participant._address == this._address; + } + + public override bool Equals(object? obj) => this.Equals(obj as RawAddressMessageParticipant); + + public static bool operator ==(RawAddressMessageParticipant? l, RawAddressMessageParticipant? r) + { + if (l is null) + { + if (r is null) + return true; + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return l.Equals(r); + } + public static bool operator !=(RawAddressMessageParticipant? l, RawAddressMessageParticipant? r) => !(l == r); } -#nullable restore } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs index c691f9bb51..00cdae22f0 100644 --- a/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs @@ -16,14 +16,14 @@ public NotificationAppService(INotificationSender notificationService) _notificationService = notificationService; } - public async Task PublishAsync(NotificationTypeConfig type, long priority, NotificationData data, Person recipient, GenericEntityReference triggeringEntity = null) + public async Task PublishAsync(NotificationTypeConfig type, long priority, NotificationData data, Person recipient, string cc = "", GenericEntityReference? triggeringEntity = null) { if (recipient == null) throw new Exception($"{nameof(recipient)} must not be null"); var sender = await GetCurrentPersonAsync(); - await _notificationService.SendNotificationAsync(type, sender, recipient, data, (RefListNotificationPriority)priority, null, triggeringEntity); + await _notificationService.SendNotificationAsync(type, sender, recipient, data, (RefListNotificationPriority)priority, null, cc, triggeringEntity); } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationExtensions.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationExtensions.cs new file mode 100644 index 0000000000..9d6ee470ad --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationExtensions.cs @@ -0,0 +1,35 @@ +using Shesha.Domain; + +namespace Shesha.Notifications +{ + internal static class NotificationExtensions + { + /// + /// Copy notification properties from + /// + public static TDestination CopyNotificationSpecificPropsFrom(this TDestination dst, INotificationTypeSpecificProps src) where TDestination: INotificationTypeSpecificProps + { + dst.IsTimeSensitive = src.IsTimeSensitive; + dst.AllowAttachments = src.AllowAttachments; + dst.Disable = src.Disable; + dst.CanOptOut = src.CanOptOut; + dst.Category = src.Category; + dst.OrderIndex = src.OrderIndex; + dst.OverrideChannels = src.OverrideChannels; + + return dst; + } + + /// + /// Copy notification template properties from + /// + public static TDestination CopyTemplatePropsFrom(this TDestination dst, INotificationTemplateProps src) where TDestination: INotificationTemplateProps + { + dst.MessageFormat = src.MessageFormat; + dst.TitleTemplate = src.TitleTemplate; + dst.BodyTemplate = src.BodyTemplate; + + return dst; + } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationManager.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationManager.cs index 9e9b0b3744..30b5217c2b 100644 --- a/shesha-core/src/Shesha.Application/Notifications/NotificationManager.cs +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationManager.cs @@ -1,33 +1,48 @@ using Abp.Dependency; using Abp.Domain.Repositories; +using Abp.Domain.Uow; using Abp.UI; +using Shesha.ConfigurationItems; +using Shesha.ConfigurationItems.Models; using Shesha.ConfigurationItems.Specifications; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Domain.Enums; +using Shesha.Dto.Interfaces; using Shesha.Extensions; using Shesha.Notifications.Configuration; +using Shesha.Notifications.Dto; using Shesha.Notifications.MessageParticipants; +using Shesha.Reflection; +using Shesha.Validations; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; namespace Shesha.Notifications { - public class NotificationManager: INotificationManager, ITransientDependency + public class NotificationManager : ConfigurationItemManager, INotificationManager, ITransientDependency { private readonly IRepository _notificationChannelRepository; private readonly IRepository _userNotificationPreference; + private readonly IRepository _templateRepository; private readonly INotificationSettings _notificationSettings; - public NotificationManager(IRepository notificationChannelRepository, - IRepository userNotificationPreference, - INotificationSettings notificationSettings) + public NotificationManager( + IRepository repository, + IRepository moduleRepository, + IUnitOfWorkManager unitOfWorkManager, + IRepository notificationChannelRepository, + IRepository userNotificationPreference, + IRepository templateRepository, + INotificationSettings notificationSettings) : base(repository, moduleRepository, unitOfWorkManager) { _notificationChannelRepository = notificationChannelRepository; _userNotificationPreference = userNotificationPreference; - _notificationSettings = notificationSettings; + _templateRepository = templateRepository; + _notificationSettings = notificationSettings; } /// @@ -55,11 +70,15 @@ public async Task> GetChannelsAsync(Notification // Step 2: Check for Parsed Override Channels if (type.ParsedOverrideChannels.Any()) { - var overrideChannels = await _notificationChannelRepository - .GetAll() - .Where(channel => type.ParsedOverrideChannels.Contains( - new NotificationChannelIdentifier(channel.Module.Name, channel.Name))) - .ToListAsync(); + var overrideChannels = new List(); + foreach (var channel in type.ParsedOverrideChannels) + { + // TODO: check versioned query + var dbChannel = await _notificationChannelRepository.GetAll().Where(new ByNameAndModuleSpecification(channel.Name, channel.Module).ToExpression()) + .FirstOrDefaultAsync(); + if (dbChannel != null) + overrideChannels.Add(dbChannel); + } return overrideChannels; } @@ -77,6 +96,7 @@ public async Task> GetChannelsAsync(Notification if (selectedNotifications == null) return new(); + // TODO: check versioned query var liveChannels = _notificationChannelRepository .GetAll() .Where(channel => channel.IsLast && channel.VersionStatus == ConfigurationItemVersionStatus.Live); @@ -89,5 +109,108 @@ public async Task> GetChannelsAsync(Notification return result; } + public override Task MapToDtoAsync(NotificationTypeConfig item) + { + + var dto = ObjectMapper.Map(item); + return Task.FromResult(dto); + } + + public override async Task CopyAsync(NotificationTypeConfig src, CopyItemInput input) + { + // todo: validate input + var module = await ModuleRepository.FirstOrDefaultAsync(input.ModuleId); + + var validationResults = new List(); + + // todo: review validation messages, add localization support + if (src == null) + validationResults.Add(new ValidationResult("Please select notification type to copy", new List { nameof(input.ItemId) })); + if (module == null) + validationResults.Add(new ValidationResult("Module is mandatory", new List { nameof(input.ModuleId) })); + if (string.IsNullOrWhiteSpace(input.Name)) + validationResults.Add(new ValidationResult("Name is mandatory", new List { nameof(input.Name) })); + + if (module != null && !string.IsNullOrWhiteSpace(input.Name)) + { + var alreadyExist = await Repository.GetAll().Where(f => f.Module == module && f.Name == input.Name).AnyAsync(); + if (alreadyExist) + validationResults.Add(new ValidationResult( + module != null + ? $"Notification Type with name `{input.Name}` already exists in module `{module.Name}`" + : $"Notification Type with name `{input.Name}` already exists" + ) + ); + } + + validationResults.ThrowValidationExceptionIfAny(L); + + var newCopy = new NotificationTypeConfig(); + newCopy.Name = input.Name; + newCopy.Module = module; + newCopy.Description = input.Description; + newCopy.Label = input.Label; + + newCopy.VersionNo = 1; + newCopy.VersionStatus = ConfigurationItemVersionStatus.Draft; + newCopy.Origin = newCopy; + + // notification specific props + newCopy.CopyNotificationSpecificPropsFrom(src.NotNull()); + + newCopy.Normalize(); + + await Repository.InsertAsync(newCopy); + + await CopyTemplatesAsync(src, newCopy); + + return newCopy; + } + + private async Task CopyTemplatesAsync(NotificationTypeConfig source, NotificationTypeConfig destination) + { + var srcItems = await _templateRepository.GetAll().Where(i => i.PartOf == source).ToListAsync(); + + foreach (var srcItem in srcItems) + { + var dstItem = srcItem.Clone(); + dstItem.PartOf = destination; + + await _templateRepository.InsertAsync(dstItem); + } + } + + public async Task CreateNewVersionWithoutDetailsAsync(NotificationTypeConfig src) + { + var newVersion = new NotificationTypeConfig(); + newVersion.Origin = src.Origin; + newVersion.Name = src.Name; + newVersion.Module = src.Module; + newVersion.Description = src.Description; + newVersion.Label = src.Label; + newVersion.TenantId = src.TenantId; + + newVersion.ParentVersion = src; // set parent version + newVersion.VersionNo = src.VersionNo + 1; // version + 1 + newVersion.VersionStatus = ConfigurationItemVersionStatus.Draft; // draft + + // notification specific props + newVersion.CopyNotificationSpecificPropsFrom(src); + + newVersion.Normalize(); + + await Repository.InsertAsync(newVersion); + + return newVersion; + } + + public override async Task CreateNewVersionAsync(NotificationTypeConfig src) + { + var newVersion = await CreateNewVersionWithoutDetailsAsync(src); + + await CopyTemplatesAsync(src, newVersion); + + return newVersion; + } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs index 9874525c9a..270da0acd9 100644 --- a/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs @@ -1,21 +1,18 @@ -using Abp.BackgroundJobs; -using Abp.Dependency; +using Abp.Dependency; using Abp.Domain.Repositories; using Abp.Domain.Uow; using Abp.Notifications; using Abp.UI; using Castle.Core.Logging; using Hangfire; -using NHibernate.Linq; using Shesha.Domain; using Shesha.Domain.Enums; using Shesha.Email.Dtos; using Shesha.EntityReferences; +using Shesha.Extensions; using Shesha.NHibernate; -using Shesha.Notifications.Configuration; using Shesha.Notifications.Dto; using Shesha.Notifications.Exceptions; -using Shesha.Notifications.Helpers; using Shesha.Notifications.MessageParticipants; using Shesha.Services; using System; @@ -29,67 +26,44 @@ namespace Shesha.Notifications public class NotificationSender: INotificationSender, ITransientDependency { private readonly INotificationChannelSender _channelSender; - private readonly IIocManager _iocManager; private readonly IRepository _notificationMessageRepository; private readonly IRepository _attachmentRepository; private readonly IUnitOfWorkManager _unitOfWorkManager; - private readonly ISessionProvider _sessionProvider; private readonly IStoredFileService _fileService; private readonly IEnumerable _channelSenders; - private readonly IRepository _notificationChannelRepository; private readonly IRepository _notificationRepository; private readonly IRepository _messageTemplateRepository; private readonly IRepository _storedFileRepository; - private readonly IRepository _typeRepo; - private readonly IRepository _personRepo; - private readonly INotificationSettings _notificationSettings; - private readonly IStoredFileService _storedFileService; - private readonly IBackgroundJobManager _backgroundJobManager; private readonly INotificationManager _notificationManager; - private readonly IRepository _userNotificationPreferenceRepository; + private readonly INotificationTemplateProcessor _templateProcessor; public ILogger Logger { get; set; } public NotificationSender(INotificationChannelSender channelSender, - IIocManager iocManager, IRepository notificationMessageRepository, IRepository attachmentRepository, IUnitOfWorkManager unitOfWorkManager, - ISessionProvider sessionProvider, IStoredFileService fileService, IEnumerable channelSenders, - IRepository notificationChannelRepository, IRepository messageTemplateRepository, IRepository notificationRepository, IRepository storedFileRepository, - IRepository typeRepo, - IRepository personRepo, - IStoredFileService storedFileService, - INotificationSettings notificationSettings, - IBackgroundJobManager backgroundJobManager, INotificationManager notificationManager, - IRepository userNotificationPreferenceRepository) + INotificationTemplateProcessor templateProcessor) { _channelSender = channelSender; - _iocManager = iocManager; + _notificationMessageRepository = notificationMessageRepository; + _attachmentRepository = attachmentRepository; _unitOfWorkManager = unitOfWorkManager; - _sessionProvider = sessionProvider; _fileService = fileService; _channelSenders = channelSenders; - _notificationChannelRepository = notificationChannelRepository; _messageTemplateRepository = messageTemplateRepository; - _notificationSettings = notificationSettings; - _storedFileService = storedFileService; - _backgroundJobManager = backgroundJobManager; _notificationRepository = notificationRepository; _storedFileRepository = storedFileRepository; _notificationManager = notificationManager; - _attachmentRepository = attachmentRepository; - _notificationMessageRepository = notificationMessageRepository; - _typeRepo = typeRepo; - _personRepo = personRepo; + _templateProcessor = templateProcessor; + Logger = NullLogger.Instance; - _userNotificationPreferenceRepository = userNotificationPreferenceRepository; } private async Task> GetAttachmentsAsync(NotificationMessage message) @@ -108,8 +82,8 @@ private async Task> GetAttachmentsAsync(NotificationMessag result.Add(new EmailAttachment(attachment.FileName, fileStream)); } return result; - } - + } + /// /// /// @@ -120,16 +94,28 @@ private async Task> GetAttachmentsAsync(NotificationMessag /// /// /// + /// /// - /// + /// + /// /// - public async Task SendNotificationAsync(NotificationTypeConfig type, IMessageSender sender, IMessageReceiver receiver, TData data, RefListNotificationPriority priority, List attachments = null, GenericEntityReference triggeringEntity = null, NotificationChannelConfig channel = null) where TData : NotificationData + public async Task SendNotificationAsync( + NotificationTypeConfig type, + IMessageSender? sender, + IMessageReceiver receiver, + TData data, + RefListNotificationPriority priority, + List? attachments = null, + string? cc = null, + GenericEntityReference? triggeringEntity = null, + NotificationChannelConfig? channel = null, + string? category = null) where TData : NotificationData { // Check if the notification type is disabled if (type.Disable) return; - if (receiver != null && type.CanOptOut) + if (type.CanOptOut) { var optedOut = await receiver.IsNotificationOptedOutAsync(type); if (optedOut) @@ -140,11 +126,12 @@ public async Task SendNotificationAsync(NotificationTypeConfig type, IMes { Name = type.Name, NotificationType = type, - FromPerson = sender.GetPerson(), + FromPerson = sender?.GetPerson(), ToPerson = receiver.GetPerson(), NotificationData = JsonSerializer.Serialize(data), TriggeringEntity = triggeringEntity, - Priority = priority + Priority = priority, + Category = category ?? string.Empty, }); await _unitOfWorkManager.Current.SaveChangesAsync(); @@ -152,7 +139,7 @@ public async Task SendNotificationAsync(NotificationTypeConfig type, IMes if (channel != null) { // Send notification to a specific channel - await SendNotificationToChannelAsync(notification, data, sender, receiver, type, priority, channel, attachments); + await SendNotificationToChannelAsync(notification, data, sender, receiver, type, priority, channel, cc, attachments); } else { @@ -161,7 +148,7 @@ public async Task SendNotificationAsync(NotificationTypeConfig type, IMes foreach (var channelConfig in channels) { - await SendNotificationToChannelAsync(notification, data, sender, receiver, type, priority, channelConfig, attachments); + await SendNotificationToChannelAsync(notification, data, sender, receiver, type, priority, channelConfig, cc, attachments); } } } @@ -174,13 +161,23 @@ public async Task SendNotificationAsync(NotificationTypeConfig type, IMes /// /// /// + /// /// /// /// /// /// /// - private async Task SendNotificationToChannelAsync(Notification notification, TData data, IMessageSender sender, IMessageReceiver receiver, NotificationTypeConfig type, RefListNotificationPriority priority, NotificationChannelConfig channelConfig, List attachments = null) where TData : NotificationData + private async Task SendNotificationToChannelAsync( + Notification notification, + TData data, + IMessageSender? sender, + IMessageReceiver receiver, + NotificationTypeConfig type, + RefListNotificationPriority priority, + NotificationChannelConfig channelConfig, + string? cc = null, + List? attachments = null) where TData : NotificationData { var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channelConfig.SupportedFormat == x.MessageFormat); @@ -192,13 +189,14 @@ private async Task SendNotificationToChannelAsync(Notification notificati { PartOf = notification, Channel = channelConfig, - Subject = TemplateHelper.ReplacePlaceholders(template.TitleTemplate, data), - Message = TemplateHelper.ReplacePlaceholders(template.BodyTemplate, data), + Subject = await GenerateContentAsync(template.TitleTemplate, data) ?? string.Empty, + Message = await GenerateContentAsync(template.BodyTemplate, data) ?? string.Empty, RetryCount = 0, ReadStatus = RefListNotificationReadStatus.Unread, Direction = RefListNotificationDirection.Outgoing, Status = RefListNotificationStatus.Preparing, - RecipientText = receiver.GetAddress(_channelSender) + RecipientText = receiver.GetAddress(_channelSender), + Cc = cc, }); await _unitOfWorkManager.Current.SaveChangesAsync(); @@ -216,7 +214,8 @@ await _attachmentRepository.InsertAsync(new NotificationMessageAttachment() File = file, FileName = attachmentDto.FileName }); - } + } + await _unitOfWorkManager.Current.SaveChangesAsync(); } if (type.IsTimeSensitive) @@ -229,6 +228,16 @@ await _attachmentRepository.InsertAsync(new NotificationMessageAttachment() } } + /// + /// Generate content based on template (uses mustache syntax) + /// + protected async Task GenerateContentAsync(string? template, TData data) where TData: class + { + return !string.IsNullOrWhiteSpace(template) + ? await _templateProcessor.GenerateAsync(template, data) + : template; + } + private INotificationChannelSender GetChannelSender(NotificationMessage message) { var sender = _channelSenders.FirstOrDefault(x => x.GetType().Name == message.Channel.SenderTypeName); @@ -239,7 +248,7 @@ private INotificationChannelSender GetChannelSender(NotificationMessage message) return sender; } - private IMessageSender GetMessageSender(NotificationMessage message) + private IMessageSender? GetMessageSenderOrNull(NotificationMessage message) { if (message.PartOf.FromPerson != null) return new PersonMessageParticipant(message.PartOf.FromPerson); @@ -247,9 +256,11 @@ private IMessageSender GetMessageSender(NotificationMessage message) if (!string.IsNullOrWhiteSpace(message.SenderText)) return new RawAddressMessageParticipant(message.SenderText); - throw new Exception($"{nameof(message.PartOf.FromPerson)} or {nameof(message.SenderText)} must not be null"); + return null; } - + + private IMessageSender GetMessageSender(NotificationMessage message) => GetMessageSenderOrNull(message) ?? throw new Exception($"{nameof(message.PartOf.FromPerson)} or {nameof(message.SenderText)} must not be null"); + private IMessageReceiver GetMessageReceiver(NotificationMessage message) { if (message.PartOf.ToPerson != null) @@ -271,7 +282,7 @@ public async Task SendAsync(Guid messageId) var attachments = await GetAttachmentsAsync(message); - var sender = GetMessageSender(message); + var sender = GetMessageSenderOrNull(message); var reciever = GetMessageReceiver(message); message.RecipientText = reciever.GetAddress(channelSender); @@ -316,7 +327,7 @@ public async Task SendAsync(Guid fromPersonId, Guid toPersonId, Guid messageId, /// Attempts to send a notification and handles exceptions internally. /// private async Task TrySendAsync( - IMessageSender sender, + IMessageSender? sender, IMessageReceiver receiver, NotificationMessage message, INotificationChannelSender notificationChannelSender, @@ -324,7 +335,7 @@ private async Task TrySendAsync( { try { - var sendStatus = await notificationChannelSender.SendAsync(sender, receiver, message, "", attachments); + var sendStatus = await notificationChannelSender.SendAsync(sender, receiver, message, attachments); return new SendStatus { @@ -344,11 +355,23 @@ private async Task TrySendAsync( } } - public async Task SendNotificationAsync(NotificationTypeConfig type, Person senderPerson, Person receiverPerson, TData data, RefListNotificationPriority priority, List attachments = null, GenericEntityReference triggeringEntity = null, NotificationChannelConfig channel = null) where TData : NotificationData + public async Task SendNotificationAsync( + NotificationTypeConfig type, + Person? senderPerson, + Person receiverPerson, + TData data, + RefListNotificationPriority priority, + List? attachments = null, + string? cc = null, + GenericEntityReference? triggeringEntity = null, + NotificationChannelConfig? channel = null, + string? category = null) where TData : NotificationData { - var sender = new PersonMessageParticipant(senderPerson); + var sender = senderPerson != null + ? new PersonMessageParticipant(senderPerson) + : null; var receiver = new PersonMessageParticipant(receiverPerson); - await SendNotificationAsync(type, sender, receiver, data, priority, attachments, triggeringEntity, channel); + await SendNotificationAsync(type, sender, receiver, data, priority, attachments, cc, triggeringEntity, channel, category); } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationTemplateProcessor.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationTemplateProcessor.cs new file mode 100644 index 0000000000..001aa9c7e9 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationTemplateProcessor.cs @@ -0,0 +1,126 @@ +using Abp.Dependency; +using Abp.Domain.Entities; +using Abp.Notifications; +using Shesha.Configuration; +using Shesha.EntityReferences; +using Shesha.Extensions; +using Shesha.Reflection; +using Stubble.Core; +using Stubble.Core.Builders; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Shesha.Notifications +{ + /// + /// Notification template processor + /// + public class NotificationTemplateProcessor : INotificationTemplateProcessor, ISingletonDependency + { + private readonly IFrontendSettings _frontendSettings; + private readonly StubbleVisitorRenderer _stubbleRenderer; + + public NotificationTemplateProcessor(IFrontendSettings frontendSettings) + { + _frontendSettings = frontendSettings; + _stubbleRenderer = MakeStubbler(); + } + + private StubbleVisitorRenderer MakeStubbler() + { + return new StubbleBuilder().Configure(settings => + { + settings.SetIgnoreCaseOnKeyLookup(true); + settings.AddValueGetter(typeof(GenericEntityReference), (object value, string key, bool ignoreCase) => + { + if (value is GenericEntityReference entityRef) + { + var entity = (Entity)entityRef; + if (entity == null) + return null; + + var propAccessor = ReflectionHelper.GetPropertyValueAccessor(entity, key); + return propAccessor.IsValueAvailable + ? propAccessor.Value + : null; + } + else + return null; + }); + settings.AddValueGetter(typeof(NotificationData), (object value, string key, bool ignoreCase) => + { + if (value is NotificationData notificationData) + { + // shortcuts + if (key == "frontEndApps") + { + return new FrontEndAppsTag(_frontendSettings); + } + + return notificationData[key]; + } + else + return null; + }); + settings.AddValueGetter(typeof(IIndexer), (object value, string key, bool ignoreCase) => + { + return value is IIndexer indexer + ? indexer[key] + : null; + }); + + + }).Build(); + } + + public async Task GenerateAsync(string template, object model) + { + return await _stubbleRenderer.RenderAsync(template, model); + } + + public interface IIndexer + { + object this[string key] { get; } + } + + public class FrontEndAppTag + { + private readonly string _appKey; + private readonly IFrontendSettings _frontendSettings; + public FrontEndAppTag(IFrontendSettings frontendSettings, string appKey) + { + _frontendSettings = frontendSettings; + _appKey = appKey; + } + + public string? PublicUrl + { + get + { + var value = _frontendSettings.PublicUrl.GetValueOrNull(new Settings.SettingManagementContext { AppKey = _appKey }); + return value; + } + } + } + + public class FrontEndAppsTag : IIndexer + { + private readonly Dictionary _apps = new(); + private readonly IFrontendSettings _frontendSettings; + + public FrontEndAppsTag(IFrontendSettings frontendSettings) + { + _frontendSettings = frontendSettings; + } + + public object this[string key] + { + get + { + return _apps.Get(key, k => new FrontEndAppTag(_frontendSettings, k)); + } + } + } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/NullNotificationSender.cs b/shesha-core/src/Shesha.Application/Notifications/NullNotificationSender.cs new file mode 100644 index 0000000000..3544f4ee62 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/NullNotificationSender.cs @@ -0,0 +1,52 @@ +using Abp.Notifications; +using Shesha.Domain; +using Shesha.Domain.Enums; +using Shesha.EntityReferences; +using Shesha.Notifications.Dto; +using Shesha.Notifications.MessageParticipants; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Shesha.Notifications +{ + /// + /// Null-object implementation of . + /// + public class NullNotificationSender : INotificationSender + { + /// + /// Gets single instance of class. + /// + public static NullNotificationSender Instance { get; } = new NullNotificationSender(); + + public Task SendNotificationAsync( + NotificationTypeConfig type, + IMessageSender? sender, + IMessageReceiver receiver, + TData data, + RefListNotificationPriority priority, + List? attachments = null, + string? cc = null, + GenericEntityReference? triggeringEntity = null, + NotificationChannelConfig? channel = null, + string? category = null) where TData : NotificationData + { + return Task.CompletedTask; + } + + public Task SendNotificationAsync( + NotificationTypeConfig type, + Person? sender, + Person receiver, + TData data, + RefListNotificationPriority priority, + List? attachments = null, + string? cc = null, + GenericEntityReference? triggeringEntity = null, + NotificationChannelConfig? channel = null, + string? category = null) where TData : NotificationData + { + return Task.CompletedTask; + } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs index 6df4a63d69..ee69397009 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs @@ -1,5 +1,4 @@ using Abp.Domain.Repositories; -using Abp.Extensions; using Castle.Core.Logging; using Shesha.Domain; using Shesha.Email.Dtos; @@ -31,7 +30,7 @@ public SmsChannelSender(ISmsSettings smsSettings,IRepository /// /// - public string GetRecipientId(Person person) + public string? GetRecipientId(Person person) { return person.MobileNumber1; } @@ -44,29 +43,29 @@ private async Task GetSettingsAsync() return await _smsSettings.SmsSettings.GetValueAsync(); } - public async Task SendAsync(IMessageSender sender, IMessageReceiver reciever, NotificationMessage message, string cc = "", List attachments = null) + public async Task SendAsync(IMessageSender? sender, IMessageReceiver reciever, NotificationMessage message, List? attachments = null) { var settings = await GetSettingsAsync(); if (!settings.IsSmsEnabled) { Logger.Warn("SMSs are disabled"); - return await Task.FromResult(new SendStatus(){ - IsSuccess= false, - Message = "SMSs are disabled" - }); + return SendStatus.Failed("SMSs are disabled"); } - return await _smsGateway.SendSmsAsync(!settings.RedirectAllMessagesTo.IsNullOrWhiteSpace() ? settings.RedirectAllMessagesTo : reciever.GetAddress(this), message.Message); + var mobileNo = !string.IsNullOrWhiteSpace(settings.RedirectAllMessagesTo) + ? settings.RedirectAllMessagesTo + : reciever.GetAddress(this); + + if (string.IsNullOrWhiteSpace(mobileNo)) + return SendStatus.Failed("Recipient mobileNo is empty"); + + return await _smsGateway.SendSmsAsync(mobileNo, message.Message); } - public async Task BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null) + public Task BroadcastAsync(NotificationTopic topic, string subject, string message, List? attachments = null) { - return await Task.FromResult(new SendStatus() - { - IsSuccess = false, - Message = "Broadcast Implementation not yet implemented!" - }); + return Task.FromResult(SendStatus.Failed("Broadcast Implementation not yet implemented!")); } } } diff --git a/shesha-core/src/Shesha.Application/Otp/CacheOtpStorage.cs b/shesha-core/src/Shesha.Application/Otp/CacheOtpStorage.cs index a7e374d194..20923458b9 100644 --- a/shesha-core/src/Shesha.Application/Otp/CacheOtpStorage.cs +++ b/shesha-core/src/Shesha.Application/Otp/CacheOtpStorage.cs @@ -1,29 +1,26 @@ -using System; +using Shesha.Otp.Dto; +using Shesha.Otp.Exceptions; +using System; using System.Threading.Tasks; -using Abp.Runtime.Caching; -using Shesha.Otp.Dto; namespace Shesha.Otp { - public class CacheOtpStorage: IOtpStorage + public class CacheOtpStorage : IOtpStorage { - private const string CacheName = "OneTimePinsCache"; - private readonly ICacheManager _cacheManager; + private readonly IOtpCache _otpCache; - public CacheOtpStorage(ICacheManager cacheManager) + public CacheOtpStorage(IOtpCache otpCache) { - _cacheManager = cacheManager; + _otpCache = otpCache; } - public ITypedCache InternalCache => _cacheManager.GetCache(CacheName); - /// inheritedDoc public async Task SaveAsync(OtpDto input) { if (input.ExpiresOn.HasValue && input.ExpiresOn.Value <= DateTime.Now) return; - await InternalCache.SetAsync(input.OperationId, + await _otpCache.Cache.SetAsync(input.OperationId, input, slidingExpireTime: input.ExpiresOn.HasValue ? input.ExpiresOn.Value - DateTime.Now @@ -39,9 +36,14 @@ public async Task UpdateAsync(Guid operationId, Func updateAction) } /// inheritedDoc + public async Task GetOrNullAsync(Guid operationId) + { + return await _otpCache.Cache.GetOrDefaultAsync(operationId); + } + public async Task GetAsync(Guid operationId) { - return await InternalCache.GetOrDefaultAsync(operationId); + return (await GetOrNullAsync(operationId)) ?? throw new OtpOperationNotFoundException(operationId); } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Otp/Configuration/OtpSettings.cs b/shesha-core/src/Shesha.Application/Otp/Configuration/OtpSettings.cs index de53cd5bc9..2ec46b397a 100644 --- a/shesha-core/src/Shesha.Application/Otp/Configuration/OtpSettings.cs +++ b/shesha-core/src/Shesha.Application/Otp/Configuration/OtpSettings.cs @@ -1,12 +1,4 @@ -using Shesha.Settings; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Otp.Configuration +namespace Shesha.Otp.Configuration { public class OtpSettings { diff --git a/shesha-core/src/Shesha.Application/Otp/DbOtpStorage.cs b/shesha-core/src/Shesha.Application/Otp/DbOtpStorage.cs index b64b1c5b3d..b0e90d4db4 100644 --- a/shesha-core/src/Shesha.Application/Otp/DbOtpStorage.cs +++ b/shesha-core/src/Shesha.Application/Otp/DbOtpStorage.cs @@ -3,6 +3,7 @@ using Abp.Domain.Uow; using Shesha.Domain; using Shesha.Otp.Dto; +using Shesha.Otp.Exceptions; using System; using System.Threading.Tasks; using System.Transactions; @@ -74,7 +75,7 @@ public async Task UpdateAsync(Guid operationId, Func updateAction) /// /// /// - public async Task GetAsync(Guid operationId) + public async Task GetOrNullAsync(Guid operationId) { using (var uow = _uowManager.Begin(TransactionScopeOption.Suppress)) { @@ -98,5 +99,10 @@ public async Task GetAsync(Guid operationId) }; } } + + public async Task GetAsync(Guid operationId) + { + return (await GetOrNullAsync(operationId)) ?? throw new OtpOperationNotFoundException(operationId); + } } } diff --git a/shesha-core/src/Shesha.Application/Otp/Exceptions/OtpException.cs b/shesha-core/src/Shesha.Application/Otp/Exceptions/OtpException.cs new file mode 100644 index 0000000000..e34652097f --- /dev/null +++ b/shesha-core/src/Shesha.Application/Otp/Exceptions/OtpException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Shesha.Otp.Exceptions +{ + public class OtpException : Exception + { + public OtpException(string message): base(message) + { + + } + } +} diff --git a/shesha-core/src/Shesha.Application/Otp/Exceptions/OtpOperationNotFoundException.cs b/shesha-core/src/Shesha.Application/Otp/Exceptions/OtpOperationNotFoundException.cs new file mode 100644 index 0000000000..704f24041d --- /dev/null +++ b/shesha-core/src/Shesha.Application/Otp/Exceptions/OtpOperationNotFoundException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Shesha.Otp.Exceptions +{ + public class OtpOperationNotFoundException : OtpException + { + public Guid OperationId { get; private set; } + + public OtpOperationNotFoundException(Guid operationId): base($"PIN for operation '{operationId}' not found") + { + OperationId = operationId; + } + } +} diff --git a/shesha-core/src/Shesha.Application/Otp/IOtpCache.cs b/shesha-core/src/Shesha.Application/Otp/IOtpCache.cs new file mode 100644 index 0000000000..68de46ecaf --- /dev/null +++ b/shesha-core/src/Shesha.Application/Otp/IOtpCache.cs @@ -0,0 +1,13 @@ +using Shesha.Cache; +using Shesha.Otp.Dto; +using System; + +namespace Shesha.Otp +{ + /// + /// OTP cache + /// + public interface IOtpCache : ICacheHolder + { + } +} diff --git a/shesha-core/src/Shesha.Application/Otp/IOtpStorage.cs b/shesha-core/src/Shesha.Application/Otp/IOtpStorage.cs index d92ce0d4a0..91bf5a5810 100644 --- a/shesha-core/src/Shesha.Application/Otp/IOtpStorage.cs +++ b/shesha-core/src/Shesha.Application/Otp/IOtpStorage.cs @@ -27,8 +27,11 @@ public interface IOtpStorage /// /// Get OTP /// - /// - /// + Task GetOrNullAsync(Guid operationId); + + /// + /// Get OTP + /// Task GetAsync(Guid operationId); } } diff --git a/shesha-core/src/Shesha.Application/Otp/OtpAppService.cs b/shesha-core/src/Shesha.Application/Otp/OtpAppService.cs index 4582a5ed8b..242593c716 100644 --- a/shesha-core/src/Shesha.Application/Otp/OtpAppService.cs +++ b/shesha-core/src/Shesha.Application/Otp/OtpAppService.cs @@ -1,9 +1,7 @@ using Abp.Dependency; -using Abp.Net.Mail; using Microsoft.AspNetCore.Mvc; using Shesha.Otp.Configuration; using Shesha.Otp.Dto; -using Shesha.Sms; using System.Threading.Tasks; namespace Shesha.Otp diff --git a/shesha-core/src/Shesha.Application/Otp/OtpCache.cs b/shesha-core/src/Shesha.Application/Otp/OtpCache.cs new file mode 100644 index 0000000000..1a88c01ab6 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Otp/OtpCache.cs @@ -0,0 +1,18 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; +using Shesha.Otp.Dto; +using System; + +namespace Shesha.Otp +{ + /// + /// OTP cache + /// + public class OtpCache : CacheHolder, IOtpCache, ISingletonDependency + { + public OtpCache(ICacheManager cacheManager) : base("OneTimePinsCache", cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Application/Otp/OtpManager.cs b/shesha-core/src/Shesha.Application/Otp/OtpManager.cs index 139fb23b92..0da4a5d4a0 100644 --- a/shesha-core/src/Shesha.Application/Otp/OtpManager.cs +++ b/shesha-core/src/Shesha.Application/Otp/OtpManager.cs @@ -29,15 +29,15 @@ public OtpManager(ISmsGateway smsGateway, IEmailSender emailSender, IOtpStorage } /// inheritedDoc - public async Task GetAsync(Guid operationId) + public async Task GetOrNullAsync(Guid operationId) { - return await _otpStorage.GetAsync(operationId); + return await _otpStorage.GetOrNullAsync(operationId); } public async Task ResendPinAsync(ResendPinInput input) { var settings = await _otpSettings.OneTimePins.GetValueAsync(); - var otp = await _otpStorage.GetAsync(input.OperationId); + var otp = await _otpStorage.GetOrNullAsync(input.OperationId); if (otp == null) throw new UserFriendlyException("OTP not found, try to request a new one"); @@ -164,10 +164,12 @@ public async Task VerifyPinAsync(VerifyPinInput input) var settings = await _otpSettings.OneTimePins.GetValueAsync(); if (!settings.IgnoreOtpValidation) { - var pinDto = await _otpStorage.GetAsync(input.OperationId); + var pinDto = await _otpStorage.GetOrNullAsync(input.OperationId); if (pinDto == null || pinDto.Pin != input.Pin) { - var message = pinDto.SendType == OtpSendType.EmailLink ? "Invalid email link" : "Wrong one time pin"; + var message = pinDto?.SendType == OtpSendType.EmailLink + ? "Invalid email link" + : "Wrong one time pin"; return VerifyPinResponse.Failed(message); } diff --git a/shesha-core/src/Shesha.Application/Permissions/ApiPermissionedObjectProvider.cs b/shesha-core/src/Shesha.Application/Permissions/ApiPermissionedObjectProvider.cs index 6a42bc66c1..91a1388c20 100644 --- a/shesha-core/src/Shesha.Application/Permissions/ApiPermissionedObjectProvider.cs +++ b/shesha-core/src/Shesha.Application/Permissions/ApiPermissionedObjectProvider.cs @@ -2,8 +2,7 @@ using Abp.AspNetCore.Mvc.Authorization; using Abp.AspNetCore.Mvc.Extensions; using Abp.Authorization; -using Abp.Collections.Extensions; -using Abp.Modules; +using Abp.Domain.Uow; using Abp.Reflection; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -27,18 +26,17 @@ public class ApiPermissionedObjectProvider : PermissionedObjectProviderBase, IPe { private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider; - private readonly IModuleManager _moduleManager; private readonly IApplicationStartupSession _startupSession; public ApiPermissionedObjectProvider( IAssemblyFinder assembleFinder, IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, + IApplicationStartupSession startupSession, IModuleManager moduleManager, - IApplicationStartupSession startupSession - ) : base(assembleFinder) + IUnitOfWorkManager unitOfWorkManager + ) : base(assembleFinder, moduleManager, unitOfWorkManager) { _apiDescriptionsProvider = apiDescriptionsProvider; - _moduleManager = moduleManager; _startupSession = startupSession; } @@ -56,11 +54,10 @@ private bool IsCrud(Type type) return false; } - public string GetObjectType(Type type) + public string? GetObjectType(Type type) { var shaServiceType = typeof(ApplicationService); var controllerType = typeof(ControllerBase); - return !(type.IsPublic && !type.IsAbstract && (shaServiceType.IsAssignableFrom(type) || controllerType.IsAssignableFrom(type))) ? null // if not controller @@ -69,71 +66,42 @@ public string GetObjectType(Type type) : ShaPermissionedObjectsTypes.WebApi; } - private string GetMethodType(string parentType) - { - return parentType == ShaPermissionedObjectsTypes.WebApi - ? ShaPermissionedObjectsTypes.WebApiAction - : parentType == ShaPermissionedObjectsTypes.WebCrudApi - ? ShaPermissionedObjectsTypes.WebCrudApiAction - : null; - } - - private Type GetModuleOfType(Type type) - { - return type.Assembly.GetTypes().FirstOrDefault(t => t.IsPublic && !t.IsAbstract && typeof(AbpModule).IsAssignableFrom(t)); - } - - private Dictionary _modules = new Dictionary(); - - private async Task GetModuleOfAssemblyAsync(Assembly assembly) - { - Module module = null; - if (_modules.TryGetValue(assembly, out module)) - { - return module; - } - module = await _moduleManager.GetOrCreateModuleAsync(assembly); - _modules.Add(assembly, module); - return module; - } - - private string GetMd5(PermissionedObjectDto dto) - { - return $"{dto.Hardcoded}|{dto.Access?.ToString() ?? "null"}|{string.Join(',', dto.Permissions)}|{dto.ModuleId}|{dto.Parent}|{dto.Name}|{string.Join("|", dto.AdditionalParameters.Select(x => x.Key + "@" + x.Value))}" - .ToMd5Fingerprint(); - } - - public async Task> GetAllAsync(string objectType = null, bool skipUnchangedAssembly = false) + public async Task> GetAllAsync(string? objectType = null, bool skipUnchangedAssembly = false) { if (objectType != null && !GetObjectTypes().Contains(objectType)) return new List(); - var modules = (await _apiDescriptionsProvider.ApiDescriptionGroups.Items.SelectManyAsync(async g => await g.Items.SelectAsync(async a => - { - var descriptor = a.ActionDescriptor.AsControllerActionDescriptor(); - var service = descriptor.ControllerTypeInfo.AsType(); - var module = GetModuleOfType(service); - var name = service.Name; - var isDynamic = service.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDynamicCrudAppService<,,>)); + var apiDescriptors = new List(); + foreach (var apiDescGroup in _apiDescriptionsProvider.ApiDescriptionGroups.Items) + { + foreach (var apiDesc in apiDescGroup.Items) + { + var descriptor = apiDesc.ActionDescriptor.AsControllerActionDescriptor(); + var service = descriptor.ControllerTypeInfo.AsType(); + var assembly = service.Assembly; + var name = service.Name; + var isDynamic = service.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDynamicCrudAppService<,,>)); + + if (isDynamic || skipUnchangedAssembly && _startupSession.AssemblyStaysUnchanged(assembly)) + continue; + + var module = await GetModuleOfAssemblyAsync(assembly); + + apiDescriptors.Add(new ApiDescriptor() + { + Description = apiDesc, + Module = module, + Service = service, + HttpMethod = apiDesc.HttpMethod, + Endpoint = apiDesc.RelativePath, + Action = descriptor.MethodInfo, + Assembly = assembly, + }); + } + } - if (isDynamic || skipUnchangedAssembly && _startupSession.AssemblyStaysUnchanged(module.Assembly)) - return null; + var modules = apiDescriptors.GroupBy(x => x.Module); - return new ApiDescriptor() - { - Description = a, - Module = await GetModuleOfAssemblyAsync(module.Assembly), - Service = service, - HttpMethod = a.HttpMethod, - Endpoint = a.RelativePath, - Action = descriptor.MethodInfo, - Assembly = module.Assembly, - }; - }))) - .Where(x => x != null) - .GroupBy(x => x.Module); - - var allApiPermissions = - new List(); + var allApiPermissions = new List(); foreach (var module in modules) { @@ -188,7 +156,7 @@ public async Task> GetAllAsync(string objectType = n var parent = new PermissionedObjectDto() { - Object = service.FullName, + Object = service.GetRequiredFullName(), ModuleId = module.Key?.Id, Module = module.Key?.Name, Name = GetName(service, serviceName), @@ -259,8 +227,11 @@ public async Task> GetAllAsync(string objectType = n Hardcoded = methodHardcoded, }; - child.AdditionalParameters.Add("HttpMethod", methodInfo.HttpMethod); - child.AdditionalParameters.Add("Endpoint", methodInfo.Endpoint); + if (!string.IsNullOrWhiteSpace(methodInfo.HttpMethod)) + child.AdditionalParameters.Add("HttpMethod", methodInfo.HttpMethod); + + if (!string.IsNullOrWhiteSpace(methodInfo.Endpoint)) + child.AdditionalParameters.Add("Endpoint", methodInfo.Endpoint); //parent.Child.Add(child); child.Md5 = GetMd5(child); @@ -274,13 +245,13 @@ public async Task> GetAllAsync(string objectType = n private class ApiDescriptor { - public ApiDescription Description { get; set; } - public Module Module { get; set; } - public Type Service { get; set; } - public MethodInfo Action { get; set; } - public string HttpMethod { get; set; } - public string Endpoint { get; set; } - public Assembly Assembly { get; set; } + public required ApiDescription Description { get; set; } + public Module? Module { get; set; } + public required Type Service { get; set; } + public required MethodInfo Action { get; set; } + public string? HttpMethod { get; set; } + public string? Endpoint { get; set; } + public required Assembly Assembly { get; set; } } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Permissions/Dtos/PermissionDto.cs b/shesha-core/src/Shesha.Application/Permissions/Dtos/PermissionDto.cs index 81e57fd837..ebf42a7592 100644 --- a/shesha-core/src/Shesha.Application/Permissions/Dtos/PermissionDto.cs +++ b/shesha-core/src/Shesha.Application/Permissions/Dtos/PermissionDto.cs @@ -7,7 +7,7 @@ namespace Shesha.Roles.Dto { [AutoMapFrom(typeof(Abp.Authorization.Permission))] - public class PermissionDto : EntityDto + public class PermissionDto : EntityDto { public PermissionDto() { @@ -15,20 +15,20 @@ public PermissionDto() } public Guid? ModuleId { get; set; } - public EntityReferenceDto Module { get; set; } + public EntityReferenceDto? Module { get; set; } public string Name { get; set; } - public string DisplayName { get; set; } + public string? DisplayName { get; set; } - public string Description { get; set; } + public string? Description { get; set; } - public string ParentName { get; set; } + public string? ParentName { get; set; } public bool IsDbPermission { get; set; } - public PermissionDto Parent { get; set; } + public PermissionDto? Parent { get; set; } - public List Child { get; set; } + public List? Child { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/Permissions/IPermissionedObjectAppService.cs b/shesha-core/src/Shesha.Application/Permissions/IPermissionedObjectAppService.cs index e06d61e1b8..42efc7479c 100644 --- a/shesha-core/src/Shesha.Application/Permissions/IPermissionedObjectAppService.cs +++ b/shesha-core/src/Shesha.Application/Permissions/IPermissionedObjectAppService.cs @@ -1,9 +1,4 @@ -using System; -using Abp.Application.Services; -using Abp.Application.Services.Dto; -using Shesha.Permissions; - -namespace Shesha.Permissions +namespace Shesha.Permissions { /// /// Person Application Service diff --git a/shesha-core/src/Shesha.Application/Permissions/PermissionAppService.cs b/shesha-core/src/Shesha.Application/Permissions/PermissionAppService.cs index b851681a7a..9fcf761e3e 100644 --- a/shesha-core/src/Shesha.Application/Permissions/PermissionAppService.cs +++ b/shesha-core/src/Shesha.Application/Permissions/PermissionAppService.cs @@ -8,6 +8,7 @@ using Shesha.Domain.ConfigurationItems; using Shesha.Extensions; using Shesha.Permissions.Dtos; +using Shesha.Reflection; using Shesha.Roles.Dto; using System; using System.Collections.Generic; @@ -21,7 +22,7 @@ public class PermissionAppService : SheshaAppServiceBase { private readonly IRepository _moduleRepository; private readonly ILocalizationContext _localizationContext; - private IShaPermissionManager _shaPermissionManager => PermissionManager as IShaPermissionManager; + private IShaPermissionManager _shaPermissionManager => PermissionManager.ForceCastAs(); private readonly IShaPermissionChecker _permissionChecker; private const string emptyId = "_"; @@ -37,10 +38,10 @@ IShaPermissionChecker permissionChecker _permissionChecker = permissionChecker; } - public async Task GetAsync(string id) + public async Task GetAsync(string id) { if (string.IsNullOrEmpty(id)) - return null; + throw new ArgumentNullException(nameof(id)); var dto = ObjectMapper.Map(PermissionManager.GetPermission(id)); dto.Module = dto.ModuleId != null @@ -132,7 +133,7 @@ public async Task CreateAsync(PermissionDto permission) [HttpPut, HttpPost] // ToDo: temporary - Allow HttpPost because permission can be created from edit mode public async Task UpdateAsync(PermissionDto permission) { - if (permission?.Id == emptyId) + if (permission.Id == emptyId) { permission.Id = null; return await CreateAsync(permission); @@ -149,7 +150,7 @@ public async Task UpdateAsync(PermissionDto permission) VersionStatus = ConfigurationItemVersionStatus.Live, }; - var res = await _shaPermissionManager.EditPermissionAsync(permission.Id, dbp); + var res = await _shaPermissionManager.EditPermissionAsync(permission.Id.NotNull(), dbp); return ObjectMapper.Map(res); } @@ -168,7 +169,7 @@ public async Task DeleteAsync(string name) } [HttpGet] - public Task> AutocompleteAsync(string term) + public Task> AutocompleteAsync(string? term) { term = (term ?? "").ToLower(); diff --git a/shesha-core/src/Shesha.Application/Permissions/PermissionedObjectAppService.cs b/shesha-core/src/Shesha.Application/Permissions/PermissionedObjectAppService.cs index f433affde1..b08e52f5c9 100644 --- a/shesha-core/src/Shesha.Application/Permissions/PermissionedObjectAppService.cs +++ b/shesha-core/src/Shesha.Application/Permissions/PermissionedObjectAppService.cs @@ -1,6 +1,6 @@ using Abp.Application.Services.Dto; -using Abp.Authorization; using Abp.Domain.Repositories; +using DocumentFormat.OpenXml.Office2010.Excel; using Shesha.Authorization; using Shesha.Domain; using Shesha.Domain.Enums; @@ -74,7 +74,7 @@ public async Task GetByObjectNameAsync(string objectName, /// /// /// - public async Task SetPermissionsAsync(string objectName, RefListPermissionedAccess access, List permissions) + public async Task SetPermissionsAsync(string objectName, RefListPermissionedAccess access, List permissions) { return await _permissionedObjectManager.SetPermissionsAsync(objectName, access, permissions); } @@ -97,7 +97,8 @@ public async Task GetApiPermissionsAsync(string serviceNa public override async Task GetAsync(EntityDto input) { if (input.Id == Guid.Empty) - return null; + throw new ArgumentNullException("id"); + return await _permissionedObjectManager.GetAsync(input.Id); } @@ -119,7 +120,7 @@ public override async Task UpdateAsync(PermissionedObject /// /// /// - public async Task SetApiPermissionsAsync(string serviceName, string actionName, RefListPermissionedAccess access, List permissions) + public async Task SetApiPermissionsAsync(string serviceName, string actionName, RefListPermissionedAccess access, List permissions) { var action = string.IsNullOrEmpty(actionName) ? "" : "@" + actionName; return await _permissionedObjectManager.SetPermissionsAsync($"{serviceName}{action}", access, permissions); diff --git a/shesha-core/src/Shesha.Application/Persons/PersonAccountDto.cs b/shesha-core/src/Shesha.Application/Persons/PersonAccountDto.cs index 4540991ac9..7a6eee3236 100644 --- a/shesha-core/src/Shesha.Application/Persons/PersonAccountDto.cs +++ b/shesha-core/src/Shesha.Application/Persons/PersonAccountDto.cs @@ -1,10 +1,10 @@ -using System; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using Abp.Application.Services.Dto; +using Abp.Application.Services.Dto; using Abp.AutoMapper; using Shesha.AutoMapper.Dto; using Shesha.Domain; +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; namespace Shesha.Persons { @@ -14,32 +14,32 @@ public class PersonAccountDto: EntityDto { [Required] [MinLength(5)] - public string UserName { get; set; } + public string? UserName { get; set; } [Required] [MinLength(1)] - public string FirstName { get; set; } + public string? FirstName { get; set; } [Required] [MinLength(1)] - public string LastName { get; set; } + public string? LastName { get; set; } [ReadOnly(true)] - public string FullName { get; set; } + public string? FullName { get; set; } - public string MobileNumber { get; set; } + public string? MobileNumber { get; set; } [EmailAddress] [Required] - public string EmailAddress { get; set; } + public string? EmailAddress { get; set; } public bool isContractor { get; set; } public EntityReferenceDto PrimaryOrganisation { get; set; } - public ReferenceListItemValueDto TypeOfAccount { get; set; } + public ReferenceListItemValueDto? TypeOfAccount { get; set; } - public string GoToUrlAfterRegistration { get; set; } + public string? GoToUrlAfterRegistration { get; set; } public long UserId { get; set; } } diff --git a/shesha-core/src/Shesha.Application/ReferenceLists/Dto/GetReferenceListByNameInput.cs b/shesha-core/src/Shesha.Application/ReferenceLists/Dto/GetReferenceListByNameInput.cs index 1edcae8dd9..084b453393 100644 --- a/shesha-core/src/Shesha.Application/ReferenceLists/Dto/GetReferenceListByNameInput.cs +++ b/shesha-core/src/Shesha.Application/ReferenceLists/Dto/GetReferenceListByNameInput.cs @@ -11,7 +11,7 @@ public class GetReferenceListByNameInput : GetReferenceListInputBase /// /// Module name /// - public string Module { get; set; } + public string? Module { get; set; } /// /// Reference list name diff --git a/shesha-core/src/Shesha.Application/ReferenceLists/Dto/GetReferenceListInputBase.cs b/shesha-core/src/Shesha.Application/ReferenceLists/Dto/GetReferenceListInputBase.cs index 2528804929..85f8409651 100644 --- a/shesha-core/src/Shesha.Application/ReferenceLists/Dto/GetReferenceListInputBase.cs +++ b/shesha-core/src/Shesha.Application/ReferenceLists/Dto/GetReferenceListInputBase.cs @@ -9,6 +9,6 @@ public class GetReferenceListInputBase /// MD5 of the reference list. Is used for the client side caching. /// If specified, the application should compare the value received from the client with a local cache and return http response with code 304 (not changed) /// - public string Md5 { get; set; } + public string? Md5 { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/ReferenceLists/ReferenceListAppService.cs b/shesha-core/src/Shesha.Application/ReferenceLists/ReferenceListAppService.cs index 7963f835cd..362644e38a 100644 --- a/shesha-core/src/Shesha.Application/ReferenceLists/ReferenceListAppService.cs +++ b/shesha-core/src/Shesha.Application/ReferenceLists/ReferenceListAppService.cs @@ -141,18 +141,6 @@ private string GetMd5(ReferenceListWithItemsDto dto) return json.ToMd5Fingerprint(); } - /// - /// Ger module by name - /// - /// - /// - private async Task GetModuleAsync(string moduleName) - { - return !string.IsNullOrWhiteSpace(moduleName) - ? await AsyncQueryableExecuter.FirstOrDefaultAsync(_moduleRepository.GetAll().Where(m => m.Name == moduleName)) - : null; - } - #endregion } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Roles/RoleAppService.cs b/shesha-core/src/Shesha.Application/Roles/RoleAppService.cs index 0aadfb91b5..9213494ab6 100644 --- a/shesha-core/src/Shesha.Application/Roles/RoleAppService.cs +++ b/shesha-core/src/Shesha.Application/Roles/RoleAppService.cs @@ -85,7 +85,7 @@ public override async Task DeleteAsync(EntityDto input) { CheckDeletePermission(); - var role = await _roleManager.FindByIdAsync(input.Id.ToString()); + var role = await _roleManager.GetRoleByNameAsync(input.Id.ToString()); var users = await _userManager.GetUsersInRoleAsync(role.NormalizedName); foreach (var user in users) @@ -110,7 +110,7 @@ protected override IQueryable CreateFilteredQuery(PagedRoleResultRequestDt return Repository.GetAllIncluding(x => x.Permissions) .WhereIf(!input.Keyword.IsNullOrWhiteSpace(), x => x.Name.Contains(input.Keyword) || x.DisplayName.Contains(input.Keyword) - || x.Description.Contains(input.Keyword)); + || x.Description != null && x.Description.Contains(input.Keyword)); } protected override async Task GetEntityByIdAsync(int id) diff --git a/shesha-core/src/Shesha.Application/SecurityQuestions/Dto/SecurityQuestionsMapProfile.cs b/shesha-core/src/Shesha.Application/SecurityQuestions/Dto/SecurityQuestionsMapProfile.cs index 84a03fc909..2de95178fe 100644 --- a/shesha-core/src/Shesha.Application/SecurityQuestions/Dto/SecurityQuestionsMapProfile.cs +++ b/shesha-core/src/Shesha.Application/SecurityQuestions/Dto/SecurityQuestionsMapProfile.cs @@ -1,13 +1,7 @@ -using Shesha.Authorization.Users; -using Shesha.AutoMapper; +using Shesha.AutoMapper; using Shesha.AutoMapper.Dto; using Shesha.Domain; -using Shesha.Extensions; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.SecurityQuestions.Dto { @@ -17,9 +11,11 @@ public SecurityQuestionsMapProfile() { CreateMap() .ForMember(a => a.User, option => option.MapFrom(e => e.User != null - ? new EntityReferenceDto(e.User.Id, e.User.FullName, e.User.GetClassName()) : null)) + ? new EntityReferenceDto(e.User) { _displayName = e.User.FullName } + : null)) .ForMember(a => a.SelectedQuestion, option => option.MapFrom(e => e.SelectedQuestion != null - ? new EntityReferenceDto(e.SelectedQuestion.Id, e.SelectedQuestion.Question, e.SelectedQuestion.GetClassName()) : null)); + ? new EntityReferenceDto(e.SelectedQuestion) { _displayName = e.SelectedQuestion.Question } + : null)); CreateMap() .ForMember(a => a.User, option => option.Ignore()) diff --git a/shesha-core/src/Shesha.Application/SecurityQuestions/Dto/UserAnswerDto.cs b/shesha-core/src/Shesha.Application/SecurityQuestions/Dto/UserAnswerDto.cs index 6be41c435a..ed9c9fbb12 100644 --- a/shesha-core/src/Shesha.Application/SecurityQuestions/Dto/UserAnswerDto.cs +++ b/shesha-core/src/Shesha.Application/SecurityQuestions/Dto/UserAnswerDto.cs @@ -3,20 +3,16 @@ using Shesha.AutoMapper.Dto; using Shesha.Domain; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.SecurityQuestions.Dto { [AutoMap(typeof(QuestionAssignment))] public class UserAnswerDto: EntityDto { - public EntityReferenceDto User { get; set; } + public EntityReferenceDto? User { get; set; } - public EntityReferenceDto SelectedQuestion { get; set; } + public EntityReferenceDto? SelectedQuestion { get; set; } - public string Answer { get; set; } + public string? Answer { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/SecurityQuestions/QuestionAnswersAppService.cs b/shesha-core/src/Shesha.Application/SecurityQuestions/QuestionAnswersAppService.cs index 0116c702a8..163e067e48 100644 --- a/shesha-core/src/Shesha.Application/SecurityQuestions/QuestionAnswersAppService.cs +++ b/shesha-core/src/Shesha.Application/SecurityQuestions/QuestionAnswersAppService.cs @@ -1,11 +1,10 @@ -using Abp.Authorization; -using Abp.Domain.Repositories; +using Abp.Domain.Repositories; using Abp.UI; using Shesha.Authorization; using Shesha.Authorization.Users; -using Shesha.Configuration; using Shesha.Configuration.Security; using Shesha.Domain; +using Shesha.Reflection; using Shesha.SecurityQuestions.Dto; using System; using System.Threading.Tasks; @@ -31,9 +30,7 @@ public override async Task CreateAsync(UserAnswerDto input) var currentUserId = AbpSession.UserId; if (!currentUserId.HasValue) - { throw new UserFriendlyException("Please log in to submit your answers to the security questions."); - } var user = await _userRepository.GetAsync(currentUserId.Value); @@ -48,6 +45,7 @@ public override async Task CreateAsync(UserAnswerDto input) throw new UserFriendlyException($"Maximum of {numberOfQuestionsAllowed} questions allowed"); } + input.SelectedQuestion.NotNull("Question is not selected"); var alreadyAnsweredQuestions = await Repository.CountAsync(q => q.User == user && q.SelectedQuestion.Id == input.SelectedQuestion.Id); if (alreadyAnsweredQuestions != 0) { diff --git a/shesha-core/src/Shesha.Application/Sessions/Dto/GetCurrentLoginInfoOutput.cs b/shesha-core/src/Shesha.Application/Sessions/Dto/GetCurrentLoginInfoOutput.cs index 51b094eec4..4df2de16aa 100644 --- a/shesha-core/src/Shesha.Application/Sessions/Dto/GetCurrentLoginInfoOutput.cs +++ b/shesha-core/src/Shesha.Application/Sessions/Dto/GetCurrentLoginInfoOutput.cs @@ -2,8 +2,8 @@ { public class GetCurrentLoginInfoOutput { - public UserLoginInfoDto User { get; set; } + public UserLoginInfoDto? User { get; set; } - public TenantLoginInfoDto Tenant { get; set; } + public TenantLoginInfoDto? Tenant { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/Sessions/Dto/UserLoginInfoDto.cs b/shesha-core/src/Shesha.Application/Sessions/Dto/UserLoginInfoDto.cs index b3a6ec8eea..ccad53e533 100644 --- a/shesha-core/src/Shesha.Application/Sessions/Dto/UserLoginInfoDto.cs +++ b/shesha-core/src/Shesha.Application/Sessions/Dto/UserLoginInfoDto.cs @@ -10,18 +10,17 @@ namespace Shesha.Sessions.Dto public class UserLoginInfoDto : EntityDto { public bool AccountFound { get; set; } - public string UserName { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string FullName { get; set; } - public string Picture { get; set; } - public string Email { get; set; } - public string MobileNumber { get; set; } + public required string UserName { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? FullName { get; set; } + public string? Picture { get; set; } + public string? Email { get; set; } + public string? MobileNumber { get; set; } public bool HasRegistered { get; set; } - public string LoginProvider { get; set; } - //public TypeOfAccount? TypeOfAccount { get; set; } + public string? LoginProvider { get; set; } public Guid PersonId { get; set; } - public string HomeUrl { get; set; } + public string? HomeUrl { get; set; } public bool IsSelfServiceUser { get; set; } public List GrantedPermissions { get; set; } = new List(); } diff --git a/shesha-core/src/Shesha.Application/Sessions/SessionAppService.cs b/shesha-core/src/Shesha.Application/Sessions/SessionAppService.cs index 88ff5fedbe..905a86fe6c 100644 --- a/shesha-core/src/Shesha.Application/Sessions/SessionAppService.cs +++ b/shesha-core/src/Shesha.Application/Sessions/SessionAppService.cs @@ -39,7 +39,7 @@ public async Task GetCurrentLoginInformationsAsync() [AllowAnonymous] public async Task GetCurrentLoginInfoAsync() { - var output = new GetCurrentLoginInfoOutput { }; + var output = new GetCurrentLoginInfoOutput(); if (AbpSession.TenantId.HasValue) { @@ -89,7 +89,7 @@ private async Task> GetGrantedPermissionsAsync() { if (await PermissionChecker.IsGrantedAsync(permissionName)) { - var permissionRoles = roles.Where(x => x.Role.Permissions.Any(p => p.Permission == permissionName)).ToList(); + var permissionRoles = roles.Where(x => x.Role != null && x.Role.Permissions.Any(p => p.Permission == permissionName)).ToList(); grantedPermissions.Add(new GrantedPermissionDto { Permission = permissionName, @@ -104,7 +104,7 @@ private async Task> GetGrantedPermissionsAsync() foreach(var role in roles) { - if (!role.Role.Permissions.Any()) + if (role.Role == null || !role.Role.Permissions.Any()) continue; foreach (var permission in role.Role.Permissions.Where(x => x.IsGranted)) @@ -135,8 +135,8 @@ public async Task> GetGrantedShaRolesAsync() if (currentUser == null) return new List(); var roles = await _roleAppointmentRepository.GetAll() - .Where(a => a.Person == currentUser) - .Select(a => a.Role.Name) + .Where(a => a.Person == currentUser && a.Role != null) + .Select(a => a.Role!.Name) .Distinct() .ToListAsync(); return roles; diff --git a/shesha-core/src/Shesha.Application/Settings/Dto/GetDynamicSettingValueInput.cs b/shesha-core/src/Shesha.Application/Settings/Dto/GetDynamicSettingValueInput.cs index accc2d5f59..1ccd682052 100644 --- a/shesha-core/src/Shesha.Application/Settings/Dto/GetDynamicSettingValueInput.cs +++ b/shesha-core/src/Shesha.Application/Settings/Dto/GetDynamicSettingValueInput.cs @@ -10,11 +10,11 @@ public class GetDynamicSettingValueInput : GetSettingValueInput /// /// Name of the type the setting value should be able to deserialise into /// - public string Datatype { get; set; } + public string? Datatype { get; set; } /// /// that default value of the setting the first time it is accessed. /// - public object DefaultValue { get; set; } + public object? DefaultValue { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Settings/Dto/GetSettingValueInput.cs b/shesha-core/src/Shesha.Application/Settings/Dto/GetSettingValueInput.cs index 3c5d643304..97004042fa 100644 --- a/shesha-core/src/Shesha.Application/Settings/Dto/GetSettingValueInput.cs +++ b/shesha-core/src/Shesha.Application/Settings/Dto/GetSettingValueInput.cs @@ -11,6 +11,6 @@ public class GetSettingValueInput : SettingIdentifierDto /// Front-end application key, see . Is used for client-specific applications only. /// NOTE: this parameter if optional with fallback to the `sha-frontend-application` header /// - public string AppKey { get; set; } + public string? AppKey { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Settings/Dto/SettingConfigurationDto.cs b/shesha-core/src/Shesha.Application/Settings/Dto/SettingConfigurationDto.cs index 32cba35f26..64e5904661 100644 --- a/shesha-core/src/Shesha.Application/Settings/Dto/SettingConfigurationDto.cs +++ b/shesha-core/src/Shesha.Application/Settings/Dto/SettingConfigurationDto.cs @@ -26,7 +26,7 @@ public SettingConfigurationDto(SettingDefinition definition) Category = new SettingApiItemBase { - Name = definition.Category, + Name = definition.Category ?? string.Empty, Accessor = definition.CategoryAccessor, }; @@ -42,7 +42,7 @@ public SettingConfigurationDto(SettingDefinition definition) public class SettingApiItemBase { public string Name { get; set; } - public string Description { get; set; } - public string Accessor { get; set; } + public string? Description { get; set; } + public string? Accessor { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Settings/Dto/UpdateSettingValueInput.cs b/shesha-core/src/Shesha.Application/Settings/Dto/UpdateSettingValueInput.cs index 8af0d902d7..30204c5363 100644 --- a/shesha-core/src/Shesha.Application/Settings/Dto/UpdateSettingValueInput.cs +++ b/shesha-core/src/Shesha.Application/Settings/Dto/UpdateSettingValueInput.cs @@ -15,6 +15,6 @@ public class UpdateSettingValueInput : SettingIdentifierDto /// /// Front-end application key, see . Is used for client-specific applications only /// - public string AppKey { get; set; } + public string? AppKey { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Settings/SettingsAppService.cs b/shesha-core/src/Shesha.Application/Settings/SettingsAppService.cs index 20064458d7..599de8ebe7 100644 --- a/shesha-core/src/Shesha.Application/Settings/SettingsAppService.cs +++ b/shesha-core/src/Shesha.Application/Settings/SettingsAppService.cs @@ -41,19 +41,26 @@ public async Task> GetValuesAsync(GetSetti return values; } + private SettingManagementContext? GetContext(string? appKey) + { + return !string.IsNullOrWhiteSpace(appKey) + ? new SettingManagementContext + { + AppKey = appKey, + // TODO: recheck usage of UserId, looks like the condition for appKey is wrong + UserId = AbpSession.UserId + } + : null; + } + /// /// Get setting value /// [HttpGet] [AllowAnonymous] - public async Task GetValueAsync(GetSettingValueInput input) + public async Task GetValueAsync(GetSettingValueInput input) { - var value = await _settingProvider.GetOrNullAsync(input.Module, input.Name, !string.IsNullOrWhiteSpace(input.AppKey) ? - new SettingManagementContext - { - AppKey = input.AppKey, - UserId = AbpSession.UserId - } : null); + var value = await _settingProvider.GetOrNullAsync(input.Module, input.Name, GetContext(input.AppKey)); return value; } @@ -62,14 +69,9 @@ public async Task GetValueAsync(GetSettingValueInput input) /// Get user setting value /// [HttpPost] - public async Task GetUserValueAsync(GetDynamicSettingValueInput input) + public async Task GetUserValueAsync(GetDynamicSettingValueInput input) { - var value = await _settingProvider.UserSpecificGetOrNullAsync(input.Module, input.Name, input.Datatype, input.DefaultValue, !string.IsNullOrWhiteSpace(input.AppKey) ? - new SettingManagementContext - { - AppKey = input.AppKey, - UserId = AbpSession.UserId - } : null); + var value = await _settingProvider.UserSpecificGetOrNullAsync(input.Module, input.Name, input.Datatype, input.DefaultValue, GetContext(input.AppKey)); return value; } @@ -82,12 +84,7 @@ public async Task GetUserValueAsync(GetDynamicSettingValueInput input) [HttpPost] public async Task UpdateUserValueAsync(UpdateDynamicSettingValueInput input) { - await _settingProvider.UpdateUserSettingAsync(input.Module, input.Name, input.Datatype, input.Value, !string.IsNullOrWhiteSpace(input.AppKey) ? - new SettingManagementContext - { - AppKey = input.AppKey, - UserId = AbpSession.UserId - } : null); + await _settingProvider.UpdateUserSettingAsync(input.Module, input.Name, input.Datatype, input.Value, GetContext(input.AppKey)); } @@ -99,12 +96,7 @@ await _settingProvider.UpdateUserSettingAsync(input.Module, input.Name, input.Da [HttpPost] public async Task UpdateValueAsync(UpdateSettingValueInput input) { - await _settingProvider.SetAsync(input.Module, input.Name, input.Value, !string.IsNullOrWhiteSpace(input.AppKey) ? - new SettingManagementContext - { - AppKey = input.AppKey, - UserId = AbpSession.UserId - } : null); + await _settingProvider.SetAsync(input.Module, input.Name, input.Value, GetContext(input.AppKey)); } [HttpGet] diff --git a/shesha-core/src/Shesha.Application/ShaRoleAppointedPersons/Dto/ShaRoleAppointedPersonMapProfile.cs b/shesha-core/src/Shesha.Application/ShaRoleAppointedPersons/Dto/ShaRoleAppointedPersonMapProfile.cs index 28725d5dfe..f89ae2298e 100644 --- a/shesha-core/src/Shesha.Application/ShaRoleAppointedPersons/Dto/ShaRoleAppointedPersonMapProfile.cs +++ b/shesha-core/src/Shesha.Application/ShaRoleAppointedPersons/Dto/ShaRoleAppointedPersonMapProfile.cs @@ -1,50 +1,29 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Abp.Domain.Repositories; using AutoMapper; using Shesha.AutoMapper.Dto; using Shesha.Domain; -using Shesha.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; namespace Shesha.ShaRoleAppointedPersons.Dto { public class ShaRoleAppointedPersonMapProfile : Profile { - private readonly IRepository _appEntityRepository; - - public ShaRoleAppointedPersonMapProfile(IRepository appEntityRepository) - { - _appEntityRepository = appEntityRepository; - } - public ShaRoleAppointedPersonMapProfile() { CreateMap(); CreateMap() .ForMember(u => u.Person, options => options.MapFrom(e => e.Person != null - ? new EntityReferenceDto(e.Person.Id, e.Person.FullName, e.Person.GetClassName()) : null)) + ? new EntityReferenceDto(e.Person) + : null + )) .ForMember(u => u.RoleId, options => options.MapFrom(e => e.Role != null ? e.Role.Id : (Guid?)null)) .ForMember(u => u.Regions, options => options.MapFrom(e => GetRegions(e))); } - /* - public static List GetRegions(ShaRoleAppointedPerson entity) - { - var regionTypeShortAlias = typeof(Area).GetEntityConfiguration().TypeShortAlias; - - var appEntityRepository = StaticContext.IocManager.Resolve>(); - - var linkedRegions = appEntityRepository.GetAll().Where(e => e.Appointment == entity && e.EntityTypeAlias == regionTypeShortAlias) - .ToList(); - var ids = linkedRegions.Select(r => r.EntityId.ToGuid()).Where(id => id != Guid.Empty).ToList(); - - return ids; - } - */ public static List> GetRegions(ShaRoleAppointedPerson entity) { - return entity.GetRelatedAreas().Select(e => new EntityReferenceDto(e.Id, e.Name, e.GetClassName())).ToList(); + return entity.GetRelatedAreas().Select(e => new EntityReferenceDto(e)).ToList(); } } } diff --git a/shesha-core/src/Shesha.Application/ShaRoleAppointedPersons/ShaRoleAppointedPersonActionsAppService.cs b/shesha-core/src/Shesha.Application/ShaRoleAppointedPersons/ShaRoleAppointedPersonActionsAppService.cs index de19ca3e44..9220b61bad 100644 --- a/shesha-core/src/Shesha.Application/ShaRoleAppointedPersons/ShaRoleAppointedPersonActionsAppService.cs +++ b/shesha-core/src/Shesha.Application/ShaRoleAppointedPersons/ShaRoleAppointedPersonActionsAppService.cs @@ -6,6 +6,7 @@ using Shesha.Domain; using Shesha.Extensions; using Shesha.ShaRoleAppointedPersons.Dto; +using Shesha.Validations; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -45,7 +46,7 @@ public override async Task CreateAsync(CreateShaRoleA await CurrentUnitOfWork.SaveChangesAsync(); - if (appointment?.Person?.User != null) + if (appointment.Person?.User != null) await _shaPermissionChecker.ClearPermissionsCacheForUserAsync(appointment.Person.User.Id, appointment.Person.User.TenantId); var dto = MapToEntityDto(appointment); @@ -61,7 +62,7 @@ private async Task BindAndValidateAsync(IShaRoleAppointe entity.Role = input.RoleId != Guid.Empty ? await _roleRepository.GetAsync(input.RoleId) : null; - entity.Person = (input.Person?.Id ?? Guid.Empty) != Guid.Empty + entity.Person = input.Person != null && input.Person.Id != null && input.Person.Id != Guid.Empty ? await _personRepository.GetAsync(input.Person.Id.Value) : null; @@ -82,8 +83,7 @@ await Repository.GetAll().AnyAsync(a => a.Role == entity.Role && a.Person == ent // todo: validate regions only for roles with region parameter - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); await Repository.InsertOrUpdateAsync(entity); @@ -120,7 +120,7 @@ public override async Task UpdateAsync(ShaRoleAppoint await CurrentUnitOfWork.SaveChangesAsync(); - if (appointment?.Person?.User != null) + if (appointment.Person?.User != null) await _shaPermissionChecker.ClearPermissionsCacheForUserAsync(appointment.Person.User.Id, appointment.Person.User.TenantId); var dto = MapToEntityDto(appointment); diff --git a/shesha-core/src/Shesha.Application/ShaRoles/Dto/CreateShaRoleDto.cs b/shesha-core/src/Shesha.Application/ShaRoles/Dto/CreateShaRoleDto.cs index 51e0b7c4d3..098328dcd4 100644 --- a/shesha-core/src/Shesha.Application/ShaRoles/Dto/CreateShaRoleDto.cs +++ b/shesha-core/src/Shesha.Application/ShaRoles/Dto/CreateShaRoleDto.cs @@ -11,17 +11,17 @@ public class CreateShaRoleDto public string Name { get; set; } [StringLength(200)] - public string NameSpace { get; set; } + public string? NameSpace { get; set; } [StringLength(2000)] - public string Description { get; set; } + public string? Description { get; set; } public Guid? Module { get; set; } - public bool CanAssignToMultiple { get; set; } - public bool CanAssignToPerson { get; set; } - public bool CanAssignToRole { get; set; } - public bool CanAssignToOrganisationRoleLevel { get; set; } - public bool CanAssignToUnit { get; set; } + public bool? CanAssignToMultiple { get; set; } + public bool? CanAssignToPerson { get; set; } + public bool? CanAssignToRole { get; set; } + public bool? CanAssignToOrganisationRoleLevel { get; set; } + public bool? CanAssignToUnit { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/ShaRoles/Dto/ShaRoleDto.cs b/shesha-core/src/Shesha.Application/ShaRoles/Dto/ShaRoleDto.cs index 28fc41f106..b3a0f4c1b7 100644 --- a/shesha-core/src/Shesha.Application/ShaRoles/Dto/ShaRoleDto.cs +++ b/shesha-core/src/Shesha.Application/ShaRoles/Dto/ShaRoleDto.cs @@ -21,21 +21,21 @@ public ShaRoleDto() public string Name { get; set; } [StringLength(200)] - public string NameSpace { get; set; } + public string? NameSpace { get; set; } [StringLength(2000)] - public string Description { get; set; } + public string? Description { get; set; } public Guid? Module { get; set; } - public ConcurrentHashSet Permissions { get; set; } + public ConcurrentHashSet Permissions { get; set; } = new(); - public bool IsRegionSpecific { get; set; } + public bool? IsRegionSpecific { get; set; } - public bool CanAssignToMultiple { get; set; } - public bool CanAssignToPerson { get; set; } - public bool CanAssignToRole { get; set; } - public bool CanAssignToOrganisationRoleLevel { get; set; } - public bool CanAssignToUnit { get; set; } + public bool? CanAssignToMultiple { get; set; } + public bool? CanAssignToPerson { get; set; } + public bool? CanAssignToRole { get; set; } + public bool? CanAssignToOrganisationRoleLevel { get; set; } + public bool? CanAssignToUnit { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/ShaRoles/Dto/ShaRoleDtoExtensions.cs b/shesha-core/src/Shesha.Application/ShaRoles/Dto/ShaRoleDtoExtensions.cs index bdf4aba019..f2fb4a127b 100644 --- a/shesha-core/src/Shesha.Application/ShaRoles/Dto/ShaRoleDtoExtensions.cs +++ b/shesha-core/src/Shesha.Application/ShaRoles/Dto/ShaRoleDtoExtensions.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; +using Shesha.Domain; +using System.Collections.Generic; using System.Linq; -using NUglify.Helpers; -using Shesha.Domain; namespace Shesha.ShaRoles.Dto { @@ -9,18 +8,16 @@ public static class ShaRoleDtoExtensions { public static List MapPermissions(this ShaRole role, IEnumerable permissions) { - var roles = role.Permissions.ToList(); - permissions.Where(x => !roles.Any(p => p.Permission == x && p.IsGranted)).ForEach(x => - { - roles.Add(new ShaRolePermission() { Permission = x, IsGranted = true, ShaRole = role }); - }); + var rolesPermissions = role.Permissions.ToList(); + var toAdd = permissions.Where(x => !rolesPermissions.Any(p => p.Permission == x && p.IsGranted)); + foreach (var permission in toAdd) + rolesPermissions.Add(new ShaRolePermission() { Permission = permission, IsGranted = true, ShaRole = role }); - roles.Where(x => permissions.All(p => x.Permission != p)).ToList().ForEach(x => - { - roles.Remove(x); - }); + var toRemove = rolesPermissions.Where(x => permissions.All(p => x.Permission != p)).ToList(); + foreach (var permission in toRemove) + rolesPermissions.Remove(permission); - return roles; + return rolesPermissions; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/ShaRoles/ShaRoleAppService.cs b/shesha-core/src/Shesha.Application/ShaRoles/ShaRoleAppService.cs index eb9b721b49..dfe6501055 100644 --- a/shesha-core/src/Shesha.Application/ShaRoles/ShaRoleAppService.cs +++ b/shesha-core/src/Shesha.Application/ShaRoles/ShaRoleAppService.cs @@ -1,5 +1,4 @@ using Abp.Application.Services.Dto; -using Abp.Authorization; using Abp.Domain.Repositories; using Abp.Runtime.Session; using Microsoft.AspNetCore.Mvc; @@ -80,7 +79,7 @@ public override async Task DeleteAsync(EntityDto input) public async Task IsRoleGrantedAsync(IsRoleGrantedInput input) { var userId = AbpSession.GetUserId(); - var isGranted = await _roleAppointmentRepository.GetAll().AnyAsync(a => a.Person != null && a.Person.User != null && a.Person.User.Id == userId && a.Role.Name == input.RoleName); + var isGranted = await _roleAppointmentRepository.GetAll().AnyAsync(a => a.Person != null && a.Person.User != null && a.Person.User.Id == userId && a.Role != null && a.Role.Name == input.RoleName); return isGranted; } } diff --git a/shesha-core/src/Shesha.Application/Shesha.Application.csproj b/shesha-core/src/Shesha.Application/Shesha.Application.csproj index 61565c9126..a62b218112 100644 --- a/shesha-core/src/Shesha.Application/Shesha.Application.csproj +++ b/shesha-core/src/Shesha.Application/Shesha.Application.csproj @@ -1,149 +1,162 @@  - - net8.0 - Shesha.Application - Shesha.Application - false - false - false - Shesha - Shesha.Application - Shesha - True - README.md - - - .\Shesha.Application.xml - - - .\Shesha.Application.xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - + + net8.0 + enable + Shesha.Application + Shesha.Application + false + false + false + Shesha + Shesha.Application + Shesha + True + README.md + + + .\Shesha.Application.xml + + + .\Shesha.Application.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/SheshaAppServiceBase.cs b/shesha-core/src/Shesha.Application/SheshaAppServiceBase.cs index 5f57b84925..b5ea57e005 100644 --- a/shesha-core/src/Shesha.Application/SheshaAppServiceBase.cs +++ b/shesha-core/src/Shesha.Application/SheshaAppServiceBase.cs @@ -21,6 +21,7 @@ using Shesha.Elmah; using Shesha.Extensions; using Shesha.MultiTenancy; +using Shesha.Reflection; using Shesha.Services; using Shesha.Validations; using System; @@ -40,50 +41,51 @@ public abstract class SheshaAppServiceBase : ApplicationService public SheshaAppServiceBase() { AsyncQueryableExecuter = NullAsyncQueryableExecuter.Instance; + LocalizationSourceName = SheshaConsts.LocalizationSourceName; } - public IAsyncQueryableExecuter AsyncQueryableExecuter { get; set; } - public IObjectValidatorManager ValidatorManager { get; set; } - public ILoggingContextCollector LoggingScope { get; set; } + public IAsyncQueryableExecuter AsyncQueryableExecuter { get; set; } = default!; + public IObjectValidatorManager ValidatorManager { get; set; } = default!; + public ILoggingContextCollector LoggingScope { get; set; } = NullLoggingContextCollector.Instance; /// /// Reference to the IoC manager. /// - public IIocManager IocManager { get; set; } + public IIocManager IocManager { get; set; } = default!; /// /// Dynamic repository /// - public IDynamicRepository DynamicRepo { protected get; set; } + public IDynamicRepository DynamicRepo { protected get; set; } = default!; /// /// Tenant manager /// - public TenantManager TenantManager { get; set; } + public TenantManager TenantManager { get; set; } = default!; /// /// User Manager /// - public UserManager UserManager { get; set; } + public UserManager UserManager { get; set; } = default!; /// /// Dynamic DTO builder /// - public IDynamicDtoTypeBuilder DtoBuilder { get; set; } + public IDynamicDtoTypeBuilder DtoBuilder { get; set; } = default!; /// /// Dynamic property manager /// - public IDynamicPropertyManager DynamicPropertyManager { get; set; } + public IDynamicPropertyManager DynamicPropertyManager { get; set; } = default!; /// /// Dynamic DTO mapping helper /// - public IDynamicDtoMappingHelper DynamicDtoMappingHelper { get; set; } + public IDynamicDtoMappingHelper DynamicDtoMappingHelper { get; set; } = default!; - public IEntityModelBinder EntityModelBinder { get; set; } + public IEntityModelBinder EntityModelBinder { get; set; } = default!; - private IUrlHelper _url; + private IUrlHelper? _url; /// /// Url helper @@ -96,6 +98,9 @@ public IUrlHelper Url { var actionContextAccessor = IocManager.Resolve(); var urlHelperFactory = IocManager.Resolve(); + if (actionContextAccessor.ActionContext == null) + throw new Exception("ActionContext is not available"); + _url = urlHelperFactory.GetUrlHelper(actionContextAccessor.ActionContext); } @@ -110,7 +115,7 @@ public IUrlHelper Url protected virtual async Task GetCurrentPersonAsync() { var personRepository = IocManager.Resolve>(); - var person = await personRepository.GetAll().FirstOrDefaultAsync(p => p.User.Id == AbpSession.GetUserId()); + var person = await personRepository.GetAll().FirstOrDefaultAsync(p => p.User != null && p.User.Id == AbpSession.GetUserId()); return person; } @@ -168,7 +173,8 @@ protected async Task SaveOrUpdateEntityAsync(Guid? id, Func actio protected async Task SaveOrUpdateEntityAsync(Guid? id, Action action) where T : class, IEntity { - return await SaveOrUpdateEntityAsync(id, a => { + return await SaveOrUpdateEntityAsync(id, a => + { action.Invoke(a); return Task.CompletedTask; }); @@ -185,8 +191,8 @@ protected async Task SaveOrUpdateEntityAsync(Guid? id, Action action) protected async Task SaveOrUpdateEntityAsync(TId? id, Func action) where T : class, IEntity where TId : struct { var item = id.HasValue - ? await GetEntityAsync(id.Value) - : (T)Activator.CreateInstance(typeof(T)); + ? (await GetEntityAsync(id.Value)).NotNull() + : ActivatorHelper.CreateNotNullInstance(); await action.Invoke(item); @@ -200,11 +206,10 @@ protected async Task SaveOrUpdateEntityAsync(TId? id, Func a /// /// Type of entity /// Id of the entity - /// Throw exception if entity not found /// - protected async Task GetEntityAsync(Guid id, bool throwException = true) where T : class, IEntity + protected async Task GetEntityOrNullAsync(Guid id) where T : class, IEntity { - return await GetEntityAsync(id, throwException); + return await GetEntityOrNullAsync(id); } /// @@ -213,19 +218,69 @@ protected async Task GetEntityAsync(Guid id, bool throwException = true) w /// Type of entity /// Id type /// Id of the entity - /// Throw exception if entity not found /// - protected async Task GetEntityAsync(TId id, bool throwException = true) where T : class, IEntity + protected async Task GetEntityOrNullAsync(TId id) where T : class, IEntity where TId : notnull { - var item = await DynamicRepo.GetAsync(typeof(T), id.ToString()); + var stringId = id.ToString(); + ArgumentException.ThrowIfNullOrWhiteSpace(stringId, nameof(id)); + + var item = await DynamicRepo.GetAsync(typeof(T), stringId); - if (item != null) - return (T)item; + return item != null ? (T)item : null; + } - if (throwException) - throw new UserFriendlyException($"{typeof(T).Name} with the specified id `{id}` not found"); + /// + /// Returns entity of the specified type with the specified + /// + /// Type of entity + /// Id of the entity + /// + protected async Task GetEntityAsync(Guid id) where T : class, IEntity + { + return await GetEntityAsync(id); + } - return null; + /// + /// Returns entity of the specified type with the specified + /// + /// Type of entity + /// Id type + /// Id of the entity + /// + protected async Task GetEntityAsync(TId id) where T : class, IEntity where TId : notnull + { + return await GetEntityOrNullAsync(id) ?? throw new UserFriendlyException($"{typeof(T).Name} with the specified id `{id}` not found"); + } + + /// + /// Returns entity of the specified type with the specified + /// + /// Type of entity + /// Id of the entity + /// Throw exception if entity not found + /// + [Obsolete("Use GetEntityOrNullAsync or GetEntityAsync without `throwException` argument")] + protected async Task GetEntityAsync(Guid id, bool throwException = true) where T : class, IEntity + { + return throwException + ? await GetEntityAsync(id) + : await GetEntityOrNullAsync(id); + } + + /// + /// Returns entity of the specified type with the specified + /// + /// Type of entity + /// Id type + /// Id of the entity + /// Throw exception if entity not found + /// + [Obsolete("Use GetEntityOrNullAsync or GetEntityAsync without `throwException` argument")] + protected async Task GetEntityAsync(TId id, bool throwException = true) where T : class, IEntity where TId : notnull + { + return throwException + ? await GetEntityAsync(id) + : await GetEntityOrNullAsync(id); } /// @@ -234,7 +289,7 @@ protected async Task GetEntityAsync(TId id, bool throwException = tru /// /// /// - protected async Task ValidateEntityAsync(TEntity entity, List validationResults) + protected async Task ValidateEntityAsync(TEntity entity, List validationResults) where TEntity : notnull { await FluentValidationsOnEntityAsync(entity, validationResults); var result = !validationResults.Any(); @@ -243,57 +298,53 @@ protected async Task ValidateEntityAsync(TEntity entity, List - /// Runs validation defined on entity through fluentValidation - /// - /// - /// - /// - protected async Task FluentValidationsOnEntityAsync(TEntity entity, List validationResults) - { - if (StaticContext.IocManager.IsRegistered(typeof(IValidator))) - { - var validators = StaticContext.IocManager.ResolveAll>(); - - foreach (var validator in validators) - { - FluentValidationResult fluentValidationResults = await validator.ValidateAsync(entity); - - //Map FluentValidationResult to normal System.ComponentModel.DataAnnotations.ValidationResult, - //so to throw one same Validations Exceptions on AbpValidationException - if (!fluentValidationResults.IsValid) - fluentValidationResults.Errors.ForEach(err => - { - var memberNames = new List() { err.PropertyName }; - var valResult = new ValidationResult(err.ErrorMessage, memberNames); - validationResults.Add(valResult); - }); - } - } - } - - #region Dynamic DTOs - - /// - /// Map entity to a - /// - /// Type of entity - /// Type of entity primary key - /// entity to map - /// mapping settings - /// - protected async Task> MapToDynamicDtoAsync(TEntity entity, IDynamicMappingSettings settings = null) where TEntity : class, IEntity + /// + /// Runs validation defined on entity through fluentValidation + /// + /// + /// + /// + protected async Task FluentValidationsOnEntityAsync(TEntity entity, List validationResults) + { + if (StaticContext.IocManager.IsRegistered(typeof(IValidator))) + { + var validators = StaticContext.IocManager.ResolveAll>(); + + foreach (var validator in validators) + { + FluentValidationResult fluentValidationResults = await validator.ValidateAsync(entity); + + //Map FluentValidationResult to normal System.ComponentModel.DataAnnotations.ValidationResult, + //so to throw one same Validations Exceptions on AbpValidationException + if (!fluentValidationResults.IsValid) + fluentValidationResults.Errors.ForEach(err => + { + var memberNames = new List() { err.PropertyName }; + var valResult = new ValidationResult(err.ErrorMessage, memberNames); + validationResults.Add(valResult); + }); + } + } + } + + #region Dynamic DTOs + + /// + /// Map entity to a + /// + /// Type of entity + /// Type of entity primary key + /// entity to map + /// mapping settings + /// + protected async Task> MapToDynamicDtoAsync(TEntity entity, IDynamicMappingSettings? settings = null) where TEntity : class, IEntity { return await MapToCustomDynamicDtoAsync, TEntity, TPrimaryKey>(entity, settings); } protected async Task>> MapToDynamicDtoListAsync(IEnumerable entities) where TEntity : class, IEntity { - var dtoList = await Task.WhenAll( - entities.Select(async entity => - { - return await MapToDynamicDtoAsync(entity); - })); + var dtoList = await entities.SelectAsync(async entity => await MapToDynamicDtoAsync(entity)); return dtoList.ToList(); } @@ -307,7 +358,7 @@ protected async Task>> MapToDynamicDtoList /// entity to map /// mapping settings /// - protected async Task MapToCustomDynamicDtoAsync(TEntity entity, IDynamicMappingSettings settings = null) + protected async Task MapToCustomDynamicDtoAsync(TEntity entity, IDynamicMappingSettings? settings = null) where TEntity : class, IEntity where TDynamicDto : class, IDynamicDto { @@ -348,29 +399,11 @@ protected async Task MapStaticPropertiesToEntityDtoAsync(TDynamicDto dto, TEntity entity) where TEntity : class, IEntity where TDynamicDto : class, IDynamicDto + where TPrimaryKey : notnull { await DynamicPropertyManager.MapDtoToEntityAsync(dto, entity); } - /*/// - /// Map all properties (dynamic and static) of dynamic DTO to a specified entity - /// The Dto object must be inherited from DynamicDto(CreateDynamicDto, UpdateDynamicDto) - /// or proxied by the DynamicBinder parameter attribute - /// - /// Type of Dynamic DTO - /// Type of entity - /// Type of primary key - /// Source DTO - /// Destination entity - /// - protected async Task MapDynamicDtoToEntityAsync(TDynamicDto dto, TEntity entity) - where TEntity : class, IEntity - where TDynamicDto : class, IDynamicDto - { - await MapStaticPropertiesToEntityDtoAsync(dto, entity); - await MapDynamicPropertiesToEntityAsync(dto, entity); - }*/ - /// /// Map all properties (dynamic and static) of dynamic DTO to a specified entity /// The Dto object must be inherited from DynamicDto(CreateDynamicDto, UpdateDynamicDto) @@ -384,14 +417,16 @@ protected async Task MapDynamicDtoToEntityAsyncAction to save entity before map dynamic data (dynamic properies) /// protected async Task MapDynamicDtoToEntityAsync( - TDynamicDto dto, - TEntity entity, - Func, Task> validateAndSaveEntityAction = null + TDynamicDto dto, + TEntity entity, + Func, Task>? validateAndSaveEntityAction = null ) - where TEntity : class, IEntity where TDynamicDto : class, IDynamicDto + where TEntity : class, IEntity + where TDynamicDto : class, IDynamicDto + where TPrimaryKey : notnull { - var jObject = (dto as IHasJObjectField)._jObject; + var jObject = dto._jObject; var result = new DynamicDtoMapingResult(); bool mapResult = true; @@ -417,9 +452,15 @@ protected async Task MapDynamicDtoToEntityAsync(dto, entity); - await MapJObjectToDynamicPropertiesEntityAsync(jObject, entity, result.ValidationResults); - + if (jObject != null) + { + await MapJObjectToDynamicPropertiesEntityAsync(jObject, entity, result.ValidationResults); + } + else + { + await MapDynamicPropertiesToEntityAsync(dto, entity); + } + return result; } @@ -437,6 +478,7 @@ protected async Task MapJObjectToEntityAsync( TEntity entity, List validationResult) where TEntity : class, IEntity + where TPrimaryKey : notnull { var result = await MapJObjectToStaticPropertiesEntityAsync(jObject, entity, validationResult); result = result && await ValidateEntityAsync(entity, validationResult); @@ -478,6 +520,7 @@ protected async Task MapJObjectToDynamicPropertiesEntityAsync validationResult) where TEntity : class, IEntity + where TPrimaryKey : notnull { await DynamicPropertyManager.MapJObjectToEntityAsync(jObject, entity); @@ -498,9 +541,9 @@ TEntity entity ? jObject.Property(nameof(IHasDelayedUpdateField._delayedUpdate))?.Value?.ToObject>() : (dto as IHasDelayedUpdateField)?._delayedUpdate; - if (delayedUpdates?.Any() ?? false) + if (delayedUpdates != null) await DelayedUpdateAsync(delayedUpdates, entity, result.ValidationResults); - + return result; } @@ -511,7 +554,8 @@ protected async Task DelayedUpdateAsync( where TEntity : class, IEntity { var delayedUpdate = jObject.Property(nameof(IHasDelayedUpdateField._delayedUpdate))?.Value?.ToObject>(); - await DelayedUpdateAsync(delayedUpdate, entity, validationResult); + if (delayedUpdate != null) + await DelayedUpdateAsync(delayedUpdate, entity, validationResult); } protected async Task DelayedUpdateAsync( @@ -520,7 +564,7 @@ protected async Task DelayedUpdateAsync( List validationResult) where TEntity : class, IEntity { - if (delayedUpdateGroups?.Any() ?? false) + if (delayedUpdateGroups.Any()) { var managers = StaticContext.IocManager.ResolveAll(); foreach (var group in delayedUpdateGroups) @@ -536,7 +580,7 @@ protected async Task DelayedUpdateAsync( } } - protected async Task DeleteCascadeAsync(TEntity entity) + protected async Task DeleteCascadeAsync(TEntity entity) where TEntity : notnull { await EntityModelBinder.DeleteCascadeAsync(entity); return true; diff --git a/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs b/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs index 2cb7fd6123..489ab34cdc 100644 --- a/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs +++ b/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs @@ -10,16 +10,18 @@ using Abp.Reflection.Extensions; using Castle.MicroKernel.Registration; using Shesha.Authorization; -using Shesha.ConfigurationItems.Distribution; +using Shesha.ConfigurationItems; using Shesha.Domain; using Shesha.Domain.Enums; using Shesha.DynamicEntities; using Shesha.Email; using Shesha.GraphQL; using Shesha.Modules; +using Shesha.Notifications; using Shesha.Notifications.Configuration; using Shesha.Notifications.Distribution.NotificationChannels; using Shesha.Notifications.Distribution.NotificationTypes; +using Shesha.Notifications.SMS; using Shesha.Otp.Configuration; using Shesha.Reflection; using Shesha.Settings.Ioc; @@ -58,10 +60,6 @@ public override void PreInitialize() // replace email sender Configuration.ReplaceService(DependencyLifeStyle.Transient); - // ToDo: migrate Notification to ABP 6.6.2 - //Configuration.Notifications.Distributers.Clear(); - //Configuration.Notifications.Distributers.Add(); - IocManager.IocContainer.Register( Component.For().Forward().Forward().ImplementedBy().LifestyleTransient(), Component.For(typeof(IEntityReorderer<,,>)).ImplementedBy(typeof(EntityReorderer<,,>)).LifestyleTransient() @@ -93,7 +91,11 @@ public override void PreInitialize() Component.For().UsingFactoryMethod(f => { var settings = f.Resolve(); - var gatewayUid = settings.SmsSettings.GetValue().SmsGateway; + var smsSettings = settings.SmsSettings.GetValueOrNull(); + if (smsSettings == null) + return new NullSmsGateway(); + + var gatewayUid = smsSettings.SmsGateway; var gatewayType = !string.IsNullOrWhiteSpace(gatewayUid) ? f.Resolve().Find(t => typeof(ISmsGateway).IsAssignableFrom(t) && t.GetClassUid() == gatewayUid).FirstOrDefault() @@ -146,44 +148,21 @@ public override void Initialize() IocManager.Register(DependencyLifeStyle.Transient); IocManager.Register(DependencyLifeStyle.Transient); + IocManager + .RegisterConfigurableItemManager() + .RegisterConfigurableItemExport() + .RegisterConfigurableItemImport() + + .RegisterConfigurableItemExport() + .RegisterConfigurableItemImport(); + + + IocManager.RegisterIfNot(DependencyLifeStyle.Transient); + IocManager.RegisterIfNot(DependencyLifeStyle.Transient); + var thisAssembly = Assembly.GetExecutingAssembly(); IocManager.RegisterAssemblyByConvention(thisAssembly); - IocManager.IocContainer.Register(Component - .For() - .Named("NotificationChannelExport") - .Forward>() - .Forward() - .ImplementedBy() - .LifestyleTransient()) - .Register(Component - .For() - .Named("NotificationChannelImport") - .Forward>() - .Forward() - .ImplementedBy() - .LifestyleTransient()) - .Register(Component - .For() - .Named("NotificationTypeExport") - .Forward>() - .Forward() - .ImplementedBy() - .LifestyleTransient()) - .Register(Component - .For() - .Named("NotificationTypeImport") - .Forward>() - .Forward() - .ImplementedBy() - .LifestyleTransient()); - - /* api not used now, this registration causes problems in the IoC. Need to solve IoC problem before uncommenting - var schemaContainer = IocManager.Resolve(); - var serviceProvider = IocManager.Resolve(); - schemaContainer.RegisterCustomSchema("api", new ApiSchema(serviceProvider)); - */ - Configuration.Modules.AbpAutoMapper().Configurators.Add( // Scan the assembly for classes which inherit from AutoMapper.Profile cfg => cfg.AddMaps(thisAssembly) diff --git a/shesha-core/src/Shesha.Application/SheshaCrudServiceBase.cs b/shesha-core/src/Shesha.Application/SheshaCrudServiceBase.cs index 841dd66be4..2c5aa38777 100644 --- a/shesha-core/src/Shesha.Application/SheshaCrudServiceBase.cs +++ b/shesha-core/src/Shesha.Application/SheshaCrudServiceBase.cs @@ -19,6 +19,7 @@ using Shesha.GraphQL.Provider; using Shesha.Metadata; using Shesha.QuickSearch; +using Shesha.Reflection; using Shesha.Specifications; using Shesha.Utilities; using Shesha.Web; @@ -36,6 +37,7 @@ public abstract class SheshaCrudServiceBase : TEntityDto, TPrimaryKey, FilteredPagedAndSortedResultRequestDto, TEntityDto, TEntityDto> where TEntity : class, IEntity where TEntityDto : IEntityDto + where TPrimaryKey : notnull { protected SheshaCrudServiceBase(IRepository repository) : base(repository) { @@ -51,6 +53,7 @@ public abstract class SheshaCrudServiceBase where TUpdateInput : IEntityDto where TGetAllInput : FilteredPagedAndSortedResultRequestDto + where TPrimaryKey : notnull { /// /// Constructor @@ -71,9 +74,10 @@ public abstract class SheshaCrudServiceBase where TGetAllInput : FilteredPagedAndSortedResultRequestDto where TGetInput : IEntityDto + where TPrimaryKey : notnull { public IQuickSearcher QuickSearcher { get; set; } = new NullQuickSearcher(); - public ISpecificationManager SpecificationManager { get; set; } = new NullSpecificationManager(); + public ISpecificationManager SpecificationManager { get; set; } = NullSpecificationManager.Instance; [EntityAction(StandardEntityActions.Create)] public override Task CreateAsync(TCreateInput input) @@ -247,7 +251,7 @@ public virtual async Task>> QueryAllAs s.Schema = schema; s.Query = query; - s.Variables = new Inputs(new Dictionary { + s.Variables = new Inputs(new Dictionary { { "filter", input.Filter }, { "specifications", input.Specifications }, { "quickSearch", input.QuickSearch }, @@ -295,7 +299,7 @@ private async Task CleanupPropertiesAsync(string properties) var sb = new StringBuilder(); - await AppendPropertiesAsync(sb, typeof(TEntity).FullName, propList.Where(x => !x.IsNullOrWhiteSpace()).ToList()); + await AppendPropertiesAsync(sb, typeof(TEntity).GetRequiredFullName(), propList.Where(x => !x.IsNullOrWhiteSpace()).ToList()); return "id " + sb.ToString(); } @@ -306,7 +310,7 @@ private async Task AppendPropertiesAsync(StringBuilder sb, string entityType, Li var entityConfig = EntityConfigurationStore.Get(entityType); var propConfigs = entityConfig != null - ? await EntityConfigCache.GetEntityPropertiesAsync(entityConfig.EntityType.FullName) + ? await EntityConfigCache.GetEntityPropertiesAsync(entityConfig.EntityType.GetRequiredFullName()) : null; while (i < propList.Count) @@ -409,8 +413,8 @@ private void AppendProperty(StringBuilder sb, EntityPropertyDto property, bool f private async Task GetGqlTopLevelPropertiesAsync(bool fullReference = false) { var sb = new StringBuilder(); - var properties = (await EntityConfigCache.GetEntityPropertiesAsync(typeof(TEntity))) - .Where(x => !x.Suppress); + var allEntityProps = await EntityConfigCache.GetEntityPropertiesAsync(typeof(TEntity)); + var properties = allEntityProps.NotNull().Where(x => !x.Suppress); foreach (var property in properties) { AppendProperty(sb, property, fullReference); diff --git a/shesha-core/src/Shesha.Application/Sms/ConfigurableSmsGateway.cs b/shesha-core/src/Shesha.Application/Sms/ConfigurableSmsGateway.cs index dd65f792eb..b181f8a9c1 100644 --- a/shesha-core/src/Shesha.Application/Sms/ConfigurableSmsGateway.cs +++ b/shesha-core/src/Shesha.Application/Sms/ConfigurableSmsGateway.cs @@ -1,5 +1,6 @@ using Abp.Configuration; using Shesha.Notifications.Dto; +using Shesha.Reflection; using System; using System.Threading.Tasks; @@ -46,7 +47,7 @@ public async Task GetSettingsAsync() /// public async Task SetSettingsAsync(object settings) { - await SetTypedSettingsAsync(settings as TSettings); + await SetTypedSettingsAsync(settings.ForceCastAs()); } /// diff --git a/shesha-core/src/Shesha.Application/Sms/Dtos/SmsGatewayDto.cs b/shesha-core/src/Shesha.Application/Sms/Dtos/SmsGatewayDto.cs index 685458dbfa..1b27484dbc 100644 --- a/shesha-core/src/Shesha.Application/Sms/Dtos/SmsGatewayDto.cs +++ b/shesha-core/src/Shesha.Application/Sms/Dtos/SmsGatewayDto.cs @@ -13,7 +13,7 @@ public class SmsGatewayDto /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Unique identifier of the Sms Gateway diff --git a/shesha-core/src/Shesha.Application/Sms/ISmsGateway.cs b/shesha-core/src/Shesha.Application/Sms/ISmsGateway.cs index 12c0e34e78..52ac823830 100644 --- a/shesha-core/src/Shesha.Application/Sms/ISmsGateway.cs +++ b/shesha-core/src/Shesha.Application/Sms/ISmsGateway.cs @@ -17,6 +17,6 @@ public interface ISmsGateway Task GetSettingsAsync(); Task SetSettingsAsync(object settings); - Type SettingsType { get; } + Type? SettingsType { get; } } } diff --git a/shesha-core/src/Shesha.Application/Sms/NullSmsGateway.cs b/shesha-core/src/Shesha.Application/Sms/NullSmsGateway.cs index 8cc3910d0f..ce5994bee8 100644 --- a/shesha-core/src/Shesha.Application/Sms/NullSmsGateway.cs +++ b/shesha-core/src/Shesha.Application/Sms/NullSmsGateway.cs @@ -20,7 +20,7 @@ public class NullSmsGateway : ISmsGateway public ILogger Logger { get; set; } - public Type SettingsType => null; + public Type? SettingsType => null; public NullSmsGateway() { diff --git a/shesha-core/src/Shesha.Application/Startup/IShaApplicationModuleConfiguration.cs b/shesha-core/src/Shesha.Application/Startup/IShaApplicationModuleConfiguration.cs index 9f6b2be0af..3a39cb5da8 100644 --- a/shesha-core/src/Shesha.Application/Startup/IShaApplicationModuleConfiguration.cs +++ b/shesha-core/src/Shesha.Application/Startup/IShaApplicationModuleConfiguration.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Startup { diff --git a/shesha-core/src/Shesha.Application/Startup/ShaApplicationModuleConfiguration.cs b/shesha-core/src/Shesha.Application/Startup/ShaApplicationModuleConfiguration.cs index 05bc83dc17..dc8d2b98e9 100644 --- a/shesha-core/src/Shesha.Application/Startup/ShaApplicationModuleConfiguration.cs +++ b/shesha-core/src/Shesha.Application/Startup/ShaApplicationModuleConfiguration.cs @@ -1,9 +1,6 @@ using Abp.Dependency; -using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Shesha.DynamicEntities; using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; namespace Shesha.Startup @@ -14,7 +11,7 @@ namespace Shesha.Startup public class ShaApplicationModuleConfiguration : IShaApplicationModuleConfiguration { public List DynamicApplicationServiceRegistrations { get; private set; } - public IocManager IocManager { get; set; } + public IIocManager IocManager { get; set; } = default!; public ShaApplicationModuleConfiguration() { @@ -24,15 +21,6 @@ public ShaApplicationModuleConfiguration() [Obsolete("Do not use, all AppServices for entities should be configured through Configuration Manager")] public void CreateAppServicesForEntities(Assembly assembly, string moduleName) { - /*var partManager = IocManager.Resolve(); - - var feature = GetFeature(partManager); - - if (feature == null) - partManager.FeatureProviders.Add(feature = new DynamicEntityControllerFeatureProvider(IocManager)); - - - RegisterDynamicApplicationServices(assembly, moduleName);*/ } [Obsolete("Do not use, all AppServices for entities should be configured through Configuration Manager")] @@ -44,10 +32,5 @@ public void RegisterDynamicApplicationServices(Assembly assembly, string moduleN ModuleName = moduleName, }); } - - private DynamicEntityControllerFeatureProvider GetFeature(ApplicationPartManager partManager) - { - return partManager.FeatureProviders.OfType().FirstOrDefault(); - } } } diff --git a/shesha-core/src/Shesha.Application/StoredFiles/Dto/CreateStoredFileInput.cs b/shesha-core/src/Shesha.Application/StoredFiles/Dto/CreateStoredFileInput.cs index dd8290b3e7..6a66317aeb 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/Dto/CreateStoredFileInput.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/Dto/CreateStoredFileInput.cs @@ -1,7 +1,8 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Shesha.StoredFiles.Dto { @@ -21,31 +22,38 @@ public class CreateStoredFileInput /// Id of the owner entity /// [BindProperty(Name = "ownerId")] - public string OwnerId { get; set; } + public string? OwnerId { get; set; } /// /// Type short alias of the owner entity /// [BindProperty(Name = "ownerType")] - public string OwnerType { get; set; } + public string? OwnerType { get; set; } /// /// Category of the file. Is used to split attachments into groups /// [BindProperty(Name = "filesCategory")] - public string FilesCategory { get; set; } + public string? FilesCategory { get; set; } /// /// Property name of the owner entity. Is used for direct links only (when owner references file using foreign key) /// [BindProperty(Name = "propertyName")] - public string PropertyName { get; set; } + public string? PropertyName { get; set; } /// /// File content /// [Required] [BindProperty(Name = "file")] - public IFormFile File { get; set; } + public IFormFile? File { get; set; } + + [MemberNotNull(nameof(File))] + public void EnsureFile() + { + if (File == null) + throw new Exception($"{nameof(File)} must not be null"); + } } } diff --git a/shesha-core/src/Shesha.Application/StoredFiles/Dto/DeleteStoredFileInput.cs b/shesha-core/src/Shesha.Application/StoredFiles/Dto/DeleteStoredFileInput.cs index b1cd6cfe39..d68d8e2603 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/Dto/DeleteStoredFileInput.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/Dto/DeleteStoredFileInput.cs @@ -12,7 +12,6 @@ public class DeleteStoredFileInput /// /// File Id /// - [Required] [BindProperty(Name = "fileId")] public Guid? FileId { get; set; } @@ -20,24 +19,24 @@ public class DeleteStoredFileInput /// Id of the owner entity /// [BindProperty(Name = "ownerId")] - public string OwnerId { get; set; } + public string? OwnerId { get; set; } /// /// Type short alias of the owner entity /// [BindProperty(Name = "ownerType")] - public string OwnerType { get; set; } + public string? OwnerType { get; set; } /// /// Property name of the owner entity. Is used for direct links only (when owner references file using foreign key) /// [BindProperty(Name = "propertyName")] - public string PropertyName { get; set; } + public string? PropertyName { get; set; } /// /// File category for owner entity if property is not set /// [BindProperty(Name = "fileCategory")] - public string FileCategory { get; set; } + public string? FileCategory { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/StoredFiles/Dto/FilesListInput.cs b/shesha-core/src/Shesha.Application/StoredFiles/Dto/FilesListInput.cs index 0478a1201b..0fa8ee9bee 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/Dto/FilesListInput.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/Dto/FilesListInput.cs @@ -5,6 +5,6 @@ namespace Shesha.StoredFiles.Dto { public class FilesListInput : StoredFilesInputBase { - public List FilesId { get; set; } + public List? FilesId { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/StoredFiles/Dto/StaticFileInput.cs b/shesha-core/src/Shesha.Application/StoredFiles/Dto/StaticFileInput.cs index 0e00d7775a..5d9735be36 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/Dto/StaticFileInput.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/Dto/StaticFileInput.cs @@ -1,11 +1,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; namespace Shesha.StoredFiles.Dto { @@ -24,19 +21,26 @@ public class StaticFileInput /// Category of the file. Is used to split attachments into groups /// [BindProperty(Name = "filesCategory")] - public string FilesCategory { get; set; } + public string? FilesCategory { get; set; } /// /// Property name of the owner entity. Is used for direct links only (when owner references file using foreign key) /// [BindProperty(Name = "propertyName")] - public string PropertyName { get; set; } + public string? PropertyName { get; set; } /// /// File content /// [Required] [BindProperty(Name = "file")] - public IFormFile File { get; set; } + public IFormFile? File { get; set; } + + [MemberNotNull(nameof(File))] + public void EnsureFile() + { + if (File == null) + throw new Exception($"{nameof(File)} must not be null"); + } } } diff --git a/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileAsPropertyDto.cs b/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileAsPropertyDto.cs index 96b68ea7c0..3b8abed09a 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileAsPropertyDto.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileAsPropertyDto.cs @@ -12,12 +12,12 @@ public class StoredFileAsPropertyDto: StoredFileOwnerDto /// Property name of the owner entity. Is used for direct links only (when owner references file using foreign key) /// [BindProperty(Name = "propertyName")] - public string PropertyName { get; set; } + public string? PropertyName { get; set; } /// /// File category for owner entity if property is not set /// [BindProperty(Name = "fileCategory")] - public string FileCategory { get; set; } + public string? FileCategory { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileDto.cs b/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileDto.cs index cfdb1ba05f..9ca1d47125 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileDto.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileDto.cs @@ -4,13 +4,13 @@ namespace Shesha.StoredFiles.Dto { public class StoredFileDto { - public string Error { get; set; } + public string? Error { get; set; } public Guid? Id { get; set; } - public string Name { get; set; } - public string FileCategory { get; set; } - public string Url { get; set; } + public string? Name { get; set; } + public string? FileCategory { get; set; } + public string? Url { get; set; } public Int64 Size { get; set; } - public string Type { get; set; } + public string? Type { get; set; } public bool Temporary { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileMapProfile.cs b/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileMapProfile.cs index 86212bbd9c..8cc699b1ca 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileMapProfile.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFileMapProfile.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Abp.Domain.Entities.Auditing; using Abp.Domain.Repositories; using AutoMapper; @@ -7,6 +5,8 @@ using Shesha.Extensions; using Shesha.Services; using Shesha.StoredFiles.Dto; +using System; +using System.Linq; namespace Shesha.StoredFilters.Dtos { @@ -28,7 +28,7 @@ public StoredFileMapProfile() .ForMember(u => u.Url, options => options.MapFrom(e => e.GetFileVersionUrl())); } - private static string GetCreatorUserFullName(ICreationAudited entity) + private static string? GetCreatorUserFullName(ICreationAudited entity) { if (entity.CreatorUserId == null) return null; diff --git a/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFilesInputBase.cs b/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFilesInputBase.cs index 8a611ee36b..972a39e8c1 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFilesInputBase.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/Dto/StoredFilesInputBase.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; namespace Shesha.StoredFiles.Dto { @@ -8,33 +7,31 @@ public class StoredFilesInputBase /// /// Id of the owner entity /// - [Required] [BindProperty(Name = "ownerId")] - public string OwnerId { get; set; } + public string? OwnerId { get; set; } /// /// Type short alias of the owner entity /// - [Required] [BindProperty(Name = "ownerType")] - public string OwnerType { get; set; } + public string? OwnerType { get; set; } /// /// Category of the file. Is used to split attachments into groups /// [BindProperty(Name = "filesCategory")] - public string FilesCategory { get; set; } + public string? FilesCategory { get; set; } /// /// Property name of the owner entity. Is used for direct links only (when owner references file using foreign key) /// [BindProperty(Name = "propertyName")] - public string PropertyName { get; set; } + public string? PropertyName { get; set; } /// /// Property name of the owner entity. Is used for assign file to the nested entities /// [BindProperty(Name = "ownerName")] - public string OwnerName { get; set; } + public string? OwnerName { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/StoredFiles/Dto/UploadFileInput.cs b/shesha-core/src/Shesha.Application/StoredFiles/Dto/UploadFileInput.cs index 40399f94bc..a6c1c91661 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/Dto/UploadFileInput.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/Dto/UploadFileInput.cs @@ -1,6 +1,8 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Shesha.StoredFiles.Dto { @@ -14,6 +16,13 @@ public class UploadFileInput: StoredFilesInputBase /// [Required] [BindProperty(Name = "file")] - public IFormFile File { get; set; } + public IFormFile? File { get; set; } + + [MemberNotNull(nameof(File))] + public void EnsureFile() + { + if (File == null) + throw new Exception($"{nameof(File)} must not be null"); + } } } diff --git a/shesha-core/src/Shesha.Application/StoredFiles/Dto/UploadFileVersionInput.cs b/shesha-core/src/Shesha.Application/StoredFiles/Dto/UploadFileVersionInput.cs index 668bd2746a..4482137a6e 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/Dto/UploadFileVersionInput.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/Dto/UploadFileVersionInput.cs @@ -20,6 +20,6 @@ public class UploadFileVersionInput /// [Required] [BindProperty(Name = "file")] - public IFormFile File { get; set; } + public IFormFile? File { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/StoredFiles/StoredFileController.cs b/shesha-core/src/Shesha.Application/StoredFiles/StoredFileController.cs index 5f13129c57..557b9f0b63 100644 --- a/shesha-core/src/Shesha.Application/StoredFiles/StoredFileController.cs +++ b/shesha-core/src/Shesha.Application/StoredFiles/StoredFileController.cs @@ -7,7 +7,6 @@ using Abp.Runtime.Validation; using Abp.UI; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Shesha.Authorization; using Shesha.Domain; using Shesha.EntityReferences; @@ -17,12 +16,14 @@ using Shesha.StoredFiles.Dto; using Shesha.StoredFiles.Enums; using Shesha.Utilities; +using Shesha.Validations; using SkiaSharp; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using ReflectionHelper = Shesha.Reflection.ReflectionHelper; @@ -44,7 +45,7 @@ public class StoredFileController : ControllerBase, ITransientDependency /// /// Reference to the object to object mapper. /// - public IObjectMapper ObjectMapper { get; set; } + public IObjectMapper ObjectMapper { get; set; } = NullObjectMapper.Instance; public StoredFileController(IRepository fileRepository, IRepository fileVersionRepository, IStoredFileService fileService, @@ -71,7 +72,9 @@ public async Task DownloadAsync(Guid id, int? versionNo) if (fileVersion.Id.ToString().ToLower() == HttpContext.Request.Headers.IfNoneMatch.ToString().ToLower()) return StatusCode(304); +#pragma warning disable IDISP001 // Dispose created. Note: this stream will be disposed by FileStreamResult var fileContents = await _fileService.GetStreamAsync(fileVersion); +#pragma warning restore IDISP001 // Dispose created await _fileService.MarkDownloadedAsync(fileVersion); HttpContext.Response.Headers.CacheControl = "no-cache, max-age=600"; //ten minuts @@ -116,19 +119,26 @@ private async Task GetStoredFileVersionAsync(Guid id, int? ve [HttpPost, Route("Upload")] [Consumes("multipart/form-data")] public async Task UploadAsync([FromForm] UploadFileInput input) - { - if (input.File == null) - ModelState.AddModelError(nameof(input.File), $"{nameof(input.File)} must not be null"); - - if (string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId)) - ModelState.AddModelError(nameof(input.OwnerType), $"{nameof(input.OwnerType)} must not be null when {nameof(input.OwnerId)} is specified"); - - if (!ModelState.IsValid) - throw new AbpValidationException("An error occured");//, ModelState.Keys.Select(k => new ValidationResult(ModelState.Values[k], new List() { k }))); + { + #region validation + + var validationResults = new ValidationResults(); + if (input.File == null) + validationResults.Add($"{nameof(input.File)} must not be null", [nameof(input.File)]); + + if (string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId)) + validationResults.Add($"{nameof(input.OwnerType)} must not be null when {nameof(input.OwnerId)} is specified", [nameof(input.OwnerType)]); + + if (validationResults.Any()) + throw new AbpValidationException("An error occured", validationResults); + + #endregion + + input.EnsureFile(); - object owner = null; + object? owner = null; - if (!input.OwnerType.IsNullOrEmpty() && !input.OwnerId.IsNullOrEmpty()) + if (!string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId)) { owner = await _dynamicRepository.GetAsync(input.OwnerType, input.OwnerId); if (owner == null) @@ -138,16 +148,14 @@ public async Task UploadAsync([FromForm] UploadFileInput input) var uploadedFile = new StoredFileDto(); var fileName = input.File.FileName.CleanupFileName(); - if (owner != null && !input.PropertyName.IsNullOrWhiteSpace()) + if (owner != null && !string.IsNullOrWhiteSpace(input.PropertyName)) { - StoredFileVersion fileVersion = null; + StoredFileVersion? fileVersion = null; // single file upload (stored as a property of an entity) var property = ReflectionHelper.GetProperty(owner, input.PropertyName, out owner); - if (property == null || property.PropertyType != typeof(StoredFile)) - throw new AbpValidationException( - $"Property '{input.PropertyName}' not found in class '{owner.GetType().FullName}'"); - if (property != null && !typeof(StoredFile).IsAssignableFrom(property.PropertyType)) + + if (!typeof(StoredFile).IsAssignableFrom(property.PropertyType)) throw new AbpValidationException( $"Wrong type of '{owner.GetType().Name}.{input.PropertyName}' property (actual: '{property.PropertyType.FullName}', expected: '{nameof(StoredFile)}')"); @@ -193,7 +201,7 @@ public async Task UploadAsync([FromForm] UploadFileInput input) if (owner != null) { // Set owner - if (!input.OwnerName.IsNullOrEmpty()) + if (!string.IsNullOrWhiteSpace(input.OwnerName)) { var prop = ReflectionHelper.GetProperty(owner, input.OwnerName); if (prop == null) @@ -208,13 +216,13 @@ public async Task UploadAsync([FromForm] UploadFileInput input) } else { - if (!input.OwnerType.IsNullOrEmpty()) + if (!string.IsNullOrWhiteSpace(input.OwnerType)) { // otherwise - mark as temporary file.Temporary = true; } } - file.Category = input.FilesCategory.ToCamelCase(); + file.Category = input.FilesCategory?.ToCamelCase(); }); await _unitOfWorkManager.Current.SaveChangesAsync(); @@ -222,12 +230,6 @@ public async Task UploadAsync([FromForm] UploadFileInput input) } } - /* - * 1. property of entity (ownerid+type+property) - * 2. attachments list (ownerid+type+category) - * 3. direct upload using id (id) - */ - return uploadedFile; } @@ -241,16 +243,19 @@ public async Task UploadAsync([FromForm] UploadFileInput input) [Consumes("multipart/form-data")] public async Task UploadStaticAsync([FromForm] StaticFileInput input) { - if (input.File == null) - ModelState.AddModelError(nameof(input.File), $"{nameof(input.File)} must not be null"); + var validationResults = new ValidationResults(); + if (input.File == null) + validationResults.Add($"{nameof(input.File)} must not be null", [nameof(input.File)]); + + if (validationResults.Any()) + throw new AbpValidationException("An error occured", validationResults); - if (!ModelState.IsValid) - throw new AbpValidationException("An error occured");//, ModelState.Keys.Select(k => new ValidationResult(ModelState.Values[k], new List() { k }))); + input.EnsureFile(); var uploadedFile = new StoredFileDto(); var fileName = input.File.FileName.CleanupFileName(); - StoredFile storedFile; + StoredFile? storedFile; // allow to use predefined Id and re-activate existing storedfile using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete)) @@ -268,7 +273,7 @@ public async Task UploadStaticAsync([FromForm] StaticFileInput in { if (input.Id.HasValue && input.Id.Value != Guid.Empty) file.Id = input.Id.Value; - file.Category = input.FilesCategory.ToCamelCase(); + file.Category = input.FilesCategory.ToCamelCaseOrNull(); }); await _unitOfWorkManager.Current.SaveChangesAsync(); @@ -300,26 +305,6 @@ public async Task UploadStaticAsync([FromForm] StaticFileInput in return uploadedFile; } - - private List GetValidationResults(ModelStateDictionary modelState) - { - var idx = 0; - var values = modelState.Values.Select(v => new { Idx = idx++, Value = v }).ToList(); - idx = 0; - var keys = modelState.Keys.Select(k => new { Idx = idx++, Key = k }).ToList(); - - var errors = values.Where(v => v.Value.Errors.Any()).Select(v => new - { - Key = keys[v.Idx].Key, - State = v.Value.Errors.Select(e => e.ErrorMessage) - - }) - .Select(s => new ValidationResult(StringHelper.Delimited(s.State, "; "), new List { s.Key })) - .ToList(); - - return errors; - } - [HttpPost, Route("UploadNewVersion")] [Consumes("multipart/form-data")] public async Task UploadNewVersionAsync([FromForm] UploadFileVersionInput input) @@ -327,11 +312,8 @@ public async Task UploadNewVersionAsync([FromForm] UploadFileVers var storedFile = await _fileRepository.GetAsync(input.Id); if (input.File == null) - ModelState.AddModelError(nameof(input.File), $"{nameof(input.File)} must not be null"); - - if (!ModelState.IsValid) - throw new AbpValidationException("An error occured", GetValidationResults(ModelState)); - + throw new AbpValidationException("An error occured", [new ValidationResult($"{nameof(input.File)} must not be null")]); + var uploadedFile = new StoredFileDto(); var fileName = input.File.FileName.CleanupFileName(); @@ -357,10 +339,11 @@ public async Task UploadNewVersionAsync([FromForm] UploadFileVers return uploadedFile; } - private StoredFileDto GetFileDto(StoredFileVersion fileVersion) + private StoredFileDto? GetFileDto(StoredFileVersion fileVersion) { if (fileVersion == null) return null; + var dto = new StoredFileDto(); MapStoredFile(fileVersion, dto); return dto; @@ -385,54 +368,50 @@ private void MapStoredFile(StoredFileVersion fileVersion, StoredFileDto fileDto) [HttpDelete, Route("Delete")] public async Task DeleteAsync([FromQuery] DeleteStoredFileInput input) { - var ownerSpecified = - !string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId); + var ownerSpecified = !string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId); + var validationResults = new ValidationResults(); if (input.FileId == null && string.IsNullOrWhiteSpace(input.OwnerId) && !string.IsNullOrWhiteSpace(input.OwnerType)) - ModelState.AddModelError(nameof(input.OwnerId), $"{nameof(input.OwnerId)} must not be null when {nameof(input.OwnerType)} is specified"); + validationResults.Add($"{nameof(input.OwnerId)} must not be null when {nameof(input.OwnerType)} is specified", [nameof(input.OwnerId)]); if (input.FileId == null && string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId)) - ModelState.AddModelError(nameof(input.OwnerType), $"{nameof(input.OwnerType)} must not be null when {nameof(input.OwnerId)} is specified"); + validationResults.Add($"{nameof(input.OwnerType)} must not be null when {nameof(input.OwnerId)} is specified", [nameof(input.OwnerType)]); if (input.FileId == null && string.IsNullOrWhiteSpace(input.PropertyName)) - { - ModelState.AddModelError(nameof(input.FileId), $"Id must not be null"); - } + validationResults.Add($"Id must not be null", [nameof(input.FileId)]); var ownerType = _typeFinder.FindAll().FirstOrDefault(x => x.IsEntityType() && (x.FullName == input.OwnerType || x.GetTypeShortAliasOrNull() == input.OwnerType)); if (ownerSpecified && ownerType == null) - ModelState.AddModelError(input.OwnerId, $"Owner type not found (type = '{input.OwnerType}')"); + validationResults.Add($"Owner type not found (type = '{input.OwnerType}')", [nameof(input.OwnerId)]); - var owner = ownerSpecified - ? await _dynamicRepository.GetAsync(ownerType?.FullName, input.OwnerId) + var owner = ownerSpecified && ownerType != null + ? await _dynamicRepository.GetAsync(ownerType.GetRequiredFullName(), input.OwnerId.NotNull()) : null; if (ownerSpecified && owner == null) - if (ownerSpecified && owner == null) - { - ModelState.AddModelError(input.OwnerId, $"Owner not found (type = '{input.OwnerType}', id = '{input.OwnerId}')"); - } + validationResults.Add($"Owner not found (type = '{input.OwnerType}', id = '{input.OwnerId}')", [nameof(input.OwnerId)]); - var processAsProperty = owner != null && !string.IsNullOrWhiteSpace(input.PropertyName); - var property = processAsProperty - ? ReflectionHelper.GetProperty(owner, input.PropertyName, out owner) - : null; - if (processAsProperty) - { - if (property == null) - ModelState.AddModelError(nameof(input.PropertyName), $"Property '{owner.GetType().Name}.{input.PropertyName}' not found"); + PropertyInfo? property = null; + if (owner != null && !string.IsNullOrWhiteSpace(input.PropertyName)) + { + var accessor = ReflectionHelper.GetPropertyValueAccessor(owner, input.PropertyName); + if (accessor.PropInfo == null) + validationResults.Add($"Property '{owner.GetType().Name}.{input.PropertyName}' not found", [nameof(input.PropertyName)]); if (property != null && !typeof(StoredFile).IsAssignableFrom(property.PropertyType)) - ModelState.AddModelError(nameof(input.PropertyName), $"Wrong type of '{owner.GetType().Name}.{input.PropertyName}' property (actual: '{property.PropertyType.FullName}', expected: '{nameof(StoredFile)}')"); + validationResults.Add($"Wrong type of '{owner.GetType().Name}.{input.PropertyName}' property (actual: '{property.PropertyType.FullName}', expected: '{nameof(StoredFile)}')", [nameof(input.PropertyName)]); + + owner = accessor.Parent; + property = accessor.PropInfo; } - if (!ModelState.IsValid) - throw new AbpValidationException("Failed to delete file", GetValidationResults(ModelState)); + if (validationResults.Any()) + throw new AbpValidationException("Failed to delete file", validationResults); var storedFile = input.FileId != null ? await _fileRepository.GetAll().FirstOrDefaultAsync(f => f.Id == input.FileId.Value) : property != null ? property.GetValue(owner) as StoredFile - : !input.FileCategory.IsNullOrWhiteSpace() + : !string.IsNullOrWhiteSpace(input.FileCategory) ? await _fileRepository.GetAll().FirstOrDefaultAsync(f => f.Owner == null && f.Category == input.FileCategory) : null; @@ -442,7 +421,7 @@ public async Task DeleteAsync([FromQuery] DeleteStoredFileInput input) if (property != null) { property.SetValue(owner, null, null); - await _dynamicRepository.SaveOrUpdateAsync(owner); + await _dynamicRepository.SaveOrUpdateAsync(owner.NotNull()); } await _fileService.DeleteAsync(storedFile); @@ -451,6 +430,23 @@ public async Task DeleteAsync([FromQuery] DeleteStoredFileInput input) return true; } + private async Task GetOwnerAsync(string ownerType, string ownerId, string? ownerName) + { + var owner = await _dynamicRepository.GetAsync(ownerType, ownerId); + if (owner == null) + throw new AbpValidationException($"Owner '{ownerType}:{ownerId}' not found"); + if (!string.IsNullOrWhiteSpace(ownerName)) + { + var prop = ReflectionHelper.GetPropertyOrNull(owner, ownerName, out owner); + if (prop == null) + throw new AbpValidationException($"Property '{ownerName}' not found in class '{owner.NotNull().GetType().FullName}'"); + owner = prop.GetValue(owner); + if (owner == null) + throw new AbpValidationException($"Field '{ownerName}' of '{ownerType}:{ownerId}' is empty"); + } + return owner; + } + /// /// Download zip archive of all files linked to a specified entity /// @@ -458,7 +454,7 @@ public async Task DeleteAsync([FromQuery] DeleteStoredFileInput input) public async Task DownloadZipAsync([FromQuery] FilesListInput input) { IList files = new List(); - if (input.OwnerId.IsNullOrEmpty()) + if (string.IsNullOrWhiteSpace(input.OwnerId) || string.IsNullOrWhiteSpace(input.OwnerType)) { if (input.FilesId?.Count > 0) { @@ -471,25 +467,14 @@ public async Task DownloadZipAsync([FromQuery] FilesListInput } } else - { - var ownerId = input.OwnerId; - var ownerType = input.OwnerType; - if (!input.OwnerName.IsNullOrEmpty()) - { - var owner = await _dynamicRepository.GetAsync(input.OwnerType, input.OwnerId); - var prop = ReflectionHelper.GetProperty(owner, input.OwnerName, out owner); - if (prop == null) - throw new AbpValidationException($"Property '{input.OwnerName}' not found in class '{owner.GetType().FullName}'"); - owner = prop.GetValue(owner); - if (owner == null) - throw new AbpValidationException($"Owner '{input.OwnerName}' is empty"); - ownerId = owner.GetType().GetProperty("Id")?.GetValue(owner, null).ToString(); - ownerType = owner.GetType().StripCastleProxyType().FullName; - } + { + var owner = await GetOwnerAsync(input.OwnerType, input.OwnerId, input.OwnerName); + var ownerId = owner.NotNull().GetId()?.ToString(); + var ownerType = owner.GetType().StripCastleProxyType().GetRequiredFullName(); - files = input.FilesCategory.IsNullOrEmpty() + files = string.IsNullOrWhiteSpace(input.FilesCategory) ? await _fileService.GetAttachmentsAsync(ownerId, ownerType) - : await _fileService.GetAttachmentsOfCategoryAsync(ownerId, ownerType, input.FilesCategory.ToCamelCase()); + : await _fileService.GetAttachmentsOfCategoryAsync(ownerId, ownerType, input.FilesCategory?.ToCamelCase()); } if (files?.Count > 0) @@ -497,9 +482,10 @@ public async Task DownloadZipAsync([FromQuery] FilesListInput // todo: move zip support to the FileService, current implementation doesn't support Azure var list = _fileService.MakeUniqueFileNames(files); - var compressedStream = await CompressionService.CompressFilesAsync(list); +#pragma warning disable IDISP001 // Dispose created. Note: compressedStream will be disposed automatically in the FileStreamResult + var compressedStream = await CompressionService.CompressFilesAsync(list); +#pragma warning restore IDISP001 // Dispose created - // note: compressedStream will be disposed automatically in the FileStreamResult return File(compressedStream, "multipart/x-zip", "files.zip"); } @@ -515,28 +501,16 @@ public async Task> FilesListAsync([FromQuery] FilesListInput if (string.IsNullOrEmpty(input.OwnerType)) throw new Exception($"`{nameof(input.OwnerType)}` must not be null"); if (string.IsNullOrEmpty(input.OwnerId)) - throw new Exception($"`{nameof(input.OwnerId)}` must not be null"); - - var owner = await _dynamicRepository.GetAsync(input.OwnerType, input.OwnerId); - - if (owner == null) - throw new Exception($"Entity `{input.OwnerType}` `{input.OwnerId}` not found"); - - if (!input.OwnerName.IsNullOrEmpty()) - { - var prop = ReflectionHelper.GetProperty(owner, input.OwnerName, out owner); - owner = prop.GetValue(owner); - if (owner == null) - return new List(); - } - + throw new Exception($"`{nameof(input.OwnerId)}` must not be null"); + + var owner = await GetOwnerAsync(input.OwnerType, input.OwnerId, input.OwnerName); var id = owner.GetId(); - var type = owner.GetType().StripCastleProxyType().FullName; - var fileVersions = input.FilesCategory.IsNullOrEmpty() + var type = owner.GetType().StripCastleProxyType().GetRequiredFullName(); + var fileVersions = string.IsNullOrWhiteSpace(input.FilesCategory) ? await _fileService.GetLastVersionsOfAttachmentsAsync(id, type) : await _fileService.GetLastVersionsOfAttachmentsAsync(id, type, input.FilesCategory.ToCamelCase()); - var list = fileVersions.Select(v => GetFileDto(v)).ToList(); + var list = fileVersions.Select(v => GetFileDto(v)).WhereNotNull().ToList(); return list; } @@ -550,52 +524,50 @@ public async Task> FilesListAsync([FromQuery] FilesListInput [HttpPut, Route("")] [Consumes("multipart/form-data")] public async Task CreateOrUpdateAsync([FromForm] CreateStoredFileInput input) - { + { + #region validation and preparation + + var validations = new ValidationResults(); if (input.File == null) - ModelState.AddModelError(nameof(input.File), $"{nameof(input.File)} must not be null"); + validations.Add($"{nameof(input.File)} must not be null", [nameof(input.File)]); - var ownerSpecified = - !string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId); + var ownerSpecified = !string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId); + var hasProperty = !string.IsNullOrWhiteSpace(input.PropertyName); if (string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId)) - ModelState.AddModelError(nameof(input.OwnerType), $"{nameof(input.OwnerType)} must not be null when {nameof(input.OwnerId)} is specified"); + validations.Add($"{nameof(input.OwnerType)} must not be null when {nameof(input.OwnerId)} is specified", [nameof(input.OwnerType)]); - if (input.Id == null && input.PropertyName.IsNullOrWhiteSpace() && !ownerSpecified && input.FilesCategory.IsNullOrWhiteSpace()) - { - ModelState.AddModelError(nameof(input.Id), $"Id must not be null"); - } - - var hasProperty = !string.IsNullOrWhiteSpace(input.PropertyName); + if (input.Id == null && string.IsNullOrWhiteSpace(input.PropertyName) && !ownerSpecified && string.IsNullOrWhiteSpace(input.FilesCategory)) + validations.Add($"Id must not be null", [nameof(input.Id)]); var ownerType = _typeFinder.FindAll().FirstOrDefault(x => x.IsEntityType() && (x.FullName == input.OwnerType || x.GetTypeShortAliasOrNull() == input.OwnerType)); if (ownerSpecified && ownerType == null) - ModelState.AddModelError(input.OwnerId, $"Owner type not found (type = '{input.OwnerType}')"); + validations.Add($"Owner type not found (type = '{input.OwnerType}')", [nameof(input.OwnerId)]); - var owner = ownerSpecified - ? await _dynamicRepository.GetAsync(ownerType?.FullName, input.OwnerId) + var owner = ownerType != null && !string.IsNullOrWhiteSpace(input.OwnerId) + ? await _dynamicRepository.GetAsync(ownerType.GetRequiredFullName(), input.OwnerId) : null; if (ownerSpecified && owner == null) - { - ModelState.AddModelError(input.OwnerId, $"Owner not found (type = '{input.OwnerType}', id = '{input.OwnerId}')"); - } + validations.Add($"Owner not found (type = '{input.OwnerType}', id = '{input.OwnerId}')", [nameof(input.OwnerId)]); - var uploadAsProperty = owner != null && hasProperty; - var property = uploadAsProperty - ? ReflectionHelper.GetProperty(owner, input.PropertyName, out owner) - : null; - if (uploadAsProperty) + PropertyInfo? property = null; + if (owner != null && !string.IsNullOrWhiteSpace(input.PropertyName)) { + property = ReflectionHelper.GetPropertyOrNull(owner, input.PropertyName, out owner); + if (property == null) - ModelState.AddModelError(nameof(input.PropertyName), $"Property '{owner.GetType().Name}.{input.PropertyName}' not found"); + validations.Add($"Property '{owner?.GetType().Name}.{input.PropertyName}' not found", [nameof(input.PropertyName)]); if (property != null && !typeof(StoredFile).IsAssignableFrom(property.PropertyType)) - ModelState.AddModelError(nameof(input.PropertyName), $"Wrong type of '{owner.GetType().Name}.{input.PropertyName}' property (actual: '{property.PropertyType.FullName}', expected: '{nameof(StoredFile)}')"); + validations.Add($"Wrong type of '{owner?.GetType().Name}.{input.PropertyName}' property (actual: '{property.PropertyType.FullName}', expected: '{nameof(StoredFile)}')", [nameof(input.PropertyName)]); } - if (!ModelState.IsValid) - throw new AbpValidationException("Failed to upload file", GetValidationResults(ModelState)); + if (validations.Any()) + throw new AbpValidationException("Failed to upload file", validations); - StoredFile storedFile; + #endregion + + StoredFile? storedFile; StoredFileVersion fileVersion; // allow to use predefined Id and re-activate existing storedfile using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete)) @@ -604,10 +576,12 @@ public async Task CreateOrUpdateAsync([FromForm] CreateStoredFile ? await _fileRepository.GetAll().FirstOrDefaultAsync(f => f.Id == input.Id.Value) : property != null && owner != null ? property.GetValue(owner) as StoredFile - : !input.FilesCategory.IsNullOrWhiteSpace() + : !string.IsNullOrWhiteSpace(input.FilesCategory) ? await _fileRepository.FirstOrDefaultAsync(x => x.Owner == null && x.Category == input.FilesCategory) : null; - } + } + + input.EnsureFile(); var fileName = input.File.FileName.CleanupFileName(); @@ -634,7 +608,7 @@ public async Task CreateOrUpdateAsync([FromForm] CreateStoredFile storedFile.Temporary = true; } - if (ownerSpecified) + if (!string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId)) { storedFile.SetOwner(input.OwnerType, input.OwnerId); } @@ -675,7 +649,7 @@ public async Task CreateOrUpdateAsync([FromForm] CreateStoredFile /// /// [HttpGet, Route("")] - public async Task GetAsync(Guid id) + public async Task GetAsync(Guid id) { var storedFile = await _fileRepository.GetAsync(id); @@ -689,11 +663,11 @@ public async Task GetAsync(Guid id) /// /// [HttpGet, Route("EntityProperty")] - public async Task GetEntityPropertyAsync([FromQuery] StoredFileAsPropertyDto input) + public async Task GetEntityPropertyAsync([FromQuery] StoredFileAsPropertyDto input) { - var hasOwner = !input.OwnerType.IsNullOrWhiteSpace() && !input.OwnerId.IsNullOrWhiteSpace(); - var hasProperty = !input.PropertyName.IsNullOrWhiteSpace(); - var hasCategory = !input.FileCategory.IsNullOrWhiteSpace(); + var hasOwner = !string.IsNullOrWhiteSpace(input.OwnerType) && !string.IsNullOrWhiteSpace(input.OwnerId); + var hasProperty = !string.IsNullOrWhiteSpace(input.PropertyName); + var hasCategory = !string.IsNullOrWhiteSpace(input.FileCategory); if ((!hasOwner || !hasProperty) && !hasCategory) return null; @@ -701,9 +675,13 @@ public async Task GetEntityPropertyAsync([FromQuery] StoredFileAs { var entity = await _dynamicRepository.GetAsync(input.OwnerType, input.OwnerId); if (entity == null) - return null; - - var property = ReflectionHelper.GetProperty(entity, input.PropertyName, out var owner); + return null; + + if (string.IsNullOrEmpty(input.PropertyName)) + throw new Exception($"`{nameof(input.PropertyName)}` must not be null"); + var property = ReflectionHelper.GetPropertyOrNull(entity, input.PropertyName, out var owner); + if (owner == null) + throw new AbpValidationException($"Field '{input.PropertyName}' of '{input.OwnerType}:{input.OwnerId}' is empty"); if (property == null) throw new Exception($"Property '{input.PropertyName}' not found in the class {owner.GetType().Name}"); @@ -751,15 +729,6 @@ public async Task> GetFileVersionsAsync(Guid file return documentUploads.Select(v => ObjectMapper.Map(v)).ToList(); } - private string GetUploadedBy(Int64? userId) - { - if (userId == null) - return string.Empty; - - var person = _personRepository.GetAll().FirstOrDefault(p => p.User != null && p.User.Id == userId); - return person?.FullName; - } - #endregion /// @@ -802,7 +771,9 @@ public async Task DownloadThumbnailAsync(Guid id, int width, int h using var data = skImage.Encode(SKEncodedImageFormat.Png, 100); // Save to result stream +#pragma warning disable IDISP001 // Dispose created. Note: this stream will be disposed by FileStreamResult var resultStream = new MemoryStream(); +#pragma warning restore IDISP001 // Dispose created data.SaveTo(resultStream); resultStream.Seek(0, SeekOrigin.Begin); // Reset stream position @@ -856,4 +827,4 @@ private static SKBitmap GenerateThumbnail(SKBitmap originalImage, int width, int } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/UserManagements/Configurations/IUserManagementSettings.cs b/shesha-core/src/Shesha.Application/UserManagements/Configurations/IUserManagementSettings.cs index 47c63296a8..6ce4d00106 100644 --- a/shesha-core/src/Shesha.Application/UserManagements/Configurations/IUserManagementSettings.cs +++ b/shesha-core/src/Shesha.Application/UserManagements/Configurations/IUserManagementSettings.cs @@ -1,11 +1,6 @@ using Shesha.Settings; -using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.UserManagements.Configurations { diff --git a/shesha-core/src/Shesha.Application/UserManagements/Configurations/UserManagementSettings.cs b/shesha-core/src/Shesha.Application/UserManagements/Configurations/UserManagementSettings.cs index 72fa351b72..be9e952037 100644 --- a/shesha-core/src/Shesha.Application/UserManagements/Configurations/UserManagementSettings.cs +++ b/shesha-core/src/Shesha.Application/UserManagements/Configurations/UserManagementSettings.cs @@ -1,10 +1,4 @@ -using Shesha.Domain.Attributes; -using Shesha.Domain.Enums; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Shesha.Domain.Enums; namespace Shesha.UserManagements.Configurations { @@ -21,7 +15,7 @@ public class UserManagementSettings /// /// /// - public string GoToUrlAfterRegistration { get; set; } + public string? GoToUrlAfterRegistration { get; set; } /// /// /// @@ -33,10 +27,10 @@ public class UserManagementSettings /// /// Form Module Name /// - public string AdditionalRegistrationInfoFormModule { get; set; } + public string? AdditionalRegistrationInfoFormModule { get; set; } /// /// Form Name /// - public string AdditionalRegistrationInfoFormName { get; set; } + public string? AdditionalRegistrationInfoFormName { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/UserManagements/CreatePersonAccountDto.cs b/shesha-core/src/Shesha.Application/UserManagements/CreatePersonAccountDto.cs index 17edae10e6..a841b7efca 100644 --- a/shesha-core/src/Shesha.Application/UserManagements/CreatePersonAccountDto.cs +++ b/shesha-core/src/Shesha.Application/UserManagements/CreatePersonAccountDto.cs @@ -9,25 +9,25 @@ namespace Shesha.Persons [AutoMapTo(typeof(User), typeof(Person))] public class CreatePersonAccountDto { - public virtual string UserName { get; set; } + public virtual string? UserName { get; set; } [Required] - public virtual string Password { get; set; } + public virtual string? Password { get; set; } [Required] - public virtual string PasswordConfirmation { get; set; } + public virtual string? PasswordConfirmation { get; set; } [Required(AllowEmptyStrings = false)] [MinLength(1)] - public virtual string FirstName { get; set; } + public virtual string? FirstName { get; set; } [Required(AllowEmptyStrings = false)] [MinLength(1)] - public virtual string LastName { get; set; } + public virtual string? LastName { get; set; } - public virtual string MobileNumber { get; set; } + public virtual string? MobileNumber { get; set; } [EmailAddress] [Required(AllowEmptyStrings = false)] - public virtual string EmailAddress { get; set; } - public virtual ReferenceListItemValueDto TypeOfAccount { get; set; } + public virtual string? EmailAddress { get; set; } + public virtual ReferenceListItemValueDto? TypeOfAccount { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/UserManagements/UserManagementAppService.cs b/shesha-core/src/Shesha.Application/UserManagements/UserManagementAppService.cs index 34346477c5..43b54d611d 100644 --- a/shesha-core/src/Shesha.Application/UserManagements/UserManagementAppService.cs +++ b/shesha-core/src/Shesha.Application/UserManagements/UserManagementAppService.cs @@ -1,13 +1,14 @@ using Abp.Domain.Repositories; using Abp.Linq.Extensions; -using Abp.Runtime.Validation; using Shesha.Authorization.Users; using Shesha.Configuration.Security; using Shesha.Domain; using Shesha.Domain.Enums; using Shesha.Extensions; using Shesha.Persons; +using Shesha.Reflection; using Shesha.UserManagements.Configurations; +using Shesha.Validations; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -22,7 +23,6 @@ namespace Shesha.UserManagements public class UserManagementAppService: SheshaAppServiceBase { private readonly UserManager _userManager; - private readonly IRepository _rolePersonRepository; private readonly IRepository _repository; private readonly IRepository _userRegistration; private readonly ISecuritySettings _securitySettings; @@ -31,28 +31,17 @@ public class UserManagementAppService: SheshaAppServiceBase public UserManagementAppService( IRepository repository, UserManager userManager, - IRepository rolePersonRepository, ISecuritySettings securitySettings, IUserManagementSettings userManagementSettings, IRepository userRegistration) { _userManager = userManager; - _rolePersonRepository = rolePersonRepository; _repository = repository; _securitySettings = securitySettings; _userManagementSettings = userManagementSettings; _userRegistration = userRegistration; } - /// - /// Default constructor - /// - /// - public UserManagementAppService(IRepository repository) - { - - } - public async Task CreateAsync(CreatePersonAccountDto input) { //CheckCreatePermission(); @@ -60,7 +49,12 @@ public async Task CreateAsync(CreatePersonAccountDto input) // Performing additional validations var validationResults = new List(); - var supportedPasswordResetMethods = new List(); + var supportedPasswordResetMethods = new List(); + + if (!registrationSettings.UserEmailAsUsername && string.IsNullOrWhiteSpace(input.UserName)) + validationResults.Add(new ValidationResult("Username is mandatory")); + if (registrationSettings.UserEmailAsUsername && string.IsNullOrWhiteSpace(input.EmailAddress)) + validationResults.Add(new ValidationResult("Email Address is mandatory")); if (input.TypeOfAccount == null) validationResults.Add(new ValidationResult("Type of account is mandatory")); @@ -80,8 +74,7 @@ public async Task CreateAsync(CreatePersonAccountDto input) if (await EmailAlreadyInUseAsync(input.EmailAddress, null)) validationResults.Add(new ValidationResult("Specified email already used by another person")); - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); // Supported password reset methods for the user // This might need reviewing since some methods might be unavailable for some users during time of @@ -103,8 +96,9 @@ public async Task CreateAsync(CreatePersonAccountDto input) var defaultMethods = supportedPasswordResetMethods.Count > 0 ? supportedPasswordResetMethods.Sum() : 0; // Creating User Account to enable login into the application + var userName = registrationSettings.UserEmailAsUsername ? input.EmailAddress : input.UserName; User user = await _userManager.CreateUserAsync( - registrationSettings.UserEmailAsUsername ? input.EmailAddress : input.UserName, + userName.NotNull(), input.TypeOfAccount?.ItemValue == (long)RefListTypeOfAccount.Internal, input.Password, input.PasswordConfirmation, @@ -134,7 +128,8 @@ public async Task CreateAsync(CreatePersonAccountDto input) UserNameOrEmailAddress = user.UserName, GoToUrlAfterRegistration = registrationSettings.GoToUrlAfterRegistration, AdditionalRegistrationInfoForm = !string.IsNullOrWhiteSpace(registrationSettings.AdditionalRegistrationInfoFormModule) && !string.IsNullOrWhiteSpace(registrationSettings.AdditionalRegistrationInfoFormName) - ? new FormIdentifier(registrationSettings.AdditionalRegistrationInfoFormModule,registrationSettings.AdditionalRegistrationInfoFormName) : null, + ? new FormIdentifier(registrationSettings.AdditionalRegistrationInfoFormModule,registrationSettings.AdditionalRegistrationInfoFormName) + : null, IsComplete = registrationSettings.AdditionalRegistrationInfo ? false : true }; @@ -151,7 +146,7 @@ public async Task CreateAsync(CreatePersonAccountDto input) /// /// /// - public async Task CompleteRegistrationAsync(long userId) + public async Task CompleteRegistrationAsync(long userId) { var userRegistration = await _userRegistration.FirstOrDefaultAsync(e => e.UserId == userId); if (userRegistration == null) @@ -171,7 +166,7 @@ public async Task CompleteRegistrationAsync(long userId) /// Checks is specified mobile number already used by another person /// /// - private async Task MobileNoAlreadyInUseAsync(string mobileNo, Guid? id) + private async Task MobileNoAlreadyInUseAsync(string? mobileNo, Guid? id) { if (string.IsNullOrWhiteSpace(mobileNo)) return false; @@ -186,7 +181,7 @@ private async Task MobileNoAlreadyInUseAsync(string mobileNo, Guid? id) /// Checks is specified email already used by another person /// /// - private async Task EmailAlreadyInUseAsync(string email, Guid? id) + private async Task EmailAlreadyInUseAsync(string? email, Guid? id) { if (string.IsNullOrWhiteSpace(email)) return false; @@ -196,6 +191,5 @@ private async Task EmailAlreadyInUseAsync(string email, Guid? id) .WhereIf(id.HasValue, e => e.Id != id) .AnyAsync(); } - } } diff --git a/shesha-core/src/Shesha.Application/UserManagements/UserManagementAutocompleteItemDto.cs b/shesha-core/src/Shesha.Application/UserManagements/UserManagementAutocompleteItemDto.cs deleted file mode 100644 index 0bdf22a465..0000000000 --- a/shesha-core/src/Shesha.Application/UserManagements/UserManagementAutocompleteItemDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Abp.Application.Services.Dto; -using System; - -namespace Shesha.UserManagements -{ - public class UserManagementAutocompleteItemDto: EntityDto - { - public string FullName { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Application/UserManagements/UserManagementDto.cs b/shesha-core/src/Shesha.Application/UserManagements/UserManagementDto.cs deleted file mode 100644 index 0331423420..0000000000 --- a/shesha-core/src/Shesha.Application/UserManagements/UserManagementDto.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Abp.Application.Services.Dto; -using Abp.AutoMapper; - -namespace Shesha.UserManagements -{ - [AutoMap(typeof(Domain.Person))] - public class UserManagementDto: EntityDto - { - public virtual string UserName { get; set; } - public virtual string Password { get; set; } - - public virtual string FirstName { get; set; } - - public virtual string LastName { get; set; } - - public virtual string MobileNumber { get; set; } - - public virtual string EmailAddress { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Application/Users/Dto/CreateUserDto.cs b/shesha-core/src/Shesha.Application/Users/Dto/CreateUserDto.cs index 99c42c2aaf..697e8c1132 100644 --- a/shesha-core/src/Shesha.Application/Users/Dto/CreateUserDto.cs +++ b/shesha-core/src/Shesha.Application/Users/Dto/CreateUserDto.cs @@ -31,7 +31,7 @@ public class CreateUserDto : IShouldNormalize public string[] RoleNames { get; set; } - public long[] SupportedPasswordResetMethods { get; set; } + public long[]? SupportedPasswordResetMethods { get; set; } [Required] [StringLength(AbpUserBase.MaxPlainPasswordLength)] diff --git a/shesha-core/src/Shesha.Application/Users/Dto/UserDto.cs b/shesha-core/src/Shesha.Application/Users/Dto/UserDto.cs index 4be7dc7301..853f31b882 100644 --- a/shesha-core/src/Shesha.Application/Users/Dto/UserDto.cs +++ b/shesha-core/src/Shesha.Application/Users/Dto/UserDto.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using Abp.Application.Services.Dto; using Abp.Authorization.Users; using Abp.AutoMapper; using Shesha.Authorization.Users; -using Shesha.AutoMapper.Dto; -using Shesha.Services.ReferenceLists.Dto; +using System; +using System.ComponentModel.DataAnnotations; namespace Shesha.Users.Dto { @@ -15,31 +12,31 @@ public class UserDto : EntityDto { [Required] [StringLength(AbpUserBase.MaxUserNameLength)] - public string UserName { get; set; } + public string? UserName { get; set; } [Required] [StringLength(AbpUserBase.MaxNameLength)] - public string Name { get; set; } + public string? Name { get; set; } [Required] [StringLength(AbpUserBase.MaxSurnameLength)] - public string Surname { get; set; } + public string? Surname { get; set; } [Required] [EmailAddress] [StringLength(AbpUserBase.MaxEmailAddressLength)] - public string EmailAddress { get; set; } + public string? EmailAddress { get; set; } public bool IsActive { get; set; } - public string FullName { get; set; } + public string? FullName { get; set; } public DateTime? LastLoginTime { get; set; } public DateTime CreationTime { get; set; } - public string[] RoleNames { get; set; } + public string[]? RoleNames { get; set; } - public long[] SupportedPasswordResetMethods { get; set; } + public long[]? SupportedPasswordResetMethods { get; set; } } } diff --git a/shesha-core/src/Shesha.Application/Users/Dto/UserMapProfile.cs b/shesha-core/src/Shesha.Application/Users/Dto/UserMapProfile.cs index 5cf466d988..b468f8e9bf 100644 --- a/shesha-core/src/Shesha.Application/Users/Dto/UserMapProfile.cs +++ b/shesha-core/src/Shesha.Application/Users/Dto/UserMapProfile.cs @@ -1,6 +1,5 @@ using AutoMapper; using Shesha.Authorization.Users; -using Shesha.AutoMapper; namespace Shesha.Users.Dto { diff --git a/shesha-core/src/Shesha.Application/Users/UserAppService.cs b/shesha-core/src/Shesha.Application/Users/UserAppService.cs index 64f7f79d45..25231b21a3 100644 --- a/shesha-core/src/Shesha.Application/Users/UserAppService.cs +++ b/shesha-core/src/Shesha.Application/Users/UserAppService.cs @@ -91,7 +91,7 @@ public override async Task CreateAsync(CreateUserDto input) user.TenantId = AbpSession.TenantId; user.IsEmailConfirmed = true; - user.SupportedPasswordResetMethods = input.SupportedPasswordResetMethods.Sum(); + user.SupportedPasswordResetMethods = input.SupportedPasswordResetMethods?.Sum(); await _userManager.InitializeOptionsAsync(AbpSession.TenantId); @@ -190,7 +190,7 @@ protected override User MapToEntity(CreateUserDto createInput) protected override void MapToEntity(UserDto updateInput, User user) { ObjectMapper.Map(updateInput, user); - user.SupportedPasswordResetMethods = updateInput.SupportedPasswordResetMethods.Sum(); + user.SupportedPasswordResetMethods = updateInput.SupportedPasswordResetMethods?.Sum(); user.SetNormalizedNames(); } @@ -214,7 +214,7 @@ protected override UserDto MapToEntityDto(User user) protected override IQueryable CreateFilteredQuery(PagedUserResultRequestDto input) { return Repository.GetAllIncluding(x => x.Roles) - .WhereIf(!input.Keyword.IsNullOrWhiteSpace(), x => x.UserName.Contains(input.Keyword) || x.Name.Contains(input.Keyword) || x.EmailAddress.Contains(input.Keyword)) + .WhereIf(!input.Keyword.IsNullOrWhiteSpace(), x => x.UserName.Contains(input.Keyword) || x.Name.Contains(input.Keyword) || x.EmailAddress != null && x.EmailAddress.Contains(input.Keyword)) .WhereIf(input.IsActive.HasValue, x => x.IsActive == input.IsActive); } @@ -242,7 +242,7 @@ protected virtual void CheckErrors(IdentityResult identityResult) #region Reset password using OTP - private async Task GetUniqueUserByMobileNoAsync(string mobileNo) + private async Task GetUniqueUserByMobileNoAsync(string mobileNo) { var users = await _userRepository.GetAll().Where(u => u.PhoneNumber == mobileNo).ToListAsync(); @@ -326,10 +326,13 @@ public async Task> GetUserPasswordResetOptionsAsync } else if (reflistItem == RefListPasswordResetMethods.EmailLink && isEmailLinkEnabled && hasEmail) { - var maskedEmail = person.EmailAddress.MaskEmail(); - methodOption.Prompt = $"Email a link to {maskedEmail}"; - methodOption.MaskedIdentifier = maskedEmail; - isAllowed = true; + if (!string.IsNullOrWhiteSpace(person.EmailAddress)) + { + var maskedEmail = person.EmailAddress.MaskEmail(); + methodOption.Prompt = $"Email a link to {maskedEmail}"; + methodOption.MaskedIdentifier = maskedEmail; + isAllowed = true; + } } else if (reflistItem == RefListPasswordResetMethods.SecurityQuestions && isSecurityQuestionsEnabled && hasQuestions) { @@ -507,6 +510,9 @@ public async Task SendEmailLinkAsync(string username) ValidateUserPasswordResetMethod(user, (long)RefListPasswordResetMethods.EmailLink); + if (string.IsNullOrWhiteSpace(user.EmailAddress)) + throw new UserFriendlyException("User has no email address"); + var lifetime = securitySettings.ResetPasswordEmailLinkLifetime; var encodedUserName = Convert.ToBase64String(Encoding.UTF8.GetBytes(username)); @@ -526,8 +532,8 @@ public async Task SendEmailLinkAsync(string username) [AbpAllowAnonymous] public async Task ResetPasswordVerifyOtpAsync(ResetPasswordVerifyOtpInput input) { - var otp = await _otpManager.GetAsync(input.OperationId); - var personId = otp?.RecipientId.ToGuid() ?? Guid.Empty; + var otp = await _otpManager.GetOrNullAsync(input.OperationId); + var personId = otp?.RecipientId?.ToGuid() ?? Guid.Empty; var user = personId != Guid.Empty ? (await _personRepository.GetAsync(personId))?.User : await GetUniqueUserByMobileNoAsync(input.MobileNo); diff --git a/shesha-core/src/Shesha.Common/AppMessages.cs b/shesha-core/src/Shesha.Common/AppMessages.cs new file mode 100644 index 0000000000..22ce984c76 --- /dev/null +++ b/shesha-core/src/Shesha.Common/AppMessages.cs @@ -0,0 +1,8 @@ +namespace Shesha +{ + public static class AppMessages + { + public const string ValidationCorrectErrorsAndTryAgain = "Validation.CorrectErrorsAndTryAgain"; + public const string ValidationSomethingWentWrong = "Validation.SomethingWentWrong"; + } +} diff --git a/shesha-core/src/Shesha.Common/DbmsHelper.cs b/shesha-core/src/Shesha.Common/DbmsHelper.cs index 5d128d0f52..7c18619d92 100644 --- a/shesha-core/src/Shesha.Common/DbmsHelper.cs +++ b/shesha-core/src/Shesha.Common/DbmsHelper.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Configuration; +using System.Configuration; namespace Shesha { @@ -10,7 +11,7 @@ public static class DbmsHelper /// /// Get current Dbms type /// - public static DbmsType GetDbmsType(this IConfigurationRoot config) + public static DbmsType GetDbmsType(this IConfiguration config) { return config.GetValue("DbmsType", DbmsType.SQLServer); } @@ -20,9 +21,24 @@ public static DbmsType GetDbmsType(this IConfigurationRoot config) /// /// /// - public static string? GetDefaultConnectionString(this IConfigurationRoot config) + public static string GetDefaultConnectionString(this IConfiguration config) { - return config.GetConnectionString("Default"); + return config.GetRequiredConnectionString("Default"); + } + + /// + /// Get conneciton string by name. Throw exception if conneciton string is unavailable + /// + /// + /// + /// + /// + public static string GetRequiredConnectionString(this IConfiguration configuration, string name) + { + var connectionString = configuration.GetConnectionString(name); + return !string.IsNullOrWhiteSpace(connectionString) + ? connectionString + : throw new ConfigurationErrorsException($"Connection string '{name}' is unavailable"); } } } diff --git a/shesha-core/src/Shesha.Common/Shesha.Common.csproj b/shesha-core/src/Shesha.Common/Shesha.Common.csproj index 7e490d4757..b9882f01ac 100644 --- a/shesha-core/src/Shesha.Common/Shesha.Common.csproj +++ b/shesha-core/src/Shesha.Common/Shesha.Common.csproj @@ -11,14 +11,23 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/shesha-core/src/Shesha.Core/Authorization/ShaCustomPermissionChecker.cs b/shesha-core/src/Shesha.Core/Authorization/ShaCustomPermissionChecker.cs index 29105dcb85..336353d996 100644 --- a/shesha-core/src/Shesha.Core/Authorization/ShaCustomPermissionChecker.cs +++ b/shesha-core/src/Shesha.Core/Authorization/ShaCustomPermissionChecker.cs @@ -1,5 +1,6 @@ using Abp.Dependency; using Abp.Domain.Repositories; +using Abp.Threading; using Shesha.AutoMapper.Dto; using Shesha.Domain; using Shesha.Extensions; @@ -25,38 +26,41 @@ public ShaCustomPermissionChecker(IRepository personRepository, IR public async Task IsGrantedAsync(long userId, string permissionName) { - var person = await _personRepository.FirstOrDefaultAsync(x => x.User.Id == userId); - var roles = _rolePersonRepository.GetAll().Where(x => x.Person == person); - return roles.SelectMany(x => x.Role.Permissions).Any(x => x.Permission == permissionName && x.IsGranted); - } - - public bool IsGranted(long userId, string permissionName) - { - var person = _personRepository.GetAll().FirstOrDefault(x => x.User.Id == userId); - var roles = _rolePersonRepository.GetAll().Where(x => x.Person == person); - return roles.SelectMany(x => x.Role.Permissions).Any(x => x.Permission == permissionName && x.IsGranted); + var person = await GetPersonAsync(userId); + if (person == null) + return false; + var roles = _rolePersonRepository.GetAll().Where(x => x.Person == person && x.Role != null); + return roles.SelectMany(x => x.Role!.Permissions).Any(x => x.Permission == permissionName && x.IsGranted); } public async Task IsGrantedAsync(long userId, string permissionName, EntityReferenceDto permissionedEntity) { - var person = await _personRepository.FirstOrDefaultAsync(x => x.User.Id == userId); - var roles = (await _rolePersonRepository.GetAll().Where(x => x.Person == person) + var person = await GetPersonAsync(userId); + if (person == null) + return false; + + var roles = (await _rolePersonRepository.GetAll().Where(x => x.Person == person && x.Role != null) .ToListAsync()) .Where(x => !x.PermissionedEntities.Any() || x.PermissionedEntities.Any(pe => pe.Id == permissionedEntity.Id && pe._className == permissionedEntity._className)); - return roles.SelectMany(x => x.Role.Permissions).Any(x => x.Permission == permissionName && x.IsGranted); + return roles.SelectMany(x => x.Role!.Permissions).Any(x => x.Permission == permissionName && x.IsGranted); } + public bool IsGranted(long userId, string permissionName) + { + return AsyncHelper.RunSync(async() => await IsGrantedAsync(userId, permissionName)); + } + + public bool IsGranted(long userId, string permissionName, EntityReferenceDto permissionedEntity) { - var person = _personRepository.GetAll().FirstOrDefault(x => x.User.Id == userId); - var roles = _rolePersonRepository.GetAll().Where(x => x.Person == person) - .ToList() - .Where(x => - !x.PermissionedEntities.Any() - || x.PermissionedEntities.Any(pe => pe.Id == permissionedEntity.Id && pe._className == permissionedEntity._className)); - return roles.SelectMany(x => x.Role.Permissions).Any(x => x.Permission == permissionName && x.IsGranted); + return AsyncHelper.RunSync(async () => await IsGrantedAsync(userId, permissionName, permissionedEntity)); + } + + private async Task GetPersonAsync(long userId) + { + return await _personRepository.FirstOrDefaultAsync(x => x.User != null && x.User.Id == userId); } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Core/Domain/Area.cs b/shesha-core/src/Shesha.Core/Domain/Area.cs index 45a7e18e26..e4abe46537 100644 --- a/shesha-core/src/Shesha.Core/Domain/Area.cs +++ b/shesha-core/src/Shesha.Core/Domain/Area.cs @@ -24,7 +24,7 @@ public class Area : FullAuditedEntity, IMayHaveTenant [StringLength(200)] public virtual string Comments { get; set; } - public virtual Area ParentArea { get; set; } + public virtual Area? ParentArea { get; set; } public virtual int? TenantId { get; set; } diff --git a/shesha-core/src/Shesha.Core/Domain/EntityHistoryEvent.cs b/shesha-core/src/Shesha.Core/Domain/EntityHistoryEvent.cs index 6b2a342b18..80145e8932 100644 --- a/shesha-core/src/Shesha.Core/Domain/EntityHistoryEvent.cs +++ b/shesha-core/src/Shesha.Core/Domain/EntityHistoryEvent.cs @@ -1,9 +1,7 @@ -using System; -using System.ComponentModel.DataAnnotations.Schema; -using System.Diagnostics; -using Abp.Domain.Entities; +using Abp.Domain.Entities; using Abp.EntityHistory; -using JetBrains.Annotations; +using System; +using System.ComponentModel.DataAnnotations.Schema; namespace Shesha.Domain { @@ -11,17 +9,17 @@ public class EntityHistoryEvent : Entity { public virtual EntityChangeSet EntityChangeSet { get; set; } - public virtual EntityChange EntityChange { get; set; } + public virtual EntityChange? EntityChange { get; set; } - public virtual EntityPropertyChange EntityPropertyChange { get; set; } + public virtual EntityPropertyChange? EntityPropertyChange { get; set; } - public virtual string EventType { get; set; } + public virtual string? EventType { get; set; } - public virtual string EventName { get; set; } + public virtual string? EventName { get; set; } - public virtual string Description { get; set; } + public virtual string? Description { get; set; } [NotMapped] - public virtual string PropertyName { get; set; } + public virtual string? PropertyName { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationReadStatus.cs b/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationReadStatus.cs index 040d533548..0e4df1f686 100644 --- a/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationReadStatus.cs +++ b/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationReadStatus.cs @@ -1,9 +1,4 @@ using Shesha.Domain.Attributes; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Domain.Enums { diff --git a/shesha-core/src/Shesha.Core/Domain/Enums/RefListPermissionedEntityIsRequired.cs b/shesha-core/src/Shesha.Core/Domain/Enums/RefListPermissionedEntityIsRequired.cs new file mode 100644 index 0000000000..e4e6a85d2e --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/Enums/RefListPermissionedEntityIsRequired.cs @@ -0,0 +1,12 @@ +using Shesha.Domain.Attributes; + +namespace Shesha.Domain.Enums +{ + [ReferenceList("PermissionedEntityIsRequired")] + public enum RefListPermissionedEntityIsRequired + { + Yes = 1, + No = 2, + Optional = 3 + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/INotificationTemplateProps.cs b/shesha-core/src/Shesha.Core/Domain/INotificationTemplateProps.cs new file mode 100644 index 0000000000..0d8eb961a9 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/INotificationTemplateProps.cs @@ -0,0 +1,11 @@ +using Shesha.Domain.Enums; + +namespace Shesha.Domain +{ + public interface INotificationTemplateProps + { + RefListNotificationMessageFormat? MessageFormat { get; set; } + string TitleTemplate { get; set; } + string BodyTemplate { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/INotificationTypeSpecificProps.cs b/shesha-core/src/Shesha.Core/Domain/INotificationTypeSpecificProps.cs new file mode 100644 index 0000000000..ebfd8824fb --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/INotificationTypeSpecificProps.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; + +namespace Shesha.Domain +{ + public interface INotificationTypeSpecificProps + { + /// + /// If true, attachments are allowed for this notification + /// + public bool AllowAttachments { get; set; } + + /// + /// + /// + public bool Disable { get; set; } + + /// + /// If true indicates that users may opt out of this notification + /// + public bool CanOptOut { get; set; } + + /// + /// + /// + public string Category { get; set; } + + /// + /// + /// + public int OrderIndex { get; set; } + + /// + /// List of NotificationChannelConfigs + /// + [StringLength(int.MaxValue)] + public string? OverrideChannels { get; set; } + + /// + /// messages without which the user should not proceed in any case e.g. OTP + /// + public bool IsTimeSensitive { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/Note.cs b/shesha-core/src/Shesha.Core/Domain/Note.cs index b2a2b4b5db..896d2cac04 100644 --- a/shesha-core/src/Shesha.Core/Domain/Note.cs +++ b/shesha-core/src/Shesha.Core/Domain/Note.cs @@ -1,18 +1,15 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Abp.Domain.Entities; -using Abp.Domain.Entities.Auditing; +using Abp.Domain.Entities; using Shesha.Domain.Attributes; using Shesha.Domain.Enums; +using System.ComponentModel.DataAnnotations; namespace Shesha.Domain { [Entity(TypeShortAlias = "Shesha.Framework.Note")] public class Note : FullPowerChildEntity { - [ReferenceList("Shesha", "NoteType")] - public virtual int? Category { get; set; } + [StringLength(50)] + public virtual string? Category { get; set; } public virtual Note Parent { get; set; } diff --git a/shesha-core/src/Shesha.Core/Domain/Notification.cs b/shesha-core/src/Shesha.Core/Domain/Notification.cs index 4cf894db5a..48ecd4fa93 100644 --- a/shesha-core/src/Shesha.Core/Domain/Notification.cs +++ b/shesha-core/src/Shesha.Core/Domain/Notification.cs @@ -21,11 +21,11 @@ public class Notification : FullAuditedEntity /// /// /// - public virtual Person ToPerson { get; set; } + public virtual Person? ToPerson { get; set; } /// /// /// - public virtual Person FromPerson { get; set; } + public virtual Person? FromPerson { get; set; } /// /// Serialized Json of the notification data /// @@ -38,10 +38,15 @@ public class Notification : FullAuditedEntity /// /// The entity that the notification pertains to /// - public virtual GenericEntityReference TriggeringEntity { get; set; } + public virtual GenericEntityReference? TriggeringEntity { get; set; } /// /// /// public virtual NotificationTopic NotificationTopic { get; set; } + + /// + /// Notification category (any string reference that can be used for analysys) + /// + public string Category { get; set; } = string.Empty; } } diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs b/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs index beb90714d0..7b6f4c23a4 100644 --- a/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs +++ b/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs @@ -1,11 +1,6 @@ using Shesha.Domain.Attributes; using Shesha.Domain.ConfigurationItems; using Shesha.Domain.Enums; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Domain { diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationChannelIdentifier.cs b/shesha-core/src/Shesha.Core/Domain/NotificationChannelIdentifier.cs new file mode 100644 index 0000000000..64a892de70 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/NotificationChannelIdentifier.cs @@ -0,0 +1,20 @@ +using System; + +namespace Shesha.Domain +{ + /// + /// Form identifier + /// + [Serializable] + public class NotificationChannelIdentifier : ConfigurationItemIdentifier, IIdentifierFactory + { + public NotificationChannelIdentifier(string? module, string name) : base(module, name) + { + } + + public static NotificationChannelIdentifier New(string? module, string name) + { + return new NotificationChannelIdentifier(module, name); + } + } +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationIdentifier.cs b/shesha-core/src/Shesha.Core/Domain/NotificationIdentifier.cs new file mode 100644 index 0000000000..40f5aef4af --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/NotificationIdentifier.cs @@ -0,0 +1,20 @@ +using System; + +namespace Shesha.Domain +{ + /// + /// Notification identifier + /// + [Serializable] + public class NotificationIdentifier : ConfigurationItemIdentifier, IIdentifierFactory + { + public NotificationIdentifier(string? module, string name) : base(module, name) + { + } + + public static NotificationIdentifier New(string? module, string name) + { + return new NotificationIdentifier(module, name); + } + } +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationMessage.cs b/shesha-core/src/Shesha.Core/Domain/NotificationMessage.cs index 31dc4cc66f..705e0afb78 100644 --- a/shesha-core/src/Shesha.Core/Domain/NotificationMessage.cs +++ b/shesha-core/src/Shesha.Core/Domain/NotificationMessage.cs @@ -3,6 +3,7 @@ using Shesha.Domain.Enums; using System; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Shesha.Domain { @@ -20,7 +21,7 @@ public class NotificationMessage : FullAuditedEntityWithExternalSync, IMay /// /// Send type (email/sms/push etc) /// - public virtual string RecipientText { get; set; } + public virtual string? RecipientText { get; set; } /// /// Sender text (email address/mobile number etc) /// @@ -29,7 +30,8 @@ public class NotificationMessage : FullAuditedEntityWithExternalSync, IMay /// /// CC emails /// - public virtual string Cc { get; set; } + [Column("cc")] + public virtual string? Cc { get; set; } /// /// /// @@ -61,7 +63,7 @@ public class NotificationMessage : FullAuditedEntityWithExternalSync, IMay /// /// /// - public virtual string ErrorMessage { get; set; } + public virtual string? ErrorMessage { get; set; } /// /// /// diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationTemplate.cs b/shesha-core/src/Shesha.Core/Domain/NotificationTemplate.cs index 130b460c87..d5680bf0d8 100644 --- a/shesha-core/src/Shesha.Core/Domain/NotificationTemplate.cs +++ b/shesha-core/src/Shesha.Core/Domain/NotificationTemplate.cs @@ -5,17 +5,15 @@ using Shesha.Domain.Enums; using Shesha.Extensions; using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; namespace Shesha.Domain { [Entity(TypeShortAlias = "Shesha.Core.NotificationTemplate")] - public class NotificationTemplate : FullAuditedEntity + public class NotificationTemplate : FullAuditedEntity, INotificationTemplateProps { public virtual RefListNotificationMessageFormat? MessageFormat { get; set; } public virtual NotificationTypeConfig PartOf { get; set; } @@ -23,6 +21,16 @@ public class NotificationTemplate : FullAuditedEntity public virtual string TitleTemplate { get; set; } [StringLength(int.MaxValue)] public virtual string BodyTemplate { get; set; } + + public NotificationTemplate Clone() + { + return new NotificationTemplate { + PartOf = PartOf, + MessageFormat = MessageFormat, + TitleTemplate = TitleTemplate, + BodyTemplate = BodyTemplate, + }; + } } public class NotificationTemplateValidator : AbstractValidator diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs b/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs index 78224c328d..3452005487 100644 --- a/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs +++ b/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs @@ -1,21 +1,18 @@ -using Microsoft.Extensions.Logging; -using Shesha.Domain.Attributes; +using Shesha.Domain.Attributes; using Shesha.Domain.ConfigurationItems; -using System; +using Shesha.Extensions; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using System.Text; using System.Text.Json; -using System.Threading.Tasks; namespace Shesha.Domain { [DiscriminatorValue(ItemTypeName)] [JoinedProperty("Core_NotificationTypeConfigs")] [Entity(TypeShortAlias = "Shesha.Domain.NotificationTypeConfig")] - public class NotificationTypeConfig: ConfigurationItemBase + public class NotificationTypeConfig : ConfigurationItemBase, INotificationTypeSpecificProps { public NotificationTypeConfig() { @@ -61,7 +58,7 @@ private void Init() /// Serialized JSON string representing override channels. /// [StringLength(int.MaxValue)] - public string OverrideChannels { get; set; } + public string? OverrideChannels { get; set; } private List _parsedOverrideChannels; @@ -77,13 +74,14 @@ public List ParsedOverrideChannels { try { - var jsonStrings = JsonSerializer.Deserialize>(OverrideChannels); + var jsonStrings = JsonSerializer.Deserialize>(OverrideChannels) ?? new(); _parsedOverrideChannels = jsonStrings .Select(json => JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })) + .WhereNotNull() .ToList(); } catch (JsonException) diff --git a/shesha-core/src/Shesha.Core/Domain/Person.cs b/shesha-core/src/Shesha.Core/Domain/Person.cs index 03e95292ce..db7ff04f5a 100644 --- a/shesha-core/src/Shesha.Core/Domain/Person.cs +++ b/shesha-core/src/Shesha.Core/Domain/Person.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Abp.Auditing; +using Abp.Auditing; using Abp.Localization; using Abp.Timing; using JetBrains.Annotations; @@ -11,7 +7,10 @@ using Shesha.Domain.Enums; using Shesha.DynamicEntities; using Shesha.EntityHistory; -using Shesha.Extensions; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Shesha.Domain { @@ -26,58 +25,58 @@ public class Person : FullPowerEntity [StringLength(13)] [Display(Name = "Identity Number")] - public virtual string IdentityNumber { get; set; } + public virtual string? IdentityNumber { get; set; } public virtual RefListPersonTitle? Title { get; set; } [StringLength(50)] [Display(Name = "First Name")] [Audited] - public virtual string FirstName { get; set; } + public virtual string? FirstName { get; set; } [StringLength(50)] [Display(Name = "Last Name")] [Audited] - public virtual string LastName { get; set; } + public virtual string? LastName { get; set; } [Display(Name = "Middle Name")] [StringLength(50)] [Audited] - public virtual string MiddleName { get; set; } + public virtual string? MiddleName { get; set; } /// /// Initials override. If empty, the first letter of FirstName is taken. /// [StringLength(10), Display(Name = "Initials")] - public virtual string Initials { get; set; } + public virtual string? Initials { get; set; } /// /// Custom short name (overrides calculated short name) /// [StringLength(60)] [Display(Name = "Custom Short Name")] - public virtual string CustomShortName { get; set; } + public virtual string? CustomShortName { get; set; } [StringLength(20)] - public virtual string HomeNumber { get; set; } + public virtual string? HomeNumber { get; set; } [StringLength(20)] [Display(Name = "Mobile Number")] [Audited] - public virtual string MobileNumber1 { get; set; } + public virtual string? MobileNumber1 { get; set; } [StringLength(20)] [Display(Name = "Alternate Mobile Number")] - public virtual string MobileNumber2 { get; set; } + public virtual string? MobileNumber2 { get; set; } [StringLength(100), EmailAddress] [Display(Name = "Email Address")] [Audited] - public virtual string EmailAddress1 { get; set; } + public virtual string? EmailAddress1 { get; set; } [StringLength(100), EmailAddress] [Display(Name = "Alternative Email Address")] - public virtual string EmailAddress2 { get; set; } + public virtual string? EmailAddress2 { get; set; } [Past] [Audited] @@ -96,7 +95,7 @@ public class Person : FullPowerEntity /// [EntityDisplayName] [ReadonlyProperty] - public virtual string FullName { get; protected set; } + public virtual string? FullName { get; protected set; } /// /// @@ -115,9 +114,9 @@ public class Person : FullPowerEntity /// [CanBeNull] [CascadeUpdateRules(false, true)] - public virtual User User { get; set; } + public virtual User? User { get; set; } - public override string ToString() + public override string? ToString() { return FullName; } diff --git a/shesha-core/src/Shesha.Core/Domain/RoleAppointmentTypeConfig.cs b/shesha-core/src/Shesha.Core/Domain/RoleAppointmentTypeConfig.cs new file mode 100644 index 0000000000..1c9d9e730b --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/RoleAppointmentTypeConfig.cs @@ -0,0 +1,35 @@ +using Shesha.Domain.Attributes; +using Shesha.Domain.ConfigurationItems; +using Shesha.Domain.Enums; + +namespace Shesha.Domain +{ + [DiscriminatorValue(RoleAppointmentTypeName)] + [JoinedProperty("Core_RoleAppointmentTypeConfigs")] + [Entity(TypeShortAlias = "Shesha.Core.RoleAppointmentTypeConfig")] + [Prefix(UsePrefixes = false)] + public class RoleAppointmentTypeConfig : ConfigurationItemBase + { + public RoleAppointmentTypeConfig() + { + Init(); + } + + private void Init() + { + VersionStatus = ConfigurationItemVersionStatus.Draft; + } + + public const string RoleAppointmentTypeName = "role-appointment-type"; + public override string ItemType => RoleAppointmentTypeName; + public virtual EntityConfig PermissionedEntity1Type { get; set; } + public virtual RefListPermissionedEntityIsRequired PermissionedEntity1IsRequired { get; set; } + public virtual EntityConfig PermissionedEntity2Type { get; set; } + public virtual RefListPermissionedEntityIsRequired PermissionedEntity2IsRequired { get; set; } + public virtual EntityConfig PermissionedEntity3Type { get; set; } + public virtual RefListPermissionedEntityIsRequired PermissionedEntity3IsRequired { get; set; } + public virtual FormConfiguration RoleAppointmentCreateForm { get; set; } + public virtual FormConfiguration RoleAppointmentDetailsForm { get; set; } + public virtual FormConfiguration RoleAppointmentListItemForm { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/SecurityQuestion.cs b/shesha-core/src/Shesha.Core/Domain/SecurityQuestion.cs index 93b3855c56..50f0c0d6f3 100644 --- a/shesha-core/src/Shesha.Core/Domain/SecurityQuestion.cs +++ b/shesha-core/src/Shesha.Core/Domain/SecurityQuestion.cs @@ -1,11 +1,7 @@ using Abp.Domain.Entities.Auditing; using Shesha.Domain.Attributes; using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Domain { diff --git a/shesha-core/src/Shesha.Core/Domain/ShaRole.cs b/shesha-core/src/Shesha.Core/Domain/ShaRole.cs index ea6be3d531..fc1c262940 100644 --- a/shesha-core/src/Shesha.Core/Domain/ShaRole.cs +++ b/shesha-core/src/Shesha.Core/Domain/ShaRole.cs @@ -31,7 +31,9 @@ public ShaRole() [StringLength(2000)] [Audited] - public override string Description { get; set; } + public override string? Description { get; set; } + + public virtual RoleAppointmentTypeConfig RoleAppointmentType { get; set; } [Obsolete] public virtual int SortIndex { get; set; } diff --git a/shesha-core/src/Shesha.Core/Domain/ShaRoleAppointedPerson.cs b/shesha-core/src/Shesha.Core/Domain/ShaRoleAppointedPerson.cs index e62072d90b..719fe433ca 100644 --- a/shesha-core/src/Shesha.Core/Domain/ShaRoleAppointedPerson.cs +++ b/shesha-core/src/Shesha.Core/Domain/ShaRoleAppointedPerson.cs @@ -7,6 +7,6 @@ namespace Shesha.Domain [DiscriminatorValue((int)ShaRoleAppointmentType.Person)] public class ShaRoleAppointedPerson : ShaRoleAppointment { - public virtual Person Person { get; set; } + public virtual Person? Person { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Core/Domain/ShaRoleAppointment.cs b/shesha-core/src/Shesha.Core/Domain/ShaRoleAppointment.cs index 7b34b8fd59..901603e44b 100644 --- a/shesha-core/src/Shesha.Core/Domain/ShaRoleAppointment.cs +++ b/shesha-core/src/Shesha.Core/Domain/ShaRoleAppointment.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using Abp.Domain.Entities; +using Abp.Domain.Entities; using Abp.Domain.Entities.Auditing; using Shesha.Domain.Attributes; using Shesha.EntityReferences; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; namespace Shesha.Domain { @@ -13,7 +12,7 @@ namespace Shesha.Domain [Discriminator] public class ShaRoleAppointment : FullAuditedEntity, IMayHaveTenant { - public virtual ShaRole Role { get; set; } + public virtual ShaRole? Role { get; set; } public virtual int? TenantId { get; set; } [EntityReference(true)] diff --git a/shesha-core/src/Shesha.Core/EntityHistory/EntityHistoryItemDto.cs b/shesha-core/src/Shesha.Core/EntityHistory/EntityHistoryItemDto.cs index aa1d250fd6..5f5758e643 100644 --- a/shesha-core/src/Shesha.Core/EntityHistory/EntityHistoryItemDto.cs +++ b/shesha-core/src/Shesha.Core/EntityHistory/EntityHistoryItemDto.cs @@ -6,34 +6,24 @@ namespace Shesha.EntityHistory { public class EntityHistoryItemDto : EntityDto { - //EntityCahgeSetId - //public virtual EntityChangeSet EntityChangeSet { get; set; } - [Display(Name = "Date")] public DateTime? CreationTime { get; set; } - //EntityChangeId - //public virtual EntityChange EntityChange { get; set; } - public int? HistoryItemType { get; set; } - public string EntityId { get; set; } + public string? EntityId { get; set; } - public string EntityTypeFullName { get; set; } + public string? EntityTypeFullName { get; set; } - public string EventType { get; set; } + public string? EventType { get; set; } - //public string EventName { get; set; } - - //public string Description { get; set; } - [Display(Name = "User")] - public string UserFullName { get; set; } + public string? UserFullName { get; set; } [Display(Name = "Type of event")] - public string EventText { get; set; } + public string? EventText { get; set; } [Display(Name = "Description")] - public string ExtendedDescription { get; set; } + public string? ExtendedDescription { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Core/EntityHistory/EntityHistoryProvider.cs b/shesha-core/src/Shesha.Core/EntityHistory/EntityHistoryProvider.cs index eb599964f3..3e73c5f78f 100644 --- a/shesha-core/src/Shesha.Core/EntityHistory/EntityHistoryProvider.cs +++ b/shesha-core/src/Shesha.Core/EntityHistory/EntityHistoryProvider.cs @@ -78,7 +78,7 @@ private async Task> GetAllModelsAsync() return _models = models.Where(x => !x.Suppress).ToList(); } - private async Task GetContainerTypeAsync(string container) + private async Task GetContainerTypeAsync(string container) { var allModels = await GetAllModelsAsync(); var models = allModels.Where(m => m.Alias == container || m.ClassName == container).ToList(); @@ -91,52 +91,49 @@ private async Task GetContainerTypeAsync(string container) public async Task> GetAuditTrailAsync(string entityId, string entityTypeFullName, bool includeEventsOnChildEntities) { - // disable SoftDeleteFilter to allow get deleted entities - _unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete); - - var itemType = await GetContainerTypeAsync(entityTypeFullName); - - var history = new List(); - - var (audit, maxDate) = await GetEntityAuditAsync(itemType, entityId); - - // Add entity history - history.AddRange(audit); - - if (itemType != null) - { - // Add many-to-many related entities - history.AddRange(await GetManyToManyEntitiesAuditAsync(itemType, entityId)); - - // Add many-to-one related entities - history.AddRange(await GetManyToOneEntitiesAuditAsync(itemType, entityId)); - - // Add child audited properties - if (includeEventsOnChildEntities) - history.AddRange(await GetChildEntitiesAuditAsync(itemType, entityId)); - - // Add generic child entities - history.AddRange(GetGenericEntitiesAudit(itemType, entityId)); - - if (maxDate != DateTime.MaxValue) - { - history = history.Where(x => x.CreationTime <= maxDate).ToList(); + using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete)) + { + var itemType = await GetContainerTypeAsync(entityTypeFullName); + + var history = new List(); + + var (audit, maxDate) = await GetEntityAuditAsync(itemType, entityId); + + // Add entity history + history.AddRange(audit); + + if (itemType != null) + { + // Add many-to-many related entities + history.AddRange(await GetManyToManyEntitiesAuditAsync(itemType, entityId)); + + // Add many-to-one related entities + history.AddRange(await GetManyToOneEntitiesAuditAsync(itemType, entityId)); + + // Add child audited properties + if (includeEventsOnChildEntities) + history.AddRange(await GetChildEntitiesAuditAsync(itemType, entityId)); + + // Add generic child entities + history.AddRange(GetGenericEntitiesAudit(itemType, entityId)); + + if (maxDate != DateTime.MaxValue) + { + history = history.Where(x => x.CreationTime <= maxDate).ToList(); + } } - } - - _unitOfWorkManager.Current.EnableFilter(AbpDataFilters.SoftDelete); - - history = history.OrderBy(x => x.CreationTime).ToList(); - - return history; + history = history.OrderBy(x => x.CreationTime).ToList(); + + return history; + } } private async Task> GetChildEntitiesAuditAsync(Type itemType, string entityId) { var list = new List(); // Check if child audited properties should be displayed - var childAuditedProperties = itemType?.GetProperties() + var childAuditedProperties = itemType.GetProperties() .Where(p => p.GetCustomAttribute() != null).ToList(); if (childAuditedProperties?.Any() ?? false) @@ -147,7 +144,7 @@ private async Task> GetChildEntitiesAuditAsync(Type i var childItem = childAuditedProperty.GetValue(item); if (childItem != null) { - var attr = childAuditedProperty.GetCustomAttribute(); + var attr = childAuditedProperty.GetCustomAttribute().NotNull(); var propDisplayName = childAuditedProperty.GetCustomAttribute()?.Name ?? childAuditedProperty.Name.ToFriendlyName(); var childType = childItem.GetType(); @@ -170,13 +167,7 @@ private async Task> GetChildEntitiesAuditAsync(Type i return list; } - /*private async Task> GetEntityAuditAsync(Type entityType, string entityId, string childName = "", string[] fields = null) - { - var fakeDate = DateTime.MaxValue; - return await GetEntityAuditAsync(entityType, entityId, out fakeDate, childName, fields); - }*/ - - private async Task<(List, DateTime)> GetEntityAuditAsync(Type entityType, string entityId, string childName = "", string[] fields = null) + private async Task<(List, DateTime)> GetEntityAuditAsync(Type? entityType, string? entityId, string childName = "", string[]? fields = null) { var maxDateTime = DateTime.MaxValue; @@ -258,13 +249,13 @@ private async Task> GetChildEntitiesAuditAsync(Type i || x.EventType == EntityHistoryCommonEventTypes.PROPERTY_CHANGE_USER_TEXT); if (propAsDescription != null) { - propsDescr.Add(propAsDescription.Description); + propsDescr.Add(propAsDescription.Description ?? string.Empty); continue; } - var prop_ = entityType?.GetProperty(propertyChange.PropertyName); - var propName = prop_ != null - ? ReflectionHelper.GetDisplayName(entityType.GetProperty(propertyChange.PropertyName)) ?? propertyChange.PropertyName + var property = entityType?.GetProperty(propertyChange.PropertyName); + var propName = property != null + ? ReflectionHelper.GetDisplayName(property) ?? propertyChange.PropertyName : propertyChange.PropertyName; propDescription = @@ -408,14 +399,14 @@ private async Task> GetManyToManyEntitiesAuditAsync(T var action = Expression.Equal(idExpression, Expression.Constant(Guid.Parse(entityId))); var lambda = Expression.Lambda(action, parameterExpression); - var res = _dynamicRepository.Where(manyToManyType, lambda).ToDynamicList(); + var res = await _dynamicRepository.Where(manyToManyType, lambda).ToDynamicListAsync(); var childItems = res .Select(x => new Relation() { Id = manyToManyType.GetProperty("Id")?.GetValue(x)?.ToString(), - RelatedObject = x as IFullAudited, + RelatedObject = x.ForceCast(), InnerObject = manyToManyType.GetProperty(attr.RelatedEntityField)?.GetValue(x) as IFullAudited, }) .Select(x => @@ -424,13 +415,13 @@ private async Task> GetManyToManyEntitiesAuditAsync(T x.Name = GetEntityName(x.InnerObject, relatedNameField); return x; }) - .ToList(); - + .ToList(); + var userIds = childItems.Select(x => x.RelatedObject.CreatorUserId).ToList(); userIds.AddRange(childItems.Select(x => x.RelatedObject.DeleterUserId)); - userIds.AddRange(childItems.Select(x => x.InnerObject.DeleterUserId)); + userIds.AddRange(childItems.Select(x => x.InnerObject?.DeleterUserId)); userIds = userIds.Distinct().Where(x => x != null).ToList(); - var persons = _personRepository.GetAll().Where(x => userIds.Contains(x.User.Id)).ToList(); + var persons = await _personRepository.GetAll().Where(x => x.User != null && userIds.Contains(x.User.Id)).ToListAsync(); foreach (var childItem in childItems) { @@ -462,7 +453,7 @@ private async Task> GetManyToManyEntitiesAuditAsync(T } else { - if (childItem.InnerObject.IsDeleted) + if (childItem.InnerObject?.IsDeleted ?? false) { var deletedBy = GetPersonByUserIdInternal(persons, childItem.InnerObject.DeleterUserId); list.Add(new EntityHistoryItemDto() @@ -593,14 +584,14 @@ private async Task> GetManyToOneEntitiesAuditAsync(Ty var action = Expression.Equal(idExpression, Expression.Constant(Guid.Parse(entityId))); var lambda = Expression.Lambda(action, parameterExpression); - var res = _dynamicRepository.Where(manyToOneType, lambda).ToDynamicList(); + var res = await _dynamicRepository.Where(manyToOneType, lambda).ToDynamicListAsync(); var childItems = res .Select(x => new Relation() { Id = manyToOneType.GetProperty("Id")?.GetValue(x)?.ToString(), - RelatedObject = x as IFullAudited, + RelatedObject = x.ForceCast(), Name = GetEntityName(x, relatedNameField) }) .ToList(); @@ -608,7 +599,7 @@ private async Task> GetManyToOneEntitiesAuditAsync(Ty var userIds = childItems.Select(x => x.RelatedObject.CreatorUserId).ToList(); userIds.AddRange(childItems.Select(x => x.RelatedObject.DeleterUserId)); userIds = userIds.Distinct().Where(x => x != null).ToList(); - var persons = _personRepository.GetAll().Where(x => userIds.Contains(x.User.Id)).ToList(); + var persons = await _personRepository.GetAll().Where(x => x.User != null && userIds.Contains(x.User.Id)).ToListAsync(); foreach (var childItem in childItems) { @@ -681,7 +672,7 @@ private List GetAuditedAsEvents(string entityTypeFullName, return events.Select(x => { - var ecs = _entityChangeSetRepository.Get(x.EntityChange.EntityChangeSetId); + var ecs = _entityChangeSetRepository.Get(x.EntityChange.NotNull().EntityChangeSetId); return new EntityHistoryItemDto() { CreationTime = x.EntityChange.ChangeTime, @@ -703,7 +694,7 @@ private List GetGenericEntitiesAudit(Type itemType, string var history = new List(); if (!attrs.Any()) return history; - var typeShortAlias = itemType.GetCustomAttribute()?.TypeShortAlias ?? itemType.FullName; + var typeShortAlias = itemType.GetCustomAttribute()?.TypeShortAlias ?? ""; foreach (var attr in attrs) { @@ -729,9 +720,12 @@ private List GetGenericEntitiesAudit(Type itemType, string var parameterExpression = Expression.Parameter(childType, "x"); var memberExpression1 = Expression.PropertyOrField(parameterExpression, ownerIdField); var action1 = Expression.Equal(memberExpression1, Expression.Constant(Guid.Parse(entityId))); - var memberExpression2 = Expression.PropertyOrField(parameterExpression, ownerTypeField); - var action2 = Expression.Equal(memberExpression2, Expression.Constant(Guid.Parse(typeShortAlias))); - var andExpression = Expression.And(action1, action2); + var memberExpression2 = Expression.PropertyOrField(parameterExpression, ownerTypeField); + var action2 = Expression.Equal(memberExpression2, Expression.Constant(typeShortAlias)); + var memberExpression3 = Expression.PropertyOrField(parameterExpression, ownerTypeField); + var action3 = Expression.Equal(memberExpression3, Expression.Constant(itemType.FullName)); + var orExpression = Expression.Or(action2, action3); + var andExpression = Expression.And(action1, orExpression); var lambda = Expression.Lambda(andExpression, parameterExpression); var res = _dynamicRepository.Where(childType, lambda).ToDynamicList(); @@ -741,7 +735,7 @@ private List GetGenericEntitiesAudit(Type itemType, string new Relation() { Id = childType.GetProperty("Id")?.GetValue(x)?.ToString(), - RelatedObject = x as IFullAudited, + RelatedObject = x.ForceCast(), Name = GetEntityName(x, childNameField) }) .ToList(); @@ -749,7 +743,7 @@ private List GetGenericEntitiesAudit(Type itemType, string var userIds = childItems.Select(x => x.RelatedObject.CreatorUserId).ToList(); userIds.AddRange(childItems.Select(x => x.RelatedObject.DeleterUserId)); userIds = userIds.Distinct().Where(x => x != null).ToList(); - var persons = _personRepository.GetAll().Where(x => userIds.Contains(x.User.Id)).ToList(); + var persons = _personRepository.GetAll().Where(x => x.User != null && userIds.Contains(x.User.Id)).ToList(); foreach (var childItem in childItems) { @@ -785,27 +779,30 @@ private List GetGenericEntitiesAudit(Type itemType, string return history; } - private string GetEntityName(object entity, string fieldName) + private string GetEntityName(object? entity, string fieldName) { - if (entity == null) return ""; + if (entity == null) return string.Empty; + + if (string.IsNullOrEmpty(fieldName)) + return entity.ToString() ?? string.Empty; var type = entity.GetType(); - return string.IsNullOrEmpty(fieldName) - ? entity.ToString() - : string.IsNullOrEmpty(type.GetProperty(fieldName)?.GetValue(entity)?.ToString()) - ? entity.ToString() - : type.GetProperty(fieldName)?.GetValue(entity)?.ToString(); + + var propValue = type.GetProperty(fieldName)?.GetValue(entity); + var propText = propValue?.ToString(); + return !string.IsNullOrEmpty(propText) + ? propText + : string.Empty; } - private Person GetPersonByUserIdInternal(IList list, long? userId) + private Person? GetPersonByUserIdInternal(IList list, long? userId) { return userId != null ? list.FirstOrDefault(x => x.User != null && x.User.Id == userId) : null; - } - private Person GetPersonByUserId(long? userId) + private Person? GetPersonByUserId(long? userId) { return userId != null ? _personRepository.GetAll().FirstOrDefault(x => x.User != null && x.User.Id == userId) @@ -815,11 +812,11 @@ private Person GetPersonByUserId(long? userId) private class Relation { - public string Id { get; set; } + public string? Id { get; set; } public IFullAudited RelatedObject { get; set; } - public IFullAudited InnerObject { get; set; } - public string InnerObjectId { get; set; } + public IFullAudited? InnerObject { get; set; } + public string? InnerObjectId { get; set; } public string Name { get; set; } } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Core/Extensions/DomainServiceExtensions.cs b/shesha-core/src/Shesha.Core/Extensions/DomainServiceExtensions.cs index cc709f1c2f..1694ae1669 100644 --- a/shesha-core/src/Shesha.Core/Extensions/DomainServiceExtensions.cs +++ b/shesha-core/src/Shesha.Core/Extensions/DomainServiceExtensions.cs @@ -1,15 +1,15 @@ -using Abp.Dependency; -using Abp.Domain.Entities; +using Abp.Domain.Entities; using Abp.Domain.Repositories; using Abp.Domain.Services; using Abp.Runtime.Session; using Abp.UI; using FluentValidation; +using Shesha.Extensions; +using Shesha.Reflection; using Shesha.Services; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Security.Cryptography; using System.Threading.Tasks; using FluentValidationResult = FluentValidation.Results.ValidationResult; @@ -20,9 +20,9 @@ public static class DomainServiceExtensions public static async Task GetCurrentPersonAsync(this DomainService service) { - var personRepository = IocManager.Instance.Resolve>(); + var personRepository = StaticContext.IocManager.Resolve>(); var _session = StaticContext.IocManager.Resolve(); - var person = await personRepository.FirstOrDefaultAsync(p => p.User.Id == _session.GetUserId()); + var person = await personRepository.FirstOrDefaultAsync(p => p.User != null && p.User.Id == _session.GetUserId()); return person; } @@ -34,7 +34,7 @@ public static async Task GetCurrentPersonAsync(this DomainService servic /// Id of the entity /// Throw exception if entity not found /// - public static async Task GetEntityAsync(this DomainService service, Guid id, bool throwException = true) where T : class, IEntity + public static async Task GetEntityAsync(this DomainService service, Guid id, bool throwException = true) where T : class, IEntity { return await GetEntityAsync(service, id, throwException); } @@ -48,11 +48,14 @@ public static async Task GetEntityAsync(this DomainService service, Guid i /// Id of the entity /// Throw exception if entity not found /// - public static async Task GetEntityAsync(this DomainService service, TId id, bool throwException = true) where T : class, IEntity + public static async Task GetEntityAsync(this DomainService service, TId id, bool throwException = true) where T : class, IEntity { + var stringId = id?.ToString(); + ArgumentException.ThrowIfNullOrWhiteSpace(stringId, nameof(id)); + var dynamicRepo = StaticContext.IocManager.Resolve(); - var item = await dynamicRepo.GetAsync(typeof(T), id.ToString()); + var item = await dynamicRepo.GetAsync(typeof(T), stringId); if (item != null) return (T)item; @@ -73,11 +76,10 @@ public static async Task GetEntityAsync(this DomainService service, T /// /// public static async Task SaveOrUpdateEntityAsync(this DomainService service, TId? id, Func action) where T : class, IEntity where TId: struct - { - var isNew = id == null; - var item = !isNew - ? await GetEntityAsync(service, id.Value) - : (T)Activator.CreateInstance(typeof(T)); + { + var item = id != null + ? (await GetEntityAsync(service, id.Value)).NotNull() + : ActivatorHelper.CreateNotNullInstance(); await action.Invoke(item); diff --git a/shesha-core/src/Shesha.Core/Extensions/PersonExtensions.cs b/shesha-core/src/Shesha.Core/Extensions/PersonExtensions.cs index c7bf9a7bd8..e7da2af921 100644 --- a/shesha-core/src/Shesha.Core/Extensions/PersonExtensions.cs +++ b/shesha-core/src/Shesha.Core/Extensions/PersonExtensions.cs @@ -13,9 +13,9 @@ public static class PersonExtensions /// /// Person /// - public static string GetEmail(this Person person) + public static string? GetEmail(this Person person) { - return StringHelper.FirstNotEmpty(person?.EmailAddress1, person?.EmailAddress2); + return StringHelper.FirstNotEmpty(person.EmailAddress1, person.EmailAddress2); } /// @@ -23,9 +23,9 @@ public static string GetEmail(this Person person) /// /// Person /// - public static string GetMobileNumber(this Person person) + public static string? GetMobileNumber(this Person person) { - return StringHelper.FirstNotEmpty(person?.MobileNumber1, person?.MobileNumber2); + return StringHelper.FirstNotEmpty(person.MobileNumber1, person.MobileNumber2); } /// diff --git a/shesha-core/src/Shesha.Core/Localization/SourceFiles/Shesha.xml b/shesha-core/src/Shesha.Core/Localization/SourceFiles/Shesha.xml index 877b461129..b426f53bce 100644 --- a/shesha-core/src/Shesha.Core/Localization/SourceFiles/Shesha.xml +++ b/shesha-core/src/Shesha.Core/Localization/SourceFiles/Shesha.xml @@ -118,5 +118,8 @@ Use Proxy Use AD Proxy Proxy Address + + Please correct the errors and try again + Something went wrong diff --git a/shesha-core/src/Shesha.Core/Migrations/M20240711103300.cs b/shesha-core/src/Shesha.Core/Migrations/M20240711103300.cs index 1d5f6fe706..81cec5d5e3 100644 --- a/shesha-core/src/Shesha.Core/Migrations/M20240711103300.cs +++ b/shesha-core/src/Shesha.Core/Migrations/M20240711103300.cs @@ -1,12 +1,6 @@ using FluentMigrator; -using NUglify; using Shesha.FluentMigrator; using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Migrations { diff --git a/shesha-core/src/Shesha.Core/Migrations/M20240725180100.cs b/shesha-core/src/Shesha.Core/Migrations/M20240725180100.cs index 2f644c8f46..1cf67aa647 100644 --- a/shesha-core/src/Shesha.Core/Migrations/M20240725180100.cs +++ b/shesha-core/src/Shesha.Core/Migrations/M20240725180100.cs @@ -1,12 +1,5 @@ using FluentMigrator; -using NUglify; -using Shesha.FluentMigrator; using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Migrations { diff --git a/shesha-core/src/Shesha.Core/Migrations/M20240726090600.cs b/shesha-core/src/Shesha.Core/Migrations/M20240726090600.cs index 6d080a3b2f..5ce0aaa35a 100644 --- a/shesha-core/src/Shesha.Core/Migrations/M20240726090600.cs +++ b/shesha-core/src/Shesha.Core/Migrations/M20240726090600.cs @@ -1,12 +1,5 @@ using FluentMigrator; -using NUglify; -using Shesha.FluentMigrator; using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Migrations { diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241203150000.cs b/shesha-core/src/Shesha.Core/Migrations/M20241203150000.cs index 820660195b..43e31f9e4b 100644 --- a/shesha-core/src/Shesha.Core/Migrations/M20241203150000.cs +++ b/shesha-core/src/Shesha.Core/Migrations/M20241203150000.cs @@ -1,13 +1,6 @@ using FluentMigrator; -using NUglify; using Shesha.FluentMigrator; -using StackExchange.Redis; using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Migrations { diff --git a/shesha-core/src/Shesha.Core/Migrations/M20250120095800.cs b/shesha-core/src/Shesha.Core/Migrations/M20250120095800.cs index f020497bf1..c3ef39cd7b 100644 --- a/shesha-core/src/Shesha.Core/Migrations/M20250120095800.cs +++ b/shesha-core/src/Shesha.Core/Migrations/M20250120095800.cs @@ -1,10 +1,4 @@ using FluentMigrator; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Shesha.FluentMigrator; namespace Shesha.Migrations { diff --git a/shesha-core/src/Shesha.Core/Migrations/M20250123114500.cs b/shesha-core/src/Shesha.Core/Migrations/M20250123114500.cs index 5e3b68baf6..5db4d89bf1 100644 --- a/shesha-core/src/Shesha.Core/Migrations/M20250123114500.cs +++ b/shesha-core/src/Shesha.Core/Migrations/M20250123114500.cs @@ -1,10 +1,4 @@ using FluentMigrator; -using System; -//using Shesha.FluentMigrator; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Migrations { diff --git a/shesha-core/src/Shesha.Core/Migrations/M20250408152599.cs b/shesha-core/src/Shesha.Core/Migrations/M20250408152599.cs new file mode 100644 index 0000000000..31dc6462ea --- /dev/null +++ b/shesha-core/src/Shesha.Core/Migrations/M20250408152599.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using Shesha.FluentMigrator; + +namespace Shesha.Migrations +{ + [Migration(20250408152599)] + public class M20250408152599 : OneWayMigration + { + public override void Up() + { + Alter.Table("Core_Notifications").AddColumn("Category").AsString(50).NotNullable().SetExistingRowsTo("").WithDefaultValue(""); + } + } +} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20250508144200.cs b/shesha-core/src/Shesha.Core/Migrations/M20250508144200.cs new file mode 100644 index 0000000000..102617be54 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Migrations/M20250508144200.cs @@ -0,0 +1,29 @@ +using FluentMigrator; +using Shesha.FluentMigrator; + +namespace Shesha.Migrations +{ + [Migration(20250508144200)] + public class M20250508144200 : AutoReversingMigration + { + public override void Up() + { + Create.Table("Core_RoleAppointmentTypeConfigs") + .WithIdAsGuid() + .WithColumn("PermissionedEntity1IsRequiredLkp").AsInt64().Nullable() + .WithColumn("PermissionedEntity2IsRequiredLkp").AsInt64().Nullable() + .WithColumn("PermissionedEntity3IsRequiredLkp").AsInt64().Nullable(); + + Alter.Table("Core_RoleAppointmentTypeConfigs") + .AddForeignKeyColumn("RoleAppointmentCreateFormId", "Frwk_FormConfigurations") + .AddForeignKeyColumn("RoleAppointmentDetailsFormId", "Frwk_FormConfigurations") + .AddForeignKeyColumn("RoleAppointmentListItemFormId", "Frwk_FormConfigurations") + .AddForeignKeyColumn("PermissionedEntity1TypeId", "Frwk_EntityConfigs") + .AddForeignKeyColumn("PermissionedEntity2TypeId", "Frwk_EntityConfigs") + .AddForeignKeyColumn("PermissionedEntity3TypeId", "Frwk_EntityConfigs"); + + Alter.Table("Core_ShaRoles") + .AddForeignKeyColumn("RoleAppointmentTypeId", "Core_RoleAppointmentTypeConfigs"); + } + } +} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20250527023700.cs b/shesha-core/src/Shesha.Core/Migrations/M20250527023700.cs new file mode 100644 index 0000000000..4dbc06f814 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Migrations/M20250527023700.cs @@ -0,0 +1,24 @@ +using FluentMigrator; +using Shesha.FluentMigrator; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Migrations +{ + [Migration(20250527023700)] + public class M20250527023700 : OneWayMigration + { + public override void Up() + { + Alter.Table("Core_Notes") + .AlterColumn("CategoryLkp") + .AsString(50) + .Nullable(); + + Rename.Column("CategoryLkp").OnTable("Core_Notes").To("Category"); + } + } +} diff --git a/shesha-core/src/Shesha.Core/Roles/Distribution/ShaRoleImport.cs b/shesha-core/src/Shesha.Core/Roles/Distribution/ShaRoleImport.cs index 8e3620867a..346506d523 100644 --- a/shesha-core/src/Shesha.Core/Roles/Distribution/ShaRoleImport.cs +++ b/shesha-core/src/Shesha.Core/Roles/Distribution/ShaRoleImport.cs @@ -1,21 +1,17 @@ using Abp.Dependency; using Abp.Domain.Repositories; -using Newtonsoft.Json; -using Shesha.Authorization; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; -using Shesha.Permissions.Distribution.Dto; using Shesha.Roles.Distribution.Dto; using Shesha.Services.ConfigurationItems; using System; -using System.IO; using System.Threading.Tasks; namespace Shesha.DynamicEntities.Distribution { /// inheritedDoc - public class ShaRoleImport : ConfigurationItemImportBase, IShaRoleImport, ITransientDependency + public class ShaRoleImport : ConfigurationItemImportBase, IShaRoleImport, ITransientDependency { private readonly IRepository _roleRepo; private readonly IRepository _rolePermissionRepo; @@ -51,7 +47,7 @@ protected async Task ImportAsync(DistributedShaRole item, // get DB config var dbItem = await _roleRepo.FirstOrDefaultAsync(x => x.Name == item.Name && x.NameSpace == item.NameSpace - && (x.Module == null && item.ModuleName == null || x.Module.Name == item.ModuleName) + && (x.Module == null && item.ModuleName == null || x.Module != null && x.Module.Name == item.ModuleName) && x.IsLast); if (dbItem != null) @@ -127,14 +123,5 @@ protected async Task MapConfigAsync(DistributedShaRole item, ShaRole db return dbItem; } - - public async Task ReadFromJsonAsync(Stream jsonStream) - { - using (var reader = new StreamReader(jsonStream)) - { - var json = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(json); - } - } } } diff --git a/shesha-core/src/Shesha.Core/Shesha.Core.csproj b/shesha-core/src/Shesha.Core/Shesha.Core.csproj index 2ae9d27566..9931765e88 100644 --- a/shesha-core/src/Shesha.Core/Shesha.Core.csproj +++ b/shesha-core/src/Shesha.Core/Shesha.Core.csproj @@ -41,14 +41,22 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.Core/Validation/ValidationHelper.cs b/shesha-core/src/Shesha.Core/Validation/ValidationHelper.cs index ab6d7e64f3..02c454c0b4 100644 --- a/shesha-core/src/Shesha.Core/Validation/ValidationHelper.cs +++ b/shesha-core/src/Shesha.Core/Validation/ValidationHelper.cs @@ -1,5 +1,4 @@ using System.Text.RegularExpressions; -using Abp.Extensions; namespace Shesha.Validation { @@ -9,10 +8,8 @@ public static class ValidationHelper public static bool IsEmail(string value) { - if (value.IsNullOrEmpty()) - { + if (string.IsNullOrWhiteSpace(value)) return false; - } var regex = new Regex(EmailRegex); return regex.IsMatch(value); diff --git a/shesha-core/src/Shesha.Core/Web/AppContextHelper.cs b/shesha-core/src/Shesha.Core/Web/AppContextHelper.cs index e89e6fd036..3a9543a88d 100644 --- a/shesha-core/src/Shesha.Core/Web/AppContextHelper.cs +++ b/shesha-core/src/Shesha.Core/Web/AppContextHelper.cs @@ -11,6 +11,6 @@ public static void Configure(IHttpContextAccessor httpContextAccessor) _httpContextAccessor = httpContextAccessor; } - public static HttpContext Current => _httpContextAccessor?.HttpContext; + public static HttpContext? Current => _httpContextAccessor?.HttpContext; } } diff --git a/shesha-core/src/Shesha.Elmah/Elmah/Domain/ReferencedError.cs b/shesha-core/src/Shesha.Elmah/Elmah/Domain/ReferencedError.cs index cbc9dadd62..f582b40a9b 100644 --- a/shesha-core/src/Shesha.Elmah/Elmah/Domain/ReferencedError.cs +++ b/shesha-core/src/Shesha.Elmah/Elmah/Domain/ReferencedError.cs @@ -29,10 +29,10 @@ public class ReferencedError: Entity public virtual string Location { get; set; } [NotMapped] - public virtual string DetailsUrl { + public virtual string? DetailsUrl { get { var linkGeneratorContext = StaticContext.IocManager.Resolve(); - if (linkGeneratorContext?.State == null) + if (linkGeneratorContext?.State == null || string.IsNullOrWhiteSpace(linkGeneratorContext.State.Host)) return null; var hostString = linkGeneratorContext.State.Port > 0 diff --git a/shesha-core/src/Shesha.Elmah/Elmah/ExceptionExtensions.cs b/shesha-core/src/Shesha.Elmah/Elmah/ExceptionExtensions.cs index 60fba39843..1a0172b37f 100644 --- a/shesha-core/src/Shesha.Elmah/Elmah/ExceptionExtensions.cs +++ b/shesha-core/src/Shesha.Elmah/Elmah/ExceptionExtensions.cs @@ -17,8 +17,8 @@ public static class ExceptionExtensions /// public static bool IsExceptionLogged(this Exception ex) { - return ex.Data[ExceptionLoggedKey] != null && - (bool)ex.Data[ExceptionLoggedKey] == true; + var loggedFlag = ex.Data[ExceptionLoggedKey]; + return loggedFlag != null && (bool)loggedFlag == true; } /// @@ -41,8 +41,9 @@ public static void SetExceptionId(this Exception ex, Guid id) public static Guid? GetExceptionId(this Exception ex) { - return ex.Data[ExceptionIdKey] != null && ex.Data[ExceptionIdKey] is Guid - ? (Guid)ex.Data[ExceptionIdKey] + var exceptionIdValue = ex.Data[ExceptionIdKey]; + return exceptionIdValue != null && exceptionIdValue is Guid + ? (Guid)exceptionIdValue : (Guid?)null; } } diff --git a/shesha-core/src/Shesha.Elmah/Elmah/ExceptionWatchDog.cs b/shesha-core/src/Shesha.Elmah/Elmah/ExceptionWatchDog.cs index 54abb6dc84..c30a087a87 100644 --- a/shesha-core/src/Shesha.Elmah/Elmah/ExceptionWatchDog.cs +++ b/shesha-core/src/Shesha.Elmah/Elmah/ExceptionWatchDog.cs @@ -6,13 +6,13 @@ namespace Shesha.Elmah /// /// Exception watchdog /// - public class ExceptionWatchDog : IDisposable + public sealed class ExceptionWatchDog : IDisposable { private Action _onError; public Action CleanupAction { get; set; } private bool _disposed = false; - public string Location { get; set; } + public string? Location { get; set; } public ExceptionWatchDog(Action onError) { @@ -21,7 +21,7 @@ public ExceptionWatchDog(Action onError) AppDomain.CurrentDomain.FirstChanceException += OnException; } - private void OnException(object sender, FirstChanceExceptionEventArgs e) + private void OnException(object? sender, FirstChanceExceptionEventArgs e) { if (_disposed) return; diff --git a/shesha-core/src/Shesha.Elmah/Elmah/ILoggingContextCollector.cs b/shesha-core/src/Shesha.Elmah/Elmah/ILoggingContextCollector.cs index e19ea7b5a3..a36e79250a 100644 --- a/shesha-core/src/Shesha.Elmah/Elmah/ILoggingContextCollector.cs +++ b/shesha-core/src/Shesha.Elmah/Elmah/ILoggingContextCollector.cs @@ -8,7 +8,7 @@ public interface ILoggingContextCollector /// /// Make a watchdog for entity to link all unhandled exception to a specified entity /// - IDisposable MakeEntityWatchDog(IEntity entity); + IDisposable MakeEntityWatchDog(IEntity entity) where TId : notnull; IDisposable MakeWatchDog(string type, string id); IDisposable MakeWatchDog(ErrorReference errorRef); @@ -24,7 +24,7 @@ public interface ILoggingContextCollector /// /// /// - IDisposable BeginScope(Action initAction = null); + IDisposable BeginScope(Action? initAction = null); /// /// Specify location scope, is used to collect user-friendly locations of unhandled exceptions diff --git a/shesha-core/src/Shesha.Elmah/Elmah/LoggingContextCollector.cs b/shesha-core/src/Shesha.Elmah/Elmah/LoggingContextCollector.cs index 2385fa9bf6..61736ee935 100644 --- a/shesha-core/src/Shesha.Elmah/Elmah/LoggingContextCollector.cs +++ b/shesha-core/src/Shesha.Elmah/Elmah/LoggingContextCollector.cs @@ -22,7 +22,7 @@ public LoggingContextCollector(IAmbientScopeProvider scopeP public LoggingContextState CurrentState => _scopeProvider.GetValue(ScopeKey); - public IDisposable MakeEntityWatchDog(IEntity entity) + public IDisposable MakeEntityWatchDog(IEntity entity) where TId: notnull { var errorRef = WatchDogExtensions.GetEntityErrorRef(entity); return MakeWatchDog(errorRef.Type, errorRef.Id); @@ -75,7 +75,7 @@ public IDisposable MakeWatchDog(string type, string id) } /// inheritedDoc - public IDisposable BeginScope(Action initAction = null) + public IDisposable BeginScope(Action? initAction = null) { var state = new LoggingContextState(); initAction?.Invoke(state); diff --git a/shesha-core/src/Shesha.Elmah/Elmah/LoggingContextState.cs b/shesha-core/src/Shesha.Elmah/Elmah/LoggingContextState.cs index 68708d10d5..5de5bdfe6c 100644 --- a/shesha-core/src/Shesha.Elmah/Elmah/LoggingContextState.cs +++ b/shesha-core/src/Shesha.Elmah/Elmah/LoggingContextState.cs @@ -14,9 +14,9 @@ public class LoggingContextState public ConcurrentHashSet ActiveWatchDogs { get; set; } = new ConcurrentHashSet(); - public ExceptionWatchDog TopWatchDog => ActiveWatchDogs.LastOrDefault(); + public ExceptionWatchDog? TopWatchDog => ActiveWatchDogs.LastOrDefault(); - public string CurrentLocation => TopWatchDog?.Location; + public string? CurrentLocation => TopWatchDog?.Location; } @@ -24,6 +24,6 @@ public class ExceptionDetails { public Exception Exception { get; set; } public ErrorReference ErrorReference { get; set; } - public string Location { get; set; } + public string? Location { get; set; } } } diff --git a/shesha-core/src/Shesha.Elmah/Elmah/NullLoggingContextCollector.cs b/shesha-core/src/Shesha.Elmah/Elmah/NullLoggingContextCollector.cs new file mode 100644 index 0000000000..0386af25f0 --- /dev/null +++ b/shesha-core/src/Shesha.Elmah/Elmah/NullLoggingContextCollector.cs @@ -0,0 +1,48 @@ +using Abp; +using Abp.Domain.Entities; +using System; + +namespace Shesha.Elmah +{ + /// + /// Null implemenhtation of . Is used for optional dependencies + /// + public class NullLoggingContextCollector : ILoggingContextCollector + { + public static readonly ILoggingContextCollector Instance = new NullLoggingContextCollector(); + + public LoggingContextState CurrentState => new LoggingContextState(); + + public IDisposable BeginScope(Action? initAction = null) + { + return NewEmptyAction(); + } + + public IDisposable MakeEntityWatchDog(IEntity entity) where TId : notnull + { + return NewEmptyAction(); + } + + public IDisposable MakeWatchDog(string type, string id) + { + return NewEmptyAction(); + } + + public IDisposable MakeWatchDog(ErrorReference errorRef) + { + return NewEmptyAction(); + } + + public IDisposable SetLocationScope(string location) + { + return NewEmptyAction(); + } + + private IDisposable NewEmptyAction() + { + return new DisposeAction(() => { + // empty action + }); + } + } +} diff --git a/shesha-core/src/Shesha.Elmah/Elmah/PostgreSql/SheshaPgsqlErrorLog.cs b/shesha-core/src/Shesha.Elmah/Elmah/PostgreSql/SheshaPgsqlErrorLog.cs index 0c38b3096e..70c9a3eea3 100644 --- a/shesha-core/src/Shesha.Elmah/Elmah/PostgreSql/SheshaPgsqlErrorLog.cs +++ b/shesha-core/src/Shesha.Elmah/Elmah/PostgreSql/SheshaPgsqlErrorLog.cs @@ -36,8 +36,6 @@ public SheshaPgsqlErrorLog(string connectionString) throw new ArgumentNullException("connectionString"); ConnectionString = connectionString; - - //PrepareDatabase(); } /// @@ -50,7 +48,7 @@ public SheshaPgsqlErrorLog(string connectionString) /// public virtual string ConnectionString { get; } - public override string Log(Error error) + public override string? Log(Error error) { var id = error.Exception?.GetExceptionId(); if (id.HasValue) @@ -79,7 +77,7 @@ public override void Log(Guid id, Error error) var exceptionDetails = provider.CurrentState?.AllExceptions?.FirstOrDefault(e => e.Exception == error.Exception); var location = exceptionDetails?.Location; - ExecuteCommand(connection, Commands.LogError(id, ApplicationName, error.HostName, error.Type, error.Source, error.Message, error.User, error.StatusCode, error.Time, errorXml, location)); + ExecuteCommand(connection, () => Commands.LogError(id, ApplicationName, error.HostName, error.Type, error.Source, error.Message, error.User, error.StatusCode, error.Time, errorXml, location)); // gather refs and log them if (error.Exception != null && provider.CurrentState != null) @@ -89,21 +87,21 @@ public override void Log(Guid id, Error error) { foreach (var item in allRefs) { - ExecuteCommand(connection, Commands.LogErrorRef(id, item.ErrorReference.Type, item.ErrorReference.Id)); + ExecuteCommand(connection, () => Commands.LogErrorRef(id, item.ErrorReference.Type, item.ErrorReference.Id)); } } } } } - private void ExecuteCommand(NpgsqlConnection connection, NpgsqlCommand command) + private void ExecuteCommand(NpgsqlConnection connection, Func commandFactory) { + using var command = commandFactory(); command.Connection = connection; command.ExecuteNonQuery(); - command.Dispose(); } - public override ErrorLogEntry GetError(string id) + public override ErrorLogEntry? GetError(string id) { if (id == null) throw new ArgumentNullException("id"); if (id.Length == 0) throw new ArgumentException(null, "id"); @@ -126,7 +124,7 @@ public override ErrorLogEntry GetError(string id) { command.Connection = connection; connection.Open(); - errorXml = (string)command.ExecuteScalar(); + errorXml = command.ExecuteScalar()?.ToString() ?? string.Empty; } if (errorXml == null) @@ -169,23 +167,6 @@ public override int GetErrors(int errorIndex, int pageSize, ICollection @@ -53,7 +51,7 @@ public SheshaSqlErrorLog(string connectionString) // ReSharper disable once MemberCanBeProtected.Global public virtual string ConnectionString { get; } - public override string Log(Error error) + public override string? Log(Error error) { var id = error.Exception?.GetExceptionId(); if (id.HasValue) @@ -81,7 +79,7 @@ public override void Log(Guid id, Error error) var exceptionDetails = provider.CurrentState?.AllExceptions?.FirstOrDefault(e => e.Exception == error.Exception); var location = exceptionDetails?.Location; - ExecuteCommand(connection, Commands.LogError(id, ApplicationName, error.HostName, error.Type, error.Source, error.Message, error.User, error.StatusCode, error.Time, errorXml, location)); + ExecuteCommand(connection, () => Commands.LogError(id, ApplicationName, error.HostName, error.Type, error.Source, error.Message, error.User, error.StatusCode, error.Time, errorXml, location)); // gather refs and log them if (error.Exception != null && provider.CurrentState != null) @@ -91,7 +89,7 @@ public override void Log(Guid id, Error error) { foreach (var item in allRefs) { - ExecuteCommand(connection, Commands.LogErrorRef(id, item.ErrorReference.Type, item.ErrorReference.Id)); + ExecuteCommand(connection, () => Commands.LogErrorRef(id, item.ErrorReference.Type, item.ErrorReference.Id)); } } } @@ -103,14 +101,14 @@ public override void Log(Guid id, Error error) } } - private void ExecuteCommand(SqlConnection connection, SqlCommand command) + private void ExecuteCommand(SqlConnection connection, Func commandFactory) { + using var command = commandFactory(); command.Connection = connection; command.ExecuteNonQuery(); - command.Dispose(); } - public override ErrorLogEntry GetError(string id) + public override ErrorLogEntry? GetError(string id) { if (id == null) throw new ArgumentNullException(nameof(id)); if (id.Length == 0) throw new ArgumentException(null, nameof(id)); @@ -171,28 +169,11 @@ public override int GetErrors(int errorIndex, int pageSize, ICollection public static class WatchDogExtensions { - public static IDisposable MakeWatchDog(this IEntity entity) + public static IDisposable MakeWatchDog(this IEntity entity) where TId : notnull { var errorRef = GetEntityErrorRef(entity); @@ -18,10 +18,12 @@ public static IDisposable MakeWatchDog(this IEntity entity) return collector.MakeWatchDog(errorRef.Type, errorRef.Id); } - public static ErrorReference GetEntityErrorRef(this IEntity entity) + public static ErrorReference GetEntityErrorRef(this IEntity entity) where TId : notnull { - var id = entity.Id?.ToString(); - var type = entity.GetRealEntityType().FullName; + var id = entity.Id.ToString(); + ArgumentException.ThrowIfNullOrWhiteSpace(id, nameof(id)); + + var type = entity.GetRealEntityType().GetRequiredFullName(); return new ErrorReference(type, id); } } diff --git a/shesha-core/src/Shesha.Elmah/Shesha.Elmah.csproj b/shesha-core/src/Shesha.Elmah/Shesha.Elmah.csproj index ed0a700b8d..b2026bc5ab 100644 --- a/shesha-core/src/Shesha.Elmah/Shesha.Elmah.csproj +++ b/shesha-core/src/Shesha.Elmah/Shesha.Elmah.csproj @@ -9,19 +9,27 @@ README.md - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/DbHelperBase.cs b/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/DbHelperBase.cs index 61818b3dea..776e554081 100644 --- a/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/DbHelperBase.cs +++ b/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/DbHelperBase.cs @@ -3,7 +3,6 @@ namespace Shesha.FluentMigrator { -#nullable enable /// /// DB helper base /// @@ -114,5 +113,4 @@ protected object BitOrBool(bool value) : value ? 1 : 0; } } -#nullable restore } diff --git a/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/SheshaAutoDbMigrator.cs b/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/SheshaAutoDbMigrator.cs index 28105a7136..3cb6c0b304 100644 --- a/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/SheshaAutoDbMigrator.cs +++ b/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/SheshaAutoDbMigrator.cs @@ -1,7 +1,6 @@ using Abp.Dependency; using Abp.MultiTenancy; using Abp.Reflection; -using FluentMigrator; using FluentMigrator.Runner; using Microsoft.Extensions.DependencyInjection; @@ -37,7 +36,8 @@ protected override void CreateOrMigrate(AbpTenantBase? tenant, Action? seedActio // Put the database update into a scope to ensure // that all resources will be disposed. var connectionSettings = new DbConnectionSettings(dbmsType, connectionString); - using var scope = CreateServices(connectionSettings).CreateScope(); + using var services = CreateServices(connectionSettings); + using var scope = services.CreateScope(); using var connectionSettingsScope = DbConnectionSettings.BeginConnectionScope(connectionSettings); // Instantiate the runner diff --git a/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/SheshaDbMigrator.cs b/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/SheshaDbMigrator.cs index d86ab70708..b61f3d845f 100644 --- a/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/SheshaDbMigrator.cs +++ b/shesha-core/src/Shesha.FluentMigrator/FluentMigrator/SheshaDbMigrator.cs @@ -54,7 +54,7 @@ public virtual void CreateOrMigrateForTenant(AbpTenantBase tenant) public virtual void CreateOrMigrateForTenant(AbpTenantBase tenant, Action? seedAction) { - if (tenant.ConnectionString.IsNullOrEmpty()) + if (string.IsNullOrWhiteSpace(tenant.ConnectionString)) return; CreateOrMigrate(tenant, seedAction); @@ -63,7 +63,7 @@ public virtual void CreateOrMigrateForTenant(AbpTenantBase tenant, Action? seedA /// /// Configure the dependency injection services /// - protected virtual IServiceProvider CreateServices(IDbConnectionSettings connectionSettings) + protected virtual ServiceProvider CreateServices(IDbConnectionSettings connectionSettings) { var services = new ServiceCollection(); services.TryAddSingleton(_moduleLocator); @@ -136,7 +136,8 @@ protected virtual void CreateOrMigrate(AbpTenantBase? tenant, Action? seedAction // Put the database update into a scope to ensure // that all resources will be disposed. var connectionSettings = new DbConnectionSettings(dbmsType, connectionString); - using var scope = CreateServices(connectionSettings).CreateScope(); + using var services = CreateServices(connectionSettings); + using var scope = services.CreateScope(); using var connectionSettingsScope = DbConnectionSettings.BeginConnectionScope(connectionSettings); // Instantiate the runner diff --git a/shesha-core/src/Shesha.FluentMigrator/Shesha.FluentMigrator.csproj b/shesha-core/src/Shesha.FluentMigrator/Shesha.FluentMigrator.csproj index 0302f7fba4..b61baccbe8 100644 --- a/shesha-core/src/Shesha.FluentMigrator/Shesha.FluentMigrator.csproj +++ b/shesha-core/src/Shesha.FluentMigrator/Shesha.FluentMigrator.csproj @@ -16,14 +16,22 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.Framework/Application/Services/Dto/FilteredPagedAndSortedResultRequestDto.cs b/shesha-core/src/Shesha.Framework/Application/Services/Dto/FilteredPagedAndSortedResultRequestDto.cs index 2f06b0681a..f6db2aac3c 100644 --- a/shesha-core/src/Shesha.Framework/Application/Services/Dto/FilteredPagedAndSortedResultRequestDto.cs +++ b/shesha-core/src/Shesha.Framework/Application/Services/Dto/FilteredPagedAndSortedResultRequestDto.cs @@ -1,27 +1,25 @@ -using Abp.Application.Services.Dto; -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; +using System.Collections.Generic; namespace Shesha.Application.Services.Dto { /// /// Filtered, pages and sorted request DTO /// - public class FilteredPagedAndSortedResultRequestDto : PagedAndSortedResultRequestDto, IFilteredPagedAndSortedResultRequestDto + public class FilteredPagedAndSortedResultRequestDto : ShaFilteredPagedAndSortedResultRequestDto, IFilteredPagedAndSortedResultRequestDto { /// /// Filter string in JsonLogic format /// - public string Filter { get; set; } + public string? Filter { get; set; } /// /// Quick search string. Is used to search entities by text /// - public string QuickSearch { get; set; } + public string? QuickSearch { get; set; } /// /// List of specifications to apply on top of query /// - public List Specifications { get; set; } = new List(); + public List Specifications { get; set; } = new(); } } diff --git a/shesha-core/src/Shesha.Framework/Application/Services/Dto/GetDynamicEntityInput.cs b/shesha-core/src/Shesha.Framework/Application/Services/Dto/GetDynamicEntityInput.cs index 4f1815cbff..83af61e23b 100644 --- a/shesha-core/src/Shesha.Framework/Application/Services/Dto/GetDynamicEntityInput.cs +++ b/shesha-core/src/Shesha.Framework/Application/Services/Dto/GetDynamicEntityInput.cs @@ -13,6 +13,6 @@ public class GetDynamicEntityInput : EntityDto /// /// List of properties to fetch in GraphQL-like syntax. Supports nested properties /// - public string Properties { get; set; } + public string? Properties { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Application/Services/Dto/IFilteredPagedAndSortedResultRequestDto.cs b/shesha-core/src/Shesha.Framework/Application/Services/Dto/IFilteredPagedAndSortedResultRequestDto.cs index ee29f1aac1..f405021dd2 100644 --- a/shesha-core/src/Shesha.Framework/Application/Services/Dto/IFilteredPagedAndSortedResultRequestDto.cs +++ b/shesha-core/src/Shesha.Framework/Application/Services/Dto/IFilteredPagedAndSortedResultRequestDto.cs @@ -10,11 +10,11 @@ public interface IFilteredPagedAndSortedResultRequestDto : IPagedAndSortedResult /// /// Filter string in JsonLogic format /// - string Filter { get; set; } + string? Filter { get; set; } /// /// Quick search string. Is used to search entities by text /// - string QuickSearch { get; set; } + string? QuickSearch { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Application/Services/Dto/PropsFilteredPagedAndSortedResultRequestDto.cs b/shesha-core/src/Shesha.Framework/Application/Services/Dto/PropsFilteredPagedAndSortedResultRequestDto.cs index 5ff22f4005..422cfc6e09 100644 --- a/shesha-core/src/Shesha.Framework/Application/Services/Dto/PropsFilteredPagedAndSortedResultRequestDto.cs +++ b/shesha-core/src/Shesha.Framework/Application/Services/Dto/PropsFilteredPagedAndSortedResultRequestDto.cs @@ -1,6 +1,4 @@ -using Abp.Application.Services.Dto; - -namespace Shesha.Application.Services.Dto +namespace Shesha.Application.Services.Dto { /// /// Filtered, pages and sorted request DTO with properties list @@ -10,6 +8,6 @@ public class PropsFilteredPagedAndSortedResultRequestDto : FilteredPagedAndSorte /// /// List of properties to fetch in GraphQL-like syntax. Supports nested properties /// - public string Properties { get; set; } + public string? Properties { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Application/Services/Dto/ShaFilteredPagedAndSortedResultRequestDto.cs b/shesha-core/src/Shesha.Framework/Application/Services/Dto/ShaFilteredPagedAndSortedResultRequestDto.cs new file mode 100644 index 0000000000..78ec4b9181 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Application/Services/Dto/ShaFilteredPagedAndSortedResultRequestDto.cs @@ -0,0 +1,14 @@ +using Abp.Application.Services.Dto; +using System.ComponentModel.DataAnnotations; + +namespace Shesha.Application.Services.Dto +{ + /// + /// Simply implements . + /// + public class ShaFilteredPagedAndSortedResultRequestDto: PagedAndSortedResultRequestDto + { + [Range(-1, int.MaxValue)] + public override int MaxResultCount { get; set; } = 10; + } +} diff --git a/shesha-core/src/Shesha.Framework/Attributes/BelongsToConfigurableModuleAttribute.cs b/shesha-core/src/Shesha.Framework/Attributes/BelongsToConfigurableModuleAttribute.cs index 8d7bdc3769..826d355f83 100644 --- a/shesha-core/src/Shesha.Framework/Attributes/BelongsToConfigurableModuleAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Attributes/BelongsToConfigurableModuleAttribute.cs @@ -1,4 +1,5 @@ using Shesha.Modules; +using Shesha.Reflection; using Shesha.Services; using System; @@ -10,7 +11,7 @@ namespace Shesha.Attributes [AttributeUsage(AttributeTargets.Assembly)] public class BelongsToConfigurableModuleAttribute : Attribute { - protected Type ModuleType { get; set; } + protected Type? ModuleType { get; set; } public string ModuleName { get; set; } public BelongsToConfigurableModuleAttribute(string moduleName) @@ -24,7 +25,7 @@ public BelongsToConfigurableModuleAttribute(Type moduleType) if (!typeof(SheshaModule).IsAssignableFrom(moduleType)) throw new ArgumentException($"Value of the `{nameof(moduleType)}` must be a subclass of {nameof(SheshaModule)}"); - var instance = StaticContext.IocManager.Resolve(moduleType) as SheshaModule; + var instance = StaticContext.IocManager.Resolve(moduleType).ForceCastAs(); ModuleName = instance.ModuleInfo.Name; } } diff --git a/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthManager.cs b/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthManager.cs index ce02ee91f2..652cdd5898 100644 --- a/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthManager.cs +++ b/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthManager.cs @@ -16,19 +16,19 @@ public ExternalAuthManager(IIocResolver iocResolver, IExternalAuthConfiguration _externalAuthConfiguration = externalAuthConfiguration; } - public Task IsValidUserAsync(string provider, string providerKey, string providerAccessCode) + public async Task IsValidUserAsync(string provider, string providerKey, string providerAccessCode) { using (var providerApi = CreateProviderApi(provider)) { - return providerApi.Object.IsValidUserAsync(providerKey, providerAccessCode); + return await providerApi.Object.IsValidUserAsync(providerKey, providerAccessCode); } } - public Task GetUserInfoAsync(string provider, string accessCode) + public async Task GetUserInfoAsync(string provider, string accessCode) { using (var providerApi = CreateProviderApi(provider)) { - return providerApi.Object.GetUserInfoAsync(accessCode); + return await providerApi.Object.GetUserInfoAsync(accessCode); } } diff --git a/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthProviderApiBase.cs b/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthProviderApiBase.cs index b73dd97094..269dbddcc1 100644 --- a/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthProviderApiBase.cs +++ b/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthProviderApiBase.cs @@ -5,7 +5,7 @@ namespace Shesha.Authentication.External { public abstract class ExternalAuthProviderApiBase : IExternalAuthProviderApi, ITransientDependency { - public ExternalLoginProviderInfo ProviderInfo { get; set; } + public ExternalLoginProviderInfo? ProviderInfo { get; set; } public void Initialize(ExternalLoginProviderInfo providerInfo) { diff --git a/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthUserInfo.cs b/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthUserInfo.cs index f9675c7f90..b1c6182c86 100644 --- a/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthUserInfo.cs +++ b/shesha-core/src/Shesha.Framework/Authentication/External/ExternalAuthUserInfo.cs @@ -2,14 +2,14 @@ { public class ExternalAuthUserInfo { - public string ProviderKey { get; set; } + public required string ProviderKey { get; set; } - public string Name { get; set; } + public required string Name { get; set; } - public string EmailAddress { get; set; } + public required string EmailAddress { get; set; } - public string Surname { get; set; } + public string? Surname { get; set; } - public string Provider { get; set; } + public required string Provider { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Authentication/External/IExternalAuthProviderApi.cs b/shesha-core/src/Shesha.Framework/Authentication/External/IExternalAuthProviderApi.cs index bd3070c9dc..c741d70ce4 100644 --- a/shesha-core/src/Shesha.Framework/Authentication/External/IExternalAuthProviderApi.cs +++ b/shesha-core/src/Shesha.Framework/Authentication/External/IExternalAuthProviderApi.cs @@ -4,7 +4,7 @@ namespace Shesha.Authentication.External { public interface IExternalAuthProviderApi { - ExternalLoginProviderInfo ProviderInfo { get; } + ExternalLoginProviderInfo? ProviderInfo { get; } Task IsValidUserAsync(string userId, string accessCode); diff --git a/shesha-core/src/Shesha.Framework/Authentication/JwtBearer/TokenAuthConfiguration.cs b/shesha-core/src/Shesha.Framework/Authentication/JwtBearer/TokenAuthConfiguration.cs index 67ee826bef..d545e49e45 100644 --- a/shesha-core/src/Shesha.Framework/Authentication/JwtBearer/TokenAuthConfiguration.cs +++ b/shesha-core/src/Shesha.Framework/Authentication/JwtBearer/TokenAuthConfiguration.cs @@ -5,13 +5,13 @@ namespace Shesha.Authentication.JwtBearer { public class TokenAuthConfiguration { - public SymmetricSecurityKey SecurityKey { get; set; } + public required SymmetricSecurityKey SecurityKey { get; set; } - public string Issuer { get; set; } + public required string Issuer { get; set; } - public string Audience { get; set; } + public required string Audience { get; set; } - public SigningCredentials SigningCredentials { get; set; } + public required SigningCredentials SigningCredentials { get; set; } public TimeSpan Expiration { get; set; } } diff --git a/shesha-core/src/Shesha.Framework/Authorization/Cache/CustomUserPermissionCacheHolder.cs b/shesha-core/src/Shesha.Framework/Authorization/Cache/CustomUserPermissionCacheHolder.cs new file mode 100644 index 0000000000..d0406b5b60 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Authorization/Cache/CustomUserPermissionCacheHolder.cs @@ -0,0 +1,15 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Authorization.Dtos; +using Shesha.Cache; + +namespace Shesha.Authorization.Cache +{ + + public class CustomUserPermissionCacheHolder : CacheHolder, ICustomUserPermissionCacheHolder, ISingletonDependency + { + public CustomUserPermissionCacheHolder(ICacheManager cacheManager) : base(CustomUserPermissionCacheItem.CacheStoreName, cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Authorization/Cache/ICustomUserPermissionCacheHolder.cs b/shesha-core/src/Shesha.Framework/Authorization/Cache/ICustomUserPermissionCacheHolder.cs new file mode 100644 index 0000000000..07c8022723 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Authorization/Cache/ICustomUserPermissionCacheHolder.cs @@ -0,0 +1,9 @@ +using Shesha.Authorization.Dtos; +using Shesha.Cache; + +namespace Shesha.Authorization.Cache +{ + public interface ICustomUserPermissionCacheHolder : ICacheHolder + { + } +} diff --git a/shesha-core/src/Shesha.Framework/Authorization/CacheManagerExtensions.cs b/shesha-core/src/Shesha.Framework/Authorization/CacheManagerExtensions.cs index 77a479a2d3..33c2e1c8a2 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/CacheManagerExtensions.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/CacheManagerExtensions.cs @@ -1,7 +1,5 @@ using Abp.Runtime.Caching; using Shesha.Authorization.Dtos; -using Shesha.Cache; -using Shesha.Permissions; namespace Shesha.Authorization { @@ -11,10 +9,5 @@ public static ITypedCache GetCustomUserPe { return cacheManager.GetCache(CustomUserPermissionCacheItem.CacheStoreName); } - - public static ITypedCache> GetPermissionedObjectCache(this ICacheManager cacheManager) - { - return cacheManager.GetCache>(PermissionedObjectDto.CacheStoreName); - } } } diff --git a/shesha-core/src/Shesha.Framework/Authorization/CustomPermissionsInvalidator.cs b/shesha-core/src/Shesha.Framework/Authorization/CustomPermissionsInvalidator.cs index a309c2b0ef..058657fc0a 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/CustomPermissionsInvalidator.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/CustomPermissionsInvalidator.cs @@ -2,25 +2,26 @@ using Abp.Dependency; using Abp.Events.Bus.Entities; using Abp.Events.Bus.Handlers; -using Abp.Runtime.Caching; +using Shesha.Authorization.Cache; +using System.Threading.Tasks; namespace Shesha.Authorization { public class CustomPermissionsInvalidator : - IEventHandler>, + IAsyncEventHandler>, ITransientDependency { - private readonly ICacheManager _cacheManager; + private readonly ICustomUserPermissionCacheHolder _cacheHolder; - public CustomPermissionsInvalidator(ICacheManager cacheManager) + public CustomPermissionsInvalidator(ICustomUserPermissionCacheHolder cacheHolder) { - _cacheManager = cacheManager; + _cacheHolder = cacheHolder; } - public void HandleEvent(EntityDeletedEventData eventData) + public async Task HandleEventAsync(EntityDeletedEventData eventData) { var cacheKey = eventData.Entity.Id + "@" + (eventData.Entity.TenantId ?? 0); - _cacheManager.GetCustomUserPermissionCache().Remove(cacheKey); + await _cacheHolder.Cache.RemoveAsync(cacheKey); } } } diff --git a/shesha-core/src/Shesha.Framework/Authorization/DBAuthorizationProvider.cs b/shesha-core/src/Shesha.Framework/Authorization/DBAuthorizationProvider.cs index d0bbfc3eb3..7de9c947d3 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/DBAuthorizationProvider.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/DBAuthorizationProvider.cs @@ -1,66 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Abp.Authorization; -using Abp.Domain.Repositories; +using Abp.Authorization; using Abp.Domain.Uow; using Shesha.Domain; using Shesha.Utilities; +using System.Collections.Generic; +using System.Linq; namespace Shesha.Authorization { public class DbAuthorizationProvider : AuthorizationProvider { - - private readonly IRepository _permissionDefinitionRepository; - - public DbAuthorizationProvider( - IRepository permissionDefinitionRepository - ) + public DbAuthorizationProvider() { - _permissionDefinitionRepository = permissionDefinitionRepository; } [UnitOfWork] public override void SetPermissions(IPermissionDefinitionContext context) { - /*var dbPermissions = _permissionDefinitionRepository.GetAllList(); - - // Update DB-related items - var dbRootPermissions = dbPermissions.Where(x => string.IsNullOrEmpty(x.Parent)).ToList(); - foreach (var dbPermission in dbRootPermissions) - { - var permission = context.CreatePermission(dbPermission.Name, dbPermission.DisplayName.L(), dbPermission.Description.L()); - CreateChildPermissions(dbPermissions, permission); - dbPermissions.Remove(dbPermission); - } - - - // Update code-related items - while (dbPermissions.Any()) - { - var dbPermission = dbPermissions.FirstOrDefault(); - if (dbPermission != null) - { - var permission = context.GetPermissionOrNull(dbPermission.Parent); - while (permission == null && dbPermissions.Any(x => x.Name == dbPermission?.Parent)) - { - dbPermission = dbPermissions.FirstOrDefault(x => x.Name == dbPermission?.Parent); - permission = context.GetPermissionOrNull(dbPermission?.Parent); - } - - if (permission != null) - { - CreateChildPermissions(dbPermissions, permission); - } - else - { - // remove permission with missed parent - _permissionDefinitionRepository.Delete(dbPermission); - } - dbPermissions.Remove(dbPermission); - } - }*/ } private void CreateChildPermissions(List dbPermissions, Abp.Authorization.Permission permission) @@ -69,7 +24,7 @@ private void CreateChildPermissions(List dbPermissions, Ab foreach (var dbChildPermission in dbChildPermissions) { var childPermission = - permission.CreateChildPermission(dbChildPermission.Name, dbChildPermission.Label.L(), dbChildPermission.Description.L()); + permission.CreateChildPermission(dbChildPermission.Name, dbChildPermission.Label?.L(), dbChildPermission.Description?.L()); CreateChildPermissions(dbPermissions, childPermission); dbPermissions.Remove(dbChildPermission); } diff --git a/shesha-core/src/Shesha.Framework/Authorization/Dtos/CustomUserPermissionCacheItem.cs b/shesha-core/src/Shesha.Framework/Authorization/Dtos/CustomUserPermissionCacheItem.cs index c08eb56ce6..eb77472873 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/Dtos/CustomUserPermissionCacheItem.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/Dtos/CustomUserPermissionCacheItem.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using ConcurrentCollections; +using ConcurrentCollections; namespace Shesha.Authorization.Dtos { diff --git a/shesha-core/src/Shesha.Framework/Authorization/IShaPermissionManager.cs b/shesha-core/src/Shesha.Framework/Authorization/IShaPermissionManager.cs index 6d759ead50..430ee4b95b 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/IShaPermissionManager.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/IShaPermissionManager.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Abp.Application.Features; +using Abp.Application.Features; using Abp.Authorization; using Abp.Localization; using Abp.MultiTenancy; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; +using System.Collections.Generic; +using System.Threading.Tasks; namespace Shesha.Authorization { @@ -14,27 +14,27 @@ public interface IShaPermissionManager: IPermissionManager Abp.Authorization.Permission CreatePermission( Abp.Authorization.Permission parent, string name, - ILocalizableString displayName = null, - ILocalizableString description = null, + ILocalizableString? displayName = null, + ILocalizableString? description = null, MultiTenancySides multiTenancySides = MultiTenancySides.Host | MultiTenancySides.Tenant, - IFeatureDependency featureDependency = null, - Dictionary properties = null); + IFeatureDependency? featureDependency = null, + Dictionary? properties = null); Abp.Authorization.Permission EditPermission( string oldName, Abp.Authorization.Permission parent, string name, - ILocalizableString displayName = null, - ILocalizableString description = null, + ILocalizableString? displayName = null, + ILocalizableString? description = null, MultiTenancySides multiTenancySides = MultiTenancySides.Host | MultiTenancySides.Tenant, - IFeatureDependency featureDependency = null, - Dictionary properties = null); + IFeatureDependency? featureDependency = null, + Dictionary? properties = null); Task CreatePermissionAsync(PermissionDefinition permission); Task EditPermissionAsync(string oldName, PermissionDefinition permission); - Task UpdateParentAsync(string name, string parentName, Module module); + Task UpdateParentAsync(string name, string? parentName, Module? module); Task DeletePermissionAsync(string name); diff --git a/shesha-core/src/Shesha.Framework/Authorization/LoginManager.cs b/shesha-core/src/Shesha.Framework/Authorization/LoginManager.cs index 536ce9208c..d402ed8976 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/LoginManager.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/LoginManager.cs @@ -5,12 +5,10 @@ using Abp.Dependency; using Abp.Domain.Repositories; using Abp.Domain.Uow; -using Abp.Extensions; using Abp.Zero.Configuration; using Microsoft.AspNetCore.Identity; using Shesha.Authorization.Roles; using Shesha.Authorization.Users; -using Shesha.Configuration; using Shesha.Configuration.Security; using Shesha.ConfigurationItems; using Shesha.Domain; @@ -56,11 +54,11 @@ IConfigurationFrameworkRuntime cfRuntime _cfRuntime = cfRuntime; } - protected override ShaLoginResult AdditionalVerification(User user, Tenant tenant) + protected override ShaLoginResult? AdditionalVerification(User user, Tenant? tenant) { if (user.AllowedFrontEndApps != null && user.AllowedFrontEndApps.Any() - && !user.AllowedFrontEndApps.Contains(_cfRuntime.FrontEndApplication) + && (string.IsNullOrWhiteSpace(_cfRuntime.FrontEndApplication) || !user.AllowedFrontEndApps.Contains(_cfRuntime.FrontEndApplication)) ) return new ShaLoginResult(ShaLoginResultType.ForbiddenFrontend, tenant, user); return null; diff --git a/shesha-core/src/Shesha.Framework/Authorization/ObjectPermissionChecker.cs b/shesha-core/src/Shesha.Framework/Authorization/ObjectPermissionChecker.cs index bfd15cfdbd..dd1ecbd0db 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/ObjectPermissionChecker.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/ObjectPermissionChecker.cs @@ -51,9 +51,7 @@ public async Task AuthorizeAsync( return; } - var methodName = PermissionedObjectManager.CrudMethods.ContainsKey(method) - ? PermissionedObjectManager.CrudMethods[method] - : method; + var methodName = PermissionedObjectManager.GetCrudMethod(method, method); var permissionName = $"{permissionedObject}@{methodName}"; var permission = await _permissionedObjectManager.GetOrDefaultAsync(permissionName, objectType); @@ -91,7 +89,7 @@ public async Task AuthorizeAsync( var ty = _permissionChecker.GetType();// (_permissionChecker as IProxyTargetAccessor).DynProxyGetTarget().GetType(); // ToDo: add RequireAll flag - await _permissionChecker.AuthorizeAsync(false, permission.ActualPermissions.ToArray()); + await _permissionChecker.AuthorizeAsync(false, permission.ActualPermissions?.ToArray()); } } } diff --git a/shesha-core/src/Shesha.Framework/Authorization/Roles/Role.cs b/shesha-core/src/Shesha.Framework/Authorization/Roles/Role.cs index 817cf35ef3..b4932412da 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/Roles/Role.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/Roles/Role.cs @@ -9,7 +9,7 @@ public class Role : AbpRole { public const int MaxDescriptionLength = 5000; - public Role()//: base(null, null) + public Role() { Claims = new List(); Permissions = new List(); @@ -26,6 +26,6 @@ public Role(int? tenantId, string name, string displayName) } [StringLength(MaxDescriptionLength)] - public virtual string Description {get; set;} + public virtual string? Description {get; set;} } } diff --git a/shesha-core/src/Shesha.Framework/Authorization/ShaLoginManager.cs b/shesha-core/src/Shesha.Framework/Authorization/ShaLoginManager.cs index b72e8acbac..876ad2dd29 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/ShaLoginManager.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/ShaLoginManager.cs @@ -13,15 +13,13 @@ using Abp.UI; using Abp.Zero.Configuration; using Microsoft.AspNetCore.Identity; -using NetTopologySuite.Operation.Valid; -using Shesha.Configuration; using Shesha.Configuration.Security; using Shesha.Domain; using Shesha.Domain.Enums; using Shesha.Extensions; -using Shesha.MultiTenancy; using Shesha.Otp; using Shesha.Otp.Dto; +using Shesha.Reflection; using Shesha.Utilities; using System; using System.Collections.Generic; @@ -86,64 +84,66 @@ public ShaLoginManager( ClientInfoProvider = NullClientInfoProvider.Instance; ShaLoginAttemptRepository = shaLoginAttemptRepository; _mobileDeviceRepository = mobileDeviceRepository; + + OtpManager = default!; } - protected virtual ShaLoginResult AdditionalVerification(TUser user, TTenant tenant) + protected virtual ShaLoginResult? AdditionalVerification(TUser user, TTenant? tenant) { return null; } - public virtual async Task> LoginAsync(UserLoginInfo login, string imei = null, string tenancyName = null) + public virtual async Task> LoginAsync(UserLoginInfo login, string? imei = null, string? tenancyName = null) { - ShaLoginResult result = null; - using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.RequiresNew)) - { - result = await LoginInternalAsync(login, tenancyName); - await uow.CompleteAsync(); - } - + var result = await LoginInternalAsync(login, tenancyName); + await SaveLoginAttemptAsync(result, imei, tenancyName, login.ProviderKey + "@" + login.LoginProvider); return result; } - protected virtual async Task> LoginInternalAsync(UserLoginInfo login, string tenancyName) + protected virtual async Task> LoginInternalAsync(UserLoginInfo login, string? tenancyName) { - if (login == null || login.LoginProvider.IsNullOrEmpty() || login.ProviderKey.IsNullOrEmpty()) - { - throw new ArgumentException("login"); - } - - return await ProcessTenancyLoginActionAsync(null, tenancyName, true, async (tenant) => { - var user = await UserManager.FindAsync(tenant?.Id, login); - if (user == null) - { - return new ShaLoginResult(ShaLoginResultType.UnknownExternalLogin, tenant); - } + using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.RequiresNew)) + { + if (login == null || string.IsNullOrWhiteSpace(login.LoginProvider) || string.IsNullOrWhiteSpace(login.ProviderKey)) + throw new ArgumentException("login"); + + var result = await ProcessTenancyLoginActionAsync(null, tenancyName, true, async (tenant) => { + var user = await UserManager.FindAsync(tenant?.Id, login); + if (user == null) + { + return new ShaLoginResult(ShaLoginResultType.UnknownExternalLogin, tenant); + } + + return await CreateLoginResultAsync(user, tenant); + }); - return await CreateLoginResultAsync(user, tenant); - }); + await uow.CompleteAsync(); + return result; + } } - public virtual async Task> AnonymousLoginViaDeviceIdAsync(Guid deviceId, string imei = null, string tenancyName = null) + public virtual async Task> AnonymousLoginViaDeviceIdAsync(Guid deviceId, string? imei = null, string? tenancyName = null) { - ShaLoginResult result = null; var user = await UserManager.FindByNameAsync(deviceId.ToString()); if (user == null) throw new UserFriendlyException("User with the specified deviceId not found"); - using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.RequiresNew)) + var result = await UnitOfWorkManager.WithUnitOfWorkAsync(async () => { - result = await ProcessTenancyLoginActionAsync(imei, tenancyName, false, async (tenant) => { + return await ProcessTenancyLoginActionAsync(imei, tenancyName, false, async (tenant) => + { return await CreateLoginResultAsync(user, tenant); }); - } + }, TransactionScopeOption.RequiresNew); + await SaveLoginAttemptAsync(result, imei, tenancyName, deviceId.ToString()); return result; } - private void CheckOtpAuthAvailability() + private void CheckOtpAuthAvailability() { if (OtpManager == null) throw new NotSupportedException("OTP authentication is not supported"); @@ -171,17 +171,14 @@ public virtual async Task SendLoginOtpAsync(TUser user, string return response; } - public virtual async Task> LoginViaOtpAsync(string mobileNo, Guid operationId, string code, string imei = null, string tenancyName = null, bool shouldLockout = true) + public virtual async Task> LoginViaOtpAsync(string mobileNo, Guid operationId, string code, string? imei = null, string? tenancyName = null, bool shouldLockout = true) { CheckOtpAuthAvailability(); - - ShaLoginResult result = null; - using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.RequiresNew)) - { - result = await ProcessTenancyLoginActionAsync(imei, tenancyName, shouldLockout, async (tenant) => { + var result = await UnitOfWorkManager.WithUnitOfWorkAsync(async () => { + return await ProcessTenancyLoginActionAsync(imei, tenancyName, shouldLockout, async (tenant) => { - var otp = await OtpManager.GetAsync(operationId); + var otp = await OtpManager.GetOrNullAsync(operationId); if (otp == null) return new ShaLoginResult(ShaLoginResultType.InvalidOTP, tenant); var verificationResult = await OtpManager.VerifyPinAsync(new VerifyPinInput @@ -212,7 +209,7 @@ public virtual async Task> LoginViaOtpAsync(string mobileN } // authenticated using internal account, check IMEI - if (!(await CheckImeiAsync(imei))) + if (!await CheckImeiAsync(imei)) return new ShaLoginResult(ShaLoginResultType.DeviceNotRegistered, tenant, user); // clear reset code @@ -221,31 +218,27 @@ public virtual async Task> LoginViaOtpAsync(string mobileN return await CreateLoginResultAsync(user, tenant); }); - - } + }, TransactionScopeOption.RequiresNew); await SaveLoginAttemptAsync(result, imei, tenancyName, mobileNo); return result; } - public virtual async Task> LoginAsync(string userNameOrEmailAddress, string plainPassword, string imei = null, string tenancyName = null, bool shouldLockout = true) - { - ShaLoginResult result = null; - using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.RequiresNew)) - { - result = await LoginInternalAsync(userNameOrEmailAddress, plainPassword, imei, tenancyName, shouldLockout); - await uow.CompleteAsync(); - } - + public virtual async Task> LoginAsync(string userNameOrEmailAddress, string plainPassword, string? imei = null, string? tenancyName = null, bool shouldLockout = true) + { + var result = await UnitOfWorkManager.WithUnitOfWorkAsync(async () => { + return await LoginInternalAsync(userNameOrEmailAddress, plainPassword, imei, tenancyName, shouldLockout); + }, TransactionScopeOption.RequiresNew); + await SaveLoginAttemptAsync(result, imei, tenancyName, userNameOrEmailAddress); return result; } - private async Task> ProcessTenancyLoginActionAsync(string imei, string tenancyName, bool shouldLockout, Func>> action) + private async Task> ProcessTenancyLoginActionAsync(string? imei, string? tenancyName, bool shouldLockout, Func>> action) { // Get and check tenant - TTenant tenant = null; + TTenant? tenant = null; using (UnitOfWorkManager.Current.SetTenantId(null)) { if (!MultiTenancyConfig.IsEnabled) @@ -276,20 +269,13 @@ private async Task> ProcessTenancyLoginActionAsync(string } } - protected virtual async Task> LoginInternalAsync(string userNameOrEmailAddress, string plainPassword, string imei, string tenancyName, bool shouldLockout) + protected virtual async Task> LoginInternalAsync(string userNameOrEmailAddress, string plainPassword, string? imei, string? tenancyName, bool shouldLockout) { - if (userNameOrEmailAddress.IsNullOrEmpty()) - { - throw new ArgumentNullException(nameof(userNameOrEmailAddress)); - } - - if (plainPassword.IsNullOrEmpty()) - { - throw new ArgumentNullException(nameof(plainPassword)); - } + ArgumentNullException.ThrowIfNullOrWhiteSpace(userNameOrEmailAddress, nameof(userNameOrEmailAddress)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(plainPassword, nameof(plainPassword)); return await ProcessTenancyLoginActionAsync(imei, tenancyName, shouldLockout, async (tenant) => { - //TryLoginFromExternalAuthenticationSources method may create the user, that's why we are calling it before AbpUserStore.FindByNameOrEmailAsync + // TryLoginFromExternalAuthenticationSources method may create the user, that's why we are calling it before AbpUserStore.FindByNameOrEmailAsync var loggedInFromExternalSource = await TryLoginFromExternalAuthenticationSourcesAsync(userNameOrEmailAddress, plainPassword, tenant); var user = await UserManager.FindByNameOrEmailAsync(tenant?.Id, userNameOrEmailAddress); @@ -325,7 +311,7 @@ protected virtual async Task> LoginInternalAsync(string us } // authenticated using internal account, check IMEI - if (!(await CheckImeiAsync(imei))) + if (!await CheckImeiAsync(imei)) return new ShaLoginResult(ShaLoginResultType.DeviceNotRegistered, tenant, user); await UserManager.ResetAccessFailedCountAsync(user); @@ -345,7 +331,7 @@ protected virtual async Task> LoginInternalAsync(string us }); } - private async Task CheckImeiAsync(string imei) + private async Task CheckImeiAsync(string? imei) { if (string.IsNullOrWhiteSpace(imei)) return true; // skip if the IMEI is null @@ -353,24 +339,24 @@ private async Task CheckImeiAsync(string imei) return await _mobileDeviceRepository.GetAll().Where(d => d.IMEI == imei.Trim() && !d.IsLocked).AnyAsync(); } - protected virtual async Task> CreateLoginResultAsync(TUser user, TTenant tenant = null) + protected virtual async Task> CreateLoginResultAsync(TUser user, TTenant? tenant = null) { if (!user.IsActive) - { return new ShaLoginResult(ShaLoginResultType.UserIsNotActive); - } var principal = await _claimsPrincipalFactory.CreateAsync(user); + if (principal.Identity is not ClaimsIdentity claimsIdentity) + throw new AbpAuthorizationException($"Failed to create claims identity for user '{user.UserName}'"); return new ShaLoginResult( tenant, user, - principal.Identity as ClaimsIdentity + claimsIdentity ); } - public virtual async Task SaveLoginAttemptAsync(ShaLoginResult loginResult, string imei, - string tenancyName, string userNameOrEmailAddress) + public virtual async Task SaveLoginAttemptAsync(ShaLoginResult loginResult, string? imei, + string? tenancyName, string userNameOrEmailAddress) { if (UnitOfWorkManager.Current != null) await UnitOfWorkManager.Current.SaveChangesAsync(); @@ -378,7 +364,7 @@ public virtual async Task SaveLoginAttemptAsync(ShaLoginResult loginResul using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress)) { var tenantId = loginResult.TenantId; - using (UnitOfWorkManager.Current.SetTenantId(tenantId)) + using (UnitOfWorkManager.GetCurrent().SetTenantId(tenantId)) { var loginAttempt = new ShaUserLoginAttempt { @@ -400,14 +386,14 @@ public virtual async Task SaveLoginAttemptAsync(ShaLoginResult loginResul : null }; - await ShaLoginAttemptRepository.InsertAsync(loginAttempt); - + await ShaLoginAttemptRepository.InsertAsync(loginAttempt); + await uow.CompleteAsync(); } } } - private async Task GetDeviceNameAsync(string imei) + private async Task GetDeviceNameAsync(string? imei) { return string.IsNullOrWhiteSpace(imei) ? null @@ -421,7 +407,7 @@ protected virtual void SaveLoginAttempt(ShaLoginResult loginResult, strin using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress)) { var tenantId = loginResult.TenantId; - using (UnitOfWorkManager.Current.SetTenantId(tenantId)) + using (UnitOfWorkManager.GetCurrent().SetTenantId(tenantId)) { var loginAttempt = new ShaUserLoginAttempt { @@ -451,7 +437,7 @@ protected virtual async Task TryLockOutAsync(int? tenantId, long userId) { using (UnitOfWorkManager.Current.SetTenantId(tenantId)) { - var user = await UserManager.FindByIdAsync(userId.ToString()); + var user = (await UserManager.FindByIdAsync(userId.ToString())).NotNull($"User with id = '{userId}' not found"); (await UserManager.AccessFailedAsync(user)).CheckErrors(); @@ -466,7 +452,7 @@ protected virtual async Task TryLockOutAsync(int? tenantId, long userId) } } - protected virtual async Task TryLoginFromExternalAuthenticationSourcesAsync(string userNameOrEmailAddress, string plainPassword, TTenant tenant) + protected virtual async Task TryLoginFromExternalAuthenticationSourcesAsync(string userNameOrEmailAddress, string plainPassword, TTenant? tenant) { if (!UserManagementConfig.ExternalAuthenticationSources.Any()) { @@ -476,7 +462,8 @@ protected virtual async Task TryLoginFromExternalAuthenticationSourcesAsyn foreach (var sourceType in UserManagementConfig.ExternalAuthenticationSources) { using (var source = IocResolver.ResolveAsDisposable>(sourceType)) - { + { +#pragma warning disable CS8604 // ABP doesn't support nullability if (await source.Object.TryAuthenticateAsync(userNameOrEmailAddress, plainPassword, tenant)) { var tenantId = tenant == null ? (int?)null : tenant.Id; @@ -495,7 +482,8 @@ protected virtual async Task TryLoginFromExternalAuthenticationSourcesAsyn if (user.Roles == null) { user.Roles = new List(); - foreach (var defaultRole in RoleManager.Roles.Where(r => r.TenantId == tenantId && r.IsDefault).ToList()) + var defaultRoles = await RoleManager.Roles.Where(r => r.TenantId == tenantId && r.IsDefault).ToListAsync(); + foreach (var defaultRole in defaultRoles) { user.Roles.Add(new UserRole(tenantId, user.Id, defaultRole.Id)); } @@ -517,6 +505,7 @@ protected virtual async Task TryLoginFromExternalAuthenticationSourcesAsyn return true; } } +#pragma warning restore CS8604 // ABP doesn't support nullability } } diff --git a/shesha-core/src/Shesha.Framework/Authorization/ShaLoginResult.cs b/shesha-core/src/Shesha.Framework/Authorization/ShaLoginResult.cs index df3c50e053..20f7f568e6 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/ShaLoginResult.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/ShaLoginResult.cs @@ -1,5 +1,6 @@ using Abp.Authorization.Users; using Abp.MultiTenancy; +using System.Diagnostics.CodeAnalysis; using System.Security.Claims; namespace Shesha.Authorization @@ -11,18 +12,21 @@ public class ShaLoginResult public int? TenantId { get; private set; } - public TUser User { get; private set; } + public TUser? User { get; private set; } - public ClaimsIdentity Identity { get; private set; } + public ClaimsIdentity? Identity { get; private set; } - public ShaLoginResult(ShaLoginResultType result, AbpTenantBase tenant = null, TUser user = null) + [MemberNotNullWhen(true, nameof(Identity))] + public bool IsSuccess => Result == ShaLoginResultType.Success; + + public ShaLoginResult(ShaLoginResultType result, AbpTenantBase? tenant = null, TUser? user = null) { Result = result; TenantId = tenant?.Id; User = user; } - public ShaLoginResult(AbpTenantBase tenant, TUser user, ClaimsIdentity identity) + public ShaLoginResult(AbpTenantBase? tenant, TUser user, ClaimsIdentity identity) : this(ShaLoginResultType.Success, tenant) { User = user; diff --git a/shesha-core/src/Shesha.Framework/Authorization/ShaLoginResultTypeHelper.cs b/shesha-core/src/Shesha.Framework/Authorization/ShaLoginResultTypeHelper.cs index 126c0310c0..1407207cd5 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/ShaLoginResultTypeHelper.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/ShaLoginResultTypeHelper.cs @@ -13,7 +13,7 @@ public ShaLoginResultTypeHelper() LocalizationSourceName = SheshaConsts.LocalizationSourceName; } - public Exception CreateExceptionForFailedLoginAttempt(ShaLoginResultType result, string usernameOrEmailAddress, string tenancyName) + public Exception CreateExceptionForFailedLoginAttempt(ShaLoginResultType result, string usernameOrEmailAddress, string? tenancyName) { switch (result) { diff --git a/shesha-core/src/Shesha.Framework/Authorization/ShaPermissionChecker.cs b/shesha-core/src/Shesha.Framework/Authorization/ShaPermissionChecker.cs index 9082fbfcd3..359c833aca 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/ShaPermissionChecker.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/ShaPermissionChecker.cs @@ -1,6 +1,7 @@ using Abp.Authorization; using Abp.Domain.Uow; using Abp.Runtime.Caching; +using Shesha.Authorization.Cache; using Shesha.Authorization.Dtos; using Shesha.Authorization.Roles; using Shesha.Authorization.Users; @@ -15,42 +16,22 @@ namespace Shesha.Authorization /// public class ShaPermissionChecker : PermissionChecker, IShaPermissionChecker { - private const string CustomUserPermissionsCacheName = "CustomUserPermissionsCache"; - - private readonly ICacheManager _cacheManager; private readonly IUnitOfWorkManager _unitOfWorkManager; - private readonly UserManager _userManager; public Guid Id { get; set; } - internal ITypedCache _cupCache; - protected ITypedCache CustomUserPermissionCache - { - get - { - if (_cupCache == null) - { - var cache = _cacheManager.GetCustomUserPermissionCache(); - _cupCache = cache; - } - - return _cupCache; - } - } + protected readonly ITypedCache CustomUserPermissionCache; /// /// Default constructor /// - public ShaPermissionChecker(UserManager userManager, ICacheManager cacheManager, IUnitOfWorkManager unitOfWorkManager) + public ShaPermissionChecker(UserManager userManager, IUnitOfWorkManager unitOfWorkManager, ICustomUserPermissionCacheHolder cacheHolder) : base(userManager) { - _cacheManager = cacheManager; _unitOfWorkManager = unitOfWorkManager; - _userManager = userManager; - Id = Guid.NewGuid(); - //Debug.WriteLine($"PermissionChecker Create: {Id.ToString()}"); + CustomUserPermissionCache = cacheHolder.Cache; } private int? GetCurrentTenantId() @@ -195,7 +176,7 @@ public bool IsGrantedCustom(long userId, string permissionName, EntityReferenceD /// Tenant Id /// Permissioned Entity /// - private string GetPermissionsCacheKey(long userId, int? tenantId, EntityReferenceDto permissionedEntity = null) + private string GetPermissionsCacheKey(long userId, int? tenantId, EntityReferenceDto? permissionedEntity = null) { if (permissionedEntity == null) return userId + "@" + (tenantId ?? 0); @@ -286,4 +267,4 @@ public bool IsGranted(long userId, string permissionName, EntityReferenceDto _permissionDefinitionRepository; - private readonly IAuthorizationConfiguration _authorizationConfiguration; - private readonly IShaPermissionChecker _shaPermissionChecker; public ShaPermissionManager( IIocManager iocManager, @@ -36,16 +34,15 @@ IShaPermissionChecker shaPermissionChecker base(iocManager, authorizationConfiguration, unitOfWorkManager, multiTenancyConfig) { _iocManager = iocManager; - _authorizationConfiguration = authorizationConfiguration; _permissionDefinitionRepository = permissionDefinitionRepository; - _shaPermissionChecker = shaPermissionChecker; } [UnitOfWork] public override void Initialize() { base.Initialize(); - InitializeDbPermissionsAsync().WaitAndUnwrapException(); + + AsyncHelper.RunSync(async () => await InitializeDbPermissionsAsync()); } @@ -75,10 +72,10 @@ private async Task InitializeDbPermissionsAsync() var permission = GetPermissionOrNull(dbPermission.Parent); while (permission == null && dbPermissions.Any(x => x.Name == dbPermission?.Parent)) { - dbPermission = dbPermissions.FirstOrDefault(x => x.Name == dbPermission?.Parent); + dbPermission = dbPermissions.First(x => x.Name == dbPermission?.Parent); permission = GetPermissionOrNull(dbPermission?.Parent); - } - + } + if (permission != null) { await CreateChildPermissionsAsync(dbPermissions, permission); @@ -86,9 +83,9 @@ private async Task InitializeDbPermissionsAsync() else { // remove permission with missed parent - await _permissionDefinitionRepository.DeleteAsync(dbPermission); + await _permissionDefinitionRepository.DeleteAsync(dbPermission.NotNull()); } - dbPermissions.Remove(dbPermission); + dbPermissions.Remove(dbPermission.NotNull()); } } } @@ -107,70 +104,66 @@ private async Task CreateChildPermissionsAsync(List dbPerm } } - public Abp.Authorization.Permission CreatePermission(Abp.Authorization.Permission parent, string name, ILocalizableString displayName = null, - ILocalizableString description = null, + public Abp.Authorization.Permission CreatePermission(Abp.Authorization.Permission parent, string name, ILocalizableString? displayName = null, + ILocalizableString? description = null, MultiTenancySides multiTenancySides = MultiTenancySides.Tenant | MultiTenancySides.Host, - IFeatureDependency featureDependency = null, Dictionary properties = null) + IFeatureDependency? featureDependency = null, Dictionary? properties = null) { throw new NotImplementedException(); } - public Abp.Authorization.Permission EditPermission(string oldName, Abp.Authorization.Permission parent, string name, ILocalizableString displayName = null, - ILocalizableString description = null, + public Abp.Authorization.Permission EditPermission(string oldName, Abp.Authorization.Permission parent, string name, ILocalizableString? displayName = null, + ILocalizableString? description = null, MultiTenancySides multiTenancySides = MultiTenancySides.Tenant | MultiTenancySides.Host, - IFeatureDependency featureDependency = null, Dictionary properties = null) + IFeatureDependency? featureDependency = null, Dictionary? properties = null) { throw new NotImplementedException(); } - public async Task CreatePermissionAsync(PermissionDefinition permission) + public async Task CreatePermissionAsync(PermissionDefinition permissionDefinition) { - var newPermission = await _CreatePermissionAsync(permission); + var newPermission = await CreatePermissionInternalAsync(permissionDefinition); - await _permissionDefinitionRepository.InsertOrUpdateAsync(permission); + await _permissionDefinitionRepository.InsertOrUpdateAsync(permissionDefinition); Permissions.AddAllPermissions(); return newPermission; } - private Task _CreatePermissionAsync(PermissionDefinition permission) + private Task CreatePermissionInternalAsync(PermissionDefinition permissionDefinition) { - Abp.Authorization.Permission newPermission = null; - if (!string.IsNullOrEmpty(permission.Parent)) + if (!string.IsNullOrEmpty(permissionDefinition.Parent)) { // add new permission to parent - var parent = GetPermission(permission.Parent); - if (parent != null) - { - newPermission = parent.CreateChildPermission(permission.Name, - (permission.Label ?? "").L(), - (permission.Description ?? "").L(), - properties: new Dictionary() - { - { "IsDbPermission", true }, - { "ModuleId", permission.Module?.Id }, - } - ); - } + var parent = GetPermission(permissionDefinition.Parent); + var permission = parent.CreateChildPermission(permissionDefinition.Name, + (permissionDefinition.Label ?? "").L(), + (permissionDefinition.Description ?? "").L(), + properties: new Dictionary() + { + { "IsDbPermission", true }, + { "ModuleId", permissionDefinition.Module?.Id }, + } + ); + return Task.FromResult(permission); } else { - newPermission = CreatePermission( - permission.Name, - (permission.Label ?? "").L(), - (permission.Description ?? "").L(), - properties: new Dictionary() + var permission = CreatePermission( + permissionDefinition.Name, + (permissionDefinition.Label ?? "").L(), + (permissionDefinition.Description ?? "").L(), + properties: new Dictionary() { { "IsDbPermission", true }, - { "ModuleId", permission.Module?.Id }, + { "ModuleId", permissionDefinition.Module?.Id }, } ); + return Task.FromResult(permission); } - - return Task.FromResult(newPermission); } - public async Task EditPermissionAsync(string oldName, PermissionDefinition permission) + public async Task EditPermissionAsync(string oldName, PermissionDefinition permissionDefinition) { var dbPermission = _permissionDefinitionRepository.GetAll().FirstOrDefault(x => x.Name == oldName); @@ -180,25 +173,25 @@ public Abp.Authorization.Permission EditPermission(string oldName, Abp.Authoriza throw new EntityNotFoundException($"Permission '{oldName}' not found"); } - if (dbPermission.Name != permission.Name - || dbPermission.Label != permission.Label - || dbPermission.Description != permission.Description - || dbPermission.Module != permission.Module + if (dbPermission.Name != permissionDefinition.Name + || dbPermission.Label != permissionDefinition.Label + || dbPermission.Description != permissionDefinition.Description + || dbPermission.Module != permissionDefinition.Module ) { - dbPermission.Name = permission.Name; - dbPermission.Description = permission.Description; - dbPermission.Label = permission.Label; - dbPermission.Parent = permission.Parent; + dbPermission.Name = permissionDefinition.Name; + dbPermission.Description = permissionDefinition.Description; + dbPermission.Label = permissionDefinition.Label; + dbPermission.Parent = permissionDefinition.Parent; RemovePermission(oldName); - var parent = !string.IsNullOrEmpty(permission.Parent) - ? GetPermissionOrNull(permission.Parent) + var parent = !string.IsNullOrEmpty(permissionDefinition.Parent) + ? GetPermissionOrNull(permissionDefinition.Parent) : null; parent?.RemoveChildPermission(oldName); - var newPermission = await _CreatePermissionAsync(permission); + var newPermission = await CreatePermissionInternalAsync(permissionDefinition); await _permissionDefinitionRepository.InsertOrUpdateAsync(dbPermission); @@ -210,7 +203,7 @@ public Abp.Authorization.Permission EditPermission(string oldName, Abp.Authoriza return GetPermissionOrNull(oldName); } - public async Task UpdateParentAsync(string name, string parentName, Module module) + public async Task UpdateParentAsync(string name, string? parentName, Module? module) { var dbPermission = _permissionDefinitionRepository.GetAll().FirstOrDefault(x => x.Name == name); @@ -224,7 +217,7 @@ public async Task UpdateParentAsync(string name, string parentName, Module modul dbPermission.Parent = parentName; dbPermission.Module = module; - await _CreatePermissionAsync(dbPermission); + await CreatePermissionInternalAsync(dbPermission); Permissions.AddAllPermissions(); await _permissionDefinitionRepository.UpdateAsync(dbPermission); @@ -279,7 +272,7 @@ private void DeleteMissedDbPermission(string name) public override void RemovePermission(string name) { - Abp.Authorization.Permission parent = null; + Abp.Authorization.Permission? parent = null; var permissions = GetAllPermissions(); foreach (var permission in permissions) { diff --git a/shesha-core/src/Shesha.Framework/Authorization/Users/User.cs b/shesha-core/src/Shesha.Framework/Authorization/Users/User.cs index 22522ca8ee..ab099367c2 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/Users/User.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/Users/User.cs @@ -34,7 +34,7 @@ public static string CreateRandomPassword() /// Email address must be unique for it's tenant. /// [StringLength(MaxEmailAddressLength)] - public override string EmailAddress { get; set; } + public override string? EmailAddress { get; set; } public override void SetNormalizedNames() { @@ -44,7 +44,7 @@ public override void SetNormalizedNames() [Display(Name = "Authentication Guid")] [StringLength(36)] - public virtual string AuthenticationGuid { get; set; } + public virtual string? AuthenticationGuid { get; set; } [Display(Name = "Authentication Guid Expiry Date")] public virtual DateTime? AuthenticationGuidExpiresOn { get; set; } @@ -101,7 +101,7 @@ public override void SetNormalizedNames() [SaveAsJson] [Display(Name = "Allow only selected FrontEnd Applications", Description = "Leave empty to allow any FrontEnd Applications")] - public virtual List AllowedFrontEndApps { get; set; } + public virtual List? AllowedFrontEndApps { get; set; } public static User CreateUser(int? tenantId, string username, string emailAddress) { diff --git a/shesha-core/src/Shesha.Framework/Authorization/Users/UserManager.cs b/shesha-core/src/Shesha.Framework/Authorization/Users/UserManager.cs index 87e41b8dd7..eb71c0593e 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/Users/UserManager.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/Users/UserManager.cs @@ -7,7 +7,6 @@ using Abp.Json; using Abp.Organizations; using Abp.Runtime.Caching; -using Abp.Runtime.Validation; using Abp.UI; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; @@ -17,7 +16,9 @@ using Shesha.Configuration; using Shesha.Configuration.Security; using Shesha.Extensions; +using Shesha.Reflection; using Shesha.Settings; +using Shesha.Validations; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -206,7 +207,17 @@ public override Task IsGrantedAsync(User user, Abp.Authorization.Permissio /// /// Returns the User object representing the newly created User Account. If parameters were incorrect will through /// a AbpValidationException exception that can be allowed through to the calling web app. - public async Task CreateUserAsync(string username, bool createLocalPassword, string password, string passwordConfirmation, string firstname, string lastname, string mobileNumber, string emailAddress, long? supportedPasswordResetMethods = null) + public async Task CreateUserAsync( + string username, + bool createLocalPassword, + string? password, + string? passwordConfirmation, + string? firstname, + string? lastname, + string? mobileNumber, + string? emailAddress, + long? supportedPasswordResetMethods = null + ) { var validationResults = new List(); @@ -231,8 +242,7 @@ public async Task CreateUserAsync(string username, bool createLocalPasswor validationResults.Add(new ValidationResult("Password Confirmation must be the same as Password")); } - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); // 1. create user var user = new User() @@ -253,7 +263,7 @@ public async Task CreateUserAsync(string username, bool createLocalPasswor await this.InitializeOptionsAsync(AbpSession.TenantId); var newPassword = createLocalPassword - ? password + ? password.NotNullOrWhiteSpace() : Guid.NewGuid().ToString(); CheckErrors(await CreateAsync(user, newPassword)); @@ -278,40 +288,45 @@ private async Task UserNameAlreadyInUseAsync(string username) return await Users.Where(u => u.NormalizedUserName == normalizedUsername).AnyAsync(); } + private IdentityOptions GetOptionsCopy() + { + return JsonConvert.DeserializeObject(_optionsAccessor.Value.ToJsonString()) ?? throw new Exception($"Failed to make a copy of '{nameof(IdentityOptions)}'"); + } + public override void InitializeOptions(int? tenantId) { - Options = JsonConvert.DeserializeObject(_optionsAccessor.Value.ToJsonString()); + Options = GetOptionsCopy(); //Lockout - Options.Lockout.AllowedForNewUsers = _securitySettings.UserLockOutEnabled.GetValue(); - var lockoutSecs = _securitySettings.DefaultAccountLockoutSeconds.GetValue(); + Options.Lockout.AllowedForNewUsers = _securitySettings.UserLockOutEnabled.GetValueOrNull(); + var lockoutSecs = _securitySettings.DefaultAccountLockoutSeconds.GetValueOrNull(); Options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromSeconds(lockoutSecs); - Options.Lockout.MaxFailedAccessAttempts = _securitySettings.MaxFailedAccessAttemptsBeforeLockout.GetValue(); + Options.Lockout.MaxFailedAccessAttempts = _securitySettings.MaxFailedAccessAttemptsBeforeLockout.GetValueOrNull(); //Password complexity - Options.Password.RequireDigit = _passwordComplexitySettings.RequireDigit.GetValue(); - Options.Password.RequireLowercase = _passwordComplexitySettings.RequireLowercase.GetValue(); - Options.Password.RequireNonAlphanumeric = _passwordComplexitySettings.RequireNonAlphanumeric.GetValue(); - Options.Password.RequireUppercase = _passwordComplexitySettings.RequireUppercase.GetValue(); - Options.Password.RequiredLength = _passwordComplexitySettings.RequiredLength.GetValue(); + Options.Password.RequireDigit = _passwordComplexitySettings.RequireDigit.GetValueOrNull(); + Options.Password.RequireLowercase = _passwordComplexitySettings.RequireLowercase.GetValueOrNull(); + Options.Password.RequireNonAlphanumeric = _passwordComplexitySettings.RequireNonAlphanumeric.GetValueOrNull(); + Options.Password.RequireUppercase = _passwordComplexitySettings.RequireUppercase.GetValueOrNull(); + Options.Password.RequiredLength = _passwordComplexitySettings.RequiredLength.GetValueOrNull(); } public override async Task InitializeOptionsAsync(int? tenantId) { - Options = JsonConvert.DeserializeObject(_optionsAccessor.Value.ToJsonString()); + Options = GetOptionsCopy(); //Lockout - Options.Lockout.AllowedForNewUsers = await _securitySettings.UserLockOutEnabled.GetValueAsync(); - var lockoutSecs = await _securitySettings.DefaultAccountLockoutSeconds.GetValueAsync(); + Options.Lockout.AllowedForNewUsers = await _securitySettings.UserLockOutEnabled.GetValueOrNullAsync(); + var lockoutSecs = await _securitySettings.DefaultAccountLockoutSeconds.GetValueOrNullAsync(); Options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromSeconds(lockoutSecs); - Options.Lockout.MaxFailedAccessAttempts = await _securitySettings.MaxFailedAccessAttemptsBeforeLockout.GetValueAsync(); + Options.Lockout.MaxFailedAccessAttempts = await _securitySettings.MaxFailedAccessAttemptsBeforeLockout.GetValueOrNullAsync(); //Password complexity - Options.Password.RequireDigit = await _passwordComplexitySettings.RequireDigit.GetValueAsync(); - Options.Password.RequireLowercase = await _passwordComplexitySettings.RequireLowercase.GetValueAsync(); - Options.Password.RequireNonAlphanumeric = await _passwordComplexitySettings.RequireNonAlphanumeric.GetValueAsync(); - Options.Password.RequireUppercase = await _passwordComplexitySettings.RequireUppercase.GetValueAsync(); - Options.Password.RequiredLength = await _passwordComplexitySettings.RequiredLength.GetValueAsync(); + Options.Password.RequireDigit = await _passwordComplexitySettings.RequireDigit.GetValueOrNullAsync(); + Options.Password.RequireLowercase = await _passwordComplexitySettings.RequireLowercase.GetValueOrNullAsync(); + Options.Password.RequireNonAlphanumeric = await _passwordComplexitySettings.RequireNonAlphanumeric.GetValueOrNullAsync(); + Options.Password.RequireUppercase = await _passwordComplexitySettings.RequireUppercase.GetValueOrNullAsync(); + Options.Password.RequiredLength = await _passwordComplexitySettings.RequiredLength.GetValueOrNullAsync(); } } } diff --git a/shesha-core/src/Shesha.Framework/Authorization/Users/UserRegistrationManager.cs b/shesha-core/src/Shesha.Framework/Authorization/Users/UserRegistrationManager.cs index 5b79868c2c..803a111047 100644 --- a/shesha-core/src/Shesha.Framework/Authorization/Users/UserRegistrationManager.cs +++ b/shesha-core/src/Shesha.Framework/Authorization/Users/UserRegistrationManager.cs @@ -16,8 +16,8 @@ namespace Shesha.Authorization.Users { public class UserRegistrationManager : DomainService { - public IAbpSession AbpSession { get; set; } - + public IAbpSession AbpSession { get; set; } = NullAbpSession.Instance; + private readonly TenantManager _tenantManager; private readonly UserManager _userManager; private readonly RoleManager _roleManager; @@ -37,7 +37,7 @@ public UserRegistrationManager( AbpSession = NullAbpSession.Instance; } - public async Task RegisterAsync(string name, string surname, string emailAddress, string userName, string plainPassword, bool isEmailConfirmed) + public async Task RegisterAsync(string name, string? surname, string emailAddress, string userName, string plainPassword, bool isEmailConfirmed) { CheckForTenant(); @@ -78,28 +78,27 @@ private void CheckForTenant() } } - private async Task GetActiveTenantAsync() + private async Task GetActiveTenantOrNullAsync() { if (!AbpSession.TenantId.HasValue) - { return null; - } return await GetActiveTenantAsync(AbpSession.TenantId.Value); } + private async Task GetActiveTenantAsync() + { + return await GetActiveTenantOrNullAsync() ?? throw new Exception("Tenant is not available"); + } + private async Task GetActiveTenantAsync(int tenantId) { var tenant = await _tenantManager.FindByIdAsync(tenantId); if (tenant == null) - { throw new UserFriendlyException(L("UnknownTenantId{0}", tenantId)); - } if (!tenant.IsActive) - { throw new UserFriendlyException(L("TenantIdIsNotActive{0}", tenantId)); - } return tenant; } diff --git a/shesha-core/src/Shesha.Framework/AutoMapper/AutoMapperExtensions.cs b/shesha-core/src/Shesha.Framework/AutoMapper/AutoMapperExtensions.cs index 363b3f8488..d46f1f5d93 100644 --- a/shesha-core/src/Shesha.Framework/AutoMapper/AutoMapperExtensions.cs +++ b/shesha-core/src/Shesha.Framework/AutoMapper/AutoMapperExtensions.cs @@ -2,6 +2,7 @@ using AutoMapper; using Shesha.AutoMapper.Dto; using Shesha.Domain; +using Shesha.Extensions; using Shesha.Reflection; using Shesha.Services; using System; @@ -111,15 +112,18 @@ public static IMappingExpression MapReferenceListValuesTo .Select(d => { var source = sourceType.GetProperty(d.Name); - return new + var reflistId = source?.GetReferenceListIdentifierOrNull(); + return source != null && reflistId != null + ? new { DstProperty = d, SrcProperty = source, - RefListIdentifier = source?.GetReferenceListIdentifierOrNull() - }; + RefListIdentifier = reflistId + } + : null; } ) - .Where(i => i.RefListIdentifier != null) + .WhereNotNull() .ToList(); foreach (var item in refListProperties) @@ -132,7 +136,7 @@ public static IMappingExpression MapReferenceListValuesTo return expression; } - private static ReferenceListItemValueDto GetRefListItemValueDto(string refListModule, string refListName, object value) + private static ReferenceListItemValueDto? GetRefListItemValueDto(string? refListModule, string refListName, object? value) { var intValue = value != null ? Convert.ToInt64(value) @@ -147,7 +151,7 @@ private static ReferenceListItemValueDto GetRefListItemValueDto(string refListMo : null; } - private static string GetRefListItemText(string refListModule, string refListName, Int64? value) + private static string? GetRefListItemText(string? refListModule, string refListName, Int64? value) { if (value == null) return null; @@ -172,8 +176,10 @@ public static IMappingExpression MapReferenceListValuesFr .Select(p => { var destination = destinationType.GetProperty(p.Name); + if (destination == null) + return null; - var propType = destination?.PropertyType.GetUnderlyingTypeIfNullable(); + var propType = destination.PropertyType.GetUnderlyingTypeIfNullable(); if (propType == null || propType != typeof(int) && !propType.IsEnum) return null; @@ -185,7 +191,7 @@ public static IMappingExpression MapReferenceListValuesFr }; } ) - .Where(i => i != null) + .WhereNotNull() .ToList(); foreach (var item in refListProperties) @@ -206,8 +212,10 @@ public static IMappingExpression MapMultiValueReferenceListValuesToDto( .Select(p => { var destinationProperty = destinationType.GetProperty(p.Name); + if (destinationProperty == null) + return null; - var propType = destinationProperty?.PropertyType.GetUnderlyingTypeIfNullable(); + var propType = destinationProperty.PropertyType.GetUnderlyingTypeIfNullable(); if (propType == null || !propType.IsSubtypeOfGeneric(typeof(List<>))) return null; @@ -223,7 +231,7 @@ public static IMappingExpression MapMultiValueReferenceListValuesToDto( }; } ) - .Where(i => i != null) + .WhereNotNull() .ToList(); foreach (var item in multiValueRefListProperties) @@ -247,7 +255,7 @@ private static object GetDecomposedMultiValueRefListValue(object owner, Property if (intVal == null) { var resultType = typeof(List<>).MakeGenericType(itemType); - return Activator.CreateInstance(resultType); + return ActivatorHelper.CreateNotNullObject(resultType); } return Shesha.Extensions.EntityExtensions.DecomposeIntoBitFlagComponents(intVal); @@ -264,8 +272,10 @@ public static IMappingExpression MapMultiValueReferenceListValuesFromDto( .Select(p => { var sourceProperty = sourceType.GetProperty(p.Name); + if (sourceProperty == null) + return null; - var propType = sourceProperty?.PropertyType.GetUnderlyingTypeIfNullable(); + var propType = sourceProperty.PropertyType.GetUnderlyingTypeIfNullable(); if (propType == null || !propType.IsSubtypeOfGeneric(typeof(List<>))) return null; @@ -281,7 +291,7 @@ public static IMappingExpression MapMultiValueReferenceListValuesFromDto( }; } ) - .Where(i => i != null) + .WhereNotNull() .ToList(); foreach (var item in multiValueRefListProperties) @@ -292,7 +302,7 @@ public static IMappingExpression MapMultiValueReferenceListValuesFromDto( return expression; } - private static object GetMultiValueRefListValue(object owner, PropertyInfo srcProperty, PropertyInfo dstProperty) + private static object? GetMultiValueRefListValue(object owner, PropertyInfo srcProperty, PropertyInfo dstProperty) { var listValue = owner != null ? srcProperty.GetValue(owner) @@ -316,7 +326,7 @@ private static object GetMultiValueRefListValue(object owner, PropertyInfo srcPr return null; } - private static object GetRefListItemValue(ReferenceListItemValueDto dto, Type srcPropType, Type dstPropType) + private static object? GetRefListItemValue(ReferenceListItemValueDto? dto, Type srcPropType, Type dstPropType) { if (dto?.ItemValue == null) return null; @@ -328,11 +338,11 @@ private static object GetRefListItemValue(ReferenceListItemValueDto dto, Type sr if (dstType.IsEnum) { var enumUnderlayingType = dstType.GetEnumUnderlyingType(); - var numericValue = System.Convert.ChangeType(dto.ItemValue.Value, enumUnderlayingType); + var numericValue = System.Convert.ChangeType(dto.ItemValue, enumUnderlayingType); return Enum.ToObject(dstType, numericValue); } - return System.Convert.ChangeType(dto.ItemValue.Value, dstType); + return System.Convert.ChangeType(dto.ItemValue, dstType); } } } diff --git a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/AutocompleteItemDto.cs b/shesha-core/src/Shesha.Framework/AutoMapper/Dto/AutocompleteItemDto.cs index 10bf4339ca..baa618304f 100644 --- a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/AutocompleteItemDto.cs +++ b/shesha-core/src/Shesha.Framework/AutoMapper/Dto/AutocompleteItemDto.cs @@ -5,7 +5,7 @@ /// public class AutocompleteItemDto { - public string Value { get; set; } - public string DisplayText { get; set; } + public required string Value { get; set; } + public required string DisplayText { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/ChildEntityDto.cs b/shesha-core/src/Shesha.Framework/AutoMapper/Dto/ChildEntityDto.cs deleted file mode 100644 index 87e390e770..0000000000 --- a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/ChildEntityDto.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Abp.Application.Services.Dto; - -namespace Shesha.AutoMapper.Dto -{ - /// - /// Generic Dto for an entity that has an link to other entity as owner - /// - public class ChildEntityDto : EntityDto - { - /// - /// Id of the owning entity - /// - public virtual string OwnerId { get; set; } - - /// - /// Type short alias of the owning entity - /// - public virtual string OwnerType { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/ChildEntityGetListInputDto.cs b/shesha-core/src/Shesha.Framework/AutoMapper/Dto/ChildEntityGetListInputDto.cs deleted file mode 100644 index e0df078f8f..0000000000 --- a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/ChildEntityGetListInputDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Abp.Application.Services.Dto; -using Shesha.Application.Services.Dto; - -namespace Shesha.AutoMapper.Dto -{ - /// - /// Generic Dto for getting list of child entities by owner - /// - public class ChildEntityGetListInputDto : FilteredPagedAndSortedResultRequestDto - { - /// - /// Id of the owner entity - /// - public virtual string OwnerId { get; set; } - - /// - /// Type short alias of the owner entity - /// - public virtual string OwnerType { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/EntityLinkDto.cs b/shesha-core/src/Shesha.Framework/AutoMapper/Dto/EntityLinkDto.cs deleted file mode 100644 index 716ee9ce15..0000000000 --- a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/EntityLinkDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Shesha.AutoMapper.Dto -{ - /// - /// Generic Dto for an entity link - /// - public class EntityLinkDto - { - /// - /// Id of the entity - /// - public virtual string EntityId { get; set; } - - /// - /// Type short alias of the entity - /// - public virtual string EntityType { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/EntityReferenceDto.cs b/shesha-core/src/Shesha.Framework/AutoMapper/Dto/EntityReferenceDto.cs index 14a2133058..383d620787 100644 --- a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/EntityReferenceDto.cs +++ b/shesha-core/src/Shesha.Framework/AutoMapper/Dto/EntityReferenceDto.cs @@ -17,7 +17,7 @@ public EntityReferenceDto() } // ToDo: AS - swap displayName and className - public EntityReferenceDto(TPrimaryKey id, string displayName, string className) + public EntityReferenceDto(TPrimaryKey id, string? displayName, string className) { Id = id; _displayName = displayName; @@ -30,18 +30,18 @@ public EntityReferenceDto(IEntity entity) { Id = entity.GetId(); _displayName = entity.GetDisplayName(); - _className = entity.GetType().FullName; + _className = entity.GetType().GetRequiredFullName(); } } /// /// Entity display name /// - public string _displayName { get; internal set; } + public string? _displayName { get; init; } /// /// Entity class name /// - public string _className { get; internal set; } + public string? _className { get; internal set; } } } diff --git a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/ReferenceListItemValueDto.cs b/shesha-core/src/Shesha.Framework/AutoMapper/Dto/ReferenceListItemValueDto.cs index ed81ba0290..a71364fecf 100644 --- a/shesha-core/src/Shesha.Framework/AutoMapper/Dto/ReferenceListItemValueDto.cs +++ b/shesha-core/src/Shesha.Framework/AutoMapper/Dto/ReferenceListItemValueDto.cs @@ -4,7 +4,7 @@ namespace Shesha.AutoMapper.Dto { public class ReferenceListItemValueDto { - public string Item { get; set; } - public Int64? ItemValue { get; set; } + public string? Item { get; set; } + public Int64 ItemValue { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/AutoMapper/EntityLinkCollectionConverter.cs b/shesha-core/src/Shesha.Framework/AutoMapper/EntityLinkCollectionConverter.cs deleted file mode 100644 index 95db1a2b7a..0000000000 --- a/shesha-core/src/Shesha.Framework/AutoMapper/EntityLinkCollectionConverter.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Abp.Dependency; -using Abp.Domain.Entities; -using Abp.Domain.Repositories; -using AutoMapper; -using Shesha.AutoMapper.Dto; -using Shesha.Domain; -using Shesha.Services; - -namespace Shesha.AutoMapper -{ - /// - /// Entity list update after receiving the list of EntityLinks from client. Note: OwnerKey+OwnerId is the key here, this is not to be used for cases when Id is the key - /// - public class EntityLinkCollectionConverter : ITypeConverter, List> where TSrc: EntityLinkDto where TDest: Entity, IHasOwningEntityLink - { - /// - /// - /// - public List Convert(List sourceItems, List destinationItems, ResolutionContext context) - { - if (sourceItems == null) - return destinationItems; - - // When mapping to a new object, create new collection - var destinationCollection = destinationItems ?? Activator.CreateInstance>(); - - var repository = StaticContext.IocManager.Resolve>(); - - // Update values (key is owner type + id), there rest is values. Id should be ignored - TDest destinationItem = null; - foreach(var sourceItem in sourceItems.Where(source => - (destinationItem = destinationCollection.FirstOrDefault(c => c.OwnerType == source.EntityType && c.OwnerId == source.EntityId)) != null)) - { - var originalId = destinationItem.Id; - destinationItem = context.Mapper.Map(sourceItem, destinationItem); - // Id should be ignored even if exists in DTO because (EntityType,EntityId) is the Id for this mapping - if (!destinationItem.Id.Equals(originalId)) - destinationItem.Id = originalId; - repository.Update(destinationItem); - } - - // Add missing items - foreach (var newItem in sourceItems.Where(source => - !destinationCollection.Any(c => c.OwnerType == source.EntityType && c.OwnerId == source.EntityId))) - { - var newEntity = context.Mapper.Map(newItem); - repository.Insert(newEntity); - destinationCollection.Add(newEntity); - } - - // Delete / soft delete missing items - if (destinationItems != null && destinationItems.Any()) - { - foreach (var missingItem in destinationItems.Where(source => - !sourceItems.Any(c => c.EntityType == source.OwnerType && c.EntityId == source.OwnerId))) - { - // Delete or Soft Delete is chosen by interceptor - repository.Delete(missingItem); - } - } - - return destinationCollection; - } - } -} diff --git a/shesha-core/src/Shesha.Framework/AutoMapper/ShaProfile.cs b/shesha-core/src/Shesha.Framework/AutoMapper/ShaProfile.cs index 5d78e2109e..238463b914 100644 --- a/shesha-core/src/Shesha.Framework/AutoMapper/ShaProfile.cs +++ b/shesha-core/src/Shesha.Framework/AutoMapper/ShaProfile.cs @@ -12,13 +12,13 @@ namespace Shesha.AutoMapper { public abstract class ShaProfile : Profile { - protected static ReferenceListItemValueDto GetRefListItemValueDto(string refListNamespace, string refListName, int? value) + protected static ReferenceListItemValueDto? GetRefListItemValueDto(string refListNamespace, string refListName, int? value) { return GetRefListItemValueDto(null, refListNamespace, refListName, value); } // todo: implement automapper convention for reference lists - protected static ReferenceListItemValueDto GetRefListItemValueDto(string refListModule, string refListNamespace, string refListName, int? value) + protected static ReferenceListItemValueDto? GetRefListItemValueDto(string? refListModule, string refListNamespace, string refListName, int? value) { return value != null ? new ReferenceListItemValueDto @@ -30,7 +30,7 @@ protected static ReferenceListItemValueDto GetRefListItemValueDto(string refList } // todo: implement automapper convention for reference lists - protected static string GetRefListItemText(string refListModule, string refListNamespace, string refListName, int? value) + protected static string? GetRefListItemText(string? refListModule, string refListNamespace, string refListName, int? value) { if (value == null) return null; @@ -39,14 +39,14 @@ protected static string GetRefListItemText(string refListModule, string refListN } // todo: implement automapper convention for nested entities - protected static T GetEntity(EntityDto dto) where T : class, IEntity + protected static T? GetEntity(EntityDto dto) where T : class, IEntity { return dto == null ? null : GetEntity(dto.Id); } - protected static T GetEntity(TId id) where T : class, IEntity + protected static T? GetEntity(TId id) where T : class, IEntity { if (id == null || id is Guid guid && guid == Guid.Empty) return null; @@ -55,7 +55,7 @@ protected static T GetEntity(TId id) where T : class, IEntity return repo.Get(id); } - protected static T GetEntity(EntityReferenceDto dto) where T : class, IEntity + protected static T? GetEntity(EntityReferenceDto dto) where T : class, IEntity { if (dto?.Id == null || dto.Id == Guid.Empty) return null; @@ -64,7 +64,7 @@ protected static T GetEntity(EntityReferenceDto dto) where T : class, return repo.Get(dto.Id.Value); } - protected static T GetEntity(Guid? id) where T : class, IEntity + protected static T? GetEntity(Guid? id) where T : class, IEntity { if (id == null || id == Guid.Empty) return null; diff --git a/shesha-core/src/Shesha.Framework/Bootstrappers/ReferenceListBootstrapper.cs b/shesha-core/src/Shesha.Framework/Bootstrappers/ReferenceListBootstrapper.cs index 142882820e..d2045763b9 100644 --- a/shesha-core/src/Shesha.Framework/Bootstrappers/ReferenceListBootstrapper.cs +++ b/shesha-core/src/Shesha.Framework/Bootstrappers/ReferenceListBootstrapper.cs @@ -9,7 +9,6 @@ using Shesha.Domain.ConfigurationItems; using Shesha.Extensions; using Shesha.Reflection; -using Shesha.Services; using Shesha.Startup; using Shesha.Utilities; using System; @@ -98,6 +97,8 @@ private async Task ProcessAssemblyAsync(Assembly assembly, List lis Logger.Warn($"Bootstrap assembly {assembly.FullName}"); var module = await _moduleManager.GetOrCreateModuleAsync(assembly); + if (module == null) + return; var listNo = 1; foreach (var list in lists) @@ -129,21 +130,16 @@ private async Task ProcessListAsync(Domain.ConfigurationItems.Module module, Ref foreach (var value in values) { var intValue = Convert.ToInt64(value); - var internalName = Enum.GetName(list.Enum, intValue); - var memberInfo = list.Enum.GetMember(internalName).FirstOrDefault(); - - var displayAttribute = memberInfo != null - ? memberInfo.GetAttribute() - : null; + var internalName = Enum.GetName(list.Enum, intValue) ?? throw new Exception($"Value '{intValue}' not found in enum '{list.Enum.FullName}'"); + var memberInfo = list.Enum.GetMember(internalName).Single(); - var descriptionAttribute = memberInfo != null - ? memberInfo.GetAttribute() - : null; + var displayAttribute = memberInfo.GetAttributeOrNull(); + var descriptionAttribute = memberInfo.GetAttributeOrNull(); if (displayAttribute != null && displayAttribute.GetAutoGenerateField() == false) continue; - var refListItemAttribute = memberInfo.GetAttribute(); + var refListItemAttribute = memberInfo.GetAttributeOrNull(); listInCode.Add(new ListItemInfo { @@ -253,12 +249,17 @@ private async Task ProcessListAsync(Domain.ConfigurationItems.Module module, Ref await _listItemRepo.DeleteAsync(item); } - var toUpdate = itemsInDb.Select(idb => new - { - ItemInDB = idb, - UpdatedItemInCode = listInCode.FirstOrDefault(i => i.Value == idb.ItemValue && (i.Name != idb.Item || !idb.HardLinkToApplication)) + var toUpdate = itemsInDb.Select(idb => { + var updatedItemInCode = listInCode.FirstOrDefault(i => i.Value == idb.ItemValue && (i.Name != idb.Item || !idb.HardLinkToApplication)); + return updatedItemInCode != null + ? new + { + ItemInDB = idb, + UpdatedItemInCode = updatedItemInCode + } + : null; }) - .Where(i => i.UpdatedItemInCode != null) + .WhereNotNull() .ToList(); Logger.Info($" items to update: {toUpdate.Count()}"); @@ -290,11 +291,11 @@ public RefListType(Assembly assembly, Type @enum, ReferenceListAttribute attribu private class ListItemInfo { - public string Name { get; set; } - public string Description { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } public Int64 Value { get; set; } public Int64 OrderIndex { get; set; } - public string Color { get; set; } + public string? Color { get; set; } } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Cache/CacheHolder.cs b/shesha-core/src/Shesha.Framework/Cache/CacheHolder.cs new file mode 100644 index 0000000000..db73a98e07 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Cache/CacheHolder.cs @@ -0,0 +1,43 @@ +using Abp.Runtime.Caching; +using System; + +namespace Shesha.Cache +{ + public class CacheHolder : ICacheHolder, IDisposable + { + private readonly string _cacheName; + private bool _disposed; + + private ITypedCache _cache; + + public ITypedCache Cache + { + get + { + ThrowIfDisposed(); + return _cache; + } + } + + public CacheHolder(string cacheName, ICacheManager cacheManager) + { + _cacheName = cacheName; + _cache = cacheManager.GetCache(_cacheName); + } + + public virtual void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _cache.Dispose(); + } + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(GetType().FullName); + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Cache/CacheItemWrapper.cs b/shesha-core/src/Shesha.Framework/Cache/CacheItemWrapper.cs index 60ba615b5f..31500a2d33 100644 --- a/shesha-core/src/Shesha.Framework/Cache/CacheItemWrapper.cs +++ b/shesha-core/src/Shesha.Framework/Cache/CacheItemWrapper.cs @@ -7,9 +7,9 @@ public class CacheItemWrapper { public TItem DefaultValue { get; private set; } - public TItem DbValue { get; private set; } + public TItem? DbValue { get; private set; } - public CacheItemWrapper(TItem defaultValue, TItem dBValue) + public CacheItemWrapper(TItem defaultValue, TItem? dBValue) { DefaultValue = defaultValue; DbValue = dBValue; diff --git a/shesha-core/src/Shesha.Framework/Cache/ICacheHolder.cs b/shesha-core/src/Shesha.Framework/Cache/ICacheHolder.cs new file mode 100644 index 0000000000..64ada49777 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Cache/ICacheHolder.cs @@ -0,0 +1,17 @@ +using Abp.Runtime.Caching; + +namespace Shesha.Cache +{ + /// + /// Cache holder. Is used as a generic wrapper of the ABP cache + /// + /// + /// + public interface ICacheHolder + { + /// + /// Cache + /// + ITypedCache Cache { get; } + } +} diff --git a/shesha-core/src/Shesha.Framework/CodeGeneration/EnumsGenerator.cs b/shesha-core/src/Shesha.Framework/CodeGeneration/EnumsGenerator.cs deleted file mode 100644 index 378d49a7b1..0000000000 --- a/shesha-core/src/Shesha.Framework/CodeGeneration/EnumsGenerator.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Linq; -using System.Text; -using Shesha.Utilities; - -namespace Shesha.CodeGeneration -{ - public class EnumsGenerator - { - public static string GenerateEnumItems(string source) - { - if (source == null) - return null; - - var lines = source.Split('\r', '\n').Select(i => i.Trim()).Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); - - lines = lines.Distinct().OrderBy(i => i).ToList(); - - var sb = new StringBuilder(); - var value = 0; - foreach (var line in lines) - { - var identifier = line - .Replace(',', ' ') - .Replace('.', ' ') - .Replace('-', ' ') - .Replace('=', ' ') - .Replace('(', ' ') - .Replace(')', ' ') - .Replace('/', ' ') - .Replace('\\', ' ') - .RemoveDoubleSpaces() - .Split(' ', '\r', '\n') - .Select(i => i.CapitalizeFirstChar()).Delimited(""); - - value++; - sb.AppendLine(@$"[Description(""{line}"")]"); - sb.AppendLine(@$"{identifier} = {value},"); - sb.AppendLine(""); - } - - return sb.ToString(); - } - } -} diff --git a/shesha-core/src/Shesha.Framework/Configuration/AppConfigurations.cs b/shesha-core/src/Shesha.Framework/Configuration/AppConfigurations.cs index 01e0f9ed49..3d8b6a63f2 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/AppConfigurations.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/AppConfigurations.cs @@ -13,7 +13,7 @@ static AppConfigurations() _configurationCache = new ConcurrentDictionary(); } - public static IConfigurationRoot Get(string path, string environmentName = null, bool addUserSecrets = false) + public static IConfigurationRoot Get(string path, string? environmentName = null, bool addUserSecrets = false) { var cacheKey = path + "#" + environmentName + "#" + addUserSecrets; return _configurationCache.GetOrAdd( @@ -22,7 +22,7 @@ public static IConfigurationRoot Get(string path, string environmentName = null, ); } - private static IConfigurationRoot BuildConfiguration(string path, string environmentName = null, bool addUserSecrets = false) + private static IConfigurationRoot BuildConfiguration(string path, string? environmentName = null, bool addUserSecrets = false) { var builder = new ConfigurationBuilder() .SetBasePath(path) diff --git a/shesha-core/src/Shesha.Framework/Configuration/ConfigurationExtensions.cs b/shesha-core/src/Shesha.Framework/Configuration/ConfigurationExtensions.cs new file mode 100644 index 0000000000..ffd05a998a --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Configuration/ConfigurationExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Configuration; + +namespace Shesha.Configuration +{ + /// + /// IConfiguration extensions + /// + public static class ConfigurationExtensions + { + /// + /// Get required configuration parameter. Throw exception if parameter is unavailable + /// + /// + public static T GetRequiredValue(this IConfiguration configuration, string key) + { + var value = configuration.GetValue(key); + return value ?? throw new ConfigurationErrorsException($"Value with key '{key}' is missing in the configuration"); + } + + /// + /// Get required configuration parameter. Throw exception if parameter is unavailable + /// + /// + public static string GetRequired(this IConfiguration configuration, string key) + { + return configuration.GetRequiredValue(key); + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Configuration/Email/EmailSettings.cs b/shesha-core/src/Shesha.Framework/Configuration/Email/EmailSettings.cs index e10ebfd94e..3e051b12ca 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/Email/EmailSettings.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/Email/EmailSettings.cs @@ -13,6 +13,6 @@ public class EmailSettings /// /// If not null or empty the all outgoing emails will be sent to this email address, is used for testing only /// - public string RedirectAllMessagesTo { get; set; } + public string? RedirectAllMessagesTo { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Configuration/Email/SmtpSettings.cs b/shesha-core/src/Shesha.Framework/Configuration/Email/SmtpSettings.cs index 50a78fe854..887ae7fa5f 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/Email/SmtpSettings.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/Email/SmtpSettings.cs @@ -8,7 +8,7 @@ public class SmtpSettings /// /// SMTP Host name/IP. /// - public string Host { get; set; } + public string? Host { get; set; } /// /// SMTP Port. @@ -18,22 +18,17 @@ public class SmtpSettings /// /// User name to login to SMTP server. /// - public string UserName { get; set; } + public string? UserName { get; set; } /// /// Password to login to SMTP server. /// - public string Password { get; set; } + public string? Password { get; set; } /// /// Domain name to login to SMTP server. /// - public string Domain { get; set; } - - /// - /// Domain name to login to Incoming server. - /// - public string IncomingServer { get; set; } + public string? Domain { get; set; } /// /// Is SSL enabled? @@ -43,12 +38,12 @@ public class SmtpSettings /// /// Default from address. /// - public string DefaultFromAddress { get; set; } + public string? DefaultFromAddress { get; set; } /// /// Default display name. /// - public string DefaultFromDisplayName { get; set; } + public string? DefaultFromDisplayName { get; set; } /// /// Use SMTP relay diff --git a/shesha-core/src/Shesha.Framework/Configuration/IFrontendSettings.cs b/shesha-core/src/Shesha.Framework/Configuration/IFrontendSettings.cs index e3ffdd8688..87e1dd5035 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/IFrontendSettings.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/IFrontendSettings.cs @@ -1,11 +1,5 @@ -using Shesha.Domain; -using Shesha.Settings; -using System; -using System.Collections.Generic; +using Shesha.Settings; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Configuration { @@ -31,5 +25,12 @@ public interface IFrontendSettings: ISettingAccessors [Display(Name = "Default URL", Description = "This is the url the user should be redirected to if the user is not authenticated and does not specify a specific page", GroupName = "Frontend")] [Setting(SheshaSettingNames.DefaultUrl, isClientSpecific: true)] ISettingAccessor DefaultUrl { get; } + + /// + /// Public URL + /// + [Display(Name = "Public URL", Description = "Is used in the notifications (especially emails) to open site links", GroupName = "Frontend")] + [Setting(SheshaSettingNames.PublicUrl, isClientSpecific: true)] + ISettingAccessor PublicUrl { get; } } } diff --git a/shesha-core/src/Shesha.Framework/Configuration/ISheshaSettings.cs b/shesha-core/src/Shesha.Framework/Configuration/ISheshaSettings.cs index d255eae2ac..9b24c838a8 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/ISheshaSettings.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/ISheshaSettings.cs @@ -1,6 +1,5 @@ using Shesha.Domain; using Shesha.Settings; -using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Shesha.Configuration diff --git a/shesha-core/src/Shesha.Framework/Configuration/MainMenuSettings.cs b/shesha-core/src/Shesha.Framework/Configuration/MainMenuSettings.cs index 1b29da3a9b..819c77a37c 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/MainMenuSettings.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/MainMenuSettings.cs @@ -4,7 +4,7 @@ namespace Shesha.Configuration { public class MainMenuSettings { - private static MainMenuSettings defaultInstance; + private static MainMenuSettings? defaultInstance; public static MainMenuSettings Default { get @@ -19,6 +19,6 @@ public static MainMenuSettings Default } public int Version { get; set; } - public List Items { get; set; } + public List Items { get; set; } = new(); } } diff --git a/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/EntityMappingMetadata.cs b/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/EntityMappingMetadata.cs index 72f628ca16..a57b1d64c6 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/EntityMappingMetadata.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/EntityMappingMetadata.cs @@ -5,9 +5,9 @@ /// public class EntityMappingMetadata { - public string TableName { get; set; } - public string SubclassTableName { get; set; } - public string DiscriminatorValue { get; set; } + public string? TableName { get; set; } + public string? SubclassTableName { get; set; } + public string? DiscriminatorValue { get; set; } public bool IsMultiTable { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/IMappingMetadataProvider.cs b/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/IMappingMetadataProvider.cs index 5e394f6328..d078e28620 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/IMappingMetadataProvider.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/IMappingMetadataProvider.cs @@ -9,7 +9,7 @@ public interface IMappingMetadataProvider { EntityMappingMetadata GetEntityMappingMetadata(Type entityType); - PropertyMappingMetadata GetPropertyMappingMetadata(Type entityType, string propertyName); + PropertyMappingMetadata? GetPropertyMappingMetadata(Type entityType, string propertyName); Task UpdateClassNamesAsync(Type entityType, List properties, string oldValue, string newValue, bool replace); } diff --git a/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/PropertyMappingMetadata.cs b/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/PropertyMappingMetadata.cs index c8f25e9250..cb2094ecd0 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/PropertyMappingMetadata.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/MappingMetadata/PropertyMappingMetadata.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Configuration.MappingMetadata +namespace Shesha.Configuration.MappingMetadata { + /// + /// Property mapping metadata + /// public class PropertyMappingMetadata { - public string TableName { get; set; } - public string[] ColumnNames { get; set; } + public required string TableName { get; init; } + public required string[] ColumnNames { get; init; } } } diff --git a/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigManager.cs b/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigManager.cs index e808e5d2b4..6fe99277d6 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigManager.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigManager.cs @@ -2,7 +2,6 @@ using Abp.Domain.Repositories; using Abp.Domain.Uow; using Abp.Runtime.Session; -using AutoMapper; using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Models; using Shesha.Domain; @@ -20,7 +19,6 @@ namespace Shesha.Configuration.Runtime /// inheritedDoc public class EntityConfigManager : ConfigurationItemManager, IEntityConfigManager, ITransientDependency { - private readonly IMapper _mapper; private readonly IRepository _propertyConfigRepo; public IAbpSession AbpSession { get; set; } = NullAbpSession.Instance; @@ -29,19 +27,17 @@ public EntityConfigManager( IRepository repository, IRepository propertyConfigRepo, IRepository moduleRepository, - IUnitOfWorkManager unitOfWorkManager, - IMapper mapper + IUnitOfWorkManager unitOfWorkManager ) : base(repository, moduleRepository, unitOfWorkManager) { _propertyConfigRepo = propertyConfigRepo; - _mapper = mapper; } - public async Task> GetMainDataListAsync(IQueryable query = null, bool? implemented = null) + public async Task> GetMainDataListAsync(IQueryable? query = null, bool? implemented = null) { // Do not change to Mapper to avoid performance issues var result = await (query ?? Repository.GetAll()) - .Where(x => !x.IsDeleted) + .Where(x => !x.IsDeleted && x.Module != null) .Select(x => new EntityConfigDto() { Id = x.Id, @@ -54,12 +50,13 @@ public async Task> GetMainDataListAsync(IQueryable !x.NotImplemented).ToList() : result; @@ -115,19 +112,6 @@ protected EntityConfig MapEntityConfig(EntityConfig src, EntityConfig dest) dest.ViewConfigurations = src.ViewConfigurations.ToList(); - // ToDo: make permissioned objects versioned - /* - if (src.Permission != null) - await _permissionedObjectManager.SetAsync(src.Permission); - if (src.PermissionGet != null) - await _permissionedObjectManager.SetAsync(src.PermissionGet); - if (src.PermissionCreate != null) - await _permissionedObjectManager.SetAsync(src.PermissionCreate); - if (src.PermissionUpdate != null) - await _permissionedObjectManager.SetAsync(src.PermissionUpdate); - if (src.PermissionDelete != null) - await _permissionedObjectManager.SetAsync(src.PermissionDelete); - */ return dest; } @@ -140,10 +124,12 @@ EntityConfig item foreach (var src in properties) { var dbItem = await _propertyConfigRepo.FirstOrDefaultAsync(x => x.Name == src.Name && x.EntityConfig == item) - ?? new EntityProperty(); + ?? new EntityProperty { + EntityConfig = item, + Name = src.Name, + DataType = src.DataType + }; - dbItem.EntityConfig = item; - dbItem.Name = src.Name; dbItem.Label = src.Label; dbItem.Description = src.Description; dbItem.DataType = src.DataType; diff --git a/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfiguration.cs b/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfiguration.cs index 714c8de0bb..981c74ba25 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfiguration.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfiguration.cs @@ -20,10 +20,7 @@ public class EntityConfiguration { private const int _typeShortAliasMaxLength = 100; - private static IDictionary _entityConfigurations = - new Dictionary(); - - private string _typeShortAlias; + private string? _typeShortAlias; #region Constructors and Initialisation @@ -44,11 +41,11 @@ public EntityConfiguration(Type entityType) /// /// The name of the property that will be used to display the entity to the user. /// - public PropertyInfo DisplayNamePropertyInfo { get; set; } + public PropertyInfo? DisplayNamePropertyInfo { get; set; } public bool HasTypeShortAlias => !string.IsNullOrEmpty(_typeShortAlias); - public string SafeTypeShortAlias => _typeShortAlias; + public string? SafeTypeShortAlias => _typeShortAlias; /// /// Type short alias of the entity type (see ) @@ -68,9 +65,13 @@ public string TypeShortAlias return _typeShortAlias; } - set => _typeShortAlias = value; } + public void SetTypeShortAlias(string? value) + { + _typeShortAlias = value; + } + public bool TypeShortAliasIsValid { get @@ -79,13 +80,13 @@ public bool TypeShortAliasIsValid } } - public string FriendlyName { get; set; } - public string Accessor { get; set; } + public string? FriendlyName { get; set; } + public string? Accessor { get; set; } - public string TableName => MappingMetadata?.TableName; - public string DiscriminatorValue => MappingMetadata?.DiscriminatorValue; + public string? TableName => MappingMetadata?.TableName; + public string? DiscriminatorValue => MappingMetadata?.DiscriminatorValue; - private EntityMappingMetadata _mappingMetadata; + private EntityMappingMetadata? _mappingMetadata; private static object _nhMetadataLock = new object(); public EntityMappingMetadata MappingMetadata { @@ -105,7 +106,7 @@ public EntityMappingMetadata MappingMetadata } private readonly object _typeShortAliasesHierarchyLock = new object(); - private List _typeShortAliasesHierarchy; + private List? _typeShortAliasesHierarchy; public List TypeShortAliasesHierarchy { get @@ -135,8 +136,8 @@ public List TypeShortAliasesHierarchy } } - public Type EntityType { get; set; } - public Type IdType => EntityType?.GetEntityIdType(); + public Type EntityType { get; private set; } + public Type IdType => EntityType.GetEntityIdType(); public IList ChangeLogConfigurations = new List(); @@ -149,8 +150,8 @@ public PropertyConfiguration this[string propertyName] if (propertyName.IndexOf('.') > -1) { // Requesting a child property - var propInfo = ReflectionHelper.GetProperty(EntityType, propertyName); - return propInfo.DeclaringType.GetEntityConfiguration()[propInfo.Name]; + var propInfo = ReflectionHelper.GetRequiredProperty(EntityType, propertyName); + return propInfo.DeclaringType.NotNull().GetEntityConfiguration()[propInfo.Name]; } else { @@ -164,7 +165,7 @@ public PropertyConfiguration this[string propertyName] /// /// Type of the default application service /// - public Type ApplicationServiceType { get; set; } + public Type? ApplicationServiceType { get; set; } public class PropertySetChangeLoggingConfiguration { @@ -173,7 +174,6 @@ public PropertySetChangeLoggingConfiguration() AuditedProperties = new List(); } - public virtual string Namespace { get; internal set; } public virtual IList AuditedProperties { get; internal set; } } } diff --git a/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigurationLoaderByReflection.cs b/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigurationLoaderByReflection.cs index f297eb22e0..15a1bbe4de 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigurationLoaderByReflection.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigurationLoaderByReflection.cs @@ -33,7 +33,7 @@ public void LoadConfiguration([NotNull]EntityConfiguration config) .Select(dp => new { IsOwn = ownProps.Contains(dp), Prop = dp }) .OrderByDescending(dp => dp.IsOwn ? 1 : 0) .Select(dp => dp.Prop) - .FirstOrDefault() + .First() ).ToList(); if (propsWithoutDuplicates.Count < props.Count) { @@ -45,8 +45,7 @@ public void LoadConfiguration([NotNull]EntityConfiguration config) { try { - var propConfig = new PropertyConfiguration(config.EntityType); - LoadPropertyConfiguration(prop, propConfig); + var propConfig = LoadPropertyConfiguration(prop, config.EntityType); config.Properties.Add(prop.Name, propConfig); } catch @@ -68,32 +67,35 @@ private void LoadEntityConfiguration(EntityConfiguration config) } config.Accessor = config.EntityType.GetTypeAccessor(); - config.TypeShortAlias = config.EntityType.GetTypeShortAliasOrNull(); + config.SetTypeShortAlias(config.EntityType.GetTypeShortAliasOrNull()); LoadChangeLoggingConfiguration(config); config.DisplayNamePropertyInfo = config.EntityType.GetDisplayNamePropertyInfoOrNull(); } - private static void LoadPropertyConfiguration(PropertyInfo prop, PropertyConfiguration propConfig) + private static PropertyConfiguration LoadPropertyConfiguration(PropertyInfo prop, Type entityType) { - propConfig.PropertyInfo = prop; - propConfig.GeneralType = GetGeneralDataType(prop); - propConfig.Category = prop.GetAttribute()?.Category; + var propConfig = new PropertyConfiguration(entityType) { + PropertyInfo = prop, + GeneralType = GetGeneralDataType(prop), + Category = prop.GetAttributeOrNull()?.Category, + Label = prop.GetDisplayName(), + }; switch (propConfig.GeneralType) { case GeneralDataType.Numeric: case GeneralDataType.ReferenceList: { - var refListAtt = prop.GetAttribute(true); + var refListAtt = prop.GetAttributeOrNull(true); var refListId = refListAtt?.GetReferenceListIdentifier(prop); if (refListId == null) { var underlyingType = prop.PropertyType.GetUnderlyingTypeIfNullable(); - if (underlyingType.IsEnum && underlyingType.HasAttribute()) + if (underlyingType.IsEnum && underlyingType.HasAttribute()) { - refListAtt = underlyingType.GetAttribute(); + refListAtt = underlyingType.GetAttributeOrNull(); refListId = refListAtt?.GetReferenceListIdentifier(underlyingType); } } @@ -103,14 +105,14 @@ private static void LoadPropertyConfiguration(PropertyInfo prop, PropertyConfigu propConfig.ReferenceListName = refListId.Name; propConfig.ReferenceListModule = refListId.Module; - propConfig.ReferenceListOrderByName = refListAtt.OrderByName; + if (refListAtt != null) + propConfig.ReferenceListOrderByName = refListAtt.OrderByName; } + break; } case GeneralDataType.Enum: - var enumType = prop.PropertyType; - if (enumType.IsNullableType()) - enumType = Nullable.GetUnderlyingType(prop.PropertyType); + var enumType = prop.PropertyType.GetUnderlyingTypeIfNullable(); propConfig.EnumType = enumType; break; case GeneralDataType.MultiValueReferenceList: @@ -128,9 +130,9 @@ private static void LoadPropertyConfiguration(PropertyInfo prop, PropertyConfigu break; } - propConfig.Label = prop.GetDisplayName(); - LoadChangeLoggingPropertyConfiguration(prop, propConfig); + + return propConfig; } private void LoadChangeLoggingConfiguration(EntityConfiguration config) @@ -205,7 +207,7 @@ public static GeneralDataType GetGeneralDataType(PropertyInfo propInfo) } else if (underlyingPropType == typeof(DateTime)) { - var dataTypeAtt = propInfo.GetAttribute(); + var dataTypeAtt = propInfo.GetAttributeOrNull(); if (dataTypeAtt != null && dataTypeAtt.GetDataTypeName().Equals("Date", StringComparison.InvariantCultureIgnoreCase)) diff --git a/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigurationStore.cs b/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigurationStore.cs index d162ed28f1..b73436fd1a 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigurationStore.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/Runtime/EntityConfigurationStore.cs @@ -3,7 +3,6 @@ using Shesha.Configuration.Runtime.Exceptions; using Shesha.Domain.Attributes; using Shesha.Extensions; -using Shesha.JsonEntities; using Shesha.Reflection; using System; using System.Collections; @@ -32,8 +31,8 @@ public EntityConfigurationStore(ITypeFinder typeFinder) protected void Initialise() { - var entityTypes = _typeFinder.FindAll().Where(t => t.IsEntityType() || t.IsJsonEntityType() && t != typeof(JsonEntity)) - .Select(t => new { Type = t, TypeShortAlias = t.GetAttribute()?.TypeShortAlias }) + var entityTypes = _typeFinder.FindAll().Where(t => t.IsEntityType() || t.IsJsonEntityType()) // && t != typeof(JsonEntity)) need to add JsonEntity for binding purposes + .Select(t => new { Type = t, TypeShortAlias = t.GetAttributeOrNull()?.TypeShortAlias }) .ToList(); // check for duplicates @@ -42,23 +41,23 @@ protected void Initialise() .GroupBy(i => i.TypeShortAlias, (t, items) => new {TypeShortAlias = t, Types = items.Select(i => i.Type)}) .Where(g => g.Types.Count() > 1).ToList(); if (duplicates.Any()) - throw new DuplicatedTypeShortAliasesException(duplicates.ToDictionary(i => i.TypeShortAlias, i => i.Types)); + throw new DuplicatedTypeShortAliasesException(duplicates.ToDictionary(i => i.TypeShortAlias ?? "empty", i => i.Types)); foreach (var entityType in entityTypes) { - _entityByClassName.Add(entityType.Type.FullName, entityType.Type); + _entityByClassName.Add(entityType.Type.GetRequiredFullName(), entityType.Type); if (!string.IsNullOrWhiteSpace(entityType.TypeShortAlias)) _entityByTypeShortAlias.Add(entityType.TypeShortAlias, entityType.Type); } } - public string GetEntityTypeAlias(Type entityType) + public string? GetEntityTypeAlias(Type entityType) { var entityConfig = Get(entityType); return entityConfig?.TypeShortAlias; } - private Type GetTypeOrNull(string nameOrAlias) + private Type? GetTypeOrNull(string nameOrAlias) { return _entityByTypeShortAlias.ContainsKey(nameOrAlias) ? _entityByTypeShortAlias[nameOrAlias] as Type @@ -68,7 +67,7 @@ private Type GetTypeOrNull(string nameOrAlias) } /// inheritedDoc - public EntityConfiguration GetOrNull(string nameOrAlias) + public EntityConfiguration? GetOrNull(string nameOrAlias) { var type = GetTypeOrNull(nameOrAlias); diff --git a/shesha-core/src/Shesha.Framework/Configuration/Runtime/Exceptions/EntityHasNoTableException.cs b/shesha-core/src/Shesha.Framework/Configuration/Runtime/Exceptions/EntityHasNoTableException.cs new file mode 100644 index 0000000000..be1441c016 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Configuration/Runtime/Exceptions/EntityHasNoTableException.cs @@ -0,0 +1,17 @@ +using System; + +namespace Shesha.Configuration.Runtime.Exceptions +{ + /// + /// Entity type not found exception + /// + public class EntityHasNoTableException : Exception + { + public Type EntityType { get; set; } + + public EntityHasNoTableException(Type entityType) : base($"Entity of type '{entityType.FullName}' is not mapped to DB table") + { + EntityType = entityType; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Configuration/Runtime/Exceptions/MemberHasNoAttributeException.cs b/shesha-core/src/Shesha.Framework/Configuration/Runtime/Exceptions/MemberHasNoAttributeException.cs new file mode 100644 index 0000000000..be1441f8ba --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Configuration/Runtime/Exceptions/MemberHasNoAttributeException.cs @@ -0,0 +1,20 @@ +using System; +using System.Reflection; + +namespace Shesha.Configuration.Runtime.Exceptions +{ + /// + /// Type has no attribute exception + /// + public class MemberHasNoAttributeException : Exception + { + public MemberInfo Member { get; set; } + public Type AttributeType { get; set; } + + public MemberHasNoAttributeException(MemberInfo member, Type attributeType) : base($"Member '{member.Name}' (declaring type: {member.DeclaringType?.FullName}) is not decorated with attribute '{attributeType.FullName}'") + { + Member = member; + AttributeType = attributeType; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Configuration/Runtime/Exceptions/TypeHasNoAttributeException.cs b/shesha-core/src/Shesha.Framework/Configuration/Runtime/Exceptions/TypeHasNoAttributeException.cs new file mode 100644 index 0000000000..83944cfc50 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Configuration/Runtime/Exceptions/TypeHasNoAttributeException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Shesha.Configuration.Runtime.Exceptions +{ + /// + /// Type has no attribute exception + /// + public class TypeHasNoAttributeException : Exception + { + public Type Type { get; set; } + public Type AttributeType { get; set; } + + public TypeHasNoAttributeException(Type type, Type attributeType) : base($"Type '{type.FullName}' is not decorated with attribute '{attributeType.FullName}'") + { + Type = type; + AttributeType = attributeType; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Configuration/Runtime/IEntityConfigManager.cs b/shesha-core/src/Shesha.Framework/Configuration/Runtime/IEntityConfigManager.cs index 3cb4050591..4590e79a44 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/Runtime/IEntityConfigManager.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/Runtime/IEntityConfigManager.cs @@ -25,6 +25,6 @@ public interface IEntityConfigManager : IConfigurationItemManager /// /// /// - Task> GetMainDataListAsync(IQueryable query = null, bool? implemented = null); + Task> GetMainDataListAsync(IQueryable? query = null, bool? implemented = null); } } diff --git a/shesha-core/src/Shesha.Framework/Configuration/Runtime/IEntityConfigurationStore.cs b/shesha-core/src/Shesha.Framework/Configuration/Runtime/IEntityConfigurationStore.cs index e7796f6797..d3e71e507c 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/Runtime/IEntityConfigurationStore.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/Runtime/IEntityConfigurationStore.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace Shesha.Configuration.Runtime { @@ -21,7 +20,7 @@ public interface IEntityConfigurationStore /// /// Returns by class name or type short alias /// - EntityConfiguration GetOrNull(string nameOrAlias); + EntityConfiguration? GetOrNull(string nameOrAlias); /// /// Register default application service for entity type diff --git a/shesha-core/src/Shesha.Framework/Configuration/Runtime/PropertyConfiguration.cs b/shesha-core/src/Shesha.Framework/Configuration/Runtime/PropertyConfiguration.cs index c6f6c1d839..87ad5816bb 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/Runtime/PropertyConfiguration.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/Runtime/PropertyConfiguration.cs @@ -1,8 +1,7 @@ -using System; -using System.Linq; -using System.Reflection; -using Abp.Dependency; +using Shesha.Domain; using Shesha.Services; +using System; +using System.Reflection; namespace Shesha.Configuration.Runtime { @@ -19,52 +18,40 @@ public class PropertyConfiguration /// /// If the property is a ReferenceList, gets or sets the name of the reference list. /// - public string ReferenceListName { get; set; } + public string? ReferenceListName { get; set; } /// /// If the property is a ReferenceList, gets or sets the module name of the reference list. /// - public string ReferenceListModule { get; set; } + public string? ReferenceListModule { get; set; } - public GeneralDataType GeneralType { get; set; } + public required GeneralDataType GeneralType { get; init; } /// /// Returns the Type of the enum referenced by the property /// - public Type EnumType { get; set; } + public Type? EnumType { get; set; } /// /// Returns the Type of the entity referenced by the property, either directly or through an Id /// provided by a property marked with an EntityReference attribute. /// - public Type EntityReferenceType { get; set; } - - /// - /// Name of the property to which any searches will be redirected. - /// E.g. this is useful if the property itself is calculated and therefore not persited to the DB - /// and therefore cannot be queried. - /// - public string BackingSearchProperty { get; set; } + public Type? EntityReferenceType { get; set; } - public PropertyInfo PropertyInfo { get; set; } + public required PropertyInfo PropertyInfo { get; init; } /// /// Label for the property. /// - public string Label { get; set; } + public required string Label { get; init; } public bool LogChanges { get; set; } - public virtual string FixedDescriptionOnChange { get; set; } public virtual bool DetailPropertyOnChange { get; set; } public virtual bool DetailOldValueOnChange { get; set; } public virtual bool DetailNewValueOnChange { get; set; } - /// - /// Provides a categorisation of the type of change logged. - /// - public virtual string AuditLogEntryNamespaceOnChange { get; set; } - public string Category { get; set; } + public string? Category { get; set; } private bool? _isMapped; @@ -97,5 +84,13 @@ public PropertyConfiguration(Type entityType) { EntityType = entityType; } + + public ReferenceListIdentifier GetRefListIdentifier() + { + if (string.IsNullOrWhiteSpace(ReferenceListName)) + throw new Exception($"{nameof(ReferenceListName)} must be specified"); + + return new ReferenceListIdentifier(ReferenceListModule, ReferenceListName); + } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs b/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs index 02dd85cdce..a3bd513929 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs @@ -18,6 +18,8 @@ public static class SheshaSettingNames public const string DefaultUrl = "Shesha.DefaultUrl"; + public const string PublicUrl = "Shesha.PublicUrl"; + public const string DefaultEndpointAccess = "Shesha.DefaultEndpointAccess"; } } diff --git a/shesha-core/src/Shesha.Framework/Configuration/ThemeSettings.cs b/shesha-core/src/Shesha.Framework/Configuration/ThemeSettings.cs index eba9dee186..c4880af436 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/ThemeSettings.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/ThemeSettings.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Configuration +namespace Shesha.Configuration { public class ThemeSettings { - private static ThemeSettings defaultInstance; + private static ThemeSettings? defaultInstance; public static ThemeSettings Default { get @@ -39,24 +33,24 @@ public static ThemeSettings Default } public class ApplicationSettings { - public string PrimaryColor { get; set; } - public string ErrorColor { get; set; } - public string WarningColor { get; set; } - public string SuccessColor { get; set; } - public string InfoColor { get; set; } + public string? PrimaryColor { get; set; } + public string? ErrorColor { get; set; } + public string? WarningColor { get; set; } + public string? SuccessColor { get; set; } + public string? InfoColor { get; set; } } public class TextSettings { - public string Default { get; set; } - public string Secondary { get; set; } + public string? Default { get; set; } + public string? Secondary { get; set; } } - public ThemeSettings.ApplicationSettings Application { get; set; } - public string Sidebar { get; set; } - public string LayoutBackground { get; set; } - public ThemeSettings.TextSettings Text { get; set; } - public string SidebarBackground { get; set; } + public ThemeSettings.ApplicationSettings? Application { get; set; } + public string? Sidebar { get; set; } + public string? LayoutBackground { get; set; } + public ThemeSettings.TextSettings? Text { get; set; } + public string? SidebarBackground { get; set; } public int? LabelSpan { get; set; } public int? ComponentSpan { get; set; } } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/ConfigurationItemCacheItem.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/ConfigurationItemCacheItem.cs index c5baeba98b..b0bf2b9f26 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/ConfigurationItemCacheItem.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/ConfigurationItemCacheItem.cs @@ -5,6 +5,6 @@ /// public class ConfigurationItemCacheItem { - public string Md5 { get; set; } + public string? Md5 { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/ConfigurationItemClientSideCache.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/ConfigurationItemClientSideCache.cs index ac3138480b..f157fd05af 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/ConfigurationItemClientSideCache.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/ConfigurationItemClientSideCache.cs @@ -5,61 +5,104 @@ using Shesha.ConfigurationItems.Models; using Shesha.Domain; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Shesha.ConfigurationItems.Cache { /// inhertiedDoc - public class ConfigurationItemClientSideCache : IConfigurationItemClientSideCache, IEventHandler> , ISingletonDependency + public sealed class ConfigurationItemClientSideCache : IConfigurationItemClientSideCache, IAsyncEventHandler> , ISingletonDependency, IDisposable { private readonly ICacheManager _cacheManager; - protected string CachePrefix => $"{this.GetType().Name}:"; + private bool _disposed; + private readonly Dictionary> _caches; + private string CachePrefix => $"{this.GetType().Name}:"; public ConfigurationItemClientSideCache(ICacheManager cacheManager) { _cacheManager = cacheManager; + _caches = new Dictionary>(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + foreach (var cache in _caches.Values) + cache.Dispose(); + } + + private ITypedCache GetCache(string itemType) + { + if (!_caches.TryGetValue(itemType, out var cache)) + { + cache = _cacheManager.GetCache(CachePrefix + itemType); + _caches[itemType] = cache; + return cache; + } + else + return cache; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP001:Dispose created", Justification = "Caches are disposed as part of main Dispose() mathod")] + private async Task UsingCacheAsync(string itemType, Func, Task> actionAsync) + { + var cache = GetCache(itemType); + return await actionAsync.Invoke(cache); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP001:Dispose created", Justification = "Caches are disposed as part of main Dispose() mathod")] + private async Task UsingCacheAsync(string itemType, Func, Task> actionAsync) + { + var cache = GetCache(itemType); + await actionAsync.Invoke(cache); } /// inhertiedDoc - public async Task GetCachedMd5Async(string itemType, string applicationKey, string module, string name, ConfigurationItemViewMode mode) + public async Task GetCachedMd5Async(string itemType, string? applicationKey, string? module, string name, ConfigurationItemViewMode mode) { var key = GetCacheKey(applicationKey, module, name, mode); - var cache = GetCache(itemType); - var value = await cache.TryGetValueAsync(key); - return value.HasValue ? value.Value.Md5 : null; + + return await UsingCacheAsync(itemType, async(cache) => { + var value = await cache.TryGetValueAsync(key); + return value.HasValue ? value.Value.Md5 : null; + }); } /// inhertiedDoc - public async Task GetCachedMd5Async(string itemType, Guid id) + public async Task GetCachedMd5Async(string itemType, Guid id) { var key = GetCacheKey(id); - var cache = GetCache(itemType); - var value = await cache.TryGetValueAsync(key); - return value.HasValue ? value.Value.Md5 : null; + + return await UsingCacheAsync(itemType, async (cache) => { + var value = await cache.TryGetValueAsync(key); + return value.HasValue ? value.Value.Md5 : null; + }); } /// inhertiedDoc - public async Task SetCachedMd5Async(string itemType, string applicationKey, string module, string name, ConfigurationItemViewMode mode, string md5) + public async Task SetCachedMd5Async(string itemType, string? applicationKey, string? module, string name, ConfigurationItemViewMode mode, string? md5) { var key = GetCacheKey(applicationKey, module, name, mode); - var cache = GetCache(itemType); - await cache.SetAsync(key, new ConfigurationItemCacheItem { Md5 = md5 }); + await UsingCacheAsync(itemType, async (cache) => { + await cache.SetAsync(key, new ConfigurationItemCacheItem { Md5 = md5 }); + }); } /// inhertiedDoc public async Task SetCachedMd5Async(string itemType, Guid id, string md5) { var key = GetCacheKey(id); - var cache = GetCache(itemType); - await cache.SetAsync(key, new ConfigurationItemCacheItem { Md5 = md5 }); - } - - private ITypedCache GetCache(string itemType) - { - return _cacheManager.GetCache(CachePrefix + itemType); + await UsingCacheAsync(itemType, async (cache) => { + await cache.SetAsync(key, new ConfigurationItemCacheItem { Md5 = md5 }); + }); } - private string GetCacheKey(string applicationKey, string module, string name, ConfigurationItemViewMode mode) + private string GetCacheKey(string? applicationKey, string? module, string name, ConfigurationItemViewMode mode) { var key = $"{module}|{name}|{mode}"; @@ -73,18 +116,19 @@ private string GetCacheKey(Guid id) return id.ToString(); } - public void HandleEvent(EntityChangedEventData eventData) + public async Task HandleEventAsync(EntityChangedEventData eventData) { var configItem = eventData.Entity; if (configItem == null || string.IsNullOrWhiteSpace(configItem.ItemType)) return; - var cache = GetCache(configItem.ItemType); - cache.Remove(GetCacheKey(configItem.Id)); - cache.Remove(GetCacheKey(configItem.Application?.AppKey, configItem.Module?.Name, configItem.Name, ConfigurationItemViewMode.Live)); - cache.Remove(GetCacheKey(configItem.Application?.AppKey, configItem.Module?.Name, configItem.Name, ConfigurationItemViewMode.Ready)); - cache.Remove(GetCacheKey(configItem.Application?.AppKey, configItem.Module?.Name, configItem.Name, ConfigurationItemViewMode.Latest)); + await UsingCacheAsync(configItem.ItemType, async (cache) => { + await cache.RemoveAsync(GetCacheKey(configItem.Id)); + await cache.RemoveAsync(GetCacheKey(configItem.Application?.AppKey, configItem.Module?.Name, configItem.Name, ConfigurationItemViewMode.Live)); + await cache.RemoveAsync(GetCacheKey(configItem.Application?.AppKey, configItem.Module?.Name, configItem.Name, ConfigurationItemViewMode.Ready)); + await cache.RemoveAsync(GetCacheKey(configItem.Application?.AppKey, configItem.Module?.Name, configItem.Name, ConfigurationItemViewMode.Latest)); + }); } public async Task ClearAsync() @@ -96,5 +140,13 @@ public async Task ClearAsync() await cache.ClearAsync(); } } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } } } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/IConfigurationItemClientSideCache.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/IConfigurationItemClientSideCache.cs index 3558c6db04..ffb25bb9a5 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/IConfigurationItemClientSideCache.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Cache/IConfigurationItemClientSideCache.cs @@ -18,7 +18,7 @@ public interface IConfigurationItemClientSideCache /// Item Name /// View mode /// - Task GetCachedMd5Async(string itemType, string applicationKey, string module, string name, ConfigurationItemViewMode mode); + Task GetCachedMd5Async(string itemType, string? applicationKey, string? module, string name, ConfigurationItemViewMode mode); /// /// Set cached MD5 for configuration item with specified name @@ -30,7 +30,7 @@ public interface IConfigurationItemClientSideCache /// View mode /// MD5 value /// - Task SetCachedMd5Async(string itemType, string applicationKey, string module, string name, ConfigurationItemViewMode mode, string md5); + Task SetCachedMd5Async(string itemType, string? applicationKey, string? module, string name, ConfigurationItemViewMode mode, string? md5); /// /// Get cached MD5 for configuration item with specified id @@ -38,7 +38,7 @@ public interface IConfigurationItemClientSideCache /// Type of configuration item /// Item id /// - Task GetCachedMd5Async(string itemType, Guid id); + Task GetCachedMd5Async(string itemType, Guid id); /// /// Set cached MD5 for configuration item with specified name diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurableModuleBootstrapper.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurableModuleBootstrapper.cs index 7992be2e12..b9d7689a3b 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurableModuleBootstrapper.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurableModuleBootstrapper.cs @@ -1,5 +1,4 @@ -using Abp.Collections.Extensions; -using Abp.Dependency; +using Abp.Dependency; using Abp.Domain.Repositories; using Abp.Domain.Uow; using Abp.Reflection; @@ -8,6 +7,7 @@ using Shesha.Domain.ConfigurationItems; using Shesha.Extensions; using Shesha.Modules; +using Shesha.Reflection; using Shesha.Startup; using Shesha.Utilities; using System; @@ -64,7 +64,7 @@ private async Task> GetCodeModulesAsync() .ToList(); foreach (var type in moduleTypes) { - var instance = _iocManager.Resolve(type) as SheshaModule; + var instance = _iocManager.Resolve(type).ForceCastAs(); var moduleInfo = instance.ModuleInfo; var version = moduleInfo.UseAssemblyVersion @@ -117,7 +117,7 @@ private async Task DoProcessAsync() .Find(t => t != null && t.IsPublic && !t.IsGenericType && !t.IsAbstract && typeof(ISheshaSubmodule).IsAssignableFrom(t)) .Where(x => !_startupSession.AssemblyStaysUnchanged(x.Assembly)) .Select(t => { - return _iocManager.Resolve(t) as ISheshaSubmodule; + return _iocManager.Resolve(t).ForceCastAs(); }) .ToList(); @@ -169,11 +169,11 @@ private async Task DoProcessAsync() private class ModuleItem { public Guid Id { get; set; } - public Type ModuleType { get; set; } - public SheshaModule Instance { get;set; } - public SheshaModuleInfo ModuleInfo { get; set; } - public string Version { get; set; } - public string Accessor { get; set; } + public required Type ModuleType { get; set; } + public required SheshaModule Instance { get;set; } + public required SheshaModuleInfo ModuleInfo { get; set; } + public string? Version { get; set; } + public string? Accessor { get; set; } public bool IsNewModule { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkMiddleware.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkMiddleware.cs index a96dbf1443..59b06b1875 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkMiddleware.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkMiddleware.cs @@ -1,7 +1,5 @@ using Abp.Dependency; -using Abp.Extensions; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; using Shesha.ConfigurationItems.Models; using System; using System.Threading.Tasks; @@ -42,7 +40,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) a.ViewMode = configItemMode.HasValue ? configItemMode.Value : ConfigurationItemViewMode.Live; - a.FrontEndApplication = frontEndApp.IsNullOrWhiteSpace() + a.FrontEndApplication = string.IsNullOrWhiteSpace(frontEndApp) ? FrontEndAppKeyConsts.SheshaDefaultFrontend : frontEndApp; })) diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkRuntime.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkRuntime.cs index a786cff376..a004695bfc 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkRuntime.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkRuntime.cs @@ -35,7 +35,7 @@ private ConfigurationFrameworkRuntimeState State public ConfigurationItemViewMode ViewMode => State.ViewMode; /// inheritedDoc - public string FrontEndApplication => State.FrontEndApplication; + public string? FrontEndApplication => State.FrontEndApplication; /// inheritedDoc public IDisposable BeginScope(Action initAction) diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkRuntimeState.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkRuntimeState.cs index cd02da2173..2d1c252196 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkRuntimeState.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationFrameworkRuntimeState.cs @@ -10,11 +10,11 @@ public class ConfigurationFrameworkRuntimeState /// /// View mode /// - public ConfigurationItemViewMode ViewMode { get; set; } + public ConfigurationItemViewMode ViewMode { get; set; } = ConfigurationItemViewMode.Live; /// /// Front-end application /// - public string FrontEndApplication { get; set; } + public string? FrontEndApplication { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemManager.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemManager.cs index 2fa7c24fac..a45587c3c5 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemManager.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemManager.cs @@ -1,13 +1,14 @@ -using Abp.Domain.Repositories; +using Abp; +using Abp.Domain.Repositories; using Abp.Domain.Uow; -using Abp.ObjectMapping; using Abp.Runtime.Validation; -using Abp.UI; using Shesha.ConfigurationItems.Models; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Dto.Interfaces; using Shesha.Extensions; +using Shesha.Reflection; +using Shesha.Validations; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -19,7 +20,7 @@ namespace Shesha.ConfigurationItems /// /// Base class of the Configuration Item Manager /// - public abstract class ConfigurationItemManager : IConfigurationItemManager where TItem : ConfigurationItemBase + public abstract class ConfigurationItemManager : AbpServiceBase, IConfigurationItemManager where TItem : ConfigurationItemBase { /// /// Configurable Item type supported by the current manager @@ -28,14 +29,11 @@ public abstract class ConfigurationItemManager : IConfigurationItemManage protected IRepository Repository { get; private set; } protected IRepository ModuleRepository { get; private set; } - protected IUnitOfWorkManager UnitOfWorkManager { get; private set; } - /// - /// Reference to the object to object mapper. - /// - public IObjectMapper ObjectMapper { get; set; } public ConfigurationItemManager(IRepository repository, IRepository moduleRepository, IUnitOfWorkManager unitOfWorkManager) { + LocalizationSourceName = SheshaConsts.LocalizationSourceName; + Repository = repository; ModuleRepository = moduleRepository; UnitOfWorkManager = unitOfWorkManager; @@ -119,8 +117,9 @@ public async virtual Task MoveToModuleAsync(TItem item, MoveItemToModuleInput in ); } - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); + + item.NotNull(); var allVersionsQuery = Repository.GetAll().Where(v => v.Origin == item.Origin); var allVersions = await allVersionsQuery.ToListAsync(); @@ -141,37 +140,37 @@ public virtual async Task DeleteAllVersionsAsync(TItem item) public async Task UpdateStatusAsync(ConfigurationItemBase item, ConfigurationItemVersionStatus status) { - await UpdateStatusAsync(item as TItem, status); + await UpdateStatusAsync((TItem)item, status); } public async Task CopyAsync(ConfigurationItemBase item, CopyItemInput input) { - return await CopyAsync(item as TItem, input) as ConfigurationItemBase; + return await CopyAsync((TItem)item, input) as ConfigurationItemBase; } public async Task CancelVersoinAsync(ConfigurationItemBase item) { - await CancelVersionAsync(item as TItem); + await CancelVersionAsync((TItem)item); } public async Task MoveToModuleAsync(ConfigurationItemBase item, MoveItemToModuleInput input) { - await MoveToModuleAsync(item as TItem, input); + await MoveToModuleAsync((TItem)item, input); } public async Task CreateNewVersionAsync(ConfigurationItemBase item) { - return await CreateNewVersionAsync(item as TItem) as ConfigurationItemBase; + return await CreateNewVersionAsync((TItem)item) as ConfigurationItemBase; } public async Task DeleteAllVersionsAsync(ConfigurationItemBase item) { - await DeleteAllVersionsAsync(item as TItem); + await DeleteAllVersionsAsync((TItem)item); } public async Task MapToDtoAsync(ConfigurationItemBase item) { - return await MapToDtoAsync(item as TItem) as IConfigurationItemDto; + return await MapToDtoAsync((TItem)item) as IConfigurationItemDto; } } } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemsExtensions.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemsExtensions.cs index f01cf9a86b..f98293c44c 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemsExtensions.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemsExtensions.cs @@ -15,20 +15,30 @@ namespace Shesha.ConfigurationItems /// public static class ConfigurationItemsExtensions { + public static IQueryable FilterByFullName(this IQueryable queryable, string? module, string name) where TItem : class, IConfigurationItem + { + return queryable.Where(new ByNameAndModuleSpecification(name, module).ToExpression()); + } + + public static IQueryable GetByByFullName(this IRepository repository, string? module, string name) where TItem : class, IConfigurationItem + { + return repository.GetAll().FilterByFullName(module, name); + } + /// /// Get by , and /// - public static async Task GetItemByFullNameAsync(this IRepository repository, string module, string name, ConfigurationItemViewMode mode) where TItem : class, IConfigurationItem + public static async Task GetItemByFullNameAsync(this IQueryable queryable, string? module, string name, ConfigurationItemViewMode mode) where TItem : class, IConfigurationItem { - var query = repository.GetAll().Where(new ByNameAndModuleSpecification(name, module).ToExpression()); + var query = queryable.FilterByFullName(module, name); switch (mode) { - case ConfigurationItems.Models.ConfigurationItemViewMode.Live: + case ConfigurationItemViewMode.Live: default: query = query.Where(f => f.VersionStatus == ConfigurationItemVersionStatus.Live); break; - case ConfigurationItems.Models.ConfigurationItemViewMode.Ready: + case ConfigurationItemViewMode.Ready: { var statuses = new ConfigurationItemVersionStatus[] { ConfigurationItemVersionStatus.Live, @@ -43,7 +53,7 @@ public static async Task GetItemByFullNameAsync(this IRepository statuses.Contains(f.VersionStatus)).OrderByDescending(f => f.VersionNo); break; } - case ConfigurationItems.Models.ConfigurationItemViewMode.Latest: + case ConfigurationItemViewMode.Latest: { var statuses = new ConfigurationItemVersionStatus[] { ConfigurationItemVersionStatus.Live, @@ -54,16 +64,33 @@ public static async Task GetItemByFullNameAsync(this IRepository + /// Get by , and + /// + public static async Task GetItemByFullNameAsync(this IRepository repository, string module, string name, ConfigurationItemViewMode mode) where TItem : class, IConfigurationItem + { + return await repository.GetAll().GetItemByFullNameAsync(module, name, mode); + } + /// /// Get by and /// public static async Task GetItemByIdAsync(this IRepository repository, ConfigurationItemIdentifier id, ConfigurationItemViewMode mode) where TItem : class, IConfigurationItem { - return await repository.GetItemByFullNameAsync(id.Module, id.Name, mode); + // TODO: make result nullable and add an override with not nullable result + return await repository.GetAll().GetItemByIdAsync(id, mode); + } + + /// + /// Get by and + /// + public static async Task GetItemByIdAsync(this IQueryable queryable, ConfigurationItemIdentifier id, ConfigurationItemViewMode mode) where TItem : class, IConfigurationItem + { + return await queryable.GetItemByFullNameAsync(id.Module, id.Name, mode); } } } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemsIocManagerExtensions.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemsIocManagerExtensions.cs index ca88fabfec..83cd34b637 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemsIocManagerExtensions.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/ConfigurationItemsIocManagerExtensions.cs @@ -1,12 +1,10 @@ using Abp.Dependency; -using Abp.Reflection; using Castle.MicroKernel.Registration; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Reflection; using System; -using System.Linq; namespace Shesha.ConfigurationItems { @@ -24,7 +22,7 @@ public static IIocManager RegisterConfigurableItemManager { iocManager.IocContainer.Register( - Component.For>().Forward().ImplementedBy().LifestyleTransient() + Component.For>().Forward().Forward().ImplementedBy().LifestyleTransient() ); return iocManager; } @@ -32,7 +30,7 @@ public static IIocManager RegisterConfigurableItemManager /// Get manager for the specified type of the /// - public static IConfigurationItemManager GetItemManager(this IIocManager iocManager, Type itemType) + public static IConfigurationItemManager? GetItemManager(this IIocManager iocManager, Type itemType) { itemType = itemType.StripCastleProxyType(); var managerType = typeof(ConfigurationItemBase).IsAssignableFrom(itemType) @@ -49,7 +47,7 @@ public static IConfigurationItemManager GetItemManager(this IIocManager iocManag /// /// Get manager for the specified /// - public static IConfigurationItemManager GetItemManager(this IIocManager iocManager, ConfigurationItemBase item) + public static IConfigurationItemManager? GetItemManager(this IIocManager iocManager, ConfigurationItemBase item) { return iocManager.GetItemManager(item.GetType()); } @@ -64,7 +62,7 @@ public static IIocManager RegisterConfigurableItemExport { iocManager.IocContainer.Register( - Component.For().Forward>().Forward().ImplementedBy().LifestyleTransient() + Component.For().Forward>().Forward().Forward().ImplementedBy().LifestyleTransient() ); return iocManager; } @@ -75,7 +73,7 @@ public static IIocManager RegisterConfigurableItemExportIocManager instance /// Type of the /// - public static IConfigurableItemExport GetItemExporter(this IIocManager iocManager, Type itemType) + public static IConfigurableItemExport? GetItemExporter(this IIocManager iocManager, Type itemType) { var exporterType = typeof(IConfigurableItemExport<>).MakeGenericType(itemType); @@ -95,12 +93,12 @@ public static IIocManager RegisterConfigurableItemImport { iocManager.IocContainer.Register( - Component.For().Forward>().Forward().ImplementedBy().LifestyleTransient() + Component.For().Forward>().Forward().Forward().ImplementedBy().LifestyleTransient() ); return iocManager; } - public static IConfigurableItemImport GetItemImporter(this IIocManager iocManager, Type itemType) + public static IConfigurableItemImport? GetItemImporter(this IIocManager iocManager, Type itemType) { var importerType = typeof(IConfigurableItemImport<>).MakeGenericType(itemType); diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/ConfigurationPackageManager.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/ConfigurationPackageManager.cs index a4ee5921e2..dda49dcc2f 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/ConfigurationPackageManager.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/ConfigurationPackageManager.cs @@ -14,6 +14,7 @@ using System.IO; using System.IO.Compression; using System.Linq; +using System.Linq.Dynamic.Core; using System.Threading.Tasks; namespace Shesha.ConfigurationItems.Distribution @@ -28,7 +29,7 @@ public class ConfigurationPackageManager : IConfigurationPackageManager, ITransi private readonly IRepository _itemsRepository; private readonly IStoredFileService _storedFileService; private readonly IRepository _importResultRepository; - public IIocManager IocManager { get; set; } + public IIocManager IocManager { get; set; } = default!; public ConfigurationPackageManager(IRepository itemsRepository, IStoredFileService storedFileService, IRepository importResultRepository) { @@ -85,10 +86,9 @@ public Task ReadPackageAsync(Stream stream, ReadPacka ModuleName = parts[0], ItemType = parts[1], StreamGetter = () => entry.Open(), - }; - - item.FileName = containsAppKey ? parts[3] : parts[2]; - item.ApplicationKey = containsAppKey ? parts[2] : null; + FileName = containsAppKey ? parts[3] : parts[2], + ApplicationKey = containsAppKey ? parts[2] : null, + }; var importer = context.GetImporter(item.ItemType); if (importer == null) @@ -133,8 +133,10 @@ private async Task ProcessItemExportAsync(ConfigurationItemBase item, Configurat var dependencies = await depsProvider.GetReferencedItemsAsync(item); foreach (var dependency in dependencies) { - var dependencyItem = await _itemsRepository.GetItemByFullNameAsync(dependency.Module, dependency.Name, context.VersionSelectionMode); - + var query = _itemsRepository.GetAll().OfType(dependency.ItemType).Cast(); + + var dependencyItem = await query.GetItemByIdAsync(dependency, context.VersionSelectionMode); + // todo: write log and include all missing items if (dependencyItem != null) await ProcessItemExportAsync(dependencyItem, exportResult, context); @@ -160,7 +162,7 @@ private List GetDependenciesProviders(ConfigurationItemBa return _dependencyProviders[requestedType] = SearchClosestDependenciesProviders(requestedType) ?? new List(); } - private List SearchClosestDependenciesProviders(Type itemType) + private List? SearchClosestDependenciesProviders(Type itemType) { var type = itemType; while (type != null) @@ -217,9 +219,9 @@ async Task updateResultAsync(Action updateAction) foreach (var group in groups) { - var importer = group.FirstOrDefault().Importer; + var importer = group.First().Importer.NotNull(); var items = new List(); - foreach (var item in group) + foreach (var item in group) { context.CancellationToken.ThrowIfCancellationRequested(); try diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/DistributedConfigurableItemBase.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/DistributedConfigurableItemBase.cs index 314a7b5af6..af0f9d776b 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/DistributedConfigurableItemBase.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/DistributedConfigurableItemBase.cs @@ -22,32 +22,32 @@ public class DistributedConfigurableItemBase /// /// Item name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Label of the con /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Item name /// - public string ItemType { get; set; } + public required string ItemType { get; set; } /// /// Item description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Module name /// - public string ModuleName { get; set; } + public string? ModuleName { get; set; } /// /// Front-end application name /// - public string FrontEndApplication { get; set; } + public string? FrontEndApplication { get; set; } /// /// Base item. Is used if the current item is inherited from another one diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/EmbeddedPackageSeeder.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/EmbeddedPackageSeeder.cs index 8c123bc6a4..6f610fff6c 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/EmbeddedPackageSeeder.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/EmbeddedPackageSeeder.cs @@ -35,7 +35,7 @@ public async Task SeedEmbeddedPackagesAsync(EmbeddedPackageSeedingContext context.Logger.Warn($"Seed packages for assembly '{context.Assembly.FullName}'"); var resources = context.Assembly.GetManifestResourceNames(); - var embeddedPackages = resources.Select(r => TryGetPackageInfo(context.Assembly, r)).Where(p => p != null).OrderBy(p => p.Date).ToList(); + var embeddedPackages = resources.Select(r => TryGetPackageInfo(context.Assembly, r)).WhereNotNull().OrderBy(p => p.Date).ToList(); if (!embeddedPackages.Any()) { @@ -60,7 +60,7 @@ public async Task SeedEmbeddedPackagesAsync(EmbeddedPackageSeedingContext context.Logger.Info($"Check package '{embeddedPackage.ResourceName}'"); - using (var stream = embeddedPackage.Assembly.GetManifestResourceStream(embeddedPackage.ResourceName)) + using (var stream = embeddedPackage.Assembly.GetEmbeddedResourceStream(embeddedPackage.ResourceName)) { var md5 = FileHelper.GetMD5(stream); @@ -108,7 +108,7 @@ public async Task SeedEmbeddedPackagesAsync(EmbeddedPackageSeedingContext return imported; } - protected EmbeddedPackageInfo TryGetPackageInfo(Assembly assembly, string resourceName) + protected EmbeddedPackageInfo? TryGetPackageInfo(Assembly assembly, string resourceName) { var packageRegex = new Regex(@"[.]*package(?'year'\d{4})(?'month'\d{2})(?'day'\d{2})_(?'hour'\d{2})(?'minute'\d{2})\.shaconfig"); var match = packageRegex.Match(resourceName); diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ConfigurationItemsExportResult.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ConfigurationItemsExportResult.cs index 58757dbbb4..0e5a075041 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ConfigurationItemsExportResult.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ConfigurationItemsExportResult.cs @@ -7,71 +7,10 @@ namespace Shesha.ConfigurationItems.Distribution.Models /// public class ConfigurationItemsExportResult { - // - - /* - Dependencies between items in the zip - option 1: include dependencies of each item into the json - pros: it works for both cases: single item export (json file) and multiple items export (zip archive) - cons: doesn't work for items not exportad as json (e.g. workflows) - option 2: add a single `relations` file into the zip - - all folders on the root are identified as modules except system names (e.g. [resources]) - - 1st level folders are identified as modules except system names (e.g. [resources]) - 2nd level folders represent item type (e.g. form, workflow etc) - all json files in the 2nd level folders are identified as individual items - - zip/ - ├── shesha/ - │ ├── forms/ - │ │ ├── form1.json - │ │ └── form2.json - │ ├── models/ - │ │ ├── organisation.json - │ │ └── person.json - │ ├── reference-lists/ - │ │ └── statuses.json - │ └── workflows/ - │ ├── disconnection.json - │ ├── new-connection.json - │ └── GenericProcess.bpmn <-- referenced by shesha/workflows/Disconnection.json - └── his.bookings/ - └── forms/ - - - zip/ - ├── [resources]/ - │ └── GenericProcess.bpmn <-- referenced by shesha/workflows/Disconnection.json - ├── package.json <-- stores information about the package (e.g. text description, user-friendly name, version of the back-end, may even contain a list of required back-end packages) - ├── shesha/ - │ ├── forms/ - │ │ ├── form1.json - │ │ └── form2.json - │ ├── models/ - │ │ ├── organisation.json - │ │ └── person.json - │ ├── reference-lists/ - │ │ └── statuses.json - │ └── workflows/ <-- store workflow definition - │ ├── disconnection.json - │ └── new-connection.json - └── his.bookings/ - └── forms/ - - Thoughts: - 1. content file without metadata doesn't make any sense, all configurable items are stored in the DB and can be exported to json - - Track dependencies by the forms designer and save as part of markup - 1. implement cleanup per component - 2. implement fill dependencies per component - - */ - /// /// Exported items /// - public List Items { get; set; } = new List(); + public List Items { get; set; } = new(); } public class ConfigurationItemsExportItem @@ -79,21 +18,16 @@ public class ConfigurationItemsExportItem /// /// Item data /// - public DistributedConfigurableItemBase ItemData { get; set; } + public required DistributedConfigurableItemBase ItemData { get; init; } /// /// Relative path in the zip /// - public string RelativePath { get; set; } - - /* - references and dependencies - item role: manually included/dependency - */ + public required string RelativePath { get; init; } /// /// Corresponding exporter /// - public IConfigurableItemExport Exporter { get; set; } + public required IConfigurableItemExport Exporter { get; init; } } } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ConfigurationItemsPackage.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ConfigurationItemsPackage.cs index 9d7e2a8572..a3d8874533 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ConfigurationItemsPackage.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ConfigurationItemsPackage.cs @@ -13,7 +13,7 @@ public class ConfigurationItemsPackage: IDisposable /// /// Packed items /// - public List Items { get; set; } = new List(); + public List Items { get; set; } = new(); private ZipArchive _zip; @@ -22,7 +22,8 @@ public ConfigurationItemsPackage(ZipArchive zip) _zip = zip; } - public void Dispose() + [System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP007:Don't dispose injected")] + public virtual void Dispose() { _zip?.Dispose(); } @@ -36,31 +37,31 @@ public class ConfigurationItemsPackageItem /// /// Module name /// - public string ModuleName { get; set; } + public required string ModuleName { get; set; } /// /// Front-end Application Key /// - public string ApplicationKey { get; set; } + public string? ApplicationKey { get; set; } /// /// Item type /// - public string ItemType { get; set; } + public required string ItemType { get; set; } /// /// File name in the zip archive /// - public string FileName { get; set; } + public required string FileName { get; set; } /// /// Stream accessor /// - public Func StreamGetter { get; set; } + public required Func StreamGetter { get; set; } /// /// Corresponding importer /// - public IConfigurableItemImport Importer { get; set; } + public IConfigurableItemImport? Importer { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/IConfigurationItemsImportContext.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/IConfigurationItemsImportContext.cs index 403f74c474..164c3ebbc7 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/IConfigurationItemsImportContext.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/IConfigurationItemsImportContext.cs @@ -1,5 +1,4 @@ -using Shesha.Domain; -using Shesha.Domain.ConfigurationItems; +using Shesha.Domain.ConfigurationItems; using System.Threading; namespace Shesha.ConfigurationItems.Distribution @@ -27,7 +26,7 @@ public interface IConfigurationItemsImportContext /// /// Import result. If specified, all imported items will be linked to the corresponding import session /// - public ConfigurationPackageImportResult ImportResult { get; } + public ConfigurationPackageImportResult? ImportResult { get; } /// /// Cancellation token. Is used for termination of the import process diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/PackageImportContext.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/PackageImportContext.cs index 7252da6316..8f7788d3cb 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/PackageImportContext.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/PackageImportContext.cs @@ -1,9 +1,7 @@ using Castle.Core.Logging; -using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using System; using System.Threading; -using System.Threading.Tasks; namespace Shesha.ConfigurationItems.Distribution { @@ -30,7 +28,7 @@ public class PackageImportContext: IConfigurationItemsImportContext /// /// Import result. If specified, all imported items will be linked to the corresponding import session /// - public ConfigurationPackageImportResult ImportResult { get; set; } + public ConfigurationPackageImportResult? ImportResult { get; set; } /// /// Cancellation token. Is used for termination of the import process @@ -40,7 +38,7 @@ public class PackageImportContext: IConfigurationItemsImportContext /// /// Filter of items. Return true to import and false to skip import of the specified item /// - public Func ShouldImportItem { get; set; } + public Func? ShouldImportItem { get; set; } /// /// Logger diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/PreparePackageContext.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/PreparePackageContext.cs index 5385e0592e..e3a132adb6 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/PreparePackageContext.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/PreparePackageContext.cs @@ -1,5 +1,4 @@ using Abp.Dependency; -using NetTopologySuite.Index.HPRtree; using Shesha.ConfigurationItems.Models; using Shesha.Domain; using Shesha.Reflection; @@ -31,9 +30,9 @@ public class PreparePackageContext private readonly IIocManager _iocManager; - private readonly Dictionary _exporters = new Dictionary(); + private readonly Dictionary _exporters = new Dictionary(); - public IConfigurableItemExport GetExporter(ConfigurationItemBase item) + public IConfigurableItemExport? GetExporter(ConfigurationItemBase item) { var itemType = item.GetType().StripCastleProxyType(); diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ReadPackageContext.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ReadPackageContext.cs index b525201c95..cbbd5f4dc5 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ReadPackageContext.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Distribution/Models/ReadPackageContext.cs @@ -22,10 +22,10 @@ public class ReadPackageContext private readonly IIocManager _iocManager; - private readonly Dictionary _importers = new Dictionary(); + private readonly Dictionary _importers = new Dictionary(); private readonly Dictionary _itemTypes = new Dictionary(); - public IConfigurableItemImport GetImporter(string itemType) + public IConfigurableItemImport? GetImporter(string itemType) { if (_importers.TryGetValue(itemType, out var importer)) return importer; @@ -47,7 +47,15 @@ public ReadPackageContext(IIocManager iocManager) var entityConfigStore = iocManager.Resolve(); var types = typeFinder.Find(t => t.IsAssignableTo(typeof(ConfigurationItemBase)) && !t.IsAbstract).ToList(); - _itemTypes = types.ToDictionary(t => entityConfigStore.Get(t).DiscriminatorValue, t => t); + _itemTypes = types.Select(t => + { + var discriminator = entityConfigStore.Get(t).DiscriminatorValue; + return !string.IsNullOrWhiteSpace(discriminator) + ? new { Discriminator = discriminator, Type = t } + : null; + }) + .WhereNotNull() + .ToDictionary(t => t.Discriminator, t => t.Type); } public ReadPackageContext() : this(StaticContext.IocManager) diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Exceptions/ConfigurationItemNotFoundException.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Exceptions/ConfigurationItemNotFoundException.cs index fd60070a5c..2df1d68566 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Exceptions/ConfigurationItemNotFoundException.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Exceptions/ConfigurationItemNotFoundException.cs @@ -17,7 +17,7 @@ public class ConfigurationItemNotFoundException : EntityNotFoundException, IHasE /// /// Module name /// - public string Module { get; set; } + public string? Module { get; set; } /// /// Form name @@ -27,14 +27,14 @@ public class ConfigurationItemNotFoundException : EntityNotFoundException, IHasE /// /// Front-end application /// - public string FrontEndApplication { get; set; } + public string? FrontEndApplication { get; set; } /// /// Error code /// public int Code { get; set; } - public ConfigurationItemNotFoundException(string itemType, string module, string name, string frontEndApplication) : base() + public ConfigurationItemNotFoundException(string itemType, string? module, string name, string? frontEndApplication) : base() { ItemType = itemType; Name = name; diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/IConfigurationFrameworkRuntime.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/IConfigurationFrameworkRuntime.cs index 53a312abd6..f9a3be8cbc 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/IConfigurationFrameworkRuntime.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/IConfigurationFrameworkRuntime.cs @@ -16,7 +16,7 @@ public interface IConfigurationFrameworkRuntime /// /// Front-end application /// - string FrontEndApplication { get; } + string? FrontEndApplication { get; } /// /// Begin runtime scope diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/IConfigurationItem.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/IConfigurationItem.cs index a62378c7a8..843e032377 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/IConfigurationItem.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/IConfigurationItem.cs @@ -1,11 +1,8 @@ using Abp.Domain.Entities; using Abp.Domain.Entities.Auditing; -using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; namespace Shesha.ConfigurationItems { @@ -24,7 +21,7 @@ public interface IConfigurationItem: IEntity, IFullAudited /// /// Module /// - Module Module { get; set; } + Module? Module { get; set; } /// /// Version number @@ -42,7 +39,7 @@ public interface IConfigurationItem: IEntity, IFullAudited /// The Guid for the Config Item. /// Different versions for the same Config Item will share this Id which the very first version of the item will be responsible for generating. /// - ConfigurationItem Origin { get; set; } + ConfigurationItem? Origin { get; set; } /// /// If true, indicates that this is a last version of the configuration item diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/IModuleManager.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/IModuleManager.cs index 95b6b0ef55..693e0e2eb7 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/IModuleManager.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/IModuleManager.cs @@ -1,5 +1,4 @@ -using Shesha.Domain.ConfigurationItems; -using System.Reflection; +using System.Reflection; using System.Threading.Tasks; using Module = Shesha.Domain.ConfigurationItems.Module; @@ -22,6 +21,6 @@ public interface IModuleManager /// /// /// - Task GetOrCreateModuleAsync(Assembly assembly); + Task GetOrCreateModuleAsync(Assembly assembly); } } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Models/ConfigurationItemViewMode.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Models/ConfigurationItemViewMode.cs index 25f4c52abf..af4e4fe659 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Models/ConfigurationItemViewMode.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Models/ConfigurationItemViewMode.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using System.Runtime.Serialization; namespace Shesha.ConfigurationItems.Models { diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Models/CopyItemInput.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Models/CopyItemInput.cs index 07dbf383b1..1b3e4c7043 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Models/CopyItemInput.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Models/CopyItemInput.cs @@ -20,16 +20,16 @@ public class CopyItemInput /// /// Name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Label /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/ModuleManager.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/ModuleManager.cs index 40da0751c9..594b419252 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/ModuleManager.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/ModuleManager.cs @@ -47,7 +47,7 @@ public async Task GetOrCreateModuleAsync(string moduleName) } /// inheritedDoc - public async Task GetOrCreateModuleAsync(Assembly assembly) + public async Task GetOrCreateModuleAsync(Assembly assembly) { var moduleName = assembly.GetConfigurableModuleName(); return !string.IsNullOrWhiteSpace(moduleName) diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByApplicationSpecification.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByApplicationSpecification.cs index 5c7846e3bc..1d96008cba 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByApplicationSpecification.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByApplicationSpecification.cs @@ -10,9 +10,9 @@ namespace Shesha.ConfigurationItems.Specifications /// public class ByApplicationSpecification : ISpecification where TItem : IMayHaveFrontEndApplication { - public string AppKey { get; private set; } + public string? AppKey { get; private set; } - public ByApplicationSpecification(string appKey) + public ByApplicationSpecification(string? appKey) { AppKey = appKey; } diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByNameAndModuleSpecification.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByNameAndModuleSpecification.cs index 18defcdf77..aca6514834 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByNameAndModuleSpecification.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByNameAndModuleSpecification.cs @@ -1,5 +1,4 @@ using Abp.Specifications; -using Shesha.Domain; using System; using System.Linq.Expressions; @@ -11,9 +10,9 @@ namespace Shesha.ConfigurationItems.Specifications public class ByNameAndModuleSpecification : ISpecification where TItem : IConfigurationItem { public string Name { get; private set; } - public string ModuleName { get; private set; } + public string? ModuleName { get; private set; } - public ByNameAndModuleSpecification(string name, string moduleName) + public ByNameAndModuleSpecification(string name, string? moduleName) { Name = name; ModuleName = moduleName; diff --git a/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByUserSpecification.cs b/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByUserSpecification.cs index dee7f97ec9..6116797953 100644 --- a/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByUserSpecification.cs +++ b/shesha-core/src/Shesha.Framework/ConfigurationItems/Specifications/ByUserSpecification.cs @@ -26,7 +26,9 @@ public bool IsSatisfiedBy(TItem obj) public Expression> ToExpression() { - return UserId == null ? item => item.User == null : item => item.User.Id == UserId; + return UserId == null + ? item => item.User == null + : item => item.User != null && item.User.Id == UserId; } } } diff --git a/shesha-core/src/Shesha.Framework/Controllers/Dtos/AssemblyInfoDto.cs b/shesha-core/src/Shesha.Framework/Controllers/Dtos/AssemblyInfoDto.cs index 7a760a17bc..56000b2b37 100644 --- a/shesha-core/src/Shesha.Framework/Controllers/Dtos/AssemblyInfoDto.cs +++ b/shesha-core/src/Shesha.Framework/Controllers/Dtos/AssemblyInfoDto.cs @@ -2,10 +2,10 @@ { public class AssemblyInfoDto { - public string Location { get; set; } - public string FullName { get; set; } - public string Version { get; set; } - public string Architecture { get; set; } - public string Description { get; set; } + public required string Location { get; set; } + public string? FullName { get; set; } + public required string Version { get; set; } + public required string Architecture { get; set; } + public string? Description { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Controllers/FrameworkController.cs b/shesha-core/src/Shesha.Framework/Controllers/FrameworkController.cs index 0e02646c41..6a7da6f7a0 100644 --- a/shesha-core/src/Shesha.Framework/Controllers/FrameworkController.cs +++ b/shesha-core/src/Shesha.Framework/Controllers/FrameworkController.cs @@ -25,7 +25,7 @@ namespace Shesha.Controllers public class FrameworkController: ControllerBase, ITransientDependency { public ILogger Logger { get; set; } = new NullLogger(); - public IIocManager IocManager { get; set; } + public IIocManager IocManager { get; set; } = default!; [HttpPost] public async Task BootstrapReferenceListsAsync() @@ -61,7 +61,7 @@ public List Assemblies(string searchString) } }) .Distinct(new AssemblyFullNameComparer()) - .Where(a => string.IsNullOrWhiteSpace(searchString) || a.FullName.Contains(searchString, StringComparison.InvariantCultureIgnoreCase)) + .Where(a => string.IsNullOrWhiteSpace(searchString) || !string.IsNullOrWhiteSpace(a.FullName) && a.FullName.Contains(searchString, StringComparison.InvariantCultureIgnoreCase)) .OrderBy(a => a.FullName); var result = assemblies.Select(a => { @@ -79,7 +79,7 @@ public List Assemblies(string searchString) { FullName = a.GetName().Name, Location = a.Location, - Version = a.GetName().Version.ToString(), + Version = a.GetName()?.Version?.ToString() ?? "unknown", Architecture = architecture, Description = descriptionAttribute?.Description, }; @@ -100,7 +100,7 @@ public long CurrentRamUsage() [HttpPost] [DontWrapResult] - public string TestException(ExceptionInput input) + public string? TestException(ExceptionInput input) { if (input.GenerateException) throw new Exception(input.Message); @@ -133,7 +133,7 @@ public async Task BootstrapEntityConfigsAsync() public class ExceptionInput { public bool GenerateException { get; set; } - public string Message { get; set; } + public string? Message { get; set; } } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Controllers/UsersAdministrationController.cs b/shesha-core/src/Shesha.Framework/Controllers/UsersAdministrationController.cs deleted file mode 100644 index 6fdc4bf97a..0000000000 --- a/shesha-core/src/Shesha.Framework/Controllers/UsersAdministrationController.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using Abp.Dependency; -using Microsoft.AspNetCore.Mvc; -using Shesha.Authorization.Users; - -namespace Shesha.Controllers -{ - /* - [Route("api/[controller]/[action]")] - [ApiController] - public class UsersAdministrationController: ControllerBase, ITransientDependency - { - public static DataTableConfig IndexTable() - { - var table = new DataTableConfig("Users_Index"); - - table.AddProperty(e => e.UserName); - table.AddProperty(e => e.Name); - table.AddProperty(e => e.Surname); - table.AddProperty(e => e.EmailAddress); - table.AddProperty(e => e.PhoneNumber); - table.AddProperty(e => e.CreatorUser); - table.AddProperty(e => e.CreatorUserId); - table.AddProperty(e => e.CreationTime); - table.AddProperty(e => e.IsActive); - - return table; - } - } - */ -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/DelayedUpdate/BaseDelayedUpdateManager.cs b/shesha-core/src/Shesha.Framework/DelayedUpdate/BaseDelayedUpdateManager.cs index 6d84bbe6d2..be6ea7f322 100644 --- a/shesha-core/src/Shesha.Framework/DelayedUpdate/BaseDelayedUpdateManager.cs +++ b/shesha-core/src/Shesha.Framework/DelayedUpdate/BaseDelayedUpdateManager.cs @@ -10,7 +10,7 @@ namespace Shesha.DelayedUpdate public abstract class BaseDelayedUpdateManager : IDelayedUpdateManager { - public TDelayedUpdateData GetData(object data) + public TDelayedUpdateData? GetData(object? data) { if (data is JObject jData) return jData.ToObject(); @@ -25,7 +25,7 @@ public virtual async Task ExecuteUpdateAsync(IEntity e { foreach (var item in items) { - TDelayedUpdateData data = GetData(item.Data); + var data = GetData(item.Data); await UpdateItemAsync(entity, item.Id, data, validationResult); } } @@ -35,7 +35,7 @@ public virtual async Task ExecuteUpdateAsync(IEntity e } } - public abstract Task UpdateItemAsync(IEntity entity, object id, TDelayedUpdateData data, List validationResult); + public abstract Task UpdateItemAsync(IEntity entity, object id, TDelayedUpdateData? data, List validationResult); public abstract bool IsApplicable(string type); } diff --git a/shesha-core/src/Shesha.Framework/DelayedUpdate/DelayedUpdateItem.cs b/shesha-core/src/Shesha.Framework/DelayedUpdate/DelayedUpdateItem.cs index 3840755cd0..dee4031c18 100644 --- a/shesha-core/src/Shesha.Framework/DelayedUpdate/DelayedUpdateItem.cs +++ b/shesha-core/src/Shesha.Framework/DelayedUpdate/DelayedUpdateItem.cs @@ -1,20 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; namespace Shesha.DelayedUpdate { public class DelayedUpdateItem { - public object Id { get; set; } - public object Data { get; set; } + public required object Id { get; set; } + public object? Data { get; set; } } public class DelayedUpdateGroup { - public string Name { get; set; } - public List Items { get; set; } + public required string Name { get; set; } + public List Items { get; set; } = new(); } } diff --git a/shesha-core/src/Shesha.Framework/DelayedUpdate/StoredFileDeferredUpdateManager.cs b/shesha-core/src/Shesha.Framework/DelayedUpdate/StoredFileDeferredUpdateManager.cs index 5fdb6bf18e..12149e668d 100644 --- a/shesha-core/src/Shesha.Framework/DelayedUpdate/StoredFileDeferredUpdateManager.cs +++ b/shesha-core/src/Shesha.Framework/DelayedUpdate/StoredFileDeferredUpdateManager.cs @@ -1,7 +1,5 @@ using Abp.Dependency; using Abp.Domain.Entities; -using Abp.Domain.Uow; -using Abp.Extensions; using Shesha.EntityReferences; using Shesha.Reflection; using Shesha.Services; @@ -18,29 +16,28 @@ public class StoredFileDelayedUpdateManager : BaseDelayedUpdateManager(IEntity entity, object id, StoredFileDelayedUpdateData data, List validationResult) + public override async Task UpdateItemAsync(IEntity entity, object id, StoredFileDelayedUpdateData? data, List validationResult) { try { - var file = await _fileService.GetOrNullAsync(Guid.Parse(id.ToString())); + var stringId = id?.ToString(); + ArgumentException.ThrowIfNullOrWhiteSpace(stringId, nameof(id)); - if (data?.PropertyName?.IsNullOrEmpty() ?? true) + var file = await _fileService.GetOrNullAsync(Guid.Parse(stringId)); + + if (string.IsNullOrWhiteSpace(data?.PropertyName)) { - object owner = entity; - if (!data?.OwnerName?.IsNullOrEmpty() ?? false) + object? owner = entity; + if (!string.IsNullOrWhiteSpace(data?.OwnerName)) { - var prop = ReflectionHelper.GetProperty(owner, data.OwnerName); - owner = prop.GetValue(owner); + var propAccessor = ReflectionHelper.GetPropertyValueAccessor(owner, data.OwnerName); + owner = propAccessor.Value; if (owner == null) { validationResult.Add(new ValidationResult($"Entity '{data.OwnerName}' not found for {entity.GetType().FullName}")); @@ -51,7 +48,7 @@ public override async Task UpdateItemAsync(IEntity ent } else { - var property = ReflectionHelper.GetProperty(entity, data.PropertyName, out var owner); + var property = ReflectionHelper.GetPropertyOrNull(entity, data.PropertyName, out var owner); if (property == null) { validationResult.Add(new ValidationResult($"Property '{data.PropertyName}' not found for {entity.GetType().FullName}")); @@ -73,4 +70,4 @@ public override bool IsApplicable(string type) return type == ItemType; } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/DelayedUpdate/StoredFileDelayedUpdateData.cs b/shesha-core/src/Shesha.Framework/DelayedUpdate/StoredFileDelayedUpdateData.cs index ab63aef013..d81acf482c 100644 --- a/shesha-core/src/Shesha.Framework/DelayedUpdate/StoredFileDelayedUpdateData.cs +++ b/shesha-core/src/Shesha.Framework/DelayedUpdate/StoredFileDelayedUpdateData.cs @@ -2,7 +2,7 @@ { public class StoredFileDelayedUpdateData { - public string PropertyName { get; set; } - public string OwnerName { get; set; } + public string? PropertyName { get; set; } + public string? OwnerName { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/ApplicationStartup.cs b/shesha-core/src/Shesha.Framework/Domain/ApplicationStartup.cs index 7c182c64f9..3218241854 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ApplicationStartup.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ApplicationStartup.cs @@ -16,14 +16,14 @@ namespace Shesha.Domain public class ApplicationStartup: Entity { [StringLength(100)] - public virtual string MachineName { get; set; } - public virtual string Folder { get; set; } + public virtual string? MachineName { get; set; } + public virtual string? Folder { get; set; } [StringLength(100)] - public virtual string Account { get; set; } + public virtual string? Account { get; set; } public virtual DateTime StartedOn { get; set; } public virtual DateTime? FinishedOn { get; set; } public virtual ApplicationStartupStatus Status { get; set; } - public virtual string ErrorMessage { get; set; } + public virtual string? ErrorMessage { get; set; } public virtual bool BootstrappersDisabled { get; set; } public virtual bool MigrationsDisabled { get; set; } diff --git a/shesha-core/src/Shesha.Framework/Domain/ApplicationStartupAssembly.cs b/shesha-core/src/Shesha.Framework/Domain/ApplicationStartupAssembly.cs index 9361b4ea27..98795db25d 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ApplicationStartupAssembly.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ApplicationStartupAssembly.cs @@ -13,14 +13,14 @@ namespace Shesha.Domain [Table("frwk_application_startup_assemblies")] public class ApplicationStartupAssembly : Entity { - public virtual string FileName { get; set; } - public virtual string FilePath { get; set; } + public required virtual string FileName { get; set; } + public virtual string? FilePath { get; set; } [StringLength(50)] - public virtual string FileMD5 { get; set; } + public required virtual string FileMD5 { get; set; } [StringLength(100)] - public virtual string FileVersion { get; set; } + public virtual string? FileVersion { get; set; } [StringLength(100)] - public virtual string ProductVersion { get; set; } - public virtual ApplicationStartup ApplicationStartup { get; set; } + public virtual string? ProductVersion { get; set; } + public required virtual ApplicationStartup ApplicationStartup { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/ClassLinkAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/ClassLinkAttribute.cs deleted file mode 100644 index a028d1fc0e..0000000000 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/ClassLinkAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace Shesha.Domain.Attributes -{ - [AttributeUsage(AttributeTargets.Property)] - public class ClassLinkAttribute : Attribute // , IMetadataAware - { - public Type BaseType { get; set; } - public Type ComparerType { get; set; } - public bool UseUids { get; set; } - - public ClassLinkAttribute() - { - - } - - public ClassLinkAttribute(Type baseType) - { - BaseType = baseType; - } - - //public void OnMetadataCreated(ModelMetadata metadata) - //{ - // var sheshaMetadata = metadata.Shesha(); - - // sheshaMetadata.EditorControl = GenericEditorControl.ClassDropdown; - // sheshaMetadata.ClassDropdownBaseClass = BaseType; - // sheshaMetadata.ClassDropdownComparerType = ComparerType; - // sheshaMetadata.ClassDropdownUseUids = UseUids; - // metadata.TemplateHint = "ClassDropdown"; - //} - } -} diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/CrudActionsAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/CrudActionsAttribute.cs new file mode 100644 index 0000000000..b00bdebbb5 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/CrudActionsAttribute.cs @@ -0,0 +1,64 @@ +using Shesha.Domain.Enums; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Shesha.Domain.Attributes +{ + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] + public class CrudAccessAttribute: Attribute + { + public RefListPermissionedAccess? All { get; set; } = null; + public List AllPermissions { get; set; } = new List(); + public RefListPermissionedAccess? Create { get; set; } = null; + public List CreatePermissions { get; set; } = new List(); + public RefListPermissionedAccess? Read { get; set; } = null; + public List ReadPermissions { get; set; } = new List(); + public RefListPermissionedAccess? Update { get; set; } = null; + public List UpdatePermissions { get; set; } = new List(); + public RefListPermissionedAccess? Delete { get; set; } = null; + public List DeletePermissions { get; set; } = new List(); + + public CrudAccessAttribute(CrudActions action, bool enabled) + { + Init(action, enabled ? RefListPermissionedAccess.Inherited : RefListPermissionedAccess.Disable, null); + } + + public CrudAccessAttribute(CrudActions action, RefListPermissionedAccess access) + { + Init(action, access, null); + } + + public CrudAccessAttribute(CrudActions action, RefListPermissionedAccess access, string[] permissions) + { + Init(action, access, permissions); + } + + public void Init(CrudActions action, RefListPermissionedAccess access, string[]? permissions) + { + switch (action) + { + case CrudActions.Create: + Create = access; + CreatePermissions = permissions?.ToList() ?? new List(); + break; + case CrudActions.Read: + Read = access; + ReadPermissions = permissions?.ToList() ?? new List(); + break; + case CrudActions.Update: + Update = access; + UpdatePermissions = permissions?.ToList() ?? new List(); + break; + case CrudActions.Delete: + Delete = access; + DeletePermissions = permissions?.ToList() ?? new List(); + break; + case CrudActions.All: + All = access; + AllPermissions = permissions?.ToList() ?? new List(); + break; + } + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/CrudDisableActionsAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/CrudDisableActionsAttribute.cs new file mode 100644 index 0000000000..07a8649e24 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/CrudDisableActionsAttribute.cs @@ -0,0 +1,26 @@ +using System; + +namespace Shesha.Domain.Attributes +{ + [AttributeUsage(AttributeTargets.Class, Inherited = true)] + public class CrudDisableActionsAttribute: Attribute + { + public CrudActions Disable { get; set; } + + public CrudDisableActionsAttribute(CrudActions disable) + { + Disable = disable; + } + } + + [Flags] + public enum CrudActions + { + None = 0, + Create = 1, + Read = 2, + Update = 4, + Delete = 8, + All = 15, + } +} diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/CustomCategoriesAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/CustomCategoriesAttribute.cs deleted file mode 100644 index efa114e993..0000000000 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/CustomCategoriesAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Shesha.Domain.Attributes -{ - [AttributeUsage(AttributeTargets.Class)] - public class CustomCategoriesAttribute : Attribute - { - public string ScanCategoriesPath { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/DiscriminatorAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/DiscriminatorAttribute.cs index 5402099813..75699089f3 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/DiscriminatorAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/DiscriminatorAttribute.cs @@ -15,7 +15,7 @@ public class DiscriminatorAttribute : Attribute /// /// Name of the discriminator column /// - public string DiscriminatorColumn { get; set; } + public string? DiscriminatorColumn { get; set; } /// /// If true, indicates that entity uses discriminator diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/EntityAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/EntityAttribute.cs index d4da4cb3e1..65bfc918da 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/EntityAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/EntityAttribute.cs @@ -11,7 +11,7 @@ public class EntityAttribute : Attribute /// /// Specifies friendly name of the entity that should be shown to user when required /// - public string FriendlyName { get; set; } + public string? FriendlyName { get; set; } /// /// This is a short version of the Type name of the entity class that is unique within @@ -20,7 +20,7 @@ public class EntityAttribute : Attribute /// the Discriminator value defined for the entity on NHibernate mapping if the entity /// is a subclass of another entity. /// - public string TypeShortAlias { get; set; } + public string? TypeShortAlias { get; set; } /// /// If AlwaysGenerateApplicationService, indicates that an Application Service should be generated for this entity. @@ -34,7 +34,7 @@ public class EntityAttribute : Attribute /// /// Name of the auto-generated applicaiton service, applicable only when is true. Leave empty to use `{class name}Crud` as a name of the applicaiton service /// - public string ApplicationServiceName { get; set; } + public string? ApplicationServiceName { get; set; } public EntityAttribute() { diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/JoinedPropertyAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/JoinedPropertyAttribute.cs index bc305b4ef7..f9217a8603 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/JoinedPropertyAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/JoinedPropertyAttribute.cs @@ -6,7 +6,7 @@ namespace Shesha.Domain.Attributes public class JoinedPropertyAttribute: Attribute { public string TableName { get; set; } - public string Schema { get; set; } + public string? Schema { get; set; } public JoinedPropertyAttribute(string tableName) { diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/LazyLoadAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/LazyLoadAttribute.cs new file mode 100644 index 0000000000..ea4273afb1 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/LazyLoadAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Shesha.Domain.Attributes +{ + /// + /// Is used to mark property as lazy loaded + /// + [AttributeUsage(AttributeTargets.Property)] + public class LazyLoadAttribute : Attribute + { + } +} diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/ManyToManyAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/ManyToManyAttribute.cs index 5e4f121a2e..7eb90952eb 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/ManyToManyAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/ManyToManyAttribute.cs @@ -18,7 +18,7 @@ public ManyToManyAttribute(string table, string childColumn, string keyColumn, s KeyColumn = keyColumn; Where = @where; } - public ManyToManyAttribute(string table, string childColumn, string keyColumn) + public ManyToManyAttribute(string? table, string? childColumn, string? keyColumn) { Table = table; ChildColumn = childColumn; @@ -70,26 +70,26 @@ public ManyToManyAttribute(bool autoGeneration) /// /// It can be set either to the table you link to or the intermediate link table (only if it has a mapping) /// - public string Table { get; set; } + public string? Table { get; set; } /// /// Column to filter by /// - public string ChildColumn { get; set; } + public string? ChildColumn { get; set; } /// /// Column to load entity from /// - public string KeyColumn { get; set; } + public string? KeyColumn { get; set; } /// /// SQL Filter /// - public string Where { get; set; } + public string? Where { get; set; } /// /// SQL Order By clause /// - public string OrderBy { get; set; } + public string? OrderBy { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/PastAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/PastAttribute.cs index 45e3692b13..a7983f0c39 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/PastAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/PastAttribute.cs @@ -16,7 +16,7 @@ public override string FormatErrorMessage(string name) return base.FormatErrorMessage(name); } - public override bool IsValid(object value) + public override bool IsValid(object? value) { if (value == null) return true; diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/PrefixAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/PrefixAttribute.cs index 149e7f51c2..c47a5cd79d 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/PrefixAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/PrefixAttribute.cs @@ -9,7 +9,7 @@ namespace Shesha.Domain.Attributes public class PrefixAttribute : Attribute { public bool UsePrefixes { get; set; } - public string Prefix { get; set; } + public string? Prefix { get; set; } public PrefixAttribute() { diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/ReferenceListAttributeBase.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/ReferenceListAttributeBase.cs index 50deab090b..185283f0a0 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/ReferenceListAttributeBase.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/ReferenceListAttributeBase.cs @@ -12,11 +12,11 @@ public abstract class ReferenceListAttributeBase: Attribute #region Properties [Obsolete("Is used for backward compatibility only")] - protected string Namespace { get; set; } + protected string? Namespace { get; set; } protected string ReferenceListName { get; set; } - private string _module; + private string? _module; public string Module { get { throw new MemberAccessException($"Use `{nameof(GetReferenceListIdentifier)}` to read correct module name"); @@ -29,7 +29,7 @@ public string Module { #endregion [Obsolete("Use constructor that accepts single parameter instead")] - public ReferenceListAttributeBase(string @namespace, string name) + public ReferenceListAttributeBase(string? @namespace, string name) { Namespace = @namespace; ReferenceListName = name; @@ -62,7 +62,7 @@ public ReferenceListIdentifier GetReferenceListIdentifier(Type enumType) public bool IsLegacy { get; private set; } [Obsolete("Is used for backward compatibility only")] - public string GetNamespace() + public string? GetNamespace() { return Namespace; } @@ -76,7 +76,7 @@ public string GetNamespace() ? $"{Namespace}.{ReferenceListName}" : ReferenceListName; - protected string GetModuleName(Assembly assembly) + protected string? GetModuleName(Assembly? assembly) { return !string.IsNullOrWhiteSpace(_module) ? _module diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/SaveAsJsonAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/SaveAsJsonAttribute.cs index e414e811c9..e3bc91703f 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/SaveAsJsonAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/SaveAsJsonAttribute.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Domain.Attributes { diff --git a/shesha-core/src/Shesha.Framework/Domain/Attributes/StoredFileAttribute.cs b/shesha-core/src/Shesha.Framework/Domain/Attributes/StoredFileAttribute.cs index ca6230533a..60fcbff18c 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Attributes/StoredFileAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Attributes/StoredFileAttribute.cs @@ -10,6 +10,6 @@ public class StoredFileAttribute : Attribute { public bool IsVersionControlled { get; set; } public bool IsEncrypted { get; set; } - public string Accept { get; set; } + public string? Accept { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Domain/ConfigurableComponent.cs b/shesha-core/src/Shesha.Framework/Domain/ConfigurableComponent.cs similarity index 78% rename from shesha-core/src/Shesha.Web.FormsDesigner/Domain/ConfigurableComponent.cs rename to shesha-core/src/Shesha.Framework/Domain/ConfigurableComponent.cs index e2b729cbf9..96ae40e69b 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Domain/ConfigurableComponent.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ConfigurableComponent.cs @@ -1,28 +1,25 @@ -using Shesha.Domain; -using Shesha.Domain.Attributes; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; - -namespace Shesha.Web.FormsDesigner.Domain -{ - /// - /// Generic configurable component. Is used mostly to store configuration of the independent front-end components (e.g. sidebar menu, theme) - /// - [Entity(TypeShortAlias = "Shesha.Framework.ConfigurableComponent")] - [JoinedProperty("Frwk_ConfigurableComponents")] - [DiscriminatorValue(ItemTypeName)] - public class ConfigurableComponent : ConfigurationItemBase - { - public const string ItemTypeName = "configurable-component"; - - /// inheritedDoc - public override string ItemType => ItemTypeName; - - /// - /// Form markup - /// - [StringLength(int.MaxValue)] - public virtual string Settings { get; set; } - } -} +using Shesha.Domain.Attributes; +using System.ComponentModel.DataAnnotations; + +namespace Shesha.Domain +{ + /// + /// Generic configurable component. Is used mostly to store configuration of the independent front-end components (e.g. sidebar menu, theme) + /// + [Entity(TypeShortAlias = "Shesha.Framework.ConfigurableComponent")] + [JoinedProperty("Frwk_ConfigurableComponents")] + [DiscriminatorValue(ItemTypeName)] + public class ConfigurableComponent : ConfigurationItemBase + { + public const string ItemTypeName = "configurable-component"; + + /// inheritedDoc + public override string ItemType => ItemTypeName; + + /// + /// Form markup + /// + [StringLength(int.MaxValue)] + public virtual string? Settings { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Framework/Domain/ConfigurationItemBase.cs b/shesha-core/src/Shesha.Framework/Domain/ConfigurationItemBase.cs index 224f4992ea..687e15224b 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ConfigurationItemBase.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ConfigurationItemBase.cs @@ -1,7 +1,5 @@ using Shesha.ConfigurationItems; using Shesha.Domain.ConfigurationItems; -using System.Collections.Generic; -using System.Threading.Tasks; namespace Shesha.Domain { diff --git a/shesha-core/src/Shesha.Framework/Domain/ConfigurationItemIdentifier.cs b/shesha-core/src/Shesha.Framework/Domain/ConfigurationItemIdentifier.cs index 184568bb28..a0d3eb8cdd 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ConfigurationItemIdentifier.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ConfigurationItemIdentifier.cs @@ -1,12 +1,8 @@ -using Abp.ObjectComparators.StringComparators; -using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; +using Shesha.ConfigurationItems; using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Domain { @@ -17,7 +13,11 @@ public abstract class ConfigurationItemIdentifier: IEquatable /// Item name @@ -27,23 +27,23 @@ public abstract class ConfigurationItemIdentifier: IEquatable /// Module name /// - public string Module { get; private set; } + public string? Module { get; private set; } - public ConfigurationItemIdentifier(string module, string name) + public ConfigurationItemIdentifier(string? module, string name) { Module = module; Name = name; } - public bool Equals(ConfigurationItemIdentifier other) + public bool Equals(ConfigurationItemIdentifier? other) { return other != null && - ItemType == other.ItemType && Module == other.Module && Name == other.Name; + ItemTypeName == other.ItemTypeName && Module == other.Module && Name == other.Name; } - public override bool Equals(object obj) => this.Equals(obj as ConfigurationItemIdentifier); + public override bool Equals(object? obj) => this.Equals(obj as ConfigurationItemIdentifier); - public static bool operator ==(ConfigurationItemIdentifier l, ConfigurationItemIdentifier r) + public static bool operator ==(ConfigurationItemIdentifier? l, ConfigurationItemIdentifier? r) { if (l is null) { @@ -56,16 +56,40 @@ public bool Equals(ConfigurationItemIdentifier other) return l.Equals(r); } - public static bool operator !=(ConfigurationItemIdentifier l, ConfigurationItemIdentifier r) => !(l == r); + public static bool operator !=(ConfigurationItemIdentifier? l, ConfigurationItemIdentifier? r) => !(l == r); [BindNever] [Bindable(false)] [JsonIgnore] - public string NormalizedFullName => $"{ItemType}:{Module}/{Name}".ToLower(); + public string NormalizedFullName => $"{ItemTypeName}:{Module}/{Name}".ToLower(); public override int GetHashCode() { return NormalizedFullName.GetHashCode(); } + + public override string ToString() + { + return NormalizedFullName; + } + } + + public abstract class ConfigurationItemIdentifier: ConfigurationItemIdentifier where TItem : IConfigurationItem + { + protected ConfigurationItemIdentifier(string? module, string name) : base(module, name) + { + } + + public override Type ItemType => typeof(TItem); + public override string ItemTypeName => ItemType.Name; + } + + public interface IIdentifierFactory + { + } + + public interface IIdentifierFactory: IIdentifierFactory where TSelf : IIdentifierFactory + { + static abstract TSelf New(string? module, string name); } } diff --git a/shesha-core/src/Shesha.Framework/Domain/ConfigurationItems/ConfigurationItem.cs b/shesha-core/src/Shesha.Framework/Domain/ConfigurationItems/ConfigurationItem.cs index 3c4d8d3d0c..d674186d4c 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ConfigurationItems/ConfigurationItem.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ConfigurationItems/ConfigurationItem.cs @@ -14,45 +14,45 @@ namespace Shesha.Domain.ConfigurationItems public class ConfigurationItem : FullAuditedEntity, IMayHaveTenant, IMayHaveFrontEndApplication { [ReadonlyProperty] - public virtual string ItemType { get; set; } + public virtual string ItemType { get; set; } = string.Empty; /// /// The Guid for the Config Item. /// Different versions for the same Config Item will share this Id which the very first version of the item will be responsible for generating. /// - public virtual ConfigurationItem Origin { get; set; } + public virtual ConfigurationItem? Origin { get; set; } /// /// Item name /// [StringLength(200)] [Display(Name = "Name", Description = "Name of the configuration item. Unique within the module.")] - public virtual string Name { get; set; } + public virtual string Name { get; set; } = string.Empty; /// /// Label of the con /// [StringLength(200)] [Display(Name = "Label", Description = "Label of the item, can be used in lists as a user friendly name")] - public virtual string Label { get; set; } + public virtual string? Label { get; set; } /// /// Item description /// [StringLength(int.MaxValue)] [DataType(DataType.MultilineText)] - public virtual string Description { get; set; } + public virtual string? Description { get; set; } /// /// Module /// - public virtual Module Module { get; set; } + public virtual Module? Module { get; set; } /// /// Base item. Is used if the current item is inherited from another one /// [Display(Name = "Base item", Description = "Is used if the current item is inherited from another one")] - public virtual ConfigurationItem BaseItem { get; set; } + public virtual ConfigurationItem? BaseItem { get; set; } /// /// Version number @@ -69,12 +69,12 @@ public class ConfigurationItem : FullAuditedEntity, IMayHaveTenant, /// /// Parent version. Note: version may have more than one child versions (e.g. new version was created and then cancelled, in this case a new version should be created in the same parent) /// - public virtual ConfigurationItem ParentVersion { get; set; } + public virtual ConfigurationItem? ParentVersion { get; set; } /// /// Import session that created this configuration item /// - public virtual ImportResult CreatedByImport { get; set; } + public virtual ImportResult? CreatedByImport { get; set; } //[NotMapped] //public virtual ConfigurationItemBase Owner { get; set; } @@ -106,6 +106,6 @@ public virtual void SetIsLast(bool value) /// /// Application the item belongs to /// - public virtual FrontEndApp Application { get; set; } + public virtual FrontEndApp? Application { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/ConfigurationItems/Module.cs b/shesha-core/src/Shesha.Framework/Domain/ConfigurationItems/Module.cs index d1cc2496ed..77ff83ef95 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ConfigurationItems/Module.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ConfigurationItems/Module.cs @@ -1,10 +1,7 @@ using Abp.Domain.Repositories; -using Abp.Linq; -using Abp.Runtime.Validation; using FluentValidation; using Shesha.Domain.Attributes; using Shesha.Extensions; -using Shesha.Services; using System; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -23,25 +20,25 @@ public class Module: FullPowerEntity /// Module name /// [StringLength(200)] - public virtual string Name { get; set; } + public required virtual string Name { get; set; } /// /// Friendly name /// [StringLength(200)] - public virtual string FriendlyName { get; set; } + public virtual string? FriendlyName { get; set; } /// /// Module description /// [StringLength(int.MaxValue)] - public virtual string Description { get; set; } + public virtual string? Description { get; set; } /// /// Publisher /// [StringLength(200)] - public virtual string Publisher { get; set; } + public virtual string? Publisher { get; set; } /// /// Is editable @@ -57,7 +54,7 @@ public class Module: FullPowerEntity /// Current version no /// [StringLength(50)] - public virtual string CurrentVersionNo { get; set; } + public virtual string? CurrentVersionNo { get; set; } /// /// First initialization date @@ -78,9 +75,9 @@ public class Module: FullPowerEntity /// Code identifier that can be used in the client-side code to reference current module /// [StringLength(200)] - public virtual string Accessor { get; set; } + public virtual string? Accessor { get; set; } - public override string ToString() + public override string? ToString() { return FriendlyName; } diff --git a/shesha-core/src/Shesha.Framework/Domain/Conventions/DefaultNamingConventions.cs b/shesha-core/src/Shesha.Framework/Domain/Conventions/DefaultNamingConventions.cs index 558c5ef241..ae15adaa4b 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Conventions/DefaultNamingConventions.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Conventions/DefaultNamingConventions.cs @@ -7,7 +7,7 @@ namespace Shesha.Domain.Conventions /// public class DefaultNamingConventions : INamingConventions { - public string GetColumnName(string prefix, string propertyName, string suffix) + public string GetColumnName(string prefix, string propertyName, string? suffix) { return $"{prefix}{propertyName}{suffix}"; } diff --git a/shesha-core/src/Shesha.Framework/Domain/Conventions/INamingConventions.cs b/shesha-core/src/Shesha.Framework/Domain/Conventions/INamingConventions.cs index 6a5fd8d8cf..ceca507cac 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Conventions/INamingConventions.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Conventions/INamingConventions.cs @@ -5,7 +5,7 @@ /// public interface INamingConventions { - string GetColumnName(string prefix, string propertyName, string suffix); + string GetColumnName(string prefix, string propertyName, string? suffix); string GetTableName(string className); } } diff --git a/shesha-core/src/Shesha.Framework/Domain/Conventions/SnakeCaseNamingConventions.cs b/shesha-core/src/Shesha.Framework/Domain/Conventions/SnakeCaseNamingConventions.cs index b803a938f2..6d3e1ff175 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Conventions/SnakeCaseNamingConventions.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Conventions/SnakeCaseNamingConventions.cs @@ -12,7 +12,7 @@ public SnakeCaseNamingConventions() } - public string GetColumnName(string prefix, string propertyName, string suffix) + public string GetColumnName(string prefix, string propertyName, string? suffix) { return $"{ConvertPrefix(prefix)}{propertyName.ToSnakeCase()}{ConvertSuffix(suffix)}"; } @@ -24,7 +24,7 @@ private string ConvertPrefix(string prefix) : prefix.ToSnakeCase().TrimEnd('_') + "_"; } - private string ConvertSuffix(string suffix) + private string? ConvertSuffix(string? suffix) { return string.IsNullOrWhiteSpace(suffix) ? suffix diff --git a/shesha-core/src/Shesha.Framework/Domain/DeviceForceUpdate.cs b/shesha-core/src/Shesha.Framework/Domain/DeviceForceUpdate.cs index a5c7c65ca0..402ca15331 100644 --- a/shesha-core/src/Shesha.Framework/Domain/DeviceForceUpdate.cs +++ b/shesha-core/src/Shesha.Framework/Domain/DeviceForceUpdate.cs @@ -1,9 +1,7 @@ using Abp.Domain.Entities; using Abp.Domain.Entities.Auditing; -using System; -using System.Collections.Generic; -using System.Text; using Shesha.Domain.Attributes; +using System; namespace Shesha.Domain { @@ -11,9 +9,9 @@ namespace Shesha.Domain [Entity(GenerateApplicationService = GenerateApplicationServiceState.DisableGenerateApplicationService)] public class DeviceForceUpdate : FullAuditedEntity { - public virtual string Name { get; set; } - public virtual string Description { get; set; } - public virtual string AppStoreUrl { get; set; } + public virtual string? Name { get; set; } + public virtual string? Description { get; set; } + public virtual string? AppStoreUrl { get; set; } public virtual int? OSType { get; set; } public virtual double? VersionNo { get; set; } } diff --git a/shesha-core/src/Shesha.Framework/Domain/EntityChangeAuditLog.cs b/shesha-core/src/Shesha.Framework/Domain/EntityChangeAuditLog.cs index 1bb9c1a73a..5e9c34059c 100644 --- a/shesha-core/src/Shesha.Framework/Domain/EntityChangeAuditLog.cs +++ b/shesha-core/src/Shesha.Framework/Domain/EntityChangeAuditLog.cs @@ -1,11 +1,6 @@ using Abp.Domain.Entities; using Shesha.Domain.Attributes; -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Domain { @@ -16,40 +11,40 @@ public class EntityChangeAuditLog: Entity /// /// The Id of the Entity of the property that is audited /// - public virtual string EntityId { get; protected set; } + public virtual string EntityId { get; protected set; } = default!; /// /// The name of the property that is audited /// - public virtual string PropertyName { get; protected set; } - + public virtual string PropertyName { get; protected set; } = default!; + /// /// The value of the property before the change /// - public virtual string OriginalValue { get; protected set; } + public virtual string? OriginalValue { get; protected set; } /// /// The value of the property after the change /// - public virtual string NewValue { get; protected set; } + public virtual string? NewValue { get; protected set; } /// /// The name of the Entity of the property that is audited /// - public virtual string EntityTypeFullName { get; protected set; } + public virtual string? EntityTypeFullName { get; protected set; } /// /// The time in which the audit was created /// - public virtual string CreationTime { get; protected set; } + public virtual string CreationTime { get; protected set; } = default!; /// /// The full name of the user that made the change /// - public virtual string ActionedBy { get; protected set; } + public virtual string? ActionedBy { get; protected set; } /// /// The type of change that was made. Options: Created, Updated, Deleted /// - public virtual string ChangeType { get; protected set; } + public virtual string? ChangeType { get; protected set; } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/EntityConfig.cs b/shesha-core/src/Shesha.Framework/Domain/EntityConfig.cs index 10d9170b9c..0c22f1b99b 100644 --- a/shesha-core/src/Shesha.Framework/Domain/EntityConfig.cs +++ b/shesha-core/src/Shesha.Framework/Domain/EntityConfig.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Threading.Tasks; namespace Shesha.Domain { @@ -20,19 +19,19 @@ public class EntityConfig : ConfigurationItemBase [EntityDisplayName] [StringLength(255)] - public virtual string FriendlyName { get; set; } + public virtual string? FriendlyName { get; set; } [StringLength(100)] - public virtual string TypeShortAlias { get; set; } + public virtual string? TypeShortAlias { get; set; } [StringLength(255)] - public virtual string TableName { get; set; } + public virtual string? TableName { get; set; } [StringLength(500)] public virtual string ClassName { get; set; } [StringLength(500)] - public virtual string Namespace { get; set; } + public virtual string? Namespace { get; set; } [StringLength(255)] - public virtual string DiscriminatorValue { get; set; } + public virtual string? DiscriminatorValue { get; set; } - public virtual EntityConfig Parent { get; set; } + public virtual EntityConfig? Parent { get; set; } public virtual bool GenerateAppService { get; set; } @@ -48,7 +47,7 @@ public class EntityConfig : ConfigurationItemBase /// [StringLength(40)] [Column("PropertiesMD5")] - public virtual string HardcodedPropertiesMD5 { get; set; } + public virtual string? HardcodedPropertiesMD5 { get; set; } public override string ItemType => ItemTypeName; @@ -70,6 +69,6 @@ public EntityConfig() /// Code identifier that can be used in the client-side code to reference current module /// [StringLength(200)] - public virtual string Accessor { get; set; } + public virtual string? Accessor { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/EntityHelper.cs b/shesha-core/src/Shesha.Framework/Domain/EntityHelper.cs index 8ba5de7655..0d53ebf161 100644 --- a/shesha-core/src/Shesha.Framework/Domain/EntityHelper.cs +++ b/shesha-core/src/Shesha.Framework/Domain/EntityHelper.cs @@ -10,7 +10,9 @@ namespace Shesha.Domain /// public static class EntityHelper { - private static IEntityConfigurationStore _configurationStore; + private static IEntityConfigurationStore? _configurationStore; + + private static IEntityConfigurationStore ConfigurationStore => _configurationStore ?? throw new Exception($"{nameof(IEntityConfigurationStore)} is unavailable"); /// /// Refresh configuration store instance using provided . Note: for internal usage and unit-tests only @@ -34,7 +36,7 @@ static EntityHelper() public static EntityConfiguration GetEntityConfiguration(this string typeShortAlias) { - return _configurationStore.Get(typeShortAlias); + return ConfigurationStore.Get(typeShortAlias); } /// @@ -42,7 +44,7 @@ public static EntityConfiguration GetEntityConfiguration(this string typeShortAl /// public static EntityConfiguration GetEntityConfiguration(this Type entityType) { - return _configurationStore.Get(entityType); + return ConfigurationStore.Get(entityType); } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/EntityProperty.cs b/shesha-core/src/Shesha.Framework/Domain/EntityProperty.cs index f40f86f9f0..df6130b84e 100644 --- a/shesha-core/src/Shesha.Framework/Domain/EntityProperty.cs +++ b/shesha-core/src/Shesha.Framework/Domain/EntityProperty.cs @@ -18,54 +18,54 @@ public class EntityProperty: FullAuditedEntity /// /// Owner entity config /// - public virtual EntityConfig EntityConfig { get; set; } + public required virtual EntityConfig EntityConfig { get; set; } /// /// Property Name /// - public virtual string Name { get; set; } + public virtual string Name { get; set; } = default!; /// /// Label (display name) /// [StringLength(300)] - public virtual string Label { get; set; } + public virtual string? Label { get; set; } /// /// Description /// [DataType(System.ComponentModel.DataAnnotations.DataType.MultilineText)] - public virtual string Description { get; set; } + public virtual string? Description { get; set; } /// /// Data type /// [StringLength(100)] - public virtual string DataType { get; set; } + public virtual string? DataType { get; set; } /// /// Data format /// [StringLength(100)] - public virtual string DataFormat { get; set; } + public virtual string? DataFormat { get; set; } /// /// Entity type. Aplicable for entity references /// [StringLength(300)] - public virtual string EntityType { get; set; } + public virtual string? EntityType { get; set; } /// /// Reference list name /// [StringLength(100)] - public virtual string ReferenceListName { get; set; } + public virtual string? ReferenceListName { get; set; } /// /// Reference list module /// [StringLength(300)] - public virtual string ReferenceListModule { get; set; } + public virtual string? ReferenceListModule { get; set; } /// /// Source of the property (code/user) @@ -80,18 +80,18 @@ public class EntityProperty: FullAuditedEntity /// /// Parent property /// - public virtual EntityProperty ParentProperty { get; set; } + public virtual EntityProperty? ParentProperty { get; set; } /// /// Child properties (applicable for objects) /// [InverseProperty("ParentPropertyId")] - public virtual IList Properties { get; set; } + public virtual IList Properties { get; set; } = new List(); /// /// Items type (applicable for arrays) /// - public virtual EntityProperty ItemsType { get; set; } + public virtual EntityProperty? ItemsType { get; set; } /// /// If true, indicates that current property is a framework-related (e.g. , ) @@ -148,12 +148,12 @@ public EntityProperty() /// /// Validation RegularExpression /// - public virtual string RegExp { get; set; } + public virtual string? RegExp { get; set; } /// /// Validation message /// - public virtual string ValidationMessage { get; set; } + public virtual string? ValidationMessage { get; set; } /// /// Allows to create child/nested entity diff --git a/shesha-core/src/Shesha.Framework/Domain/EntityPropertyValue.cs b/shesha-core/src/Shesha.Framework/Domain/EntityPropertyValue.cs index 5396ef10b5..12fdb5c354 100644 --- a/shesha-core/src/Shesha.Framework/Domain/EntityPropertyValue.cs +++ b/shesha-core/src/Shesha.Framework/Domain/EntityPropertyValue.cs @@ -1,12 +1,4 @@ -using Abp.Domain.Entities.Auditing; -using Shesha.Domain.Attributes; -using Shesha.Domain.Enums; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text; -using JetBrains.Annotations; +using Shesha.Domain.Attributes; namespace Shesha.Domain { diff --git a/shesha-core/src/Shesha.Framework/Domain/EntityVisibility.cs b/shesha-core/src/Shesha.Framework/Domain/EntityVisibility.cs index d1653e225d..155a1501ad 100644 --- a/shesha-core/src/Shesha.Framework/Domain/EntityVisibility.cs +++ b/shesha-core/src/Shesha.Framework/Domain/EntityVisibility.cs @@ -1,8 +1,6 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Shesha.Domain.Attributes; +using Shesha.Domain.Attributes; using Shesha.Domain.Enums; +using System.ComponentModel.DataAnnotations.Schema; namespace Shesha.Domain { diff --git a/shesha-core/src/Shesha.Framework/Domain/FilterCriteria.cs b/shesha-core/src/Shesha.Framework/Domain/FilterCriteria.cs deleted file mode 100644 index a008b63689..0000000000 --- a/shesha-core/src/Shesha.Framework/Domain/FilterCriteria.cs +++ /dev/null @@ -1,83 +0,0 @@ -using JetBrains.Annotations; -using System; -using System.Collections.Generic; - -namespace Shesha.Domain -{ - public class FilterCriteria : ICloneable - { - public enum FilterMethod - { - DynamicLinq, - Hql, - } - - public FilterCriteria(FilterCriteria.FilterMethod filteringMethod) - { - Init(); - FilteringMethod = filteringMethod; - } - - private void Init() - { - FilterClauses = new List(); - FilterParameters = new Dictionary(); - FilteringMethod = FilterMethod.DynamicLinq; - } - - public FilterMethod FilteringMethod { get; protected set; } - public List FilterClauses { get; protected set; } - public Dictionary FilterParameters { get; protected set; } - - /// - /// Copies all the 's FilterClauses and FilterParameters to this - /// FilterCriteria. - /// - /// - public void AppendCriteria([NotNull]FilterCriteria criteria) - { - if (criteria.FilteringMethod != this.FilteringMethod) - throw new Exception("The filtering method used is incompatible and may cause problems due to differences in syntax."); - - FilterClauses.AddRange(criteria.FilterClauses); - - foreach (var param in criteria.FilterParameters) - { - FilterParameters.Add(param.Key, param.Value); - } - } - - /// - /// Adds a criterion to the criteria. - /// - /// String specifying the criterion e.g. 'ent.PropertyName = {0}' - /// where: - /// - it is assumed that ent is the alias assigned to the root entity being queried. - /// - {0} indicates the position where the parameter will be inserted. The parameter name will be automatically assigned. - /// The parameter value. - public void AddParameterisedCriterion(string criterionPattern, object parameterValue) - { - var queryParamName = "p" + FilterParameters.Count.ToString(); - FilterClauses.Add(string.Format(criterionPattern, ":" + queryParamName)); - FilterParameters.Add(queryParamName, parameterValue); - } - - #region ICloneable Members - - public object Clone() - { - var clone = new FilterCriteria(FilteringMethod); - - clone.FilteringMethod = FilteringMethod; - foreach (var o in FilterClauses) - clone.FilterClauses.Add(o); - foreach (var o in FilterParameters) - clone.FilterParameters.Add(o.Key, o.Value); // Assumes that the value is a value type or else will still be referencing the same object which presents a slight risk i.e. changes on the clone will impact changes on the original copy. - - return clone; - } - - #endregion - } - -} diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Domain/FormConfiguration.cs b/shesha-core/src/Shesha.Framework/Domain/FormConfiguration.cs similarity index 69% rename from shesha-core/src/Shesha.Web.FormsDesigner/Domain/FormConfiguration.cs rename to shesha-core/src/Shesha.Framework/Domain/FormConfiguration.cs index dac34d75af..c7051a5480 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Domain/FormConfiguration.cs +++ b/shesha-core/src/Shesha.Framework/Domain/FormConfiguration.cs @@ -1,9 +1,7 @@ -using Shesha.Domain; -using Shesha.Domain.Attributes; -using Shesha.NHibernate.Attributes; +using Shesha.Domain.Attributes; using System.ComponentModel.DataAnnotations; -namespace Shesha.Web.FormsDesigner.Domain +namespace Shesha.Domain { /// /// Form configuration @@ -19,20 +17,14 @@ public class FormConfiguration : ConfigurationItemBase /// Form markup /// [StringLength(int.MaxValue)] - [Lazy] - public virtual string Markup { get; set; } + [LazyLoad] + public virtual string? Markup { get; set; } /// /// ModelType /// [StringLength(int.MaxValue)] - public virtual string ModelType { get; set; } - - /// - /// Type - /// - [StringLength(100)] - public virtual string Type { get; set; } + public virtual string? ModelType { get; set; } public override string ItemType => ItemTypeName; @@ -44,7 +36,7 @@ public class FormConfiguration : ConfigurationItemBase /// /// Template that was used for the form creation /// - public virtual FormConfiguration Template { get; set; } + public virtual FormConfiguration? Template { get; set; } public virtual string FullName => Module != null ? $"{Module.Name}.{Name}" diff --git a/shesha-core/src/Shesha.Framework/Domain/FormIdentifier.cs b/shesha-core/src/Shesha.Framework/Domain/FormIdentifier.cs index fa65869669..8f2ab1df5e 100644 --- a/shesha-core/src/Shesha.Framework/Domain/FormIdentifier.cs +++ b/shesha-core/src/Shesha.Framework/Domain/FormIdentifier.cs @@ -1,7 +1,4 @@ -using Newtonsoft.Json; -using Shesha.EntityReferences; -using System; -using System.ComponentModel; +using System; namespace Shesha.Domain { @@ -9,13 +6,15 @@ namespace Shesha.Domain /// Form identifier /// [Serializable] - public class FormIdentifier : ConfigurationItemIdentifier + public class FormIdentifier : ConfigurationItemIdentifier, IIdentifierFactory { - public FormIdentifier(string module, string name) : base(module, name) + public FormIdentifier(string? module, string name) : base(module, name) { } - [JsonIgnore] - public override string ItemType => "form"; + public static FormIdentifier New(string? module, string name) + { + return new FormIdentifier(module, name); + } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Domain/FullAuditedEntityWithExternalSync.cs b/shesha-core/src/Shesha.Framework/Domain/FullAuditedEntityWithExternalSync.cs index 5b42e6675d..7c307e62aa 100644 --- a/shesha-core/src/Shesha.Framework/Domain/FullAuditedEntityWithExternalSync.cs +++ b/shesha-core/src/Shesha.Framework/Domain/FullAuditedEntityWithExternalSync.cs @@ -1,8 +1,7 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Abp.Domain.Entities.Auditing; -using Shesha.Domain.Attributes; +using Abp.Domain.Entities.Auditing; using Shesha.Domain.Enums; +using System; +using System.ComponentModel.DataAnnotations; namespace Shesha.Domain { diff --git a/shesha-core/src/Shesha.Framework/Domain/FullPowerChildEntity.cs b/shesha-core/src/Shesha.Framework/Domain/FullPowerChildEntity.cs index f7ef82c85d..4449817b8a 100644 --- a/shesha-core/src/Shesha.Framework/Domain/FullPowerChildEntity.cs +++ b/shesha-core/src/Shesha.Framework/Domain/FullPowerChildEntity.cs @@ -1,9 +1,7 @@ -using System; +using Abp.Domain.Entities; +using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Abp.Domain.Entities; -using Abp.Domain.Entities.Auditing; -using Shesha.Domain.Enums; namespace Shesha.Domain { @@ -20,18 +18,18 @@ public abstract class FullPowerChildEntity: FullPowerEntity, IHasOwningEntityLin /// Owning entity ID. ID is intentionally bigger because in some cases an owner ID is set to a data table ID string /// [Column("Frwk_OwnerId"), StringLength(255)] - public virtual string OwnerId { get; set; } + public virtual string? OwnerId { get; set; } /// /// Owning entity type alias /// [Column("Frwk_OwnerType"), StringLength(100)] - public virtual string OwnerType { get; set; } + public virtual string? OwnerType { get; set; } /// /// Sets owner with an entity object /// - public virtual void SetOwner(IEntity entity) + public virtual void SetOwner(IEntity entity) where TId: notnull { var config = entity.GetType().GetEntityConfiguration(); if (string.IsNullOrEmpty(config.TypeShortAlias)) diff --git a/shesha-core/src/Shesha.Framework/Domain/FullPowerManyToManyLinkEntity.cs b/shesha-core/src/Shesha.Framework/Domain/FullPowerManyToManyLinkEntity.cs index 7979d8f086..0d0df2cb36 100644 --- a/shesha-core/src/Shesha.Framework/Domain/FullPowerManyToManyLinkEntity.cs +++ b/shesha-core/src/Shesha.Framework/Domain/FullPowerManyToManyLinkEntity.cs @@ -28,7 +28,7 @@ public abstract class FullPowerManyToManyLinkEntity : FullPowerChildEntity, IHas /// /// Sets Entity from an entity object /// - public virtual void SetEntity(IEntity entity) + public virtual void SetEntity(IEntity entity) where TId : notnull { var config = entity.GetType().GetEntityConfiguration(); if (string.IsNullOrEmpty(config.TypeShortAlias)) diff --git a/shesha-core/src/Shesha.Framework/Domain/IHasEntityLink.cs b/shesha-core/src/Shesha.Framework/Domain/IHasEntityLink.cs index dbceb5e1a1..fe234a02bc 100644 --- a/shesha-core/src/Shesha.Framework/Domain/IHasEntityLink.cs +++ b/shesha-core/src/Shesha.Framework/Domain/IHasEntityLink.cs @@ -22,7 +22,7 @@ public interface IHasEntityLink /// Sets the owners of the entity. /// /// The entity which can be used as a Entity for the entity implementing this interface. - void SetEntity(IEntity entity); + void SetEntity(IEntity entity) where TId : notnull; /// /// Sets the Entity of the entity. diff --git a/shesha-core/src/Shesha.Framework/Domain/IHasOwningEntityLink.cs b/shesha-core/src/Shesha.Framework/Domain/IHasOwningEntityLink.cs index b3ff0fb619..30e8cf8b43 100644 --- a/shesha-core/src/Shesha.Framework/Domain/IHasOwningEntityLink.cs +++ b/shesha-core/src/Shesha.Framework/Domain/IHasOwningEntityLink.cs @@ -13,24 +13,18 @@ public interface IHasOwningEntityLink /// /// Owning entity ID /// - string OwnerId { get; } + string? OwnerId { get; } /// /// Owning entity type alias /// - string OwnerType { get; } - - /*/// - /// Gets the owning entity object - /// - IEntity GetOwner(); - */ + string? OwnerType { get; } /// /// Sets the owners of the entity. /// /// The entity which owns the entity implementing this interface. - void SetOwner(IEntity owner); + void SetOwner(IEntity owner) where TId : notnull; /// /// Sets the owners of the entity. diff --git a/shesha-core/src/Shesha.Framework/Domain/IMayHaveFrontEndApplication.cs b/shesha-core/src/Shesha.Framework/Domain/IMayHaveFrontEndApplication.cs index d9f07da496..5cc0f7ae79 100644 --- a/shesha-core/src/Shesha.Framework/Domain/IMayHaveFrontEndApplication.cs +++ b/shesha-core/src/Shesha.Framework/Domain/IMayHaveFrontEndApplication.cs @@ -8,6 +8,6 @@ public interface IMayHaveFrontEndApplication /// /// Front-end application /// - FrontEndApp Application { get; set; } + FrontEndApp? Application { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/IMayHaveUser.cs b/shesha-core/src/Shesha.Framework/Domain/IMayHaveUser.cs index 8a998d05e2..eea5056797 100644 --- a/shesha-core/src/Shesha.Framework/Domain/IMayHaveUser.cs +++ b/shesha-core/src/Shesha.Framework/Domain/IMayHaveUser.cs @@ -1,10 +1,4 @@ -using Abp.Auditing; -using Abp.Domain.Entities; -using Abp.Domain.Entities.Auditing; -using Shesha.Authorization.Users; -using Shesha.ConfigurationItems; -using System; -using System.ComponentModel.DataAnnotations; +using Shesha.Authorization.Users; namespace Shesha.Domain { @@ -13,6 +7,6 @@ namespace Shesha.Domain /// public interface IMayHaveUser { - User User { get; set; } + User? User { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/Imports/ImportResult.cs b/shesha-core/src/Shesha.Framework/Domain/Imports/ImportResult.cs index dbe95f104d..c03fe011d4 100644 --- a/shesha-core/src/Shesha.Framework/Domain/Imports/ImportResult.cs +++ b/shesha-core/src/Shesha.Framework/Domain/Imports/ImportResult.cs @@ -1,8 +1,4 @@ -using Abp.Domain.Entities; -using Abp.Domain.Entities.Auditing; -using Shesha.Domain; -using Shesha.Domain.Attributes; -using Shesha.Domain.Enums; +using Shesha.Domain.Attributes; using System; using System.ComponentModel.DataAnnotations; diff --git a/shesha-core/src/Shesha.Framework/Domain/MappingHelper.cs b/shesha-core/src/Shesha.Framework/Domain/MappingHelper.cs index 49f3a14020..a5cd059048 100644 --- a/shesha-core/src/Shesha.Framework/Domain/MappingHelper.cs +++ b/shesha-core/src/Shesha.Framework/Domain/MappingHelper.cs @@ -1,5 +1,6 @@ using Abp.Domain.Entities; using PluralizeService.Core; +using Shesha.Configuration.Runtime.Exceptions; using Shesha.Domain.Attributes; using Shesha.Domain.Conventions; using Shesha.Domain.Interfaces; @@ -12,6 +13,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -25,6 +27,8 @@ public static class MappingHelper public static void AddDatabasePrefixForAssembly(Assembly assembly, string databasePrefix) { + ArgumentException.ThrowIfNullOrWhiteSpace(assembly.FullName); + Prefixes[assembly.FullName] = databasePrefix; } @@ -76,17 +80,22 @@ private static void GetBaseClasses(Type type, ref List classes) } } + public static string GetTableName(Type entityType) + { + return GetTableNameOrNull(entityType) ?? throw new EntityHasNoTableException(entityType); + } + /// /// Returns table name for the specified entity type /// /// /// - public static string GetTableName(Type entityType) + public static string? GetTableNameOrNull(Type entityType) { if (IsRootEntity(entityType)) { // If the `TableAttribute` exists - use it. Note: we search the attribute in base classes too, it allows to use it in the abstract classes - var tableAttribute = entityType.Closest(t => t.BaseType, t => t.HasAttribute())?.GetAttribute(); + var tableAttribute = entityType.Closest(t => t.BaseType, t => t.HasAttribute())?.GetAttributeOrNull(); if (tableAttribute != null) return tableAttribute.Name; @@ -97,13 +106,18 @@ public static string GetTableName(Type entityType) return name; } - else - return GetTableName(GetRootEntity(entityType)); + else + { + var root = GetRootEntity(entityType); + return root != null + ? GetTableName(root) + : null; + } } public static (Type parentType, Type parentIdType, Type childType, Type childIdType) GetManyToManyTableData(MemberInfo property) { - var parentType = property.DeclaringType; + var parentType = property.DeclaringType.NotNull(); var parentIdType = parentType.GetProperty("Id")?.PropertyType; if (parentIdType == null) throw new NullReferenceException($"'Id' property not found for '{parentType.FullName}'"); @@ -123,16 +137,21 @@ public static (Type parentType, Type parentIdType, Type childType, Type childIdT /// /// /// - public static string GetSchemaName(Type entityType) + public static string? GetSchemaName(Type entityType) { if (IsRootEntity(entityType)) { // If the `TableAttribute` exists - use it. Note: we search the attribute in base classes too, it allows to use it in the abstract classes - var tableAttribute = entityType.Closest(t => t.BaseType, t => t.HasAttribute())?.GetAttribute(); + var tableAttribute = entityType.Closest(t => t.BaseType, t => t.HasAttribute())?.GetAttributeOrNull(); return tableAttribute?.Schema; } - else - return GetSchemaName(GetRootEntity(entityType)); + else + { + var root = GetRootEntity(entityType); + return root != null + ? GetSchemaName(root) + : null; + } } /// @@ -140,13 +159,13 @@ public static string GetSchemaName(Type entityType) /// public static string GetColumnName(MemberInfo memberInfo) { - var columnAttribute = memberInfo.GetAttribute(true); + var columnAttribute = memberInfo.GetAttributeOrNull(true); if (!string.IsNullOrWhiteSpace(columnAttribute?.Name)) return columnAttribute.Name; // Handle overrided properties. Try to search the same property in the base class and if it declared on the upper level - use name from base class - if (!IsRootEntity(memberInfo.DeclaringType) && memberInfo.DeclaringType.BaseType != null) + if (!IsRootEntity(memberInfo.DeclaringType.NotNull()) && memberInfo.DeclaringType.BaseType != null) { var upperLevelProperty = memberInfo.DeclaringType.BaseType.GetProperty(memberInfo.Name); if (upperLevelProperty != null) @@ -172,7 +191,7 @@ public static string GetColumnName(MemberInfo memberInfo) /// public static string GetIdColumnName(MemberInfo memberInfo) { - var columnAttribute = memberInfo.GetAttribute(true); + var columnAttribute = memberInfo.GetAttributeOrNull(true); if (!string.IsNullOrWhiteSpace(columnAttribute?.Name)) return columnAttribute.Name; @@ -183,7 +202,7 @@ public static string GetIdColumnName(MemberInfo memberInfo) /// /// Get column name for the specified member (property/field) with explicitly specified prefix, name and suffix /// - public static string GetNameForMember(MemberInfo memberInfo, string prefix, string name, string suffix) + public static string GetNameForMember(MemberInfo memberInfo, string prefix, string name, string? suffix) { var conventions = GetNamingConventions(memberInfo); return conventions.GetColumnName(prefix, name, suffix); @@ -192,7 +211,7 @@ public static string GetNameForMember(MemberInfo memberInfo, string prefix, stri /// /// Get column name for the specified entity type /// - public static string GetNameForColumn(Type type, string prefix, string name, string suffix) + public static string GetNameForColumn(Type type, string prefix, string name, string? suffix) { var conventions = GetNamingConventions(type); return conventions.GetColumnName(prefix, name, suffix); @@ -200,13 +219,13 @@ public static string GetNameForColumn(Type type, string prefix, string name, str private static INamingConventions GetNamingConventions(MemberInfo memberInfo) { - return GetNamingConventions(memberInfo.ReflectedType); + return GetNamingConventions(memberInfo.ReflectedType.NotNull()); } private static INamingConventions GetNamingConventions(Type type) { - var conventionsType = type.GetAttribute(false)?.ConventionsType ?? typeof(DefaultNamingConventions); - return Activator.CreateInstance(conventionsType) as INamingConventions; + var conventionsType = type.GetAttributeOrNull(false)?.ConventionsType ?? typeof(DefaultNamingConventions); + return ActivatorHelper.CreateNotNullObject(conventionsType).ForceCastAs(); } public static Type GetPropertyOrFieldType(this MemberInfo propertyOrField) @@ -235,20 +254,26 @@ public static string GetColumnPrefix(Type type) var prefixAttribute = type.GetCustomAttribute(); if (prefixAttribute != null) { - return !prefixAttribute.UsePrefixes - ? "" - : prefixAttribute.Prefix; + return prefixAttribute.UsePrefixes && !string.IsNullOrWhiteSpace(prefixAttribute.Prefix) + ? prefixAttribute.Prefix + : string.Empty; } if (!IsRootEntity(type)) { - Type rootType = GetRootEntity(type); - if (rootType != null && rootType.Assembly.FullName != type.Assembly.FullName) + var rootType = GetRootEntity(type); + + if (rootType != null) { - // This column extends a table created in another module - we should add a prefix - if (!Prefixes.ContainsKey(rootType.Assembly.FullName) - || Prefixes[rootType.Assembly.FullName] != Prefixes[type.Assembly.FullName]) - return GetTablePrefix(type); + var typeAssemblyName = type.GetAssemblyFullName(); + var rootTypeAssemblyName = rootType.GetAssemblyFullName(); + if (rootTypeAssemblyName != typeAssemblyName) + { + // This column extends a table created in another module - we should add a prefix + if (!Prefixes.ContainsKey(rootTypeAssemblyName) + || Prefixes[rootTypeAssemblyName] != Prefixes[typeAssemblyName]) + return GetTablePrefix(type); + } } } @@ -262,9 +287,9 @@ public static string GetColumnPrefix(Type type) /// public static string GetTablePrefix(Type type) { - return Prefixes.ContainsKey(type.Assembly.FullName) - ? Prefixes[type.Assembly.FullName] ?? "" - : ""; + return Prefixes.TryGetValue(type.GetAssemblyFullName(), out var prefix) + ? prefix ?? string.Empty + : string.Empty; } @@ -273,7 +298,7 @@ public static string GetTablePrefix(Type type) /// /// /// - private static Type GetRootEntity(Type type) + private static Type? GetRootEntity(Type? type) { if (type == null) return null; @@ -288,7 +313,7 @@ private static Type GetRootEntity(Type type) /// /// /// - public static bool IsRootEntity(Type type) + public static bool IsRootEntity(Type? type) { return (IsEntity(type) && !type.HasAttribute() @@ -299,7 +324,7 @@ public static bool IsRootEntity(Type type) /// /// Returns true if the specified type is an entity type /// - public static bool IsEntity(Type type) + public static bool IsEntity([NotNullWhen(true)]Type? type) { // todo: use global helper return type != null && @@ -377,11 +402,11 @@ public static bool IsDictionaryType(Type type) /// public static string GetForeignKeyColumn(MemberInfo prop) { - var foreignKeyAttribute = prop.GetAttribute(true); + var foreignKeyAttribute = prop.GetAttributeOrNull(true); if (foreignKeyAttribute != null) return foreignKeyAttribute.Name; - var columnPrefix = GetColumnPrefix(prop.DeclaringType); + var columnPrefix = GetColumnPrefix(prop.DeclaringType.NotNull()); var conventions = GetNamingConventions(prop); return conventions.GetColumnName(columnPrefix, prop.Name, "Id"); @@ -394,12 +419,12 @@ public static string GetForeignKeyColumn(MemberInfo prop) /// public static object GetDiscriminatorValue(Type type) { - var discriminatorValueAttribute = type.GetAttribute(); + var discriminatorValueAttribute = type.GetAttributeOrNull(); if (discriminatorValueAttribute != null) return discriminatorValueAttribute.Value; - var prefix = Prefixes[type.Assembly.FullName]?.TrimEnd('_', '.'); + var prefix = Prefixes[type.Assembly.FullName.NotNull()]?.TrimEnd('_', '.'); var discriminatorValue = !string.IsNullOrWhiteSpace(prefix) ? prefix + "." + type.Name @@ -417,7 +442,7 @@ public static object GetDiscriminatorValue(Type type) /// /// /// - public static string GetDiscriminatorColumn(Type type) + public static string? GetDiscriminatorColumn(Type type) { var attribute = type.GetCustomAttributes(true) .Where(a => a is DiscriminatorAttribute) diff --git a/shesha-core/src/Shesha.Framework/Domain/NotificationChannelIdentifier.cs b/shesha-core/src/Shesha.Framework/Domain/NotificationChannelIdentifier.cs deleted file mode 100644 index 64631a7818..0000000000 --- a/shesha-core/src/Shesha.Framework/Domain/NotificationChannelIdentifier.cs +++ /dev/null @@ -1,22 +0,0 @@ - -using Newtonsoft.Json; -using Shesha.EntityReferences; -using System; -using System.ComponentModel; - -namespace Shesha.Domain -{ - /// - /// Form identifier - /// - [Serializable] - public class NotificationChannelIdentifier : ConfigurationItemIdentifier - { - public NotificationChannelIdentifier(string module, string name) : base(module, name) - { - } - - [JsonIgnore] - public override string ItemType => "notification-channel"; - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Domain/NotificationIdentifier.cs b/shesha-core/src/Shesha.Framework/Domain/NotificationIdentifier.cs deleted file mode 100644 index 941217e318..0000000000 --- a/shesha-core/src/Shesha.Framework/Domain/NotificationIdentifier.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace Shesha.Domain -{ - /// - /// Notification identifier - /// - [Serializable] - public class NotificationIdentifier : ConfigurationItemIdentifier - { - public NotificationIdentifier(string module, string name) : base(module, name) - { - } - - [JsonIgnore] - public override string ItemType => "notification"; - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Domain/OtpAuditItem.cs b/shesha-core/src/Shesha.Framework/Domain/OtpAuditItem.cs index 5a986bdb97..0e1679bad2 100644 --- a/shesha-core/src/Shesha.Framework/Domain/OtpAuditItem.cs +++ b/shesha-core/src/Shesha.Framework/Domain/OtpAuditItem.cs @@ -21,10 +21,10 @@ public class OtpAuditItem: FullAuditedEntity public virtual OtpSendType SendType { get; set; } [StringLength(100)] - public virtual string RecipientType { get; set; } + public virtual string? RecipientType { get; set; } [StringLength(40)] - public virtual string RecipientId { get; set; } + public virtual string? RecipientId { get; set; } /// /// Expiration date @@ -40,11 +40,11 @@ public class OtpAuditItem: FullAuditedEntity public virtual string Otp { get; set; } [StringLength(100)] - public virtual string ActionType { get; set; } + public virtual string? ActionType { get; set; } public virtual OtpSendStatus SendStatus { get; set; } [StringLength(int.MaxValue)] - public virtual string ErrorMessage { get; set; } + public virtual string? ErrorMessage { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/PermissionDefinition.cs b/shesha-core/src/Shesha.Framework/Domain/PermissionDefinition.cs index 479aaa0549..bf927a7fe8 100644 --- a/shesha-core/src/Shesha.Framework/Domain/PermissionDefinition.cs +++ b/shesha-core/src/Shesha.Framework/Domain/PermissionDefinition.cs @@ -12,6 +12,6 @@ public class PermissionDefinition : ConfigurationItemBase /// /// Parent of this permission if one exists. /// - public virtual string Parent { get; set; } + public virtual string? Parent { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Domain/PermissionedObject.cs b/shesha-core/src/Shesha.Framework/Domain/PermissionedObject.cs index 9db6c7f29b..30eb7cec8e 100644 --- a/shesha-core/src/Shesha.Framework/Domain/PermissionedObject.cs +++ b/shesha-core/src/Shesha.Framework/Domain/PermissionedObject.cs @@ -24,35 +24,35 @@ public class PermissionedObject : FullAuditedEntity /// /// Category for grouping objects /// - public virtual string Category { get; set; } + public virtual string? Category { get; set; } /// /// Shesha Module of the permissioned object /// [NotNull] - public virtual Module Module { get; set; } + public virtual Module? Module { get; set; } /// /// Type of the permissioned object /// [NotNull] - public virtual string Type { get; set; } + public virtual string? Type { get; set; } /// /// Name for display in the configurator /// [NotNull] - public virtual string Name { get; set; } + public virtual string? Name { get; set; } /// /// Description for display in the configurator /// - public virtual string Description { get; set; } + public virtual string? Description { get; set; } /// /// List of permissions required to access this securable (comma-separated permission identifiers) /// - public virtual string Permissions { get; set; } + public virtual string? Permissions { get; set; } /// /// Object inherits permissions from parent object @@ -67,7 +67,7 @@ public class PermissionedObject : FullAuditedEntity /// /// Text identifier of the parent object /// - public virtual string Parent { get; set; } + public virtual string? Parent { get; set; } public virtual bool Hidden { get; set; } diff --git a/shesha-core/src/Shesha.Framework/Domain/QueryBuilder/JoinClause.cs b/shesha-core/src/Shesha.Framework/Domain/QueryBuilder/JoinClause.cs deleted file mode 100644 index eaee369c63..0000000000 --- a/shesha-core/src/Shesha.Framework/Domain/QueryBuilder/JoinClause.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Shesha.Domain.QueryBuilder -{ - /// - /// Join class - /// - public class JoinClause - { - public string Reference { get; set; } - public string Alias { get; set; } - public JoinType JoinType { get; set; } - public string Condition { get; set; } - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Domain/QueryBuilder/JoinType.cs b/shesha-core/src/Shesha.Framework/Domain/QueryBuilder/JoinType.cs deleted file mode 100644 index df705af65b..0000000000 --- a/shesha-core/src/Shesha.Framework/Domain/QueryBuilder/JoinType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Shesha.Domain.QueryBuilder -{ - /// - /// Join type - /// - public enum JoinType - { - Left, - Inner - } -} diff --git a/shesha-core/src/Shesha.Framework/Domain/QueryBuilder/QueryBuildingContext.cs b/shesha-core/src/Shesha.Framework/Domain/QueryBuilder/QueryBuildingContext.cs deleted file mode 100644 index 6f978318ae..0000000000 --- a/shesha-core/src/Shesha.Framework/Domain/QueryBuilder/QueryBuildingContext.cs +++ /dev/null @@ -1,28 +0,0 @@ -using JetBrains.Annotations; -using System; -using System.Collections.Generic; - -namespace Shesha.Domain.QueryBuilder -{ - /// - /// Query building context - /// - public class QueryBuildingContext - { - [NotNull] - public FilterCriteria FilterCriteria { get; private set; } - - [NotNull] - public Type RootClass { get; private set; } - - public List Joins { get; private set; } = new List(); - - public string OrderBy { get; set; } - - public QueryBuildingContext(Type rootClass) - { - RootClass = rootClass; - FilterCriteria = new FilterCriteria(FilterCriteria.FilterMethod.Hql); - } - } -} diff --git a/shesha-core/src/Shesha.Framework/Domain/ReferenceList.cs b/shesha-core/src/Shesha.Framework/Domain/ReferenceList.cs index d2bd728228..773564680f 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ReferenceList.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ReferenceList.cs @@ -1,9 +1,7 @@ using Abp.Domain.Entities; using Shesha.Domain.Attributes; using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; namespace Shesha.Domain { @@ -24,7 +22,7 @@ public ReferenceList() [StringLength(300)] [Display(Name = "Namespace")] [Obsolete("Is used for backward compatibility only")] - public virtual string Namespace { get; set; } + public virtual string? Namespace { get; set; } /// /// If true indicates that the application logic references diff --git a/shesha-core/src/Shesha.Framework/Domain/ReferenceListIdentifier.cs b/shesha-core/src/Shesha.Framework/Domain/ReferenceListIdentifier.cs index 52e0f6cefb..eeaa06cd0b 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ReferenceListIdentifier.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ReferenceListIdentifier.cs @@ -1,22 +1,30 @@ -namespace Shesha.Domain +using Newtonsoft.Json; +using System; + +namespace Shesha.Domain { /// /// Identifier of the (Module, Namespace and Name) /// - public class ReferenceListIdentifier : ConfigurationItemIdentifier + [Serializable] + public class ReferenceListIdentifier : ConfigurationItemIdentifier, IIdentifierFactory { - public override string ItemType => "ref-list"; - - public ReferenceListIdentifier(string module, string name) : base(module, name) + [JsonConstructor] + public ReferenceListIdentifier(string? module, string name) : base(module, name) { } - public ReferenceListIdentifier(string module, string @namespace, string name): base( + public ReferenceListIdentifier(string? module, string? @namespace, string name): base( module, !string.IsNullOrWhiteSpace(@namespace) ? $"{@namespace}.{name}" : name) { } + + public static ReferenceListIdentifier New(string? module, string name) + { + return new ReferenceListIdentifier(module, name); + } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Domain/ReferenceListItem.cs b/shesha-core/src/Shesha.Framework/Domain/ReferenceListItem.cs index edd27c5f7d..ae32f91b37 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ReferenceListItem.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ReferenceListItem.cs @@ -12,13 +12,13 @@ public class ReferenceListItem : FullAuditedEntity, IMayHaveTenant [Required(AllowEmptyStrings = false), StringLength(300)] //[EntityDisplayName] [Display(Name = "Name")] - public virtual string Item { get; set; } + public virtual string? Item { get; set; } [Range(0, Int64.MaxValue)] [Display(Name = "Value")] public virtual Int64 ItemValue { get; set; } [StringLength(int.MaxValue)] - public virtual string Description { get; set; } + public virtual string? Description { get; set; } [Display(Name = "Order Index")] public virtual Int64 OrderIndex { get; set; } @@ -30,37 +30,25 @@ public class ReferenceListItem : FullAuditedEntity, IMayHaveTenant public virtual ReferenceList ReferenceList { get; set; } //[Lazy(Laziness = HbmLaziness.False)] - public virtual ReferenceListItem Parent { get; set; } + public virtual ReferenceListItem? Parent { get; set; } /// /// Color associated with the item /// [StringLength(50)] - public virtual string Color { get; set; } + public virtual string? Color { get; set; } /// /// Icon associated with the item /// [StringLength(50)] - public virtual string Icon { get; set; } + public virtual string? Icon { get; set; } /// /// Short alias /// [StringLength(50)] - public virtual string ShortAlias { get; set; } - - /* - public override ICollection ValidationResults() - { - var results = base.ValidationResults(); - - if (ReferenceList != null && ReferenceList.Items.Any(i => i.ItemValue == this.ItemValue && i.Id != this.Id)) - results.Add(new ValidationResult("Item with the same value already exists", new List { "ItemValue" })); - - return results; - } - */ + public virtual string? ShortAlias { get; set; } public virtual void SetHardLinkToApplication(bool value) { diff --git a/shesha-core/src/Shesha.Framework/Domain/SeedHelper.cs b/shesha-core/src/Shesha.Framework/Domain/SeedHelper.cs deleted file mode 100644 index 60cfd760f0..0000000000 --- a/shesha-core/src/Shesha.Framework/Domain/SeedHelper.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Abp.Dependency; - -namespace Shesha.Domain -{ - /// - /// DB seed helper - /// - public static class SeedHelper - { - public static void SeedHostDb(IIocResolver iocResolver) - { - var roleAndUserBuilder = iocResolver.Resolve(); - roleAndUserBuilder?.Create(); - } - } -} diff --git a/shesha-core/src/Shesha.Framework/Domain/SettingConfiguration.cs b/shesha-core/src/Shesha.Framework/Domain/SettingConfiguration.cs index 634d8bd435..c2cf9e56fe 100644 --- a/shesha-core/src/Shesha.Framework/Domain/SettingConfiguration.cs +++ b/shesha-core/src/Shesha.Framework/Domain/SettingConfiguration.cs @@ -1,9 +1,7 @@ using Abp.Auditing; using Shesha.Domain.Attributes; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Threading.Tasks; namespace Shesha.Domain { @@ -24,11 +22,11 @@ public class SettingConfiguration: ConfigurationItemBase [StringLength(200)] [Audited] - public virtual string EditorFormName { get; set; } + public virtual string? EditorFormName { get; set; } [StringLength(200)] [Audited] - public virtual string EditorFormModule { get; set; } + public virtual string? EditorFormModule { get; set; } /// /// Allows ordering the setting in a logical manner within its category @@ -40,7 +38,7 @@ public class SettingConfiguration: ConfigurationItemBase /// [StringLength(200)] [Audited] - public virtual string Category { get; set; } + public virtual string? Category { get; set; } /// /// If true, indicates that this setting should be specific to each of the application's clients diff --git a/shesha-core/src/Shesha.Framework/Domain/SettingConfigurationIdentifier.cs b/shesha-core/src/Shesha.Framework/Domain/SettingConfigurationIdentifier.cs index dd8d4ba4d5..2d11da1bdd 100644 --- a/shesha-core/src/Shesha.Framework/Domain/SettingConfigurationIdentifier.cs +++ b/shesha-core/src/Shesha.Framework/Domain/SettingConfigurationIdentifier.cs @@ -1,18 +1,17 @@ -using Shesha.EntityReferences; -using System; -using System.ComponentModel; - -namespace Shesha.Domain +namespace Shesha.Domain { /// /// Setting configuration identifier /// - public class SettingConfigurationIdentifier : ConfigurationItemIdentifier + public class SettingConfigurationIdentifier : ConfigurationItemIdentifier, IIdentifierFactory { - public SettingConfigurationIdentifier(string module, string name) : base(module, name) + public SettingConfigurationIdentifier(string? module, string name) : base(module, name) { } - public override string ItemType => "setting-config"; + public static SettingConfigurationIdentifier New(string? module, string name) + { + return new SettingConfigurationIdentifier(module, name); + } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Domain/SettingValue.cs b/shesha-core/src/Shesha.Framework/Domain/SettingValue.cs index 518a1ce8de..37e046eb21 100644 --- a/shesha-core/src/Shesha.Framework/Domain/SettingValue.cs +++ b/shesha-core/src/Shesha.Framework/Domain/SettingValue.cs @@ -27,12 +27,12 @@ public class SettingValue: AuditedEntity, IMayHaveFrontEndApplication, IMa /// Front-end application, is used for application specific settings only /// [Audited] - public virtual FrontEndApp Application { get; set; } + public virtual FrontEndApp? Application { get; set; } /// /// This is used for user specific settings only /// [Audited] - public virtual User User { get; set; } + public virtual User? User { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Domain/ShaUserLoginAttempt.cs b/shesha-core/src/Shesha.Framework/Domain/ShaUserLoginAttempt.cs index 9a52c355f5..08d9852830 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ShaUserLoginAttempt.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ShaUserLoginAttempt.cs @@ -49,7 +49,7 @@ public class ShaUserLoginAttempt : Entity, IHasCreationTime, IMayHaveTenan /// Tenancy name. /// [StringLength(MaxTenancyNameLength)] - public virtual string TenancyName { get; set; } + public virtual string? TenancyName { get; set; } /// /// User's Id, if was a valid username or email address. @@ -96,9 +96,9 @@ public ShaUserLoginAttempt() } [StringLength(20)] - public virtual string IMEI { get; set; } + public virtual string? IMEI { get; set; } [StringLength(255)] - public virtual string DeviceName { get; set; } + public virtual string? DeviceName { get; set; } public virtual int? LoginAttemptNumber { get; set; } } diff --git a/shesha-core/src/Shesha.Framework/Domain/ShaUserRegistration.cs b/shesha-core/src/Shesha.Framework/Domain/ShaUserRegistration.cs index 22357d3506..355013a8d2 100644 --- a/shesha-core/src/Shesha.Framework/Domain/ShaUserRegistration.cs +++ b/shesha-core/src/Shesha.Framework/Domain/ShaUserRegistration.cs @@ -1,13 +1,9 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Abp.Authorization.Users; -using Abp.Domain.Entities; +using Abp.Domain.Entities; using Abp.Domain.Entities.Auditing; -using Abp.MultiTenancy; using Abp.Timing; -using Shesha.Authorization; using Shesha.Domain.Attributes; +using System; +using System.ComponentModel.DataAnnotations.Schema; namespace Shesha.Domain { @@ -28,7 +24,7 @@ public class ShaUserRegistration : Entity, IHasCreationTime /// /// Module name /// - public virtual FormIdentifier AdditionalRegistrationInfoForm { get; set; } + public virtual FormIdentifier? AdditionalRegistrationInfoForm { get; set; } /// /// @@ -43,7 +39,7 @@ public class ShaUserRegistration : Entity, IHasCreationTime /// /// /// - public virtual string GoToUrlAfterRegistration { get; set; } + public virtual string? GoToUrlAfterRegistration { get; set; } /// /// Initializes a new instance of the class. diff --git a/shesha-core/src/Shesha.Framework/Domain/StoredFile.cs b/shesha-core/src/Shesha.Framework/Domain/StoredFile.cs index aba679db37..9db9007375 100644 --- a/shesha-core/src/Shesha.Framework/Domain/StoredFile.cs +++ b/shesha-core/src/Shesha.Framework/Domain/StoredFile.cs @@ -1,9 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Globalization; -using Abp.Dependency; -using Abp.Domain.Entities; +using Abp.Domain.Entities; using Abp.Domain.Entities.Auditing; using JetBrains.Annotations; using Shesha.Configuration.Runtime; @@ -11,7 +6,8 @@ using Shesha.EntityReferences; using Shesha.Extensions; using Shesha.Reflection; -using Shesha.Services; +using System; +using System.ComponentModel.DataAnnotations; namespace Shesha.Domain { @@ -28,26 +24,16 @@ public StoredFile() { } - /// - /// Constructor - /// - public StoredFile(IEntityConfigurationStore entityConfigurationStore) - { - _entityConfigurationStore = entityConfigurationStore; - } - - private IEntityConfigurationStore _entityConfigurationStore; - [EntityDisplayName] public virtual string FileName { get; set; } public virtual string FileType { get; set; } - public virtual string Category { get; set; } + public virtual string? Category { get; set; } [DataType(DataType.MultilineText)] - public virtual string Description { get; set; } + public virtual string? Description { get; set; } public virtual int SortOrder { get; set; } @@ -60,7 +46,7 @@ public StoredFile(IEntityConfigurationStore entityConfigurationStore) /// The setter is private as it should be set once on creation and not changed. /// //[LogChanges] - public virtual string Folder { get; set; } + public virtual string? Folder { get; set; } /// /// If true, the file is version controlled and the full version @@ -86,7 +72,7 @@ public static StoredFile NewFor([NotNull]IEntity owner, IEntityConfigu if (owner.IsTransient()) throw new Exception("Owner is not persisted to the DB"); - var file = new StoredFile(entityConfigurationStore) + var file = new StoredFile { Owner = new GenericEntityReference(owner) }; @@ -99,9 +85,9 @@ public static StoredFile NewFor([NotNull]IEntity owner, IEntityConfigu /// /// Id type of the owner /// Owner entity - public virtual void SetOwner(IEntity entity) + public virtual void SetOwner(IEntity entity) where TId: notnull { - Owner = new GenericEntityReference(entity.Id.ToString(), entity.GetType().StripCastleProxyType().FullName); + Owner = new GenericEntityReference(entity.Id.ToString().NotNull(), entity.GetType().StripCastleProxyType().GetRequiredFullName()); } /// @@ -125,38 +111,5 @@ public virtual void SetOwner(string ownerType, string ownerId) public virtual bool Temporary { get; set; } #endregion - - /* - potentially unneeded properties/methods - - /// - /// Returns the metadata on the last version available. - /// - public virtual StoredFileVersion LastVersion - { - get { return Versions.OrderByDescending(o => o.VersionNo).FirstOrDefault(); } - } - - /// - /// Returns file version by version number. A latest version will be returner if versionNo = null - /// - /// Number of the version to search - /// - public virtual StoredFileVersion GetVersion(int? versionNo) - { - return versionNo.HasValue - ? Versions.FirstOrDefault(v => v.VersionNo == versionNo.Value) - : LastVersion; - } - - /// - /// The setters are private as they should be set through the methods on the service. - /// - public virtual bool Locked { get; protected set; } - - public virtual string LockedBy { get; protected set; } - public virtual string LockedDate { get; protected set; } - - */ } } diff --git a/shesha-core/src/Shesha.Framework/Dto/ConfigurationItemDto.cs b/shesha-core/src/Shesha.Framework/Dto/ConfigurationItemDto.cs new file mode 100644 index 0000000000..170de06f1b --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Dto/ConfigurationItemDto.cs @@ -0,0 +1,51 @@ +using Abp.Application.Services.Dto; +using Shesha.Domain.ConfigurationItems; +using Shesha.Dto.Interfaces; +using System; + +namespace Shesha.Dto +{ + /// + /// Base class of ConfigurationItem DTO + /// + public abstract class ConfigurationItemDto : EntityDto, IConfigurationItemDto + { + /// + /// Module Id + /// + public Guid? ModuleId { get; set; } + + /// + /// Origin id + /// + public Guid? OriginId { get; set; } + + /// + /// Module name + /// + public string? Module { get; set; } + + /// + /// Version number + /// + public int VersionNo { get; set; } + + /// + /// If true, indicates that this is a last version of the form + /// + public bool IsLastVersion { get; set; } + + /// + /// Version status + /// + public ConfigurationItemVersionStatus VersionStatus { get; set; } + + public string Name { get; set; } = string.Empty; + public string? Label { get; set; } + public string? Description { get; set; } + public bool HardLinkToApplication { get; set; } + public string? Namespace { get; set; } + public int? NoSelectionValue { get; set; } + public bool Suppress { get; set; } + } +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Dto/Interfaces/IConfigurationItemDto.cs b/shesha-core/src/Shesha.Framework/Dto/Interfaces/IConfigurationItemDto.cs index aa4e068970..27d727980a 100644 --- a/shesha-core/src/Shesha.Framework/Dto/Interfaces/IConfigurationItemDto.cs +++ b/shesha-core/src/Shesha.Framework/Dto/Interfaces/IConfigurationItemDto.cs @@ -1,10 +1,6 @@ using Shesha.Domain.ConfigurationItems; using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Dto.Interfaces { @@ -19,7 +15,7 @@ public interface IConfigurationItemDto /// /// Module name /// - public string Module { get; set; } + public string? Module { get; set; } /// /// Item name @@ -33,14 +29,14 @@ public interface IConfigurationItemDto /// [StringLength(200)] [Display(Name = "Label", Description = "Label of the item, can be used in lists as a user friendly name")] - string Label { get; set; } + string? Label { get; set; } /// /// Item description /// [StringLength(int.MaxValue)] [DataType(DataType.MultilineText)] - string Description { get; set; } + string? Description { get; set; } /// /// Version number diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Binder/EntityModelBinder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Binder/EntityModelBinder.cs index 4854f0a770..8ef43526da 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Binder/EntityModelBinder.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Binder/EntityModelBinder.cs @@ -90,29 +90,32 @@ private bool IsFrameworkFields(JProperty prop) ; } + private List GetFormFields(JObject jobject, List? formFields = null) + { + if (formFields != null) + return formFields; + + var _formFields = jobject.Property(nameof(IHasFormFieldsList._formFields)); + var formFieldsArray = _formFields?.Value as JArray; + return formFieldsArray?.Select(f => f.Value()).WhereNotNull().ToList() ?? new List(); + } + [UnitOfWork] public async Task BindPropertiesAsync(JObject jobject, object entity, EntityModelBindingContext context, - string propertyName = null, List formFields = null) + string? propertyName = null, List? formFields = null) { var entityType = entity.GetType().StripCastleProxyType(); var properties = entityType.GetProperties().ToList(); var entityIdValue = entity.GetType().GetProperty("Id")?.GetValue(entity)?.ToString(); - if (!entityIdValue.IsNullOrEmpty() && entityIdValue != Guid.Empty.ToString()) + if (!string.IsNullOrWhiteSpace(entityIdValue) && entityIdValue != Guid.Empty.ToString()) properties = properties.Where(p => p.Name != "Id").ToList(); - var config = await _modelConfigurationManager.GetModelConfigurationOrNullAsync(entityType.Namespace, entityType.Name); + var config = await _modelConfigurationManager.GetModelConfigurationAsync(entityType.Namespace, entityType.Name); context.LocalValidationResult = new List(); - var formFieldsInternal = formFields; - if (formFields == null) - { - var _formFields = jobject.Property(nameof(IHasFormFieldsList._formFields)); - var formFieldsArray = _formFields?.Value as JArray; - formFieldsInternal = formFieldsArray?.Select(f => f.Value()).ToList() ?? new List(); - } - + var formFieldsInternal = GetFormFields(jobject, formFields); formFieldsInternal = formFieldsInternal.Select(x => x.ToCamelCase()).ToList(); var jProps = jobject.Properties().ToList(); @@ -213,7 +216,7 @@ public async Task BindPropertiesAsync(JObject jobject, object entity, Enti case DataTypes.ReferenceListItem: case DataTypes.Time: // ToDo: Review parsing of time //case DataTypes.Enum: // Enum binded as integer - object parsedValue = null; + object? parsedValue = null; result = Parser.TryParseToValueType(jproperty.Value.ToString(), property.PropertyType, out parsedValue, isDateOnly: propType.DataType == DataTypes.Date); if (result && dbValue?.ToString() != parsedValue?.ToString()) if (await ValidateAsync(entity, jFullName, parsedValue, context)) @@ -262,7 +265,7 @@ public async Task BindPropertiesAsync(JObject jobject, object entity, Enti foreach (var item in jList) { context.ArrayItemIndex = arrayItemIndex++; - object newItem = null; + object? newItem = null; if (!item.IsNullOrEmpty()) { if (paramType.IsEntityType()) @@ -272,10 +275,10 @@ public async Task BindPropertiesAsync(JObject jobject, object entity, Enti } else { - newItem = await GetObjectOrObjectReferenceAsync(paramType, item as JObject, context, childFormFields); - r = await _objectValidatorManager.ValidateObjectAsync(newItem, context.LocalValidationResult) && newItem != null && r; + newItem = await GetObjectOrObjectReferenceAsync(paramType, item.ForceCastAs(), context, childFormFields); + r = newItem != null && await _objectValidatorManager.ValidateObjectAsync(newItem, context.LocalValidationResult) && r; } - addMethod?.Invoke(newArray, new[] { newItem }); + addMethod?.Invoke(newArray, [newItem]); } } if (r) @@ -339,7 +342,9 @@ public async Task BindPropertiesAsync(JObject jobject, object entity, Enti if (!int.TryParse(valComponents[i], out val)) { // Try parse enum - var prop = entity.GetType().GetProperty(ExtractName(propertyName)); + var prop = !string.IsNullOrWhiteSpace(propertyName) + ? entity.GetType().GetProperty(ExtractName(propertyName)) + : null; if (prop != null && prop.PropertyType.IsEnum) { var type = prop.PropertyType.GetUnderlyingTypeIfNullable(); @@ -363,7 +368,7 @@ public async Task BindPropertiesAsync(JObject jobject, object entity, Enti totalVal += val; } } - object refValue = null; + object? refValue = null; result = Parser.TryParseToValueType(totalVal.ToString(), property.PropertyType, out refValue); if (result && (await ValidateAsync(entity, jFullName, refValue, context))) property.SetValue(entity, refValue); @@ -388,7 +393,7 @@ public async Task BindPropertiesAsync(JObject jobject, object entity, Enti r = childObject != null; } } - if ((await _objectValidatorManager.ValidateObjectAsync(childObject, context.LocalValidationResult)) && r) + if (childObject == null || (await _objectValidatorManager.ValidateObjectAsync(childObject, context.LocalValidationResult)) && r) property.SetValue(entity, childObject); } else @@ -433,20 +438,20 @@ public async Task BindPropertiesAsync(JObject jobject, object entity, Enti private async Task PerformEntityReferenceAsync( object entity, PropertyInfo property, - ModelPropertyDto propConfig, + ModelPropertyDto? propConfig, JToken jValue, string jPropertyName, - object dbValue, - List childFormFields, + object? dbValue, + List? childFormFields, EntityModelBindingContext context, - Action action + Action action ) { // Get the rules of cascade update var cascadeAttr = property.GetCustomAttribute() ?? property.PropertyType.GetCustomAttribute() ?? (propConfig == null ? null - : new CascadeUpdateRulesAttribute(propConfig.CascadeCreate, propConfig.CascadeUpdate, propConfig.CascadeDeleteUnreferenced)); + : new CascadeUpdateRulesAttribute(propConfig.CascadeCreate ?? false, propConfig.CascadeUpdate ?? false, propConfig.CascadeDeleteUnreferenced ?? false)); var propertyType = property.PropertyType.IsGenericType ? property.PropertyType.GetGenericArguments()[0] @@ -461,15 +466,15 @@ Action action var allowedTypes = propertyType == typeof(GenericEntityReference) ? property.GetCustomAttribute()?.AllowableTypes : null; - if (!jchildClassName.IsNullOrEmpty() && allowedTypes != null && allowedTypes.Any() && !allowedTypes.Contains(jchildClassName)) + if (!string.IsNullOrWhiteSpace(jchildClassName) && allowedTypes != null && allowedTypes.Any() && !allowedTypes.Contains(jchildClassName)) { context.LocalValidationResult.Add(new ValidationResult($"`{jchildClassName}` is not allowed for `{property.Name}`.")); return; } - var childEntityType = jchildClassName.IsNullOrEmpty() + var childEntityType = string.IsNullOrEmpty(jchildClassName) ? JsonEntityProxy.GetUnproxiedType(propertyType) - : _entityConfigurationStore.Get(jchildClassName)?.EntityType; //: _typeFinder.Find(x => x.FullName == jchildClassName).FirstOrDefault(); + : _entityConfigurationStore.Get(jchildClassName)?.EntityType; if (childEntityType == null) { context.LocalValidationResult.Add(new ValidationResult($"Type `{jchildClassName}` not found.")); @@ -479,10 +484,10 @@ Action action if (!string.IsNullOrEmpty(jchildId)) { var newChildEntity = dbValue; - var childId = dbValue?.GetId().ToString(); + var childId = dbValue?.GetId()?.ToString(); // if child entity is specified - if (childId?.ToLower() != jchildId?.ToLower() + if (childId?.ToLower() != jchildId.ToLower() || propertyType.FullName != jchildClassName) { // id or class changed @@ -504,7 +509,7 @@ Action action context.LocalValidationResult.Add(new ValidationResult($"`{property.Name}` is not allowed to be updated.")); return; } - r = await BindPropertiesAsync(jEntity, newChildEntity, context, null, childFormFields); + r = await BindPropertiesAsync(jEntity, newChildEntity.NotNull(), context, null, childFormFields); r = r && await _objectValidatorManager.ValidateObjectAsync(newChildEntity, context.LocalValidationResult); } @@ -513,7 +518,7 @@ Action action if (r) { if (propertyType == typeof(GenericEntityReference)) - action(new GenericEntityReference(newChildEntity)); + action(new GenericEntityReference(newChildEntity.NotNull())); else action(newChildEntity); @@ -535,7 +540,7 @@ Action action return; } - var childEntity = Activator.CreateInstance(childEntityType); + var childEntity = ActivatorHelper.CreateNotNullObject(childEntityType); // create a new object if (!await BindPropertiesAsync(jEntity, childEntity, context, null, childFormFields)) return; @@ -585,7 +590,7 @@ Action action var childId = dbValue?.GetType().GetProperty("Id")?.GetValue(dbValue)?.ToString(); // if child entity is specified - if (childId?.ToLower() != jchildId?.ToLower()) + if (childId?.ToLower() != jchildId.ToLower()) { // id changed newChildEntity = await GetEntityByIdAsync(propertyType, jchildId, "", jPropertyName, context); @@ -607,7 +612,7 @@ Action action } } - private async Task GetEntityByIdAsync(Type entityType, string id, string displayName, string propertyPath, EntityModelBindingContext context) + private async Task GetEntityByIdAsync(Type entityType, string id, string? displayName, string propertyPath, EntityModelBindingContext context) { if (context.GetEntityById != null) return context.GetEntityById(entityType, id, displayName, propertyPath, context); @@ -619,26 +624,27 @@ private async Task GetEntityByIdAsync(Type entityType, string id, string return newChildEntity; } - private async Task GetObjectOrObjectReferenceAsync(Type objectType, JObject jobject, EntityModelBindingContext context, List formFields = null) + private async Task GetObjectOrObjectReferenceAsync(Type objectType, JObject jobject, EntityModelBindingContext context, List? formFields = null) { if (context.GetObjectOrObjectReference != null) return context.GetObjectOrObjectReference(objectType, jobject, context, formFields); - var _className = jobject.ContainsKey(nameof(IJsonEntity._className).ToCamelCase()) - ? jobject.GetValue(nameof(IJsonEntity._className).ToCamelCase()).ToString() + var classNameKey = nameof(IJsonEntity._className).ToCamelCase(); + var _className = jobject.ContainsKey(classNameKey) + ? jobject.GetValue(classNameKey)?.ToString() : null; if (_className != null) - objectType = _typeFinder.Find(t => t.FullName == _className).FirstOrDefault(); + objectType = _typeFinder.Find(t => t.FullName == _className).First(); - object newItem; // use properties binding to validate properties - newItem = Activator.CreateInstance(JsonEntityProxy.GetUnproxiedType(objectType)); + var unproxiedType = JsonEntityProxy.GetUnproxiedType(objectType); + var newItem = Activator.CreateInstance(unproxiedType) ?? throw new Exception($"Failed to create instance of type '{unproxiedType.FullName}'"); var r = await BindPropertiesAsync(jobject, newItem, context, null, formFields); return r ? newItem : null; } - private async Task ValidateAsync(object obj, string propertyName, object value, EntityModelBindingContext context) + private async Task ValidateAsync(object obj, string propertyName, object? value, EntityModelBindingContext context) { return context.SkipValidation && context.LocalSkipValidation || await _objectValidatorManager.ValidatePropertyAsync(obj, propertyName, value, context.ValidationResult); } @@ -649,7 +655,7 @@ public async Task DeleteCascadeAsync(object entity) var props = entityType.GetProperties(); var result = false; - var config = await _modelConfigurationManager.GetModelConfigurationOrNullAsync(entityType.Namespace, entityType.Name); + var config = await _modelConfigurationManager.GetModelConfigurationAsync(entityType.Namespace, entityType.Name); foreach (var prop in props) { var propConfig = config.Properties.FirstOrDefault(x => x.Name == prop.Name); @@ -674,7 +680,7 @@ private async Task DeleteUnreferencedEntityAsync(object entity, object par var entityType = entity.GetType().StripCastleProxyType(); var entityTypeName = entityType.FullName; var references = _entityPropertyRepository.GetAll().Where(x => x.EntityType == typeShortAlias || x.EntityType == entityTypeName); - if (!references.Any()) + if (!await references.AnyAsync()) return false; var parentId = parentEntity.GetId(); @@ -692,7 +698,8 @@ private async Task DeleteUnreferencedEntityAsync(object entity, object par && (x.Name == reference.EntityConfig.ClassName || x.GetTypeShortAliasOrNull() == reference.EntityConfig.ClassName)) .FirstOrDefault(); // Do not raise error becase some EntityConfig can be irrelevant - if (refType == null || !refType.IsEntityType()) continue; + if (refType == null || !refType.IsEntityType() || string.IsNullOrWhiteSpace(reference.Name)) + continue; var refParam = Expression.Parameter(refType); var query = Expression.Lambda( @@ -706,7 +713,8 @@ private async Task DeleteUnreferencedEntityAsync(object entity, object par if (idType == null) continue; var repoType = typeof(IRepository<,>).MakeGenericType(refType, idType); var repo = _iocManager.Resolve(repoType); - var where = (repoType.GetMethod("GetAll")?.Invoke(repo, null) as IQueryable).Where(query); + var getAllMethodName = nameof(IRepository, Guid>.GetAll); + var where = (repoType.GetRequiredMethod(getAllMethodName).Invoke(repo, null) as IQueryable).NotNull().Where(query); if (refType.IsAssignableFrom(parentEntity.GetType())) { @@ -742,31 +750,5 @@ private string ExtractName(string propName) var parts = propName.RemovePostfix(".").Split('.'); return parts[parts.Length - 1]; } - - /*private class FormField - { - public static List GetList(JProperty _formFields) - { - var list = new List(); - var formFieldsArray = _formFields.Value as JArray; - var formFields = formFieldsArray.Select(f => f.Value()).ToList(); - foreach (var formField in formFields) - { - var parts = formField.Split("."); - var field = new FormField() { Name = parts[0] }; - list.Add(field); - foreach (var part in parts.Skip(1)) - { - var hField = new FormField() { Name = part, Parent = field }; - field.ch - } - } - return list; - } - - public FormField Parent { get; set; } - public string Name { get; set; } - public List FormFields { get; set; } = new List(); - }*/ } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Binder/EntityModelBindingContext.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Binder/EntityModelBindingContext.cs index ffde4bf0b4..9ed602535f 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Binder/EntityModelBindingContext.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Binder/EntityModelBindingContext.cs @@ -2,9 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.DynamicEntities.Binder { @@ -28,9 +25,9 @@ public class EntityModelBindingContext /// public bool LocalSkipValidation { get; set; } = false; - public Func, object> GetObjectOrObjectReference { get; set; } + public Func?, object> GetObjectOrObjectReference { get; set; } - public Func GetEntityById { get; set; } + public Func GetEntityById { get; set; } public int? ArrayItemIndex { get; set; } = null; } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/DynamicTypeCacheHolder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/DynamicTypeCacheHolder.cs new file mode 100644 index 0000000000..67cae9b410 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/DynamicTypeCacheHolder.cs @@ -0,0 +1,14 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; +using System; + +namespace Shesha.DynamicEntities.Cache +{ + public class DynamicTypeCacheHolder : CacheHolder, IDynamicTypeCacheHolder, ISingletonDependency + { + public DynamicTypeCacheHolder(ICacheManager cacheManager) : base("DynamicTypeCache", cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/EntityConfigCache.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/EntityConfigCache.cs index 937788e819..3746e9b5f0 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/EntityConfigCache.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/EntityConfigCache.cs @@ -23,31 +23,28 @@ public class EntityConfigCache : IEntityConfigCache, ITransientDependency, IEventHandler>, IEventHandler> { - private readonly ICacheManager _cacheManager; private readonly IRepository _propertyRepository; private readonly IRepository _configReprository; private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IObjectMapper _mapper; private readonly ITypeFinder _typeFinder; - - public ITypedCache InternalPropertyCache => - _cacheManager.GetCache($"{this.GetType().Name}"); + private readonly ITypedCache _propertyCache; public EntityConfigCache( - ICacheManager cacheManager, IRepository propertyRepository, IRepository configReprository, IUnitOfWorkManager unitOfWorkManager, IObjectMapper mapper, - ITypeFinder typeFinder + ITypeFinder typeFinder, + IEntityConfigPropertyCacheHolder entityConfigPropertyCacheHolder ) { - _cacheManager = cacheManager; _propertyRepository = propertyRepository; _configReprository = configReprository; _unitOfWorkManager = unitOfWorkManager; _mapper = mapper; _typeFinder = typeFinder; + _propertyCache = entityConfigPropertyCacheHolder.Cache; } private string GetPropertiesCacheKey(Type entityType) @@ -60,7 +57,7 @@ private string GetCacheKey(EntityConfig entityConfig) return GetCacheKey(entityConfig.Namespace, entityConfig.ClassName); } - private string GetCacheKey(string @namespace, string name) + private string GetCacheKey(string? @namespace, string name) { return $"{@namespace}.{name}"; } @@ -87,34 +84,36 @@ private async Task FetchConfigAsync(Type entityType) } } - public async Task GetEntityConfigAsync(string entityType, bool raiseException = false) + public async Task GetEntityConfigAsync(string entityType, bool raiseException = false) { - var item = await InternalPropertyCache.GetAsync(entityType, async (entityType) => + var item = await _propertyCache.GetAsync(entityType, async (entityType) => { var eType = _typeFinder.Find(x => x.FullName == entityType).FirstOrDefault(); if (eType == null && raiseException) throw new EntityNotFoundException($"Entity {entityType} not found"); - return eType == null ? null : await FetchConfigAsync(eType); + return eType == null + ? null + : await FetchConfigAsync(eType); }); - return item.EntityConfig; + return item?.EntityConfig; } - public async Task GetEntityConfigAsync(Type entityType) + public async Task GetEntityConfigAsync(Type entityType) { var key = GetPropertiesCacheKey(entityType); - var item = await InternalPropertyCache.GetAsync(key, async (key) => + var item = await _propertyCache.GetAsync(key, async (key) => { var item = await FetchConfigAsync(entityType); return item; }); - return item.EntityConfig; + return item?.EntityConfig; } - public async Task> GetEntityPropertiesAsync(string entityType, bool raiseException = false) + public async Task?> GetEntityPropertiesAsync(string entityType, bool raiseException = false) { - var item = await InternalPropertyCache.GetAsync(entityType, async (entityType) => + var item = await _propertyCache.GetAsync(entityType, async (entityType) => { var eType = _typeFinder.Find(x => x.FullName == entityType).FirstOrDefault(); if (eType == null && raiseException) @@ -124,15 +123,15 @@ public async Task> GetEntityPropertiesAsync(string entit return item?.Properties; } - public async Task> GetEntityPropertiesAsync(Type entityType) + public async Task?> GetEntityPropertiesAsync(Type entityType) { var key = GetPropertiesCacheKey(entityType); - var item = await InternalPropertyCache.GetAsync(key, async (key) => + var item = await _propertyCache.GetAsync(key, async (key) => { var item = await FetchConfigAsync(entityType); return item; }); - return item.Properties; + return item?.Properties; } public void HandleEvent(EntityChangedEventData eventData) @@ -140,7 +139,7 @@ public void HandleEvent(EntityChangedEventData eventData) if (eventData.Entity?.EntityConfig == null) return; - InternalPropertyCache.Remove(GetCacheKey(eventData.Entity.EntityConfig)); + _propertyCache.Remove(GetCacheKey(eventData.Entity.EntityConfig)); } public void HandleEvent(EntityChangingEventData eventData) @@ -148,13 +147,13 @@ public void HandleEvent(EntityChangingEventData eventData) var conf = _configReprository.GetAll().FirstOrDefault(x => x.Id == eventData.Entity.Id); if (conf != null) { - InternalPropertyCache.Remove(GetCacheKey(conf)); + _propertyCache.Remove(GetCacheKey(conf)); }; } public void HandleEvent(EntityChangedEventData eventData) { - InternalPropertyCache.Remove(GetCacheKey(eventData.Entity)); + _propertyCache.Remove(GetCacheKey(eventData.Entity)); } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/EntityConfigPropertyCacheHolder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/EntityConfigPropertyCacheHolder.cs new file mode 100644 index 0000000000..31e410c31e --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/EntityConfigPropertyCacheHolder.cs @@ -0,0 +1,13 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; + +namespace Shesha.DynamicEntities.Cache +{ + public class EntityConfigPropertyCacheHolder : CacheHolder, IEntityConfigPropertyCacheHolder, ISingletonDependency + { + public EntityConfigPropertyCacheHolder(ICacheManager cacheManager) : base("EntityConfigPropertyCache", cacheManager) + { + } + } +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/FullProxyCacheHolder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/FullProxyCacheHolder.cs new file mode 100644 index 0000000000..54084ca4b5 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/FullProxyCacheHolder.cs @@ -0,0 +1,13 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; + +namespace Shesha.DynamicEntities.Cache +{ + public class FullProxyCacheHolder : CacheHolder, IFullProxyCacheHolder, ISingletonDependency + { + public FullProxyCacheHolder(ICacheManager cacheManager) : base("FullProxyCache", cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IDynamicTypeCacheHolder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IDynamicTypeCacheHolder.cs new file mode 100644 index 0000000000..bdb706874c --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IDynamicTypeCacheHolder.cs @@ -0,0 +1,9 @@ +using Shesha.Cache; +using System; + +namespace Shesha.DynamicEntities.Cache +{ + public interface IDynamicTypeCacheHolder : ICacheHolder + { + } +} diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IEntityConfigCache.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IEntityConfigCache.cs index e17bb39f21..7b9de754e6 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IEntityConfigCache.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IEntityConfigCache.cs @@ -1,8 +1,6 @@ using Shesha.DynamicEntities.Dtos; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace Shesha.DynamicEntities.Cache @@ -17,10 +15,10 @@ public interface IEntityConfigCache /// /// /// - Task> GetEntityPropertiesAsync(Type entityType); - Task> GetEntityPropertiesAsync(string entityType, bool raiseException = false); + Task?> GetEntityPropertiesAsync(Type entityType); + Task?> GetEntityPropertiesAsync(string entityType, bool raiseException = false); - Task GetEntityConfigAsync(Type entityType); - Task GetEntityConfigAsync(string entityType, bool raiseException = false); + Task GetEntityConfigAsync(Type entityType); + Task GetEntityConfigAsync(string entityType, bool raiseException = false); } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IEntityConfigPropertyCacheHolder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IEntityConfigPropertyCacheHolder.cs new file mode 100644 index 0000000000..cf985fc759 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IEntityConfigPropertyCacheHolder.cs @@ -0,0 +1,8 @@ +using Shesha.Cache; + +namespace Shesha.DynamicEntities.Cache +{ + public interface IEntityConfigPropertyCacheHolder : ICacheHolder + { + } +} diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IFullProxyCacheHolder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IFullProxyCacheHolder.cs new file mode 100644 index 0000000000..739e0fa65b --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IFullProxyCacheHolder.cs @@ -0,0 +1,8 @@ +using Shesha.Cache; + +namespace Shesha.DynamicEntities.Cache +{ + public interface IFullProxyCacheHolder : ICacheHolder + { + } +} diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IModelConfigsCacheHolder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IModelConfigsCacheHolder.cs new file mode 100644 index 0000000000..f2302bd301 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/IModelConfigsCacheHolder.cs @@ -0,0 +1,9 @@ +using Shesha.Cache; +using Shesha.DynamicEntities.Dtos; + +namespace Shesha.DynamicEntities.Cache +{ + public interface IModelConfigsCacheHolder : ICacheHolder + { + } +} diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/ModelConfigsCacheHolder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/ModelConfigsCacheHolder.cs new file mode 100644 index 0000000000..e9a9330c46 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Cache/ModelConfigsCacheHolder.cs @@ -0,0 +1,14 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; +using Shesha.DynamicEntities.Dtos; + +namespace Shesha.DynamicEntities.Cache +{ + public class ModelConfigsCacheHolder : CacheHolder, IModelConfigsCacheHolder, ISingletonDependency + { + public ModelConfigsCacheHolder(ICacheManager cacheManager) : base("ModelConfigsCache", cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/CascadeEntityCreator.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/CascadeEntityCreator.cs index 3447abb001..b8d6b676e1 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/CascadeEntityCreator.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/CascadeEntityCreator.cs @@ -1,37 +1,35 @@ using Abp.Dependency; using Abp.Domain.Entities; using Abp.Domain.Repositories; +using Shesha.Reflection; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Security.Cryptography.Xml; -using System.Text; -using System.Threading.Tasks; namespace Shesha.DynamicEntities { public interface ICascadeEntityCreator { IIocManager IocManager { get; set; } - object FindEntity(CascadeRuleEntityFinderInfo info); + object? FindEntity(CascadeRuleEntityFinderInfo info); bool VerifyEntity(CascadeRuleEntityFinderInfo info, List validationResult); object PrepareEntity(CascadeRuleEntityFinderInfo info); } public abstract class CascadeEntityCreatorBase : ICascadeEntityCreator where T : class, IEntity { - public IIocManager IocManager { get; set; } + public IIocManager IocManager { get; set; } = default!; private CascadeRuleEntityFinderInfo GetNewInfo(CascadeRuleEntityFinderInfo info) { return new CascadeRuleEntityFinderInfo((T)info._NewObject) { - _Repository = (IRepository)info._Repository ?? IocManager.Resolve>(), + _Repository = info._Repository ?? IocManager.Resolve>(), }; } - public object FindEntity(CascadeRuleEntityFinderInfo info) + public object? FindEntity(CascadeRuleEntityFinderInfo info) { return FindEntity(GetNewInfo(info)); } @@ -54,7 +52,7 @@ public bool VerifyEntity(CascadeRuleEntityFinderInfo info, ListInput data /// Found Entity. Null if not found. Throw exception if found any constraints /// Throw exception of this type if found any constraints - public virtual T FindEntity(CascadeRuleEntityFinderInfo info) + public virtual T? FindEntity(CascadeRuleEntityFinderInfo info) { return null; } @@ -91,7 +89,7 @@ public CascadeRuleEntityFinderInfo(object newObject) } public object _NewObject { get; set; } - public IRepository _Repository { get; set; } + public IRepository? _Repository { get; set; } } public class CascadeRuleEntityFinderInfo : CascadeRuleEntityFinderInfo where T : class, IEntity @@ -101,7 +99,7 @@ public CascadeRuleEntityFinderInfo(T newObject) : base(newObject) } public T NewObject => (T)_NewObject; - public IRepository Repository => (IRepository)_Repository; + public IRepository? Repository => _Repository?.ForceCastAs>(); } /// diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/CascadeUpdateRulesAttribute.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/CascadeUpdateRulesAttribute.cs index f17bc70a9b..789ca3ec9b 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/CascadeUpdateRulesAttribute.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/CascadeUpdateRulesAttribute.cs @@ -1,13 +1,4 @@ -using Abp.Dependency; -using Abp.Domain.Entities; -using Abp.Domain.Repositories; -using Shesha.EntityHistory; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; +using System; namespace Shesha.DynamicEntities { @@ -32,9 +23,9 @@ public class CascadeUpdateRulesAttribute : Attribute /// public bool DeleteUnreferenced { get; set; } - public Type CascadeEntityCreator { get; set; } + public Type? CascadeEntityCreator { get; set; } - public CascadeUpdateRulesAttribute(bool canCreate = false, bool canUpdate = false, bool deleteUnreferenced = false, Type cascadeEntityCreator = null) + public CascadeUpdateRulesAttribute(bool canCreate = false, bool canUpdate = false, bool deleteUnreferenced = false, Type? cascadeEntityCreator = null) { CanUpdate = canUpdate; CanCreate = canCreate; diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Converter/DynamicDtoConverter.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Converter/DynamicDtoConverter.cs index d8be28d1fe..8739ae6cde 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Converter/DynamicDtoConverter.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Converter/DynamicDtoConverter.cs @@ -1,5 +1,4 @@ -using Abp.Json; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Shesha.JsonLogic; using System; @@ -9,14 +8,14 @@ namespace Shesha.DynamicEntities.Converter public class DynamicDtoConverter : JsonConverter { public override bool CanRead => false; - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { - JObject json = null; + JObject? json = null; // if there is a Dto with _jObject field and _jObject is not empty - just use it instead of full serialization if (value is IHasJObjectField obj && !obj._jObject.IsNullOrEmpty()) json = obj._jObject; diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/Dto/DistributedEntityConfig.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/Dto/DistributedEntityConfig.cs index 45db18c47c..77f170f3c2 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/Dto/DistributedEntityConfig.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/Dto/DistributedEntityConfig.cs @@ -11,12 +11,12 @@ namespace Shesha.DynamicEntities.Distribution.Dto /// public class DistributedEntityConfig: DistributedConfigurableItemBase { - public string FriendlyName { get; set; } - public string TypeShortAlias { get; set; } - public string TableName { get; set; } + public string? FriendlyName { get; set; } + public string? TypeShortAlias { get; set; } + public string? TableName { get; set; } public string ClassName { get; set; } - public string Namespace { get; set; } - public string DiscriminatorValue { get; set; } + public string? Namespace { get; set; } + public string? DiscriminatorValue { get; set; } public bool GenerateAppService { get; set; } @@ -27,7 +27,7 @@ public class DistributedEntityConfig: DistributedConfigurableItemBase /// /// MD5 hash of the hardcoded properties, is used for performance optimization of the bootstrapper /// - public string PropertiesMD5 { get; set; } + public string? PropertiesMD5 { get; set; } public List ViewConfigurations { get; set; } = new List(); diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/Dto/DistributedEntityConfigProperty.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/Dto/DistributedEntityConfigProperty.cs index 511e8ccbf8..bc452c264c 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/Dto/DistributedEntityConfigProperty.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/Dto/DistributedEntityConfigProperty.cs @@ -23,37 +23,37 @@ public DistributedEntityConfigProperty() /// /// Label (display name) /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Data type /// - public string DataType { get; set; } + public string? DataType { get; set; } /// /// Data format /// - public string DataFormat { get; set; } + public string? DataFormat { get; set; } /// /// Entity type. Aplicable for entity references /// - public string EntityType { get; set; } + public string? EntityType { get; set; } /// /// Reference list name /// - public string ReferenceListName { get; set; } + public string? ReferenceListName { get; set; } /// /// Reference list module /// - public string ReferenceListModule { get; set; } + public string? ReferenceListModule { get; set; } /// /// Source of the property (code/user) @@ -73,7 +73,7 @@ public DistributedEntityConfigProperty() /// /// Items type (applicable for arrays) /// - public DistributedEntityConfigProperty ItemsType { get; set; } + public DistributedEntityConfigProperty? ItemsType { get; set; } /// /// If true, indicates that current property is a framework-related (e.g. , ) @@ -124,12 +124,12 @@ public DistributedEntityConfigProperty() /// /// Validation RegularExpression /// - public string RegExp { get; set; } + public string? RegExp { get; set; } /// /// Validation message /// - public string ValidationMessage { get; set; } + public string? ValidationMessage { get; set; } /// /// Allows to create child/nested entity diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/EntityConfigExport.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/EntityConfigExport.cs index 30a685fd1e..5f4eef6f4b 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/EntityConfigExport.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/EntityConfigExport.cs @@ -103,9 +103,6 @@ private async Task> MapPropertiesAsync(Ent private async Task MapPropertyAsync(EntityProperty src) { - if (src == null) - return null; - var property = new DistributedEntityConfigProperty(); property.Name = src.Name; property.Label = src.Label; @@ -118,7 +115,9 @@ private async Task MapPropertyAsync(EntityPrope property.Source = src.Source; property.SortOrder = src.SortOrder; - property.ItemsType = await MapPropertyAsync(src.ItemsType); + property.ItemsType = src.ItemsType != null + ? await MapPropertyAsync(src.ItemsType) + : null; property.IsFrameworkRelated = src.IsFrameworkRelated; property.Suppress = src.Suppress; property.Required = src.Required; @@ -145,7 +144,7 @@ private async Task MapPropertyAsync(EntityPrope private List MapViewConfigurations(EntityConfig entityConfig) { - return entityConfig.ViewConfigurations?.ToList(); + return entityConfig.ViewConfigurations?.ToList() ?? new(); } /// inheritedDoc diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/EntityConfigImport.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/EntityConfigImport.cs index 7881134d89..2f2ccc609a 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/EntityConfigImport.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Distribution/EntityConfigImport.cs @@ -2,25 +2,24 @@ using Abp.Domain.Repositories; using Abp.Domain.Uow; using Abp.Runtime.Caching; -using Newtonsoft.Json; using Shesha.Configuration.Runtime; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; +using Shesha.DynamicEntities.Cache; using Shesha.DynamicEntities.Distribution.Dto; using Shesha.DynamicEntities.Dtos; using Shesha.Permissions; using Shesha.Services.ConfigurationItems; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; namespace Shesha.DynamicEntities.Distribution { /// inheritedDoc - public class EntityConfigImport : ConfigurationItemImportBase, IEntityConfigImport, ITransientDependency + public class EntityConfigImport : ConfigurationItemImportBase, IEntityConfigImport, ITransientDependency { public string ItemType => EntityConfig.ItemTypeName; @@ -29,11 +28,7 @@ public class EntityConfigImport : ConfigurationItemImportBase, IEntityConfigImpo private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IPermissionedObjectManager _permissionedObjectManager; private readonly IEntityConfigManager _entityConfigManager; - private readonly ICacheManager _cacheManager; - - public ITypedCache ModelConfigsCache => - _cacheManager.GetCache($"{this.GetType().Name}_models"); - + private readonly ITypedCache _modelConfigsCache; public EntityConfigImport( IRepository moduleRepo, @@ -42,16 +37,16 @@ public EntityConfigImport( IRepository propertyConfigRepo, IPermissionedObjectManager permissionedObjectManager, IEntityConfigManager entityConfigManager, - ICacheManager cacheManager, - IUnitOfWorkManager unitOfWorkManager - ): base (moduleRepo, frontEndAppRepo) + IUnitOfWorkManager unitOfWorkManager, + IModelConfigsCacheHolder modelConfigsCacheHolder + ) : base (moduleRepo, frontEndAppRepo) { _entityConfigRepo = entityConfigRepo; _propertyConfigRepo = propertyConfigRepo; _unitOfWorkManager = unitOfWorkManager; _permissionedObjectManager = permissionedObjectManager; _entityConfigManager = entityConfigManager; - _cacheManager = cacheManager; + _modelConfigsCache = modelConfigsCacheHolder.Cache; } public async Task ImportItemAsync(DistributedConfigurableItemBase item, IConfigurationItemsImportContext context) @@ -74,7 +69,7 @@ protected async Task ImportEntityConfigAsync(DistributedE var dbItem = await _entityConfigRepo.FirstOrDefaultAsync(x => x.Namespace == item.Namespace && x.ClassName == item.ClassName //x.Name == item.Name - && (x.Module == null && item.ModuleName == null || x.Module.Name == item.ModuleName) + && (x.Module == null && item.ModuleName == null || x.Module != null && x.Module.Name == item.ModuleName) && x.IsLast); if (dbItem != null) @@ -88,48 +83,9 @@ protected async Task ImportEntityConfigAsync(DistributedE await MapPropertiesAsync(dbItem, item.Properties); - await ModelConfigsCache.RemoveAsync($"{dbItem.Namespace}|{dbItem.ClassName}"); + await _modelConfigsCache.RemoveAsync($"{dbItem.Namespace}|{dbItem.ClassName}"); return dbItem; - - /*switch (dbItem.VersionStatus) - { - case ConfigurationItemVersionStatus.Draft: - case ConfigurationItemVersionStatus.Ready: - { - // cancel existing version - await _entityConfigManager.CancelVersoinAsync(dbItem); - break; - } - } - // mark existing live form as retired if we import new form as live - if (statusToImport == ConfigurationItemVersionStatus.Live) - { - var liveForm = dbItem.VersionStatus == ConfigurationItemVersionStatus.Live - ? dbItem - : await _entityConfigRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module.Name == item.ModuleName) && f.VersionStatus == ConfigurationItemVersionStatus.Live); - if (liveForm != null) - { - await _entityConfigManager.UpdateStatusAsync(liveForm, ConfigurationItemVersionStatus.Retired); - await _unitOfWorkManager.Current.SaveChangesAsync(); // save changes to guarantee sequence of update - } - } - - // create new version - var newItemVersion = await _entityConfigManager.CreateNewVersionAsync(dbItem); - await MapEntityConfigAync(item, newItemVersion, context); - await MapPropertiesAsync(dbItem, item.Properties); - - // important: set status according to the context - newItemVersion.VersionStatus = statusToImport; - newItemVersion.CreatedByImport = context.ImportResult; - newItemVersion.Normalize(); - - await _entityConfigRepo.UpdateAsync(newItemVersion); - - await ModelConfigsCache.RemoveAsync($"{newItemVersion.Namespace}|{newItemVersion.ClassName}"); - - return newItemVersion;*/ } else { @@ -161,10 +117,6 @@ protected async Task MapEntityConfigAsync(DistributedEntityConfig dbItem.Application = await GetFrontEndAppAsync(item.FrontEndApplication, context); dbItem.ItemType = item.ItemType; - //dbItem.Origin = item.OriginId; - //dbItem.BaseItem = item.BaseItem; - //dbItem.ParentVersion = item.ParentVersionId; - dbItem.Label = item.Label; dbItem.Description = item.Description; dbItem.VersionNo = item.VersionNo; @@ -223,7 +175,7 @@ List Properties foreach (var src in Properties) { var dbItem = await _propertyConfigRepo.FirstOrDefaultAsync(x => x.Name == src.Name && x.EntityConfig == item) - ?? new EntityProperty(); + ?? new EntityProperty() { EntityConfig = item }; dbItem.EntityConfig = item; dbItem.Name = src.Name; @@ -254,14 +206,5 @@ List Properties await _propertyConfigRepo.InsertOrUpdateAsync(dbItem); } } - - public async Task ReadFromJsonAsync(Stream jsonStream) - { - using (var reader = new StreamReader(jsonStream)) - { - var json = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(json); - } - } } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/DynamicDto.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/DynamicDto.cs index 414b34ce7c..5f32440816 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/DynamicDto.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/DynamicDto.cs @@ -9,7 +9,7 @@ namespace Shesha.DynamicEntities.Dtos [JsonConverter(typeof(DynamicDtoConverter))] public class DynamicDto : EntityDto, IDynamicDto where TEntity : IEntity { - public virtual JObject _jObject { get; set; } + public virtual JObject? _jObject { get; set; } } [JsonConverter(typeof(DynamicDtoConverter))] diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/DynamicEntitiesMapProfile.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/DynamicEntitiesMapProfile.cs index 3b6cf68229..778dcb0090 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/DynamicEntitiesMapProfile.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/DynamicEntitiesMapProfile.cs @@ -70,7 +70,7 @@ public DynamicEntitiesMapProfile() private bool AllowConfigureAppService(EntityConfig entityConfig) { var attr = StaticContext.IocManager.Resolve().GetOrNull(entityConfig.FullClassName)? - .EntityType?.GetAttribute(); + .EntityType?.GetAttributeOrNull(); return attr == null || attr.GenerateApplicationService == GenerateApplicationServiceState.UseConfiguration; } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/EntityConfigDto.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/EntityConfigDto.cs index ad92dc7535..7aa300a52a 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/EntityConfigDto.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/EntityConfigDto.cs @@ -1,15 +1,11 @@ using Abp.Application.Services.Dto; -using Abp.Dependency; using Shesha.Configuration.Runtime; using Shesha.Domain.ConfigurationItems; using Shesha.Domain.Enums; using Shesha.Dto.Interfaces; using Shesha.Services; using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text; using System.Text.Json.Serialization; namespace Shesha.DynamicEntities.Dtos @@ -20,17 +16,17 @@ namespace Shesha.DynamicEntities.Dtos public class EntityConfigDto: EntityDto, IConfigurationItemDto { [StringLength(255)] - public string FriendlyName { get; set; } + public string? FriendlyName { get; set; } [StringLength(100)] - public string TypeShortAlias { get; set; } + public string? TypeShortAlias { get; set; } [StringLength(255)] - public string TableName { get; set; } + public string? TableName { get; set; } [StringLength(500)] - public string ClassName { get; set; } + public string? ClassName { get; set; } [StringLength(500)] - public string Namespace { get; set; } + public string? Namespace { get; set; } [StringLength(255)] - public string DiscriminatorValue { get; set; } + public string? DiscriminatorValue { get; set; } /// /// Source of the entity (code/user) @@ -43,9 +39,9 @@ public class EntityConfigDto: EntityDto, IConfigurationItemDto // From ConfigurationItem public bool Suppress { get; set; } - public string Module { get; set; } + public string? Module { get; set; } public string Name { get; set; } - public string Label { get; set; } + public string? Label { get; set; } private bool? _notImplemented; public bool NotImplemented => _notImplemented ??= @@ -56,7 +52,7 @@ public class EntityConfigDto: EntityDto, IConfigurationItemDto public virtual string FullClassName => $"{Namespace}.{ClassName}"; public Guid? ModuleId { get; set; } - public string Description { get; set; } + public string? Description { get; set; } public int VersionNo { get; set; } public ConfigurationItemVersionStatus VersionStatus { get; set; } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/EntityViewConfigurationDto.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/EntityViewConfigurationDto.cs index c3091b3e43..4f308c1bfb 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/EntityViewConfigurationDto.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/EntityViewConfigurationDto.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; namespace Shesha.DynamicEntities.Dtos { @@ -12,18 +8,19 @@ public class EntityViewConfigurationDto public string Type { get; set; } - public FormIdFullNameDto FormId { get; set; } + public FormIdFullNameDto? FormId { get; set; } } + // TODO: replace with FormIdentifier public class FormIdFullNameDto { - public string Name { get; set; } - public string Module { get; set; } + public string? Name { get; set; } + public string? Module { get; set; } } public static class FormIdFullNameDtoExtensions { - public static bool IsEmpty(this FormIdFullNameDto formId) + public static bool IsEmpty([NotNullWhen(false)]this FormIdFullNameDto? formId) { return formId == null || string.IsNullOrWhiteSpace(formId.Name); } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/ModelConfigurationDto.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/ModelConfigurationDto.cs index 391b192acc..8b3c8f5da2 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/ModelConfigurationDto.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/ModelConfigurationDto.cs @@ -22,17 +22,17 @@ public class ModelConfigurationDto : EntityDto, IConfigurationItemDto public bool AllowConfigureAppService { get; set; } - public List Properties { get; set; } + public List Properties { get; set; } = new List(); - public string HardcodedPropertiesMD5 { get; set; } + public string? HardcodedPropertiesMD5 { get; set; } public DateTime ChangeTime { get; set; } // ConfigurationItem public Guid? ModuleId { get; set; } - public string Module { get; set; } + public string? Module { get; set; } public string Name { get; set; } - public string Label { get; set; } - public string Description { get; set; } + public string? Label { get; set; } + public string? Description { get; set; } public int VersionNo { get; set; } public ConfigurationItemVersionStatus VersionStatus { get; set; } public bool Suppress { get; set; } @@ -46,15 +46,15 @@ public class ModelConfigurationDto : EntityDto, IConfigurationItemDto public virtual EntityConfigTypes? EntityConfigType { get; set; } // Permissions - public PermissionedObjectDto Permission { get; set; } - public PermissionedObjectDto PermissionGet { get; set; } - public PermissionedObjectDto PermissionCreate { get; set; } - public PermissionedObjectDto PermissionUpdate { get; set; } - public PermissionedObjectDto PermissionDelete { get; set; } + public PermissionedObjectDto? Permission { get; set; } + public PermissionedObjectDto? PermissionGet { get; set; } + public PermissionedObjectDto? PermissionCreate { get; set; } + public PermissionedObjectDto? PermissionUpdate { get; set; } + public PermissionedObjectDto? PermissionDelete { get; set; } public List ViewConfigurations { get; set; } = new List(); - public void NormalizeViewConfigurations(EntityConfig model = null) + public void NormalizeViewConfigurations(EntityConfig model) { var list = new List(); if (model.EntityConfigType == EntityConfigTypes.Class) diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/ModelPropertyDto.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/ModelPropertyDto.cs index 2201a081df..0c94b889b1 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/ModelPropertyDto.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Dtos/ModelPropertyDto.cs @@ -19,12 +19,12 @@ public class ModelPropertyDto : EntityDto /// /// Label (display name) /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Data type @@ -34,37 +34,37 @@ public class ModelPropertyDto : EntityDto /// /// Data format /// - public string DataFormat { get; set; } + public string? DataFormat { get; set; } /// /// Entity type. Aplicable for entity references /// - public string EntityType { get; set; } + public string? EntityType { get; set; } /// /// Module the entity belongs to. Aplicable for entity references /// - public string EntityModule { get; set; } + public string? EntityModule { get; set; } /// /// Type accessor /// - public string TypeAccessor { get; set; } + public string? TypeAccessor { get; set; } /// /// Module accessor /// - public string ModuleAccessor { get; set; } + public string? ModuleAccessor { get; set; } /// /// Reference list name /// - public string ReferenceListName { get; set; } + public string? ReferenceListName { get; set; } /// /// Reference list module /// - public string ReferenceListModule { get; set; } + public string? ReferenceListModule { get; set; } /// /// Source type (ApplicationCode = 1, UserDefined = 2) @@ -79,35 +79,35 @@ public class ModelPropertyDto : EntityDto /// /// Child properties, applicable for complex data types (e.g. object, array) /// - public List Properties { get; set; } = new List(); + public List? Properties { get; set; } = new List(); /// /// If true, indicates that current property is a framework-related (e.g. , ) /// - public bool IsFrameworkRelated { get; set; } + public bool? IsFrameworkRelated { get; set; } /// /// If true, the property is not returned from Get end-points and is ignored if submitted on Create/Update end-points /// The property should also not be listed on the form configurator property list /// - public bool Suppress { get; set; } + public bool? Suppress { get; set; } /// /// Indicates if a property value is required in order to save /// - public bool Required { get; set; } + public bool? Required { get; set; } /// /// If true, the property cannot be edited via the dynamically generated create/update end-points: /// - property should not be listed on create/update end-points /// - should be set to 'True' and not editable for read-only properties of domain classes /// - public bool ReadOnly { get; set; } + public bool? ReadOnly { get; set; } /// /// Equivalent to Audited attribute on the property /// - public bool Audited { get; set; } + public bool? Audited { get; set; } /// /// Validation min @@ -130,53 +130,53 @@ public class ModelPropertyDto : EntityDto /// /// Validation RegularExpression /// - public string RegExp { get; set; } + public string? RegExp { get; set; } /// /// Validation message /// - public string ValidationMessage { get; set; } + public string? ValidationMessage { get; set; } /// /// Allows to create child/nested entity /// - public bool CascadeCreate { get; set; } + public bool? CascadeCreate { get; set; } /// /// Allows to update child/nested entity /// - public bool CascadeUpdate { get; set; } + public bool? CascadeUpdate { get; set; } /// /// Delete child/nested entity if reference was removed and the child/nested entity doesn't have nother references /// - public bool CascadeDeleteUnreferenced { get; set; } + public bool? CascadeDeleteUnreferenced { get; set; } = false; - public bool SuppressHardcoded { get; set; } + public bool? SuppressHardcoded { get; set; } - public bool RequiredHardcoded { get; set; } + public bool? RequiredHardcoded { get; set; } - public bool ReadOnlyHardcoded { get; set; } + public bool? ReadOnlyHardcoded { get; set; } - public bool AuditedHardcoded { get; set; } + public bool? AuditedHardcoded { get; set; } - public bool SizeHardcoded { get; set; } + public bool? SizeHardcoded { get; set; } - public bool RegExpHardcoded { get; set; } + public bool? RegExpHardcoded { get; set; } /// /// Allows to create child/nested entity /// - public bool CascadeCreateHardcoded { get; set; } + public bool? CascadeCreateHardcoded { get; set; } /// /// Allows to update child/nested entity /// - public bool CascadeUpdateHardcoded { get; set; } + public bool? CascadeUpdateHardcoded { get; set; } /// /// Delete child/nested entity if reference was removed and the child/nested entity doesn't have nother references /// - public bool CascadeDeleteUnreferencedHardcoded { get; set; } + public bool? CascadeDeleteUnreferencedHardcoded { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicBinderAttribute.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicBinderAttribute.cs index 6e7e19f305..2627f4f1d7 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicBinderAttribute.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicBinderAttribute.cs @@ -1,15 +1,21 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.DynamicEntities { + /// + /// Dynamic mapping settings + /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] public class DynamicBinderAttribute: Attribute, IDynamicMappingSettings { + /// + /// If true, indicates that entity references should be mapped as DTOs (id and display name) instead of raw values (id) + /// public bool UseDtoForEntityReferences { get; set; } = false; + + /// + /// If true, indicates that need to create DynamicDtoProxy + /// public bool UseDynamicDtoProxy { get; set; } = false; } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicDtoExtensions.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicDtoExtensions.cs index 9854af4640..f810c8d616 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicDtoExtensions.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicDtoExtensions.cs @@ -1,9 +1,6 @@ using Shesha.DynamicEntities.Dtos; using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.DynamicEntities { @@ -19,7 +16,7 @@ public static bool IsDynamicDto(this Type type) return GetGenericDtoInterface(type) != null; } - private static Type GetGenericDtoInterface(Type type) + private static Type? GetGenericDtoInterface(Type type) { return type.GetInterfaces().FirstOrDefault(x => x.IsGenericType && @@ -31,12 +28,14 @@ private static Type GetGenericDtoInterface(Type type) /// /// Type of the dynamic DTO, /// - public static Type GetDynamicDtoEntityType(this Type type) + public static Type? GetDynamicDtoEntityType(this Type type) { if (!type.IsDynamicDto()) return null; var interfaceType = GetGenericDtoInterface(type); + if (interfaceType == null) + return null; var arguments = interfaceType.GetGenericArguments(); return arguments.FirstOrDefault(); diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicDtoTypeBuilder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicDtoTypeBuilder.cs index 4d6cd2ae88..20cea44377 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicDtoTypeBuilder.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicDtoTypeBuilder.cs @@ -13,6 +13,7 @@ using Shesha.DynamicEntities.Json; using Shesha.EntityReferences; using Shesha.Metadata; +using Shesha.Reflection; using System; using System.Collections.Generic; using System.Linq; @@ -27,15 +28,11 @@ public class DynamicDtoTypeBuilder : IEventHandler /// Cache of proxy classes /// - protected ITypedCache FullProxyCache => _cacheManager.GetCache("DynamicDtoTypeBuilder_FullProxyCache"); - - protected ITypedCache DynamicTypeCache => _cacheManager.GetCache($"{nameof(DynamicDtoTypeBuilder)}.{nameof(DynamicTypeCache)}"); + private readonly ITypedCache _fullProxyCache; + private readonly ITypedCache _dynamicTypeCache; /// /// Reference to the logger to write logs. @@ -45,14 +42,14 @@ public class DynamicDtoTypeBuilder : IEventHandler BuildDtoProxyTypeAsync(DynamicDtoTypeBuildingContext con public async Task> GetEntityPropertiesAsync(Type entityType) { - return await _entityConfigCache.GetEntityPropertiesAsync(entityType); + return await _entityConfigCache.GetEntityPropertiesAsync(entityType) ?? new(); } public async Task> GetDynamicPropertiesAsync(Type type, DynamicDtoTypeBuildingContext context) @@ -104,7 +101,7 @@ public async Task> GetDynamicPropertiesAsync(Type type, Dy /// /// Returns .Net type that is used to store data for the specified DTO property (according to the property settings) /// - public async Task GetDtoPropertyTypeAsync(EntityPropertyDto propertyDto, DynamicDtoTypeBuildingContext context) + public async Task GetDtoPropertyTypeAsync(EntityPropertyDto propertyDto, DynamicDtoTypeBuildingContext context) { var dataType = propertyDto.DataType; var dataFormat = propertyDto.DataFormat; @@ -157,15 +154,12 @@ public async Task GetDtoPropertyTypeAsync(EntityPropertyDto propertyDto, D case DataTypes.Array: if (propertyDto.ItemsType != null) { - var nestedType = await GetDtoPropertyTypeAsync(propertyDto.ItemsType, context); - var arrayType = typeof(List<>).MakeGenericType(nestedType); + var arrayType = typeof(List<>).MakeGenericType(nestedType.NotNull()); return arrayType; } if (propertyDto.DataFormat == ArrayFormats.ObjectReference) { - //var tt = _typeFinder.Find(x => x.FullName == propertyDto.EntityType); - //Type t = tt.Any() ? tt[0] : typeof(object); var arrayType = typeof(List<>).MakeGenericType(typeof(object)); return arrayType; } @@ -179,7 +173,7 @@ public async Task GetDtoPropertyTypeAsync(EntityPropertyDto propertyDto, D } } - private Type GetEntityReferenceType(EntityPropertyDto propertyDto, DynamicDtoTypeBuildingContext context) + private Type? GetEntityReferenceType(EntityPropertyDto propertyDto, DynamicDtoTypeBuildingContext context) { if (propertyDto.DataType != DataTypes.EntityReference) throw new NotSupportedException($"DataType {propertyDto.DataType} is not supported. Expected {DataTypes.EntityReference}"); @@ -188,17 +182,17 @@ private Type GetEntityReferenceType(EntityPropertyDto propertyDto, DynamicDtoTyp return null; var entityConfig = _entityConfigurationStore.Get(propertyDto.EntityType); - if (entityConfig == null) + if (entityConfig == null || entityConfig.IdType == null) return null; return context.UseDtoForEntityReferences ? typeof(EntityReferenceDto<>).MakeGenericType(entityConfig.IdType) - : typeof(Nullable<>).MakeGenericType(entityConfig?.IdType); + : typeof(Nullable<>).MakeGenericType(entityConfig.IdType); } private async Task GetNestedTypeAsync(EntityPropertyDto propertyDto, DynamicDtoTypeBuildingContext context) { - var t = await DynamicTypeCache.GetOrDefaultAsync(propertyDto.Id.ToString()); + var t = await _dynamicTypeCache.GetOrDefaultAsync(propertyDto.Id.ToString()); if (t == null) { t = propertyDto.Properties?.Any() ?? false @@ -206,7 +200,7 @@ private async Task GetNestedTypeAsync(EntityPropertyDto propertyDto, Dynam ? typeof(object) : await BuildNestedTypeAsync(propertyDto, context) : typeof(object); - await DynamicTypeCache.SetAsync(propertyDto.Id.ToString(), t); + await _dynamicTypeCache.SetAsync(propertyDto.Id.ToString(), t); } return t; @@ -229,9 +223,8 @@ private async Task BuildNestedTypeAsync(EntityPropertyDto propertyDto, Dyn foreach (var property in propertyDto.Properties) { - //if (propertyFilter == null || propertyFilter.Invoke(property.PropertyName)) var propertyType = await GetDtoPropertyTypeAsync(property, context); - CreateProperty(tb, property.Name, propertyType); + CreateProperty(tb, property.Name, propertyType.NotNull()); } if (!propertyDto.Properties.Any(p => p.Name == nameof(IHasClassNameField._className))) @@ -332,22 +325,6 @@ private static void CreateProperty(TypeBuilder tb, string propertyName, Type pro propertyBuilder.SetSetMethod(setPropMthdBldr); AddPropertyAttributes(propertyBuilder, propertyType); - //propertyBuilder - - // https://stackoverflow.com/questions/1822047/how-to-emit-explicit-interface-implementation-using-reflection-emit - // DefineMethodOverride is used to associate the method - // body with the interface method that is being implemented. - // - /* - if (propertyName == "Id") - { - var getMethod = typeof(IEntity).GetMethod("get_Id"); - tb.DefineMethodOverride(getPropMthdBldr, getMethod); - - var setMethod = typeof(IEntity).GetMethod("set_Id"); - tb.DefineMethodOverride(setPropMthdBldr, setMethod); - } - */ } private static void AddPropertyAttributes(PropertyBuilder propertyBuilder, Type propertyType) @@ -355,8 +332,8 @@ private static void AddPropertyAttributes(PropertyBuilder propertyBuilder, Type if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)) { var attrCtorParams = new Type[] { typeof(Type) }; - var attrCtorInfo = typeof(JsonConverterAttribute).GetConstructor(attrCtorParams); - var attrBuilder = new CustomAttributeBuilder(attrCtorInfo, new object[] { typeof(DateConverter) }); + var attrCtorInfo = typeof(JsonConverterAttribute).GetConstructor(attrCtorParams).NotNull($"Constructor not found in type '{typeof(JsonConverterAttribute).FullName}'"); + var attrBuilder = new CustomAttributeBuilder(attrCtorInfo, [typeof(DateConverter)]); propertyBuilder.SetCustomAttribute(attrBuilder); } } @@ -365,7 +342,7 @@ private string GetProxyTypeName(Type type, string suffix) { if (type.IsDynamicDto()) { - var entityType = type.GetDynamicDtoEntityType(); + var entityType = type.GetDynamicDtoEntityType().NotNull(); return $"DynamicDto_{entityType.Name}{suffix}"; } else @@ -375,7 +352,7 @@ private string GetProxyTypeName(Type type, string suffix) public async Task BuildDtoFullProxyTypeAsync(Type baseType, DynamicDtoTypeBuildingContext context) { var cacheKey = GetTypeCacheKey(baseType, context); - var cachedDtoItem = await FullProxyCache.GetOrDefaultAsync(cacheKey); + var cachedDtoItem = await _fullProxyCache.GetOrDefaultAsync(cacheKey); if (cachedDtoItem != null) { context.Classes = cachedDtoItem.NestedClasses.ToDictionary(i => i.Key, i => i.Value); @@ -397,7 +374,7 @@ public async Task BuildDtoFullProxyTypeAsync(Type baseType, DynamicDtoType var type = await CompileResultTypeAsync(baseType, proxyClassName, interfaces, properties, context); - await FullProxyCache.SetAsync(cacheKey, new DynamicDtoProxyCacheItem + await _fullProxyCache.SetAsync(cacheKey, new DynamicDtoProxyCacheItem { DtoType = type, NestedClasses = context.Classes.ToDictionary(i => i.Key, i => i.Value) @@ -447,15 +424,15 @@ public void HandleEvent(EntityChangedEventData eventData) { var cacheKey = $"{entityConfig.Namespace}.{entityConfig.ClassName}"; - FullProxyCache.Remove(cacheKey); + _fullProxyCache.Remove(cacheKey); } var prop = eventData.Entity; while (prop != null) { - DynamicTypeCache.Remove(prop.Id.ToString()); + _dynamicTypeCache.Remove(prop.Id.ToString()); prop = prop.ParentProperty; } } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicMappingSettings.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicMappingSettings.cs index 582c0fe405..a764fe3f9d 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicMappingSettings.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicMappingSettings.cs @@ -9,6 +9,10 @@ public class DynamicMappingSettings: IDynamicMappingSettings /// If true, indicates that entity references should be mapped as DTOs (id and display name) instead of raw values (id) /// public bool UseDtoForEntityReferences { get; set; } + + /// + /// If true, indicates that need to create DynamicDtoProxy + /// public bool UseDynamicDtoProxy { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicPropertyManager.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicPropertyManager.cs index 051b6cbb78..86b43b3600 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicPropertyManager.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/DynamicPropertyManager.cs @@ -8,6 +8,7 @@ using Shesha.DynamicEntities.Dtos; using Shesha.Extensions; using Shesha.Metadata; +using Shesha.Reflection; using Shesha.Services; using System; using System.Linq; @@ -41,7 +42,7 @@ IDynamicRepository dynamicRepository _dynamicRepository = dynamicRepository; } - public async Task GetValueAsync(IEntity entity, EntityPropertyDto property) + public async Task GetValueAsync(IEntity entity, EntityPropertyDto property) where TId : notnull { var config = entity.GetType().GetEntityConfiguration(); @@ -53,14 +54,16 @@ public async Task GetValueAsync(IEntity entity, EntityProperty } // todo: get IsVersioned flag from the EntityPropertyDto - public async Task SetValueAsync(IEntity entity, EntityPropertyDto property, string value, bool createNewVersion) + public async Task SetValueAsync(IEntity entity, EntityPropertyDto property, string value, bool createNewVersion) where TId: notnull { var config = entity.GetType().GetEntityConfiguration(); - var prop = _entityPropertyValueRepository.GetAll() + var prop = await _entityPropertyValueRepository + .GetAll() .Where(x => x.EntityProperty.Id == property.Id && x.OwnerId == entity.Id.ToString() && x.OwnerType == config.TypeShortAlias) - .OrderByDescending(x => x.CreationTime).FirstOrDefault(); + .OrderByDescending(x => x.CreationTime) + .FirstOrDefaultAsync(); if (prop?.Value == value) return; @@ -83,6 +86,7 @@ public async Task SetValueAsync(IEntity entity, EntityPropertyDto prop public async Task MapDtoToEntityAsync(TDynamicDto dynamicDto, TEntity entity) where TEntity : class, IEntity where TDynamicDto : class, IDynamicDto + where TId : notnull { await MapPropertiesAsync(entity, dynamicDto, async (ent, dto, entProp, dtoProp) => { @@ -95,6 +99,7 @@ await MapPropertiesAsync(entity, dynamicDto, async (ent, dto, entProp, dtoProp) public async Task MapEntityToDtoAsync(TEntity entity, TDynamicDto dynamicDto) where TEntity : class, IEntity where TDynamicDto : class, IDynamicDto + where TId : notnull { await MapPropertiesAsync(entity, dynamicDto, async (ent, dto, entProp, dtoProp) => { @@ -107,7 +112,7 @@ await MapPropertiesAsync(entity, dynamicDto, async (ent, dto, entProp, dtoProp) } public async Task MapPropertiesAsync(IEntity entity, TDynamicDto dto, - Func, TDynamicDto, EntityPropertyDto, PropertyInfo, Task> action) + Func, TDynamicDto, EntityPropertyDto, PropertyInfo, Task> action) where TDynamicDto: notnull { var dynamicProperties = (await DtoTypeBuilder.GetEntityPropertiesAsync(entity.GetType())) .Where(p => p.Source == MetadataSourceType.UserDefined).ToList(); @@ -124,6 +129,7 @@ public async Task MapPropertiesAsync(IEntity entity, TDyn public async Task MapJObjectToEntityAsync(JObject jObject, TEntity entity) where TEntity : class, IEntity + where TId : notnull { var dynamicProperties = (await DtoTypeBuilder.GetEntityPropertiesAsync(entity.GetType())) .Where(p => p.Source == MetadataSourceType.UserDefined).ToList(); @@ -140,20 +146,21 @@ public async Task MapJObjectToEntityAsync(JObject jObject, TEntity } } - public async Task GetPropertyAsync(object entity, string propertyName) + public async Task GetPropertyAsync(object entity, string propertyName) { try { if (entity == null) return null; - var getterMethod = this.GetType().GetMethod(nameof(GetEntityPropertyAsync)); + var getterMethod = this.GetType().GetRequiredMethod(nameof(GetEntityPropertyAsync)); var entityType = entity.GetType(); var idType = entityType.GetEntityIdType(); var genericGetterMethod = getterMethod.MakeGenericMethod(entityType, idType); - return await (genericGetterMethod.Invoke(this, new object[] { entity, propertyName }) as Task); + var result = genericGetterMethod.Invoke(this, [entity, propertyName]).ForceCastAs>(); + return await result; } catch (Exception) { @@ -161,8 +168,9 @@ public async Task GetPropertyAsync(object entity, string propertyName) } } - public async Task GetEntityPropertyAsync(TEntity entity, string propertyName) + public async Task GetEntityPropertyAsync(TEntity entity, string propertyName) where TEntity : class, IEntity + where TId : notnull { var dynamicProperty = (await DtoTypeBuilder.GetEntityPropertiesAsync(entity.GetType())) .FirstOrDefault(p => p.Source == MetadataSourceType.UserDefined && p.Name == propertyName); @@ -174,17 +182,18 @@ public async Task GetEntityPropertyAsync(TEntity entity, s if (serializedValue == null) return null; - Type simpleType = null; + Type? simpleType = null; switch (dynamicProperty.DataType) { case DataTypes.EntityReference: { var entityConfig = _entityConfigurationStore.Get(dynamicProperty.EntityType); - var id = SerializationManager.DeserializeProperty(entityConfig.IdType, serializedValue); - if (id == null) + var id = SerializationManager.DeserializeProperty(entityConfig.IdType.NotNull(), serializedValue); + var stringId = id?.ToString(); + if (string.IsNullOrWhiteSpace(stringId)) return null; - return await _dynamicRepository.GetAsync(entityConfig.EntityType, id.ToString()); + return await _dynamicRepository.GetAsync(entityConfig.EntityType, stringId); } case DataTypes.Boolean: simpleType = typeof(bool?); diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/EntityConfigsBootstrapper.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/EntityConfigsBootstrapper.cs index a1df7bd284..df51456a46 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/EntityConfigsBootstrapper.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/EntityConfigsBootstrapper.cs @@ -11,7 +11,6 @@ using Shesha.Domain.ConfigurationItems; using Shesha.Domain.Enums; using Shesha.Extensions; -using Shesha.JsonEntities; using Shesha.Metadata; using Shesha.Metadata.Dtos; using Shesha.Reflection; @@ -68,7 +67,7 @@ public async Task ProcessAsync() var assemblies = _assembleFinder.GetAllAssemblies() .Distinct(new AssemblyFullNameComparer()) .Where(a => !a.IsDynamic - && a.GetTypes().Any(t => MappingHelper.IsEntity(t) || MappingHelper.IsJsonEntity(t) && t != typeof(JsonEntity)) + && a.GetTypes().Any(t => MappingHelper.IsEntity(t) || MappingHelper.IsJsonEntity(t)) // && t != typeof(JsonEntity)) need to add JsonEntity for binding purposes ) .ToList(); @@ -92,41 +91,13 @@ public async Task ProcessAsync() Logger.Warn($"Bootstrap assembly {assembly.FullName} - finished"); } Logger.Warn("Bootstrap entity configs finished successfully"); - // update inheritance - /*foreach (var assembly in assemblies) - { - await ProcessInheritanceAsync(assembly); - }*/ - } - - private async Task ProcessInheritanceAsync(Assembly assembly) - { - var entityTypes = assembly.GetTypes().Where(t => MappingHelper.IsEntity(t)).ToList(); - - foreach (var entityType in entityTypes) - { - var name = entityType.Name; - var parent = entityType; - EntityConfig dbParent = null; - while (parent != typeof(object) && dbParent == null) - { - parent = parent.BaseType; - dbParent = await _entityConfigRepository.GetAll().Where(x => x.Namespace == parent.Namespace && x.ClassName == parent.Name).FirstOrDefaultAsync(); - } - if (dbParent != null) - { - var dbEntity = await _entityConfigRepository.GetAll().Where(x => x.Namespace == entityType.Namespace && x.ClassName == entityType.Name).FirstOrDefaultAsync(); - dbEntity.Parent = dbParent; - await _entityConfigRepository.UpdateAsync(dbEntity); - } - } } private async Task ProcessAssemblyAsync(Assembly assembly) { var module = await _moduleManager.GetOrCreateModuleAsync(assembly); - var entityTypes = assembly.GetTypes().Where(t => MappingHelper.IsEntity(t) || MappingHelper.IsJsonEntity(t) && t != typeof(JsonEntity)) + var entityTypes = assembly.GetTypes().Where(t => MappingHelper.IsEntity(t) || MappingHelper.IsJsonEntity(t)) .ToList(); Logger.Info($"{assembly.FullName}: found {entityTypes.Count()} entity types"); @@ -157,8 +128,8 @@ private async Task ProcessAssemblyAsync(Assembly assembly) { db = ec, code = entitiesConfigs.FirstOrDefault(c => c.Config.EntityType.Name.Equals(ec.ClassName, StringComparison.InvariantCultureIgnoreCase) && - c.Config.EntityType.Namespace.Equals(ec.Namespace, StringComparison.InvariantCultureIgnoreCase) - ) + string.Equals(c.Config.EntityType.Namespace, ec.Namespace, StringComparison.InvariantCultureIgnoreCase) + ) }) .Select( ec => @@ -166,7 +137,7 @@ private async Task ProcessAssemblyAsync(Assembly assembly) { db = ec.db, code = ec.code, - attr = ec.code?.Config.EntityType.GetAttribute() + attr = ec.code?.Config.EntityType.GetAttributeOrNull() } ).ToList(); @@ -196,7 +167,7 @@ private async Task ProcessAssemblyAsync(Assembly assembly) foreach (var config in toUpdate) { - config.db.FriendlyName = config.code.Config.FriendlyName; + config.db.FriendlyName = config.code.NotNull().Config.FriendlyName; config.db.Accessor = config.code.Config.Accessor; config.db.TableName = config.code.Config.TableName; config.db.TypeShortAlias = config.code.Config.SafeTypeShortAlias; @@ -223,7 +194,8 @@ private async Task ProcessAssemblyAsync(Assembly assembly) // Add news configs var toAdd = entitiesConfigs.Where(c => !dbEntities.Any(ec => ec.ClassName.Equals(c.Config.EntityType.Name, StringComparison.InvariantCultureIgnoreCase) && - ec.Namespace.Equals(c.Config.EntityType.Namespace, StringComparison.InvariantCultureIgnoreCase))) + string.Equals(ec.Namespace, c.Config.EntityType.Namespace, StringComparison.InvariantCultureIgnoreCase) + )) .ToList(); Logger.Info(toUpdate.Any() ? $"{assembly.FullName}: found {toUpdate.Count()} entities to add" @@ -231,7 +203,7 @@ private async Task ProcessAssemblyAsync(Assembly assembly) foreach (var config in toAdd) { - var attr = config.Config.EntityType.GetAttribute(); + var attr = config.Config.EntityType.GetAttributeOrNull(); var ec = new EntityConfig() { FriendlyName = config.Config.FriendlyName, @@ -248,7 +220,7 @@ private async Task ProcessAssemblyAsync(Assembly assembly) ? EntityConfigTypes.Interface : EntityConfigTypes.Class, - Source = Domain.Enums.MetadataSourceType.ApplicationCode + Source = MetadataSourceType.ApplicationCode }; // ToDo: AS - Get Module, Description and Suppress @@ -271,7 +243,7 @@ private async Task ProcessAssemblyAsync(Assembly assembly) } private async Task UpdatePropertiesAsync( - EntityConfig entityConfig, List codeProperties, List dbProperties, EntityProperty parentProp = null) + EntityConfig entityConfig, List codeProperties, List dbProperties, EntityProperty? parentProp = null) { try { @@ -289,7 +261,7 @@ private async Task UpdatePropertiesAsync( dbp = new EntityProperty { EntityConfig = entityConfig, - Source = Domain.Enums.MetadataSourceType.ApplicationCode, + Source = MetadataSourceType.ApplicationCode, SortOrder = nextSortOrder++, ParentProperty = parentProp, Label = cp.Label, @@ -303,7 +275,7 @@ private async Task UpdatePropertiesAsync( { MapProperty(cp, dbp); // update hardcoded part - dbp.Source = Domain.Enums.MetadataSourceType.ApplicationCode; + dbp.Source = MetadataSourceType.ApplicationCode; // restore property dbp.IsDeleted = false; @@ -328,7 +300,7 @@ private async Task UpdatePropertiesAsync( // todo: inactivate missing properties var deletedProperties = dbProperties .Where(p => - p.Source == Domain.Enums.MetadataSourceType.ApplicationCode + p.Source == MetadataSourceType.ApplicationCode && !codeProperties.Any(cp => cp.Path.Equals(p.Name, StringComparison.InvariantCultureIgnoreCase)) && p.ParentProperty?.Id == parentProp?.Id ) @@ -370,21 +342,9 @@ private async Task UpdatePropertiesAsync(EntityConfig entityConfig, Type entityT private async Task UpdateItemsTypeAsync(EntityProperty dbp, PropertyMetadataDto cp) { - var shouldHaveItemsType = dbp.DataType == DataTypes.Array && cp.ItemsType != null; - - if (!shouldHaveItemsType) + if (dbp.DataType == DataTypes.Array && cp.ItemsType != null) { - // delete item type if exists - if (dbp.ItemsType != null) - { - await _entityPropertyRepository.DeleteAsync(dbp.ItemsType); - dbp.ItemsType = null; - await _entityPropertyRepository.UpdateAsync(dbp); - } - } - else - { - var itemsTypeProp = dbp.ItemsType ?? (dbp.ItemsType = new EntityProperty()); + var itemsTypeProp = dbp.ItemsType ?? (dbp.ItemsType = new EntityProperty() { EntityConfig = dbp.EntityConfig }); itemsTypeProp.EntityConfig = dbp.EntityConfig; // keep label and description??? @@ -393,7 +353,7 @@ private async Task UpdateItemsTypeAsync(EntityProperty dbp, PropertyMetadataDto MapProperty(cp.ItemsType, itemsTypeProp); - itemsTypeProp.Source = Domain.Enums.MetadataSourceType.ApplicationCode; + itemsTypeProp.Source = MetadataSourceType.ApplicationCode; itemsTypeProp.SortOrder = 0; itemsTypeProp.ParentProperty = dbp; @@ -406,6 +366,16 @@ private async Task UpdateItemsTypeAsync(EntityProperty dbp, PropertyMetadataDto await _entityPropertyRepository.UpdateAsync(dbp); } + else + { + // delete item type if exists + if (dbp.ItemsType != null) + { + await _entityPropertyRepository.DeleteAsync(dbp.ItemsType); + dbp.ItemsType = null; + await _entityPropertyRepository.UpdateAsync(dbp); + } + } } private void MapProperty(PropertyMetadataDto src, EntityProperty dst) diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/EntityToDtoModelExtesions.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/EntityToDtoModelExtesions.cs index 7edc3a2612..a89a86e5fd 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/EntityToDtoModelExtesions.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/EntityToDtoModelExtesions.cs @@ -5,6 +5,7 @@ using Shesha.DynamicEntities.Dtos; using Shesha.Extensions; using Shesha.JsonEntities.Proxy; +using Shesha.Reflection; using System; using System.Collections.Generic; using System.Linq; @@ -14,38 +15,20 @@ namespace Shesha.DynamicEntities { public static class EntityToDtoModelExtesions { - public static Task MapToCustomDynamicDtoAsync(TEntity entity, IDynamicMappingSettings settings = null) + public static Task MapToCustomDynamicDtoAsync(TEntity entity, IDynamicMappingSettings? settings = null) where TEntity : class, IEntity where TDynamicDto : class, IDynamicDto { var modelType = typeof(TDynamicDto); - TDynamicDto dto = null; + var dto = ActivatorHelper.CreateNotNullInstance(); if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(DynamicDto<,>)) { - // AS - disabled because DynamicDtoConverter allows to improve performance - - // build dto type - //var context = new DynamicDtoTypeBuildingContext() - //{ - // ModelType = modelType, - // UseDtoForEntityReferences = settings?.UseDtoForEntityReferences ?? false - //}; - - //var dtoBuilder = StaticContext.IocManager.Resolve(); - //var dtoType = await dtoBuilder.BuildDtoFullProxyTypeAsync(context.ModelType, context); - - //dto = (TDynamicDto)Activator.CreateInstance(dtoType); - //MapProps(entity, dto); - - // AS - Should only be used with a DynamicDtoConverter - dto = (TDynamicDto)Activator.CreateInstance(modelType); dto._jObject = ObjectToJsonExtension.GetJObjectFromObject(entity); } else { - dto = (TDynamicDto)Activator.CreateInstance(modelType); MapProps(entity, dto); } @@ -59,13 +42,6 @@ public static void MapProps(object src, object dest) foreach (var destProp in destProps) { - //if (destProp.Name == nameof(IHasMetaField._meta)) - //{ - // var meta = src.GetType().IsJsonEntityType() ? ((IJsonEntity)src)._meta : new MetaDto() { ClassName = src.GetType().FullName }; - // destProp.SetValue(dest, meta, null); - // continue; - //} - var srcProp = srcProps.FirstOrDefault(x => x.Name == destProp.Name); if (srcProp != null) { @@ -78,7 +54,7 @@ public static void MapProps(object src, object dest) if (srcProp.PropertyType.IsClass && !srcProp.PropertyType.IsEntityType() && !srcProp.PropertyType.IsSystemType()) { // Nested object - var innerDest = Activator.CreateInstance(JsonEntityProxy.GetUnproxiedType(srcValue.GetType())); + var innerDest = ActivatorHelper.CreateNotNullObject(JsonEntityProxy.GetUnproxiedType(srcValue.GetType())); MapProps(srcValue, innerDest); destProp.SetValue(dest, innerDest, null); continue; @@ -94,10 +70,10 @@ public static void MapProps(object src, object dest) var id = srcValue.GetId(); if (destProp.PropertyType.GetGenericTypeDefinition() == typeof(EntityReferenceDto<>)) { - var idType = typeof(EntityReferenceDto<>).MakeGenericType(srcValue.GetType().GetProperty("Id").PropertyType); + var idType = typeof(EntityReferenceDto<>).MakeGenericType(srcValue.GetType().GetRequiredProperty("Id").PropertyType); var idWithName = Activator.CreateInstance(idType); - idType.GetProperty(nameof(EntityReferenceDto.Id)).SetValue(idWithName, id); - idType.GetProperty(nameof(EntityReferenceDto._displayName)).SetValue(idWithName, srcValue.GetEntityDisplayName()); + idType.GetRequiredProperty(nameof(EntityReferenceDto.Id)).SetValue(idWithName, id); + idType.GetRequiredProperty(nameof(EntityReferenceDto._displayName)).SetValue(idWithName, srcValue.GetEntityDisplayName()); destProp.SetValue(dest, idWithName); } @@ -126,7 +102,7 @@ public static void MapProps(object src, object dest) // Property if (destProp.PropertyType == typeof(Int64?)) // workaround to convert int to Nullable: Object of type 'System.Int32' cannot be converted to type 'System.Nullable`1[System.Int64]' - destProp.SetValue(dest, Convert.ChangeType(srcValue, Nullable.GetUnderlyingType(typeof(long?)))); + destProp.SetValue(dest, Convert.ChangeType(srcValue, typeof(long))); else destProp.SetValue(dest, srcValue); } @@ -138,4 +114,4 @@ public static void MapProps(object src, object dest) } } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Exceptions/ModelConfigurationNotFoundException.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Exceptions/ModelConfigurationNotFoundException.cs new file mode 100644 index 0000000000..63fa0a7a88 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Exceptions/ModelConfigurationNotFoundException.cs @@ -0,0 +1,16 @@ +using System; + +namespace Shesha.DynamicEntities.Exceptions +{ + public class ModelConfigurationNotFoundException : Exception + { + public string? Namespace { get; private set; } + public string Name { get; private set; } + + public ModelConfigurationNotFoundException(string? @namespace, string name): base($"Model configuration '{@namespace}.{name}' not found") + { + Namespace = @namespace; + Name = name; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/IDynamicDtoTypeBuilder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/IDynamicDtoTypeBuilder.cs index f2ec37886a..7bf37e1f42 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/IDynamicDtoTypeBuilder.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/IDynamicDtoTypeBuilder.cs @@ -40,6 +40,6 @@ public interface IDynamicDtoTypeBuilder /// /// Returns .Net type that is used to store data for the specified DTO property (according to the property settings) /// - Task GetDtoPropertyTypeAsync(EntityPropertyDto propertyDto, DynamicDtoTypeBuildingContext context); + Task GetDtoPropertyTypeAsync(EntityPropertyDto propertyDto, DynamicDtoTypeBuildingContext context); } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/IDynamicPropertyManager.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/IDynamicPropertyManager.cs index 6c82b47fc6..b55bcde689 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/IDynamicPropertyManager.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/IDynamicPropertyManager.cs @@ -18,7 +18,7 @@ public interface IDynamicPropertyManager /// Entity /// Property to get value /// - Task GetValueAsync(IEntity entity, EntityPropertyDto property); + Task GetValueAsync(IEntity entity, EntityPropertyDto property) where TId : notnull; // todo: get IsVersioned flag from the EntityPropertyDto /// @@ -30,7 +30,7 @@ public interface IDynamicPropertyManager /// Value /// True if need to create a new version of value /// - Task SetValueAsync(IEntity entity, EntityPropertyDto property, string value, bool createNewVersion); + Task SetValueAsync(IEntity entity, EntityPropertyDto property, string value, bool createNewVersion) where TId : notnull; /// /// Map values of dynamic properties from Dto to Entity @@ -43,7 +43,8 @@ public interface IDynamicPropertyManager /// Task MapDtoToEntityAsync(TDynamicDto dynamicDto, TEntity entity) where TEntity : class, IEntity - where TDynamicDto : class, IDynamicDto; + where TDynamicDto : class, IDynamicDto + where TId : notnull; /// /// Map values of dynamic properties from JObject to Entity @@ -54,7 +55,8 @@ Task MapDtoToEntityAsync(TDynamicDto dynamicDto, TEnt /// Entity /// Task MapJObjectToEntityAsync(JObject jObject, TEntity entity) - where TEntity : class, IEntity; + where TEntity : class, IEntity + where TId : notnull; /// /// Map values of dynamic properties from Entity to Dto @@ -67,7 +69,8 @@ Task MapJObjectToEntityAsync(JObject jObject, TEntity entity) /// Task MapEntityToDtoAsync(TEntity entity, TDynamicDto dynamicDto) where TEntity : class, IEntity - where TDynamicDto : class, IDynamicDto; + where TDynamicDto : class, IDynamicDto + where TId : notnull; /// /// @@ -77,8 +80,9 @@ Task MapEntityToDtoAsync(TEntity entity, TDynamicDto /// /// /// - Task GetEntityPropertyAsync(TEntity entity, string propertyName) - where TEntity : class, IEntity; + Task GetEntityPropertyAsync(TEntity entity, string propertyName) + where TEntity : class, IEntity + where TId : notnull; /// /// Get entity property value @@ -86,6 +90,6 @@ Task GetEntityPropertyAsync(TEntity entity, string propert /// /// /// - Task GetPropertyAsync(object entity, string propertyName); + Task GetPropertyAsync(object entity, string propertyName); } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/IEntityModelBinder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/IEntityModelBinder.cs index b5fd8fe0b5..69b6298e01 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/IEntityModelBinder.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/IEntityModelBinder.cs @@ -1,15 +1,19 @@ using Newtonsoft.Json.Linq; using Shesha.DynamicEntities.Binder; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; namespace Shesha.DynamicEntities { public interface IEntityModelBinder { - Task BindPropertiesAsync(JObject jobject, object entity, EntityModelBindingContext context, - string propertyName = null, List formFields = null); + Task BindPropertiesAsync( + JObject jobject, + object entity, + EntityModelBindingContext context, + string? propertyName = null, + List? formFields = null + ); /// /// Deletes nested entities from properties that configured as CascadeDeleteUnreferenced (Attributte or PropertyConfig) diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/IHasClassNameFiled.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/IHasClassNameFiled.cs index d0177bfd0c..80f9ac14d5 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/IHasClassNameFiled.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/IHasClassNameFiled.cs @@ -2,6 +2,6 @@ { public interface IHasClassNameField { - string _className { get; } + string? _className { get; } } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/IHasDisplayName.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/IHasDisplayName.cs index 32718d1de0..04e35f3aca 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/IHasDisplayName.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/IHasDisplayName.cs @@ -1,14 +1,7 @@ -using Shesha.DynamicEntities.Dtos; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.DynamicEntities +namespace Shesha.DynamicEntities { public interface IHasDisplayNameField { - string _displayName { get; } + string? _displayName { get; } } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/IHasJObjectField.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/IHasJObjectField.cs index 758b9c1d06..63f4c27671 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/IHasJObjectField.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/IHasJObjectField.cs @@ -1,11 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.DynamicEntities { @@ -19,10 +14,6 @@ public interface IHasJObjectField /// [Display(Name = "JSON data", Description = "Is used for ")] [JsonIgnore] - JObject _jObject { get; set; } - - //bool _changed { get; set; } - - //void Update(); + JObject? _jObject { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/IModelConfigurationManager.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/IModelConfigurationManager.cs index 5f3b38581a..d126e771f7 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/IModelConfigurationManager.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/IModelConfigurationManager.cs @@ -1,9 +1,7 @@ using Shesha.Domain; using Shesha.DynamicEntities.Dtos; using Shesha.Metadata.Dtos; -using System; using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace Shesha.DynamicEntities @@ -26,12 +24,17 @@ public interface IModelConfigurationManager /// /// Get model configuration /// - Task GetModelConfigurationAsync(EntityConfig modelConfig, List hardCodedProps = null); + Task GetModelConfigurationAsync(EntityConfig modelConfig, List? hardCodedProps = null); /// /// Get model configuration /// - Task GetModelConfigurationOrNullAsync(string @namespace, string name, List hardCodedProps = null); + Task GetModelConfigurationOrNullAsync(string? @namespace, string name, List? hardCodedProps = null); + + /// + /// Get model configuration + /// + Task GetModelConfigurationAsync(string? @namespace, string name, List? hardCodedProps = null); /// /// Create model configuration @@ -46,7 +49,5 @@ public interface IModelConfigurationManager /// Model configuration Dto /// Task UpdateAsync(ModelConfigurationDto input); - - } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/ISerializationManager.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/ISerializationManager.cs index d84e46172e..104d8f6cd4 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/ISerializationManager.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/ISerializationManager.cs @@ -1,9 +1,5 @@ using Shesha.DynamicEntities.Dtos; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.DynamicEntities { @@ -18,7 +14,7 @@ public interface ISerializationManager /// /// /// - string SerializeProperty(EntityPropertyDto propertyDto, object value); + string SerializeProperty(EntityPropertyDto propertyDto, object? value); /// /// Deserialize property value @@ -26,6 +22,6 @@ public interface ISerializationManager /// /// /// - object DeserializeProperty(Type propertyType, string value); + object? DeserializeProperty(Type propertyType, string value); } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Json/DateConverter.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Json/DateConverter.cs index 5d40e17a9d..4d09fa67ff 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Json/DateConverter.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Json/DateConverter.cs @@ -1,11 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.DynamicEntities.Json { @@ -16,7 +12,7 @@ public class DateConverter: IsoDateTimeConverter { private const string DefaultDateFormat = "yyyy'-'MM'-'dd"; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { string text; @@ -44,7 +40,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s } else { - throw new JsonSerializationException($"Unexpected value when converting date. Expected DateTime or DateTimeOffset, got {value.GetType().FullName}."); + throw new JsonSerializationException($"Unexpected value when converting date. Expected DateTime or DateTimeOffset, got {value?.GetType().FullName}."); } writer.WriteValue(text); diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/Cache/IMapperCacheHolder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/Cache/IMapperCacheHolder.cs new file mode 100644 index 0000000000..e441916db6 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/Cache/IMapperCacheHolder.cs @@ -0,0 +1,11 @@ +using Shesha.Cache; +using System.Collections.Generic; +using static Shesha.DynamicEntities.Mapper.DynamicDtoMappingHelper; + +namespace Shesha.DynamicEntities.Mapper.Cache +{ + + public interface IMapperCacheHolder : ICacheHolder> + { + } +} diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/Cache/MapperCacheHolder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/Cache/MapperCacheHolder.cs new file mode 100644 index 0000000000..03c17981bf --- /dev/null +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/Cache/MapperCacheHolder.cs @@ -0,0 +1,15 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; +using System.Collections.Generic; +using static Shesha.DynamicEntities.Mapper.DynamicDtoMappingHelper; + +namespace Shesha.DynamicEntities.Mapper.Cache +{ + public class MapperCacheHolder : CacheHolder>, IMapperCacheHolder, ISingletonDependency + { + public MapperCacheHolder(ICacheManager cacheManager) : base("DynamicDtoMappersCache", cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/DynamicDtoMappingHelper.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/DynamicDtoMappingHelper.cs index e1ce7316e4..6e240384f2 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/DynamicDtoMappingHelper.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/DynamicDtoMappingHelper.cs @@ -1,12 +1,11 @@ using Abp.Dependency; -using Abp.Domain.Repositories; -using Abp.Domain.Uow; using Abp.Events.Bus.Entities; using Abp.Events.Bus.Handlers; using Abp.Runtime.Caching; using AutoMapper; using Shesha.AutoMapper; using Shesha.Domain; +using Shesha.DynamicEntities.Mapper.Cache; using Shesha.Extensions; using System; using System.Collections.Generic; @@ -17,29 +16,15 @@ namespace Shesha.DynamicEntities.Mapper { public class DynamicDtoMappingHelper : IEventHandler>, IDynamicDtoMappingHelper, ITransientDependency { - private readonly ICacheManager _cacheManager; - private readonly IRepository _propertyRepository; - private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IocManager _iocManager; - - public ITypedCache> InternalCache - { - get - { - return _cacheManager.GetCache>(this.GetType().Name); - } - } + private readonly ITypedCache> _internalCache; public DynamicDtoMappingHelper( IocManager iocManager, - ICacheManager cacheManager, - IRepository propertyRepository, - IUnitOfWorkManager unitOfWorkManager) + IMapperCacheHolder mapperCacheHolder) { _iocManager = iocManager; - _cacheManager = cacheManager; - _propertyRepository = propertyRepository; - _unitOfWorkManager = unitOfWorkManager; + _internalCache = mapperCacheHolder.Cache; } private string GetCacheKey(Type sourceType, Type destinationType) @@ -53,7 +38,7 @@ private string GetCacheKey(Type sourceType, Type destinationType) throw new NotSupportedException("This method supports only mapping from/to entity type"); } - private string GetCacheKey(string @namespace, string name) + private string GetCacheKey(string? @namespace, string name) { return $"{@namespace}.{name}"; } @@ -69,7 +54,7 @@ public void HandleEvent(EntityChangedEventData eventData) return; var cacheKey = GetCacheKey(eventData.Entity.EntityConfig); - InternalCache.Remove(cacheKey); + _internalCache.Remove(cacheKey); } private IMapper GetMapper(Type srcType, Type dstType) @@ -116,9 +101,10 @@ private async Task GetMapperAsync(Type entityType, Type dtoType, Mappin return cacheItem; }); - IMapper mapper = null; + IMapper? mapper = null; - var mappers = await InternalCache.GetAsync(cacheKey, () => { + // TODO: review cache usage and test + var mappers = await _internalCache.GetAsync(cacheKey, () => { var cacheItem = itemFactory(); mapper = cacheItem.Mapper; @@ -137,7 +123,7 @@ private async Task GetMapperAsync(Type entityType, Type dtoType, Mappin // cache exists but it doesn't contain mapper for specified DTO - create it and update cache cacheItem = itemFactory(); mappers.Add(itemFactory()); - await InternalCache.SetAsync(cacheKey, mappers); + await _internalCache.SetAsync(cacheKey, mappers); return cacheItem.Mapper; } @@ -162,4 +148,4 @@ public MapperItem(Type dtoType, MappingDirection direction, IMapper mapper) } } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityReferenceDtoToEntityConverter.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityReferenceDtoToEntityConverter.cs index cf36443768..3a93421d4d 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityReferenceDtoToEntityConverter.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityReferenceDtoToEntityConverter.cs @@ -4,16 +4,15 @@ using Shesha.AutoMapper.Dto; using Shesha.Services; using System; -using System.Linq; namespace Shesha.DynamicEntities.Mapper { /// /// EntityReferenceDto to Entity converter /// - public class EntityReferenceDtoToEntityConverter : ITypeConverter, TEntity> where TEntity: class, IEntity + public class EntityReferenceDtoToEntityConverter : ITypeConverter?, TEntity?> where TEntity: class, IEntity { - public TEntity Convert(EntityReferenceDto source, TEntity destination, ResolutionContext context) + public TEntity? Convert(EntityReferenceDto? source, TEntity? destination, ResolutionContext context) { if (source == null || (source.Id is Guid guid) && guid == Guid.Empty) return null; diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityToEntityReferenceDtoConverter.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityToEntityReferenceDtoConverter.cs index 4e7024c887..9a4fe5de6b 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityToEntityReferenceDtoConverter.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityToEntityReferenceDtoConverter.cs @@ -1,19 +1,16 @@ using Abp.Domain.Entities; -using Abp.Domain.Repositories; using AutoMapper; using Shesha.AutoMapper.Dto; using Shesha.Extensions; -using Shesha.Services; -using System; namespace Shesha.DynamicEntities.Mapper { /// /// Entity to EntityToEntityReferenceDto converter /// - public class EntityToEntityReferenceDtoConverter : ITypeConverter> where TEntity : class, IEntity + public class EntityToEntityReferenceDtoConverter : ITypeConverter?> where TEntity : class, IEntity { - public EntityReferenceDto Convert(TEntity source, EntityReferenceDto destination, ResolutionContext context) + public EntityReferenceDto? Convert(TEntity? source, EntityReferenceDto? destination, ResolutionContext context) { return source == null ? null diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityToIdConverter.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityToIdConverter.cs index 42265fc520..f85ad65efe 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityToIdConverter.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/EntityToIdConverter.cs @@ -1,17 +1,14 @@ using Abp.Domain.Entities; -using Abp.Domain.Repositories; using AutoMapper; -using Shesha.Services; -using System; namespace Shesha.DynamicEntities.Mapper { /// /// Id to Entity converter /// - public class EntityToIdConverter : ITypeConverter where TEntity: class, IEntity + public class EntityToIdConverter : ITypeConverter where TEntity: class, IEntity { - public TId Convert(TEntity source, TId destination, ResolutionContext context) + public TId? Convert(TEntity? source, TId? destination, ResolutionContext context) { return source == null ? default(TId) diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/IdToEntityConverter.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/IdToEntityConverter.cs index 141c2c985a..0db0786746 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/IdToEntityConverter.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/IdToEntityConverter.cs @@ -9,9 +9,9 @@ namespace Shesha.DynamicEntities.Mapper /// /// Id to Entity converter /// - public class IdToEntityConverter : ITypeConverter where TEntity: class, IEntity + public class IdToEntityConverter : ITypeConverter where TEntity: class, IEntity { - public TEntity Convert(TId source, TEntity destination, ResolutionContext context) + public TEntity? Convert(TId? source, TEntity? destination, ResolutionContext context) { if (source == null || (source is Guid guid) && guid == Guid.Empty) return null; diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/NumericToEnumTypeConverter.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/NumericToEnumTypeConverter.cs index ef6f44d140..6630d49003 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/NumericToEnumTypeConverter.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/NumericToEnumTypeConverter.cs @@ -13,7 +13,7 @@ public TDestination Convert(TSource source, TDestination destination, Resolution var enumUnderlayingType = typeof(TDestination).GetEnumUnderlyingType(); var numericValue = System.Convert.ChangeType(source, enumUnderlayingType); - return (TDestination)numericValue; + return numericValue != null ? (TDestination)numericValue : throw new Exception($"Failed to convert value '{numericValue}' of type '{typeof(TSource).FullName}' to type '{typeof(TDestination).FullName}'"); } } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/ReferenceListMapProfile.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/ReferenceListMapProfile.cs index 57a79f6529..169aca2039 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/ReferenceListMapProfile.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Mapper/ReferenceListMapProfile.cs @@ -30,11 +30,6 @@ static ReferenceListMapProfile() Converters.Add(new ConverterDefinition(reflistType, numericType, typeof(EnumToNumericTypeConverter<,>).MakeGenericType(reflistType, numericType))); Converters.Add(new ConverterDefinition(numericType, reflistType, typeof(NumericToEnumTypeConverter<,>).MakeGenericType(numericType, reflistType))); - - /* - var listType = typeof(List); - Converters.Add(new ConverterDefinition(listType, reflistType, typeof(NumericToEnumTypeConverter<,>).MakeGenericType(numericType, reflistType))); - */ } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Middleware/RequestToGqlMiddleware.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Middleware/RequestToGqlMiddleware.cs index e7756b89e7..196a13c9de 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Middleware/RequestToGqlMiddleware.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Middleware/RequestToGqlMiddleware.cs @@ -1,11 +1,5 @@ using Abp.Dependency; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -15,21 +9,22 @@ public class RequestToGqlMiddleware : IMiddleware, ISingletonDependency { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { + var path = context.Request.Path.Value; if (context.Request.Query.ContainsKey("properties") - && context.Request.Path.Value != null) + && path != null) { - if (context.Request.Path.Value.ToLower().Contains("crud/get")) + if (path.ToLower().Contains("crud/get")) context.Request.Path = - new PathString(Regex.Replace(context.Request.Path.Value, "/Get", "/Query", RegexOptions.IgnoreCase)); + new PathString(Regex.Replace(path, "/Get", "/Query", RegexOptions.IgnoreCase)); - if (context.Request.Path.Value.ToLower().EndsWith("crud/create")) + if (path.ToLower().EndsWith("crud/create")) context.Request.Path = - new PathString(Regex.Replace(context.Request.Path.Value, "/Create", "/Creategql", RegexOptions.IgnoreCase)); + new PathString(Regex.Replace(path, "/Create", "/Creategql", RegexOptions.IgnoreCase)); - if (context.Request.Path.Value.ToLower().EndsWith("crud/update") - && !context.Request.Path.Value.ToLower().Contains("crud/updategql")) + if (path.ToLower().EndsWith("crud/update") + && !path.ToLower().Contains("crud/updategql")) context.Request.Path = - new PathString(Regex.Replace(context.Request.Path.Value, "/Update", "/Updategql", RegexOptions.IgnoreCase)); + new PathString(Regex.Replace(path, "/Update", "/Updategql", RegexOptions.IgnoreCase)); } await next(context); diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/ModelConfigurationManager.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/ModelConfigurationManager.cs index 28f71cb62e..8faf0f9c36 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/ModelConfigurationManager.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/ModelConfigurationManager.cs @@ -1,5 +1,4 @@ using Abp.Dependency; -using Abp.Domain.Entities; using Abp.Domain.Repositories; using Abp.Domain.Services; using Abp.Reflection; @@ -9,7 +8,9 @@ using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Domain.Enums; +using Shesha.DynamicEntities.Cache; using Shesha.DynamicEntities.Dtos; +using Shesha.DynamicEntities.Exceptions; using Shesha.EntityReferences; using Shesha.Extensions; using Shesha.Metadata; @@ -34,11 +35,8 @@ public class ModelConfigurationManager : DomainService, IModelConfigurationManag private readonly ITypeFinder _typeFinder; private readonly IHardcodeMetadataProvider _metadataProvider; private readonly IMappingMetadataProvider _mappingMetadataProvider; - private readonly ICacheManager _cacheManager; private readonly IRepository _moduleRepository; - - public ITypedCache ModelConfigsCache => - _cacheManager.GetCache($"{this.GetType().Name}_models"); + private readonly ITypedCache _modelConfigsCache; public ModelConfigurationManager( IRepository entityConfigRepository, @@ -48,8 +46,8 @@ public ModelConfigurationManager( IHardcodeMetadataProvider metadataProvider, IRepository configurationItemRepository, IMappingMetadataProvider mappingMetadataProvider, - ICacheManager cacheManager, - IRepository moduleRepository + IRepository moduleRepository, + IModelConfigsCacheHolder modelConfigsCacheHolder ) { _entityConfigRepository = entityConfigRepository; @@ -59,8 +57,8 @@ public ModelConfigurationManager( _metadataProvider = metadataProvider; _configurationItemRepository = configurationItemRepository; _mappingMetadataProvider = mappingMetadataProvider; - _cacheManager = cacheManager; _moduleRepository = moduleRepository; + _modelConfigsCache = modelConfigsCacheHolder.Cache; } public async Task MergeConfigurationsAsync(EntityConfig source, EntityConfig destination, bool deleteAfterMerge, bool deepUpdate) @@ -88,7 +86,7 @@ public async Task MergeConfigurationsAsync(EntityConfig source, EntityConfig des await _configurationItemRepository.DeleteAsync(source.Id); } - await ModelConfigsCache.RemoveAsync($"{destination.Namespace}|{destination.ClassName}"); + await _modelConfigsCache.RemoveAsync($"{destination.Namespace}|{destination.ClassName}"); } private void CopyViewConfigs(EntityConfig source, EntityConfig destination) @@ -101,7 +99,7 @@ private void CopyViewConfigs(EntityConfig source, EntityConfig destination) foreach (var configuration in source.ViewConfigurations) { - var vconfig = destination.ViewConfigurations?.FirstOrDefault(x => x.Type == configuration.Type); + var vconfig = destination.ViewConfigurations.FirstOrDefault(x => x.Type == configuration.Type); if (vconfig == null) { destination.ViewConfigurations.Add( @@ -128,61 +126,60 @@ private void CopyViewConfigs(EntityConfig source, EntityConfig destination) } } + private async Task CopyPropertiesAsync(EntityConfig destination, List? destPs, List sourcePs, EntityProperty? parent) + { + foreach (var prop in sourcePs) + { + var destProp = destPs?.FirstOrDefault(x => x.Name == prop.Name); + if (destProp == null && prop.Source == MetadataSourceType.UserDefined) + { + destProp = new EntityProperty() + { + Name = prop.Name, + EntityConfig = destination, + DataType = prop.DataType, + DataFormat = prop.DataFormat, + EntityType = prop.EntityType, + IsFrameworkRelated = prop.IsFrameworkRelated, + ItemsType = prop.ItemsType, + ReferenceListName = prop.ReferenceListName, + ReferenceListModule = prop.ReferenceListModule, + Source = destination.Source == MetadataSourceType.ApplicationCode ? prop.Source : MetadataSourceType.UserDefined, + Suppress = prop.Suppress, + ParentProperty = parent + }; + } + + if (destProp != null) + { + destProp.Audited = prop.Audited; + destProp.Description = prop.Description; + destProp.Label = prop.Label; + destProp.Max = prop.Max; + destProp.Min = prop.Min; + destProp.Required = prop.Required; + destProp.MaxLength = prop.MaxLength; + destProp.MinLength = prop.MinLength; + destProp.ReadOnly = prop.ReadOnly; + destProp.RegExp = prop.RegExp; + destProp.CascadeCreate = destProp.CascadeCreate || prop.CascadeCreate; + destProp.CascadeUpdate = destProp.CascadeUpdate || prop.CascadeUpdate; + destProp.CascadeDeleteUnreferenced = destProp.CascadeDeleteUnreferenced || prop.CascadeDeleteUnreferenced; + + await _entityPropertyRepository.InsertOrUpdateAsync(destProp); + + if (prop.Properties?.Any() ?? false) + await CopyPropertiesAsync(destination, destProp.Properties?.ToList(), prop.Properties.ToList(), destProp); + } + } + } + private async Task CopyPropertiesAsync(EntityConfig source, EntityConfig destination) { var destProps = await _entityPropertyRepository.GetAll().Where(x => x.EntityConfig.Id == destination.Id).ToListAsync(); var sourceProps = await _entityPropertyRepository.GetAll().Where(x => x.EntityConfig.Id == source.Id).ToListAsync(); - Func, List, EntityProperty, Task> copyProps = null; - copyProps = async (List destPs, List sourcePs, EntityProperty parent) => - { - foreach (var prop in sourcePs) - { - var destProp = destPs?.FirstOrDefault(x => x.Name == prop.Name); - if (destProp == null && prop.Source == MetadataSourceType.UserDefined) - { - destProp = new EntityProperty() - { - Name = prop.Name, - EntityConfig = destination, - DataType = prop.DataType, - DataFormat = prop.DataFormat, - EntityType = prop.EntityType, - IsFrameworkRelated = prop.IsFrameworkRelated, - ItemsType = prop.ItemsType, - ReferenceListName = prop.ReferenceListName, - ReferenceListModule = prop.ReferenceListModule, - Source = destination.Source == MetadataSourceType.ApplicationCode ? prop.Source : MetadataSourceType.UserDefined, - Suppress = prop.Suppress, - ParentProperty = parent - }; - } - - if (destProp != null) - { - destProp.Audited = prop.Audited; - destProp.Description = prop.Description; - destProp.Label = prop.Label; - destProp.Max = prop.Max; - destProp.Min = prop.Min; - destProp.Required = prop.Required; - destProp.MaxLength = prop.MaxLength; - destProp.MinLength = prop.MinLength; - destProp.ReadOnly = prop.ReadOnly; - destProp.RegExp = prop.RegExp; - destProp.CascadeCreate = destProp.CascadeCreate || prop.CascadeCreate; - destProp.CascadeUpdate = destProp.CascadeUpdate || prop.CascadeUpdate; - destProp.CascadeDeleteUnreferenced = destProp.CascadeDeleteUnreferenced || prop.CascadeDeleteUnreferenced; - - await _entityPropertyRepository.InsertOrUpdateAsync(destProp); - - if (prop.Properties?.Any() ?? false) - await copyProps(destProp.Properties?.ToList(), prop.Properties.ToList(), destProp); - } - } - }; - - await copyProps(destProps, sourceProps, null); + await CopyPropertiesAsync(destination, destProps, sourceProps, null); } private async Task CopyPermissionsAsync(EntityConfig source, EntityConfig destination) @@ -193,11 +190,16 @@ private async Task CopyPermissionsAsync(EntityConfig source, EntityConfig destin var destinationPermission = await _permissionedObjectManager.GetOrDefaultAsync($"{destination.FullClassName}{method}", type); destinationPermission.Access = sourcePermission.Access; destinationPermission.Type = type; - sourcePermission.Permissions.ToList().ForEach(x => - { - if (!destinationPermission.Permissions.Contains(x)) - destinationPermission.Permissions.Add(x); - }); + if (sourcePermission.Permissions != null) + { + destinationPermission.Permissions ??= new(); + + sourcePermission.Permissions.ToList().ForEach(x => + { + if (!destinationPermission.Permissions.Contains(x)) + destinationPermission.Permissions.Add(x); + }); + } await _permissionedObjectManager.SetAsync(destinationPermission); }; @@ -258,7 +260,7 @@ public async Task UpdateAsync(ModelConfigurationDto input { var modelConfig = await _entityConfigRepository.GetAll().Where(m => m.Id == input.Id).FirstOrDefaultAsync(); if (modelConfig == null) - new EntityNotFoundException("Model configuration not found"); + throw new ModelConfigurationNotFoundException(input.Namespace, input.Name); if (modelConfig.Source == MetadataSourceType.UserDefined) { @@ -269,7 +271,7 @@ public async Task UpdateAsync(ModelConfigurationDto input // todo: add validation var res = await CreateOrUpdateAsync(modelConfig, input, false); - await ModelConfigsCache.RemoveAsync($"{res.Namespace}|{res.ClassName}"); + await _modelConfigsCache.RemoveAsync($"{res.Namespace}|{res.ClassName}"); return res; } @@ -352,22 +354,51 @@ private async Task CreateOrUpdateAsync(EntityConfig model var dto = await GetModelConfigurationAsync(modelConfig); // update permissions from the input because data is not saved to DB yet - dto.Permission = input.Permission; - dto.Permission.ActualAccess = input.Permission.Access; - dto.Permission.ActualPermissions = input.Permission.Permissions; - dto.PermissionGet = input.PermissionGet; - dto.PermissionGet.ActualAccess = input.PermissionGet.Access; - dto.PermissionGet.ActualPermissions = input.PermissionGet.Permissions; - dto.PermissionUpdate = input.PermissionUpdate; - dto.PermissionUpdate.ActualAccess = input.PermissionUpdate.Access; - dto.PermissionUpdate.ActualPermissions = input.PermissionUpdate.Permissions; - dto.PermissionDelete = input.PermissionDelete; - dto.PermissionDelete.ActualAccess = input.PermissionDelete.Access; - dto.PermissionDelete.ActualPermissions = input.PermissionDelete.Permissions; - dto.PermissionCreate = input.PermissionCreate; - dto.PermissionCreate.ActualAccess = input.PermissionCreate.Access; - dto.PermissionCreate.ActualPermissions = input.PermissionCreate.Permissions; - + if (input.Permission != null) + { + dto.Permission = input.Permission; + dto.Permission.ActualAccess = input.Permission.Access; + dto.Permission.ActualPermissions = input.Permission.Permissions; + } + else + dto.Permission = null; + + if (input.PermissionGet != null) + { + dto.PermissionGet = input.PermissionGet; + dto.PermissionGet.ActualAccess = input.PermissionGet.Access; + dto.PermissionGet.ActualPermissions = input.PermissionGet.Permissions; + } + else + dto.PermissionGet = null; + + if (input.PermissionUpdate != null) + { + dto.PermissionUpdate = input.PermissionUpdate; + dto.PermissionUpdate.ActualAccess = input.PermissionUpdate.Access; + dto.PermissionUpdate.ActualPermissions = input.PermissionUpdate.Permissions; + } + else + dto.PermissionUpdate = null; + + if (input.PermissionDelete != null) + { + dto.PermissionDelete = input.PermissionDelete; + dto.PermissionDelete.ActualAccess = input.PermissionDelete.Access; + dto.PermissionDelete.ActualPermissions = input.PermissionDelete.Permissions; + } + else + dto.PermissionDelete = null; + + if (input.PermissionCreate != null) + { + dto.PermissionCreate = input.PermissionCreate; + dto.PermissionCreate.ActualAccess = input.PermissionCreate.Access; + dto.PermissionCreate.ActualPermissions = input.PermissionCreate.Permissions; + } + else + dto.PermissionCreate = null; + return dto; } @@ -403,7 +434,7 @@ private void ActionPropertiesRecursive(List properties, Action } } - private async Task BindPropertiesAsync(Dictionary mappers, List allProperties, List inputProperties, EntityConfig modelConfig, EntityProperty parentProperty) + private async Task BindPropertiesAsync(Dictionary mappers, List allProperties, List inputProperties, EntityConfig modelConfig, EntityProperty? parentProperty) { if (inputProperties == null) return; var sortOrder = 0; @@ -459,7 +490,7 @@ private IMapper GetPropertyMapper(MetadataSourceType sourceType) return propertyMapperConfig.CreateMapper(); } - public async Task GetModelConfigurationAsync(EntityConfig modelConfig, List hardCodedProps = null) + public async Task GetModelConfigurationAsync(EntityConfig modelConfig, List? hardCodedProps = null) { var dto = ObjectMapper.Map(modelConfig); @@ -484,10 +515,10 @@ public async Task GetModelConfigurationAsync(EntityConfig var hardCodedProp = hardCodedProps.FirstOrDefault(pp => pp.Path == prop.Name); if (hardCodedProp != null) { - prop.Suppress = !hardCodedProp.IsVisible || prop.Suppress; - prop.Required = hardCodedProp.Required || prop.Required; - prop.ReadOnly = hardCodedProp.Readonly || prop.ReadOnly; - prop.Audited = hardCodedProp.Audited || prop.Audited; + prop.Suppress = !hardCodedProp.IsVisible || (prop.Suppress ?? false); + prop.Required = hardCodedProp.Required || (prop.Required ?? false); + prop.ReadOnly = hardCodedProp.Readonly || (prop.ReadOnly ?? false); + prop.Audited = hardCodedProp.Audited || (prop.Audited ?? false); prop.MinLength = hardCodedProp.MinLength ?? prop.MinLength; prop.MaxLength = hardCodedProp.MaxLength ?? prop.MaxLength; prop.Min = hardCodedProp.Min ?? prop.Min; @@ -535,18 +566,29 @@ public async Task GetModelConfigurationAsync(EntityConfig return dto; } - public async Task GetModelConfigurationOrNullAsync(string @namespace, string name, List hardCodedProps = null) + public async Task GetModelConfigurationOrNullAsync(string? @namespace, string name, List? hardCodedProps = null) { var cacheKey = $"{@namespace}|{name}"; - var result = await ModelConfigsCache.GetAsync(cacheKey, async () => { - var modelConfig = await _entityConfigRepository.GetAll().Where(m => m.ClassName == name && m.Namespace == @namespace && !m.IsDeleted).FirstOrDefaultAsync(); - if (modelConfig == null) - return null; - - return await GetModelConfigurationAsync(modelConfig, hardCodedProps); + var result = await _modelConfigsCache.GetAsync(cacheKey, async () => { + using (var uow = UnitOfWorkManager.Begin(System.Transactions.TransactionScopeOption.RequiresNew)) + { + var modelConfig = await _entityConfigRepository.GetAll().Where(m => m.ClassName == name && m.Namespace == @namespace && !m.IsDeleted).FirstOrDefaultAsync(); + if (modelConfig == null) + return null; + + var result = await GetModelConfigurationAsync(modelConfig, hardCodedProps); + await uow.CompleteAsync(); + return result; + } }); return result; } + + public async Task GetModelConfigurationAsync(string? @namespace, string name, List? hardCodedProps = null) + { + var result = await GetModelConfigurationOrNullAsync(@namespace, name, hardCodedProps); + return result ?? throw new ModelConfigurationNotFoundException(@namespace, name); + } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/ObjectToJsonExtension.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/ObjectToJsonExtension.cs index e0f310d91e..45a0c5801f 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/ObjectToJsonExtension.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/ObjectToJsonExtension.cs @@ -1,6 +1,5 @@ using Abp.Json; using AutoMapper.Internal; -using Castle.Core.Internal; using NetTopologySuite.Geometries; using NetTopologySuite.IO; using Newtonsoft.Json; @@ -31,12 +30,12 @@ public static bool IsServiceDtoField(PropertyInfo p) || name == nameof(IJsonEntityProxy._references).ToCamelCase(); } - public static void ObjectToJObject(object obj, JObject jobj, bool addMissedProperties = true) + public static void ObjectToJObject(object? obj, JObject jobj, bool addMissedProperties = true) { GetJObjectFromObject(obj, jobj, addMissedProperties); } - public static JObject GetJObjectFromObject(object obj, JObject jobj = null, bool addMissedProperties = true) + public static JObject GetJObjectFromObject(object? obj, JObject? jobj = null, bool addMissedProperties = true) { jobj ??= new JObject(); @@ -44,7 +43,7 @@ public static JObject GetJObjectFromObject(object obj, JObject jobj = null, bool var props = obj.GetType().GetProperties().Where(p => p.CanRead && p.IsPublic() - && !p.HasAttribute() + && !p.HasAttribute() && !p.HasAttribute() && !IsServiceDtoField(p)) .ToList(); @@ -57,7 +56,7 @@ public static JObject GetJObjectFromObject(object obj, JObject jobj = null, bool if (!addMissedProperties) continue; else jobj.Add(prop.Name.ToCamelCase(), null); - var jprop = jobj.Property(prop.Name.ToCamelCase()); + var jprop = jobj.Property(prop.Name.ToCamelCase()).NotNull(); var val = prop.GetValue(obj); @@ -85,12 +84,13 @@ public static JObject GetJObjectFromObject(object obj, JObject jobj = null, bool return jobj; } - public static JToken ValueToJson(Type propType, object val, JToken jval, bool addMissedProperties = true) + public static JToken ValueToJson(Type propType, object? val, JToken? jval, bool addMissedProperties = true) { - if (val == null || !val.GetType().IsAssignableTo(propType)) return null; + if (val == null || !val.GetType().IsAssignableTo(propType)) + return JValue.CreateNull(); - if (Extensions.ObjectExtensions.IsListType(propType) - || Extensions.ObjectExtensions.IsDictionaryType(propType)) + if (ObjectExtensions.IsListType(propType) + || ObjectExtensions.IsDictionaryType(propType)) { jval = jval.IsNullOrEmpty() ? new JArray() : jval; if (val is IEnumerable list && list.Any() && jval is JArray jlist) @@ -122,23 +122,21 @@ public static JToken ValueToJson(Type propType, object val, JToken jval, bool ad if (jval.IsNullOrEmpty()) jval = newjval; return jval; - } - + } + if (propType.IsEntityType()) { var jref = new JObject(); - jref.Add(nameof(EntityReferenceDto._displayName).ToCamelCase(), JProperty.FromObject(val.GetEntityDisplayName())); - jref.Add(nameof(EntityReferenceDto._className).ToCamelCase(), JProperty.FromObject(propType.FullName)); - jref.Add(nameof(EntityReferenceDto.Id).ToCamelCase(), JProperty.FromObject(val.GetId())); + jref.Add(nameof(EntityReferenceDto._displayName).ToCamelCase(), JProperty.FromObject(val.GetEntityDisplayName() ?? string.Empty)); + jref.Add(nameof(EntityReferenceDto._className).ToCamelCase(), JProperty.FromObject(propType.GetRequiredFullName())); + jref.Add(nameof(EntityReferenceDto.Id).ToCamelCase(), JProperty.FromObject(val.GetId().NotNull())); return jref; - } - - if (val != null && jval.IsNullOrEmpty() - || val == null && !jval.IsNullOrEmpty() - || !val.Equals(jval?.ToObject(propType))) + } + + if (jval.IsNullOrEmpty() || !val.Equals(jval.ToObject(propType))) return JProperty.FromObject(val); - return jval; + return jval ?? JValue.CreateNull(); } private static JToken ConvertGeometry(Geometry geometry) diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/SerializationManager.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/SerializationManager.cs index 35e1ce0b67..a519692b8e 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/SerializationManager.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/SerializationManager.cs @@ -2,13 +2,9 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Shesha.DynamicEntities.Dtos; -using Shesha.DynamicEntities.Json; using Shesha.Metadata; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.DynamicEntities { @@ -16,13 +12,13 @@ namespace Shesha.DynamicEntities public class SerializationManager : ISerializationManager, ITransientDependency { /// inheritedDoc - public object DeserializeProperty(Type propertyType, string value) + public object? DeserializeProperty(Type propertyType, string value) { return JsonConvert.DeserializeObject(value, propertyType); } /// inheritedDoc - public string SerializeProperty(EntityPropertyDto propertyDto, object value) + public string SerializeProperty(EntityPropertyDto propertyDto, object? value) { switch (propertyDto.DataType) { diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/ShaEntityTypeBuilder.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/ShaEntityTypeBuilder.cs index 5d4f861e15..b5b287bd13 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/ShaEntityTypeBuilder.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/ShaEntityTypeBuilder.cs @@ -1,4 +1,5 @@ using Abp.Domain.Entities; +using Shesha.Extensions; using System; using System.Reflection; using System.Reflection.Emit; @@ -10,7 +11,7 @@ public static class ShaEntityTypeBuilder public static object CreateNewObject(DynamicEntity entityMetadata) { var dynamicType = CompileResultType(entityMetadata); - var dynamicObject = Activator.CreateInstance(dynamicType); + var dynamicObject = ActivatorHelper.CreateNotNullObject(dynamicType); return dynamicObject; } public static Type CompileResultType(DynamicEntity entityMetadata) @@ -26,16 +27,6 @@ public static Type CompileResultType(DynamicEntity entityMetadata) foreach (var property in entityMetadata.Properties) CreateProperty(tb, property.PropertyName, property.PropertyType); - /* - var isTransientMethodBuilder = tb.DefineMethod(nameof(IEntity.IsTransient), - MethodAttributes.Public | MethodAttributes.Virtual, - null, - null); - var isTransientMethodIL = isTransientMethodBuilder.GetILGenerator(); - isTransientMethodIL.Emit(OpCodes.Ret); - var isTransientMethod = typeof(IEntity).GetMethod(nameof(IEntity.IsTransient)); - tb.DefineMethodOverride(isTransientMethodBuilder, isTransientMethod); - */ var objectType = tb.CreateType(); return objectType; } @@ -99,21 +90,6 @@ private static void CreateProperty(TypeBuilder tb, string propertyName, Type pro propertyBuilder.SetGetMethod(getPropMthdBldr); propertyBuilder.SetSetMethod(setPropMthdBldr); - - // https://stackoverflow.com/questions/1822047/how-to-emit-explicit-interface-implementation-using-reflection-emit - // DefineMethodOverride is used to associate the method - // body with the interface method that is being implemented. - // - /* - if (propertyName == "Id") - { - var getMethod = typeof(IEntity).GetMethod("get_Id"); - tb.DefineMethodOverride(getPropMthdBldr, getMethod); - - var setMethod = typeof(IEntity).GetMethod("set_Id"); - tb.DefineMethodOverride(setPropMthdBldr, setMethod); - } - */ } } } diff --git a/shesha-core/src/Shesha.Framework/DynamicEntities/Swagger/DynamicDtoSchemaFilter.cs b/shesha-core/src/Shesha.Framework/DynamicEntities/Swagger/DynamicDtoSchemaFilter.cs index c021615fdb..01b22c945f 100644 --- a/shesha-core/src/Shesha.Framework/DynamicEntities/Swagger/DynamicDtoSchemaFilter.cs +++ b/shesha-core/src/Shesha.Framework/DynamicEntities/Swagger/DynamicDtoSchemaFilter.cs @@ -1,7 +1,5 @@ using Abp.Domain.Entities.Auditing; using Microsoft.OpenApi.Models; -using Nito.AsyncEx.Synchronous; -using Shesha.Configuration.Runtime; using Shesha.Domain.Attributes; using Shesha.DynamicEntities.Cache; using Shesha.DynamicEntities.Dtos; @@ -37,7 +35,9 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) if (!context.Type.IsDynamicDto() || isProxy) return; - var modelType = isGeneric ? context.Type : context.Type.FindBaseGenericType(typeof(DynamicDto<,>)); + var modelType = isGeneric + ? context.Type + : context.Type.FindBaseGenericType(typeof(DynamicDto<,>)).NotNull(); var isCreateDto = modelType.GetGenericTypeDefinition().IsAssignableTo(typeof(CreateDynamicDto<,>)); var isUpdateDto = modelType.GetGenericTypeDefinition().IsAssignableTo(typeof(UpdateDynamicDto<,>)); @@ -47,7 +47,7 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) var entityReferenceProperties = srcProperties?.Where(x => x.PropertyType.IsEntityType()).Select(x => x.Name).ToList(); var propertiesConfigs = srcType != null - ? _entityConfigCache.GetEntityPropertiesAsync(srcType).WaitAndUnwrapException() + ? AsyncHelper.RunSync(async () => await _entityConfigCache.GetEntityPropertiesAsync(srcType)) : null; var dtoBuilder = StaticContext.IocManager.Resolve(); @@ -75,15 +75,15 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) && // skip special properties (propertyName != nameof(FullAuditedEntity.Id) && propertyName.IsSpecialProperty() || // skip system Readonly attributes - srcProperty?.GetAttribute(true) != null + srcProperty?.GetAttributeOrNull(true) != null || // skip EntityConfig Reaadonly (propertiesConfigs?.FirstOrDefault(x => x.Name == propertyName)?.ReadOnly ?? false) || !(srcProperty?.CanWrite ?? true) ) || // skip Shesha Readonly attributes - isCreateDto && !(srcProperty?.GetAttribute(true)?.Insert ?? true) + isCreateDto && !(srcProperty?.GetAttributeOrNull(true)?.Insert ?? true) || // skip Shesha Readonly attributes - isUpdateDto && !(srcProperty?.GetAttribute(true)?.Update ?? true) + isUpdateDto && !(srcProperty?.GetAttributeOrNull(true)?.Update ?? true) ) continue; @@ -115,7 +115,7 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) } // add `_formFields` with comment - var formFieldsProp = typeof(IHasFormFieldsList).GetProperty(nameof(IHasFormFieldsList._formFields)); + var formFieldsProp = typeof(IHasFormFieldsList).GetRequiredProperty(nameof(IHasFormFieldsList._formFields)); if (!propNames.Contains(formFieldsProp.Name.ToLower())) { var formFieldsSchema = context.SchemaGenerator.GenerateSchema(formFieldsProp.PropertyType, context.SchemaRepository, memberInfo: formFieldsProp); diff --git a/shesha-core/src/Shesha.Framework/EntityHistory/AuditedBooleanAttribute.cs b/shesha-core/src/Shesha.Framework/EntityHistory/AuditedBooleanAttribute.cs index 304c0c5400..fdff5fbb13 100644 --- a/shesha-core/src/Shesha.Framework/EntityHistory/AuditedBooleanAttribute.cs +++ b/shesha-core/src/Shesha.Framework/EntityHistory/AuditedBooleanAttribute.cs @@ -5,7 +5,7 @@ namespace Shesha.EntityHistory [AttributeUsage(AttributeTargets.Property)] public class AuditedBooleanAttribute : Attribute { - public string EventText { get; set; } + public string? EventText { get; set; } public string TrueText { get; set; } public string FalseText { get; set; } diff --git a/shesha-core/src/Shesha.Framework/EntityHistory/DisplayChildAuditTrailAttribute.cs b/shesha-core/src/Shesha.Framework/EntityHistory/DisplayChildAuditTrailAttribute.cs index b29b108356..a0b9292333 100644 --- a/shesha-core/src/Shesha.Framework/EntityHistory/DisplayChildAuditTrailAttribute.cs +++ b/shesha-core/src/Shesha.Framework/EntityHistory/DisplayChildAuditTrailAttribute.cs @@ -5,7 +5,7 @@ namespace Shesha.EntityHistory [AttributeUsage(AttributeTargets.Property)] public class DisplayChildAuditTrailAttribute : Attribute { - public string[] AuditedFields { get; set; } + public string[]? AuditedFields { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/EntityHistory/DisplayGenericEntitesAuditTrailAttribute.cs b/shesha-core/src/Shesha.Framework/EntityHistory/DisplayGenericEntitesAuditTrailAttribute.cs index 19171e7157..d5e9daacfe 100644 --- a/shesha-core/src/Shesha.Framework/EntityHistory/DisplayGenericEntitesAuditTrailAttribute.cs +++ b/shesha-core/src/Shesha.Framework/EntityHistory/DisplayGenericEntitesAuditTrailAttribute.cs @@ -1,5 +1,4 @@ using System; -using Shesha.Domain; namespace Shesha.EntityHistory { @@ -8,17 +7,13 @@ public class DisplayGenericEntitesAuditTrailAttribute : Attribute { public Type EntityType { get; set; } - public string OwnerIdField { get; set; } + public string? OwnerIdField { get; set; } - public string OwnerTypeField { get; set; } + public string? OwnerTypeField { get; set; } - public string DisplayName { get; set; } + public string? DisplayName { get; set; } - public string NameField { get; set; } - - public string CategoryField { get; set; } - - public object CategoryValue { get; set; } + public string? NameField { get; set; } public DisplayGenericEntitesAuditTrailAttribute(Type entityType) { diff --git a/shesha-core/src/Shesha.Framework/EntityHistory/DisplayManyToManyAuditTrailAttribute.cs b/shesha-core/src/Shesha.Framework/EntityHistory/DisplayManyToManyAuditTrailAttribute.cs index 12966af71b..d1fdeec10e 100644 --- a/shesha-core/src/Shesha.Framework/EntityHistory/DisplayManyToManyAuditTrailAttribute.cs +++ b/shesha-core/src/Shesha.Framework/EntityHistory/DisplayManyToManyAuditTrailAttribute.cs @@ -1,7 +1,5 @@ -using System; -using System.Linq; -using Abp.Domain.Entities; -using Shesha.Utilities; +using Shesha.Utilities; +using System; namespace Shesha.EntityHistory { @@ -12,17 +10,17 @@ public class DisplayManyToManyAuditTrailAttribute : Attribute public string DisplayName { get; set; } - public string OwnEntityField { get; set; } + public string? OwnEntityField { get; set; } public string RelatedEntityField { get; set; } - public Type RelatedEntityType { get; set; } + public Type? RelatedEntityType { get; set; } public bool AnyRelatedEntityType { get; set; } - public string NameField { get; set; } + public string? NameField { get; set; } - public string[] AuditedFields { get; set; } + public string[]? AuditedFields { get; set; } public DisplayManyToManyAuditTrailAttribute(Type manyToManyEntityType, string relatedEntityField) { diff --git a/shesha-core/src/Shesha.Framework/EntityHistory/EntityHistoryEventCreatorBase.cs b/shesha-core/src/Shesha.Framework/EntityHistory/EntityHistoryEventCreatorBase.cs index 800d68739e..8d645c4a61 100644 --- a/shesha-core/src/Shesha.Framework/EntityHistory/EntityHistoryEventCreatorBase.cs +++ b/shesha-core/src/Shesha.Framework/EntityHistory/EntityHistoryEventCreatorBase.cs @@ -54,7 +54,7 @@ public virtual EntityHistoryEventInfo CreateEvent(string eventType, string event /* Many to many parent event features */ - public virtual EntityHistoryEventInfo CreateManyToManyEvent(EntityChangesInfo change) + public virtual EntityHistoryEventInfo? CreateManyToManyEvent(EntityChangesInfo change) { var changeSet = new EntityChangesInfo>() { @@ -72,7 +72,7 @@ public virtual EntityHistoryEventInfo CreateManyToManyEvent(EntityChangesInfo ch return CreateManyToManyEvent(changeSet); } - public virtual EntityHistoryEventInfo CreateManyToManyEvent(EntityChangesInfo> change) + public virtual EntityHistoryEventInfo? CreateManyToManyEvent(EntityChangesInfo> change) { var (added, removed) = GetListNewAndRemoved(change); @@ -86,13 +86,13 @@ public virtual EntityHistoryEventInfo CreateManyToManyEvent(EntityChangesInfo displayProperty?.GetValue(x)?.ToString() ?? x.ToString())) + ". "; + description += String.Join(", ", added.Select(x => displayProperty?.GetValue(x)?.ToString() ?? x?.ToString())) + ". "; } if (removed.Any()) { description += "Removed: "; - description += String.Join(", ", removed.Select(x => displayProperty?.GetValue(x)?.ToString() ?? x.ToString())) + ". "; + description += String.Join(", ", removed.Select(x => displayProperty?.GetValue(x)?.ToString() ?? x?.ToString())) + ". "; } return CreateEvent($"'{propName}' updated", description); @@ -110,7 +110,7 @@ public virtual (IEnumerable addedValues, IEnumerable removedValu /* Many to many child event features */ - public virtual EntityHistoryEventInfo CreateManyToManyRelationEvent(EntityChangesInfo change) + public virtual EntityHistoryEventInfo? CreateManyToManyRelationEvent(EntityChangesInfo change) { var changeSet = new EntityChangeRelationInfo() { @@ -136,9 +136,9 @@ public virtual EntityHistoryEventInfo CreateManyToManyAddRelationEvent(EntityCha { var propName = ReflectionHelper.GetDisplayName(change.Property).TruncateWithPostfix(EntityPropertyChange.MaxPropertyNameLength); var childDisplayProperty = typeof(T).GetDisplayNamePropertyInfoOrNull(); - var childName = childDisplayProperty?.GetValue(change.ChildEntity, null) ?? change.ChildEntity.ToString(); + var childName = childDisplayProperty?.GetValue(change.ChildEntity, null) ?? change.ChildEntity?.ToString(); var parentDisplayProperty = typeof(E).GetDisplayNamePropertyInfoOrNull(); - var parentName = parentDisplayProperty?.GetValue(change.ParentEntity, null) ?? change.ParentEntity.ToString(); + var parentName = parentDisplayProperty?.GetValue(change.ParentEntity, null) ?? change.ParentEntity?.ToString(); return CreateEvent($"Added to a list...", $"'{childName}' added to '{propName}' of '{parentName}'"); } @@ -147,9 +147,9 @@ public virtual EntityHistoryEventInfo CreateManyToManyRemoveRelationEvent(Entity { var propName = ReflectionHelper.GetDisplayName(change.Property).TruncateWithPostfix(EntityPropertyChange.MaxPropertyNameLength); var childDisplayProperty = typeof(T).GetDisplayNamePropertyInfoOrNull(); - var childName = childDisplayProperty?.GetValue(change.ChildEntity, null) ?? change.ChildEntity.ToString(); + var childName = childDisplayProperty?.GetValue(change.ChildEntity, null) ?? change.ChildEntity?.ToString(); var parentDisplayProperty = typeof(E).GetDisplayNamePropertyInfoOrNull(); - var parentName = parentDisplayProperty?.GetValue(change.ParentEntity, null) ?? change.ParentEntity.ToString(); + var parentName = parentDisplayProperty?.GetValue(change.ParentEntity, null) ?? change.ParentEntity?.ToString(); return CreateEvent($"Removed from a list...", $"'{childName}' removed from '{propName}' of '{parentName}'"); } diff --git a/shesha-core/src/Shesha.Framework/EntityHistory/EntityHistoryEventInfo.cs b/shesha-core/src/Shesha.Framework/EntityHistory/EntityHistoryEventInfo.cs index 63c30c47f8..3d9dca0764 100644 --- a/shesha-core/src/Shesha.Framework/EntityHistory/EntityHistoryEventInfo.cs +++ b/shesha-core/src/Shesha.Framework/EntityHistory/EntityHistoryEventInfo.cs @@ -16,8 +16,8 @@ public class EntityChangesInfo { public virtual object Entity { get; set; } public virtual PropertyInfo Property { get; set; } - public virtual object OldValue { get; set; } - public virtual object NewValue { get; set; } + public virtual object? OldValue { get; set; } + public virtual object? NewValue { get; set; } public DateTime DateTime { get; set; } } diff --git a/shesha-core/src/Shesha.Framework/EntityHistory/IEntityHistoryCreator.cs b/shesha-core/src/Shesha.Framework/EntityHistory/IEntityHistoryCreator.cs index 74844ca83d..bd06a059a5 100644 --- a/shesha-core/src/Shesha.Framework/EntityHistory/IEntityHistoryCreator.cs +++ b/shesha-core/src/Shesha.Framework/EntityHistory/IEntityHistoryCreator.cs @@ -1,7 +1,6 @@ -using System; -using Abp.EntityHistory; +using Abp.EntityHistory; using Abp.Runtime.Session; -using Microsoft.AspNetCore.Http; +using System; namespace Shesha.EntityHistory { @@ -9,7 +8,7 @@ public interface IEntityHistoryCreator { bool TypeAllowed(Type type); - EntityChange GetEntityChange(object entity, IAbpSession abpSession, string[] propertyNames, object[] loadedState, object[] currentState, Int32[] dirtyProps); + EntityChange? GetEntityChange(object entity, IAbpSession abpSession, string[] propertyNames, object[] loadedState, object[] currentState, Int32[] dirtyProps); } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/EntityHistory/IEntityHistoryEventCreator.cs b/shesha-core/src/Shesha.Framework/EntityHistory/IEntityHistoryEventCreator.cs index 9cb5e46f05..4bf81916bf 100644 --- a/shesha-core/src/Shesha.Framework/EntityHistory/IEntityHistoryEventCreator.cs +++ b/shesha-core/src/Shesha.Framework/EntityHistory/IEntityHistoryEventCreator.cs @@ -1,5 +1,4 @@ -using EasyNetQ.Events; -using System.Collections.Generic; +using System.Collections.Generic; namespace Shesha.EntityHistory { @@ -7,9 +6,9 @@ public interface IEntityHistoryEventCreator { EntityHistoryEventInfo CreateEvent(EntityChangesInfo change); - EntityHistoryEventInfo CreateManyToManyEvent(EntityChangesInfo change); + EntityHistoryEventInfo? CreateManyToManyEvent(EntityChangesInfo change); - EntityHistoryEventInfo CreateManyToManyRelationEvent(EntityChangesInfo change); + EntityHistoryEventInfo? CreateManyToManyRelationEvent(EntityChangesInfo change); (IEnumerable addedValues, IEnumerable removedValues) GetListNewAndRemoved(EntityChangesInfo change); } diff --git a/shesha-core/src/Shesha.Framework/EntityHistory/Settings/SettingsHistoryCreator.cs b/shesha-core/src/Shesha.Framework/EntityHistory/Settings/SettingsHistoryCreator.cs index 005ef9f759..99a4324c39 100644 --- a/shesha-core/src/Shesha.Framework/EntityHistory/Settings/SettingsHistoryCreator.cs +++ b/shesha-core/src/Shesha.Framework/EntityHistory/Settings/SettingsHistoryCreator.cs @@ -1,12 +1,12 @@ -using System; -using System.Collections.Generic; -using Abp.Json; -using Abp.Configuration; +using Abp.Configuration; using Abp.Dependency; using Abp.EntityHistory; using Abp.Events.Bus.Entities; using Abp.Extensions; +using Abp.Json; using Abp.Runtime.Session; +using System; +using System.Collections.Generic; namespace Shesha.EntityHistory.Settings { @@ -24,10 +24,11 @@ public bool TypeAllowed(Type type) return typeof(Setting).IsAssignableFrom(type); } - public EntityChange GetEntityChange(object entity, IAbpSession abpSession, string[] propertyNames, object[] loadedState, object[] currentState, Int32[] dirtyProps) + public EntityChange? GetEntityChange(object entity, IAbpSession abpSession, string[] propertyNames, object[] loadedState, object[] currentState, Int32[] dirtyProps) { var setting = entity as Setting; - if (setting == null) return null; + if (setting == null) + return null; var entityChange = new EntityChange { @@ -39,8 +40,8 @@ public EntityChange GetEntityChange(object entity, IAbpSession abpSession, strin TenantId = abpSession.TenantId, }; - object oldValue; - object newValue; + object? oldValue; + object? newValue; var valueIndex = Array.IndexOf(propertyNames, nameof(Setting.Value)); if (loadedState == null) diff --git a/shesha-core/src/Shesha.Framework/EntityReferences/EntityReferenceAttribute.cs b/shesha-core/src/Shesha.Framework/EntityReferences/EntityReferenceAttribute.cs index b6cdffa95b..f6724db59d 100644 --- a/shesha-core/src/Shesha.Framework/EntityReferences/EntityReferenceAttribute.cs +++ b/shesha-core/src/Shesha.Framework/EntityReferences/EntityReferenceAttribute.cs @@ -8,9 +8,9 @@ public class EntityReferenceAttribute: Attribute public string[] AllowableTypes { get; internal set; } = new string[0]; public bool StoreDisplayName { get; internal set; } = false; - public string IdColumnName { get; set; } = null; - public string ClassNameColumnName { get; set; } = null; - public string DisplayNameColumnName { get; set; } = null; + public string? IdColumnName { get; set; } + public string? ClassNameColumnName { get; set; } + public string? DisplayNameColumnName { get; set; } public EntityReferenceAttribute(string[] allowableTypes, bool storeDisplayName) { diff --git a/shesha-core/src/Shesha.Framework/EntityReferences/EntityTypeIndentifier.cs b/shesha-core/src/Shesha.Framework/EntityReferences/EntityTypeIndentifier.cs index 443b34d8c8..240ef989de 100644 --- a/shesha-core/src/Shesha.Framework/EntityReferences/EntityTypeIndentifier.cs +++ b/shesha-core/src/Shesha.Framework/EntityReferences/EntityTypeIndentifier.cs @@ -13,9 +13,9 @@ public class EntityTypeIndentifier /// /// Accessor of the module /// - public string Module { get; set; } + public string? Module { get; set; } - public EntityTypeIndentifier(string module, string name) + public EntityTypeIndentifier(string? module, string name) { Module = module; Name = name; diff --git a/shesha-core/src/Shesha.Framework/EntityReferences/GenericEntityReference.cs b/shesha-core/src/Shesha.Framework/EntityReferences/GenericEntityReference.cs index 93e0114559..76a19e6379 100644 --- a/shesha-core/src/Shesha.Framework/EntityReferences/GenericEntityReference.cs +++ b/shesha-core/src/Shesha.Framework/EntityReferences/GenericEntityReference.cs @@ -1,5 +1,5 @@ using Abp.Domain.Entities; -using Abp.Extensions; +using Newtonsoft.Json; using Shesha.Extensions; using Shesha.Reflection; using Shesha.Services; @@ -10,13 +10,14 @@ namespace Shesha.EntityReferences [Serializable] public class GenericEntityReference : IEquatable, IGenericEntityReference { - private object _entity; - - public GenericEntityReference(string id, string typeName, string dysplayName = null) + private object? _entity; + + [JsonConstructor] + public GenericEntityReference(string id, string _className, string? _displayName = null) { Id = id; - _className = typeName; - _displayName = dysplayName; + this._className = _className; + this._displayName = _displayName; } public GenericEntityReference(object entity) @@ -26,19 +27,21 @@ public GenericEntityReference(object entity) _entity = entity; if (_entity.GetType().GetProperty("Id") == null) throw new NullReferenceException($"entity.{nameof(GenericEntityReference.Id)} not found"); - Id = _entity.GetType().GetProperty("Id")?.GetValue(_entity)?.ToString(); - if (Id.IsNullOrEmpty()) + var id = _entity.GetType().GetProperty("Id")?.GetValue(_entity)?.ToString(); + if (string.IsNullOrWhiteSpace(id)) throw new NullReferenceException($"entity.{nameof(GenericEntityReference.Id)} can not be NULL"); - _className = _entity.GetType().StripCastleProxyType().FullName; + Id = id; + _className = _entity.GetType().StripCastleProxyType().GetRequiredFullName(); _displayName = _entity.GetEntityDisplayName(); } + [JsonProperty("id")] public virtual string Id { get; internal set; } public virtual string _className { get; internal set; } - public virtual string _displayName { get; internal set; } + public virtual string? _displayName { get; internal set; } public static implicit operator Entity(GenericEntityReference reference) => GetEntity(reference); private static Entity GetEntity(GenericEntityReference reference) @@ -57,31 +60,34 @@ private static GenericEntityReference SetEntity(Entity entity) return new GenericEntityReference(entity); } - public override bool Equals(object obj) => this.Equals(obj as GenericEntityReference); + public override bool Equals(object? obj) => this.Equals(obj as GenericEntityReference); - public bool Equals(GenericEntityReference obj) + public bool Equals(GenericEntityReference? obj) { return obj != null && Id == obj.Id && _className == obj._className; } - public static bool operator ==(GenericEntityReference l, GenericEntityReference r) + public static bool operator ==(GenericEntityReference? l, GenericEntityReference? r) { - if (l is null && r is null) - return true; - - if ((l is null) != (r is null)) + if (l is null) + { + if (r is null) + return true; + // Only the left side is null. return false; - + } // Equals handles case of null on right side. return l.Equals(r); } - public static bool operator !=(GenericEntityReference l, GenericEntityReference r) => !(l == r); + public static bool operator !=(GenericEntityReference? l, GenericEntityReference? r) => !(l == r); public override int GetHashCode() { - return Id.IsNullOrEmpty() ? 0 : Id.GetHashCode(); + return string.IsNullOrWhiteSpace(Id) + ? 0 + : Id.GetHashCode(); } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/EntityReferences/IGenericEntityReference.cs b/shesha-core/src/Shesha.Framework/EntityReferences/IGenericEntityReference.cs index 4a2bbaeefb..556e6b43aa 100644 --- a/shesha-core/src/Shesha.Framework/EntityReferences/IGenericEntityReference.cs +++ b/shesha-core/src/Shesha.Framework/EntityReferences/IGenericEntityReference.cs @@ -4,7 +4,6 @@ namespace Shesha.EntityReferences { public interface IGenericEntityReference: IHasClassNameField, IHasDisplayNameField { - string Id { get; } + string? Id { get; } } - } diff --git a/shesha-core/src/Shesha.Framework/Exceptions/ActivatorException.cs b/shesha-core/src/Shesha.Framework/Exceptions/ActivatorException.cs new file mode 100644 index 0000000000..527ee4530c --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Exceptions/ActivatorException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Shesha.Exceptions +{ + public class ActivatorException: Exception + { + public Type RequestedType { get; private set; } + + public ActivatorException(Type requestedType) : this(requestedType, $"Failed to create instance of type '{requestedType.FullName}'") + { + + } + + public ActivatorException(Type requestedType, string message) : base(message) + { + RequestedType = requestedType; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Exceptions/FieldNotFoundException.cs b/shesha-core/src/Shesha.Framework/Exceptions/FieldNotFoundException.cs new file mode 100644 index 0000000000..ac2c11d89d --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Exceptions/FieldNotFoundException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Shesha.Exceptions +{ + /// + /// Indicates that requested field not found in a specified type + /// + public class FieldNotFoundException : Exception + { + public Type OwnerType { get; private set; } + public string FieldName { get; private set; } + + public FieldNotFoundException(Type type, string fieldName): base($"Field '{fieldName}' not found in type '{type.FullName}'") + { + OwnerType = type; + FieldName = fieldName; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Exceptions/ManifestResourceStreamNotFoundException.cs b/shesha-core/src/Shesha.Framework/Exceptions/ManifestResourceStreamNotFoundException.cs new file mode 100644 index 0000000000..0e80296395 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Exceptions/ManifestResourceStreamNotFoundException.cs @@ -0,0 +1,19 @@ +using System; +using System.Reflection; + +namespace Shesha.Exceptions +{ + /// + /// Indicates that embedded resource not found in the specified assembly + /// + public class ManifestResourceStreamNotFoundException: Exception + { + public Assembly Assembly { get; private set; } + public string RecourceName { get; private set; } + public ManifestResourceStreamNotFoundException(Assembly assembly, string resourceName): base($"Embedded resource '{resourceName}' not found in assembly '{assembly.FullName}'") + { + Assembly = assembly; + RecourceName = resourceName; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Exceptions/MethodNotFoundException.cs b/shesha-core/src/Shesha.Framework/Exceptions/MethodNotFoundException.cs new file mode 100644 index 0000000000..a8eaca90f9 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Exceptions/MethodNotFoundException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Shesha.Exceptions +{ + /// + /// Indicates that requested method not found in a specified type + /// + public class MethodNotFoundException : Exception + { + public Type OwnerType { get; private set; } + public string MethodName { get; private set; } + + public MethodNotFoundException(Type type, string methodName): base($"Method '{methodName}' not found in type '{type.FullName}'") + { + OwnerType = type; + MethodName = methodName; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Exceptions/PropertyNotFoundException.cs b/shesha-core/src/Shesha.Framework/Exceptions/PropertyNotFoundException.cs new file mode 100644 index 0000000000..53e84a6296 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Exceptions/PropertyNotFoundException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Shesha.Exceptions +{ + /// + /// Indicates that requested property not found in a specified type + /// + public class PropertyNotFoundException : Exception + { + public Type OwnerType { get; private set; } + public string PropertyName { get; private set; } + + public PropertyNotFoundException(Type type, string propertyName): base($"Property '{propertyName}' not found in type '{type.FullName}'") + { + OwnerType = type; + PropertyName = propertyName; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Exceptions/UnitOfWorkUnavailableException.cs b/shesha-core/src/Shesha.Framework/Exceptions/UnitOfWorkUnavailableException.cs new file mode 100644 index 0000000000..09c405a17e --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Exceptions/UnitOfWorkUnavailableException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Shesha.Exceptions +{ + public class UnitOfWorkUnavailableException: Exception + { + public UnitOfWorkUnavailableException(): base("Unit Of Work is unavailable") + { + + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Extensions/ActivatorHelper.cs b/shesha-core/src/Shesha.Framework/Extensions/ActivatorHelper.cs new file mode 100644 index 0000000000..ae03b91d74 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Extensions/ActivatorHelper.cs @@ -0,0 +1,40 @@ +using Shesha.Exceptions; +using System; + +namespace Shesha.Extensions +{ + /// + /// Activator helper. Extends static class + /// + public static class ActivatorHelper + { + + /// + /// Create new instance of type + /// + /// + public static object CreateNotNullObject(Type type) + { + return Activator.CreateInstance(type) ?? throw new ActivatorException(type); + } + + /// + /// Create new instance of type + /// + /// + public static object CreateNotNullObject(Type type, params object?[]? args) + { + return Activator.CreateInstance(type, args) ?? throw new ActivatorException(type); + } + + /// + /// Create new instance of type + /// + /// + /// + public static T CreateNotNullInstance() where T : class + { + return Activator.CreateInstance() ?? throw new ActivatorException(typeof(T)); + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Extensions/AssemblyExtensions.cs b/shesha-core/src/Shesha.Framework/Extensions/AssemblyExtensions.cs index fea2b190a3..b77be5ed53 100644 --- a/shesha-core/src/Shesha.Framework/Extensions/AssemblyExtensions.cs +++ b/shesha-core/src/Shesha.Framework/Extensions/AssemblyExtensions.cs @@ -1,4 +1,5 @@ -using Abp.Reflection; +using Abp.Dependency; +using Abp.Reflection; using Shesha.Modules; using Shesha.Services; using System; @@ -13,59 +14,24 @@ namespace Shesha.Extensions /// public static class AssemblyExtensions { - private static List _cacheItems; - static AssemblyExtensions () + private static ModuleCacheItem? GetCacheItem(Assembly assembly) { - var typeFinder = StaticContext.IocManager.Resolve(); - - var moduleTypes = typeFinder.Find(t => !t.IsAbstract && typeof(SheshaModule).IsAssignableFrom(t)).ToList(); - var moduleItems = moduleTypes.Select(mt => { - var instance = GetModuleInstance(mt); - return instance != null - ? new MainModuleCacheItem(instance) - : null; - - }) - .Where(item => item != null) - .ToList(); - - var subModuleTypes = typeFinder.Find(t => !t.IsAbstract && typeof(ISheshaSubmodule).IsAssignableFrom(t)).ToList(); - var subModuleItems = subModuleTypes.Select(sm => { - var subModuleInstance = StaticContext.IocManager.IsRegistered(sm) - ? StaticContext.IocManager.Resolve(sm) as ISheshaSubmodule - : null; - if (subModuleInstance != null) - { - var mainModuleItem = moduleItems.FirstOrDefault(i => i.ModuleType == subModuleInstance.ModuleType); - if (mainModuleItem != null) - return new SubModuleCacheItem(subModuleInstance, mainModuleItem); - } - return null; - }) - .Where(item => item != null) - .ToList(); - - _cacheItems = moduleItems.Cast().ToList(); - _cacheItems.AddRange(subModuleItems); - } - - private static SheshaModule GetModuleInstance(Type moduleType) - { - return StaticContext.IocManager.IsRegistered(moduleType) - ? StaticContext.IocManager.Resolve(moduleType) as SheshaModule - : null; - } - - private static ModuleCacheItem GetCacheItem(Assembly assembly) - { - return _cacheItems.FirstOrDefault(i => i.Assembly == assembly); + try + { + var store = StaticContext.IocManager.Resolve(); + return store.Modules.FirstOrDefault(i => i.Assembly == assembly); + } + catch + { + throw; + } } /// /// Get type of the configurable module to which the specified assembly belongs to /// /// - public static Type GetConfigurableModuleType(this Assembly assembly) + public static Type? GetConfigurableModuleType(this Assembly assembly) { return GetCacheItem(assembly)?.ModuleType; } @@ -74,7 +40,7 @@ public static Type GetConfigurableModuleType(this Assembly assembly) /// Get info of the configurable module to which the specified assembly belongs to /// /// - public static SheshaModuleInfo GetConfigurableModuleInfo(this Assembly assembly) + public static SheshaModuleInfo? GetConfigurableModuleInfo(this Assembly assembly) { return GetCacheItem(assembly)?.ModuleInfo; } @@ -83,37 +49,89 @@ public static SheshaModuleInfo GetConfigurableModuleInfo(this Assembly assembly) /// Get name of the configurable module to which the specified assembly belongs to /// /// - public static string GetConfigurableModuleName(this Assembly assembly) + public static string? GetConfigurableModuleName(this Assembly assembly) { return GetCacheItem(assembly)?.ModuleInfo.Name; } + } - private class ModuleCacheItem + public class ModuleCacheItem + { + public Assembly Assembly { get; protected set; } + public Type ModuleType { get; protected set; } + public SheshaModuleInfo ModuleInfo { get; protected set; } + + protected ModuleCacheItem(Assembly assembly, Type moduleType, SheshaModuleInfo moduleInfo) { - public Assembly Assembly { get; protected set; } - public Type ModuleType { get; protected set; } - public SheshaModuleInfo ModuleInfo { get; protected set; } + Assembly = assembly; + ModuleType = moduleType; + ModuleInfo = moduleInfo; } + } - private class MainModuleCacheItem: ModuleCacheItem + public class MainModuleCacheItem : ModuleCacheItem + { + public MainModuleCacheItem(SheshaModule module) : base(module.GetType().Assembly, module.GetType(), module.ModuleInfo) { - public MainModuleCacheItem(SheshaModule module) - { - var moduleType = module.GetType(); - Assembly = moduleType.Assembly; - ModuleType = moduleType; - ModuleInfo = module.ModuleInfo; - } } + } - private class SubModuleCacheItem : ModuleCacheItem + public class SubModuleCacheItem : ModuleCacheItem + { + public SubModuleCacheItem(ISheshaSubmodule submodule, MainModuleCacheItem mainModuleItem) : base(submodule.GetType().Assembly, mainModuleItem.ModuleType, mainModuleItem.ModuleInfo) { - public SubModuleCacheItem(ISheshaSubmodule submodule, MainModuleCacheItem mainModuleItem) - { - Assembly = submodule.GetType().Assembly; - ModuleType = mainModuleItem.ModuleType; - ModuleInfo = mainModuleItem.ModuleInfo; - } } } -} + + public interface IModuleList + { + public List Modules { get; } + } + public class ModuleList : IModuleList, ISingletonDependency + { + public List Modules { get; private set; } + private readonly IIocResolver _iocResolver; + + public ModuleList(IIocResolver iocResolver, ITypeFinder typeFinder) + { + _iocResolver = iocResolver; + + var moduleTypes = typeFinder.Find(t => !t.IsAbstract && typeof(SheshaModule).IsAssignableFrom(t)).ToList(); + var moduleItems = moduleTypes.Select(mt => { + var instance = GetModuleInstance(mt); + return instance != null + ? new MainModuleCacheItem(instance) + : null; + + }) + .WhereNotNull() + .ToList(); + + var subModuleTypes = typeFinder.Find(t => !t.IsAbstract && typeof(ISheshaSubmodule).IsAssignableFrom(t)).ToList(); + var subModuleItems = subModuleTypes.Select(sm => { + var subModuleInstance = StaticContext.IocManager.IsRegistered(sm) + ? StaticContext.IocManager.Resolve(sm) as ISheshaSubmodule + : null; + if (subModuleInstance != null) + { + var mainModuleItem = moduleItems.FirstOrDefault(i => i.ModuleType == subModuleInstance.ModuleType); + if (mainModuleItem != null) + return new SubModuleCacheItem(subModuleInstance, mainModuleItem); + } + return null; + }) + .WhereNotNull() + .ToList(); + + Modules = moduleItems.Cast().ToList(); + Modules.AddRange(subModuleItems); + } + + private SheshaModule? GetModuleInstance(Type moduleType) + { + return _iocResolver.IsRegistered(moduleType) + ? _iocResolver.Resolve(moduleType) as SheshaModule + : null; + } + } +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Extensions/DictionaryExtensions.cs b/shesha-core/src/Shesha.Framework/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000000..947490cdb1 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Extensions/DictionaryExtensions.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Shesha.Extensions +{ + /// + /// Dictionary extensions + /// + public static class DictionaryExtensions + { + /// + /// Gets an item from the dictionary. Uses the factory method to get the object if key is missing in the dictionary. + /// + /// + /// + /// Dictionary + /// Key to search for + /// Factory method to create item if not exists + /// Existing item or item created by + public static async Task GetAsync(this IDictionary dictionary, TKey key, Func> factory) + { + if (dictionary.TryGetValue(key, out var value)) + return value; + + value = await factory(key); + dictionary.Add(key, value); + return value; + } + + /// + /// Gets an item from the dictionary. Uses the factory method to get the object if key is missing in the dictionary. + /// + /// + /// + /// Dictionary + /// Key to search for + /// Factory method to create item if not exists + /// Existing item or item created by + public static TValue Get(this IDictionary dictionary, TKey key, Func factory) + { + if (dictionary.TryGetValue(key, out var value)) + return value; + + value = factory(key); + dictionary.Add(key, value); + return value; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Extensions/EntityExtensions.cs b/shesha-core/src/Shesha.Framework/Extensions/EntityExtensions.cs index 176340ad37..98422abb2f 100644 --- a/shesha-core/src/Shesha.Framework/Extensions/EntityExtensions.cs +++ b/shesha-core/src/Shesha.Framework/Extensions/EntityExtensions.cs @@ -1,5 +1,4 @@ using Abp.Auditing; -using Abp.Dependency; using Abp.Domain.Entities; using Abp.Domain.Entities.Auditing; using Abp.Domain.Repositories; @@ -22,7 +21,6 @@ namespace Shesha.Extensions { -#nullable enable /// /// Entity extensions /// @@ -152,7 +150,7 @@ public static string GetTypeShortAlias(this Type entityType) var value = expression.Compile().Invoke(owner); if (value == null) return null; - var refListAttribute = prop.GetAttribute() ?? prop.PropertyType.GetUnderlyingTypeIfNullable().GetAttribute(); + var refListAttribute = prop.GetAttributeOrNull() ?? prop.PropertyType.GetUnderlyingTypeIfNullable().GetAttributeOrNull(); if (refListAttribute != null) { return StaticContext.IocManager.Resolve().GetItemDisplayText(refListAttribute.GetReferenceListIdentifier(prop), value.Value.ToInt32(CultureInfo.InvariantCulture)); @@ -190,71 +188,42 @@ public static TId GetId(this IEntity entity) : entity.GetType().GetProperty("Id")?.GetValue(entity); } - //[Obsolete("To be reviewed")] public static string? GetPropertyDisplayText(this object entity, string propertyName, string defaultValue = "") { try { - var val = ReflectionHelper.GetPropertyValue(entity, propertyName, out object parentEntity, out var propInfo); - if (val == null) + var propValue = ReflectionHelper.GetPropertyValueAccessor(entity, propertyName); + if (!propValue.IsValueAvailable || propValue.Value == null) return defaultValue; - var propConfig = parentEntity.GetType().GetEntityConfiguration()[propInfo.Name]; - var generalDataType = propConfig.GeneralType; + entity = propValue.Parent; + propertyName = propValue.PropInfo.Name; + var value = propValue.Value; - entity = parentEntity; - propertyName = propInfo.Name; + var propConfig = entity.GetType().GetEntityConfiguration()[propertyName]; + var generalDataType = propConfig.GeneralType; + switch (generalDataType) { case GeneralDataType.Enum: - var itemValue = Convert.ToInt32(val); - return ReflectionHelper.GetEnumDescription(propConfig.EnumType, itemValue); + var itemValue = Convert.ToInt32(value); + return ReflectionHelper.GetEnumDescription(propConfig.EnumType.NotNull($"{nameof(propConfig.EnumType)} must not be null"), itemValue); case GeneralDataType.ReferenceList: { var refListHelper = StaticContext.IocManager.Resolve(); - return refListHelper.GetItemDisplayText(new ReferenceListIdentifier(propConfig.ReferenceListModule, propConfig.ReferenceListName), (int)val); + return refListHelper.GetItemDisplayText(propConfig.GetRefListIdentifier(), (int)value); } case GeneralDataType.EntityReference: { - var displayProperty = val.GetType().GetEntityConfiguration().DisplayNamePropertyInfo; + var displayProperty = value.GetType().GetEntityConfiguration().DisplayNamePropertyInfo; return displayProperty != null - ? displayProperty.GetValue(val)?.ToString() - : val.ToString(); + ? displayProperty.GetValue(value)?.ToString() + : value.ToString(); } default: - return GetPrimitiveTypePropertyDisplayText(val, propInfo, defaultValue); - //return val.ToString(); + return GetPrimitiveTypePropertyDisplayText(value, propValue.PropInfo, defaultValue); } - // todo: review and remove - /* - var propConfig = EntityConfiguration.Get(parentEntity.GetType())[propInfo.Name]; - var generalDataType = propConfig.GeneralType; - - entity = parentEntity; - propertyName = propInfo.Name; - - switch (generalDataType) - { - case GeneralDataType.Enum: - var itemValue = (int)val; - return ReflectionHelper.GetEnumDescription(propConfig.EnumType, itemValue); - case GeneralDataType.ReferenceList: - return entity.GetReferenceListItemName(propertyName, defaultValue); - case GeneralDataType.MultiValueReferenceList: - return entity.GetMultiValueReferenceListItemNames(propertyName, defaultValue); - case GeneralDataType.EntityReference: - case GeneralDataType.StoredFile: - var referencedEntity = entity.GetReferencedEntity(propertyName); - - if (referencedEntity == null) - return defaultValue ?? ""; - else - return referencedEntity.GetDisplayName(); - default: - return GetPrimitiveTypePropertyDisplayText(val, propInfo, defaultValue); - } - */ } catch (Exception ex) { @@ -281,7 +250,7 @@ public static TId GetId(this IEntity entity) private static string? GetPropertyDisplayFormat(PropertyInfo propInfo) { - return propInfo.GetAttribute(true)?.DataFormatString; + return propInfo.GetAttributeOrNull(true)?.DataFormatString; } private static string? FormatValue(object val, PropertyInfo propInfo, string? format, bool isForEdit) @@ -302,9 +271,9 @@ public static TId GetId(this IEntity entity) else if (underlyingType == typeof(DateTime)) { var typedVal = (DateTime)val; - string finalFormat; + string? finalFormat; - var dataTypeAtt = propInfo.GetAttribute(); + var dataTypeAtt = propInfo.GetAttributeOrNull(); if (dataTypeAtt != null && dataTypeAtt.GetDataTypeName().Equals("Date", StringComparison.InvariantCultureIgnoreCase)) { finalFormat = StringHelper.FirstValue(format, defaultDateFormat); @@ -350,7 +319,7 @@ public static TId GetId(this IEntity entity) public static GeneralDataType GetGeneralDataType(this PropertyInfo propInfo) { - return propInfo.DeclaringType.GetEntityConfiguration()[propInfo.Name].GeneralType; + return propInfo.DeclaringType.NotNull().GetEntityConfiguration()[propInfo.Name].GeneralType; } public static List GetFullChain(this T entity, Func parentFunc) where T : class @@ -430,97 +399,6 @@ public static string FullyQualifiedEntityId(this TEntity entity) w #region Multivalue reference list todo: review - /// - /// Returns a comma separated list of Reference List Item referenced by the property. - /// - /// - /// Name of the Reference List Item property. - /// Value to return if reference list item value is null or empty. - /// - /// If property value is null returns the , else - /// returns a comma separated list of the Rerefence List Items. - [Obsolete("Should use native MVC functionality and related DataAnnotations e.g. Display")] - public static string GetMultiValueReferenceListItemNames(this object entity, string propertyName, string? defaultValue = null, string separator = ", ") - { - var items = GetMultiValueReferenceListItemNamesAr(entity, propertyName); - if (defaultValue != null && items.Length == 0) - { - return defaultValue; - } - else - { - return String.Join(separator, items); - } - } - - /// - /// Returns a string array of allReference List items referenced by the property. - /// - /// Entity - /// Name of the Reference List Item property. - /// If property value is null or zero-length string, returns a zero length array, else - /// returns a list Rerefence List items specified by the property value. - public static string[] GetMultiValueReferenceListItemNamesAr(this object entity, string propertyName) - { - //string refListNamespace, refListName; - var itemsInt = GetMultiValueReferenceListItemValuesAr(entity, propertyName, out var refListModule, out var refListName); - - var result = new string[itemsInt.Length]; - - var refListHelper = StaticContext.IocManager.Resolve(); - - var property = entity.GetType().GetProperty(propertyName); - if (property == null) - throw new ArgumentException($"Property '{propertyName}' not found in type '{entity.GetType().FullName}'"); - - for (int i = 0; i < itemsInt.Length; i++) - { - if (refListName == "" && refListModule == "") - result[i] = ReflectionHelper.GetEnumDescription(ReflectionHelper.GetUnderlyingTypeIfNullable(property.PropertyType), itemsInt[i]); - else - result[i] = refListHelper.GetItemDisplayText(new ReferenceListIdentifier(refListModule, refListName), itemsInt[i]); - } - - return result; - } - - private static Int64[] GetMultiValueReferenceListItemValuesAr(this object entity, string propertyName, out string refListModule, out string refListName) - { - var propInfo = ReflectionHelper.GetProperty(entity, propertyName, out var propertyEntity); - - var propConfig = entity.GetType().GetEntityConfiguration()[propertyName]; - if (propConfig.GeneralType != GeneralDataType.MultiValueReferenceList) - throw new InvalidOperationException($"GetMultiValueReferenceListItemNamesAr cannot be called on property '{propertyName}' of type '{entity.GetType().FullName}' as property is not defined as a MultiValueReferenceList property."); - - refListModule = propConfig.ReferenceListModule; - refListName = propConfig.ReferenceListName; - - var propValue = propInfo.GetValue(propertyEntity, null); - - if (propValue == null) - return new Int64[] { }; - - long intVal; - - try - { - intVal = Convert.ToInt64(propValue); - } - catch - { - throw new ConfigurationErrorsException($"Property '{propertyName}' on type '{entity.GetType().FullName}' is marked as MultiValueReferenceList but it's type cannot be cast to 'int'."); - } - - if (refListName == "" && refListModule == "") - { - return - ReflectionHelper.FlagEnumToListOfValues( - ReflectionHelper.GetUnderlyingTypeIfNullable(propInfo.PropertyType), intVal).ToArray(); - } - - return DecomposeIntoBitFlagComponents(intVal); - } - /// /// Decomposes an integers into an array of its constituent BitFlag values (i.e. 1, 2, 4, etc...). /// @@ -562,9 +440,8 @@ public static Int64[] DecomposeIntoBitFlagComponents(long? val) /// public static Type GetRealEntityType(this IEntity entity) { - var provider = IocManager.Instance.Resolve(); + var provider = StaticContext.IocManager.Resolve(); return provider.GetEntityType(entity); } } -#nullable restore -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Extensions/EnumerableExtensions.cs b/shesha-core/src/Shesha.Framework/Extensions/EnumerableExtensions.cs index 7ff8ba1ac1..3205af3a9e 100644 --- a/shesha-core/src/Shesha.Framework/Extensions/EnumerableExtensions.cs +++ b/shesha-core/src/Shesha.Framework/Extensions/EnumerableExtensions.cs @@ -69,7 +69,7 @@ public static async Task> SelectManyAsync public static TResult MinOrDefault(this IEnumerable source, Func selector, TResult defaultValue) { return source.Any() - ? source.Min(selector) + ? source.Min(selector) ?? defaultValue : defaultValue; } @@ -86,7 +86,7 @@ public static TResult MinOrDefault(this IEnumerable s public static TResult MaxOrDefault(this IEnumerable source, Func selector, TResult defaultValue) { return source.Any() - ? source.Max(selector) + ? source.Max(selector) ?? defaultValue : defaultValue; } @@ -96,7 +96,7 @@ public static TResult MaxOrDefault(this IEnumerable s public static TSource MaxOrDefault(this IEnumerable source, TSource defaultValue) { return source.Any() - ? source.Max() + ? source.Max() ?? defaultValue : defaultValue; } @@ -108,10 +108,11 @@ public static TSource MaxOrDefault(this IEnumerable source, TS /// /// /// - public static (IEnumerable addedValues, IEnumerable removedValues) GetListNewAndRemoved(this object oldValue, object newValue) + public static (IEnumerable addedValues, IEnumerable removedValues) GetListNewAndRemoved(this object? oldValue, object? newValue) { + // TODO: Alex, please review and simplify if (newValue == null) - return (new List(), ((IEnumerable)oldValue).Cast()); + return (new List(), oldValue != null ? ((IEnumerable)oldValue).Cast() : new List()); if (oldValue == null) return (((IEnumerable)newValue).Cast(), new List()); @@ -122,6 +123,24 @@ public static (IEnumerable addedValues, IEnumerable removedValues) GetLi var removedValues = oldV.Where(x => !newV.Contains(x)).ToList(); return (addedValues.Cast(), removedValues.Cast()); + } + + /// + /// Returns a sequence with the `null` instances removed. + /// + public static IEnumerable WhereNotNull(this IEnumerable source) where T : class + { + return source.Where(x => x != null).OfType(); + } + + /// + /// Returns a sequence of strings without nulls and empty strings + /// + /// + /// + public static IEnumerable WhereNotNullOrWhiteSpace(this IEnumerable source) + { + return source.Where(x => !string.IsNullOrWhiteSpace(x)).OfType(); } } } diff --git a/shesha-core/src/Shesha.Framework/Extensions/IQueryableExtensions.cs b/shesha-core/src/Shesha.Framework/Extensions/IQueryableExtensions.cs index c77b99ca95..444a57db78 100644 --- a/shesha-core/src/Shesha.Framework/Extensions/IQueryableExtensions.cs +++ b/shesha-core/src/Shesha.Framework/Extensions/IQueryableExtensions.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Threading; using System.Threading.Tasks; namespace Shesha.Extensions @@ -17,22 +16,22 @@ namespace Shesha.Extensions /// public static class IQueryableExtensions { - public static IOrderedQueryable OrderBy(this IQueryable query, string propertyName, IComparer comparer = null) + public static IOrderedQueryable OrderBy(this IQueryable query, string propertyName, IComparer? comparer = null) { return CallOrderedQueryable(query, "OrderBy", propertyName, comparer); } - public static IOrderedQueryable OrderByDescending(this IQueryable query, string propertyName, IComparer comparer = null) + public static IOrderedQueryable OrderByDescending(this IQueryable query, string propertyName, IComparer? comparer = null) { return CallOrderedQueryable(query, "OrderByDescending", propertyName, comparer); } - public static IOrderedQueryable ThenBy(this IOrderedQueryable query, string propertyName, IComparer comparer = null) + public static IOrderedQueryable ThenBy(this IOrderedQueryable query, string propertyName, IComparer? comparer = null) { return CallOrderedQueryable(query, "ThenBy", propertyName, comparer); } - public static IOrderedQueryable ThenByDescending(this IOrderedQueryable query, string propertyName, IComparer comparer = null) + public static IOrderedQueryable ThenByDescending(this IOrderedQueryable query, string propertyName, IComparer? comparer = null) { return CallOrderedQueryable(query, "ThenByDescending", propertyName, comparer); } @@ -41,7 +40,7 @@ public static IOrderedQueryable ThenByDescending(this IOrderedQueryable /// Builds the Queryable functions using a TSource property name. /// public static IOrderedQueryable CallOrderedQueryable(this IQueryable query, string methodName, string propertyName, - IComparer comparer = null) + IComparer? comparer = null) { var param = Expression.Parameter(typeof(T), "x"); @@ -52,7 +51,7 @@ public static IOrderedQueryable CallOrderedQueryable(this IQueryable qu Expression.Call( typeof(Queryable), methodName, - new[] { typeof(T), body.Type }, + [typeof(T), body.Type], query.Expression, Expression.Lambda(body, param), Expression.Constant(comparer) @@ -62,7 +61,7 @@ public static IOrderedQueryable CallOrderedQueryable(this IQueryable qu Expression.Call( typeof(Queryable), methodName, - new[] { typeof(T), body.Type }, + [typeof(T), body.Type], query.Expression, Expression.Lambda(body, param) ) @@ -75,7 +74,7 @@ public static IOrderedQueryable CallOrderedQueryable(this IQueryable qu /// Queryable to be filtered /// String representation of JsonLogic filter /// - public static IQueryable ApplyFilter(this IQueryable query, string filter) where TEntity : class, IEntity + public static IQueryable ApplyFilter(this IQueryable query, string? filter) where TEntity : class, IEntity { if (string.IsNullOrWhiteSpace(filter)) return query; @@ -85,7 +84,9 @@ public static IQueryable ApplyFilter(this IQueryable(); var expression = jsonLogicConverter.ParseExpressionOf(jsonLogic); - return query.Where(expression); + return expression != null + ? query.Where(expression) + : query; } #region IAsyncQueryableExecuter extensions diff --git a/shesha-core/src/Shesha.Framework/Extensions/LinqExtensions.cs b/shesha-core/src/Shesha.Framework/Extensions/LinqExtensions.cs index 0389d6dafe..3db7425b09 100644 --- a/shesha-core/src/Shesha.Framework/Extensions/LinqExtensions.cs +++ b/shesha-core/src/Shesha.Framework/Extensions/LinqExtensions.cs @@ -1,12 +1,12 @@ -using System; +using JetBrains.Annotations; +using Shesha.Reflection; +using Shesha.Utilities; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Threading; using System.Threading.Tasks; -using Abp.Specifications; -using Shesha.Utilities; namespace Shesha.Extensions { @@ -26,7 +26,7 @@ public static async Task MaxOrDefaultAsync(this IQuer Expression> selector, TResult defaultValue) { return await source.AnyAsync() - ? source.Max(selector) // todo: update ABP and use async version or implement Shesha-specific interface + ? source.Max(selector) ?? defaultValue : defaultValue; } @@ -39,16 +39,18 @@ public static async Task MaxOrDefaultAsync(this IQuer /// public static IOrderedEnumerable OrderByDynamic(this IEnumerable items, string propertyName, string direction = "asc") { - var property = typeof(T).GetProperties().FirstOrDefault(x => x.Name.ToCamelCase() == propertyName.ToCamelCase()); + var property = typeof(T).GetProperties().First(x => x.Name.ToCamelCase() == propertyName.ToCamelCase()); var result = typeof(LinqExtensions) - .GetMethod("OrderByDynamic_Private", BindingFlags.NonPublic | BindingFlags.Static) + .GetRequiredMethod(nameof(OrderByDynamic_Private), BindingFlags.NonPublic | BindingFlags.Static) .MakeGenericMethod(typeof(T), property.PropertyType) - .Invoke(null, new object[] { items, propertyName, direction }); - - return (IOrderedEnumerable)result; + .Invoke>(null, [items, propertyName, direction]) + .NotNull(); + + return result; } + [UsedImplicitly] private static IOrderedEnumerable OrderByDynamic_Private(IEnumerable items, string propertyName, string direction) { var parameter = Expression.Parameter(typeof(T), "x"); @@ -64,50 +66,5 @@ private static IOrderedEnumerable OrderByDynamic_Private(IEnumerable throw new Exception("Invalid Sort Direction"); } - - /// - /// Like by properties and filter value - /// - /// A sequence of values to order - /// Names of properties - /// Filter value - /// - public static IEnumerable LikeDynamic(this IEnumerable items, string filterValue, string[] propertyNames = null) - { - var result = typeof(LinqExtensions) - .GetMethod("LikeDynamic_Private", BindingFlags.NonPublic | BindingFlags.Static) - .MakeGenericMethod(typeof(T)) - .Invoke(null, new object[] { items, filterValue, propertyNames }); - - return (IEnumerable)result; - } - - private static IEnumerable LikeDynamic_Private(IEnumerable items, string filterValue, string[] propertyNames = null) - { - Expression> fullExpression = null; - - if (!propertyNames?.Any() ?? true) - { - propertyNames = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.CanRead).Select(x => x.Name).ToArray(); - } - - foreach (var propertyName in propertyNames) - { - // Necessarily create a new string variable to use in different expressions - var tPropName = propertyName; - Expression> expr = x => - (x.GetType().InvokeMember( - tPropName, - BindingFlags.GetProperty, - null, - x, null) - ?? string.Empty).ToString().IndexOf(filterValue, StringComparison.InvariantCultureIgnoreCase) >= 0; - - // add expressions to OR condition - fullExpression = fullExpression == null ? expr : fullExpression.Or(expr); - } - - return fullExpression == null ? items : items.Where(fullExpression.Compile()); - } } } diff --git a/shesha-core/src/Shesha.Framework/Extensions/ObjectExtensions.cs b/shesha-core/src/Shesha.Framework/Extensions/ObjectExtensions.cs index 466f827ff9..11081ae226 100644 --- a/shesha-core/src/Shesha.Framework/Extensions/ObjectExtensions.cs +++ b/shesha-core/src/Shesha.Framework/Extensions/ObjectExtensions.cs @@ -1,18 +1,20 @@ -using System; -using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using Abp.Domain.Entities; -using NetTopologySuite.Geometries; +using NetTopologySuite.Geometries; using Shesha.Domain; using Shesha.Domain.Attributes; using Shesha.Reflection; +using System; namespace Shesha.Extensions { public static class ObjectExtensions { - public static string GetClassName(this object obj) + public static bool NullEquals(this object? first, object? second) + { + return first == null && second == null || (first?.Equals(second) ?? false); + } + + public static string? GetClassName(this object obj) { return obj == null ? null : obj.GetType().FullName; } @@ -32,7 +34,7 @@ public static bool IsEntity(this object obj) /// public static bool IsSystemType(this Type type) { - return type.FullName.StartsWith("System."); + return !string.IsNullOrWhiteSpace(type.FullName) && type.FullName.StartsWith("System."); } /// @@ -51,9 +53,9 @@ public static bool IsNotAnyEntityAndSystemType(this Type type) /// /// Indicates is the specified type a type of entity /// - public static bool IsEntityType(this Type type) + public static bool IsEntityType(this Type? type) { - return MappingHelper.IsEntity(type); + return type != null && MappingHelper.IsEntity(type); } /// @@ -108,16 +110,14 @@ public static bool IsDictionary(this object obj) public static bool IsReferenceListType(this Type type) { return type != null && type.IsPublic && type.IsEnum && type.HasAttribute(); - } - + } + /// /// Get type of the `Id` property. Applicable for entity types /// - /// - /// - public static Type GetEntityIdType(this Type type) - { - return type?.GetProperty(SheshaDatabaseConsts.IdColumn)?.PropertyType; - } + public static Type GetEntityIdType(this Type type) + { + return type.GetRequiredProperty(SheshaDatabaseConsts.IdColumn).PropertyType; + } } } diff --git a/shesha-core/src/Shesha.Framework/Extensions/StoredFileExtensions.cs b/shesha-core/src/Shesha.Framework/Extensions/StoredFileExtensions.cs index e4c643e1e9..2d3b799bc7 100644 --- a/shesha-core/src/Shesha.Framework/Extensions/StoredFileExtensions.cs +++ b/shesha-core/src/Shesha.Framework/Extensions/StoredFileExtensions.cs @@ -26,7 +26,7 @@ public static string GetContentType(this string fileName) /// /// /// - public static string GetFileUrl(this StoredFile storedFile) + public static string? GetFileUrl(this StoredFile storedFile) { return GetUrl("Download", "StoredFile", new { Id = storedFile.Id }); } @@ -36,15 +36,20 @@ public static string GetFileUrl(this StoredFile storedFile) /// /// /// - public static string GetFileVersionUrl(this StoredFileVersion storedFileVersion) + public static string? GetFileVersionUrl(this StoredFileVersion storedFileVersion) { return GetUrl("Download", "StoredFile", new { Id = storedFileVersion.File.Id, versionNo = storedFileVersion.VersionNo }); } - private static string GetUrl(string action = default, string controller = default, object values = default) + private static string? GetUrl(string action, string controller, object values) { var linkGeneratorContext = StaticContext.IocManager.Resolve(); var linkGenerator = StaticContext.IocManager.Resolve(); + if (linkGeneratorContext.State == null) + return string.Empty; + + if (string.IsNullOrWhiteSpace(linkGeneratorContext.State.Scheme) || string.IsNullOrWhiteSpace(linkGeneratorContext.State.Host)) + return string.Empty; var port = linkGeneratorContext.State.Port; @@ -60,7 +65,7 @@ private static string GetUrl(string action = default, string controller = defaul ); } - private static string GetPath(string action = default, string controller = default, object values = default) + private static string? GetPath(string action, string controller, object values) { var linkGenerator = StaticContext.IocManager.Resolve(); @@ -72,7 +77,7 @@ private static string GetPath(string action = default, string controller = defau /// /// /// - public static string GetFilePath(this StoredFile storedFile) + public static string? GetFilePath(this StoredFile storedFile) { return GetPath("Download", "StoredFile", new { Id = storedFile.Id }); } @@ -82,7 +87,7 @@ public static string GetFilePath(this StoredFile storedFile) /// /// /// - public static string GetFileVersionPath(this StoredFileVersion storedFileVersion) + public static string? GetFileVersionPath(this StoredFileVersion storedFileVersion) { return GetPath("Download", "StoredFile", new { Id = storedFileVersion.File.Id, versionNo = storedFileVersion.VersionNo }); } diff --git a/shesha-core/src/Shesha.Framework/Extensions/TypeExtensions.cs b/shesha-core/src/Shesha.Framework/Extensions/TypeExtensions.cs index 3278a202ef..a27205637b 100644 --- a/shesha-core/src/Shesha.Framework/Extensions/TypeExtensions.cs +++ b/shesha-core/src/Shesha.Framework/Extensions/TypeExtensions.cs @@ -1,21 +1,12 @@ -using System; +using Shesha.Reflection; +using System; +using System.Reflection; namespace Shesha.Extensions { public static class TypeExtensions { - public static object GetTypeDefaultValue(this Type type) - { - if (type == null) - throw new ArgumentNullException("type"); - - return - type.IsValueType - ? Activator.CreateInstance(type) //value type - : null; //reference type - } - - public static Type FindBaseGenericType(this Type type, Type baseGenericType) + public static Type? FindBaseGenericType(this Type type, Type baseGenericType) { var btype = type; while (btype != null && (!btype.IsGenericType || btype.GetGenericTypeDefinition() != baseGenericType)) @@ -26,5 +17,37 @@ public static Type FindBaseGenericType(this Type type, Type baseGenericType) return btype; } + + /// + /// Return of the specified . Throws exception if it's null or empty + /// + /// + /// + public static string GetRequiredFullName(this Type type) + { + return !string.IsNullOrWhiteSpace(type.FullName) + ? type.FullName + : throw new Exception($"{nameof(type.FullName)} is empty for type '{type.Name}' in namespace '{type.Namespace}'"); + } + + /// + /// Return of the specified . Throws exception if it's null or empty + /// + public static string GetRequiredFullName(this Assembly assembly) + { + return !string.IsNullOrWhiteSpace(assembly.FullName) + ? assembly.FullName + : throw new Exception($"{nameof(assembly.FullName)} is empty"); + } + + /// + /// Return assembly fullname of the specified + /// + /// + /// + public static string GetAssemblyFullName(this Type type) + { + return type.Assembly.FullName.NotNull($"Assembly full name is null for type '{type.FullName}'"); + } } } diff --git a/shesha-core/src/Shesha.Framework/Extensions/UnitOfWorkExtensions.cs b/shesha-core/src/Shesha.Framework/Extensions/UnitOfWorkExtensions.cs new file mode 100644 index 0000000000..176c309566 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Extensions/UnitOfWorkExtensions.cs @@ -0,0 +1,53 @@ +using Abp.Domain.Uow; +using Shesha.Exceptions; +using System; +using System.Threading.Tasks; +using System.Transactions; + +namespace Shesha.Extensions +{ + /// + /// Unit of work extensions + /// + public static class UnitOfWorkExtensions + { + /// + /// Perform action in a new unit of work + /// + /// + /// + /// + /// + /// + public static async Task WithUnitOfWorkAsync(this IUnitOfWorkManager manager, Func> action, TransactionScopeOption scope) + { + using (var uow = manager.Begin(scope)) + { + var result = await action.Invoke(); + await uow.CompleteAsync(); + return result; + } + } + + /// + /// Get current unit of work. Throws exception if unit of work is unavailable + /// + /// + /// + /// + public static IActiveUnitOfWork GetCurrent(this IUnitOfWorkManager manager) + { + return manager.Current ?? throw new UnitOfWorkUnavailableException(); + } + + /// + /// Get current unit of work or null if unavailable + /// + /// + /// + public static IActiveUnitOfWork? GetCurrentOrNull(this IUnitOfWorkManager manager) + { + return manager.Current; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/FrontEndAppKeyConsts.cs b/shesha-core/src/Shesha.Framework/FrontEndAppKeyConsts.cs index 9bc500616d..6501a348ba 100644 --- a/shesha-core/src/Shesha.Framework/FrontEndAppKeyConsts.cs +++ b/shesha-core/src/Shesha.Framework/FrontEndAppKeyConsts.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha +namespace Shesha { public class FrontEndAppKeyConsts { diff --git a/shesha-core/src/Shesha.Framework/Generators/SnakeCaseNameGenerator.cs b/shesha-core/src/Shesha.Framework/Generators/SnakeCaseNameGenerator.cs index fb152b537b..8592cf6574 100644 --- a/shesha-core/src/Shesha.Framework/Generators/SnakeCaseNameGenerator.cs +++ b/shesha-core/src/Shesha.Framework/Generators/SnakeCaseNameGenerator.cs @@ -1,13 +1,10 @@ using Abp.Dependency; -using Abp.Extensions; using Shesha.Domain; +using Shesha.Reflection; using Shesha.Utilities; using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Generators { @@ -29,16 +26,15 @@ public virtual (string tableName, string parentTableName, string childTableName, } public virtual (string tableName, string parentTableName, string childTableName, string parentColumnName, string childColumnName) - GetManyToManyTableNames(MemberInfo propertyInfo, string prefix = "", string suffix = "") + GetManyToManyTableNames(MemberInfo propertyInfo, string? prefix = null, string? suffix = null) { var (parentType, parentIdType, childType, childIdType) = MappingHelper.GetManyToManyTableData(propertyInfo); var tablePrefix = (prefix ?? GetTablePrefix(parentType) ?? "").TrimEnd('_'); var parentTypeName = parentType.Name; var propertyName = propertyInfo.Name; - var suffixLength = suffix.IsNullOrEmpty() ? 0 : suffix.Length + 1; + var suffixLength = string.IsNullOrWhiteSpace(suffix) ? 0 : suffix.Length + 1; - //$"{(tablePrefix.IsNullOrEmpty() ? "" : $"{tablePrefix}_").ToSnakeCase()}{parentTypeName.ToSnakeCase()}_{property.Name.ToSnakeCase()}{(suffix.IsNullOrEmpty() ? "" : $"_{suffix}").ToSnakeCase()}"; var tableName = GetSnakeCaseText(new[] {tablePrefix, parentTypeName, propertyName}, MaxLenght - suffixLength) + (suffixLength == 0 ? "" : $"_{suffix}").ToSnakeCase(); var childTypeName = childType.Name.ToSnakeCase(); @@ -64,9 +60,9 @@ private string GetSnakeCaseText(string[] parts, int? lenght = null) case GeneratorMaxLengthActionEnum.TrimRight: return res.Substring(0, maxLenght); case GeneratorMaxLengthActionEnum.TrimParts: - return parts.SnakeCaseTrim(maxLenght); + return parts.SnakeCaseTrim(maxLenght).NotNullOrWhiteSpace(); case GeneratorMaxLengthActionEnum.TrimWords: - return res.SnakeCaseTrim(maxLenght); + return res.SnakeCaseTrim(maxLenght).NotNullOrWhiteSpace(); } } return res; diff --git a/shesha-core/src/Shesha.Framework/Json/JsonStringConverter.cs b/shesha-core/src/Shesha.Framework/Json/JsonStringConverter.cs index fb191e300d..5f4fdb0786 100644 --- a/shesha-core/src/Shesha.Framework/Json/JsonStringConverter.cs +++ b/shesha-core/src/Shesha.Framework/Json/JsonStringConverter.cs @@ -5,7 +5,7 @@ namespace Shesha.Json { public class JsonStringConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { var jsonContent = (value as JsonString)?.GetJson(); @@ -20,7 +20,7 @@ public override bool CanConvert(Type objectType) public override bool CanRead => false; - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } diff --git a/shesha-core/src/Shesha.Framework/JsonEntities/Converters/AnyJsonEntityConverter.cs b/shesha-core/src/Shesha.Framework/JsonEntities/Converters/AnyJsonEntityConverter.cs deleted file mode 100644 index 25d213b00f..0000000000 --- a/shesha-core/src/Shesha.Framework/JsonEntities/Converters/AnyJsonEntityConverter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Abp.Dependency; -using Abp.Json; -using Abp.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; -using Shesha.DynamicEntities.Dtos; -using Shesha.Services; -using Shesha.Utilities; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Xml; -using System.Xml.Linq; - -namespace Shesha.JsonEntities.Converters -{ - // ToDo AS: Don't use. Is not completed! - [Obsolete("Don't use. Is not completed!")] - public class AnyJsonEntityConverter : JsonConverter - { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteRawValue(((IJsonEntity)value).GetJson()); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var obj = JObject.Load(reader); - - var _className = obj.ContainsKey(nameof(IJsonEntity._className).ToCamelCase()) - ? obj.GetValue(nameof(IJsonEntity._className).ToCamelCase()).ToString() - : null; - - if (_className != null) - { - var jType = StaticContext.IocManager.Resolve().Find(t => t.FullName == _className).FirstOrDefault(); - return serializer.Deserialize(obj.CreateReader(), jType); - } - - return null; - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(IJsonEntity); - } - } - -} diff --git a/shesha-core/src/Shesha.Framework/JsonEntities/Converters/JsonEntityConverter.cs b/shesha-core/src/Shesha.Framework/JsonEntities/Converters/JsonEntityConverter.cs index a7d13d9212..a391233072 100644 --- a/shesha-core/src/Shesha.Framework/JsonEntities/Converters/JsonEntityConverter.cs +++ b/shesha-core/src/Shesha.Framework/JsonEntities/Converters/JsonEntityConverter.cs @@ -1,10 +1,9 @@ -using Abp.Json; -using Abp.Reflection; +using Abp.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Shesha.DynamicEntities; -using Shesha.DynamicEntities.Dtos; using Shesha.JsonEntities.Proxy; +using Shesha.Reflection; using Shesha.Services; using Shesha.Utilities; using System; @@ -15,13 +14,14 @@ namespace Shesha.JsonEntities.Converters { public class JsonEntityConverter : JsonConverter { - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var jObj = JObject.Load(reader); - var _className = jObj.ContainsKey(nameof(IJsonEntity._className).ToCamelCase()) - ? jObj.GetValue(nameof(IJsonEntity._className).ToCamelCase()).ToString() + var classNameProp = nameof(IJsonEntity._className).ToCamelCase().NotNull(); + var _className = jObj.ContainsKey(classNameProp) + ? jObj.GetValue(classNameProp)?.ToString() : null; var classNameObjectType = _className != null @@ -38,7 +38,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist return proxyFactory.GetNewProxiedJsonEntity(classNameObjectType, jObj); } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { writer.Formatting = Formatting.None; if (value is IJsonEntityProxy proxy) diff --git a/shesha-core/src/Shesha.Framework/JsonEntities/Converters/JsonEntityReferenceConverter.cs b/shesha-core/src/Shesha.Framework/JsonEntities/Converters/JsonEntityReferenceConverter.cs deleted file mode 100644 index b73889082c..0000000000 --- a/shesha-core/src/Shesha.Framework/JsonEntities/Converters/JsonEntityReferenceConverter.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Abp.Domain.Entities; -using Abp.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Shesha.Domain; -using Shesha.Extensions; -using Shesha.Services; -using System; -using JsonSerializer = Newtonsoft.Json.JsonSerializer; - -namespace Shesha.JsonEntities.Converters -{ - // ToDo AS: Don't use. Is not completed! - [Obsolete("Don't use. Is not completed!")] - public class JsonEntityReferenceConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType.IsEntityType(); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var jObj = JObject.Load(reader); - var id = jObj?.GetValue("id"); - if (id != null) - return StaticContext.IocManager.Resolve().Get(objectType, id.ToString()); - return null; - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value is IEntity entity) - { - var entityType = entity.GetType(); - var id = entityType.GetProperty("Id")?.GetValue(entity); - if (id != null) - { - var _displayNamePropertyInfo = entityType.GetEntityConfiguration()?.DisplayNamePropertyInfo; - var dysplayName = _displayNamePropertyInfo != null ? _displayNamePropertyInfo.GetValue(entity).ToString() : entity.ToString(); - serializer.Serialize(writer, new { id, _dysplayName = dysplayName }); - } - } - } - } - -} diff --git a/shesha-core/src/Shesha.Framework/JsonEntities/JsonEntity.cs b/shesha-core/src/Shesha.Framework/JsonEntities/JsonEntity.cs index 7c31d4e163..ceca1e19e9 100644 --- a/shesha-core/src/Shesha.Framework/JsonEntities/JsonEntity.cs +++ b/shesha-core/src/Shesha.Framework/JsonEntities/JsonEntity.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Shesha.Extensions; using Shesha.JsonEntities.Converters; namespace Shesha.JsonEntities @@ -12,7 +13,7 @@ public virtual string _className { get { - return __className ??= this.GetType().FullName; + return __className ??= this.GetType().GetRequiredFullName(); } } diff --git a/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonEntityInterceptor.cs b/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonEntityInterceptor.cs index b5491316d2..10d73679b8 100644 --- a/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonEntityInterceptor.cs +++ b/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonEntityInterceptor.cs @@ -54,7 +54,8 @@ public void Intercept(IInvocation invocation) { var propName = invocation.Method.Name.Substring(4, invocation.Method.Name.Length - 4); var eref = proxy._getEntityReference(propName.ToCamelCase()); - if (eref?.Id != null) + var stringId = eref?.Id?.ToString(); + if (!string.IsNullOrWhiteSpace(stringId)) { var objType = invocation.InvocationTarget.GetType(); var property = objType.GetProperty(propName); @@ -63,7 +64,7 @@ public void Intercept(IInvocation invocation) var value = property.GetValue(invocation.InvocationTarget); if (value == null) { - value = _dynamicRepository.Get(property.PropertyType, eref?.Id.ToString()); + value = _dynamicRepository.Get(property.PropertyType, stringId); property.SetValue(invocation.InvocationTarget, value); } invocation.ReturnValue = value; diff --git a/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonEntityProxy.cs b/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonEntityProxy.cs index 4417e49925..e55f25a148 100644 --- a/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonEntityProxy.cs +++ b/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonEntityProxy.cs @@ -1,12 +1,11 @@ -using AutoMapper.Internal; +using Abp.Threading; +using AutoMapper.Internal; using Castle.DynamicProxy; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Nito.AsyncEx.Synchronous; using Shesha.DynamicEntities; using Shesha.DynamicEntities.Binder; using Shesha.Services; -using StackExchange.Redis; using System; using System.Collections.Generic; using System.Linq; @@ -31,9 +30,9 @@ public interface IJsonEntityProxy [JsonIgnore] bool? _innerChanged { get; set; } - JsonReference _getEntityReference(Expression> expr); + JsonReference? _getEntityReference(Expression> expr); - JsonReference _getEntityReference(string propName); + JsonReference? _getEntityReference(string propName); void _initialize(object proxy); void _changed(); @@ -52,7 +51,7 @@ public static object GetUnproxiedInstance(object pobj) return ProxyUtil.IsProxy(pobj) ? ProxyUtil.GetUnproxiedInstance(pobj) : pobj; } - public static JObject GetJson(object proxyObj, JObject jObj = null) + public static JObject GetJson(object proxyObj, JObject? jObj = null) { JObject json = jObj ?? new JObject(); if (proxyObj == null) return json; @@ -70,7 +69,7 @@ public static JObject GetJson(object proxyObj, JObject jObj = null) return json; } - public static bool IsChanged(object proxyObj) + public static bool IsChanged(object? proxyObj) { if (proxyObj == null) return false; @@ -102,10 +101,10 @@ public static bool IsChanged(object proxyObj) public virtual bool _isInitialized { get; internal set; } = false; [JsonIgnore] - public virtual JObject _json { get; internal set; } = null; + public virtual JObject _json { get; internal set; } [JsonIgnore] - public virtual Dictionary _references { get; } = new Dictionary(); + public virtual Dictionary _references { get; } = new (); [JsonIgnore] public virtual bool _isThisChanged { get; internal set; } = false; @@ -121,7 +120,7 @@ public JsonEntityProxy(JObject json, IJsonEntityProxyFactory factory) _factory = factory; } - public virtual JsonReference _getEntityReference(Expression> expr) + public virtual JsonReference? _getEntityReference(Expression> expr) { if (expr is LambdaExpression lambda) if (lambda.Body is MemberExpression member) @@ -129,20 +128,18 @@ public virtual JsonReference _getEntityReference(Expression> throw new NotSupportedException("Expression is incorrect, please use only properties"); } - public virtual JsonReference _getEntityReference(string propName) + public virtual JsonReference? _getEntityReference(string propName) { return _references.ContainsKey(propName) ? _references[propName] : null; } public void _initialize(object proxy) { - _isInitialized = true; - var context = new EntityModelBindingContext() { GetEntityById = (entityType, id, displayName, propPath, ctx) => { - _references.Add(propPath.Split(".").LastOrDefault(), new JsonReference() { Id = id, _displayName = displayName }); + _references.Add(propPath.Split(".").Last(), new JsonReference() { Id = id, _displayName = displayName }); return null; }, GetObjectOrObjectReference = (objectType, jobject, ctx, formFields) => @@ -150,7 +147,10 @@ public void _initialize(object proxy) return _factory.GetNewProxiedJsonEntity(objectType, jobject); } }; - StaticContext.IocManager.Resolve().BindPropertiesAsync(_json, proxy, context).WaitAndUnwrapException(); + var modelBinder = StaticContext.IocManager.Resolve(); + AsyncHelper.RunSync(async () => await modelBinder.BindPropertiesAsync(_json, proxy, context)); + + _isInitialized = true; } public void _changed() @@ -167,4 +167,4 @@ public void _reset(JObject json, bool isInitialized = true) var obj = JsonEntityProxy.GetUnproxiedInstance(this); } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonReference.cs b/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonReference.cs index dff0e8b1ab..54d236057f 100644 --- a/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonReference.cs +++ b/shesha-core/src/Shesha.Framework/JsonEntities/Proxy/JsonReference.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.JsonEntities.Proxy +namespace Shesha.JsonEntities.Proxy { public interface IJsonReference { @@ -15,7 +8,7 @@ public interface IJsonReference public class JsonReference { - public virtual object Id { get; set; } - public virtual string _displayName { get; set; } + public virtual object? Id { get; set; } + public virtual string? _displayName { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/JsonLogic/ExpressionExtensions.cs b/shesha-core/src/Shesha.Framework/JsonLogic/ExpressionExtensions.cs index e701c0841e..d6e2d1af3f 100644 --- a/shesha-core/src/Shesha.Framework/JsonLogic/ExpressionExtensions.cs +++ b/shesha-core/src/Shesha.Framework/JsonLogic/ExpressionExtensions.cs @@ -1,9 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using Abp; +using System; +using System.Globalization; using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; +using System.Threading; namespace Shesha.JsonLogic { @@ -66,5 +65,24 @@ public static Expression ReplaceParameter(this Expression expression, ParameterE { return new ExpressionParameterReplaceVisitor(parameter, newParameter).Visit(expression); } + + public static string? ToInvariantString(this Expression expression) + { + using (UsingCulture(CultureInfo.InvariantCulture)) + { + return expression.ToString(); + } + } + + private static IDisposable UsingCulture(CultureInfo culture) + { + var prevCulture = Thread.CurrentThread.CurrentCulture; + + Thread.CurrentThread.CurrentCulture = culture; + + return new DisposeAction(() => { + Thread.CurrentThread.CurrentCulture = prevCulture; + }); + } } } diff --git a/shesha-core/src/Shesha.Framework/JsonLogic/IJsonLogic2LinqConverter.cs b/shesha-core/src/Shesha.Framework/JsonLogic/IJsonLogic2LinqConverter.cs index e2fcc119cc..b145d5137b 100644 --- a/shesha-core/src/Shesha.Framework/JsonLogic/IJsonLogic2LinqConverter.cs +++ b/shesha-core/src/Shesha.Framework/JsonLogic/IJsonLogic2LinqConverter.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json.Linq; using System; using System.Linq.Expressions; -using System.Text.Json; namespace Shesha.JsonLogic { @@ -16,7 +15,7 @@ public interface IJsonLogic2LinqConverter /// /// /// - Expression> ParseExpressionOf(JObject rule); + Expression>? ParseExpressionOf(JObject rule); /// /// Parse JsonLogic expression @@ -24,7 +23,7 @@ public interface IJsonLogic2LinqConverter /// /// /// - Expression> ParseExpressionOf(string rule); + Expression>? ParseExpressionOf(string rule); /// /// Parse predicate @@ -32,7 +31,7 @@ public interface IJsonLogic2LinqConverter /// /// /// - Func ParsePredicateOf(JObject rule); + Func? ParsePredicateOf(JObject rule); /// /// Parse predicate @@ -40,7 +39,7 @@ public interface IJsonLogic2LinqConverter /// /// /// - Func ParsePredicateOf(string rule); + Func? ParsePredicateOf(string rule); /// /// Evaluate JsonLogic predicate for the specified diff --git a/shesha-core/src/Shesha.Framework/JsonLogic/JsonExtensions.cs b/shesha-core/src/Shesha.Framework/JsonLogic/JsonExtensions.cs index 5bcafb89f4..cbaf3c72a2 100644 --- a/shesha-core/src/Shesha.Framework/JsonLogic/JsonExtensions.cs +++ b/shesha-core/src/Shesha.Framework/JsonLogic/JsonExtensions.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json.Linq; using System; +using System.Diagnostics.CodeAnalysis; namespace Shesha.JsonLogic { @@ -13,7 +14,7 @@ public static class JsonExtensions /// /// /// - public static bool IsNullOrEmpty(this JToken token) + public static bool IsNullOrEmpty([NotNullWhen(false)]this JToken? token) { return (token == null) || (token.Type == JTokenType.Array && !token.HasValues) || @@ -27,7 +28,7 @@ public static bool IsNullOrEmpty(this JToken token) /// /// /// - public static bool IsNull(this JToken token) + public static bool IsNull([NotNullWhen(true)] this JToken? token) { return (token == null) || (token.Type == JTokenType.Array && !token.HasValues) || diff --git a/shesha-core/src/Shesha.Framework/JsonLogic/JsonLogic2LinqConverter.cs b/shesha-core/src/Shesha.Framework/JsonLogic/JsonLogic2LinqConverter.cs index 8be2926ece..d30d2ba378 100644 --- a/shesha-core/src/Shesha.Framework/JsonLogic/JsonLogic2LinqConverter.cs +++ b/shesha-core/src/Shesha.Framework/JsonLogic/JsonLogic2LinqConverter.cs @@ -1,9 +1,7 @@ using Abp.Dependency; using Abp.Domain.Entities; using Abp.Timing; -using Castle.Core; using Newtonsoft.Json.Linq; -using NUglify; using Shesha.EntityReferences; using Shesha.Extensions; using Shesha.JsonLogic.Exceptions; @@ -13,12 +11,10 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; namespace Shesha.JsonLogic { @@ -50,36 +46,48 @@ public JsonLogic2LinqConverter(ISpecificationsFinder specificationsFinder, ISpec private delegate Expression Binder(Expression left, Expression right); + private Expression Reduce(Expression? acc, Expression right, Binder binder) + { + return acc == null + ? right + : binder(acc, right); + } + private Expression CombineExpressions(JToken[] tokens, Binder binder, ParameterExpression param) { - Expression acc = null; - - Expression bind(Expression acc, Expression right) => acc == null ? right : binder(acc, right); + if (!tokens.Any()) + throw new ArgumentException($"tokens list must not be empty", nameof(tokens)); + Expression? acc = null; foreach (var argument in tokens) { var parsedArgument = ParseTree(argument, param); - acc = bind(acc, parsedArgument); + acc = Reduce(acc, parsedArgument, binder); } - return acc; + return acc ?? throw new Exception("Failed to combine linq expressions"); } private Expression CombineExpressions(Expression[] expressions, Binder binder, ParameterExpression param) { - Expression acc = null; - - Expression bind(Expression acc, Expression right) => acc == null ? right : binder(acc, right); + if (!expressions.Any()) + throw new ArgumentException($"expressions list must not be empty", nameof(expressions)); + Expression? acc = null; foreach (var expression in expressions) { - acc = bind(acc, expression); + acc = Reduce(acc, expression, binder); } - return acc; + return acc ?? throw new Exception("Failed to combine linq expressions"); + } + + private Expression ParseTree(JToken rule, ParameterExpression param) + { + return ParseTreeOrNull(rule, param) ?? throw new JsonLogicParsingFailedException("Expression parsing returns null", rule); } - private Expression ParseTree( + private Expression? ParseTreeOrNull( JToken rule, ParameterExpression param) { @@ -123,7 +131,7 @@ private Expression ParseTree( return Compare(param, @operator.Arguments, pair => pair, pair => { - Expression expr = null; + Expression? expr = null; #region datetime // try to compare var and datetime const (normal order) @@ -193,7 +201,7 @@ out expr return Compare(param, @operator.Arguments, pair => pair, pair => { - Expression expr = null; + Expression? expr = null; #region datetime // try to compare var and datetime const (normal order) @@ -263,7 +271,7 @@ out expr return Compare(param, @operator.Arguments, pair => pair, pair => { - Expression expr = null; + Expression? expr = null; #region datetime // try to compare var and datetime const (normal order) @@ -333,7 +341,7 @@ out expr return Compare(param, @operator.Arguments, pair => pair, pair => { - Expression expr = null; + Expression? expr = null; #region datetime // try to compare var and datetime const (normal order) @@ -437,6 +445,8 @@ out expr throw new Exception($"{JsOperators.Var} operator must contain exactly one argument"); var name = GetStringValue(@operator.Arguments.First()); + if (string.IsNullOrWhiteSpace(name)) + throw new JsonLogicParsingFailedException($"Name is mandatory for '{JsOperators.Var}' operator", rule); return ExpressionExtensions.GetMemberExpression(param, name); } @@ -466,7 +476,7 @@ out expr return CombineExpressions(arrExpressions, Expression.OrElse, param); } else { - var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }); + var containsMethod = typeof(string).GetRequiredMethod(nameof(string.Contains), [typeof(string)]); var arg1 = ParseTree(@operator.Arguments[0], param); var arg2 = ParseTree(@operator.Arguments[1], param); @@ -481,7 +491,7 @@ out expr case JsOperators.EndsWith: { - var endsWithMethod = typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }); + var endsWithMethod = typeof(string).GetRequiredMethod(nameof(string.EndsWith), [typeof(string)]); if (@operator.Arguments.Count() != 2) throw new Exception($"{JsOperators.EndsWith} operator require two arguments"); @@ -499,7 +509,7 @@ out expr { // note: now it supports only strings // todo: add check - var startsWithMethod = typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }); + var startsWithMethod = typeof(string).GetRequiredMethod(nameof(string.StartsWith), [typeof(string)]); if (@operator.Arguments.Count() != 2) throw new Exception($"{JsOperators.StartsWith} operator require two arguments"); @@ -588,7 +598,7 @@ out expr { case "day": { - var addMethod = typeof(DateTime).GetMethod(nameof(DateTime.Add), new Type[] { typeof(TimeSpan) }); + var addMethod = typeof(DateTime).GetRequiredMethod(nameof(DateTime.Add), [ typeof(TimeSpan) ]); var timeSpan = TimeSpan.FromDays(number.Value); return Expression.Call( date, @@ -598,7 +608,7 @@ out expr } case "week": { - var addMethod = typeof(DateTime).GetMethod(nameof(DateTime.Add), new Type[] { typeof(TimeSpan) }); + var addMethod = typeof(DateTime).GetRequiredMethod(nameof(DateTime.Add), [ typeof(TimeSpan) ]); var timeSpan = TimeSpan.FromDays(number.Value * 7); return Expression.Call( date, @@ -608,7 +618,7 @@ out expr } case "month": { - var addMonthsMethod = typeof(DateTime).GetMethod(nameof(DateTime.AddMonths), new Type[] { typeof(int) }); + var addMonthsMethod = typeof(DateTime).GetRequiredMethod(nameof(DateTime.AddMonths), [ typeof(int) ]); return Expression.Call( date, addMonthsMethod, @@ -617,7 +627,7 @@ out expr } case "year": { - var addYearsMethod = typeof(DateTime).GetMethod(nameof(DateTime.AddYears), new Type[] { typeof(int) }); + var addYearsMethod = typeof(DateTime).GetRequiredMethod(nameof(DateTime.AddYears), [ typeof(int) ]); return Expression.Call( date, addYearsMethod, @@ -634,7 +644,7 @@ out expr throw new Exception($"{JsOperators.Upper} operator require 1 argument"); var arg = ParseTree(@operator.Arguments[0], param); - var toUpperMethod = typeof(string).GetMethod(nameof(string.ToUpper), new Type[] { }); + var toUpperMethod = typeof(string).GetRequiredMethod(nameof(string.ToUpper), []); return Expression.Call(arg, toUpperMethod); } case JsOperators.Lower: @@ -643,7 +653,7 @@ out expr throw new Exception($"{JsOperators.Lower} operator require 1 argument"); var arg = ParseTree(@operator.Arguments[0], param); - var toLowerMethod = typeof(string).GetMethod(nameof(string.ToLower), new Type[] { }); + var toLowerMethod = typeof(string).GetRequiredMethod(nameof(string.ToLower), []); return Expression.Call(arg, toLowerMethod); } default: @@ -654,11 +664,11 @@ out expr return null; } - private string GetAsString(JToken token) + private string? GetAsString(JToken token) { if (token is JValue value) { - return value.Value.ToString(); + return value.Value?.ToString(); } else throw new NotSupportedException(); @@ -676,13 +686,13 @@ private string GetAsString(JToken token) throw new NotSupportedException(); } - private bool TryCompareMemberAndDateTime(Expression left, Expression right, Func dateConverter, Binder binder, out Expression expression) + private bool TryCompareMemberAndDateTime(Expression left, Expression right, Func dateConverter, Binder binder, [NotNullWhen(true)]out Expression? expression) { expression = null; if (IsDateTimeMember(left) && right is ConstantExpression constExpr && constExpr.Type == typeof(DateTime)) { - var dateExpr = Expression.Constant(dateConverter.Invoke((DateTime)constExpr.Value)); + var dateExpr = Expression.Constant(dateConverter.Invoke(EnsureDateTime(constExpr.Value))); expression = SafeNullable(left, dateExpr, binder); return true; } @@ -690,13 +700,13 @@ private bool TryCompareMemberAndDateTime(Expression left, Expression right, Func return false; } - private bool TryCompareMemberAndDate(Expression left, Expression right, Func dateConverter, Binder binder, out Expression expression) + private bool TryCompareMemberAndDate(Expression left, Expression right, Func dateConverter, Binder binder, [NotNullWhen(true)] out Expression? expression) { expression = null; if (IsDateMember(left) && right is ConstantExpression constExpr && constExpr.Type == typeof(DateTime)) { - var dateExpr = Expression.Constant(dateConverter.Invoke((DateTime)constExpr.Value)); + var dateExpr = Expression.Constant(dateConverter.Invoke(EnsureDateTime(constExpr.Value))); expression = SafeNullable(left, dateExpr, binder); return true; } @@ -704,7 +714,7 @@ private bool TryCompareMemberAndDate(Expression left, Expression right, Func timeConverter, Binder binder, out Expression expression) + private bool TryCompareMemberAndTime(Expression left, Expression right, Func timeConverter, Binder binder, [NotNullWhen(true)] out Expression? expression) { expression = null; if (IsTimeMember(left) && right.NodeType == ExpressionType.Constant) @@ -789,7 +799,7 @@ private bool IsDateMember(Expression expression) if (!(expression is MemberExpression memberExpr) || expression.Type.GetUnderlyingTypeIfNullable() != typeof(DateTime)) return false; - var dataTypeAttribute = memberExpr.Member.GetAttribute(); + var dataTypeAttribute = memberExpr.Member.GetAttributeOrNull(); return dataTypeAttribute?.DataType == DataType.Date; } @@ -797,7 +807,7 @@ private Expression ConvertDateTimeConst(Expression expression, Func(); + var dataTypeAttribute = memberExpr.Member.GetAttributeOrNull(); return dataTypeAttribute == null || dataTypeAttribute?.DataType == DataType.DateTime; } @@ -836,7 +846,7 @@ private void ConvertGuids(Expression a, ref Expression b) { if (a.Type.GetUnderlyingTypeIfNullable() == typeof(Guid) && b.Type == typeof(string)) { - var toGuidMethod = typeof(StringHelper).GetMethod(nameof(StringHelper.ToGuid), new Type[] { typeof(string) }); + var toGuidMethod = typeof(StringHelper).GetRequiredMethod(nameof(StringHelper.ToGuid), [typeof(string)]); b = Expression.Call( null, @@ -852,7 +862,7 @@ private void ConvertNumericConsts(Expression memberExpressionToCompare, ref Expr if (memberExpr.Type.GetUnderlyingTypeIfNullable() == typeof(int)) { - if (constExpr.Type == typeof(Int64)) + if (constExpr.Type == typeof(Int64) && constExpr.Value != null) { var constValue = (Int64)constExpr.Value; if (constValue <= int.MaxValue) @@ -861,10 +871,10 @@ private void ConvertNumericConsts(Expression memberExpressionToCompare, ref Expr throw new OverflowException($"Constant value must be not grester than {int.MaxValue} (max int size) to compare with {memberExpr.Member.Name}, currtent value is {constValue}"); } else - if (constExpr.Type == typeof(string) && int.TryParse((string)constExpr.Value, out var intValue)) + if (constExpr.Type == typeof(string) && int.TryParse((string?)constExpr.Value, out var intValue)) numericConstToConvert = Expression.Constant(intValue); } - if (memberExpr.Type.GetUnderlyingTypeIfNullable() == typeof(Int64)) + if (memberExpr.Type.GetUnderlyingTypeIfNullable() == typeof(Int64) && constExpr.Value != null) { if (constExpr.Type == typeof(int)) numericConstToConvert = Expression.Constant((Int64)constExpr.Value); @@ -875,7 +885,7 @@ private void ConvertNumericConsts(Expression memberExpressionToCompare, ref Expr { if (constExpr.Type == typeof(string)) { - if (double.TryParse((string)constExpr.Value, out var doubleValue)) + if (double.TryParse((string?)constExpr.Value, out var doubleValue)) numericConstToConvert = Expression.Constant(doubleValue); } else numericConstToConvert = Expression.Constant(Convert.ToDouble(constExpr.Value)); @@ -884,7 +894,7 @@ private void ConvertNumericConsts(Expression memberExpressionToCompare, ref Expr { if (constExpr.Type == typeof(string)) { - if (decimal.TryParse((string)constExpr.Value, out var decimalValue)) + if (decimal.TryParse((string?)constExpr.Value, out var decimalValue)) numericConstToConvert = Expression.Constant(decimalValue); } else @@ -894,7 +904,7 @@ private void ConvertNumericConsts(Expression memberExpressionToCompare, ref Expr { if (constExpr.Type == typeof(string)) { - if (Single.TryParse((string)constExpr.Value, out var singleValue)) + if (Single.TryParse((string?)constExpr.Value, out var singleValue)) numericConstToConvert = Expression.Constant(singleValue); } else @@ -904,7 +914,7 @@ private void ConvertNumericConsts(Expression memberExpressionToCompare, ref Expr { if (constExpr.Type == typeof(string)) { - if (byte.TryParse((string)constExpr.Value, out var byteValue)) + if (byte.TryParse((string?)constExpr.Value, out var byteValue)) numericConstToConvert = Expression.Constant(byteValue); } else @@ -916,7 +926,7 @@ private void ConvertTicksTimeSpan(Expression a, ref Expression b) { if (a.Type == typeof(TimeSpan) && b.Type == typeof(Int64)) { - var fromSecondsMethod = typeof(TimeSpan).GetMethod(nameof(TimeSpan.FromSeconds), new Type[] { typeof(double) }); + var fromSecondsMethod = typeof(TimeSpan).GetRequiredMethod(nameof(TimeSpan.FromSeconds), new Type[] { typeof(double) }); b = Expression.Call( null, @@ -1020,7 +1030,7 @@ private IEnumerable SplitArgumentsIntoPair(Expression[] argument return pairs; } - private string GetStringValue(JToken arg) + private string? GetStringValue(JToken arg) { if (arg == null) return null; @@ -1036,7 +1046,7 @@ private string GetStringValue(JToken arg) /// /// /// - public OperationProps GetOperation(JToken rule) + public OperationProps? GetOperation(JToken rule) { if (rule is JObject ruleObj) { @@ -1044,7 +1054,7 @@ public OperationProps GetOperation(JToken rule) var operationName = p.Name; var operationArguments = (p.Value is JArray jArrayArgs) ? jArrayArgs.ToArray() - : new JToken[] { p.Value }; + : [p.Value]; return new OperationProps { Name = operationName, @@ -1060,7 +1070,7 @@ public bool IsOperator(JToken rule) return TryGetOperator(rule, out var _); } - public bool TryGetOperator(JToken rule, out OperationProps @operator) + public bool TryGetOperator(JToken rule, [NotNullWhen(true)] out OperationProps? @operator) { if (!(rule is JObject ruleObj) || ruleObj.Properties().Count() != 1) { @@ -1072,7 +1082,7 @@ public bool TryGetOperator(JToken rule, out OperationProps @operator) var operationName = p.Name; var operationArguments = (p.Value is JArray jArrayArgs) ? jArrayArgs.ToArray() - : new JToken[] { p.Value }; + : [p.Value]; @operator = new OperationProps { Name = operationName, @@ -1082,7 +1092,7 @@ public bool TryGetOperator(JToken rule, out OperationProps @operator) } /// inheritedDoc - public Expression> ParseExpressionOf(string rule) + public Expression>? ParseExpressionOf(string rule) { if (string.IsNullOrWhiteSpace(rule)) return null; @@ -1094,7 +1104,7 @@ public Expression> ParseExpressionOf(string rule) } /// inheritedDoc - public Expression> ParseExpressionOf(JObject rule) + public Expression>? ParseExpressionOf(JObject rule) { if (rule.IsNullOrEmpty()) return null; @@ -1110,7 +1120,7 @@ public Expression> ParseExpressionOf(JObject rule) return query; } - public Func ParsePredicateOf(string rule) + public Func? ParsePredicateOf(string rule) { if (string.IsNullOrWhiteSpace(rule)) return null; @@ -1121,35 +1131,65 @@ public Func ParsePredicateOf(string rule) return ParsePredicateOf(jsonLogic); } - public Func ParsePredicateOf(JObject rule) + public Func? ParsePredicateOf(JObject rule) { if (rule.IsNullOrEmpty()) return null; - var query = ParseExpressionOf(rule); + var query = ParseExpressionOf(rule).NotNull(); return query.Compile(); } private Expression TrimStringMember(MemberExpression member) { - var trimMethod = typeof(string).GetMethod(nameof(string.Trim), new Type[] { }); + var trimMethod = typeof(string).GetRequiredMethod(nameof(string.Trim), []); return Expression.Call(member, trimMethod); } public bool EvaluatePredicateInternal(T model, string predicate) { - var expression = ParsePredicateOf(predicate); + var expression = ParsePredicateOf(predicate).NotNull(); return expression.Invoke(model); } public bool EvaluatePredicate(object model, string predicate) { - var method = this.GetType().GetMethod(nameof(EvaluatePredicateInternal)); + var method = this.GetType().GetRequiredMethod(nameof(EvaluatePredicateInternal)); var modelType = model.GetType(); var genericMethod = method.MakeGenericMethod(modelType); - return (bool)genericMethod.Invoke(this, new object[] { model, predicate }); + + var result = genericMethod.Invoke(this, [model, predicate]); + return EnsureBool(result); } + + #region Type guards + + private DateTime EnsureDateTime(object? value) + { + if (value == null || value.GetType().GetUnderlyingTypeIfNullable() != typeof(DateTime)) + throw new ArgumentException($"Value must be of type {typeof(DateTime).FullName}"); + + return (DateTime)value; + } + + private TimeSpan EnsureTimeSpan(object? value) + { + if (value == null || value.GetType().GetUnderlyingTypeIfNullable() != typeof(TimeSpan)) + throw new ArgumentException($"Value must be of type {typeof(TimeSpan).FullName}"); + + return (TimeSpan)value; + } + + private bool EnsureBool(object? value) + { + if (value == null || value.GetType().GetUnderlyingTypeIfNullable() != typeof(bool)) + throw new ArgumentException($"Value must be of type {typeof(bool).FullName}"); + + return (bool)value; + } + + #endregion } public static class JsOperators diff --git a/shesha-core/src/Shesha.Framework/Locks/RedisLockFactory.cs b/shesha-core/src/Shesha.Framework/Locks/RedisLockFactory.cs index 68e714ac22..b0a58258c7 100644 --- a/shesha-core/src/Shesha.Framework/Locks/RedisLockFactory.cs +++ b/shesha-core/src/Shesha.Framework/Locks/RedisLockFactory.cs @@ -68,16 +68,11 @@ public bool DoExclusive(string resource, TimeSpan expiryTime, TimeSpan waitTime, /// /// Dispose lock factory /// - public void Dispose() + public virtual void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } + _redLockFactory.Dispose(); - protected virtual void Dispose(bool disposing) - { - // Cleanup - _redLockFactory?.Dispose(); - } + GC.SuppressFinalize(this); + } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Metadata/ApiModelProvider.cs b/shesha-core/src/Shesha.Framework/Metadata/ApiModelProvider.cs index ad33b99c5a..1de7d559f3 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/ApiModelProvider.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/ApiModelProvider.cs @@ -13,11 +13,11 @@ namespace Shesha.Metadata { - public class ApiModelProvider : BaseModelProvider, ITransientDependency + public class ApiModelProvider : BaseModelProvider, ISingletonDependency { private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider; - public ApiModelProvider(ICacheManager cacheManager, IApiDescriptionGroupCollectionProvider apiDescriptionsProvider) : base(cacheManager) + public ApiModelProvider(ICacheManager cacheManager, IApiDescriptionGroupCollectionProvider apiDescriptionsProvider) : base("ApiModelProviderCache", cacheManager) { _apiDescriptionsProvider = apiDescriptionsProvider; } @@ -46,9 +46,8 @@ protected override Task> FetchModelsAsync() return types; }) .ToList(); - Func, List> getTypes = null; - getTypes = (IEnumerable types) => + List getTypes(IEnumerable types) { // Paremeters types var parameterTypes = types @@ -88,7 +87,8 @@ protected override Task> FetchModelsAsync() t != typeof(string) && t != typeof(object) && !(t.Namespace ?? string.Empty).StartsWith("Abp")) - .Select(t => t.GetElementType()); + .Select(t => t.GetElementType()) + .WhereNotNull(); if (arrayTypes?.Count() > 0) { @@ -105,7 +105,7 @@ protected override Task> FetchModelsAsync() .OrderBy(x => x.Name) .Select(p => new ModelDto { - ClassName = p.FullName, + ClassName = p.GetRequiredFullName(), Type = p, Description = ReflectionHelper.GetDescription(p), Alias = null @@ -116,9 +116,9 @@ protected override Task> FetchModelsAsync() private class ParameterTypeComparer : IEqualityComparer { - bool IEqualityComparer.Equals(Type x, Type y) + bool IEqualityComparer.Equals(Type? x, Type? y) { - return x.FullName == y.FullName; + return x != null && y != null && x.FullName == y.FullName || x == null && y == null; } int IEqualityComparer.GetHashCode(Type obj) diff --git a/shesha-core/src/Shesha.Framework/Metadata/AttributeModelProvider.cs b/shesha-core/src/Shesha.Framework/Metadata/AttributeModelProvider.cs index c72f82671a..aea2304a91 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/AttributeModelProvider.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/AttributeModelProvider.cs @@ -2,23 +2,20 @@ using Abp.Reflection; using Abp.Runtime.Caching; using Shesha.Attributes; -using Shesha.Domain; using Shesha.Extensions; -using Shesha.JsonEntities; using Shesha.Metadata.Dtos; using Shesha.Reflection; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading.Tasks; namespace Shesha.Metadata { - public class AttributeModelProvider : BaseModelProvider, ITransientDependency + public class AttributeModelProvider : BaseModelProvider, ISingletonDependency { private readonly IAssemblyFinder _assembleFinder; - public AttributeModelProvider(ICacheManager cacheManager, IAssemblyFinder assembleFinder) : base(cacheManager) + public AttributeModelProvider(ICacheManager cacheManager, IAssemblyFinder assembleFinder) : base("AttributeModelProviderCache", cacheManager) { _assembleFinder = assembleFinder; } @@ -29,14 +26,14 @@ protected override Task> FetchModelsAsync() .Distinct(new AssemblyFullNameComparer()) .SelectMany(a => a.GetTypes()) .Where(t => - t.GetAttribute() != null && + t.GetAttributeOrNull() != null && // skip entity types, they shouldn't be returned by the application service at all !t.IsEntityType() ).ToList(); var dtos = types.Select(p => new ModelDto { - ClassName = p.FullName, + ClassName = p.GetRequiredFullName(), Type = p, Description = ReflectionHelper.GetDescription(p), Alias = null diff --git a/shesha-core/src/Shesha.Framework/Metadata/BaseModelProvider.cs b/shesha-core/src/Shesha.Framework/Metadata/BaseModelProvider.cs index 4daf679f69..4a68d56708 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/BaseModelProvider.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/BaseModelProvider.cs @@ -1,4 +1,5 @@ using Abp.Runtime.Caching; +using Shesha.Cache; using Shesha.Metadata.Dtos; using System; using System.Collections.Generic; @@ -10,40 +11,19 @@ namespace Shesha.Metadata /// /// Base model provider /// - public abstract class BaseModelProvider : IModelProvider where TModel : ModelDto + public abstract class BaseModelProvider : CacheHolder>, IModelProvider where TModel : ModelDto { protected const string MainListCacheKey = ""; - private readonly ICacheManager _cacheManager; - - private ITypedCache> _modelsCache; - - /// - /// Cache of the ReferenceListItems - /// - protected ITypedCache> ModelsCache - { - get - { - if (_modelsCache == null) - { - var cache = _cacheManager.GetCache>($"{this.GetType().Name}ModelsCache"); - cache.DefaultSlidingExpireTime = TimeSpan.FromHours(24); - _modelsCache = cache; - } - return _modelsCache; - } - } - - public BaseModelProvider(ICacheManager cacheManager) - { - _cacheManager = cacheManager; - } - + protected BaseModelProvider(string cacheName, ICacheManager cacheManager) : base(cacheName, cacheManager) + { + Cache.DefaultSlidingExpireTime = TimeSpan.FromHours(24); + } + /// inheritedDoc public async Task> GetModelsAsync() { - return await ModelsCache.GetAsync(MainListCacheKey, async (key) => + return await Cache.GetAsync(MainListCacheKey, async (key) => { var result = await FetchModelsAsync(); @@ -54,7 +34,7 @@ public async Task> GetModelsAsync() protected abstract Task> FetchModelsAsync(); /// inheritedDoc - public async Task GetModelTypeAsync(string nameOrAlias) + public async Task GetModelTypeAsync(string nameOrAlias) { var models = await GetModelsAsync(); return models.FirstOrDefault(m => m.Alias == nameOrAlias || m.ClassName == nameOrAlias)?.Type; @@ -62,7 +42,7 @@ public async Task GetModelTypeAsync(string nameOrAlias) public async Task ClearCacheAsync() { - await ModelsCache.ClearAsync(); + await Cache.ClearAsync(); } async Task> IModelProvider.GetModelsAsync() @@ -74,7 +54,7 @@ async Task> IModelProvider.GetModelsAsync() public abstract class BaseModelProvider : BaseModelProvider { - protected BaseModelProvider(ICacheManager cacheManager) : base(cacheManager) + protected BaseModelProvider(string cacheName, ICacheManager cacheManager) : base(cacheName, cacheManager) { } } diff --git a/shesha-core/src/Shesha.Framework/Metadata/DataTypeInfo.cs b/shesha-core/src/Shesha.Framework/Metadata/DataTypeInfo.cs index efb08d1677..95b4916888 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/DataTypeInfo.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/DataTypeInfo.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Shesha.Metadata +namespace Shesha.Metadata { /// /// Data type and format info @@ -17,24 +13,24 @@ public class DataTypeInfo /// /// Data format /// - public string DataFormat { get; set; } + public string? DataFormat { get; set; } /// /// Object type /// - public string ObjectType { get; set; } + public string? ObjectType { get; set; } public DataTypeInfo(string dataType) { DataType = dataType; } - public DataTypeInfo(string dataType, string dataFormat): this(dataType) + public DataTypeInfo(string dataType, string? dataFormat): this(dataType) { DataFormat = dataFormat; } - public DataTypeInfo(string dataType, string dataFormat, string objectType) : this(dataType, dataFormat) + public DataTypeInfo(string dataType, string? dataFormat, string? objectType) : this(dataType, dataFormat) { ObjectType = objectType; } diff --git a/shesha-core/src/Shesha.Framework/Metadata/Dtos/ApiEndpointDto.cs b/shesha-core/src/Shesha.Framework/Metadata/Dtos/ApiEndpointDto.cs index ac2ce7ea8d..a33a965a28 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/Dtos/ApiEndpointDto.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/Dtos/ApiEndpointDto.cs @@ -8,11 +8,11 @@ public class ApiEndpointDto /// /// Http verb (get/post/put etc) /// - public string HttpVerb { get; set; } + public required string HttpVerb { get; init; } /// /// Url /// - public string Url { get; set; } + public required string Url { get; init; } } } diff --git a/shesha-core/src/Shesha.Framework/Metadata/Dtos/EntityModelDto.cs b/shesha-core/src/Shesha.Framework/Metadata/Dtos/EntityModelDto.cs index b72031f32a..061d8f3ad3 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/Dtos/EntityModelDto.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/Dtos/EntityModelDto.cs @@ -4,9 +4,9 @@ namespace Shesha.Metadata.Dtos { public class EntityModelDto: ModelDto { - public string Accessor { get; set; } - public string ModuleAccessor { get; set; } - public string Md5 { get; set; } + public string? Accessor { get; set; } + public string? ModuleAccessor { get; set; } + public string? Md5 { get; set; } public DateTime? ModificationTime { get; set; } public MetadataDto Metadata { get; set; } diff --git a/shesha-core/src/Shesha.Framework/Metadata/Dtos/MetadataDto.cs b/shesha-core/src/Shesha.Framework/Metadata/Dtos/MetadataDto.cs index 0f43aedd80..599c41d543 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/Dtos/MetadataDto.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/Dtos/MetadataDto.cs @@ -18,7 +18,7 @@ public class MetadataDto /// /// Module name current model belongs to /// - public string Module { get; set; } + public string? Module { get; set; } /// /// Type accessor @@ -28,7 +28,7 @@ public class MetadataDto /// /// Module accessor /// - public string ModuleAccessor { get; set; } + public string? ModuleAccessor { get; set; } /// /// Propeties @@ -51,7 +51,7 @@ public class MetadataDto [JsonProperty("md5")] [JsonPropertyName("md5")] - public string Md5 { get; set; } + public string? Md5 { get; set; } public DateTime? ChangeTime { get; set; } /// diff --git a/shesha-core/src/Shesha.Framework/Metadata/Dtos/MethodMetadataDto.cs b/shesha-core/src/Shesha.Framework/Metadata/Dtos/MethodMetadataDto.cs index 9939fdb223..9e6ab7081f 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/Dtos/MethodMetadataDto.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/Dtos/MethodMetadataDto.cs @@ -1,6 +1,4 @@ -using StackExchange.Redis; -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Shesha.Metadata.Dtos { @@ -10,9 +8,9 @@ namespace Shesha.Metadata.Dtos public class MethodMetadataDto { public string Name { get; set; } - public string Description { get; set; } + public string? Description { get; set; } public List Arguments { get; set; } - public DataTypeInfo ReturnType { get; set; } + public DataTypeInfo? ReturnType { get; set; } public bool IsAsync { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Metadata/Dtos/ModelDto.cs b/shesha-core/src/Shesha.Framework/Metadata/Dtos/ModelDto.cs index 54c16c6359..ce2480fc6f 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/Dtos/ModelDto.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/Dtos/ModelDto.cs @@ -1,5 +1,4 @@ -using Shesha.Domain.Enums; -using System; +using System; namespace Shesha.Metadata.Dtos { @@ -9,9 +8,9 @@ namespace Shesha.Metadata.Dtos public class ModelDto { public string ClassName { get; set; } - public string Alias { get; set; } - public string Description { get; set; } - public Type Type { get; set; } + public string? Alias { get; set; } + public string? Description { get; set; } + public Type? Type { get; set; } public bool Suppress { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Metadata/Dtos/PropertyMetadataDto.cs b/shesha-core/src/Shesha.Framework/Metadata/Dtos/PropertyMetadataDto.cs index 93ad048414..ef414acf95 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/Dtos/PropertyMetadataDto.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/Dtos/PropertyMetadataDto.cs @@ -34,43 +34,43 @@ public class PropertyMetadataDto /// /// Validation RegularExpression /// - public string RegExp { get; set; } + public string? RegExp { get; set; } /// /// Validation message /// - public virtual string ValidationMessage { get; set; } + public virtual string? ValidationMessage { get; set; } public string Path { get; set; } - public string Label { get; set; } - public string Description { get; set; } + public string? Label { get; set; } + public string? Description { get; set; } public string DataType { get; set; } - public string DataFormat { get; set; } + public string? DataFormat { get; set; } /// /// Type of the entity. Applicable when DataType = /// [JsonProperty("entityType")] [JsonPropertyName("entityType")] - public string EntityType { get; set; } + public string? EntityType { get; set; } /// /// Module the entity belongs to. Applicable when DataType = /// - public string EntityModule { get; set; } + public string? EntityModule { get; set; } public string TypeAccessor { get; set; } public string ModuleAccessor { get; set; } [Newtonsoft.Json.JsonIgnore] [System.Text.Json.Serialization.JsonIgnore] - public Type EnumType { get; set; } + public Type? EnumType { get; set; } - public string ReferenceListName { get; set; } - public string ReferenceListModule { get; set; } + public string? ReferenceListName { get; set; } + public string? ReferenceListModule { get; set; } public int OrderIndex { get; set; } - public string GroupName { get; set; } + public string? GroupName { get; set; } /// /// If true, indicates that current property is a framework-related (e.g. , ) @@ -95,7 +95,7 @@ public class PropertyMetadataDto /// /// Items type (applicable for arrays) /// - public PropertyMetadataDto ItemsType { get; set; } + public PropertyMetadataDto? ItemsType { get; set; } public MetadataSourceType Source { get; set; } @@ -108,8 +108,8 @@ public static string GetPropertiesMD5(List dtos) { var propertyProps = typeof(PropertyMetadataDto).GetProperties().OrderBy(p => p.Name).ToList(); - Action, List> expr = null; - expr = (List l, List props) => + + void expr(List l, List props) { foreach (var prop in props) { @@ -140,4 +140,4 @@ public static string GetPropertiesMD5(List dtos) return sb.ToString().ToMd5Fingerprint(); } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Metadata/Dtos/SpecificationDto.cs b/shesha-core/src/Shesha.Framework/Metadata/Dtos/SpecificationDto.cs index 1def5c6869..07a57397ea 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/Dtos/SpecificationDto.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/Dtos/SpecificationDto.cs @@ -18,6 +18,6 @@ public class SpecificationDto /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Metadata/EntityModelProvider.cs b/shesha-core/src/Shesha.Framework/Metadata/EntityModelProvider.cs index b5495d19cc..cc51c786b6 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/EntityModelProvider.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/EntityModelProvider.cs @@ -1,9 +1,7 @@ using Abp.Dependency; using Abp.Domain.Repositories; -using Abp.Domain.Uow; using Abp.Events.Bus.Entities; using Abp.Events.Bus.Handlers; -using Abp.Reflection; using Abp.Runtime.Caching; using Shesha.Configuration.Runtime; using Shesha.Domain; @@ -11,7 +9,6 @@ using Shesha.Extensions; using Shesha.Metadata.Dtos; using Shesha.Reflection; -using Shesha.Utilities; using System; using System.Collections.Generic; using System.Linq; @@ -19,45 +16,36 @@ namespace Shesha.Metadata { - public class EntityModelProvider : BaseModelProvider, IEntityModelProvider, ITransientDependency, - IEventHandler>, - IEventHandler> + public class EntityModelProvider : BaseModelProvider, IEntityModelProvider, ISingletonDependency, + IAsyncEventHandler>, + IAsyncEventHandler> { - private readonly ITypeFinder _typeFinder; private readonly IRepository _entityConfigRepository; private readonly IEntityConfigurationStore _entityConfigurationStore; - private readonly IUnitOfWorkManager _uowManager; private readonly IMetadataProvider _metadataProvider; public EntityModelProvider( ICacheManager cacheManager, IEntityConfigurationStore entityConfigurationStore, - ITypeFinder typeFinder, IRepository entityConfigRepository, - IUnitOfWorkManager uowManager, IMetadataProvider metadataProvider - ) : base(cacheManager) + ) : base("EntityModelProviderCache", cacheManager) { - _typeFinder = typeFinder; _entityConfigurationStore = entityConfigurationStore; _entityConfigRepository = entityConfigRepository; - _uowManager = uowManager; _metadataProvider = metadataProvider; } - public void HandleEvent(EntityChangedEventData eventData) - { - AsyncHelper.RunSync(() => { return ClearCacheAsync(); }); - } - - public void HandleEvent(EntityChangingEventData eventData) - { - if (_entityConfigRepository.GetAll().Any(x => x.Id == eventData.Entity.Id)) - { - AsyncHelper.RunSync(() => { return ClearCacheAsync(); }); - }; - } - + public async Task HandleEventAsync(EntityChangedEventData eventData) + { + await Cache.ClearAsync(); + } + + public async Task HandleEventAsync(EntityChangingEventData eventData) + { + await Cache.ClearAsync(); + } + protected async override Task> FetchModelsAsync() { var entityConfigs = await _entityConfigRepository.GetAll().ToListAsync(); @@ -70,13 +58,13 @@ protected async override Task> FetchModelsAsync() && (config == null || config.EntityType.FullName != t.FullClassName /*skip aliases*/)) return null; - var metadata = await _metadataProvider.GetAsync(config.EntityType, t.FullClassName); + var metadata = await _metadataProvider.GetAsync(config?.EntityType, t.FullClassName); return new EntityModelDto { Suppress = t.Suppress, ClassName = t.FullClassName, Type = config?.EntityType, - Description = t.Description ?? (config?.EntityType != null ? ReflectionHelper.GetDescription(config?.EntityType) : ""), + Description = t.Description ?? (config != null && config.EntityType != null ? ReflectionHelper.GetDescription(config.EntityType) : ""), Alias = string.IsNullOrWhiteSpace(t.TypeShortAlias) ? config?.SafeTypeShortAlias : t.TypeShortAlias, Accessor = t.Accessor, ModuleAccessor = t.Module?.Accessor, @@ -85,10 +73,10 @@ protected async override Task> FetchModelsAsync() Metadata = metadata, }; })) - .Where(t => t != null) + .WhereNotNull() .ToList(); return dtos; } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Metadata/HardcodeMetadataProvider.cs b/shesha-core/src/Shesha.Framework/Metadata/HardcodeMetadataProvider.cs index 6a9f44836c..cba583f516 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/HardcodeMetadataProvider.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/HardcodeMetadataProvider.cs @@ -1,5 +1,4 @@ -using Abp.Collections.Extensions; -using Abp.Dependency; +using Abp.Dependency; using Abp.Domain.Entities; using Abp.Domain.Entities.Auditing; using JetBrains.Annotations; @@ -38,7 +37,7 @@ IEntityConfigurationStore entityConfigurationStore } /// inheritedDoc - public List GetProperties(Type containerType, MetadataContext context = null) + public List GetProperties(Type containerType, MetadataContext? context = null) { context ??= new MetadataContext(containerType); @@ -63,7 +62,7 @@ public List GetProperties(Type containerType, MetadataConte private double? GetMin(PropertyInfo property) { - var value = property.GetAttribute()?.Minimum; + var value = property.GetAttributeOrNull()?.Minimum; return value != null ? Convert.ToDouble(value) : null; @@ -71,7 +70,7 @@ public List GetProperties(Type containerType, MetadataConte private double? GetMax(PropertyInfo property) { - var value = property.GetAttribute()?.Maximum; + var value = property.GetAttributeOrNull()?.Maximum; return value != null ? Convert.ToDouble(value) : null; @@ -94,7 +93,7 @@ public PropertyMetadataDto GetPropertyMetadata(PropertyInfo property, MetadataCo var epc = entityConfig?[property.Name]; var dataType = GetDataType(property); - var cascadeAttribute = property.GetAttribute() + var cascadeAttribute = property.GetAttributeOrNull() ?? property.PropertyType.GetCustomAttribute(); var result = new PropertyMetadataDto @@ -102,22 +101,22 @@ public PropertyMetadataDto GetPropertyMetadata(PropertyInfo property, MetadataCo Path = path, Label = ReflectionHelper.GetDisplayName(property), Description = ReflectionHelper.GetDescription(property), - IsVisible = property.GetAttribute()?.Browsable ?? true, + IsVisible = property.GetAttributeOrNull()?.Browsable ?? true, Required = property.HasAttribute(), Readonly = !property.CanWrite || property.HasAttribute() - || (property.GetAttribute()?.IsReadOnly ?? false), + || (property.GetAttributeOrNull()?.IsReadOnly ?? false), Min = GetMin(property), Max = GetMax(property), - MinLength = property.GetAttribute()?.MinimumLength - ?? property.GetAttribute()?.Length, - MaxLength = property.GetAttribute()?.MaximumLength - ?? property.GetAttribute()?.Length, + MinLength = property.GetAttributeOrNull()?.MinimumLength + ?? property.GetAttributeOrNull()?.Length, + MaxLength = property.GetAttributeOrNull()?.MaximumLength + ?? property.GetAttributeOrNull()?.Length, Audited = property.IsAuditedProperty(), - RegExp = property.GetAttribute()?.Pattern, - ValidationMessage = property.GetAttribute()?.ErrorMessage - ?? property.GetAttribute()?.ErrorMessage - ?? property.GetAttribute()?.ErrorMessage, + RegExp = property.GetAttributeOrNull()?.Pattern, + ValidationMessage = property.GetAttributeOrNull()?.ErrorMessage + ?? property.GetAttributeOrNull()?.ErrorMessage + ?? property.GetAttributeOrNull()?.ErrorMessage, CascadeCreate = cascadeAttribute?.CanCreate, CascadeUpdate = cascadeAttribute?.CanUpdate, CascadeDeleteUnreferenced = cascadeAttribute?.DeleteUnreferenced, @@ -127,7 +126,7 @@ public PropertyMetadataDto GetPropertyMetadata(PropertyInfo property, MetadataCo ReferenceListModule = epc?.ReferenceListModule, ReferenceListName = epc?.ReferenceListName, EnumType = epc?.EnumType, - OrderIndex = property.GetAttribute()?.GetOrder() ?? -1, + OrderIndex = property.GetAttributeOrNull()?.GetOrder() ?? -1, IsFrameworkRelated = IsFrameworkRelatedProperty(property), IsNullable = property.IsNullable(), //ConfigurableByUser = property.GetAttribute()?.Bindable ?? true, @@ -139,9 +138,13 @@ public PropertyMetadataDto GetPropertyMetadata(PropertyInfo property, MetadataCo if (dataType.DataType == DataTypes.Array) { - result.ItemsType = GetItemsType(property, context); - result.EntityType = result.DataFormat == DataTypes.EntityReference ? result.ItemsType.EntityType : null; - result.EntityModule = result.DataFormat == DataTypes.EntityReference ? result.ItemsType.EntityModule : null; + result.ItemsType = GetItemsType(property, context); + + var entityType = result.DataFormat == DataTypes.EntityReference + ? result.ItemsType + : null; + result.EntityType = entityType?.EntityType; + result.EntityModule = entityType?.EntityModule; } else if (!context.ProcessedTypes.Contains(property.PropertyType) && property.PropertyType.IsNotAnyEntityAndSystemType()) @@ -181,7 +184,7 @@ private void FillEntityRelatedProperties(PropertyMetadataDto propertyDto, Type p } } - private PropertyMetadataDto GetItemsType(PropertyInfo property, MetadataContext context) + private PropertyMetadataDto? GetItemsType(PropertyInfo property, MetadataContext context) { if (property.IsMultiValueReferenceListProperty()) { @@ -267,18 +270,18 @@ private bool IsInterfaceProperty(PropertyInfo property, Type @interface, string return false; return @interface.IsGenericType - ? property.DeclaringType.GetInterfaces().Any(x => + ? property.DeclaringType.NotNull().GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == @interface) - : property.DeclaringType.GetInterfaces().Contains(@interface); + : property.DeclaringType.NotNull().GetInterfaces().Contains(@interface); } - private string GetStringFormat([CanBeNull]MemberInfo propInfo) + private string? GetStringFormat([CanBeNull]MemberInfo? propInfo) { if (propInfo == null) return null; - var dataTypeAtt = propInfo.GetAttribute(); + var dataTypeAtt = propInfo.GetAttributeOrNull(); switch (dataTypeAtt?.DataType) { @@ -311,12 +314,12 @@ public DataTypeInfo GetDataType(PropertyInfo propInfo) return GetDataTypeByPropertyType(propType, propInfo) ?? throw new NotSupportedException($"Data type not supported: {propType.FullName}"); } - public DataTypeInfo GetDataTypeByPropertyType(Type propType, [CanBeNull] MemberInfo propInfo) + public DataTypeInfo? GetDataTypeByPropertyType(Type propType, [CanBeNull] MemberInfo? propInfo) { if (propType == typeof(Guid)) return new DataTypeInfo(DataTypes.Guid); - var dataTypeAtt = propInfo?.GetAttribute(); + var dataTypeAtt = propInfo?.GetAttributeOrNull(); // for enums - use underlaying type if (propType.IsEnum) @@ -367,18 +370,10 @@ public DataTypeInfo GetDataTypeByPropertyType(Type propType, [CanBeNull] MemberI if (IsList(propType)) { - var paramType = GetListElementType(propType); + var paramType = GetListElementType(propType).NotNull(); var elementDataType = GetDataTypeByPropertyType(paramType, null); - /* - var format = paramType.IsClass - ? paramType.IsEntityType() - ? ArrayFormats.EntityReference - : paramType.IsJsonEntityType() - ? ArrayFormats.ObjectReference - : GetDataTypeByPropertyType(paramType, null)?.DataType - : null; - */ + return new DataTypeInfo(DataTypes.Array, elementDataType?.DataType, elementDataType?.DataFormat); } else if (propType.IsClass) @@ -399,7 +394,7 @@ public static bool IsList(Type type) type.GetInterface(nameof(IEnumerable)) != null; } - public static Type GetListElementType(Type type) + public static Type? GetListElementType(Type type) { if (!IsList(type)) return null; diff --git a/shesha-core/src/Shesha.Framework/Metadata/IHardcodeMetadataProvider.cs b/shesha-core/src/Shesha.Framework/Metadata/IHardcodeMetadataProvider.cs index e60db0d1ff..12a1733826 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/IHardcodeMetadataProvider.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/IHardcodeMetadataProvider.cs @@ -1,11 +1,8 @@ using JetBrains.Annotations; -using Shesha.Configuration.Runtime.Exceptions; using Shesha.Metadata.Dtos; using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Threading.Tasks; namespace Shesha.Metadata { @@ -28,7 +25,7 @@ public interface IHardcodeMetadataProvider /// Type /// Metadata context /// - List GetProperties(Type type, MetadataContext context = null); + List GetProperties(Type type, MetadataContext? context = null); /// /// Get data type of the specified property @@ -42,7 +39,7 @@ public interface IHardcodeMetadataProvider /// /// /// - DataTypeInfo GetDataTypeByPropertyType(Type propType, [CanBeNull] MemberInfo propInfo); + DataTypeInfo? GetDataTypeByPropertyType(Type propType, [CanBeNull] MemberInfo? propInfo); /// /// Returns true if the property is a framework related one diff --git a/shesha-core/src/Shesha.Framework/Metadata/IMetadataProvider.cs b/shesha-core/src/Shesha.Framework/Metadata/IMetadataProvider.cs index d1cd8acbfa..6fdf65b7e0 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/IMetadataProvider.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/IMetadataProvider.cs @@ -10,7 +10,7 @@ namespace Shesha.Metadata /// public interface IMetadataProvider { - Task GetAsync(Type containerType, string containerName); + Task GetAsync(Type? containerType, string containerName); Task> GetApiEndpointsAsync(Type containerType); diff --git a/shesha-core/src/Shesha.Framework/Metadata/IModelProvider.cs b/shesha-core/src/Shesha.Framework/Metadata/IModelProvider.cs index 72242be8ba..38965b949c 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/IModelProvider.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/IModelProvider.cs @@ -20,7 +20,7 @@ public interface IModelProvider /// Get model type by name or alias /// /// - Task GetModelTypeAsync(string nameOrAlias); + Task GetModelTypeAsync(string nameOrAlias); /// /// Clear models cache diff --git a/shesha-core/src/Shesha.Framework/Metadata/MetadataContext.cs b/shesha-core/src/Shesha.Framework/Metadata/MetadataContext.cs index 738175266b..71e922df26 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/MetadataContext.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/MetadataContext.cs @@ -1,19 +1,22 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Metadata { public class MetadataContext { + public Type? MainType { get; private set; } public List ProcessedTypes { get; set; } - public MetadataContext(Type baseType) + public MetadataContext(Type mainType): this() { - ProcessedTypes = new List(); - ProcessedTypes.Add(baseType); + MainType = mainType; + ProcessedTypes.Add(mainType); + } + + public MetadataContext() + { + ProcessedTypes = new(); } } } diff --git a/shesha-core/src/Shesha.Framework/Metadata/MetadataProvider.cs b/shesha-core/src/Shesha.Framework/Metadata/MetadataProvider.cs index 3a628c762b..44d9a0959a 100644 --- a/shesha-core/src/Shesha.Framework/Metadata/MetadataProvider.cs +++ b/shesha-core/src/Shesha.Framework/Metadata/MetadataProvider.cs @@ -1,12 +1,10 @@ using Abp.Dependency; using Abp.ObjectMapping; using Abp.Reflection; -using Abp.Runtime.Validation; using Abp.Threading; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.VisualBasic; using Shesha.Attributes; using Shesha.Configuration.Runtime; using Shesha.DynamicEntities; @@ -30,7 +28,7 @@ namespace Shesha.Metadata public class MetadataProvider: IMetadataProvider, ITransientDependency { private readonly IEntityConfigurationStore _entityConfigurationStore; - private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; + private readonly IActionDescriptorCollectionProvider? _actionDescriptorCollectionProvider; private readonly ISpecificationsFinder _specificationsFinder; private readonly IModelConfigurationManager _modelConfigurationProvider; private readonly IHardcodeMetadataProvider _hardcodeMetadataProvider; @@ -60,31 +58,34 @@ IAssemblyFinder assemblyFinder } // ToDo: support Dynamic entities - public async Task GetAsync(Type containerType, string containerName) + public async Task GetAsync(Type? containerType, string containerName = "") { - var isEntity = containerType.IsEntityType(); - var isJsonEntity = containerType.IsJsonEntityType(); - - var moduleInfo = containerType.GetConfigurableModuleInfo(); + var isEntity = containerType != null && containerType.IsEntityType(); + var isJsonEntity = containerType != null && containerType.IsJsonEntityType(); var (changeTime, properties) = await GetPropertiesInternalAsync(containerType, containerName); var dto = new MetadataDto { - DataType = isEntity ? DataTypes.EntityReference : DataTypes.Object,// todo: check other types + DataType = isEntity + ? DataTypes.EntityReference + : isJsonEntity + ? DataTypes.ObjectReference + : DataTypes.Object,// todo: check other types Properties = properties, Specifications = await GetSpecificationsAsync(containerType), Methods = await GetMethodsAsync(containerType), ApiEndpoints = await GetApiEndpointsAsync(containerType), - ClassName = containerType.FullName, - + ClassName = containerType?.GetRequiredFullName() ?? containerName, ChangeTime = changeTime, }; UpdateMd5(dto); - if (isEntity || isJsonEntity) + if (containerType != null && (isEntity || isJsonEntity)) { + var moduleInfo = containerType.GetConfigurableModuleInfo(); + dto.TypeAccessor = containerType.GetTypeAccessor(); dto.Module = moduleInfo?.Name; dto.ModuleAccessor = moduleInfo?.GetModuleAccessor(); @@ -100,8 +101,11 @@ public async Task GetAsync(Type containerType, string containerName return dto; } - private async Task> GetMethodsAsync(Type containerType) + private async Task> GetMethodsAsync(Type? containerType) { + if (containerType == null) + return new List(); + var assemblies = _assemblyFinder.GetAllAssemblies(); var methods = ReflectionHelper.GetExtensionMethods(_assemblyFinder, containerType) @@ -156,7 +160,7 @@ private bool MethodSupported(MethodInfo method, Func typeSupported) return true; } - private Task GetMethodReturnTypeAsync(MethodInfo method) + private Task GetMethodReturnTypeAsync(MethodInfo method) { var result = _hardcodeMetadataProvider.GetDataTypeByPropertyType(method.ReturnType, null); return Task.FromResult(result); @@ -169,17 +173,18 @@ private Task> GetMethodArgumentsAsync(MethodInfo method) var parameters = method.GetParameters().Skip(1); foreach (var parameter in parameters) { - result.Add(new VariableDef - { - Name = parameter.Name, - DataType = _hardcodeMetadataProvider.GetDataTypeByPropertyType(parameter.ParameterType, null), - }); + if (!string.IsNullOrWhiteSpace(parameter.Name)) + result.Add(new VariableDef + { + Name = parameter.Name, + DataType = _hardcodeMetadataProvider.GetDataTypeByPropertyType(parameter.ParameterType, null).NotNull(), + }); } return Task.FromResult(result); } - public Task> GetApiEndpointsAsync(Type containerType) + public Task> GetApiEndpointsAsync(Type? containerType) { var result = new Dictionary(); if (_actionDescriptorCollectionProvider == null || containerType == null || !containerType.IsEntityType()) @@ -196,18 +201,20 @@ public Task> GetApiEndpointsAsync(Type contai .ToList(); foreach (var actionDescriptor in actionDescriptors) { - var entityActionAttribute = actionDescriptor.MethodInfo.GetAttribute(true); + var entityActionAttribute = actionDescriptor.MethodInfo.GetAttributeOrNull(true); if (entityActionAttribute != null) { var url = actionDescriptor.AttributeRouteInfo?.Template; - var httpVerbs = actionDescriptor.ActionConstraints.OfType() - .SelectMany(c => c.HttpMethods) - .ToList(); + var httpVerbs = actionDescriptor.ActionConstraints != null + ? actionDescriptor.ActionConstraints.OfType() + .SelectMany(c => c.HttpMethods) + .ToList() + : []; if (!string.IsNullOrWhiteSpace(url) && httpVerbs.Count() == 1) result.Add(entityActionAttribute.Action, new ApiEndpointDto { - HttpVerb = httpVerbs.FirstOrDefault(), + HttpVerb = httpVerbs.First(), Url = url, }); } @@ -220,7 +227,7 @@ public Task> GetApiEndpointsAsync(Type contai /// Get specifications available for the specified entityType /// /// - public Task> GetSpecificationsAsync(Type entityType) + public Task> GetSpecificationsAsync(Type? entityType) { if (entityType == null || !entityType.IsEntityType()) return Task.FromResult(new List()); @@ -247,9 +254,12 @@ public async Task> GetPropertiesAsync(Type containerTy return properties; } - private async Task<(DateTime?, List)> GetPropertiesInternalAsync(Type containerType, string containerName) + private async Task<(DateTime?, List)> GetPropertiesInternalAsync(Type? containerType, string containerName) { - var metadataContext = new MetadataContext(containerType); + var metadataContext = containerType != null + ? new MetadataContext(containerType) + : new MetadataContext(); + var hardCodedProps = containerType == null ? new List() : containerType.GetProperties(BindingFlags.Public | BindingFlags.Instance) @@ -280,7 +290,7 @@ public async Task> GetPropertiesAsync(Type containerTy }) .OrderBy(p => p.OrderIndex) .ToList(); - result = props.Select(p => RemoveSuppressed(p)).Where(p => p != null).ToList(); + result = props.Select(p => RemoveSuppressed(p)).WhereNotNull().ToList(); } else result = hardCodedProps; @@ -288,11 +298,11 @@ public async Task> GetPropertiesAsync(Type containerTy return (modelConfig?.ChangeTime, result); } - private PropertyMetadataDto RemoveSuppressed(PropertyMetadataDto prop) + private PropertyMetadataDto? RemoveSuppressed(PropertyMetadataDto prop) { if (prop.IsVisible) { - prop.Properties = prop.Properties.Select(pp => RemoveSuppressed(pp)).Where(p => p != null).ToList(); + prop.Properties = prop.Properties.Select(pp => RemoveSuppressed(pp)).WhereNotNull().ToList(); return prop; } return null; diff --git a/shesha-core/src/Shesha.Framework/Migrations/M20240802160400.cs b/shesha-core/src/Shesha.Framework/Migrations/M20240802160400.cs index 940c1b182f..001cfffaf8 100644 --- a/shesha-core/src/Shesha.Framework/Migrations/M20240802160400.cs +++ b/shesha-core/src/Shesha.Framework/Migrations/M20240802160400.cs @@ -1,7 +1,6 @@ using FluentMigrator; using Shesha.Configuration; using Shesha.FluentMigrator; -using static Castle.MicroKernel.ModelBuilder.Descriptors.InterceptorDescriptor; namespace Shesha.Migrations { @@ -12,7 +11,7 @@ public override void Up() { this.Shesha().SettingCreate(SheshaSettingNames.ThemeSettings, "Theme settings") .IsClientSpecific() - .AsComplexObject(typeof(ThemeSettings).FullName) + .AsComplexObject("Shesha.Configuration.ThemeSettings") .WithEditForm("Shesha", "theme-settings") .WithCategory("Frontend"); diff --git a/shesha-core/src/Shesha.Framework/Migrations/M20240805190400.cs b/shesha-core/src/Shesha.Framework/Migrations/M20240805190400.cs index 5959c1e457..ee826e9611 100644 --- a/shesha-core/src/Shesha.Framework/Migrations/M20240805190400.cs +++ b/shesha-core/src/Shesha.Framework/Migrations/M20240805190400.cs @@ -1,7 +1,6 @@ using FluentMigrator; using Shesha.Configuration; using Shesha.FluentMigrator; -using static Castle.MicroKernel.ModelBuilder.Descriptors.InterceptorDescriptor; namespace Shesha.Migrations { @@ -12,7 +11,7 @@ public override void Up() { this.Shesha().SettingCreate(SheshaSettingNames.MainMenuSettings, "Main menu settings") .IsClientSpecific() - .AsComplexObject(typeof(MainMenuSettings).FullName) + .AsComplexObject("Shesha.Configuration.MainMenuSettings") .WithEditForm("Shesha", "main-menu-settings") .WithCategory("Frontend"); diff --git a/shesha-core/src/Shesha.Framework/Migrations/M20250311123399.cs b/shesha-core/src/Shesha.Framework/Migrations/M20250311123399.cs new file mode 100644 index 0000000000..41a9bfe6d9 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Migrations/M20250311123399.cs @@ -0,0 +1,44 @@ +using FluentMigrator; +using Shesha.FluentMigrator; + +namespace Shesha.Migrations +{ + [Migration(20250311123399)] + public class M20250311123399 : OneWayMigration + { + public override void Up() + { + // Rebuild vw_Frwk_PermissionedObjectsFull using code from M20240705170100 + IfDatabase("SqlServer").Execute.Sql(@" +CREATE OR ALTER VIEW vw_Frwk_PermissionedObjectsFull +AS +with notDeleted as ( + select * from Frwk_PermissionedObjects where IsDeleted = 0 +), +allData as ( + select + po.*, + AccessLkp as InheritedAccessLkp, + Permissions as InheritedPermissions + from notDeleted po where po.Parent is null or po.Parent = '' + + union all + + select + po.*, + case when ad.AccessLkp = 2 then ad.InheritedAccessLkp else ad.AccessLkp end as InheritedAccessLkp, + case when ad.AccessLkp = 2 then ad.InheritedPermissions else ad.Permissions end as InheritedPermissions + from notDeleted po + join allData ad on ad.Object = po.Parent +) +select + ad.*, + m.Name as ModuleName, + case when ad.AccessLkp = 2 then ad.InheritedAccessLkp else ad.AccessLkp end as ActualAccessLkp, + case when ad.AccessLkp = 2 then ad.InheritedPermissions else ad.Permissions end as ActualPermissions +from allData ad +left join Frwk_Modules m on m.Id = ad.ModuleId + "); + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Migrations/M20250414174799.cs b/shesha-core/src/Shesha.Framework/Migrations/M20250414174799.cs new file mode 100644 index 0000000000..d9b4590834 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Migrations/M20250414174799.cs @@ -0,0 +1,34 @@ +using FluentMigrator; +using Shesha.FluentMigrator; + +namespace Shesha.Migrations +{ + [Migration(20250414174799)] + public class M20250414174799 : OneWayMigration + { + public override void Up() + { + IfDatabase("PostgreSql").Execute.Sql(@"create or replace VIEW ""vw_Frwk_EntityChangeAuditLogs"" + AS + SELECT + ec.""Id"" as ""Id"", + cast(ec.""EntityId"" as varchar(50)) as ""EntityId"", + epc.""PropertyName"", + epc.""OriginalValue"", + epc.""NewValue"", + ec.""EntityTypeFullName"", + ecs.""CreationTime"", + Concat(abpU.""Name"", ' ', abpU.""Surname"") as ""ActionedBy"", + CASE + WHEN ""ChangeType"" = 0 THEN 'Created' + WHEN ""ChangeType"" = 1 THEN 'Updated' + WHEN ""ChangeType"" = 2 THEN 'Deleted' + ELSE 'Other' + END AS ""ChangeType"" + FROM ""AbpEntityChanges"" ec + LEFT JOIN ""AbpEntityPropertyChanges"" epc on epc.""EntityChangeId"" = ec.""Id"" + LEFT JOIN ""AbpEntityChangeSets"" ecs on ecs.""Id"" = ec.""EntityChangeSetId"" + INNER JOIN ""AbpUsers"" abpU on abpU.""Id"" = ecs.""UserId"""); + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Migrations/MultiEntityReferenceMigration.cs b/shesha-core/src/Shesha.Framework/Migrations/MultiEntityReferenceMigration.cs index 3df6990fc9..7c018c32bf 100644 --- a/shesha-core/src/Shesha.Framework/Migrations/MultiEntityReferenceMigration.cs +++ b/shesha-core/src/Shesha.Framework/Migrations/MultiEntityReferenceMigration.cs @@ -1,10 +1,5 @@ -using Abp.Dependency; -using Abp.Modules; -using Abp.Reflection; +using Abp.Reflection; using Castle.Core.Internal; -using FluentMigrator.Builders.Create.Table; -using FluentMigrator.Runner.Extensions; -using Shesha.Bootstrappers; using Shesha.Domain; using Shesha.Domain.Attributes; using Shesha.Extensions; @@ -12,12 +7,7 @@ using Shesha.Generators; using Shesha.Reflection; using Shesha.Startup; -using System; -using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Migrations { @@ -41,7 +31,7 @@ public override void Process() var mtmProperties = entityTypes .SelectMany(x => x.GetProperties().Where(p => p.HasAttribute())) .Where(x => x.GetAttribute().AutoGeneration) - .DistinctBy(x => $"{x.DeclaringType.Name}_{x.Name}") + .DistinctBy(x => $"{x.DeclaringType.NotNull().Name}_{x.Name}") .ToList(); if (!Schema.Schema(MappingHelper.AutoGeneratorSchema).Exists()) diff --git a/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateModel.cs b/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateModel.cs index 178aae832d..bae6cab04e 100644 --- a/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateModel.cs +++ b/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateModel.cs @@ -7,17 +7,17 @@ public class AuthenticateModel { [Required] [StringLength(AbpUserBase.MaxEmailAddressLength)] - public string UserNameOrEmailAddress { get; set; } + public required string UserNameOrEmailAddress { get; set; } [Required] [StringLength(AbpUserBase.MaxPlainPasswordLength)] - public string Password { get; set; } + public required string Password { get; set; } //public bool RememberClient { get; set; } /// /// Optional IMEI number. Is used for mobile applications /// - public string IMEI { get; set; } + public string? IMEI { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateResultModel.cs b/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateResultModel.cs index 4d2a23c0f4..2f07f1b084 100644 --- a/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateResultModel.cs +++ b/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateResultModel.cs @@ -4,22 +4,22 @@ namespace Shesha.Models.TokenAuth { public class AuthenticateResultModel { - public string AccessToken { get; set; } + public string? AccessToken { get; set; } - public string EncryptedAccessToken { get; set; } + public string? EncryptedAccessToken { get; set; } public int ExpireInSeconds { get; set; } public DateTime ExpireOn { get; set; } - public long UserId { get; set; } + public long? UserId { get; set; } public Guid? PersonId { get; set; } - public string DeviceName { get; set; } + public string? DeviceName { get; set; } public AuthenticateResultType ResultType { get; set; } - public string RedirectUrl { get; set; } - public string RedirectModule { get; set; } - public string RedirectForm { get; set; } + public string? RedirectUrl { get; set; } + public string? RedirectModule { get; set; } + public string? RedirectForm { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateResultType.cs b/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateResultType.cs index dee3b77e37..e3a9033918 100644 --- a/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateResultType.cs +++ b/shesha-core/src/Shesha.Framework/Models/TokenAuth/AuthenticateResultType.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Models.TokenAuth +namespace Shesha.Models.TokenAuth { public enum AuthenticateResultType { diff --git a/shesha-core/src/Shesha.Framework/Modules/SheshaBaseModule.cs b/shesha-core/src/Shesha.Framework/Modules/SheshaBaseModule.cs index 9fb06d81df..b0a9044f87 100644 --- a/shesha-core/src/Shesha.Framework/Modules/SheshaBaseModule.cs +++ b/shesha-core/src/Shesha.Framework/Modules/SheshaBaseModule.cs @@ -17,7 +17,9 @@ protected async Task ImportConfigurationAsync() { Logger = this.Logger, }; - return await seeder?.SeedEmbeddedPackagesAsync(context); + return seeder != null + ? await seeder.SeedEmbeddedPackagesAsync(context) + : false; } } } diff --git a/shesha-core/src/Shesha.Framework/Otp/Dto/IOtpDto.cs b/shesha-core/src/Shesha.Framework/Otp/Dto/IOtpDto.cs index 2f54575a43..599c101288 100644 --- a/shesha-core/src/Shesha.Framework/Otp/Dto/IOtpDto.cs +++ b/shesha-core/src/Shesha.Framework/Otp/Dto/IOtpDto.cs @@ -23,17 +23,17 @@ public interface IOtpDto /// OtpSendType SendType { get; } - string RecipientType { get; } - string RecipientId { get; } + string? RecipientType { get; } + string? RecipientId { get; } /// /// Type of action (e.g. 'password restore'). May be used for audit purposes and template selection /// - string ActionType { get; } + string? ActionType { get; } DateTime? SentOn { get; } OtpSendStatus SendStatus { get; } - string ErrorMessage { get; } + string? ErrorMessage { get; } } } diff --git a/shesha-core/src/Shesha.Framework/Otp/Dto/ISendPinInput.cs b/shesha-core/src/Shesha.Framework/Otp/Dto/ISendPinInput.cs index 5da67a50b9..7cc1c68190 100644 --- a/shesha-core/src/Shesha.Framework/Otp/Dto/ISendPinInput.cs +++ b/shesha-core/src/Shesha.Framework/Otp/Dto/ISendPinInput.cs @@ -20,8 +20,8 @@ public interface ISendPinInput [Required] OtpSendType SendType { get; } - string RecipientType { get; } - string RecipientId { get; } + string? RecipientType { get; } + string? RecipientId { get; } /// /// Lifetime of the one time password in seconds @@ -31,6 +31,6 @@ public interface ISendPinInput /// /// Type of action (e.g. 'password restore'). May be used for audit purposes and template selection /// - string ActionType { get; } + string? ActionType { get; } } } diff --git a/shesha-core/src/Shesha.Framework/Otp/Dto/OtpDto.cs b/shesha-core/src/Shesha.Framework/Otp/Dto/OtpDto.cs index 25813b6733..eb4c3bbb30 100644 --- a/shesha-core/src/Shesha.Framework/Otp/Dto/OtpDto.cs +++ b/shesha-core/src/Shesha.Framework/Otp/Dto/OtpDto.cs @@ -20,17 +20,17 @@ public class OtpDto: IOtpDto /// public OtpSendType SendType { get; set; } - public string RecipientType { get; set; } - public string RecipientId { get; set; } + public string? RecipientType { get; set; } + public string? RecipientId { get; set; } /// /// Type of action (e.g. 'password restore'). May be used for audit purposes and template selection /// - public string ActionType { get; set; } + public string? ActionType { get; set; } public DateTime? SentOn { get; set; } public OtpSendStatus SendStatus { get; set; } - public string ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Otp/Dto/SendPinInput.cs b/shesha-core/src/Shesha.Framework/Otp/Dto/SendPinInput.cs index 8641ae5bfe..eb66a22a44 100644 --- a/shesha-core/src/Shesha.Framework/Otp/Dto/SendPinInput.cs +++ b/shesha-core/src/Shesha.Framework/Otp/Dto/SendPinInput.cs @@ -17,8 +17,8 @@ public class SendPinInput: ISendPinInput [Required] public OtpSendType SendType { get; set; } - public string RecipientType { get; set; } - public string RecipientId { get; set; } + public string? RecipientType { get; set; } + public string? RecipientId { get; set; } /// /// Lifetime of the one time password in seconds @@ -28,6 +28,6 @@ public class SendPinInput: ISendPinInput /// /// Type of action (e.g. 'password restore'). May be used for audit purposes and template selection /// - public string ActionType { get; set; } + public string? ActionType { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Otp/IOtpManager.cs b/shesha-core/src/Shesha.Framework/Otp/IOtpManager.cs index d0a5d09c50..24c04cf504 100644 --- a/shesha-core/src/Shesha.Framework/Otp/IOtpManager.cs +++ b/shesha-core/src/Shesha.Framework/Otp/IOtpManager.cs @@ -26,6 +26,6 @@ public interface IOtpManager /// /// /// - Task GetAsync(Guid operationId); + Task GetOrNullAsync(Guid operationId); } } diff --git a/shesha-core/src/Shesha.Framework/Permissions/Cache/IPermissionedObjectsCacheHolder.cs b/shesha-core/src/Shesha.Framework/Permissions/Cache/IPermissionedObjectsCacheHolder.cs new file mode 100644 index 0000000000..6802ee3981 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Permissions/Cache/IPermissionedObjectsCacheHolder.cs @@ -0,0 +1,8 @@ +using Shesha.Cache; + +namespace Shesha.Permissions.Cache +{ + public interface IPermissionedObjectsCacheHolder : ICacheHolder> + { + } +} diff --git a/shesha-core/src/Shesha.Framework/Permissions/Cache/IRelationsCacheHolder.cs b/shesha-core/src/Shesha.Framework/Permissions/Cache/IRelationsCacheHolder.cs new file mode 100644 index 0000000000..79bc571a12 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Permissions/Cache/IRelationsCacheHolder.cs @@ -0,0 +1,9 @@ +using Shesha.Cache; + +namespace Shesha.Permissions.Cache +{ + + public interface IRelationsCacheHolder : ICacheHolder + { + } +} diff --git a/shesha-core/src/Shesha.Framework/Permissions/Cache/PermissionedObjectRelations.cs b/shesha-core/src/Shesha.Framework/Permissions/Cache/PermissionedObjectRelations.cs new file mode 100644 index 0000000000..d5227e70ed --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Permissions/Cache/PermissionedObjectRelations.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Shesha.Permissions.Cache +{ + public class PermissionedObjectRelations + { + public PermissionedObjectRelations() + { + Children = new List(); + } + + public List Children { get; set; } + + public void AddChildren(string key) + { + if (!Children.Contains(key)) + Children.Add(key); + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Permissions/Cache/PermissionedObjectsCacheHolder.cs b/shesha-core/src/Shesha.Framework/Permissions/Cache/PermissionedObjectsCacheHolder.cs new file mode 100644 index 0000000000..d3766094dd --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Permissions/Cache/PermissionedObjectsCacheHolder.cs @@ -0,0 +1,15 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; +using System; + +namespace Shesha.Permissions.Cache +{ + public class PermissionedObjectsCacheHolder : CacheHolder>, IPermissionedObjectsCacheHolder, ISingletonDependency + { + public PermissionedObjectsCacheHolder(ICacheManager cacheManager) : base(PermissionedObjectDto.CacheStoreName, cacheManager) + { + Cache.DefaultSlidingExpireTime = TimeSpan.FromHours(24); + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Permissions/Cache/RelationsCacheHolder.cs b/shesha-core/src/Shesha.Framework/Permissions/Cache/RelationsCacheHolder.cs new file mode 100644 index 0000000000..062a1c0e33 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Permissions/Cache/RelationsCacheHolder.cs @@ -0,0 +1,15 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; +using System; + +namespace Shesha.Permissions.Cache +{ + public class RelationsCacheHolder : CacheHolder, IRelationsCacheHolder, ISingletonDependency + { + public RelationsCacheHolder(ICacheManager cacheManager) : base($"Relations-{PermissionedObjectDto.CacheStoreName}", cacheManager) + { + Cache.DefaultSlidingExpireTime = TimeSpan.FromHours(24); + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Permissions/Distribution/Dto/DistributedPermissionDefinition.cs b/shesha-core/src/Shesha.Framework/Permissions/Distribution/Dto/DistributedPermissionDefinition.cs index f856c6f8f0..1a6461078b 100644 --- a/shesha-core/src/Shesha.Framework/Permissions/Distribution/Dto/DistributedPermissionDefinition.cs +++ b/shesha-core/src/Shesha.Framework/Permissions/Distribution/Dto/DistributedPermissionDefinition.cs @@ -7,6 +7,6 @@ namespace Shesha.Permissions.Distribution.Dto /// public class DistributedPermissionDefinition : DistributedConfigurableItemBase { - public virtual string Parent { get; set; } + public virtual string? Parent { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Permissions/Distribution/PermissionDefinitionImport.cs b/shesha-core/src/Shesha.Framework/Permissions/Distribution/PermissionDefinitionImport.cs index dd0b3fa735..8f932ab7ba 100644 --- a/shesha-core/src/Shesha.Framework/Permissions/Distribution/PermissionDefinitionImport.cs +++ b/shesha-core/src/Shesha.Framework/Permissions/Distribution/PermissionDefinitionImport.cs @@ -1,24 +1,20 @@ using Abp.Dependency; using Abp.Domain.Repositories; -using NetTopologySuite.Index.HPRtree; -using Newtonsoft.Json; using Shesha.Authorization; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; -using Shesha.DynamicEntities.Distribution.Dto; using Shesha.Permissions.Distribution.Dto; using Shesha.Services.ConfigurationItems; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; namespace Shesha.DynamicEntities.Distribution { /// inheritedDoc - public class PermissionDefinitionImport : ConfigurationItemImportBase, IPermissionDefinitionImport, ITransientDependency + public class PermissionDefinitionImport : ConfigurationItemImportBase, IPermissionDefinitionImport, ITransientDependency { public string ItemType => PermissionDefinition.ItemTypeName; @@ -47,8 +43,8 @@ public override Task> SortItemsAsync(List< var result = new List(); - var addItems = (string parent) => { }; - addItems = (string parent) => + var addItems = (string? parent) => { }; + addItems = (string? parent) => { var list = loaclItems.Where(x => x.Parent == parent); result.AddRange(list); @@ -82,7 +78,7 @@ protected async Task ImportAsync(DistributedPermissionDef // get DB config var dbItem = await _permissionDefinitionRepo.FirstOrDefaultAsync(x => x.Name == item.Name - && (x.Module == null && item.ModuleName == null || x.Module.Name == item.ModuleName) + && (x.Module == null && item.ModuleName == null || x.Module != null && x.Module.Name == item.ModuleName) && x.IsLast); if (dbItem != null) @@ -125,10 +121,6 @@ protected async Task MapConfigAsync(DistributedPermissionD dbItem.Application = await GetFrontEndAppAsync(item.FrontEndApplication, context); dbItem.ItemType = item.ItemType; - //dbItem.Origin = item.OriginId; - //dbItem.BaseItem = item.BaseItem; - //dbItem.ParentVersion = item.ParentVersionId; - dbItem.Label = item.Label; dbItem.Description = item.Description; dbItem.VersionNo = item.VersionNo; @@ -139,14 +131,5 @@ protected async Task MapConfigAsync(DistributedPermissionD dbItem.Parent = item.Parent; return dbItem; } - - public async Task ReadFromJsonAsync(Stream jsonStream) - { - using (var reader = new StreamReader(jsonStream)) - { - var json = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(json); - } - } } } diff --git a/shesha-core/src/Shesha.Framework/Permissions/Dtos/PermissionedObjectDto.cs b/shesha-core/src/Shesha.Framework/Permissions/Dtos/PermissionedObjectDto.cs index 87cf11046b..6634e1b266 100644 --- a/shesha-core/src/Shesha.Framework/Permissions/Dtos/PermissionedObjectDto.cs +++ b/shesha-core/src/Shesha.Framework/Permissions/Dtos/PermissionedObjectDto.cs @@ -24,21 +24,21 @@ public PermissionedObjectDto() public string Object { get; set; } - public string Category { get; set; } + public string? Category { get; set; } - public string Module { get; set; } + public string? Module { get; set; } public Guid? ModuleId { get; set; } public string Type { get; set; } - public string Name { get; set; } + public string? Name { get; set; } - public string Description { get; set; } + public string? Description { get; set; } - public List Permissions { get; set; } + public List? Permissions { get; set; } - public List ActualPermissions { get; set; } - public List InheritedPermissions { get; set; } + public List? ActualPermissions { get; set; } + public List? InheritedPermissions { get; set; } public RefListPermissionedAccess? Access { get; set; } @@ -46,24 +46,28 @@ public PermissionedObjectDto() public RefListPermissionedAccess? ActualAccess { get; set; } public RefListPermissionedAccess? InheritedAccess { get; set; } - public string Parent { get; set; } + public string? Parent { get; set; } - public List Children { get; set; } + public List Children { get; set; } = new List(); - public bool Hidden { get; set; } + public bool? Hidden { get; set; } - public Dictionary AdditionalParameters { get; set; } + public Dictionary AdditionalParameters { get; set; } = new Dictionary(); public override string ToString() { - var permissions = Hidden + var permissions = Hidden ?? false ? "Hidden" : Access == RefListPermissionedAccess.RequiresPermissions - ? string.Join(", ", Permissions) - : Access.ToString(); + ? PermissionsDelimited + : Access.ToString(); return $"{Object} -> ({permissions})"; } - public string Md5 { get; set; } + public string PermissionsDelimited => Permissions != null + ? string.Join(", ", Permissions) + : string.Empty; + + public string? Md5 { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Permissions/Dtos/PermissionedObjectMapProfile.cs b/shesha-core/src/Shesha.Framework/Permissions/Dtos/PermissionedObjectMapProfile.cs index c4368217f9..960a6c16fd 100644 --- a/shesha-core/src/Shesha.Framework/Permissions/Dtos/PermissionedObjectMapProfile.cs +++ b/shesha-core/src/Shesha.Framework/Permissions/Dtos/PermissionedObjectMapProfile.cs @@ -11,15 +11,15 @@ public class PermissionedObjectMapProfile : ShaProfile public PermissionedObjectMapProfile() { CreateMap() - .ForMember(e => e.Permissions, c => c.MapFrom(e => string.Join(",", e.Permissions))) + .ForMember(e => e.Permissions, c => c.MapFrom(e => e.Permissions != null ? string.Join(",", e.Permissions) : string.Empty)) .ForMember(e => e.Module, c => c.Ignore()); CreateMap() .ForMember(e => e.Permissions, c => c.MapFrom(e => e.Permissions == null ? new List() : e.Permissions.Split(",", StringSplitOptions.RemoveEmptyEntries).ToList())) - .ForMember(e => e.ModuleId, c => c.MapFrom(m => (object)(m.Module == null ? null : m.Module.Id))) - .ForMember(e => e.Module, c => c.MapFrom(m => (object)(m.Module == null ? null : m.Module.Name))); + .ForMember(e => e.ModuleId, c => c.MapFrom(m => m.Module == null ? (Guid?)null : m.Module.Id)) + .ForMember(e => e.Module, c => c.MapFrom(m => m.Module == null ? null : m.Module.Name)); CreateMap() .ForMember(e => e.Permissions, c => c.MapFrom(e => e.Permissions == null diff --git a/shesha-core/src/Shesha.Framework/Permissions/EntityPermissionedObjectProvider.cs b/shesha-core/src/Shesha.Framework/Permissions/EntityPermissionedObjectProvider.cs index daa637f64d..f32245768b 100644 --- a/shesha-core/src/Shesha.Framework/Permissions/EntityPermissionedObjectProvider.cs +++ b/shesha-core/src/Shesha.Framework/Permissions/EntityPermissionedObjectProvider.cs @@ -1,117 +1,174 @@ -using Abp.Domain.Entities; +using Abp.Domain.Repositories; +using Abp.Domain.Uow; using Abp.Reflection; -using Shesha.Permissions; +using Castle.Core.Internal; +using Shesha.ConfigurationItems; +using Shesha.Domain; +using Shesha.Domain.Attributes; +using Shesha.Domain.Enums; +using Shesha.Extensions; +using Shesha.Reflection; +using Shesha.Startup; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Module = Shesha.Domain.ConfigurationItems.Module; -namespace Shesha.Permission +namespace Shesha.Permissions { - /*public class EntityPermissionedObjectProvider : PermissionedObjectProviderBase, IPermissionedObjectProvider + public class EntityPermissionedObjectProvider : PermissionedObjectProviderBase, IPermissionedObjectProvider { - public EntityPermissionedObjectProvider(IAssemblyFinder assembleFinder) : base(assembleFinder) + private readonly IApplicationStartupSession _startupSession; + private readonly IRepository _permissionedObjectRepository; + + public EntityPermissionedObjectProvider( + IAssemblyFinder assembleFinder, + IModuleManager moduleManager, + IUnitOfWorkManager unitOfWorkManager, + IApplicationStartupSession startupSession, + IRepository permissionedObjectRepository + ) : base(assembleFinder, moduleManager, unitOfWorkManager) { + _startupSession = startupSession; + _permissionedObjectRepository = permissionedObjectRepository; } public List GetObjectTypes() { - return new List {ShaPermissionedObjectsTypes.Entity} ; + return new List() { ShaPermissionedObjectsTypes.Entity }; } - public string GetObjectType(Type type) + public string? GetObjectType(Type type) { - var entityType = typeof(IEntity<>); - - return type.IsPublic && !type.IsAbstract - && type.GetInterfaces().Any(x => - x.IsGenericType && - x.GetGenericTypeDefinition() == entityType) - ? ShaPermissionedObjectsTypes.Entity - : null; + return type.IsPublic && !type.IsAbstract && type.IsEntityType() + ? ShaPermissionedObjectsTypes.Entity + : null; } - public List GetAll(string objectType = null) + public async Task> GetAllAsync(string? objectType = null, bool skipUnchangedAssembly = false) { - if (!GetObjectTypes().Contains(objectType)) return new List(); + var list = new List(); + + var hardcoded = (await _permissionedObjectRepository.GetAll() + .Where(x => + (x.Type == ShaPermissionedObjectsTypes.Entity || x.Type == ShaPermissionedObjectsTypes.EntityAction) + && + x.Hardcoded == true + ).ToListAsync()).Select(x => { return x.Object.Split("@")[0]; }).Distinct().ToList(); - var assemblies = _assembleFinder.GetAllAssemblies().Distinct(new AssemblyFullNameComparer()).Where(a => !a.IsDynamic).ToList(); - var allPermissions = new List(); + if (objectType != null && !GetObjectTypes().Contains(objectType)) + return list; - var entityType = typeof(IEntity<>); + var assemblies = _assembleFinder.GetAllAssemblies() + .Distinct(new AssemblyFullNameComparer()) + .Where(a => !a.IsDynamic && a.GetTypes().Any(MappingHelper.IsEntity)) + .ToList(); + assemblies = assemblies.Where(x => !_startupSession.AssemblyStaysUnchanged(x)).ToList(); foreach (var assembly in assemblies) { - - var module = assembly.GetTypes().FirstOrDefault(t => - t.IsPublic && !t.IsAbstract && typeof(AbpModule).IsAssignableFrom(t)); - - var services = assembly.GetTypes() - .Where(t => t.IsPublic && !t.IsAbstract && t.GetInterfaces().Any(x => - x.IsGenericType && - x.GetGenericTypeDefinition() == entityType)) - .Where(t => !t.Name.EndsWith("Base") && !t.Name.Contains("`")) - .ToList(); - foreach (var service in services) + using (var unitOfWork = _unitOfWorkManager.Begin()) { - var parent = new PermissionedObjectDto() + using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete)) { - Name = service.Name, - Module = module?.FullName ?? "", - Object = service.FullName, - Type = PermissionedObjectsSheshaTypes.Entity, - Description = GetDescription(service) - }; - allPermissions.Add(parent); + list.AddRange(await ProcessAssemblyAsync(assembly, hardcoded)); + } + await unitOfWork.CompleteAsync(); + } + } + return list; + } - var child = new PermissionedObjectDto() - { - Name = "Create", - Module = module?.FullName ?? "", - Object = service.FullName + "@Create", - Type = PermissionedObjectsSheshaTypes.EntityAction, - Parent = service.FullName, - Description = "Create" - }; - allPermissions.Add(child); - - child = new PermissionedObjectDto() - { - Name = "Update", - Module = module?.FullName ?? "", - Object = service.FullName + "@Update", - Type = PermissionedObjectsSheshaTypes.EntityAction, - Parent = service.FullName, - Description = "Update" - }; - allPermissions.Add(child); - - child = new PermissionedObjectDto() - { - Name = "Delete", - Module = module?.FullName ?? "", - Object = service.FullName + "@Delete", - Type = PermissionedObjectsSheshaTypes.EntityAction, - Parent = service.FullName, - Description = "Delete" - }; - allPermissions.Add(child); - - child = new PermissionedObjectDto() - { - Name = "Get", - Module = module?.FullName ?? "", - Object = service.FullName + "@Get", - Type = PermissionedObjectsSheshaTypes.EntityAction, - Parent = service.FullName, - Description = "Get" - }; - allPermissions.Add(child); - } + private PermissionedObjectDto MakeDto( + string objectName, + string? parent, + string name, + string description, + Module? module, + RefListPermissionedAccess? access, + List? permissions, + bool hardcoded, + string type + ) + { + RefListPermissionedAccess? _access = access; + var _permissions = new List(); + if (_access == RefListPermissionedAccess.RequiresPermissions) + { + _access = permissions?.Any() ?? false + ? RefListPermissionedAccess.RequiresPermissions + : RefListPermissionedAccess.AnyAuthenticated; + _permissions = permissions?.ToList(); } - return allPermissions; + var dto = new PermissionedObjectDto() + { + Object = objectName, + Parent = parent, + ModuleId = module?.Id, + Module = module?.Name, + Name = name, + Type = type, + Description = description, + Access = _access, + Permissions = _permissions, + Hardcoded = hardcoded, + }; + dto.Md5 = GetMd5(dto); + return dto; + } + + private async Task> ProcessAssemblyAsync(Assembly assembly, List hardcoded) + { + var m = await _moduleManager.GetOrCreateModuleAsync(assembly); + + var entityTypes = assembly.GetTypes().Where(MappingHelper.IsEntity).ToList(); + + var list = new List(); + + foreach (var et in entityTypes) + { + // Get all CrudAccessAttribute + var accessAttributes = et.GetAttributes(); + var disableAttribute = et.GetAttribute(); + if (disableAttribute == null && !accessAttributes.Any() && !hardcoded.Contains(et.GetRequiredFullName())) + continue; + + // But use only one for each type of action + var all = accessAttributes.FirstOrDefault(x => x.All != null); + var c = accessAttributes.FirstOrDefault(x => x.Create != null); + var r = accessAttributes.FirstOrDefault(x => x.Read != null); + var u = accessAttributes.FirstOrDefault(x => x.Update != null); + var d = accessAttributes.FirstOrDefault(x => x.Delete != null); + var fn = et.GetRequiredFullName(); + var e = ShaPermissionedObjectsTypes.Entity; + var ea = ShaPermissionedObjectsTypes.EntityAction; + + if (disableAttribute != null) + { + if (disableAttribute.Disable.HasFlag(CrudActions.All)) + all = new CrudAccessAttribute(CrudActions.All, false); + if (disableAttribute.Disable.HasFlag(CrudActions.Create)) + c = new CrudAccessAttribute(CrudActions.Create, false); + if (disableAttribute.Disable.HasFlag(CrudActions.Read)) + r = new CrudAccessAttribute(CrudActions.Read, false); + if (disableAttribute.Disable.HasFlag(CrudActions.Update)) + u = new CrudAccessAttribute(CrudActions.Update, false); + if (disableAttribute.Disable.HasFlag(CrudActions.Delete)) + d = new CrudAccessAttribute(CrudActions.Delete, false); + } + + list.Add(MakeDto(fn, null, et.Name, GetDescription(et), m, all?.All ?? RefListPermissionedAccess.Inherited, all?.AllPermissions, all != null, e)); + list.Add(MakeDto($"{fn}@Create", fn, "Create", "Create CRUD operation", m, c?.Create, c?.CreatePermissions, c != null, ea)); + list.Add(MakeDto($"{fn}@Get", fn, "Get", "Read CRUD operation", m, r?.Read, r?.ReadPermissions, r != null, ea)); + list.Add(MakeDto($"{fn}@Update", fn, "Update", "Update CRUD operation", m, u?.Update, u?.UpdatePermissions, u != null, ea)); + list.Add(MakeDto($"{fn}@Delete", fn, "Delete", "Delete CRUD operation", m, d?.Delete, d?.DeletePermissions, d != null, ea)); + } + return list; } - }*/ + } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Permissions/IPermissionedObjectManager.cs b/shesha-core/src/Shesha.Framework/Permissions/IPermissionedObjectManager.cs index e59532ecff..96fcce820e 100644 --- a/shesha-core/src/Shesha.Framework/Permissions/IPermissionedObjectManager.cs +++ b/shesha-core/src/Shesha.Framework/Permissions/IPermissionedObjectManager.cs @@ -13,7 +13,7 @@ public interface IPermissionedObjectManager /// /// Type /// - string GetObjectType(Type type); + string? GetObjectType(Type type); /// /// Get list of protected objects @@ -22,7 +22,7 @@ public interface IPermissionedObjectManager /// /// Show hidden protected objects /// - Task> GetAllFlatAsync(string type = null, bool withNested = true, bool withHidden = false); + Task> GetAllFlatAsync(string? type = null, bool withNested = true, bool withHidden = false); /// /// Get hierarchical list of protected objects @@ -30,12 +30,12 @@ public interface IPermissionedObjectManager /// /// Show hidden protected objects /// - Task> GetAllTreeAsync(string type = null, bool withHidden = false); + Task> GetAllTreeAsync(string? type = null, bool withHidden = false); /// /// Get Protected Object by object name with children /// - Task GetObjectWithChildAsync(string objectName, string type = null, bool withHidden = false); + Task GetObjectWithChildOrNullAsync(string objectName, string? type = null, bool withHidden = false); /// /// Get Protected Object by object name @@ -44,10 +44,10 @@ public interface IPermissionedObjectManager /// /// Name of parent object /// - Task GetOrCreateAsync(string objectName, string objectType, string inheritedFromName = null); + Task GetOrCreateAsync(string objectName, string objectType, string? inheritedFromName = null); - Task GetOrNullAsync(string objectName, string objectType); + Task GetOrNullAsync(string objectName, string objectType); /// /// Get Protected Object by object name @@ -59,7 +59,7 @@ public interface IPermissionedObjectManager Task GetAsync(Guid id); - Task CopyAsync(string srcObjectName, string dstObjectName, string srcObjectType, string dstObjectType = null); + Task CopyAsync(string srcObjectName, string dstObjectName, string srcObjectType, string? dstObjectType = null); /// /// Get Protected Object by object name @@ -69,7 +69,7 @@ public interface IPermissionedObjectManager /// PermissionedObjectDto Get(string objectName, string objectType); - List GetActualPermissions(string objectName, string objectType = null, bool useInherited = true); + List GetActualPermissions(string objectName, string objectType, bool useInherited = true); /// /// Set Protected Object data (save to DB and cache) @@ -85,7 +85,7 @@ public interface IPermissionedObjectManager /// Get permission data from the parent Protected Object if value is Inherited /// Required permissions for Protected Object. Will be ignored if Inherited is True /// - Task SetPermissionsAsync(string objectName, RefListPermissionedAccess access, List permissions); + Task SetPermissionsAsync(string objectName, RefListPermissionedAccess access, List permissions); /// /// Checks if the action descriptor is not disabled as permissioned object diff --git a/shesha-core/src/Shesha.Framework/Permissions/IPermissionedObjectProvider.cs b/shesha-core/src/Shesha.Framework/Permissions/IPermissionedObjectProvider.cs index 175ad184ca..3295543e94 100644 --- a/shesha-core/src/Shesha.Framework/Permissions/IPermissionedObjectProvider.cs +++ b/shesha-core/src/Shesha.Framework/Permissions/IPermissionedObjectProvider.cs @@ -7,7 +7,7 @@ namespace Shesha.Permissions public interface IPermissionedObjectProvider { List GetObjectTypes(); - string GetObjectType(Type type); - Task> GetAllAsync(string objectType = null, bool skipUnchangedAssembly = false); + string? GetObjectType(Type type); + Task> GetAllAsync(string? objectType = null, bool skipUnchangedAssembly = false); } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectExtensions.cs b/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectExtensions.cs deleted file mode 100644 index 645e174597..0000000000 --- a/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Abp.Dependency; -using ConcurrentCollections; -using Shesha.Domain; -using System.Collections.Generic; - -namespace Shesha.Permissions -{ - public static class PermissionedObjectExtensions - { - public static List GetActualPermissions(this PermissionedObject permissionedObject, bool useInherited = true) - { - var permissionedObjectManager = IocManager.Instance.Resolve(); - return permissionedObjectManager?.GetActualPermissions(permissionedObject.Object, permissionedObject.Type, useInherited) ?? new List(); - } - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectManager.cs b/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectManager.cs index 3f9472955d..cffcd20640 100644 --- a/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectManager.cs +++ b/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectManager.cs @@ -3,19 +3,18 @@ using Abp.Domain.Uow; using Abp.Events.Bus.Entities; using Abp.Events.Bus.Handlers; -using Abp.Extensions; using Abp.Linq.Extensions; using Abp.ObjectMapping; using Abp.Runtime.Caching; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Shesha.Application.Services; -using Shesha.Authorization; using Shesha.Cache; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Domain.Enums; using Shesha.Extensions; +using Shesha.Permissions.Cache; using Shesha.Reflection; using Shesha.Utilities; using System; @@ -24,12 +23,10 @@ using System.Threading.Tasks; namespace Shesha.Permissions -{ +{ public class PermissionedObjectManager : IPermissionedObjectManager, ITransientDependency, IEventHandler> { - private const string PermissionedObjectsCacheName = "PermissionedObjectsCache"; - public static readonly Dictionary CrudMethods = new Dictionary() { @@ -43,49 +40,44 @@ public class PermissionedObjectManager : IPermissionedObjectManager, ITransientD { "UpdateGql", "Update" }, { "Delete", "Delete" }, }; - + public static string? GetCrudMethod(string method, string? defaultValue = null) => + CrudMethods.ContainsKey(method.RemovePostfix("Async")) ? CrudMethods[method.RemovePostfix("Async")] : defaultValue; private readonly IRepository _permissionedObjectRepository; private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IObjectMapper _objectMapper; private readonly IRepository _moduleReporsitory; - private readonly ICacheManager _cacheManager; private readonly IIocResolver _iocResolver; - private readonly ITypedCache RelationsCache; - private readonly ITypedCache> PermissionedObjectsCache; + private readonly ITypedCache _relationsCache; + private readonly ITypedCache> _permissionedObjectsCache; public PermissionedObjectManager( IRepository permissionedObjectRepository, IUnitOfWorkManager unitOfWorkManager, IObjectMapper objectMapper, - ICacheManager cacheManager, IRepository moduleReporsitory, - IIocResolver iocResolver + IIocResolver iocResolver, + IRelationsCacheHolder relationsCacheHolder, + IPermissionedObjectsCacheHolder permissionedObjectsCacheHolder ) { _permissionedObjectRepository = permissionedObjectRepository; _unitOfWorkManager = unitOfWorkManager; _objectMapper = objectMapper; - _cacheManager = cacheManager; _moduleReporsitory = moduleReporsitory; _iocResolver = iocResolver; - var cache = _cacheManager.GetPermissionedObjectCache(); - cache.DefaultSlidingExpireTime = TimeSpan.FromHours(24); - PermissionedObjectsCache = cache; - - var relationCache = cacheManager.GetCache($"Relations-{PermissionedObjectDto.CacheStoreName}"); - relationCache.DefaultSlidingExpireTime = TimeSpan.FromHours(24); - RelationsCache = relationCache; + _relationsCache = relationsCacheHolder.Cache; + _permissionedObjectsCache = permissionedObjectsCacheHolder.Cache; } - private string GetCacheKey(string objectName, string objectType) + private string GetCacheKey(string? objectName, string? objectType) { return $"{objectName}"; // TODO: AS review using Permissioned objects types _{objectType}"; } - public virtual string GetObjectType(Type type) + public virtual string? GetObjectType(Type type) { var providers = _iocResolver.ResolveAll(); foreach (var permissionedObjectProvider in providers) @@ -98,19 +90,19 @@ public virtual string GetObjectType(Type type) return null; } - private async Task SetCacheAsync(PermissionedObjectDto dto, CacheItemWrapper item = null) + private async Task SetCacheAsync(PermissionedObjectDto dto, CacheItemWrapper? item = null) { var parentKey = GetCacheKey(dto.Parent, dto.Type); var childKey = GetCacheKey(dto.Object, dto.Type); - await PermissionedObjectsCache.SetAsync(childKey, item ?? new CacheItemWrapper(dto, dto)); - var cache = await RelationsCache.TryGetValueAsync(parentKey); + await _permissionedObjectsCache.SetAsync(childKey, item ?? new CacheItemWrapper(dto, dto)); + var cache = await _relationsCache.TryGetValueAsync(parentKey); var relation = cache.HasValue ? cache.Value : new PermissionedObjectRelations(); relation.AddChildren(childKey); - await RelationsCache.SetAsync(parentKey, relation); + await _relationsCache.SetAsync(parentKey, relation); } [UnitOfWork] - public virtual async Task> GetAllFlatAsync(string type = null, bool withNested = true, bool withHidden = false) + public virtual async Task> GetAllFlatAsync(string? type = null, bool withNested = true, bool withHidden = false) { var rootItems = await _permissionedObjectRepository.GetAll() .WhereIf(!string.IsNullOrEmpty(type?.Trim()), x => x.Type == type) @@ -124,7 +116,7 @@ public virtual async Task> GetAllFlatAsync(string ty if (withNested && !string.IsNullOrEmpty(type?.Trim())) { var nestedItems = await _permissionedObjectRepository.GetAll() - .Where(x => x.Type.StartsWith($"{type}.")) + .Where(x => x.Type != null && x.Type.StartsWith($"{type}.")) .WhereIf(!withHidden, x => !x.Hidden) .ToListAsync(); var nested = (await nestedItems.SelectAsync(async x => await GetCacheOrDtoAsync(x))) @@ -137,7 +129,7 @@ public virtual async Task> GetAllFlatAsync(string ty } [UnitOfWork] - public virtual async Task> GetAllTreeAsync(string type = null, bool withHidden = false) + public virtual async Task> GetAllTreeAsync(string? type = null, bool withHidden = false) { var allTyped = await GetAllFlatAsync(type, true); @@ -148,7 +140,7 @@ public virtual async Task> GetAllTreeAsync(string ty .ToList(); } - public virtual async Task GetObjectWithChildAsync(string objectName, string type = null, bool withHidden = false) + public virtual async Task GetObjectWithChildOrNullAsync(string objectName, string? type = null, bool withHidden = false) { var pObject = (await _permissionedObjectRepository.GetAll() .WhereIf(!string.IsNullOrEmpty(type?.Trim()), x => x.Type == type) @@ -157,7 +149,9 @@ public virtual async Task GetObjectWithChildAsync(string if (pObject == null) return null; - var obj = await GetDtoAsync(pObject); + var obj = await GetDtoOrNullAsync(pObject); + if (obj == null) + return null; var allTyped = await GetAllFlatAsync($"{pObject.Type}.", true); return GetObjectWithChild(obj, allTyped, withHidden); } @@ -166,7 +160,7 @@ private PermissionedObjectDto GetObjectWithChild(PermissionedObjectDto dto, List { var childQuery = list.Where(x => x.Parent == dto.Object); if (!withHidden) - childQuery = childQuery.Where(x => !x.Hidden); + childQuery = childQuery.Where(x => !(x.Hidden ?? false)); var child = childQuery .OrderBy(x => x.Name) @@ -190,14 +184,14 @@ public virtual PermissionedObjectDto Get(string objectName, string objectType) public List GetActualPermissions(string objectName, string objectType, bool useInherited = true) { var obj = Get(objectName, objectType); - return obj != null + return (obj != null ? useInherited ? obj.ActualPermissions : obj.Permissions - : new List(); + : null) ?? new List(); } - private PermissionedObject InternalCreate(string objectName, string objectType, string inheritedFromName = null, Module module = null) + private PermissionedObject InternalCreate(string objectName, string objectType, string? inheritedFromName = null, Module? module = null) { var inh = inheritedFromName; if (inheritedFromName == null) @@ -225,7 +219,7 @@ private PermissionedObject InternalCreate(string objectName, string objectType, return dbObj; } - private PermissionedObjectDto InternalDtoCreate(string objectName, string objectType, string inheritedFromName = null, Module module = null) + private PermissionedObjectDto InternalDtoCreate(string objectName, string objectType, string? inheritedFromName = null, Module? module = null) { var inh = inheritedFromName; if (inheritedFromName == null) @@ -254,96 +248,77 @@ private PermissionedObjectDto InternalDtoCreate(string objectName, string object return dtoObj; } - private async Task GetDtoAsync(PermissionedObjectDto dtoObj, bool useInherited = true, bool useHidden = false) + private async Task GetDtoOrNullAsync(PermissionedObjectDto dtoObj, bool useInherited = true, bool useHidden = false) { - // Check hidden and inherited - if (dtoObj != null) - { - dtoObj.ActualPermissions = dtoObj.Access == RefListPermissionedAccess.RequiresPermissions ? dtoObj.Permissions : new List(); - dtoObj.ActualAccess = dtoObj.Access; + dtoObj.ActualPermissions = dtoObj.Access == RefListPermissionedAccess.RequiresPermissions ? dtoObj.Permissions : new List(); + dtoObj.ActualAccess = dtoObj.Access; - // skip hidden - if (!useHidden && dtoObj.Hidden) - return null; + // skip hidden + if (!useHidden && (dtoObj.Hidden ?? false)) + return null; - var parent = !string.IsNullOrEmpty(dtoObj.Parent) - ? await GetOrDefaultAsync(dtoObj.Parent, "") - : null; - dtoObj.InheritedAccess = RefListPermissionedAccess.Inherited; + var parent = !string.IsNullOrEmpty(dtoObj.Parent) + ? await GetOrDefaultAsync(dtoObj.Parent, "") + : null; + dtoObj.InheritedAccess = RefListPermissionedAccess.Inherited; - // check parent - if (parent != null) - { - dtoObj.InheritedPermissions = parent.ActualPermissions; - dtoObj.InheritedAccess = parent.ActualAccess; - } + // check parent + if (parent != null) + { + dtoObj.InheritedPermissions = parent.ActualPermissions; + dtoObj.InheritedAccess = parent.ActualAccess; + } - // if current object is inherited - if (useInherited && dtoObj.Inherited && parent != null) + // if current object is inherited + if (useInherited && dtoObj.Inherited && parent != null) + { + // check parent + if (parent.ActualAccess != RefListPermissionedAccess.Inherited) { - // check parent - if (parent.ActualAccess != RefListPermissionedAccess.Inherited) - { - dtoObj.ActualPermissions = parent.ActualPermissions; - dtoObj.ActualAccess = parent.ActualAccess; - return dtoObj; - } + dtoObj.ActualPermissions = parent.ActualPermissions; + dtoObj.ActualAccess = parent.ActualAccess; + return dtoObj; } - return dtoObj; - } - return null; + } + return dtoObj; } - private async Task GetDtoAsync(PermissionedObject dbObj, bool useInherited = true, bool useHidden = false) + private async Task GetDtoOrNullAsync(PermissionedObject dbObj, bool useInherited = true, bool useHidden = false) { - // Check hidden and inherited - if (dbObj != null) - { - var obj = _objectMapper.Map(dbObj); - - return await GetDtoAsync(obj, useInherited, useHidden); - } - return null; + var obj = _objectMapper.Map(dbObj); + return await GetDtoOrNullAsync(obj, useInherited, useHidden); } private async Task GetCacheOrDtoAsync(PermissionedObject dbObj) { var key = GetCacheKey(dbObj.Object, dbObj.Type); - var cacheObj = await PermissionedObjectsCache.TryGetValueAsync(key); + var cacheObj = await _permissionedObjectsCache.TryGetValueAsync(key); if (cacheObj.HasValue) return cacheObj.Value.DbValue ?? cacheObj.Value.DefaultValue; - var dto = await GetDtoAsync(dbObj); + var dto = await GetDtoOrNullAsync(dbObj, true, true); - await SetCacheAsync(dto); + await SetCacheAsync(dto.NotNull()); return dto; } [UnitOfWork] - public virtual async Task CreateAsync(string objectName, string objectType, string inheritedFromName = null) + public virtual async Task CreateAsync(string objectName, string objectType, string? inheritedFromName = null) { var dbObj = InternalCreate(objectName, objectType, inheritedFromName); await _permissionedObjectRepository.InsertAsync(dbObj); - var obj = await GetDtoAsync(dbObj); + var obj = await GetDtoOrNullAsync(dbObj, true, true); - return obj; + return obj.NotNull(); } [UnitOfWork] - public virtual async Task GetOrCreateAsync(string objectName, string objectType, string inheritedFromName = null) + public virtual async Task GetOrCreateAsync(string objectName, string objectType, string? inheritedFromName = null) { - PermissionedObjectDto obj = null; var dbObj = await _permissionedObjectRepository.GetAll().Where(x => x.Object == objectName).FirstOrDefaultAsync(); - if (dbObj != null) - { - obj = await GetDtoAsync(dbObj); - } - else - { - obj = await CreateAsync(objectName, objectType, inheritedFromName); - } - - return obj; + return dbObj != null + ? (await GetDtoOrNullAsync(dbObj, true, true)).NotNull() + : await CreateAsync(objectName, objectType, inheritedFromName); } public virtual async Task GetAsync(Guid id) @@ -355,31 +330,31 @@ public virtual async Task GetAsync(Guid id) public virtual async Task> GetInternalAsync(string objectName, string objectType) { var key = GetCacheKey(objectName, objectType); - var cacheObj = await PermissionedObjectsCache.TryGetValueAsync(key); + var cacheObj = await _permissionedObjectsCache.TryGetValueAsync(key); if (cacheObj.HasValue) return cacheObj.Value; - CacheItemWrapper item = null; + CacheItemWrapper? item = null; using (var uow = _unitOfWorkManager.Current == null ? _unitOfWorkManager.Begin() : null) { var dbObj = await _permissionedObjectRepository.GetAll() - .WhereIf(!objectType.IsNullOrEmpty(), x => x.Type == objectType) + .WhereIf(!string.IsNullOrWhiteSpace(objectType), x => x.Type == objectType) .Where(x => x.Object == objectName) .FirstOrDefaultAsync(); var dto = dbObj != null - ? await GetDtoAsync(dbObj) + ? await GetDtoOrNullAsync(dbObj) : null; if (uow != null) await uow.CompleteAsync(); - var def = await GetDtoAsync(InternalDtoCreate(objectName, objectType)); - item = new CacheItemWrapper(def, dto); + var def = await GetDtoOrNullAsync(InternalDtoCreate(objectName, objectType), true, true); + item = new CacheItemWrapper(def.NotNull(), dto); await SetCacheAsync(def, item); } return item; } - public virtual async Task GetOrNullAsync(string objectName, string objectType) + public virtual async Task GetOrNullAsync(string objectName, string objectType) { var obj = await GetInternalAsync(objectName, objectType); return obj.DbValue; @@ -396,7 +371,7 @@ public virtual async Task GetOrDefaultAsync(string object public virtual async Task SetAsync(PermissionedObjectDto permissionedObject) { // ToDo: AS - check if permission names exist - var obj = + var obj = await _permissionedObjectRepository.GetAll() .Where(x => x.Object == permissionedObject.Object && x.Type == permissionedObject.Type).FirstOrDefaultAsync() ?? @@ -405,10 +380,10 @@ await _permissionedObjectRepository.GetAll() Object = permissionedObject.Object, Type = permissionedObject.Type, Module = permissionedObject.Module != null - ? await _moduleReporsitory.FirstOrDefaultAsync(x => x.Id == permissionedObject.ModuleId) - : !permissionedObject.Module.IsNullOrEmpty() - ? await _moduleReporsitory.FirstOrDefaultAsync(x => x.Name == permissionedObject.Module) - : null, + ? await _moduleReporsitory.FirstOrDefaultAsync(x => x.Id == permissionedObject.ModuleId) + : !string.IsNullOrWhiteSpace(permissionedObject.Module) + ? await _moduleReporsitory.FirstOrDefaultAsync(x => x.Name == permissionedObject.Module) + : null, Parent = permissionedObject.Parent, Name = permissionedObject.Name ?? permissionedObject.Object, }; @@ -416,32 +391,33 @@ await _permissionedObjectRepository.GetAll() obj.Category = permissionedObject.Category; obj.Description = permissionedObject.Description; obj.Permissions = string.Join(",", permissionedObject.Permissions ?? new List()); - obj.Hidden = permissionedObject.Hidden; + obj.Hidden = permissionedObject.Hidden ?? false; obj.Access = permissionedObject.Access ?? RefListPermissionedAccess.Inherited; var newObj = await _permissionedObjectRepository.InsertOrUpdateAsync(obj); - var dto = await GetDtoAsync(newObj); - return dto; + var dto = await GetDtoOrNullAsync(newObj, true, true); + return dto.NotNull(); } [UnitOfWork] - public virtual async Task SetPermissionsAsync(string objectName, RefListPermissionedAccess access, List permissions) + public virtual async Task SetPermissionsAsync(string objectName, RefListPermissionedAccess access, List permissions) { // ToDo: AS - check permission names exist var obj = await _permissionedObjectRepository.GetAll().Where(x => x.Object == objectName).FirstOrDefaultAsync(); - if (obj == null) return null; + if (obj == null) + return null; obj.Permissions = string.Join(",", permissions ?? new List()); obj.Access = (RefListPermissionedAccess)access; await _permissionedObjectRepository.InsertOrUpdateAsync(obj); - var dto = await GetDtoAsync(obj); + var dto = await GetDtoOrNullAsync(obj); return dto; } - public async Task CopyAsync(string srcObjectName, string dstObjectName, string srcObjectType, string dstObjectType = null) + public async Task CopyAsync(string srcObjectName, string dstObjectName, string srcObjectType, string? dstObjectType = null) { var permission = await GetOrNullAsync(srcObjectName, srcObjectType); if (permission != null) @@ -460,16 +436,14 @@ public async Task IsActionDescriptorEnabledAsync(ActionDescriptor actionDe { var methodName = descriptor.MethodInfo.Name.RemovePostfix("Async"); // remove disabled endpoints - var method = PermissionedObjectManager.CrudMethods.ContainsKey(methodName) - ? PermissionedObjectManager.CrudMethods[methodName] - : null; + var method = GetCrudMethod(methodName); var obj = ""; - if (descriptor.ControllerTypeInfo.ImplementsGenericInterface(typeof(IEntityAppService<,>)) && !method.IsNullOrEmpty()) + if (descriptor.ControllerTypeInfo.ImplementsGenericInterface(typeof(IEntityAppService<,>)) && !string.IsNullOrWhiteSpace(method)) { // entity service - var genericInterface = descriptor.ControllerTypeInfo.GetGenericInterfaces(typeof(IEntityAppService<,>)).FirstOrDefault(); - var entityType = genericInterface.GenericTypeArguments.FirstOrDefault(); + var genericInterface = descriptor.ControllerTypeInfo.GetGenericInterfaces(typeof(IEntityAppService<,>)).First(); + var entityType = genericInterface.GenericTypeArguments.First(); obj = $"{entityType.FullName}@{method}"; } else @@ -484,9 +458,9 @@ public async Task IsActionDescriptorEnabledAsync(ActionDescriptor actionDe private void RemoveCache(string key) { - PermissionedObjectsCache.Remove(key); + _permissionedObjectsCache.Remove(key); PermissionedObjectRelations relation; - if (RelationsCache.TryGetValue(key, out relation)) + if (_relationsCache.TryGetValue(key, out relation)) { foreach(var childKey in relation.Children) { @@ -495,24 +469,24 @@ private void RemoveCache(string key) }; } - private void RemoveCache(string objectName, string objectType, string parentName) + private void RemoveCache(string objectName, string? objectType, string? parentName) { var key = ""; var pkey = GetCacheKey(objectName, objectType); - var parent = PermissionedObjectsCache.GetOrDefault(pkey); + var parent = _permissionedObjectsCache.GetOrDefault(pkey); while (parent != null) { key = pkey; if (parent != null) { pkey = GetCacheKey(parent.DefaultValue.Parent, objectType); - parent = PermissionedObjectsCache.GetOrDefault(pkey); + parent = _permissionedObjectsCache.GetOrDefault(pkey); } } - PermissionedObjectsCache.Remove(key); + _permissionedObjectsCache.Remove(key); PermissionedObjectRelations relation; - if (RelationsCache.TryGetValue(key, out relation)) + if (_relationsCache.TryGetValue(key, out relation)) { foreach (var childKey in relation.Children) { @@ -541,21 +515,5 @@ public async Task> GetObjectsByAccessAsync(string ty .ToList(); return forms; } - - private class PermissionedObjectRelations - { - public PermissionedObjectRelations() - { - Children = new List(); - } - - public List Children { get; set; } - - public void AddChildren(string key) - { - if (!Children.Contains(key)) - Children.Add(key); - } - } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectProviderBase.cs b/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectProviderBase.cs index 486c35861a..642de20ecc 100644 --- a/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectProviderBase.cs +++ b/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectProviderBase.cs @@ -1,24 +1,61 @@ -using System; -using System.ComponentModel; -using System.Reflection; -using System.Xml; -using Abp.Dependency; +using Abp.Dependency; +using Abp.Domain.Uow; +using Abp.Modules; using Abp.Reflection; +using Shesha.ConfigurationItems; using Shesha.Reflection; using Shesha.Utilities; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Module = Shesha.Domain.ConfigurationItems.Module; namespace Shesha.Permissions { public class PermissionedObjectProviderBase : ITransientDependency { protected readonly IAssemblyFinder _assembleFinder; + protected readonly IModuleManager _moduleManager; + protected readonly IUnitOfWorkManager _unitOfWorkManager; - public PermissionedObjectProviderBase(IAssemblyFinder assembleFinder) + public PermissionedObjectProviderBase( + IAssemblyFinder assembleFinder, + IModuleManager moduleManager, + IUnitOfWorkManager unitOfWorkManager + ) { _assembleFinder = assembleFinder; + _moduleManager = moduleManager; + _unitOfWorkManager = unitOfWorkManager; + } + + protected string GetMd5(PermissionedObjectDto dto) + { + return $"{dto.Hardcoded}|{dto.Access?.ToString() ?? "null"}|{dto.PermissionsDelimited}|{dto.ModuleId}|{dto.Parent}|{dto.Name}|{string.Join("|", dto.AdditionalParameters.Select(x => x.Key + "@" + x.Value))}" + .ToMd5Fingerprint(); + } + + private Dictionary _modules = new Dictionary(); + + protected async Task GetModuleOfAssemblyAsync(Assembly assembly) + { + if (_modules.TryGetValue(assembly, out var module)) + return module; + + module = await _moduleManager.GetOrCreateModuleAsync(assembly); + _modules.Add(assembly, module); + return module; + } + + protected Type? GetModuleOfType(Type type) + { + return type.Assembly.GetTypes().FirstOrDefault(t => t.IsPublic && !t.IsAbstract && typeof(AbpModule).IsAssignableFrom(t)); } - protected string GetName(Type service, string defaultName = null) + protected string GetName(Type service, string? defaultName = null) { var name = service.GetCustomAttribute()?.DisplayName; return string.IsNullOrEmpty(name) ? defaultName ?? service.Name : name; @@ -29,7 +66,7 @@ protected string GetDescription(Type service) var description = service.GetCustomAttribute()?.Description; if (string.IsNullOrEmpty(description)) { - XmlElement documentation = DocsByReflection.XMLFromType(service); + var documentation = DocsByReflection.XMLFromType(service); description = documentation?["summary"]?.InnerText.Trim(); if (string.IsNullOrEmpty(description)) { @@ -40,7 +77,7 @@ protected string GetDescription(Type service) return description; } - protected string GetName(MethodInfo method, string defaultName = null) + protected string GetName(MethodInfo method, string? defaultName = null) { var name = method.GetCustomAttribute()?.DisplayName; return string.IsNullOrEmpty(name) ? defaultName ?? method.Name : name; @@ -51,7 +88,7 @@ protected string GetDescription(MethodInfo method) var description = method.GetCustomAttribute()?.Description; if (string.IsNullOrEmpty(description)) { - XmlElement documentation = DocsByReflection.XMLFromMember(method); + var documentation = DocsByReflection.XMLFromMember(method); description = documentation?["summary"]?.InnerText.Trim(); if (string.IsNullOrEmpty(description)) { diff --git a/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectsBootstrapper.cs b/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectsBootstrapper.cs index a82be389f3..5ec8cd3b79 100644 --- a/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectsBootstrapper.cs +++ b/shesha-core/src/Shesha.Framework/Permissions/PermissionedObjectsBootstrapper.cs @@ -51,13 +51,15 @@ public async Task ProcessAsync() var items = await permissionedObjectProvider.GetAllAsync(objectType, true); var dbItems = await _permissionedObjectRepository.GetAll() - .Where(x => x.Type == objectType || x.Type.Contains($"{objectType}.")).ToListAsync(); + .Where(x => x.Type != null && (x.Type == objectType || x.Type.Contains($"{objectType}."))).ToListAsync(); // Add news items var toAdd = items.Where(i => dbItems.All(dbi => dbi.Object != i.Object)) .ToList(); foreach (var item in toAdd) - { + { + if (item.Access == null) continue; + var dbItem = _objectMapper.Map(item); dbItem.Module = await _moduleReporsitory.FirstOrDefaultAsync(x => x.Id == item.ModuleId); var obj = await _permissionedObjectRepository.InsertAsync(dbItem); @@ -73,35 +75,26 @@ public async Task ProcessAsync() { var item = items.FirstOrDefault(x => x.Object == dbItem.Object); if (item == null) continue; + dbItem.Module = await _moduleReporsitory.FirstOrDefaultAsync(x => x.Id == item.ModuleId); dbItem.Parent = item.Parent; dbItem.Name = item.Name; if (dbItem.Hardcoded.HasValue && (item.Hardcoded == true || item.Hardcoded != dbItem.Hardcoded)) { - dbItem.Access = item.Access ?? Domain.Enums.RefListPermissionedAccess.Inherited; - dbItem.Permissions = string.Join(",", item.Permissions); + dbItem.Access = item.Access ?? dbItem.Access; + dbItem.Permissions = item.Permissions != null ? string.Join(",", item.Permissions) : dbItem.Permissions; } dbItem.Hardcoded = item.Hardcoded ?? false; - dbItem.Md5 = item.Md5; + dbItem.Md5 = item.Md5 ?? ""; await _permissionedObjectRepository.UpdateAsync(dbItem); foreach (var parameter in item.AdditionalParameters) { await _versionedFieldManager.SetVersionedFieldValueAsync(dbItem, parameter.Key, parameter.Value, false); } } - - // TODO: AS - think how to inactivate deleted items take into account skipped Assembly - // Inactivate deleted items - /*var toDelete = dbItems.Where(dbi => items.All(i => dbi.Object != i.Object)).ToList(); - foreach (var item in toDelete) - { - await _permissionedObjectRepository.DeleteAsync(item); - }*/ } } - - // todo: write changelog } } } diff --git a/shesha-core/src/Shesha.Framework/QuickSearch/Cache/IQuickSearchPropertiesCacheHolder.cs b/shesha-core/src/Shesha.Framework/QuickSearch/Cache/IQuickSearchPropertiesCacheHolder.cs new file mode 100644 index 0000000000..dfbd9ff0e0 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/QuickSearch/Cache/IQuickSearchPropertiesCacheHolder.cs @@ -0,0 +1,9 @@ +using Shesha.Cache; +using System.Collections.Generic; + +namespace Shesha.QuickSearch.Cache +{ + public interface IQuickSearchPropertiesCacheHolder : ICacheHolder> + { + } +} diff --git a/shesha-core/src/Shesha.Framework/QuickSearch/Cache/QuickSearchPropertiesCacheHolder.cs b/shesha-core/src/Shesha.Framework/QuickSearch/Cache/QuickSearchPropertiesCacheHolder.cs new file mode 100644 index 0000000000..5cdaadab55 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/QuickSearch/Cache/QuickSearchPropertiesCacheHolder.cs @@ -0,0 +1,14 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; +using System.Collections.Generic; + +namespace Shesha.QuickSearch.Cache +{ + public class QuickSearchPropertiesCacheHolder : CacheHolder>, IQuickSearchPropertiesCacheHolder, ISingletonDependency + { + public QuickSearchPropertiesCacheHolder(ICacheManager cacheManager) : base("QuickSearchPropertiesCache", cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Framework/QuickSearch/Exceptions/QuickSearchException.cs b/shesha-core/src/Shesha.Framework/QuickSearch/Exceptions/QuickSearchException.cs new file mode 100644 index 0000000000..a57ad1e46d --- /dev/null +++ b/shesha-core/src/Shesha.Framework/QuickSearch/Exceptions/QuickSearchException.cs @@ -0,0 +1,20 @@ +using System; + +namespace Shesha.QuickSearch.Exceptions +{ + /// + /// Indicates problems during JsonLogic parsing + /// + public class QuickSearchException : Exception + { + /// + /// Expression whose parsing has failed + /// + public string QuickSearch { get; private set; } + + public QuickSearchException(string quickSearch) : base($"Failed to build linq expression for quick search expression '{quickSearch}'") + { + QuickSearch = quickSearch; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/QuickSearch/IQuickSearcher.cs b/shesha-core/src/Shesha.Framework/QuickSearch/IQuickSearcher.cs index 9080207351..f3343bb83b 100644 --- a/shesha-core/src/Shesha.Framework/QuickSearch/IQuickSearcher.cs +++ b/shesha-core/src/Shesha.Framework/QuickSearch/IQuickSearcher.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; namespace Shesha.QuickSearch { @@ -19,7 +17,7 @@ public interface IQuickSearcher /// Quick search text /// List of properties to search. Supports dot notation (e.g. User.Username) /// - Expression> GetQuickSearchExpression(string quickSearch, List properties); + Expression>? GetQuickSearchExpression(string quickSearch, List properties); /// /// Get quick search linq expression. Uses root level properties of the specified entity diff --git a/shesha-core/src/Shesha.Framework/QuickSearch/QuickSearchPropertyInfo.cs b/shesha-core/src/Shesha.Framework/QuickSearch/QuickSearchPropertyInfo.cs index e3a37b63c2..4a60696d35 100644 --- a/shesha-core/src/Shesha.Framework/QuickSearch/QuickSearchPropertyInfo.cs +++ b/shesha-core/src/Shesha.Framework/QuickSearch/QuickSearchPropertyInfo.cs @@ -20,11 +20,11 @@ public class QuickSearchPropertyInfo /// /// Referencelist module /// - public string ReferenceListModule { get; set; } + public string? ReferenceListModule { get; set; } /// /// Referencelist name /// - public string ReferenceListName { get; set; } + public string? ReferenceListName { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/QuickSearch/QuickSearcher.cs b/shesha-core/src/Shesha.Framework/QuickSearch/QuickSearcher.cs index 4d64f7aef4..f5e38628ff 100644 --- a/shesha-core/src/Shesha.Framework/QuickSearch/QuickSearcher.cs +++ b/shesha-core/src/Shesha.Framework/QuickSearch/QuickSearcher.cs @@ -3,12 +3,15 @@ using Abp.Runtime.Caching; using Shesha.Configuration.Runtime; using Shesha.Domain; +using Shesha.Extensions; using Shesha.JsonLogic; +using Shesha.QuickSearch.Cache; using Shesha.Reflection; using Shesha.Services; using Shesha.Utilities; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -24,23 +27,28 @@ public class QuickSearcher: IQuickSearcher, ITransientDependency private readonly ICacheManager _cacheManager; private readonly IRepository _refListItemRepository; private readonly IReferenceListHelper _refListHelper; + /// + /// Cache of the quick search properties + /// + private readonly ITypedCache> _quickSearchPropertiesCache; - private readonly MethodInfo stringContainsMethod = typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }); - private readonly MethodInfo queryableAnyMethod = typeof(Queryable).GetMethods().FirstOrDefault(m => m.Name == nameof(Queryable.Any) && m.GetParameters().Count() == 2); + private readonly MethodInfo stringContainsMethod = typeof(string).GetMethod(nameof(string.Contains), [typeof(string)]) ?? throw new Exception($"Method {nameof(string.Contains)} not found in type '{typeof(string).FullName}'"); + private readonly MethodInfo queryableAnyMethod = typeof(Queryable).GetMethods().FirstOrDefault(m => m.Name == nameof(Queryable.Any) && m.GetParameters().Count() == 2) ?? throw new Exception($"Method {nameof(Queryable.Any)} not found in type '{typeof(Queryable).FullName}'"); - public QuickSearcher(IEntityConfigurationStore entityConfigurationStore, IRepository refListItemRepository, ICacheManager cacheManager, IReferenceListHelper refListHelper) + public QuickSearcher( + IEntityConfigurationStore entityConfigurationStore, + IRepository refListItemRepository, + ICacheManager cacheManager, + IReferenceListHelper refListHelper, + IQuickSearchPropertiesCacheHolder quickSearchPropertiesCacheHolder) { _entityConfigurationStore = entityConfigurationStore; _refListItemRepository = refListItemRepository; _cacheManager = cacheManager; _refListHelper = refListHelper; + _quickSearchPropertiesCache = quickSearchPropertiesCacheHolder.Cache; } - /// - /// Cache of the quick search properties - /// - protected ITypedCache> QuickSearchPropertiesCache => _cacheManager.GetCache>(nameof(QuickSearchPropertiesCache)); - /// /// Get quick search expression for the specified entity type /// @@ -51,7 +59,7 @@ public QuickSearcher(IEntityConfigurationStore entityConfigurationStore, IReposi public Expression> GetQuickSearchExpression(string quickSearch, List properties) { var itemExpression = Expression.Parameter(typeof(T), "ent"); - var conditions = GetQuickSearchExpression(quickSearch, properties, itemExpression); + var conditions = GetQuickSearchExpression(quickSearch, properties, itemExpression).NotNull(); if (conditions.CanReduce) { conditions = conditions.ReduceAndCheck(); @@ -81,7 +89,7 @@ public Expression> GetQuickSearchExpression(string quickSearch) /// List of properties in dot notation (e.g. FirstName, User.Username, AccountType) /// Parameter expression /// - public Expression GetQuickSearchExpression(string quickSearch, List properties, ParameterExpression parameter) + public Expression? GetQuickSearchExpression(string quickSearch, List properties, ParameterExpression parameter) { if (string.IsNullOrWhiteSpace(quickSearch)) return null; @@ -158,7 +166,7 @@ public Expression GetQuickSearchExpression(string quickSearch, List p var query = _refListItemRepository.GetAll(); - var anyMethod = typeof(Queryable).GetMethods().FirstOrDefault(m => m.Name == nameof(Queryable.Any) && m.GetParameters().Count() == 2); + var anyMethod = typeof(Queryable).GetMethods().First(m => m.Name == nameof(Queryable.Any) && m.GetParameters().Count() == 2); var anyGeneric = anyMethod.MakeGenericMethod(typeof(ReferenceListItem)); var call = Expression.Call( @@ -256,7 +264,7 @@ public IQueryable ApplyQuickSearch(IQueryable queryab /// Entity parameter expression (i.e. `e` part in the `e => foo`) /// Comparison rule /// - private Expression GetCommonRefListExpression(string module, string name, string propName, string quickSearch, ParameterExpression entityExpression, RefListItemComparer comparer) + private Expression GetCommonRefListExpression(string? module, string name, string propName, string quickSearch, ParameterExpression entityExpression, RefListItemComparer comparer) { var refList = _refListHelper.GetReferenceList(new ReferenceListIdentifier(module, name)); @@ -283,33 +291,6 @@ private Expression GetCommonRefListExpression(string module, string name, string var query = Expression.Lambda>(expr, param); return query; - /* - var moduleExpr = string.IsNullOrWhiteSpace(module) - ? Expression.Equal(ExpressionExtensions.GetMemberExpression(param, $"{nameof(ReferenceListItem.ReferenceList)}.{nameof(ReferenceListItem.ReferenceList.Configuration)}.{nameof(ReferenceListItem.ReferenceList.Configuration.Module)}"), Expression.Constant(null)) - : Expression.Equal(ExpressionExtensions.GetMemberExpression(param, $"{nameof(ReferenceListItem.ReferenceList)}.{nameof(ReferenceListItem.ReferenceList.Configuration)}.{nameof(ReferenceListItem.ReferenceList.Configuration.Module)}.{nameof(ReferenceListItem.ReferenceList.Configuration.Module.Name)}"), Expression.Constant(module)); - - var nameExpr = Expression.Equal(ExpressionExtensions.GetMemberExpression(param, $"{nameof(ReferenceListItem.ReferenceList)}.{nameof(ReferenceListItem.ReferenceList.Configuration)}.{nameof(ReferenceListItem.ReferenceList.Configuration.Name)}"), Expression.Constant(name)); - - var propExpression = ExpressionExtensions.GetMemberExpression(entityExpression, propName); - var valuePredicateExpr = comparer.Invoke( - Expression.Convert(propExpression, typeof(Int64?)), - Expression.Convert(ExpressionExtensions.GetMemberExpression(param, nameof(ReferenceListItem.ItemValue)), typeof(Int64?)) - ); - - var constExpression = Expression.Constant(quickSearch); - - var itemTextExpr = ExpressionExtensions.GetMemberExpression(param, nameof(ReferenceListItem.Item)); - var containsExpression = Expression.Call( - itemTextExpr, - stringContainsMethod, - constExpression); - - var expr = CombineExpressions(new List { moduleExpr, nameExpr, valuePredicateExpr, containsExpression }, Expression.AndAlso, entityExpression); - - var query = Expression.Lambda>(expr, param); - - return query; - */ } /// @@ -320,6 +301,13 @@ private Expression GetCommonRefListExpression(string module, string name, string /// private delegate Expression Binder(Expression left, Expression right); + private Expression Reduce(Expression? acc, Expression right, Binder binder) + { + return acc == null + ? right + : binder(acc, right); + } + /// /// Combine expressions list using specified /// @@ -329,16 +317,17 @@ private Expression GetCommonRefListExpression(string module, string name, string /// private Expression CombineExpressions(List expressions, Binder binder, ParameterExpression param) { - Expression acc = null; + if (!expressions.Any()) + throw new ArgumentException($"expressions list must not be empty", nameof(expressions)); - Expression bind(Expression acc, Expression right) => acc == null ? right : binder(acc, right); + Expression? acc = null; foreach (var expression in expressions) { - acc = bind(acc, expression); + acc = Reduce(acc, expression, binder); } - return acc; + return acc ?? throw new Exception("Failed to combine linq expressions"); } /// @@ -348,15 +337,15 @@ private Expression CombineExpressions(List expressions, Binder binde /// List of properties in dot notation (e.g. FirstName, User.Username, AccountType) /// Cache key. Live null to skip caching /// - private List GetPropertiesForSqlQuickSearch(List properties, string cacheKey) + private List GetPropertiesForSqlQuickSearch(List properties, string? cacheKey) { if (string.IsNullOrWhiteSpace(cacheKey)) return DoGetPropertiesForSqlQuickSearch(properties); - return QuickSearchPropertiesCache.Get(cacheKey, (s) => DoGetPropertiesForSqlQuickSearch(properties)); + return _quickSearchPropertiesCache.Get(cacheKey, (s) => DoGetPropertiesForSqlQuickSearch(properties)); } - private bool TryGetProperty(EntityConfiguration entityConfig, string name, out PropertyConfiguration propConfig) + private bool TryGetProperty(EntityConfiguration entityConfig, string name, [NotNullWhen(true)] out PropertyConfiguration? propConfig) { if (name == EntityConstants.DisplayNameField) { @@ -390,7 +379,7 @@ private List DoGetPropertiesForSqlQuickSearch( var effectivePathParts = new List(); var currentEntityConfig = entityConfig; - PropertyConfiguration property = null; + PropertyConfiguration? property = null; if (propName.Contains('.')) { var parts = propName.Split('.'); @@ -418,21 +407,13 @@ private List DoGetPropertiesForSqlQuickSearch( } } else { - TryGetProperty(currentEntityConfig, propName, out property); - if (property != null) + if (TryGetProperty(currentEntityConfig, propName, out property)) effectivePathParts.Add(property.PropertyInfo.Name); } if (property == null) return null; - /* - if (property.PropertyInfo.Name == currentEntityConfig.CreatedUserPropertyInfo?.Name || - property.PropertyInfo.Name == currentEntityConfig.LastUpdatedUserPropertyInfo?.Name || - property.PropertyInfo.Name == currentEntityConfig.InactivateUserPropertyInfo?.Name) - return null; - */ - if (!property.IsMapped) return null; @@ -444,7 +425,7 @@ private List DoGetPropertiesForSqlQuickSearch( ReferenceListName = property.ReferenceListName }; }) - .Where(i => i != null) + .WhereNotNull() .Select(i => new QuickSearchPropertyInfo() { Name = i.Path, diff --git a/shesha-core/src/Shesha.Framework/Reflection/AssemblyFullNameComparer.cs b/shesha-core/src/Shesha.Framework/Reflection/AssemblyFullNameComparer.cs index bdf840c3c0..f802793c3c 100644 --- a/shesha-core/src/Shesha.Framework/Reflection/AssemblyFullNameComparer.cs +++ b/shesha-core/src/Shesha.Framework/Reflection/AssemblyFullNameComparer.cs @@ -1,18 +1,19 @@ -using System.Collections.Generic; +using Shesha.Extensions; +using System.Collections.Generic; using System.Reflection; namespace Shesha.Reflection { public class AssemblyFullNameComparer : EqualityComparer { - public override bool Equals(Assembly x, Assembly y) + public override bool Equals(Assembly? x, Assembly? y) { - return x.FullName == y.FullName; + return x != null && y != null && x.FullName == y.FullName || x == null && y == null; } public override int GetHashCode(Assembly obj) { - return obj.FullName.GetHashCode(); + return obj.GetRequiredFullName().GetHashCode(); } } } diff --git a/shesha-core/src/Shesha.Framework/Reflection/DocsByReflection.cs b/shesha-core/src/Shesha.Framework/Reflection/DocsByReflection.cs index bbeaeb208b..9250e980ef 100644 --- a/shesha-core/src/Shesha.Framework/Reflection/DocsByReflection.cs +++ b/shesha-core/src/Shesha.Framework/Reflection/DocsByReflection.cs @@ -23,7 +23,7 @@ public class DocsByReflection : ISingletonDependency /// /// The MethodInfo (reflection data ) of the member to find documentation for /// The XML fragment describing the method - public static XmlElement XMLFromMember(MethodInfo methodInfo) + public static XmlElement? XMLFromMember(MethodInfo methodInfo) { // Calculate the parameter string as this is in the member name in the XML string parametersString = ""; @@ -39,9 +39,9 @@ public static XmlElement XMLFromMember(MethodInfo methodInfo) //AL: 15.04.2008 ==> BUG-FIX remove “()” if parametersString is empty if (parametersString.Length > 0) - return XMLFromName(methodInfo.DeclaringType, 'M', methodInfo.Name + "(" + parametersString + ")"); + return XMLFromName(methodInfo.DeclaringType.NotNull(), 'M', methodInfo.Name + "(" + parametersString + ")"); else - return XMLFromName(methodInfo.DeclaringType, 'M', methodInfo.Name); + return XMLFromName(methodInfo.DeclaringType.NotNull(), 'M', methodInfo.Name); } /// @@ -49,10 +49,10 @@ public static XmlElement XMLFromMember(MethodInfo methodInfo) /// /// The MemberInfo (reflection data) or the member to find documentation for /// The XML fragment describing the member - public static XmlElement XMLFromMember(MemberInfo memberInfo) + public static XmlElement? XMLFromMember(MemberInfo memberInfo) { // First character [0] of member type is prefix character in the name in the XML - return XMLFromName(memberInfo.DeclaringType, memberInfo.MemberType.ToString()[0], memberInfo.Name); + return XMLFromName(memberInfo.DeclaringType.NotNull(), memberInfo.MemberType.ToString()[0], memberInfo.Name); } /// @@ -60,7 +60,7 @@ public static XmlElement XMLFromMember(MemberInfo memberInfo) /// /// Type to find the documentation for /// The XML fragment that describes the type - public static XmlElement XMLFromType(Type type) + public static XmlElement? XMLFromType(Type type) { // Prefix in type names is T return XMLFromName(type, 'T', ""); @@ -74,7 +74,7 @@ public static XmlElement XMLFromType(Type type) /// The prefix as seen in the name attribute in the documentation XML /// Where relevant, the full name qualifier for the element /// The member that has a name that describes the specified reflection element - private static XmlElement XMLFromName(Type type, char prefix, string name) + private static XmlElement? XMLFromName(Type type, char prefix, string name) { string fullName; @@ -87,13 +87,15 @@ private static XmlElement XMLFromName(Type type, char prefix, string name) fullName = prefix + ":" + type.FullName + "." + name; } - XmlDocument xmlDocument = XMLFromAssembly(type.Assembly); + var xmlDocument = XMLFromAssembly(type.Assembly); if (xmlDocument == null) return null; - XmlNode matchedElement = null; + XmlNode? matchedElement = null; - var members = xmlDocument["doc"]["members"]; + var members = xmlDocument["doc"]?["members"]; + if (members == null) + return null; foreach (XmlNode xmlElement in members) { @@ -108,23 +110,18 @@ private static XmlElement XMLFromName(Type type, char prefix, string name) } } - /*if (matchedElement == null) - { - throw new DocsByReflectionException("Could not find documentation for specified element", null); - }*/ - return matchedElement as XmlElement; } /// /// A cache used to remember Xml documentation for assemblies /// - static Dictionary cache = new Dictionary(); + static Dictionary cache = new(); /// /// A cache used to store failure exceptions for assembly lookups /// - static Dictionary failCache = new Dictionary(); + static Dictionary failCache = new(); /// /// Obtains the documentation file for the specified assembly @@ -133,7 +130,7 @@ private static XmlElement XMLFromName(Type type, char prefix, string name) /// The XML document /// This version uses a cache to preserve the assemblies, so that /// the XML file is not loaded and parsed on every single lookup - public static XmlDocument XMLFromAssembly(Assembly assembly) + public static XmlDocument? XMLFromAssembly(Assembly assembly) { if (failCache.ContainsKey(assembly)) { @@ -163,7 +160,7 @@ public static XmlDocument XMLFromAssembly(Assembly assembly) /// /// The assembly to find the XML document for /// The XML document - private static XmlDocument XMLFromAssemblyNonCached(Assembly assembly) + private static XmlDocument? XMLFromAssemblyNonCached(Assembly assembly) { var assemblyFilename = assembly.Location; @@ -201,7 +198,7 @@ class DocsByReflectionException : Exception /// /// The error message that explains the reason for the exception. /// The exception that is the cause of the current exception, or null if none. - public DocsByReflectionException(string message, Exception innerException) + public DocsByReflectionException(string message, Exception? innerException) : base(message, innerException) { diff --git a/shesha-core/src/Shesha.Framework/Reflection/ReflectionHelper.SubClasses.cs b/shesha-core/src/Shesha.Framework/Reflection/ReflectionHelper.SubClasses.cs new file mode 100644 index 0000000000..4273b7ad5e --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Reflection/ReflectionHelper.SubClasses.cs @@ -0,0 +1,25 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Shesha.Reflection +{ + public static partial class ReflectionHelper + { + public class PropertyValueAccessor + { + public PropertyInfo? PropInfo { get; private set; } + public object? Parent { get; private set; } + + [MemberNotNullWhen(true, nameof(PropInfo), nameof(Parent))] + public bool IsValueAvailable => Parent != null && PropInfo!= null; + + public object? Value => IsValueAvailable ? PropInfo.GetValue(Parent, null) : null; + + public PropertyValueAccessor(PropertyInfo? propInfo, object? parent) + { + PropInfo = propInfo; + Parent = parent; + } + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Reflection/ReflectionHelper.cs b/shesha-core/src/Shesha.Framework/Reflection/ReflectionHelper.cs index a192efe14f..9ee682041a 100644 --- a/shesha-core/src/Shesha.Framework/Reflection/ReflectionHelper.cs +++ b/shesha-core/src/Shesha.Framework/Reflection/ReflectionHelper.cs @@ -1,9 +1,11 @@ -using Abp.Dependency; -using Abp.Domain.Entities; +using Abp.Domain.Entities; using Abp.Reflection; using Shesha.Attributes; +using Shesha.Configuration.Runtime.Exceptions; using Shesha.Domain; using Shesha.Domain.Attributes; +using Shesha.EntityReferences; +using Shesha.Exceptions; using Shesha.Extensions; using Shesha.Modules; using Shesha.Services; @@ -13,6 +15,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Configuration; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -20,7 +23,7 @@ namespace Shesha.Reflection { - public static class ReflectionHelper + public static partial class ReflectionHelper { /// /// Returns true if the is a closed generic of the type @@ -69,49 +72,46 @@ public static bool IsReadOnly(this PropertyInfo memberInfo, bool inherit = false /// public static bool HasAttribute(this MemberInfo memberInfo, bool inherit = false) where T : Attribute { - return memberInfo.GetAttribute() != null; + return memberInfo.GetAttributeOrNull() != null; } - // todo: added unique check - public static T GetUniqueAttribute(this MemberInfo memberInfo) where T : Attribute + public static T? GetUniqueAttribute(this MemberInfo memberInfo) where T : Attribute { - return GetAttribute(memberInfo); + return GetAttributeOrNull(memberInfo); } - public static T GetAttribute(this MemberInfo memberInfo, bool inherit = false) where T : Attribute + public static T? GetAttributeOrNull(this MemberInfo memberInfo, bool inherit = false) where T : Attribute { return memberInfo.GetCustomAttributes(typeof(T), inherit).Cast().FirstOrDefault(); } - public static T GetAttribute(this Type type, bool inherit = false) where T : Attribute + public static T GetAttribute(this MemberInfo memberInfo, bool inherit = false) where T : Attribute + { + return memberInfo.GetAttributeOrNull(inherit) ?? throw new MemberHasNoAttributeException(memberInfo, typeof(T)); + } + + public static T? GetAttributeOrNull(this Type type, bool inherit = false) where T : Attribute { return type.GetCustomAttributes(typeof(T), inherit).Cast().FirstOrDefault(); } + public static T GetAttribute(this Type type, bool inherit = false) where T : Attribute + { + return type.GetAttributeOrNull(inherit) ?? throw new TypeHasNoAttributeException(type, typeof(T)); + } + /// - /// Returns the value of the property specified. + /// Returns extended info about specified property. /// /// Object whose property value is to be retreived. - /// Name of the property or property hierarchy - /// e.g. 'Property1.SubProperty2.SubSubProperty3' - /// Returns the parent object the last property in the hierarchy. - /// Returns null if did not manage to reach the end of the hierarchy because of null values - /// along the way. - /// Returns the of - /// the last property in the property hierachy. - /// - public static object GetPropertyValue(object obj, string propertyName, out object parent, - out PropertyInfo propInfo) - { - //Entity propertyEntity; - propInfo = GetProperty(obj, propertyName, out parent); - if (parent != null) - return propInfo.GetValue(parent, null); - else - return null; + /// Name of the property or property hierarchy + public static PropertyValueAccessor GetPropertyValueAccessor(object obj, string propertyName) + { + var propInfo = GetPropertyOrNull(obj, propertyName, out var parent); + return new PropertyValueAccessor(propInfo, parent); } - public static object GetPropertyValue(object obj, string propertyName, object defaultValue) + public static object? GetPropertyValue(object obj, string propertyName, object? defaultValue) { if (obj == null) return null; @@ -120,12 +120,22 @@ public static object GetPropertyValue(object obj, string propertyName, object de return propInfo == null ? defaultValue : propInfo.GetValue(obj, null) ?? defaultValue; } - public static object GetPropertyValue(IEntity obj, string propertyName) + #region GetProperty + + public static PropertyInfo? GetProperty(object entity, string propertyName) { - return GetPropertyValue(obj, propertyName, out var parent, out var propInfo); + return GetPropertyOrNull(entity, propertyName, out var propertyEntity); } - #region GetProperty + public static PropertyInfo GetProperty(object entity, string propertyName, out object propertyEntity) + { + var property = GetPropertyOrNull(entity, propertyName, out var owner); + if (property == null) + throw new PropertyNotFoundException(entity.GetType(), propertyName); + + propertyEntity = owner.NotNull(); + return property; + } /// /// Returns the PropertyInfo for the specified property on the specified Entity. @@ -139,25 +149,15 @@ public static object GetPropertyValue(IEntity obj, string propertyName) /// child object e.g. 'Parent.ReferencedChildEntity.ChildEntityPropertyName', then will return the /// child entity the property belongs to i.e. 'ReferencedChildEntity' from the example. /// Return the requested PropertyInfo. - public static PropertyInfo GetProperty(object entity, string propertyName, out object propertyEntity) - { - return GetProperty(entity, null, propertyName, out propertyEntity); - } - - public static PropertyInfo GetProperty(object entity, string propertyName) - { - return GetProperty(entity, null, propertyName, out var propertyEntity); - } - - private static PropertyInfo GetProperty(object entity, Type type, string propertyName, out object propertyEntity) + public static PropertyInfo? GetPropertyOrNull(object entity, string propertyName, out object? propertyEntity) { var propTokens = propertyName.Split('.'); var currentEntity = entity; - Type currentType = (entity != null) ? entity.GetType() : type; + Type currentType = entity.GetType(); for (int i = 0; i < propTokens.Length; i++) { - PropertyInfo propInfo; + PropertyInfo? propInfo; var entityType = StripCastleProxyType(currentType); var properties = entityType.GetProperties(); try @@ -197,62 +197,27 @@ private static PropertyInfo GetProperty(object entity, Type type, string propert throw new Exception($"Property '{propertyName}' does not exist on entity type '{currentType.FullName}'"); } - public static PropertyInfo FindHighestLevelProperty(string propertyName, Type entityType) - { - //PropertyInfo propInfo; + private static PropertyInfo? GetHighestLevelPropertyOrNull(string propertyName, Type entityType) + { var propInfo = entityType.GetProperties() .FirstOrDefault(prop => prop.Name.Equals(propertyName, StringComparison.InvariantCultureIgnoreCase) && prop.DeclaringType == entityType); - //var propInfo = entityType.GetProperty(propertyName); if (propInfo == null) - return FindHighestLevelProperty(propertyName, entityType.BaseType); + return entityType.BaseType != null + ? GetHighestLevelPropertyOrNull(propertyName, entityType.BaseType) + : null; else return propInfo; } - /// - /// Gets the PropertyInfo for the specified property. - /// WARNING!!!: This will return the PropertyInfo where the Declaring Type is the base class. - /// This may therefore cause problems if you wish to retreive Attribute information (e.g. ReferenceList attribute) - /// from sub-classes. - /// - public static PropertyInfo GetProperty(Expression> property) - { - return GetProperty(property); - } - - /// - /// Gets the PropertyInfo for the specified property. - /// WARNING!!!: This will return the PropertyInfo where the Declaring Type is the base class. - /// This may therefore cause problems if you wish to retreive Attribute information (e.g. ReferenceList attribute) - /// from sub-classes. - /// - public static PropertyInfo GetProperty(Expression> property) - { - MemberExpression memberExpression = GetMemberExpression(property); - var propInfo = memberExpression.Member as PropertyInfo; - - return propInfo; - } - - public static MemberExpression GetMemberExpression(Expression> expression) - { - return GetMemberExpression(expression); - } - - public static MemberExpression GetMemberExpression( - Expression> expression) + /// + /// Find property with name at the highest position of class hierarchy of type + /// + /// + public static PropertyInfo FindHighestLevelProperty(string propertyName, Type entityType) { - MemberExpression memberExpression; - if (expression.Body.NodeType == ExpressionType.MemberAccess) - memberExpression = expression.Body as MemberExpression; - else if (expression.Body.NodeType == ExpressionType.Convert) - memberExpression = ((UnaryExpression)expression.Body).Operand as MemberExpression; - else - throw new ArgumentException( - $"Expressions of type '{Enum.GetName(typeof(ExpressionType), expression.NodeType)}' are not supported"); - return memberExpression; + return GetHighestLevelPropertyOrNull(propertyName, entityType) ?? throw new PropertyNotFoundException(entityType, propertyName); } #endregion @@ -288,26 +253,17 @@ public static Type StripCastleProxyType(this Type type) /// Returns the of property marked with the /// specified attribute. Returns Null if no property was marked with the specified attribute. /// If more than one property is marked with the attribute a is thrown. - public static PropertyInfo FindPropertyWithUniqueAttribute(Type type, Type attributeType) + public static PropertyInfo? FindPropertyWithUniqueAttribute(Type type, Type attributeType) { return FindPropertyWithUniqueAttribute(type, attributeType, null); } - public static object DynamicCast(object entity, Type to) - { - var openCast = typeof(ReflectionHelper).GetMethod("Cast", BindingFlags.Static | BindingFlags.Public); - var closeCast = openCast.MakeGenericMethod(to); - - return closeCast.Invoke(entity, new[] { entity }); - } - - public static T Cast(object entity) where T : class + public static T? Cast(object entity) where T : class { return entity as T; } - public static PropertyInfo FindPropertyWithUniqueAttribute(Type type, Type attributeType, - Type expectedPropertyType) + public static PropertyInfo? FindPropertyWithUniqueAttribute(Type type, Type attributeType, Type? expectedPropertyType) { type = StripCastleProxyType(type); @@ -329,7 +285,6 @@ public static PropertyInfo FindPropertyWithUniqueAttribute(Type type, Type attri if (expectedPropertyType != null && !(markedProperties[0].PropertyType == expectedPropertyType || markedProperties[0].PropertyType.IsSubclassOf(expectedPropertyType))) - //Could also try the following additional flexibility: !expectedPropertyType.IsAssignableFrom(markedProperties[0].PropertyType { throw new ConfigurationErrorsException( $"Property '{markedProperties[0].Name}' marked with attribute '{attributeType.FullName}' must be of type '{expectedPropertyType.Name}'."); @@ -359,8 +314,8 @@ public static bool IsNullableType(this Type type) public static bool IsNullable(this PropertyInfo property) { return property.PropertyType.IsEntityType() - ? !property.HasAttribute() - : property.PropertyType.IsNullableType() || property.HasAttribute(); + ? !property.HasAttribute() + : property.PropertyType.IsNullableType() || property.HasAttribute(); } /// @@ -369,9 +324,7 @@ public static bool IsNullable(this PropertyInfo property) /// public static Type GetUnderlyingTypeIfNullable(this Type type) { - return IsNullableType(type) - ? Nullable.GetUnderlyingType(type) - : type; + return Nullable.GetUnderlyingType(type) ?? type; } /// @@ -381,17 +334,7 @@ public static Type GetUnderlyingTypeIfNullable(this Type type) /// public static bool IsEnumType(this Type type) { - return GetNonNullableType(type).IsEnum; - } - - /// - /// Returns underlying type `T` if the type is Nullable{T} - /// - /// - /// - public static Type GetNonNullableType(Type type) - { - return IsNullableType(type) ? type.GetGenericArguments()[0] : type; + return GetUnderlyingTypeIfNullable(type).IsEnum; } /// @@ -422,18 +365,18 @@ public static string GetPropertyName(Expression - returns /// If the member is decorated with - returns /// - public static string GetCategory(this MemberInfo member) + public static string? GetCategory(this MemberInfo member) { - var categoryAttribute = member.GetAttribute(); + var categoryAttribute = member.GetAttributeOrNull(); if (categoryAttribute != null) return categoryAttribute.Category; - var displayAttribute = member.GetAttribute(); + var displayAttribute = member.GetAttributeOrNull(); return displayAttribute?.GetGroupName(); } @@ -466,7 +408,7 @@ public static string GetCategory(this MemberInfo member) /// public static string GetDisplayName(this MemberInfo property) { - var displayAttribute = property.GetAttribute(); + var displayAttribute = property.GetAttributeOrNull(); return displayAttribute?.GetName() ?? property.Name.ToFriendlyName(); } @@ -474,10 +416,10 @@ public static string GetDisplayName(this MemberInfo property) /// /// Return description of the specified member /// - public static string GetDescription(this MemberInfo property) + public static string? GetDescription(this MemberInfo property) { - var descriptionAttribute = property.GetAttribute(); - var displayAttribute = property.GetAttribute(); + var descriptionAttribute = property.GetAttributeOrNull(); + var displayAttribute = property.GetAttributeOrNull(); var description = descriptionAttribute?.Description; if (string.IsNullOrWhiteSpace(description)) @@ -489,35 +431,14 @@ public static string GetDescription(this MemberInfo property) /// /// Returns description of enum item /// - public static string GetEnumDescription(Type enumType, Int64? itemValue) + public static string? GetEnumDescription(Type enumType, Int64? itemValue) { if (!itemValue.HasValue) return ""; var itemName = Enum.GetName(enumType, itemValue); if (string.IsNullOrEmpty(itemName)) return ""; - var fi = enumType.GetField(itemName); - - var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); - if (attributes.Length > 0) - return attributes[0].Description; - - var displayAttributes = (DisplayAttribute[])fi.GetCustomAttributes(typeof(DisplayAttribute), false); - if (displayAttributes.Any()) - return displayAttributes[0].Name; - - return itemName; - } - - /// - /// Returns description of enum item - /// - public static string GetEnumDescription(Type enumType, string itemName) - { - var fi = enumType.GetField(itemName); - - if (fi == null) - return null; + var fi = enumType.GetRequiredField(itemName); var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); if (attributes.Length > 0) @@ -530,19 +451,6 @@ public static string GetEnumDescription(Type enumType, string itemName) return itemName; } - /// - /// Returns attribute of enum item - /// - public static TAttribute GetEnumItemAttribute(Type enumType, string itemName) - where TAttribute : Attribute - { - var fi = enumType.GetField(itemName); - return - (TAttribute)fi.GetCustomAttributes( - typeof(TAttribute), - false).FirstOrDefault(); - } - /// /// Indicates is the specified property is a multivalue Reference List property /// @@ -566,15 +474,15 @@ public static bool IsReferenceListProperty(this MemberInfo memberInfo) /// /// /// - public static ReferenceListIdentifier GetReferenceListIdentifierOrNull(this MemberInfo memberInfo) + public static ReferenceListIdentifier? GetReferenceListIdentifierOrNull(this MemberInfo memberInfo) { // 1. check ReferenceListAttribute on the property - var refListAttribute = memberInfo.GetAttribute(); + var refListAttribute = memberInfo.GetAttributeOrNull(); if (refListAttribute != null) return refListAttribute.GetReferenceListIdentifier(memberInfo); // 2. check MultiValueReferenceListAttribute on the property - var multiValueAttribute = memberInfo.GetAttribute(); + var multiValueAttribute = memberInfo.GetAttributeOrNull(); if (multiValueAttribute != null) return multiValueAttribute.GetReferenceListIdentifier(memberInfo); @@ -583,7 +491,7 @@ public static ReferenceListIdentifier GetReferenceListIdentifierOrNull(this Memb return null; // 3. check ReferenceListAttribute on the enum type - refListAttribute = propertyType.GetAttribute(); + refListAttribute = propertyType.GetAttributeOrNull(); return refListAttribute?.GetReferenceListIdentifier(propertyType); } @@ -603,47 +511,18 @@ private static Type GetPropertyOrFieldType(this MemberInfo propertyOrField) "Expected PropertyInfo or FieldInfo; found :" + propertyOrField.MemberType); } - /// - /// Returns list of selected values for a flag enum - /// - public static IEnumerable FlagEnumToListOfValues(Type enumType, long valueToParse) - { - foreach (var value in Enum.GetValues(enumType).Cast().Select(o => new {Item = o, Value = Convert.ToInt64(o) })) - { - if (GetEnumItemIsVisible(value.Item) && (valueToParse & value.Value) != 0) - yield return value.Value; - } - } - #region Class Uid /// /// Returns class Uid or null of the specified class. Class Uid can be specified using /// - public static string GetClassUid(this Type type) + public static string? GetClassUid(this Type type) { - return type.GetAttribute()?.Uid; + return type.GetAttributeOrNull()?.Uid; } #endregion - /// - /// Returns whether this enum item must be shown in enum dropdowns or not - /// - public static bool GetEnumItemIsVisible(TEnum value) - { - var fi = value.GetType().GetField(value.ToString()); - - if (fi == null) - return false; - - var displayAttributes = (DisplayAttribute[])fi.GetCustomAttributes(typeof(DisplayAttribute), false); - var autoGenerateValue = displayAttributes.Any() - ? displayAttributes[0].GetAutoGenerateField() - : null; - return autoGenerateValue ?? true; // By default, show all items - } - /// /// Search property with specified name in the current type. Supports dot notation /// @@ -651,14 +530,14 @@ public static bool GetEnumItemIsVisible(TEnum value) /// Name of property, supports dot notation /// Set to true to compare property names in camel case /// - public static PropertyInfo GetProperty(this Type type, string propertyName, bool useCamelCase = false) + public static PropertyInfo? GetProperty(this Type type, string propertyName, bool useCamelCase = false) { var propTokens = propertyName.Split('.'); var currentType = type; for (int i = 0; i < propTokens.Length; i++) { - PropertyInfo propInfo; + PropertyInfo? propInfo; var containerType = currentType.StripCastleProxyType(); try { @@ -676,7 +555,7 @@ public static PropertyInfo GetProperty(this Type type, string propertyName, bool { // Property may have been overriden using the 'new' keyword hence there are multiple properties with the same name. // Will look for the one declared at the highest level. - propInfo = ReflectionHelper.FindHighestLevelProperty(propTokens[i], containerType); + propInfo = FindHighestLevelProperty(propTokens[i], containerType); } if (propInfo == null) @@ -710,7 +589,7 @@ public static IEnumerable GetGenericInterfaces(this Type type, Type interf /// /// /// - public static string GetConfigurableModuleName(this Type type) + public static string? GetConfigurableModuleName(this Type type) { return type.Assembly.GetConfigurableModuleName(); } @@ -720,7 +599,7 @@ public static string GetConfigurableModuleName(this Type type) /// /// /// - public static Type GetConfigurableModuleType(this Type type) + public static Type? GetConfigurableModuleType(this Type type) { return type.Assembly.GetConfigurableModuleType(); } @@ -729,7 +608,7 @@ public static Type GetConfigurableModuleType(this Type type) /// Returns information about the configurable module current belongs to /// /// - public static SheshaModuleInfo GetConfigurableModuleInfo(this Type type) + public static SheshaModuleInfo? GetConfigurableModuleInfo(this Type type) { return type.Assembly.GetConfigurableModuleInfo(); } @@ -744,11 +623,17 @@ public static List GetPropertiesWithoutHidden(this Type containerT { var propertiesWithoutHiddenOnes = containerType.GetProperties(bindingAttr) .GroupBy(prop => prop.Name) - .Select(group => group.Aggregate((mostSpecificProp, other) => mostSpecificProp.DeclaringType.IsSubclassOf(other.DeclaringType) ? mostSpecificProp : other)) + .Select(group => group.Aggregate((mostSpecificProp, other) => mostSpecificProp.DeclaringType.NotNull().IsSubclassOf(other.DeclaringType.NotNull()) ? mostSpecificProp : other)) .ToList(); return propertiesWithoutHiddenOnes; } + /// + /// Get extension methods of the specified declared in a specified + /// + /// Assembly to search extension methods in + /// Type to search extended methods for + /// public static List GetExtensionMethods(Assembly assembly, Type extendedType) { var isGenericTypeDefinition = extendedType.IsGenericType && extendedType.IsTypeDefinition; @@ -786,5 +671,118 @@ public static List GetExtensionTypes(Assembly assembly, Type extendedType) var methods = GetExtensionMethods(assembly, extendedType); return methods.Select(method => method.DeclaringType).Distinct().OfType().ToList(); } + + /// + /// Cast to type . An exception will be throws on unsuccessfull casting + /// + /// + /// + /// + /// + public static TDestination ForceCastAs(this object? source) where TDestination : class + { + if (source is TDestination dest) + return dest; + + if (source is GenericEntityReference genericEntity && typeof(Entity).IsAssignableFrom(typeof(TDestination))) { + var entity = (Entity)genericEntity.NotNull(); + return entity.ForceCastAs(); + } + + throw new InvalidCastException(source != null + ? $"Failed to cast value of type '{source.GetType().FullName}' to type '{typeof(TDestination).FullName}'" + : "Failed to cast null to type '{typeof(TDestination).FullName}'" + ); + } + + /// + /// Invoke method and return typed result. Throws exception when result is of wrong type + /// + /// + /// + /// + /// + /// + public static TResult? Invoke(this MethodBase method, object? obj, object?[]? parameters) where TResult : class + { + var result = method.Invoke(obj, parameters); + return result != null + ? result.ForceCastAs() + : null; + } + + public static T NotNull([NotNull]this T? value, string message = "Value must not be null") + { + return value ?? throw new Exception(message); + } + + /// + /// Guarantee that current string is not null or whitespace + /// + /// + /// + /// + /// + public static string NotNullOrWhiteSpace([NotNull] this string? value, string message = "String must not be null or empty") + { + return !string.IsNullOrWhiteSpace(value) + ? value + : throw new Exception(message); + } + + /// + /// Get method by name. Throws if method not found + /// + /// Type to search method in + /// Method name + /// Argument types + /// + /// + public static MethodInfo GetRequiredMethod(this Type type, string name, Type[] types) + { + return type.GetMethod(name, types) ?? throw new MethodNotFoundException(type, name); + } + + /// + /// Get method by name. Throws if method not found + /// + /// Type to search method in + /// Method name + /// A bitwise combination of the enumeration values that specify how the search is conducted. + /// + /// + public static MethodInfo GetRequiredMethod(this Type type, string name, BindingFlags bindingAttr) + { + return type.GetMethod(name, bindingAttr) ?? throw new MethodNotFoundException(type, name); + } + + /// + /// Get method by name. Throws if method not found + /// + /// Type to search method in + /// Method name + /// + /// + public static MethodInfo GetRequiredMethod(this Type type, string name) + { + return type.GetMethod(name) ?? throw new MethodNotFoundException(type, name); + } + + /// + /// Get property by name. Throws if property not found + /// + /// Type to search property in + /// Property name + /// + /// + public static PropertyInfo GetRequiredProperty(this Type type, string propertyName) + { + return type.GetProperty(propertyName) ?? throw new PropertyNotFoundException(type, propertyName); + } + + public static FieldInfo GetRequiredField(this Type type, string fieldName) + { + return type.GetField(fieldName) ?? throw new FieldNotFoundException(type, fieldName); + } } } diff --git a/shesha-core/src/Shesha.Framework/Scheduler/Attributes/ScheduledJobAttribute.cs b/shesha-core/src/Shesha.Framework/Scheduler/Attributes/ScheduledJobAttribute.cs index ae934202c0..cc8a771a20 100644 --- a/shesha-core/src/Shesha.Framework/Scheduler/Attributes/ScheduledJobAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Scheduler/Attributes/ScheduledJobAttribute.cs @@ -1,6 +1,6 @@ -using System; -using Shesha.Scheduler.Domain.Enums; +using Shesha.Scheduler.Domain.Enums; using Shesha.Utilities; +using System; namespace Shesha.Scheduler.Attributes { @@ -14,16 +14,22 @@ public class ScheduledJobAttribute : Attribute /// public Guid Uid { get; set; } - public string Description { get; set; } + public string? Description { get; set; } - public string CronString { get; set; } + public string? CronString { get; set; } public StartUpMode StartupMode { get; set; } public LogMode LogMode { get; set; } - public string LogFolder { get; set; } + public string? LogFolder { get; set; } - public ScheduledJobAttribute(string uid, StartUpMode startupMode = StartUpMode.Automatic, string cronString = null, string description = null, LogMode logMode = LogMode.FileSystem, string logFolder = null) + public ScheduledJobAttribute( + string uid, + StartUpMode startupMode = StartUpMode.Automatic, + string? cronString = null, + string? description = null, + LogMode logMode = LogMode.FileSystem, + string? logFolder = null) { Uid = uid.ToGuid(); StartupMode = startupMode; diff --git a/shesha-core/src/Shesha.Framework/Scheduler/Domain/ScheduledJob.cs b/shesha-core/src/Shesha.Framework/Scheduler/Domain/ScheduledJob.cs index 84c4e02124..4a71d24eea 100644 --- a/shesha-core/src/Shesha.Framework/Scheduler/Domain/ScheduledJob.cs +++ b/shesha-core/src/Shesha.Framework/Scheduler/Domain/ScheduledJob.cs @@ -22,14 +22,14 @@ public class ScheduledJob: FullAuditedEntity /// Namespace /// [StringLength(300, MinimumLength = 3)] - public virtual string JobNamespace { get; set; } + public virtual string? JobNamespace { get; set; } /// /// Description of the job /// [DataType(DataType.MultilineText)] [StringLength(int.MaxValue)] - public virtual string JobDescription { get; set; } + public virtual string? JobDescription { get; set; } /// /// Job status (Active/Inactive). Is used to switch job on/off. @@ -50,7 +50,7 @@ public class ScheduledJob: FullAuditedEntity /// Folder to store the logs /// [StringLength(int.MaxValue)] - public virtual string LogFolder { get; set; } + public virtual string? LogFolder { get; set; } /// /// Used to specify if job class uses generic definition of ScheduledJobBase or default. diff --git a/shesha-core/src/Shesha.Framework/Scheduler/Domain/ScheduledJobExecution.cs b/shesha-core/src/Shesha.Framework/Scheduler/Domain/ScheduledJobExecution.cs index ad044b595c..2abc752e6e 100644 --- a/shesha-core/src/Shesha.Framework/Scheduler/Domain/ScheduledJobExecution.cs +++ b/shesha-core/src/Shesha.Framework/Scheduler/Domain/ScheduledJobExecution.cs @@ -30,33 +30,33 @@ public class ScheduledJobExecution : FullAuditedEntity /// Log file path /// [StringLength(500)] - public virtual string LogFilePath { get; set; } + public virtual string? LogFilePath { get; set; } /// /// Stored file log (if enabled on the job) /// - public virtual StoredFile LogFile { get; set; } + public virtual StoredFile? LogFile { get; set; } /// /// User who started the job (if it was done manually) /// - public virtual User StartedBy { get; set; } + public virtual User? StartedBy { get; set; } /// /// Executed scheduled job /// - public virtual ScheduledJob Job { get; set; } + public virtual ScheduledJob Job { get; set; } = default!; /// /// Trigger by which the job was executed. May be null for manual jobs /// - public virtual ScheduledJobTrigger Trigger { get; set; } + public virtual ScheduledJobTrigger? Trigger { get; set; } /// /// Error message /// [StringLength(int.MaxValue)] - public virtual string ErrorMessage { get; set; } + public virtual string? ErrorMessage { get; set; } /// /// Status of the execution @@ -67,11 +67,11 @@ public class ScheduledJobExecution : FullAuditedEntity /// /// Parent execution of the current one. Is used for execution retries /// - public virtual ScheduledJobExecution ParentExecution { get; set; } + public virtual ScheduledJobExecution? ParentExecution { get; set; } /// /// This is a JSON string containing the job stats. /// - public virtual ScheduledJobStatistic JobStatistics { get; set; } + public virtual ScheduledJobStatistic? JobStatistics { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Scheduler/ScheduledJobBase.cs b/shesha-core/src/Shesha.Framework/Scheduler/ScheduledJobBase.cs index faffa88938..2a2190541c 100644 --- a/shesha-core/src/Shesha.Framework/Scheduler/ScheduledJobBase.cs +++ b/shesha-core/src/Shesha.Framework/Scheduler/ScheduledJobBase.cs @@ -2,6 +2,7 @@ using Abp.Domain.Repositories; using Abp.Domain.Uow; using Abp.Extensions; +using Castle.Core.Logging; using Microsoft.Extensions.Configuration; using Shesha.Authorization.Users; using Shesha.Domain; @@ -30,7 +31,7 @@ namespace Shesha.Scheduler { public virtual T JobStatistics { get; private set; } - protected ScheduledJobBase() + protected ScheduledJobBase(): base() { JobStatistics = new T(); } @@ -44,7 +45,7 @@ public override void SuccessExecutionInfo() e.Status = ExecutionStatus.Completed; if (_logMode == LogMode.StoredFile) - e.LogFile = AsyncHelper.RunSync(() => { return CreateStoredFileLogAsync(LogFilePath, LogFileName, LogFolderName); }); + e.LogFile = AsyncHelper.RunSync(async () => { return await CreateStoredFileLogAsync(); }); if (JobStatistics != null) e.JobStatistics = JobStatistics; @@ -53,19 +54,26 @@ public override void SuccessExecutionInfo() } public abstract class ScheduledJobBase - { + { + protected ScheduledJobBase() + { + #region optional dependencies, they are initialized by IoC after constructor + UserRepository = default!; + PathHelper = default!; + JobRepository = default!; + TriggerRepository = default!; + JobExecutionRepository = default!; + UnitOfWorkManager = default!; + #endregion + } + public IRepository UserRepository { get; set; } - private IStoredFileService _storedFileService => IocManager?.Resolve(); + private IStoredFileService _storedFileService => IocManager.Resolve(); public string Name => GetType().StripCastleProxyType().Name; public string TriggerName => $"{Name}_Trigger"; - /// - /// Type of the parameters class - /// - public virtual Type ParametersType { get; } - private Guid? _id; public Guid Id { @@ -73,7 +81,7 @@ public Guid Id { if (!_id.HasValue) { - var attribute = GetType().GetAttribute(true); + var attribute = GetType().GetAttributeOrNull(true); if (attribute == null) throw new Exception($"ScheduledJobAttribute should be applied on the {GetType().Name} class"); @@ -88,7 +96,7 @@ protected LogMode _logMode { get { - var attribute = GetType().GetAttribute(true); + var attribute = GetType().GetAttributeOrNull(true); if (attribute == null) throw new Exception($"ScheduledJobAttribute should be applied on the {GetType().Name} class"); return attribute.LogMode; @@ -98,7 +106,7 @@ protected LogMode _logMode /// /// IoC manager /// - public IIocManager IocManager { get; set; } + public IIocManager IocManager { get; set; } = default!; /// /// Path Helper @@ -125,8 +133,7 @@ protected LogMode _logMode /// public IRepository JobExecutionRepository { get; set; } - protected readonly CancellationTokenSource CancellationTokenSource; - protected CancellationToken CancellationToken; + protected CancellationToken? CancellationToken; /// /// Reference to the logger to write logs. @@ -136,21 +143,21 @@ protected LogMode _logMode /// /// Default logger, it used when instance logger is not set /// - private ILogger _defaultLogger; + private ILogger? _defaultLogger; - public ILogger Log { get; set; } + public ILogger Log { get; set; } = NullLogger.Instance; public Guid JobExecutionId { get; set; } - public string LogFileName { get; set; } - public string LogFilePath { get; set; } - public string LogFolderPath + public string? LogFileName { get; set; } + public string? LogFilePath { get; set; } + public string? LogFolderPath { get { - var config = IocManager?.Resolve(); + var config = IocManager.Resolve(); var logFolderPath = config.GetValue("DefaultLogFolder"); - if (logFolderPath.IsNullOrEmpty() || logFolderPath.IsNullOrWhiteSpace()) + if (string.IsNullOrWhiteSpace(logFolderPath) || logFolderPath.IsNullOrWhiteSpace()) { logFolderPath = "~/App_Data/logs/jobs"; } @@ -163,11 +170,13 @@ public string LogFolderName { get { - var attribute = GetType().GetAttribute(true); + var attribute = GetType().GetAttributeOrNull(true); if (attribute == null) throw new Exception($"ScheduledJobAttribute should be applied on the {GetType().Name} class"); - string logFolder = (!attribute.LogFolder.IsNullOrEmpty() && !attribute.LogFolder.IsNullOrWhiteSpace()) ? attribute.LogFolder: Name; + string logFolder = !string.IsNullOrWhiteSpace(attribute.LogFolder) + ? attribute.LogFolder + : Name; return logFolder; } @@ -185,14 +194,13 @@ public string LogFolderName [UnitOfWork] public async Task ExecuteAsync(Guid executionId, Int64? startedById, CancellationToken cancellationToken) { + CancellationToken = cancellationToken; var task = Task.Run(async () => { try { - //SaveJobExecutionIdForLogging(); - - var method = this.GetType().GetMethod(nameof(DoExecuteAsync)); - var unitOfWorkAttribute = method.GetAttribute(true); + var method = this.GetType().GetRequiredMethod(nameof(DoExecuteAsync)); + var unitOfWorkAttribute = method.GetAttributeOrNull(true); if (unitOfWorkAttribute != null && unitOfWorkAttribute.IsDisabled) { @@ -231,44 +239,22 @@ public async Task ExecuteAsync(Guid executionId, Int64? startedById, Cancellatio await OnFailAsync(e); } } - }, CancellationToken); + }, cancellationToken); await task; + CancellationToken = null; } - /* - protected Tp GetParamsOrDefault(IJobExecutionContext context, Tp defaultValue = null) where Tp : class, new() - { - return GetParams(context, false) ?? new Tp(); - } - - protected Tp GetParams(IJobExecutionContext context, bool throwIfEmpty = true) where Tp : class - { - var explicitParams = context.MergedJobDataMap.Get("explicitParams") as Tp; - if (explicitParams != null) - return explicitParams; - - var trigger = GetTrigger(context); - var parametersJson = trigger.GetJsonParameters() as Tp; - if (parametersJson == null && throwIfEmpty) - throw new Exception("Settings are empty"); - return parametersJson; - } - */ - - public virtual async Task OnSuccessAsync() { try { using (var uow = UnitOfWorkManager.Begin()) { - var trigger = GetTrigger(); + var trigger = GetTriggerOrNull(); if (trigger == null) return; - // todo: implement notifications - await uow.CompleteAsync(); } } @@ -284,12 +270,10 @@ public virtual async Task OnFailAsync(Exception ex) { using (var uow = UnitOfWorkManager.Begin()) { - var trigger = GetTrigger(); + var trigger = GetTriggerOrNull(); if (trigger == null) return; - // todo: implement notifications - await uow.CompleteAsync(); } } @@ -304,7 +288,7 @@ protected ScheduledJobExecution GetExecutionRecord() return JobExecutionRepository.Get(JobExecutionId); } - protected ScheduledJobTrigger GetTrigger() + protected ScheduledJobTrigger? GetTriggerOrNull() { return TriggerId.HasValue ? TriggerRepository.Get(TriggerId.Value) @@ -330,7 +314,7 @@ public virtual void SuccessExecutionInfo() e.Status = ExecutionStatus.Completed; if (_logMode == LogMode.StoredFile) - e.LogFile = AsyncHelper.RunSync(() => { return CreateStoredFileLogAsync(LogFilePath, LogFileName, LogFolderName); }); + e.LogFile = AsyncHelper.RunSync(async () => { return await CreateStoredFileLogAsync(); }); }); } @@ -390,7 +374,7 @@ public async Task CreateExecutionRecordAsync(Guid executionId, Action ex.Id == executionId).FirstOrDefaultAsync(); - ScheduledJobExecution jobExecution = null; + ScheduledJobExecution? jobExecution = null; if (existingExecution != null && existingExecution.Status == ExecutionStatus.Enqueued) { @@ -400,7 +384,7 @@ public async Task CreateExecutionRecordAsync(Guid executionId, Action CreateExecutionRecordAsync(Guid executionId, Action CreateStoredFileLogAsync(string logPath, string filename, string folder = "") + protected async Task CreateStoredFileLogAsync() { - using var stream = File.OpenRead(logPath); - var storedFile = await _storedFileService.SaveFileAsync(stream, filename, file => - { - file.Folder = folder; - }); - return storedFile; + if (string.IsNullOrWhiteSpace(LogFilePath)) { + Logger.Warn($"{nameof(LogFilePath)} is empty, creation of log file skipped"); + return null; + } + + if (string.IsNullOrWhiteSpace(LogFileName)) + { + Logger.Warn($"{nameof(LogFileName)} is empty, creation of log file skipped"); + return null; + } + + if (!File.Exists(LogFilePath)) + { + Logger.Error($"Log file `{nameof(LogFilePath)}` is empty, creation of log file skipped"); + return null; + } + + try + { + using var stream = File.OpenRead(LogFilePath); + var storedFile = await _storedFileService.SaveFileAsync(stream, LogFileName, file => + { + file.Folder = LogFolderName; + }); + return storedFile; + } + catch (Exception e) + { + Logger.Error($"Failed to save log file `{LogFilePath}` of the job `{this.GetType().FullName}`", e); + return null; + } } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Services/CastleProxyStripper.cs b/shesha-core/src/Shesha.Framework/Services/CastleProxyStripper.cs index d91ab16119..c0370c2069 100644 --- a/shesha-core/src/Shesha.Framework/Services/CastleProxyStripper.cs +++ b/shesha-core/src/Shesha.Framework/Services/CastleProxyStripper.cs @@ -1,5 +1,6 @@ using Abp.Dependency; using Castle.DynamicProxy; +using JetBrains.Annotations; using System; using System.Linq; @@ -8,12 +9,13 @@ namespace Shesha.Services /// /// Castle.Core proxy stripper /// + [UsedImplicitly] public class CastleProxyStripper : IProxyStripper, ITransientDependency { public Type StripProxy(Type type) { #pragma warning disable 612, 618 - if (type.GetInterfaces().Any(i => i == typeof(IProxyTargetAccessor))) + if (type.GetInterfaces().Any(i => i == typeof(IProxyTargetAccessor)) && type.BaseType != null) { return type.BaseType; } diff --git a/shesha-core/src/Shesha.Framework/Services/ConfigurationItems/ConfigurationItemImportBase.cs b/shesha-core/src/Shesha.Framework/Services/ConfigurationItems/ConfigurationItemImportBase.cs index 2d17e86a32..bb2501689d 100644 --- a/shesha-core/src/Shesha.Framework/Services/ConfigurationItems/ConfigurationItemImportBase.cs +++ b/shesha-core/src/Shesha.Framework/Services/ConfigurationItems/ConfigurationItemImportBase.cs @@ -1,11 +1,13 @@ -using Shesha.ConfigurationItems.Distribution; +using Abp.Domain.Repositories; +using Abp.Domain.Uow; +using Newtonsoft.Json; +using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; -using System.Threading.Tasks; -using System; -using Abp.Domain.Repositories; using Shesha.Domain.ConfigurationItems; -using Abp.Domain.Uow; +using System; using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; namespace Shesha.Services.ConfigurationItems { @@ -16,7 +18,7 @@ public abstract class ConfigurationItemImportBase { protected IRepository ModuleRepo { get; private set; } protected IRepository FrontendAppRepo { get; private set; } - public IUnitOfWorkManager UnitOfWorkManager { get; set; } + public IUnitOfWorkManager UnitOfWorkManager { get; set; } = default!; public ConfigurationItemImportBase(IRepository _moduleRepo, IRepository _frontendAppRepo) { @@ -31,7 +33,7 @@ public ConfigurationItemImportBase(IRepository _moduleRepo, IRepos /// Import context /// /// - protected async Task GetModuleAsync(string name, IConfigurationItemsImportContext context) + protected async Task GetModuleAsync(string? name, IConfigurationItemsImportContext context) { if (string.IsNullOrWhiteSpace(name)) return null; @@ -59,7 +61,7 @@ protected async Task GetModuleAsync(string name, IConfigurationItemsImpo /// Import context /// /// - protected async Task GetFrontEndAppAsync(string appKey, IConfigurationItemsImportContext context) + protected async Task GetFrontEndAppAsync(string? appKey, IConfigurationItemsImportContext context) { if (string.IsNullOrWhiteSpace(appKey)) return null; @@ -84,4 +86,30 @@ public virtual Task> SortItemsAsync(List : ConfigurationItemImportBase + where TItem : ConfigurationItemBase + where TDistributedItem : DistributedConfigurableItemBase + { + protected ConfigurationItemImportBase(IRepository _moduleRepo, IRepository _frontendAppRepo) : base(_moduleRepo, _frontendAppRepo) + { + } + + public virtual async Task ReadFromJsonAsync(Stream jsonStream) + { + using (var reader = new StreamReader(jsonStream)) + { + var json = await reader.ReadToEndAsync(); + + var result = !string.IsNullOrWhiteSpace(json) + ? JsonConvert.DeserializeObject(json) + : null; + + if (result == null) + throw new Exception($"Failed to read {typeof(TDistributedItem).FullName} from json"); + + return result; + } + } + } } diff --git a/shesha-core/src/Shesha.Framework/Services/FileExtensionContentTypeProvider.cs b/shesha-core/src/Shesha.Framework/Services/FileExtensionContentTypeProvider.cs index aebada473d..ead528d818 100644 --- a/shesha-core/src/Shesha.Framework/Services/FileExtensionContentTypeProvider.cs +++ b/shesha-core/src/Shesha.Framework/Services/FileExtensionContentTypeProvider.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Diagnostics.CodeAnalysis; namespace Shesha.Services { @@ -424,9 +424,9 @@ public FileExtensionContentTypeProvider(IDictionary mapping) /// A file path /// The resulting MIME type /// True if MIME type could be determined - public bool TryGetContentType(string subpath, out string contentType) + public bool TryGetContentType(string subpath, [NotNullWhen(true)]out string? contentType) { - string extension = GetExtension(subpath); + var extension = GetExtension(subpath); if (extension == null) { contentType = null; @@ -435,22 +435,18 @@ public bool TryGetContentType(string subpath, out string contentType) return Mappings.TryGetValue(extension, out contentType); } - private static string GetExtension(string path) + private static string? GetExtension(string path) { // Don't use Path.GetExtension as that may throw an exception if there are // invalid characters in the path. Invalid characters should be handled // by the FileProviders if (string.IsNullOrWhiteSpace(path)) - { return null; - } int index = path.LastIndexOf('.'); if (index < 0) - { return null; - } return path.Substring(index); } diff --git a/shesha-core/src/Shesha.Framework/Services/IDynamicRepository.cs b/shesha-core/src/Shesha.Framework/Services/IDynamicRepository.cs index 1cd73881c1..f77bd84761 100644 --- a/shesha-core/src/Shesha.Framework/Services/IDynamicRepository.cs +++ b/shesha-core/src/Shesha.Framework/Services/IDynamicRepository.cs @@ -1,9 +1,8 @@ -using System; -using System.Collections.Generic; +using Shesha.Configuration.Runtime; +using System; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; -using Shesha.Configuration.Runtime; namespace Shesha.Services { diff --git a/shesha-core/src/Shesha.Framework/Services/IReferenceListHelper.cs b/shesha-core/src/Shesha.Framework/Services/IReferenceListHelper.cs index d0571ba11b..f69d26eaec 100644 --- a/shesha-core/src/Shesha.Framework/Services/IReferenceListHelper.cs +++ b/shesha-core/src/Shesha.Framework/Services/IReferenceListHelper.cs @@ -52,7 +52,7 @@ public interface IReferenceListHelper /// Reference list identifier /// Value of the /// - string GetItemDisplayText(ReferenceListIdentifier refListId, Int64? value); + string? GetItemDisplayText(ReferenceListIdentifier refListId, Int64? value); /// /// Decompose into list of items. Is used for MultiValueReferenceLists diff --git a/shesha-core/src/Shesha.Framework/Services/IStoredFileServiceBase.cs b/shesha-core/src/Shesha.Framework/Services/IStoredFileServiceBase.cs index 6003cbe7cf..1443c053d2 100644 --- a/shesha-core/src/Shesha.Framework/Services/IStoredFileServiceBase.cs +++ b/shesha-core/src/Shesha.Framework/Services/IStoredFileServiceBase.cs @@ -12,16 +12,16 @@ namespace Shesha.Services { Task MarkDownloadedAsync(StoredFileVersion fileVersion); - Task> GetAttachmentsOfCategoryAsync([NotNull] IEntity owner, string fileCategory); - Task> GetAttachmentsOfCategoryAsync(TId id, string typeShortAlias, string fileCategory); + Task> GetAttachmentsOfCategoryAsync([NotNull] IEntity owner, string? fileCategory); + Task> GetAttachmentsOfCategoryAsync(TId id, string typeShortAlias, string? fileCategory); Task> GetAttachmentsAsync(IEntity owner); Task> GetAttachmentsAsync(TId id, string typeShortAlias); - Task> GetLastVersionsOfAttachmentsAsync(TId id, string typeShortAlias, string fileCategory); + Task> GetLastVersionsOfAttachmentsAsync(TId id, string typeShortAlias, string? fileCategory); Task> GetLastVersionsOfAttachmentsAsync(TId id, string typeShortAlias); - Task HasAttachmentsOfCategoryAsync(IEntity owner, string fileCategory); - Task HasAttachmentsOfCategoryAsync(TId id, string typeShortAlias, string fileCategory); + Task HasAttachmentsOfCategoryAsync(IEntity owner, string? fileCategory); + Task HasAttachmentsOfCategoryAsync(TId id, string typeShortAlias, string? fileCategory); Task GetStreamAsync(StoredFileVersion fileVersion); Task GetStreamAsync(StoredFile file); Task GetStreamAsync(string filePath); @@ -38,7 +38,7 @@ namespace Shesha.Services Task UpdateVersionContentAsync(StoredFileVersion version, Stream stream); [Obsolete("Use CreateFileAsync instead")] - Task SaveFileAsync(Stream stream, string fileName, Action prepareFileAction = null); + Task SaveFileAsync(Stream stream, string fileName, Action? prepareFileAction = null); /// /// Create new file and return created version @@ -47,7 +47,7 @@ namespace Shesha.Services /// File name /// Custom preparation of file entity /// - Task CreateFileAsync(Stream stream, string fileName, Action prepareFileAction = null); + Task CreateFileAsync(Stream stream, string fileName, Action? prepareFileAction = null); /// /// Update file content and name @@ -92,4 +92,4 @@ namespace Shesha.Services /// void Delete(StoredFile storedFile); } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Services/MimeMappingService.cs b/shesha-core/src/Shesha.Framework/Services/MimeMappingService.cs index 0db96456c3..7d076f81b8 100644 --- a/shesha-core/src/Shesha.Framework/Services/MimeMappingService.cs +++ b/shesha-core/src/Shesha.Framework/Services/MimeMappingService.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Abp.Dependency; +using Abp.Dependency; namespace Shesha.Services { @@ -17,12 +14,9 @@ public MimeMappingService() public string Map(string fileName) { - string contentType; - if (!_contentTypeProvider.TryGetContentType(fileName, out contentType)) - { - contentType = "application/octet-stream"; - } - return contentType; + return _contentTypeProvider.TryGetContentType(fileName, out var contentType) + ? contentType + : "application/octet-stream"; } } } diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceListHelper.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceListHelper.cs index 06ecec52e1..fc8a9ca00e 100644 --- a/shesha-core/src/Shesha.Framework/Services/ReferenceListHelper.cs +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceListHelper.cs @@ -5,7 +5,6 @@ using Abp.Events.Bus.Handlers; using Abp.ObjectMapping; using Abp.Runtime.Caching; -using NetTopologySuite.Index.HPRtree; using Shesha.AutoMapper.Dto; using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Cache; @@ -14,6 +13,7 @@ using Shesha.Domain.ConfigurationItems; using Shesha.DynamicEntities; using Shesha.Extensions; +using Shesha.Services.ReferenceLists.Cache; using Shesha.Services.ReferenceLists.Dto; using Shesha.Services.ReferenceLists.Exceptions; using Shesha.Utilities; @@ -28,46 +28,40 @@ public class ReferenceListHelper: IEventHandler>, IReferenceListHelper, ITransientDependency { - private const string ListItemsItemsCacheName = "ReferenceListItemsCache"; - private const string ListItemsIdsCacheName = "ReferenceListIdsCache"; - private readonly IRepository _listRepository; private readonly IRepository _itemsRepository; private readonly IUnitOfWorkManager _unitOfWorkManager; - private readonly ICacheManager _cacheManager; private readonly IConfigurationFrameworkRuntime _cfRuntime; private readonly IConfigurationItemClientSideCache _clientSideCache; - /// - /// Reference to the object to object mapper. - /// - public IObjectMapper ObjectMapper { get; set; } - - /// - /// Cache of the ReferenceListItems - /// - protected ITypedCache> ListItemsCache => _cacheManager.GetCache>(ListItemsItemsCacheName); + private readonly ITypedCache> _listItemsCache; /// /// Cache of reference list ids /// - protected ITypedCache ListIdsCache => _cacheManager.GetCache(ListItemsIdsCacheName); + private readonly ITypedCache _listIdsCache; + /// + /// Reference to the object to object mapper. + /// + public IObjectMapper ObjectMapper { get; set; } = NullObjectMapper.Instance; public ReferenceListHelper( IRepository listRepository, IRepository itemsRepository, IUnitOfWorkManager unitOfWorkManager, - ICacheManager cacheManager, IConfigurationFrameworkRuntime cfRuntime, - IConfigurationItemClientSideCache clientSideCache + IConfigurationItemClientSideCache clientSideCache, + IReferenceListItemsCacheHolder listItemsCache, + IReferenceListIdsCacheHolder listIdsCache ) { _listRepository = listRepository; _itemsRepository = itemsRepository; _unitOfWorkManager = unitOfWorkManager; - _cacheManager = cacheManager; _cfRuntime = cfRuntime; _clientSideCache = clientSideCache; + _listItemsCache = listItemsCache.Cache; + _listIdsCache = listIdsCache.Cache; } private void ValidateRefListId(ReferenceListIdentifier refListId) @@ -88,7 +82,7 @@ private void ValidateRefListId(ReferenceListIdentifier refListId) /// Referencve list identifier /// Value of the /// - public string GetItemDisplayText(ReferenceListIdentifier refListId, Int64? value) + public string? GetItemDisplayText(ReferenceListIdentifier refListId, Int64? value) { ValidateRefListId(refListId); @@ -124,7 +118,7 @@ public async Task> GetItemsAsync(ReferenceListIdentif { var listId = await GetListIdAsync(refListId); - return await ListItemsCache.GetAsync(listId, async id => { + return await _listItemsCache.GetAsync(listId, async id => { return await GetItemsAsync(id); }); } @@ -134,7 +128,7 @@ public List GetItems(ReferenceListIdentifier refListId) { var listId = GetListId(refListId); - return ListItemsCache.Get(listId, id => { + return _listItemsCache.Get(listId, id => { return GetItems(id); }); } @@ -237,7 +231,7 @@ private string GetListIdCacheKey(ReferenceListIdentifier refListId) private async Task GetListIdAsync(ReferenceListIdentifier refListId) { var idCacheKey = GetListIdCacheKey(refListId); - var listId = await ListIdsCache.GetAsync(idCacheKey, async key => { + var listId = await _listIdsCache.GetAsync(idCacheKey, async key => { var list = await GetReferenceListAsync(refListId); return list.Id; }); @@ -247,7 +241,7 @@ private async Task GetListIdAsync(ReferenceListIdentifier refListId) private Guid GetListId(ReferenceListIdentifier refListId) { var idCacheKey = GetListIdCacheKey(refListId); - var listId = ListIdsCache.Get(idCacheKey, key => { + var listId = _listIdsCache.Get(idCacheKey, key => { var list = GetReferenceList(refListId); return list.Id; }); @@ -267,20 +261,19 @@ public void HandleEvent(EntityChangedEventData eventData) private void ClearCacheForRefList(ReferenceList refList) { // clear items cache by Id - ListItemsCache.Remove(refList.Id); + _listItemsCache.Remove(refList.Id); // clear ids cache by module, nameapce and name var modes = Enum.GetValues(typeof(ConfigurationItemViewMode)).Cast().ToList(); var refListId = refList.GetReferenceListIdentifier(); var keys = modes.Select(mode => GetListIdCacheKey(refListId, mode)).ToArray(); - ListIdsCache.Remove(keys); + _listIdsCache.Remove(keys); // clear client-side cache AsyncHelper.RunSync(async () => { await _clientSideCache.SetCachedMd5Async(ReferenceList.ItemTypeName, null, refListId.Module, refListId.Name, _cfRuntime.ViewMode, null); - } - ); + }); } /// @@ -288,7 +281,7 @@ private void ClearCacheForRefList(ReferenceList refList) /// public async Task ClearCacheAsync() { - await ListItemsCache.ClearAsync(); + await _listItemsCache.ClearAsync(); } /// @@ -297,18 +290,17 @@ public async Task ClearCacheAsync() /// /// /// - public static List DecomposeMultiReferenceListValue(T rawValue) where T : struct, IConvertible + public static List DecomposeMultiReferenceListValue(Int64 rawValue) where T : struct, IConvertible { var result = new List(); if (rawValue.ToString() == "0") return result; - var flag = Enum.Parse(typeof(T), rawValue.ToString()) as Enum; - - foreach (var r in (long[])Enum.GetValues(typeof(T))) + var enumValues = Enum.GetValues(typeof(T)).Cast(); + foreach (var r in enumValues) { - if ((Convert.ToInt64(flag) & r) == r) + if ((rawValue & r) == r) { var nameValue = new ReferenceListItemValueDto() { @@ -338,4 +330,4 @@ public void HandleEvent(EntityReorderedEventData eventD } } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/IReferenceListIdsCacheHolder.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/IReferenceListIdsCacheHolder.cs new file mode 100644 index 0000000000..28cf3e07c9 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/IReferenceListIdsCacheHolder.cs @@ -0,0 +1,10 @@ +using Shesha.Cache; +using System; + +namespace Shesha.Services.ReferenceLists.Cache +{ + + public interface IReferenceListIdsCacheHolder : ICacheHolder + { + } +} diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/IReferenceListItemsCacheHolder.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/IReferenceListItemsCacheHolder.cs new file mode 100644 index 0000000000..9bd33ab0ca --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/IReferenceListItemsCacheHolder.cs @@ -0,0 +1,12 @@ +using Shesha.Cache; +using Shesha.Services.ReferenceLists.Dto; +using System; +using System.Collections.Generic; + +namespace Shesha.Services.ReferenceLists.Cache +{ + + public interface IReferenceListItemsCacheHolder : ICacheHolder> + { + } +} diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/ReferenceListIdsCacheHolder.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/ReferenceListIdsCacheHolder.cs new file mode 100644 index 0000000000..e46376335d --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/ReferenceListIdsCacheHolder.cs @@ -0,0 +1,14 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; +using System; + +namespace Shesha.Services.ReferenceLists.Cache +{ + public class ReferenceListIdsCacheHolder : CacheHolder, IReferenceListIdsCacheHolder, ISingletonDependency + { + public ReferenceListIdsCacheHolder(ICacheManager cacheManager) : base("ReferenceListIdsCache", cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/ReferenceListItemsCacheHolder.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/ReferenceListItemsCacheHolder.cs new file mode 100644 index 0000000000..562e924559 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Cache/ReferenceListItemsCacheHolder.cs @@ -0,0 +1,16 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; +using Shesha.Services.ReferenceLists.Dto; +using System; +using System.Collections.Generic; + +namespace Shesha.Services.ReferenceLists.Cache +{ + public class ReferenceListItemsCacheHolder : CacheHolder>, IReferenceListItemsCacheHolder, ISingletonDependency + { + public ReferenceListItemsCacheHolder(ICacheManager cacheManager) : base("ReferenceListItemsCache", cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/DistributedReferenceListItem.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/DistributedReferenceListItem.cs index ce1e0f9c78..5506beb0a9 100644 --- a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/DistributedReferenceListItem.cs +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/DistributedReferenceListItem.cs @@ -11,7 +11,7 @@ public class DistributedReferenceListItem /// /// Item text /// - public string Item { get; set; } + public string? Item { get; set; } /// /// Item value @@ -21,7 +21,7 @@ public class DistributedReferenceListItem /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Order index @@ -36,17 +36,17 @@ public class DistributedReferenceListItem /// /// Assicoated color /// - public string Color { get; set; } + public string? Color { get; set; } /// /// Assiciated icon /// - public string Icon { get; set; } + public string? Icon { get; set; } /// /// Short alias /// - public string ShortAlias { get; set; } + public string? ShortAlias { get; set; } /// /// Child items diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/ReferenceListExport.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/ReferenceListExport.cs index b43e8b7e4a..9184e3d262 100644 --- a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/ReferenceListExport.cs +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/ReferenceListExport.cs @@ -60,11 +60,6 @@ public async Task ExportItemAsync(Configuration // reflist specific properties Items = await ExportRefListItemsAsync(refList), - /* - Markup = refList.Markup, - ModelType = refList.ModelType, - TemplateId = refList.Template?.Id, - */ }; return result; @@ -75,7 +70,7 @@ private async Task> ExportRefListItemsAsync(R { var items = await _refListItemRepo.GetAll().Where(item => item.ReferenceList == refList).ToListAsync(); - async Task ProcessRecursiveAsync(ReferenceListItem parent, List container) + async Task ProcessRecursiveAsync(ReferenceListItem? parent, List container) { var levelItems = items.Where(item => item.Parent == parent).OrderBy(item => item.OrderIndex).ToList(); foreach (var item in levelItems) diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/ReferenceListImport.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/ReferenceListImport.cs index 4c157d45cf..17fbcb8d80 100644 --- a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/ReferenceListImport.cs +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Distribution/ReferenceListImport.cs @@ -1,13 +1,14 @@ using Abp.Dependency; using Abp.Domain.Repositories; using Abp.Domain.Uow; -using Newtonsoft.Json; +using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; +using Shesha.Extensions; +using Shesha.Services.ConfigurationItems; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; @@ -16,7 +17,7 @@ namespace Shesha.Services.ReferenceLists.Distribution /// /// Reference list import /// - public class ReferenceListImport: IReferenceListImport, ITransientDependency + public class ReferenceListImport: ConfigurationItemImportBase, IReferenceListImport, ITransientDependency { private readonly IRepository _refListRepo; private readonly IRepository _refListItemRepo; @@ -24,7 +25,12 @@ public class ReferenceListImport: IReferenceListImport, ITransientDependency private readonly IReferenceListManager _refListManger; private readonly IUnitOfWorkManager _unitOfWorkManager; - public ReferenceListImport(IReferenceListManager formManger, IRepository refListRepo, IRepository refListItemRepo, IRepository moduleRepo, IUnitOfWorkManager unitOfWorkManager) + public ReferenceListImport(IReferenceListManager formManger, + IRepository refListRepo, + IRepository refListItemRepo, + IRepository moduleRepo, + IRepository frontEndAppRepo, + IUnitOfWorkManager unitOfWorkManager): base (moduleRepo, frontEndAppRepo) { _refListManger = formManger; _refListRepo = refListRepo; @@ -47,21 +53,11 @@ public async Task ImportItemAsync(DistributedConfigurable return await ImportRefListAsync(refListItem, context); } - /// inheritedDoc - public async Task ReadFromJsonAsync(Stream jsonStream) - { - using (var reader = new StreamReader(jsonStream)) - { - var json = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(json); - } - } - /// inheritedDoc protected async Task ImportRefListAsync(DistributedReferenceList item, IConfigurationItemsImportContext context) { // check if form exists - var existingList = await _refListRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module.Name == item.ModuleName) && f.IsLast); + var existingList = await _refListRepo.GetByByFullName(item.ModuleName, item.Name).FirstOrDefaultAsync(e => e.IsLast); // use status specified in the context with fallback to imported value var statusToImport = context.ImportStatusAs ?? item.VersionStatus; @@ -82,7 +78,7 @@ protected async Task ImportRefListAsync(DistributedRefere { var liveVersion = existingList.VersionStatus == ConfigurationItemVersionStatus.Live ? existingList - : await _refListRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module.Name == item.ModuleName) && f.VersionStatus == ConfigurationItemVersionStatus.Live); + : await _refListRepo.GetByByFullName(item.ModuleName, item.Name).FirstOrDefaultAsync(f => f.VersionStatus == ConfigurationItemVersionStatus.Live); if (liveVersion != null) { await _refListManger.UpdateStatusAsync(liveVersion, ConfigurationItemVersionStatus.Retired); @@ -134,7 +130,7 @@ private async Task ImportListItemsAsync(ReferenceList refList, List items, ReferenceListItem parent) + private async Task ImportListItemLevelAsync(ReferenceList refList, List items, ReferenceListItem? parent) { foreach (var distributedItem in items) { @@ -158,32 +154,11 @@ private void MapListItem(DistributedReferenceListItem src, ReferenceListItem dst dst.ItemValue = src.ItemValue; dst.Description = src.Description; dst.OrderIndex = src.OrderIndex; - // todo: decide how to handle hard linked items - //dst.HardLinkToApplication = src.HardLinkToApplication; dst.Color = src.Color; dst.Icon = src.Icon; dst.ShortAlias = src.ShortAlias; } - private async Task GetModuleAsync(string name, IConfigurationItemsImportContext context) - { - if (string.IsNullOrWhiteSpace(name)) - return null; - - var module = await _moduleRepo.FirstOrDefaultAsync(m => m.Name == name); - if (module == null) - { - if (context.CreateModules) - { - module = new Module { Name = name, IsEnabled = true }; - await _moduleRepo.InsertAsync(module); - } else - throw new NotSupportedException($"Module `{name}` is missing in the destination"); - } - - return module; - } - private void MapToRefList(DistributedReferenceList item, ReferenceList refList) { refList.Name = item.Name; @@ -192,10 +167,5 @@ private void MapToRefList(DistributedReferenceList item, ReferenceList refList) refList.VersionStatus = item.VersionStatus; refList.Suppress = item.Suppress; } - - public Task> SortItemsAsync(List items) - { - return Task.FromResult(items); - } } } diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Dto/CreateReferenceListDto.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Dto/CreateReferenceListDto.cs index 96e79fd025..e9436ad7f1 100644 --- a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Dto/CreateReferenceListDto.cs +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Dto/CreateReferenceListDto.cs @@ -15,16 +15,16 @@ public class CreateReferenceListDto /// /// Reference list name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Label /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Dto/ReferenceListDto.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Dto/ReferenceListDto.cs index 67041052b6..595fc7660b 100644 --- a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Dto/ReferenceListDto.cs +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Dto/ReferenceListDto.cs @@ -1,53 +1,13 @@ -using System; -using Abp.Application.Services.Dto; -using Abp.AutoMapper; -using Shesha.Domain; -using Shesha.Domain.ConfigurationItems; -using Shesha.Dto.Interfaces; +using Shesha.Domain; +using Shesha.Dto; namespace Shesha.Services.ReferenceLists.Dto { /// /// Dto of the /// - public class ReferenceListDto: EntityDto, IConfigurationItemDto + public class ReferenceListDto: ConfigurationItemDto { - /// - /// Module Id - /// - public Guid? ModuleId { get; set; } - - /// - /// Origin id - /// - public Guid? OriginId { get; set; } - - /// - /// Module name - /// - public string Module { get; set; } - - /// - /// Version number - /// - public int VersionNo { get; set; } - - /// - /// If true, indicates that this is a last version of the form - /// - public bool IsLastVersion { get; set; } - - /// - /// Version status - /// - public ConfigurationItemVersionStatus VersionStatus { get; set; } - - public string Name { get; set; } - public string Label { get; set; } - public string Description { get; set; } - public bool HardLinkToApplication { get; set; } - public string Namespace { get; set; } - public int? NoSelectionValue { get; set; } - public bool Suppress { get; set; } + } } diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Exceptions/ReferenceListNotFoundException.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Exceptions/ReferenceListNotFoundException.cs index e939c77bf4..1b98e69b4c 100644 --- a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Exceptions/ReferenceListNotFoundException.cs +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/Exceptions/ReferenceListNotFoundException.cs @@ -10,7 +10,7 @@ public class ReferenceListNotFoundException : ConfigurationItemNotFoundException { public string Namespace { get; set; } - public ReferenceListNotFoundException(string module, string name) : base(ReferenceList.ItemTypeName, module, name, null) + public ReferenceListNotFoundException(string? module, string name) : base(ReferenceList.ItemTypeName, module, name, null) { } diff --git a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/ReferenceListManager.cs b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/ReferenceListManager.cs index c7b3b8b407..40b6e3c5bf 100644 --- a/shesha-core/src/Shesha.Framework/Services/ReferenceLists/ReferenceListManager.cs +++ b/shesha-core/src/Shesha.Framework/Services/ReferenceLists/ReferenceListManager.cs @@ -1,14 +1,15 @@ using Abp.Dependency; using Abp.Domain.Repositories; using Abp.Domain.Uow; -using Abp.Runtime.Validation; using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Models; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Dto.Interfaces; using Shesha.Extensions; +using Shesha.Reflection; using Shesha.Services.ReferenceLists.Dto; +using Shesha.Validations; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -47,8 +48,7 @@ public ReferenceListManager(IRepository repository, : $"Reference List with name `{input.Name}` already exists" ) ); - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); var refList = new ReferenceList(); refList.Name = input.Name; @@ -105,8 +105,7 @@ private async Task UpdateNameAndModuleAsync(ReferenceList refList, Guid? moduleI ); } - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); var allVersions = await Repository.GetAll().Where(v => v.Name == refList.Name && v.Module == refList.Module).ToListAsync(); @@ -128,15 +127,13 @@ public override Task MapToDtoAsync(ReferenceList item) public override async Task CopyAsync(ReferenceList item, CopyItemInput input) { - var srcList = item as ReferenceList; - // todo: validate input var module = await ModuleRepository.FirstOrDefaultAsync(input.ModuleId); var validationResults = new List(); // todo: review validation messages, add localization support - if (srcList == null) + if (item == null) validationResults.Add(new ValidationResult("Please select a referencelist to copy", new List { nameof(input.ItemId) })); if (module == null) validationResults.Add(new ValidationResult("Module is mandatory", new List { nameof(input.ModuleId) })); @@ -155,8 +152,9 @@ public override async Task CopyAsync(ReferenceList item, CopyItem ); } - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); + + var srcList = item.ForceCastAs(); var listCopy = new ReferenceList(); listCopy.Name = input.Name; @@ -185,7 +183,7 @@ private async Task CopyItemsAsync(ReferenceList source, ReferenceList destinatio await CopyItemsAsync(srcItems, null, null, destination); } - private async Task CopyItemsAsync(List sourceItems, ReferenceListItem sourceParent, ReferenceListItem destinationParent, ReferenceList destination) + private async Task CopyItemsAsync(List sourceItems, ReferenceListItem? sourceParent, ReferenceListItem? destinationParent, ReferenceList destination) { var levelItems = sourceItems.Where(i => i.Parent == sourceParent).ToList(); foreach (var srcItem in levelItems) diff --git a/shesha-core/src/Shesha.Framework/Services/Settings/Cache/CachedSettingValue.cs b/shesha-core/src/Shesha.Framework/Services/Settings/Cache/CachedSettingValue.cs new file mode 100644 index 0000000000..55c0dac94b --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Services/Settings/Cache/CachedSettingValue.cs @@ -0,0 +1,13 @@ +namespace Shesha.Services.Settings.Cache +{ + /// + /// Value wrapper for setting value cache. Is used to store null values in cache + /// + public class CachedSettingValue + { + public string? Value { get; set; } + public CachedSettingValue(string? value) { + Value = value; + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Services/Settings/Cache/ISettingCacheHolder.cs b/shesha-core/src/Shesha.Framework/Services/Settings/Cache/ISettingCacheHolder.cs new file mode 100644 index 0000000000..63bd18983c --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Services/Settings/Cache/ISettingCacheHolder.cs @@ -0,0 +1,11 @@ +using Shesha.Cache; + +namespace Shesha.Services.Settings.Cache +{ + /// + /// Settings cache holder + /// + public interface ISettingCacheHolder : ICacheHolder + { + } +} diff --git a/shesha-core/src/Shesha.Framework/Services/Settings/Cache/SettingCacheHolder.cs b/shesha-core/src/Shesha.Framework/Services/Settings/Cache/SettingCacheHolder.cs new file mode 100644 index 0000000000..3e147a42ea --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Services/Settings/Cache/SettingCacheHolder.cs @@ -0,0 +1,13 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Shesha.Cache; + +namespace Shesha.Services.Settings.Cache +{ + public class SettingCacheHolder : CacheHolder, ISettingCacheHolder, ISingletonDependency + { + public SettingCacheHolder(ICacheManager cacheManager) : base("SettingsCache", cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/DistributedSettingConfiguration.cs b/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/DistributedSettingConfiguration.cs index c5daa62b79..349c220d24 100644 --- a/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/DistributedSettingConfiguration.cs +++ b/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/DistributedSettingConfiguration.cs @@ -10,10 +10,10 @@ namespace Shesha.Services.Settings.Distribution public class DistributedSettingConfiguration : DistributedConfigurableItemBase { public string DataType { get; set; } - public string EditorFormName { get; set; } - public string EditorFormModule { get; set; } + public string? EditorFormName { get; set; } + public string? EditorFormModule { get; set; } public int OrderIndex { get; set; } - public string Category { get; set; } + public string? Category { get; set; } public bool IsClientSpecific { get; set; } public SettingAccessMode AccessMode { get; set; } public bool IsUserSpecific { get; set; } diff --git a/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/DistributedSettingValue.cs b/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/DistributedSettingValue.cs index 22697fbbd6..4f19907b1d 100644 --- a/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/DistributedSettingValue.cs +++ b/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/DistributedSettingValue.cs @@ -1,6 +1,4 @@ -using Shesha.ConfigurationItems.Distribution; - -namespace Shesha.Services.Settings.Distribution +namespace Shesha.Services.Settings.Distribution { /// /// Distributed setting value @@ -8,6 +6,6 @@ namespace Shesha.Services.Settings.Distribution public class DistributedSettingValue { public string Value { get; set; } - public string AppKey { get; set; } + public string? AppKey { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/SettingImport.cs b/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/SettingImport.cs index 77451371cd..266274bf6e 100644 --- a/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/SettingImport.cs +++ b/shesha-core/src/Shesha.Framework/Services/Settings/Distribution/SettingImport.cs @@ -1,7 +1,6 @@ using Abp.Dependency; using Abp.Domain.Repositories; using Abp.Domain.Uow; -using Newtonsoft.Json; using Shesha.ConfigurationItems.Distribution; using Shesha.ConfigurationItems.Specifications; using Shesha.Domain; @@ -10,7 +9,6 @@ using Shesha.Services.ConfigurationItems; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; @@ -19,7 +17,7 @@ namespace Shesha.Services.Settings.Distribution /// /// Setting import /// - public class SettingImport: ConfigurationItemImportBase, ISettingImport, ITransientDependency + public class SettingImport: ConfigurationItemImportBase, ISettingImport, ITransientDependency { private readonly IRepository _settingConfigRepo; private readonly IRepository _settingValueRepo; @@ -53,21 +51,11 @@ public async Task ImportItemAsync(DistributedConfigurable return await ImportSettingAsync(setting, context); } - /// inheritedDoc - public async Task ReadFromJsonAsync(Stream jsonStream) - { - using (var reader = new StreamReader(jsonStream)) - { - var json = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(json); - } - } - /// inheritedDoc protected async Task ImportSettingAsync(DistributedSettingConfiguration item, IConfigurationItemsImportContext context) { // check if form exists - var existingSetting = await _settingConfigRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module.Name == item.ModuleName) && f.IsLast); + var existingSetting = await _settingConfigRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module != null && f.Module.Name == item.ModuleName) && f.IsLast); // use status specified in the context with fallback to imported value var statusToImport = context.ImportStatusAs ?? item.VersionStatus; @@ -88,7 +76,7 @@ protected async Task ImportSettingAsync(DistributedSettin { var liveVersion = existingSetting.VersionStatus == ConfigurationItemVersionStatus.Live ? existingSetting - : await _settingConfigRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module.Name == item.ModuleName) && f.VersionStatus == ConfigurationItemVersionStatus.Live); + : await _settingConfigRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module != null && f.Module.Name == item.ModuleName) && f.VersionStatus == ConfigurationItemVersionStatus.Live); if (liveVersion != null) { await _settingStore.UpdateStatusAsync(liveVersion, ConfigurationItemVersionStatus.Retired); diff --git a/shesha-core/src/Shesha.Framework/Services/Settings/Dto/CreateSettingDefinitionDto.cs b/shesha-core/src/Shesha.Framework/Services/Settings/Dto/CreateSettingDefinitionDto.cs index d4e62b9da4..0f930ad9a1 100644 --- a/shesha-core/src/Shesha.Framework/Services/Settings/Dto/CreateSettingDefinitionDto.cs +++ b/shesha-core/src/Shesha.Framework/Services/Settings/Dto/CreateSettingDefinitionDto.cs @@ -17,34 +17,34 @@ public class CreateSettingDefinitionDto /// /// Reference list name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Label /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Setting category /// - public string Category { get; set; } + public string? Category { get; set; } /// /// Data type of the value /// [StringLength(100)] - public string DataType { get; set; } + public required string DataType { get; set; } [StringLength(200)] - public string EditorFormName { get; set; } + public string? EditorFormName { get; set; } [StringLength(200)] - public string EditorFormModule { get; set; } + public string? EditorFormModule { get; set; } /// /// Allows ordering the setting in a logical manner within its category diff --git a/shesha-core/src/Shesha.Framework/Services/Settings/Dto/SettingDefinitionDto.cs b/shesha-core/src/Shesha.Framework/Services/Settings/Dto/SettingDefinitionDto.cs index 5fa21a463b..70c6321f40 100644 --- a/shesha-core/src/Shesha.Framework/Services/Settings/Dto/SettingDefinitionDto.cs +++ b/shesha-core/src/Shesha.Framework/Services/Settings/Dto/SettingDefinitionDto.cs @@ -18,7 +18,7 @@ public class SettingDefinitionDto : EntityDto, IConfigurationItemDto /// /// Module name /// - public string Module { get; set; } + public string? Module { get; set; } /// /// Version number @@ -36,8 +36,8 @@ public class SettingDefinitionDto : EntityDto, IConfigurationItemDto public ConfigurationItemVersionStatus VersionStatus { get; set; } public string Name { get; set; } - public string Label { get; set; } - public string Description { get; set; } + public string? Label { get; set; } + public string? Description { get; set; } public bool Suppress { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Services/Settings/ISettingStore.cs b/shesha-core/src/Shesha.Framework/Services/Settings/ISettingStore.cs index dabecd2d03..5c5743bbe9 100644 --- a/shesha-core/src/Shesha.Framework/Services/Settings/ISettingStore.cs +++ b/shesha-core/src/Shesha.Framework/Services/Settings/ISettingStore.cs @@ -30,5 +30,13 @@ public interface ISettingStore: IConfigurationItemManager /// Setting context /// Task GetSettingValueAsync(SettingDefinition setting, SettingManagementContext context); + + /// + /// Get setting value + /// + /// Setting definition + /// Setting context + /// + Task GetValueAsync(SettingDefinition setting, SettingManagementContext context); } } diff --git a/shesha-core/src/Shesha.Framework/Services/Settings/SettingStore.cs b/shesha-core/src/Shesha.Framework/Services/Settings/SettingStore.cs index f689f90fb2..eab9b5d991 100644 --- a/shesha-core/src/Shesha.Framework/Services/Settings/SettingStore.cs +++ b/shesha-core/src/Shesha.Framework/Services/Settings/SettingStore.cs @@ -1,7 +1,8 @@ using Abp.Dependency; using Abp.Domain.Repositories; using Abp.Domain.Uow; -using Abp.Runtime.Validation; +using Abp.Events.Bus.Entities; +using Abp.Events.Bus.Handlers; using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Models; using Shesha.ConfigurationItems.Specifications; @@ -9,8 +10,11 @@ using Shesha.Domain.ConfigurationItems; using Shesha.Dto.Interfaces; using Shesha.Extensions; +using Shesha.Services.Settings.Cache; using Shesha.Services.Settings.Dto; using Shesha.Settings; +using Shesha.Utilities; +using Shesha.Validations; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -21,31 +25,39 @@ namespace Shesha.Services.Settings { /// inheritedDoc - public class SettingStore : ConfigurationItemManager, ISettingStore, ITransientDependency + public class SettingStore : ConfigurationItemManager, ISettingStore, ITransientDependency, + IAsyncEventHandler>, + IAsyncEventHandler> { - private readonly IRepository _settingConfigurationRepository; private readonly IRepository _settingValueRepository; private readonly IConfigurationFrameworkRuntime _cfRuntime; + private readonly ISettingCacheHolder _cacheHolder; + + /// + /// Default constructor + /// public SettingStore( IRepository repository, IRepository configurationItemRepository, IRepository moduleRepository, IUnitOfWorkManager unitOfWorkManager, IRepository settingValueRepository, - IRepository settingConfigurationRepository, - IConfigurationFrameworkRuntime cfRuntime) : base(repository, moduleRepository, unitOfWorkManager) + IConfigurationFrameworkRuntime cfRuntime, + ISettingCacheHolder cacheHolder) : base(repository, moduleRepository, unitOfWorkManager) { _settingValueRepository = settingValueRepository; - _settingConfigurationRepository = settingConfigurationRepository; _cfRuntime = cfRuntime; + _cacheHolder = cacheHolder; } + /// inheritedDoc public override Task CopyAsync(SettingConfiguration item, CopyItemInput input) { throw new System.NotImplementedException(); } + /// inheritedDoc public async Task CreateSettingConfigurationAsync(CreateSettingDefinitionDto input) { var module = input.ModuleId.HasValue @@ -62,8 +74,7 @@ public async Task CreateSettingConfigurationAsync(CreateSe : $"Setting Definition with name `{input.Name}` already exists" ) ); - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); var definition = new SettingConfiguration(); definition.Name = input.Name; @@ -94,6 +105,7 @@ public async Task CreateSettingConfigurationAsync(CreateSe return definition; } + /// inheritedDoc public override async Task CreateNewVersionAsync(SettingConfiguration item) { var newVersion = new SettingConfiguration(); @@ -125,23 +137,26 @@ public override async Task CreateNewVersionAsync(SettingCo return newVersion; } - public async Task GetSettingConfigurationAsync(ConfigurationItemIdentifier id) + /// inheritedDoc + public Task GetSettingConfigurationAsync(ConfigurationItemIdentifier id) { - return await Repository.GetAll() + return Repository.GetAll() .Where(new ByNameAndModuleSpecification(id.Name, id.Module).ToExpression()) .Where(s => s.IsLast && s.VersionStatus == ConfigurationItemVersionStatus.Live) .FirstOrDefaultAsync(); } + /// inheritedDoc public override Task MapToDtoAsync(SettingConfiguration item) { var dto = ObjectMapper.Map(item); return Task.FromResult(dto); } - public async Task GetSettingValueAsync(SettingDefinition setting, SettingManagementContext context) + /// inheritedDoc + public Task GetSettingValueAsync(SettingDefinition setting, SettingManagementContext context) { - return await WithUnitOfWorkAsync(async () => { + return WithUnitOfWorkAsync(async () => { var settingConfiguration = await GetSettingConfigurationAsync(setting); var query = _settingValueRepository.GetAll() .Where(v => v.SettingConfiguration.Id == settingConfiguration.Id); @@ -161,9 +176,9 @@ public async Task GetSettingValueAsync(SettingDefinition setting, }); } - private async Task GetSettingConfigurationAsync(SettingDefinition setting) + private Task GetSettingConfigurationAsync(SettingDefinition setting) { - return await GetSettingConfigurationAsync(new SettingConfigurationIdentifier(setting.ModuleName, setting.Name)); + return GetSettingConfigurationAsync(new SettingConfigurationIdentifier(setting.ModuleName, setting.Name)); } private async Task WithUnitOfWorkAsync(Func> action) @@ -179,5 +194,80 @@ private async Task WithUnitOfWorkAsync(Func> act return result; } } + + private string GetCacheKey(CacheKeyArgs args) + { + var parts = new List { + !string.IsNullOrWhiteSpace(args.Module) ? $"{args.Module}/{args.Name}" : args.Name, + args.IsClientSpecific ? args.AppKey ?? string.Empty : null, + args.IsUserSpecific ? args.UserId?.ToString() ?? string.Empty : null, + }.WhereNotNull(); + + return parts.Delimited("|"); + } + + /// inheritedDoc + public async Task GetValueAsync(SettingDefinition setting, SettingManagementContext context) + { + var cacheKey = GetCacheKey(new CacheKeyArgs(setting, context)); + + var result = await _cacheHolder.Cache.GetAsync(cacheKey, async(key) => { + var value = await GetSettingValueAsync(setting, context); + return new CachedSettingValue(value?.Value); + }); + + return result.Value; + } + + /// + /// SettingDefinition changing handler + /// + public Task HandleEventAsync(EntityChangedEventData eventData) + { + // TODO: clear cache partially on configuration update (maybe using tags which are under development currently) + return _cacheHolder.Cache.ClearAsync(); + } + + /// + /// SettingValue changing handler + /// + public async Task HandleEventAsync(EntityChangingEventData eventData) + { + if (eventData.Entity == null) + return; + + var cacheKey = GetCacheKey(new CacheKeyArgs(eventData.Entity)); + await _cacheHolder.Cache.RemoveAsync(cacheKey); + } + private class CacheKeyArgs + { + public string? Module { get; private set; } + public string Name { get; private set; } + public bool IsClientSpecific { get; private set; } + public string? AppKey { get; private set; } + public bool IsUserSpecific { get; private set; } + public Int64? UserId { get; private set; } + + public CacheKeyArgs(SettingDefinition setting, SettingManagementContext context) + { + Module = setting.ModuleName; + Name = setting.Name; + IsClientSpecific = setting.IsClientSpecific; + IsUserSpecific = setting.IsUserSpecific; + AppKey = context.AppKey; + UserId = context.UserId; + } + public CacheKeyArgs(SettingValue settingValue) + { + var config = settingValue.SettingConfiguration; + + Module = config.Module?.Name; + Name = config.Name; + IsClientSpecific = config.IsClientSpecific; + IsUserSpecific = config.IsUserSpecific; + AppKey = settingValue.Application?.AppKey; + UserId = settingValue.User?.Id; + } + } } } diff --git a/shesha-core/src/Shesha.Framework/Services/StaticContext.cs b/shesha-core/src/Shesha.Framework/Services/StaticContext.cs index 47b13167cb..9ccbd13049 100644 --- a/shesha-core/src/Shesha.Framework/Services/StaticContext.cs +++ b/shesha-core/src/Shesha.Framework/Services/StaticContext.cs @@ -4,7 +4,8 @@ using Shesha.Extensions; using Shesha.Utilities; using System.Collections.Generic; -using System.Linq; +using System.Linq; +using System.Threading; namespace Shesha.Services { @@ -13,20 +14,22 @@ namespace Shesha.Services /// public static class StaticContext { - private static IIocManager _iocManager; - + private static readonly AsyncLocal _asyncLocalIocManager = new AsyncLocal(); + /// /// Ioc manager /// - public static IIocManager IocManager => _iocManager ?? Abp.Dependency.IocManager.Instance; - +#pragma warning disable SHA001 // Restricted static property access + public static IIocManager IocManager => _asyncLocalIocManager.Value ?? Abp.Dependency.IocManager.Instance; +#pragma warning restore SHA001 // Restricted static property access + /// /// Set IocManager, is used in unit tests only /// /// public static void SetIocManager(IIocManager iocManager) { - _iocManager = iocManager; + _asyncLocalIocManager.Value = iocManager; } public static void LogUow(string message) diff --git a/shesha-core/src/Shesha.Framework/Services/StoredFiles/AzureStoredFileService.cs b/shesha-core/src/Shesha.Framework/Services/StoredFiles/AzureStoredFileService.cs index 7556ac84c2..44f47d16f9 100644 --- a/shesha-core/src/Shesha.Framework/Services/StoredFiles/AzureStoredFileService.cs +++ b/shesha-core/src/Shesha.Framework/Services/StoredFiles/AzureStoredFileService.cs @@ -1,7 +1,4 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Abp.Dependency; +using Abp.Dependency; using Abp.Domain.Repositories; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; @@ -10,6 +7,9 @@ using Microsoft.Extensions.Hosting; using Shesha.Configuration; using Shesha.Domain; +using System; +using System.IO; +using System.Threading.Tasks; namespace Shesha.Services.StoredFiles { @@ -20,7 +20,7 @@ public class AzureStoredFileService : StoredFileServiceBase, IStoredFileService private const string ContainerName = "files"; private readonly IocManager _iocManager; private readonly IConfigurationRoot _configuration; - private BlobContainerClient _blobContainerClient; + private BlobContainerClient? _blobContainerClient; public AzureStoredFileService(IRepository fileService, IRepository versionService, IocManager iocManager) : base(fileService, versionService) @@ -38,7 +38,7 @@ private IConfigurationRoot GetConfiguration() /// /// Returns connection string. Note: for the Azure environment - uses standard environment variable /// - private string GetConnectionString() => _configuration.GetConnectionString(ConnectionStringName); + private string GetConnectionString() => _configuration.GetRequiredConnectionString(ConnectionStringName); private BlobContainerClient BlobContainerClient { @@ -77,7 +77,7 @@ private async Task GetStreamInternalAsync(string filePath) var props = await blob.GetPropertiesAsync(); if (props.Value.ContentLength > 0) { - await blob.DownloadToAsync(stream); + using var response = await blob.DownloadToAsync(stream); stream.Seek(0, SeekOrigin.Begin); } @@ -98,7 +98,7 @@ public override Stream GetStream(StoredFileVersion fileVersion) var props = blob.GetProperties(); if (props.Value.ContentLength > 0) { - blob.DownloadTo(stream); + using var response = blob.DownloadTo(stream); stream.Seek(0, SeekOrigin.Begin); } @@ -137,14 +137,14 @@ protected override void CopyFile(StoredFileVersion source, StoredFileVersion des protected override async Task DeleteFromStorageAsync(StoredFileVersion version) { var blob = GetBlobClient(GetAzureFileName(version)); - await blob.DeleteAsync(); + using var response = await blob.DeleteAsync(); } /// inheritedDoc protected override void DeleteFromStorage(StoredFileVersion version) { var blob = GetBlobClient(GetAzureFileName(version)); - blob.Delete(); + using var response = blob.Delete(); } public override async Task FileExistsAsync(Guid id) diff --git a/shesha-core/src/Shesha.Framework/Services/StoredFiles/StoredFileService.cs b/shesha-core/src/Shesha.Framework/Services/StoredFiles/StoredFileService.cs index 8807ce848a..974f170825 100644 --- a/shesha-core/src/Shesha.Framework/Services/StoredFiles/StoredFileService.cs +++ b/shesha-core/src/Shesha.Framework/Services/StoredFiles/StoredFileService.cs @@ -1,11 +1,10 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Abp.Domain.Repositories; +using Abp.Domain.Repositories; using Shesha.Configuration; using Shesha.Domain; -using Shesha.Extensions; using Shesha.Utilities; +using System; +using System.IO; +using System.Threading.Tasks; namespace Shesha.Services.StoredFiles { @@ -23,27 +22,16 @@ public StoredFileService(IRepository fileService, IRepository< _pathHelper = pathHelper; } - /// - /// Returns physical path of the latest version of the specified - /// - public string PhysicalFilePath(StoredFile file) - { - return PhysicalFilePath(file?.LastVersion()); - } - /// /// Returns physical path of the specified version /// public string PhysicalFilePath(StoredFileVersion fileVersion) { - if (fileVersion == null) - return null; - var folder = fileVersion.File.Folder; var path = _pathHelper.Combine( !string.IsNullOrWhiteSpace(folder) && (Path.IsPathRooted(folder) || folder.StartsWith("~")) ? "" - : _sheshaSettings.UploadFolder.GetValue(), + : _sheshaSettings.UploadFolder.GetValueOrNull() ?? string.Empty, folder ?? "", fileVersion.Id + fileVersion.FileType); @@ -52,9 +40,6 @@ public string PhysicalFilePath(StoredFileVersion fileVersion) public override async Task GetStreamAsync(StoredFileVersion fileVersion) { - if (fileVersion == null) - return null; - await using(var fileStream = new FileStream(PhysicalFilePath(fileVersion), FileMode.Open, FileAccess.Read)) { // copy to MemoryStream to prevent sharing violations @@ -69,9 +54,6 @@ public override async Task GetStreamAsync(StoredFileVersion fileVersion) public override Stream GetStream(StoredFileVersion fileVersion) { - if (fileVersion == null) - return null; - using (var fileStream = new FileStream(PhysicalFilePath(fileVersion), FileMode.Open, FileAccess.Read)) { // copy to MemoryStream to prevent sharing violations @@ -85,9 +67,6 @@ public override Stream GetStream(StoredFileVersion fileVersion) } public async Task GetStreamAsync(string filePath) { - if (string.IsNullOrEmpty(filePath)) - return null; - await using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { // copy to MemoryStream to prevent sharing violations @@ -159,7 +138,7 @@ public override async Task FileExistsAsync(Guid id) return false; var path = PhysicalFilePath(lastVersion); - return File.Exists(path); + return File.Exists(path); } } } diff --git a/shesha-core/src/Shesha.Framework/Services/StoredFiles/StoredFileServiceBase.cs b/shesha-core/src/Shesha.Framework/Services/StoredFiles/StoredFileServiceBase.cs index ad62e3396b..1322872ad0 100644 --- a/shesha-core/src/Shesha.Framework/Services/StoredFiles/StoredFileServiceBase.cs +++ b/shesha-core/src/Shesha.Framework/Services/StoredFiles/StoredFileServiceBase.cs @@ -5,6 +5,7 @@ using Shesha.Domain; using Shesha.EntityReferences; using Shesha.Extensions; +using Shesha.Reflection; using System; using System.Collections.Generic; using System.IO; @@ -43,7 +44,7 @@ protected StoredFileServiceBase(IRepository fileService, IRepo /// /// Returns list of files attached to the specified entity with the specified /// - public async Task> GetAttachmentsOfCategoryAsync([NotNull] IEntity owner, string fileCategory) + public async Task> GetAttachmentsOfCategoryAsync([NotNull] IEntity owner, string? fileCategory) { return await GetAttachmentsInternalAsync(owner.Id, owner.GetTypeShortAlias(), f => f.Category == fileCategory); } @@ -51,7 +52,7 @@ public async Task> GetAttachmentsOfCategoryAsync([NotNull /// /// Returns list of files attached to the entity with the specified , and /// - public async Task> GetAttachmentsOfCategoryAsync(TId id, string typeShortAlias, string fileCategory) + public async Task> GetAttachmentsOfCategoryAsync(TId id, string typeShortAlias, string? fileCategory) { return await GetAttachmentsInternalAsync(id, typeShortAlias, f => f.Category == fileCategory); } @@ -72,17 +73,17 @@ public async Task> GetAttachmentsAsync(TId id, string typ return await GetAttachmentsInternalAsync(id, typeShortAlias); } - private async Task> GetAttachmentsInternalAsync(TId id, string typeShortAlias, Expression> filterPredicate = null) + private async Task> GetAttachmentsInternalAsync(TId id, string typeShortAlias, Expression>? filterPredicate = null) { return await GetAttachmentsQuery(id, typeShortAlias, filterPredicate).OrderBy(e => e.SortOrder).ToListAsync(); } - private async Task> GetLastVersionsOfAttachmentsInternalAsync(TId id, string typeShortAlias, Expression> filterPredicate = null) + private async Task> GetLastVersionsOfAttachmentsInternalAsync(TId id, string typeShortAlias, Expression>? filterPredicate = null) { return await GetAttachmentsLastVersionsQuery(id, typeShortAlias, filterPredicate).OrderBy(e => e.File.SortOrder).ToListAsync(); } - public async Task> GetLastVersionsOfAttachmentsAsync(TId id, string typeShortAlias, string fileCategory) + public async Task> GetLastVersionsOfAttachmentsAsync(TId id, string typeShortAlias, string? fileCategory) { return await GetLastVersionsOfAttachmentsInternalAsync(id, typeShortAlias, f => f.File.Category == fileCategory); } @@ -95,12 +96,12 @@ public async Task> GetLastVersionsOfAttachmentsAsync HasAttachmentsOfCategoryAsync(IEntity owner, string fileCategory) + public async Task HasAttachmentsOfCategoryAsync(IEntity owner, string? fileCategory) { return await HasAttachmentsOfCategoryAsync(owner.Id, owner.GetTypeShortAlias(), fileCategory); } - public async Task HasAttachmentsOfCategoryAsync(TId id, string typeShortAlias, string fileCategory) + public async Task HasAttachmentsOfCategoryAsync(TId id, string typeShortAlias, string? fileCategory) { return await GetAttachmentsQuery(id, typeShortAlias, f => f.Category == fileCategory).AnyAsync(); } @@ -125,7 +126,7 @@ public async Task GetStreamAsync(StoredFile file) public Stream GetStream(StoredFile file) { - var lastVersion = GetLastVersion(file); + var lastVersion = GetLastVersion(file) ?? throw new EntityNotFoundException($"Failed to find last version of stored file '{file.Id}'"); return GetStream(lastVersion); } @@ -140,7 +141,7 @@ public virtual async Task CopyToOwnerAsync(StoredFile file, IEn { // todo: move to the base class and reuse in the AzureFileService - var newFile = new StoredFile(EntityConfigurationStore) + var newFile = new StoredFile { Description = file.Description, FileName = file.FileName, @@ -205,16 +206,17 @@ public async Task CopyAttachmentsToAsync(IEntity GetAttachmentsQuery(TId id, string typeShortAlias, Expression> filterPredicate = null) + private IQueryable GetAttachmentsQuery(TId id, string typeShortAlias, Expression>? filterPredicate = null) { - IQueryable query = null; + IQueryable? query = null; var ecs = StaticContext.IocManager.Resolve(); var config = ecs.Get(typeShortAlias); + var stringId = id?.ToString().NotNullOrWhiteSpace(); if (config != null) { var className = config.EntityType.FullName; - query = FileRepository.GetAll().Where(e => e.Owner.Id == id.ToString()); + query = FileRepository.GetAll().Where(e => e.Owner.Id == stringId); query = config.HasTypeShortAlias ? query.Where(e => e.Owner._className == className || e.Owner._className == config.TypeShortAlias) : query.Where(e => e.Owner._className == className); @@ -224,7 +226,7 @@ private IQueryable GetAttachmentsQuery(TId id, string typeShort } else { - query = FileRepository.GetAll().Where(e => e.Owner.Id == id.ToString() && e.Owner._className == typeShortAlias); + query = FileRepository.GetAll().Where(e => e.Owner.Id == stringId && e.Owner._className == typeShortAlias); if (filterPredicate != null) query = query.Where(filterPredicate); } @@ -232,17 +234,19 @@ private IQueryable GetAttachmentsQuery(TId id, string typeShort return query; } - private IQueryable GetAttachmentsLastVersionsQuery(TId id, string typeShortAlias, Expression> filterPredicate = null) + private IQueryable GetAttachmentsLastVersionsQuery(TId id, string typeShortAlias, Expression>? filterPredicate = null) { IQueryable query = VersionRepository.GetAll().Where(e => e.IsLast); var ecs = StaticContext.IocManager.Resolve(); var config = ecs.Get(typeShortAlias); + var stringId = id?.ToString().NotNullOrWhiteSpace(); + if (config != null) { var className = config.EntityType.FullName; - query = VersionRepository.GetAll().Where(e => e.File.Owner.Id == id.ToString()); + query = VersionRepository.GetAll().Where(e => e.File.Owner.Id == stringId); query = config.HasTypeShortAlias ? query.Where(e => e.File.Owner._className == className || e.File.Owner._className == config.TypeShortAlias) : query.Where(e => e.File.Owner._className == className); @@ -252,7 +256,7 @@ private IQueryable GetAttachmentsLastVersionsQuery(TId i } else { - query = VersionRepository.GetAll().Where(e => e.File.Owner.Id == id.ToString() && e.File.Owner._className == typeShortAlias); + query = VersionRepository.GetAll().Where(e => e.File.Owner.Id == stringId && e.File.Owner._className == typeShortAlias); if (filterPredicate != null) query = query.Where(filterPredicate); } @@ -275,8 +279,8 @@ public Task MarkDownloadedAsync(StoredFileVersion fileVersion) /// public async Task> GetAttachmentsCategoriesAsync(IEntity owner) { - return await GetAttachmentsQuery(owner.Id, owner.GetTypeShortAlias()).Select(f => f.Category).Distinct() - .ToListAsync(); + var categories = await GetAttachmentsQuery(owner.Id, owner.GetTypeShortAlias()).Select(f => f.Category).Distinct().ToListAsync(); + return categories.WhereNotNullOrWhiteSpace().ToList(); } /// @@ -352,7 +356,7 @@ protected async Task GetLastVersionAsync(Guid fileId) /// /// File Id /// - protected StoredFileVersion GetLastVersion(Guid fileId) + protected StoredFileVersion? GetLastVersion(Guid fileId) { return QueryLastVersion(fileId).FirstOrDefault(); } @@ -372,7 +376,7 @@ public async Task GetLastVersionAsync(StoredFile file) /// /// /// - public StoredFileVersion GetLastVersion(StoredFile file) + public StoredFileVersion? GetLastVersion(StoredFile file) { return GetLastVersion(file.Id); } @@ -463,7 +467,7 @@ public async Task GetOrNullAsync(Guid id) public abstract Task UpdateVersionContentAsync(StoredFileVersion version, Stream stream); /// inheritedDoc - public async Task CreateFileAsync(Stream stream, string fileName, Action prepareFileAction = null) + public async Task CreateFileAsync(Stream stream, string fileName, Action? prepareFileAction = null) { if (stream == null) throw new Exception($"{nameof(stream)} must not be null"); @@ -471,7 +475,7 @@ public async Task CreateFileAsync(Stream stream, string fileN throw new Exception($"{nameof(fileName)} must not be null or empty"); // create new file - var storedFile = new StoredFile(EntityConfigurationStore) + var storedFile = new StoredFile { FileName = Path.GetFileName(fileName), FileType = Path.GetExtension(fileName), @@ -495,7 +499,7 @@ public async Task CreateFileAsync(Stream stream, string fileN } /// inheritedDoc - public async Task SaveFileAsync(Stream stream, string fileName, Action prepareFileAction = null) + public async Task SaveFileAsync(Stream stream, string fileName, Action? prepareFileAction = null) { var fileVersion = await CreateFileAsync(stream, fileName, prepareFileAction); diff --git a/shesha-core/src/Shesha.Framework/Services/Urls/ILinkGeneratorContext.cs b/shesha-core/src/Shesha.Framework/Services/Urls/ILinkGeneratorContext.cs index fc595d74e4..80cf37ce10 100644 --- a/shesha-core/src/Shesha.Framework/Services/Urls/ILinkGeneratorContext.cs +++ b/shesha-core/src/Shesha.Framework/Services/Urls/ILinkGeneratorContext.cs @@ -1,12 +1,10 @@ -using Microsoft.AspNetCore.Http; -using Shesha.ConfigurationItems; -using System; +using System; namespace Shesha.Services.Urls { public interface ILinkGeneratorContext { - public LinkGeneratorState State { get; } + public LinkGeneratorState? State { get; } /// /// Begin scope diff --git a/shesha-core/src/Shesha.Framework/Services/Urls/LinkGeneratorContext.cs b/shesha-core/src/Shesha.Framework/Services/Urls/LinkGeneratorContext.cs index 7649a210e5..1263bfe94b 100644 --- a/shesha-core/src/Shesha.Framework/Services/Urls/LinkGeneratorContext.cs +++ b/shesha-core/src/Shesha.Framework/Services/Urls/LinkGeneratorContext.cs @@ -1,7 +1,6 @@ using Abp.Dependency; using Abp.Runtime; using Microsoft.AspNetCore.Http; -using Shesha.ConfigurationItems; using System; namespace Shesha.Services.Urls @@ -18,9 +17,9 @@ public LinkGeneratorContext(IAmbientScopeProvider scopeProvi _httpContextAccessor = httpContextAccessor; } - private LinkGeneratorState _defaultState; + private LinkGeneratorState? _defaultState; - private LinkGeneratorState GetDefaultState() + private LinkGeneratorState? GetDefaultState() { var request = _httpContextAccessor.HttpContext?.Request; return request != null @@ -28,7 +27,7 @@ private LinkGeneratorState GetDefaultState() : null; } - public LinkGeneratorState State + public LinkGeneratorState? State { get { diff --git a/shesha-core/src/Shesha.Framework/Services/Urls/LinkGeneratorState.cs b/shesha-core/src/Shesha.Framework/Services/Urls/LinkGeneratorState.cs index 7f5f14f453..30049067cb 100644 --- a/shesha-core/src/Shesha.Framework/Services/Urls/LinkGeneratorState.cs +++ b/shesha-core/src/Shesha.Framework/Services/Urls/LinkGeneratorState.cs @@ -4,10 +4,10 @@ namespace Shesha.Services.Urls { public class LinkGeneratorState { - public string Scheme { get; set; } - public string Host { get; set; } + public string? Scheme { get; set; } + public string? Host { get; set; } public int Port { get; set; } - public string PathBase { get; set; } + public string? PathBase { get; set; } public LinkGeneratorState(HttpRequest request) { @@ -17,14 +17,6 @@ public LinkGeneratorState(HttpRequest request) PathBase = request?.PathBase; } - public LinkGeneratorState(string scheme, string host, int port, string pathBase = default) - { - Scheme = scheme; - Host = host; - Port = port; - PathBase = pathBase; - } - public LinkGeneratorState() { diff --git a/shesha-core/src/Shesha.Framework/Services/VersionedFields/IVersionedFieldManager.cs b/shesha-core/src/Shesha.Framework/Services/VersionedFields/IVersionedFieldManager.cs index a0d5a0856a..0cd6a4e005 100644 --- a/shesha-core/src/Shesha.Framework/Services/VersionedFields/IVersionedFieldManager.cs +++ b/shesha-core/src/Shesha.Framework/Services/VersionedFields/IVersionedFieldManager.cs @@ -1,7 +1,4 @@ using Abp.Domain.Entities; -using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace Shesha.Services.VersionedFields @@ -19,7 +16,7 @@ public interface IVersionedFieldManager /// /// /// - Task GetVersionedFieldValueAsync(TEntity owner, string fieldName) where TEntity : IEntity; + Task GetVersionedFieldValueAsync(TEntity owner, string fieldName) where TEntity : IEntity where TId : notnull; /// /// Set versioned field value @@ -31,6 +28,6 @@ public interface IVersionedFieldManager /// /// /// - Task SetVersionedFieldValueAsync(TEntity owner, string fieldName, string value, bool createNewVersion) where TEntity : IEntity; + Task SetVersionedFieldValueAsync(TEntity owner, string fieldName, string value, bool createNewVersion) where TEntity : IEntity where TId : notnull; } } diff --git a/shesha-core/src/Shesha.Framework/Services/VersionedFields/VersionedFieldManager.cs b/shesha-core/src/Shesha.Framework/Services/VersionedFields/VersionedFieldManager.cs index 26654f2988..e873d6a4a8 100644 --- a/shesha-core/src/Shesha.Framework/Services/VersionedFields/VersionedFieldManager.cs +++ b/shesha-core/src/Shesha.Framework/Services/VersionedFields/VersionedFieldManager.cs @@ -30,7 +30,9 @@ public VersionedFieldManager(IRepository fieldRepository, _currentUowProvider = currentUowProvider; } - public async Task GetVersionedFieldAsync(TEntity owner, string fieldName) where TEntity : IEntity + public async Task GetVersionedFieldAsync(TEntity owner, string fieldName) + where TEntity : IEntity + where TId : notnull { var config = _entityConfigurationStore.Get(typeof(TEntity)); @@ -39,7 +41,9 @@ public async Task GetVersionedFieldAsync(TEntity o .FirstOrDefaultAsync(); } - public VersionedField GetVersionedField(TEntity owner, string fieldName) where TEntity : IEntity + public VersionedField? GetVersionedField(TEntity owner, string fieldName) + where TEntity : IEntity + where TId : notnull { var config = _entityConfigurationStore.Get(typeof(TEntity)); @@ -50,7 +54,9 @@ public VersionedField GetVersionedField(TEntity owner, string fiel /// /// Creates versioned field is missing /// - public async Task GetOrCreateFieldAsync(TEntity owner, string fieldName, Action initAction = null) where TEntity : IEntity + public async Task GetOrCreateFieldAsync(TEntity owner, string fieldName, Action? initAction = null) + where TEntity : IEntity + where TId : notnull { var field = await GetVersionedFieldAsync(owner, fieldName); if (field != null) @@ -77,7 +83,9 @@ public async Task GetOrCreateFieldAsync(TEntity ow return field; } - public async Task CreateFieldAsync(TEntity owner, string fieldName, Action initAction = null) where TEntity : IEntity + public async Task CreateFieldAsync(TEntity owner, string fieldName, Action? initAction = null) + where TEntity : IEntity + where TId: notnull { var field = new VersionedField { @@ -91,7 +99,9 @@ public async Task CreateFieldAsync(TEntity owner, return field; } - public VersionedField CreateField(TEntity owner, string fieldName, Action initAction = null) where TEntity : IEntity + public VersionedField CreateField(TEntity owner, string fieldName, Action? initAction = null) + where TEntity : IEntity + where TId : notnull { var field = new VersionedField { @@ -111,7 +121,9 @@ public async Task GetLastVersionAsync(VersionedField fiel return version; } - public async Task GetVersionedFieldValueAsync(TEntity owner, string fieldName) where TEntity : IEntity + public async Task GetVersionedFieldValueAsync(TEntity owner, string fieldName) + where TEntity : IEntity + where TId : notnull { var field = await GetVersionedFieldAsync(owner, fieldName); var version = field != null @@ -121,7 +133,9 @@ public async Task GetVersionedFieldValueAsync(TEntity owne return version?.Content; } - public async Task SetVersionedFieldValueAsync(TEntity owner, string fieldName, string value, bool createNewVersion) where TEntity : IEntity + public async Task SetVersionedFieldValueAsync(TEntity owner, string fieldName, string value, bool createNewVersion) + where TEntity : IEntity + where TId : notnull { var field = await GetOrCreateFieldAsync(owner, fieldName); diff --git a/shesha-core/src/Shesha.Framework/Settings/Attributes/SettingAttribute.cs b/shesha-core/src/Shesha.Framework/Settings/Attributes/SettingAttribute.cs index 7ae1a4fe3d..0137dcea17 100644 --- a/shesha-core/src/Shesha.Framework/Settings/Attributes/SettingAttribute.cs +++ b/shesha-core/src/Shesha.Framework/Settings/Attributes/SettingAttribute.cs @@ -10,7 +10,7 @@ public class SettingAttribute : Attribute { public string Name { get; set; } public bool IsClientSpecific { get; set; } - public string EditorFormName { get; set; } + public string? EditorFormName { get; set; } public bool IsUserSpecific { get; set; } public SettingAttribute(string name) : this(name, false, null, false) @@ -25,7 +25,7 @@ public SettingAttribute(string name, bool isClientSpecific, string editorFormNam { } - public SettingAttribute(string name, bool isClientSpecific = false, string editorFormName = null, bool isUserSpecific = false) + public SettingAttribute(string name, bool isClientSpecific = false, string? editorFormName = null, bool isUserSpecific = false) { Name = name; IsClientSpecific = isClientSpecific; diff --git a/shesha-core/src/Shesha.Framework/Settings/DefaultSettingDefinitionProvider.cs b/shesha-core/src/Shesha.Framework/Settings/DefaultSettingDefinitionProvider.cs index 4f64943941..2617acefc5 100644 --- a/shesha-core/src/Shesha.Framework/Settings/DefaultSettingDefinitionProvider.cs +++ b/shesha-core/src/Shesha.Framework/Settings/DefaultSettingDefinitionProvider.cs @@ -1,16 +1,13 @@ using Abp.Dependency; using Shesha.Attributes; -using Shesha.ConfigurationItems; using Shesha.Domain; using Shesha.Extensions; using Shesha.Modules; using Shesha.Reflection; using Shesha.Settings.Ioc; using Shesha.Utilities; -using System; using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; namespace Shesha.Settings { @@ -36,7 +33,7 @@ public void Define(ISettingDefinitionContext context) { var accessorType = accessor.GetType(); - var interfaceType = accessorType.GetInterfaces().Where(t => t != typeof(ISettingAccessors) && typeof(ISettingAccessors).IsAssignableFrom(t)).FirstOrDefault(); + var interfaceType = accessorType.GetInterfaces().Where(t => t != typeof(ISettingAccessors) && typeof(ISettingAccessors).IsAssignableFrom(t)).First(); var moduleInfo = interfaceType.GetConfigurableModuleInfo(); if (moduleInfo == null) @@ -58,7 +55,7 @@ public void Define(ISettingDefinitionContext context) } } - private SettingDefinition GetSettingDefinition(ISettingAccessor propertyInstance, PropertyInfo property, SheshaModuleInfo moduleInfo) + private SettingDefinition? GetSettingDefinition(ISettingAccessor propertyInstance, PropertyInfo property, SheshaModuleInfo moduleInfo) { var valueType = property.PropertyType.IsGenericType ? property.PropertyType.GenericTypeArguments[0] @@ -68,7 +65,7 @@ private SettingDefinition GetSettingDefinition(ISettingAccessor propertyInstance var definitionType = typeof(SettingDefinition<>).MakeGenericType(valueType); - var settingAttribute = property.GetAttribute(); + var settingAttribute = property.GetAttributeOrNull(); if (settingAttribute == null) return null; @@ -79,7 +76,7 @@ private SettingDefinition GetSettingDefinition(ISettingAccessor propertyInstance var defaultValue = propertyInstance.GetDefaultValue(); - var definition = Activator.CreateInstance(definitionType, name, defaultValue, displayName) as SettingDefinition; + var definition = ActivatorHelper.CreateNotNullObject(definitionType, name, defaultValue, displayName).ForceCastAs(); definition.Accessor = property.GetPropertyAccessor(); @@ -95,8 +92,8 @@ private SettingDefinition GetSettingDefinition(ISettingAccessor propertyInstance ? new SettingConfigurationIdentifier(definition.ModuleName, settingAttribute.EditorFormName) : null; - definition.Category = property.GetCategory() ?? property.DeclaringType.GetCategory(); - definition.CategoryAccessor = CodeNamingHelper.GetAccessor(UnwrapSettingAccessorName(property.DeclaringType.Name), property.DeclaringType.GetAttribute()?.Alias); + definition.Category = property.GetCategory() ?? property.DeclaringType?.GetCategory(); + definition.CategoryAccessor = CodeNamingHelper.GetAccessor(UnwrapSettingAccessorName(property.DeclaringType.NotNull().Name), property.DeclaringType?.GetAttributeOrNull()?.Alias); return definition; } diff --git a/shesha-core/src/Shesha.Framework/Settings/DynamicSettingDefinitionProvider.cs b/shesha-core/src/Shesha.Framework/Settings/DynamicSettingDefinitionProvider.cs index 25b7aeb9cf..2ee8f12004 100644 --- a/shesha-core/src/Shesha.Framework/Settings/DynamicSettingDefinitionProvider.cs +++ b/shesha-core/src/Shesha.Framework/Settings/DynamicSettingDefinitionProvider.cs @@ -2,6 +2,7 @@ using Abp.Domain.Repositories; using Abp.Domain.Uow; using Shesha.Domain; +using Shesha.Reflection; using System; using System.Linq; @@ -34,12 +35,12 @@ public void Define(ISettingDefinitionContext context) var dbSettings = _settingConfigurationRepository.GetAll().Where(sc => sc.Module != null).ToList(); var dynamicallyCreatedSettings = dbSettings - .Where(c => !settings.Any(d => d.Value.Name == c.Name && d.Value.ModuleName == c.Module.Name)) + .Where(c => !settings.Any(d => d.Value.Name == c.Name && d.Value.ModuleName == c.Module.NotNull().Name)) .ToList(); foreach (var setting in dynamicallyCreatedSettings) { - var definition = _settingDefinitionManager.CreateUserSettingDefinition(setting.Module.Name, setting.Name, setting.DataType, null); + var definition = _settingDefinitionManager.CreateUserSettingDefinition(setting.Module.NotNull().Name, setting.Name, setting.DataType, null); var id = new SettingIdentifier(definition.ModuleName, definition.Name); if (settings.ContainsKey(id)) diff --git a/shesha-core/src/Shesha.Framework/Settings/Exceptions/UnexpectedNullSettingValueException.cs b/shesha-core/src/Shesha.Framework/Settings/Exceptions/UnexpectedNullSettingValueException.cs new file mode 100644 index 0000000000..b345df6b45 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Settings/Exceptions/UnexpectedNullSettingValueException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Shesha.Settings.Exceptions +{ + /// + /// Indicated that setting value is null unexpectedly + /// + public class UnexpectedNullSettingValueException: Exception + { + public string SettingModule { get; private set; } + public string SettingName { get; private set; } + + public UnexpectedNullSettingValueException(string module, string name): base($"Setting '{module}.{name}' value is null but requested as not nullable") + { + SettingModule = module; + SettingName = name; + } + } +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Settings/Extensions.cs b/shesha-core/src/Shesha.Framework/Settings/Extensions.cs index 726cad421e..d01d8bab56 100644 --- a/shesha-core/src/Shesha.Framework/Settings/Extensions.cs +++ b/shesha-core/src/Shesha.Framework/Settings/Extensions.cs @@ -1,5 +1,4 @@ -using Abp.Configuration; -using Abp.Extensions; +using Abp.Extensions; using Shesha.Extensions; using Shesha.Metadata; using Shesha.Reflection; @@ -87,7 +86,7 @@ public static DataTypeInfo GetSettingDataType(this SettingDefinition definition) ? propType.GetGenericArguments()[0] : propType.GetElementType(); - var format = paramType.IsClass + var format = paramType.NotNull().IsClass ? paramType.IsEntityType() ? ArrayFormats.EntityReference : paramType.IsJsonEntityType() diff --git a/shesha-core/src/Shesha.Framework/Settings/ISettingAccessor.cs b/shesha-core/src/Shesha.Framework/Settings/ISettingAccessor.cs index d5c7932924..632c0e0e8a 100644 --- a/shesha-core/src/Shesha.Framework/Settings/ISettingAccessor.cs +++ b/shesha-core/src/Shesha.Framework/Settings/ISettingAccessor.cs @@ -11,7 +11,7 @@ public interface ISettingAccessor /// Get default value /// /// - object GetDefaultValue(); + object? GetDefaultValue(); } /// @@ -22,17 +22,27 @@ public interface ISettingAccessor : ISettingAccessor /// /// Get setting value /// - Task GetValueAsync(); + Task GetValueOrNullAsync(SettingManagementContext? context = null); /// /// Get setting value /// - TValue GetValue(); + Task GetValueAsync(SettingManagementContext? context = null); + + /// + /// Get setting value + /// + TValue? GetValueOrNull(SettingManagementContext? context = null); + + /// + /// Get setting value + /// + TValue GetValue(SettingManagementContext? context = null); /// /// Set setting value /// - Task SetValueAsync(TValue value); + Task SetValueAsync(TValue? value); /// /// Sets default value of the setting diff --git a/shesha-core/src/Shesha.Framework/Settings/ISettingDefinitionManager.cs b/shesha-core/src/Shesha.Framework/Settings/ISettingDefinitionManager.cs index b81c9d1ee3..4cfd0466e1 100644 --- a/shesha-core/src/Shesha.Framework/Settings/ISettingDefinitionManager.cs +++ b/shesha-core/src/Shesha.Framework/Settings/ISettingDefinitionManager.cs @@ -1,5 +1,4 @@ using JetBrains.Annotations; -using System; using System.Collections.Generic; namespace Shesha.Settings @@ -9,15 +8,41 @@ namespace Shesha.Settings /// public interface ISettingDefinitionManager { + /// + /// Get settings definition by module and name. Throws exception when definition not found + /// + /// Module name + /// Settings name [NotNull] SettingDefinition Get([NotNull] string moduleName, [NotNull] string name); + /// + /// Get all settings definitions + /// + /// IReadOnlyList GetAll(); + /// + /// Add setting definition + /// + /// void AddDefinition(SettingDefinition definition); + /// + /// Get settings definition by module and name. Returns null when definition not found + /// + /// Module name + /// Settings name SettingDefinition GetOrNull([NotNull] string moduleName, string name); - SettingDefinition CreateUserSettingDefinition(string module, string name, string dataType, object value); + /// + /// Create user setting definition + /// + /// Module name + /// Setting name + /// Data type + /// Value + /// + SettingDefinition CreateUserSettingDefinition(string module, string name, string dataType, object? value); } } diff --git a/shesha-core/src/Shesha.Framework/Settings/IShaSettingManager.cs b/shesha-core/src/Shesha.Framework/Settings/IShaSettingManager.cs index f22f8fcf33..fd404e47bd 100644 --- a/shesha-core/src/Shesha.Framework/Settings/IShaSettingManager.cs +++ b/shesha-core/src/Shesha.Framework/Settings/IShaSettingManager.cs @@ -16,7 +16,7 @@ public interface IShaSettingManager /// Setting name /// /// - Task GetJObjectOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext context = null); + Task GetJObjectOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext? context = null); /// /// Get setting value @@ -25,7 +25,7 @@ public interface IShaSettingManager /// Setting name /// /// - Task GetOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext context = null); + Task GetOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext? context = null); /// /// @@ -36,7 +36,7 @@ public interface IShaSettingManager /// /// /// - Task UserSpecificGetOrNullAsync([NotNull] string module, [NotNull] string name, string dataType, TValue defaultValue, SettingManagementContext context = null); + Task UserSpecificGetOrNullAsync([NotNull] string module, [NotNull] string name, string? dataType, TValue? defaultValue, SettingManagementContext? context = null); /// /// Get setting value or null @@ -45,7 +45,7 @@ public interface IShaSettingManager /// Setting name /// /// - Task GetOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext context = null); + Task GetOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext? context = null); /// /// Set setting value @@ -55,7 +55,7 @@ public interface IShaSettingManager /// Setting value /// /// - Task SetAsync([NotNull] string module, [NotNull] string name, [CanBeNull] TValue value, SettingManagementContext context = null); + Task SetAsync([NotNull] string module, [NotNull] string name, [CanBeNull] TValue? value, SettingManagementContext? context = null); /// /// @@ -67,6 +67,6 @@ public interface IShaSettingManager /// /// /// - Task UpdateUserSettingAsync([NotNull] string module, [NotNull] string name, string dataType, [CanBeNull] TValue value, SettingManagementContext context = null); + Task UpdateUserSettingAsync([NotNull] string module, [NotNull] string name, string dataType, [CanBeNull] TValue? value, SettingManagementContext? context = null); } } diff --git a/shesha-core/src/Shesha.Framework/Settings/Ioc/IocManagerExtensions.cs b/shesha-core/src/Shesha.Framework/Settings/Ioc/IocManagerExtensions.cs index 0ec134a636..68f95b447a 100644 --- a/shesha-core/src/Shesha.Framework/Settings/Ioc/IocManagerExtensions.cs +++ b/shesha-core/src/Shesha.Framework/Settings/Ioc/IocManagerExtensions.cs @@ -15,7 +15,7 @@ public static class IocManagerExtensions /// Interface of the settings group. Must implement the interface /// IoC Manager /// Initializer of the setting instance. Is used to provide default values of the settings - public static void RegisterSettingAccessor(this IIocManager iocManager, Action initializer = null) where TAccessor: class, ISettingAccessors + public static void RegisterSettingAccessor(this IIocManager iocManager, Action? initializer = null) where TAccessor: class, ISettingAccessors { if (!iocManager.IsRegistered()) iocManager.IocContainer.Register( diff --git a/shesha-core/src/Shesha.Framework/Settings/Ioc/SettingsAccessorFactory.cs b/shesha-core/src/Shesha.Framework/Settings/Ioc/SettingsAccessorFactory.cs index 5797016f57..645f9b0e15 100644 --- a/shesha-core/src/Shesha.Framework/Settings/Ioc/SettingsAccessorFactory.cs +++ b/shesha-core/src/Shesha.Framework/Settings/Ioc/SettingsAccessorFactory.cs @@ -1,42 +1,39 @@ using Abp.Dependency; using Castle.MicroKernel; -using Shesha.Configuration; -using System; -using System.Collections.Generic; +using Shesha.Extensions; +using Shesha.Reflection; using System.Linq; namespace Shesha.Settings.Ioc { public interface ISettingsAccessorFactory { - TAccessor BuildAccessor() where TAccessor : ISettingAccessors; + TAccessor BuildAccessor() where TAccessor : class, ISettingAccessors; } public class SettingsAccessorFactory: ISettingsAccessorFactory { private readonly IocManager _iocManager; - private readonly Dictionary _accessorTypes; public SettingsAccessorFactory(IocManager iocManager) { _iocManager = iocManager; - _accessorTypes = new Dictionary(); } - public TAccessor BuildAccessor() where TAccessor : ISettingAccessors + public TAccessor BuildAccessor() where TAccessor : class, ISettingAccessors { var builder = new SettingsTypeBuilder(); var interfaceType = typeof(TAccessor); var classType = builder.BuildType(interfaceType); - var instance = (TAccessor)Activator.CreateInstance(classType); + var instance = ActivatorHelper.CreateNotNullObject(classType).ForceCastAs(); // fill all ISettingAccessor properties var interfaceProperties = interfaceType.GetProperties().Where(p => typeof(ISettingAccessor).IsAssignableFrom(p.PropertyType)).ToList(); foreach (var interfaceProperty in interfaceProperties) { - var classProperty = classType.GetProperty(interfaceProperty.Name); + var classProperty = classType.GetRequiredProperty(interfaceProperty.Name); var settingAccessor = _iocManager.IocContainer.Resolve(interfaceProperty.PropertyType, new Arguments { { "property", interfaceProperty } }); diff --git a/shesha-core/src/Shesha.Framework/Settings/Ioc/SettingsTypeBuilder.cs b/shesha-core/src/Shesha.Framework/Settings/Ioc/SettingsTypeBuilder.cs index 1f762705ba..91fef47353 100644 --- a/shesha-core/src/Shesha.Framework/Settings/Ioc/SettingsTypeBuilder.cs +++ b/shesha-core/src/Shesha.Framework/Settings/Ioc/SettingsTypeBuilder.cs @@ -1,12 +1,9 @@ using Abp.Extensions; -using Shesha.Configuration; -using Shesha.DynamicEntities.Json; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; -using System.Security.Cryptography.Xml; namespace Shesha.Settings.Ioc { @@ -122,16 +119,16 @@ private static CustomAttributeBuilder ToAttributeBuilder(CustomAttributeData dat throw new ArgumentNullException("data"); } - var constructorArguments = new List(); + var constructorArguments = new List(); foreach (var ctorArg in data.ConstructorArguments) { constructorArguments.Add(ctorArg.Value); } var propertyArguments = new List(); - var propertyArgumentValues = new List(); + var propertyArgumentValues = new List(); var fieldArguments = new List(); - var fieldArgumentValues = new List(); + var fieldArgumentValues = new List(); foreach (var namedArg in data.NamedArguments) { var fi = namedArg.MemberInfo as FieldInfo; @@ -170,52 +167,5 @@ private static List GetAttributeBuilders(MemberInfo oldM return result; } - /* - private static CustomAttributeBuilder AddAttributesToMemberInfo(MemberInfo oldMember) - { - CustomAttributeBuilder ct = null; - IList customMethodAttributes = CustomAttributeData.GetCustomAttributes(oldMember); - foreach (CustomAttributeData att in customMethodAttributes) - { - List namedFieldValues = new List(); - List fields = new List(); - List constructorArguments = new List(); - foreach (CustomAttributeTypedArgument cata in att.ConstructorArguments) - { - constructorArguments.Add(cata.Value); - } - if (att.NamedArguments.Count > 0) - { - FieldInfo[] possibleFields = att.GetType().GetFields(); - - foreach (CustomAttributeNamedArgument cana in att.NamedArguments) - { - for (int x = 0; x < possibleFields.Length; x++) - { - if (possibleFields[x].Name.CompareTo(cana.MemberInfo.Name) == 0) - { - fields.Add(possibleFields[x]); - namedFieldValues.Add(cana.TypedValue.Value); - } - } - - - } - } - - if (namedFieldValues.Count > 0) - { - ct = new CustomAttributeBuilder(att.Constructor, constructorArguments.ToArray(), fields.ToArray(), namedFieldValues.ToArray()); - } - else - { - ct = new CustomAttributeBuilder(att.Constructor, constructorArguments.ToArray()); - } - - - } - return ct; - } - */ } } diff --git a/shesha-core/src/Shesha.Framework/Settings/Json/NullToDefaultConverter.cs b/shesha-core/src/Shesha.Framework/Settings/Json/NullToDefaultConverter.cs index da391cc802..4dd566bac1 100644 --- a/shesha-core/src/Shesha.Framework/Settings/Json/NullToDefaultConverter.cs +++ b/shesha-core/src/Shesha.Framework/Settings/Json/NullToDefaultConverter.cs @@ -13,7 +13,7 @@ public override bool CanConvert(Type objectType) return defaultValue != null; } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { var token = JToken.Load(reader); if (token.Type == JTokenType.Null) @@ -25,7 +25,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist // Return false I don't want default values to be written as null public override bool CanWrite => false; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new NotImplementedException(); } diff --git a/shesha-core/src/Shesha.Framework/Settings/SettingAccessor.cs b/shesha-core/src/Shesha.Framework/Settings/SettingAccessor.cs index bda984cbe0..a1a8a0450a 100644 --- a/shesha-core/src/Shesha.Framework/Settings/SettingAccessor.cs +++ b/shesha-core/src/Shesha.Framework/Settings/SettingAccessor.cs @@ -1,5 +1,6 @@ using Abp.Dependency; using Shesha.Reflection; +using Shesha.Settings.Exceptions; using Shesha.Utilities; using System.Reflection; using System.Threading.Tasks; @@ -34,23 +35,36 @@ private void Init(PropertyInfo property) ? settingAttribute.Name : property.Name; - Module = property.DeclaringType.GetConfigurableModuleName(); + Module = property.DeclaringType.NotNull().GetConfigurableModuleName().NotNull(); } /// inheritedDoc - public async Task GetValueAsync() + public async Task GetValueOrNullAsync(SettingManagementContext? context = null) { - return await _settingManager.GetOrNullAsync(Module, Name); + return await _settingManager.GetOrNullAsync(Module, Name, context); } /// inheritedDoc - public TValue GetValue() + public async Task GetValueAsync(SettingManagementContext? context = null) { - return AsyncHelper.RunSync(() => GetValueAsync()); + var value = await GetValueOrNullAsync(context); + return value ?? throw new UnexpectedNullSettingValueException(Module, Name); } /// inheritedDoc - public async Task SetValueAsync(TValue value) + public TValue? GetValueOrNull(SettingManagementContext? context = null) + { + return AsyncHelper.RunSync(() => GetValueOrNullAsync(context)); + } + + /// inheritedDoc + public TValue GetValue(SettingManagementContext? context = null) + { + return GetValueOrNull(context) ?? throw new UnexpectedNullSettingValueException(Module, Name); + } + + /// inheritedDoc + public async Task SetValueAsync(TValue? value) { await _settingManager.SetAsync(Module, Name, value); } @@ -62,6 +76,6 @@ public void WithDefaultValue(TValue value) } /// inheritedDoc - public object GetDefaultValue() => DefaultValue; + public object? GetDefaultValue() => DefaultValue; } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Settings/SettingDefinition.cs b/shesha-core/src/Shesha.Framework/Settings/SettingDefinition.cs index a336980e6e..1191e43be8 100644 --- a/shesha-core/src/Shesha.Framework/Settings/SettingDefinition.cs +++ b/shesha-core/src/Shesha.Framework/Settings/SettingDefinition.cs @@ -22,12 +22,12 @@ public abstract class SettingDefinition public string DisplayName { get; set; } [CanBeNull] - public string Description { get; set; } + public string? Description { get; set; } /// /// Category of the setting, is used for groupping in the UI only /// - public string Category { get; set; } + public string? Category { get; set; } /// /// Accessor of the category (camelCased name or alias if specified) @@ -48,9 +48,9 @@ public abstract class SettingDefinition /// Edit form /// [CanBeNull] - public ConfigurationItemIdentifier EditForm { get; set; } + public ConfigurationItemIdentifier? EditForm { get; set; } - public abstract object GetDefaultValue(); + public abstract object? GetDefaultValue(); public abstract Type GetValueType(); /// @@ -61,7 +61,7 @@ public abstract class SettingDefinition /// /// Accessor of the module (usually it's camelCased name or alias if specified) /// - public string ModuleAccessor { get; set; } + public string? ModuleAccessor { get; set; } public string FullName => !string.IsNullOrWhiteSpace(ModuleName) ? $"{ModuleName}.{Name}" @@ -86,7 +86,7 @@ public SettingDefinition(string name, TValue defaultValue, string displayName): DefaultValue = defaultValue; } - public override object GetDefaultValue() + public override object? GetDefaultValue() { return DefaultValue; } diff --git a/shesha-core/src/Shesha.Framework/Settings/SettingDefinitionContext.cs b/shesha-core/src/Shesha.Framework/Settings/SettingDefinitionContext.cs index d2c70d6a00..8a263ae1c7 100644 --- a/shesha-core/src/Shesha.Framework/Settings/SettingDefinitionContext.cs +++ b/shesha-core/src/Shesha.Framework/Settings/SettingDefinitionContext.cs @@ -1,7 +1,6 @@ using Abp.Collections.Extensions; using Shesha.Reflection; using System.Collections.Generic; -using System.Collections.Immutable; namespace Shesha.Settings { @@ -12,7 +11,7 @@ public class SettingDefinitionContext : ISettingDefinitionContext { protected Dictionary Settings { get; } - public string ModuleName { get; private set; } + public string? ModuleName { get; private set; } public SettingDefinitionContext(Dictionary settings, ISettingDefinitionProvider provider) { diff --git a/shesha-core/src/Shesha.Framework/Settings/SettingDefinitionManager.cs b/shesha-core/src/Shesha.Framework/Settings/SettingDefinitionManager.cs index b0924d707b..7aac536aae 100644 --- a/shesha-core/src/Shesha.Framework/Settings/SettingDefinitionManager.cs +++ b/shesha-core/src/Shesha.Framework/Settings/SettingDefinitionManager.cs @@ -1,19 +1,13 @@ using Abp; using Abp.Collections.Extensions; -using Abp.Configuration; using Abp.Dependency; -using Abp.Domain.Repositories; using Abp.Reflection; -using Castle.MicroKernel.SubSystems.Conversion; -using Newtonsoft.Json.Linq; -using Shesha.ConfigurationItems.Specifications; -using Shesha.Domain; +using Shesha.Extensions; +using Shesha.Reflection; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Reflection; -using System.Threading.Tasks; namespace Shesha.Settings { @@ -24,15 +18,13 @@ public class SettingDefinitionManager : ISettingDefinitionManager, ISingletonDep { private readonly ITypeFinder _typeFinder; private readonly IIocManager _iocManager; - private readonly IRepository _settingConfigurationRepository; protected Lazy> SettingDefinitions { get; } - public SettingDefinitionManager(ITypeFinder typeFinder, IIocManager iocManager, IRepository settingConfigurationRepository) + public SettingDefinitionManager(ITypeFinder typeFinder, IIocManager iocManager) { _typeFinder = typeFinder; _iocManager = iocManager; - _settingConfigurationRepository = settingConfigurationRepository; SettingDefinitions = new Lazy>(CreateSettingDefinitions, true); } @@ -46,7 +38,10 @@ protected virtual IDictionary CreateSettin _iocManager.IsRegistered(t) ).ToList(); - var definitionProviders = definitionProvidersTypes.Select(t => _iocManager.Resolve(t) as ISettingDefinitionProvider).OrderBy(p => (p as IOrderedSettingDefinitionProvider)?.OrderIndex ?? int.MaxValue).ToList(); + var definitionProviders = definitionProvidersTypes + .Select(t => _iocManager.Resolve(t).ForceCastAs()) + .OrderBy(p => (p as IOrderedSettingDefinitionProvider)?.OrderIndex ?? int.MaxValue) + .ToList(); var settings = new Dictionary(); @@ -89,23 +84,23 @@ public virtual SettingDefinition GetOrNull(string moduleName, string name) } // Generic method to create a SettingDefinition - public virtual SettingDefinition CreateUserSettingDefinition(string module, string name, string dataType, object value = default) + public virtual SettingDefinition CreateUserSettingDefinition(string module, string name, string dataType, object? value = default) { var type = GetTypeFromName(dataType); - object convertedDefaultValue = value == null && type.IsValueType ? Activator.CreateInstance(type) : ConvertToType(value, type); + object? convertedDefaultValue = value == null && type.IsValueType + ? Activator.CreateInstance(type) + : ConvertToType(value, type); // Create the generic type SettingDefinition Type genericType = typeof(SettingDefinition<>).MakeGenericType(type); // Create an instance of the generic type - ConstructorInfo constructor = genericType.GetConstructor(new[] { typeof(string), type, typeof(string) }); + var constructor = genericType.GetConstructor([typeof(string), type, typeof(string)]); if (constructor == null) - { throw new InvalidOperationException($"Constructor not found for type '{genericType.FullName}'."); - } - SettingDefinition setting = (SettingDefinition)constructor.Invoke(new[] { name, convertedDefaultValue, name }); + SettingDefinition setting = (SettingDefinition)constructor.Invoke([name, convertedDefaultValue, name]); setting.ModuleName = module; setting.DisplayName = name; @@ -128,7 +123,7 @@ public virtual Type GetTypeFromName(string dataType) return type; } - public static object ConvertToType(object value, Type type) + public static object? ConvertToType(object? value, Type type) { if (value == null || type.IsAssignableFrom(value.GetType())) { diff --git a/shesha-core/src/Shesha.Framework/Settings/SettingIdentifier.cs b/shesha-core/src/Shesha.Framework/Settings/SettingIdentifier.cs index 20db07c51e..033af23555 100644 --- a/shesha-core/src/Shesha.Framework/Settings/SettingIdentifier.cs +++ b/shesha-core/src/Shesha.Framework/Settings/SettingIdentifier.cs @@ -26,12 +26,12 @@ public override int GetHashCode() { return NormalizedFullName.GetHashCode(); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { return Equals(obj as SettingIdentifier); } - public bool Equals(SettingIdentifier obj) + public bool Equals(SettingIdentifier? obj) { return obj != null && obj.NormalizedFullName == this.NormalizedFullName; } diff --git a/shesha-core/src/Shesha.Framework/Settings/SettingManagementContext.cs b/shesha-core/src/Shesha.Framework/Settings/SettingManagementContext.cs index 3e4e3651a4..b94c75454e 100644 --- a/shesha-core/src/Shesha.Framework/Settings/SettingManagementContext.cs +++ b/shesha-core/src/Shesha.Framework/Settings/SettingManagementContext.cs @@ -10,7 +10,7 @@ public class SettingManagementContext /// /// AppKey of the front-end application () /// - public string AppKey { get; set; } + public string? AppKey { get; set; } /// /// Tenant Id diff --git a/shesha-core/src/Shesha.Framework/Settings/SettingsBootstrapper.cs b/shesha-core/src/Shesha.Framework/Settings/SettingsBootstrapper.cs index 713adb2556..cf1fcd16c8 100644 --- a/shesha-core/src/Shesha.Framework/Settings/SettingsBootstrapper.cs +++ b/shesha-core/src/Shesha.Framework/Settings/SettingsBootstrapper.cs @@ -7,6 +7,7 @@ using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Extensions; +using Shesha.Reflection; using Shesha.Services.Settings; using Shesha.Services.Settings.Dto; using Shesha.Settings.Exceptions; @@ -50,7 +51,7 @@ public async Task ProcessAsync() var configurationsInDb = await _settingConfigurationRepository.GetAll().ToListAsync(); - var modules = _moduleRepository.GetAll().ToList(); + var modules = await _moduleRepository.GetAll().ToListAsync(); // delete only the ones which are hard linked to the app //var toDelete = configurationsInDb @@ -101,7 +102,7 @@ await _settingStore.CreateSettingConfigurationAsync(new CreateSettingDefinitionD var toUpdate = consolidated.Where(i => i.Db != null).ToList(); foreach (var itemToUpdate in toUpdate) { - var config = itemToUpdate.Db; + var config = itemToUpdate.Db.NotNull(); var definition = itemToUpdate.Code; var module = !string.IsNullOrWhiteSpace(definition.ModuleName) diff --git a/shesha-core/src/Shesha.Framework/Settings/ShaSettingManager.cs b/shesha-core/src/Shesha.Framework/Settings/ShaSettingManager.cs index 291db3e52d..88161d3ad1 100644 --- a/shesha-core/src/Shesha.Framework/Settings/ShaSettingManager.cs +++ b/shesha-core/src/Shesha.Framework/Settings/ShaSettingManager.cs @@ -12,8 +12,8 @@ using Shesha.Settings.Json; using System; using System.ComponentModel; +using System.Diagnostics; using System.Globalization; -using System.Reflection; using System.Threading.Tasks; using Module = Shesha.Domain.ConfigurationItems.Module; @@ -32,7 +32,10 @@ public class ShaSettingManager : IShaSettingManager, ITransientDependency public IAbpSession AbpSession { get; set; } = NullAbpSession.Instance; - public ShaSettingManager(ISettingDefinitionManager settingDefinitionManager, IConfigurationFrameworkRuntime cfRuntime, IRepository settingValueRepository, Services.Settings.ISettingStore settingStore, + public ShaSettingManager(ISettingDefinitionManager settingDefinitionManager, + IConfigurationFrameworkRuntime cfRuntime, + IRepository settingValueRepository, + Services.Settings.ISettingStore settingStore, IRepository appRepository, IRepository moduleRepository, IRepository userRepository) @@ -46,62 +49,61 @@ public ShaSettingManager(ISettingDefinitionManager settingDefinitionManager, ICo _userRepository = userRepository; } - public async Task GetJObjectOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext context = null) + public async Task GetJObjectOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext? context = null) { var setting = _settingDefinitionManager.Get(module, name); - var settingValue = await _settingStore.GetSettingValueAsync(setting, context ?? GetCurrentContext()); + var settingValue = await _settingStore.GetValueAsync(setting, context ?? GetCurrentContext()); return settingValue != null - ? JObject.Parse(settingValue.Value) + ? JObject.Parse(settingValue) : JObject.FromObject(JsonConvert.SerializeObject(setting.GetDefaultValue())); } - public async Task GetOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext context = null) + public async Task GetOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext? context = null) { var setting = _settingDefinitionManager.Get(module, name); - var settingValue = await _settingStore.GetSettingValueAsync(setting, context ?? GetCurrentContext()); + var settingValue = await _settingStore.GetValueAsync(setting, context ?? GetCurrentContext()); return settingValue != null - ? Deserialize(settingValue.Value, setting.GetValueType()) + ? Deserialize(settingValue, setting.GetValueType()) : setting.GetDefaultValue(); } - public async Task UserSpecificGetOrNullAsync([NotNull] string module, [NotNull] string name, string dataType, TValue defaultValue, SettingManagementContext context = null) + public async Task UserSpecificGetOrNullAsync([NotNull] string module, [NotNull] string name, string? dataType, TValue? defaultValue, SettingManagementContext? context = null) { var setting = _settingDefinitionManager.GetOrNull(module, name); - if (setting == null) + if (setting == null && dataType != null) { setting = _settingDefinitionManager.CreateUserSettingDefinition(module, name, dataType, defaultValue); _settingDefinitionManager.AddDefinition(setting); - var configuration = await EnsureConfigurationAsync(setting); - } + await EnsureConfigurationAsync(setting); - var value = await _settingStore.GetSettingValueAsync(setting, context ?? GetCurrentContext()); + var value = await _settingStore.GetSettingValueAsync(setting, context ?? GetCurrentContext()); + return value != null + ? Deserialize(value.Value, setting.GetValueType()) + : setting.GetDefaultValue(); + } - return value != null - ? Deserialize(value.Value, setting.GetValueType()) - : setting.GetDefaultValue(); + return null; } - public async Task UpdateUserSettingAsync([NotNull] string module, [NotNull] string name, string dataType,[CanBeNull] TValue value, SettingManagementContext context = null) + public async Task UpdateUserSettingAsync([NotNull] string module, [NotNull] string name, string dataType,[CanBeNull] TValue? value, SettingManagementContext? context = null) { context = context ?? GetCurrentContext(); var setting = _settingDefinitionManager.GetOrNull(module, name); - SettingConfiguration configuration = null; - if (setting == null) { setting = _settingDefinitionManager.CreateUserSettingDefinition(module, name, dataType, value); _settingDefinitionManager.AddDefinition(setting); } - configuration = await EnsureConfigurationAsync(setting); + var configuration = await EnsureConfigurationAsync(setting); var settingValue = await _settingStore.GetSettingValueAsync(setting, context); if (settingValue == null) @@ -132,20 +134,20 @@ public async Task UpdateUserSettingAsync([NotNull] string module, [NotNu await _settingValueRepository.InsertOrUpdateAsync(settingValue); } - public async Task GetOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext context = null) + public async Task GetOrNullAsync([NotNull] string module, [NotNull] string name, SettingManagementContext? context = null) { var setting = _settingDefinitionManager.Get(module, name); - var settingValue = await _settingStore.GetSettingValueAsync(setting, context ?? GetCurrentContext()); + var settingValue = await _settingStore.GetValueAsync(setting, context ?? GetCurrentContext()); return settingValue != null - ? Deserialize(settingValue.Value) + ? Deserialize(settingValue) : setting.GetDefaultValue() is TValue typedValue ? typedValue : default; } - public async Task SetAsync([NotNull] string module, [NotNull] string name, [CanBeNull] TValue value, SettingManagementContext context = null) + public async Task SetAsync([NotNull] string module, [NotNull] string name, [CanBeNull] TValue? value, SettingManagementContext? context = null) { context = context ?? GetCurrentContext(); var setting = _settingDefinitionManager.Get(module, name); @@ -203,6 +205,7 @@ private async Task EnsureConfigurationAsync(SettingDefinit }); } + [DebuggerStepThrough] private SettingManagementContext GetCurrentContext() { return new SettingManagementContext { @@ -212,7 +215,7 @@ private SettingManagementContext GetCurrentContext() }; } - private TValue Deserialize(string value) + private TValue? Deserialize(string value) { if (typeof(TValue).IsClass) { @@ -222,7 +225,7 @@ private TValue Deserialize(string value) return To(value); } - private object Deserialize(string value, Type targetType) + private object? Deserialize(string value, Type targetType) { if (targetType.IsClass) { @@ -240,18 +243,20 @@ private object Deserialize(string value, Type targetType) return To(value, targetType); } - private static T To(object obj) + private static T? To(object obj) { if (typeof(T) == typeof(Guid) || typeof(T) == typeof(TimeSpan)) { - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(obj.ToString()); + var converter = TypeDescriptor.GetConverter(typeof(T)); + var value = converter.ConvertFromInvariantString(obj.ToString() ?? string.Empty); + return (T?)value; } if (typeof(T).IsEnum) { if (Enum.IsDefined(typeof(T), obj)) { - return (T)Enum.Parse(typeof(T), obj.ToString()); + return (T)Enum.Parse(typeof(T), obj.ToString() ?? string.Empty); } throw new ArgumentException($"Enum type undefined '{obj}'."); @@ -260,18 +265,18 @@ private static T To(object obj) return (T)Convert.ChangeType(obj, typeof(T), CultureInfo.InvariantCulture); } - private static object To(object obj, Type targetType) + private static object? To(object obj, Type targetType) { if (targetType == typeof(Guid) || targetType == typeof(TimeSpan)) { - return TypeDescriptor.GetConverter(targetType).ConvertFromInvariantString(obj.ToString()); + return TypeDescriptor.GetConverter(targetType).ConvertFromInvariantString(obj.ToString() ?? string.Empty); } if (targetType.IsEnum) { if (Enum.IsDefined(targetType, obj)) { - return Enum.Parse(targetType, obj.ToString()); + return Enum.Parse(targetType, obj.ToString() ?? string.Empty); } throw new ArgumentException($"Enum type undefined '{obj}'."); diff --git a/shesha-core/src/Shesha.Framework/Shesha.Framework.csproj b/shesha-core/src/Shesha.Framework/Shesha.Framework.csproj index 349e22ff52..350fb1d4e3 100644 --- a/shesha-core/src/Shesha.Framework/Shesha.Framework.csproj +++ b/shesha-core/src/Shesha.Framework/Shesha.Framework.csproj @@ -14,10 +14,12 @@ .\Shesha.Framework.xml + .\Shesha.Framework.xml + @@ -30,7 +32,7 @@ - + @@ -110,19 +112,27 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.Framework/SheshaFrameworkModule.cs b/shesha-core/src/Shesha.Framework/SheshaFrameworkModule.cs index 0a21772983..cc97347402 100644 --- a/shesha-core/src/Shesha.Framework/SheshaFrameworkModule.cs +++ b/shesha-core/src/Shesha.Framework/SheshaFrameworkModule.cs @@ -150,6 +150,7 @@ public override void Initialize() IocManager.RegisterSettingAccessor(s => { s.Theme.WithDefaultValue(ThemeSettings.Default); s.MainMenu.WithDefaultValue(MainMenuSettings.Default); + s.PublicUrl.WithDefaultValue("http://localhost:3000"); }); IocManager.RegisterSettingAccessor(s => { @@ -165,7 +166,6 @@ public override void Initialize() public override void PostInitialize() { IocManager.Resolve().Initialize(); - //IocManager.Resolve().Initialize(); var def = IocManager.Resolve(); diff --git a/shesha-core/src/Shesha.Framework/Specifications/ISpecificationInfo.cs b/shesha-core/src/Shesha.Framework/Specifications/ISpecificationInfo.cs index 2fbb6954ed..0a827eb97d 100644 --- a/shesha-core/src/Shesha.Framework/Specifications/ISpecificationInfo.cs +++ b/shesha-core/src/Shesha.Framework/Specifications/ISpecificationInfo.cs @@ -1,6 +1,4 @@ -using System; - -namespace Shesha.Specifications +namespace Shesha.Specifications { /// /// Stores basic information about specifications @@ -30,6 +28,6 @@ public interface ISpecificationInfo: ISpecificationBaseInfo /// /// Description /// - string Description { get; } + string? Description { get; } } } diff --git a/shesha-core/src/Shesha.Framework/Specifications/ISpecificationManager.cs b/shesha-core/src/Shesha.Framework/Specifications/ISpecificationManager.cs index 286f5291f4..69edede22c 100644 --- a/shesha-core/src/Shesha.Framework/Specifications/ISpecificationManager.cs +++ b/shesha-core/src/Shesha.Framework/Specifications/ISpecificationManager.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Shesha.Specifications { diff --git a/shesha-core/src/Shesha.Framework/Specifications/InheritedSpecification.cs b/shesha-core/src/Shesha.Framework/Specifications/InheritedSpecification.cs index 1fea331a91..f4250c360f 100644 --- a/shesha-core/src/Shesha.Framework/Specifications/InheritedSpecification.cs +++ b/shesha-core/src/Shesha.Framework/Specifications/InheritedSpecification.cs @@ -30,7 +30,7 @@ public Expression> ToExpression() var baseExpression = _baseSpecs.ToExpression(); // get parameter from the base expression (of type TBase) - var parameter = baseExpression.Parameters.FirstOrDefault(); + var parameter = baseExpression.Parameters.First(); if (!(parameter is ParameterExpression parameterExpression)) throw new NotSupportedException($"Parameter of type `{parameter.GetType().FullName}` is not supported, expected: `{nameof(ParameterExpression)}`"); diff --git a/shesha-core/src/Shesha.Framework/Specifications/NullSpecificationManager.cs b/shesha-core/src/Shesha.Framework/Specifications/NullSpecificationManager.cs index e759b1a680..000ff20f5a 100644 --- a/shesha-core/src/Shesha.Framework/Specifications/NullSpecificationManager.cs +++ b/shesha-core/src/Shesha.Framework/Specifications/NullSpecificationManager.cs @@ -1,7 +1,9 @@ -using Abp.Specifications; +using Abp; +using Abp.Specifications; using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; namespace Shesha.Specifications { @@ -10,6 +12,8 @@ namespace Shesha.Specifications /// public class NullSpecificationManager : ISpecificationManager { + public static readonly ISpecificationManager Instance = new NullSpecificationManager(); + public List SpecificationTypes => throw new NotImplementedException(); public IQueryable ApplySpecifications(IQueryable queryable) @@ -24,12 +28,12 @@ public IQueryable ApplySpecifications(IQueryable queryable, List { }); } public ISpecification GetSpecificationInstance(ISpecificationInfo specInfo) { - return null; + return new NullSpecification(); } public List> GetSpecifications() @@ -44,12 +48,25 @@ public List> GetSpecifications(List specifications) public ISpecificationsContext Use() where TSpec : ISpecification { - return null; + return new SpecificationsContext(typeof(TSpec), typeof(TEntity)); } public IDisposable Use(params Type[] specificationType) { - return null; + return new DisposeAction(() => { }); + } + + public class NullSpecification : ISpecification + { + public bool IsSatisfiedBy(T obj) + { + return true; + } + + public Expression> ToExpression() + { + return (e) => true; + } } } } diff --git a/shesha-core/src/Shesha.Framework/Specifications/ShaSpecification.cs b/shesha-core/src/Shesha.Framework/Specifications/ShaSpecification.cs index 2492748699..bb42b2f7ef 100644 --- a/shesha-core/src/Shesha.Framework/Specifications/ShaSpecification.cs +++ b/shesha-core/src/Shesha.Framework/Specifications/ShaSpecification.cs @@ -12,9 +12,9 @@ namespace Shesha.Specifications /// The type of the object to which the specification is applied. public abstract class ShaSpecification : ISpecification, ITransientDependency { - public IAbpSession AbpSession { get; set; } - public IIocManager IocManager { get; set; } - public ISpecificationManager SpecificationManager { get; set; } + public IAbpSession AbpSession { get; set; } = NullAbpSession.Instance; + public IIocManager IocManager { get; set; } = default!; + public ISpecificationManager SpecificationManager { get; set; } = NullSpecificationManager.Instance; /// /// Returns a value which indicates whether the specification diff --git a/shesha-core/src/Shesha.Framework/Specifications/SpecificationInfo.cs b/shesha-core/src/Shesha.Framework/Specifications/SpecificationInfo.cs index b92e3dcb6f..cf6a6274e5 100644 --- a/shesha-core/src/Shesha.Framework/Specifications/SpecificationInfo.cs +++ b/shesha-core/src/Shesha.Framework/Specifications/SpecificationInfo.cs @@ -40,6 +40,6 @@ public class SpecificationInfo: ISpecificationInfo /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Specifications/SpecificationManager.cs b/shesha-core/src/Shesha.Framework/Specifications/SpecificationManager.cs index 7ddcf65e5c..92a38837bd 100644 --- a/shesha-core/src/Shesha.Framework/Specifications/SpecificationManager.cs +++ b/shesha-core/src/Shesha.Framework/Specifications/SpecificationManager.cs @@ -1,8 +1,8 @@ using Abp; using Abp.Dependency; using Abp.Specifications; -using Castle.Core.Internal; -using Shesha.Specifications.Exceptions; +using Shesha.Extensions; +using Shesha.Reflection; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -18,11 +18,9 @@ public class SpecificationManager : ISpecificationManager, ITransientDependency { private static readonly AsyncLocal InternalState = new AsyncLocal(); - private static readonly AsyncLocal IsDisabled = new AsyncLocal(); - private readonly ISpecificationsFinder _specificationsFinder; - public IIocManager IocManager { get; set; } + public IIocManager IocManager { get; set; } = default!; public SpecificationManager(ISpecificationsFinder specificationsFinder) { @@ -91,7 +89,7 @@ public ISpecification GetSpecificationInstance(ISpecificationInfo specInfo { var specInstance = IocManager.IsRegistered(specInfo.SpecificationsType) ? IocManager.Resolve(specInfo.SpecificationsType) - : Activator.CreateInstance(specInfo.SpecificationsType); + : ActivatorHelper.CreateNotNullObject(specInfo.SpecificationsType); if (specInstance is ISpecification thisTypeSpec) return thisTypeSpec; @@ -101,8 +99,8 @@ public ISpecification GetSpecificationInstance(ISpecificationInfo specInfo throw new InvalidCastException($"Type `{typeof(T).FullName}` is not inherited from `{specInfo.EntityType}`"); var specType = typeof(InheritedSpecification<,>).MakeGenericType(specInfo.EntityType, typeof(T)); - var instance = Activator.CreateInstance(specType, specInstance); - return instance as ISpecification; + var instance = ActivatorHelper.CreateNotNullObject(specType, specInstance); + return instance.ForceCastAs>(); } public ISpecificationsContext Use() where TSpec : ISpecification diff --git a/shesha-core/src/Shesha.Framework/Specifications/SpecificationsContext.cs b/shesha-core/src/Shesha.Framework/Specifications/SpecificationsContext.cs index 254f41f52e..0317bac9a8 100644 --- a/shesha-core/src/Shesha.Framework/Specifications/SpecificationsContext.cs +++ b/shesha-core/src/Shesha.Framework/Specifications/SpecificationsContext.cs @@ -36,14 +36,14 @@ public SpecificationsContext(Type specificationsType, Type entityType) } /// inheritedDoc - public event EventHandler Disposed; + public event EventHandler? Disposed; /// /// Called to trigger event. /// protected virtual void OnDisposed() { - Disposed.InvokeSafely(this); + Disposed?.InvokeSafely(this); } /// @@ -52,7 +52,7 @@ protected virtual void OnDisposed() public bool IsDisposed { get; private set; } /// inheritedDoc - public void Dispose() + public virtual void Dispose() { if (IsDisposed) return; diff --git a/shesha-core/src/Shesha.Framework/Specifications/SpecificationsHelper.cs b/shesha-core/src/Shesha.Framework/Specifications/SpecificationsHelper.cs index 098923b582..cde9c726f3 100644 --- a/shesha-core/src/Shesha.Framework/Specifications/SpecificationsHelper.cs +++ b/shesha-core/src/Shesha.Framework/Specifications/SpecificationsHelper.cs @@ -1,5 +1,5 @@ using Abp.Specifications; -using Shesha.Extensions; +using Shesha.Extensions; using Shesha.Reflection; using System; using System.Collections.Generic; @@ -28,7 +28,7 @@ public static List GetSpecificationsInfo(Type specificationTy SpecificationsType = specificationType, EntityType = et, - Name = specificationType.FullName, + Name = specificationType.GetRequiredFullName(), FriendlyName = ReflectionHelper.GetDisplayName(specificationType), Description = specificationType.GetDescription(), IsGlobal = specificationType.IsGlobalSpecificationType(), diff --git a/shesha-core/src/Shesha.Framework/Startup/ApplicationStartupDto.cs b/shesha-core/src/Shesha.Framework/Startup/ApplicationStartupDto.cs index 19c8d5ab2c..38872bdc92 100644 --- a/shesha-core/src/Shesha.Framework/Startup/ApplicationStartupDto.cs +++ b/shesha-core/src/Shesha.Framework/Startup/ApplicationStartupDto.cs @@ -13,6 +13,6 @@ public class ApplicationStartupDto : EntityDto public ApplicationStartupStatus Status { get; set; } public bool BootstrappersDisabled { get; set; } public bool MigrationsDisabled { get; set; } - public List Assemblies { get; set; } = new List(); + public List Assemblies { get; set; } = new(); } } diff --git a/shesha-core/src/Shesha.Framework/Startup/ApplicationStartupSession.cs b/shesha-core/src/Shesha.Framework/Startup/ApplicationStartupSession.cs index 92adda62f5..2e7415eec5 100644 --- a/shesha-core/src/Shesha.Framework/Startup/ApplicationStartupSession.cs +++ b/shesha-core/src/Shesha.Framework/Startup/ApplicationStartupSession.cs @@ -1,4 +1,5 @@ -using Abp.Dependency; + +using Abp.Dependency; using Abp.Domain.Repositories; using Abp.Domain.Uow; using Abp.Reflection; @@ -27,10 +28,10 @@ public class ApplicationStartupSession : IApplicationStartupSession, ISingletonD /// /// Gets or sets the logger. /// - public ILogger Logger { get; set; } + public ILogger Logger { get; set; } = NullLogger.Instance; - public ApplicationStartupDto PreviousStartup { get; private set; } - public ApplicationStartupDto CurrentStartup { get; private set; } + public ApplicationStartupDto? PreviousStartup { get; private set; } + public ApplicationStartupDto? CurrentStartup { get; private set; } public bool AllAssembliesStayUnchanged { get; private set; } private List _unchangedAssemblies; @@ -143,13 +144,16 @@ public async Task LogApplicationStartAsync(LogApplication var assemblies = await LogAssembliesAsync(startup); await uow.CompleteAsync(); - CurrentStartup = MapStartupToDto(startup, assemblies); - FillUnchangedAssemblies(); - return CurrentStartup; + var startupDto = MapStartupToDto(startup, assemblies); + FillUnchangedAssemblies(startupDto); + + CurrentStartup = startupDto; + + return startupDto; } } - private void FillUnchangedAssemblies() + private void FillUnchangedAssemblies(ApplicationStartupDto startup) { // don't fill the list if previous startup is missing or not full and successful if (PreviousStartup == null || PreviousStartup.MigrationsDisabled || PreviousStartup.BootstrappersDisabled || PreviousStartup.Status != Domain.Enums.ApplicationStartupStatus.Success) @@ -159,7 +163,7 @@ private void FillUnchangedAssemblies() return; } - _unchangedAssemblies = CurrentStartup.Assemblies.Where(curr => + _unchangedAssemblies = startup.Assemblies.Where(curr => PreviousStartup.Assemblies.Any(prev => prev.FileName == curr.FileName && prev.FileVersion == curr.FileVersion && prev.ProductVersion == curr.ProductVersion && @@ -168,22 +172,20 @@ private void FillUnchangedAssemblies() ) .ToList(); - AllAssembliesStayUnchanged = _unchangedAssemblies.Count() == CurrentStartup.Assemblies.Count() && - CurrentStartup.Assemblies.Count() == PreviousStartup.Assemblies.Count(); + AllAssembliesStayUnchanged = _unchangedAssemblies.Count() == startup.Assemblies.Count() && + startup.Assemblies.Count() == PreviousStartup.Assemblies.Count(); } private ApplicationStartupDto MapStartupToDto(ApplicationStartup startup, List assemblies) { - return startup != null - ? new ApplicationStartupDto() + return new ApplicationStartupDto() { Id = startup.Id, Status = startup.Status, MigrationsDisabled = startup.MigrationsDisabled, BootstrappersDisabled = startup.BootstrappersDisabled, Assemblies = assemblies.Select(a => MapAssemblyToDto(a)).ToList(), - } - : null; + }; } private ApplicationStartupAssemblyDto MapAssemblyToDto(ApplicationStartupAssembly assembly) @@ -233,9 +235,9 @@ await _startupRepository.UpdateAsync(id, startup => { } } - private async Task GetPreviousStartupDetailsAsync() + private async Task GetPreviousStartupDetailsAsync() { - ApplicationStartupDto result = null; + ApplicationStartupDto? result = null; using (var uow = _uowManager.Begin()) { var prevStartup = await _startupRepository.GetAll().OrderByDescending(s => s.StartedOn).FirstOrDefaultAsync(); diff --git a/shesha-core/src/Shesha.Framework/Startup/AssemblyBaseDto.cs b/shesha-core/src/Shesha.Framework/Startup/AssemblyBaseDto.cs index 71cb135499..3d75fdd582 100644 --- a/shesha-core/src/Shesha.Framework/Startup/AssemblyBaseDto.cs +++ b/shesha-core/src/Shesha.Framework/Startup/AssemblyBaseDto.cs @@ -5,9 +5,9 @@ /// public class AssemblyBaseDto { - public string FileName { get; set; } - public string FileMD5 { get; set; } - public string FileVersion { get; set; } - public string ProductVersion { get; set; } + public required string FileName { get; set; } + public required string FileMD5 { get; set; } + public string? FileVersion { get; set; } + public string? ProductVersion { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Startup/AssemblyDto.cs b/shesha-core/src/Shesha.Framework/Startup/AssemblyDto.cs index 559d43357a..cf1b8915f1 100644 --- a/shesha-core/src/Shesha.Framework/Startup/AssemblyDto.cs +++ b/shesha-core/src/Shesha.Framework/Startup/AssemblyDto.cs @@ -5,6 +5,6 @@ /// public class AssemblyDto: AssemblyBaseDto { - public string FilePath { get; set; } + public string? FilePath { get; set; } } } diff --git a/shesha-core/src/Shesha.Framework/Startup/IApplicationStartupSession.cs b/shesha-core/src/Shesha.Framework/Startup/IApplicationStartupSession.cs index 7eb48a093c..05bd2a84c2 100644 --- a/shesha-core/src/Shesha.Framework/Startup/IApplicationStartupSession.cs +++ b/shesha-core/src/Shesha.Framework/Startup/IApplicationStartupSession.cs @@ -1,5 +1,4 @@ -using Shesha.Domain; -using System; +using System; using System.Reflection; using System.Threading.Tasks; @@ -38,12 +37,12 @@ public interface IApplicationStartupSession /// /// Information about previous startup /// - ApplicationStartupDto PreviousStartup { get; } + ApplicationStartupDto? PreviousStartup { get; } /// /// Information about current startup /// - ApplicationStartupDto CurrentStartup { get; } + ApplicationStartupDto? CurrentStartup { get; } /// /// Checks is assembly was not changed since previous startup session diff --git a/shesha-core/src/Shesha.Framework/Swagger/CachingSwaggerProvider.cs b/shesha-core/src/Shesha.Framework/Swagger/CachingSwaggerProvider.cs index 2a939a62c3..691bb73de6 100644 --- a/shesha-core/src/Shesha.Framework/Swagger/CachingSwaggerProvider.cs +++ b/shesha-core/src/Shesha.Framework/Swagger/CachingSwaggerProvider.cs @@ -1,7 +1,6 @@ using Abp.Dependency; using Abp.Events.Bus.Entities; using Abp.Events.Bus.Handlers; -using Abp.Runtime.Caching; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; @@ -9,64 +8,59 @@ using Shesha.Permissions; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; +using System.Threading.Tasks; namespace Shesha.Swagger { - public class CachingSwaggerProvider : ISwaggerProvider, - IEventHandler>, - IEventHandler>, - IEventHandler>, - ITransientDependency + public class CachingSwaggerProvider: + ISwaggerProvider, + IAsyncEventHandler>, + IAsyncEventHandler>, + IAsyncEventHandler>, + ISingletonDependency { - private readonly ICacheManager _cacheManager; - private readonly SwaggerGenerator _swaggerGenerator; - - /// - /// Cache of the Swagger docs - /// - protected ITypedCache SwaggerCache => _cacheManager.GetCache("SwaggerCache"); + private readonly ISwaggerDocumentCacheHolder _cacheHolder; public CachingSwaggerProvider( IOptions optionsAccessor, - ICacheManager cacheManager, + ISwaggerDocumentCacheHolder cacheHolder, IIocResolver iocResolver) { - _cacheManager = cacheManager; - var apiDescriptionsProvider = iocResolver.Resolve(); var schemaGenerator = iocResolver.Resolve(); _swaggerGenerator = new SwaggerGenerator(optionsAccessor.Value, apiDescriptionsProvider, schemaGenerator); + _cacheHolder = cacheHolder; } - public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null) + public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null) { - return SwaggerCache.Get(documentName, (_) => _swaggerGenerator.GetSwagger(documentName, host, basePath)); + return _cacheHolder.Cache.Get(documentName, (_) => _swaggerGenerator.GetSwagger(documentName, host, basePath)); } - public void ClearCache() - { - SwaggerCache.Clear(); + public async Task ClearCacheAsync() + { + await _cacheHolder.Cache.ClearAsync(); } - public void HandleEvent(EntityChangedEventData eventData) + public async Task HandleEventAsync(EntityChangedEventData eventData) { - ClearCache(); + await ClearCacheAsync(); } - public void HandleEvent(EntityChangedEventData eventData) + public async Task HandleEventAsync(EntityChangedEventData eventData) { - ClearCache(); + await ClearCacheAsync(); } - public void HandleEvent(EntityChangedEventData eventData) + public async Task HandleEventAsync(EntityChangedEventData eventData) { if (eventData.Entity.Type == ShaPermissionedObjectsTypes.EntityAction || eventData.Entity.Type == ShaPermissionedObjectsTypes.Entity || eventData.Entity.Type == ShaPermissionedObjectsTypes.WebApiAction || eventData.Entity.Type == ShaPermissionedObjectsTypes.WebApi) - ClearCache(); - } + await _cacheHolder.Cache.ClearAsync(); + } } } diff --git a/shesha-core/src/Shesha.Framework/Swagger/ISwaggerDocumentCacheHolder.cs b/shesha-core/src/Shesha.Framework/Swagger/ISwaggerDocumentCacheHolder.cs new file mode 100644 index 0000000000..037925b76d --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Swagger/ISwaggerDocumentCacheHolder.cs @@ -0,0 +1,9 @@ +using Microsoft.OpenApi.Models; +using Shesha.Cache; + +namespace Shesha.Swagger +{ + public interface ISwaggerDocumentCacheHolder : ICacheHolder + { + } +} diff --git a/shesha-core/src/Shesha.Framework/Swagger/SwaggerDocumentCacheHolder.cs b/shesha-core/src/Shesha.Framework/Swagger/SwaggerDocumentCacheHolder.cs new file mode 100644 index 0000000000..7738eee767 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Swagger/SwaggerDocumentCacheHolder.cs @@ -0,0 +1,15 @@ +using Abp.Dependency; +using Abp.Runtime.Caching; +using Microsoft.OpenApi.Models; +using Shesha.Cache; + +namespace Shesha.Swagger +{ + + public class SwaggerDocumentCacheHolder : CacheHolder, ISwaggerDocumentCacheHolder, ISingletonDependency + { + public SwaggerDocumentCacheHolder(ICacheManager cacheManager) : base("SwaggerDocumentCache", cacheManager) + { + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Swagger/SwaggerEndpointEnumerator.cs b/shesha-core/src/Shesha.Framework/Swagger/SwaggerEndpointEnumerator.cs index ac8be7e78b..4a08270c5a 100644 --- a/shesha-core/src/Shesha.Framework/Swagger/SwaggerEndpointEnumerator.cs +++ b/shesha-core/src/Shesha.Framework/Swagger/SwaggerEndpointEnumerator.cs @@ -1,15 +1,13 @@ -using Abp.Dependency; -using Abp.Domain.Uow; -using Microsoft.Extensions.DependencyInjection; +using Abp.Domain.Uow; using Shesha.Application.Services; using Shesha.Domain.Attributes; using Shesha.Domain.Enums; using Shesha.DynamicEntities; using Shesha.Permissions; using Shesha.Reflection; +using Shesha.Services; using Shesha.Utilities; using Swashbuckle.AspNetCore.SwaggerUI; -using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -25,9 +23,9 @@ public SwaggerEndpointEnumerator() public IEnumerator GetEnumerator() { - var entityConfigs = IocManager.Instance.Resolve(); - var pmo = IocManager.Instance.Resolve(); - var _uowManager = IocManager.Instance.Resolve(); + var entityConfigs = StaticContext.IocManager.Resolve(); + var pmo = StaticContext.IocManager.Resolve(); + var _uowManager = StaticContext.IocManager.Resolve(); var types = SwaggerHelper.ServiceTypesFunc(); @@ -39,19 +37,23 @@ public IEnumerator GetEnumerator() if (service.ImplementsGenericInterface(typeof(IEntityAppService<,>))) { // entity service - var genericInterface = service.GetGenericInterfaces(typeof(IEntityAppService<,>)).FirstOrDefault(); - var entityType = genericInterface.GenericTypeArguments.FirstOrDefault(); + var genericInterface = service.GetGenericInterfaces(typeof(IEntityAppService<,>)).First(); + var entityType = genericInterface.GenericTypeArguments.First(); var model = AsyncHelper.RunSync(() => entityConfigs.GetModelConfigurationOrNullAsync(entityType.Namespace, entityType.Name)); - var entityAttribute = entityType.GetAttribute(); + model.NotNull(); + var entityAttribute = entityType.GetAttributeOrNull(); + var crudAttribute = entityType.GetAttributeOrNull(); + var permission = pmo.Get($"{entityType.FullName}", ShaPermissionedObjectsTypes.Entity); if (entityAttribute?.GenerateApplicationService == GenerateApplicationServiceState.DisableGenerateApplicationService + || (permission != null && permission.ActualAccess == RefListPermissionedAccess.Disable) + || crudAttribute?.All == RefListPermissionedAccess.Disable || !model.GenerateAppService) continue; } else { // api service - var obj = $"{service.FullName}"; - var permission = pmo.Get(obj, ShaPermissionedObjectsTypes.WebApi); + var permission = pmo.Get($"{service.FullName}", ShaPermissionedObjectsTypes.WebApi); if (permission != null && permission.ActualAccess == RefListPermissionedAccess.Disable) continue; } @@ -69,4 +71,4 @@ public IEnumerator GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Swagger/SwaggerHelper.cs b/shesha-core/src/Shesha.Framework/Swagger/SwaggerHelper.cs index 96b7b34c2f..a8524059fc 100644 --- a/shesha-core/src/Shesha.Framework/Swagger/SwaggerHelper.cs +++ b/shesha-core/src/Shesha.Framework/Swagger/SwaggerHelper.cs @@ -9,7 +9,6 @@ using Shesha.Domain; using Shesha.DynamicEntities; using Shesha.DynamicEntities.Dtos; -using Shesha.JsonEntities; using Shesha.Reflection; using Shesha.Services; using Shesha.Utilities; @@ -53,8 +52,8 @@ public static IList EntityTypesFunc() return assemblyFinder.GetAllAssemblies() .Distinct(new AssemblyFullNameComparer()) .Where(a => !a.IsDynamic && - a.GetTypes().Any(t => MappingHelper.IsEntity(t) || MappingHelper.IsJsonEntity(t) && t != typeof(JsonEntity)) - ).SelectMany(a => a.GetTypes().Where(t => MappingHelper.IsEntity(t) || MappingHelper.IsJsonEntity(t) && t != typeof(JsonEntity))) + a.GetTypes().Any(t => MappingHelper.IsEntity(t) || MappingHelper.IsJsonEntity(t)) // && t != typeof(JsonEntity)) need to add JsonEntity for binding purposes + ).SelectMany(a => a.GetTypes().Where(t => MappingHelper.IsEntity(t) || MappingHelper.IsJsonEntity(t))) // && t != typeof(JsonEntity)) need to add JsonEntity for binding purposes .ToList(); } @@ -157,7 +156,7 @@ public static string GetSchemaId(Type modelType) var test = typeName + modelType.GetGenericArguments().Select(genericArg => GetSchemaId(genericArg)).Aggregate((previous, current) => previous + current); return test; } - else if (modelType.HasInterface(typeof(IDynamicDtoProxy))) + else if (modelType.HasInterface(typeof(IDynamicDtoProxy)) && modelType.BaseType != null) return "Proxy" + GetSchemaId(modelType.BaseType); else return modelType.Name; diff --git a/shesha-core/src/Shesha.Framework/Swagger/SwaggerOperationFilter.cs b/shesha-core/src/Shesha.Framework/Swagger/SwaggerOperationFilter.cs index f4632db9ce..3e836a103b 100644 --- a/shesha-core/src/Shesha.Framework/Swagger/SwaggerOperationFilter.cs +++ b/shesha-core/src/Shesha.Framework/Swagger/SwaggerOperationFilter.cs @@ -44,6 +44,9 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) private void AddPropertiesToCrudCreateUpdate(OpenApiOperation operation, OperationFilterContext context) { + if (string.IsNullOrWhiteSpace(context.ApiDescription.RelativePath)) + return; + if (context.ApiDescription.RelativePath.ToLower().EndsWith("crud/create") || context.ApiDescription.RelativePath.ToLower().EndsWith("crud/update")) { @@ -72,7 +75,7 @@ private void DecorateWrappedResponses(OpenApiOperation operation, OperationFilte var unwrappedReturnType = context.MethodInfo.ReturnType == typeof(Task) ? typeof(AjaxResponseBase) : context.MethodInfo.ReturnType.IsSubtypeOfGeneric(typeof(Task<>)) - ? context.MethodInfo.ReturnType.GenericTypeArguments.FirstOrDefault() + ? context.MethodInfo.ReturnType.GenericTypeArguments.First() : context.MethodInfo.ReturnType; var okResponse = ((int)HttpStatusCode.OK).ToString(); @@ -141,7 +144,7 @@ private void DecorateWrappedResponses(OpenApiOperation operation, OperationFilte /// MemberInfo /// Default value (null as default) /// Inherit attribute from base classes - public static TAttribute GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(MemberInfo memberInfo, TAttribute defaultValue = default, bool inherit = true) + public static TAttribute GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(MemberInfo memberInfo, TAttribute defaultValue, bool inherit = true) where TAttribute : class { return memberInfo.GetCustomAttributes(true).OfType().FirstOrDefault() @@ -167,10 +170,7 @@ private void DecorateOperaionParameters(OpenApiOperation operation, OperationFil { parameter.Description ??= description?.ModelMetadata?.Description; - parameter.Required |= description.IsRequired; - } - else - { + parameter.Required |= description != null && description.IsRequired; } } } diff --git a/shesha-core/src/Shesha.Framework/Utilities/AsyncHelper.cs b/shesha-core/src/Shesha.Framework/Utilities/AsyncHelper.cs index ab5b562cfd..114fff4ac6 100644 --- a/shesha-core/src/Shesha.Framework/Utilities/AsyncHelper.cs +++ b/shesha-core/src/Shesha.Framework/Utilities/AsyncHelper.cs @@ -14,9 +14,10 @@ public static class AsyncHelper public static void RunSync(Func task) { var oldContext = SynchronizationContext.Current; - var synch = new ExclusiveSynchronizationContext(); + using var synch = new ExclusiveSynchronizationContext(); SynchronizationContext.SetSynchronizationContext(synch); #pragma warning disable VSTHRD101 // Rethrow to preserve stack details +#pragma warning disable AsyncFixer03 synch.Post(async _ => { try @@ -33,6 +34,7 @@ public static void RunSync(Func task) synch.EndMessageLoop(); } }, null); +#pragma warning restore AsyncFixer03 #pragma warning restore VSTHRD101 // Rethrow to preserve stack details synch.BeginMessageLoop(); @@ -48,10 +50,11 @@ public static void RunSync(Func task) public static T RunSync(Func> task) { var oldContext = SynchronizationContext.Current; - var synch = new ExclusiveSynchronizationContext(); + using var synch = new ExclusiveSynchronizationContext(); SynchronizationContext.SetSynchronizationContext(synch); - T ret = default(T); + var ret = default(T); #pragma warning disable VSTHRD101 // Rethrow to preserve stack details +#pragma warning disable AsyncFixer03 // Avoid unsupported fire-and-forget async-void methods or delegates. Unhandled exceptions will crash the process. synch.Post(async _ => { try @@ -67,33 +70,44 @@ public static T RunSync(Func> task) { synch.EndMessageLoop(); } - }, null); + }, null); +#pragma warning restore AsyncFixer03 // Avoid unsupported fire-and-forget async-void methods or delegates. Unhandled exceptions will crash the process. #pragma warning restore VSTHRD101 // Rethrow to preserve stack details synch.BeginMessageLoop(); - SynchronizationContext.SetSynchronizationContext(oldContext); - return ret; + SynchronizationContext.SetSynchronizationContext(oldContext); +#pragma warning disable CS8603 // Possible null reference return. + return ret; +#pragma warning restore CS8603 // Possible null reference return. } - private class ExclusiveSynchronizationContext : SynchronizationContext + private sealed class ExclusiveSynchronizationContext : SynchronizationContext, IDisposable { - private bool done; + private bool done; + private bool disposed; + public Exception InnerException { get; set; } - readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false); - readonly Queue> items = - new Queue>(); + private readonly AutoResetEvent _workItemsWaiting; + readonly Queue> items = new Queue>(); + +#pragma warning disable CS8618 + public ExclusiveSynchronizationContext() + { + _workItemsWaiting = new AutoResetEvent(false); + } +#pragma warning restore CS8618 - public override void Send(SendOrPostCallback d, object state) + public override void Send(SendOrPostCallback d, object? state) { throw new NotSupportedException("We cannot send to our same thread"); } - public override void Post(SendOrPostCallback d, object state) + public override void Post(SendOrPostCallback d, object? state) { lock (items) { items.Enqueue(Tuple.Create(d, state)); } - workItemsWaiting.Set(); + _workItemsWaiting.Set(); } public void EndMessageLoop() @@ -105,7 +119,7 @@ public void BeginMessageLoop() { while (!done) { - Tuple task = null; + Tuple? task = null; lock (items) { if (items.Count > 0) @@ -123,7 +137,7 @@ public void BeginMessageLoop() } else { - workItemsWaiting.WaitOne(); + _workItemsWaiting.WaitOne(); } } } @@ -131,7 +145,18 @@ public void BeginMessageLoop() public override SynchronizationContext CreateCopy() { return this; + } + + public void Dispose() + { + if (disposed) + { + return; + } + + disposed = true; + _workItemsWaiting.Dispose(); } } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Utilities/CodeNamingHelper.cs b/shesha-core/src/Shesha.Framework/Utilities/CodeNamingHelper.cs index 7649709cc7..79e2c2909b 100644 --- a/shesha-core/src/Shesha.Framework/Utilities/CodeNamingHelper.cs +++ b/shesha-core/src/Shesha.Framework/Utilities/CodeNamingHelper.cs @@ -1,14 +1,11 @@ -using Abp.Domain.Entities; -using Shesha.Attributes; +using Shesha.Attributes; using Shesha.EntityReferences; using Shesha.Exceptions; using Shesha.Extensions; using Shesha.Modules; using Shesha.Reflection; using System; -using System.Collections.Generic; using System.Reflection; -using System.Text.RegularExpressions; namespace Shesha.Utilities { @@ -37,7 +34,7 @@ public static void ValidateCodeIdentifier(string identifier) /// Alias (optional) /// If true, accessor will be in PascalCase otherwise - camelCase /// - public static string GetAccessor(string name, string alias = null, bool pascalCase = false) + public static string GetAccessor(string name, string? alias = null, bool pascalCase = false) { if (string.IsNullOrWhiteSpace(alias)) { @@ -70,7 +67,7 @@ public static string GetModuleAccessor(this SheshaModuleInfo moduleInfo) /// public static string GetPropertyAccessor(this PropertyInfo property) { - var aliasAttribute = property.GetAttribute(); + var aliasAttribute = property.GetAttributeOrNull(); return GetAccessor(property.Name, aliasAttribute?.Alias); } diff --git a/shesha-core/src/Shesha.Framework/Utilities/DateHelper.cs b/shesha-core/src/Shesha.Framework/Utilities/DateHelper.cs index 0acb6c1c07..e2f940dacb 100644 --- a/shesha-core/src/Shesha.Framework/Utilities/DateHelper.cs +++ b/shesha-core/src/Shesha.Framework/Utilities/DateHelper.cs @@ -297,7 +297,7 @@ public static double ToUnixTimestampJs(this DateTime date) return GetDateTimeFromUnixTimestamp(value / 1000); } - public static DateRange DatesIntersection(DateRange range1, DateRange range2) + public static DateRange? DatesIntersection(DateRange range1, DateRange range2) { if (range1.From == range1.To || range2.From == range2.To) return null; diff --git a/shesha-core/src/Shesha.Framework/Utilities/DateRange.cs b/shesha-core/src/Shesha.Framework/Utilities/DateRange.cs index 8bb386ee21..b213fbae7f 100644 --- a/shesha-core/src/Shesha.Framework/Utilities/DateRange.cs +++ b/shesha-core/src/Shesha.Framework/Utilities/DateRange.cs @@ -13,7 +13,7 @@ public DateRange(DateTime from, DateTime to) To = to; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { var range = obj as DateRange; if (range == null) diff --git a/shesha-core/src/Shesha.Framework/Utilities/ExpressionHelper.cs b/shesha-core/src/Shesha.Framework/Utilities/ExpressionHelper.cs deleted file mode 100644 index 867a9e4072..0000000000 --- a/shesha-core/src/Shesha.Framework/Utilities/ExpressionHelper.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace Shesha.Utilities -{ - /// - /// Utility class to get the text from an Expression. - /// Taken from System.Web.Mvc.ExpressionHelper but modified to support Convert operations. - /// - public static class ExpressionHelper - { - public static string GetExpressionText(Expression> expression) - { - return GetExpressionText((LambdaExpression)expression); - } - - public static string GetExpressionText(LambdaExpression expression) - { - // Split apart the expression string for property/field accessors to create its name - Stack nameParts = new Stack(); - Expression part = expression.Body; - - while (part != null) - { - if (part.NodeType == ExpressionType.Call) - { - MethodCallExpression methodExpression = (MethodCallExpression)part; - - if (!IsSingleArgumentIndexer(methodExpression)) - { - break; - } - - nameParts.Push( - GetIndexerInvocation( - methodExpression.Arguments.Single(), - expression.Parameters.ToArray() - ) - ); - - part = methodExpression.Object; - } - else if (part.NodeType == ExpressionType.ArrayIndex) - { - BinaryExpression binaryExpression = (BinaryExpression)part; - - nameParts.Push( - GetIndexerInvocation( - binaryExpression.Right, - expression.Parameters.ToArray() - ) - ); - - part = binaryExpression.Left; - } - else if (part.NodeType == ExpressionType.MemberAccess) - { - MemberExpression memberExpressionPart = (MemberExpression)part; - nameParts.Push("." + memberExpressionPart.Member.Name); - part = memberExpressionPart.Expression; - } - else if (part.NodeType == ExpressionType.Convert) - { - // NEW STUFF TO SUPPORT CONVERT (CASTING) OPERATIONS - part = ((UnaryExpression)part).Operand; - } - else if (part.NodeType == ExpressionType.Parameter) - { - // Dev10 Bug #907611 - // When the expression is parameter based (m => m.Something...), we'll push an empty - // string onto the stack and stop evaluating. The extra empty string makes sure that - // we don't accidentally cut off too much of m => m.Model. - nameParts.Push(String.Empty); - part = null; - } - else - { - break; - } - } - - // If it starts with "model", then strip that away - if (nameParts.Count > 0 && String.Equals(nameParts.Peek(), ".model", StringComparison.OrdinalIgnoreCase)) - { - nameParts.Pop(); - } - - if (nameParts.Count > 0) - { - return nameParts.Aggregate((left, right) => left + right).TrimStart('.'); - } - - return String.Empty; - } - - private static string GetIndexerInvocation(Expression expression, ParameterExpression[] parameters) - { - Expression converted = Expression.Convert(expression, typeof(object)); - ParameterExpression fakeParameter = Expression.Parameter(typeof(object), null); - Expression> lambda = Expression.Lambda>(converted, fakeParameter); - Func func; - - try - { - func = lambda.Compile(); - } - catch (InvalidOperationException ex) - { - throw new InvalidOperationException("Invalid Indexer expresion", ex); - } - - return "[" + Convert.ToString(func(null), CultureInfo.InvariantCulture) + "]"; - } - - internal static bool IsSingleArgumentIndexer(Expression expression) - { - MethodCallExpression methodExpression = expression as MethodCallExpression; - if (methodExpression == null || methodExpression.Arguments.Count != 1) - { - return false; - } - - return methodExpression.Method - .DeclaringType - .GetDefaultMembers() - .OfType() - .Any(p => p.GetGetMethod() == methodExpression.Method); - } - - public static Expression> AndAlso(this Expression> expr1, Expression> expr2) - { - var parameter = Expression.Parameter(typeof(T)); - - var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter); - var left = leftVisitor.Visit(expr1.Body); - - var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter); - var right = rightVisitor.Visit(expr2.Body); - - return Expression.Lambda>( - Expression.AndAlso(left, right), parameter); - } - - public static Expression> OrElse(this Expression> expr1, Expression> expr2) - { - var parameter = Expression.Parameter(typeof(T)); - - var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter); - var left = leftVisitor.Visit(expr1.Body); - - var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter); - var right = rightVisitor.Visit(expr2.Body); - - return Expression.Lambda>( - Expression.OrElse(left, right), parameter); - } - - private class ReplaceExpressionVisitor - : ExpressionVisitor - { - private readonly Expression _oldValue; - private readonly Expression _newValue; - - public ReplaceExpressionVisitor(Expression oldValue, Expression newValue) - { - _oldValue = oldValue; - _newValue = newValue; - } - - public override Expression Visit(Expression node) - { - if (node == _oldValue) - return _newValue; - return base.Visit(node); - } - } - } -} diff --git a/shesha-core/src/Shesha.Framework/Utilities/FileHelper.cs b/shesha-core/src/Shesha.Framework/Utilities/FileHelper.cs index aa8f11c0b8..51cae58141 100644 --- a/shesha-core/src/Shesha.Framework/Utilities/FileHelper.cs +++ b/shesha-core/src/Shesha.Framework/Utilities/FileHelper.cs @@ -16,11 +16,10 @@ public static class FileHelper public static string CleanupFileName(this string fileName) { return fileName.Contains('\\') - ? fileName.Split(new char[] { '\\' }).LastOrDefault() + ? fileName.Split(new char[] { '\\' }).Last() : fileName; } - /// /// Returns MD5 hash of the specified /// @@ -68,4 +67,4 @@ private static string MD5ToString(byte[] bytes) return BitConverter.ToString(bytes).Replace("-", "").ToLower(); } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Framework/Utilities/Parser.cs b/shesha-core/src/Shesha.Framework/Utilities/Parser.cs index 3f5b36f3f1..628f87682c 100644 --- a/shesha-core/src/Shesha.Framework/Utilities/Parser.cs +++ b/shesha-core/src/Shesha.Framework/Utilities/Parser.cs @@ -1,13 +1,7 @@ -using Abp.Domain.Entities; -using Shesha.Configuration.Runtime; -using Shesha.Domain; -using Shesha.Extensions; -using Shesha.Reflection; +using Shesha.Reflection; using System; -using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Reflection; -using EntityExtensions = Shesha.Extensions.EntityExtensions; namespace Shesha.Utilities { @@ -17,68 +11,6 @@ namespace Shesha.Utilities /// public class Parser { - public static bool TryParseToMultiValueReferenceListValue(string value, out int? parsedValue, string referenceListModule, string referenceListName) - { - try - { - if (string.IsNullOrEmpty(value) || value == "-999" || value == "(null)") - { - parsedValue = null; - return true; - } - else - { - if (value.Contains(',')) - { - // Values provided as comma separated list. - parsedValue = 0; - var valuesAr = value.Split(','); - foreach (var val in valuesAr) - { - if (!string.IsNullOrWhiteSpace(val)) - { - parsedValue += int.Parse(val.Trim()); - } - } - } - else - { - // Single value provided - int intValue; - if (!int.TryParse(value, out intValue)) - { - parsedValue = null; - return false; - } - - var intVal = int.Parse(value); - parsedValue = intVal; - } - - // Checking that all the items are valid reference list values. - //var itemsAr = EntityExtensions.DecomposeIntoBitFlagComponents(parsedValue.Value); - - /* Don't fail when item not from list was provided. - var refList = ReferenceListService.GetCachedList(referenceListNamespace, referenceListName); - for (int i = 0; i < itemsAr.Length; i++) - { - if (refList.Items.FirstOrDefault(item => item.ItemValue == itemsAr[i]) == null) - { - throw new ArgumentException(string.Format("'{0}' is not a valid value from Reference List '{1}'.", itemsAr[i], referenceListNamespace + "." + referenceListName)); - } - }*/ - } - } - catch (Exception) - { - //Logger<>.WriteLog(LogLevel.DEBUG, "Value parsing error", ex); - parsedValue = null; - return false; - } - - return true; - } - #region TryParseToValueType /// /// Tries to convert from a string value to the specified Property's type. @@ -90,7 +22,7 @@ public static bool TryParseToMultiValueReferenceListValue(string value, out int? /// /// /// Returns true if the value could be converted successfully to the target property's type. - public static bool TryParseToValueType(string value, Type targetType, out object parsedValue, string format = null, bool isDateOnly = false, bool returnNullEvenIfNonNullable = false) + public static bool TryParseToValueType(string? value, Type targetType, out object? parsedValue, string? format = null, bool isDateOnly = false, bool returnNullEvenIfNonNullable = false) { try { @@ -144,15 +76,11 @@ public static bool TryParseToValueType(string value, Type targetType, out object } else { - Type nonNullableTargetType; - if (ReflectionHelper.IsNullableType(targetType)) - nonNullableTargetType = Nullable.GetUnderlyingType(targetType); - else - nonNullableTargetType = targetType; + var nonNullableTargetType = targetType.GetUnderlyingTypeIfNullable(); if (nonNullableTargetType.IsEnum) { - parsedValue = System.Enum.Parse(nonNullableTargetType, value); + parsedValue = Enum.Parse(nonNullableTargetType, value); } else { @@ -193,7 +121,7 @@ private static object ParseBoolean(string value) return parsedValue; } - public static TimeSpan? ParseTime(string value, string format = null) + public static TimeSpan? ParseTime(string? value, string? format = null) { if (string.IsNullOrWhiteSpace(value)) return null; @@ -201,7 +129,7 @@ private static object ParseBoolean(string value) if (int.TryParse(value, out var seconds) && seconds < TimeSpan.FromHours(24).TotalSeconds) return TimeSpan.FromSeconds(seconds); - var timeFormats = new string[] { + var timeFormats = new string?[] { format, /*note: `hh` is always 24-hours for TimeSpan*/ @"hh\:mm\:ss", @@ -215,7 +143,7 @@ private static object ParseBoolean(string value) return null; } - public static DateTime ParseDate(string value, string format = null, bool isDateOnly = true) + public static DateTime ParseDate(string value, string? format = null, bool isDateOnly = true) { var dateVal = new DateTime(); bool parseSuccessful = false; @@ -265,301 +193,7 @@ public static DateTime ParseDate(string value, string format = null, bool isDate #endregion - public static bool TryParseToEntity(string value, Type entityType, out IEntity parsedValue, out bool hasNewEntityIndicator) - { - throw new NotImplementedException(); - /* - hasNewEntityIndicator = false; - - try - { - if (string.IsNullOrEmpty(value) - || string.Equals("(null)", value, StringComparison.InvariantCultureIgnoreCase) - || string.Equals("-999", value, StringComparison.InvariantCultureIgnoreCase)) // -999 is used by some controls to denote 'no/null selection'. - { - parsedValue = null; - return true; - } - else if (value.Contains('|')) - { - // Value is FullyQualifiedEntityId specifies the type of entity. - var referencedEntity = DynamicRepository.Get(value); - - if (referencedEntity == null) - throw new ArgumentException($"Entity referenced by value '{value}' does not exist."); - - parsedValue = referencedEntity; - return true; - } - else - { - if (entityType == null) - throw new ArgumentException("If the value does not provide the entity type and Id, the entityType parameter must be supplied."); - - // Getting the referenced entity. - var referencedEntityId = ParseId(value, entityType); - - var isNull = referencedEntityId != null && referencedEntityId is Guid - ? (Guid)referencedEntityId == Guid.Empty - : referencedEntityId == null; - - if (!isNull) - { - var referencedEntity = NHibernateSession.Current.Get(entityType, referencedEntityId) as IEntity; - - if (referencedEntity == null) - throw new ArgumentException( - $"Entity of type '{entityType.FullName}' with Id '{referencedEntityId}' does not exist."); - - parsedValue = referencedEntity; - return true; - } - else - { - // Reference is to a new entity which should not be set. - parsedValue = null; - hasNewEntityIndicator = true; - return true; - } - - } - } - catch (Exception ex) - { - Logger<>.WriteLog(LogLevel.DEBUG, "Value parsing error", ex); - parsedValue = null; - return false; - } - */ - } - - #region ParseToTargetPropertyType - - /// - /// Parses a string value into the type the specified entity property would expect. - /// - /// String value to be parsed. - /// PropertyInfo for the entity property into which the parsed value should be able to be set. - /// The returned value only has meaning in cases where the entity property is an entity references. - /// If so this will be set to true if the parsed value indicates that the entity reference should be for a new entity. - /// A format string to help with the parsing. - /// - /// Returns the parsed value in the appropriate type. - public static object ParseToTargetPropertyType(string value, PropertyInfo propInfo, out bool hasNewEntityIndicator, string format = null, bool returnNullEvenIfNonNullable = false) - { - object parsedValue; - var res = TryParseToTargetPropertyType(value, propInfo, out parsedValue, out hasNewEntityIndicator, format, returnNullEvenIfNonNullable); - - return parsedValue; - } - - public static bool TryParseToTargetPropertyType(string value, PropertyInfo propInfo, out object parsedValue, out bool hasNewEntityIndicator, string format = null, bool returnNullEvenIfNonNullable = false) - { - hasNewEntityIndicator = false; - - if (string.IsNullOrEmpty(format)) - { - var displayFormatAtt = propInfo.GetAttribute(); - if (displayFormatAtt != null) - { - if (displayFormatAtt.ApplyFormatInEditMode && !string.IsNullOrEmpty(displayFormatAtt.DataFormatString)) - format = displayFormatAtt.DataFormatString; - } - } - - var generalDataType = EntityExtensions.GetGeneralDataType(propInfo); - switch (generalDataType) - { - case GeneralDataType.Enum: - return TryParseToEnumPropertyType(value, propInfo, out parsedValue, returnNullEvenIfNonNullable); - case GeneralDataType.ReferenceList: - return TryParseToReferenceListPropertyType(value, propInfo, out parsedValue, returnNullEvenIfNonNullable); - case GeneralDataType.MultiValueReferenceList: - return TryParseToMultiValueReferenceListPropertyType(value, propInfo, out parsedValue); - case GeneralDataType.EntityReference: - return TryParseToEntityReferencePropertyType(value, propInfo, out parsedValue, out hasNewEntityIndicator); - default: - return TryParseToValueTypePropertyType(value, propInfo, out parsedValue, format, returnNullEvenIfNonNullable); - } - } - - internal static bool TryParseToEnumPropertyType(string value, PropertyInfo propInfo, out object parsedValue, bool returnNullEvenIfNonNullable = false) - { - parsedValue = null; - string propertyName = propInfo.DeclaringType.Name + "." + propInfo.Name; - - var propConfig = propInfo.DeclaringType.GetEntityConfiguration()[propInfo.Name]; - - if (string.IsNullOrEmpty(value)) - parsedValue = null; - else - parsedValue = ConvertRefListValueToPropertyType(propInfo, propertyName, (int)Enum.Parse(propConfig.EnumType, value)); - return true; - } - - internal static bool TryParseToReferenceListPropertyType(string value, PropertyInfo propInfo, out object parsedValue, bool returnNullEvenIfNonNullable = false) - { - throw new NotImplementedException(); - /* - parsedValue = null; - string propertyName = propInfo.DeclaringType.Name + "." + propInfo.Name; - - var propConfig = EntityConfiguration.Get(propInfo.DeclaringType)[propInfo.Name]; - - int? parsedValueInt; - var res = Parser.TryParseToReferenceListValue(value, out parsedValueInt, propConfig.ReferenceListNamespace, propConfig.ReferenceListName, propInfo); - if (!res) - return false; - - if (parsedValueInt == null) - { - return (ReflectionHelper.IsNullableType(propInfo.PropertyType) || returnNullEvenIfNonNullable); - } - else - { - parsedValue = ConvertRefListValueToPropertyType(propInfo, propertyName, parsedValueInt); - return true; - } - */ - } - - internal static bool TryParseToMultiValueReferenceListPropertyType(string value, PropertyInfo propInfo, out object parsedValue) - { - parsedValue = null; - string propertyName = propInfo.DeclaringType.Name + "." + propInfo.Name; - - var propConfig = propInfo.DeclaringType.GetEntityConfiguration()[propInfo.Name]; - - int? parsedValueInt; - var res = Parser.TryParseToMultiValueReferenceListValue(value, out parsedValueInt, propConfig.ReferenceListModule, propConfig.ReferenceListName); - - if (!res) - return false; - - if (parsedValueInt == null) - { - if (ReflectionHelper.IsNullableType(propInfo.PropertyType)) - parsedValue = null; - else - parsedValue = 0; - return true; - } - else - { - parsedValue = ConvertRefListValueToPropertyType(propInfo, propertyName, parsedValueInt); - return true; - } - } - - private static object ConvertRefListValueToPropertyType(PropertyInfo propInfo, string propertyName, int? parsedValue) - { - var nonNullablePropType = ReflectionHelper.GetUnderlyingTypeIfNullable(propInfo.PropertyType); - - if (nonNullablePropType.IsEnum) - { - return System.Enum.ToObject(nonNullablePropType, parsedValue.Value); - } - else if (nonNullablePropType == typeof(int) - || nonNullablePropType == typeof(long) - || nonNullablePropType == typeof(short)) - { - return Convert.ChangeType(parsedValue.Value, nonNullablePropType); - } - else - { - throw new Exception( - $"ReferenceList property '{propertyName}' was of an unexpected type '{propInfo.PropertyType.FullName}'."); - } - } - - private static bool TryParseToValueTypePropertyType(string value, PropertyInfo propInfo, GeneralDataType generalDataType, out object parsedValue, string format = null, bool returnNullEvenIfNonNullable = false) - { - bool isDateOnly = generalDataType == GeneralDataType.Date; - - return Parser.TryParseToValueType(value, propInfo.PropertyType, out parsedValue, format, isDateOnly, returnNullEvenIfNonNullable); - } - - internal static bool TryParseToValueTypePropertyType(string value, PropertyInfo propInfo, out object parsedValue, string format = null, bool returnNullEvenIfNonNullable = false) - { - var generalDataType = EntityExtensions.GetGeneralDataType(propInfo); - return TryParseToValueTypePropertyType(value, propInfo, generalDataType, out parsedValue, format, returnNullEvenIfNonNullable); - } - - private static bool TryParseToEntityReferencePropertyType(string value, PropertyInfo propInfo, out object parsedValue, out bool hasNewEntityIndicator) - { - parsedValue = null; - string propertyName = propInfo.DeclaringType.Name + "." + propInfo.Name; - - var nonNullablePropType = ReflectionHelper.GetUnderlyingTypeIfNullable(propInfo.PropertyType); - Type entityType; - if (typeof(IEntity).IsAssignableFrom(nonNullablePropType)) - { - entityType = nonNullablePropType; - } - else - { - throw new InvalidOperationException($"'{propertyName}' is not a valid Entity reference property type and not marked with the EntityReferenceAttribute."); - /* - var entityFerefenceAttribute = propInfo.GetAttribute(); - if (entityFerefenceAttribute == null) - throw new InvalidOperationException($"'{propertyName}' is not a valid Entity reference property type and not marked with the EntityReferenceAttribute."); - - entityType = entityFerefenceAttribute.EntityType; - */ - } - - IEntity parsedEntity; - var res = Parser.TryParseToEntity(value, entityType, out parsedEntity, out hasNewEntityIndicator); - if (!res) - return false; - - if (parsedEntity == null) - { - if (hasNewEntityIndicator) - { - parsedValue = null; - return true; - } - - if (typeof(IEntity).IsAssignableFrom(nonNullablePropType) || ReflectionHelper.IsNullableType(propInfo.PropertyType)) - { - parsedValue = null; - return true; - } - else - { - parsedValue = nonNullablePropType.GetTypeDefaultValue(); - return true; - } - } - else - { - if (typeof(IEntity).IsAssignableFrom(nonNullablePropType)) - { - parsedValue = parsedEntity; - return true; - } - else - { - parsedValue = parsedEntity.GetId(); - return true; - } - } - } - - #endregion - - public static T ParseTo(string value) - { - return (T)ParseTo(value, typeof(T)); - } - - public static bool CanParseTo(string value) - { - return CanParseTo(value, typeof(T)); - } - - public static bool CanParseTo(string value, Type type) + public static bool CanParseTo([NotNullWhen(true)] string? value, Type type) { try { @@ -575,7 +209,7 @@ public static bool CanParseTo(string value, Type type) /// /// Parses the Id of the entity of the specified type /// - public static object ParseId(string value, Type entityType) + public static object? ParseId(string? value, Type entityType) { if (string.IsNullOrWhiteSpace(value) || value == "-999") return null; @@ -586,12 +220,12 @@ public static object ParseId(string value, Type entityType) var result = ParseTo(value, idProp.PropertyType); - return typeof(Int64).IsAssignableFrom(idProp.PropertyType) && ((Int64)result <= 0) + return typeof(Int64).IsAssignableFrom(idProp.PropertyType) && result != null && ((Int64)result <= 0) ? null : result; } - public static bool CanParseId(string value, Type entityType) + public static bool CanParseId([NotNullWhen(true)]string? value, Type entityType) { if (string.IsNullOrWhiteSpace(value) || value == "-999") return false; @@ -603,7 +237,7 @@ public static bool CanParseId(string value, Type entityType) return CanParseTo(value, idProp.PropertyType); } - public static object ParseTo(string value, Type type) + public static object? ParseTo(string? value, Type type) { if (string.IsNullOrEmpty(value) || type == null) return null; diff --git a/shesha-core/src/Shesha.Framework/Utilities/PathHelper.cs b/shesha-core/src/Shesha.Framework/Utilities/PathHelper.cs index ab5be6cc85..04f06445a2 100644 --- a/shesha-core/src/Shesha.Framework/Utilities/PathHelper.cs +++ b/shesha-core/src/Shesha.Framework/Utilities/PathHelper.cs @@ -11,7 +11,7 @@ namespace Shesha.Utilities /// public class PathHelper: IPathHelper, ITransientDependency { - public IocManager IocManager { get; set; } + public IIocManager IocManager { get; set; } = default!; /// /// Performs similar function to Path.Combine() which combine multiple file path segments into @@ -70,7 +70,7 @@ public string MapVirtualPath(string path) var env = IocManager.Resolve(); var basePath = string.IsNullOrEmpty(env.ContentRootPath) - ? Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) // Running outside Web Hosting context e.g. unit testing + ? Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty // Running outside Web Hosting context e.g. unit testing : env.ContentRootPath; basePath = basePath.TrimEnd('\\').TrimEnd('/'); diff --git a/shesha-core/src/Shesha.Framework/Utilities/ReportingHelper.cs b/shesha-core/src/Shesha.Framework/Utilities/ReportingHelper.cs index 3b936723ae..d698d070fd 100644 --- a/shesha-core/src/Shesha.Framework/Utilities/ReportingHelper.cs +++ b/shesha-core/src/Shesha.Framework/Utilities/ReportingHelper.cs @@ -1,9 +1,10 @@ -using System.IO; +using Shesha.Exceptions; +using System.IO; using System.Reflection; namespace Shesha.Utilities { - public class ReportingHelper + public static class ReportingHelper { public static MemoryStream GetResourceStream(string resourceName) { @@ -18,19 +19,20 @@ public static MemoryStream GetResourceStream(string resourceName) /// public static MemoryStream GetResourceStream(string resourceName, Assembly assembly) { - var resourceStream = assembly.GetManifestResourceStream(resourceName); - var result = new MemoryStream(); - var buffer = new byte[8 * 1024]; - int len; - while ((len = resourceStream.Read(buffer, 0, buffer.Length)) > 0) - { - result.Write(buffer, 0, len); + using (var resourceStream = assembly.GetEmbeddedResourceStream(resourceName)) + { + var buffer = new byte[8 * 1024]; + int len; + while ((len = resourceStream.Read(buffer, 0, buffer.Length)) > 0) + { + result.Write(buffer, 0, len); + } + + result.Position = 0; } - result.Position = 0; - return result; } @@ -48,5 +50,21 @@ public static string GetResourceString(string resourceName, Assembly assembly) return sr.ReadToEnd(); } } + + /// + /// Get embedded resource stream. Throws exception if stream not found + /// + /// Assembly containing embeddeed resource + /// Resource name + /// + /// + public static Stream GetEmbeddedResourceStream(this Assembly assembly, string resourceName) + { + var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + throw new ManifestResourceStreamNotFoundException(assembly, resourceName); + + return stream; + } } } diff --git a/shesha-core/src/Shesha.Framework/Utilities/SerializationHelper.cs b/shesha-core/src/Shesha.Framework/Utilities/SerializationHelper.cs deleted file mode 100644 index b0fc0b05cf..0000000000 --- a/shesha-core/src/Shesha.Framework/Utilities/SerializationHelper.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.IO; -using System.Runtime.Serialization.Formatters.Binary; -using System.Text; -using System.Xml.Serialization; - -namespace Shesha.Utilities -{ - /// - /// Helper class with serialization related methods. - /// - public class SerializationHelper - { - /// - /// Deserializes specified to an object of type - /// - public static T DeserializeXml(string xml) - { - // Deserializing the Xml to the settings object. - if (string.IsNullOrWhiteSpace(xml)) - return default(T); - - var deserializer = new XmlSerializer(typeof(T)); - - using (var textReader = new StringReader(xml)) - { - var deserialized = (T)deserializer.Deserialize(textReader); - textReader.Close(); - - return deserialized; - } - } - - /// - /// Serializes specified to XML - /// - public static string SerializeToXml(object obj) - { - var sb = new StringBuilder(); - - var serializer = new XmlSerializer(obj.GetType()); - - using (var textWriter = new StringWriter(sb)) - { - serializer.Serialize(textWriter, obj); - textWriter.Flush(); - textWriter.Close(); - - return sb.ToString(); - } - } - } -} diff --git a/shesha-core/src/Shesha.Framework/Utilities/StringHelper.cs b/shesha-core/src/Shesha.Framework/Utilities/StringHelper.cs index 4415a4048a..9274a9e1f3 100644 --- a/shesha-core/src/Shesha.Framework/Utilities/StringHelper.cs +++ b/shesha-core/src/Shesha.Framework/Utilities/StringHelper.cs @@ -1,10 +1,11 @@ -using Abp.Extensions; -using Abp.Localization; +using Abp.Localization; using HtmlAgilityPack; using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -27,7 +28,7 @@ public enum ProcessDirection public static class StringHelper { - public static string ToNullIsWhiteSpace(this string value) + public static string? ToNullIsWhiteSpace(this string value) { return string.IsNullOrWhiteSpace(value) ? null : value; } @@ -36,7 +37,7 @@ public static string ToNullIsWhiteSpace(this string value) /// Returns first not empty string from the specified /// /// List of strings - public static string FirstNotEmpty(params string[] values) + public static string? FirstNotEmpty(params string?[] values) { return values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)); } @@ -143,7 +144,7 @@ public static string CapitalizeWords(this string value, Func isDelim return result.ToString(); } - public static string CapitalizeFirstChar(this string s) + public static string? CapitalizeFirstChar(this string s) { if (s == null) return null; @@ -156,7 +157,7 @@ public static string CapitalizeFirstChar(this string s) return new string(a); } - public static string DecapitalizeFirstChar(this string s) + public static string? DecapitalizeFirstChar(this string s) { if (s == null) return null; @@ -187,9 +188,9 @@ public static string ToFriendlyName(this string itemName) /// string where we need to replace tags /// dictionary like tag=value /// - public static string ReplaceTags(this string value, Dictionary tags) + public static string? ReplaceTags(this string? value, Dictionary tags) { - return String.IsNullOrEmpty(value) ? value : tags.Aggregate(value, (current, tag) => current.Replace(tag.Key, tag.Value)); + return string.IsNullOrEmpty(value) ? value : tags.Aggregate(value, (current, tag) => current.Replace(tag.Key, tag.Value)); } public static string LeftPart(this string value, char delimiter, ProcessDirection direction) @@ -212,16 +213,16 @@ public static string RightPart(this string value, char delimiter) return value.RightPart(delimiter, ProcessDirection.RightToLeft); } - public static string Delimited(this IEnumerable list, string delimiter) + public static string Delimited(this IEnumerable list, string delimiter) { if (!list.Any()) - return String.Empty; + return string.Empty; - var res = String.Empty; + var res = string.Empty; foreach (var item in list) { - res += (String.IsNullOrWhiteSpace(res) ? String.Empty : delimiter) + item ?? String.Empty; + res += (string.IsNullOrWhiteSpace(res) ? string.Empty : delimiter) + item ?? string.Empty; } return res; } @@ -236,9 +237,9 @@ public static string Delimited(this IEnumerable list, string delimiter) private static readonly char[] IllegalUrlCharacters = new[] { ';', '/', '\\', '?', ':', '@', '&', '=', '+', '$', ',', '<', '>', '#', '%', '.', '!', '*', '\'', '"', '(', ')', '[', ']', '{', '}', '|', '^', '`', '~', '–', '‘', '’', '“', '”', '»', '«' }; [DebuggerStepThrough] - public static bool IsEmail(this string target) + public static bool IsEmail(this string? target) { - return !String.IsNullOrEmpty(target) && EmailExpression.IsMatch(target); + return !string.IsNullOrEmpty(target) && EmailExpression.IsMatch(target); } /// @@ -262,11 +263,11 @@ public static Guid ToGuid(this string value) } [DebuggerStepThrough] - public static T ToEnum(this string target, T defaultValue) where T : IComparable, IFormattable + public static T ToEnum(this string? target, T defaultValue) where T : IComparable, IFormattable { T convertedValue = defaultValue; - if (!String.IsNullOrEmpty(target)) + if (!string.IsNullOrEmpty(target)) { try { @@ -327,11 +328,11 @@ public static string Replace(this string target, ICollection oldValues, /// Strings from which to return the first available string. /// Returns the value of the first element which is not null or empty or white spaces. If all /// the strings are null, empty or whitespaces, null is returned. - public static string FirstValue(params string[] args) + public static string? FirstValue(params string?[] args) { for (int i = 0; i < args.Length; i++) { - if (!String.IsNullOrWhiteSpace(args[i])) + if (!string.IsNullOrWhiteSpace(args[i])) return args[i]; } return null; @@ -434,7 +435,7 @@ public static string ToCurrencyInWords(this decimal amount) var c = (int)((amount - r) * 100); var result = r.NumberToWords() + - (c > 0 ? String.Format(" and {0} cents", c.NumberToWords()) : ""); + (c > 0 ? string.Format(" and {0} cents", c.NumberToWords()) : ""); return result; } @@ -445,7 +446,8 @@ public static string ToCurrencyInWords(this decimal amount) public static string ToMd5Fingerprint(this string s) { var bytes = Encoding.Unicode.GetBytes(s.ToCharArray()); - var hash = MD5.Create().ComputeHash(bytes); + using var md5 = MD5.Create(); + var hash = md5.ComputeHash(bytes); // concat the hash bytes into one long string return hash.Aggregate(new StringBuilder(32), @@ -459,18 +461,18 @@ public static bool IsNumeric(this string val) return Double.TryParse(val, NumberStyles.Any, CultureInfo.CurrentCulture, out result); } - public static string ToHtmlParagraphs(this string text) + public static string? ToHtmlParagraphs(this string? text) { return text == null ? null : text .Replace("\n\r", "\n").Replace("\r\n", "\n") .Split('\n') - .Select(p => String.Format("

{0}

", p)) + .Select(p => string.Format("

{0}

", p)) .Delimited(""); } - public static string ToHtmlWithBreaks(this string text) + public static string? ToHtmlWithBreaks(this string? text) { return text == null ? null @@ -498,14 +500,14 @@ public static List ParseToIntList(this string value) { return (value ?? "").Split(',') .Select(v => v.Trim()) - .Where(v => !String.IsNullOrWhiteSpace(v)) + .Where(v => !string.IsNullOrWhiteSpace(v)) .Select(v => Convert.ToInt32(v)) .ToList(); } - public static bool EqualsOrEmpty(this string s1, string s2, StringComparison comparisonType = StringComparison.InvariantCultureIgnoreCase) + public static bool EqualsOrEmpty(this string? s1, string? s2, StringComparison comparisonType = StringComparison.InvariantCultureIgnoreCase) { - return String.IsNullOrEmpty(s1) && String.IsNullOrEmpty(s2) || + return string.IsNullOrEmpty(s1) && string.IsNullOrEmpty(s2) || s1 != null && s2 != null && s1.Equals(s2, comparisonType); } @@ -520,9 +522,9 @@ public static string RemoveDoubleSpaces(this string source) ///
/// comma separated string to be converted to list /// - public static List ConvertCommaSeparatedStringToList(string value) + public static List? ConvertCommaSeparatedStringToList(string? value) { - if (!String.IsNullOrEmpty(value)) + if (!string.IsNullOrEmpty(value)) { return value.Split(',') @@ -533,7 +535,6 @@ public static List ConvertCommaSeparatedStringToList(string value) { return null; } - } public static string FormatMobileNo(this string value) @@ -567,9 +568,7 @@ public static string RemovePathIllegalCharacters(this string fileName, string wh public static string ToCamelCase(this string s) { if (string.IsNullOrEmpty(s) || !char.IsUpper(s[0])) - { return s; - } char[] chars = s.ToCharArray(); @@ -598,6 +597,13 @@ public static string ToCamelCase(this string s) return new string(chars); } + public static string? ToCamelCaseOrNull(this string? s) + { + return string.IsNullOrWhiteSpace(s) + ? s + : ToCamelCase(s); + } + /// /// Converts string to array of bytes /// @@ -625,7 +631,7 @@ public static string GetString(this byte[] bytes) /// /// Convert string to int /// - public static int ToInt(this string source, int defaultValue) + public static int ToInt(this string? source, int defaultValue) { if (string.IsNullOrEmpty(source) || !Int32.TryParse(source, out var result)) @@ -636,7 +642,7 @@ public static int ToInt(this string source, int defaultValue) /// /// Convert string to int /// - public static int? ToIntOrNull(this string source, int? defaultValue = null) + public static int? ToIntOrNull(this string? source, int? defaultValue = null) { if (string.IsNullOrEmpty(source) || !Int32.TryParse(source, out var result)) return defaultValue; @@ -670,7 +676,7 @@ public static string ToRoman(this int number) /// /// Convert string to long /// - public static long ToLong(this string source, long defaultValue) + public static long ToLong(this string? source, long defaultValue) { if (string.IsNullOrEmpty(source) || !long.TryParse(source, out var result)) @@ -681,10 +687,10 @@ public static long ToLong(this string source, long defaultValue) /// /// Convert string to decimal /// - public static decimal ToDecimal(this string source, decimal defaultValue) + public static decimal ToDecimal(this string? source, decimal defaultValue) { - if (String.IsNullOrEmpty(source) || - !Decimal.TryParse(source, out var result)) + if (string.IsNullOrEmpty(source) || + !decimal.TryParse(source, out var result)) return defaultValue; return result; } @@ -692,9 +698,9 @@ public static decimal ToDecimal(this string source, decimal defaultValue) /// /// Returns a string formatted into a proper telephone number. /// - public static string FormatAsTelephoneNumber(this string telNumber) + public static string? FormatAsTelephoneNumber(this string? telNumber) { - if (telNumber == null) + if (string.IsNullOrWhiteSpace(telNumber)) return null; var cleanNo = telNumber.Trim().Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", ""); @@ -721,7 +727,7 @@ public static string RemoveAllNonNumericCharacters(this string initialString) if (initialString == null) return ""; var nonNumericCharacters = new System.Text.RegularExpressions.Regex(@"\D"); - return nonNumericCharacters.Replace(initialString, String.Empty); + return nonNumericCharacters.Replace(initialString, string.Empty); } /// @@ -732,12 +738,12 @@ public static string RemoveAllNonAlphaNumericCharacters(this string initialStrin if (initialString == null) return ""; var nonAlphaNumericCharacters = new System.Text.RegularExpressions.Regex(@"[^a-zA-Z\d]"); - return nonAlphaNumericCharacters.Replace(initialString, String.Empty); + return nonAlphaNumericCharacters.Replace(initialString, string.Empty); } public static string ToCurrencyString(this decimal currency, bool showEmptyIfZero = false) { - return showEmptyIfZero && currency == 0 ? "" : String.Format("{0:R#,##0.00}", currency); + return showEmptyIfZero && currency == 0 ? "" : string.Format("{0:R#,##0.00}", currency); } public static string ToCurrencyString(this decimal? currency, bool showEmptyIfZero = false) @@ -833,13 +839,13 @@ public static string[] SplitUpperCase(this string source) if (Char.IsUpper(letters[i])) { //Grab everything before the current index. - words.Add(new String(letters, wordStartIndex, i - wordStartIndex)); + words.Add(new string(letters, wordStartIndex, i - wordStartIndex)); wordStartIndex = i; } } //We need to have the last word. - words.Add(new String(letters, wordStartIndex, letters.Length - wordStartIndex)); + words.Add(new string(letters, wordStartIndex, letters.Length - wordStartIndex)); //Copy to a string array. string[] wordArray = new string[words.Count]; @@ -860,23 +866,20 @@ public static string[] SplitUpperCase(this string source) /// public static string SplitUpperCaseToString(this string source) { - return String.Join(" ", SplitUpperCase(source)); + return string.Join(" ", SplitUpperCase(source)); } - public static bool IsValidEmail(this string inputEmail) + public static bool IsValidEmail([NotNullWhen(true)]this string? inputEmail) { - string strRegex = @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}" + - @"\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\" + - @".)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$"; - Regex re = new Regex(strRegex); - if (re.IsMatch(inputEmail)) - return (true); - else - return (false); + if (string.IsNullOrWhiteSpace(inputEmail)) + return false; + + var emailAttribute = new EmailAddressAttribute(); + return emailAttribute.IsValid(inputEmail); } [DebuggerStepThrough] - public static string StripHtml(this string target) + public static string StripHtml(this string? target) { if (string.IsNullOrEmpty(target)) return ""; @@ -885,9 +888,9 @@ public static string StripHtml(this string target) var result = StringHelper.HtmlTrim(target, ' ', '\n', '\r', '\t'); result = StripHTMLStyleExpression - .Replace(target, String.Empty); + .Replace(target, string.Empty); result = StripHTMLExpression - .Replace(result, String.Empty) + .Replace(result, string.Empty) .Replace(" ", " ") .Replace(" ", " ") .Replace(">", ">") @@ -901,9 +904,9 @@ public static string StripHtml(this string target) /// /// /// - public static string WrapAsHtmlDocument(this string html) + public static string? WrapAsHtmlDocument(this string? html) { - if (html == null) + if (string.IsNullOrWhiteSpace(html)) return html; if (!Regex.IsMatch(html, @"\")) @@ -975,8 +978,6 @@ public static string DoubleQuote(this string value) /// public static string ToSnakeCase(this string input) { - if (string.IsNullOrEmpty(input)) { return input; } - return Regex.Replace(Regex.Replace(input, "(.)([A-Z][a-z]+)", "$1_$2"), "([a-z0-9])([A-Z])", "$1_$2").ToLower().RemoveDoubleUndescores(); } @@ -995,11 +996,11 @@ public static string RemoveDoubleUndescores(this string input) /// snake_case text /// Max length /// - public static string SnakeCaseTrim(this string text, int length) + public static string? SnakeCaseTrim(this string? text, int length) { - if (text.IsNullOrEmpty()) + if (string.IsNullOrEmpty(text)) return text; - var delta = text?.Length - length; + var delta = text.Length - length; if (delta < 1) return text; var parts = text.Split('_'); @@ -1023,13 +1024,13 @@ public static string SnakeCaseTrim(this string text, int length) /// parts to compile snake_case text /// Max length /// - public static string SnakeCaseTrim(this string[] parts, int length) + public static string? SnakeCaseTrim(this string[] parts, int length) { - var text = string.Join("_", parts).ToSnakeCase(); + var text = string.Join("_", parts)?.ToSnakeCase(); - if (text.IsNullOrEmpty()) + if (string.IsNullOrWhiteSpace(text)) return text; - var delta = text?.Length - length; + var delta = text.Length - length; if (delta < 1) return text; if (parts.Length < 2) diff --git a/shesha-core/src/Shesha.Framework/Utilities/UtilityHelper.cs b/shesha-core/src/Shesha.Framework/Utilities/UtilityHelper.cs deleted file mode 100644 index e4161135c7..0000000000 --- a/shesha-core/src/Shesha.Framework/Utilities/UtilityHelper.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Shesha.AutoMapper.Dto; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Utilities -{ - public static class UtilityHelper - { - /// - /// - /// - /// - /// - /// - public static List GetMultiReferenceListItemValueList(T role) where T : struct, IConvertible - { - var result = new List(); - - if (role.ToString() == "0") - return result; - - var flag = Enum.Parse(typeof(T), role.ToString()) as Enum; - - foreach (var r in (long[])Enum.GetValues(typeof(T))) - { - if ((Convert.ToInt32(flag) & r) == r) - { - var nameValue = new ReferenceListItemValueDto() - { - Item = Enum.GetName(typeof(T), r), - ItemValue = r - }; - - result.Add(nameValue); - } - } - return result; - } - } -} diff --git a/shesha-core/src/Shesha.Framework/Validations/EntityPropertyValidator.cs b/shesha-core/src/Shesha.Framework/Validations/EntityPropertyValidator.cs index a195d2967c..337cbb1dd2 100644 --- a/shesha-core/src/Shesha.Framework/Validations/EntityPropertyValidator.cs +++ b/shesha-core/src/Shesha.Framework/Validations/EntityPropertyValidator.cs @@ -1,6 +1,5 @@ using Abp.Dependency; using Abp.Domain.Entities; -using Shesha.Configuration.Runtime; using Shesha.DynamicEntities.Cache; using Shesha.DynamicEntities.Dtos; using Shesha.Extensions; @@ -16,7 +15,7 @@ namespace Shesha.Validations { public abstract class EntityPropertyValidator : IPropertyValidator where TEntity : class, IEntity { - public async Task ValidateObjectAsync(object obj, List validationResult, List propertiesToValidate = null) + public async Task ValidateObjectAsync(object obj, List validationResult, List? propertiesToValidate = null) { if (obj is TEntity entity) { @@ -24,12 +23,12 @@ public async Task ValidateObjectAsync(object obj, List v } return true; } - public virtual async Task ValidateEntityAsync(TEntity entity, List validationResult, List propertiesToValidate = null) + public virtual async Task ValidateEntityAsync(TEntity entity, List validationResult, List? propertiesToValidate = null) { return await Task.FromResult(true); } - public async Task ValidatePropertyAsync(object obj, string propertyName, object value, List validationResult) + public async Task ValidatePropertyAsync(object obj, string propertyName, object? value, List validationResult) { if (obj is TEntity entity) { @@ -37,7 +36,7 @@ public async Task ValidatePropertyAsync(object obj, string propertyName, o } return true; } - public virtual async Task ValidateEntityPropertyAsync(TEntity entity, string propertyName, object value, List validationResult) + public virtual async Task ValidateEntityPropertyAsync(TEntity entity, string propertyName, object? value, List validationResult) { return await Task.FromResult(true); } @@ -47,15 +46,13 @@ public virtual async Task ValidateEntityPropertyAsync(TEntity entity, stri public class EntityPropertyValidator : IPropertyValidator, ITransientDependency { private IEntityConfigCache _entityConfigCache; - private IEntityConfigurationStore _entityConfigurationStore; - public EntityPropertyValidator(IEntityConfigCache entityConfigCache, IEntityConfigurationStore entityConfigurationStore) + public EntityPropertyValidator(IEntityConfigCache entityConfigCache) { _entityConfigCache = entityConfigCache; - _entityConfigurationStore = entityConfigurationStore; } - public async Task ValidatePropertyAsync(object obj, string propertyName, object value, List validationResult) + public async Task ValidatePropertyAsync(object obj, string propertyName, object? value, List validationResult) { if (!obj.GetType().IsEntityType() && !obj.GetType().IsJsonEntityType()) @@ -69,49 +66,12 @@ public async Task ValidatePropertyAsync(object obj, string propertyName, o return Validate(obj, propertyName, value, validationResult, props, true); } - public Task ValidateObjectAsync(object obj, List validationResult, List propertiesToValidate = null) + public Task ValidateObjectAsync(object obj, List validationResult, List? propertiesToValidate = null) { return Task.FromResult(true); - - #region Validate all properties. Not needed if use ValidateObject - /* if (!EntityHelper.IsEntity(obj.GetType())) - return true; - - var props = await _entityConfigCache.GetEntityPropertiesAsync(obj.GetType()); - var config = await _entityConfigCache.GetEntityConfigAsync(obj.GetType()); - - var pList = new List(); - - if (propertiesToValidate == null || !propertiesToValidate.Any()) - { - Action, string> propAdd = null; - propAdd = (List props, string root) => - { - foreach (var property in props.Where(x => !x.Suppress)) - { - pList.Add(root + property.Name); - propAdd(property.Properties, root + property.Name + "."); - } - }; - propAdd(props, ""); - } - else - { - pList.AddRange(propertiesToValidate); - } - - var vr = new List(); - foreach (var prop in pList.OrderBy(x => x)) - { - Validate(obj, prop, null, vr, props, false); - } - - validationResult.AddRange(vr); - return !vr.Any();*/ - #endregion } - public bool Validate(object obj, string propertyName, object value, List validationResult, + public bool Validate(object obj, string propertyName, object? value, List validationResult, List props, bool useNewValue) { var parts = propertyName.Split('.').Select(x => x.ToCamelCase()).ToArray(); @@ -120,7 +80,7 @@ public bool Validate(object obj, string propertyName, object value, List x.Name.ToCamelCase() == parts[0]); + var propInfo = obj.GetType().GetProperties().First(x => x.Name.ToCamelCase() == parts[0]); var innerObj = propInfo.GetValue(obj, null); var friendlyNameList = new List() { propConfig.Label }; @@ -128,7 +88,7 @@ public bool Validate(object obj, string propertyName, object value, List x.Name.ToCamelCase() == parts[i]); + propConfig = propConfig.Properties.First(x => x.Name.ToCamelCase() == parts[i]); propInfo = innerObj?.GetType().GetProperties().FirstOrDefault(x => x.Name.ToCamelCase() == parts[i]); innerObj = propInfo?.GetValue(innerObj, null); friendlyNameList.Add(propConfig.Label); @@ -174,21 +134,22 @@ public bool Validate(object obj, string propertyName, object value, List propConfig.MaxLength) + if (propConfig.MaxLength.HasValue && stringValueOrEmpty.Length > propConfig.MaxLength) { validationResult.Add(new ValidationResult(hasMessage ? propConfig.ValidationMessage : $"Property '{friendlyName}' should have value length less then {propConfig.MaxLength + 1} symbols")); return false; } - if (!string.IsNullOrWhiteSpace(propConfig.RegExp) && !(new Regex(propConfig.RegExp)).IsMatch(value?.ToString())) + if (!string.IsNullOrWhiteSpace(propConfig.RegExp) && !(new Regex(propConfig.RegExp)).IsMatch(stringValueOrEmpty)) { validationResult.Add(new ValidationResult(hasMessage ? propConfig.ValidationMessage diff --git a/shesha-core/src/Shesha.Framework/Validations/IObjectValidatorManager.cs b/shesha-core/src/Shesha.Framework/Validations/IObjectValidatorManager.cs index 115d173d57..d820e400d8 100644 --- a/shesha-core/src/Shesha.Framework/Validations/IObjectValidatorManager.cs +++ b/shesha-core/src/Shesha.Framework/Validations/IObjectValidatorManager.cs @@ -6,8 +6,8 @@ namespace Shesha.Validations { public interface IObjectValidatorManager { - Task ValidatePropertyAsync(object obj, string propertyName, object value, List validationResult); + Task ValidatePropertyAsync(object obj, string propertyName, object? value, List validationResult); - Task ValidateObjectAsync(object obj, List validationResult, List propertiesToValidate = null); + Task ValidateObjectAsync(object obj, List validationResult, List? propertiesToValidate = null); } } diff --git a/shesha-core/src/Shesha.Framework/Validations/IPropertyValidator.cs b/shesha-core/src/Shesha.Framework/Validations/IPropertyValidator.cs index 3b68541fa1..0dd55f3096 100644 --- a/shesha-core/src/Shesha.Framework/Validations/IPropertyValidator.cs +++ b/shesha-core/src/Shesha.Framework/Validations/IPropertyValidator.cs @@ -6,8 +6,8 @@ namespace Shesha.Validations { public interface IPropertyValidator { - Task ValidatePropertyAsync(object obj, string propertyName, object value, List validationResult); + Task ValidatePropertyAsync(object obj, string propertyName, object? value, List validationResult); - Task ValidateObjectAsync(object obj, List validationResult, List propertiesToValidate = null); + Task ValidateObjectAsync(object obj, List validationResult, List? propertiesToValidate = null); } } diff --git a/shesha-core/src/Shesha.Framework/Validations/ObjectValidatorManager.cs b/shesha-core/src/Shesha.Framework/Validations/ObjectValidatorManager.cs index 0ee4c5df2d..cfcb0db83a 100644 --- a/shesha-core/src/Shesha.Framework/Validations/ObjectValidatorManager.cs +++ b/shesha-core/src/Shesha.Framework/Validations/ObjectValidatorManager.cs @@ -18,13 +18,13 @@ public ObjectValidatorManager(IIocManager iocManager) _iocManager = iocManager; } - public async Task ValidatePropertyAsync(object obj, string propertyName, object value, List validationResult) + public async Task ValidatePropertyAsync(object obj, string propertyName, object? value, List validationResult) { validationResult ??= new List(); return await ValidateAsync((v) => v.ValidatePropertyAsync(obj, propertyName, value, validationResult)); } - public async Task ValidateObjectAsync(object obj, List validationResult = null, List propertiesToValidate = null) + public async Task ValidateObjectAsync(object obj, List? validationResult = null, List? propertiesToValidate = null) { validationResult ??= new List(); return await ValidateAsync((v) => v.ValidateObjectAsync(obj, validationResult, propertiesToValidate)); diff --git a/shesha-core/src/Shesha.Framework/Validations/ValidationExtensions.cs b/shesha-core/src/Shesha.Framework/Validations/ValidationExtensions.cs new file mode 100644 index 0000000000..228681a3d6 --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Validations/ValidationExtensions.cs @@ -0,0 +1,74 @@ +using Abp.Runtime.Validation; +using Shesha.Utilities; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Shesha.Validations +{ + /// + /// Validation extensions + /// + public static class ValidationExtensions + { + /// + /// Throw if has any errors + /// + /// Validation results + /// Common error message + /// + public static void ThrowValidationExceptionIfAny(this IList? validationResults, string errorMessage) + { + if (validationResults != null && validationResults.Any()) + throw new AbpValidationException(errorMessage, validationResults); + } + + /// + /// Throw if has any errors + /// + /// Validation results + /// Localizer, is used for localization of common error message + public static void ThrowValidationExceptionIfAny(this IList? validationResults, Func localizer) + { + validationResults.ThrowValidationExceptionIfAny(localizer(AppMessages.ValidationCorrectErrorsAndTryAgain)); + } + + /// + /// Throw validation exception. When is not empty uses `Correct errors and try again...` message, otherwise uses common `Something went wrong` + /// Is used in cases when you are sure that something went wrong but details may be unavailable + /// + /// + /// + /// + public static void ForceThrowValidationException(this IList? validationResults, Func localizer) + { + if (validationResults != null && validationResults.Any()) + throw new AbpValidationException(localizer(AppMessages.ValidationCorrectErrorsAndTryAgain), validationResults); + else + throw new AbpValidationException(localizer(AppMessages.ValidationSomethingWentWrong)); + } + + /// + /// Throw exception if has any errors + /// Note: for single error throws , for multiple errors - + /// + /// + /// + public static void ThrowArgumentExceptionIfAny(this IList? validationResults) + { + if (validationResults != null && validationResults.Any()) + { + if (validationResults.Count() == 1) + throw ValidationResult2ArgumentException(validationResults.Single()); + else + throw new AggregateException(validationResults.Select(r => ValidationResult2ArgumentException(r))); + } + } + + private static ArgumentException ValidationResult2ArgumentException(ValidationResult validationResult) + { + return new ArgumentException(validationResult.ErrorMessage, validationResult.MemberNames.Delimited(", ")); + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Validations/ValidationResults.cs b/shesha-core/src/Shesha.Framework/Validations/ValidationResults.cs new file mode 100644 index 0000000000..6974468ecf --- /dev/null +++ b/shesha-core/src/Shesha.Framework/Validations/ValidationResults.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Shesha.Validations +{ + /// + /// List of . Is used for simplification of coding + /// + public class ValidationResults: List + { + public void Add(string errorMessage) + { + Add(new ValidationResult(errorMessage)); + } + + public void Add(string errorMessage, IEnumerable memberNames) + { + Add(new ValidationResult(errorMessage, memberNames)); + } + } +} diff --git a/shesha-core/src/Shesha.GraphQL/GraphQL/GraphQLNhListener.cs b/shesha-core/src/Shesha.GraphQL/GraphQL/GraphQLNhListener.cs deleted file mode 100644 index afd3c9f887..0000000000 --- a/shesha-core/src/Shesha.GraphQL/GraphQL/GraphQLNhListener.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Abp.Domain.Uow; -using GraphQL.Execution; -using GraphQL.Validation; -using System.Threading.Tasks; - -namespace Shesha.GraphQL -{ - /// - /// GraphQL NHibernate listener. To be moved to a separate project - /// - public class GraphQLNhListener : IDocumentExecutionListener - { - private IUnitOfWorkCompleteHandle _unitOfWorkCompleteHandle; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - public GraphQLNhListener(IUnitOfWorkManager unitOfWorkManager) - { - _unitOfWorkManager = unitOfWorkManager; - } - - public async Task AfterExecutionAsync(IExecutionContext context) - { - await _unitOfWorkCompleteHandle?.CompleteAsync(); - } - - public Task AfterValidationAsync(IExecutionContext context, IValidationResult validationResult) - { - return Task.CompletedTask; - } - - public Task BeforeExecutionAsync(IExecutionContext context) - { - _unitOfWorkCompleteHandle = _unitOfWorkManager.Begin(); - - return Task.CompletedTask; - } - } -} diff --git a/shesha-core/src/Shesha.GraphQL/GraphQL/GraphQLUnitOfWorkListener.cs b/shesha-core/src/Shesha.GraphQL/GraphQL/GraphQLUnitOfWorkListener.cs index 0889db7fdb..436df2fce1 100644 --- a/shesha-core/src/Shesha.GraphQL/GraphQL/GraphQLUnitOfWorkListener.cs +++ b/shesha-core/src/Shesha.GraphQL/GraphQL/GraphQLUnitOfWorkListener.cs @@ -1,14 +1,16 @@ using Abp.Domain.Uow; using GraphQL.Execution; using GraphQL.Validation; +using System; using System.Threading.Tasks; namespace Shesha.GraphQL.UnitOfWork { - public class GraphQLUnitOfWorkListener : IDocumentExecutionListener + public sealed class GraphQLUnitOfWorkListener : IDocumentExecutionListener, IDisposable { private IUnitOfWorkCompleteHandle _unitOfWorkCompleteHandle; - private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly IUnitOfWorkManager _unitOfWorkManager; + private bool _disposed; public IUnitOfWorkManager UnitOfWorkManager => _unitOfWorkManager; @@ -29,9 +31,27 @@ public Task AfterValidationAsync(IExecutionContext context, IValidationResult va public Task BeforeExecutionAsync(IExecutionContext context) { + _unitOfWorkCompleteHandle?.Dispose(); _unitOfWorkCompleteHandle = UnitOfWorkManager.Begin(); return Task.CompletedTask; + } + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _unitOfWorkCompleteHandle?.Dispose(); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } } } } diff --git a/shesha-core/src/Shesha.GraphQL/GraphQL/Middleware/GraphQLUserContext.cs b/shesha-core/src/Shesha.GraphQL/GraphQL/Middleware/GraphQLUserContext.cs index 8293af4124..41667a7314 100644 --- a/shesha-core/src/Shesha.GraphQL/GraphQL/Middleware/GraphQLUserContext.cs +++ b/shesha-core/src/Shesha.GraphQL/GraphQL/Middleware/GraphQLUserContext.cs @@ -6,7 +6,7 @@ namespace Shesha.GraphQL.Middleware /// /// GraphQL user context /// - public class GraphQLUserContext : Dictionary + public class GraphQLUserContext : Dictionary { /// /// Current user diff --git a/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/GraphQLDotnetGraphQLQueryProvider.cs b/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/GraphQLDotnetGraphQLQueryProvider.cs index 182c24d8c4..4370850fc6 100644 --- a/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/GraphQLDotnetGraphQLQueryProvider.cs +++ b/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/GraphQLDotnetGraphQLQueryProvider.cs @@ -32,8 +32,6 @@ public GraphQLDotnetGraphQLQueryProvider( public virtual async Task /*Task>*/ ExecuteAsync(string operationName, string query, Dictionary variables, string defaultSchemaName = null) { - // change input type to GraphQLRequest - var schema = await _schemaContainer.GetOrDefaultAsync(operationName, defaultSchemaName); variables ??= new Dictionary(); @@ -72,45 +70,6 @@ public GraphQLDotnetGraphQLQueryProvider( var deserializedResult = await _writer.ReadAsync(memoryStream); return deserializedResult; - - /* - using var memoryStream = new MemoryStream(); - - await _writer.WriteAsync(memoryStream, result); - - memoryStream.Seek(0, SeekOrigin.Begin); - - var response = await JsonSerializer.DeserializeAsync>(memoryStream); - return response; - */ } - /* - protected virtual Task WriteResponseAsync(HttpResponse httpResponse, IGraphQLSerializer serializer, CancellationToken cancellationToken, TResult result) - { - httpResponse.ContentType = "application/json"; - httpResponse.StatusCode = result is not ExecutionResult executionResult || executionResult.Executed ? 200 : 400; // BadRequest when fails validation; OK otherwise - - return serializer.WriteAsync(httpResponse.Body, result, cancellationToken); - } - */ - - /* - protected virtual Task ExecuteRequestAsync( - GraphQLRequest gqlRequest, - IDictionary userContext, - IDocumentExecuter executer, - IServiceProvider requestServices, - CancellationToken token) - => executer.ExecuteAsync(new ExecutionOptions - { - Query = gqlRequest.Query, - OperationName = gqlRequest.OperationName, - Variables = gqlRequest.Variables, - Extensions = gqlRequest.Extensions, - UserContext = userContext, - RequestServices = requestServices, - CancellationToken = token - }); - */ } } diff --git a/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/GraphTypes/GraphQLGenericType.cs b/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/GraphTypes/GraphQLGenericType.cs index d03a48adf6..83cbea0690 100644 --- a/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/GraphTypes/GraphQLGenericType.cs +++ b/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/GraphTypes/GraphQLGenericType.cs @@ -246,6 +246,7 @@ private void EmitField(PropertyInfo propertyInfo) Field(listType, propertyInfo.Name); break; } + case nameof(Guid): Field(GraphTypeMapper.GetGraphType(propertyInfo.PropertyType, isInput: false), propertyInfo.Name); break; case nameof(Boolean): Field(GraphTypeMapper.GetGraphType(propertyInfo.PropertyType, isInput: false), propertyInfo.Name); break; case nameof(Int32): Field(GraphTypeMapper.GetGraphType(propertyInfo.PropertyType, isInput: false), propertyInfo.Name); break; case nameof(Int64): Field(GraphTypeMapper.GetGraphType(propertyInfo.PropertyType, isInput: false), propertyInfo.Name); break; @@ -276,6 +277,13 @@ private void EmitField(PropertyInfo propertyInfo) { switch (underlyingType.Name) { + case nameof(Guid): + Field(GraphTypeMapper.GetGraphType(underlyingType, isInput: false), propertyInfo.Name, resolve: context => + { + var nullableGuid = propertyInfo.GetValue(context.Source) as Guid?; + if (nullableGuid.HasValue) return nullableGuid.Value; + else return null; + }); break; case nameof(Int32): Field(GraphTypeMapper.GetGraphType(underlyingType, isInput: false), propertyInfo.Name, resolve: context => { diff --git a/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/IGraphQLQueryProvider.cs b/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/IGraphQLQueryProvider.cs index 05195deb86..9dfd019686 100644 --- a/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/IGraphQLQueryProvider.cs +++ b/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/IGraphQLQueryProvider.cs @@ -16,9 +16,9 @@ public interface IGraphQLQueryProvider /// Used to get a schema if the operation name was not specified. /// A dictionary result. Task ExecuteAsync( - [CanBeNull] string operationName, + [CanBeNull] string? operationName, [NotNull] string query, - [CanBeNull] Dictionary variables, - [CanBeNull] string defaultSchemaName = null); + [CanBeNull] Dictionary? variables, + [CanBeNull] string? defaultSchemaName = null); } } diff --git a/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/SchemaContainer.cs b/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/SchemaContainer.cs index 66e4da5011..36a0bd4f38 100644 --- a/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/SchemaContainer.cs +++ b/shesha-core/src/Shesha.GraphQL/GraphQL/Provider/SchemaContainer.cs @@ -5,6 +5,7 @@ using Abp.Reflection; using Abp.Runtime.Caching; using GraphQL.Types; +using Shesha.Cache; using Shesha.Domain; using Shesha.Extensions; using Shesha.GraphQL.Provider.Schemas; @@ -16,35 +17,26 @@ namespace Shesha.GraphQL.Provider { - public class SchemaContainer : ISchemaContainer, ISingletonDependency, IEventHandler> + public class SchemaContainer : CacheHolder, ISchemaContainer, ISingletonDependency, IEventHandler> { private readonly ICacheManager _cacheManager; private readonly ITypeFinder _typeFinder; private readonly IServiceProvider _serviceProvider; + private readonly Schema _defaultSchema; - protected ISchema DefaultSchema { get; set; } /// /// Custom schemas /// protected Dictionary CustomSchemas { get; set; } - protected ITypedCache EntitySchemasCache - { - get - { - return _cacheManager.GetCache($"{this.GetType().Name}.{nameof(EntitySchemasCache)}"); - } - } - public SchemaContainer( - //IOptions options, IServiceProvider serviceProvider, IEnumerable customSchemas, ITypeFinder typeFinder, - ICacheManager cacheManager) + ICacheManager cacheManager): base($"{nameof(SchemaContainer)}Cache", cacheManager) { - DefaultSchema = new Schema(); - DefaultSchema.Query = new EmptyQuery(); + _defaultSchema = new Schema(); + _defaultSchema.Query = new EmptyQuery(); _typeFinder = typeFinder; _cacheManager = cacheManager; _serviceProvider = serviceProvider; @@ -60,7 +52,7 @@ private ISchema GetEntitySchema(string schemaName) if (entityType == null) return null; - return EntitySchemasCache.Get(schemaName, sn => GetEntitySchema(entityType, _serviceProvider)); + return Cache.Get(schemaName, sn => GetEntitySchema(entityType, _serviceProvider)); } private static ISchema GetEntitySchema(Type entityType, IServiceProvider serviceProvider) @@ -96,7 +88,7 @@ public static string GetEntitySchemaName(string entityTypeName) public virtual Task GetOrDefaultAsync(string schemaName, string defaultSchemaName = null) { ISchema schema = null; - if (!schemaName.IsNullOrEmpty()) + if (!string.IsNullOrWhiteSpace(schemaName)) { schema = CustomSchemas.ContainsKey(schemaName) ? CustomSchemas[schemaName] @@ -104,7 +96,7 @@ public virtual Task GetOrDefaultAsync(string schemaName, string default } if (schema == null) - schema = defaultSchemaName != null ? CustomSchemas[defaultSchemaName] : DefaultSchema; + schema = defaultSchemaName != null ? CustomSchemas[defaultSchemaName] : _defaultSchema; return Task.FromResult(schema); } @@ -114,12 +106,18 @@ public void HandleEvent(EntityChangedEventData eventData) if (eventData.Entity?.EntityConfig == null) return; - EntitySchemasCache.Remove(GetEntitySchemaName(eventData.Entity.EntityConfig.ClassName)); + Cache.Remove(GetEntitySchemaName(eventData.Entity.EntityConfig.ClassName)); } public void RegisterCustomSchema(string name, ISchema schema) { CustomSchemas[name] = schema; } + + public override void Dispose() + { + base.Dispose(); + _defaultSchema?.Dispose(); + } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.GraphQL/Shesha.GraphQL.csproj b/shesha-core/src/Shesha.GraphQL/Shesha.GraphQL.csproj index 204343b05d..24970c45d4 100644 --- a/shesha-core/src/Shesha.GraphQL/Shesha.GraphQL.csproj +++ b/shesha-core/src/Shesha.GraphQL/Shesha.GraphQL.csproj @@ -18,15 +18,23 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.Import/ImportApplicationService.cs b/shesha-core/src/Shesha.Import/ImportApplicationService.cs index 6b1df9fc6b..37b97ffcd7 100644 --- a/shesha-core/src/Shesha.Import/ImportApplicationService.cs +++ b/shesha-core/src/Shesha.Import/ImportApplicationService.cs @@ -16,6 +16,7 @@ using Shesha.Domain.Enums; using Shesha.Exceptions; using Shesha.Import.Dto; +using Shesha.Reflection; using Shesha.Services; using Shesha.Utilities; @@ -51,6 +52,8 @@ public virtual async Task StartImportAsync([FromForm] StartImportInput inp if (validationResults.Any()) throw new AbpValidationException("Failed to start import", validationResults); + input.File.NotNull(); + var fileName = input.File.FileName.CleanupFileName(); await using (var fileStream = input.File.OpenReadStream()) { diff --git a/shesha-core/src/Shesha.Import/ImportServiceBase.cs b/shesha-core/src/Shesha.Import/ImportServiceBase.cs index c7e7595a2b..885aefd1a9 100644 --- a/shesha-core/src/Shesha.Import/ImportServiceBase.cs +++ b/shesha-core/src/Shesha.Import/ImportServiceBase.cs @@ -37,7 +37,7 @@ public abstract class ImportServiceBase: IImport where T : ImportResult /// /// Reference to the object to object mapper. /// - public IObjectMapper ObjectMapper { get; set; } + public IObjectMapper ObjectMapper { get; set; } = NullObjectMapper.Instance; private string _loggerName; private string _logGroupName; @@ -247,7 +247,7 @@ public async Task ImportAsync(string fileName, Stream stream, CancellationToken await ImportAsync(fileName, stream, cancellationToken, null); } - public async Task ImportAsync(string fileName, Stream stream, CancellationToken cancellationToken, Action prepareResultAction) + public async Task ImportAsync(string fileName, Stream stream, CancellationToken cancellationToken, Action? prepareResultAction) { try { diff --git a/shesha-core/src/Shesha.Import/Shesha.Import.csproj b/shesha-core/src/Shesha.Import/Shesha.Import.csproj index 072f38545f..a1977f6089 100644 --- a/shesha-core/src/Shesha.Import/Shesha.Import.csproj +++ b/shesha-core/src/Shesha.Import/Shesha.Import.csproj @@ -20,13 +20,21 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.MongoRepository/Mongo/MongoRepository.cs b/shesha-core/src/Shesha.MongoRepository/Mongo/MongoRepository.cs index 68f91c2145..2b1e967a90 100644 --- a/shesha-core/src/Shesha.MongoRepository/Mongo/MongoRepository.cs +++ b/shesha-core/src/Shesha.MongoRepository/Mongo/MongoRepository.cs @@ -3,6 +3,7 @@ using MongoDB.Driver.Linq; using MongoDbRepository; using Shesha.MongoRepository.Utils; +using Shesha.Reflection; using System; using System.Collections.Generic; using System.Linq; @@ -17,7 +18,7 @@ public class MongoRepository : ITransientDependency, IMongoRepository wher public MongoRepository() { - context = new MongoDbContext(MongoUtil.ConnectionString); + context = new MongoDbContext(MongoUtil.ConnectionString.NotNullOrWhiteSpace()); } #region Properties diff --git a/shesha-core/src/Shesha.MongoRepository/Mongo/SheshaMongoModule.cs b/shesha-core/src/Shesha.MongoRepository/Mongo/SheshaMongoModule.cs index 1b43771ab9..6ac0fec21c 100644 --- a/shesha-core/src/Shesha.MongoRepository/Mongo/SheshaMongoModule.cs +++ b/shesha-core/src/Shesha.MongoRepository/Mongo/SheshaMongoModule.cs @@ -1,10 +1,7 @@ -using System.Reflection; -using Abp.Localization.Dictionaries.Xml; -using Abp.Localization.Sources; -using Abp.Modules; +using Abp.Modules; using Abp.Zero; -using Abp.AspNetCore.Configuration; using Castle.MicroKernel.Registration; +using System.Reflection; namespace Shesha.MongoRepository.Mongo { diff --git a/shesha-core/src/Shesha.MongoRepository/Shesha.MongoRepository.csproj b/shesha-core/src/Shesha.MongoRepository/Shesha.MongoRepository.csproj index ca0943d9a2..23e9fc9db1 100644 --- a/shesha-core/src/Shesha.MongoRepository/Shesha.MongoRepository.csproj +++ b/shesha-core/src/Shesha.MongoRepository/Shesha.MongoRepository.csproj @@ -16,11 +16,19 @@ .\Shesha.NHibernate.xml - + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.MongoRepository/Utils/MongoUtil.cs b/shesha-core/src/Shesha.MongoRepository/Utils/MongoUtil.cs index c54d9d7776..34b7de6704 100644 --- a/shesha-core/src/Shesha.MongoRepository/Utils/MongoUtil.cs +++ b/shesha-core/src/Shesha.MongoRepository/Utils/MongoUtil.cs @@ -11,13 +11,16 @@ public class MongoUtil /// /// Connection string with password /// - public static string ConnectionString { get => _connectionString ?? GetConnectionString("MongoDBConnection"); set => _connectionString = value; } - private static string _connectionString; + public static string? ConnectionString { + get => _connectionString ?? GetConnectionString("MongoDBConnection"); + set => _connectionString = value; + } + private static string? _connectionString; /// /// Returns connection string. Note: for the Azure environment - uses standard environment variable /// - public static string GetConnectionString(string name) + public static string? GetConnectionString(string name) { var env = StaticContext.IocManager.IocContainer.Resolve(); var configuration = AppConfigurations.Get(env.ContentRootPath, env.EnvironmentName, env.IsDevelopment()); diff --git a/shesha-core/src/Shesha.NHibernate.PostGis/NHibernate/PostGis/SheshaPostGisExtensions.cs b/shesha-core/src/Shesha.NHibernate.PostGis/NHibernate/PostGis/SheshaPostGisExtensions.cs index d3c3e7f11b..20e9f2ba38 100644 --- a/shesha-core/src/Shesha.NHibernate.PostGis/NHibernate/PostGis/SheshaPostGisExtensions.cs +++ b/shesha-core/src/Shesha.NHibernate.PostGis/NHibernate/PostGis/SheshaPostGisExtensions.cs @@ -23,10 +23,13 @@ public static IShaNHibernateModuleConfiguration UsePostGis(this IShaNHibernateMo { global::NHibernate.Spatial.Metadata.Metadata.AddMapping(cfg, metadataClass); } - + // rebuild session factory if mapping were changed - if (supportedMetadataClasses.Any()) + if (supportedMetadataClasses.Any()) + { + sessionFactory.Dispose(); sessionFactory = cfg.BuildSessionFactory(); + } } return sessionFactory; diff --git a/shesha-core/src/Shesha.NHibernate.PostGis/Shesha.NHibernate.PostGis.csproj b/shesha-core/src/Shesha.NHibernate.PostGis/Shesha.NHibernate.PostGis.csproj index bd7c74a23c..ea6755585a 100644 --- a/shesha-core/src/Shesha.NHibernate.PostGis/Shesha.NHibernate.PostGis.csproj +++ b/shesha-core/src/Shesha.NHibernate.PostGis/Shesha.NHibernate.PostGis.csproj @@ -7,11 +7,19 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -19,7 +27,7 @@ - + diff --git a/shesha-core/src/Shesha.NHibernate/CodeGeneration/MigrationGenerator.cs b/shesha-core/src/Shesha.NHibernate/CodeGeneration/MigrationGenerator.cs index 559521347e..ff38743e86 100644 --- a/shesha-core/src/Shesha.NHibernate/CodeGeneration/MigrationGenerator.cs +++ b/shesha-core/src/Shesha.NHibernate/CodeGeneration/MigrationGenerator.cs @@ -244,7 +244,7 @@ private static void AddColumns(Type entityType, StringBuilder sb, List()?.MaximumLength; + var maxLength = property.GetAttributeOrNull()?.MaximumLength; sb.Append(maxLength != null && maxLength < int.MaxValue ? $@".{nameof(IColumnTypeSyntax.AsString)}({maxLength})" : $@".{nameof(SheshaFluentMigratorExtensions.AsStringMax)}()"); @@ -302,7 +302,7 @@ private static void AddColumns(Type entityType, StringBuilder sb, List(); + var precisionAttribute = property.GetAttributeOrNull(); if (precisionAttribute != null) sb.Append($@".{nameof(IColumnTypeSyntax.AsDecimal)}({precisionAttribute.Precision}, {precisionAttribute.Scale})"); else @@ -469,7 +469,7 @@ private static List ProcessFrameworkColumns(StringBuilder sb, List { properties.Remove(formProperty); - var prefix = MappingHelper.GetColumnPrefix(formProperty.DeclaringType); + var prefix = MappingHelper.GetColumnPrefix(formProperty.DeclaringType.NotNull()); var moduleColumn = MappingHelper.GetNameForMember(formProperty, prefix, formProperty.Name, nameof(FormIdentifier.Module)); var nameColumn = MappingHelper.GetNameForMember(formProperty, prefix, formProperty.Name, nameof(FormIdentifier.Name)); diff --git a/shesha-core/src/Shesha.NHibernate/CodeGeneration/TableColumn.cs b/shesha-core/src/Shesha.NHibernate/CodeGeneration/TableColumn.cs deleted file mode 100644 index 8288aa204c..0000000000 --- a/shesha-core/src/Shesha.NHibernate/CodeGeneration/TableColumn.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Reflection; - -namespace Shesha.CodeGeneration -{ - public class TableColumn - { - public string ColumnName { get; set; } - public string PropertyName { get; set; } - public Type DataType { get; set; } - public PropertyInfo Property { get; set; } - public int Index { get; set; } - public int? Size { get; set; } - public int? Precision { get; set; } - } -} diff --git a/shesha-core/src/Shesha.NHibernate/JsonLogic/HqlOperators.cs b/shesha-core/src/Shesha.NHibernate/JsonLogic/HqlOperators.cs deleted file mode 100644 index 4b2a9f53c9..0000000000 --- a/shesha-core/src/Shesha.NHibernate/JsonLogic/HqlOperators.cs +++ /dev/null @@ -1,462 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using JsonLogic.Net; -using Newtonsoft.Json.Linq; -using Shesha.Configuration.Runtime; -using Shesha.Domain.Attributes; -using Shesha.Metadata; -using Shesha.Utilities; - -namespace Shesha.JsonLogic -{ - /// - /// JsonLogic operators - /// - public class HqlOperators - { - private Dictionary> registry; - - public static HqlOperators Default { get; } = new HqlOperators(); - - public HqlOperators() - { - registry = new Dictionary>(); - AddDefaultOperations(); - } - - public void AddOperator(string name, Func operation) - { - registry[name] = operation; - } - - public void DeleteOperator(string name) - { - registry.Remove(name); - } - - public Func GetOperator(string name) - { - return registry[name]; - } - - public static bool IsAny(params object[] subjects) - { - return subjects.Any(x => x != null && x is T); - } - - private void AddDefaultOperations() - { - AddOperator("==", (p, args, data) => - { - return ConvertArguments(p, args, data).Delimited(" = "); - }); - - AddOperator("===", GetOperator("==")); - - AddOperator("!=", (p, args, data) => - { - return ConvertArguments(p, args, data).Delimited(" <> "); - }); - - AddOperator("!==", GetOperator("!=")); - - AddOperator("<", (p, args, data) => - { - return ConvertArguments(p, args, data).Delimited(" < "); - }); - - AddOperator("<=", (p, args, data) => - { - return SequenceConditions(ConvertArguments(p, args, data), " <= "); // ConvertArguments(p, args, data).Delimited(" <= "); - }); - - AddOperator(">", (p, args, data) => - { - return ConvertArguments(p, args, data).Delimited(" > "); - }); - - AddOperator(">=", (p, args, data) => - { - return ConvertArguments(p, args, data).Delimited(" >= "); - }); - - AddOperator("var", (p, args, data) => - { - const string prefix = "ent"; - - if (!args.Any()) return - prefix; - - var name = GetStringValue(args.First()); - name = p.ResolveVariable(name, data); - - if (name == null) - return prefix; - - // handle entity references - // todo: implement support of nested entities - var dataType = data.FieldsMetadata.TryGetValue(name, out var meta) ? meta.DataType : null; - if (dataType == DataTypes.EntityReference) - { - name = $"{name}.Id"; - } - - return $"{prefix}.{name}"; - - /* - try - { - var result = GetValueByName(data, names.ToString()); - // This will return JValue or null if missing. Actual value of null will be wrapped in JToken with value null - if (result is JValue) - { - // permit correct type wrangling to occur (AdjustType) without duplicating code - result = p.Apply((JValue)result, null); - - } - else if (result == null && args.Count() == 2) - { - object defaultValue = p.Apply(args.Last(), data); - result = defaultValue; - } - - return result; - } - catch - { - object defaultValue = (args.Count() == 2) ? p.Apply(args.Last(), data) : null; - return defaultValue; - } - */ - }); - - AddOperator("and", (p, args, data) => { - return ConvertArguments(p, args, data, true).Delimited(" and "); - }); - - AddOperator("or", (p, args, data) => - { - return ConvertArguments(p, args, data, true).Delimited(" or "); - }); - - AddOperator("!!", (p, args, data) => - { - return $"{p.Convert(args.First(), data)} is not null"; - }); - - AddOperator("!", (p, args, data) => - { - // special handling of single string argument - var argument = args.First(); - var convertedArgument = p.Convert(argument, data); - - var nestedOperation = p.GetOperation(argument); - if (nestedOperation.Name == "var") - { - // check datatype of the argument and add " or trim({convertedArgument}) = ''" for strings - return $"{convertedArgument} is null"; - } - - return $"not ({convertedArgument})"; - }); - - AddOperator("not", GetOperator("!")); - - AddOperator("in", (p, args, data) => { - var first = p.Convert(args[0], data); - var second = p.Convert(args[1], data); - - var isArray = args[1] is JArray; - if (isArray) - { - return $"{first} in ({second})"; - } - else - { - // process as string - return $"{second} like '%' + {first.Trim('\'')} + '%'"; - } - }); - - AddOperator("startsWith", (p, args, data) => { - var first = p.Convert(args[0], data); - var second = p.Convert(args[1], data); - - return $"{first} like {second.Trim('\'')} + '%'"; - }); - - AddOperator("endsWith", (p, args, data) => { - var first = p.Convert(args[0], data); - var second = p.Convert(args[1], data); - - return $"{first} like '%' + {second.Trim('\'')}"; - }); - - /* not supported standard operators - AddOperator("+", (p, args, data) => Min2From(args.Select(a => p.Apply(a, data))).Aggregate((prev, next) => - { - try - { - return Convert.ToDouble(prev ?? 0d) + Convert.ToDouble(next); - } - catch - { - return (prev ?? string.Empty).ToString() + next.ToString(); - } - })); - - AddOperator("-", ReduceDoubleArgs(0d, (prev, next) => prev - next)); - - AddOperator("/", ReduceDoubleArgs(1, (prev, next) => prev / next)); - - AddOperator("*", ReduceDoubleArgs(1, (prev, next) => prev * next)); - - AddOperator("%", ReduceDoubleArgs(1, (prev, next) => prev % next)); - - AddOperator("max", (p, args, data) => args.Select(a => Convert.ToDouble(p.Apply(a, data))) - .Aggregate((prev, next) => prev > next ? prev : next)); - - AddOperator("min", (p, args, data) => args.Select(a => Convert.ToDouble(p.Apply(a, data))) - .Aggregate((prev, next) => (prev < next) ? prev : next)); - - AddOperator("if", (p, args, data) => { - for (var i = 0; i < args.Length - 1; i += 2) - { - if (p.Apply(args[i], data).IsTruthy()) return p.Apply(args[i + 1], data); - } - if (args.Length % 2 == 0) return null; - return p.Apply(args[args.Length - 1], data); - }); - - AddOperator("?:", GetOperator("if")); - - AddOperator("missing", (p, args, data) => { - var names = args.Select(a => p.Apply(a, data)); - if (names.Count() == 1 && names.First().IsEnumerable()) names = names.First().MakeEnumerable(); - if (data == null) return names.ToArray(); - return names.Select(n => n.ToString()).Where(n => { - try - { - GetValueByName(data, n); - return false; - } - catch - { - return true; - } - }).ToArray(); - }); - AddOperator("cat", (p, args, data) => args.Select(a => p.Apply(a, data)).Aggregate("", (acc, current) => acc + current.ToString())); - - AddOperator("substr", (p, args, data) => { - string value = p.Apply(args[0], data).ToString(); - int start = Convert.ToInt32(p.Apply(args[1], data)); - while (start < 0) start += value.Length; - if (args.Count() == 2) return value.Substring(start); - - int length = Convert.ToInt32(p.Apply(args[2], data)); - if (length >= 0) return value.Substring(start, length); - - int end = length; - while (end < 0) end += value.Length; - return value.Substring(start, end - start); - }); - - AddOperator("log", (p, args, data) => { - object value = p.Apply(args[0], data); - System.Diagnostics.Debug.WriteLine(value); - return value; - }); - - AddOperator("within", (p, args, data) => { - double first = Convert.ToDouble(p.Apply(args[0], data)); - double second = Convert.ToDouble(p.Apply(args[1], data)); - double precision = Convert.ToDouble(p.Apply(args[2], data)); - return Math.Abs(first - second) <= precision; - }); - - // Local processing operator changes scope of evaluations of its - // second argument to result of the first argument - // i.e. "local": [ "sourceDataOrLogicAppliedToFullData", "logicToApplyToSourceOnly"] - AddOperator("local", (p, args, data) => - { - // if blank arguments, return null, since nothing matches conditions - if (args.Length < 1) return null; - var local = p.Apply(args.First(), data); - if (args.Length >= 2) return p.Apply(args[1], local); - else return local; // return result of source evaluation - }); - AddOperator("missing_some", (p, args, data) => { - var minRequired = Convert.ToDouble(p.Apply(args[0], data)); - var keys = (args[1] as JArray).ToArray(); - var missingKeys = GetOperator("missing").Invoke(p, keys, data) as IEnumerable; - var validKeyCount = keys.Length - missingKeys.Count(); - return (validKeyCount >= minRequired) ? new object[0] : missingKeys; - }); - - AddOperator("map", (p, args, data) => { - object arr = p.Apply(args[0], data); - if (arr == null) return new object[0]; - - return arr.MakeEnumerable().Select(item => p.Apply(args[1], item)).ToArray(); - }); - - AddOperator("filter", (p, args, data) => { - // if first part fails to retrieve data, make enumerable will fail - IEnumerable arr = p.Apply(args[0], data)?.MakeEnumerable(); - return arr?.Where(item => p.Apply(args[1], item).IsTruthy()).ToArray(); - }); - - AddOperator("reduce", (p, args, data) => { - var initialValue = p.Apply(args[2], data); - object arr = p.Apply(args[0], data); - if (!arr.IsEnumerable()) return initialValue; - - return arr.MakeEnumerable().Aggregate(initialValue, (acc, current) => { - object result = p.Apply(args[1], new { current = current, accumulator = acc }); - return result; - }); - }); - - AddOperator("all", (p, args, data) => { - IEnumerable arr = p.Apply(args[0], data).MakeEnumerable(); - if (arr.Count() == 0) return false; - return arr.All(item => Convert.ToBoolean(p.Apply(args[1], item))); - }); - - AddOperator("none", (p, args, data) => { - IEnumerable arr = p.Apply(args[0], data).MakeEnumerable(); - return !arr.Any(item => Convert.ToBoolean(p.Apply(args[1], item))); - }); - - AddOperator("some", (p, args, data) => { - IEnumerable arr = p.Apply(args[0], data).MakeEnumerable(); - return arr.Any(item => Convert.ToBoolean(p.Apply(args[1], item))); - }); - - AddOperator("merge", (p, args, data) => args.Select(a => p.Apply(a, data)).Aggregate(new object[0], (acc, current) => { - try - { - return acc.Concat(current.MakeEnumerable()).ToArray(); - } - catch - { - return acc.Concat(new object[] { current }).ToArray(); - } - })); - */ - } - - private string SequenceConditions(List arguments, string @operator) - { - if (arguments.Count() < 2) - throw new Exception("Number of arguments should be 2 or more"); - - var result = ""; - for (int i = 0; i < arguments.Count() - 1; i++) - { - if (!string.IsNullOrWhiteSpace(result)) - result += " and "; - result += $"({arguments[i]} {@operator} {arguments[i + 1]})"; - } - - return result; - } - - private object GetValueByName(object data, string namePath) - { - if (string.IsNullOrEmpty(namePath)) return data; - - if (data == null) throw new ArgumentNullException(nameof(data)); - - string[] names = namePath.Split('.'); - object d = data; - foreach (string name in names) - { - if (d == null) return null; - if (d.GetType().IsArray) - { - d = (d as Array).GetValue(int.Parse(name)); - } - else if (DictionaryType(d) != null) - { - var type = DictionaryType(d); - var prop = type.GetTypeInfo().DeclaredProperties.FirstOrDefault(p => p.Name == "Item"); - d = prop.GetValue(d, new object[] { name }); - } - else if (d is IEnumerable) - { - d = (d as IEnumerable).Skip(int.Parse(name)).First(); - } - else - { - var property = d.GetType().GetTypeInfo().GetDeclaredProperty(name); - if (property == null) throw new Exception(); - d = property.GetValue(d); - } - } - return d; - } - - private Type DictionaryType(object d) - { - return d.GetType().GetTypeInfo().ImplementedInterfaces.FirstOrDefault(t => t.GetTypeInfo().IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>)); - } - - /* - private Func DoubleArgsSatisfy(Func criteria) - { - return (p, args, data) => { - var values = args.Select(a => p.Apply(a, data)) - .Select(a => a == null ? 0d : double.Parse(a.ToString())) - .ToArray(); - - for (int i = 1; i < values.Length; i++) - { - if (!criteria(values[i - 1], values[i])) return false; - } - return true; - }; - } - */ - /* - private static Func ReduceDoubleArgs(double defaultValue, Func reducer) - { - return (p, args, data) => Min2From(args.Select(a => p.Apply(a, data))).Select(a => a == null ? defaultValue : Convert.ToDouble(a)).Aggregate(reducer); - } - - private static IEnumerable Min2From(IEnumerable source) - { - var count = source.Count(); - if (count >= 2) return source; - - return new object[] { null, count == 0 ? null : source.First() }; - } - */ - - private string GetStringValue(JToken arg) - { - if (arg == null) - return null; - - if (arg is JValue value && value.Value is string stringValue) - return stringValue; - - throw new NotSupportedException("Not supported value type"); - } - - private List ConvertArguments(IJsonLogic2HqlConverter converter, JToken[] args, JsonLogic2HqlConverterContext context, bool wrap = false) - { - return args.Select(a => - { - var arg = converter.Convert(a, context); - return wrap - ? $"({arg})" - : arg; - }) - .ToList(); - } - } -} diff --git a/shesha-core/src/Shesha.NHibernate/JsonLogic/IJsonLogic2HqlConverter.cs b/shesha-core/src/Shesha.NHibernate/JsonLogic/IJsonLogic2HqlConverter.cs deleted file mode 100644 index 4436af53ff..0000000000 --- a/shesha-core/src/Shesha.NHibernate/JsonLogic/IJsonLogic2HqlConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace Shesha.JsonLogic -{ - /// - /// Json Logic to HQL converter - /// - public interface IJsonLogic2HqlConverter - { - /// - /// Convert Json Logic to HQL - /// - /// - /// - /// - string Convert(JToken rule, JsonLogic2HqlConverterContext context); - - /// - /// Get props of the Json Logic operation - /// - /// - /// - OperationProps GetOperation(JToken rule); - - /// - /// Resolve variable - /// - /// - /// - /// - string ResolveVariable(string value, JsonLogic2HqlConverterContext context); - } -} diff --git a/shesha-core/src/Shesha.NHibernate/JsonLogic/JsonLogic2HqlConverter.cs b/shesha-core/src/Shesha.NHibernate/JsonLogic/JsonLogic2HqlConverter.cs deleted file mode 100644 index 1f8302aded..0000000000 --- a/shesha-core/src/Shesha.NHibernate/JsonLogic/JsonLogic2HqlConverter.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Linq; -using Abp.Dependency; -using JsonLogic.Net; -using Newtonsoft.Json.Linq; -using Shesha.Utilities; - -namespace Shesha.JsonLogic -{ - /// - /// HQL Json Logic evaluator. Converts JsonLogic expression to hql - /// - public class JsonLogic2HqlConverter: IJsonLogic2HqlConverter, ITransientDependency - { - private HqlOperators operations; - - public JsonLogic2HqlConverter() - { - this.operations = HqlOperators.Default; - } - - public string Convert(JToken rule, JsonLogic2HqlConverterContext context) - { - if (rule is JValue value) return - AdjustType(value.Value, context); - if (rule is JArray array) - return array.Select(r => Convert(r, context)).ToArray().Delimited(","); // todo: check and replace with a delimiter constant + add parameters support if needed - - if (rule is JObject ruleObj) - { - var operationProps = GetOperation(ruleObj); - var jsOperator = operations.GetOperator(operationProps.Name); - return jsOperator(this, operationProps.Arguments, context); - } - - return null; - } - - /// - /// Get operation props - /// - /// - /// - public OperationProps GetOperation(JToken rule) - { - if (rule is JObject ruleObj) - { - var p = ruleObj.Properties().First(); - var operationName = p.Name; - var operationArguments = (p.Value is JArray jArrayArgs) - ? jArrayArgs.ToArray() - : new JToken[] { p.Value }; - return new OperationProps - { - Name = operationName, - Arguments = operationArguments, - }; - } - - return null; - } - - /// inheritedDoc - public string ResolveVariable(string value, JsonLogic2HqlConverterContext context) - { - return context.VariablesResolvers.ContainsKey(value) - ? context.VariablesResolvers[value] - : value; - } - - private string AdjustType(object value, JsonLogic2HqlConverterContext context) - { - if (value == null) - return "null"; - - // todo: add parameters support! replace value with parameter reference and add parameter to the context - - // convert boolean to 1/0 - if (value is bool boolValue) - return boolValue ? "1" : "0"; - - var paramName = context.AddParameter(FixValue(value)); - return $":{paramName}"; - - /* - if (value is string stringValue) - return $"'{stringValue}'"; - //if (value is string stringValue) - // return stringValue; - - return value.IsNumeric() - ? System.Convert.ToDouble(value).ToString() - : value.ToString(); - */ - } - - private static object FixValue(object value) - { - if (value is string stringValue) - { - var guidValue = stringValue.ToGuidOrNull(); - if (guidValue != null) - return guidValue; - } - - return value; - } - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.NHibernate/JsonLogic/JsonLogic2HqlConverterContext.cs b/shesha-core/src/Shesha.NHibernate/JsonLogic/JsonLogic2HqlConverterContext.cs deleted file mode 100644 index 52a0eb04fd..0000000000 --- a/shesha-core/src/Shesha.NHibernate/JsonLogic/JsonLogic2HqlConverterContext.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using Shesha.Metadata; - -namespace Shesha.JsonLogic -{ - /// - /// Json Logic to HQL converter context - /// - public class JsonLogic2HqlConverterContext - { - /// - /// Query parameters prefix - /// - public string ParametersPrefix { get; set; } = "par"; - - /// - /// List of query parameters - /// - public Dictionary FilterParameters { get; set; } = new Dictionary(); - - /// - /// Variable resolvers - /// - public Dictionary VariablesResolvers { get; set; } = new Dictionary(); - - /// - /// Fields metadata dictionary - /// - public Dictionary FieldsMetadata { get; set; } = new Dictionary(); - - /// - /// Constructor - /// - /// - public JsonLogic2HqlConverterContext(string parametersPrefix) - { - ParametersPrefix = parametersPrefix; - } - - /// - /// Default constructor - /// - public JsonLogic2HqlConverterContext() - { - - } - - /// - /// Add new parameter and get it's name - /// - /// - /// - public string AddParameter(object value) - { - var name = $"{ParametersPrefix}{FilterParameters.Count + 1}"; - FilterParameters.Add(name, value); - - return name; - } - } -} diff --git a/shesha-core/src/Shesha.NHibernate/MappingMetadata/MappingMetadataProvider.cs b/shesha-core/src/Shesha.NHibernate/MappingMetadata/MappingMetadataProvider.cs index 036bfd36f4..857d93d0af 100644 --- a/shesha-core/src/Shesha.NHibernate/MappingMetadata/MappingMetadataProvider.cs +++ b/shesha-core/src/Shesha.NHibernate/MappingMetadata/MappingMetadataProvider.cs @@ -1,29 +1,28 @@ using Abp.Dependency; using NHibernate; using NHibernate.Persister.Entity; -using NHibernate.Type; -using Shesha.Services; +using Shesha.Configuration.MappingMetadata; +using Shesha.Extensions; +using Shesha.NHibernate.Session; +using Shesha.NHibernate.Utilites; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; -using Shesha.Configuration.MappingMetadata; -using System.Reflection; -using NHibernate.Mapping; -using NUglify; -using Shesha.NHibernate.Utilites; namespace Shesha.MappingMetadata { public class MappingMetadataProvider: IMappingMetadataProvider, ITransientDependency { - private readonly ISessionFactory _sessionFactory; + private readonly INhCurrentSessionContext _currentSessionContext; - public MappingMetadataProvider(ISessionFactory sessionFactory) + public MappingMetadataProvider(ISessionFactory sessionFactory, INhCurrentSessionContext currentSessionContext) { _sessionFactory = sessionFactory; + _currentSessionContext = currentSessionContext; } public EntityMappingMetadata GetEntityMappingMetadata(Type entityType) @@ -43,23 +42,26 @@ public EntityMappingMetadata GetEntityMappingMetadata(Type entityType) return mappingMetadata; } - public PropertyMappingMetadata GetPropertyMappingMetadata(Type entityType, string propertyName) + public PropertyMappingMetadata? GetPropertyMappingMetadata(Type entityType, string propertyName) { - var persister = _sessionFactory.GetClassMetadata(entityType) as SingleTableEntityPersister; - - var propertyMetadata = new PropertyMappingMetadata - { - ColumnNames = persister?.GetPropertyColumnNames(propertyName), - TableName = persister?.GetPropertyTableName(propertyName), - }; - return propertyMetadata; + if (_sessionFactory.GetClassMetadata(entityType) is SingleTableEntityPersister persister) + { + var propertyMetadata = new PropertyMappingMetadata + { + ColumnNames = persister.GetPropertyColumnNames(propertyName), + TableName = persister.GetPropertyTableName(propertyName), + }; + return propertyMetadata; + } + else + return null; } public async Task UpdateClassNamesAsync(Type entityType, List properties, string oldValue, string newValue, bool replace) { - var session = _sessionFactory.GetCurrentSession(); + var session = _currentSessionContext.Session; - var propsMetadata = properties.Select(x => GetPropertyMappingMetadata(entityType, x.Name)).ToList(); + var propsMetadata = properties.Select(x => GetPropertyMappingMetadata(entityType, x.Name)).WhereNotNull().ToList(); if (replace) { diff --git a/shesha-core/src/Shesha.NHibernate/Migrations/M20250220151500.cs b/shesha-core/src/Shesha.NHibernate/Migrations/M20250220151500.cs new file mode 100644 index 0000000000..9e7f881eba --- /dev/null +++ b/shesha-core/src/Shesha.NHibernate/Migrations/M20250220151500.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using Shesha.FluentMigrator; +using System; + +namespace Shesha.Migrations +{ + [Migration(20250220151500)] + public class M20250220151500 : OneWayMigration + { + public override void Up() + { + Alter.Column("RecipientId").OnTable("Frwk_OtpAuditItems").AsString(100).Nullable(); + } + } +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Attributes/LazyAttribute.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Attributes/NhLazyLoadAttribute.cs similarity index 86% rename from shesha-core/src/Shesha.NHibernate/NHibernate/Attributes/LazyAttribute.cs rename to shesha-core/src/Shesha.NHibernate/NHibernate/Attributes/NhLazyLoadAttribute.cs index 3e7a559008..6aabf255a4 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Attributes/LazyAttribute.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Attributes/NhLazyLoadAttribute.cs @@ -1,13 +1,14 @@ using System; using NHibernate.Cfg.MappingSchema; using NHibernate.Mapping.ByCode; +using Shesha.Domain.Attributes; namespace Shesha.NHibernate.Attributes { [AttributeUsage(AttributeTargets.Property)] - public class LazyAttribute : Attribute + public class NhLazyLoadAttribute : LazyLoadAttribute { - public LazyAttribute() + public NhLazyLoadAttribute() { // NoProxy is used by default, see http://ayende.com/blog/4378/nhibernate-new-feature-no-proxy-associations Laziness = HbmLaziness.NoProxy; diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Configuration/IShaNHibernateModuleConfiguration.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Configuration/IShaNHibernateModuleConfiguration.cs index d42b5db32b..84b390b1e4 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Configuration/IShaNHibernateModuleConfiguration.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Configuration/IShaNHibernateModuleConfiguration.cs @@ -2,8 +2,6 @@ using NHibernate; using NHibernate.Dialect; using NHibernate.Driver; -using Shesha.Configuration; -using Shesha.FluentMigrator; using System; namespace Shesha.NHibernate.Configuration @@ -33,17 +31,17 @@ public interface IShaNHibernateModuleConfiguration /// /// Custom dialect /// - Type CustomDialect { get; } + Type? CustomDialect { get; } /// /// Custom driver /// - Type CustomDriver { get; } + Type? CustomDriver { get; } /// /// Session factory builder /// - Func SessionFactoryBuilder { get; } + Func? SessionFactoryBuilder { get; } /// /// Set MS Sql as a DBMS type diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Configuration/ShaNHibernateModuleConfiguration.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Configuration/ShaNHibernateModuleConfiguration.cs index 1bee8de421..633d34b8e4 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Configuration/ShaNHibernateModuleConfiguration.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Configuration/ShaNHibernateModuleConfiguration.cs @@ -7,7 +7,7 @@ using NHibernate.Driver; using Shesha.Configuration; using Shesha.Exceptions; -using Shesha.FluentMigrator; +using Shesha.Reflection; using System; namespace Shesha.NHibernate.Configuration @@ -17,9 +17,9 @@ internal class ShaNHibernateModuleConfiguration : IShaNHibernateModuleConfigurat private readonly IWebHostEnvironment _env; private readonly IAbpStartupConfiguration _startupConfig; - public Type CustomDialect { get; private set; } - public Type CustomDriver { get; private set; } - public Func SessionFactoryBuilder { get; private set; } + public Type? CustomDialect { get; private set; } + public Type? CustomDriver { get; private set; } + public Func? SessionFactoryBuilder { get; private set; } public ShaNHibernateModuleConfiguration(IWebHostEnvironment env, IAbpStartupConfiguration startupConfig) { @@ -32,13 +32,13 @@ public ShaNHibernateModuleConfiguration(IWebHostEnvironment env, IAbpStartupConf public global::NHibernate.Cfg.Configuration NhConfiguration { get; } private DbmsType _databaseType = DbmsType.NotSpecified; - private string _connectionString; + private string? _connectionString; /// inheritedDoc public DbmsType DatabaseType => _databaseType; /// inheritedDoc - public string ConnectionString => _connectionString; + public string ConnectionString => _connectionString.NotNullOrWhiteSpace("Connection string is unavailable"); private void SetConnectionOptions(DbmsType dbmsType, string connectionString) { @@ -79,7 +79,7 @@ public void UseMsSql(Func connectionStringGetter) /// inheritedDoc public void UseMsSql() { - UseMsSql(c => c.GetConnectionString(SheshaConsts.ConnectionStringName)); + UseMsSql(c => c.GetRequiredConnectionString(SheshaConsts.ConnectionStringName)); } #endregion @@ -93,7 +93,7 @@ public void UsePostgreSql(string connectionString) } /// inheritedDoc - public void UsePostgreSql(Func connectionStringGetter = null) + public void UsePostgreSql(Func connectionStringGetter) { var connectionString = GetConnectionStringFromEnvironment(connectionStringGetter); UsePostgreSql(connectionString); @@ -102,7 +102,7 @@ public void UsePostgreSql(Func connectionStringGette /// inheritedDoc public void UsePostgreSql() { - UsePostgreSql(c => c.GetConnectionString(SheshaConsts.ConnectionStringName)); + UsePostgreSql(c => c.GetRequiredConnectionString(SheshaConsts.ConnectionStringName)); } #endregion diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Connection/SheshaNhConnectionProvider.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Connection/SheshaNhConnectionProvider.cs index 70078cc590..2b74663fe9 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Connection/SheshaNhConnectionProvider.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Connection/SheshaNhConnectionProvider.cs @@ -1,5 +1,5 @@ -using Abp.Dependency; -using NHibernate.Connection; +using NHibernate.Connection; +using Shesha.Services; using System; using System.Data.Common; @@ -7,18 +7,13 @@ namespace Shesha.NHibernate.Connection { public class SheshaNhConnectionProvider: DriverConnectionProvider { - private readonly IIocManager _iocManager; - private readonly IDbConnectionFactory _connectionFactory; - public SheshaNhConnectionProvider(IIocManager iocManager) + private readonly IDbConnectionFactory? _connectionFactory; + public SheshaNhConnectionProvider() { - _iocManager = iocManager; - _connectionFactory = iocManager.IsRegistered() - ? iocManager.Resolve() + _connectionFactory = StaticContext.IocManager.IsRegistered() + ? StaticContext.IocManager.Resolve() : null; } - public SheshaNhConnectionProvider(): this(IocManager.Instance) - { - } public override DbConnection GetConnection(string connectionString) { diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/DbPerTenantConnectionStringResolver.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/DbPerTenantConnectionStringResolver.cs index cc0bf86b83..1276983de2 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/DbPerTenantConnectionStringResolver.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/DbPerTenantConnectionStringResolver.cs @@ -60,7 +60,7 @@ public virtual string GetNameOrConnectionString(DbPerTenantConnectionStringResol } var tenantCacheItem = _tenantCache.Get(args.TenantId.Value); - if (tenantCacheItem.ConnectionString.IsNullOrEmpty()) + if (string.IsNullOrWhiteSpace(tenantCacheItem.ConnectionString)) { //Tenant has not dedicated database return base.GetNameOrConnectionString(args); diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/EntityHistoryExtension.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/EntityHistoryExtension.cs index a40e2e25ac..f01868da14 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/EntityHistoryExtension.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/EntityHistoryExtension.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq.Expressions; -using Abp.Dependency; -using Abp.Domain.Uow; +using Abp.Domain.Uow; using Abp.EntityHistory; using Abp.Events.Bus.Entities; using Abp.Extensions; @@ -16,6 +13,8 @@ using Shesha.NHibernate.UoW; using Shesha.Reflection; using Shesha.Services; +using System; +using System.Linq.Expressions; namespace Shesha.NHibernate.EntityHistory { @@ -27,7 +26,7 @@ public static class EntityHistoryExtension /// Changed entity /// Description of changes /// Changed property - public static void AddPropertyChangeDescription(this TModel entity, string description, Expression> property) + public static void AddPropertyChangeDescription(this TModel entity, string description, Expression> property) where TModel: notnull { try { @@ -46,7 +45,7 @@ public static void AddPropertyChangeDescription(this TModel enti /// Changed entity /// Comment of changes /// Changed property - public static void AddPropertyChangeComment(this TModel entity, string description, Expression> property) + public static void AddPropertyChangeComment(this TModel entity, string description, Expression> property) where TModel : notnull { try { @@ -147,21 +146,24 @@ public static void AddHistoryEvent(this object entity, string eventType, string { e.LogError(); } - } + } - public static void AddHistoryEvent(this object entity, string eventType, string eventName, string description, string propertyName) + public static void AddHistoryEvent(this object entity, string? eventType, string? eventName, string description, string? propertyName) { try { var uow = StaticContext.IocManager.Resolve().Current; var entityHistoryHelper = (uow as NhUnitOfWork)?.EntityHistoryHelper; - var session = StaticContext.IocManager.Resolve().GetCurrentSession(); - + var sessionFactory = StaticContext.IocManager.Resolve(); + + using var sessionContext = StaticContext.IocManager.Resolve(); + var session = sessionContext.Session; + var abpSession = StaticContext.IocManager.Resolve(); var id = ""; - var entry = session.GetEntry(entity); + var entry = session.GetEntryOrNull(entity); id = entry != null ? entry.Id.ToString() : entity.ToString(); @@ -193,7 +195,6 @@ public static void AddHistoryEvent(this object entity, string eventType, string { e.LogError(); } - } + } } - } \ No newline at end of file diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/EntityHistoryHelper.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/EntityHistoryHelper.cs index 91172036e1..2d62031860 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/EntityHistoryHelper.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/EntityHistoryHelper.cs @@ -10,14 +10,10 @@ using Abp.Json; using Abp.Reflection; using Abp.Runtime.Session; +using Abp.Threading; using Abp.Timing; -using Microsoft.AspNetCore.Mvc.Infrastructure; using NHibernate; -using NHibernate.Engine; -using NHibernate.Intercept; using NHibernate.Proxy; -using Nito.AsyncEx.Synchronous; -using Shesha.Configuration.Runtime; using Shesha.Domain; using Shesha.Domain.Attributes; using Shesha.DynamicEntities; @@ -30,7 +26,6 @@ using Shesha.Services; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reflection; using SessionExtensions = Shesha.NHibernate.Session.SessionExtensions; @@ -48,8 +43,6 @@ public class EntityHistoryHelper : EntityHistoryHelperBase, IEntityHistoryHelper private readonly IRepository _historyEventRepository; private readonly IIocResolver _iocResolver; private readonly IModelConfigurationManager _modelConfigurationManager; - //private readonly ISession _session; - //private readonly IAbpSession _abpSession; //[DebuggerStepThrough] @@ -61,9 +54,7 @@ public EntityHistoryHelper( NHibernateEntityHistoryStore historyStore, IRepository historyEventRepository, IModelConfigurationManager modelConfigurationManager, - IIocResolver iocResolver/*, - ISession session, - IAbpSession abpSession*/ + IIocResolver iocResolver ) : base(configuration, unitOfWorkManager) { @@ -77,16 +68,13 @@ IAbpSession abpSession*/ _historyEventRepository = historyEventRepository; _iocResolver = iocResolver; _modelConfigurationManager = modelConfigurationManager; - //_session = session; - //_abpSession = abpSession; } public Guid Id { get; set; } - public EntityChangeSet EntityChangeSet { get; set; } public List EntityChanges { get; set; } public List EntityHistoryEvents { get; set; } - public ISession Session => (UnitOfWorkManager.Current as NhUnitOfWork)?.GetSession(); + public ISession? Session => (UnitOfWorkManager.Current as NhUnitOfWork)?.GetSessionOrNull(); public virtual void AddEntityChange(object entity) { @@ -104,11 +92,14 @@ public virtual void AddEntityChange(object entity) } } - public virtual EntityChange CreateEntityChange(object entity) + public virtual EntityChange? CreateEntityChange(object entity) { if (!IsEntityHistoryEnabled) return null; if (IsChangesEntity(entity)) return null; + if (Session == null) + throw new SessionException("Session is not available"); + var typeOfEntity = entity.GetType().StripCastleProxyType(); if (!IsTypeOfEntity(typeOfEntity)) @@ -116,9 +107,7 @@ public virtual EntityChange CreateEntityChange(object entity) return null; } - var entityConfig = _modelConfigurationManager - .GetModelConfigurationOrNullAsync(typeOfEntity.Namespace, typeOfEntity.Name) - .WaitAndUnwrapException(); + var entityConfig = AsyncHelper.RunSync(async () => await _modelConfigurationManager.GetModelConfigurationOrNullAsync(typeOfEntity.Namespace, typeOfEntity.Name)); var isTracked = IsTypeOfTrackedEntity(typeOfEntity); if (isTracked != null && !isTracked.Value) return null; @@ -140,15 +129,19 @@ public virtual EntityChange CreateEntityChange(object entity) } var entityTypeFullName = typeOfEntity.FullName; - EntityEntry entityEntry; + var entityEntry = Session.GetEntryOrNull(entity); + if (entityEntry == null) + return null; - if ((entityEntry = Session?.GetEntry(entity, false)) == null) return null; var id = entityEntry.Id; EntityChangeType changeType; if (Session.IsEntityDeleted(entity)) changeType = EntityChangeType.Deleted; - else if (entityEntry.LoadedState == null) changeType = EntityChangeType.Created; - else changeType = EntityChangeType.Updated; + else + if (entityEntry.LoadedState == null) + changeType = EntityChangeType.Created; + else + changeType = EntityChangeType.Updated; var className = NHibernateProxyHelper.GuessClass(entity).FullName; var sessionImpl = Session.GetSessionImplementation(); @@ -165,7 +158,7 @@ public virtual EntityChange CreateEntityChange(object entity) { if (_iocResolver.Resolve(creatorType) is IEntityHistoryCreator creator && creator.TypeAllowed(entity.GetType())) { - return creator.GetEntityChange(entity, AbpSession, persister.PropertyNames, entityEntry.LoadedState, currentState, dirtyP); + return creator.GetEntityChange(entity, AbpSession, persister.PropertyNames, entityEntry.LoadedState ?? [], currentState, dirtyP); } } @@ -187,8 +180,9 @@ public virtual EntityChange CreateEntityChange(object entity) var propertyChanges = new List(); if (changeType != EntityChangeType.Created) { - propertyChanges.AddRange(GetPropertyChanges((isAudited ?? false) || (isTracked ?? false), entityChange, - typeOfEntity, entity, entityConfig, dirtyProps)); + entityConfig.NotNull("EntityConfig must not be null"); + propertyChanges.AddRange(GetPropertyChanges((isAudited ?? false) || (isTracked ?? false), entityChange, typeOfEntity, entity, entityConfig, dirtyProps)); + if (propertyChanges.Count == 0 && //changeType != EntityChangeType.Created && EntityHistoryEvents.All(x => x.EntityChange != entityChange)) { @@ -255,17 +249,17 @@ private ICollection GetPropertyChanges(bool fullAudited, E if (propType.GetInterfaces().Contains(typeof(IEntity)) || propType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntity<>))) { - EntityPropertyChange propchange = null; + EntityPropertyChange? propchange = null; // skip creating property changes if (propInfo.GetCustomAttribute()?.SaveFullInfo ?? true) { propchange = CreateEntityPropertyChange( property.OldValue != null - ? propType.GetProperty("Id")?.GetValue(property.OldValue).ToString() ?? + ? propType.GetProperty("Id")?.GetValue(property.OldValue)?.ToString() ?? property.OldValue.ToString() : "", property.NewValue != null - ? propType.GetProperty("Id")?.GetValue(property.NewValue).ToString() ?? + ? propType.GetProperty("Id")?.GetValue(property.NewValue)?.ToString() ?? property.NewValue.ToString() : "", propInfo @@ -305,7 +299,7 @@ private ICollection GetPropertyChanges(bool fullAudited, E var oldValue = property.OldValue; var newValue = property.NewValue; - EntityPropertyChange propchange = null; + EntityPropertyChange? propchange = null; // skip creating property changes if (propInfo.GetCustomAttribute()?.SaveFullInfo ?? true) { @@ -335,7 +329,7 @@ private ICollection GetPropertyChanges(bool fullAudited, E var attr = propInfo.GetCustomAttribute(); if (attr != null) { - var description = (bool)newValue ? attr.TrueText : attr.FalseText; + var description = newValue != null && (bool)newValue ? attr.TrueText : attr.FalseText; // Add extended (friendly) description for Reference types EntityHistoryEvents.Add(new EntityHistoryEvent() { @@ -366,7 +360,7 @@ private ICollection GetPropertyChanges(bool fullAudited, E : Convert.ToInt64(value); } - private bool AddAuditedAsEvent(PropertyInfo propInfo, SessionExtensions.DirtyPropertyInfo property, EntityPropertyChange propertyChange, EntityChange entityChange, object entity) + private bool AddAuditedAsEvent(PropertyInfo propInfo, SessionExtensions.DirtyPropertyInfo property, EntityPropertyChange? propertyChange, EntityChange entityChange, object entity) { var auditedAsEvent = propInfo.GetCustomAttribute(); if (auditedAsEvent != null) @@ -402,7 +396,7 @@ private bool AddAuditedAsEvent(PropertyInfo propInfo, SessionExtensions.DirtyPro return false; } - public bool AddAuditedAsManyToMany(object entity, PropertyInfo propInfo, object oldValue, object newValue) + public bool AddAuditedAsManyToMany(object entity, PropertyInfo propInfo, object? oldValue, object? newValue) { // resolve here because EntityHistoryHelper can be resolved befeore IAbpSession registered var abpSession = _iocResolver.Resolve(); @@ -412,9 +406,7 @@ public bool AddAuditedAsManyToMany(object entity, PropertyInfo propInfo, object ? propInfo.PropertyType.GetGenericArguments()[0] : propInfo.PropertyType; - var entityConfig = _modelConfigurationManager - .GetModelConfigurationOrNullAsync(entityType.Namespace, entityType.Name) - .WaitAndUnwrapException(); + var entityConfig = AsyncHelper.RunSync(async () => await _modelConfigurationManager.GetModelConfigurationAsync(entityType.Namespace, entityType.Name)); var configuredAudit = (entityConfig.Properties.FirstOrDefault(x => x.Name.ToCamelCase() == propInfo.Name.ToCamelCase())?.Audited ?? false); var audited = propInfo.GetCustomAttribute(); @@ -453,7 +445,7 @@ public bool AddAuditedAsManyToMany(object entity, PropertyInfo propInfo, object if (eventInfo == null) return false; - var id = entity.GetId().ToString(); + var id = entity.GetId().NotNull().ToString(); var entityChange = new EntityChange { @@ -557,7 +549,7 @@ public bool AddAuditedAsManyToMany(object entity, PropertyInfo propInfo, object return false; } - private EntityPropertyChange CreateEntityPropertyChange(object oldValue, object newValue, PropertyInfo propertyInfo) + private EntityPropertyChange CreateEntityPropertyChange(object? oldValue, object? newValue, PropertyInfo propertyInfo) { var proprtyName = propertyInfo.Name.TruncateWithPostfix(EntityPropertyChange.MaxPropertyNameLength); var propertyChange = new EntityPropertyChange() @@ -572,7 +564,7 @@ private EntityPropertyChange CreateEntityPropertyChange(object oldValue, object return propertyChange; } - public virtual EntityChangeSet CreateEntityChangeSet() + public virtual EntityChangeSet? CreateEntityChangeSet() { if (!IsEntityHistoryEnabled) return null; @@ -591,14 +583,15 @@ public virtual EntityChangeSet CreateEntityChangeSet() }; // Add description for property change - foreach (var entityHistoryEvent in EntityHistoryEvents.Where(x => - !string.IsNullOrEmpty(x.PropertyName) - && x.EntityChange != null - && x.EventType != EntityHistoryCommonEventTypes.PROPERTY_CHANGE_AS_EVENT)) + var entityHistoryEvents = EntityHistoryEvents.Where(x => !string.IsNullOrEmpty(x.PropertyName) && + x.EntityChange != null && + x.EventType != EntityHistoryCommonEventTypes.PROPERTY_CHANGE_AS_EVENT) + .ToList(); + foreach (var entityHistoryEvent in entityHistoryEvents) { var prop = EntityChanges .FirstOrDefault(x => - x.EntityId == entityHistoryEvent.EntityChange.EntityId + x.EntityId == entityHistoryEvent.EntityChange.NotNull().EntityId && x.EntityTypeFullName == entityHistoryEvent.EntityChange.EntityTypeFullName && x.PropertyChanges != null) ?.PropertyChanges.FirstOrDefault(x => x.PropertyName == entityHistoryEvent.PropertyName); @@ -607,7 +600,8 @@ public virtual EntityChangeSet CreateEntityChangeSet() { entityHistoryEvent.EntityPropertyChange = prop; - EntityChanges.Remove(entityHistoryEvent.EntityChange); + if (entityHistoryEvent.EntityChange != null) + EntityChanges.Remove(entityHistoryEvent.EntityChange); entityHistoryEvent.EntityChange = null; } } @@ -687,4 +681,4 @@ private bool IsChangesEntity(object entity) } } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/IEntityHistoryHelper.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/IEntityHistoryHelper.cs index 5afbf9520d..5b9f74865d 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/IEntityHistoryHelper.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/IEntityHistoryHelper.cs @@ -1,17 +1,11 @@ -using System.Collections.Generic; +using Abp.EntityHistory; +using System.Collections.Generic; using System.Reflection; -using Abp.EntityHistory; -using Shesha.Domain; namespace Shesha.NHibernate.EntityHistory { public interface IEntityHistoryHelper { - /// - /// Set of entity changes - /// - EntityChangeSet EntityChangeSet { get; set; } - /// /// List of entity changes /// @@ -32,20 +26,20 @@ public interface IEntityHistoryHelper /// Changed collection property /// Old collection /// New collection - bool AddAuditedAsManyToMany(object entity, PropertyInfo propInfo, object oldValue, object newValue); + bool AddAuditedAsManyToMany(object entity, PropertyInfo propInfo, object? oldValue, object? newValue); /// /// Create EntityChange item for the Entity /// /// Entity /// EntityChange object - EntityChange CreateEntityChange(object entity); + EntityChange? CreateEntityChange(object entity); /// /// Create entity changes set /// /// - EntityChangeSet CreateEntityChangeSet(); + EntityChangeSet? CreateEntityChangeSet(); //Task SaveAsync(DbContext context, EntityChangeSet changeSet); diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/NHibernateEntityHistoryStore.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/NHibernateEntityHistoryStore.cs index aad503731e..a977134e2d 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/NHibernateEntityHistoryStore.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/EntityHistory/NHibernateEntityHistoryStore.cs @@ -40,18 +40,24 @@ public virtual async Task SaveAsync(EntityChangeSet changeSet) obj.EntityChanges = null; var csId = await _changeSetRepository.InsertAndGetIdAsync(obj); - foreach (var entityChange in entityChanges) + if (entityChanges != null) { - var propChanges = entityChange.PropertyChanges; - entityChange.PropertyChanges = null; - entityChange.EntityChangeSetId = csId; - var cId = await _changesRepository.InsertAndGetIdAsync(entityChange); - foreach (var propChange in propChanges) + foreach (var entityChange in entityChanges) { - propChange.EntityChangeId = cId; - var pId = await _propChangesRepository.InsertAndGetIdAsync(propChange); - } - } + var propChanges = entityChange.PropertyChanges; + entityChange.PropertyChanges = null; + entityChange.EntityChangeSetId = csId; + var cId = await _changesRepository.InsertAndGetIdAsync(entityChange); + if (propChanges != null) + { + foreach (var propChange in propChanges) + { + propChange.EntityChangeId = cId; + var pId = await _propChangesRepository.InsertAndGetIdAsync(propChange); + } + } + } + } } } catch (Exception e) @@ -70,22 +76,29 @@ public virtual void Save(EntityChangeSet changeSet) obj.EntityChanges = null; var csId = _changeSetRepository.InsertAndGetId(obj); - foreach (var entityChange in entityChanges) + if (entityChanges != null) { - var propChanges = entityChange.PropertyChanges; - entityChange.PropertyChanges = null; - entityChange.EntityChangeSetId = csId; - if (string.IsNullOrEmpty(entityChange.EntityId)) - { - // update id for inserted entity - entityChange.EntityId = entityChange.EntityEntry.GetType().GetProperty(nameof(IEntity.Id))?.GetValue(entityChange.EntityEntry)?.ToString(); - } - var cId = _changesRepository.InsertAndGetId(entityChange); - foreach (var propChange in propChanges) + foreach (var entityChange in entityChanges) { - propChange.EntityChangeId = cId; - var pId = _propChangesRepository.InsertAndGetId(propChange); - } + var propChanges = entityChange.PropertyChanges; + entityChange.PropertyChanges = null; + entityChange.EntityChangeSetId = csId; + if (string.IsNullOrEmpty(entityChange.EntityId)) + { + // update id for inserted entity + entityChange.EntityId = entityChange.EntityEntry.GetType().GetProperty(nameof(IEntity.Id))?.GetValue(entityChange.EntityEntry)?.ToString(); + } + var cId = _changesRepository.InsertAndGetId(entityChange); + + if (propChanges != null) + { + foreach (var propChange in propChanges) + { + propChange.EntityChangeId = cId; + var pId = _propChangesRepository.InsertAndGetId(propChange); + } + } + } } } catch (Exception e) diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Generators/GuidAssignedGeneratorDef.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Generators/GuidAssignedGeneratorDef.cs index 970fd0cbaf..2374749492 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Generators/GuidAssignedGeneratorDef.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Generators/GuidAssignedGeneratorDef.cs @@ -1,13 +1,14 @@ using System; using NHibernate.Mapping.ByCode; +using Shesha.Reflection; namespace Shesha.NHibernate.Generators { public class GuidAssignedGeneratorDef : IGeneratorDef { - public string Class => typeof(GuidAssignedGenerator).AssemblyQualifiedName; + public string Class => typeof(GuidAssignedGenerator).AssemblyQualifiedName.NotNullOrWhiteSpace(); - public object Params => null; + public object? Params => null; public Type DefaultReturnType => typeof(Guid); diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Interceptors/SheshaNHibernateInterceptor.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Interceptors/SheshaNHibernateInterceptor.cs index 7513e0749e..1deb3d745a 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Interceptors/SheshaNHibernateInterceptor.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Interceptors/SheshaNHibernateInterceptor.cs @@ -15,6 +15,7 @@ using NHibernate.SqlCommand; using NHibernate.Type; using Shesha.NHibernate.UoW; +using Shesha.Reflection; using System; using System.Collections.Generic; using System.Linq; @@ -32,50 +33,43 @@ public class SheshaNHibernateInterceptor : EmptyInterceptor private readonly Lazy _eventBus; public ILogger Logger { get; set; } = new NullLogger(); - public EntityHistory.IEntityHistoryHelper EntityHistoryHelper => (_iocManager.Resolve().Current as NhUnitOfWork)?.EntityHistoryHelper as EntityHistory.IEntityHistoryHelper; - public ISession Session => (_iocManager.Resolve().Current as NhUnitOfWork)?.GetSession(true); + public EntityHistory.IEntityHistoryHelper? EntityHistoryHelper => (_iocManager.Resolve().Current as NhUnitOfWork)?.EntityHistoryHelper as EntityHistory.IEntityHistoryHelper; + public ISession? Session => (_iocManager.Resolve().Current as NhUnitOfWork)?.GetSessionOrNull(true); public SheshaNHibernateInterceptor(IIocManager iocManager) { - try - { - _iocManager = iocManager; - - _abpSession = - new Lazy( - () => _iocManager.IsRegistered(typeof(IAbpSession)) - ? _iocManager.Resolve() - : NullAbpSession.Instance, - isThreadSafe: true - ); - _guidGenerator = - new Lazy( - () => _iocManager.IsRegistered(typeof(IGuidGenerator)) - ? _iocManager.Resolve() - : SequentialGuidGenerator.Instance, - isThreadSafe: true - ); - - _eventBus = - new Lazy( - () => _iocManager.IsRegistered(typeof(IEventBus)) - ? _iocManager.Resolve() - : NullEventBus.Instance, - isThreadSafe: true + EntityChangeEventHelper = default!; + _iocManager = iocManager; + + _abpSession = + new Lazy( + () => _iocManager.IsRegistered(typeof(IAbpSession)) + ? _iocManager.Resolve() + : NullAbpSession.Instance, + isThreadSafe: true ); - } - catch (Exception e) - { - Logger.Error(e.Message, e); - } + _guidGenerator = + new Lazy( + () => _iocManager.IsRegistered(typeof(IGuidGenerator)) + ? _iocManager.Resolve() + : SequentialGuidGenerator.Instance, + isThreadSafe: true + ); + + _eventBus = + new Lazy( + () => _iocManager.IsRegistered(typeof(IEventBus)) + ? _iocManager.Resolve() + : NullEventBus.Instance, + isThreadSafe: true + ); } public override bool OnSave(object entity, object id, object[] state, string[] propertyNames, IType[] types) { //Set Id for Guids - if (entity is IEntity) + if (entity is IEntity guidEntity) { - var guidEntity = entity as IEntity; if (guidEntity.IsTransient()) { guidEntity.Id = _guidGenerator.Value.Create(); @@ -83,25 +77,25 @@ public override bool OnSave(object entity, object id, object[] state, string[] p } //Set CreationTime for new entity - if (entity is IHasCreationTime) + if (entity is IHasCreationTime hasCreationTime) { for (var i = 0; i < propertyNames.Length; i++) { if (propertyNames[i] == "CreationTime") { - state[i] = (entity as IHasCreationTime).CreationTime = Clock.Now; + state[i] = hasCreationTime.CreationTime = Clock.Now; } } } //Set CreatorUserId for new entity - if (entity is ICreationAudited && _abpSession.Value.UserId.HasValue) + if (entity is ICreationAudited creationAudited && _abpSession.Value.UserId.HasValue) { for (var i = 0; i < propertyNames.Length; i++) { if (propertyNames[i] == "CreatorUserId") { - state[i] = (entity as ICreationAudited).CreatorUserId = _abpSession.Value.UserId; + state[i] = creationAudited.CreatorUserId = _abpSession.Value.UserId; } } } @@ -120,25 +114,25 @@ public override bool OnFlushDirty(object entity, object id, object[] currentStat var updated = false; //Set modification audits - if (entity is IHasModificationTime) + if (entity is IHasModificationTime hasModificationTime) { for (var i = 0; i < propertyNames.Length; i++) { if (propertyNames[i] == "LastModificationTime") { - currentState[i] = (entity as IHasModificationTime).LastModificationTime = Clock.Now; + currentState[i] = hasModificationTime.LastModificationTime = Clock.Now; updated = true; } } } - if (entity is IModificationAudited && _abpSession.Value.UserId.HasValue) + if (entity is IModificationAudited modificationAudited && _abpSession.Value.UserId.HasValue) { for (var i = 0; i < propertyNames.Length; i++) { if (propertyNames[i] == "LastModifierUserId") { - currentState[i] = (entity as IModificationAudited).LastModifierUserId = _abpSession.Value.UserId; + currentState[i] = modificationAudited.LastModifierUserId = _abpSession.Value.UserId; updated = true; } } @@ -160,26 +154,26 @@ public override bool OnFlushDirty(object entity, object id, object[] currentStat if (!previousIsDeleted) { //set DeletionTime - if (entity is IHasDeletionTime) + if (entity is IHasDeletionTime hasDeletionTime) { for (var i = 0; i < propertyNames.Length; i++) { if (propertyNames[i] == "DeletionTime") { - currentState[i] = (entity as IHasDeletionTime).DeletionTime = Clock.Now; + currentState[i] = hasDeletionTime.DeletionTime = Clock.Now; updated = true; } } } //set DeleterUserId - if (entity is IDeletionAudited && _abpSession.Value.UserId.HasValue) + if (entity is IDeletionAudited deletionAudited && _abpSession.Value.UserId.HasValue) { for (var i = 0; i < propertyNames.Length; i++) { if (propertyNames[i] == "DeleterUserId") { - currentState[i] = (entity as IDeletionAudited).DeleterUserId = _abpSession.Value.UserId; + currentState[i] = deletionAudited.DeleterUserId = _abpSession.Value.UserId; updated = true; } } @@ -226,7 +220,7 @@ public override void OnCollectionRecreate(object collection, object key) if (collection is IPersistentCollection map) { // find exact property by loop because map.Role is empty here - PropertyInfo property = null; + PropertyInfo? property = null; var props = map.Owner.GetType().GetProperties(); foreach (var prop in props) { @@ -256,7 +250,7 @@ public override void OnCollectionRemove(object collection, object key) if (collection is IPersistentCollection map) { var propertyName = map.Role.Split('.').Last(); - var property = map.Owner.GetType().GetProperty(propertyName); + var property = map.Owner.GetType().GetRequiredProperty(propertyName); var newValue = property.GetValue(map.Owner, null); EntityHistoryHelper?.AddAuditedAsManyToMany(map.Owner, property, collection, newValue); } @@ -280,39 +274,9 @@ public override void OnCollectionUpdate(object collection, object key) if (collection is IPersistentCollection map) { var propertyName = map.Role.Split('.').Last(); - var property = map.Owner.GetType().GetProperty(propertyName); + var property = map.Owner.GetType().GetRequiredProperty(propertyName); var newValue = property.GetValue(map.Owner, null); EntityHistoryHelper?.AddAuditedAsManyToMany(map.Owner, property, map.StoredSnapshot, newValue); - - /* AS: Experiments for using one table for linking two entities with several list properties with the same entity types */ - /*var (added, removed) = IEnumerableExtensions.GetListNewAndRemoved(map.StoredSnapshot, newValue); - if (added.Any()) - { - foreach (var item in added) - { - var parentId = map.Owner.GetType().GetProperty("Id").GetValue(map.Owner); - var childId = item.GetType().GetProperty("Id").GetValue(item); - - var instance = Activator.CreateInstance("Boxfusion.SheshaFunctionalTests.Common.Application", "Boxfusion.SheshaFunctionalTests.Common.Application.Services.OrganisationTestDirectPersons")? - .Unwrap(); - var objType = instance.GetType(); - objType.GetProperty("OrganisationTestId").SetValue(instance, parentId); - objType.GetProperty("PersonId").SetValue(instance, childId); - objType.GetProperty("Test").SetValue(instance, propertyName); - - Session.Save(instance); - } - }*/ - /*foreach (var item in removed) - { - var childId = item.GetType().GetProperty("Id").GetValue(item); - - var instance = Activator.CreateInstance("Boxfusion.SheshaFunctionalTests.Common.Application", "oxfusion.SheshaFunctionalTests.Common.Application.Services.OrganisationTestDirectPersons"); - var objType = instance.GetType(); - objType.GetProperty("OrganisationTestId").SetValue(instance, parentId); - objType.GetProperty("PersonId").SetValue(instance, childId); - objType.GetProperty("Test").SetValue(instance, propertyName); - }*/ } } catch (HibernateException e) @@ -407,18 +371,14 @@ private static void NormalizeDateTimePropertiesForEntity(object entity, object[] } } - private static void NormalizeDateTimePropertiesForComponentType(object componentObject, IType type) + private static void NormalizeDateTimePropertiesForComponentType(object? componentObject, IType type) { if (componentObject == null) - { return; - } var componentType = type as ComponentType; if (componentType == null) - { return; - } for (int i = 0; i < componentType.PropertyNames.Length; i++) { diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/Conventions.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/Conventions.cs index 774547d363..0b6eaaf1af 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/Conventions.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/Conventions.cs @@ -46,10 +46,10 @@ namespace Shesha.NHibernate.Maps /// public class Conventions { - private LazyRelation _defaultLazyRelation; + private LazyRelation? _defaultLazyRelation; private readonly INameGenerator _nameGenerator; - public Conventions(INameGenerator nameGenerator, Func> idMapper = null) + public Conventions(INameGenerator nameGenerator, Func?>? idMapper = null) { _nameGenerator = nameGenerator; _entitiesToMap = new List(); @@ -95,10 +95,9 @@ public Conventions(INameGenerator nameGenerator, Func> i }); _defaultMapper = new ModelMapperWithNamingConventions(); - DefaultModelInspector = _defaultMapper.ModelInspector; } - private readonly Func> _idMapper; + private readonly Func?> _idMapper; private readonly List _assemblies = new List(); public bool AssemblyAdded(Assembly assembly) @@ -106,14 +105,13 @@ public bool AssemblyAdded(Assembly assembly) return _assemblies.Contains(assembly); } - public void AddAssembly(Assembly assembly, string databasePrefix = "") + public void AddAssembly(Assembly assembly, string? databasePrefix = null) { _assemblies.Add(assembly); - MappingHelper.AddDatabasePrefixForAssembly(assembly, databasePrefix); + MappingHelper.AddDatabasePrefixForAssembly(assembly, databasePrefix ?? string.Empty); } - public static IModelInspector DefaultModelInspector { get; set; } - private static ModelMapperWithNamingConventions _defaultMapper; + private ModelMapperWithNamingConventions _defaultMapper; private List _entitiesToMap; public void Compile(NHcfg.Configuration configuration) @@ -260,13 +258,11 @@ public void Compile(NHcfg.Configuration configuration) { var propertyType = ByCode.TypeExtensions.GetPropertyOrFieldType(member.LocalMember); - var lazyAttribute = member.LocalMember.GetAttribute(true); + var lazyAttribute = member.LocalMember.GetAttributeOrNull(true); if (lazyAttribute != null) propertyCustomizer.Lazy(true); var columnName = MappingHelper.GetColumnName(member.LocalMember); - string sqlType = null; - IType columnType = null; if (member.LocalMember.DeclaringType == typeof(GenericEntityReference) || member.LocalMember.GetMemberType() == typeof(GenericEntityReference)) @@ -299,19 +295,25 @@ public void Compile(NHcfg.Configuration configuration) if (propertyType.IsAssignableTo(typeof(ConfigurationItemIdentifier))) { - var prefix = MappingHelper.GetColumnPrefix(member.LocalMember.DeclaringType); - var moduleColumn = MappingHelper.GetNameForMember(member.LocalMember, prefix, member.LocalMember.Name, nameof(FormIdentifier.Module)); - var nameColumn = MappingHelper.GetNameForMember(member.LocalMember, prefix, member.LocalMember.Name, nameof(FormIdentifier.Name)); + if (!propertyType.IsAssignableTo(typeof(IIdentifierFactory))) + throw new Exception($"Type '{propertyType.Name}' must implement '{nameof(IIdentifierFactory)}'"); + + var prefix = MappingHelper.GetColumnPrefix(member.LocalMember.DeclaringType.NotNull()); + var moduleColumn = MappingHelper.GetNameForMember(member.LocalMember, prefix, member.LocalMember.Name, nameof(ConfigurationItemIdentifier.Module)); + var nameColumn = MappingHelper.GetNameForMember(member.LocalMember, prefix, member.LocalMember.Name, nameof(ConfigurationItemIdentifier.Name)); propertyCustomizer.Columns( c => { c.Name(moduleColumn); }, c => { c.Name(nameColumn); } ); - var gtype = typeof(ConfigurationItemIdentifierUserType); + var gtype = typeof(ConfigurationItemIdentifierUserType<>).MakeGenericType(propertyType); propertyCustomizer.Type(gtype, null); return; } + string? sqlType = null; + IType? columnType = null; + if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)) { columnType = NHibernateUtil.DateTime; @@ -407,15 +409,14 @@ public void Compile(NHcfg.Configuration configuration) var idColumn = MappingHelper.GetIdColumnName(idProp); // get Id mapper - var idMapper = _idMapper.Invoke(idProp.PropertyType); - if (idMapper != null) + var idMapper = imMutable + ? null + : _idMapper.Invoke(idProp.PropertyType); + classCustomizer.Id(p => { - classCustomizer.Id(p => - { - idMapper.Invoke(p); - p.Column(idColumn); - }); - } + idMapper?.Invoke(p); + p.Column(idColumn); + }); } } else @@ -447,7 +448,7 @@ public void Compile(NHcfg.Configuration configuration) // IMayHaveTenant support if (typeof(IMustHaveTenant).IsAssignableFrom(type)) { - var tenantIdProp = type.GetProperty(nameof(IMustHaveTenant.TenantId)); + var tenantIdProp = type.GetRequiredProperty(nameof(IMustHaveTenant.TenantId)); var tenantIdColumnName = MappingHelper.GetColumnName(tenantIdProp); classCustomizer.Filter(AbpDataFilters.MustHaveTenant, m => { @@ -459,7 +460,7 @@ public void Compile(NHcfg.Configuration configuration) // IMayHaveTenant support if (typeof(IMayHaveTenant).IsAssignableFrom(type)) { - var tenantIdProp = type.GetProperty(nameof(IMayHaveTenant.TenantId)); + var tenantIdProp = type.GetRequiredProperty(nameof(IMayHaveTenant.TenantId)); var tenantIdColumnName = MappingHelper.GetColumnName(tenantIdProp); classCustomizer.Filter(AbpDataFilters.MayHaveTenant, m => { @@ -471,7 +472,7 @@ public void Compile(NHcfg.Configuration configuration) // ISoftDelete support if (typeof(ISoftDelete).IsAssignableFrom(type)) { - var isDeletedProp = type.GetProperty(nameof(ISoftDelete.IsDeleted)); + var isDeletedProp = type.GetRequiredProperty(nameof(ISoftDelete.IsDeleted)); var isDeletedColumnName = MappingHelper.GetColumnName(isDeletedProp); classCustomizer.Filter(AbpDataFilters.SoftDelete, m => @@ -492,10 +493,14 @@ public void Compile(NHcfg.Configuration configuration) mapper.BeforeMapManyToOne += (modelInspector, propertyPath, map) => { - string columnPrefix = MappingHelper.GetColumnPrefix(propertyPath.LocalMember.DeclaringType); - - var lazyAttribute = propertyPath.LocalMember.GetAttribute(true); - var lazyRelation = lazyAttribute != null ? lazyAttribute.GetLazyRelation() : _defaultLazyRelation; + string columnPrefix = MappingHelper.GetColumnPrefix(propertyPath.LocalMember.DeclaringType.NotNull()); + + var lazyAttribute = propertyPath.LocalMember.GetAttributeOrNull(true); + var lazyRelation = lazyAttribute != null + ? lazyAttribute is NhLazyLoadAttribute nhLazy + ? nhLazy.GetLazyRelation() + : LazyRelation.NoProxy + : _defaultLazyRelation; if (lazyRelation != null) map.Lazy(lazyRelation); @@ -512,14 +517,14 @@ public void Compile(NHcfg.Configuration configuration) map.Update(false); } - var cascadeAttribute = propertyPath.LocalMember.GetAttribute(true); + var cascadeAttribute = propertyPath.LocalMember.GetAttributeOrNull(true); map.Cascade(cascadeAttribute?.Cascade ?? ByCode.Cascade.Persist); map.Class(ByCode.TypeExtensions.GetPropertyOrFieldType(propertyPath.LocalMember)); }; mapper.BeforeMapBag += (modelInspector, propertyPath, map) => { - var inversePropertyAttribute = propertyPath.LocalMember.GetAttribute(true); + var inversePropertyAttribute = propertyPath.LocalMember.GetAttributeOrNull(true); if (inversePropertyAttribute != null) map.Key(keyMapper => keyMapper.Column(inversePropertyAttribute.Property)); else @@ -530,7 +535,7 @@ public void Compile(NHcfg.Configuration configuration) var bagMapper = map as NMIMPL.BagMapper; - var manyToManyAttribute = propertyPath.LocalMember.GetAttribute(true); + var manyToManyAttribute = propertyPath.LocalMember.GetAttributeOrNull(true); if (manyToManyAttribute != null) { @@ -564,15 +569,6 @@ public void Compile(NHcfg.Configuration configuration) map.Where(manyToManyAttribute.Where); if (!string.IsNullOrEmpty(manyToManyAttribute.OrderBy)) map.OrderBy(manyToManyAttribute.OrderBy); - - /* AS: Experiments for using one table for linking two entities with several list properties with the same entity types - map.Where($"PropertyName = '{propertyPath.LocalMember.Name}'"); - map.SqlInsert($"INSERT INTO {tableName} (Id, {parentColumnName}, {childColumnName}, PropertyName) VALUES (NEWID(), ?, ?, '{propertyPath.LocalMember.Name}')", SqlCheck.None); - map.SqlUpdate("-- skip update MultiEntityReference table", SqlCheck.None); - map.SqlDelete("-- skip update MultiEntityReference table", SqlCheck.None); - map.SqlDeleteAll("-- skip update MultiEntityReference table", SqlCheck.None); - map.Persister(typeof(Persister)); - */ } } else if (bagMapper != null && typeof(ISoftDelete).IsAssignableFrom(bagMapper.ElementType)) @@ -581,7 +577,6 @@ public void Compile(NHcfg.Configuration configuration) map.Filter("SoftDelete", m => { }); - // map.Where($"{SheshaDatabaseConsts.IsDeletedColumn.DoubleQuote()} = 0"); } }; @@ -590,7 +585,7 @@ public void Compile(NHcfg.Configuration configuration) { //map.NotFound(NotFoundMode.Ignore); disabled due to performance issues, this option breaks lazy loading - var manyToManyAttribute = propertyPath.LocalMember.GetAttribute(true); + var manyToManyAttribute = propertyPath.LocalMember.GetAttributeOrNull(true); if (manyToManyAttribute != null) { if (!string.IsNullOrEmpty(manyToManyAttribute.ChildColumn)) @@ -644,19 +639,6 @@ public void Compile(NHcfg.Configuration configuration) // sort entity types by hierarchy _entitiesToMap = MappingHelper.SortEntityTypesByInheritance(_entitiesToMap); - /* for debug - foreach (var ent in _entitiesToMap) - { - try - { - var mapping1 = mapper.CompileMappingFor(new List { ent }); - } - catch (Exception e) - { - throw; - } - } - */ HbmMapping mapping = mapper.CompileMappingFor(_entitiesToMap); @@ -669,17 +651,16 @@ public void Compile(NHcfg.Configuration configuration) public void SaveXml(string filename) { - TextWriter wr = new StreamWriter(filename); + using var wr = new StreamWriter(filename); wr.Write(LastCompiledXml); - wr.Close(); } /// /// Last compiled conventions in the xml format /// - public static string LastCompiledXml { get; set; } + public static string? LastCompiledXml { get; set; } - private static Type GetEntityTypeByMapping(Type mappingType) + private static Type? GetEntityTypeByMapping(Type mappingType) { var genericMapping = mappingType.BaseType; return genericMapping != null && genericMapping.GenericTypeArguments.Any() @@ -696,7 +677,7 @@ public static bool IsClassMapping(Type type) if (it.GetGenericTypeDefinition() == typeof(ClassMapping<>) || it.GetGenericTypeDefinition() == typeof(SubclassMapping<>) || it.GetGenericTypeDefinition() == typeof(JoinedSubclassMapping<>)) return true; - Type baseType = type.BaseType; + var baseType = type.BaseType; if (baseType == null) return false; return baseType.IsGenericType && @@ -706,19 +687,4 @@ public static bool IsClassMapping(Type type) IsClassMapping(baseType); } } - - /* AS: Experiments for using one tbale for linking two entities with several list properties with the same entity types - public class Persister : BasicCollectionPersister - { - public Persister(Collection collection, ICacheConcurrencyStrategy cache, ISessionFactoryImplementor factory) : base(collection, cache, factory) - { - } - - protected override SqlCommandInfo GenerateInsertRowString() - { - var res = base.GenerateInsertRowString(); - return res; - } - } - */ -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/MappingHelper.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/MappingHelper.cs index 2ce9b75a2d..0cd511f826 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/MappingHelper.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/MappingHelper.cs @@ -16,7 +16,7 @@ public static bool IsPersistentProperty(MemberInfo prop) if (!MappingHelper.IsPersistentProperty(prop)) return false; - if (!MappingHelper.IsRootEntity(prop.DeclaringType) && prop.DeclaringType.BaseType != null && !prop.DeclaringType.BaseType.IsAbstract) + if (prop.DeclaringType != null && !MappingHelper.IsRootEntity(prop.DeclaringType) && prop.DeclaringType.BaseType != null && !prop.DeclaringType.BaseType.IsAbstract) { var upperLevelProperty = prop.DeclaringType.BaseType.GetProperty(prop.Name); if (upperLevelProperty != null) diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/ModelMapperWithNamingConventions.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/ModelMapperWithNamingConventions.cs index 48332a44a5..810bac932c 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/ModelMapperWithNamingConventions.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Maps/ModelMapperWithNamingConventions.cs @@ -1,9 +1,9 @@ -using System; +using NHibernate.Mapping.ByCode; +using Shesha.Reflection; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using Abp.Dependency; -using NHibernate.Mapping.ByCode; namespace Shesha.NHibernate.Maps { @@ -55,7 +55,7 @@ public static class PropertyPathExtensions { public static Type Owner(this PropertyPath member) { - return member.GetRootMember().DeclaringType; + return member.GetRootMember().DeclaringType.NotNull(); } public static Type CollectionElementType(this PropertyPath member) diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/ModuleLocator.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/ModuleLocator.cs index 48660b8c4a..dddfac838b 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/ModuleLocator.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/ModuleLocator.cs @@ -13,7 +13,7 @@ public class ModuleLocator : IModuleLocator, ISingletonDependency /// inheritedDoc public string GetModuleName(Type migrationType) { - return migrationType.GetConfigurableModuleName(); + return migrationType.GetConfigurableModuleName().NotNullOrWhiteSpace(); } } } diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/NHibernateEntityFetcher.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/NHibernateEntityFetcher.cs index a73e4587f1..9a76fa6107 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/NHibernateEntityFetcher.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/NHibernateEntityFetcher.cs @@ -2,6 +2,7 @@ using Abp.Linq; using NHibernate.Linq; using Shesha.Extensions; +using Shesha.Reflection; using Shesha.Services; using System; using System.Collections.Generic; @@ -27,7 +28,7 @@ public NHibernateEntityFetcher(IAsyncQueryableExecuter asyncExecuter) public Task> ToListAsync(IQueryable queryable, List properties) { - IQueryable fetcher = null; + IQueryable? fetcher = null; foreach (var propName in properties) { @@ -51,9 +52,9 @@ private IQueryable FetchNested(IQueryable queryable, string propName) var property = ReflectionHelper.GetProperty(typeof(T), firstPart, useCamelCase: true); if (property != null && property.PropertyType.IsEntityType()) { - var method = typeof(NHibernateEntityFetcher).GetMethod(nameof(MakeFetch), BindingFlags.Static | BindingFlags.Public); + var method = typeof(NHibernateEntityFetcher).GetRequiredMethod(nameof(MakeFetch), BindingFlags.Static | BindingFlags.Public); var invoker = method.MakeGenericMethod(typeof(T), property.PropertyType); - var fetcher = invoker.Invoke(null, new object[] { queryable, firstPart }) as IQueryable; + var fetcher = invoker.Invoke(null, [queryable, firstPart]).ForceCastAs>(); var nestedParts = parts.Skip(1).ToArray(); if (nestedParts.Any()) @@ -64,9 +65,9 @@ private IQueryable FetchNested(IQueryable queryable, string propName) var nestedProperty = ReflectionHelper.GetProperty(currentType, nestedPart, useCamelCase: true); if (nestedProperty != null && nestedProperty.PropertyType.IsEntityType()) { - var nestMethod = typeof(NHibernateEntityFetcher).GetMethod(nameof(MakeFetchNested), BindingFlags.Static | BindingFlags.Public); + var nestMethod = typeof(NHibernateEntityFetcher).GetRequiredMethod(nameof(MakeFetchNested), BindingFlags.Static | BindingFlags.Public); var nestInvoker = nestMethod.MakeGenericMethod(typeof(T), currentType, nestedProperty.PropertyType); - fetcher = nestInvoker.Invoke(null, new object[] { fetcher, nestedPart }) as IQueryable; + fetcher = nestInvoker.Invoke(null, [fetcher, nestedPart]).ForceCastAs>(); currentType = nestedProperty.PropertyType; } diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/NhActiveTransactionProvider.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/NhActiveTransactionProvider.cs deleted file mode 100644 index 1fb5c9af55..0000000000 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/NhActiveTransactionProvider.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Data; -using System.Reflection; -using System.Threading.Tasks; -using Abp.Data; -using Abp.Dependency; -using Abp.Extensions; -using NHibernate; -using NHibernate.Transaction; - -namespace Shesha.NHibernate -{ - public class NhActiveTransactionProvider : IActiveTransactionProvider, ITransientDependency - { - private readonly ISessionProvider _sessionProvider; - - public NhActiveTransactionProvider(ISessionProvider sessionProvider) - { - _sessionProvider = sessionProvider; - } - - public async Task GetActiveTransactionAsync(ActiveTransactionProviderArgs args) - { - return await Task.FromResult(GetActiveTransaction(args)); - } - - public IDbTransaction GetActiveTransaction(ActiveTransactionProviderArgs args) - { - var adoTransaction = _sessionProvider.Session.GetCurrentTransaction()?.As(); - if (adoTransaction == null) - return null; - - var dbTransaction = GetFieldValue(typeof(AdoTransaction), adoTransaction, "trans").As(); - return dbTransaction; - } - - public async Task GetActiveConnectionAsync(ActiveTransactionProviderArgs args) - { - return await Task.FromResult(GetActiveConnection(args)); - } - - public IDbConnection GetActiveConnection(ActiveTransactionProviderArgs args) - { - return _sessionProvider.Session.Connection; - } - - private static object GetFieldValue(Type type, object instance, string fieldName) - { - return type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) - .GetValue(instance); - } - } -} diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/NhAsyncQueryableExecuter.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/NhAsyncQueryableExecuter.cs index 7e24314b61..d5ce7dcde3 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/NhAsyncQueryableExecuter.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/NhAsyncQueryableExecuter.cs @@ -28,12 +28,12 @@ public Task> ToListAsync(IQueryable queryable) return Task.FromResult(queryable.ToList()); } - public Task FirstOrDefaultAsync(IQueryable queryable) + public async Task FirstOrDefaultAsync(IQueryable queryable) { - if ((queryable.Provider is INhQueryProvider nhQueryProvider)) - return LinqExtensionMethods.FirstOrDefaultAsync(queryable); + if (queryable.Provider is INhQueryProvider nhQueryProvider) + return await LinqExtensionMethods.FirstOrDefaultAsync(queryable); - return Task.FromResult(queryable.FirstOrDefault()); + return queryable.FirstOrDefault(); } public Task AnyAsync(IQueryable queryable) diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/NhibernateProxyStripper.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/NhibernateProxyStripper.cs index a76b607824..7e0adbdff2 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/NhibernateProxyStripper.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/NhibernateProxyStripper.cs @@ -16,7 +16,7 @@ public class NhibernateProxyStripper : IProxyStripper, ITransientDependency public Type StripProxy(Type type) { #pragma warning disable 612, 618 - if (type.GetInterfaces().Any(i => i == typeof(INHibernateProxy) || i == typeof(IProxy) || i == typeof(IFieldInterceptorAccessor))) + if (type.GetInterfaces().Any(i => i == typeof(INHibernateProxy) || i == typeof(IProxy) || i == typeof(IFieldInterceptorAccessor)) && type.BaseType != null) { return type.BaseType; } diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/QuotedNamingStrategy.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/QuotedNamingStrategy.cs index cce4ad47d3..026b5a84c6 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/QuotedNamingStrategy.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/QuotedNamingStrategy.cs @@ -11,7 +11,7 @@ internal class QuotedNamingStrategy : INamingStrategy { private static readonly INamingStrategy Wrapped = DefaultNamingStrategy.Instance; - private static QuotedNamingStrategy _postgreNamingStrategy; + private static QuotedNamingStrategy? _postgreNamingStrategy; public static INamingStrategy Instance { diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Repositories/NhRepositoryBaseOfTEntityAndTPrimaryKey.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Repositories/NhRepositoryBaseOfTEntityAndTPrimaryKey.cs index 66df6c284d..7ab67ab219 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Repositories/NhRepositoryBaseOfTEntityAndTPrimaryKey.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Repositories/NhRepositoryBaseOfTEntityAndTPrimaryKey.cs @@ -45,6 +45,9 @@ public class NhRepositoryBase : AbpRepositoryBase QueryAll() diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Session/INhCurrentSessionContext.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Session/INhCurrentSessionContext.cs new file mode 100644 index 0000000000..fd7374f83c --- /dev/null +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Session/INhCurrentSessionContext.cs @@ -0,0 +1,14 @@ +using NHibernate; +using System; + +namespace Shesha.NHibernate.Session +{ + public interface INhCurrentSessionContext : IDisposable + { + /// + /// + /// + ISession Session { get; } + ISession? SessionOrNull { get; } + } +} diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Session/NhCurrentSessionContext.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Session/NhCurrentSessionContext.cs new file mode 100644 index 0000000000..82fcd07fd3 --- /dev/null +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Session/NhCurrentSessionContext.cs @@ -0,0 +1,42 @@ +using Abp.Dependency; +using NHibernate; +using NHibernate.Context; +using System; + +namespace Shesha.NHibernate.Session +{ + /// + /// Replacement of , allows to write code that doesn't break nullability and disposable rules + /// + public sealed class NhCurrentSessionContext : INhCurrentSessionContext, ITransientDependency + { + private bool _disposed; + + private readonly IIocResolver _iocResolver; + public NhCurrentSessionContext(IIocResolver iocResolver) + { + _iocResolver = iocResolver; + } + + public ISession? SessionOrNull + { + get + { + var factory = _iocResolver.Resolve(); + return factory?.GetCurrentSession(); + } + } + + public ISession Session => SessionOrNull ?? throw new Exception("Session is not available"); + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + } + } +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Session/SessionExtensions.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Session/SessionExtensions.cs index 0c43ff1cc8..e9c86424cc 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Session/SessionExtensions.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Session/SessionExtensions.cs @@ -1,19 +1,12 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using Abp.Domain.Entities; +using Abp.Extensions; using NHibernate; -using NHibernate.Impl; -using NHibernate.Proxy; -using NHibernate.Util; using NHibernate.Engine; -using Abp.Domain.Entities; -using Abp.Extensions; -using Shesha.Domain; -using Shesha.NHibernate; +using NHibernate.Proxy; using Shesha.NHibernate.Interceptors; -using Shesha.Domain.QueryBuilder; +using System; +using System.Collections.Generic; +using System.Linq; namespace Shesha.NHibernate.Session { @@ -44,28 +37,24 @@ public static List GetDirtyProperties(this ISession session, .ToList() : new List(); } - - public static EntityEntry GetEntry(this ISession session, Object entity, bool assert = true) - { + + public static EntityEntry? GetEntryOrNull(this ISession session, Object entity) + { var sessionImpl = session.GetSessionImplementation(); var oldEntry = sessionImpl.PersistenceContext.GetEntry(entity); - if (oldEntry == null) - { - if (entity is INHibernateProxy proxy) - { - Object obj = sessionImpl.PersistenceContext.Unproxy(proxy); - oldEntry = sessionImpl.PersistenceContext.GetEntry(obj); - } - else - { - if (assert) - System.Diagnostics.Debug.Assert(false, "Entity was likely retrieved using an NHibernate session which is no longer available."); - else return null; - } + if (entity is INHibernateProxy proxy) + { + Object obj = sessionImpl.PersistenceContext.Unproxy(proxy); + oldEntry = sessionImpl.PersistenceContext.GetEntry(obj); } return oldEntry; } + public static EntityEntry GetEntry(this ISession session, Object entity) + { + return session.GetEntryOrNull(entity) ?? throw new Exception("Entity was likely retrieved using an NHibernate session which is no longer available."); + } + public static bool IsEntityDeleted(this ISession session, Object entity) { var entityEntry = session.GetEntry(entity); @@ -76,213 +65,11 @@ public static bool IsEntityDeleted(this ISession session, Object entity) return entity is ISoftDelete && entity.As().IsDeleted; } - /// - /// Ends session and commit transaction if active - /// - /// - /// - public static void EndSession(this ISession session, bool commitTransaction = true) - { - try - { - var transaction = session.GetCurrentTransaction(); - if (transaction != null && transaction.IsActive) - { - if (commitTransaction) - { - try - { - transaction.Commit(); - } - catch - { - transaction.Rollback(); - throw; - } - } - else - { - transaction.Rollback(); - } - } - } - finally - { - if (session.IsOpen) - session.Close(); - - session.Dispose(); - } - } - - public static IQuery CreateQuery(this ISession session, System.Type entityType, FilterCriteria criteria) - { - return CreateQuery(session, entityType, "select ent ", criteria, null); - } - public static IQuery CreateQuery(this ISession session, System.Type entityType, FilterCriteria criteria, string orderByClause) - { - return CreateQuery(session, entityType, "select ent ", criteria, orderByClause); - } - - public static IQuery CreateQuery(this ISession session, QueryBuildingContext queryContext) - { - var sb = new StringBuilder(); - sb.Append("select ent "); - - AppendHqlClauses(queryContext, sb); - - if (!string.IsNullOrEmpty(queryContext.OrderBy)) - { - sb.Append(" order by "); - sb.Append(queryContext.OrderBy.Replace("ascending", "asc").Replace("descending", "desc")); - } - - var q = session.CreateQuery(sb.ToString()); - TransferHqlParameters(q, queryContext.FilterCriteria); - - return q; - - } - - public static IQuery CreateQuery(this ISession session, System.Type entityType, string hqlStart, FilterCriteria criteria, string orderByClause) - { - var sb = new StringBuilder(); - if (!string.IsNullOrEmpty(hqlStart)) - sb.Append(hqlStart); - - AppendHqlClauses(entityType, criteria, sb); - - if (!string.IsNullOrEmpty(orderByClause)) - { - sb.Append(" order by "); - sb.Append(orderByClause.Replace("ascending", "asc").Replace("descending", "desc")); - } - - var q = session.CreateQuery(sb.ToString()); - TransferHqlParameters(q, criteria); - - return q; - } - - public static IQuery CreateQueryCount(this ISession session, System.Type entityType, FilterCriteria criteria) - { - return CreateQuery(session, entityType, "select count(*) ", criteria, null); - } - - public static IQuery CreateQueryCount(this ISession session, System.Type entityType, QueryBuildingContext queryContext) - { - var sb = new StringBuilder(); - - sb.Append("select count(*) "); - - AppendHqlClauses(queryContext, sb); - - var q = session.CreateQuery(sb.ToString()); - TransferHqlParameters(q, queryContext.FilterCriteria); - - return q; - } - - public static void TransferHqlParameters(IQuery q, FilterCriteria criteria) - { - foreach (var param in criteria.FilterParameters) - { - if (q.NamedParameters.Contains(param.Key)) - { - if (param.Value != null && param.Value is IList) - { - var list = param.Value as IList; - if (Enumerable.Any(list.Cast())) - q.SetParameterList(param.Key, (IEnumerable)param.Value); - else - q.SetParameter(param.Key, null); - } - else - q.SetParameter(param.Key, param.Value); - } - } - } - - private static void AppendHqlClauses(System.Type entityType, FilterCriteria criteria, StringBuilder sb) - { - sb.Append(" from "); - sb.Append(entityType.FullName); - sb.Append(" ent"); - - if (criteria.FilterClauses.Count > 0) - { - sb.Append(" where "); - bool hasClause = false; - foreach (var clause in criteria.FilterClauses) - { - if (hasClause) - sb.Append(" and "); - - sb.Append("("); - sb.Append(clause); - sb.Append(")"); - hasClause = true; - } - } - } - - private static string GetJoinTypeString(JoinType joinType) - { - switch (joinType) - { - case JoinType.Left: - return "left"; - case JoinType.Inner: - return "inner"; - default: - throw new NotSupportedException($"Unsupported join type: {joinType}"); - - } - } - - private static void AppendHqlClauses(QueryBuildingContext queryContext, StringBuilder sb) - { - sb.Append(" from "); - sb.Append(queryContext.RootClass.FullName); - sb.Append(" ent"); - - // add joins - foreach (var join in queryContext.Joins) - { - sb.Append($" {GetJoinTypeString(join.JoinType)} join {join.Reference} {join.Alias}"); - if (!string.IsNullOrWhiteSpace(join.Condition)) { - sb.Append(" on "); - sb.Append(join.Condition); - } - } - - if (queryContext.FilterCriteria.FilterClauses.Count > 0) - { - sb.Append(" where "); - bool hasClause = false; - foreach (var clause in queryContext.FilterCriteria.FilterClauses) - { - if (hasClause) - sb.Append(" and "); - - sb.Append("("); - sb.Append(clause); - sb.Append(")"); - hasClause = true; - } - } - } - - public static Guid GetId(this ISession session) - { - return ((SessionImpl)session).SessionId; - } - public class DirtyPropertyInfo { - public string Name { get; set; } - public object OldValue { get; set; } - public object NewValue { get; set; } + public required string Name { get; init; } + public object? OldValue { get; init; } + public object? NewValue { get; init; } } /// @@ -290,7 +77,7 @@ public class DirtyPropertyInfo /// /// /// - public static SheshaNHibernateInterceptor LocalInterceptor(this ISession session) + public static SheshaNHibernateInterceptor? LocalInterceptor(this ISession session) { return session.GetSessionImplementation().Interceptor as SheshaNHibernateInterceptor; } @@ -307,6 +94,6 @@ public static void DoAfterTransaction(this ISession session, Action action) throw new Exception("Session's interceptor should be specified"); interceptor.AddAfterTransactionAction(action); - } - } + } + } } diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Session/UnitOfWorkSessionContext.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Session/UnitOfWorkSessionContext.cs index c441e480ff..5bdcda372c 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Session/UnitOfWorkSessionContext.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Session/UnitOfWorkSessionContext.cs @@ -1,5 +1,4 @@ -using Abp.Dependency; -using Abp.Domain.Uow; +using Abp.Domain.Uow; using NHibernate; using NHibernate.Context; using Shesha.NHibernate.UoW; @@ -24,12 +23,12 @@ public UnitOfWorkSessionContext() /// Returns current session /// /// - public ISession CurrentSession() + public ISession? CurrentSession() { var uowProvider = StaticContext.IocManager.Resolve(); return uowProvider.Current is NhUnitOfWork nhUow - ? nhUow.GetSession() + ? nhUow.GetSessionOrNull() : null; } } diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateInstaller.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateInstaller.cs index e942b5779f..ce5c41db1f 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateInstaller.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateInstaller.cs @@ -21,7 +21,7 @@ public void Install(IWindsorContainer container, IConfigurationStore store) Component.For().ImplementedBy(typeof(UnitOfWorkSessionContext)).LifestyleTransient(), Component.For().UsingFactory(f => f.OpenSession()).LifestyleScoped(), Component.For().ImplementedBy(typeof(NHibernateEntityHistoryStore)).LifestyleTransient(), - Component.For().ImplementedBy(typeof(EntityHistory.EntityHistoryHelper)).LifestyleTransient() + Component.For().ImplementedBy(typeof(EntityHistoryHelper)).LifestyleTransient() ); } } diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs index 48345283c6..63616d3141 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs @@ -11,23 +11,12 @@ using Abp.Runtime.Caching; using Abp.Threading; using Castle.MicroKernel.Registration; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using NHibernate; -using NHibernate.Dialect; -using NHibernate.Driver; -using NHibernate.Engine; -using NHibernate.Spatial.Dialect; -using NHibernate.Spatial.Mapping; -using NHibernate.Spatial.Metadata; -using NHibernate.Type; -using Npgsql; using Shesha.Attributes; using Shesha.Bootstrappers; -using Shesha.Configuration; using Shesha.Configuration.Startup; using Shesha.Domain; -using Shesha.Exceptions; using Shesha.FluentMigrator; using Shesha.Generators; using Shesha.Locks; @@ -41,7 +30,6 @@ using Shesha.NHibernate.Repositories; using Shesha.NHibernate.Session; using Shesha.NHibernate.Uow; -using Shesha.NHibernate.Utilites; using Shesha.Reflection; using Shesha.Services; using Shesha.Startup; @@ -59,12 +47,14 @@ namespace Shesha.NHibernate typeof(AbpAspNetCoreModule), typeof(SheshaFrameworkModule) )] - public class SheshaNHibernateModule : AbpModule + public sealed class SheshaNHibernateModule : AbpModule, IDisposable { public const string SkipMigrationsSetting = "skipMigrations"; - public const string SkipBootstrappersSetting = "skipBootstrappers"; - - /* Used it tests to skip dbcontext registration */ + public const string SkipBootstrappersSetting = "skipBootstrappers"; + private bool _disposed; + + + /* Used it tests to skip dbcontext registration */ public bool SkipDbContextRegistration { get; set; } public bool SkipDbSeed { get; set; } public string ConnectionString { get; set; } @@ -74,7 +64,7 @@ public class SheshaNHibernateModule : AbpModule /// /// NHibernate session factory object. /// - private ISessionFactory _sessionFactory; + private ISessionFactory? _sessionFactory; private global::NHibernate.Cfg.Configuration _nhConfig; public override void PreInitialize() @@ -156,6 +146,7 @@ public override void Initialize() // ToDo: ABP662, some ABP entities (WebhookEvent, DynamicProperty) contain not virtual properties _nhConfig.Properties.Add("use_proxy_validator", "false"); + _sessionFactory?.Dispose(); _sessionFactory = config.SessionFactoryBuilder != null ? config.SessionFactoryBuilder.Invoke(_nhConfig) : _nhConfig.BuildSessionFactory(); @@ -174,6 +165,26 @@ public override void Initialize() ); IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); + } + + private void FreeSessionFactory() + { + if (_sessionFactory != null) + { + _sessionFactory.Dispose(); + _sessionFactory = null; + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + FreeSessionFactory(); } private IDbmsSpecificConfigurationProvider GetConfigProvider(DbmsType dbmsType) @@ -244,7 +255,7 @@ private async Task SeedDatabaseAsync() const string seedDbKey = "AppStart:SeedDb"; const string seedDbFinishedOnKey = "SeedDbFinishedOn"; - var cache = CacheManagerExtensions.GetCache(cacheManager, seedDbKey); + using var cache = cacheManager.GetCache(seedDbKey); var initializationStart = DateTime.Now.ToUniversalTime(); var initializedByCurrentInstance = false; @@ -296,7 +307,7 @@ await lockFactory.DoExclusiveAsync(seedDbKey, expiry, wait, retry, async () => } // Log application start if DB was not ready for logging before the migrations - if (!dbIsReadyForLogging) + if (startupDto == null) startupDto = await appStartup.LogApplicationStartAsync(appStartLogArgs); if (!skipBootstrappers) @@ -313,8 +324,8 @@ await lockFactory.DoExclusiveAsync(seedDbKey, expiry, wait, retry, async () => { Logger.Warn($"Run bootstrapper: {bootstrapperType.Name}..."); - var method = bootstrapperType.GetMethod(nameof(IBootstrapper.ProcessAsync)); - var unitOfWorkAttribute = method.GetAttribute(true); + var method = bootstrapperType.GetRequiredMethod(nameof(IBootstrapper.ProcessAsync)); + var unitOfWorkAttribute = method.GetAttributeOrNull(true); var useDefaultUnitOfWork = unitOfWorkAttribute == null || !unitOfWorkAttribute.IsDisabled; if (useDefaultUnitOfWork) @@ -362,7 +373,7 @@ private List SortByDependencies(List types) var withDeps = types.Select(t => new { Type = t, - Dependencies = t.GetAttribute()?.DependedBootstrappers?.ToList() + Dependencies = t.GetAttributeOrNull()?.DependedBootstrappers?.ToList() }) .ToList(); @@ -395,7 +406,15 @@ private List SortByDependencies(List types) /// public override void Shutdown() { - _sessionFactory?.Dispose(); + FreeSessionFactory(); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/UnitOfWorkExtensions.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/UnitOfWorkExtensions.cs index 0a73186e79..0994a31cdc 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/UnitOfWorkExtensions.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/UnitOfWorkExtensions.cs @@ -1,8 +1,8 @@ -using System; -using Abp.Domain.Uow; +using Abp.Domain.Uow; using NHibernate; using Shesha.NHibernate.Session; using Shesha.NHibernate.UoW; +using System; namespace Shesha.NHibernate { @@ -42,16 +42,11 @@ public static void DoAfterTransaction(this IActiveUnitOfWork unitOfWork, Action private static NhUnitOfWork GetNhUnitOfWork(IActiveUnitOfWork unitOfWork) { if (unitOfWork == null) - { throw new ArgumentNullException(nameof(unitOfWork)); - } - if (!(unitOfWork is NhUnitOfWork)) - { - throw new ArgumentException("unitOfWork is not type of " + typeof(NhUnitOfWork).FullName, "unitOfWork"); - } - - return unitOfWork as NhUnitOfWork; + return unitOfWork is NhUnitOfWork nhUnitOfWork + ? nhUnitOfWork + : throw new ArgumentException("unitOfWork is not type of " + typeof(NhUnitOfWork).FullName, "unitOfWork"); } } } diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/NhUnitOfWork.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/NhUnitOfWork.cs index 684c80a8c3..55b83e9495 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/NhUnitOfWork.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/NhUnitOfWork.cs @@ -1,14 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Diagnostics; -using System.Threading.Tasks; using Abp.Dependency; using Abp.Domain.Uow; using Abp.EntityHistory; using Abp.Runtime.Session; using Abp.Transactions.Extensions; using NHibernate; +using System; +using System.Data.Common; +using System.Diagnostics; +using System.Threading.Tasks; namespace Shesha.NHibernate.UoW { @@ -16,18 +15,30 @@ namespace Shesha.NHibernate.UoW /// Implements Unit of work for NHibernate. /// public class NhUnitOfWork : UnitOfWorkBase, ITransientDependency - { + { + /// + /// NHibernate session factory + /// + private readonly ISessionFactory _sessionFactory; + /// /// NH session /// - private ISession _session; + [System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP006:Implement IDisposable", Justification = $"Disposed in {nameof(DisposeUow)}")] + private ISession? _session; + + /// + /// Active transaction + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP006:Implement IDisposable", Justification = $"Disposed in {nameof(DisposeUow)}")] + private ITransaction? _transaction; /// /// Returns current session or starts a new one is missing and is true /// /// /// - public ISession GetSession(bool startNewIfMissing = true) + public ISession? GetSessionOrNull(bool startNewIfMissing = true) { if (_session == null && startNewIfMissing) { @@ -38,24 +49,23 @@ public ISession GetSession(bool startNewIfMissing = true) } return _session; + } + + public ISession GetSession() + { + return GetSessionOrNull() ?? throw new SessionException("Session is not available"); } - /// /// uses this DbConnection if it's set. /// This is usually set in tests. /// - public DbConnection DbConnection { get; set; } + public DbConnection? DbConnection { get; set; } /// /// Entity history helper /// - public EntityHistoryHelperBase EntityHistoryHelper { get; set; } - - private readonly ISessionFactory _sessionFactory; - private ITransaction _transaction; - - private readonly SheshaNHibernateModule _nhModule; + public EntityHistoryHelperBase EntityHistoryHelper { get; set; } = default!; /// /// Creates a new instance of . @@ -65,17 +75,13 @@ public NhUnitOfWork( ISessionFactory sessionFactory, IConnectionStringResolver connectionStringResolver, IUnitOfWorkDefaultOptions defaultOptions, - IUnitOfWorkFilterExecuter filterExecuter, - - SheshaNHibernateModule nhModule) + IUnitOfWorkFilterExecuter filterExecuter) : base( connectionStringResolver, defaultOptions, filterExecuter) { _sessionFactory = sessionFactory; - - _nhModule = nhModule; } /// @@ -86,6 +92,7 @@ protected ISession BeginSession() var session = DbConnection != null ? _sessionFactory.WithOptions().Connection(DbConnection).OpenSession() : _sessionFactory.OpenSession(); + _transaction?.Dispose(); session.FlushMode = FlushMode.Commit; @@ -120,12 +127,16 @@ private void ApplySessionOptions(ISession session) /// inheritedDoc protected override void BeginUow() - { - // Note: configuration of filters is still required irrespectively of the laziness of the session - - // set default value for IsDeleted parameter - SetFilterParameter(AbpDataFilters.SoftDelete, AbpDataFilters.Parameters.IsDeleted, false); - + { + // Note: configuration of filters is still required irrespectively of the laziness of the session + + // set default value for IsDeleted parameter + //ApplyFilterParameterValue(AbpDataFilters.SoftDelete, AbpDataFilters.Parameters.IsDeleted, false); + +#pragma warning disable IDISP004 // Don't ignore created IDisposable + SetFilterParameter(AbpDataFilters.SoftDelete, AbpDataFilters.Parameters.IsDeleted, false); +#pragma warning restore IDISP004 // Don't ignore created IDisposable + CheckAndSetSoftDelete(); CheckAndSetMayHaveTenant(); CheckAndSetMustHaveTenant(); @@ -233,7 +244,11 @@ protected override void DisposeUow() _transaction = null; } - _session?.Dispose(); + if (_session != null) + { + _session.Dispose(); + _session = null; + } } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/NhUnitOfWorkFilterExecuter.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/NhUnitOfWorkFilterExecuter.cs index bcea7eca69..6d6524d2be 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/NhUnitOfWorkFilterExecuter.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/NhUnitOfWorkFilterExecuter.cs @@ -1,9 +1,9 @@ -using System.Linq; -using Abp; +using Abp; using Abp.Domain.Uow; -using Abp.Extensions; using NHibernate; using Shesha.NHibernate.UoW; +using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Shesha.NHibernate.Uow { @@ -65,10 +65,10 @@ public void ApplyFilterParameterValue(IUnitOfWork unitOfWork, string filterName, /// /// /// - private bool SkipProcessing(IUnitOfWork unitOfWork, out ISession session) + private bool SkipProcessing(IUnitOfWork unitOfWork, [NotNullWhen(false)]out ISession? session) { session = unitOfWork is NhUnitOfWork nhUnitOwWork - ? nhUnitOwWork.GetSession(false) + ? nhUnitOwWork.GetSessionOrNull(false) : null; return session == null; diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/SheshaNhUnitOfWork.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/SheshaNhUnitOfWork.cs index 6906f4393b..1a4800268e 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/SheshaNhUnitOfWork.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/SheshaNhUnitOfWork.cs @@ -13,10 +13,8 @@ public SheshaNhUnitOfWork( ISessionFactory sessionFactory, IConnectionStringResolver connectionStringResolver, IUnitOfWorkDefaultOptions defaultOptions, - IUnitOfWorkFilterExecuter filterExecuter, - - SheshaNHibernateModule nhModule) - : base(sessionFactory, connectionStringResolver, defaultOptions, filterExecuter, nhModule) + IUnitOfWorkFilterExecuter filterExecuter) + : base(sessionFactory, connectionStringResolver, defaultOptions, filterExecuter) { } } diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/UnitOfWorkSessionProvider.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/UnitOfWorkSessionProvider.cs index 15f5a0d315..777b1ecb1e 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/UnitOfWorkSessionProvider.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/UoW/UnitOfWorkSessionProvider.cs @@ -9,6 +9,7 @@ namespace Shesha.NHibernate.Uow /// public class UnitOfWorkSessionProvider : ISessionProvider, ITransientDependency { + [System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP012:Property should not return created disposable", Justification = "`_sessionContext.CurrentSession()` call doesn't create new IDisposable")] public ISession Session => _sessionContext.CurrentSession(); private ICurrentSessionContext _sessionContext; diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/ConfigurationItemIdentifierUserType.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/ConfigurationItemIdentifierUserType.cs index 9881de6b92..4cfefa6e18 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/ConfigurationItemIdentifierUserType.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/ConfigurationItemIdentifierUserType.cs @@ -8,13 +8,13 @@ namespace Shesha.NHibernate.UserTypes { - internal class ConfigurationItemIdentifierUserType : ICompositeUserType + internal class ConfigurationItemIdentifierUserType : ICompositeUserType where TIdentifier: ConfigurationItemIdentifier, IIdentifierFactory { - public string[] PropertyNames => new string[] { nameof(FormIdentifier.Module), nameof(FormIdentifier.Name) }; + public string[] PropertyNames => [nameof(ConfigurationItemIdentifier.Module), nameof(ConfigurationItemIdentifier.Name)]; - public IType[] PropertyTypes => new IType[] { NHibernateUtil.String, NHibernateUtil.String }; + public IType[] PropertyTypes => [NHibernateUtil.String, NHibernateUtil.String]; - public Type ReturnedClass => typeof(FormIdentifier); + public Type ReturnedClass => typeof(TIdentifier); public bool IsMutable => true; @@ -23,10 +23,10 @@ public object Assemble(object cached, ISessionImplementor session, object owner) throw new NotImplementedException(); } - public object DeepCopy(object value) + public object? DeepCopy(object value) { - if (value is FormIdentifier er) - return new FormIdentifier(er.Module, er.Name); + if (value is ConfigurationItemIdentifier er) + return TIdentifier.New(er.Module, er.Name); return null; } @@ -39,7 +39,7 @@ public object Disassemble(object value, ISessionImplementor session) { if (x == null && y == null) return true; - if (x is FormIdentifier erx && y is FormIdentifier ery) + if (x is TIdentifier erx && y is TIdentifier ery) return erx == ery; return false; } @@ -49,9 +49,9 @@ public int GetHashCode(object x) return x == null ? 0 : x.GetHashCode(); } - public object GetPropertyValue(object component, int property) + public object? GetPropertyValue(object component, int property) { - if (component is FormIdentifier formId) + if (component is TIdentifier formId) { switch (property) { @@ -62,7 +62,7 @@ public object GetPropertyValue(object component, int property) return null; } - public object NullSafeGet(DbDataReader dr, string[] names, ISessionImplementor session, object owner) + public object? NullSafeGet(DbDataReader dr, string[] names, ISessionImplementor session, object owner) { if (names.Length != 2) throw new InvalidOperationException("Only expecting two column..."); @@ -75,7 +75,7 @@ public object NullSafeGet(DbDataReader dr, string[] names, ISessionImplementor s return null; } - return new FormIdentifier(module, name); + return TIdentifier.New(module, name); } public void NullSafeSet(DbCommand cmd, object value, int index, bool[] settable, ISessionImplementor session) @@ -83,7 +83,7 @@ public void NullSafeSet(DbCommand cmd, object value, int index, bool[] settable, var parameterId = (DbParameter)cmd.Parameters[index]; var parameterType = (DbParameter)cmd.Parameters[index + 1]; - if (value is FormIdentifier formId) + if (value is TIdentifier formId) { parameterId.Value = formId.Module; parameterType.Value = formId.Name; diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/EntityReferenceUserType.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/EntityReferenceUserType.cs index 3317f000d7..6b6cdf445f 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/EntityReferenceUserType.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/EntityReferenceUserType.cs @@ -6,7 +6,6 @@ using Shesha.EntityReferences; using System; using System.Data.Common; -using System.Data.SqlTypes; namespace Shesha.NHibernate.UserTypes { @@ -25,7 +24,7 @@ public object Assemble(object cached, ISessionImplementor session, object owner) throw new NotImplementedException(); } - public object DeepCopy(object value) + public object? DeepCopy(object value) { if (value is GenericEntityReference er) return new GenericEntityReference(er.Id, er._className, er._displayName); @@ -51,7 +50,7 @@ public int GetHashCode(object x) return x == null ? 0 : x.GetHashCode(); } - public object GetPropertyValue(object component, int property) + public object? GetPropertyValue(object component, int property) { if (component is GenericEntityReference entityRef) { @@ -64,7 +63,7 @@ public object GetPropertyValue(object component, int property) return null; } - public object NullSafeGet(DbDataReader dr, string[] names, ISessionImplementor session, object owner) + public object? NullSafeGet(DbDataReader dr, string[] names, ISessionImplementor session, object owner) { if (names.Length != 2) throw new InvalidOperationException("Only expecting two column..."); diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/EntityReferenceWithDisplayUserType.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/EntityReferenceWithDisplayUserType.cs index 2dd77c52fd..ee4833fa55 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/EntityReferenceWithDisplayUserType.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/EntityReferenceWithDisplayUserType.cs @@ -24,7 +24,7 @@ public object Assemble(object cached, ISessionImplementor session, object owner) throw new NotImplementedException(); } - public object DeepCopy(object value) + public object? DeepCopy(object value) { if (value is GenericEntityReference er) return new GenericEntityReference(er.Id, er._className, er._displayName); @@ -50,7 +50,7 @@ public int GetHashCode(object x) return x == null ? 0 : x.GetHashCode(); } - public object GetPropertyValue(object component, int property) + public object? GetPropertyValue(object component, int property) { if (component is GenericEntityReference entityRef) { @@ -64,7 +64,7 @@ public object GetPropertyValue(object component, int property) return null; } - public object NullSafeGet(DbDataReader dr, string[] names, ISessionImplementor session, object owner) + public object? NullSafeGet(DbDataReader dr, string[] names, ISessionImplementor session, object owner) { if (names.Length != 3) throw new InvalidOperationException("Only expecting two column..."); diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/JsonUserType.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/JsonUserType.cs index 31e2cc2e38..a862af3a76 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/JsonUserType.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/UserTypes/JsonUserType.cs @@ -1,5 +1,4 @@ -using Abp.Json; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using NHibernate.Engine; using NHibernate.SqlTypes; @@ -20,7 +19,7 @@ public class JsonUserType : IUserType public bool IsMutable => true; - public object Assemble(object cached, object owner) + public object? Assemble(object cached, object? owner) { var str = cached as string; @@ -30,7 +29,7 @@ public object Assemble(object cached, object owner) return JsonConvert.DeserializeObject(str); } - public object DeepCopy(object value) + public object? DeepCopy(object value) { if (value == null) return null; @@ -46,7 +45,7 @@ public object DeepCopy(object value) return JsonConvert.DeserializeObject(serialized); } - public object Disassemble(object value) + public object? Disassemble(object value) { if (value == null) return null; @@ -80,7 +79,7 @@ public int GetHashCode(object x) return x == null ? 0 : x.GetHashCode(); } - public object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor session, object owner) + public object? NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor session, object owner) { if (names.Length != 1) throw new InvalidOperationException("Only expecting one column..."); diff --git a/shesha-core/src/Shesha.NHibernate/NHibernate/Utilites/NHibernateUtilites.cs b/shesha-core/src/Shesha.NHibernate/NHibernate/Utilites/NHibernateUtilites.cs index c25c0111e9..726054df75 100644 --- a/shesha-core/src/Shesha.NHibernate/NHibernate/Utilites/NHibernateUtilites.cs +++ b/shesha-core/src/Shesha.NHibernate/NHibernate/Utilites/NHibernateUtilites.cs @@ -1,6 +1,4 @@ -using FluentMigrator.Runner.BatchParser.RangeSearchers; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using NHibernate.Dialect; using Shesha.Configuration; @@ -22,7 +20,7 @@ public static string GetConnectionString(string name) { var env = StaticContext.IocManager.IocContainer.Resolve(); var configuration = AppConfigurations.Get(env.ContentRootPath, env.EnvironmentName, env.IsDevelopment()); - return configuration.GetConnectionString(name); + return configuration.GetRequiredConnectionString(name); } /// diff --git a/shesha-core/src/Shesha.NHibernate/Services/Dtos/ExecuteHqlInput.cs b/shesha-core/src/Shesha.NHibernate/Services/Dtos/ExecuteHqlInput.cs deleted file mode 100644 index 45a31afbe8..0000000000 --- a/shesha-core/src/Shesha.NHibernate/Services/Dtos/ExecuteHqlInput.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Shesha.Services.Dtos -{ - /// - /// Execute HQL input - /// - public class ExecuteHqlInput - { - [Required] - [DataType(DataType.MultilineText)] - public string Query { get; set; } - } -} diff --git a/shesha-core/src/Shesha.NHibernate/Services/DynamicRepository.cs b/shesha-core/src/Shesha.NHibernate/Services/DynamicRepository.cs index 0fc197b774..8094f28d16 100644 --- a/shesha-core/src/Shesha.NHibernate/Services/DynamicRepository.cs +++ b/shesha-core/src/Shesha.NHibernate/Services/DynamicRepository.cs @@ -2,10 +2,12 @@ using Abp.Domain.Entities; using Abp.Domain.Uow; using NHibernate; -using NHibernate.Context; using Shesha.Configuration.Runtime; using Shesha.Domain.Attributes; +using Shesha.Extensions; using Shesha.NHibernate.Repositories; +using Shesha.NHibernate.Session; +using Shesha.Reflection; using Shesha.Utilities; using System; using System.Collections.Generic; @@ -22,14 +24,15 @@ namespace Shesha.Services public class DynamicRepository : IDynamicRepository, ITransientDependency { private readonly IEntityConfigurationStore _entityConfigurationStore; - private readonly ICurrentSessionContext _currentSessionContext; - + private readonly INhCurrentSessionContext _currentSessionContext; // note: current session doesn't work in unit tests because of static context usage - private ISession CurrentSession => _currentSessionContext.CurrentSession(); + private ISession CurrentSession => _currentSessionContext.Session; - public DynamicRepository(IEntityConfigurationStore entityConfigurationStore, ICurrentSessionContext currentSessionContext) + public DynamicRepository(IEntityConfigurationStore entityConfigurationStore, INhCurrentSessionContext currentSessionContext) { + CurrentUnitOfWorkProvider = default!; + _entityConfigurationStore = entityConfigurationStore; _currentSessionContext = currentSessionContext; } @@ -145,9 +148,9 @@ public IQueryable Where(Type entityType, LambdaExpression lambda) { var where = new Func, Expression>, IQueryable>(Queryable.Where).Method; var whereForMyType = where.GetGenericMethodDefinition().MakeGenericMethod(entityType); - var query = CurrentSession.Query(entityType.FullName).Cast(entityType.FullName); - return (IQueryable)whereForMyType.Invoke(query, new object[] { query, lambda }); - } + var query = CurrentSession.Query(entityType.FullName).Cast(entityType.GetRequiredFullName()); + return whereForMyType.Invoke(query, [query, lambda]).ForceCastAs(); + } } } diff --git a/shesha-core/src/Shesha.NHibernate/Services/NHibernateAppService.cs b/shesha-core/src/Shesha.NHibernate/Services/NHibernateAppService.cs index c4045b3da8..bcdb24619e 100644 --- a/shesha-core/src/Shesha.NHibernate/Services/NHibernateAppService.cs +++ b/shesha-core/src/Shesha.NHibernate/Services/NHibernateAppService.cs @@ -11,10 +11,9 @@ using Shesha.Extensions; using Shesha.Migrations; using Shesha.NHibernate.Maps; +using Shesha.NHibernate.Session; using Shesha.Reflection; -using Shesha.Services.Dtos; using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -27,10 +26,12 @@ namespace Shesha.Services public class NHibernateAppService: IApplicationService { private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly INhCurrentSessionContext _currentSessionContext; - public NHibernateAppService(IUnitOfWorkManager unitOfWorkManager) + public NHibernateAppService(IUnitOfWorkManager unitOfWorkManager, INhCurrentSessionContext currentSessionContext) { _unitOfWorkManager = unitOfWorkManager; + _currentSessionContext = currentSessionContext; } /// @@ -38,22 +39,11 @@ public NHibernateAppService(IUnitOfWorkManager unitOfWorkManager) /// /// [DontWrapResult] - public Task GetConventionsAsync() + public Task GetConventionsAsync() { return Task.FromResult(Conventions.LastCompiledXml); } - [HttpPost] - [Consumes("multipart/form-data")] - [DontWrapResult] - public IList ExecuteHql([FromForm] ExecuteHqlInput input) - { - var sessionFactory = StaticContext.IocManager.Resolve(); - var session = sessionFactory.GetCurrentSession(); - var list = session.CreateQuery(input.Query).List(); - return list; - } - /// /// NOTE: to be removed /// @@ -120,7 +110,7 @@ private void TryFetch(Type type, Action onException) try { var hql = $"from {type.FullName}"; - var session = sessionFactory.GetCurrentSession(); + var session = _currentSessionContext.Session; var list = session.CreateQuery(hql).SetMaxResults(1).List(); } catch (Exception e) diff --git a/shesha-core/src/Shesha.NHibernate/Shesha.NHibernate.csproj b/shesha-core/src/Shesha.NHibernate/Shesha.NHibernate.csproj index fd7e09f1bd..1bfa5b60b3 100644 --- a/shesha-core/src/Shesha.NHibernate/Shesha.NHibernate.csproj +++ b/shesha-core/src/Shesha.NHibernate/Shesha.NHibernate.csproj @@ -20,15 +20,23 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -36,6 +44,7 @@ + @@ -49,4 +58,8 @@ + + + + diff --git a/shesha-core/src/Shesha.RestSharp/RestSharp/ApiHelper.cs b/shesha-core/src/Shesha.RestSharp/RestSharp/ApiHelper.cs index e1dece319d..36c466ebc9 100644 --- a/shesha-core/src/Shesha.RestSharp/RestSharp/ApiHelper.cs +++ b/shesha-core/src/Shesha.RestSharp/RestSharp/ApiHelper.cs @@ -10,11 +10,11 @@ namespace Shesha.RestSharp /// /// /// - public class ApiHelper where PT: class + public class ApiHelper where PT : class { - public async Task PostOrPutMethodAsync(Method httpMethod, PT postObj, List headers, string apiMethod) + public async Task PostOrPutMethodAsync(Method httpMethod, PT postObj, List headers, string apiMethod) { - var client = new RestClient(apiMethod); + using var client = new RestClient(apiMethod); var request = new RestRequest() { Method = httpMethod }; foreach (var header in headers) @@ -26,16 +26,16 @@ public async Task PostOrPutMethodAsync(Method httpMethod, PT postObj, List(response.Content); } return default; } - public async Task GetApiMethodAsync(string apiMethod, List headers) + public async Task GetApiMethodAsync(string apiMethod, List headers) { - var client = new RestClient(apiMethod); + using var client = new RestClient(apiMethod); var request = new RestRequest() { Method = Method.Get }; foreach (var header in headers) @@ -45,7 +45,7 @@ public async Task GetApiMethodAsync(string apiMethod, List heade request.AddHeader("Cache-Control", "no-cache"); request.AddHeader("ContentType", "application/json"); var response = await client.ExecuteAsync(request); - if (response.StatusCode == System.Net.HttpStatusCode.OK) + if (response.StatusCode == System.Net.HttpStatusCode.OK && !string.IsNullOrWhiteSpace(response.Content)) { return JsonConvert.DeserializeObject(response.Content); } diff --git a/shesha-core/src/Shesha.RestSharp/Shesha.RestSharp.csproj b/shesha-core/src/Shesha.RestSharp/Shesha.RestSharp.csproj index 17b93f3475..8cb2ae4e14 100644 --- a/shesha-core/src/Shesha.RestSharp/Shesha.RestSharp.csproj +++ b/shesha-core/src/Shesha.RestSharp/Shesha.RestSharp.csproj @@ -9,10 +9,18 @@ README.md - + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.Scheduler/Attributes/ForwardDisableConcurrentExecutionAttribute.cs b/shesha-core/src/Shesha.Scheduler/Attributes/ForwardDisableConcurrentExecutionAttribute.cs index aa9639883e..a636ce2666 100644 --- a/shesha-core/src/Shesha.Scheduler/Attributes/ForwardDisableConcurrentExecutionAttribute.cs +++ b/shesha-core/src/Shesha.Scheduler/Attributes/ForwardDisableConcurrentExecutionAttribute.cs @@ -6,7 +6,6 @@ using Shesha.Services; using System; using System.Linq; -using System.Reflection; namespace Shesha.Scheduler.Attributes { @@ -31,23 +30,17 @@ public void OnPerforming(PerformingContext filterContext) var jobType = jobManager.GetJobType(triggerId); - var jobAttribute = jobType.GetAttribute(); + var jobAttribute = jobType.GetAttributeOrNull(); if (jobAttribute == null) throw new NotSupportedException($"Job '{jobType.FullName}' must be decorated with '{nameof(ScheduledJobAttribute)}'"); - var disableConcurrentAttribute = jobType.GetAttribute(); + var disableConcurrentAttribute = jobType.GetAttributeOrNull(); if (disableConcurrentAttribute == null) return; var resource = jobAttribute.Uid.ToString(); - var timeoutField = typeof(DisableConcurrentExecutionAttribute).GetField("_timeoutInSeconds", BindingFlags.NonPublic | BindingFlags.Instance); - if (timeoutField == null) - throw new NotSupportedException($"Failed to find timeout field on the '{nameof(DisableConcurrentExecutionAttribute)}'"); - - var timeoutSeconds = (int)timeoutField.GetValue(disableConcurrentAttribute); - - var timeout = TimeSpan.FromSeconds(timeoutSeconds); + var timeout = TimeSpan.FromSeconds(disableConcurrentAttribute.TimeoutSec); var distributedLock = filterContext.Connection.AcquireDistributedLock(resource, timeout); filterContext.Items[DistributedLockKey] = distributedLock; diff --git a/shesha-core/src/Shesha.Scheduler/Bootstrappers/ScheduledJobBootstrapper.cs b/shesha-core/src/Shesha.Scheduler/Bootstrappers/ScheduledJobBootstrapper.cs index 8aa9dd07cb..5fdeac5725 100644 --- a/shesha-core/src/Shesha.Scheduler/Bootstrappers/ScheduledJobBootstrapper.cs +++ b/shesha-core/src/Shesha.Scheduler/Bootstrappers/ScheduledJobBootstrapper.cs @@ -63,8 +63,7 @@ private async Task DoProcessAsync() { Class = e, Attribute = e.GetAttribute() - }) - .Where(x => !_startupSession.AssemblyStaysUnchanged(x.Class.Assembly)) + }) .ToList(); // deactivate all jobs which are missing in the code @@ -113,14 +112,15 @@ private async Task DoProcessAsync() JobDescription = jobInfo.Attribute.Description, LogMode = jobInfo.Attribute.LogMode, LogFolder = jobInfo.Attribute.LogFolder, - JobType = jobInfo.Class.FullName + JobType = jobInfo.Class.GetRequiredFullName() }; await _jobRepo.InsertAsync(job); // create a trigger if (jobInfo.Attribute.StartupMode == StartUpMode.Automatic && - CronStringHelper.IsValidCronExpression(jobInfo.Attribute.CronString ?? string.Empty)) + !string.IsNullOrWhiteSpace(jobInfo.Attribute.CronString) && + CronStringHelper.IsValidCronExpression(jobInfo.Attribute.CronString)) { var trigger = new ScheduledJobTrigger() { diff --git a/shesha-core/src/Shesha.Scheduler/Hangfire/HangfireAuthorizationFilter.cs b/shesha-core/src/Shesha.Scheduler/Hangfire/HangfireAuthorizationFilter.cs index d4fff8c863..b0c1ab8635 100644 --- a/shesha-core/src/Shesha.Scheduler/Hangfire/HangfireAuthorizationFilter.cs +++ b/shesha-core/src/Shesha.Scheduler/Hangfire/HangfireAuthorizationFilter.cs @@ -1,9 +1,9 @@ using Abp.Authorization; -using Abp.Dependency; using Abp.Runtime.Session; using Hangfire.Dashboard; using Shesha.Authorization; using Shesha.Authorization.Users; +using Shesha.Services; namespace Shesha.Scheduler.Hangfire { @@ -11,23 +11,23 @@ public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter { public bool Authorize(DashboardContext context) { - var username = context.GetHttpContext() - .GetUsernameFromJwtToken(); + var username = context.GetHttpContext().GetUsernameFromJwtToken(); if (string.IsNullOrEmpty(username) || string.IsNullOrWhiteSpace(username)) return false; - var userManager = IocManager.Instance.Resolve(); + var iocResolver = StaticContext.IocManager; + var userManager = iocResolver.Resolve(); var user = userManager.FindByNameOrEmail(username); if (user == null) return false; - var session = IocManager.Instance.Resolve(); + var session = iocResolver.Resolve(); var isGranted = false; using (session.Use(user.TenantId, user.Id)) { - var permissionChecker = IocManager.Instance.Resolve(); + var permissionChecker = iocResolver.Resolve(); if (permissionChecker == null) return false; diff --git a/shesha-core/src/Shesha.Scheduler/IScheduledJobManager.cs b/shesha-core/src/Shesha.Scheduler/IScheduledJobManager.cs index 471e5500e7..0f7a913c6c 100644 --- a/shesha-core/src/Shesha.Scheduler/IScheduledJobManager.cs +++ b/shesha-core/src/Shesha.Scheduler/IScheduledJobManager.cs @@ -59,9 +59,14 @@ public interface IScheduledJobManager /// Get job type by Id /// /// Job Id - /// Type GetJobTypeById(Guid id); + /// + /// Get job type by Id + /// + /// Job Id + Type? GetJobTypeByIdOrNull(Guid id); + /// /// Get job instance by Id /// diff --git a/shesha-core/src/Shesha.Scheduler/Logging/ScheduledJobLogger.cs b/shesha-core/src/Shesha.Scheduler/Logging/ScheduledJobLogger.cs index 6ba14f67ee..b0092b8b76 100644 --- a/shesha-core/src/Shesha.Scheduler/Logging/ScheduledJobLogger.cs +++ b/shesha-core/src/Shesha.Scheduler/Logging/ScheduledJobLogger.cs @@ -3,6 +3,7 @@ using log4net; using log4net.Core; using log4net.Util; +using Shesha.Reflection; using ILogger = Castle.Core.Logging.ILogger; namespace Shesha.Scheduler.Logging @@ -14,7 +15,7 @@ public class ScheduledJobLogger : { private static readonly Type DeclaringType = typeof(ScheduledJobLogger); - public ScheduledJobLogger(log4net.Core.ILogger logger, ScheduledJobLoggerFactory factory) + public ScheduledJobLogger(log4net.Core.ILogger logger, ScheduledJobLoggerFactory? factory) { Logger = logger; Factory = factory; @@ -24,7 +25,7 @@ internal ScheduledJobLogger() { } - internal ScheduledJobLogger(ILog log, ScheduledJobLoggerFactory factory) + internal ScheduledJobLogger(ILog log, ScheduledJobLoggerFactory? factory) : this(log.Logger, factory) { } @@ -39,18 +40,18 @@ internal ScheduledJobLogger(ILog log, ScheduledJobLoggerFactory factory) public bool IsWarnEnabled => Logger.IsEnabledFor(Level.Warn); - protected internal ScheduledJobLoggerFactory Factory { get; set; } + protected internal ScheduledJobLoggerFactory? Factory { get; set; } protected internal log4net.Core.ILogger Logger { get; set; } - public override string ToString() + public override string? ToString() { return Logger.ToString(); } public virtual global::Castle.Core.Logging.ILogger CreateChildLogger(string name) { - return Factory.Create(Logger.Name + "." + name); + return Factory.NotNull().Create(Logger.Name + "." + name); } public void Trace(string message) diff --git a/shesha-core/src/Shesha.Scheduler/Logging/ScheduledJobLoggerFactory.cs b/shesha-core/src/Shesha.Scheduler/Logging/ScheduledJobLoggerFactory.cs index 0dfcdf3668..6e729e3ee7 100644 --- a/shesha-core/src/Shesha.Scheduler/Logging/ScheduledJobLoggerFactory.cs +++ b/shesha-core/src/Shesha.Scheduler/Logging/ScheduledJobLoggerFactory.cs @@ -1,11 +1,11 @@ -using System; -using System.IO; -using System.Xml; -using Abp.Reflection.Extensions; +using Abp.Reflection.Extensions; using Castle.Core.Logging; using log4net; using log4net.Config; using log4net.Repository; +using System; +using System.IO; +using System.Xml; namespace Shesha.Scheduler.Logging { @@ -27,7 +27,8 @@ public ScheduledJobLoggerFactory(string configFileName) ); var log4NetConfig = new XmlDocument(); - log4NetConfig.Load(File.OpenRead(configFileName)); + using var configFile = File.OpenRead(configFileName); + log4NetConfig.Load(configFile); XmlConfigurator.Configure(_loggerRepository, log4NetConfig["log4net"]); } @@ -45,7 +46,8 @@ public ScheduledJobLoggerFactory(string configFileName, bool reloadOnChange) else { var log4NetConfig = new XmlDocument(); - log4NetConfig.Load(File.OpenRead(configFileName)); + using var configFile = File.OpenRead(configFileName); + log4NetConfig.Load(configFile); XmlConfigurator.Configure(_loggerRepository, log4NetConfig["log4net"]); } } diff --git a/shesha-core/src/Shesha.Scheduler/ScheduledJobManager.cs b/shesha-core/src/Shesha.Scheduler/ScheduledJobManager.cs index 2ab34043a4..b4dac4e323 100644 --- a/shesha-core/src/Shesha.Scheduler/ScheduledJobManager.cs +++ b/shesha-core/src/Shesha.Scheduler/ScheduledJobManager.cs @@ -46,7 +46,7 @@ public class ScheduledJobManager: IScheduledJobManager, ITransientDependency private readonly IIocManager _iocManager; private readonly IServiceProvider _serviceProvider; - public Castle.Core.Logging.ILogger Logger { get; set; } + public ILogger Logger { get; set; } public ScheduledJobManager(IRepository triggerRepository, IUnitOfWorkManager unitOfWorkManager, ITypeFinder typeFinder, IIocManager iocManager, IServiceProvider serviceProvider) { @@ -60,8 +60,8 @@ public ScheduledJobManager(IRepository triggerReposit private const string JobExecutionIdKey = "JobExecutionId"; - private IHubContext _signalrHub; - internal IHubContext SignalrHub => _signalrHub ??= _iocManager?.Resolve>(); + private IHubContext? _signalrHub; + internal IHubContext SignalrHub => _signalrHub ??= _iocManager.Resolve>(); /// inheritedDoc public async Task EnqueueAllAsync() @@ -73,13 +73,16 @@ public async Task EnqueueAllAsync() .ToListAsync(); // remove all unused triggers - var allRecurringJobs = JobStorage.Current.GetConnection().GetRecurringJobs(); - var jobsToRemove = allRecurringJobs.Where(j => activeTriggers.All(t => t.Id.ToString() != j.Id)).ToList(); - - foreach (var jobDto in jobsToRemove) - { - RecurringJob.RemoveIfExists(jobDto.Id); - } + using (var storageConnection = JobStorage.Current.GetConnection()) + { + var allRecurringJobs = storageConnection.GetRecurringJobs(); + var jobsToRemove = allRecurringJobs.Where(j => activeTriggers.All(t => t.Id.ToString() != j.Id)).ToList(); + + foreach (var jobDto in jobsToRemove) + { + RecurringJob.RemoveIfExists(jobDto.Id); + } + } // update existing triggers foreach (var trigger in activeTriggers) @@ -121,7 +124,7 @@ public Type GetJobType(Guid triggerId) uow.Complete(); } - var jobType = _typeFinder.Find(t => t.GetAttribute()?.Uid == jobId).FirstOrDefault(); + var jobType = _typeFinder.Find(t => t.GetAttributeOrNull()?.Uid == jobId).FirstOrDefault(); if (jobType == null) throw new Exception($"Job with Id = '{jobId}' not found"); @@ -182,10 +185,10 @@ public async Task RunJobAsync(Guid jobId, Guid executionId, Int64? startedById, [ForwardDisableConcurrentExecution] public async Task RunJobAsync(Guid jobId, string jobType, Guid executionId, Int64? startedById, CancellationToken cancellationToken) { - await ExecuteJobMethodAsync(jobId, jobType, "ExecuteAsync", new object[] { executionId, startedById, cancellationToken }); + await ExecuteJobMethodAsync(jobId, jobType, "ExecuteAsync", [executionId, startedById, cancellationToken]); } - public async Task ExecuteJobMethodAsync(Guid jobId, string jobType, string methodName, object[] methodArgs) + public async Task ExecuteJobMethodAsync(Guid jobId, string jobType, string methodName, object?[] methodArgs) { var recordedType = !string.IsNullOrEmpty(jobType) ? Type.GetType(jobType) : GetJobTypeById(jobId).BaseType; @@ -193,28 +196,31 @@ public async Task ExecuteJobMethodAsync(Guid jobId, string jobType, string metho { var jobInstanceType = GetJobTypeById(jobId); var jobInstance = _serviceProvider.GetService(jobInstanceType); - var methodInfo = recordedType.GetMethod(methodName); + var methodInfo = recordedType.GetRequiredMethod(methodName); - Task task = (Task)methodInfo.Invoke(jobInstance, methodArgs); + Task task = methodInfo.Invoke(jobInstance, methodArgs).NotNull(); await task; } } /// inheritedDoc - public Type GetJobTypeById(Guid id) + public Type? GetJobTypeByIdOrNull(Guid id) { - return _typeFinder.Find(t => t.GetAttribute()?.Uid == id).FirstOrDefault(); + return _typeFinder.Find(t => t.GetAttributeOrNull()?.Uid == id).FirstOrDefault(); + } + + // inheritedDoc + public Type GetJobTypeById(Guid id) + { + return GetJobTypeByIdOrNull(id) ?? throw new Exception($"Type of job with id = '{id}' is unavailable"); } /// inheritedDoc public ScheduledJobBase GetJobInstanceById(Guid id) { var jobType = GetJobTypeById(id); - if (jobType == null) - throw new Exception($"Job with Id = '{id}' not found"); - - var jobInstance = _iocManager.Resolve(jobType) as ScheduledJobBase; + var jobInstance = _iocManager.Resolve(jobType).ForceCastAs(); return jobInstance; } @@ -239,8 +245,8 @@ private void ConfigureLogger(string name, out Logger logger, out ILog log) repository.Root.Level = log4net.Core.Level.Info; - logger = repository.GetCurrentLoggers().FirstOrDefault(l => l.Name == name) as Logger; - if (logger == null) + var existingLogger = repository.GetCurrentLoggers().FirstOrDefault(l => l.Name == name) as Logger; + if (existingLogger == null) { // Configure default logger for scheduled job logger = repository.LoggerFactory.CreateLogger(repository, name); @@ -256,7 +262,8 @@ private void ConfigureLogger(string name, out Logger logger, out ILog log) // Mark repository as configured and notify that is has changed. repository.Configured = true; repository.RaiseConfigurationChanged(EventArgs.Empty); - } + } else + logger = existingLogger; log = LogManager.GetLogger(Assembly.GetCallingAssembly(), name); } @@ -317,7 +324,7 @@ private void AddInstanceAppenders(ScheduledJobBase job, Logger logger) #region Configure file appender - FileAppender fileAppender = null; + FileAppender? fileAppender = null; if (!string.IsNullOrWhiteSpace(job.LogFilePath)) { diff --git a/shesha-core/src/Shesha.Scheduler/Services/ScheduledJobs/ScheduledJobAppService.cs b/shesha-core/src/Shesha.Scheduler/Services/ScheduledJobs/ScheduledJobAppService.cs index bbc1bb49b7..4f60b329d2 100644 --- a/shesha-core/src/Shesha.Scheduler/Services/ScheduledJobs/ScheduledJobAppService.cs +++ b/shesha-core/src/Shesha.Scheduler/Services/ScheduledJobs/ScheduledJobAppService.cs @@ -3,7 +3,6 @@ using Abp.Domain.Uow; using Hangfire; using Microsoft.AspNetCore.Mvc; -using Shesha.Authorization.Users; using Shesha.DynamicEntities.Dtos; using Shesha.Scheduler.Attributes; using Shesha.Scheduler.Bootstrappers; @@ -23,8 +22,6 @@ public class ScheduledJobAppService : DynamicCrudAppService _jobRepo; - public IRepository UserRepository { get; set; } - /// /// Default constructor /// @@ -80,7 +77,7 @@ public async Task RunTriggerAsync(Guid triggerId, CancellationToken cancellation { var triggerService = IocManager.Resolve>(); Guid jobId; - string jobType = null; + string? jobType = null; using (var uow = UnitOfWorkManager.Begin()) { diff --git a/shesha-core/src/Shesha.Scheduler/Services/ScheduledJobs/ScheduledJobExecutionAppService.cs b/shesha-core/src/Shesha.Scheduler/Services/ScheduledJobs/ScheduledJobExecutionAppService.cs index f952a2cd5d..62ff6533da 100644 --- a/shesha-core/src/Shesha.Scheduler/Services/ScheduledJobs/ScheduledJobExecutionAppService.cs +++ b/shesha-core/src/Shesha.Scheduler/Services/ScheduledJobs/ScheduledJobExecutionAppService.cs @@ -68,7 +68,7 @@ public async Task> GetEventLogItemsAsync(Guid id) } - var logItems = JsonConvert.DeserializeObject>("[" + logFileContent + "]"); + var logItems = JsonConvert.DeserializeObject>("[" + logFileContent + "]") ?? []; return logItems; } @@ -78,7 +78,7 @@ public async Task> GetEventLogItemsAsync(Guid id) /// /// /// - public async Task GetExecutionStatisticsAsync(Guid id) + public async Task GetExecutionStatisticsAsync(Guid id) { if (id == Guid.Empty) return null; @@ -108,9 +108,12 @@ public async Task DownloadLogFileAsync(Guid id) var fileVersion = jobExecution.LogFile.LastVersion(); if (fileVersion == null) - throw new Exception("File version not found"); - - var fileContentStream = await _storedFileService.GetStreamAsync(fileVersion); + throw new Exception("File version not found"); + +#pragma warning disable IDISP001 // Dispose created + var fileContentStream = await _storedFileService.GetStreamAsync(fileVersion); +#pragma warning restore IDISP001 // Dispose created + await _storedFileService.MarkDownloadedAsync(fileVersion); result = new FileStreamResult(fileContentStream, fileVersion.FileType.GetContentType()) @@ -124,9 +127,12 @@ public async Task DownloadLogFileAsync(Guid id) throw new EntityNotFoundException("Path to the log file for the specified job execution is not specified"); if (!File.Exists(jobExecution.LogFilePath)) - throw new EntityNotFoundException("Log file is missing on disk"); + throw new EntityNotFoundException("Log file is missing on disk"); + +#pragma warning disable IDISP001 // Dispose created + var stream = new FileStream(jobExecution.LogFilePath, FileMode.Open); +#pragma warning restore IDISP001 // Dispose created - var stream = new FileStream(jobExecution.LogFilePath, FileMode.Open); var fileName = Path.GetFileName(jobExecution.LogFilePath); var contentType = _mimeMappingService.Map(fileName); diff --git a/shesha-core/src/Shesha.Scheduler/Shesha.Scheduler.csproj b/shesha-core/src/Shesha.Scheduler/Shesha.Scheduler.csproj index 1675ab788e..73359390fe 100644 --- a/shesha-core/src/Shesha.Scheduler/Shesha.Scheduler.csproj +++ b/shesha-core/src/Shesha.Scheduler/Shesha.Scheduler.csproj @@ -23,19 +23,27 @@ - + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.Scheduler/SignalR/SignalrAppender.cs b/shesha-core/src/Shesha.Scheduler/SignalR/SignalrAppender.cs index 2f5e157716..15f2c2d53c 100644 --- a/shesha-core/src/Shesha.Scheduler/SignalR/SignalrAppender.cs +++ b/shesha-core/src/Shesha.Scheduler/SignalR/SignalrAppender.cs @@ -7,21 +7,19 @@ namespace Shesha.Scheduler.SignalR { public class SignalrAppender : AppenderSkeleton { - //private readonly List - private readonly IHubContext _hub; /// /// Name of the signalR group /// - public readonly string GroupName; + public readonly string? GroupName; /// /// Default constructor /// /// /// - public SignalrAppender(IHubContext hub, string groupName = null) + public SignalrAppender(IHubContext hub, string? groupName = null) { _hub = hub; GroupName = groupName; diff --git a/shesha-core/src/Shesha.Sms.Clickatell/Clickatell/ClickatellSmsGateway.cs b/shesha-core/src/Shesha.Sms.Clickatell/Clickatell/ClickatellSmsGateway.cs index 19ff66bed2..2801cef525 100644 --- a/shesha-core/src/Shesha.Sms.Clickatell/Clickatell/ClickatellSmsGateway.cs +++ b/shesha-core/src/Shesha.Sms.Clickatell/Clickatell/ClickatellSmsGateway.cs @@ -22,15 +22,15 @@ public ClickatellSmsGateway(IClickatellSettings settings) { Logger = NullLogger.Instance; _settings = settings; - } - + } + /// /// Sends an SMS message. /// /// Mobile number to send message to. Must be a South African number. /// Message to be sent. /// Returns true if message was accepted by the gateway, else returns false. - public override async Task SendSmsAsync(string mobileNumber, string body) + public override async Task SendSmsAsync(string mobileNumber, string body) { var settings = await _settings.ClickatellGateway.GetValueAsync(); @@ -43,24 +43,34 @@ public override async Task SendSmsAsync(string mobileNumber, string query["user"] = settings.Username; query["password"] = settings.Password; query["to"] = MobileHelper.CleanupMobileNo(mobileNumber); - //query["text"] = HttpUtility.UrlEncode(body); query["text"] = body; - var url = $"https://api.clickatell.com/http/sendmsg?{query}"; + // Calculate message parts based on length + int messagePartsCount = 1; + if (body.Length > settings.SingleMessageMaxLength) + { + // Calculate number of parts needed for multipart message + messagePartsCount = (int)Math.Ceiling((double)body.Length / settings.MessagePartLength); + } + + // Add concat parameter for multipart messages + if (messagePartsCount > 1) + { + query["concat"] = messagePartsCount.ToString(); + } + var url = $"https://api.clickatell.com/http/sendmsg?{query}"; try { // Send the GET request - var response = await httpClient.GetAsync(url); - + using var response = await httpClient.GetAsync(url); var responseContent = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode && responseContent.StartsWith("ID")) { - Logger.InfoFormat("SMS successfully sent, response: {0}", response); + Logger.InfoFormat("SMS successfully sent as {0} part(s), response: {1}", + messagePartsCount, responseContent); return SendStatus.Success(); } - var errorMessage = $"Could not send SMS to '{mobileNumber}'. Response: '{responseContent}'"; Logger.ErrorFormat(errorMessage); return SendStatus.Failed(errorMessage); @@ -69,7 +79,7 @@ public override async Task SendSmsAsync(string mobileNumber, string { Logger.ErrorFormat($"An error occurred: {ex.Message}"); return SendStatus.Failed($"An error occurred: {ex.Message}"); - } + } } public override async Task GetTypedSettingsAsync() diff --git a/shesha-core/src/Shesha.Sms.Clickatell/Shesha.Sms.Clickatell.csproj b/shesha-core/src/Shesha.Sms.Clickatell/Shesha.Sms.Clickatell.csproj index 7fdeea60b6..ce48a53340 100644 --- a/shesha-core/src/Shesha.Sms.Clickatell/Shesha.Sms.Clickatell.csproj +++ b/shesha-core/src/Shesha.Sms.Clickatell/Shesha.Sms.Clickatell.csproj @@ -10,7 +10,7 @@ - + @@ -19,7 +19,15 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.Web.Core/Authorization/SheshaWebCoreAuthorizationProvider.cs b/shesha-core/src/Shesha.Web.Core/Authorization/SheshaWebCoreAuthorizationProvider.cs index 81ec30085b..a1a21626e8 100644 --- a/shesha-core/src/Shesha.Web.Core/Authorization/SheshaWebCoreAuthorizationProvider.cs +++ b/shesha-core/src/Shesha.Web.Core/Authorization/SheshaWebCoreAuthorizationProvider.cs @@ -29,7 +29,8 @@ public override void SetPermissions(IPermissionDefinitionContext context) foreach (var permission in permissions) { - context.CreatePermission(permission.Name, L(permission.Description ?? permission.Name)); + if (!string.IsNullOrWhiteSpace(permission.Name)) + context.CreatePermission(permission.Name, L(permission.Description ?? permission.Name)); } } diff --git a/shesha-core/src/Shesha.Web.Core/Authorization/SheshaWebCorePermissionChecker.cs b/shesha-core/src/Shesha.Web.Core/Authorization/SheshaWebCorePermissionChecker.cs index 1bd4718555..ccce9865b0 100644 --- a/shesha-core/src/Shesha.Web.Core/Authorization/SheshaWebCorePermissionChecker.cs +++ b/shesha-core/src/Shesha.Web.Core/Authorization/SheshaWebCorePermissionChecker.cs @@ -32,7 +32,7 @@ public SheshaWebCorePermissionChecker(IRepository personRepository /// inheritedDoc public async Task IsGrantedAsync(long userId, string permissionName) { - var person = await _personRepository.FirstOrDefaultAsync(p => p.User.Id == userId); + var person = await _personRepository.FirstOrDefaultAsync(p => p.User != null && p.User.Id == userId); if (person == null) return false; @@ -49,7 +49,7 @@ public async Task IsGrantedAsync(long userId, string permissionName) public async Task IsInAnyOfRolesAsync(Person person, params string[] roles) { return await _rolePersonRepository.GetAll() - .Where(e => roles.Contains(e.Role.Name) && e.Person == person) + .Where(e => e.Role != null && roles.Contains(e.Role.Name) && e.Person == person) .AnyAsync(); } diff --git a/shesha-core/src/Shesha.Web.Core/Identity/ExternalLoginInfoHelper.cs b/shesha-core/src/Shesha.Web.Core/Identity/ExternalLoginInfoHelper.cs deleted file mode 100644 index 8b146a6a46..0000000000 --- a/shesha-core/src/Shesha.Web.Core/Identity/ExternalLoginInfoHelper.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Abp.Extensions; - -namespace Shesha.Identity -{ - public class ExternalLoginInfoHelper - { - public static (string name, string surname) GetNameAndSurnameFromClaims(List claims) - { - string name = null; - string surname = null; - - var givennameClaim = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName); - if (givennameClaim != null && !givennameClaim.Value.IsNullOrEmpty()) - { - name = givennameClaim.Value; - } - - var surnameClaim = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname); - if (surnameClaim != null && !surnameClaim.Value.IsNullOrEmpty()) - { - surname = surnameClaim.Value; - } - - if (name == null || surname == null) - { - var nameClaim = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name); - if (nameClaim != null) - { - var nameSurName = nameClaim.Value; - if (!nameSurName.IsNullOrEmpty()) - { - var lastSpaceIndex = nameSurName.LastIndexOf(' '); - if (lastSpaceIndex < 1 || lastSpaceIndex > (nameSurName.Length - 2)) - { - name = surname = nameSurName; - } - else - { - name = nameSurName.Substring(0, lastSpaceIndex); - surname = nameSurName.Substring(lastSpaceIndex); - } - } - } - } - - return (name, surname); - } - } -} diff --git a/shesha-core/src/Shesha.Web.Core/Shesha.Web.Core.csproj b/shesha-core/src/Shesha.Web.Core/Shesha.Web.Core.csproj index e777ad55c7..a166810cad 100644 --- a/shesha-core/src/Shesha.Web.Core/Shesha.Web.Core.csproj +++ b/shesha-core/src/Shesha.Web.Core/Shesha.Web.Core.csproj @@ -24,7 +24,7 @@ - + @@ -40,10 +40,18 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.Web.Core/SheshaWebCoreModule.cs b/shesha-core/src/Shesha.Web.Core/SheshaWebCoreModule.cs index 4e01f9a67d..3e52ffa6ce 100644 --- a/shesha-core/src/Shesha.Web.Core/SheshaWebCoreModule.cs +++ b/shesha-core/src/Shesha.Web.Core/SheshaWebCoreModule.cs @@ -59,15 +59,6 @@ public override void PreInitialize() ConfigureTokenAuth(); - /* - Configuration.Modules.AbpAutoMapper().Configurators.Add(config => - { - config.CreateMap() - .ForMember(u => u.Password, options => options.Ignore()) - .ForMember(u => u.Email, options => options.MapFrom(input => input.EmailAddress)); - }); - */ - Configuration.Modules.AbpAspNetCore().CreateControllersForAppServices( this.GetType().Assembly, moduleName: "Shesha", @@ -80,9 +71,9 @@ private void ConfigureTokenAuth() var tokenAuthConfig = IocManager.Resolve(); - tokenAuthConfig.SecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_appConfiguration["Authentication:JwtBearer:SecurityKey"])); - tokenAuthConfig.Issuer = _appConfiguration["Authentication:JwtBearer:Issuer"]; - tokenAuthConfig.Audience = _appConfiguration["Authentication:JwtBearer:Audience"]; + tokenAuthConfig.SecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_appConfiguration.GetRequired("Authentication:JwtBearer:SecurityKey"))); + tokenAuthConfig.Issuer = _appConfiguration.GetRequired("Authentication:JwtBearer:Issuer"); + tokenAuthConfig.Audience = _appConfiguration.GetRequired("Authentication:JwtBearer:Audience"); tokenAuthConfig.SigningCredentials = new SigningCredentials(tokenAuthConfig.SecurityKey, SecurityAlgorithms.HmacSha256); tokenAuthConfig.Expiration = TimeSpan.FromDays(1); } diff --git a/shesha-core/src/Shesha.Web.Core/SolutionGenerator/SolutionGenerator.cs b/shesha-core/src/Shesha.Web.Core/SolutionGenerator/SolutionGenerator.cs index 3d39d5fa5e..a082ca12d0 100644 --- a/shesha-core/src/Shesha.Web.Core/SolutionGenerator/SolutionGenerator.cs +++ b/shesha-core/src/Shesha.Web.Core/SolutionGenerator/SolutionGenerator.cs @@ -1,13 +1,11 @@ -using System; +using Abp.Dependency; +using Shesha.Utilities; +using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; -using System.IO.Pipelines; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Abp.Dependency; -using Shesha.Utilities; namespace Shesha.Tests.SolutionGenerator { @@ -46,7 +44,7 @@ public async Task GenerateAsync( private async Task PrepareFileAndCopyAsync(string path, Stream dstStream, Dictionary tags, CancellationToken cancellationToken) { var extension = Path.GetExtension(path)?.ToLower(); - if (_sourceFileExtensions.Contains(extension)) + if (!string.IsNullOrWhiteSpace(extension) && _sourceFileExtensions.Contains(extension)) { using (var sr = new StreamReader(path)) { @@ -80,7 +78,8 @@ private void FillFilesListRecursive(string basePath, string directory, Func//, IValidatableObject + public class ComplexTestDto : DynamicDto { public virtual string Name { get; set; } @@ -92,32 +92,8 @@ public override EntityHistoryEventInfo CreateEvent(EntityChangesInfo - { - [SaveAsJson] - public override JsonEntity Any { get; set; } - } - - [Table("Test_ComplexTest")] - public class ComplexPersonTest : BaseComplexTest - { - [SaveAsJson] - [Column("Any")] - public override JsonPerson Any { get; set; } } - [Table("Test_ComplexTest")] - public class ComplexAddressTest : BaseComplexTest - { - [SaveAsJson] - public override JsonPerson Any { get; set; } - }*/ - public class InnerComplexTest { public virtual string ComplexName { get; set; } @@ -145,20 +121,18 @@ public class JsonOrganisation : JsonBase public class JsonPerson : JsonBase { - public virtual Person Person { get; set; } + public virtual Person? Person { get; set; } public virtual JsonAddress Address { get; set; } public virtual List Persons { get; set; } - //public virtual JsonPerson JPerson { get; set; } - public virtual string FirstName { get; set; } public virtual string LastName { get; set; } } public class JsonEntityReference where T : IEntity { - private PropertyInfo _displayNamePropertyInfo; + private PropertyInfo? _displayNamePropertyInfo; public JsonEntityReference() { _displayNamePropertyInfo = typeof(T).GetEntityConfiguration()?.DisplayNamePropertyInfo; @@ -167,8 +141,12 @@ public JsonEntityReference() [JsonIgnore] public virtual T Entity { get; set; } - public virtual TId Id => Entity != null ? Entity.Id : default; + public virtual TId? Id => Entity != null ? Entity.Id : default; - public virtual string DisplayName => Entity != null && _displayNamePropertyInfo != null ? _displayNamePropertyInfo.GetValue(Entity).ToString() : Entity.ToString(); + public virtual string? DisplayName => Entity != null + ? _displayNamePropertyInfo != null + ? _displayNamePropertyInfo.GetValue(Entity)?.ToString() + : Entity.ToString() + : null; } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Web.Core/_Test/ComplexTestAppService.cs b/shesha-core/src/Shesha.Web.Core/_Test/ComplexTestAppService.cs index 8feba40d57..0f7bcc3a63 100644 --- a/shesha-core/src/Shesha.Web.Core/_Test/ComplexTestAppService.cs +++ b/shesha-core/src/Shesha.Web.Core/_Test/ComplexTestAppService.cs @@ -71,11 +71,11 @@ public object GetDifferentEntities() public Task> CheckGenericEntityAsync() { var entities = Repository.GetAll().Where(x => x.AnyEntity != null).ToList(); - var entity = entities.FirstOrDefault(x => x.AnyEntity._className == typeof(Person).FullName && x.AnyEntity.Id == "b3b60f2e-5b88-4f44-b8eb-d3987a8483d9"); + var entity = entities.First(x => x.AnyEntity._className == typeof(Person).FullName && x.AnyEntity.Id == "b3b60f2e-5b88-4f44-b8eb-d3987a8483d9"); var person = (Person)entity.AnyEntity; - var dbEntity = Repository.GetAll().FirstOrDefault(x => x.AnyEntity._className == typeof(Person).FullName && x.AnyEntity.Id == "b3b60f2e-5b88-4f44-b8eb-d3987a8483d9"); + var dbEntity = Repository.GetAll().First(x => x.AnyEntity._className == typeof(Person).FullName && x.AnyEntity.Id == "b3b60f2e-5b88-4f44-b8eb-d3987a8483d9"); var dbPerson = (Person)dbEntity.AnyEntity; @@ -186,7 +186,7 @@ public async Task CreateTableAsync() using (var con = new SqlConnection(connStr)) { await con.OpenAsync(); - var backupCommand = new SqlCommand(@" + using var backupCommand = new SqlCommand(@" CREATE TABLE [dbo].[Test_ComplexTest]( [Id] [uniqueidentifier] NOT NULL, [Name] [nvarchar](100) NULL, @@ -215,7 +215,7 @@ public async Task DeleteTableAsync() using (var con = new SqlConnection(connStr)) { await con.OpenAsync(); - var backupCommand = new SqlCommand("DROP TABLE [dbo].[Test_ComplexTest]", con); + using var backupCommand = new SqlCommand("DROP TABLE [dbo].[Test_ComplexTest]", con); await backupCommand.ExecuteNonQueryAsync(); } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/ConfigurableComponentDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/ConfigurableComponentDto.cs index bddab89248..5ac96686be 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/ConfigurableComponentDto.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/ConfigurableComponentDto.cs @@ -11,21 +11,21 @@ public class ConfigurableComponentDto : EntityDto /// /// Form name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Settings in JSON format /// - public string Settings { get; set; } + public string? Settings { get; set; } /// /// Cache MD5, is used for client-side caching /// - public string CacheMd5 { get; set; } + public string? CacheMd5 { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/ConfigurableComponentUpdateSettingsInput.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/ConfigurableComponentUpdateSettingsInput.cs index 35fdcabbc4..e4105f56b7 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/ConfigurableComponentUpdateSettingsInput.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/ConfigurableComponentUpdateSettingsInput.cs @@ -1,7 +1,4 @@ -using System; -using Abp.Application.Services.Dto; - -namespace Shesha.Web.FormsDesigner.Dtos +namespace Shesha.Web.FormsDesigner.Dtos { /// /// Update component settings input @@ -11,12 +8,12 @@ public class ConfigurableComponentUpdateSettingsInput /// /// Module name /// - public string Module { get; set; } + public string? Module { get; set; } /// /// Component name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// If true, indicates that component is application specific @@ -26,6 +23,6 @@ public class ConfigurableComponentUpdateSettingsInput /// /// Settings in JSON format /// - public string Settings { get; set; } + public string? Settings { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/CreateConfigurableComponentDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/CreateConfigurableComponentDto.cs index 5f2088f779..da9517ee29 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/CreateConfigurableComponentDto.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/CreateConfigurableComponentDto.cs @@ -16,32 +16,32 @@ public class CreateConfigurableComponentDto : EntityDto /// /// Form name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Label /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Markup in JSON format /// - public virtual string Markup { get; set; } + public string? Markup { get; set; } /// /// Type of the form model /// - public string ModelType { get; set; } + public string? ModelType { get; set; } /// /// Type of form (index, detail etc) /// - public string Type { get; set; } + public string? Type { get; set; } /// /// Template that is used for the form creation diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/GetConfigurableComponentByNameInput.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/GetConfigurableComponentByNameInput.cs index fb1a695c62..19f987c6ae 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/GetConfigurableComponentByNameInput.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/GetConfigurableComponentByNameInput.cs @@ -5,12 +5,12 @@ public class GetConfigurableComponentByNameInput : GetConfigurationItemInputBase /// /// Module name /// - public string Module { get; set; } + public string? Module { get; set; } /// /// Component name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// If true, indicates that component is application specific diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/UpdateConfigurableComponentDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/UpdateConfigurableComponentDto.cs index 21ebe57c4e..471ee18d9c 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/UpdateConfigurableComponentDto.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Components/UpdateConfigurableComponentDto.cs @@ -11,21 +11,21 @@ public class UpdateConfigurableComponentDto : EntityDto /// /// Label /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Markup in JSON format /// - public virtual string Markup { get; set; } + public string? Markup { get; set; } /// /// Type of the form model /// - public string ModelType { get; set; } + public string? ModelType { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/CopyInput.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/CopyInput.cs deleted file mode 100644 index b5471105af..0000000000 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/CopyInput.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace Shesha.Web.FormsDesigner.Dtos -{ - /// - /// Copy for input - /// - public class CopyInput - { - /// - /// Item id - /// - public Guid ItemId { get; set; } - - /// - /// Module Id - /// - public Guid ModuleId { get; set; } - - /// - /// Name - /// - public string Name { get; set; } - - /// - /// Label - /// - public string Label { get; set; } - - /// - /// Description - /// - public string Description { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/CreateFormConfigurationDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/CreateFormConfigurationDto.cs index 62e2ea3117..3b4d282070 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/CreateFormConfigurationDto.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/CreateFormConfigurationDto.cs @@ -16,32 +16,32 @@ public class CreateFormConfigurationDto : EntityDto /// /// Form name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Label /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Markup in JSON format /// - public virtual string Markup { get; set; } + public string? Markup { get; set; } /// /// Type of the form model /// - public string ModelType { get; set; } + public string? ModelType { get; set; } /// /// Type of form (index, detail etc) /// - public string Type { get; set; } + public string? Type { get; set; } /// /// Template that is used for the form creation diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/DesignerAutomapperProfile.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/DesignerAutomapperProfile.cs index 2ef97d2457..67e874badd 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/DesignerAutomapperProfile.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/DesignerAutomapperProfile.cs @@ -1,5 +1,5 @@ using Shesha.AutoMapper; -using Shesha.Web.FormsDesigner.Domain; +using Shesha.Domain; using System; namespace Shesha.Web.FormsDesigner.Dtos diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/DuplicateFormDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/DuplicateFormDto.cs deleted file mode 100644 index d26b66bc2e..0000000000 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/DuplicateFormDto.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Web.FormsDesigner.Dtos -{ - public class DuplicateFormDto - { - public Guid Id { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ExportConfigurationDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ExportConfigurationDto.cs deleted file mode 100644 index b0222627b7..0000000000 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ExportConfigurationDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Web.FormsDesigner.Dtos -{ - public class ExportConfigurationDto - { - public Guid[] Components { get; set; } - - } -} diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ExportConfigurationInput.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ExportConfigurationInput.cs deleted file mode 100644 index abf983b9ce..0000000000 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ExportConfigurationInput.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Shesha.Web.FormsDesigner.Dtos -{ - /// - /// Export form configuration import - /// - public class ExportConfigurationInput - { - /// - /// Filter string in JsonLogic format - /// - public string Filter { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormByFullNamePermissionsDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormByFullNamePermissionsDto.cs index 10b23961eb..a1533300fb 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormByFullNamePermissionsDto.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormByFullNamePermissionsDto.cs @@ -5,16 +5,16 @@ public class FormByFullNamePermissionsDto /// /// Module name /// - public string Module { get; set; } + public string? Module { get; set; } /// /// Form name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Form permissions /// - public string[] Permissions { get; set; } + public required string[] Permissions { get; set; } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormConfigurationDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormConfigurationDto.cs index 1d54c458e4..cd87be9a09 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormConfigurationDto.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormConfigurationDto.cs @@ -25,32 +25,32 @@ public class FormConfigurationDto: EntityDto, IConfigurationItemDto /// /// Module name /// - public string Module { get; set; } + public string? Module { get; set; } /// /// Form name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Label /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Markup in JSON format /// - public string Markup { get; set; } + public string? Markup { get; set; } /// /// Type of the form model /// - public string ModelType { get; set; } + public string? ModelType { get; set; } /// /// Version number @@ -71,11 +71,11 @@ public class FormConfigurationDto: EntityDto, IConfigurationItemDto public RefListPermissionedAccess? Access { get; set; } - public List Permissions { get; set; } + public List? Permissions { get; set; } /// /// Cache MD5, is used for client-side caching /// - public string CacheMd5 { get; set; } + public string? CacheMd5 { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormDto.cs deleted file mode 100644 index efedbd1683..0000000000 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormDto.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Abp.Application.Services.Dto; -using System; - -namespace Shesha.Web.FormsDesigner.Dtos -{ - /// - /// Form DTO - /// - public class FormDto: EntityDto - { - /// - /// Form path/id is used to identify a form - /// - public string Path { get; set; } - - /// - /// Form name - /// - public string Name { get; set; } - - /// - /// Description - /// - public string Description { get; set; } - - /// - /// Form markup (components) in JSON format - /// - public string Markup { get; set; } - - /// - /// Type of the form model - /// - public string ModelType { get; set; } - - /// - /// Type - /// - public string Type { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormListItemDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormListItemDto.cs deleted file mode 100644 index 4b3882e955..0000000000 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormListItemDto.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Abp.Application.Services.Dto; - -namespace Shesha.Web.FormsDesigner.Dtos -{ - /// - /// Forms short info, is used for lists (e.g. autocomplete) - /// - public class FormListItemDto: EntityDto - { - /// - /// Form path/id is used to identify a form - /// - public string Path { get; set; } - - /// - /// Form name - /// - public string Name { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormUpdateMarkupInput.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormUpdateMarkupInput.cs index 596987876b..c1674d2452 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormUpdateMarkupInput.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/FormUpdateMarkupInput.cs @@ -13,10 +13,10 @@ public class FormUpdateMarkupInput : EntityDto /// /// Form markup (components) in JSON format /// - public string Markup { get; set; } + public string? Markup { get; set; } public RefListPermissionedAccess? Access { get; set; } - public List Permissions { get; set; } + public List? Permissions { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/GetFormByFullNameInput.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/GetFormByFullNameInput.cs index db68255ae5..dc8331ab93 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/GetFormByFullNameInput.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/GetFormByFullNameInput.cs @@ -5,12 +5,12 @@ public class GetFormByFullNameInput: GetConfigurationItemInputBase /// /// Module name /// - public string Module { get; set; } + public string? Module { get; set; } /// /// Form name /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Form version number. Last published form is used when missing diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ImportConfigDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ImportConfigDto.cs deleted file mode 100644 index 3e617a9954..0000000000 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ImportConfigDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Web.FormsDesigner.Dtos -{ - public class ImportConfigDto - { - public IFormFile File { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ImportFormJsonInput.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ImportFormJsonInput.cs index d0ee6405c5..05cfeef243 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ImportFormJsonInput.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/ImportFormJsonInput.cs @@ -20,6 +20,6 @@ public class ImportFormJsonInput /// [Required] [BindProperty(Name = "file")] - public IFormFile File { get; set; } + public required IFormFile File { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/PublishConfigurationInput.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/PublishConfigurationInput.cs deleted file mode 100644 index ab7ec44f9b..0000000000 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/PublishConfigurationInput.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Shesha.Web.FormsDesigner.Dtos -{ - /// - /// Publish form configuration import - /// - public class PublishConfigurationInput - { - /// - /// Filter string in JsonLogic format - /// - public string Filter { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/UpdateConfigurationStatusInput.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/UpdateConfigurationStatusInput.cs index 02cede8338..f5de5c1058 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/UpdateConfigurationStatusInput.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/UpdateConfigurationStatusInput.cs @@ -12,7 +12,7 @@ public class UpdateConfigurationStatusInput /// Filter string in JsonLogic format /// [Required] - public string Filter { get; set; } + public required string Filter { get; set; } /// /// New status diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/UpdateFormConfigurationDto.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/UpdateFormConfigurationDto.cs index e72478be13..48c6dfa119 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/UpdateFormConfigurationDto.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/Forms/UpdateFormConfigurationDto.cs @@ -11,25 +11,25 @@ public class UpdateFormConfigurationDto : EntityDto /// /// Label /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Label /// - public string Label { get; set; } + public string? Label { get; set; } /// /// Description /// - public string Description { get; set; } + public string? Description { get; set; } /// /// Markup in JSON format /// - public virtual string Markup { get; set; } + public virtual string? Markup { get; set; } /// /// Type of the form model /// - public string ModelType { get; set; } + public string? ModelType { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/GetConfigurationItemInputBase.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/GetConfigurationItemInputBase.cs index b152e9f006..469ad4d647 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/GetConfigurationItemInputBase.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Dtos/GetConfigurationItemInputBase.cs @@ -9,6 +9,6 @@ public class GetConfigurationItemInputBase /// MD5 of the item. Is used for the client side caching. /// If specified, the application should compare the value received from the client with a local cache and return http response with code 304 (not changed) /// - public string Md5 { get; set; } + public string? Md5 { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Exceptions/ConfigurableComponentNotFoundException.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Exceptions/ConfigurableComponentNotFoundException.cs index 863006df53..354b084e0e 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Exceptions/ConfigurableComponentNotFoundException.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Exceptions/ConfigurableComponentNotFoundException.cs @@ -1,5 +1,5 @@ using Shesha.ConfigurationItems.Exceptions; -using Shesha.Web.FormsDesigner.Domain; +using Shesha.Domain; namespace Shesha.Web.FormsDesigner.Exceptions { @@ -8,7 +8,7 @@ namespace Shesha.Web.FormsDesigner.Exceptions /// public class ConfigurableComponentNotFoundException : ConfigurationItemNotFoundException { - public ConfigurableComponentNotFoundException(string frontEndApplication, string module, string name): base(ConfigurableComponent.ItemTypeName, module, name, frontEndApplication) + public ConfigurableComponentNotFoundException(string? frontEndApplication, string? module, string name): base(ConfigurableComponent.ItemTypeName, module, name, frontEndApplication) { FrontEndApplication = frontEndApplication; Name = name; diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Exceptions/FormNotFoundException.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Exceptions/FormNotFoundException.cs index 05ef46db4e..a67677e475 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Exceptions/FormNotFoundException.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Exceptions/FormNotFoundException.cs @@ -1,5 +1,5 @@ using Shesha.ConfigurationItems.Exceptions; -using Shesha.Web.FormsDesigner.Domain; +using Shesha.Domain; namespace Shesha.Web.FormsDesigner.Exceptions { @@ -8,7 +8,7 @@ namespace Shesha.Web.FormsDesigner.Exceptions /// public class FormNotFoundException: ConfigurationItemNotFoundException { - public FormNotFoundException(string module, string name): base(FormConfiguration.ItemTypeName, module, name, null) + public FormNotFoundException(string? module, string name): base(FormConfiguration.ItemTypeName, module, name, null) { Name = name; Module = module; diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/ConfigurableComponentAppService.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/ConfigurableComponentAppService.cs index 671b3c8d67..c6bbcbec93 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/ConfigurableComponentAppService.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/ConfigurableComponentAppService.cs @@ -5,11 +5,11 @@ using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Cache; using Shesha.ConfigurationItems.Exceptions; +using Shesha.ConfigurationItems.Models; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Exceptions; using Shesha.Utilities; -using Shesha.Web.FormsDesigner.Domain; using Shesha.Web.FormsDesigner.Dtos; using Shesha.Web.FormsDesigner.Exceptions; using System; @@ -147,10 +147,10 @@ private async Task GetComponentAsync(GetComponentInput in switch (mode) { - case ConfigurationItems.Models.ConfigurationItemViewMode.Live: + case ConfigurationItemViewMode.Live: query = query.Where(f => f.VersionStatus == ConfigurationItemVersionStatus.Live); break; - case ConfigurationItems.Models.ConfigurationItemViewMode.Ready: + case ConfigurationItemViewMode.Ready: { var statuses = new ConfigurationItemVersionStatus[] { ConfigurationItemVersionStatus.Live, @@ -160,7 +160,7 @@ private async Task GetComponentAsync(GetComponentInput in query = query.Where(f => statuses.Contains(f.VersionStatus)).OrderByDescending(f => f.VersionNo); break; } - case ConfigurationItems.Models.ConfigurationItemViewMode.Latest: + case ConfigurationItemViewMode.Latest: { var statuses = new ConfigurationItemVersionStatus[] { ConfigurationItemVersionStatus.Live, @@ -181,14 +181,14 @@ private async Task GetComponentAsync(GetComponentInput in /// /// /// - private async Task GetModuleAsync(string moduleName) + private async Task GetModuleAsync(string? moduleName) { return !string.IsNullOrWhiteSpace(moduleName) ? await AsyncQueryableExecuter.FirstOrDefaultAsync(_moduleRepository.GetAll().Where(m => m.Name == moduleName)) : null; } - private async Task GetFrontEndAppAsync(string appKey) + private async Task GetFrontEndAppAsync(string appKey) { return !string.IsNullOrWhiteSpace(appKey) ? await AsyncQueryableExecuter.FirstOrDefaultAsync(_frontEndAppRepository.GetAll().Where(m => m.AppKey == appKey)) @@ -208,22 +208,22 @@ public class GetComponentInput /// /// Module name /// - public string Module { get; set; } + public string? Module { get; init; } /// /// Component name /// - public string Name { get; set; } + public required string Name { get; init; } /// /// Application key /// - public string FrontEndApplication { get; set; } + public required string? FrontEndApplication { get; init; } /// /// If true, indicates that component is application specific /// - public bool IsApplicationSpecific { get; set; } + public required bool IsApplicationSpecific { get; init; } } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/ConfigurableComponentManager.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/ConfigurableComponentManager.cs index 2cd09c8770..4d1f0a609a 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/ConfigurableComponentManager.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/ConfigurableComponentManager.cs @@ -4,10 +4,10 @@ using Abp.Runtime.Session; using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Models; +using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Dto.Interfaces; using Shesha.Extensions; -using Shesha.Web.FormsDesigner.Domain; using System; using System.Linq; using System.Threading.Tasks; diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/ConfigurableComponentExport.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/ConfigurableComponentExport.cs index 3dd3632363..0439976274 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/ConfigurableComponentExport.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/ConfigurableComponentExport.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; -using Shesha.Web.FormsDesigner.Domain; using System; using System.IO; using System.Threading.Tasks; diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/ConfigurableComponentImport.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/ConfigurableComponentImport.cs index ce42befe24..f5c820ef33 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/ConfigurableComponentImport.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/ConfigurableComponentImport.cs @@ -1,16 +1,14 @@ using Abp.Dependency; using Abp.Domain.Repositories; using Abp.Domain.Uow; -using Newtonsoft.Json; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Extensions; using Shesha.Services.ConfigurationItems; -using Shesha.Web.FormsDesigner.Domain; using System; -using System.IO; using System.Linq; +using System.Linq.Dynamic.Core; using System.Threading.Tasks; namespace Shesha.Web.FormsDesigner.Services.Distribution @@ -18,7 +16,7 @@ namespace Shesha.Web.FormsDesigner.Services.Distribution /// /// Configurable component import /// - public class ConfigurableComponentImport: ConfigurationItemImportBase, IConfigurableComponentImport, ITransientDependency + public class ConfigurableComponentImport: ConfigurationItemImportBase, IConfigurableComponentImport, ITransientDependency { private readonly IRepository _componentRepo; private readonly IConfigurableComponentManager _componentManger; @@ -49,25 +47,15 @@ public async Task ImportItemAsync(DistributedConfigurable return await ImportComponentAsync(formItem, context); } - /// inheritedDoc - public async Task ReadFromJsonAsync(Stream jsonStream) - { - using (var reader = new StreamReader(jsonStream)) - { - var json = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(json); - } - } - private async Task GetLiveVersionForAsync(DistributedConfigurableComponent item) { var query = _componentRepo.GetAll().Where(f => f.Name == item.Name && f.VersionStatus == ConfigurationItemVersionStatus.Live); query = query.Where(!string.IsNullOrWhiteSpace(item.ModuleName) - ? f => f.Module.Name == item.ModuleName + ? f => f.Module != null && f.Module.Name == item.ModuleName : f => f.Module == null ); query = query.Where(!string.IsNullOrWhiteSpace(item.FrontEndApplication) - ? f => f.Application.AppKey == item.FrontEndApplication + ? f => f.Application != null && f.Application.AppKey == item.FrontEndApplication : f => f.Application == null ); @@ -79,8 +67,8 @@ protected async Task ImportComponentAsync(DistributedConf { // check if form exists var existingComponent = await _componentRepo.FirstOrDefaultAsync(f => f.Name == item.Name && - (f.Module == null && item.ModuleName == null || f.Module.Name == item.ModuleName) && - (f.Application == null && item.FrontEndApplication == null || f.Application.AppKey == item.FrontEndApplication) && + (f.Module == null && item.ModuleName == null || f.Module != null && f.Module.Name == item.ModuleName) && + (f.Application == null && item.FrontEndApplication == null || f.Application != null && f.Application.AppKey == item.FrontEndApplication) && f.IsLast ); diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/DistributedConfigurableComponentConfiguration.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/DistributedConfigurableComponentConfiguration.cs index 0b24113d46..7c1609f08e 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/DistributedConfigurableComponentConfiguration.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/DistributedConfigurableComponentConfiguration.cs @@ -10,6 +10,6 @@ public class DistributedConfigurableComponent: DistributedConfigurableItemBase /// /// Settings /// - public virtual string Settings { get; set; } + public virtual string? Settings { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/IConfigurableComponentExport.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/IConfigurableComponentExport.cs index 2a6664b415..69c6b063ed 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/IConfigurableComponentExport.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/IConfigurableComponentExport.cs @@ -1,5 +1,5 @@ using Shesha.ConfigurationItems.Distribution; -using Shesha.Web.FormsDesigner.Domain; +using Shesha.Domain; namespace Shesha.Web.FormsDesigner.Services.Distribution { diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/IConfigurableComponentImport.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/IConfigurableComponentImport.cs index 97f0e3f773..92cc76b220 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/IConfigurableComponentImport.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Components/IConfigurableComponentImport.cs @@ -1,5 +1,5 @@ using Shesha.ConfigurationItems.Distribution; -using Shesha.Web.FormsDesigner.Domain; +using Shesha.Domain; namespace Shesha.Web.FormsDesigner.Services.Distribution { diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/DistributedFormConfiguration.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/DistributedFormConfiguration.cs index 9fb071d16e..f8befe0e93 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/DistributedFormConfiguration.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/DistributedFormConfiguration.cs @@ -13,12 +13,12 @@ public class DistributedFormConfiguration: DistributedConfigurableItemBase /// /// Form markup /// - public string Markup { get; set; } + public string? Markup { get; set; } /// /// ModelType /// - public string ModelType { get; set; } + public string? ModelType { get; set; } public Guid? TemplateId { get; set; } @@ -26,6 +26,6 @@ public class DistributedFormConfiguration: DistributedConfigurableItemBase public RefListPermissionedAccess? Access { get; set; } - public List Permissions { get; set; } + public List? Permissions { get; set; } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/FormConfigurationExport.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/FormConfigurationExport.cs index 1ca2eef7bb..710befafe2 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/FormConfigurationExport.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/FormConfigurationExport.cs @@ -1,11 +1,9 @@ using Abp.Dependency; -using Abp.Domain.Entities; using Abp.Domain.Repositories; using Newtonsoft.Json; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.Permissions; -using Shesha.Web.FormsDesigner.Domain; using System; using System.IO; using System.Threading.Tasks; diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/FormConfigurationImport.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/FormConfigurationImport.cs index 19f2dbc372..3efb999c8c 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/FormConfigurationImport.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/FormConfigurationImport.cs @@ -1,16 +1,13 @@ using Abp.Dependency; using Abp.Domain.Repositories; using Abp.Domain.Uow; -using GraphQL; -using Newtonsoft.Json; using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Permissions; using Shesha.Services.ConfigurationItems; -using Shesha.Web.FormsDesigner.Domain; +using Shesha.Web.FormsDesigner.Exceptions; using System; -using System.IO; using System.Threading.Tasks; namespace Shesha.Web.FormsDesigner.Services.Distribution @@ -18,13 +15,16 @@ namespace Shesha.Web.FormsDesigner.Services.Distribution /// /// Form configuration import /// - public class FormConfigurationImport: ConfigurationItemImportBase, IFormConfigurationImport, ITransientDependency + public class FormConfigurationImport: ConfigurationItemImportBase, IFormConfigurationImport, ITransientDependency { private readonly IRepository _formConfigRepo; private readonly IFormManager _formManger; private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IPermissionedObjectManager _permissionedObjectManager; + /// + /// Default constructor + /// public FormConfigurationImport(IRepository moduleRepo, IRepository frontEndAppRepo, IFormManager formManger, @@ -39,6 +39,9 @@ IPermissionedObjectManager permissionedObjectManager _permissionedObjectManager = permissionedObjectManager; } + /// + /// Iten type + /// public string ItemType => FormConfiguration.ItemTypeName; /// inheritedDoc @@ -50,24 +53,33 @@ public async Task ImportItemAsync(DistributedConfigurable if (!(item is DistributedFormConfiguration formItem)) throw new NotSupportedException($"{this.GetType().FullName} supports only items of type {nameof(DistributedFormConfiguration)}. Actual type is {item.GetType().FullName}"); - return await ImportFormAsync(formItem, context); + var form = await ImportFormAsync(formItem, context); + await _unitOfWorkManager.Current.SaveChangesAsync(); + + return form; } - /// inheritedDoc - public async Task ReadFromJsonAsync(Stream jsonStream) + private bool FormsAreEqual(FormConfiguration? form, DistributedFormConfiguration item) { - using (var reader = new StreamReader(jsonStream)) - { - var json = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(json); - } + return form != null && + (form.Module == null ? string.IsNullOrWhiteSpace(item.ModuleName) : form.Module.Name == item.ModuleName) && + form.Markup == item.Markup && + form.Name == item.Name && + form.Label == item.Label && + form.Description == item.Description && + form.ModelType == item.ModelType && + form.Suppress == item.Suppress && + form.IsTemplate == item.IsTemplate; } /// inheritedDoc protected async Task ImportFormAsync(DistributedFormConfiguration item, IConfigurationItemsImportContext context) { // check if form exists - var existingForm = await _formConfigRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module.Name == item.ModuleName) && f.IsLast); + var existingForm = await _formConfigRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module != null && f.Module.Name == item.ModuleName) && f.IsLast); + + if (FormsAreEqual(existingForm, item)) + return existingForm; // use status specified in the context with fallback to imported value var statusToImport = context.ImportStatusAs ?? item.VersionStatus; @@ -88,7 +100,7 @@ protected async Task ImportFormAsync(DistributedFormConfi { var liveForm = existingForm.VersionStatus == ConfigurationItemVersionStatus.Live ? existingForm - : await _formConfigRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module.Name == item.ModuleName) && f.VersionStatus == ConfigurationItemVersionStatus.Live); + : await _formConfigRepo.FirstOrDefaultAsync(f => f.Name == item.Name && (f.Module == null && item.ModuleName == null || f.Module != null && f.Module.Name == item.ModuleName) && f.VersionStatus == ConfigurationItemVersionStatus.Live); if (liveForm != null) { await _formManger.UpdateStatusAsync(liveForm, ConfigurationItemVersionStatus.Retired); @@ -145,8 +157,8 @@ private async Task SetPermissionsAsync(DistributedFormConfiguration item, FormCo { Object = FormManager.GetFormPermissionedObjectName(form.Module?.Name, form.Name), Name = $"{form.Module?.Name}.{form.Name}", - Module = form.Module.Name, - ModuleId = form.Module.Id, + Module = form.Module?.Name, + ModuleId = form.Module?.Id, Type = ShaPermissionedObjectsTypes.Form, Access = item.Access, Permissions = item.Permissions, @@ -167,17 +179,6 @@ private void MapToForm(DistributedFormConfiguration item, FormConfiguration form form.Markup = item.Markup; form.ModelType = item.ModelType; form.IsTemplate = item.IsTemplate; - - // todo: decide how to handle other properties - /* - form.Configuration.Origin - form.Configuration.Module - form.Configuration.BaseItem - form.Configuration.VersionNo - form.Configuration.ParentVersion - form.Configuration.CreatedByImport - form.Configuration.TenantId - */ } } } diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/IFormConfigurationExport.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/IFormConfigurationExport.cs index e5d80bd685..db10c89a37 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/IFormConfigurationExport.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/IFormConfigurationExport.cs @@ -1,5 +1,5 @@ using Shesha.ConfigurationItems.Distribution; -using Shesha.Web.FormsDesigner.Domain; +using Shesha.Domain; namespace Shesha.Web.FormsDesigner.Services.Distribution { diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/IFormConfigurationImport.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/IFormConfigurationImport.cs index 249d933b1f..71ba7fbea1 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/IFormConfigurationImport.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/Distribution/Forms/IFormConfigurationImport.cs @@ -1,5 +1,5 @@ using Shesha.ConfigurationItems.Distribution; -using Shesha.Web.FormsDesigner.Domain; +using Shesha.Domain; namespace Shesha.Web.FormsDesigner.Services.Distribution { diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/FormConfigurationAppService.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/FormConfigurationAppService.cs index da0dd50096..9d25278774 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/FormConfigurationAppService.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/FormConfigurationAppService.cs @@ -8,11 +8,11 @@ using Newtonsoft.Json; using Shesha.Application.Services.Dto; using Shesha.Attributes; -using Shesha.AutoMapper.Dto; using Shesha.Configuration.Runtime; using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Cache; using Shesha.ConfigurationItems.Models; +using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Domain.Enums; using Shesha.DynamicEntities; @@ -20,8 +20,9 @@ using Shesha.Extensions; using Shesha.Mvc; using Shesha.Permissions; +using Shesha.Reflection; using Shesha.Utilities; -using Shesha.Web.FormsDesigner.Domain; +using Shesha.Validations; using Shesha.Web.FormsDesigner.Dtos; using Shesha.Web.FormsDesigner.Exceptions; using System; @@ -65,7 +66,7 @@ IPermissionedObjectManager permissionedObjectManager _permissionedObjectManager = permissionedObjectManager; } - private async Task GetFormPermissionsAsync(string module, string name) + private async Task GetFormPermissionsAsync(string? module, string name) { var permission = await _permissionedObjectManager.GetOrDefaultAsync( FormManager.GetFormPermissionedObjectName(module, name), @@ -85,7 +86,7 @@ public async Task> GetAnonymousFormsAsync() return await _permissionedObjectManager.GetObjectsByAccessAsync(ShaPermissionedObjectsTypes.Form, RefListPermissionedAccess.AllowAnonymous); } - private async Task CheckFormPermissionsAsync(string module, string name) + private async Task CheckFormPermissionsAsync(string? module, string name) { var permission = await _permissionedObjectManager.GetOrDefaultAsync( FormManager.GetFormPermissionedObjectName(module, name), @@ -100,7 +101,7 @@ private async Task CheckFormPermissionsAsync(string module, string name) throw new AbpAuthorizationException("You are not authorized for this form"); if (access == RefListPermissionedAccess.RequiresPermissions) { - var permissions = permission.Permissions.ToArray() ?? []; + var permissions = permission?.Permissions?.ToArray() ?? []; return await PermissionChecker.IsGrantedAsync(false, permissions); } return true; @@ -184,10 +185,10 @@ public async Task GetByNameAsync(GetFormByFullNameInput in else { switch (mode) { - case ConfigurationItems.Models.ConfigurationItemViewMode.Live: + case ConfigurationItemViewMode.Live: query = query.Where(f => f.VersionStatus == ConfigurationItemVersionStatus.Live); break; - case ConfigurationItems.Models.ConfigurationItemViewMode.Ready: + case ConfigurationItemViewMode.Ready: { var statuses = new ConfigurationItemVersionStatus[] { ConfigurationItemVersionStatus.Live, @@ -197,7 +198,7 @@ public async Task GetByNameAsync(GetFormByFullNameInput in query = query.Where(f => statuses.Contains(f.VersionStatus)).OrderByDescending(f => f.VersionNo); break; } - case ConfigurationItems.Models.ConfigurationItemViewMode.Latest: + case ConfigurationItemViewMode.Latest: { var statuses = new ConfigurationItemVersionStatus[] { ConfigurationItemVersionStatus.Live, @@ -281,8 +282,8 @@ public async Task UpdateMarkupAsync(FormUpdateMarkupInput input) { Object = FormManager.GetFormPermissionedObjectName(form.Module?.Name, form.Name), Name = $"{form.Module?.Name}.{form.Name}", - Module = form.Module.Name, - ModuleId = form.Module.Id, + Module = form.Module?.Name, + ModuleId = form.Module?.Id, Type = ShaPermissionedObjectsTypes.Form, Access = input.Access, Permissions = input.Permissions, @@ -305,8 +306,7 @@ public async Task UpdateStatusAsync(UpdateConfigurationStatusInput input) var validationResults = new List(); if (string.IsNullOrWhiteSpace(input.Filter)) validationResults.Add(new ValidationResult("Filter is mandatory", new string[] { nameof(input.Filter) })); - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); var forms = await GetAllFilteredAsync(input.Filter); @@ -400,8 +400,7 @@ public async Task CancelVersionAsync(CancelVersionInput in if (!item.IsLast) validationResults.Add(new ValidationResult($"This operation is allowed only for last version of form")); - if (validationResults.Any()) - throw new AbpValidationException("Failed to cancel version", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); await _formManager.CancelVersoinAsync(item); await CurrentUnitOfWork.SaveChangesAsync(); @@ -447,6 +446,7 @@ public async Task ImportJsonAsync([FromForm] ImportFormJso if (validationResults.Any()) throw new AbpValidationException("Failed to import JSON", validationResults); + input.File.NotNull(); using (var fileStream = input.File.OpenReadStream()) { using (var reader = new StreamReader(fileStream)) @@ -469,7 +469,7 @@ public override async Task UpdateAsync(UpdateFormConfigura var validationResults = new List(); - var alreadyExist = await Repository.GetAll().Where(f => f.Id != input.Id && f.Module.Name == input.ModelType && f.Name == input.Name).AnyAsync(); + var alreadyExist = await Repository.GetAll().Where(f => f.Id != input.Id && f.Module != null && f.Module.Name == input.ModelType && f.Name == input.Name).AnyAsync(); if (alreadyExist) validationResults.Add(new ValidationResult( input.ModelType != null @@ -530,58 +530,24 @@ public async Task CopyAsync(CopyItemInput input) return await MapToEntityDtoAsync(form); } - [HttpGet] - public async Task> AutocompleteAsync(string term, string selectedValue) - { - var isPreselection = string.IsNullOrWhiteSpace(term) && !string.IsNullOrWhiteSpace(selectedValue); - var models = (await AsyncQueryableExecuter.ToListAsync( - Repository.GetAll().Select(x => new { Name = x.Name, Module = x.Module.Name })) - ).Distinct(); - - var formIdJson = (string name, string module) => { return $"{{\"name\":\"{name}\",\"module\":\"{module}\"}}"; }; - - var entities = isPreselection - ? models.Where(e => formIdJson(e.Name, e.Module) == selectedValue).ToList() - : models - .Where(e => - !string.IsNullOrWhiteSpace(e.Name) && - (string.IsNullOrWhiteSpace(term) - || (e.Name ?? "").Contains(term, StringComparison.InvariantCultureIgnoreCase) - || (e.Module ?? "").Contains(term, StringComparison.InvariantCultureIgnoreCase)) - ) - .OrderBy(e => $"{e.Module}.{e.Name}") - .Take(10) - .ToList(); - - var result = entities - .Select(e => new AutocompleteItemDto - { - DisplayText = $"{e.Module}.{e.Name}", - Value = formIdJson(e.Name, e.Module) - }) - .ToList(); - - return result; - } - public async Task> GetFormsWithNotImplementedAsync() { var configs = (await _entityConfigManager.GetMainDataListAsync()).Where(x => x.NotImplemented).ToList(); var list = new List(); - var forms = Repository.GetAll().Where(x => + var forms = await Repository.GetAll().Where(x => (int)x.VersionStatus < 4 && !x.Name.Contains(".json") && x.Markup != null) .Select(x => new { x.FullName, x.VersionNo, x.VersionStatus, x.Markup }) - .ToList(); + .ToListAsync(); foreach (var config in configs) { var className = $"\"{config.FullClassName}\""; var typeShortAlias = $"\"{config.TypeShortAlias}\""; var usetsa = !config.TypeShortAlias.IsNullOrWhiteSpace(); - var formConfigs = forms.Where(x =>/*usetsa && x.Markup.Contains(typeShortAlias) ||*/ x.Markup.Contains(className)).ToList(); + var formConfigs = forms.Where(x => x.Markup != null && x.Markup.Contains(className)).ToList(); list.AddRange(formConfigs.Select(x => { return new @@ -604,7 +570,7 @@ public async Task> GetFormsWithNotImplementedAsync() /// /// /// - private async Task GetModuleAsync(string moduleName) + private async Task GetModuleAsync(string? moduleName) { return !string.IsNullOrWhiteSpace(moduleName) ? await AsyncQueryableExecuter.FirstOrDefaultAsync(_moduleRepository.GetAll().Where(m => m.Name == moduleName)) @@ -613,40 +579,6 @@ private async Task GetModuleAsync(string moduleName) #endregion - public async Task ExportAllAsync(ExportAllInput input) - { - if (!Directory.Exists(input.Path)) - Directory.CreateDirectory(input.Path); - - var forms = await Repository.GetAll() - .Select(f => new - { - Name = f.Name, - Version = f.VersionNo, - Markup = f.Markup, - Module = f.Module != null ? f.Module.Name : "[no-module]" - }).ToListAsync(); - - foreach (var form in forms) - { - if (!string.IsNullOrWhiteSpace(form.Markup)) - { - try - { - var fileName = Path.Combine(input.Path, form.Module, $"{form.Name}.v{form.Version}.json".RemovePathIllegalCharacters()); - var directory = Path.GetDirectoryName(fileName); - if (!Directory.Exists(directory)) - Directory.CreateDirectory(directory); - - await File.WriteAllTextAsync(fileName, form.Markup); - } - catch (Exception e) { - e.LogError(); - } - } - } - } - [EntityAction(StandardEntityActions.List)] public override async Task> GetAllAsync(FilteredPagedAndSortedResultRequestDto input) { @@ -672,10 +604,5 @@ public override async Task> GetAllAsync(Fil dtos ); } - - public class ExportAllInput - { - public string Path { get; set; } - } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/FormManager.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/FormManager.cs index ff13892c5a..6430cc1bc1 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/FormManager.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/FormManager.cs @@ -2,14 +2,15 @@ using Abp.Domain.Repositories; using Abp.Domain.Uow; using Abp.Runtime.Session; -using Abp.Runtime.Validation; using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Models; +using Shesha.Domain; using Shesha.Domain.ConfigurationItems; using Shesha.Dto.Interfaces; using Shesha.Extensions; using Shesha.Permissions; -using Shesha.Web.FormsDesigner.Domain; +using Shesha.Reflection; +using Shesha.Validations; using Shesha.Web.FormsDesigner.Dtos; using System; using System.Collections.Generic; @@ -37,7 +38,7 @@ IPermissionedObjectManager permissionedObjectManager public IAbpSession AbpSession { get; set; } = NullAbpSession.Instance; - public static string GetFormPermissionedObjectName(string module, string name) + public static string GetFormPermissionedObjectName(string? module, string name) { return $"{module}.{name}"; } @@ -61,7 +62,6 @@ public override async Task CreateNewVersionAsync(FormConfigur newVersion.Markup = form.Markup; newVersion.ModelType = form.ModelType; - newVersion.Type = form.Type; newVersion.IsTemplate = form.IsTemplate; newVersion.Template = form.Template; newVersion.Normalize(); @@ -129,8 +129,6 @@ public async Task MoveToModuleAsync(MoveToModuleInput input) var validationResults = new List(); // todo: review validation messages, add localization support - if (form == null) - validationResults.Add(new ValidationResult("Please select a form to move", new List { nameof(input.ItemId) })); if (module == null) validationResults.Add(new ValidationResult("Module is mandatory", new List { nameof(input.ModuleId) })); if (module != null && form != null) @@ -141,8 +139,9 @@ public async Task MoveToModuleAsync(MoveToModuleInput input) ); } - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); + + form.NotNull(); var allVersionsQuery = Repository.GetAll().Where(v => v.Origin == form.Origin); var allVersions = await allVersionsQuery.ToListAsync(); @@ -171,8 +170,7 @@ public async Task CreateAsync(CreateFormConfigurationDto inpu : $"Form with name `{input.Name}` already exists" ) ); - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); var template = input.TemplateId.HasValue ? await Repository.GetAsync(input.TemplateId.Value) @@ -188,9 +186,8 @@ public async Task CreateAsync(CreateFormConfigurationDto inpu form.VersionStatus = ConfigurationItemVersionStatus.Draft; form.Origin = form; - form.Markup = input.Markup; + form.Markup = input.Markup ?? ""; form.ModelType = input.ModelType; - form.Type = input.Type; form.IsTemplate = input.IsTemplate; form.Template = template; form.Normalize(); @@ -207,7 +204,7 @@ public override Task MapToDtoAsync(FormConfiguration item public override async Task CopyAsync(FormConfiguration item, CopyItemInput input) { - var srcForm = item as FormConfiguration; + var srcForm = item; // todo: validate input var module = await ModuleRepository.FirstOrDefaultAsync(input.ModuleId); @@ -215,8 +212,6 @@ public override async Task CopyAsync(FormConfiguration item, var validationResults = new List(); // todo: review validation messages, add localization support - if (srcForm == null) - validationResults.Add(new ValidationResult("Please select a form to copy", new List { nameof(input.ItemId) })); if (module == null) validationResults.Add(new ValidationResult("Module is mandatory", new List { nameof(input.ModuleId) })); if (string.IsNullOrWhiteSpace(input.Name)) @@ -234,8 +229,7 @@ public override async Task CopyAsync(FormConfiguration item, ); } - if (validationResults.Any()) - throw new AbpValidationException("Please correct the errors and try again", validationResults); + validationResults.ThrowValidationExceptionIfAny(L); var form = new FormConfiguration(); form.Name = input.Name; @@ -249,7 +243,6 @@ public override async Task CopyAsync(FormConfiguration item, form.Markup = srcForm.Markup; form.ModelType = srcForm.ModelType; - form.Type = srcForm.Type; form.IsTemplate = srcForm.IsTemplate; form.Template = srcForm.Template; form.Normalize(); diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/IConfigurableComponentManager.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/IConfigurableComponentManager.cs index 638cd423fa..0df83ce18a 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/IConfigurableComponentManager.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/IConfigurableComponentManager.cs @@ -1,6 +1,6 @@ using Shesha.ConfigurationItems; +using Shesha.Domain; using Shesha.Domain.ConfigurationItems; -using Shesha.Web.FormsDesigner.Domain; using System.Threading.Tasks; namespace Shesha.Web.FormsDesigner.Services diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Services/IFormManager.cs b/shesha-core/src/Shesha.Web.FormsDesigner/Services/IFormManager.cs index 5e7c391b4c..4f3aa006a9 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Services/IFormManager.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Services/IFormManager.cs @@ -1,7 +1,7 @@ using Shesha.ConfigurationItems; using Shesha.ConfigurationItems.Models; +using Shesha.Domain; using Shesha.Domain.ConfigurationItems; -using Shesha.Web.FormsDesigner.Domain; using Shesha.Web.FormsDesigner.Dtos; using System; using System.Collections.Generic; diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/Shesha.Web.FormsDesigner.csproj b/shesha-core/src/Shesha.Web.FormsDesigner/Shesha.Web.FormsDesigner.csproj index b113c1dd4d..2d15694577 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/Shesha.Web.FormsDesigner.csproj +++ b/shesha-core/src/Shesha.Web.FormsDesigner/Shesha.Web.FormsDesigner.csproj @@ -23,7 +23,7 @@ - + @@ -32,8 +32,16 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-core/src/Shesha.Web.FormsDesigner/SheshaFormsDesignerModule.cs b/shesha-core/src/Shesha.Web.FormsDesigner/SheshaFormsDesignerModule.cs index 1cff294feb..51ae03aa21 100644 --- a/shesha-core/src/Shesha.Web.FormsDesigner/SheshaFormsDesignerModule.cs +++ b/shesha-core/src/Shesha.Web.FormsDesigner/SheshaFormsDesignerModule.cs @@ -4,8 +4,8 @@ using Abp.Modules; using Abp.Reflection.Extensions; using Shesha.ConfigurationItems; +using Shesha.Domain; using Shesha.Modules; -using Shesha.Web.FormsDesigner.Domain; using Shesha.Web.FormsDesigner.Services; using Shesha.Web.FormsDesigner.Services.Distribution; using System.Reflection; diff --git a/shesha-core/src/Shesha.Web.Host/Startup/AuthConfigurer.cs b/shesha-core/src/Shesha.Web.Host/Startup/AuthConfigurer.cs index 3a7d66baf1..3d748226f1 100644 --- a/shesha-core/src/Shesha.Web.Host/Startup/AuthConfigurer.cs +++ b/shesha-core/src/Shesha.Web.Host/Startup/AuthConfigurer.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Abp.Runtime.Security; +using Shesha.Configuration; namespace Shesha.Web.Host.Startup { @@ -14,7 +15,7 @@ public static class AuthConfigurer { public static void Configure(IServiceCollection services, IConfiguration configuration) { - if (bool.Parse(configuration["Authentication:JwtBearer:IsEnabled"])) + if (bool.TryParse(configuration["Authentication:JwtBearer:IsEnabled"], out var jwtEnabled) && jwtEnabled) { services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "JwtBearer"; @@ -27,15 +28,15 @@ public static void Configure(IServiceCollection services, IConfiguration configu { // The signing key must match! ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["Authentication:JwtBearer:SecurityKey"])), + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration.GetRequired("Authentication:JwtBearer:SecurityKey"))), // Validate the JWT Issuer (iss) claim ValidateIssuer = true, - ValidIssuer = configuration["Authentication:JwtBearer:Issuer"], + ValidIssuer = configuration.GetRequired("Authentication:JwtBearer:Issuer"), // Validate the JWT Audience (aud) claim ValidateAudience = true, - ValidAudience = configuration["Authentication:JwtBearer:Audience"], + ValidAudience = configuration.GetRequired("Authentication:JwtBearer:Audience"), // Validate the token expiry ValidateLifetime = true, diff --git a/shesha-core/src/Shesha.Web.Host/Startup/GraphQLNhListener.cs b/shesha-core/src/Shesha.Web.Host/Startup/GraphQLNhListener.cs deleted file mode 100644 index c76a6f2858..0000000000 --- a/shesha-core/src/Shesha.Web.Host/Startup/GraphQLNhListener.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Abp.Domain.Uow; -using GraphQL.Execution; -using GraphQL.Validation; -using System.Threading.Tasks; - -namespace Shesha.Web.Host.Startup -{ - public class GraphQLNhListener : IDocumentExecutionListener - { - private IUnitOfWorkCompleteHandle _unitOfWorkCompleteHandle; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - public GraphQLNhListener(IUnitOfWorkManager unitOfWorkManager) - { - _unitOfWorkManager = unitOfWorkManager; - } - - public async Task AfterExecutionAsync(IExecutionContext context) - { - await _unitOfWorkCompleteHandle?.CompleteAsync(); - } - - public Task AfterValidationAsync(IExecutionContext context, IValidationResult validationResult) - { - return Task.CompletedTask; - } - - public Task BeforeExecutionAsync(IExecutionContext context) - { - _unitOfWorkCompleteHandle = _unitOfWorkManager.Begin(); - - return Task.CompletedTask; - } - } -} diff --git a/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs b/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs index 634ac520bd..b3590a9c7c 100644 --- a/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs +++ b/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs @@ -86,6 +86,7 @@ public IServiceProvider ConfigureServices(IServiceCollection services) options.Filters.AddService(typeof(SheshaExceptionFilter), order: 1); }) .AddApiExplorer() + .AddDataAnnotations() .AddNewtonsoftJson(options => { options.UseCamelCasing(true); diff --git a/shesha-core/test/Directory.Build.props b/shesha-core/test/Directory.Build.props new file mode 100644 index 0000000000..542944e4a3 --- /dev/null +++ b/shesha-core/test/Directory.Build.props @@ -0,0 +1,13 @@ + + + enable + true + $(NoWarn);CS1591;612;618;809;AsyncFixer01;8618; + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/shesha-core/test/Shesha.Tests/CamelCase/CamelCase_Tests.cs b/shesha-core/test/Shesha.Tests/CamelCase/CamelCase_Tests.cs index 5e72b9babb..7e8da08c5e 100644 --- a/shesha-core/test/Shesha.Tests/CamelCase/CamelCase_Tests.cs +++ b/shesha-core/test/Shesha.Tests/CamelCase/CamelCase_Tests.cs @@ -6,6 +6,7 @@ namespace Shesha.Tests.CamelCase /// /// /// + [Trait("RunOnPipeline", "yes")] public class CamelCase_Tests { /// diff --git a/shesha-core/test/Shesha.Tests/CodeGeneration/CodeGenerator_Tests.cs b/shesha-core/test/Shesha.Tests/CodeGeneration/CodeGenerator_Tests.cs deleted file mode 100644 index a32a0dfd00..0000000000 --- a/shesha-core/test/Shesha.Tests/CodeGeneration/CodeGenerator_Tests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Shesha.CodeGeneration; -using Xunit; - -namespace Shesha.Tests.CodeGeneration -{ - public class CodeGenerator_Tests - { - [Fact] - public void Enum_Test() - { - var s = EnumsGenerator.GenerateEnumItems(@"Credit to Prepaid meter Retrofit -Disconnection - OHM -Disconnection - UGM -Hard disconnection at CDU (Single phase) -Hard disconnection at CDU (Three phase) -Hard disconnection at the pole (Single phase) -Hard disconnection at the pole (Three phase) -Hard reconnection at CDU (Single phase) -Hard reconnection at CDU (Three phase) -Hard reconnection at the pole (Single phase) -Hard reconnection at the pole (Three phase) -Keypad Replacemnt (PLC/SMART) -Keypad Replacemt (wired comms wires) -Meter Replacements (Single phase) -Meter Replacements (Three phase) -Normal Disconnection (Single or Three phase) -Normal Reconnection (Single or Three phase) -PLC Meter Changes -Reconnection - OHM -Reconnection - UGM -Remove Meter -Remove all Gear - RAG -Replacement of a fuse base and carrier with similar or circuit breaker. -Replacement of a split meter -Service Cable Removal - SCR -Site Call -Special hard Disconnection -Split meter conversion"); - } - } -} diff --git a/shesha-core/test/Shesha.Tests/CompressionService/CompressionService_Tests.cs b/shesha-core/test/Shesha.Tests/CompressionService/CompressionService_Tests.cs index cd59b34d33..1ea8173649 100644 --- a/shesha-core/test/Shesha.Tests/CompressionService/CompressionService_Tests.cs +++ b/shesha-core/test/Shesha.Tests/CompressionService/CompressionService_Tests.cs @@ -1,13 +1,14 @@ -using System; +using Shesha.Services; +using Shouldly; +using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Shesha.Services; -using Shouldly; using Xunit; namespace Shesha.Tests.CompressionHelper { + [Trait("RunOnPipeline", "yes")] public class CompressionServiceComplex_Tests { [Fact] diff --git a/shesha-core/test/Shesha.Tests/ConfigurationItems/FormConfiguration_Tests.cs b/shesha-core/test/Shesha.Tests/ConfigurationItems/FormConfiguration_Tests.cs index 95941c6032..c80a272091 100644 --- a/shesha-core/test/Shesha.Tests/ConfigurationItems/FormConfiguration_Tests.cs +++ b/shesha-core/test/Shesha.Tests/ConfigurationItems/FormConfiguration_Tests.cs @@ -9,11 +9,12 @@ using Shesha.ConfigurationItems.Distribution.Models; using Shesha.Domain; using Shesha.Domain.ConfigurationItems; +using Shesha.Extensions; using Shesha.Permissions; +using Shesha.Reflection; using Shesha.Services; +using Shesha.Tests.Fixtures; using Shesha.Utilities; -using Shesha.Web.FormsDesigner.Domain; -using Shesha.Web.FormsDesigner.Services; using Shesha.Web.FormsDesigner.Services.Distribution; using Shouldly; using System; @@ -25,8 +26,13 @@ namespace Shesha.Tests.ConfigurationItems { - public class FormConfiguration_Tests: SheshaNhTestBase + [Collection(SqlServerCollection.Name)] + public class FormConfigurationTests : SheshaNhTestBase { + public FormConfigurationTests(SqlServerFixture fixture) : base(fixture) + { + } + [Fact] public async Task ShouldExport_TestAsync() { @@ -65,257 +71,294 @@ public async Task ShouldExport_TestAsync() public async Task When_Import_Missing_FormAsync() { var src = PrepareImportContext(); - var srcForm = await src.AddFormAsync(async c => + var srcModule = await src.GetOrCreateModuleAsync("test-import-missing-form-module"); + var srcForm = await src.AddFormAsync(c => { c.ModelType = "test-modelType"; c.Markup = "{ components: [], settings: {} }"; - c.Name = "test-form"; + c.Name = "test-import-missing-form"; c.Label = "test-label"; c.Description = "test-description"; c.VersionNo = 2; c.SetIsLast(true); c.VersionStatus = ConfigurationItemVersionStatus.Live; - c.Module = await src.GetOrCreateModuleAsync("test-module"); + c.Module = srcModule; + return Task.CompletedTask; }).ConfigureAwait(true); var permissionedObjectManager = Resolve(); var export = new FormConfigurationExport(src.FormRepo, permissionedObjectManager); var exported = await export.ExportItemAsync(srcForm.Id); - var dst = PrepareImportContext(); + await WithUnitOfWorkAsync(async() => { + var dstFormRepo = Resolve>(); - var asyncExecuter = Resolve(); - var uowManager = Resolve(); - var formManager = new FormManager(dst.FormRepo, dst.ModuleRepo, uowManager, permissionedObjectManager); - var importer = new FormConfigurationImport(dst.ModuleRepo, dst.FrontEndAppRepo, formManager, dst.FormRepo, uowManager, permissionedObjectManager); - var importContext = new PackageImportContext() - { - CreateModules = true - }; - var imported = await importer.ImportItemAsync(exported, importContext) as FormConfiguration; - imported.ShouldNotBeNull(); + var formExistsBeforeImport = await dstFormRepo.GetAll() + .Where(f => f.Name == srcForm.Name && f.Module != null && f.Module.Name == srcModule.Name) + .OrderByDescending(f => f.VersionNo) + .AnyAsync(); + formExistsBeforeImport.ShouldBeFalse("Form must not exist before import"); + + var dstModuleRepo = Resolve>(); + var importer = Resolve(); + + var importContext = new PackageImportContext() + { + CreateModules = true + }; + var imported = await importer.ImportItemAsync(exported, importContext) as FormConfiguration; + imported.ShouldNotBeNull(); - var dstModule = srcForm.Module != null - ? dst.ModuleRepo.GetAll().FirstOrDefault(m => m.Name == srcForm.Module.Name) - : null; + var dstModule = srcForm.Module != null + ? await dstModuleRepo.GetAll().FirstOrDefaultAsync(m => m.Name == srcForm.Module.Name) + : null; - var importedFormFromRepo = dst.FormRepo.GetAll() - .Where(f => f.Name == srcForm.Name && f.Module == dstModule) - .OrderByDescending(f => f.VersionNo) - .FirstOrDefault(); + var importedFormFromRepo = await dstFormRepo.GetAll() + .Where(f => f.Name == srcForm.Name && f.Module == dstModule) + .OrderByDescending(f => f.VersionNo) + .FirstOrDefaultAsync(); - imported.ShouldBe(importedFormFromRepo, "Imported form should be a form with latest version number in the repo"); + imported.ShouldBe(importedFormFromRepo, "Imported form should be a form with latest version number in the repo"); - imported.ShouldNotBeNull("Form should be created in the destination DB"); - imported.Id.ShouldNotBe(srcForm.Id, "A new Id should be generated for the form during the import"); - imported.Name.ShouldBe(srcForm.Name); - imported.Label.ShouldBe(srcForm.Label); - imported.Description.ShouldBe(srcForm.Description); - imported.Module?.Name.ShouldBe(srcForm.Module?.Name); + imported.ShouldNotBeNull("Form should be created in the destination DB"); + imported.Id.ShouldNotBe(srcForm.Id, "A new Id should be generated for the form during the import"); + imported.Name.ShouldBe(srcForm.Name); + imported.Label.ShouldBe(srcForm.Label); + imported.Description.ShouldBe(srcForm.Description); + imported.Module?.Name.ShouldBe(srcForm.Module?.Name); - imported.VersionNo.ShouldBe(1); - imported.VersionStatus.ShouldBe(srcForm.VersionStatus); + imported.VersionNo.ShouldBe(1); + imported.VersionStatus.ShouldBe(srcForm.VersionStatus); - imported.Markup.ShouldBe(srcForm.Markup); - imported.ModelType.ShouldBe(srcForm.ModelType); + imported.Markup.ShouldBe(srcForm.Markup); + imported.ModelType.ShouldBe(srcForm.ModelType); + }); } [Fact] public async Task When_Import_Existing_FormAsync() { var src = PrepareImportContext(); - var srcForm = await src.AddFormAsync(async c => { + var srcModule = await src.GetOrCreateModuleAsync("test-import-existing-form-module"); + var srcForm = await src.AddFormAsync(c => { c.ModelType = "test-modelType"; c.Markup = "src-markup"; - c.Name = "test-form"; + c.Name = "test-import-existing-form"; c.Label = "test-label"; c.Description = "test-description"; c.VersionNo = 2; c.SetIsLast(true); c.VersionStatus = ConfigurationItemVersionStatus.Live; - c.Module = await src.GetOrCreateModuleAsync("test-module"); + c.Module = srcModule; + return Task.CompletedTask; }); var permissionedObjectManager = Resolve(); var export = new FormConfigurationExport(src.FormRepo, permissionedObjectManager); var exported = await export.ExportItemAsync(srcForm.Id); - var dst = PrepareImportContext(); - var dstForm = await dst.AddFormAsync(async c => { - c.ModelType = "dst-test-modelType"; - c.Markup = "dst-markup"; - c.Name = "test-form"; - c.Label = "dst-test-label"; - c.Description = "dst-test-description"; + + var dstFormRepo = Resolve>(); + var dstModuleRepo = Resolve>(); + Guid dstFormId = Guid.Empty; + await WithUnitOfWorkAsync(async() => { + var dstModule = await GetOrCreateModuleAsync(dstModuleRepo, srcModule.Name); - c.VersionNo = 10; - c.SetIsLast(true); - c.VersionStatus = ConfigurationItemVersionStatus.Live; - c.Module = await dst.GetOrCreateModuleAsync("test-module"); + var dstForm = await dstFormRepo.InsertAsync(new FormConfiguration + { + ModelType = "dst-test-modelType", + Markup = "dst-markup", + Name = srcForm.Name, + Label = "dst-test-label", + Description = "dst-test-description", + + VersionNo = 10, + VersionStatus = ConfigurationItemVersionStatus.Live, + Module = dstModule, + }); + dstFormId = dstForm.Id; }); + dstFormId.ShouldNotBe(Guid.Empty, "Failed to create test form"); - var asyncExecuter = Resolve(); - var uowManager = Resolve(); - var formManager = new FormManager(dst.FormRepo, dst.ModuleRepo, uowManager, permissionedObjectManager); - var importer = new FormConfigurationImport(dst.ModuleRepo, dst.FrontEndAppRepo, formManager, dst.FormRepo, uowManager, permissionedObjectManager); - var importContext = new PackageImportContext() - { - CreateModules = true - }; - var imported = await importer.ImportItemAsync(exported, importContext) as FormConfiguration; - imported.ShouldNotBeNull(); + await WithUnitOfWorkAsync(async() => { + var importer = Resolve(); + var importContext = new PackageImportContext() + { + CreateModules = true + }; - var dstModule = srcForm.Module != null - ? dst.ModuleRepo.GetAll().FirstOrDefault(m => m.Name == srcForm.Module.Name) - : null; + var imported = await importer.ImportItemAsync(exported, importContext) as FormConfiguration; + imported.ShouldNotBeNull(); - var importedFormFromRepo = dst.FormRepo.GetAll() - .Where(f => f.Name == srcForm.Name && f.Module == dstModule) - .OrderByDescending(f => f.VersionNo) - .FirstOrDefault(); + var dstModule = srcForm.Module != null + ? dstModuleRepo.GetAll().FirstOrDefault(m => m.Name == srcForm.Module.Name) + : null; - imported.ShouldBe(importedFormFromRepo, "Imported form should be a form with latest version number in the repo"); + var importedFormFromRepo = await dstFormRepo.GetAll() + .Where(f => f.Name == srcForm.Name && f.Module == dstModule) + .OrderByDescending(f => f.VersionNo) + .FirstOrDefaultAsync(); - imported.ShouldNotBeNull("Form should be created in the destination DB"); - imported.Id.ShouldNotBe(srcForm.Id, "A new Id should be generated for the form during the import"); - imported.Name.ShouldBe(srcForm.Name); - imported.Label.ShouldBe(srcForm.Label); - imported.Description.ShouldBe(srcForm.Description); - imported.Module?.Name.ShouldBe(srcForm.Module?.Name); + var dstForm = await dstFormRepo.GetAsync(dstFormId); - imported.VersionNo.ShouldBe(dstForm.VersionNo + 1); - imported.VersionStatus.ShouldBe(srcForm.VersionStatus); + imported.ShouldBe(importedFormFromRepo, "Imported form should be a form with latest version number in the repo"); - // check prevoius version - imported.ParentVersion.ShouldBe(dstForm, $"{nameof(imported.ParentVersion)} should be set to last version of the form in the destination"); - dstForm.VersionStatus.ShouldBe(ConfigurationItemVersionStatus.Retired, $"Last version of the form in the destination should be marked as {ConfigurationItemVersionStatus.Retired}"); + imported.ShouldNotBeNull("Form should be created in the destination DB"); + imported.Id.ShouldNotBe(srcForm.Id, "A new Id should be generated for the form during the import"); + imported.Name.ShouldBe(srcForm.Name); + imported.Label.ShouldBe(srcForm.Label); + imported.Description.ShouldBe(srcForm.Description); + imported.Module?.Name.ShouldBe(srcForm.Module?.Name); - imported.Markup.ShouldBe(srcForm.Markup); - imported.ModelType.ShouldBe(srcForm.ModelType); + imported.VersionNo.ShouldBe(dstForm.VersionNo + 1); + imported.VersionStatus.ShouldBe(srcForm.VersionStatus); + + // check prevoius version + imported.ParentVersion.ShouldBe(dstForm, $"{nameof(imported.ParentVersion)} should be set to last version of the form in the destination"); + dstForm.VersionStatus.ShouldBe(ConfigurationItemVersionStatus.Retired, $"Last version of the form in the destination should be marked as {ConfigurationItemVersionStatus.Retired}"); + + imported.Markup.ShouldBe(srcForm.Markup); + imported.ModelType.ShouldBe(srcForm.ModelType); + }); } [Fact] public async Task When_Import_Existing_Form_As_DraftAsync() { var src = PrepareImportContext(); - var srcForm = await src.AddFormAsync(async c => { + var srcModule = await src.GetOrCreateModuleAsync("test-import-existing-form-as-draft-module"); + var srcForm = await src.AddFormAsync(c => { c.ModelType = "test-modelType"; c.Markup = "src-markup"; - c.Name = "test-form"; + c.Name = "test-import-existing-form-as-draft"; c.Label = "test-label"; c.Description = "test-description"; c.VersionNo = 2; c.SetIsLast(true); c.VersionStatus = ConfigurationItemVersionStatus.Live; - c.Module = await src.GetOrCreateModuleAsync("test-module"); + c.Module = srcModule; + return Task.CompletedTask; }); var permissionedObjectManager = Resolve(); var export = new FormConfigurationExport(src.FormRepo, permissionedObjectManager); var exported = await export.ExportItemAsync(srcForm.Id); - var dst = PrepareImportContext(); - var dstForm = await dst.AddFormAsync(async c => { - c.ModelType = "dst-test-modelType"; - c.Markup = "dst-markup"; - c.Name = "test-form"; - c.Label = "dst-test-label"; - c.Description = "dst-test-description"; - - c.VersionNo = 10; - c.SetIsLast(true); - c.VersionStatus = ConfigurationItemVersionStatus.Live; - c.Module = await dst.GetOrCreateModuleAsync("test-module"); + var dstFormRepo = Resolve>(); + var dstModuleRepo = Resolve>(); + Guid dstFormId = Guid.Empty; + await WithUnitOfWorkAsync(async () => { + var dstModule = await GetOrCreateModuleAsync(dstModuleRepo, srcModule.Name); + + var dstForm = await dstFormRepo.InsertAsync(new FormConfiguration { + ModelType = "dst-test-modelType", + Markup = "dst-markup", + Name = srcForm.Name, + Label = "dst-test-label", + Description = "dst-test-description", + VersionNo = 10, + VersionStatus = ConfigurationItemVersionStatus.Live, + Module = dstModule, + }); + dstFormId = dstForm.Id; }); + dstFormId.ShouldNotBe(Guid.Empty, "Failed to create test form"); + + await WithUnitOfWorkAsync(async() => { + var importer = Resolve(); - var asyncExecuter = Resolve(); - var uowManager = Resolve(); - var formManager = new FormManager(dst.FormRepo, dst.ModuleRepo, uowManager, permissionedObjectManager); - var importer = new FormConfigurationImport(dst.ModuleRepo, dst.FrontEndAppRepo, formManager, dst.FormRepo, uowManager, permissionedObjectManager); - var importContext = new PackageImportContext() - { - CreateModules = true, - ImportStatusAs = ConfigurationItemVersionStatus.Draft, - }; - var imported = await importer.ImportItemAsync(exported, importContext) as FormConfiguration; - imported.ShouldNotBeNull(); + var importContext = new PackageImportContext() + { + CreateModules = true, + ImportStatusAs = ConfigurationItemVersionStatus.Draft, + }; + var imported = await importer.ImportItemAsync(exported, importContext) as FormConfiguration; + imported.ShouldNotBeNull(); + + var dstModule = srcForm.Module != null + ? await dstModuleRepo.GetAll().FirstOrDefaultAsync(m => m.Name == srcForm.Module.Name) + : null; - var dstModule = srcForm.Module != null - ? dst.ModuleRepo.GetAll().FirstOrDefault(m => m.Name == srcForm.Module.Name) - : null; + var importedFormFromRepo = await dstFormRepo.GetAll() + .Where(f => f.Name == srcForm.Name && f.Module == dstModule) + .OrderByDescending(f => f.VersionNo) + .FirstOrDefaultAsync(); - var importedFormFromRepo = dst.FormRepo.GetAll() - .Where(f => f.Name == srcForm.Name && f.Module == dstModule) - .OrderByDescending(f => f.VersionNo) - .FirstOrDefault(); + var dstForm = await dstFormRepo.GetAsync(dstFormId); - imported.ShouldBe(importedFormFromRepo, "Imported form should be a form with latest version number in the repo"); + imported.ShouldBe(importedFormFromRepo, "Imported form should be a form with latest version number in the repo"); - imported.ShouldNotBeNull("Form should be created in the destination DB"); - imported.Id.ShouldNotBe(srcForm.Id, "A new Id should be generated for the form during the import"); - imported.Name.ShouldBe(srcForm.Name); - imported.Label.ShouldBe(srcForm.Label); - imported.Description.ShouldBe(srcForm.Description); - imported.Module?.Name.ShouldBe(srcForm.Module?.Name); + imported.ShouldNotBeNull("Form should be created in the destination DB"); + imported.Id.ShouldNotBe(srcForm.Id, "A new Id should be generated for the form during the import"); + imported.Name.ShouldBe(srcForm.Name); + imported.Label.ShouldBe(srcForm.Label); + imported.Description.ShouldBe(srcForm.Description); + imported.Module?.Name.ShouldBe(srcForm.Module?.Name); - imported.VersionNo.ShouldBe(dstForm.VersionNo + 1); - imported.VersionStatus.ShouldBe(ConfigurationItemVersionStatus.Draft); + imported.VersionNo.ShouldBe(dstForm.VersionNo + 1); + imported.VersionStatus.ShouldBe(ConfigurationItemVersionStatus.Draft); - // check prevoius version - imported.ParentVersion.ShouldBe(dstForm, $"{nameof(imported.ParentVersion)} should be set to last version of the form in the destination"); - dstForm.VersionStatus.ShouldBe(ConfigurationItemVersionStatus.Live, $"Status of existing form should remain Live"); + // check prevoius version + imported.ParentVersion.ShouldBe(dstForm, $"{nameof(imported.ParentVersion)} should be set to last version of the form in the destination"); + dstForm.VersionStatus.ShouldBe(ConfigurationItemVersionStatus.Live, $"Status of existing form should remain Live"); - imported.Markup.ShouldBe(srcForm.Markup); - imported.ModelType.ShouldBe(srcForm.ModelType); + imported.Markup.ShouldBe(srcForm.Markup); + imported.ModelType.ShouldBe(srcForm.ModelType); + }); } [Fact] public async Task When_Import_The_Same_Form_TwiceAsync() { - var src = PrepareImportContext(); - var srcForm = await src.AddFormAsync(async c => { + var src = PrepareImportContext(); + var srcModule = await src.GetOrCreateModuleAsync("test-import-same-form-twice-module"); + var srcForm = await src.AddFormAsync(c => { c.ModelType = "test-modelType"; c.Markup = "src-markup"; - c.Name = "test-form"; + c.Name = "test-import-same-form-twice"; c.Label = "test-label"; c.Description = "test-description"; c.VersionNo = 2; c.SetIsLast(true); c.VersionStatus = ConfigurationItemVersionStatus.Live; - c.Module = await src.GetOrCreateModuleAsync("test-module"); + c.Module = srcModule; + return Task.CompletedTask; }); var permissionedObjectManager = Resolve(); var export = new FormConfigurationExport(src.FormRepo, permissionedObjectManager); var exported = await export.ExportItemAsync(srcForm.Id); - var dst = PrepareImportContext(); - var dstForm = await dst.AddFormAsync(async c => { - c.ModelType = "dst-test-modelType"; - c.Markup = "dst-markup"; - c.Name = "test-form"; - c.Label = "dst-test-label"; - c.Description = "dst-test-description"; - - c.VersionNo = 10; - c.SetIsLast(true); - c.VersionStatus = ConfigurationItemVersionStatus.Live; - c.Module = await dst.GetOrCreateModuleAsync("test-module"); + var dstFormRepo = Resolve>(); + var dstModuleRepo = Resolve>(); + Guid dstFormId = Guid.Empty; + await WithUnitOfWorkAsync(async () => { + var dstModule = await GetOrCreateModuleAsync(dstModuleRepo, srcModule.Name); + + var dstForm = await dstFormRepo.InsertAsync(new FormConfiguration { + ModelType = "dst-test-modelType", + Markup = "dst-markup", + Name = srcForm.Name, + Label = "dst-test-label", + Description = "dst-test-description", + + VersionNo = 10, + VersionStatus = ConfigurationItemVersionStatus.Live, + Module = dstModule, + }); }); + - var formsCountBeforeImport = await dst.FormRepo.CountAsync(); - - var asyncExecuter = Resolve(); var uowManager = Resolve(); - using (var uow = uowManager.Begin()) { - var formManager = new FormManager(dst.FormRepo, dst.ModuleRepo, uowManager, permissionedObjectManager); - var importer = new FormConfigurationImport(dst.ModuleRepo, dst.FrontEndAppRepo, formManager, dst.FormRepo, uowManager, permissionedObjectManager); + var formsCountBeforeImport = await dstFormRepo.CountAsync(); + + var importer = Resolve(); var importContext = new PackageImportContext() { CreateModules = true @@ -323,55 +366,17 @@ public async Task When_Import_The_Same_Form_TwiceAsync() var imported1 = await importer.ImportItemAsync(exported, importContext) as FormConfiguration; imported1.ShouldNotBeNull(); - var formsCountAfterFirstImport = await dst.FormRepo.CountAsync(); + var formsCountAfterFirstImport = await dstFormRepo.CountAsync(); formsCountAfterFirstImport.ShouldBe(formsCountBeforeImport + 1, "First import should create one form"); var imported2 = await importer.ImportItemAsync(exported, importContext) as FormConfiguration; imported2.ShouldNotBeNull(); - var formsCountAfterSecondImport = await dst.FormRepo.CountAsync(); + var formsCountAfterSecondImport = await dstFormRepo.CountAsync(); formsCountAfterSecondImport.ShouldBe(formsCountBeforeImport + 1, "Second import shouldn't create new form"); await uow.CompleteAsync(); } - - /* - 1. Store Id of the source item during the import - 2. Before the item import check existing versions of this item, if the item was imported earlier - skip import (allow to override in in the import context) - 3. if imported version is the last one - check hash and update item. Need to find a way to identify was the item changed by the user after import. - If the user made changes manually - don't apply auto update - Try to use ExtSysLastSyncDate here - */ - - /* - var dstModule = srcForm.Module != null - ? dst.ModuleRepo.GetAll().FirstOrDefault(m => m.Name == srcForm.Module.Name) - : null; - - var importedFormFromRepo = dst.FormRepo.GetAll() - .Where(f => f.Name == srcForm.Name && f.Module == dstModule) - .OrderByDescending(f => f.VersionNo) - .FirstOrDefault(); - - imported.ShouldBe(importedFormFromRepo, "Imported form should be a form with latest version number in the repo"); - - imported.ShouldNotBeNull("Form should be created in the destination DB"); - imported.Id.ShouldNotBe(srcForm.Id, "A new Id should be generated for the form during the import"); - imported.Name.ShouldBe(srcForm.Name); - imported.Label.ShouldBe(srcForm.Label); - imported.Description.ShouldBe(srcForm.Description); - imported.Module?.Name.ShouldBe(srcForm.Module?.Name); - - imported.VersionNo.ShouldBe(dstForm.VersionNo + 1); - imported.VersionStatus.ShouldBe(srcForm.VersionStatus); - - // check prevoius version - imported.ParentVersion.ShouldBe(dstForm.Configuration, $"{nameof(imported.ParentVersion)} should be set to last version of the form in the destination"); - dstForm.VersionStatus.ShouldBe(ConfigurationItemVersionStatus.Retired, $"Last version of the form in the destination should be marked as {ConfigurationItemVersionStatus.Retired}"); - - imported.Markup.ShouldBe(srcForm.Markup); - imported.ModelType.ShouldBe(srcForm.ModelType); - */ } [Fact] @@ -380,7 +385,7 @@ public async Task When_Export_Multiple_ItemsAsync() var dataFolder = Path.Combine(AppContext.BaseDirectory, "test-data"); if (!Directory.Exists(dataFolder)) Directory.CreateDirectory(dataFolder); - var zipFileName = Path.Combine(dataFolder, "export.zip"); + var zipFileName = Path.Combine(dataFolder, "package-export.zip"); if (File.Exists(zipFileName)) File.Delete(zipFileName); @@ -391,12 +396,6 @@ public async Task When_Export_Multiple_ItemsAsync() var exporter = new FormConfigurationExport(formRepo, permissionedObjectManager); var packageManager = Resolve(); - /* - var filter = @"{ and: { ""=="": [{ ""var"": ""configuration.module.name"" }, ""shesha""] } }"; - var jslConverter = Resolve(); - var query = jslConverter.ParseExpressionOf(filter); - */ - var formsToExport = new List { "forms", "form-create", "form-details", "form-templates", "form-template-create", "form-template-details", @@ -406,7 +405,6 @@ await WithUnitOfWorkAsync(async () => { var items = await asyncExecuter.ToListAsync(itemsBaseRepo.GetAll() .Where(i => i.IsLast) .Where(i => i.Module != null && i.Module.Name.ToLower() == "shesha" && formsToExport.Contains(i.Name)) - //.Where(query) ); var exportContext = new PreparePackageContext(items, LocalIocManager); @@ -438,49 +436,73 @@ public async Task When_Import_Multiple_ItemsAsync() var dataFolder = Path.Combine(AppContext.BaseDirectory, "test-data"); if (!Directory.Exists(dataFolder)) Directory.CreateDirectory(dataFolder); - var zipFileName = Path.Combine(dataFolder, "export.zip"); + var zipFileName = Path.Combine(dataFolder, "package-import.zip"); if (!File.Exists(zipFileName)) throw new Exception($"Export file '{zipFileName}' doesn't exist"); - var dst = PrepareImportContext(); - var asyncExecuter = Resolve(); - var uowManager = Resolve(); - var permissionedObjectManager = Resolve(); - var formManager = new FormManager(dst.FormRepo, dst.ModuleRepo, uowManager, permissionedObjectManager); - var importer = new FormConfigurationImport(dst.ModuleRepo, dst.FrontEndAppRepo, formManager, dst.FormRepo, uowManager, permissionedObjectManager); - - var packageManager = Resolve(); + await WithUnitOfWorkAsync(async() => { + var importer = Resolve(); - //var importers = DistributionHelper.GetRegisteredImportersDictionary(LocalIocManager); - var importers = DistributionHelper.ToImportersDictionary(new List { - importer, - }); - var importContext = new PackageImportContext { - CreateModules = true, - }; - using (var stream = new FileStream(zipFileName, FileMode.Open)) - { - using (var pack = await packageManager.ReadPackageAsync(stream, new ReadPackageContext(LocalIocManager))) - { - var unsupportedItemTypes = pack.Items.Where(i => i.Importer == null).Select(i => i.ItemType).Distinct().ToList(); - if (unsupportedItemTypes.Any()) - throw new NotSupportedException("Following item types are not supported by the import process: " + unsupportedItemTypes.Delimited(", ")); + var packageManager = Resolve(); - foreach (var item in pack.Items) + var importers = DistributionHelper.ToImportersDictionary(new List { + importer, + }); + var importContext = new PackageImportContext + { + CreateModules = true, + }; + using (var stream = new FileStream(zipFileName, FileMode.Open)) + { + using (var pack = await packageManager.ReadPackageAsync(stream, new ReadPackageContext(LocalIocManager))) { - using (var jsonStream = item.StreamGetter()) + var unsupportedItemTypes = pack.Items.Where(i => i.Importer == null).Select(i => i.ItemType).Distinct().ToList(); + if (unsupportedItemTypes.Any()) + throw new NotSupportedException("Following item types are not supported by the import process: " + unsupportedItemTypes.Delimited(", ")); + + foreach (var item in pack.Items) { - var itemDto = await item.Importer.ReadFromJsonAsync(jsonStream); - await item.Importer.ImportItemAsync(itemDto, importContext); + using (var jsonStream = item.StreamGetter()) + { + var itemDto = await item.Importer.NotNull().ReadFromJsonAsync(jsonStream); + await item.Importer.ImportItemAsync(itemDto, importContext); + } } } } - } + }); + + await WithUnitOfWorkAsync(async() => { + var moduleName = "imported-module"; + + var moduleRepo = Resolve>(); + var module = await moduleRepo.GetAll().FirstOrDefaultAsync(e => e.Name == moduleName); + module.ShouldNotBeNull($"Module '{moduleName}' should be imported from the package"); + + var formRepo = Resolve>(); + var form1Name = "imported-form-1"; + var form1 = await formRepo.FirstOrDefaultAsync(e => e.Module == module && e.Name == form1Name); + form1.ShouldNotBeNull($"form '{form1Name}' should be imported from package"); + + var form2Name = "imported-form-2"; + var form2 = await formRepo.FirstOrDefaultAsync(e => e.Module == module && e.Name == form2Name); + form2.ShouldNotBeNull($"form '{form2Name}' should be imported from package"); + }); } #region private declarations + private async Task GetOrCreateModuleAsync(IRepository repo, string moduleName) + { + var module = await repo.GetAll().FirstOrDefaultAsync(e => e.Name == moduleName); + if (module == null) + { + module = await repo.InsertAsync(new Module { Name = moduleName }); + } + return module; + } + private TestImportContext PrepareImportContext() { return new TestImportContext(GetMemoryDbProvider()); @@ -505,7 +527,7 @@ private static async Task MockFormConfigurationAsync(Func(); - builder.UseInMemoryDatabase(Guid.NewGuid().ToString()).UseInternalServiceProvider(serviceProvider); - - iocManager.IocContainer.Register( - Component - .For>() - .Instance(builder.Options) - .LifestyleSingleton() - ); - */ + WindsorRegistrationHelper.CreateServiceProvider(iocManager.IocContainer, services); } } } diff --git a/shesha-core/test/Shesha.Tests/DomainAttributes/JsonUserType_Tests.cs b/shesha-core/test/Shesha.Tests/DomainAttributes/JsonUserType_Tests.cs index 017bc66c74..d5768f40b8 100644 --- a/shesha-core/test/Shesha.Tests/DomainAttributes/JsonUserType_Tests.cs +++ b/shesha-core/test/Shesha.Tests/DomainAttributes/JsonUserType_Tests.cs @@ -1,19 +1,10 @@ -using Abp.Dependency; -using Newtonsoft.Json; -using NHibernate.Dialect; -using NHibernate.Driver; -using Shesha.Generators; -using Shesha.NHibernate.Filters; -using Shesha.NHibernate.Maps; -using Shesha.NHibernate.Session; +using Newtonsoft.Json; using Shesha.NHibernate.UserTypes; -using Shouldly; -using System; using Xunit; namespace Shesha.Tests.DomainAttributes { - public class JsonUserType_Tests: SheshaNhTestBase + public class JsonUserType_Tests { [Fact] public void Should_Assemble() @@ -31,9 +22,9 @@ public void Should_Assemble() var jsonifiedPerson = JsonConvert.SerializeObject(person); var userType = new JsonUserType(); - var deserialized = (PersonEntity) userType.Assemble(jsonifiedPerson, null); + var deserialized = userType.Assemble(jsonifiedPerson, null) as PersonEntity; - Assert.NotNull(deserialized); + Assert.NotNull(deserialized); Assert.Equal(person.Name, deserialized.Name); Assert.Equal(person.HomeAddress.Street, deserialized.HomeAddress.Street); Assert.Equal(person.HomeAddress.PostalCode, deserialized.HomeAddress.PostalCode); @@ -59,97 +50,7 @@ public void Should_Disassemble() Assert.NotNull(serialized); Assert.IsType(serialized); - Assert.Contains("Street", serialized.ToString()); - } - - [Fact] - public void Should_SavePropertyAsJSON() - { - var person = new PersonEntity - { - Name = "Someone special", - HomeAddress = new Address - { - Street = "24 Fitter Rd", - Town = "Cresslawn", - PostalCode = 1619, - Coordinates = new GeometricCoordinates - { - Latitude = 24.01256, - Longitude = -23.12533 - } - }, - OfficeAddress = new Address - { - Street = "265 West Avenue", - Town = "Die Howes", - PostalCode = 1002, - Coordinates = new GeometricCoordinates - { - Latitude = 24.123654, - Longitude = -23.452365 - } - } - - }; - var personAssembly = person.GetType().Assembly; - - var conventions = new Conventions(Resolve()); - conventions.AddAssembly(personAssembly, "Test_"); - - var nhConfig = new global::NHibernate.Cfg.Configuration(); - nhConfig.DataBaseIntegration(db => - { - db.ConnectionString = @"Data Source=.\MSSQLSERVER2019;Initial Catalog=DsdNpo;User ID=sa;password=1;MultipleActiveResultSets=True"; - - db.Dialect(); - db.Driver(); - db.Timeout = 150; - db.LogFormattedSql = true; - }) - .SetProperty("hbm2ddl.keywords", "auto-quote") - .CurrentSessionContext(); - - nhConfig.AddFilterDefinition(SoftDeleteFilter.GetDefinition()); - nhConfig.AddFilterDefinition(MayHaveTenantFilter.GetDefinition()); - nhConfig.AddFilterDefinition(MustHaveTenantFilter.GetDefinition()); - - conventions.Compile(nhConfig); - - var sessionFactory = nhConfig.BuildSessionFactory(); - - using (var session = sessionFactory.OpenSession()) - { - session.SaveOrUpdate(person); - session.Flush(); - - person.Id.ShouldNotBe(Guid.Empty); - - - var homeQuery = @"select HomeAddress from Test_PersonEntities where Id=:id"; - - var dbItem = session.CreateSQLQuery(homeQuery) - .SetGuid("id", person.Id) - .UniqueResult(); - var homeJsonStr = JsonConvert.SerializeObject(person.HomeAddress); - - Assert.Equal(dbItem, homeJsonStr); - - // Check the office address while I'm at it - var officeQuery = @"select OfficeAddress from Test_PersonEntities where Id=:id"; - var officeAddr = session.CreateSQLQuery(officeQuery) - .SetGuid("id", person.Id) - .UniqueResult(); - var officeJsonStr = JsonConvert.SerializeObject(person.OfficeAddress); - Assert.Equal(officeAddr, officeJsonStr); - - // Checking the get method to see if it's deserializing correctly - var entityItem = session.Get(person.Id); - Assert.NotEqual(default, entityItem.Id); - - Assert.Equal(person.HomeAddress, entityItem.HomeAddress); - Assert.Equal(person.OfficeAddress, entityItem.OfficeAddress); - } + Assert.Contains("street", serialized.ToString()); } } } diff --git a/shesha-core/test/Shesha.Tests/DomainModel/DomainModelTestsBase.cs b/shesha-core/test/Shesha.Tests/DomainModel/DomainModelTestsBase.cs new file mode 100644 index 0000000000..27dd446e54 --- /dev/null +++ b/shesha-core/test/Shesha.Tests/DomainModel/DomainModelTestsBase.cs @@ -0,0 +1,83 @@ +using Abp.Authorization.Roles; +using Abp.Authorization.Users; +using Abp.Domain.Entities; +using Abp.Domain.Repositories; +using Abp.Reflection; +using Shesha.Extensions; +using Shesha.Reflection; +using Shesha.Services; +using Shesha.Tests.Fixtures; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Shesha.Tests.DomainModel +{ + public abstract class DomainModelTestsBase : SheshaNhTestBase + { + protected DomainModelTestsBase(IDatabaseFixture fixture) : base(fixture) + { + } + + [Fact] + public async Task Repository_ShouldFetchAllEntityTypes_Async() + { + var typeFinder = StaticContext.IocManager.Resolve(); + + var types = typeFinder.FindAll().Where(t => t.IsEntityType() + && t != typeof(AggregateRoot) + && t != typeof(UserPermissionSetting) + && t != typeof(RolePermissionSetting) + && t.Assembly != this.GetType().Assembly + ).ToList(); + + var errors = new Dictionary(); + + await WithUnitOfWorkAsync(async() => { + foreach (var type in types) + { + var idType = type.GetEntityIdType(); + var testerType = typeof(RepositoryTester<,>).MakeGenericType(type, idType); + var repositoryType = typeof(IRepository<,>).MakeGenericType(type, idType); + var repository = Resolve(repositoryType); + + var tester = Resolve(testerType, repository).ForceCastAs(); + + try + { + await tester.TryFetchAsync(); + } + catch (Exception e) + { + errors.Add(type, e); + } + } + }); + + // Assert + errors.ShouldBeEmpty(); + } + + public interface IRepositoryTester + { + Task TryFetchAsync(); + } + + public class RepositoryTester: IRepositoryTester where T: class, IEntity + { + private readonly IRepository _repo; + + public RepositoryTester(IRepository repo) + { + _repo = repo; + } + public Task TryFetchAsync() + { + return _repo.GetAll().OrderBy(e => e.Id).FirstOrDefaultAsync(); + } + } + } +} \ No newline at end of file diff --git a/shesha-core/test/Shesha.Tests/DomainModel/DomainModel_Tests.cs b/shesha-core/test/Shesha.Tests/DomainModel/DomainModel_Tests.cs deleted file mode 100644 index 40e7e39e15..0000000000 --- a/shesha-core/test/Shesha.Tests/DomainModel/DomainModel_Tests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Abp.Reflection; -using Shesha.Extensions; -using Shesha.Services; -using Shouldly; -using System; -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace Shesha.Tests.DomainModel -{ - public class DomainModel_Tests : SheshaNhTestBase - { - public DomainModel_Tests() - { - - } - - [Fact] - public void EntitiesRead_Test() - { - var typeFinder = StaticContext.IocManager.Resolve(); - - var types = typeFinder.FindAll().Where(t => t.IsEntityType() && !t.Assembly.FullName.StartsWith("Abp")).ToList(); - - var errors = new Dictionary(); - - UsingDbSession(session => - { - foreach (var type in types) - { - try - { - var hql = $"select Id from {type.FullName}"; - var list = session.CreateQuery(hql).SetMaxResults(1).List(); - } - catch (Exception e) - { - errors.Add(type, e); - } - } - }); - - // Assert - errors.Any().ShouldBe(false); - } - } -} diff --git a/shesha-core/test/Shesha.Tests/DomainModel/PostgreSqlDomainModelTests.cs b/shesha-core/test/Shesha.Tests/DomainModel/PostgreSqlDomainModelTests.cs new file mode 100644 index 0000000000..c6283582a2 --- /dev/null +++ b/shesha-core/test/Shesha.Tests/DomainModel/PostgreSqlDomainModelTests.cs @@ -0,0 +1,13 @@ +using Shesha.Tests.Fixtures; +using Xunit; + +namespace Shesha.Tests.DomainModel +{ + [Collection(PostgreSqlCollection.Name)] + public class PostgreSqlDomainModelTests : DomainModelTestsBase + { + public PostgreSqlDomainModelTests(PostgreSqlFixture fixture) : base(fixture) + { + } + } +} \ No newline at end of file diff --git a/shesha-core/test/Shesha.Tests/DomainModel/SqlServerDomainModelTests.cs b/shesha-core/test/Shesha.Tests/DomainModel/SqlServerDomainModelTests.cs new file mode 100644 index 0000000000..16fc927cf4 --- /dev/null +++ b/shesha-core/test/Shesha.Tests/DomainModel/SqlServerDomainModelTests.cs @@ -0,0 +1,13 @@ +using Shesha.Tests.Fixtures; +using Xunit; + +namespace Shesha.Tests.DomainModel +{ + [Collection(SqlServerCollection.Name)] + public class SqlServerDomainModelTests : DomainModelTestsBase + { + public SqlServerDomainModelTests(SqlServerFixture fixture) : base(fixture) + { + } + } +} \ No newline at end of file diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDtoModelBinder_Tests.cs b/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDtoModelBinder_Tests.cs index 78720ba04b..2847435083 100644 --- a/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDtoModelBinder_Tests.cs +++ b/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDtoModelBinder_Tests.cs @@ -1,6 +1,4 @@ -using Abp.Runtime.Caching; -using Abp.TestBase; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -13,7 +11,11 @@ using Shesha.DynamicEntities; using Shesha.DynamicEntities.Cache; using Shesha.DynamicEntities.Dtos; +using Shesha.Exceptions; +using Shesha.Reflection; using Shesha.Tests.DynamicEntities.Mvc; +using Shesha.Tests.Fixtures; +using Shesha.Utilities; using Shouldly; using System; using System.Collections.Generic; @@ -24,17 +26,23 @@ namespace Shesha.Tests.DynamicEntities { - public class DynamicDtoModelBinder_Tests : AbpIntegratedTestBase + [Collection(SqlServerCollection.Name)] + public class DynamicDtoModelBinder_Tests : SheshaNhTestBase { + public DynamicDtoModelBinder_Tests(SqlServerFixture fixture) : base(fixture) + { + } + [Fact] public async Task BindFlatModel_TestAsync() { - var bindingResult = await BindAsync("flatModel.json", "flatModel.metadata.json"); + var bindingResult = await BindAsync("flatModel.json", "flatModel-metadata.json"); // Assert Assert.True(bindingResult.IsModelSet); var model = bindingResult.Model; + model.ShouldNotBeNull(); var testItems = new Dictionary { @@ -56,14 +64,17 @@ public async Task BindFlatModel_TestAsync() [Fact] public async Task Bind2NestedLevels_TestAsync() { - var bindingResult = await BindAsync("nested2Levels.json", "nested2Levels.metadata.json"); + var bindingResult = await BindAsync("nested2Levels.json", "nested2Levels-metadata.json"); // Assert Assert.True(bindingResult.IsModelSet); var model = bindingResult.Model; + model.ShouldNotBeNull(); var (level1_Prop, level1_Value) = GetPropertyAndValue(model, "NestedLevel1"); + level1_Value.ShouldNotBeNull(); + var (level1_Prop1_Prop, level1_Prop1_Value) = GetPropertyAndValue(level1_Value, "NestedLevel1_Prop1"); level1_Prop1_Value.ShouldBe("NestedLevel1_Prop1 string value"); } @@ -71,32 +82,38 @@ public async Task Bind2NestedLevels_TestAsync() [Fact] public async Task Bind3NestedLevels_TestAsync() { - var bindingResult = await BindAsync("nested3Levels.json", "nested3Levels.metadata.json"); + var bindingResult = await BindAsync("nested3Levels.json", "nested3Levels-metadata.json"); // Assert Assert.True(bindingResult.IsModelSet); var model = bindingResult.Model; + model.ShouldNotBeNull(); var (level1_Prop, level1_Value) = GetPropertyAndValue(model, "NestedLevel1"); + level1_Value.ShouldNotBeNull(); + var (level1_Prop1_Prop, level1_Prop1_Value) = GetPropertyAndValue(level1_Value, "NestedLevel1_Prop1"); level1_Prop1_Value.ShouldBe("NestedLevel1_Prop1 string value"); var (level1_level2_Prop, level1_level2_Value) = GetPropertyAndValue(level1_Value, "NestedLevel2"); + level1_level2_Value.ShouldNotBeNull(); var (level1_level2_Prop1_Prop, level1_level2_Prop1_Value) = GetPropertyAndValue(level1_level2_Value, "NestedLevel2_Prop1"); level1_level2_Prop1_Value.ShouldBe("NestedLevel2_Prop1 string value"); } + /* TODO: review and uncomment [Fact] public async Task BindEntityReference_DtoMode_TestAsync() { - var bindingResult = await BindAsync("entityReference_DtoMode.json", "entityReference_DtoMode.metadata.json"); + var bindingResult = await BindAsync("entityReference_DtoMode.json", "entityReference_DtoMode-metadata.json"); // Assert Assert.True(bindingResult.IsModelSet); var model = bindingResult.Model; + model.ShouldNotBeNull(); var testItems = new Dictionary { @@ -114,10 +131,11 @@ public async Task BindEntityReference_DtoMode_TestAsync() value.ShouldBe(item.Value); } } + */ #region private methods - private (PropertyInfo, object) GetPropertyAndValue(object container, string propertyName, bool requireProperty = true, bool requireValue = true) + private (PropertyInfo?, object?) GetPropertyAndValue(object container, string propertyName, bool requireProperty = true, bool requireValue = true) { var property = container.GetType().GetProperty(propertyName); if (requireProperty && property == null) @@ -146,11 +164,13 @@ private async Task BindAsync(string jsonResourceName .Verifiable(); var inputFormatter = mockInputFormatter.Object; - var bindingContext = GetBindingContext(typeof(TModel)); + using var jsonContentStream = GetType().Assembly.GetEmbeddedResourceStream($"{GetType().Namespace}.Resources.{jsonResourceName}"); + var bindingContext = GetBindingContext(typeof(TModel), jsonContentStream); var mockDtoBuilder = await GetDtoBuilderAsync(schemaResourceName); var binder = CreateBinder(new[] { inputFormatter }, mockDtoBuilder); + // Act await binder.BindModelAsync(bindingContext); @@ -167,12 +187,12 @@ private async Task ReadJsonRequestAsync(Type modelType, string jsonResou { var content = await GetResourceStringAsync($"{this.GetType().Namespace}.Resources.{jsonResourceName}", this.GetType().Assembly); var deserialized = JsonConvert.DeserializeObject(content, modelType); - return deserialized; + return deserialized.NotNull(); } private async Task GetResourceStringAsync(string resourceName, Assembly assembly) { - using (var stream = assembly.GetManifestResourceStream(resourceName)) + using (var stream = assembly.GetEmbeddedResourceStream(resourceName)) { using (var sr = new StreamReader(stream)) { @@ -185,7 +205,11 @@ private static DynamicDtoModelBinder CreateBinder(IList formatt { var options = new MvcOptions(); var binder = CreateBinder(formatters, options, dtoBuilder); - //binder.AllowEmptyBody = treatEmptyInputAsDefaultValueOption; + binder.SetDefaultSettings(new DynamicMappingSettings() + { + UseDtoForEntityReferences = true, + UseDynamicDtoProxy = true, + }); return binder; } @@ -195,20 +219,19 @@ private static DynamicDtoModelBinder CreateBinder(IList formatt return new DynamicDtoModelBinder(formatters, new TestHttpRequestStreamReaderFactory(), NullLoggerFactory.Instance, mvcOptions, dtoBuilder); } - private static DefaultModelBindingContext GetBindingContext( - Type modelType, - HttpContext httpContext = null, - IModelMetadataProvider metadataProvider = null) + private static DefaultModelBindingContext GetBindingContext(Type modelType, Stream jsonContentStream) { - if (httpContext == null) + var httpContext = new DefaultHttpContext() { - httpContext = new DefaultHttpContext(); - } + Request = + { + Body = jsonContentStream, + ContentLength = jsonContentStream.Length, + ContentType = "application/json" + } + }; - if (metadataProvider == null) - { - metadataProvider = new EmptyModelMetadataProvider(); - } + var metadataProvider = new EmptyModelMetadataProvider(); var bindingContext = new DefaultModelBindingContext { @@ -216,7 +239,6 @@ private static DefaultModelBindingContext GetBindingContext( { HttpContext = httpContext, }, - //FieldName = "someField", IsTopLevelObject = true, ModelMetadata = metadataProvider.GetMetadataForType(modelType), ModelName = "", @@ -236,12 +258,13 @@ private Task GetDtoBuilderAsync(string schemaResourceNam .Returns(async () => { var schema = await ReadJsonRequestAsync(typeof(List), schemaResourceName) as List; - return schema; + return schema.NotNull(); }); var entityConfigStore = LocalIocManager.Resolve(); - var cacheManager = LocalIocManager.Resolve(); - var builder = new DynamicDtoTypeBuilder(entityConfigCacheMock.Object, entityConfigStore, cacheManager); + var fullProxyCacheHolder = LocalIocManager.Resolve(); + var dynamicTypeCacheHolder = LocalIocManager.Resolve(); + var builder = new DynamicDtoTypeBuilder(entityConfigCacheMock.Object, entityConfigStore, fullProxyCacheHolder, dynamicTypeCacheHolder); return Task.FromResult(builder); } diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDtoTypeBuildingContext_Tests.cs b/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDtoTypeBuildingContext_Tests.cs index f6e787a274..edb4a4f177 100644 --- a/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDtoTypeBuildingContext_Tests.cs +++ b/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDtoTypeBuildingContext_Tests.cs @@ -1,11 +1,10 @@ -using Abp.TestBase; -using Shesha.DynamicEntities; +using Shesha.DynamicEntities; using Shouldly; using Xunit; namespace Shesha.Tests.DynamicEntities { - public class DynamicDtoTypeBuildingContext_Tests : AbpIntegratedTestBase + public class DynamicDtoTypeBuildingContext_Tests { [Fact] public void NamePrefixSequence_Test() diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDto_Tests.cs b/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDto_Tests.cs index 021639f571..425b2673a7 100644 --- a/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDto_Tests.cs +++ b/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicDto_Tests.cs @@ -1,6 +1,4 @@ -using Abp.Runtime.Caching; -using Abp.TestBase; -using Moq; +using Moq; using Shesha.Configuration.Runtime; using Shesha.Domain; using Shesha.DynamicEntities; @@ -8,6 +6,7 @@ using Shesha.DynamicEntities.Dtos; using Shesha.Metadata; using Shesha.Tests.DynamicEntities.Dtos; +using Shesha.Tests.Fixtures; using Shouldly; using System; using System.Collections.Generic; @@ -17,8 +16,13 @@ namespace Shesha.Tests.DynamicEntities { - public class DynamicDto_Tests : AbpIntegratedTestBase//SheshaNhTestBase - { + [Collection(SqlServerCollection.Name)] + public class DynamicDto_Tests : SheshaNhTestBase + { + public DynamicDto_Tests(SqlServerFixture fixture) : base(fixture) + { + } + [Fact] public async Task BuildFullDynamicDto_TestAsync() { @@ -32,12 +36,14 @@ public async Task BuildFullDynamicDto_TestAsync() var r = result as List; - return Task.FromResult(r); + return Task.FromResult?>(r); }); - var entityConfigStore = LocalIocManager.Resolve(); - var cacheManager = LocalIocManager.Resolve(); - var builder = new DynamicDtoTypeBuilder(entityConfigCacheMock.Object, entityConfigStore, cacheManager); + var entityConfigStore = LocalIocManager.Resolve(); + var fullProxyCacheHolder = LocalIocManager.Resolve(); + var dynamicTypeCacheHolder = LocalIocManager.Resolve(); + + var builder = new DynamicDtoTypeBuilder(entityConfigCacheMock.Object, entityConfigStore, fullProxyCacheHolder, dynamicTypeCacheHolder); var baseDtoType = typeof(DynamicDto); @@ -65,12 +71,14 @@ public async Task BuildFullDynamicDto_HasNoSideEffect_TestAsync() .Returns(() => { var result = new EntityPropertyDtoList(); var r = result as List; - return Task.FromResult(r); + return Task.FromResult?>(r); }); var entityConfigStore = LocalIocManager.Resolve(); - var cacheManager = LocalIocManager.Resolve(); - var builder = new DynamicDtoTypeBuilder(entityConfigCacheMock.Object, entityConfigStore, cacheManager); + var fullProxyCacheHolder = LocalIocManager.Resolve(); + var dynamicTypeCacheHolder = LocalIocManager.Resolve(); + + var builder = new DynamicDtoTypeBuilder(entityConfigCacheMock.Object, entityConfigStore, fullProxyCacheHolder, dynamicTypeCacheHolder); var baseDtoType = typeof(DynamicDto); @@ -117,12 +125,14 @@ public async Task BuildDynamicDto_TestAsync() var r = result as List; - return Task.FromResult(r); + return Task.FromResult?>(r); }); var entityConfigStore = LocalIocManager.Resolve(); - var cacheManager = LocalIocManager.Resolve(); - var builder = new DynamicDtoTypeBuilder(entityConfigCacheMock.Object, entityConfigStore, cacheManager); + var fullProxyCacheHolder = LocalIocManager.Resolve(); + var dynamicTypeCacheHolder = LocalIocManager.Resolve(); + + var builder = new DynamicDtoTypeBuilder(entityConfigCacheMock.Object, entityConfigStore, fullProxyCacheHolder, dynamicTypeCacheHolder); var baseDtoType = typeof(DynamicDto); @@ -153,4 +163,4 @@ public async Task BuildDynamicDto_TestAsync() nestedProperties.ShouldContain(p => p.Name == supervisorLastNamePropName); } } -} +} \ No newline at end of file diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicEntity_Tests.cs b/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicEntity_Tests.cs deleted file mode 100644 index 3c46c264f8..0000000000 --- a/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicEntity_Tests.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Abp.Domain.Entities; -using Abp.TestBase; -using NHibernate.Dialect; -using NHibernate.Driver; -using Shesha.DynamicEntities; -using Shesha.Generators; -using Shesha.Migrations; -using Shesha.NHibernate.Filters; -using Shesha.NHibernate.Maps; -using Shesha.NHibernate.Session; -using Shouldly; -using System; -using System.Collections.Generic; - -namespace Shesha.Tests.DynamicEntities -{ - public class DynamicEntity_Tests : AbpIntegratedTestBase//SheshaNhTestBase - { - //[Fact] - public void ConstructDynamicEntity_Test() - { - try - { - const string firstNamePropName = "FirstName"; - - var dynamicEntityMeta = new DynamicEntity - { - Name = "DynamicPerson", - Properties = new DynamicPropertyList - { - { "Id", typeof(Guid) }, - { firstNamePropName, typeof(string) }, - { "LastName", typeof(string) }, - { "Age", typeof(int) }, - { "BirthDate", typeof(DateTime?) }, - } - }; - - var dynamicPersonType = ShaEntityTypeBuilder.CompileResultType(dynamicEntityMeta); - var dynamicPerson = Activator.CreateInstance(dynamicPersonType); - - dynamicPerson.ShouldNotBeNull(); - - var firstNameTestValue = "John"; - var propInfo = dynamicPerson.GetType().GetProperty(firstNamePropName); - propInfo.SetValue(dynamicPerson, firstNameTestValue); - - var firstNameValue = propInfo.GetValue(dynamicPerson); - - firstNameValue.ShouldBe(firstNameTestValue); - - var nhConfig = new global::NHibernate.Cfg.Configuration(); - nhConfig.DataBaseIntegration(db => - { - db.ConnectionString = @"Data Source=.\sql2019;Initial Catalog=SheshaDemo;Integrated Security=True"; - - db.Dialect(); - db.Driver(); - db.Timeout = 150; - db.LogFormattedSql = true; - }) - .SetProperty("hbm2ddl.keywords", "auto-quote") - .CurrentSessionContext(); - - // register filters - nhConfig.AddFilterDefinition(SoftDeleteFilter.GetDefinition()); - nhConfig.AddFilterDefinition(MayHaveTenantFilter.GetDefinition()); - nhConfig.AddFilterDefinition(MustHaveTenantFilter.GetDefinition()); - - var conventions = new Conventions(Resolve()); - var dynamicPersonAssembly = dynamicPerson.GetType().Assembly; - conventions.AddAssembly(dynamicPersonAssembly, "Test_"); - conventions.Compile(nhConfig); - - var sessionFactory = nhConfig.BuildSessionFactory(); - - var migrationsGenerator = LocalIocManager.Resolve(); - var migration = migrationsGenerator.GenerateMigrations(new List { dynamicPersonType }); - - using (var session = sessionFactory.OpenSession()) - { - // save our person - session.SaveOrUpdate(dynamicPerson); - session.Flush(); - var person = dynamicPerson as Entity; - person.ShouldNotBeNull(); - person.Id.ShouldNotBe(Guid.Empty); - - var query = session.CreateQuery($"select ent from {dynamicPersonType.FullName} ent"); - var items = query.List(); - items.Count.ShouldBeGreaterThan(0); - - var fetchedPerson = session.Get(dynamicPersonType, person.Id) as Entity; - fetchedPerson.ShouldNotBeNull(); - fetchedPerson.Id.ShouldBe(person.Id); - } - } - catch (Exception) - { - throw; - } - } - } -} diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicPropertyManager_Tests.cs b/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicPropertyManager_Tests.cs index d9f0b8da35..2a8b6a0cc4 100644 --- a/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicPropertyManager_Tests.cs +++ b/shesha-core/test/Shesha.Tests/DynamicEntities/DynamicPropertyManager_Tests.cs @@ -1,6 +1,4 @@ using Abp.Domain.Repositories; -using Abp.Domain.Uow; -using Abp.Runtime.Caching; using Moq; using Shesha.Configuration.Runtime; using Shesha.Domain; @@ -9,8 +7,9 @@ using Shesha.DynamicEntities.Cache; using Shesha.DynamicEntities.Dtos; using Shesha.Metadata; -using Shesha.NHibernate.UoW; +using Shesha.Reflection; using Shesha.Services; +using Shesha.Tests.Fixtures; using Shouldly; using System; using System.Collections.Generic; @@ -20,14 +19,16 @@ namespace Shesha.Tests.DynamicEntities { + [Collection(SqlServerCollection.Name)] public class DynamicPropertyManager_Tests : SheshaNhTestBase { + private readonly IRepository _propertyValueRepo; + private readonly IRepository _propertyRepo; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - public DynamicPropertyManager_Tests() + public DynamicPropertyManager_Tests(SqlServerFixture fixture) : base(fixture) { - _unitOfWorkManager = Resolve(); + _propertyValueRepo = Resolve>(); + _propertyRepo = Resolve>(); } [Fact] @@ -82,43 +83,45 @@ public async Task SaveLoad_TestAsync() { result.Add(prop.Key); } - return Task.FromResult(result); + return Task.FromResult?>(result); }); - var entityConfigStore = LocalIocManager.Resolve(); - var cacheManager = LocalIocManager.Resolve(); - var builder = new DynamicDtoTypeBuilder(entityConfigCacheMock.Object, entityConfigStore, cacheManager); + var entityConfigStore = Resolve(); + var fullProxyCacheHolder = Resolve(); + var dynamicTypeCacheHolder = Resolve(); + var builder = new DynamicDtoTypeBuilder(entityConfigCacheMock.Object, entityConfigStore, fullProxyCacheHolder, dynamicTypeCacheHolder); var baseDtoType = typeof(DynamicDto); var context = new DynamicDtoTypeBuildingContext() { ModelType = baseDtoType }; var dtoType = await builder.BuildDtoFullProxyTypeAsync(baseDtoType, context); var dto = Activator.CreateInstance(dtoType) as DynamicDto; + dto.ShouldNotBeNull(); - var properties = dto?.GetType().GetProperties(); + var properties = dto.GetType().GetProperties(); var serializationManager = Resolve(); // Update DTO properties values with test data foreach (var prop in props) { - var p = properties?.FirstOrDefault(x => prop.Key.Name.ToLower() == x.Name.ToLower()); + var p = properties.Single(x => prop.Key.Name.ToLower() == x.Name.ToLower()); var val = prop.Value; if (prop.Key.DataType == DataTypes.Object) { - val = serializationManager.DeserializeProperty(p?.PropertyType, serializationManager.SerializeProperty(prop.Key, val)); + val = serializationManager.DeserializeProperty(p.PropertyType, serializationManager.SerializeProperty(prop.Key, val)); } - p?.SetValue(dto, val); + p.SetValue(dto, val); } - using (var uow = _unitOfWorkManager.Begin()) + using (var uow = NewNhUnitOfWork()) { - var nhuow = uow as NhUnitOfWork; - var session = nhuow?.GetSession(); +#pragma warning disable IDISP001 // Dispose created + var session = uow.GetSession(); +#pragma warning restore IDISP001 // Dispose created // Create temporary Entity Properties configs var entityConfigRepo = Resolve>(); var entityPropRepo = Resolve>(); - var config = entityConfigRepo.GetAll().FirstOrDefault(x => - x.TypeShortAlias == typeof(Person).GetEntityConfiguration().TypeShortAlias); + var config = entityConfigRepo.GetAll().First(x => x.TypeShortAlias == typeof(Person).GetEntityConfiguration().TypeShortAlias); foreach (var prop in props) { var propConf = new EntityProperty() @@ -135,15 +138,15 @@ public async Task SaveLoad_TestAsync() { ParentProperty = propConf, EntityConfig = config, - Name = prop.Key.Name, - DataType = prop.Key.DataType, + Name = childProp.Name, + DataType = childProp.DataType, Source = MetadataSourceType.UserDefined }; childProp.Id = (await entityPropRepo.InsertAsync(childPropConf)).Id; } } - session?.Flush(); + session.Flush(); try { @@ -164,17 +167,17 @@ public async Task SaveLoad_TestAsync() // Save dynamic properties to DB await dynamicPropertyManager.MapDtoToEntityAsync, Person, Guid>(dto, entity); - session?.Flush(); + session.Flush(); - session?.Clear(); + session.Clear(); // Get entity from DB var newEntity = await personRepo.GetAsync(id); // Create new DTO and map values from entity to DTO var newDto = Activator.CreateInstance(dtoType) as DynamicDto; - await dynamicPropertyManager.MapEntityToDtoAsync, Person, Guid>(newEntity, - newDto); + newDto.ShouldNotBeNull(); + await dynamicPropertyManager.MapEntityToDtoAsync, Person, Guid>(newEntity, newDto); // Check values. Values from the DB should be the same as from the test list var newProperties = newDto?.GetType().GetProperties(); @@ -184,12 +187,12 @@ await dynamicPropertyManager.MapEntityToDtoAsync, Perso var dtoValue = p?.GetValue(newDto); if (prop.Key.DataType == DataTypes.Object) { - var childProperties = dtoValue.GetType().GetProperties(); + var childProperties = dtoValue.NotNull().GetType().GetProperties(); foreach (var childProp in prop.Key.Properties) { - var childP = childProperties.FirstOrDefault(x => childProp.Name.ToLower() == x.Name.ToLower()); + var childP = childProperties.First(x => childProp.Name.ToLower() == x.Name.ToLower()); var dtoChildValue = childP.GetValue(dtoValue); - dtoChildValue.ShouldBe(prop.Value.GetType().GetProperty(childProp.Name).GetValue(prop.Value)); + dtoChildValue.ShouldBe(prop.Value.NotNull().GetType().GetRequiredProperty(childProp.Name).GetValue(prop.Value)); } } else @@ -206,21 +209,26 @@ await dynamicPropertyManager.MapEntityToDtoAsync, Perso } finally { + var propertyRepo = Resolve>(); // delete temporary values and properties configs foreach (var prop in props) { - session.CreateSQLQuery($"delete from Frwk_EntityPropertyValues where EntityPropertyId = '{prop.Key.Id}'").ExecuteUpdate(); - if (prop.Key.DataType == DataTypes.Object) - { - foreach (var childProp in prop.Key.Properties) - { - session.CreateSQLQuery($"delete from Frwk_EntityProperties where Id = '{childProp.Id}'").ExecuteUpdate(); - } - } - session.CreateSQLQuery($"delete from Frwk_EntityProperties where Id = '{prop.Key.Id}'").ExecuteUpdate(); + await DeletePropertyAsync(prop.Key); } } } } + + private async Task DeletePropertyAsync(EntityPropertyDto dto) + { + await _propertyValueRepo.HardDeleteAsync(e => e.EntityProperty != null && e.EntityProperty.Id == dto.Id); + if (dto.DataType == DataTypes.Object) + { + foreach (var childProp in dto.Properties) + await DeletePropertyAsync(childProp); + } + + await _propertyRepo.HardDeleteAsync(e => e.Id == dto.Id); + } } } diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/EntityModelBinder_Tests.cs b/shesha-core/test/Shesha.Tests/DynamicEntities/EntityModelBinder_Tests.cs index 26118d9f95..41400c5182 100644 --- a/shesha-core/test/Shesha.Tests/DynamicEntities/EntityModelBinder_Tests.cs +++ b/shesha-core/test/Shesha.Tests/DynamicEntities/EntityModelBinder_Tests.cs @@ -1,11 +1,13 @@ -using Abp.Domain.Repositories; +using Abp.Auditing; +using Abp.Domain.Repositories; using Abp.Domain.Uow; using Newtonsoft.Json.Linq; using Shesha.Domain; using Shesha.Domain.Attributes; using Shesha.DynamicEntities; using Shesha.DynamicEntities.Binder; -using Shesha.NHibernate.UoW; +using Shesha.Reflection; +using Shesha.Tests.Fixtures; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -16,13 +18,14 @@ namespace Shesha.Tests.DynamicEntities { + [Collection(SqlServerCollection.Name)] public class EntityModelBinder_Tests : SheshaNhTestBase { private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IEntityModelBinder _entityModelBinder; private readonly IRepository _personRepo; - public EntityModelBinder_Tests() + public EntityModelBinder_Tests(SqlServerFixture fixture) : base(fixture) { _unitOfWorkManager = Resolve(); _entityModelBinder = Resolve(); @@ -34,10 +37,9 @@ public async Task Datetime_TestAsync() { LoginAsHostAdmin(); - using (var uow = _unitOfWorkManager.Begin()) + using (var uow = NewNhUnitOfWork()) { - var nhuow = uow as NhUnitOfWork; - Person testPerson = null; + Person? testPerson = null; try { @@ -60,10 +62,9 @@ public async Task RefList_TestAsync() { LoginAsHostAdmin(); - using (var uow = _unitOfWorkManager.Begin()) + using (var uow = NewNhUnitOfWork()) { - var nhuow = uow as NhUnitOfWork; - TestOrganisationAllowContactUpdate newTestOrg1 = null; + TestOrganisationAllowContactUpdate? newTestOrg1 = null; try { @@ -95,11 +96,10 @@ public async Task FormField_TestAsync() var repoOrg = Resolve>(); var testOrgRepo = Resolve>(); - using (var uow = _unitOfWorkManager.Begin()) + using (var uow = NewNhUnitOfWork()) { - var nhuow = uow as NhUnitOfWork; - Person newTestPerson1 = null; - TestOrganisationAllowContactUpdate newTestOrg1 = null; + Person? newTestPerson1 = null; + TestOrganisationAllowContactUpdate? newTestOrg1 = null; try { @@ -112,8 +112,10 @@ public async Task FormField_TestAsync() var testResult1 = await _entityModelBinder.BindPropertiesAsync(jObject1, newTestOrg1, new EntityModelBindingContext()); Assert.True(testResult1); await testOrgRepo.InsertAsync(newTestOrg1); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); newTestOrg1 = testOrgRepo.GetAll().FirstOrDefault(x => x.Name == "TestOrganisation"); + Assert.True(newTestOrg1 != null); + newTestPerson1 = _personRepo.GetAll().FirstOrDefault(x => x.FirstName == "TestPerson"); Assert.True(newTestPerson1 != null); Assert.True(string.IsNullOrEmpty(newTestOrg1.Description)); @@ -123,7 +125,7 @@ public async Task FormField_TestAsync() { if (newTestOrg1 != null) testOrgRepo.HardDelete(newTestOrg1); if (newTestPerson1 != null) _personRepo.HardDelete(newTestPerson1); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); } } } @@ -132,14 +134,12 @@ public async Task FormField_TestAsync() public async Task CascadeRuleEntityFinder_TestAsync() { LoginAsHostAdmin(); - using (var uow = _unitOfWorkManager.Begin()) + using (var uow = NewNhUnitOfWork()) { - var nhuow = uow as NhUnitOfWork; - var ent = new Person() { FirstName = "TestPerson", LastName = "TestPerson", DateOfBirth = new DateTime(1978, 09, 24, 13, 24, 17) }; await _personRepo.InsertAsync(ent); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); var checkEnt = new Person() { FirstName = "TestPerson", LastName = "TestPerson", DateOfBirth = new DateTime(1978, 09, 24) }; @@ -152,7 +152,7 @@ public async Task CascadeRuleEntityFinder_TestAsync() Assert.Null(newEnt); _personRepo.HardDelete(ent); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); } } @@ -164,25 +164,24 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() var repoOrg = Resolve>(); var testOrgRepo = Resolve>(); - using (var uow = _unitOfWorkManager.Begin()) + using (var uow = NewNhUnitOfWork()) { - var nhuow = uow as NhUnitOfWork; - Person newTestPerson1 = null; - Person newTestPerson2 = null; - TestOrganisationAllowContactUpdate newTestOrg1 = null; - TestOrganisationAllowContactUpdate newTestOrg2 = null; - TestOrganisationAllowContactUpdate newTestOrg3 = null; - TestOrganisationAllowContactUpdate newTestOrg4 = null; - TestOrganisationAllowContactUpdate newTestOrg5 = null; - TestOrganisationAllowContactUpdate newTestOrg6 = null; - TestOrganisationAllowContactUpdate newTestOrg7 = null; - TestOrganisationAllowContactUpdate newTestOrg8 = null; + Person? newTestPerson1 = null; + Person? newTestPerson2 = null; + TestOrganisationAllowContactUpdate? newTestOrg1 = null; + TestOrganisationAllowContactUpdate? newTestOrg2 = null; + TestOrganisationAllowContactUpdate? newTestOrg3 = null; + TestOrganisationAllowContactUpdate? newTestOrg4 = null; + TestOrganisationAllowContactUpdate? newTestOrg5 = null; + TestOrganisationAllowContactUpdate? newTestOrg6 = null; + TestOrganisationAllowContactUpdate? newTestOrg7 = null; + TestOrganisationAllowContactUpdate? newTestOrg8 = null; try { // Child creation is not allowed var errors = new List(); - var newOrg = new Organisation(); + var newOrg = new TestOrganisationDisallowContactUpdate(); var json1 = @"{ 'name': 'TestOrganisation', 'primaryContact': { 'firstName': 'TestPerson' } }"; var jObject1 = JObject.Parse(json1); var result = await _entityModelBinder.BindPropertiesAsync(jObject1, newOrg, new EntityModelBindingContext()); @@ -194,8 +193,9 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() var testResult1 = await _entityModelBinder.BindPropertiesAsync(jObject1, newTestOrg1, new EntityModelBindingContext()); Assert.True(testResult1); await testOrgRepo.InsertAsync(newTestOrg1); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); newTestOrg1 = testOrgRepo.GetAll().FirstOrDefault(x => x.Name == "TestOrganisation"); + Assert.NotNull(newTestOrg1); newTestPerson1 = _personRepo.GetAll().FirstOrDefault(x => x.FirstName == "TestPerson"); Assert.True(newTestPerson1 != null); // Check for prepared value @@ -217,7 +217,7 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() var testResult3 = await _entityModelBinder.BindPropertiesAsync(jObject3, newTestOrg3, new EntityModelBindingContext()); Assert.True(testResult3); await testOrgRepo.InsertAsync(newTestOrg3); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); Assert.True(newTestOrg3.PrimaryContact.Id == newTestPerson1?.Id); // Child creation is allowed and choosing by FirstName with updating child LastName @@ -229,8 +229,9 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() var testResult4 = await _entityModelBinder.BindPropertiesAsync(jObject4, newTestOrg4, new EntityModelBindingContext()); Assert.True(testResult4); await testOrgRepo.InsertAsync(newTestOrg4); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); newTestOrg4 = testOrgRepo.GetAll().FirstOrDefault(x => x.Name == "TestOrganisation4"); + Assert.NotNull(newTestOrg4); Assert.True(newTestOrg4.PrimaryContact.Id == newTestPerson1?.Id); Assert.True(newTestOrg4.PrimaryContact.LastName == "TestLastName"); Assert.True(newTestOrg4.PrimaryContact.LastName != lastName); @@ -240,8 +241,9 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() // Create test person 2 newTestPerson2 = new Person() { FirstName = "TestPerson2" }; await _personRepo.InsertAsync(newTestPerson2); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); newTestPerson2 = _personRepo.GetAll().FirstOrDefault(x => x.FirstName == "TestPerson2"); + Assert.NotNull(newTestPerson2); // Change child by Id and check if TestPerson1 is not deleted by DeleteUnreferenced because is referenced to TestOrganisation3 and TestOrganisation4 var testErrors5 = new List(); @@ -250,7 +252,7 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() newTestOrg5 = await testOrgRepo.GetAsync(newTestOrg1.Id); var testResult5 = await _entityModelBinder.BindPropertiesAsync(jObject5, newTestOrg5, new EntityModelBindingContext()); Assert.True(testResult5); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); newTestOrg5 = await testOrgRepo.GetAsync(newTestOrg1.Id); newTestPerson1 = await _personRepo.GetAsync(newTestPerson1.Id); Assert.True(newTestOrg5.Name == "TestOrganisation1"); @@ -265,7 +267,7 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() newTestOrg6 = await testOrgRepo.GetAsync(newTestOrg1.Id); var testResult6 = await _entityModelBinder.BindPropertiesAsync(jObject6, newTestOrg6, new EntityModelBindingContext()); Assert.True(testResult6); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); newTestOrg6 = await testOrgRepo.GetAsync(newTestOrg1.Id); newTestPerson2 = await _personRepo.GetAsync(newTestPerson2.Id); Assert.True(newTestOrg6.Name == "TestOrganisation1"); @@ -278,7 +280,7 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() newTestOrg7 = await testOrgRepo.GetAsync(newTestOrg3.Id); var testResult7 = await _entityModelBinder.BindPropertiesAsync(jObject7, newTestOrg7, new EntityModelBindingContext()); Assert.True(testResult7); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); newTestOrg7 = await testOrgRepo.GetAsync(newTestOrg3.Id); Assert.True(newTestOrg7.Name == "TestOrganisation3"); Assert.True(newTestOrg7.PrimaryContact?.Id == newTestPerson2.Id); @@ -291,10 +293,11 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() Assert.True(newTestOrg8.PrimaryContact?.Id == newTestPerson1.Id); var testResult8 = await _entityModelBinder.BindPropertiesAsync(jObject8, newTestOrg8, new EntityModelBindingContext()); Assert.True(testResult8); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); newTestOrg8 = await testOrgRepo.GetAsync(newTestOrg4.Id); newTestPerson1 = _personRepo.GetAll().FirstOrDefault(x => x.FirstName == "TestPerson1"); newTestPerson2 = _personRepo.GetAll().FirstOrDefault(x => x.FirstName == "TestPerson22"); + Assert.NotNull(newTestPerson2); Assert.True(newTestPerson1 == null); Assert.True(newTestOrg8.Name == "TestOrganisation4"); Assert.True(newTestOrg8.PrimaryContact?.Id == newTestPerson2.Id && newTestPerson2.FirstName == "TestPerson22" && newTestPerson2.LastName == "TestLastName22"); @@ -311,7 +314,7 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() if (newTestOrg8 != null) testOrgRepo.HardDelete(newTestOrg8); if (newTestPerson1 != null) _personRepo.HardDelete(newTestPerson1); if (newTestPerson2 != null) _personRepo.HardDelete(newTestPerson2); - await nhuow.SaveChangesAsync(); + await uow.SaveChangesAsync(); } } } @@ -319,7 +322,7 @@ public async Task CascadeCreateUpdateAndRules_TestAsync() public class Finder : CascadeEntityCreatorBase { - public override Person FindEntity(CascadeRuleEntityFinderInfo info) + public override Person? FindEntity(CascadeRuleEntityFinderInfo info) { var p = info.NewObject; @@ -328,7 +331,7 @@ public override Person FindEntity(CascadeRuleEntityFinderInfo info if (p.DateOfBirth == null) throw new Exception($"`{nameof(Person.DateOfBirth)}` is mandatory"); var sd = p.DateOfBirth?.Date; var ed = sd?.AddDays(1); - return info.Repository.GetAll().FirstOrDefault(x => x.FirstName == p.FirstName && x.LastName == p.LastName && x.DateOfBirth > sd && x.DateOfBirth < ed); + return info.Repository.NotNull().GetAll().FirstOrDefault(x => x.FirstName == p.FirstName && x.LastName == p.LastName && x.DateOfBirth > sd && x.DateOfBirth < ed); } } @@ -346,9 +349,9 @@ public override Person PrepareEntity(CascadeRuleEntityFinderInfo i return info.NewObject; } - public override Person FindEntity(CascadeRuleEntityFinderInfo info) + public override Person? FindEntity(CascadeRuleEntityFinderInfo info) { - return info.Repository.GetAll().FirstOrDefault(x => x.FirstName == info.NewObject.FirstName); + return info.Repository.NotNull().GetAll().FirstOrDefault(x => x.FirstName == info.NewObject.FirstName); } } @@ -359,6 +362,13 @@ public class TestOrganisationAllowContactUpdate : Organisation public override Person PrimaryContact { get; set; } } + [DiscriminatorValue("Test.Organisaion")] + public class TestOrganisationDisallowContactUpdate : Organisation + { + [CascadeUpdateRules(false, false)] + public override Person PrimaryContact { get; set; } + } + [Flags, ReferenceList("Test", "TestEnum")] public enum TestEnum { diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/NumericToEnumTypeConverter_Test.cs b/shesha-core/test/Shesha.Tests/DynamicEntities/NumericToEnumTypeConverter_Test.cs index 56c18e9f5f..160bcd3e75 100644 --- a/shesha-core/test/Shesha.Tests/DynamicEntities/NumericToEnumTypeConverter_Test.cs +++ b/shesha-core/test/Shesha.Tests/DynamicEntities/NumericToEnumTypeConverter_Test.cs @@ -1,13 +1,19 @@ using AutoMapper; using Shesha.Domain.Enums; using Shesha.DynamicEntities.Mapper; +using Shesha.Tests.Fixtures; using System; using Xunit; namespace Shesha.Tests.DynamicEntities { - public class NumericToEnumTypeConverter_Test: SheshaNhTestBase + [Collection(SqlServerCollection.Name)] + public class NumericToEnumTypeConverter_Test : SheshaNhTestBase { + public NumericToEnumTypeConverter_Test(SqlServerFixture fixture) : base(fixture) + { + } + [Fact] public void ConvertInt64ToPersonTitle_Test() { diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/Resources/entityReference_DtoMode.metadata.json b/shesha-core/test/Shesha.Tests/DynamicEntities/Resources/entityReference_DtoMode-metadata.json similarity index 100% rename from shesha-core/test/Shesha.Tests/DynamicEntities/Resources/entityReference_DtoMode.metadata.json rename to shesha-core/test/Shesha.Tests/DynamicEntities/Resources/entityReference_DtoMode-metadata.json diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/Resources/flatModel.metadata.json b/shesha-core/test/Shesha.Tests/DynamicEntities/Resources/flatModel-metadata.json similarity index 100% rename from shesha-core/test/Shesha.Tests/DynamicEntities/Resources/flatModel.metadata.json rename to shesha-core/test/Shesha.Tests/DynamicEntities/Resources/flatModel-metadata.json diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/Resources/nested2Levels.metadata.json b/shesha-core/test/Shesha.Tests/DynamicEntities/Resources/nested2Levels-metadata.json similarity index 100% rename from shesha-core/test/Shesha.Tests/DynamicEntities/Resources/nested2Levels.metadata.json rename to shesha-core/test/Shesha.Tests/DynamicEntities/Resources/nested2Levels-metadata.json diff --git a/shesha-core/test/Shesha.Tests/DynamicEntities/Resources/nested3Levels.metadata.json b/shesha-core/test/Shesha.Tests/DynamicEntities/Resources/nested3Levels-metadata.json similarity index 100% rename from shesha-core/test/Shesha.Tests/DynamicEntities/Resources/nested3Levels.metadata.json rename to shesha-core/test/Shesha.Tests/DynamicEntities/Resources/nested3Levels-metadata.json diff --git a/shesha-core/test/Shesha.Tests/DynamicRepository/DynamicRepository_Tests.cs b/shesha-core/test/Shesha.Tests/DynamicRepository/DynamicRepository_Tests.cs deleted file mode 100644 index b9f7f354dd..0000000000 --- a/shesha-core/test/Shesha.Tests/DynamicRepository/DynamicRepository_Tests.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Abp.Domain.Uow; -using Shesha.MultiTenancy; -using Shesha.Services; -using Shouldly; -using System.Threading.Tasks; -using Xunit; - -namespace Shesha.Tests.DynamicRepository -{ - public class DynamicRepository_Tests: SheshaNhTestBase - { - [Fact] - public async Task GetEntity_TestAsync() - { - var uowManager = Resolve(); - - using (var uow = uowManager.Begin()) - { - var rep = Resolve(); - var tenant = await rep.GetAsync(typeof(Tenant), "1"); - - tenant.ShouldNotBeNull(); - } - } - } -} diff --git a/shesha-core/test/Shesha.Tests/EntityReferenceTest/EntityReference_Tests.cs b/shesha-core/test/Shesha.Tests/EntityReferenceTest/EntityReference_Tests.cs deleted file mode 100644 index 6f7e7e095b..0000000000 --- a/shesha-core/test/Shesha.Tests/EntityReferenceTest/EntityReference_Tests.cs +++ /dev/null @@ -1,135 +0,0 @@ -using Abp.Domain.Entities; -using Abp.Domain.Repositories; -using Abp.Domain.Uow; -using Abp.Timing; -using Shesha.Domain; -using Shesha.Domain.ConfigurationItems; -using Shesha.DynamicEntities; -using Shesha.EntityReferences; -using Shesha.Extensions; -using Shesha.NHibernate; -using Shesha.NHibernate.Session; -using System; -using System.Threading.Tasks; -using Xunit; -using static Shesha.Tests.JsonEntity.JsonEntity_Tests; - -namespace Shesha.Tests.EntityReferenceTest -{ - public class EntityReference_Tests : SheshaNhTestBase - { - private readonly IUnitOfWorkManager _unitOfWorkManager; - private readonly IRepository _personRepository; - private readonly IRepository _organisationRepository; - private readonly IRepository _jsonStringRepository; - private readonly IEntityModelBinder _entityModelBinder; - private readonly IRepository _moduleRepo; - - public EntityReference_Tests() - { - _unitOfWorkManager = Resolve(); - _personRepository = Resolve>(); - _organisationRepository = Resolve>(); - _jsonStringRepository = Resolve>(); - _entityModelBinder = Resolve(); - - _moduleRepo = Resolve>(); - } - - private static EntityIdentifier GetEntityIdentifier(GenericEntityReference genericEntity) - { - EntityIdentifier entityIdentifier = null; - - Entity entity = genericEntity; - - if (entity is not null) - { - entityIdentifier = new EntityIdentifier(entity.GetType(), entity.Id); - } - - return entityIdentifier; - } - - [Fact] - public async Task TestGnericEntityReferenceConvertAsync() - { - LoginAsHostAdmin(); - - using (var uow = _unitOfWorkManager.Begin()) - { - GenericEntityReference i = new GenericEntityReference("32E2B3DD-4D99-4542-AF71-134EC7C0E2CE", "Shesha.Domain.Person"); - //i = new Person(); - - Entity e = i; - - var t = e.GetType(); - var id = e.Id; - await uow.CompleteAsync(); - } - } - - [Fact] - public async Task TestGnericEntityReferenceAsync() - { - LoginAsHostAdmin(); - - using (var uow = _unitOfWorkManager.Begin()) - { - - var repo = LocalIocManager.Resolve>(); - var items = await repo.GetAllListAsync(); - - var sessionProvider = LocalIocManager.Resolve(); - var session = sessionProvider.Session; - - //GenericEntityReference i = null; - - foreach (var item in items) - { - //var b = i == item.PermissionedEntity1; - //i = item.PermissionedEntity1; - var entry = session?.GetEntry(item, false); - var dirty = session.GetDirtyProperties(item); - } - - await uow.CompleteAsync(); - } - } - - [Fact] - public async Task CheckUowAsync() - { - LoginAsHostAdmin(); - - using (var uow = _unitOfWorkManager.Begin(new UnitOfWorkOptions - { - IsTransactional = true, - IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted - })) - { - var dbModules = await _moduleRepo.GetAll().ToListAsync(); - - var dbModule = new Module - { - Name = "Test", - FriendlyName = "Test", - Description = "Test", - Publisher = "Test", - IsEditable = true, - IsRootModule = true, - IsEnabled = true, - - CurrentVersionNo = "1", - FirstInitializedDate = Clock.Now, - }; - await _moduleRepo.InsertAsync(dbModule); - - await _unitOfWorkManager.Current.SaveChangesAsync(); - - var dbModules2 = await _moduleRepo.GetAllListAsync(); - - await uow.CompleteAsync(); - } - } - } -} \ No newline at end of file diff --git a/shesha-core/test/Shesha.Tests/Fixtures/IDatabaseFixture.cs b/shesha-core/test/Shesha.Tests/Fixtures/IDatabaseFixture.cs new file mode 100644 index 0000000000..229239e7f7 --- /dev/null +++ b/shesha-core/test/Shesha.Tests/Fixtures/IDatabaseFixture.cs @@ -0,0 +1,18 @@ +namespace Shesha.Tests.Fixtures +{ + /// + /// Database fixture + /// + public interface IDatabaseFixture + { + /// + /// Connection string + /// + string ConnectionString { get; } + + /// + /// DBMS type + /// + DbmsType DbmsType { get; } + } +} diff --git a/shesha-core/test/Shesha.Tests/Fixtures/PostgreSqlCollection.cs b/shesha-core/test/Shesha.Tests/Fixtures/PostgreSqlCollection.cs new file mode 100644 index 0000000000..35134c6e4c --- /dev/null +++ b/shesha-core/test/Shesha.Tests/Fixtures/PostgreSqlCollection.cs @@ -0,0 +1,14 @@ +using Xunit; + +namespace Shesha.Tests.Fixtures +{ + /// + /// Shared PostgreSql fixture. + /// Note: This class has no code, just marks the collection + /// + [CollectionDefinition(Name)] + public class PostgreSqlCollection : ICollectionFixture + { + public const string Name = "SharedPostgreSql"; + } +} diff --git a/shesha-core/test/Shesha.Tests/Fixtures/PostgreSqlFixture.cs b/shesha-core/test/Shesha.Tests/Fixtures/PostgreSqlFixture.cs new file mode 100644 index 0000000000..22d29909c2 --- /dev/null +++ b/shesha-core/test/Shesha.Tests/Fixtures/PostgreSqlFixture.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Testcontainers.PostgreSql; +using Xunit; + +namespace Shesha.Tests.Fixtures +{ + [Collection("Sequential")] + public class PostgreSqlFixture : IDatabaseFixture, IAsyncLifetime + { + private const string Login = "postgres"; + private const string Password = "postgres"; + private const string DbName = "TestDB"; + + private readonly PostgreSqlContainer _postgresContainer; + + public string ConnectionString { get; private set; } + + public DbmsType DbmsType => DbmsType.PostgreSQL; + + public PostgreSqlFixture() + { + // Configure the PostgreSQL container + _postgresContainer = new PostgreSqlBuilder() + .WithImage("boxfusionregistry.azurecr.io/shesha-framework-postgresql:1.0") // Use the latest PostgreSQL image + .WithDatabase(DbName) // Set the database name + .WithUsername(Login) // Set the username + .WithPassword(Password) // Set the password + .WithExposedPort(5432) // Expose the default PostgreSQL port + .Build(); + } + + public async Task InitializeAsync() + { + // Start the container and initialize the connection string + await _postgresContainer.StartAsync(); + ConnectionString = $"Host=127.0.0.1;Port={_postgresContainer.GetMappedPublicPort(5432)};Database={DbName};Username={Login};Password={Password};"; + } + + public async Task DisposeAsync() + { + // Stop and remove the container + await _postgresContainer.StopAsync(); + } + } +} diff --git a/shesha-core/test/Shesha.Tests/Fixtures/SqlServerCollection.cs b/shesha-core/test/Shesha.Tests/Fixtures/SqlServerCollection.cs new file mode 100644 index 0000000000..8d3f740043 --- /dev/null +++ b/shesha-core/test/Shesha.Tests/Fixtures/SqlServerCollection.cs @@ -0,0 +1,14 @@ +using Xunit; + +namespace Shesha.Tests.Fixtures +{ + /// + /// Shared Sql Server fixture. + /// Note: This class has no code, just marks the collection + /// + [CollectionDefinition(Name)] + public class SqlServerCollection : ICollectionFixture + { + public const string Name = "SharedSqlServer"; + } +} diff --git a/shesha-core/test/Shesha.Tests/Fixtures/SqlServerFixture.cs b/shesha-core/test/Shesha.Tests/Fixtures/SqlServerFixture.cs new file mode 100644 index 0000000000..4365bdb8fb --- /dev/null +++ b/shesha-core/test/Shesha.Tests/Fixtures/SqlServerFixture.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Testcontainers.MsSql; +using Xunit; + +namespace Shesha.Tests.Fixtures +{ + public class SqlServerFixture : IDatabaseFixture, IAsyncLifetime + { + private const string Login = "sa"; + private const string Password = "YourStrong!Passw0rd"; + private const string DbName = "TestDB"; + + private readonly MsSqlContainer _mssqlContainer; + public string ConnectionString { get; private set; } + + public DbmsType DbmsType => DbmsType.SQLServer; + + public SqlServerFixture() + { + // Configure the SQL Server container + _mssqlContainer = new MsSqlBuilder() + .WithImage("boxfusionregistry.azurecr.io/shesha-framework-mssql:1.0") // Specify the SQL Server image + .WithPassword(Password) // Set the SA password + .WithExposedPort(1433) // Expose the default SQL Server port + .Build(); + } + + public async Task InitializeAsync() + { + // Start the container and initialize the connection string + await _mssqlContainer.StartAsync(); + ConnectionString = $"Server=127.0.0.1,{_mssqlContainer.GetMappedPublicPort(1433)};Database={DbName};User Id={Login};Password={Password};TrustServerCertificate=True;"; + } + + public Task DisposeAsync() + { + // Stop and remove the container + return _mssqlContainer.StopAsync(); + } + } +} diff --git a/shesha-core/test/Shesha.Tests/GraphQL/GraphQL_Tests.cs b/shesha-core/test/Shesha.Tests/GraphQL/GraphQL_Tests.cs deleted file mode 100644 index 334a9f20b7..0000000000 --- a/shesha-core/test/Shesha.Tests/GraphQL/GraphQL_Tests.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Abp.TestBase; - -namespace Shesha.Tests.GraphQL -{ - public class GraphQL_Tests : AbpIntegratedTestBase - { - } -} diff --git a/shesha-core/test/Shesha.Tests/Interceptors/MethodInterceptor_Test.cs b/shesha-core/test/Shesha.Tests/Interceptors/MethodInterceptor_Test.cs deleted file mode 100644 index b21dc1bf3b..0000000000 --- a/shesha-core/test/Shesha.Tests/Interceptors/MethodInterceptor_Test.cs +++ /dev/null @@ -1,197 +0,0 @@ -using Abp.Dependency; -using Abp.TestBase; -using Castle.DynamicProxy; -using Castle.MicroKernel; -using Shesha.Services; -using Shouldly; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; -using Xunit; - -namespace Shesha.Tests.Interceptors -{ - public class MethodInterceptor_Test : AbpIntegratedTestBase - { - public class MyTest - { - public string Name { get; set; } - - public string Description { get; set; } - - public MyTest Link { get; set; } - } - - private string GetSqlExpression(Expression expr, Dictionary pars, Dictionary tables) - { - if (expr is BinaryExpression binary) - { - switch (binary.NodeType) - { - case ExpressionType.And: - case ExpressionType.AndAlso: - return $"{GetSqlExpression(binary.Left, pars, tables).Trim()} and {GetSqlExpression(binary.Right, pars, tables).Trim()} "; - case ExpressionType.Or: - case ExpressionType.OrElse: - return $"({GetSqlExpression(binary.Left, pars, tables).Trim()} or {GetSqlExpression(binary.Right, pars, tables).Trim()}) "; - case ExpressionType.Equal: - return $"{GetSqlExpression(binary.Left, pars, tables).Trim()} = {GetSqlExpression(binary.Right, pars, tables).Trim()} "; - case ExpressionType.NotEqual: - return $"({GetSqlExpression(binary.Left, pars, tables).Trim().Trim()} != {GetSqlExpression(binary.Right, pars, tables).Trim()}) "; - case ExpressionType.LessThan: - return $"({GetSqlExpression(binary.Left, pars, tables)} < {GetSqlExpression(binary.Right, pars, tables).Trim()}) "; - case ExpressionType.LessThanOrEqual: - return $"({GetSqlExpression(binary.Left, pars, tables).Trim()} <= {GetSqlExpression(binary.Right, pars, tables).Trim()}) "; - case ExpressionType.GreaterThan: - return $"({GetSqlExpression(binary.Left, pars, tables).Trim()} > {GetSqlExpression(binary.Right, pars, tables).Trim()}) "; - case ExpressionType.GreaterThanOrEqual: - return $"({GetSqlExpression(binary.Left, pars, tables).Trim()} >= {GetSqlExpression(binary.Right, pars, tables).Trim()}) "; - } - } - if (expr is UnaryExpression unary) - { - switch (unary.NodeType) - { - case ExpressionType.Not: - return $"not ({GetSqlExpression(unary.Operand, pars, tables)}) "; - } - } - if (expr is MemberExpression member) - { - var tName = member.Expression.ToString().Replace(".", "_"); - if (!tables.ContainsKey(tName)) - { - tables.Add(tName, member.Expression); - } - - return $"{tName}.{member.Member.Name} "; - } - if (expr is ConstantExpression cons) - { - var pname = $"@P{pars.Count + 1} "; - pars.Add(pname, cons.Value); - return $"{pname} "; - } - - return ""; - //LogicalBinaryExpression - //InstanceMethodCallExpression - } - - private string GetSql(Expression> expr, Dictionary pars, Dictionary tables) - { - if (expr is LambdaExpression lambda) - { - return GetSqlExpression(lambda.Body, pars, tables); - } - - return ""; - } - - [Fact] - public void Main_Test() - { - try - { - var shuriks = StaticContext.IocManager.ResolveAll(); - var ss = shuriks.Select(x => new { Name = x.Name, NewP = "asd" }).ToList().FirstOrDefault(); - - foreach (var shurik in shuriks) - { - shurik.Name = "Shurik"; - shurik.Name.ShouldBe("Shurik: Property Intercepted"); - shurik.SetName("Shurik"); - shurik.Name.ShouldBe("Shurik: Method Intercepted"); - } - } - catch (Exception) - { - throw; - } - } - } - - public interface IShurik - { - string Name { get; set; } - void SetName(string name); - } - - public class Shurik : IShurik, ITransientDependency - { - public string Name { get; set; } - - public void SetName(string name) - { - Name = name; - } - } - - public class NewShurik : Shurik, ITransientDependency { } - - public class ShurikInterceptor : IAsyncInterceptor, ITransientDependency - { - public static void Configure(string key, IHandler handler) - { - var implementationType = handler.ComponentModel.Implementation.GetTypeInfo(); - if (implementationType.ImplementedInterfaces.Contains(typeof(IShurik))) - { - handler.ComponentModel.Interceptors.Add( - new Castle.Core.InterceptorReference(typeof(AbpAsyncDeterminationInterceptor)) - ); - } - } - - public void InterceptAsynchronous(IInvocation invocation) - { - var proceedInfo = invocation.CaptureProceedInfo(); - proceedInfo.Invoke(); - var task = (Task)invocation.ReturnValue; - invocation.ReturnValue = task; - return; - } - - public void InterceptAsynchronous(IInvocation invocation) - { - var proceedInfo = invocation.CaptureProceedInfo(); - proceedInfo.Invoke(); - var task = (Task)invocation.ReturnValue; - invocation.ReturnValue = task; - return; - } - - public void InterceptSynchronous(IInvocation invocation) - { - var method = GetMethodInfo(invocation); - if (method.Name == "set_Name") - { - invocation.Arguments[0] = $"{invocation.Arguments[0]}: Property Intercepted"; - } - if (method.Name == "SetName") - { - invocation.Arguments[0] = $"{invocation.Arguments[0]}: Method Intercepted"; - } - invocation.Proceed(); - return; - } - - private static MethodInfo GetMethodInfo(IInvocation invocation) - { - MethodInfo method; - try - { - method = invocation.MethodInvocationTarget; - } - catch - { - method = invocation.GetConcreteMethod(); - } - - return method; - } - - } -} diff --git a/shesha-core/test/Shesha.Tests/JsonEntity/JsonEntity_Tests.cs b/shesha-core/test/Shesha.Tests/JsonEntity/JsonEntity_Tests.cs deleted file mode 100644 index 4b0f88c37e..0000000000 --- a/shesha-core/test/Shesha.Tests/JsonEntity/JsonEntity_Tests.cs +++ /dev/null @@ -1,570 +0,0 @@ -using Abp.Domain.Entities; -using Abp.Domain.Repositories; -using Abp.Domain.Uow; -using Abp.Reflection; -using Castle.DynamicProxy; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NHibernate.Linq; -using NHibernate.Proxy; -using Shesha.AutoMapper.Dto; -using Shesha.Configuration.MappingMetadata; -using Shesha.Domain; -using Shesha.DynamicEntities; -using Shesha.DynamicEntities.Binder; -using Shesha.DynamicEntities.Dtos; -using Shesha.EntityReferences; -using Shesha.Extensions; -using Shesha.JsonEntities.Proxy; -using Shesha.NHibernate.UoW; -using Shesha.Services; -using Shesha.Test; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Linq.Expressions; -using System.Threading.Tasks; -using Xunit; - -namespace Shesha.Tests.JsonEntity -{ - public class JsonEntity_Tests : SheshaNhTestBase - { - private readonly IUnitOfWorkManager _unitOfWorkManager; - private readonly IRepository _personRepository; - private readonly IRepository _organisationRepository; - private readonly IRepository _jsonRepository; - private readonly IRepository _jsonStringRepository; - //private readonly IRepository _jsonPersonRepository; - private readonly IEntityModelBinder _entityModelBinder; - private readonly ITypeFinder _typeFinder; - private readonly IDynamicRepository _dynamicRepository; - - public JsonEntity_Tests() - { - _unitOfWorkManager = Resolve(); - _personRepository = Resolve>(); - _organisationRepository = Resolve>(); - _jsonRepository = Resolve>(); - _jsonStringRepository = Resolve>(); - //_jsonPersonRepository = Resolve>(); - _entityModelBinder = Resolve(); - _typeFinder = Resolve(); - _dynamicRepository = Resolve(); - } - - public class Shurik - { - public virtual string Name { get; set; } - public string Test { get; set; } - } - - public class MyJsonEntity - { - public virtual string Name { get; set; } - public virtual Person Person { get; set; } - } - - [Table("Test_ComplexTest")] - public class ComplexTestString : Entity - { - public virtual string Name { get; set; } - public virtual string Description { get; set; } - public virtual Person Person { get; set; } - - public virtual string JsonList { get; set; } - - public virtual string JsonPerson { get; set; } - - public virtual string AnyJson { get; set; } - } - - public class TestInt - { - public int I { get; set; } - public Int64? Ii { get; set; } - } - - [Fact] - public async Task DynamicExpressionAsync() - { - LoginAsHostAdmin(); - - using var uow = (NhUnitOfWork)_unitOfWorkManager.Begin(); - - var session = uow.GetSession(); - - var myType = typeof(Organisation); - - var parameterExpression = Expression.Parameter(myType, "x"); - var memberExpression = Expression.PropertyOrField(parameterExpression, "PrimaryContact"); - var idExpression = Expression.PropertyOrField(memberExpression, "Id"); - var action = Expression.Equal(idExpression, Expression.Constant(Guid.Parse("32E2B3DD-4D99-4542-AF71-134EC7C0E2CE"))); - var lambda = Expression.Lambda(action, parameterExpression); - - var res = await _dynamicRepository.Where(myType, lambda).ToDynamicListAsync(); - } - - [Fact] - public async Task GetAllJsonPropertiesAsync() - { - LoginAsHostAdmin(); - - using var uow = (NhUnitOfWork)_unitOfWorkManager.Begin(); - - var session = uow.GetSession(); - - var oldValue = "Shesha.Test.JsonPerson1"; - var newValue = "Shesha.Test.JsonPerson"; - - var entityTypes = _typeFinder.FindAll().Where(t => t.IsEntityType()).ToList(); - - var mapProvider = Resolve(); - - foreach (var entityType in entityTypes) - { - try - { - var jsonProps = entityType.GetProperties().Where(x => x.PropertyType.IsJsonEntityType()).ToList(); - var genericProps = entityType.GetProperties().Where(x => x.PropertyType == typeof(GenericEntityReference)).ToList(); - if (jsonProps.Any()) - await mapProvider.UpdateClassNamesAsync(entityType, jsonProps, oldValue, newValue, true); - if (genericProps.Any()) - await mapProvider.UpdateClassNamesAsync(entityType, genericProps, oldValue, newValue, false); - } - catch (Exception) - { - } - } - } - - [Fact] - public async Task GetJsonEntityAsync() - { - var i = new TestInt(); - - i.I = 1111; - i.Ii = 2222; - - i.Ii = i.I; - - var pi = i.GetType().GetProperty("I"); - var pii = i.GetType().GetProperty("Ii"); - - pii.SetValue(i, Convert.ToInt64(pi.GetValue(i))); - pii.SetValue(i, Convert.ChangeType(pi.GetValue(i), typeof(Int64?))); - pii.SetValue(i, pi.GetValue(i)); - - - var p = pii.PropertyType == typeof(Int64?); - var pp = pii.PropertyType.Equals(typeof(Int64?)); - var ppp = pii.PropertyType == Nullable.GetUnderlyingType(typeof(long?)); - - var iii = pi.GetValue(i);// Convert.ChangeType(pi.GetValue(i), Nullable.GetUnderlyingType(typeof(long?))); - pii.SetValue(i, iii); - - LoginAsHostAdmin(); - - using (var uow = _unitOfWorkManager.Begin()) - { - var obj = await _jsonRepository.GetAsync(Guid.Parse("FB57A090-6FDC-452C-8945-9356377271B9")); - - var fn = obj.JsonPerson.FirstName; - - var fn1 = (obj.JsonList[0] as JsonAddress).Town; - - var slist = await _jsonStringRepository.GetAll().Take(25).ToListAsync(); - - var list = await _jsonRepository.GetAll().Take(25).ToListAsync(); - - var persList = await _personRepository.GetAll().Take(25).ToListAsync(); - } - } - - [Fact] - public async Task GetUpdateAsync() - { - LoginAsHostAdmin(); - - using (var uow = _unitOfWorkManager.Begin()) - { - var obj = await _jsonRepository.GetAsync(Guid.Parse("FB57A090-6FDC-452C-8945-9356377271B9")); - await _jsonRepository.InsertOrUpdateAsync(obj); - } - } - - [Fact] - public void CheckEmptyJsonEntity() - { - using (var uow = _unitOfWorkManager.Begin()) - { - var person = _personRepository.GetAll().FirstOrDefault(); - - var json = @" -{ - ""id"":""03AD51DD-DC1B-4E93-B255-C05641D97411"", - ""name"":""New Person test"", - ""description"":""New Person Description"", - ""jsonPerson"":{ - ""person"":{""id"":""B3B60F2E-5B88-4F44-B8EB-D3987A8483D9"", ""displayText"":""Shurikan""}, - ""firstName"":""11111"", - ""lastName"":""22222"", - ""address"":{""town"":""jsonPerson Town"",""street"":""jsonPerson street"",""postalCode"":12345678,""_meta"":{""className"":""Shesha.Test.JsonAddress""}}, - ""_meta"":{""className"":""Shesha.Test.JsonPerson""}}, - ""any"":{""town"":""My Town"",""street"":""My street"",""postalCode"":12345678,""_meta"":{""className"":""Shesha.Test.JsonAddress""}} -} -"; - var complex = JsonConvert.DeserializeObject(json); - - var jsonRes = JsonEntityProxy.GetJson(complex.JsonPerson).ToString(); - - var b = JsonEntityProxy.IsChanged(complex.JsonPerson as IJsonEntityProxy); - var i = (complex.JsonPerson as IJsonEntityProxy)._isInitialized; - var tc = (complex.JsonPerson as IJsonEntityProxy)._isThisChanged; - - var fn = complex.JsonPerson.FirstName; - - b = JsonEntityProxy.IsChanged(complex.JsonPerson as IJsonEntityProxy); - i = (complex.JsonPerson as IJsonEntityProxy)._isInitialized; - tc = (complex.JsonPerson as IJsonEntityProxy)._isThisChanged; - - var ja = complex.JsonPerson.Address; - - b = JsonEntityProxy.IsChanged(complex.JsonPerson as IJsonEntityProxy); - i = (complex.JsonPerson as IJsonEntityProxy)._isInitialized; - tc = (complex.JsonPerson as IJsonEntityProxy)._isThisChanged; - - var jat = complex.JsonPerson.Address?.Town; - - b = JsonEntityProxy.IsChanged(complex.JsonPerson as IJsonEntityProxy); - i = (complex.JsonPerson as IJsonEntityProxy)._isInitialized; - tc = (complex.JsonPerson as IJsonEntityProxy)._isThisChanged; - - complex.JsonPerson.Person = person; - - jsonRes = JsonEntityProxy.GetJson(complex.JsonPerson).ToString(); - - b = JsonEntityProxy.IsChanged(complex.JsonPerson as IJsonEntityProxy); - i = (complex.JsonPerson as IJsonEntityProxy)._isInitialized; - tc = (complex.JsonPerson as IJsonEntityProxy)._isThisChanged; - - complex.JsonPerson.Address.Town = "Test town"; - - jsonRes = JsonEntityProxy.GetJson(complex.JsonPerson).ToString(); - - b = JsonEntityProxy.IsChanged(complex.JsonPerson as IJsonEntityProxy); - i = (complex.JsonPerson as IJsonEntityProxy)._isInitialized; - tc = (complex.JsonPerson as IJsonEntityProxy)._isThisChanged; - - jsonRes = JsonEntityProxy.GetJson(complex).ToString(); - - } - } - - [Fact] - public async Task ObjectToJsonAsync() - { - using (var uow = _unitOfWorkManager.Begin()) - { - var person = _personRepository.GetAll().FirstOrDefault(); - - var o = new ComplexTest() - { - Name = "New Person test", - Description = "New Person Description", - Person = person, - - JsonList = new List() - { - new JsonAddress() { Town = "Listed Town", Street = "Listed street" }, - new JsonPerson() { FirstName = "11", LastName = "qq" }, - new JsonPerson() { FirstName = "22", LastName = "ww" }, - new JsonPerson() { FirstName = "33", LastName = "ee" }, - }, - - AnyJson = new JsonAddress() - { - Town = "My Town", - Street = "My street", - PostalCode = 12345678 - }, - - JsonPerson = new JsonPerson() - { - FirstName = "11111", - LastName = "22222", - Person = person, - } - }; - - var dto = await EntityToDtoModelExtesions.MapToCustomDynamicDtoAsync(o); - - - var jObj = new JObject(); - ObjectToJsonExtension.ObjectToJObject(dto, jObj); - - dto.JsonPerson.FirstName = "Shurik"; - dto.Description = "+++++++"; - (dto.JsonList[3] as JsonPerson).FirstName = "Trheeeeeee"; - - ObjectToJsonExtension.ObjectToJObject(dto, jObj); - } - } - - [Fact] - public void Proxy() - { - var obj = new Shurik() { Name = "Shurik", Test = "+++" }; - var mixin = new JsonReference() { _displayName = "Test" }; - - var p = new ProxyGenerationOptions(); - p.AddMixinInstance(mixin); - - var model1 = (Shurik)new ProxyGenerator().CreateClassProxyWithTarget(obj.GetType(), /*new[] { typeof(IInner) },*/ obj, p); - - var b = ProxyUtil.IsProxy(model1); - b = ProxyUtil.IsProxy(obj); - - const System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; - var proxyType = model1.GetType(); - var instanceField = proxyType.GetField("__target", flags); - var unproxy = instanceField?.GetValue(model1); - - var s1 = (model1 as IJsonReference)._displayName; - model1.Test = "==="; - var s2 = model1.Test; - - mixin._displayName = "!!!"; - - var model2 = new ProxyGenerator().CreateClassProxyWithTarget(obj.GetType(), /*new[] { typeof(IInner) },*/ obj, p); - - (model1 as IJsonReference)._displayName = "111"; - (model2 as IJsonReference)._displayName = "222"; - - var s = (model1 as IJsonReference)._displayName; - } - - [Fact] - public async Task BindModelToDtoAsync() - { - LoginAsHostAdmin(); - - using (var uow = _unitOfWorkManager.Begin()) - { - var person = _personRepository.GetAll().FirstOrDefault(); - - var json = @" -{ - ""id"":""03AD51DD-DC1B-4E93-B255-C05641D97411"", - ""name"":""New Person test"", - ""description"":""New Person Description"", - ""jsonPerson"":{ - ""person"":{""id"":""B3B60F2E-5B88-4F44-B8EB-D3987A8483D9"", ""displayText"":""Shurikan""}, - ""firstName"":""11111"", - ""lastName"":""22222"", - ""_meta"":{""className"":""Shesha.Test.JsonPerson""}}, - ""jsonList"":[ - {""town"":""Listed Town"",""street"":""Listed street"",""postalCode"":0,""_meta"":{""className"":""Shesha.Test.JsonAddress""}}, - {""person"":null,""firstName"":""11"",""lastName"":""qq"",""_meta"":{""className"":""Shesha.Test.JsonPerson""}}, - {""person"":null,""firstName"":""22"",""lastName"":""ww"",""_meta"":{""className"":""Shesha.Test.JsonPerson""}}, - {""person"":null,""firstName"":""33"",""lastName"":""ee"",""_meta"":{""className"":""Shesha.Test.JsonPerson""}}], - ""any"":{""town"":""My Town"",""street"":""My street"",""postalCode"":12345678,""_meta"":{""className"":""Shesha.Test.JsonAddress""}} -} -"; - var complex = JsonConvert.DeserializeObject(json); - - var json2 = JsonConvert.SerializeObject(complex); - - var name = complex.JsonPerson?.FirstName; - - var pp = complex.JsonPerson.Person; - - var json3 = JsonConvert.SerializeObject(complex); - - var jObject1 = JObject.Parse(json); - var testErrors1 = new List(); - var obj = new ComplexTest(); - var testResult1 = await _entityModelBinder.BindPropertiesAsync(jObject1, obj, new EntityModelBindingContext()); - } - } - - [Fact] - public void DeserializeAny() - { - var json = "{\"person\":null,\"firstName\":\"AnyJson first name\",\"lastName\":\"AnyJson last name\",\"_meta\":{\"className\":\"Shesha.Test.JsonPerson\"}}"; - var obj = JsonConvert.DeserializeObject(json); - var s = JsonConvert.SerializeObject(obj); - Assert.Equal(s, json); - } - - [Fact] - public void DeserializeAnyFailed() - { - try - { - var json = "{\"person\":null,\"firstName\":\"AnyJson first name\",\"lastName\":\"AnyJson last name\",\"_meta\":{\"className\":\"Shesha.Test.JsonPerson\"}}"; - // use JsonEntity instead of IJsonEntity - var obj = JsonConvert.DeserializeObject(json); - } - catch - { - } - } - - [Fact] - public async Task CreateJsonEntityAsync() - { - for (var i = 0; i < 1000; i++) - { - var o = new ComplexTest() - { - Name = $"{300 + i} JsonEntity Person test", - Description = "{i} New Person Description", - JsonList = new List() - { - new JsonAddress() { Town = "Listed Town", Street = "Listed street" }, - new JsonPerson() { FirstName = "11", LastName = "qq" }, - new JsonPerson() { FirstName = "22", LastName = "ww" }, - new JsonPerson() { FirstName = "33", LastName = "ee" }, - }, - AnyJson = new JsonPerson() - { - FirstName = "JsonEntity", - LastName = "JsonEntity", - Person = await _personRepository.GetAsync(Guid.Parse("B3B60F2E-5B88-4F44-B8EB-D3987A8483D9")) - }, - JsonPerson = new JsonPerson() - { - FirstName = "11111", - LastName = "22222", - //Person = _personRepository.Get(Guid.Parse("B3B60F2E-5B88-4F44-B8EB-D3987A8483D9")) - } - }; - await _jsonRepository.InsertOrUpdateAsync(o); - } - /*var op = new ComplexPersonTest() - { - Name = "JsonPerson Person test", - Description = "New Person Description", - JsonList = new List() - { - new JsonAddress() { Town = "Listed Town", Street = "Listed street" }, - new JsonPerson() { FirstName = "11", LastName = "qq" }, - new JsonPerson() { FirstName = "22", LastName = "ww" }, - new JsonPerson() { FirstName = "33", LastName = "ee" }, - }, - AnyJson = new JsonPerson() - { - FirstName = "JsonPerson", - LastName = "JsonPerson", - Person = _personRepository.Get(Guid.Parse("B3B60F2E-5B88-4F44-B8EB-D3987A8483D9")) - }, - JsonPerson = new JsonPerson() - { - FirstName = "11111", - LastName = "22222", - //Person = _personRepository.Get(Guid.Parse("B3B60F2E-5B88-4F44-B8EB-D3987A8483D9")) - } - }; - try - { - await _jsonPersonRepository.InsertOrUpdateAsync(op); - } - catch (Exception e) - { - }*/ - } - - [Fact] - public async Task GetDtoAsync() - { - using (var uow = _unitOfWorkManager.Begin()) - { - var person = _personRepository.GetAll().FirstOrDefault(); - - var o = new ComplexTest() - { - Name = "New Person test", - Description = "New Person Description", - JsonList = new List() - { - new JsonAddress() { Town = "Listed Town", Street = "Listed street" }, - new JsonPerson() { FirstName = "11", LastName = "qq" }, - new JsonPerson() { FirstName = "22", LastName = "ww" }, - new JsonPerson() { FirstName = "33", LastName = "ee" }, - }, - AnyJson = new JsonAddress() - { - Town = "My Town", - Street = "My street", - PostalCode = 12345678 - }, - JsonPerson = new JsonPerson() - { - FirstName = "11111", - LastName = "22222", - //Person = person - } - }; - - var dto = await EntityToDtoModelExtesions.MapToCustomDynamicDtoAsync, ComplexTest, Guid>(o); - - //var dto2 = await EntityToDtoModelExtesions.MapToCustomDynamicDtoAsync(o); - } - } - - [Fact] - public void SerializeJsonEntity() - { - try - { - LoginAsHostAdmin(); - - using (var uow = _unitOfWorkManager.Begin()) - { - var nhuow = uow as NhUnitOfWork; - - var person = _personRepository.GetAll().FirstOrDefault(); - - /*var person = new Person() - { - Id = Guid.NewGuid(), - FirstName = "Shurik 111", - LastName = "Shurik 222", - };*/ - - var pers = new JsonPerson() - { - FirstName = "Name 111", - LastName = "Name 222", - Person = person, - Persons = new List { new JsonPerson() { FirstName = "111", LastName = "222" } }, - Address = new JsonAddress { Town = "My Town", Street = "My street", PostalCode = 12345678 } - }; - - var ps = JsonConvert.SerializeObject(pers, new JsonSerializerSettings() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); - - var pd = JsonConvert.DeserializeObject(ps); - - var obj = new ComplexTestDto() - { - Name = "Name !!!", - Person = new EntityReferenceDto(_personRepository.Get(Guid.Parse("B3B60F2E-5B88-4F44-B8EB-D3987A8483D9"))), - //AnyJson = pers - }; - - var s = JsonConvert.SerializeObject(obj); - - var cs = JsonConvert.DeserializeObject(s); - } - } - catch (Exception) - { - - } - } - } -} diff --git a/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverterBaseTests.cs b/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverterBaseTests.cs index 19ceba3282..0c879ce873 100644 --- a/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverterBaseTests.cs +++ b/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverterBaseTests.cs @@ -5,21 +5,30 @@ using Abp.Timing; using Newtonsoft.Json.Linq; using Shesha.JsonLogic; +using Shesha.Reflection; +using Shesha.Tests.Fixtures; using Shesha.Tests.TestingUtils; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Linq.Expressions; +using System.Threading; using System.Threading.Tasks; +using Xunit; namespace Shesha.Tests.JsonLogic { /// /// Base class of tests /// - public abstract class JsonLogic2LinqConverterBaseTests: SheshaNhTestBase + public abstract class JsonLogic2LinqConverterBaseTests : SheshaNhTestBase { - protected Expression> ConvertToExpression(string jsonLogicExpression) + protected JsonLogic2LinqConverterBaseTests(IDatabaseFixture fixture) : base(fixture) + { + } + + protected Expression>? ConvertToExpression(string jsonLogicExpression) { var converter = Resolve(); @@ -30,27 +39,25 @@ protected Expression> ConvertToExpression(string jsonLogicExpre return expression; } - protected async Task> TryFetchDataAsync(string jsonLogicExpression, Func, IQueryable> prepareQueryable = null, Action> assertions = null) where T : class, IEntity + protected async Task> TryFetchDataAsync(string jsonLogicExpression, Func, IQueryable>? prepareQueryable = null, Action>? assertions = null) where T : class, IEntity { - var expression = ConvertToExpression(jsonLogicExpression); + var expression = ConvertToExpression(jsonLogicExpression).NotNull(); var repository = LocalIocManager.Resolve>(); var asyncExecuter = LocalIocManager.Resolve(); - List data = null; - - await WithUnitOfWorkAsync(async () => { + return await WithUnitOfWorkAsync(async () => { var query = repository.GetAll().Where(expression); if (prepareQueryable != null) query = prepareQueryable.Invoke(query); - data = await asyncExecuter.ToListAsync(query); + var data = await asyncExecuter.ToListAsync(query); assertions?.Invoke(data); - }); - return data; + return data; + }); } protected IDisposable FreezeTime() @@ -66,5 +73,17 @@ protected IDisposable FreezeTime() Clock.Provider = prevProvider; }); } + + protected override void PreInitialize() + { + base.PreInitialize(); + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + } + + public override void Dispose() + { + + base.Dispose(); + } } } diff --git a/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverter_FieldTests.cs b/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverter_FieldTests.cs index 2dd7d756b5..a7505e6f61 100644 --- a/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverter_FieldTests.cs +++ b/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverter_FieldTests.cs @@ -1,12 +1,18 @@ using Shesha.Domain; +using Shesha.JsonLogic; +using Shesha.Tests.Fixtures; using System; using System.Threading.Tasks; using Xunit; namespace Shesha.Tests.JsonLogic { - public class JsonLogic2LinqConverter_FieldTests: JsonLogic2LinqConverterBaseTests + [Collection(SqlServerCollection.Name)] + public class JsonLogic2LinqConverter_FieldTests : JsonLogic2LinqConverterBaseTests { + public JsonLogic2LinqConverter_FieldTests(SqlServerFixture fixture) : base(fixture) + { + } #region string private static class StringExpressions @@ -64,7 +70,7 @@ public void StringField_Convert(string name, string jsonLogicExpression, string Console.WriteLine($"Test: '{name}'"); var linqExpression = ConvertToExpression(jsonLogicExpression); - Assert.Equal(expectation, linqExpression.ToString()); + Assert.Equal(expectation, linqExpression?.ToInvariantString()); } [Theory] @@ -220,7 +226,7 @@ public void NumberField_Convert(string name, string jsonLogicExpression, string Console.WriteLine($"Test: '{name}'"); var linqExpression = ConvertToExpression(jsonLogicExpression); - Assert.Equal(expectation, linqExpression.ToString()); + Assert.Equal(expectation, linqExpression?.ToInvariantString()); } [Theory] @@ -387,7 +393,7 @@ public void DateField_Convert(string name, string jsonLogicExpression, string ex Console.WriteLine($"Test: '{name}'"); var linqExpression = ConvertToExpression(jsonLogicExpression); - Assert.Equal(expectation, linqExpression.ToString()); + Assert.Equal(expectation, linqExpression?.ToInvariantString()); } [Theory] @@ -452,7 +458,7 @@ public void BoolField_Convert(string name, string jsonLogicExpression, string ex Console.WriteLine($"Test: '{name}'"); var linqExpression = ConvertToExpression(jsonLogicExpression); - Assert.Equal(expectation, linqExpression.ToString()); + Assert.Equal(expectation, linqExpression?.ToInvariantString()); } [Theory] @@ -510,7 +516,7 @@ public void EntityField_Convert(string name, string jsonLogicExpression, string Console.WriteLine($"Test: '{name}'"); var linqExpression = ConvertToExpression(jsonLogicExpression); - Assert.Equal(expectation, linqExpression.ToString()); + Assert.Equal(expectation, linqExpression?.ToInvariantString()); } [Theory] @@ -569,7 +575,7 @@ public void RefListField_Convert(string name, string jsonLogicExpression, string Console.WriteLine($"Test: '{name}'"); var linqExpression = ConvertToExpression(jsonLogicExpression); - Assert.Equal(expectation, linqExpression.ToString()); + Assert.Equal(expectation, linqExpression?.ToInvariantString()); } [Theory] diff --git a/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverter_ValueTests.cs b/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverter_ValueTests.cs index 8205da88b9..4dd658c95b 100644 --- a/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverter_ValueTests.cs +++ b/shesha-core/test/Shesha.Tests/JsonLogic/JsonLogic2LinqConverter_ValueTests.cs @@ -3,7 +3,9 @@ using Shesha.Authorization.Users; using Shesha.Domain; using Shesha.Extensions; +using Shesha.JsonLogic; using Shesha.Services; +using Shesha.Tests.Fixtures; using Shesha.Tests.JsonLogic.Models; using Shesha.Utilities; using System; @@ -17,8 +19,13 @@ namespace Shesha.Tests.JsonLogic /// /// JsonLogic2LinqConverter tests /// + [Collection(SqlServerCollection.Name)] public class JsonLogic2LinqConverter_ValueTests : JsonLogic2LinqConverterBaseTests { + public JsonLogic2LinqConverter_ValueTests(SqlServerFixture fixture) : base(fixture) + { + } + #region string operations private readonly string _stringField_Equals_expression = @"{ @@ -38,7 +45,7 @@ public class JsonLogic2LinqConverter_ValueTests : JsonLogic2LinqConverterBaseTes public void StringField_Equals_Convert() { var expression = ConvertToExpression(_stringField_Equals_expression); - Assert.Equal(@"ent => (ent.FirstName == ""Bob"")", expression.ToString()); + Assert.Equal(@"ent => (ent.FirstName == ""Bob"")", expression?.ToInvariantString()); } [Fact] @@ -65,7 +72,7 @@ public async Task StringField_Equals_FetchAsync() public void StringField_NotEquals_Convert() { var expression = ConvertToExpression(_stringField_NotEquals_expression); - Assert.Equal(@"ent => Not((ent.FirstName == ""Bob""))", expression.ToString()); + Assert.Equal(@"ent => Not((ent.FirstName == ""Bob""))", expression?.ToInvariantString()); } [Fact] @@ -91,7 +98,7 @@ public async Task StringField_NotEquals_FetchAsync() public void StringField_Like_Convert() { var expression = ConvertToExpression(_stringField_Like_expression); - Assert.Equal(@"ent => ent.FirstName.Contains(""trick"")", expression.ToString()); + Assert.Equal(@"ent => ent.FirstName.Contains(""trick"")", expression?.ToInvariantString()); } [Fact] @@ -120,7 +127,7 @@ public async Task StringField_Like_FetchAsync() public void StringField_NotLike_Convert() { var expression = ConvertToExpression(_stringField_NotLike_expression); - Assert.Equal(@"ent => Not(ent.FirstName.Contains(""trick""))", expression.ToString()); + Assert.Equal(@"ent => Not(ent.FirstName.Contains(""trick""))", expression?.ToInvariantString()); } [Fact] @@ -143,7 +150,7 @@ public async Task StringField_NotLike_FetchAsync() public void StringField_IsEmpty_Convert() { var expression = ConvertToExpression(_stringField_IsEmpty_expression); - Assert.Equal(@"ent => (ent.FirstName == null)", expression.ToString()); + Assert.Equal(@"ent => (ent.FirstName == null)", expression?.ToInvariantString()); } [Fact] @@ -167,7 +174,7 @@ public async Task StringField_IsEmpty_FetchAsync() public void StringField_IsNotEmpty_Test() { var expression = ConvertToExpression(_stringField_IsNotEmpty_expression); - Assert.Equal(@"ent => ((ent.FirstName != null) AndAlso (ent.FirstName.Trim() != """"))", expression.ToString()); + Assert.Equal(@"ent => ((ent.FirstName != null) AndAlso (ent.FirstName.Trim() != """"))", expression?.ToInvariantString()); } [Fact] @@ -194,7 +201,7 @@ public async Task StringField_IsNotEmpty_FetchAsync() public void StringField_StartsWith_Convert() { var expression = ConvertToExpression(_stringField_StartsWith_expression); - Assert.Equal(@"ent => ent.FirstName.StartsWith(""bo"")", expression.ToString()); + Assert.Equal(@"ent => ent.FirstName.StartsWith(""bo"")", expression?.ToInvariantString()); } [Fact] @@ -221,7 +228,7 @@ public async Task StringField_StartsWith_FetchAsync() public void StringField_EndsWith_Convert() { var expression = ConvertToExpression(_stringField_EndsWith_expression); - Assert.Equal(@"ent => ent.FirstName.EndsWith(""ck"")", expression.ToString()); + Assert.Equal(@"ent => ent.FirstName.EndsWith(""ck"")", expression?.ToInvariantString()); } [Fact] @@ -250,7 +257,7 @@ public void Int32Field_EqualsInt64_Convert() } ] }"); - Assert.Equal($@"ent => (ent.IntProp == 100)", expression.ToString()); + Assert.Equal($@"ent => (ent.IntProp == 100)", expression?.ToInvariantString()); } [Fact] @@ -268,7 +275,7 @@ public void NullableInt32Field_EqualsInt64_Convert() } ] }"); - Assert.Equal($@"ent => (ent.NullableIntProp == Convert(100, Nullable`1))", expression.ToString()); + Assert.Equal($@"ent => (ent.NullableIntProp == Convert(100, Nullable`1))", expression?.ToInvariantString()); } #endregion @@ -293,7 +300,7 @@ public void NullableInt32Field_EqualsInt64_Convert() public void Int64Field_Equals_Convert() { var expression = ConvertToExpression(_int64Field_Equals_expression); - Assert.Equal($@"ent => (ent.{nameof(Person.CreatorUserId)} == Convert(100, Nullable`1))", expression.ToString()); + Assert.Equal($@"ent => (ent.{nameof(Person.CreatorUserId)} == Convert(100, Nullable`1))", expression?.ToInvariantString()); } [Fact] @@ -323,7 +330,7 @@ public async Task Int64Field_Equals_FetchAsync() public void Int64Field_LessThan_Convert() { var expression = ConvertToExpression(_int64Field_LessThan_expression); - Assert.Equal($@"ent => (ent.{nameof(Person.CreatorUserId)} < Convert(100, Nullable`1))", expression.ToString()); + Assert.Equal($@"ent => (ent.{nameof(Person.CreatorUserId)} < Convert(100, Nullable`1))", expression?.ToInvariantString()); } [Fact] @@ -354,7 +361,7 @@ public async Task Int64Field_LessThan_FetchAsync() public void Int64Field_LessThanOrEqual_Convert() { var expression = ConvertToExpression(_int64Field_LessThanOrEqual_expression); - Assert.Equal($@"ent => (ent.{nameof(Person.CreatorUserId)} <= Convert(100, Nullable`1))", expression.ToString()); + Assert.Equal($@"ent => (ent.{nameof(Person.CreatorUserId)} <= Convert(100, Nullable`1))", expression?.ToInvariantString()); } [Fact] @@ -386,7 +393,7 @@ public async Task Int64Field_LessThanOrEqual_FetchAsync() public void Int64Field_GreaterThan_Convert() { var expression = ConvertToExpression(_int64Field_GreaterThan_expression); - Assert.Equal($@"ent => (ent.{nameof(Person.CreatorUserId)} > Convert(100, Nullable`1))", expression.ToString()); + Assert.Equal($@"ent => (ent.{nameof(Person.CreatorUserId)} > Convert(100, Nullable`1))", expression?.ToInvariantString()); } [Fact] @@ -417,7 +424,7 @@ public async Task Int64Field_GreaterThan_FetchAsync() public void Int64Field_GreaterThanOrEqual_Convert() { var expression = ConvertToExpression(_int64Field_GreaterThanOrEqual_expression); - Assert.Equal($@"ent => (ent.{nameof(Person.CreatorUserId)} >= Convert(100, Nullable`1))", expression.ToString()); + Assert.Equal($@"ent => (ent.{nameof(Person.CreatorUserId)} >= Convert(100, Nullable`1))", expression?.ToInvariantString()); } [Fact] @@ -450,7 +457,7 @@ public void BooleanField_Equals_Convert() { var expression = ConvertToExpression(_booleanField_Equals_expression); - Assert.Equal($@"ent => (ent.{nameof(User.OtpEnabled)} == True)", expression.ToString()); + Assert.Equal($@"ent => (ent.{nameof(User.OtpEnabled)} == True)", expression?.ToInvariantString()); } [Fact] @@ -477,7 +484,7 @@ public void BooleanField_NotEquals_Convert() { var expression = ConvertToExpression(_booleanField_NotEquals_expression); - Assert.Equal($@"ent => Not((ent.{nameof(User.OtpEnabled)} == True))", expression.ToString()); + Assert.Equal($@"ent => Not((ent.{nameof(User.OtpEnabled)} == True))", expression?.ToInvariantString()); } @@ -522,10 +529,10 @@ public async Task BooleanField_NotEquals_FetchAsync() public void NestedColumnResolver_Convert() { var expression = ConvertToExpression(_nestedColumnResolver_expression); - Assert.Equal(@"ent => (ent.User.UserName == ""admin"")", expression.ToString()); + Assert.Equal(@"ent => (ent.User.UserName == ""admin"")", expression?.ToInvariantString()); var expression2 = ConvertToExpression(_nestedColumnResolver_expression2); - Assert.Equal(@"ent => (ent.PrimaryOrganisation.PrimaryAddress.Id == ""D80526AB-BB64-41FA-BFB8-F74A9332C0CA"".ToGuid())", expression2.ToString()); + Assert.Equal(@"ent => (ent.PrimaryOrganisation.PrimaryAddress.Id == ""D80526AB-BB64-41FA-BFB8-F74A9332C0CA"".ToGuid())", expression2?.ToString()); } [Fact] @@ -555,7 +562,7 @@ public void NullableDatetimeField_LessThan_Convert() ] }"); - Assert.Equal(@"ent => (ent.NullableDateTimeProp < Convert(25/04/2021 6:13:00 AM, Nullable`1))", expression.ToString()); + Assert.Equal("ent => (ent.NullableDateTimeProp < Convert(04/25/2021 06:13:00, Nullable`1))", expression?.ToInvariantString()); } [Fact] @@ -574,7 +581,7 @@ public void NullableDatetimeField_LessThanOrEquals_Convert() ] }"); - Assert.Equal(@"ent => (ent.NullableDateTimeProp <= Convert(25/04/2021 6:13:59 AM, Nullable`1))", expression.ToString()); + Assert.Equal("ent => (ent.NullableDateTimeProp <= Convert(04/25/2021 06:13:59, Nullable`1))", expression?.ToInvariantString()); } [Fact] @@ -593,7 +600,7 @@ public void NullableDatetimeField_GreaterThan_Convert() ] }"); - Assert.Equal(@"ent => (ent.NullableDateTimeProp > Convert(25/04/2021 6:13:59 AM, Nullable`1))", expression.ToString()); + Assert.Equal("ent => (ent.NullableDateTimeProp > Convert(04/25/2021 06:13:59, Nullable`1))", expression?.ToInvariantString()); } [Fact] @@ -612,7 +619,7 @@ public void NullableDatetimeField_GreaterThanOrEquals_Convert() ] }"); - Assert.Equal(@"ent => (ent.NullableDateTimeProp >= Convert(25/04/2021 6:13:00 AM, Nullable`1))", expression.ToString()); + Assert.Equal("ent => (ent.NullableDateTimeProp >= Convert(04/25/2021 06:13:00, Nullable`1))", expression?.ToInvariantString()); } [Fact] @@ -631,7 +638,7 @@ public void NullableDateTimeField_Equals_Convert() ] }"); - Assert.Equal(@"ent => ((Convert(25/04/2021 6:13:00 AM, Nullable`1) <= ent.NullableDateTimeProp) AndAlso (ent.NullableDateTimeProp <= Convert(25/04/2021 6:13:59 AM, Nullable`1)))", expression.ToString()); + Assert.Equal("ent => ((Convert(04/25/2021 06:13:00, Nullable`1) <= ent.NullableDateTimeProp) AndAlso (ent.NullableDateTimeProp <= Convert(04/25/2021 06:13:59, Nullable`1)))", expression?.ToInvariantString()); } [Fact] @@ -650,7 +657,7 @@ public void NullableDateTimeField_NotEquals_Convert() ] }"); - Assert.Equal(@"ent => Not(((Convert(25/04/2021 6:13:00 AM, Nullable`1) <= ent.NullableDateTimeProp) AndAlso (ent.NullableDateTimeProp <= Convert(25/04/2021 6:13:59 AM, Nullable`1))))", expression.ToString()); + Assert.Equal("ent => Not(((Convert(04/25/2021 06:13:00, Nullable`1) <= ent.NullableDateTimeProp) AndAlso (ent.NullableDateTimeProp <= Convert(04/25/2021 06:13:59, Nullable`1))))", expression?.ToInvariantString()); } [Fact] @@ -658,7 +665,7 @@ public void NullableDateTimeField_Between_Convert() { var expression = ConvertToExpression(@"{""<="":[""2022-08-01T16:46:00Z"",{""var"":""CreationTime""},""2022-08-04T16:47:00Z""]}"); - Assert.Equal(@"ent => ((ent.CreationTime >= 01/08/2022 4:46:00 PM) AndAlso (ent.CreationTime <= 04/08/2022 4:47:59 PM))", expression.ToString()); + Assert.Equal("ent => ((ent.CreationTime >= 08/01/2022 16:46:00) AndAlso (ent.CreationTime <= 08/04/2022 16:47:59))", expression?.ToInvariantString()); } #endregion @@ -681,7 +688,7 @@ public void DateField_LessThan_Convert() ] }"); - Assert.Equal(@"ent => (ent.DateProp < 25/04/2021 12:00:00 AM)", expression.ToString()); + Assert.Equal(@"ent => (ent.DateProp < 04/25/2021 00:00:00)", expression?.ToInvariantString()); } [Fact] @@ -700,7 +707,7 @@ public void DateField_LessThanOrEquals_Convert() ] }"); - Assert.Equal(@"ent => (ent.DateProp <= 25/04/2021 11:59:59 PM)", expression.ToString()); + Assert.Equal(@"ent => (ent.DateProp <= 04/25/2021 23:59:59)", expression?.ToInvariantString()); } [Fact] @@ -719,7 +726,7 @@ public void DateField_GreaterThan_Convert() ] }"); - Assert.Equal(@"ent => (ent.DateProp > 25/04/2021 11:59:59 PM)", expression.ToString()); + Assert.Equal(@"ent => (ent.DateProp > 04/25/2021 23:59:59)", expression?.ToInvariantString()); } [Fact] @@ -738,7 +745,7 @@ public void DateField_GreaterThanOrEquals_Convert() ] }"); - Assert.Equal(@"ent => (ent.DateProp >= 25/04/2021 12:00:00 AM)", expression.ToString()); + Assert.Equal(@"ent => (ent.DateProp >= 04/25/2021 00:00:00)", expression?.ToInvariantString()); } [Fact] @@ -757,7 +764,7 @@ public void DateField_Equals_Convert() ] }"); - Assert.Equal(@"ent => ((25/04/2021 12:00:00 AM <= ent.DateProp) AndAlso (ent.DateProp <= 25/04/2021 11:59:59 PM))", expression.ToString()); + Assert.Equal(@"ent => ((04/25/2021 00:00:00 <= ent.DateProp) AndAlso (ent.DateProp <= 04/25/2021 23:59:59))", expression?.ToInvariantString()); } [Fact] @@ -776,7 +783,7 @@ public void DateField_NotEquals_Convert() ] }"); - Assert.Equal(@"ent => Not(((25/04/2021 12:00:00 AM <= ent.DateProp) AndAlso (ent.DateProp <= 25/04/2021 11:59:59 PM)))", expression.ToString()); + Assert.Equal(@"ent => Not(((04/25/2021 00:00:00 <= ent.DateProp) AndAlso (ent.DateProp <= 04/25/2021 23:59:59)))", expression?.ToInvariantString()); } [Fact] @@ -784,7 +791,7 @@ public void DateField_Between_Convert() { var expression = ConvertToExpression(@"{""<="":[""2022-08-01T16:46:00Z"",{""var"":""DateProp""},""2022-08-04T16:47:00Z""]}"); - Assert.Equal(@"ent => ((ent.DateProp >= 01/08/2022 12:00:00 AM) AndAlso (ent.DateProp <= 04/08/2022 11:59:59 PM))", expression.ToString()); + Assert.Equal(@"ent => ((ent.DateProp >= 08/01/2022 00:00:00) AndAlso (ent.DateProp <= 08/04/2022 23:59:59))", expression?.ToInvariantString()); } #endregion @@ -807,7 +814,7 @@ public void TimeField_LessThan_Convert() ] }"); - Assert.Equal(@"ent => (ent.TimeProp < 02:34:00)", expression.ToString()); + Assert.Equal(@"ent => (ent.TimeProp < 02:34:00)", expression?.ToInvariantString()); } [Fact] @@ -826,7 +833,7 @@ public void TimeField_LessThanOrEquals_Convert() ] }"); - Assert.Equal(@"ent => (ent.TimeProp <= 02:34:59.9990000)", expression.ToString()); + Assert.Equal(@"ent => (ent.TimeProp <= 02:34:59.9990000)", expression?.ToInvariantString()); } [Fact] @@ -845,7 +852,7 @@ public void TimeField_GreaterThan_Convert() ] }"); - Assert.Equal(@"ent => (ent.TimeProp > 02:34:59.9990000)", expression.ToString()); + Assert.Equal(@"ent => (ent.TimeProp > 02:34:59.9990000)", expression?.ToInvariantString()); } [Fact] @@ -864,7 +871,7 @@ public void TimeField_GreaterThanOrEquals_Convert() ] }"); - Assert.Equal(@"ent => (ent.TimeProp >= 02:34:00)", expression.ToString()); + Assert.Equal(@"ent => (ent.TimeProp >= 02:34:00)", expression?.ToInvariantString()); } [Fact] @@ -883,7 +890,7 @@ public void TimeField_Equals_Convert() ] }"); - Assert.Equal(@"ent => ((02:34:00 <= ent.TimeProp) AndAlso (ent.TimeProp <= 02:34:59.9990000))", expression.ToString()); + Assert.Equal(@"ent => ((02:34:00 <= ent.TimeProp) AndAlso (ent.TimeProp <= 02:34:59.9990000))", expression?.ToInvariantString()); } [Fact] @@ -902,7 +909,7 @@ public void TimeField_NotEquals_Convert() ] }"); - Assert.Equal(@"ent => Not(((02:34:00 <= ent.TimeProp) AndAlso (ent.TimeProp <= 02:34:59.9990000)))", expression.ToString()); + Assert.Equal(@"ent => Not(((02:34:00 <= ent.TimeProp) AndAlso (ent.TimeProp <= 02:34:59.9990000)))", expression?.ToInvariantString()); } [Fact] @@ -910,7 +917,7 @@ public void TimeField_Between_Convert() { var expression = ConvertToExpression(@"{""<="":[3600,{""var"":""TimeProp""},7200]}"); - Assert.Equal(@"ent => ((ent.TimeProp >= 01:00:00) AndAlso (ent.TimeProp <= 02:00:59.9990000))", expression.ToString()); + Assert.Equal(@"ent => ((ent.TimeProp >= 01:00:00) AndAlso (ent.TimeProp <= 02:00:59.9990000))", expression?.ToInvariantString()); } #endregion @@ -935,7 +942,7 @@ public void EntityReference_Equals_Convert() { var expression = ConvertToExpression(_entityReference_Equals_expression); - Assert.Equal($@"ent => (ent.{nameof(ShaRolePermission.ShaRole)}.Id == ""852c4011-4e94-463a-9e0d-b0054ab88f7d"".ToGuid())", expression.ToString()); + Assert.Equal($@"ent => (ent.{nameof(ShaRolePermission.ShaRole)}.Id == ""852c4011-4e94-463a-9e0d-b0054ab88f7d"".ToGuid())", expression?.ToInvariantString()); } [Fact] @@ -964,7 +971,7 @@ public void EntityReference_Int_Id_Equals_Convert() var expression = ConvertToExpression(gqlExpression); - Assert.Equal($@"ent => (ent.{nameof(EntityWithIntId.Child)}.Id == 500)", expression.ToString()); + Assert.Equal($@"ent => (ent.{nameof(EntityWithIntId.Child)}.Id == 500)", expression?.ToInvariantString()); } [Fact] @@ -980,7 +987,7 @@ public void EntityReference_IsEmpty_Convert_Test() ] }"); - Assert.Equal(@"ent => (ent.ShaRole == null)", expression.ToString()); + Assert.Equal(@"ent => (ent.ShaRole == null)", expression?.ToInvariantString()); } @@ -1001,7 +1008,7 @@ public void EntityReference_IsNull_Convert() { var expression = ConvertToExpression(_entityReference_IsNull_expression); - Assert.Equal($@"ent => (ent.ShaRole == null)", expression.ToString()); + Assert.Equal($@"ent => (ent.ShaRole == null)", expression?.ToInvariantString()); } [Fact] @@ -1029,7 +1036,7 @@ public void EntityReference_IsNotNull_Convert() { var expression = ConvertToExpression(_entityReference_IsNotNull_expression); - Assert.Equal($@"ent => Not((ent.ShaRole == null))", expression.ToString()); + Assert.Equal($@"ent => Not((ent.ShaRole == null))", expression?.ToInvariantString()); } [Fact] @@ -1081,7 +1088,7 @@ public void ComplexExpression_Convert() { var expression = ConvertToExpression(_complex_expression); - Assert.Equal(@"ent => ((ent.ShaRole.Id == ""852c4011-4e94-463a-9e0d-b0054ab88f7d"".ToGuid()) OrElse ((ent.ShaRole.LastModificationTime > Convert(25/04/2021 8:13:59 AM, Nullable`1)) AndAlso (ent.IsGranted == False)))", expression.ToString()); + Assert.Equal("ent => ((ent.ShaRole.Id == \"852c4011-4e94-463a-9e0d-b0054ab88f7d\".ToGuid()) OrElse ((ent.ShaRole.LastModificationTime > Convert(04/25/2021 08:13:59, Nullable`1)) AndAlso (ent.IsGranted == False)))", expression?.ToInvariantString()); } [Fact] @@ -1171,14 +1178,14 @@ await TryFetchDataAsync(_booleanField_NotEquals_expression, public void ReflistField_Equals_Convert() { var expression = ConvertToExpression(@"{""and"":[{""=="":[{""var"":""title""},1]}]}"); - Assert.Equal(@"ent => (Convert(ent.Title, Int64) == 1)", expression.ToString()); + Assert.Equal(@"ent => (Convert(ent.Title, Int64) == 1)", expression?.ToInvariantString()); } [Fact] public void NullableReflistField_Equals_Convert() { var expression = ConvertToExpression(@"{""and"":[{""=="":[{""var"":""nullableTitle""},1]}]}"); - Assert.Equal(@"ent => (Convert(ent.NullableTitle, Nullable`1) == Convert(1, Nullable`1))", expression.ToString()); + Assert.Equal(@"ent => (Convert(ent.NullableTitle, Nullable`1) == Convert(1, Nullable`1))", expression?.ToInvariantString()); } #endregion @@ -1201,7 +1208,7 @@ public void NullableReflistField_Equals_Convert() public void NullableReflistField_Contains_Convert() { var expression = ConvertToExpression(_nullable_reflistField_Contains_expression); - Assert.Equal(@"ent => (((Convert(ent.Title, Nullable`1) == Convert(1, Nullable`1)) OrElse (Convert(ent.Title, Nullable`1) == Convert(2, Nullable`1))) OrElse (Convert(ent.Title, Nullable`1) == Convert(3, Nullable`1)))", expression.ToString()); + Assert.Equal(@"ent => (((Convert(ent.Title, Nullable`1) == Convert(1, Nullable`1)) OrElse (Convert(ent.Title, Nullable`1) == Convert(2, Nullable`1))) OrElse (Convert(ent.Title, Nullable`1) == Convert(3, Nullable`1)))", expression?.ToInvariantString()); } [Fact] @@ -1232,7 +1239,7 @@ public async Task NullableReflistField_Contains_FetchAsync() public void EntityReference_In_Convert() { var expression = ConvertToExpression(_entityReference_In_Convert_expression); - Assert.Equal(@"ent => ((ent.Id == ""24007BA5-697B-417C-91BA-ED92F3F31F3A"".ToGuid()) OrElse (ent.Id == ""D197A7B3-5505-430C-9D98-CD64F1A638FA"".ToGuid()))", expression.ToString()); + Assert.Equal(@"ent => ((ent.Id == ""24007BA5-697B-417C-91BA-ED92F3F31F3A"".ToGuid()) OrElse (ent.Id == ""D197A7B3-5505-430C-9D98-CD64F1A638FA"".ToGuid()))", expression?.ToInvariantString()); } [Fact] @@ -1263,7 +1270,7 @@ public void Custom_Date_Funcs_Convert() var nowEom = Expression.Constant(Clock.Now.EndOfTheMinute()).ToString(); var expected = $@"ent => ((Convert({now}.Add(-5.00:00:00), Nullable`1) <= ent.User.LastLoginDate) AndAlso (ent.User.LastLoginDate <= Convert({nowEom}, Nullable`1)))"; - Assert.Equal(expected, expression.ToString()); + Assert.Equal(expected, expression?.ToInvariantString()); } } @@ -1327,7 +1334,7 @@ public void Custom_String_Funcs_Convert() var expression = ConvertToExpression(_custom_string_funcs_Convert_expression); var expected = $@"ent => (((ent.FirstName == ""TeSt"".ToLower()) AndAlso (ent.LastName == ""VaLuE"".ToUpper())) AndAlso (ent.EmailAddress1 == ent.EmailAddress2.ToLower()))"; - Assert.Equal(expected, expression.ToString()); + Assert.Equal(expected, expression?.ToInvariantString()); } [Fact] @@ -1479,7 +1486,7 @@ public void DecimalField_Convert(string name, string jsonLogicExpression, string Console.WriteLine($"Test: '{name}'"); var linqExpression = ConvertToExpression>(jsonLogicExpression); - Assert.Equal(expectation, linqExpression.ToString()); + Assert.Equal(expectation, linqExpression?.ToInvariantString()); } [Theory] @@ -1496,7 +1503,7 @@ public void FloatField_Convert(string name, string jsonLogicExpression, string e Console.WriteLine($"Test: '{name}'"); var linqExpression = ConvertToExpression>(jsonLogicExpression); - Assert.Equal(expectation, linqExpression.ToString()); + Assert.Equal(expectation, linqExpression?.ToInvariantString()); } [Theory] @@ -1513,7 +1520,7 @@ public void DoubleField_Convert(string name, string jsonLogicExpression, string Console.WriteLine($"Test: '{name}'"); var linqExpression = ConvertToExpression>(jsonLogicExpression); - Assert.Equal(expectation, linqExpression.ToString()); + Assert.Equal(expectation, linqExpression?.ToInvariantString()); } [Theory] @@ -1530,7 +1537,7 @@ public void ByteField_Convert(string name, string jsonLogicExpression, string ex Console.WriteLine($"Test: '{name}'"); var linqExpression = ConvertToExpression>(jsonLogicExpression); - Assert.Equal(expectation, linqExpression.ToString()); + Assert.Equal(expectation, linqExpression?.ToInvariantString()); } #endregion diff --git a/shesha-core/test/Shesha.Tests/Metadata/MetadataProvider_Tests.cs b/shesha-core/test/Shesha.Tests/Metadata/MetadataProvider_Tests.cs index 4f2edc55f8..439087e2f5 100644 --- a/shesha-core/test/Shesha.Tests/Metadata/MetadataProvider_Tests.cs +++ b/shesha-core/test/Shesha.Tests/Metadata/MetadataProvider_Tests.cs @@ -1,13 +1,18 @@ -using Abp.TestBase; -using Shesha.Metadata; +using Shesha.Metadata; +using Shesha.Tests.Fixtures; using System.Linq; using System.Threading.Tasks; using Xunit; namespace Shesha.Tests.Metadata { - public class MetadataProvider_Tests: AbpIntegratedTestBase + [Collection(SqlServerCollection.Name)] + public class MetadataProvider_Tests : SheshaNhTestBase { + public MetadataProvider_Tests(SqlServerFixture fixture) : base(fixture) + { + } + [Fact] public async Task ShouldInclude_NotMappedProperties_TestAsync() { diff --git a/shesha-core/test/Shesha.Tests/Notifications/NotificationTemplateContext_Tests.cs b/shesha-core/test/Shesha.Tests/Notifications/NotificationTemplateContext_Tests.cs new file mode 100644 index 0000000000..03de4019d2 --- /dev/null +++ b/shesha-core/test/Shesha.Tests/Notifications/NotificationTemplateContext_Tests.cs @@ -0,0 +1,123 @@ +using Abp.Notifications; +using Shouldly; +using Stubble.Core.Builders; +using System; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Xunit; + +namespace Shesha.Tests.Notifications +{ + /// + /// Temp test for debug only, to be removed + /// + public class NotificationTemplateContext_Tests + { + [Fact] + public async Task HelperVsStubblePlainTestAsync() + { + var template = @"Dear {{FirstName}} {{LastName}}. You have been invited to join {{Organisation}}. Follow this link {{RegistrationUrl}} to accept your invitation."; + + var data = GetTestData(); + var viaHelper = TemplateHelper.ReplacePlaceholders(template, data); + var viaStubble = await GenerateContentAsync(template, data); + + viaStubble.ShouldBe(viaHelper); + } + + [Fact] + public async Task HelperVsStubbleComplexTestAsync() + { + var template = @"Dear {{FirstName}} {{LastName}}. You have been invited to join {{Organisation}}. Follow this link {{RegistrationUrl}} to accept your invitation. Message: {{Nested.Message}}"; + + var data = GetTestData(); + var viaHelper = TemplateHelper.ReplacePlaceholders(template, data); + var viaStubble = await GenerateContentAsync(template, data); + + // TemplateHelper doesn't support nested objects + viaHelper.ShouldContain("{{"); + viaStubble.ShouldNotBeNull(); + viaStubble.ShouldNotContain("{{"); + } + + [Fact] + public async Task HelperVsStubblePlainCamelCaseTestAsync() + { + var template = @"Dear {{firstName}} {{lastName}}. You have been invited to join {{organisation}}. Follow this link {{registrationUrl}} to accept your invitation."; + + var data = GetTestData(); + var viaHelper = TemplateHelper.ReplacePlaceholders(template, data); + var viaStubble = await GenerateContentAsync(template, data); + + viaStubble.ShouldBe(viaHelper); + } + + private TestNotificationData GetTestData() + { + return new TestNotificationData + { + FirstName = "John", + LastName = "Doe", + Organisation = "Big corp", + RegistrationUrl = "http://big.corp", + Nested = new TestNotificationNestedData + { + Message = "Hi, man!" + } + }; + } + + protected async Task GenerateContentAsync(string? template, TData data) + { + var stubbleRenderer = new StubbleBuilder().Configure(settings => + { + settings.SetIgnoreCaseOnKeyLookup(true); + }).Build(); + + return !string.IsNullOrWhiteSpace(template) + ? await stubbleRenderer.RenderAsync(template, data) + : template; + } + + public class TestNotificationNestedData + { + public string Message { get; set; } + } + + public class TestNotificationData: NotificationData + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string Organisation { get; set; } + public string RegistrationUrl { get; set; } + public TestNotificationNestedData Nested { get; set; } + } + + public static class TemplateHelper + { + // TODO: review and this implementation + public static string ReplacePlaceholders(string template, TData data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + // Use regex to find placeholders in the form {{propertyName}} + return Regex.Replace(template, @"\{\{(\w+)\}\}", match => + { + var propertyName = match.Groups[1].Value; + + // Use the runtime type of the object for property lookup + var propertyInfo = data.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(prop => string.Equals(prop.Name, propertyName, StringComparison.OrdinalIgnoreCase)); + + return propertyInfo != null + ? propertyInfo.GetValue(data)?.ToString() ?? string.Empty + : string.Empty; + }); + } + } + } +} diff --git a/shesha-core/test/Shesha.Tests/Notifications/TemplateDataEntitySerialization_Tests.cs b/shesha-core/test/Shesha.Tests/Notifications/TemplateDataEntitySerialization_Tests.cs new file mode 100644 index 0000000000..c31c76c5b8 --- /dev/null +++ b/shesha-core/test/Shesha.Tests/Notifications/TemplateDataEntitySerialization_Tests.cs @@ -0,0 +1,111 @@ +using Abp.Domain.Entities; +using Abp.Domain.Repositories; +using Abp.Json; +using Shesha.Domain; +using Shesha.EntityReferences; +using Shesha.Reflection; +using Shesha.Tests.Fixtures; +using Shesha.Utilities; +using Shouldly; +using Stubble.Core.Builders; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Shesha.Tests.Notifications +{ + [Collection(SqlServerCollection.Name)] + public class TemplateDataEntitySerialization_Tests : SheshaNhTestBase + { + private readonly IRepository _personRepository; + + public TemplateDataEntitySerialization_Tests(SqlServerFixture fixture) : base(fixture) + { + _personRepository = Resolve>(); + } + + [Fact] + public Task PersonSerialization_TestAsync() + { + return WithUnitOfWorkAsync(async() => { + var author = await GetAuthorAsync(); + var liveData = new SourceData { + Message = "Hi!", + Author = author + }; + var dataBefore = new CustomTemplateData + { + Message = liveData.Message, + Author = liveData.Author, + }; + + var json = dataBefore.ToJsonString(); + + var dataAfter = json.FromJsonString().NotNull(); + + var restoredPerson = (Person)dataAfter.Author; + restoredPerson.FirstName.ShouldBe(author.FirstName); + + var restoredPersonForceCast = dataAfter.Author.ForceCastAs(); + restoredPersonForceCast.FirstName.ShouldBe(author.FirstName); + + var template = @"{{Author.FirstName}} {{Author.LastName}} says: '{{Message}}'"; + + var messageLive = await GenerateContentAsync(template, liveData); + var messageBefore = await GenerateContentAsync(template, dataBefore); + var messageAfter = await GenerateContentAsync(template, dataAfter); + + messageBefore.ShouldBe(messageLive); + messageAfter.ShouldBe(messageBefore); + }); + } + + protected async Task GenerateContentAsync(string? template, TData data) + { + var stubbleRenderer = new StubbleBuilder().Configure(settings => + { + settings.SetIgnoreCaseOnKeyLookup(true); + + settings.AddValueGetter(typeof(GenericEntityReference), (object value, string key, bool ignoreCase) => { + if (value is GenericEntityReference entityRef) + { + var entity = (Entity)entityRef; + + return entity != null + ? ReflectionHelper.GetPropertyValue(entity, key, null) + : null; + } else + return null; + }); + }).Build(); + + return !string.IsNullOrWhiteSpace(template) + ? await stubbleRenderer.RenderAsync(template, data) + : template; + } + + private async Task GetAuthorAsync() + { + var id = "6F395F2B-5BA4-4E2D-B1F2-DD2EB426E3FA".ToGuid(); + var person = await _personRepository.FirstOrDefaultAsync(id); + if (person == null) + { + person = new Person { Id = id, FirstName = "John", LastName = "Doe", EmailAddress1 = "john.doe@mail.com" }; + await _personRepository.InsertAsync(person); + } + return person; + } + + public class SourceData + { + public string Message { get; set; } + public Person Author { get; set; } + } + + public class CustomTemplateData + { + public string Message { get; set; } + public GenericEntityReference Author { get; set; } + } + } +} diff --git a/shesha-core/test/Shesha.Tests/Otp/OtpManager_Tests.cs b/shesha-core/test/Shesha.Tests/Otp/OtpManager_Tests.cs index 86c06237d0..55eaf3ee9b 100644 --- a/shesha-core/test/Shesha.Tests/Otp/OtpManager_Tests.cs +++ b/shesha-core/test/Shesha.Tests/Otp/OtpManager_Tests.cs @@ -5,6 +5,7 @@ using Shesha.Otp.Configuration; using Shesha.Otp.Dto; using Shesha.Sms; +using Shesha.Tests.Fixtures; using Shouldly; using System; using System.Collections.Generic; @@ -13,8 +14,13 @@ namespace Shesha.Tests.Otp { - public class OtpManager_Tests: SheshaNhTestBase + [Collection(SqlServerCollection.Name)] + public class OtpManager_Tests : SheshaNhTestBase { + public OtpManager_Tests(SqlServerFixture fixture) : base(fixture) + { + } + [Fact] public async Task SuccessOtp_TestAsync() { @@ -43,7 +49,7 @@ public async Task SuccessEmailLink_TestAsync() response.ErrorMessage.ShouldBeNullOrEmpty(); } - private async Task CheckOtpCommonAsync(Action transformAction) + private async Task CheckOtpCommonAsync(Action? transformAction) { // todo: implement settings and register using normal way /* @@ -63,7 +69,7 @@ private async Task CheckOtpCommonAsync(Action s.GetAsync(It.IsAny())).Returns(id => Task.FromResult(new OtpDto + otpStorage.Setup(s => s.GetOrNullAsync(It.IsAny())).Returns(id => Task.FromResult(new OtpDto { Pin = storage[id], OperationId = id, @@ -95,7 +101,7 @@ private async Task CheckOtpCommonAsync(Action CheckEmailLinkAsync(Action action) + private async Task CheckEmailLinkAsync(Action? action) { var settings = LocalIocManager.Resolve(); @@ -110,7 +116,7 @@ private async Task CheckEmailLinkAsync(Action s.GetAsync(It.IsAny())).Returns(id => Task.FromResult(new OtpDto + otpStorage.Setup(s => s.GetOrNullAsync(It.IsAny())).Returns(id => Task.FromResult(new OtpDto { Pin = storage[id], OperationId = id, @@ -143,4 +149,4 @@ private async Task CheckEmailLinkAsync(Action(); - var passwordLengthAccessor = new Mock>(); - passwordLengthAccessor.Setup(s => s.GetValue()).Returns(length); + var otpSettings = new OtpSettings { + Alphabet = alphabet, + PasswordLength = length, + }; + var otpSettingsMock = new Mock>(); + otpSettingsMock.Setup(s => s.GetValueOrNull(null)).Returns(otpSettings); + otpSettingsMock.Setup(s => s.GetValue(null)).Returns(otpSettings); - settings.SetupGet(s => s.OneTimePins.GetValue().PasswordLength).Returns(passwordLengthAccessor.Object.GetValue()); - - var alphabetAccessor = new Mock>(); - alphabetAccessor.Setup(s => s.GetValue()).Returns(alphabet); - settings.SetupGet(s => s.OneTimePins.GetValue().Alphabet).Returns(alphabetAccessor.Object.GetValue()); - + settings.SetupGet(s => s.OneTimePins).Returns(otpSettingsMock.Object); var generator = new Shesha.Otp.OtpGenerator(settings.Object); diff --git a/shesha-core/test/Shesha.Tests/Permissions/Permissions_Tests.cs b/shesha-core/test/Shesha.Tests/Permissions/Permissions_Tests.cs deleted file mode 100644 index 7e1c515913..0000000000 --- a/shesha-core/test/Shesha.Tests/Permissions/Permissions_Tests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Abp.Domain.Repositories; -using Abp.Domain.Uow; -using Shesha.Domain; -using Shesha.DynamicEntities; -using Shesha.Permissions; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Xunit; - -namespace Shesha.Tests.Permissions -{ - public class Permissions_Tests : SheshaNhTestBase - { - private readonly IUnitOfWorkManager _unitOfWorkManager; - private readonly IRepository _personRepository; - private readonly IRepository _organisationRepository; - private readonly PermissionedObjectManager _permissionedObjectManager; - private readonly IEntityModelBinder _entityModelBinder; - - public Permissions_Tests() - { - _unitOfWorkManager = Resolve(); - _personRepository = Resolve>(); - _organisationRepository = Resolve>(); - _permissionedObjectManager = Resolve(); - _entityModelBinder = Resolve(); - } - - [Fact] - public async Task GetAllApi_TestAsync() - { - var api = await Task.Run(() => _permissionedObjectManager.GetAllTreeAsync()); - } - - private Task GetAsync(int i) - { - return Task.Run(() => - { - Debug.WriteLine($"Start item {i}"); - var s = DateTime.Now; - var t = _permissionedObjectManager.Get($"shurik@{i}", ""); - Debug.WriteLine($"Executed item {i}"); - var e = DateTime.Now; - var d = e - s; - - Debug.WriteLine($"item {i}, time: {d.Milliseconds}"); - }); - } - - [Fact] - public void AsyncHelperTest() - { - LoginAsHostAdmin(); - - /*using (var uow = _unitOfWorkManager.Begin(new UnitOfWorkOptions() - { - Timeout = TimeSpan.FromSeconds(100) - })) - {*/ - var l = new List(); - - for(var i = 0; i < 1; i++) - { - l.Add(GetAsync(i)); - } - - while (l.Any(x => !x.IsCompleted)) - { - - } - - /*await uow.CompleteAsync(); - }*/ - } - } -} diff --git a/shesha-core/test/Shesha.Tests/Person/PersonAppService_Tests.cs b/shesha-core/test/Shesha.Tests/Person/PersonAppService_Tests.cs deleted file mode 100644 index 99679f3f9e..0000000000 --- a/shesha-core/test/Shesha.Tests/Person/PersonAppService_Tests.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Threading.Tasks; -using Abp.Domain.Repositories; -using NHibernate.Linq; -using Shouldly; -using Xunit; - -namespace Shesha.Tests.Users -{ - public class PersonAppService_Tests : SheshaNhTestBase - { - private readonly IRepository _personRepository; - - public PersonAppService_Tests() - { - _personRepository = Resolve>(); - } - - /* - [Fact] - public async Task CreatePerson_Test() - { - LoginAsHostAdmin(); - - var userName = "john.doe"; - - await _personRepository.InsertAsync( - new Domain.Person - { - UserName = userName, - FirstName = "John", - LastName = "Doe" - }); - - await UsingDbSessionAsync(async session => - { - var johnNashUser = await session.Query().FirstOrDefaultAsync(u => u.UserName == userName); - johnNashUser.ShouldNotBeNull(); - }); - } -*/ - } -} diff --git a/shesha-core/test/Shesha.Tests/QuickSearch/QuickSearcher_Tests.cs b/shesha-core/test/Shesha.Tests/QuickSearch/QuickSearcher_Tests.cs index 6a9da48d1e..2e6fb2c1d4 100644 --- a/shesha-core/test/Shesha.Tests/QuickSearch/QuickSearcher_Tests.cs +++ b/shesha-core/test/Shesha.Tests/QuickSearch/QuickSearcher_Tests.cs @@ -9,7 +9,9 @@ using Shesha.Domain.Attributes; using Shesha.Domain.Enums; using Shesha.QuickSearch; +using Shesha.QuickSearch.Cache; using Shesha.Services; +using Shesha.Tests.Fixtures; using System; using System.Collections.Generic; using System.Linq; @@ -21,8 +23,14 @@ namespace Shesha.Tests.QuickSearch /// /// Tests for /// + [Collection(SqlServerCollection.Name)] public class QuickSearcher_Tests : SheshaNhTestBase { + public QuickSearcher_Tests(SqlServerFixture fixture) : base(fixture) + { + + } + [Fact] public void SearchPerson_TextFields_Convert_Test() { @@ -132,7 +140,9 @@ public void SearchTest_Organisation_MultiValueRefList_Convert_Test() nameof(TestOrganisation.ContactMethods) }); - Assert.Equal(@"ent => value(NHibernate.Linq.NhQueryable`1[Shesha.Domain.ReferenceListItem]).Any(entContactMethods => ((((entContactMethods.ReferenceList.Configuration.Module.Name == ""Shesha"") AndAlso (entContactMethods.ReferenceList.Configuration.Name == ""Shesha.Core.PreferredContactMethod"")) AndAlso ((Convert(ent.ContactMethods, Nullable`1) & Convert(entContactMethods.ItemValue, Nullable`1)) > Convert(0, Nullable`1))) AndAlso entContactMethods.Item.Contains(""email"")))", expression.ToString()); + var refListId = GetReflistId(new ReferenceListIdentifier("Shesha", "Shesha.Core", "PreferredContactMethod")); + + Assert.Equal($@"ent => value(NHibernate.Linq.NhQueryable`1[Shesha.Domain.ReferenceListItem]).Any(entContactMethods => (((entContactMethods.ReferenceList.Id == {refListId}) AndAlso ((Convert(ent.ContactMethods, Nullable`1) & Convert(entContactMethods.ItemValue, Nullable`1)) > Convert(0, Nullable`1))) AndAlso entContactMethods.Item.Contains(""email"")))", expression.ToString()); } [Fact] @@ -177,7 +187,8 @@ public void SearchTest_Organisation_MultiValueRefList_Fetch_Test() return refList; }); - var quickSearcher = new QuickSearcher(Resolve(), rliRepoMock.Object, Resolve(), refListHelperRepoMock.Object); + var quickSearchPropertiesCacheHolder = Resolve(); + var quickSearcher = new QuickSearcher(Resolve(), rliRepoMock.Object, Resolve(), refListHelperRepoMock.Object, quickSearchPropertiesCacheHolder); var expression = quickSearcher.GetQuickSearchExpression("Email", new List { nameof(TestOrganisation.ContactMethods) @@ -197,25 +208,22 @@ public void SearchTest_Organisation_MultiValueRefList_Fetch_Test() #region private methods - private async Task> TryFetchDataAsync(Func, IQueryable> prepareQueryable = null, Action> assertions = null) where T : class, IEntity + private async Task> TryFetchDataAsync(Func, IQueryable>? prepareQueryable = null, Action>? assertions = null) where T : class, IEntity { var repository = LocalIocManager.Resolve>(); var asyncExecuter = LocalIocManager.Resolve(); - List data = null; - - await WithUnitOfWorkAsync(async () => { + return await WithUnitOfWorkAsync(async () => { var query = repository.GetAll(); if (prepareQueryable != null) query = prepareQueryable.Invoke(query); - data = await asyncExecuter.ToListAsync(query); + var data = await asyncExecuter.ToListAsync(query); assertions?.Invoke(data); + return data; }); - - return data; } #endregion diff --git a/shesha-core/test/Shesha.Tests/Settings/SettingsAudit_Tests.cs b/shesha-core/test/Shesha.Tests/Settings/SettingsAudit_Tests.cs deleted file mode 100644 index 9b6e0d2f6c..0000000000 --- a/shesha-core/test/Shesha.Tests/Settings/SettingsAudit_Tests.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Threading.Tasks; -using Abp.Configuration; -using Abp.Domain.Uow; -using Shesha.Configuration; -using Shesha.NHibernate.UoW; -using Shesha.Reflection; -using Shesha.Services; -using Shouldly; -using Xunit; - -namespace Shesha.Tests.Settings -{ - public class SettingsAudit_Tests : SheshaNhTestBase - { - - private readonly IUnitOfWorkManager _unitOfWorkManager; - private readonly ISettingManager _settingManager; - - public SettingsAudit_Tests() - { - _unitOfWorkManager = Resolve(); - _settingManager = Resolve(); - } - - [Fact] - public void Save_Test() - { - LoginAsHostAdmin(); - try - { - using (var uow = _unitOfWorkManager.Begin()) - { - var nhuow = uow as NhUnitOfWork; - var session = nhuow?.GetSession(); - - var testSetting = _settingManager.GetSettingValue("TestSetting"); - testSetting.ShouldNotBeNull(); - - _settingManager.ChangeSettingForApplication("TestSetting", "2"); - session.Flush(); - - session.CreateSQLQuery("select count(1) from AbpSettings where Name = 'TestSetting'") - .UniqueResult().ShouldBe(1); - session.CreateSQLQuery( - "select count(1) from AbpEntityPropertyChanges where EntityChangeId in (select id from AbpEntityChanges where EntityId = 'TestSetting')") - .UniqueResult().ShouldBe(1); - session.CreateSQLQuery("select count(1) from AbpEntityChanges where EntityId = 'TestSetting'") - .UniqueResult().ShouldBe(1); - session.CreateSQLQuery("select count(1) from AbpEntityChangeSets where id in (select EntityChangeSetId from AbpEntityChanges where EntityId = 'TestSetting')") - .UniqueResult().ShouldBe(1); - } - using (var uow = _unitOfWorkManager.Begin()) - { - var nhuow = uow as NhUnitOfWork; - var session = nhuow?.GetSession(); - - var testSetting = _settingManager.GetSettingValue("TestSetting"); - testSetting.ShouldNotBeNull(); - - _settingManager.ChangeSettingForApplication("TestSetting", "3"); - session.Flush(); - session.CreateSQLQuery("select count(1) from AbpSettings where Name = 'TestSetting'") - .UniqueResult().ShouldBe(1); - session.CreateSQLQuery( - "select count(1) from AbpEntityPropertyChanges where EntityChangeId in (select id from AbpEntityChanges where EntityId = 'TestSetting')") - .UniqueResult().ShouldBe(2); - session.CreateSQLQuery("select count(1) from AbpEntityChanges where EntityId = 'TestSetting'") - .UniqueResult().ShouldBe(2); - session.CreateSQLQuery("select count(1) from AbpEntityChangeSets where id in (select EntityChangeSetId from AbpEntityChanges where EntityId = 'TestSetting')") - .UniqueResult().ShouldBe(2); - } - using (var uow = _unitOfWorkManager.Begin()) - { - var nhuow = uow as NhUnitOfWork; - var session = nhuow?.GetSession(); - - var testSetting = _settingManager.GetSettingValue("TestSetting"); - testSetting.ShouldNotBeNull(); - - _settingManager.ChangeSettingForApplication("TestSetting", "1"); - session.Flush(); - session.CreateSQLQuery("select count(1) from AbpSettings where Name = 'TestSetting'") - .UniqueResult().ShouldBe(0); - session.CreateSQLQuery( - "select count(1) from AbpEntityPropertyChanges where EntityChangeId in (select id from AbpEntityChanges where EntityId = 'TestSetting')") - .UniqueResult().ShouldBe(3); - session.CreateSQLQuery("select count(1) from AbpEntityChanges where EntityId = 'TestSetting'") - .UniqueResult().ShouldBe(3); - session.CreateSQLQuery("select count(1) from AbpEntityChangeSets where id in (select EntityChangeSetId from AbpEntityChanges where EntityId = 'TestSetting')") - .UniqueResult().ShouldBe(3); - } - using (var uow = _unitOfWorkManager.Begin()) - { - var nhuow = uow as NhUnitOfWork; - var session = nhuow?.GetSession(); - - var testSetting = _settingManager.GetSettingValue("TestSetting"); - testSetting.ShouldNotBeNull(); - - _settingManager.ChangeSettingForApplication("TestSetting", "2"); - session.Flush(); - session.CreateSQLQuery("select count(1) from AbpSettings where Name = 'TestSetting'") - .UniqueResult().ShouldBe(1); - session.CreateSQLQuery( - "select count(1) from AbpEntityPropertyChanges where EntityChangeId in (select id from AbpEntityChanges where EntityId = 'TestSetting')") - .UniqueResult().ShouldBe(4); - session.CreateSQLQuery("select count(1) from AbpEntityChanges where EntityId = 'TestSetting'") - .UniqueResult().ShouldBe(4); - session.CreateSQLQuery("select count(1) from AbpEntityChangeSets where id in (select EntityChangeSetId from AbpEntityChanges where EntityId = 'TestSetting')") - .UniqueResult().ShouldBe(4); - } - } - finally - { - using (var uow = _unitOfWorkManager.Begin()) - { - var nhuow = uow as NhUnitOfWork; - var session = nhuow?.GetSession(); - - // delete temporary values - var entityChangeSetId = - session.CreateSQLQuery("select EntityChangeSetId from AbpEntityChanges where EntityId = 'TestSetting'") - .List(); - - session.CreateSQLQuery("delete from AbpEntityPropertyChanges where EntityChangeId in (select id from AbpEntityChanges where EntityId = 'TestSetting')") - .ExecuteUpdate(); - session.CreateSQLQuery("delete from AbpEntityChanges where EntityId = 'TestSetting'") - .ExecuteUpdate(); - foreach (var id in entityChangeSetId) - { - session.CreateSQLQuery($"delete from AbpEntityChangeSets where id = {id}") - .ExecuteUpdate(); - } - session.CreateSQLQuery($"delete from AbpSettings where Name = 'TestSetting'") - .ExecuteUpdate(); - - session.Flush(); - } - } - } - - /* todo: review and uncomment */ - [Fact] - public void SaveTwoSettings_Test() - { - LoginAsHostAdmin(); - try - { - using (var uow = _unitOfWorkManager.Begin()) - { - var nhuow = uow as NhUnitOfWork; - var session = nhuow?.GetSession(); - - var testSetting = _settingManager.GetSettingValue("TestSetting"); - testSetting.ShouldNotBeNull(); - var testSetting2 = _settingManager.GetSettingValue("TestSetting2"); - testSetting2.ShouldNotBeNull(); - - _settingManager.ChangeSettingForApplication("TestSetting", "2"); - _settingManager.ChangeSettingForApplication("TestSetting2", "b"); - session.Flush(); - } - } - finally - { - using (var uow = _unitOfWorkManager.Begin()) - { - var nhuow = uow as NhUnitOfWork; - var session = nhuow?.GetSession(); - - // delete temporary values - var entityChangeSetId = - session.CreateSQLQuery("select EntityChangeSetId from AbpEntityChanges where EntityId like 'TestSetting%'") - .List(); - - session.CreateSQLQuery("delete from AbpEntityPropertyChanges where EntityChangeId in (select id from AbpEntityChanges where EntityId like 'TestSetting%')") - .ExecuteUpdate(); - session.CreateSQLQuery("delete from AbpEntityChanges where EntityId like 'TestSetting%'") - .ExecuteUpdate(); - foreach (var id in entityChangeSetId) - { - session.CreateSQLQuery($"delete from AbpEntityChangeSets where id = {id}") - .ExecuteUpdate(); - } - session.CreateSQLQuery($"delete from AbpSettings where Name like 'TestSetting%'") - .ExecuteUpdate(); - - session.Flush(); - } - } - } - - /* todo: review and uncomment */ - [Fact] - public void SaveNullDefaultSettings_Test() - { - LoginAsHostAdmin(); - try - { - using (var uow = _unitOfWorkManager.Begin()) - { - var nhuow = uow as NhUnitOfWork; - var session = nhuow?.GetSession(); - - var testSetting = _settingManager.GetSettingValue("TestSetting"); - testSetting.ShouldNotBeNull(); - var testSetting2 = _settingManager.GetSettingValue("TestSetting2"); - testSetting2.ShouldNotBeNull(); - - _settingManager.ChangeSettingForApplication("TestSetting", "2"); - _settingManager.ChangeSettingForApplication("TestSetting2", "b"); - - /*var testSetting2 = _settingManager.GetSettingValue("NullTestSetting.Round1"); - testSetting2.ShouldBeNull();*/ - - _settingManager.ChangeSettingForApplication("TestSetting.Boxfusion.Shesha.GDE.LowestPriorityApplicationTypeToProcessOffersFor.Round1", 9.ToString()); - session.Flush(); - } - } - finally - { - using (var uow = _unitOfWorkManager.Begin()) - { - var nhuow = uow as NhUnitOfWork; - var session = nhuow?.GetSession(); - - // delete temporary values - var entityChangeSetId = - session.CreateSQLQuery("select EntityChangeSetId from AbpEntityChanges where EntityId like '%TestSetting%'") - .List(); - - session.CreateSQLQuery("delete from AbpEntityPropertyChanges where EntityChangeId in (select id from AbpEntityChanges where EntityId like '%TestSetting%')") - .ExecuteUpdate(); - session.CreateSQLQuery("delete from AbpEntityChanges where EntityId like '%TestSetting%'") - .ExecuteUpdate(); - foreach (var id in entityChangeSetId) - { - session.CreateSQLQuery($"delete from AbpEntityChangeSets where id = {id}") - .ExecuteUpdate(); - } - session.CreateSQLQuery($"delete from AbpSettings where Name like '%TestSetting%'") - .ExecuteUpdate(); - - session.Flush(); - } - } - } - - } -} diff --git a/shesha-core/test/Shesha.Tests/Settings/SettingsCache_Tests.cs b/shesha-core/test/Shesha.Tests/Settings/SettingsCache_Tests.cs new file mode 100644 index 0000000000..fb2f23427f --- /dev/null +++ b/shesha-core/test/Shesha.Tests/Settings/SettingsCache_Tests.cs @@ -0,0 +1,87 @@ +using Abp.Domain.Repositories; +using Moq; +using Shesha.Configuration; +using Shesha.Domain; +using Shesha.Domain.ConfigurationItems; +using Shesha.Reflection; +using Shesha.Services.Settings; +using Shesha.Services.Settings.Cache; +using Shesha.Settings; +using Shesha.Tests.Fixtures; +using Shesha.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Shesha.Tests.Settings +{ + [Collection(SqlServerCollection.Name)] + public class SettingsCache_Tests : SheshaNhTestBase + { + public SettingsCache_Tests(SqlServerFixture fixture) : base(fixture) + { + } + + [Fact] + public async Task GetCachedValueTest_Async() + { + var settingName = SheshaSettingNames.UploadFolder; + var settingModule = typeof(ISheshaSettings).GetConfigurableModuleName().NotNull(); + + var module = new Module { Name = settingModule }; + var testConfiguration = new SettingConfiguration { + Id = "268A7384-72F5-4CCF-A411-BFCF5E5DDAD7".ToGuid(), + Module = module, + Name = settingName, + VersionStatus = ConfigurationItemVersionStatus.Live, + }; + testConfiguration.SetIsLast(true); + + var testValue = new SettingValue { + Id = "DCCFCC16-4EAA-4C1C-855D-6AE29A7F5286".ToGuid(), + SettingConfiguration = testConfiguration, + Value = "Test Value", + }; + + var mockValueRepo = new Mock>(); + mockValueRepo.Setup(repo => repo.GetAll()).Returns(() => { + return new List { testValue }.AsQueryable(); + }); + var mockConfigRepo = new Mock>(); + mockConfigRepo.Setup(repo => repo.GetAll()).Returns(() => { + return new List { testConfiguration }.AsQueryable(); + }); + var mockModuleRepo = new Mock>(); + mockModuleRepo.Setup(repo => repo.GetAll()).Returns(() => { + return new List { module }.AsQueryable(); + }); + + var cacheHolder = Resolve(); + await cacheHolder.Cache.ClearAsync(); + + var manager = Resolve(new { + repository = mockConfigRepo.Object, + settingValueRepository = mockValueRepo.Object, + moduleRepository = mockModuleRepo.Object, + cacheHolder, + }); + + var definitionManager = Resolve(); + var definition = definitionManager.GetAll().First(d => d.Name == settingName && d.ModuleName == settingModule); + + var value1 = await manager.GetValueAsync(definition, new SettingManagementContext()); + + Assert.Equal(value1, testValue.Value); + mockValueRepo.Verify(repo => repo.GetAll(), Times.Once); + + var value2 = await manager.GetValueAsync(definition, new SettingManagementContext()); + var value3 = await manager.GetValueAsync(definition, new SettingManagementContext()); + + Assert.Equal(value1, value2); + Assert.Equal(value2, value3); + mockValueRepo.Verify(repo => repo.GetAll(), Times.Once); + } + } +} diff --git a/shesha-core/test/Shesha.Tests/Settings/TestSettingsProvider.cs b/shesha-core/test/Shesha.Tests/Settings/TestSettingsProvider.cs deleted file mode 100644 index 1d067d5471..0000000000 --- a/shesha-core/test/Shesha.Tests/Settings/TestSettingsProvider.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using Abp.Configuration; -using Abp.Localization; -using Shesha.Configuration; - -namespace Shesha.Tests.DynamicEntities -{ - public class TestSettingsProvider : SettingProvider - { - public override IEnumerable GetSettingDefinitions(SettingDefinitionProviderContext context) - { - return new[] - { - new SettingDefinition("TestSetting", "1" ), - new SettingDefinition("TestSetting2", "a" ), - new SettingDefinition("TestSetting.Boxfusion.Shesha.GDE.LowestPriorityApplicationTypeToProcessOffersFor.Round1", - null, - new FixedLocalizableString("NullTestSetting"), - scopes: SettingScopes.Application | SettingScopes.Tenant, - isInherited: false ) - }; - } - } -} \ No newline at end of file diff --git a/shesha-core/test/Shesha.Tests/ShaIntegratedTestBase.cs b/shesha-core/test/Shesha.Tests/ShaIntegratedTestBase.cs new file mode 100644 index 0000000000..e036b5f020 --- /dev/null +++ b/shesha-core/test/Shesha.Tests/ShaIntegratedTestBase.cs @@ -0,0 +1,216 @@ +using Abp; +using Abp.Dependency; +using Abp.Domain.Uow; +using Abp.Modules; +using Abp.Runtime.Session; +using Abp.TestBase.Runtime.Session; +using Castle.MicroKernel.Registration; +using Shesha.Services; +using Shesha.Tests.Fixtures; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace Shesha.Tests +{ + /// + /// This is the base class for all tests integrated to ABP. + /// + public abstract class ShaIntegratedTestBase : IDisposable + where TStartupModule : AbpModule + { + private readonly IIocManager? _ownIocManager; + private readonly IIocManager? _externalIocManager; + + /// + /// Local used for this test. + /// + protected IIocManager LocalIocManager => _externalIocManager ?? _ownIocManager ?? throw new Exception($"Failed to get IocManager. Looks like `{this.GetType().FullName}` has been initialized by a wrong way."); + + protected AbpBootstrapper AbpBootstrapper { get; } + + /// + /// Gets Session object. Can be used to change current user and tenant in tests. + /// + protected TestAbpSession AbpSession { get; private set; } + + protected ShaIntegratedTestBase(IDatabaseFixture databaseFixture, bool initializeAbp = true, IIocManager? localIocManager = null) + { + _externalIocManager = localIocManager; + if (localIocManager == null) + _ownIocManager = new IocManager(); + StaticContext.SetIocManager(LocalIocManager); + + LocalIocManager.IocContainer.Register( + Component.For().Instance(databaseFixture) + ); + + AbpBootstrapper = AbpBootstrapper.Create(options => + { + options.IocManager = LocalIocManager; + }); + + if (initializeAbp) + { + InitializeAbp(); + } + } + + protected void InitializeAbp() + { + LocalIocManager.RegisterIfNot(); + + PreInitialize(); + + AbpBootstrapper.Initialize(); + + PostInitialize(); + + AbpSession = LocalIocManager.Resolve(); + } + + /// + /// This method can be overrided to replace some services with fakes. + /// + protected virtual void PreInitialize() + { + + } + + /// + /// This method can be overrided to replace some services with fakes. + /// + protected virtual void PostInitialize() + { + + } + + public virtual void Dispose() + { + AbpBootstrapper.Dispose(); + LocalIocManager.Dispose(); + } + + /// + /// A shortcut to resolve an object from . + /// Also registers as transient if it's not registered before. + /// + /// Type of the object to get + /// The object instance + protected T Resolve() + { + EnsureClassRegistered(typeof(T)); + return LocalIocManager.Resolve(); + } + + /// + /// A shortcut to resolve an object from . + /// Also registers as transient if it's not registered before. + /// + /// Type of the object to get + /// Constructor arguments + /// The object instance + protected T Resolve(object argumentsAsAnonymousType) + { + EnsureClassRegistered(typeof(T)); + return LocalIocManager.Resolve(argumentsAsAnonymousType); + } + + /// + /// A shortcut to resolve an object from . + /// Also registers as transient if it's not registered before. + /// + /// Type of the object to get + /// The object instance + protected object Resolve(Type type) + { + EnsureClassRegistered(type); + return LocalIocManager.Resolve(type); + } + + /// + /// A shortcut to resolve an object from . + /// Also registers as transient if it's not registered before. + /// + /// Type of the object to get + /// Constructor arguments + /// The object instance + protected object Resolve(Type type, object argumentsAsAnonymousType) + { + EnsureClassRegistered(type); + return LocalIocManager.Resolve(type, argumentsAsAnonymousType); + } + + /// + /// Registers given type if it's not registered before. + /// + /// Type to check and register + /// Lifestyle + protected void EnsureClassRegistered(Type type, DependencyLifeStyle lifeStyle = DependencyLifeStyle.Transient) + { + if (!LocalIocManager.IsRegistered(type)) + { + if (!type.GetTypeInfo().IsClass || type.GetTypeInfo().IsAbstract) + { + throw new AbpException("Can not register " + type.Name + ". It should be a non-abstract class. If not, it should be registered before."); + } + + LocalIocManager.Register(type, lifeStyle); + } + } + + protected virtual void WithUnitOfWork(Action action, UnitOfWorkOptions? options = null) + { + using (var uowManager = LocalIocManager.ResolveAsDisposable()) + { + using (var uow = uowManager.Object.Begin(options ?? new UnitOfWorkOptions())) + { + action(); + uow.Complete(); + } + } + } + + protected virtual void WithUnitOfWork(int? tenantId, Action action, UnitOfWorkOptions? options = null) + { + using (var uowManager = LocalIocManager.ResolveAsDisposable()) + { + using (var uow = uowManager.Object.Begin(options ?? new UnitOfWorkOptions())) + { + using (uowManager.Object.Current.SetTenantId(tenantId)) + { + action(); + uow.Complete(); + } + } + } + } + + protected virtual async Task WithUnitOfWorkAsync(Func action, UnitOfWorkOptions? options = null) + { + using (var uowManager = LocalIocManager.ResolveAsDisposable()) + { + using (var uow = uowManager.Object.Begin(options ?? new UnitOfWorkOptions())) + { + await action(); + await uow.CompleteAsync(); + } + } + } + + protected async Task WithUnitOfWorkAsync(int? tenantId, Func action, UnitOfWorkOptions? options = null) + { + using (var uowManager = LocalIocManager.ResolveAsDisposable()) + { + using (var uow = uowManager.Object.Begin(options ?? new UnitOfWorkOptions())) + { + using (uowManager.Object.Current.SetTenantId(tenantId)) + { + await action(); + await uow.CompleteAsync(); + } + } + } + } + } +} diff --git a/shesha-core/test/Shesha.Tests/Shesha.Tests.csproj b/shesha-core/test/Shesha.Tests/Shesha.Tests.csproj index 5567a1d2b7..edf429aeb1 100644 --- a/shesha-core/test/Shesha.Tests/Shesha.Tests.csproj +++ b/shesha-core/test/Shesha.Tests/Shesha.Tests.csproj @@ -21,13 +21,9 @@ - - - - @@ -39,11 +35,11 @@ - + - - - + + + @@ -57,10 +53,18 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -68,9 +72,12 @@ - - - + + + + + + all runtime; build; native; contentfiles; analyzers @@ -81,6 +88,9 @@ Always + + Always + diff --git a/shesha-core/test/Shesha.Tests/SheshaNhTestBase.cs b/shesha-core/test/Shesha.Tests/SheshaNhTestBase.cs index fae1b33e08..c5df2a4b5f 100644 --- a/shesha-core/test/Shesha.Tests/SheshaNhTestBase.cs +++ b/shesha-core/test/Shesha.Tests/SheshaNhTestBase.cs @@ -1,28 +1,29 @@ using Abp; using Abp.Authorization.Users; +using Abp.Dependency; +using Abp.Domain.Uow; using Abp.Modules; using Abp.MultiTenancy; using Abp.Runtime.Session; -using Abp.TestBase; using NHibernate; using NHibernate.Linq; using Shesha.Authorization.Users; using Shesha.Domain; using Shesha.MultiTenancy; -using Shesha.Services; +using Shesha.NHibernate.UoW; +using Shesha.Tests.Fixtures; using System; using System.Linq; using System.Threading.Tasks; namespace Shesha.Tests { - public abstract class SheshaNhTestBase : AbpIntegratedTestBase where TStartupModule : AbpModule + public abstract class SheshaNhTestBase : ShaIntegratedTestBase where TStartupModule : AbpModule { - protected SheshaNhTestBase(): base() + protected SheshaNhTestBase(IDatabaseFixture fixture) : base(fixture) { LoginAsHostAdmin(); - StaticContext.SetIocManager(LocalIocManager); EntityHelper.RefreshStore(LocalIocManager); } @@ -188,10 +189,45 @@ protected async Task GetCurrentTenantAsync() var tenantId = AbpSession.GetTenantId(); return await UsingDbSession(session => session.Query().SingleAsync(t => t.Id == tenantId)); } + + protected void UsingNhSession(Action action) + { + using (var uow = NewNhUnitOfWork()) + { +#pragma warning disable IDISP001 // Dispose created + var session = uow.GetSession(); +#pragma warning restore IDISP001 // Dispose created + action.Invoke(session); + uow.Complete(); + } + } + + protected NhUnitOfWork NewNhUnitOfWork() + { + var unitOfWorkManager = Resolve(); + return unitOfWorkManager.Begin() is NhUnitOfWork nhuow + ? nhuow + : throw new Exception($"Unexpected type of UnitOfWork. Expected '{nameof(NhUnitOfWork)}'"); + } + + protected virtual async Task WithUnitOfWorkAsync(Func> action, UnitOfWorkOptions? options = null) + { + using (var uowManager = LocalIocManager.ResolveAsDisposable()) + { + using (var uow = uowManager.Object.Begin(options ?? new UnitOfWorkOptions())) + { + var result = await action(); + await uow.CompleteAsync(); + return result; + } + } + } } public abstract class SheshaNhTestBase : SheshaNhTestBase - { - + { + protected SheshaNhTestBase(IDatabaseFixture fixture) : base(fixture) + { + } } } diff --git a/shesha-core/test/Shesha.Tests/SheshaTestModule.cs b/shesha-core/test/Shesha.Tests/SheshaTestModule.cs index 97e964db1e..5c43d8fa88 100644 --- a/shesha-core/test/Shesha.Tests/SheshaTestModule.cs +++ b/shesha-core/test/Shesha.Tests/SheshaTestModule.cs @@ -12,18 +12,14 @@ using Abp.Zero.Configuration; using Castle.Facilities.Logging; using Castle.MicroKernel.Registration; -using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Configuration; -using Moq; using Shesha.Configuration.Startup; using Shesha.FluentMigrator; using Shesha.NHibernate; using Shesha.Services; using Shesha.Tests.DependencyInjection; -using Shesha.Tests.DynamicEntities; -using Shesha.Tests.Interceptors; +using Shesha.Tests.Fixtures; using Shesha.Web.FormsDesigner; using System; using System.Collections.Generic; @@ -53,11 +49,20 @@ public override void PreInitialize() { IocManager.MockWebHostEnvirtonment(); - var config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - var connectionString = config.GetConnectionString("TestDB"); - var nhConfig = Configuration.Modules.ShaNHibernate(); - nhConfig.UseMsSql(connectionString); + + var dbFixture = IocManager.IsRegistered() + ? IocManager.Resolve() + : null; + if (dbFixture != null /*&& false*/) + { + nhConfig.UseDbms(c => dbFixture.DbmsType, c => dbFixture.ConnectionString); + } + else + { + var config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + nhConfig.UseDbms(c => config.GetDbmsType(), c => config.GetRequiredConnectionString("TestDB")); + } Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30); Configuration.UnitOfWork.IsTransactional = false; @@ -70,7 +75,7 @@ public override void PreInitialize() // mock IWebHostEnvironment //var hostingEnvironment = Mock.Of(e => e.ApplicationName == "test"); - var inMemorySettings = new Dictionary { + var inMemorySettings = new Dictionary { /* in memory settings: {"TopLevelKey", "TopLevelValue"}, {"SectionName:SomeKey", "SectionValue"}, @@ -104,21 +109,14 @@ public override void PreInitialize() Configuration.ReplaceService(DependencyLifeStyle.Singleton); - Configuration.Settings.Providers.Add(); - Configuration.EntityHistory.Selectors.Add("Settings", typeof(Setting)); if (!IocManager.IsRegistered()) IocManager.IocContainer.Register(Component.For().ImplementedBy()); - - StaticContext.SetIocManager(IocManager); } public override void Initialize() { - IocManager.Register(typeof(AbpAsyncDeterminationInterceptor), DependencyLifeStyle.Transient); - IocManager.IocContainer.Kernel.ComponentRegistered += ShurikInterceptor.Configure; - var thisAssembly = Assembly.GetExecutingAssembly(); IocManager.RegisterAssemblyByConvention(thisAssembly); diff --git a/shesha-core/test/Shesha.Tests/Specifications/Specifications_Tests.cs b/shesha-core/test/Shesha.Tests/Specifications/Specifications_Tests.cs index 8447c0dc81..52b098e22b 100644 --- a/shesha-core/test/Shesha.Tests/Specifications/Specifications_Tests.cs +++ b/shesha-core/test/Shesha.Tests/Specifications/Specifications_Tests.cs @@ -1,5 +1,4 @@ using Abp.Domain.Repositories; -using Abp.TestBase; using Moq; using NHibernate; using Shesha.Domain; @@ -7,6 +6,7 @@ using Shesha.NHibernate; using Shesha.NHibernate.Repositories; using Shesha.Specifications; +using Shesha.Tests.Fixtures; using Shouldly; using System; using System.Collections.Generic; @@ -16,8 +16,13 @@ namespace Shesha.Tests.Specifications { - public class Specifications_Tests: AbpIntegratedTestBase + [Collection(SqlServerCollection.Name)] + public class Specifications_Tests : SheshaNhTestBase { + public Specifications_Tests(SqlServerFixture fixture) : base(fixture) + { + } + [Fact] public async Task Test_NestedContexts_Async() { @@ -85,7 +90,7 @@ public async Task Test_NestedContexts_Fetch_Async() await Task.Delay(1); - var allPersonsCount = await repository.CountAsync(); + var allPersonsCount = await repository.GetAll().CountAsync(); allPersonsCount.ShouldBe(expectedAllPersonsCount); using (specAccessor.Use()) @@ -103,7 +108,7 @@ public async Task Test_NestedContexts_Fetch_Async() specAccessor.SpecificationTypes.ShouldBe(new[] { typeof(Age18PlusSpecification), typeof(HasNoAccountSpecification) }, ignoreOrder: true); - var persons18PlusWithoutAccount = repository.GetAll().ToList(); + var persons18PlusWithoutAccount = await repository.GetAll().ToListAsync(); persons18PlusWithoutAccount.ShouldBe(expectedPersons18PlusWithoutAccount, ignoreOrder: true); } @@ -111,7 +116,7 @@ public async Task Test_NestedContexts_Fetch_Async() specAccessor.SpecificationTypes.ShouldBe(new[] { typeof(Age18PlusSpecification) }, ignoreOrder: true); - var persons18Plus2 = repository.GetAll().ToList(); + var persons18Plus2 = await repository.GetAll().ToListAsync(); persons18Plus2.ShouldBe(expectedPersons18Plus, ignoreOrder: true); } @@ -119,11 +124,11 @@ public async Task Test_NestedContexts_Fetch_Async() specAccessor.SpecificationTypes.ShouldBeEmpty(); - var allPersonsCount2 = repository.GetAll().Count(); + var allPersonsCount2 = await repository.GetAll().CountAsync(); allPersonsCount2.ShouldBe(expectedAllPersonsCount); } - private Person TestPerson(string firstName, string lastName, DateTime? dob, string username) + private Person TestPerson(string firstName, string lastName, DateTime? dob, string? username) { return new Person { Id = Guid.NewGuid(), diff --git a/shesha-core/test/Shesha.Tests/UnitTestHelper.cs b/shesha-core/test/Shesha.Tests/UnitTestHelper.cs index 3cd998a797..f19375a580 100644 --- a/shesha-core/test/Shesha.Tests/UnitTestHelper.cs +++ b/shesha-core/test/Shesha.Tests/UnitTestHelper.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Moq; using NSubstitute; +using Shesha.Reflection; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; using System.Collections.Generic; @@ -26,7 +27,7 @@ public static void MockWebHostEnvirtonment(this IIocManager iocManager) hostingEnvironment.Setup(x => x.ApplicationName).Returns("test"); hostingEnvironment.Setup(x => x.EnvironmentName).Returns("test"); - var rootPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var rootPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location).NotNullOrWhiteSpace(); hostingEnvironment.Setup(x => x.ContentRootPath).Returns(rootPath); var hh = hostingEnvironment.As().Object; diff --git a/shesha-core/test/Shesha.Tests/Users/UserAppService_Tests.cs b/shesha-core/test/Shesha.Tests/Users/UserAppService_Tests.cs index b85e56626f..d5d392ee8a 100644 --- a/shesha-core/test/Shesha.Tests/Users/UserAppService_Tests.cs +++ b/shesha-core/test/Shesha.Tests/Users/UserAppService_Tests.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Abp.Domain.Repositories; +using Abp.Domain.Repositories; using Abp.Domain.Uow; +using Abp.Net.Mail; using Castle.MicroKernel.Registration; -using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using Moq; using NHibernate.Linq; @@ -16,19 +12,25 @@ using Shesha.Models.TokenAuth; using Shesha.Otp; using Shesha.Otp.Dto; +using Shesha.Tests.Fixtures; using Shesha.Users; using Shesha.Users.Dto; using Shouldly; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; using Xunit; namespace Shesha.Tests.Users { - public class DomainModel_Tests : SheshaNhTestBase + [Collection(SqlServerCollection.Name)] + public class UserAppService_Tests : SheshaNhTestBase { private readonly IRepository _userRepository; private readonly IUnitOfWorkManager _unitOfWorkManager; - public DomainModel_Tests() + public UserAppService_Tests(SqlServerFixture fixture) : base(fixture) { _userRepository = Resolve>(); _unitOfWorkManager = Resolve(); @@ -59,7 +61,7 @@ public async Task ResetPasswordUsingOtp_TestAsync() storage.Add(dto.OperationId, dto.Pin); return Task.CompletedTask; }); - otpStorage.Setup(s => s.GetAsync(It.IsAny())).Returns(id => Task.FromResult(new OtpDto + otpStorage.Setup(s => s.GetOrNullAsync(It.IsAny())).Returns(id => Task.FromResult(new OtpDto { Pin = storage[id], OperationId = id, @@ -151,6 +153,9 @@ public async Task ResetPasswordUsingEmail_TestAsync() #region OTP hack + var emailSenderMock = new Mock(); + LocalIocManager.IocContainer.Register(Component.For().Instance(emailSenderMock.Object).IsDefault()); + var storage = new Dictionary(); var otpStorage = new Mock(); otpStorage.Setup(s => s.SaveAsync(It.IsAny())).Returns(dto => @@ -159,7 +164,7 @@ public async Task ResetPasswordUsingEmail_TestAsync() storage.Add(dto.OperationId, dto.Pin); return Task.CompletedTask; }); - otpStorage.Setup(s => s.GetAsync(It.IsAny())).Returns(id => Task.FromResult(new OtpDto + otpStorage.Setup(s => s.GetOrNullAsync(It.IsAny())).Returns(id => Task.FromResult(new OtpDto { Pin = storage[id], OperationId = id, @@ -171,7 +176,7 @@ public async Task ResetPasswordUsingEmail_TestAsync() #endregion // get IUserAppService with hacked otp storage - var userAppService = LocalIocManager.Resolve(); + var userAppService = LocalIocManager.Resolve(); // create new user @@ -192,7 +197,9 @@ public async Task ResetPasswordUsingEmail_TestAsync() firstLoginAttempt.ShouldBeTrue("Failed to login as a new user"); // send OTP for password reset - var response = await userAppService.SendEmailLinkAsync(userName); + var response = await WithUnitOfWorkAsync(async() => { + return await userAppService.SendEmailLinkAsync(userName); + }); response.ShouldBeTrue(); var token = ""; @@ -207,7 +214,7 @@ public async Task ResetPasswordUsingEmail_TestAsync() var verifyResponse = await userAppService.ValidateResetCodeAsync(new ResetPasswordValidateCodeInput() { Code = currentPin, - Username = userName, + Username = Convert.ToBase64String(Encoding.UTF8.GetBytes(userName)), Method = (long)RefListPasswordResetMethods.EmailLink }); @@ -217,13 +224,12 @@ public async Task ResetPasswordUsingEmail_TestAsync() } // change password - var resetPasswordResponse = - await userAppService.ResetPasswordUsingTokenAsync(new ResetPasswordUsingTokenInput() - { - Username = userName, - Token = token, - NewPassword = newPassword - }); + var resetPasswordResponse = await WithUnitOfWorkAsync(async() => await userAppService.ResetPasswordUsingTokenAsync(new ResetPasswordUsingTokenInput() + { + Username = userName, + Token = token, + NewPassword = newPassword + })); // try to login using old password var failedAttempt = await ValidateCredentialsAsync(userName, oldPassword); @@ -313,7 +319,7 @@ private void ConfigureTokenAuth() LocalIocManager.Register(); var tokenAuthConfig = LocalIocManager.Resolve(); - tokenAuthConfig.SecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("SheshaTest_C421AAEE0D114E9C")); + tokenAuthConfig.SecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("ZQJK9FR81M9NDCKX161B2L9RXDBHL7GS")); tokenAuthConfig.Issuer = "SheshaTest"; tokenAuthConfig.Audience = "SheshaTest"; tokenAuthConfig.SigningCredentials = new SigningCredentials(tokenAuthConfig.SecurityKey, SecurityAlgorithms.HmacSha256); diff --git a/shesha-core/test/Shesha.Tests/VersionedFields/VersionedField_Tests.cs b/shesha-core/test/Shesha.Tests/VersionedFields/VersionedField_Tests.cs index 5dd12e2035..a67419cfe6 100644 --- a/shesha-core/test/Shesha.Tests/VersionedFields/VersionedField_Tests.cs +++ b/shesha-core/test/Shesha.Tests/VersionedFields/VersionedField_Tests.cs @@ -2,17 +2,20 @@ using Abp.Domain.Uow; using Shesha.Domain; using Shesha.Services.VersionedFields; +using Shesha.Tests.Fixtures; +using Shouldly; using System; using System.Threading.Tasks; using Xunit; namespace Shesha.Tests.VersionedFields { + [Collection(SqlServerCollection.Name)] public class VersionedField_Tests: SheshaNhTestBase { private readonly IUnitOfWorkManager _unitOfWorkManager; - public VersionedField_Tests() + public VersionedField_Tests(SqlServerFixture fixture) : base(fixture) { _unitOfWorkManager = Resolve(); } @@ -20,38 +23,34 @@ public VersionedField_Tests() [Fact] public async Task ReadWrite_TestAsync() { - try + LoginAsHostAdmin(); + + using (var uow = _unitOfWorkManager.Begin()) { - LoginAsHostAdmin(); + var areaRepo = LocalIocManager.Resolve>(); - using (var uow = _unitOfWorkManager.Begin()) + // create temp area + var area = new Area() { - var areaRepo = LocalIocManager.Resolve>(); - - // create temp area - var area = new Area() - { - Name = "Test Area", - }; - await areaRepo.InsertAsync(area); + Name = "Test Area", + }; + await areaRepo.InsertAsync(area); - var manager = Resolve(); - var fieldName = "DynamicField"; - - var fieldValue = await manager.GetVersionedFieldValueAsync(area, fieldName); + var manager = Resolve(); + var fieldName = "DynamicField"; - await manager.SetVersionedFieldValueAsync(area, fieldName, "test value", true); + var fieldValue = await manager.GetVersionedFieldValueAsync(area, fieldName); + fieldValue.ShouldBeNull(); + + const string testValue = "test value"; + await manager.SetVersionedFieldValueAsync(area, fieldName, testValue, true); - var fieldValue2 = await manager.GetVersionedFieldValueAsync(area, fieldName); + var fieldValue2 = await manager.GetVersionedFieldValueAsync(area, fieldName); + fieldValue2.ShouldBe(testValue); - // delete temp area - await areaRepo.HardDeleteAsync(area); - await uow.CompleteAsync(); - } - } - catch (Exception) - { - throw; + // delete temp area + await areaRepo.HardDeleteAsync(area); + await uow.CompleteAsync(); } } } diff --git a/shesha-core/test/Shesha.Tests/log4net.config b/shesha-core/test/Shesha.Tests/log4net.config index ef8b779b7b..9ec528ead6 100644 --- a/shesha-core/test/Shesha.Tests/log4net.config +++ b/shesha-core/test/Shesha.Tests/log4net.config @@ -31,7 +31,7 @@ - + diff --git a/shesha-core/test/Shesha.Tests/test-data/package-import.zip b/shesha-core/test/Shesha.Tests/test-data/package-import.zip new file mode 100644 index 0000000000..b34997c55d Binary files /dev/null and b/shesha-core/test/Shesha.Tests/test-data/package-import.zip differ diff --git a/shesha-functional-tests/adminportal/package-lock.json b/shesha-functional-tests/adminportal/package-lock.json index e6be655c22..4ab3a094ba 100644 --- a/shesha-functional-tests/adminportal/package-lock.json +++ b/shesha-functional-tests/adminportal/package-lock.json @@ -27,9 +27,7 @@ "htmlescape": "^1.1.1", "moment": "^2.30.1", "nanoid": "^3.1.23", - "next": "^14.2.4", - "next-nprogress-bar": "^2.3.12", - "nprogress": "^0.2.0", + "next": "^14.2.24", "prettier": "^2.2.1", "qs": "^6.10.1", "rc-picker": "4.5.0", @@ -698,6 +696,37 @@ "dev": true, "license": "MIT" }, + "node_modules/@bprogress/core": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@bprogress/core/-/core-1.3.3.tgz", + "integrity": "sha512-Vz9vzZE8xvfkgY09e0S6kzNz/ev/2lUw7A/z0TW7N0ZBG9xmVXrzSsFe3uvypxDa5ltn+xLBtfDtxBc8KsrvBQ==" + }, + "node_modules/@bprogress/next": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@bprogress/next/-/next-3.2.10.tgz", + "integrity": "sha512-uHu7risLFdXgyJzg1kL5Q+g8YGw9RBMqcyLWuDq7LGk95Ok/gWXMXg2pcSzpQ9pGxNQpq3LRzm67a8RFH2RpPQ==", + "dependencies": { + "@bprogress/core": "^1.3.3", + "@bprogress/react": "^1.2.4" + }, + "peerDependencies": { + "next": ">=13.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@bprogress/react": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@bprogress/react/-/react-1.2.5.tgz", + "integrity": "sha512-q389gPI3ckUQzKf8GbZiZgM1ErbSw9ynEavuIUunxiQYiPxyNuRisnEFZjbZV+48AZBhbwyU4vf6zBZ2ycGsow==", + "dependencies": { + "@bprogress/core": "^1.3.3" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@ctrl/tinycolor": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", @@ -1877,10 +1906,9 @@ } }, "node_modules/@next/env": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.16.tgz", - "integrity": "sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag==", - "license": "MIT" + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.24.tgz", + "integrity": "sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==" }, "node_modules/@next/eslint-plugin-next": { "version": "14.2.16", @@ -1893,13 +1921,12 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.16.tgz", - "integrity": "sha512-uFT34QojYkf0+nn6MEZ4gIWQ5aqGF11uIZ1HSxG+cSbj+Mg3+tYm8qXYd3dKN5jqKUm5rBVvf1PBRO/MeQ6rxw==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.24.tgz", + "integrity": "sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1909,13 +1936,12 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.16.tgz", - "integrity": "sha512-mCecsFkYezem0QiZlg2bau3Xul77VxUD38b/auAjohMA22G9KTJneUYMv78vWoCCFkleFAhY1NIvbyjj1ncG9g==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.24.tgz", + "integrity": "sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1925,13 +1951,12 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.16.tgz", - "integrity": "sha512-yhkNA36+ECTC91KSyZcgWgKrYIyDnXZj8PqtJ+c2pMvj45xf7y/HrgI17hLdrcYamLfVt7pBaJUMxADtPaczHA==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.24.tgz", + "integrity": "sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1941,13 +1966,12 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.16.tgz", - "integrity": "sha512-X2YSyu5RMys8R2lA0yLMCOCtqFOoLxrq2YbazFvcPOE4i/isubYjkh+JCpRmqYfEuCVltvlo+oGfj/b5T2pKUA==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.24.tgz", + "integrity": "sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1957,13 +1981,12 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.16.tgz", - "integrity": "sha512-9AGcX7VAkGbc5zTSa+bjQ757tkjr6C/pKS7OK8cX7QEiK6MHIIezBLcQ7gQqbDW2k5yaqba2aDtaBeyyZh1i6Q==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.24.tgz", + "integrity": "sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1973,13 +1996,12 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.16.tgz", - "integrity": "sha512-Klgeagrdun4WWDaOizdbtIIm8khUDQJ/5cRzdpXHfkbY91LxBXeejL4kbZBrpR/nmgRrQvmz4l3OtttNVkz2Sg==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.24.tgz", + "integrity": "sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1989,13 +2011,12 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.16.tgz", - "integrity": "sha512-PwW8A1UC1Y0xIm83G3yFGPiOBftJK4zukTmk7DI1CebyMOoaVpd8aSy7K6GhobzhkjYvqS/QmzcfsWG2Dwizdg==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.24.tgz", + "integrity": "sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -2005,13 +2026,12 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.16.tgz", - "integrity": "sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.24.tgz", + "integrity": "sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==", "cpu": [ "ia32" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -2021,13 +2041,12 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.16.tgz", - "integrity": "sha512-OA7NtfxgirCjfqt+02BqxC3MIgM/JaGjw9tOe4fyZgPsqfseNiMPnCRP44Pfs+Gpo9zPN+SXaFsgP6vk8d571A==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.24.tgz", + "integrity": "sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -2298,10 +2317,10 @@ "node_modules/@shesha-io/reactjs": { "version": "0.34.1-local", "resolved": "file:shesha-local.tgz", - "integrity": "sha512-03oZKdtnuH36qQoWTgMrb/Zv4S8fHyWbpuNFPhbOVUvzgO31p41+441LEmrp+kacZ3222GFXzqQy5f4sKcY1aw==", - "license": "GPL-3.0", + "integrity": "sha512-vNUTesFKnWVxhAaccebkpBxd7hV+hSsPT0riQw6tDgufHjTR1zvvrYW5iwjAEDVczv52hG5lLZu8J/bYP8SbBg==", "dependencies": { "@ant-design/nextjs-registry": "^1.0.1", + "@bprogress/next": "^3.2.10", "@monaco-editor/react": "^4.6.0", "@react-awesome-query-builder/antd": "^6.5.2", "@scarf/scarf": "^1.3.0", @@ -2327,7 +2346,7 @@ "monaco-editor": "^0.50.0", "mustache": "^4.2.0", "nested-property": "^4.0.0", - "next": "^14.2.4", + "next": "^14.2.24", "number-to-words": "^1.2.4", "object-hash": "^3.0.0", "point-in-polygon": "^1.1.0", @@ -9597,12 +9616,11 @@ "license": "MIT" }, "node_modules/next": { - "version": "14.2.16", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.16.tgz", - "integrity": "sha512-LcO7WnFu6lYSvCzZoo1dB+IO0xXz5uEv52HF1IUN0IqVTUIZGHuuR10I5efiLadGt+4oZqTcNZyVVEem/TM5nA==", - "license": "MIT", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.24.tgz", + "integrity": "sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==", "dependencies": { - "@next/env": "14.2.16", + "@next/env": "14.2.24", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -9617,15 +9635,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.16", - "@next/swc-darwin-x64": "14.2.16", - "@next/swc-linux-arm64-gnu": "14.2.16", - "@next/swc-linux-arm64-musl": "14.2.16", - "@next/swc-linux-x64-gnu": "14.2.16", - "@next/swc-linux-x64-musl": "14.2.16", - "@next/swc-win32-arm64-msvc": "14.2.16", - "@next/swc-win32-ia32-msvc": "14.2.16", - "@next/swc-win32-x64-msvc": "14.2.16" + "@next/swc-darwin-arm64": "14.2.24", + "@next/swc-darwin-x64": "14.2.24", + "@next/swc-linux-arm64-gnu": "14.2.24", + "@next/swc-linux-arm64-musl": "14.2.24", + "@next/swc-linux-x64-gnu": "14.2.24", + "@next/swc-linux-x64-musl": "14.2.24", + "@next/swc-win32-arm64-msvc": "14.2.24", + "@next/swc-win32-ia32-msvc": "14.2.24", + "@next/swc-win32-x64-msvc": "14.2.24" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -9646,15 +9664,6 @@ } } }, - "node_modules/next-nprogress-bar": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/next-nprogress-bar/-/next-nprogress-bar-2.3.14.tgz", - "integrity": "sha512-r2zdo5SFakm1CSYLBo2/+9X2F5NRnbV/LI1b/Iu3mFLa9ln+Dlwx3vjJc3kVHI4i42jRC0an++obVHeWkYU6gA==", - "license": "MIT", - "dependencies": { - "nprogress": "^0.2.0" - } - }, "node_modules/nock": { "version": "13.5.5", "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.5.tgz", @@ -9737,12 +9746,6 @@ "node": ">=8" } }, - "node_modules/nprogress": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", - "license": "MIT" - }, "node_modules/number-to-words": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/number-to-words/-/number-to-words-1.2.4.tgz", diff --git a/shesha-functional-tests/adminportal/package.json b/shesha-functional-tests/adminportal/package.json index 2ad5907210..87bb7e5660 100644 --- a/shesha-functional-tests/adminportal/package.json +++ b/shesha-functional-tests/adminportal/package.json @@ -9,8 +9,8 @@ "build-shesha": "echo \"Build Shesha\" && cd ../../shesha-reactjs && npm install && npm run build && npm pack && shx mv shesha-io-reactjs-*.tgz ../shesha-functional-tests/adminportal/shesha-local.tgz", "preinstall": "npm run build-shesha && npm install shesha-local.tgz --force", "export": "next export", - "start": "cross-env NODE_ENV=production node --max-old-space-size=8192 server.js", - "build:analyze": "cross-env ANALYZE=true npm run build", + "start": "NODE_ENV=production node --max-old-space-size=8192 server.js", + "build:analyze": "ANALYZE=true npm run build", "lint": "eslint src/**/*.ts{,x}", "lint-errors": "eslint src/**/*.ts{,x} --quiet", "prettier": "prettier --write **/*.{js,jsx,ts,tsx}", @@ -38,9 +38,7 @@ "htmlescape": "^1.1.1", "moment": "^2.30.1", "nanoid": "^3.1.23", - "next": "^14.2.4", - "next-nprogress-bar": "^2.3.12", - "nprogress": "^0.2.0", + "next": "^14.2.24", "prettier": "^2.2.1", "qs": "^6.10.1", "rc-picker": "4.5.0", diff --git a/shesha-functional-tests/adminportal/shesha-local.tgz b/shesha-functional-tests/adminportal/shesha-local.tgz index 8cea5ebcc2..d7c0cda9a1 100644 Binary files a/shesha-functional-tests/adminportal/shesha-local.tgz and b/shesha-functional-tests/adminportal/shesha-local.tgz differ diff --git a/shesha-functional-tests/adminportal/src/app/app-provider.tsx b/shesha-functional-tests/adminportal/src/app/app-provider.tsx index fab8f3e964..df1e8810de 100644 --- a/shesha-functional-tests/adminportal/src/app/app-provider.tsx +++ b/shesha-functional-tests/adminportal/src/app/app-provider.tsx @@ -8,7 +8,6 @@ import { useNextRouter, } from '@shesha-io/reactjs'; import { OrganisationsActionsProvider } from '@/components/dynamic-list/dynamic-actions'; -import { ProgressBar } from './progressBar'; export interface IAppProviderProps { backendUrl: string; @@ -24,7 +23,6 @@ export const AppProvider: FC> = ({ children router={nextRouter} noAuth={nextRouter.path?.includes('/no-auth')} > - {children} diff --git a/shesha-functional-tests/adminportal/src/app/progressBar.tsx b/shesha-functional-tests/adminportal/src/app/progressBar.tsx deleted file mode 100644 index 12d4af6b23..0000000000 --- a/shesha-functional-tests/adminportal/src/app/progressBar.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useTheme } from 'antd-style'; -import { AppProgressBar } from 'next-nprogress-bar'; -import React, { FC } from 'react'; - -export interface IProgressBarProps { - -} - -export const ProgressBar: FC = () => { - const theme = useTheme(); - return ( - - ); -}; \ No newline at end of file diff --git a/shesha-functional-tests/backend/Directory.Build.props b/shesha-functional-tests/backend/Directory.Build.props new file mode 100644 index 0000000000..4ed4239faf --- /dev/null +++ b/shesha-functional-tests/backend/Directory.Build.props @@ -0,0 +1,7 @@ + + + true + enable + $(NoWarn);CS1591;612;618;809;AsyncFixer01;8618; + + diff --git a/shesha-functional-tests/backend/nuget.config b/shesha-functional-tests/backend/nuget.config new file mode 100644 index 0000000000..305e6f7c08 --- /dev/null +++ b/shesha-functional-tests/backend/nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Boxfusion.SheshaFunctionalTests.Common.Application.csproj b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Boxfusion.SheshaFunctionalTests.Common.Application.csproj index a82bc32646..aba1970c9b 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Boxfusion.SheshaFunctionalTests.Common.Application.csproj +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Boxfusion.SheshaFunctionalTests.Common.Application.csproj @@ -7,7 +7,19 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/ActivateMembershipAppService.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/ActivateMembershipAppService.cs index bb5162fda7..8b68d2f5a4 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/ActivateMembershipAppService.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/ActivateMembershipAppService.cs @@ -1,6 +1,5 @@ using Abp.Domain.Repositories; using Abp.Runtime.Validation; -using Abp.UI; using Boxfusion.SheshaFunctionalTests.Common.Application.Services.Dto; using Boxfusion.SheshaFunctionalTests.Common.Domain.Domain; using Boxfusion.SheshaFunctionalTests.Common.Domain.Domain.Enum; @@ -8,13 +7,8 @@ using NHibernate.Linq; using Shesha; using Shesha.DynamicEntities.Dtos; -using Shesha.Utilities; -using System; -using System.Collections.Generic; +using Shesha.Extensions; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Boxfusion.SheshaFunctionalTests.Common.Application.Services { @@ -36,7 +30,7 @@ public ActivateMembershipAppService(IRepository memberRepo, IRepos /// /// [HttpPut, Route("[action]")] - public async Task> ActivateMembership(Guid memberId) + public async Task> ActivateMembershipAsync(Guid memberId) { var member = await _memberRepo.GetAsync(memberId); var payments = await _membershipPaymentRepo.GetAllListAsync(data => data.Member.Id == memberId); @@ -67,31 +61,26 @@ public async Task> ActivateMembership(Guid memberId) /// /// /// - public async Task>> GetMemberPayments (Guid memberId) + public async Task>> GetMemberPaymentsAsync(Guid memberId) { - var memberPayments = await Task.WhenAll(_membershipPaymentRepo.GetAll().Where(data => data.Member.Id == memberId) - .ToList() - .Select(async m => - { - var result = await MapToDynamicDtoAsync(m); - return result; - })); - return memberPayments.ToList(); + var memberPayments = await _membershipPaymentRepo.GetAll() + .Where(data => data.Member.Id == memberId) + .ToListAsync(); + + var dtos = (await memberPayments.SelectAsync(async m => await MapToDynamicDtoAsync(m))).ToList(); + return dtos; } /// /// /// /// - public async Task>> GetAllMembershipPayments() + public async Task>> GetAllMembershipPaymentsAsync() { - var memberPayments = await Task.WhenAll(_membershipPaymentRepo.GetAll().ToList() - .Select(async m => - { - var result = await MapToDynamicDtoAsync(m); - return result; - })); - return memberPayments.ToList(); + var memberPayments = await _membershipPaymentRepo.GetAll().ToListAsync(); + + var dtos = (await memberPayments.SelectAsync(async m => await MapToDynamicDtoAsync(m))).ToList(); + return dtos; } @@ -99,7 +88,7 @@ public async Task>> GetAllMembershipPay /// /// /// - public async Task> CreateMemberPayment(MembershipPaymentDto input) + public async Task> CreateMemberPaymentAsync(MembershipPaymentDto input) { var memberPayment = ObjectMapper.Map(input); await _membershipPaymentRepo.InsertAsync(memberPayment); diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/BankMemberAppService.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/BankMemberAppService.cs index edadc751b3..27f89dc05d 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/BankMemberAppService.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/BankMemberAppService.cs @@ -30,7 +30,7 @@ public async Task> CreateBankWithMembersAsync (BankMember return await MapToDynamicDtoAsync(bank); } - public async Task GetBankWithMembers (Guid id) + public async Task GetBankWithMembersAsync(Guid id) { var bank = await _bankRepo.GetAsync(id); var bankMembers = new BankMemberDto() @@ -44,7 +44,7 @@ public async Task GetBankWithMembers (Guid id) return ObjectMapper.Map(bankMembers); } - public async Task> GetAllBankWithMembers() + public Task> GetAllBankWithMembersAsync() { var bankMembers = _bankRepo.GetAll().Select(x => new BankMemberDto { @@ -54,7 +54,9 @@ public async Task> GetAllBankWithMembers() Name = x.Name, Members = _memberRepo.GetAll().Where(m => m.Bank.Id == x.Id).Select(n => n.Id).ToList() }).ToList(); - return ObjectMapper.Map>(bankMembers); + var result = ObjectMapper.Map>(bankMembers); + + return Task.FromResult(result); } } } \ No newline at end of file diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/BookAppService.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/BookAppService.cs index 1b23bb0f97..46dc62239f 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/BookAppService.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/BookAppService.cs @@ -1,14 +1,9 @@ -using Abp.Domain.Repositories; -using Abp.Runtime.Validation; +using Abp.Runtime.Validation; using Boxfusion.SheshaFunctionalTests.Common.Domain.Domain; using Shesha; using Shesha.DynamicEntities.Dtos; -using System; -using System.Collections.Generic; +using Shesha.Reflection; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Boxfusion.SheshaFunctionalTests.Common.Application.Services { @@ -24,13 +19,13 @@ public BookAppService() /// /// /// - public async Task> CreateBook(DynamicDto input) + public async Task> CreateBookAsync(DynamicDto input) { var book = await SaveOrUpdateEntityAsync(null, async item => { var validationResults = new List(); - var result = await MapJObjectToEntityAsync(input._jObject, item, validationResults); + var result = await MapJObjectToEntityAsync(input._jObject.NotNull(), item, validationResults); if (!result) throw new AbpValidationException("Please correct the errors and try again", validationResults); }); return await MapToDynamicDtoAsync(book); diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/ChartsTestAppService.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/ChartsTestAppService.cs new file mode 100644 index 0000000000..fd0e010522 --- /dev/null +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/ChartsTestAppService.cs @@ -0,0 +1,31 @@ +using Shesha; + +namespace Boxfusion.SheshaFunctionalTests.Common.Application.Services +{ + public class ChartsTestAppService: SheshaAppServiceBase + { + public ChartsTestAppService() { } + + /// + /// Test endpoint for charts when they have to use URL to request for already prepared data + /// + /// Calculated Chart Data + public object GetChartData() + { + return new + { + labels = new[] { "Boxfire", "Meta Org", "SpaceTech" }, + datasets = new[] + { + new + { + label = "emailAddress (count)", + data = new[] { 9, 1, 1 }, + backgroundColor = new[] { "hsla(250.84687042236328, 70.84687042236328%, 67.84687042236328%, 0.75)", "hsla(53.773223876953125, 83.77322387695312%, 70.77322387695312%, 0.75)", "hsla(227.58676147460938, 77.58676147460938%, 64.58676147460938%, 0.75)" }, + borderColor = "white" + } + } + }; + } + } +} diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Dto/BulkNotificationDto.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Dto/BulkNotificationDto.cs index 1cc4833c0b..2fced3a4d7 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Dto/BulkNotificationDto.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Dto/BulkNotificationDto.cs @@ -6,6 +6,10 @@ namespace Boxfusion.SheshaFunctionalTests.Common.Application.Services.Dto { public class BulkNotificationDto { + public string? SchoolId { get; set; } + /// + /// + /// public EntityDto? Type { get; set; } /// /// diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Dto/NotificationTestDto.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Dto/NotificationTestDto.cs index 9e62e81795..6e2c791099 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Dto/NotificationTestDto.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Dto/NotificationTestDto.cs @@ -6,6 +6,10 @@ namespace Boxfusion.SheshaFunctionalTests.Common.Application.Services.Dto { public class NotificationDto { + public string? SchoolId { get; set; } + /// + /// + /// public EntityDto? Type { get; set; } /// /// @@ -26,6 +30,10 @@ public class NotificationDto /// /// /// + public string? Cc { get; set; } + /// + /// + /// public GenericEntityReference? TriggeringEntity { get; set; } } } diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/DynamicColumnsTest.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/DynamicColumnsTest.cs index 1fa5d4f819..66a414ea2e 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/DynamicColumnsTest.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/DynamicColumnsTest.cs @@ -19,15 +19,15 @@ public DynamicColumnsTest(IRepository memberRepo, IRepository> GetPersonList() + public async Task> GetPersonListAsync() { return await _memberRepo.GetAll().Take(5).Select(x => new PersonSelect { Id = x.Id, Name = x.FirstName }).ToListAsync(); } - public async Task> GetPersonDetails(PropsFilteredPagedAndSortedResultRequestDto input) + public async Task> GetPersonDetailsAsync(PropsFilteredPagedAndSortedResultRequestDto input) { var columns = input.Properties?.Replace(',', ' ').Split(' ') ?? []; diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/NotificationTestAppService.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/NotificationTestAppService.cs index 88f179bf41..4fc795568a 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/NotificationTestAppService.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/NotificationTestAppService.cs @@ -1,11 +1,12 @@ using Abp.Domain.Repositories; using Boxfusion.SheshaFunctionalTests.Common.Application.Services.Dto; +using NHibernate.Linq; using Shesha; using Shesha.Domain; +using Shesha.Extensions; using Shesha.Notifications; using Shesha.Notifications.Dto; using Shesha.Notifications.MessageParticipants; -using Shesha.Services; namespace Boxfusion.SheshaFunctionalTests.Common.Application.Services { @@ -14,16 +15,22 @@ public class NotificationTestAppService: SheshaAppServiceBase private readonly INotificationSender _notificationService; private readonly IRepository _notificationChannelRepository; private readonly IRepository _personRepository; - private readonly IRepository _notificationTypeRepository; - private readonly IStoredFileService _storedFileService; + private readonly IRepository _notificationTypeRepository; + private readonly IRepository _storedFileRepository; - public NotificationTestAppService(INotificationSender notificationService, IRepository notificationChannelRepository, IRepository personRepository, IRepository notificationTypeRepository, IStoredFileService storedFileService) + public NotificationTestAppService(INotificationSender notificationService, + IRepository notificationChannelRepository, + IRepository personRepository, + IRepository notificationTypeRepository, + IRepository storedFileService + + ) { _notificationService = notificationService; _notificationChannelRepository = notificationChannelRepository; _personRepository = personRepository; _notificationTypeRepository = notificationTypeRepository; - _storedFileService = storedFileService; + _storedFileRepository = storedFileService; } public async Task TestNotificationAsync(NotificationDto notification) @@ -46,19 +53,16 @@ public async Task TestNotificationAsync(NotificationDto notification) { Name = recipientPerson?.FullName ?? "Unknown Recipient", Subject = type.Name, - Body = type.Description + Body = type.Description ?? string.Empty }; - // Get attachments only if recipient is provided - var files = recipientPerson != null - ? await _storedFileService.GetAttachmentsAsync(recipientPerson) - : null; + var files = await _storedFileRepository.GetAllIncluding().Where(x => x.Owner.Id == notification.SchoolId).ToListAsync(); - var attachments = files?.Select(x => new NotificationAttachmentDto() + var attachments = files.Select(x => new NotificationAttachmentDto() { FileName = x.FileName, StoredFileId = x.Id, - }).ToList(); + }).ToList(); var senderPerson = await GetCurrentPersonAsync(); if (senderPerson == null) @@ -78,6 +82,7 @@ await _notificationService.SendNotificationAsync( data, notification.Priority, attachments, + notification.Cc, null, channel ); @@ -96,7 +101,16 @@ public async Task BulkPublishAsync(BulkNotificationDto notification) var data = new TestNotificationData() { Name = "Test Name", - }; + }; + + var files = await _storedFileRepository.GetAllIncluding().Where(x => x.Owner.Id == notification.SchoolId).ToListAsync(); + + var attachments = files.Select(x => new NotificationAttachmentDto() + { + FileName = x.FileName, + StoredFileId = x.Id, + }).ToList(); + // Get the current person var senderPerson = await GetCurrentPersonAsync(); if (senderPerson == null) @@ -115,6 +129,7 @@ await _notificationService.SendNotificationAsync( receiver, data, notification.Priority, + attachments, null, null, channel @@ -135,6 +150,7 @@ await _notificationService.SendNotificationAsync( receiver, data, notification.Priority, + attachments, null, null, channel diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Persons/PersonTestAppService.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Persons/PersonTestAppService.cs index e3a8cf507d..3a367ba916 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Persons/PersonTestAppService.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/Persons/PersonTestAppService.cs @@ -6,6 +6,7 @@ using Shesha.Domain; using Shesha.DynamicEntities; using Shesha.DynamicEntities.Dtos; +using Shesha.Reflection; using Shesha.Specifications; using System.ComponentModel.DataAnnotations; @@ -45,7 +46,7 @@ public async Task TestPersonAsync(PersonTestDto input) await MapDynamicDtoToEntityAsync(input, p); var pp = new Person(); var v = new List(); - await MapJObjectToEntityAsync(input._jObject, pp, v); + await MapJObjectToEntityAsync(input._jObject.NotNull(), pp, v); } public override async Task> UpdateAsync([DynamicBinder(UseDtoForEntityReferences = true, UseDynamicDtoProxy = true)] DynamicDto input) diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/PlaygroundAppService.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/PlaygroundAppService.cs index b80cc421ad..0ae0d0012a 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/PlaygroundAppService.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/PlaygroundAppService.cs @@ -1,9 +1,11 @@ using Abp.Domain.Repositories; -using Abp.Localization; +using Abp.Json; +using Boxfusion.SheshaFunctionalTests.Common.Domain.Domain; using Microsoft.AspNetCore.Routing; using Newtonsoft.Json; using Shesha; using Shesha.Domain; +using Shesha.EntityReferences; using Shesha.Extensions; using Shesha.Services; using Shesha.Services.Urls; @@ -12,24 +14,40 @@ namespace Boxfusion.SheshaFunctionalTests.Common.Application.Services { public class PlaygroundAppService: SheshaAppServiceBase { - public async Task TestAuditAsync() + + private readonly IRepository _testClassRepo; + public PlaygroundAppService( + IRepository testClassRepo + ) { - var repoP = IocManager.Resolve>(); - var repoL = IocManager.Resolve>(); + _testClassRepo = testClassRepo; + } - var person = repoP.GetAll().FirstOrDefault(x => x.Id == Guid.Parse("D519B92F-86E9-4F0F-8DF4-00AAE8A43158")); + public class TestDto + { + public string Name { get; set; } + public GenericEntityReference Test { get; set; } + } - var ll = repoL.GetAll().ToList(); - var l = ll.FirstOrDefault(x => !person.PreferredLanguages.Select(z => z.Id).Contains(x.Id)); + public async Task TestGenericEntityReferenceAsync() + { + var test = await _testClassRepo.GetAll().FirstOrDefaultAsync(); + var dto = new TestDto + { + Name = "Test", + Test = test + }; - person.PreferredLanguages.Add(l); + var json = dto.ToJsonString(); + var obj = json.FromJsonString(); - await repoP.InsertOrUpdateAsync(person); + var newRef = obj.Test; + var newTest = (TestClass)obj.Test; - return "Ok"; + return "OK"; } - public async Task TestFileVersionUrl(Guid id) + public async Task TestFileVersionUrlAsync(Guid id) { var repo = IocManager.Resolve>(); var version = await repo.GetAsync(id); @@ -49,7 +67,7 @@ public async Task TestFileVersionUrl(Guid id) } - public async Task TestLinkState() + public Task TestLinkStateAsync() { var linkGeneratorContext = StaticContext.IocManager.Resolve(); if (linkGeneratorContext == null) @@ -57,7 +75,7 @@ public async Task TestLinkState() if (linkGeneratorContext.State == null) throw new Exception("linkGeneratorContext.State is null"); - return JsonConvert.SerializeObject(linkGeneratorContext.State); + return Task.FromResult(JsonConvert.SerializeObject(linkGeneratorContext.State) ?? string.Empty); } } } diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/SchoolAppService.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/SchoolAppService.cs index 87558864f6..5556b91475 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/SchoolAppService.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/Services/SchoolAppService.cs @@ -4,6 +4,7 @@ using Boxfusion.SheshaFunctionalTests.Common.Domain.Domain; using Shesha; using Shesha.DynamicEntities.Dtos; +using Shesha.Reflection; using System.ComponentModel.DataAnnotations; namespace Boxfusion.SheshaFunctionalTests.Common.Application.Services @@ -24,13 +25,13 @@ public SchoolsAppService(IRepository schoolRepo, IRepository /// /// - public async Task> CreateSchoolCustom(DynamicDto input) + public async Task> CreateSchoolCustomAsync(DynamicDto input) { var school = await SaveOrUpdateEntityAsync(null, async item => { var validationResults = new List(); - var result = await MapJObjectToEntityAsync(input._jObject, item, validationResults); + var result = await MapJObjectToEntityAsync(input._jObject.NotNull(), item, validationResults); if (!result) throw new AbpValidationException("Please correct the errors and try again", validationResults); }); return await MapToDynamicDtoAsync(school); @@ -42,13 +43,13 @@ public async Task> CreateSchoolCustom(DynamicDto /// /// - public async Task> UpdateSchoolCustom(DynamicDto input) + public async Task> UpdateSchoolCustomAsync(DynamicDto input) { var school = await SaveOrUpdateEntityAsync(input.Id, async item => { var validationResults = new List(); - var result = await MapJObjectToEntityAsync(input._jObject, item, validationResults); + var result = await MapJObjectToEntityAsync(input._jObject.NotNull(), item, validationResults); if (!result) throw new AbpValidationException("Please correct the errors and try again", validationResults); }); return await MapToDynamicDtoAsync(school); @@ -60,13 +61,13 @@ public async Task> UpdateSchoolCustom(DynamicDto /// /// - public async Task> CreateSubjectCustom(DynamicDto input) + public async Task> CreateSubjectCustomAsync(DynamicDto input) { var subject = await SaveOrUpdateEntityAsync(null, async item => { var validationResults = new List(); - var result = await MapJObjectToEntityAsync(input._jObject, item, validationResults); + var result = await MapJObjectToEntityAsync(input._jObject.NotNull(), item, validationResults); if (!result) throw new AbpValidationException("Please correct the errors and try again", validationResults); }); return await MapToDynamicDtoAsync(subject); @@ -78,19 +79,19 @@ public async Task> CreateSubjectCustom(DynamicDto /// /// - public async Task> UpdateSubjectCustom(DynamicDto input) + public async Task> UpdateSubjectCustomAsync(DynamicDto input) { var subject = await SaveOrUpdateEntityAsync(input.Id, async item => { var validationResults = new List(); - var result = await MapJObjectToEntityAsync(input._jObject, item, validationResults); + var result = await MapJObjectToEntityAsync(input._jObject.NotNull(), item, validationResults); if (!result) throw new AbpValidationException("Please correct the errors and try again", validationResults); }); return await MapToDynamicDtoAsync(subject); } - public async Task GetSchool(Guid id) + public Task GetSchoolAsync(Guid id) { var school = _schoolRepo.GetAll().Where(n => n.Id == id).Select(x => new SchoolDto { @@ -98,10 +99,10 @@ public async Task GetSchool(Guid id) Name = x.Name, ContactNumber = x.ContactNumber, }).FirstOrDefault(); - return ObjectMapper.Map(school); + return Task.FromResult(ObjectMapper.Map(school)); } - public async Task> GetSchoolSubjects(Guid id) + public Task> GetSchoolSubjectsAsync(Guid id) { var school = _subjectRepo.GetAll().Where(n => n.School.Id == id).Select(x => new SubjectDto { @@ -109,7 +110,7 @@ public async Task> GetSchoolSubjects(Guid id) Name = x.Name, Description = x.Description, }).ToList(); - return ObjectMapper.Map>(school); + return Task.FromResult(ObjectMapper.Map>(school)); } } } diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/SheshaFunctionalTestsCommonApplicationModule.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/SheshaFunctionalTestsCommonApplicationModule.cs index a2c05ed2bb..0ab353f5ad 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/SheshaFunctionalTestsCommonApplicationModule.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Common.Application/SheshaFunctionalTestsCommonApplicationModule.cs @@ -1,5 +1,4 @@ -using System.Reflection; -using Abp.AspNetCore; +using Abp.AspNetCore; using Abp.AspNetCore.Configuration; using Abp.AutoMapper; using Abp.Modules; @@ -7,8 +6,8 @@ using Intent.RoslynWeaver.Attributes; using Shesha; using Shesha.Modules; -using Shesha.Startup; using Shesha.Web.FormsDesigner; +using System.Reflection; [assembly: DefaultIntentManaged(Mode.Fully)] [assembly: IntentTemplate("Boxfusion.Modules.Application.Services.AppService", Version = "1.0")] @@ -46,48 +45,31 @@ public override void PreInitialize() { base.PreInitialize(); - Configuration.Modules.ShaApplication().CreateAppServicesForEntities(typeof(SheshaCoreModule).Assembly, "Core"); Configuration.Modules.AbpAspNetCore() .CreateControllersForAppServices( typeof(SheshaCoreModule).GetAssembly() ); - Configuration.Modules.ShaApplication().CreateAppServicesForEntities(typeof(SheshaApplicationModule).Assembly, "App"); Configuration.Modules.AbpAspNetCore() .CreateControllersForAppServices( typeof(SheshaApplicationModule).GetAssembly() ); - Configuration.Modules.ShaApplication().CreateAppServicesForEntities(typeof(SheshaFormsDesignerModule).Assembly, "App"); Configuration.Modules.AbpAspNetCore() .CreateControllersForAppServices( typeof(SheshaFormsDesignerModule).GetAssembly() ); - Configuration.Modules.ShaApplication().CreateAppServicesForEntities(typeof(SheshaFrameworkModule).Assembly, "Shesha"); - Configuration.Modules.AbpAspNetCore() - .CreateControllersForAppServices( - typeof(SheshaFrameworkModule).GetAssembly() - ); - Configuration.Modules.AbpAspNetCore().CreateControllersForAppServices( typeof(SheshaFunctionalTestsCommonApplicationModule).Assembly, moduleName: "SheshaFunctionalTestsCommon", useConventionalHttpVerbs: true); - Configuration.Modules.ShaApplication().CreateAppServicesForEntities( - typeof(SheshaFunctionalTestsCommonModule).Assembly, - moduleName: "SheshaFunctionalTestsCommon"); - Configuration.Modules.AbpAspNetCore() .CreateControllersForAppServices( typeof(SheshaFunctionalTestsCommonModule).GetAssembly() ); - Configuration.Modules.ShaApplication().CreateAppServicesForEntities( - assembly: typeof(SheshaFunctionalTestsCommonApplicationModule).Assembly, - moduleName: "SheshaFunctionalTestsCommon"); - Configuration.Modules.AbpAspNetCore() .CreateControllersForAppServices( typeof(SheshaFunctionalTestsCommonApplicationModule).GetAssembly() diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Authorization/ISheshaFunctionalTestsPermissionChecker.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Authorization/ISheshaFunctionalTestsPermissionChecker.cs index f28d61794e..1e7592e86b 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Authorization/ISheshaFunctionalTestsPermissionChecker.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Authorization/ISheshaFunctionalTestsPermissionChecker.cs @@ -13,76 +13,20 @@ public interface ISheshaFunctionalTestsPermissionChecker /// /// /// - Task IsDataAdministrator(Person person); + Task IsDataAdministratorAsync(Person person); /// /// /// /// /// - Task IsGlobalAdmin(Person person); - - ///// - ///// - ///// - ///// - ///// - //Task IsFacilityAdmin(Person person); - - ///// - ///// - ///// - ///// - ///// - //Task IsCapturer(Person person); - - ///// - ///// - ///// - ///// - ///// - //Task IsViewer(Person person); - - ///// - ///// - ///// - ///// - ///// - //Task IsApproverLevel1(Person person); - - ///// - ///// - ///// - ///// - ///// - //Task IsApproverLevel2(Person person); - - ///// - ///// - ///// - ///// - ///// - //Task IsManager(Person person); + Task IsGlobalAdminAsync(Person person); /// /// /// /// /// - Task IsAdmin(Person person); - - /// - /// - /// - /// - /// - //Task IsScheduleManager(Person person); - - ///// - ///// - ///// - ///// - ///// - //Task IsScheduleFulfiller(Person person); + Task IsAdminAsync(Person person); } } diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Authorization/SheshaFunctionalTestsPermissionChecker.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Authorization/SheshaFunctionalTestsPermissionChecker.cs index 378bfa6aff..dadb4dd456 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Authorization/SheshaFunctionalTestsPermissionChecker.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Authorization/SheshaFunctionalTestsPermissionChecker.cs @@ -1,6 +1,6 @@ using Abp.Domain.Repositories; +using Abp.Threading; using NHibernate.Linq; -using Nito.AsyncEx.Synchronous; using Shesha.Authorization; using Shesha.AutoMapper.Dto; using Shesha.Domain; @@ -37,11 +37,11 @@ public SheshaFunctionalTestsPermissionChecker(IRepository personRe /// public async Task IsGrantedAsync(long userId, string permissionName) { - var person = await _personRepository.GetAll().Where(p => p.User.Id == userId).FirstOrDefaultAsync(); + var person = await _personRepository.GetAll().Where(p => p.User != null && p.User.Id == userId).FirstOrDefaultAsync(); if (person == null) return false; - if (person.User.UserName.ToLower() == "admin") + if (person.User?.UserName.ToLower() == "admin") return true; switch (permissionName) @@ -59,7 +59,7 @@ public async Task IsGrantedAsync(long userId, string permissionName) /// public bool IsGranted(long userId, string permissionName) { - return IsGrantedAsync(userId, permissionName).WaitAndUnwrapException(); + return AsyncHelper.RunSync(async () => await IsGrantedAsync(userId, permissionName)); } /// @@ -68,10 +68,10 @@ public bool IsGranted(long userId, string permissionName) /// /// /// - public async Task IsInAnyOfRoles(Person person, params string[] roles) + public async Task IsInAnyOfRolesAsync(Person person, params string[] roles) { return await _rolePersonRepository.GetAll() - .Where(e => roles.Contains(e.Role.Name) && e.Person == person).AnyAsync(); + .Where(e => e.Role != null && roles.Contains(e.Role.Name) && e.Person == person).AnyAsync(); } /// @@ -79,9 +79,9 @@ public async Task IsInAnyOfRoles(Person person, params string[] roles) /// /// /// - public async Task IsDataAdministrator(Person person) + public async Task IsDataAdministratorAsync(Person person) { - return await IsInAnyOfRoles(person, CommonRoles.DataAdministrator); + return await IsInAnyOfRolesAsync(person, CommonRoles.DataAdministrator); } /// @@ -89,9 +89,9 @@ public async Task IsDataAdministrator(Person person) /// /// /// - public async Task IsAdmin(Person person) + public async Task IsAdminAsync(Person person) { - return await IsInAnyOfRoles(person, CommonRoles.SystemAdministrator); + return await IsInAnyOfRolesAsync(person, CommonRoles.SystemAdministrator); } /// @@ -99,9 +99,9 @@ public async Task IsAdmin(Person person) /// /// /// - public async Task IsGlobalAdmin(Person person) + public async Task IsGlobalAdminAsync(Person person) { - return await IsInAnyOfRoles(person, CommonRoles.GlobalAdmin); + return await IsInAnyOfRolesAsync(person, CommonRoles.GlobalAdmin); } public async Task IsGrantedAsync(long userId, string permissionName, EntityReferenceDto permissionedEntity) @@ -114,4 +114,4 @@ public bool IsGranted(long userId, string permissionName, EntityReferenceDto + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Boxfusion.SheshaFunctionalTests.Web.Core.xml b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Boxfusion.SheshaFunctionalTests.Web.Core.xml index 6113f0f138..5ce48f53fb 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Boxfusion.SheshaFunctionalTests.Web.Core.xml +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Boxfusion.SheshaFunctionalTests.Web.Core.xml @@ -29,21 +29,21 @@ SheshaFunctionalTests Permission checker - + - + - + @@ -76,7 +76,7 @@ - + inheritedDoc @@ -84,21 +84,21 @@ - + - + - + diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Identity/ExternalLoginInfoHelper.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Identity/ExternalLoginInfoHelper.cs deleted file mode 100644 index 500b49e716..0000000000 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/Identity/ExternalLoginInfoHelper.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Abp.Extensions; - -namespace Shesha.Identity -{ - public class ExternalLoginInfoHelper - { - public static (string name, string surname) GetNameAndSurnameFromClaims(List claims) - { - string name = null; - string surname = null; - - var givennameClaim = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName); - if (givennameClaim != null && !givennameClaim.Value.IsNullOrEmpty()) - { - name = givennameClaim.Value; - } - - var surnameClaim = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname); - if (surnameClaim != null && !surnameClaim.Value.IsNullOrEmpty()) - { - surname = surnameClaim.Value; - } - - if (name == null || surname == null) - { - var nameClaim = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name); - if (nameClaim != null) - { - var nameSurName = nameClaim.Value; - if (!nameSurName.IsNullOrEmpty()) - { - var lastSpaceIndex = nameSurName.LastIndexOf(' '); - if (lastSpaceIndex < 1 || lastSpaceIndex > (nameSurName.Length - 2)) - { - name = surname = nameSurName; - } - else - { - name = nameSurName.Substring(0, lastSpaceIndex); - surname = nameSurName.Substring(lastSpaceIndex); - } - } - } - } - - return (name, surname); - } - } -} diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/SheshaFunctionalTestsWebCoreModule.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/SheshaFunctionalTestsWebCoreModule.cs index 2db0f39ba6..c406a4213a 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/SheshaFunctionalTestsWebCoreModule.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Core/SheshaFunctionalTestsWebCoreModule.cs @@ -64,9 +64,9 @@ private void ConfigureTokenAuth() IocManager.Register(); var tokenAuthConfig = IocManager.Resolve(); - tokenAuthConfig.SecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_appConfiguration["Authentication:JwtBearer:SecurityKey"])); - tokenAuthConfig.Issuer = _appConfiguration["Authentication:JwtBearer:Issuer"]; - tokenAuthConfig.Audience = _appConfiguration["Authentication:JwtBearer:Audience"]; + tokenAuthConfig.SecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_appConfiguration.GetRequired("Authentication:JwtBearer:SecurityKey"))); + tokenAuthConfig.Issuer = _appConfiguration.GetRequired("Authentication:JwtBearer:Issuer"); + tokenAuthConfig.Audience = _appConfiguration.GetRequired("Authentication:JwtBearer:Audience"); tokenAuthConfig.SigningCredentials = new SigningCredentials(tokenAuthConfig.SecurityKey, SecurityAlgorithms.HmacSha256); tokenAuthConfig.Expiration = TimeSpan.FromDays(5); } diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Boxfusion.SheshaFunctionalTests.Web.Host.csproj b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Boxfusion.SheshaFunctionalTests.Web.Host.csproj index c9337f831c..e7cda5eda3 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Boxfusion.SheshaFunctionalTests.Web.Host.csproj +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Boxfusion.SheshaFunctionalTests.Web.Host.csproj @@ -51,12 +51,24 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -71,7 +83,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Startup/AuthConfigurer.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Startup/AuthConfigurer.cs index 41ba0445be..a09439445d 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Startup/AuthConfigurer.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Startup/AuthConfigurer.cs @@ -8,6 +8,7 @@ using Microsoft.IdentityModel.Tokens; using Abp.Runtime.Security; using Shesha; +using Shesha.Configuration; namespace Boxfusion.SheshaFunctionalTests.Web.Host.Startup { @@ -15,7 +16,7 @@ public static class AuthConfigurer { public static void Configure(IServiceCollection services, IConfiguration configuration) { - if (bool.Parse(configuration["Authentication:JwtBearer:IsEnabled"])) + if (bool.TryParse(configuration["Authentication:JwtBearer:IsEnabled"], out var isEnabled) && isEnabled) { services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "JwtBearer"; @@ -28,15 +29,15 @@ public static void Configure(IServiceCollection services, IConfiguration configu { // The signing key must match! ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["Authentication:JwtBearer:SecurityKey"])), + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration.GetRequired("Authentication:JwtBearer:SecurityKey"))), // Validate the JWT Issuer (iss) claim ValidateIssuer = true, - ValidIssuer = configuration["Authentication:JwtBearer:Issuer"], + ValidIssuer = configuration.GetRequired("Authentication:JwtBearer:Issuer"), // Validate the JWT Audience (aud) claim ValidateAudience = true, - ValidAudience = configuration["Authentication:JwtBearer:Audience"], + ValidAudience = configuration.GetRequired("Authentication:JwtBearer:Audience"), // Validate the token expiry ValidateLifetime = true, diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Startup/Startup.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Startup/Startup.cs index 4735b02100..e19617c7f2 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Startup/Startup.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Startup/Startup.cs @@ -78,6 +78,7 @@ public IServiceProvider ConfigureServices(IServiceCollection services) options.Filters.AddService(typeof(SpecificationsActionFilter), order: 1); }) .AddApiExplorer() + .AddDataAnnotations() .AddNewtonsoftJson(options => { options.UseCamelCasing(true); diff --git a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Swagger/SwaggerDefaultValues.cs b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Swagger/SwaggerDefaultValues.cs index 7581e6ffb1..3b1730acd9 100644 --- a/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Swagger/SwaggerDefaultValues.cs +++ b/shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/Swagger/SwaggerDefaultValues.cs @@ -1,10 +1,10 @@ -using System; -using System.Linq; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; +using System; +using System.Linq; namespace Boxfusion.SheshaFunctionalTests.Web.Host.Swagger { @@ -28,10 +28,8 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) { parameter.Description ??= description?.ModelMetadata?.Description; - parameter.Required |= description.IsRequired; - } - else - { + if (description != null) + parameter.Required |= description.IsRequired; } } } diff --git a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Boxfusion.SheshaFunctionalTests.Common.Domain.csproj b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Boxfusion.SheshaFunctionalTests.Common.Domain.csproj index 1b11806227..05c9e8323a 100644 --- a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Boxfusion.SheshaFunctionalTests.Common.Domain.csproj +++ b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Boxfusion.SheshaFunctionalTests.Common.Domain.csproj @@ -10,10 +10,22 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Account.cs b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Account.cs index baa86ab203..5b5f71a1f6 100644 --- a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Account.cs +++ b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Account.cs @@ -1,7 +1,7 @@ using Abp.Domain.Entities; using Boxfusion.SheshaFunctionalTests.Common.Domain.Domain.Enum; +using Shesha.Domain; using Shesha.Domain.Attributes; -using Shesha.Web.FormsDesigner.Domain; using System; namespace Boxfusion.SheshaFunctionalTests.Common.Domain.Domain diff --git a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Bus.cs b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Bus.cs index 43fa7c6f11..3c3a8f327d 100644 --- a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Bus.cs +++ b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Bus.cs @@ -1,13 +1,8 @@ using Abp.Auditing; -using Abp.Domain.Entities; using Shesha.Domain; using Shesha.Domain.Attributes; using Shesha.EntityHistory; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Boxfusion.SheshaFunctionalTests.Common.Domain.Domain { diff --git a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Member.cs b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Member.cs index 26b94c377d..2520062868 100644 --- a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Member.cs +++ b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/Member.cs @@ -50,8 +50,8 @@ public class Member : Person /// /// Identification document for the Member /// - [NotMapped] - public virtual StoredFile IdDocument { get; set; } + //[NotMapped] + //public virtual StoredFile IdDocument { get; set; } /// /// The status of the membership /// diff --git a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/OrganisationTest.cs b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/OrganisationTest.cs index 3f83fbc81c..66be65088d 100644 --- a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/OrganisationTest.cs +++ b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/OrganisationTest.cs @@ -15,16 +15,4 @@ public class OrganisationTest : Organisation [AuditedAsManyToMany] public virtual IList DirectPersons { get; set; } = new List(); } - - /* AS: Experiments for using one table for linking two entities with several list properties with the same entity types - [Table("ShaAutoGen_OrganisationTest_DirectPersons_Reference")] - public class OrganisationTestDirectPersons : Entity - { - public Guid OrganisationTestId { get; set; } - - public Guid PersonId { get; set; } - - //public string PropertyName { get; set; } - } - */ } diff --git a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/TestClass.cs b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/TestClass.cs index 115884605a..f5bf9a7959 100644 --- a/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/TestClass.cs +++ b/shesha-functional-tests/backend/src/Module/Boxfusion.SheshaFunctionalTests.Common.Domain/Domain/TestClass.cs @@ -2,6 +2,7 @@ using Boxfusion.SheshaFunctionalTests.Common.Domain.Domain.Enum; using Shesha.Domain.Attributes; using Shesha.EntityReferences; +using Shesha.JsonEntities; using System; using System.Collections; using System.Collections.Generic; @@ -20,12 +21,12 @@ public class TestClass: Entity /// /// /// - public virtual JsonCar JsonProp { get; set; } + public virtual TestJsonEntity JsonProp { get; set; } /// /// /// [SaveAsJson] - public virtual IList TestListOfJsonEntitiesProp { get; set; } + public virtual IList TestListOfJsonEntitiesProp { get; set; } /// /// /// diff --git a/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests.csproj b/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests.csproj index f2279ff698..ef4064db5f 100644 --- a/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests.csproj +++ b/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests.csproj @@ -23,9 +23,21 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -34,14 +46,14 @@ - + - - - + + + all runtime; build; native; contentfiles; analyzers diff --git a/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/SheshaFunctionalTestsCommonDomainTestModule.cs b/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/SheshaFunctionalTestsCommonDomainTestModule.cs index 407b6c9a34..cbdaa4a7c2 100644 --- a/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/SheshaFunctionalTestsCommonDomainTestModule.cs +++ b/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/SheshaFunctionalTestsCommonDomainTestModule.cs @@ -46,7 +46,7 @@ public class SheshaFunctionalTestsCommonDomainTestModule : AbpModule public SheshaFunctionalTestsCommonDomainTestModule(SheshaNHibernateModule nhModule) { var appConfig = new ConfigurationBuilder().AddJsonFile("appsettings.Test.json").Build(); - ConnectionString = appConfig.GetConnectionString("TestDB"); + ConnectionString = appConfig.GetRequiredConnectionString("TestDB"); var nhConfig = Configuration.Modules.ShaNHibernate(); nhConfig.UseMsSql(ConnectionString); @@ -122,8 +122,6 @@ public override void Initialize() IocManager.IocContainer.AddFacility(f => f.UseAbpLog4Net().WithConfig("log4netTest.config")); - StaticContext.SetIocManager(IocManager); - ServiceCollectionRegistrar.Register(IocManager); } diff --git a/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/SheshaNhTestBase.cs b/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/SheshaNhTestBase.cs index cca1d81771..8d268f6b28 100644 --- a/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/SheshaNhTestBase.cs +++ b/shesha-functional-tests/backend/test/Boxfusion.SheshaFunctionalTests.Common.Domain.Tests/SheshaNhTestBase.cs @@ -4,13 +4,11 @@ using Abp.MultiTenancy; using Abp.Runtime.Session; using Abp.TestBase; -using Boxfusion.SheshaFunctionalTests.Common.Tests; using NHibernate; using NHibernate.Linq; using Shesha.Authorization.Users; using Shesha.Domain; using Shesha.MultiTenancy; -using Shesha.Services; using System; using System.Linq; using System.Threading.Tasks; @@ -24,25 +22,11 @@ protected SheshaNhTestBase() : base() // Seed initial data for host AbpSession.TenantId = null; - UsingDbSession(session => - { - //new InitialHostDbBuilder(session).Create(); - //new DefaultTenantBuilder(session).Create(); - }); - - // Seed initial data for default tenant AbpSession.TenantId = 1; - UsingDbSession(session => - { - //new TenantRoleAndUserBuilder(session, 1).Create(); - }); - //LoginAsDefaultTenantAdmin(); LoginAsHostAdmin(); - //AbpSession.TenantId = null; // workaround - StaticContext.SetIocManager(LocalIocManager); EntityHelper.RefreshStore(LocalIocManager); } diff --git a/shesha-functional-tests/publicportal/package-lock.json b/shesha-functional-tests/publicportal/package-lock.json index 5d3b31240e..3b7abffd96 100644 --- a/shesha-functional-tests/publicportal/package-lock.json +++ b/shesha-functional-tests/publicportal/package-lock.json @@ -28,10 +28,7 @@ "jodit": "^4.0.6", "less": "^4.2.0", "nanoid": "^3.1.23", - "next": "^14.1.0", - "next-nprogress-bar": "^2.1.2", - "next-with-less": "^3.0.1", - "nprogress": "^0.2.0", + "next": "^14.2.24", "prettier": "^2.2.1", "qs": "^6.11.2", "react": "^18.2.0", @@ -741,6 +738,37 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@bprogress/core": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@bprogress/core/-/core-1.3.3.tgz", + "integrity": "sha512-Vz9vzZE8xvfkgY09e0S6kzNz/ev/2lUw7A/z0TW7N0ZBG9xmVXrzSsFe3uvypxDa5ltn+xLBtfDtxBc8KsrvBQ==" + }, + "node_modules/@bprogress/next": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@bprogress/next/-/next-3.2.10.tgz", + "integrity": "sha512-uHu7risLFdXgyJzg1kL5Q+g8YGw9RBMqcyLWuDq7LGk95Ok/gWXMXg2pcSzpQ9pGxNQpq3LRzm67a8RFH2RpPQ==", + "dependencies": { + "@bprogress/core": "^1.3.3", + "@bprogress/react": "^1.2.4" + }, + "peerDependencies": { + "next": ">=13.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@bprogress/react": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@bprogress/react/-/react-1.2.5.tgz", + "integrity": "sha512-q389gPI3ckUQzKf8GbZiZgM1ErbSw9ynEavuIUunxiQYiPxyNuRisnEFZjbZV+48AZBhbwyU4vf6zBZ2ycGsow==", + "dependencies": { + "@bprogress/core": "^1.3.3" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@ctrl/tinycolor": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", @@ -1849,9 +1877,9 @@ } }, "node_modules/@next/env": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", - "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==" + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.24.tgz", + "integrity": "sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==" }, "node_modules/@next/eslint-plugin-next": { "version": "14.2.5", @@ -1863,9 +1891,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", - "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.24.tgz", + "integrity": "sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==", "cpu": [ "arm64" ], @@ -1878,9 +1906,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", - "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.24.tgz", + "integrity": "sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==", "cpu": [ "x64" ], @@ -1893,9 +1921,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", - "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.24.tgz", + "integrity": "sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==", "cpu": [ "arm64" ], @@ -1908,9 +1936,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", - "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.24.tgz", + "integrity": "sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==", "cpu": [ "arm64" ], @@ -1923,9 +1951,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", - "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.24.tgz", + "integrity": "sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==", "cpu": [ "x64" ], @@ -1938,9 +1966,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", - "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.24.tgz", + "integrity": "sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==", "cpu": [ "x64" ], @@ -1953,9 +1981,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", - "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.24.tgz", + "integrity": "sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==", "cpu": [ "arm64" ], @@ -1968,9 +1996,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", - "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.24.tgz", + "integrity": "sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==", "cpu": [ "ia32" ], @@ -1983,9 +2011,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", - "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.24.tgz", + "integrity": "sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw==", "cpu": [ "x64" ], @@ -2272,10 +2300,10 @@ "node_modules/@shesha-io/reactjs": { "version": "0.34.1-local", "resolved": "file:shesha-local.tgz", - "integrity": "sha512-I8p7GYfqJL6WqTgzTZvxB5Qtqg9lDltJCzSw74toBtcaz6Ac3aMPinx7iQmigTAccXJLKrYwKD7ptlvmulLiUA==", - "license": "GPL-3.0", + "integrity": "sha512-vNUTesFKnWVxhAaccebkpBxd7hV+hSsPT0riQw6tDgufHjTR1zvvrYW5iwjAEDVczv52hG5lLZu8J/bYP8SbBg==", "dependencies": { "@ant-design/nextjs-registry": "^1.0.1", + "@bprogress/next": "^3.2.10", "@monaco-editor/react": "^4.6.0", "@react-awesome-query-builder/antd": "^6.5.2", "@scarf/scarf": "^1.3.0", @@ -2301,7 +2329,7 @@ "monaco-editor": "^0.50.0", "mustache": "^4.2.0", "nested-property": "^4.0.0", - "next": "^14.2.4", + "next": "^14.2.24", "number-to-words": "^1.2.4", "object-hash": "^3.0.0", "point-in-polygon": "^1.1.0", @@ -4096,19 +4124,6 @@ "node": ">=0.8" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6212,17 +6227,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -6250,14 +6254,6 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/isomorphic-unfetch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", @@ -7397,14 +7393,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -7439,32 +7427,6 @@ "source-map": "~0.6.0" } }, - "node_modules/less-loader": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", - "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", - "peer": true, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "less": "^3.5.0 || ^4.0.0", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/less/node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -9144,11 +9106,11 @@ "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==" }, "node_modules/next": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", - "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.24.tgz", + "integrity": "sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==", "dependencies": { - "@next/env": "14.2.5", + "@next/env": "14.2.24", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -9163,15 +9125,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.5", - "@next/swc-darwin-x64": "14.2.5", - "@next/swc-linux-arm64-gnu": "14.2.5", - "@next/swc-linux-arm64-musl": "14.2.5", - "@next/swc-linux-x64-gnu": "14.2.5", - "@next/swc-linux-x64-musl": "14.2.5", - "@next/swc-win32-arm64-msvc": "14.2.5", - "@next/swc-win32-ia32-msvc": "14.2.5", - "@next/swc-win32-x64-msvc": "14.2.5" + "@next/swc-darwin-arm64": "14.2.24", + "@next/swc-darwin-x64": "14.2.24", + "@next/swc-linux-arm64-gnu": "14.2.24", + "@next/swc-linux-arm64-musl": "14.2.24", + "@next/swc-linux-x64-gnu": "14.2.24", + "@next/swc-linux-x64-musl": "14.2.24", + "@next/swc-win32-arm64-msvc": "14.2.24", + "@next/swc-win32-ia32-msvc": "14.2.24", + "@next/swc-win32-x64-msvc": "14.2.24" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -9192,27 +9154,6 @@ } } }, - "node_modules/next-nprogress-bar": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/next-nprogress-bar/-/next-nprogress-bar-2.3.13.tgz", - "integrity": "sha512-eJQDvPSUwZ2yRyNKqAH6xTFI4RtIFvs+qtY8essTtfooyaxLGmkanEO7vL83U166bullMtVpixD6FQk5SQkhMw==", - "dependencies": { - "nprogress": "^0.2.0" - } - }, - "node_modules/next-with-less": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/next-with-less/-/next-with-less-3.0.1.tgz", - "integrity": "sha512-lVJQ+dNWGpR1ccWM/LjY+8i28DC2oPa1Ivrc+h4+DFPJJN6O2EGKZIFBGrd9GLbwAEjFzKPs7yUk6bnrbY0qcw==", - "dependencies": { - "clone-deep": "^4.0.1" - }, - "peerDependencies": { - "less": "*", - "less-loader": ">= 7.0.0", - "next": ">= 11.0.1" - } - }, "node_modules/nock": { "version": "13.5.4", "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", @@ -9278,11 +9219,6 @@ "node": ">=8" } }, - "node_modules/nprogress": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==" - }, "node_modules/number-to-words": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/number-to-words/-/number-to-words-1.2.4.tgz", @@ -11390,17 +11326,6 @@ "node": ">=6.9" } }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/shallow-equal": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", diff --git a/shesha-functional-tests/publicportal/package.json b/shesha-functional-tests/publicportal/package.json index 637210bc7e..1a16a5e682 100644 --- a/shesha-functional-tests/publicportal/package.json +++ b/shesha-functional-tests/publicportal/package.json @@ -40,10 +40,7 @@ "jodit": "^4.0.6", "less": "^4.2.0", "nanoid": "^3.1.23", - "next": "^14.1.0", - "next-nprogress-bar": "^2.1.2", - "next-with-less": "^3.0.1", - "nprogress": "^0.2.0", + "next": "^14.2.24", "prettier": "^2.2.1", "qs": "^6.11.2", "react": "^18.2.0", diff --git a/shesha-functional-tests/publicportal/shesha-local.tgz b/shesha-functional-tests/publicportal/shesha-local.tgz index 433eaee176..d7c0cda9a1 100644 Binary files a/shesha-functional-tests/publicportal/shesha-local.tgz and b/shesha-functional-tests/publicportal/shesha-local.tgz differ diff --git a/shesha-functional-tests/publicportal/src/app/app-provider.tsx b/shesha-functional-tests/publicportal/src/app/app-provider.tsx index 0c01c1d13d..5b5bffbc2d 100644 --- a/shesha-functional-tests/publicportal/src/app/app-provider.tsx +++ b/shesha-functional-tests/publicportal/src/app/app-provider.tsx @@ -12,7 +12,6 @@ import { PublicPortalApplicationPlugin, } from "@shesha-io/pd-publicportal"; import { OrganisationsActionsProvider } from "@/components/dynamic-list/dynamic-actions"; -import { ProgressBar } from "./progressBar"; export interface IAppProviderProps { backendUrl: string; @@ -32,7 +31,6 @@ export const AppProvider: FC> = ({ noAuth={nextRouter.path?.includes("/no-auth")} applicationKey="public-portal" > - diff --git a/shesha-functional-tests/publicportal/src/app/progressBar.tsx b/shesha-functional-tests/publicportal/src/app/progressBar.tsx deleted file mode 100644 index 12d4af6b23..0000000000 --- a/shesha-functional-tests/publicportal/src/app/progressBar.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useTheme } from 'antd-style'; -import { AppProgressBar } from 'next-nprogress-bar'; -import React, { FC } from 'react'; - -export interface IProgressBarProps { - -} - -export const ProgressBar: FC = () => { - const theme = useTheme(); - return ( - - ); -}; \ No newline at end of file diff --git a/shesha-io.shesha-framework-test.yml b/shesha-io.shesha-framework-test.yml index d4928a94b8..79ba7f9bee 100644 --- a/shesha-io.shesha-framework-test.yml +++ b/shesha-io.shesha-framework-test.yml @@ -14,11 +14,11 @@ variables: - name: isMain value: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')] -pr: - branches: - include: - - releases/* - - main +# pr: +# branches: +# include: +# - releases/* +# - main trigger: tags: include: @@ -37,9 +37,67 @@ pool: stages: - stage: build jobs: + - job: UnitTests_1 + displayName: 'Unit Tests' + pool: + name: Azure Pipelines + vmImage: ubuntu-22.04 + steps: + - checkout: self + - task: NuGetToolInstaller@1 + inputs: + versionSpec: '6.x' + checkLatest: true + - task: NuGetCommand@2 + condition: ne(variables.CACHE_RESTORED, true) + displayName: Nuget Restore + inputs: + command: 'restore' + feedsToUse: 'config' + restoreSolution: 'shesha-core/Shesha.sln' + nugetConfigPath: 'shesha-core/.nuget/NuGet.Azure.Config' + - task: UseDotNet@2 + displayName: Use .NET Core sdk 8.x + retryCountOnTaskFailure: 3 + inputs: + version: 8.x + - task: DotNetCoreCLI@2 + displayName: dotnet build + inputs: + projects: shesha-core/Shesha.sln + arguments: --configuration $(BuildParameters.configuration) + - task: DockerInstaller@0 + inputs: + dockerVersion: '20.10.11' + - script: | + docker --version + sudo systemctl start docker + displayName: 'Verify Docker' + - task: Docker@2 + inputs: + containerRegistry: 'docker-images' + command: 'login' + - script: dotnet test Shesha.Tests.csproj --configuration $(buildConfiguration) --no-build --logger "trx;LogFileName=testresults.trx" + workingDirectory: 'shesha-core/test/Shesha.Tests' + displayName: 'Run tests with Testcontainers' + env: + DOCKER_HOST: 'unix:///var/run/docker.sock' + TESTCONTAINERS_RYUK_DISABLED: 'true' + - task: PublishTestResults@2 + displayName: 'Publish test results' + inputs: + testResultsFormat: 'VSTest' + testResultsFiles: '**/*.trx' + failTaskOnFailedTests: true + failTaskOnFailureToPublishResults: true + failTaskOnMissingResultsFile: true + testRunTitle: 'Unit Tests' + condition: succeededOrFailed() - job: Job_1 displayName: Build_Functional_Shesha_API - dependsOn: Job_4 + dependsOn: + - Job_4 + - UnitTests_1 pool: name: Azure Pipelines vmImage: windows-latest @@ -108,9 +166,9 @@ stages: displayName: dotnet Restore inputs: command: 'restore' - projects: shesha-functional-tests/backend/**/*.csproj - feedsToUse: 'select' - vstsFeed: '44b4667a-8963-403d-9962-bde66225adbd' + projects: 'shesha-functional-tests/backend/**/*.csproj' + feedsToUse: 'config' + nugetConfigPath: './shesha-core/.nuget/NuGet.Azure.Config' - task: DotNetCoreCLI@2 displayName: dotnet build inputs: diff --git a/shesha-reactjs/README.md b/shesha-reactjs/README.md index 6460a30434..bd77e7f5fb 100644 --- a/shesha-reactjs/README.md +++ b/shesha-reactjs/README.md @@ -20,7 +20,7 @@ Visit [https://docs.shesha.io/docs/get-started/Introduction](https://docs.shesha ## Getting Started -Visist [https://docs.shesha.io/docs/category/front-end-basics](https://docs.shesha.io/docs/category/front-end-basics) to get started with @shesha-io/reactjs +Visit [https://docs.shesha.io/docs/category/front-end-basics](https://docs.shesha.io/docs/category/front-end-basics) to get started with @shesha-io/reactjs

diff --git a/shesha-reactjs/package-lock.json b/shesha-reactjs/package-lock.json index 142843dfeb..2afc744736 100644 --- a/shesha-reactjs/package-lock.json +++ b/shesha-reactjs/package-lock.json @@ -10,6 +10,7 @@ "license": "GPL-3.0", "dependencies": { "@ant-design/nextjs-registry": "^1.0.1", + "@bprogress/next": "^3.2.10", "@monaco-editor/react": "^4.6.0", "@react-awesome-query-builder/antd": "^6.5.2", "@scarf/scarf": "^1.3.0", @@ -18,7 +19,7 @@ "async-validator": "^3.5.2", "babel-plugin-module-resolver": "^5.0.0", "camelcase-keys": "^9.1.3", - "chart.js": "^4.4.4", + "chart.js": "^4.4.9", "clean-deep": "^3.4.0", "constrained-editor-plugin": "^1.3.0", "convert-css-color-name-to-hex": "^0.1.1", @@ -35,15 +36,16 @@ "monaco-editor": "^0.50.0", "mustache": "^4.2.0", "nested-property": "^4.0.0", - "next": "^14.2.4", + "next": "^14.2.5", "number-to-words": "^1.2.4", "object-hash": "^3.0.0", "point-in-polygon": "^1.1.0", "rc-picker": "4.5.0", "react-ace": "^10.1.0", "react-beautiful-dnd": "^13.1.0", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-error-boundary": "^3.1.4", + "react-fast-marquee": "^1.6.5", "react-icons": "^5.1.0", "react-idle-timer": "^5.5.3", "react-markdown": "^8.0.3", @@ -55,7 +57,9 @@ "react-syntax-highlighter": "^15.5.0", "react-table": "^7.7.0", "remark-gfm": "^3.0.1", + "scheduler": "^0.26.0", "tree-to-list": "^3.0.2", + "use-context-selector": "^2.0.0", "use-debounce": "^6.0.1" }, "devDependencies": { @@ -119,8 +123,6 @@ "lodash": "^4.17.20", "moment": "^2.25.3", "nanoid": "^3.3.7", - "next-nprogress-bar": "^2.3.12", - "nprogress": "^0.2.0", "prettier": "^3.0.3", "qs": "^6.10.1", "react": "^18.2.0", @@ -2172,6 +2174,37 @@ "dev": true, "license": "MIT" }, + "node_modules/@bprogress/core": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@bprogress/core/-/core-1.3.3.tgz", + "integrity": "sha512-Vz9vzZE8xvfkgY09e0S6kzNz/ev/2lUw7A/z0TW7N0ZBG9xmVXrzSsFe3uvypxDa5ltn+xLBtfDtxBc8KsrvBQ==" + }, + "node_modules/@bprogress/next": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@bprogress/next/-/next-3.2.10.tgz", + "integrity": "sha512-uHu7risLFdXgyJzg1kL5Q+g8YGw9RBMqcyLWuDq7LGk95Ok/gWXMXg2pcSzpQ9pGxNQpq3LRzm67a8RFH2RpPQ==", + "dependencies": { + "@bprogress/core": "^1.3.3", + "@bprogress/react": "^1.2.4" + }, + "peerDependencies": { + "next": ">=13.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@bprogress/react": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@bprogress/react/-/react-1.2.5.tgz", + "integrity": "sha512-q389gPI3ckUQzKf8GbZiZgM1ErbSw9ynEavuIUunxiQYiPxyNuRisnEFZjbZV+48AZBhbwyU4vf6zBZ2ycGsow==", + "dependencies": { + "@bprogress/core": "^1.3.3" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@commitlint/config-validator": { "version": "19.5.0", "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.5.0.tgz", @@ -3586,10 +3619,9 @@ } }, "node_modules/@next/env": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz", - "integrity": "sha512-CysUC9IO+2Bh0omJ3qrb47S8DtsTKbFidGm6ow4gXIG6reZybqxbkH2nhdEm1tC8SmgzDdpq3BIML0PWsmyUYA==", - "license": "MIT" + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.24.tgz", + "integrity": "sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==" }, "node_modules/@next/eslint-plugin-next": { "version": "15.1.4", @@ -3602,13 +3634,12 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.23.tgz", - "integrity": "sha512-WhtEntt6NcbABA8ypEoFd3uzq5iAnrl9AnZt9dXdO+PZLACE32z3a3qA5OoV20JrbJfSJ6Sd6EqGZTrlRnGxQQ==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.24.tgz", + "integrity": "sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -3618,13 +3649,12 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.23.tgz", - "integrity": "sha512-vwLw0HN2gVclT/ikO6EcE+LcIN+0mddJ53yG4eZd0rXkuEr/RnOaMH8wg/sYl5iz5AYYRo/l6XX7FIo6kwbw1Q==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.24.tgz", + "integrity": "sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -3634,13 +3664,12 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.23.tgz", - "integrity": "sha512-uuAYwD3At2fu5CH1wD7FpP87mnjAv4+DNvLaR9kiIi8DLStWSW304kF09p1EQfhcbUI1Py2vZlBO2VaVqMRtpg==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.24.tgz", + "integrity": "sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3650,13 +3679,12 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.23.tgz", - "integrity": "sha512-Mm5KHd7nGgeJ4EETvVgFuqKOyDh+UMXHXxye6wRRFDr4FdVRI6YTxajoV2aHE8jqC14xeAMVZvLqYqS7isHL+g==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.24.tgz", + "integrity": "sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3666,13 +3694,12 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.23.tgz", - "integrity": "sha512-Ybfqlyzm4sMSEQO6lDksggAIxnvWSG2cDWnG2jgd+MLbHYn2pvFA8DQ4pT2Vjk3Cwrv+HIg7vXJ8lCiLz79qoQ==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.24.tgz", + "integrity": "sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3682,13 +3709,12 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.23.tgz", - "integrity": "sha512-OSQX94sxd1gOUz3jhhdocnKsy4/peG8zV1HVaW6DLEbEmRRtUCUQZcKxUD9atLYa3RZA+YJx+WZdOnTkDuNDNA==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.24.tgz", + "integrity": "sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3698,13 +3724,12 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.23.tgz", - "integrity": "sha512-ezmbgZy++XpIMTcTNd0L4k7+cNI4ET5vMv/oqNfTuSXkZtSA9BURElPFyarjjGtRgZ9/zuKDHoMdZwDZIY3ehQ==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.24.tgz", + "integrity": "sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -3714,13 +3739,12 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.23.tgz", - "integrity": "sha512-zfHZOGguFCqAJ7zldTKg4tJHPJyJCOFhpoJcVxKL9BSUHScVDnMdDuOU1zPPGdOzr/GWxbhYTjyiEgLEpAoFPA==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.24.tgz", + "integrity": "sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==", "cpu": [ "ia32" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -3730,13 +3754,12 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.23.tgz", - "integrity": "sha512-xCtq5BD553SzOgSZ7UH5LH+OATQihydObTrCTvVzOro8QiWYKdBVwcB2Mn2MLMo6DGW9yH1LSPw7jS7HhgJgjw==", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.24.tgz", + "integrity": "sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -6400,6 +6423,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/babel-plugin-module-resolver/node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "license": "MIT" + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.12", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", @@ -6906,9 +6935,9 @@ "license": "MIT" }, "node_modules/chart.js": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", - "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" @@ -15117,12 +15146,11 @@ "license": "MIT" }, "node_modules/next": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.23.tgz", - "integrity": "sha512-mjN3fE6u/tynneLiEg56XnthzuYw+kD7mCujgVqioxyPqbmiotUCGJpIZGS/VaPg3ZDT1tvWxiVyRzeqJFm/kw==", - "license": "MIT", + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.24.tgz", + "integrity": "sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==", "dependencies": { - "@next/env": "14.2.23", + "@next/env": "14.2.24", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -15137,15 +15165,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.23", - "@next/swc-darwin-x64": "14.2.23", - "@next/swc-linux-arm64-gnu": "14.2.23", - "@next/swc-linux-arm64-musl": "14.2.23", - "@next/swc-linux-x64-gnu": "14.2.23", - "@next/swc-linux-x64-musl": "14.2.23", - "@next/swc-win32-arm64-msvc": "14.2.23", - "@next/swc-win32-ia32-msvc": "14.2.23", - "@next/swc-win32-x64-msvc": "14.2.23" + "@next/swc-darwin-arm64": "14.2.24", + "@next/swc-darwin-x64": "14.2.24", + "@next/swc-linux-arm64-gnu": "14.2.24", + "@next/swc-linux-arm64-musl": "14.2.24", + "@next/swc-linux-x64-gnu": "14.2.24", + "@next/swc-linux-x64-musl": "14.2.24", + "@next/swc-win32-arm64-msvc": "14.2.24", + "@next/swc-win32-ia32-msvc": "14.2.24", + "@next/swc-win32-x64-msvc": "14.2.24" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -15166,16 +15194,6 @@ } } }, - "node_modules/next-nprogress-bar": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/next-nprogress-bar/-/next-nprogress-bar-2.4.3.tgz", - "integrity": "sha512-069WmFALkpCyKiIy89q6dWcPiHhUmXh6EZz6oLpOS7zsIunNOYLscloKFALjIQNCOg3ZO9dWRPowYW4ULho4HQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "nprogress-v2": "^1.0.4" - } - }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -15319,20 +15337,6 @@ "node": ">=8" } }, - "node_modules/nprogress": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nprogress-v2": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/nprogress-v2/-/nprogress-v2-1.0.4.tgz", - "integrity": "sha512-HPhTEgqsQMFXXuaftvu6bevqEXvcQ/hogvkAswlcNyS9eRGjvo7wZY1O4H3aZS4Uq8oWSO6sL3dOj6Oq/ZJN5A==", - "dev": true, - "license": "MIT" - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -17845,6 +17849,15 @@ "react": "^18.3.1" } }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/react-error-boundary": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", @@ -17861,6 +17874,16 @@ "react": ">=16.13.1" } }, + "node_modules/react-fast-marquee": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/react-fast-marquee/-/react-fast-marquee-1.6.5.tgz", + "integrity": "sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.8.0 || ^18.0.0", + "react-dom": ">= 16.8.0 || ^18.0.0" + } + }, "node_modules/react-icons": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", @@ -18481,12 +18504,6 @@ "dev": true, "license": "MIT" }, - "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", - "license": "MIT" - }, "node_modules/reserved-words": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", @@ -19004,13 +19021,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/screenfull": { "version": "5.2.0", @@ -20990,6 +21004,16 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-context-selector": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-2.0.0.tgz", + "integrity": "sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==", + "license": "MIT", + "peerDependencies": { + "react": ">=18.0.0", + "scheduler": ">=0.19.0" + } + }, "node_modules/use-debounce": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-6.0.1.tgz", diff --git a/shesha-reactjs/package.json b/shesha-reactjs/package.json index 9015df5019..6c0e94b0f3 100644 --- a/shesha-reactjs/package.json +++ b/shesha-reactjs/package.json @@ -120,8 +120,6 @@ "lodash": "^4.17.20", "moment": "^2.25.3", "nanoid": "^3.3.7", - "next-nprogress-bar": "^2.3.12", - "nprogress": "^0.2.0", "prettier": "^3.0.3", "qs": "^6.10.1", "react": "^18.2.0", @@ -151,6 +149,7 @@ ], "dependencies": { "@ant-design/nextjs-registry": "^1.0.1", + "@bprogress/next": "^3.2.10", "@monaco-editor/react": "^4.6.0", "@react-awesome-query-builder/antd": "^6.5.2", "@scarf/scarf": "^1.3.0", @@ -159,7 +158,7 @@ "async-validator": "^3.5.2", "babel-plugin-module-resolver": "^5.0.0", "camelcase-keys": "^9.1.3", - "chart.js": "^4.4.4", + "chart.js": "^4.4.9", "clean-deep": "^3.4.0", "constrained-editor-plugin": "^1.3.0", "convert-css-color-name-to-hex": "^0.1.1", @@ -176,15 +175,16 @@ "monaco-editor": "^0.50.0", "mustache": "^4.2.0", "nested-property": "^4.0.0", - "next": "^14.2.4", + "next": "^14.2.5", "number-to-words": "^1.2.4", "object-hash": "^3.0.0", "point-in-polygon": "^1.1.0", "rc-picker": "4.5.0", "react-ace": "^10.1.0", "react-beautiful-dnd": "^13.1.0", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-error-boundary": "^3.1.4", + "react-fast-marquee": "^1.6.5", "react-icons": "^5.1.0", "react-idle-timer": "^5.5.3", "react-markdown": "^8.0.3", @@ -196,7 +196,9 @@ "react-syntax-highlighter": "^15.5.0", "react-table": "^7.7.0", "remark-gfm": "^3.0.1", + "scheduler": "^0.26.0", "tree-to-list": "^3.0.2", + "use-context-selector": "^2.0.0", "use-debounce": "^6.0.1" }, "config": { diff --git a/shesha-reactjs/src/apis/note.ts b/shesha-reactjs/src/apis/note.ts index 6b5f98a101..6bf06309eb 100644 --- a/shesha-reactjs/src/apis/note.ts +++ b/shesha-reactjs/src/apis/note.ts @@ -20,7 +20,7 @@ export interface NoteDto { /** * Category of the note. Is used to split notes into groups */ - category?: number | null; + category?: string; /** * Note importance (priority) */ @@ -49,7 +49,7 @@ export interface CreateNoteDto { /** * Category of the note. Is used to split notes into groups */ - category?: number | null; + category?: string; /** * Note importance (priority) */ @@ -76,7 +76,7 @@ export interface NoteGetListQueryParams { /** * Category of the note. Is used to split notes into groups */ - category?: number; + category?: string; /** * Set to true to get notes of all categories */ diff --git a/shesha-reactjs/src/app/app-provider.tsx b/shesha-reactjs/src/app/app-provider.tsx index cc8c4e3b6f..4a01d6b926 100644 --- a/shesha-reactjs/src/app/app-provider.tsx +++ b/shesha-reactjs/src/app/app-provider.tsx @@ -5,7 +5,6 @@ import { GlobalStateProvider, ShaApplicationProvider, StoredFilesProvider } from import { useNextRouter } from '@/hooks/useNextRouter'; import { EntityCrudActions } from '@/providers/dynamicActions/implementations/entityCrudActions'; import { StandardApis } from '@/providers/dynamicActions/implementations/standardApis'; -import { ProgressBar } from './progressBar'; import { UrlActions } from '@/providers/dynamicActions/implementations/dataSourceDynamicMenu/urlDynamicMenuItem'; import { EntityActions } from '@/providers/dynamicActions/implementations/dataSourceDynamicMenu/entityDynamicMenuItem'; @@ -23,7 +22,6 @@ export const AppProvider: FC> = ({ children router={nextRouter} noAuth={nextRouter.path?.includes('/no-auth')} > - @@ -38,4 +36,4 @@ export const AppProvider: FC> = ({ children ); -}; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/app/progressBar.tsx b/shesha-reactjs/src/app/progressBar.tsx deleted file mode 100644 index 12d4af6b23..0000000000 --- a/shesha-reactjs/src/app/progressBar.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useTheme } from 'antd-style'; -import { AppProgressBar } from 'next-nprogress-bar'; -import React, { FC } from 'react'; - -export interface IProgressBarProps { - -} - -export const ProgressBar: FC = () => { - const theme = useTheme(); - return ( - - ); -}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/appConfigurator/switchToEditModeConfirmation.tsx b/shesha-reactjs/src/components/appConfigurator/switchToEditModeConfirmation.tsx index 6580e38bf0..635861ea0f 100644 --- a/shesha-reactjs/src/components/appConfigurator/switchToEditModeConfirmation.tsx +++ b/shesha-reactjs/src/components/appConfigurator/switchToEditModeConfirmation.tsx @@ -6,13 +6,13 @@ export interface IProps {} export const SwitchToEditModeConfirmation: FC = () => { const { editModeConfirmationVisible, switchApplicationMode, toggleEditModeConfirmation } = useAppConfigurator(); + return ( toggleEditModeConfirmation(false)} onOk={() => switchApplicationMode('edit')} - okButtonProps={{}} >

Would you like to leave 'Live Mode' and launch 'Edit Mode'?

diff --git a/shesha-reactjs/src/components/attributeDecorator/index.tsx b/shesha-reactjs/src/components/attributeDecorator/index.tsx new file mode 100644 index 0000000000..bf619c5309 --- /dev/null +++ b/shesha-reactjs/src/components/attributeDecorator/index.tsx @@ -0,0 +1,46 @@ +import React, { useEffect, useRef, useState } from 'react'; + +interface AttributeDecoratorProps { + /** Object of attributes to be injected into the element */ + attributes?: Record; + /** The element to be decorated */ + children: React.ReactElement; +} + +/** + * Takes decoration attributes as props.attributes [{ key: value }, ...] and injects them as custom data attributes to the element that is being wrapped / 'decorated'. + * The children must be a single element. + * Based on https://stackoverflow.com/questions/65123286/inject-custom-data-attribute-to-react-props-children/75875667#75875667 with some modifications to make it more flexible. + * Modified because the original code re-create children component twice (due to change Wrapper component to Fragment). + * + * 1. Create Div and childen component one after the other + * 2. Get reference to the Div + * 3. Get index of the childen component in the parent element + * 4. Inject attributes + * 5. Replace Div with Fragment (remove Div) + * + * @returns The decorated element with the attributes injected into it + */ +const AttributeDecorator: React.FC = props => { + const [Wrapper, setWrapper] = useState('div'); + const ref = useRef(null); + + useEffect(() => { + if (ref.current?.parentElement) { + const targetIndex = Array.from(ref.current.parentElement.children).indexOf(ref.current) + 1; + if (props.attributes) { + Object.entries(props.attributes).forEach(([key, value]) => { + ref.current?.parentElement?.children[targetIndex]?.setAttribute(key, value); + }); + } + setWrapper(React.Fragment); + } + }, [ref.current]); + + return <> + + {props.children} + ; +}; + +export default AttributeDecorator; \ No newline at end of file diff --git a/shesha-reactjs/src/components/autocomplete/entityAutocomplete.tsx b/shesha-reactjs/src/components/autocomplete/entityAutocomplete.tsx deleted file mode 100644 index 9050b2d6cd..0000000000 --- a/shesha-reactjs/src/components/autocomplete/entityAutocomplete.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import React, { useMemo, useRef, useState } from 'react'; -import { - AutocompleteItemDto, - CustomLabeledValue, - IEntityAutocompleteProps, - ISelectOption -} from './models'; -import { ReadOnlyDisplayFormItem } from './../readOnlyDisplayFormItem'; -import { Select } from 'antd'; -import { useDebouncedCallback } from 'use-debounce'; -import { useEntityAutocomplete } from '@/utils/autocomplete'; - -/** - * Entity autocomplete - */ - -export const EntityAutocomplete = (props: IEntityAutocompleteProps) => { - const { - value, - defaultValue, - placeholder, - typeShortAlias, - entityDisplayProperty, - //allowInherited, - onChange, - disabled, - bordered = true, - style, - size, - mode, - notFoundContent, - getOptionFromFetchedItem, - getLabeledValue, - readOnly, - //readOnlyMultipleMode = 'raw', - disableSearch, - quickviewEnabled, - quickviewFormPath, - quickviewDisplayPropertyName, - quickviewGetEntityUrl, - quickviewWidth, - filter, - } = props; - - - - const { - data: fetchedData, - loading, - error: fetchError, - search: searchEntity, - } = useEntityAutocomplete({ - entityType: typeShortAlias, - value: value as any, - filter, - displayProperty: entityDisplayProperty, - }); - - const selectRef = useRef(null); - - const [autocompleteText, setAutocompleteText] = useState(null); - - const extractProperty = (item: object, propertyName: string): string => { - const propName = propertyName || '_displayName'; - var path = propName.split('.'); - var result = path.reduce((val, name) => (val ? val[name] : null), item); - return result; - }; - - const getFetchedItems = (): AutocompleteItemDto[] => { - return fetchedData.map((e) => ({ - value: e.id.toString(), - displayText: extractProperty(e, entityDisplayProperty), - })); - }; - - const handleSelect = () => { - selectRef.current.blur(); - }; - - const debouncedFetchItems = useDebouncedCallback<(value: string) => void>( - (localValue) => { - searchEntity(localValue); - }, - // delay in ms - 200 - ); - - const wrapValue = (localValue: TValue | TValue[], allOptions: ISelectOption[]): CustomLabeledValue | CustomLabeledValue[] => { - if (!Boolean(localValue)) return undefined; - if (mode === 'multiple') { - return Array.isArray(localValue) - ? (localValue as TValue[]).map>((o) => { - return getLabeledValue(o, allOptions); - }) - : [getLabeledValue(localValue as TValue, allOptions)]; - } else return getLabeledValue(localValue as TValue, allOptions); - }; - - const options = useMemo[]>(() => { - const localData = getFetchedItems() || []; - - const fetchedItems = localData.map>((item) => { - const option = Boolean(getOptionFromFetchedItem) - ? (getOptionFromFetchedItem(item) as ISelectOption) - : (item as ISelectOption); - - return option; - }); - - const selectedItem = wrapValue(value, fetchedItems); - - // Remove items which are already exist in the fetched items. - // Note: we shouldn't process full list and make it unique because by this way we'll hide duplicates received from the back-end - const selectedItems = selectedItem - ? (Array.isArray(selectedItem) ? selectedItem : [selectedItem]).filter( - (i) => fetchedItems.findIndex((fi) => fi.value === i.value) === -1 - ) - : []; - - const result = [...fetchedItems, ...selectedItems]; - return result; - }, [value, autocompleteText, fetchedData]); - - const handleSearch = (localValue: string) => { - setAutocompleteText(localValue); - if (localValue) { - debouncedFetchItems(localValue); - } - }; - - const handleChange = (_value: CustomLabeledValue, option: any) => { - setAutocompleteText(null); - - if (!Boolean(onChange)) return; - const selectedValue = Boolean(option) - ? Array.isArray(option) - ? (option as ISelectOption[]).map((o) => o.data) - : (option as ISelectOption).data - : undefined; - - if (mode === 'multiple') { - onChange(Array.isArray(selectedValue) ? selectedValue : [selectedValue]); - } else onChange(selectedValue); - }; - - const dataLoaded = fetchedData && fetchedData.length > 0; - const autocompleteValue = value || dataLoaded || fetchError ? wrapValue(value, options) : undefined; - const selectPlaceholder = value && !dataLoaded && loading ? 'Loading...' : placeholder ?? ''; - - if (readOnly) { - return ( - - ); - } - - const onFocus = () => { - debouncedFetchItems(autocompleteText); - }; - - const onClear = () => { - setAutocompleteText(null); - }; - - return ( - | CustomLabeledValue[]> - className="sha-dropdown" - dropdownStyle={{...style, height: 'auto'}} - showSearch={!disableSearch} - labelInValue={true} - notFoundContent={notFoundContent} - defaultActiveFirstOption={false} - filterOption={false} - onSearch={handleSearch} - defaultValue={wrapValue(defaultValue, options)} - value={autocompleteValue} - onChange={handleChange} - allowClear={true} - onClear={onClear} - onFocus={onFocus} - loading={loading} - placeholder={selectPlaceholder} - disabled={disabled} - variant={!bordered ? 'borderless' : undefined} - onSelect={handleSelect} - style={style} - size={size} - ref={selectRef} - mode={value && mode === 'multiple' ? mode : undefined} // When mode is multiple and value is null, the control shows an empty tag - > - {options?.map(({ value: localValue, label, data }) => ( - - - {label} - - - ))} - - ); -}; diff --git a/shesha-reactjs/src/components/autocomplete/index.tsx b/shesha-reactjs/src/components/autocomplete/index.tsx index 435052977f..7c8c187a18 100644 --- a/shesha-reactjs/src/components/autocomplete/index.tsx +++ b/shesha-reactjs/src/components/autocomplete/index.tsx @@ -1,95 +1,415 @@ -import React from 'react'; -import { IEntityReferenceDto } from '@/interfaces'; -import { EntityAutocomplete } from './entityAutocomplete'; -import { IAutocompleteProps, ISelectOption, AutocompleteDataSourceType, CustomLabeledValue } from './models'; -import { UrlAutocomplete } from './urlAutocomplete'; - -/** - * A component for working with dynamic autocomplete - */ +import React, { FC, useEffect, useId, useMemo, useRef, useState } from 'react'; +import { DataTableProvider, evaluateString, getUrlKeyParam, useActualContextData, useDataTableStore, useDeepCompareMemo, useShaFormInstance } from '@/index'; +import { Select, Typography } from 'antd'; +import { useDebouncedCallback } from 'use-debounce'; +import { AutocompleteDataSourceType, DisplayValueFunc, FilterSelectedFunc, IAutocompleteBaseProps, IAutocompleteProps, ISelectOption, KayValueFunc, OutcomeValueFunc, getColumns } from './models'; +import QueryString from 'qs'; +import { isPropertySettings } from '@/designer-components/_settings/utils'; +import ReadOnlyDisplayFormItem from '../readOnlyDisplayFormItem'; +import { getValueByPropertyName } from '@/utils/object'; +import { isDataColumn } from '@/providers/dataTable/interfaces'; +import { ValueRenderer } from '../valueRenderer'; +import { isEqual, uniqWith } from 'lodash'; +import { useDeepCompareEffect } from '@/hooks/useDeepCompareEffect'; +import { useStyles } from './style'; -const Autocomplete = (props: IAutocompleteProps) => { - return props.dataSourceType === 'entitiesList' - ? - : ; -}; +const AutocompleteInner: FC = (props: IAutocompleteBaseProps) => { + const {allowClear = true } = props; + const { style = {} } = props; + + const { styles } = useStyles({ style }); + + + // sources + const source = useDataTableStore(false); + const allData = {};//useAvailableConstantsData(); + const selectRef = useRef(null); + + // init props + // --- For backward compatibility + const keyPropName = props.keyPropName || (props.dataSourceType === 'entitiesList' ? 'id' : 'value'); + const displayPropName = props.displayPropName || (props.dataSourceType === 'entitiesList' ? '_displayName' : 'displayText'); + // --- + const keyValueFunc: KayValueFunc = props.keyValueFunc ?? + ((value: any) => (getValueByPropertyName(value, keyPropName) ?? value)?.toString()?.toLowerCase()); + const filterKeysFunc: FilterSelectedFunc = props.filterKeysFunc ?? + ((value: any) => ({ in: [{ var: `${keyPropName}` }, Array.isArray(value) ? value.map(x => keyValueFunc(x, allData)) : [keyValueFunc(value, allData)]] })); + const filterNotKeysFunc: FilterSelectedFunc = ((value: any) => { + const filter = filterKeysFunc(value); + return filter ? { "!": filter } : null; + }); + const displayValueFunc: DisplayValueFunc = props.displayValueFunc ?? + ((value: any) => (Boolean(value) ? getValueByPropertyName(value, displayPropName) ?? value?.toString() : '')); + const outcomeValueFunc: OutcomeValueFunc = props.outcomeValueFunc ?? + // --- For backward compatibility + (props.dataSourceType === 'entitiesList' + ? ((value: any) => ({ id: value.id, _displayName: getValueByPropertyName(value, displayPropName), _className: value._className })) + // --- + : ((value: any) => getValueByPropertyName(value, keyPropName) ?? value)); + + // register columns + useDeepCompareEffect(() => source?.registerConfigurableColumns(props.uid, getColumns(props.fields)), [props.fields]); + + // init state + const [open, setOpen] = useState(false); + const [loadingValues, setLoadingValues] = useState(false); + const selected = useRef>([]); + const lastSearchText = useRef(''); + const [autocompleteText, setAutocompleteText] = useState(null); + + const keys = useMemo(() => { + const res = props.value + ? Array.isArray(props.value) + ? props.value.map((x) => keyValueFunc(x, allData)) + : [keyValueFunc(props.value, allData)] + : []; + return res; + }, [props.value]); + + // update local store of values details + useEffect(() => { + if (props.dataSourceType === 'entitiesList' && props.entityType + || props.dataSourceType === 'url' && props.dataSourceUrl + ) { + // use _displayName from value if dataSourceType === 'entitiesList' and displayPropName is empty + if (keys.length) { + const displayNameValue = (Array.isArray(props.value) ? props.value[0] : props.value)['_displayName']; + const hasDisplayName = displayNameValue !== undefined && displayNameValue !== null; + + if (props.dataSourceType === 'entitiesList' && !props.displayValueFunc && !props.displayPropName && hasDisplayName) { + setLoadingValues(false); + const values = Array.isArray(props.value) ? props.value : [props.value]; + selected.current = keys.map((x) => values.find((y) => keyValueFunc(outcomeValueFunc(y, allData), allData) === x)); + return; + } + } + props.disableRefresh.current = false; + if (keys.length) { + const allExist = keys.every((x) => selected.current?.find((y) => keyValueFunc(outcomeValueFunc(y, allData), allData) === x)); + if (!loadingValues && !allExist) { + // request full details for values + setLoadingValues(true); + const selectedFilter = filterKeysFunc(props.value); + source?.setPredefinedFilters([{id: 'selectedFilter', name: 'selectedFilter', expression: selectedFilter}]); + } + if (loadingValues && source?.tableData?.length) { + // update local store with full details + setLoadingValues(false); + selected.current = keys.map((x) => source?.tableData.find((y) => keyValueFunc(outcomeValueFunc(y, allData), allData) === x)); + } + } + } + }, [props.value, source?.tableData, props.dataSourceType, props.entityType, props.dataSourceUrl, props.readOnly]); + + const debouncedSearch = useDebouncedCallback<(searchText: string, force?: boolean) => void>( + (searchText, force = false) => { + if (props.readOnly || !force && lastSearchText.current === searchText) + return; + source?.performQuickSearch(searchText); + lastSearchText.current = searchText; + }, 200); + + const handleSearch = (searchText: string) => { + if (props.allowFreeText) + setAutocompleteText(searchText); + debouncedSearch(searchText); + if (props.onSearch) + props.onSearch(searchText); + }; + + const handleSelect = () => { + selectRef.current.blur(); + }; + + const handleChange = (_value, option: any) => { + selected.current = Boolean(option) + ? Array.isArray(option) + ? (option as ISelectOption[]).map((o) => o.data) + : [(option as ISelectOption).data] + : []; + + const selectedValue = Boolean(option) + ? Array.isArray(option) + ? (option as ISelectOption[]).map((o) => outcomeValueFunc(o.data, allData)) + : outcomeValueFunc((option as ISelectOption).data, allData) + : null; + + const selectedFilter = selectedValue && (!Array.isArray(selectedValue) || selectedValue.length) + ? filterNotKeysFunc(selectedValue) + : null; + source?.setPredefinedFilters([{ id: 'selectedFilter', name: 'selectedFilter', expression: selectedFilter }]); + debouncedSearch(''); + + if (!Boolean(props.onChange)) + return; + if (props.mode === 'multiple') { + props.onChange(Array.isArray(selectedValue) ? selectedValue : [selectedValue]); + } else + props.onChange(selectedValue); + }; + + const renderOption = (row, index) => { + const value = outcomeValueFunc(row, allData); + const key = keyValueFunc(value, allData); + const label = displayValueFunc(row, allData); + return ( + + + + ); + }; -export type IDtoType = IEntityReferenceDto | IEntityReferenceDto[]; - -export const EntityDtoAutocomplete = (props: IAutocompleteProps) => { - const getDtoFromFetchedItem = (item): ISelectOption => { - return { - value: item['value'], - label: item['displayText'], - data: { - id: item['value'], - _displayName: item['displayText'], - _className: props.typeShortAlias - }, - }; + const renderGroupTitle = (value: any, propertyName: string) => { + if (value === null || value === undefined) + return (empty); + const column = source?.groupingColumns.find((c) => isDataColumn(c) && c.propertyName === propertyName); + const propertyMeta = isDataColumn(column) ? column.metadata : null; + return ; }; - const labeledValueGetter = (itemValue: IEntityReferenceDto, options: ISelectOption[]) => { - if (!Boolean(itemValue)) return null; - // update label value from fetched data (necessary for JsonEntity because it stores DisplayText in the Json and might be inconsistent) - const item = options?.find(i => i.value === itemValue.id); + const selectedValuesList = useMemo(() => { + return selected.current?.map((row, index) => renderOption(row, 10 + index)); + }, [selected.current]); + + const freeTextValuesList = useMemo(() => { + return props.allowFreeText && autocompleteText && source.tableData.findIndex(x => x[displayPropName]?.toLowerCase() === autocompleteText.toLowerCase()) === -1 + ? renderOption({[keyPropName]: autocompleteText, [displayPropName]: autocompleteText}, 'freeText') + : null; + }, [autocompleteText, source.tableData]); + + const list = useMemo(() => { + const list = source?.tableData + // filter already selected items to avoid duplicate keys in options + ?.filter((x) => !keys.find((y) => isEqual(y, keyValueFunc(outcomeValueFunc(x, allData), allData)))) + ?? []; + + // render grouped data + if (props.grouping && source?.tableData?.length) { + const groupProp = props.grouping.propertyName; + const groups = uniqWith(source?.tableData.map(row => getValueByPropertyName(row, groupProp)), (a, b) => isEqual(a, b)); + const res = <> + {groups.map((group, index) => { + const groupTitle = renderGroupTitle(group, groupProp) ?? 'empty'; + return + {list.filter((x) => isEqual(getValueByPropertyName(x, groupProp), group)).map((row, index) => renderOption(row, index))} + ; + })} + ; + return res; + } - return { - value: itemValue.id, - label: item?.label ?? itemValue._displayName, - data: itemValue, - }; + return <> + {list.map((row, index) => renderOption(row, index))} + {props.dataSourceType === 'entitiesList' && source?.totalRows > 7 + && {`Total found: ${source?.totalRows} ...`}} + ; + }, [selected.current, source?.tableData, props.grouping]); + + const title = useMemo(() => { + return selected.current.length === 1 ? displayValueFunc(selected.current[0], allData) : null; + }, [selected.current]); + + const onDropdownVisibleChange = (open: boolean) => { + if (!open) { + setOpen(false); + } else { + const selectedValue = selected.current?.length + ? selected.current.map((s) => outcomeValueFunc(s, allData)) + : undefined; + const selectedFilter = selectedValue ? filterNotKeysFunc(selectedValue) : null; + source?.setPredefinedFilters([{ id: 'selectedFilter', name: 'selectedFilter', expression: selectedFilter }]); + setOpen(true); + } }; + + if (props.readOnly) { + if (!props.value) + return null; + const readonlyValue = props.mode === 'multiple' + ? selected.current?.map((x) => ({ label: displayValueFunc(x, allData), value: keyValueFunc(outcomeValueFunc(x, allData), allData) })) + : { + id: keyValueFunc(outcomeValueFunc(selected.current[0], allData), allData), + _displayName: displayValueFunc(selected.current[0], allData), + _className: selected.current[0]?._className + }; + return ( + + ); + } + + const {width, ...restOfDropdownStyles} = style ?? {}; + return ( - + <> + + ); }; +const Autocomplete: FC = (props: IAutocompleteProps) => { + const { formData } = useShaFormInstance(false) ?? {}; + const disableRefresh = useRef(true); + const [searchText, setSearchText] = useState(''); + const uid = useId(); -// RawAutocomplete updated to use with input data in the IEntityReferenceDto format. -// Necessary for JsonEntity because it stores EntityReference in the IEntityReferenceDto format -// and form designer migth not know which exactly format should be used -// It still returns data in the raw format -export type IRawAutocompleteType = string | IEntityReferenceDto; + const fields = [...(props.fields ?? [])]; + if (props.displayPropName && fields.findIndex((x) => x === props.displayPropName) === -1) + fields.push(props.displayPropName); + if (props.keyPropName && fields.findIndex((x) => x === props.keyPropName) === -1) + fields.push(props.keyPropName); + if (props.dataSourceType === 'entitiesList') { + if (fields.findIndex((x) => x === 'id') === -1) + fields.push('id'); + if (fields.findIndex((x) => x === '_className') === -1) + fields.push('_className'); + if (fields.findIndex((x) => x === '_displayName') === -1) + fields.push('_displayName'); + } + if (props.grouping && fields.findIndex((x) => x === props.grouping.propertyName) === -1) + fields.push(props.grouping.propertyName); -export const RawAutocomplete = (props: IAutocompleteProps) => { - const getDtoFromFetchedItem = (item): ISelectOption => { - return { - value: item['value'], - label: item['displayText'], - data: item['value'], - }; - }; + const q = { q: isPropertySettings(props.queryParams) ? { ...props.queryParams } : props.queryParams }; + const queryParams = useActualContextData(q, null, { searchText, value: props.value })?.q; + + const queryParamsObj = useDeepCompareMemo(() => { + const queryParamObj = {}; + if (props.dataSourceType === 'url') { + if (queryParams && queryParams !== null && typeof (queryParams) === 'object') { + if (Array.isArray(queryParams)) { + queryParams.forEach(({ param, value }) => { + const valueAsString = value as string; + if (param?.length && valueAsString.length) { + queryParamObj[param] = /{.*}/i.test(valueAsString) ? evaluateString(valueAsString, { data: formData }) : value; + } + }); + } else + Object.assign(queryParamObj, queryParams); + } + // add autocomplete standard query params if not exists + if (queryParamObj['term'] === undefined) + queryParamObj['term'] = searchText; + if (queryParamObj['selectedValue'] === undefined) + queryParamObj['selectedValue'] = props.value; + } + return queryParamObj; + }, [props.dataSourceType, queryParams, formData, searchText]); + + + const key = getUrlKeyParam(props.dataSourceUrl); - const labeledValueGetter = (itemValue: IRawAutocompleteType , options: ISelectOption[]) => { - var val = (itemValue as IEntityReferenceDto)?.id ?? itemValue as string; - if (!Boolean(val)) return null; - const item = options?.find(i => i.value === val); + const url = useDeepCompareMemo(() => { + return props.dataSourceUrl + ? `${props.dataSourceUrl}${key}${QueryString.stringify(queryParamsObj)}` + : null; + }, [props.dataSourceUrl, queryParamsObj]); - return { - value: val, - label: item?.label ?? (itemValue as IEntityReferenceDto)?._displayName ?? 'unknown', - data: itemValue, - } ; + const handleSearch = (searchText: string) => { + setSearchText(searchText); }; return ( - - getOptionFromFetchedItem={getDtoFromFetchedItem} - getLabeledValue={labeledValueGetter} + + + + ); +}; + +/** + * @deprecated The method should not be used + */ +export const EntityDtoAutocomplete = (props: IAutocompleteProps) => { + return ( + + ); +}; + +/** + * @deprecated The method should not be used + */ +export const RawAutocomplete = (props: IAutocompleteProps) => { + return ( + ); }; type InternalAutocompleteType = typeof Autocomplete; interface IInternalAutocompleteInterface extends InternalAutocompleteType { + /** + * @deprecated The method should not be used, please use Autocomplete + */ Raw: typeof RawAutocomplete; + /** + * @deprecated The method should not be used, please use Autocomplete + */ EntityDto: typeof EntityDtoAutocomplete; } + const AutocompleteInterface = Autocomplete as IInternalAutocompleteInterface; AutocompleteInterface.Raw = RawAutocomplete; AutocompleteInterface.EntityDto = EntityDtoAutocomplete; @@ -99,5 +419,5 @@ export { type IAutocompleteProps, type ISelectOption, type AutocompleteDataSourceType, - type CustomLabeledValue + //type CustomLabeledValue }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/autocomplete/models.tsx b/shesha-reactjs/src/components/autocomplete/models.tsx index a3068621de..83045115ea 100644 --- a/shesha-reactjs/src/components/autocomplete/models.tsx +++ b/shesha-reactjs/src/components/autocomplete/models.tsx @@ -1,196 +1,133 @@ -import React, { ReactNode, Key } from 'react'; -import { SizeType } from 'antd/lib/config-provider/SizeContext'; -import { LabeledValue } from 'antd/lib/select'; -import { IReadOnly } from '@/interfaces/readOnly'; -import { FormIdentifier } from '@/providers'; +import { FormIdentifier, IEntityReferenceDto } from "@/index"; +import { IDataColumnsProps } from "@/providers/datatableColumnsConfigurator/models"; +import { Key, MutableRefObject, ReactNode } from "react"; +import { GroupingItem, ISortingItem } from "@/providers/dataTable/interfaces"; +import { SizeType } from "antd/lib/config-provider/SizeContext"; + +/** + * Converts array of strings into IDataColumnsProps array + * @param fields - array of strings + * @returns IDataColumnsProps array + */ +export function getColumns(fields: string[]): IDataColumnsProps[] { + return (fields ?? []).map((field, index) => { + return { + id: `${index}`, + columnType: 'data', + caption: '', + sortOrder: index, + isVisible: true, + itemType: 'item', + propertyName: field, + allowSorting: false, + }; + }); +} export type AutocompleteDataSourceType = 'entitiesList' | 'url'; +export type QueryParamFunc = (searchText: string, selected: any[]) => object; +export type FilterSelectedFunc = (value: any) => object; +export type KayValueFunc = (value: any, args: any) => string; +export type DisplayValueFunc = (value: any, args: any) => string; +export type OutcomeValueFunc = (value: any, args: any) => string | string[] | IEntityReferenceDto | IEntityReferenceDto[] | any; + export interface ISelectOption { // TODO: make generic value: string | number; label: string | React.ReactNode; data: TValue; + color?: string; + icon?: string; } -interface IQueryParams { - // tslint:disable-next-line:typedef-whitespace - [prop: string]: Key; +interface IQueryParamProp { + id: string; + param?: string; + value?: Key; } -export type CustomLabeledValue = LabeledValue & { data: TValue }; +export interface IAutocompleteBaseProps { + disableRefresh?: MutableRefObject; -export interface ICommonAutocompleteProps extends IReadOnly { - /** - * The value of the autocomplete - * - * If the value is of this form, then we do not need to fetch items from the server - */ - value?: TValue | TValue[]; + uid: string; + onChange?: (value: any) => void; + onSearch?: (searchText: string) => void; + value?: any; - /** - * Default value - */ - defaultValue?: TValue | TValue[]; - - /** - * Get option from an item fetched from the back-end - */ - getOptionFromFetchedItem?: (fetchedItem: object) => ISelectOption; - - /** - * Get CustomLabeledValue from value - */ - getLabeledValue?: (value: TValue, options: ISelectOption[]) => CustomLabeledValue; - - /** - * Specify content to show when no result matches - */ - notFoundContent?: ReactNode; - - /** - * The placeholder to display on the autocomplete - */ + /** Type of entity */ + entityType?: string; + /** Data source type */ + dataSourceType: AutocompleteDataSourceType; + /** Data source URL (required for dataSourceType === 'url', alternative for dataSourceType === 'entitiesList') */ + dataSourceUrl?: string; + /** Placeholder */ placeholder?: string; - - /** - * A callback for when the value of this component changes - */ - onChange?: any; - - /** - * Whether this control is disabled - */ - disabled?: boolean; - - /** - * Wether the component is bordered - */ - bordered?: boolean; - - /** - * Styles to apply to the select component that gets rendered by this control - */ - style?: React.CSSProperties; - - /** - * The size of the control - */ - size?: SizeType; - - /** - * The size of the control - */ - mode?: 'multiple' | 'single'; - - allowClear?: boolean; - - /** - * If true, the automplete will be in read-only mode. This is not the same sa disabled mode - */ + /** Hide border */ + hideBorder?: boolean; + /** A property used as label */ + displayPropName?: string; + /** A property used as key/value */ + keyPropName?: string; + /** Permanent filter (json logig) */ + filter?: object; + /** Read only */ readOnly?: boolean; - - /** - * If true, search mode will be disabled for the autocomplete - */ + /** Disable text search */ disableSearch?: boolean; - - /** - * - */ - readOnlyMultipleMode?: 'raw' | 'tags'; - - /** - * @deprecated - * A list of event names which, when triggered, will trigger the autocomplete to refetch items - */ - subscribedEventNames?: string[]; -} - -export interface IAutocompleteProps - extends IEntityAutocompleteProps, - IUrlAutocompleteProps { - /** - * Data source of this Autocomplete - */ - dataSourceType: AutocompleteDataSourceType; - className?: string; -} - -export interface IEntityAutocompleteProps extends ICommonAutocompleteProps { - /** - * The short alias if this is a reference list - */ - typeShortAlias?: string; - - /** - * Name of the property to display. Live empty to use default display name property defined on the back-end - */ - entityDisplayProperty?: string; - - /** - * Applies if this is a reference list - */ - allowInherited?: boolean; - - /** - * Deteremines if quickview is enabled when in read only mode - */ + /** Selection mode */ + mode?: 'single' | 'multiple'; + /** Fields to fetch */ + fields?: string[]; + /** Query params, applicable only for dataSourceType === 'url' */ + queryParams?: IQueryParamProp[]; + + /** Quickview setting */ + /** Use Quickview */ quickviewEnabled?: boolean; - - /** - * Specifies the form to use when quickview is enabled - */ + /** Form path */ quickviewFormPath?: FormIdentifier; - - /** - * Specifies the form to use a specific module - */ - module?: string; - /** - * Specifies which property to display for the quickview - */ + /** A property used as label, шf empty, the displayPropName field is used. */ quickviewDisplayPropertyName?: string; - - /** - * The Url that details of the entity are retreived - */ + /** Get Entity details Url */ quickviewGetEntityUrl?: string; - - /** - * The width of the quickview - */ + /** Quickview form width */ quickviewWidth?: number; - filter?: string; -} + /** Not found content */ + notFoundContent?: ReactNode; + /** Style */ + style?: React.CSSProperties; + /** Filter (json logic) that used for filter selected values */ + filterKeysFunc?: FilterSelectedFunc | null | undefined; + /** Function for get key (string) from value (outcome value format) */ + keyValueFunc?: KayValueFunc | null | undefined; + /** Function for get label from item (received from the backend)*/ + displayValueFunc?: DisplayValueFunc | null | undefined; + /** Function for get value (outcome value format) from item (received from the backend) */ + outcomeValueFunc?: OutcomeValueFunc | null | undefined; + /** Sorting */ + sorting?: ISortingItem[]; + /** Grouping */ + grouping?: GroupingItem; + /** Size */ + size?: SizeType; -export interface IUrlAutocompleteProps extends ICommonAutocompleteProps { - /** - * Data source url. Applies if `dataSourceType` is `url` - */ - dataSourceUrl?: string; + allowFreeText?: boolean; + allowClear?: boolean; - /** - * Query string params - */ - queryParams?: IQueryParams; + // not implemented + defaultValue?: any | any[]; + + // need to review (not used) + allowInherited?: boolean; /** - * If true, autocomplete allows to use free text that is missing in the source + * @deprecated */ - allowFreeText?: boolean; + typeShortAlias?: string; } -export interface IUrlFetcherQueryParams { - term?: string | null; - selectedValue?: string | null; -} +export interface IAutocompleteProps extends Omit { -/** - * Generic DTO of the simple autocomplete item - */ -export interface AutocompleteItemDto { - value?: string | null; - displayText?: string | null; -} +} \ No newline at end of file diff --git a/shesha-reactjs/src/components/autocomplete/style.ts b/shesha-reactjs/src/components/autocomplete/style.ts new file mode 100644 index 0000000000..eb5cb1fa2a --- /dev/null +++ b/shesha-reactjs/src/components/autocomplete/style.ts @@ -0,0 +1,23 @@ +import { createStyles } from '@/styles'; +import { CSSProperties } from 'react'; + +export const useStyles = createStyles(({ css, cx, token }, { style }: { style: CSSProperties }) => { + + const autocomplete = cx("sha-autocomplete", css` + --ant-color-text: ${style?.color} !important; + --ant-font-size: ${style?.fontSize} !important; + --ant-font-weight-strong: ${style?.fontWeight} !important; + + .ant-select-selection-item { + font-weight: var(--ant-font-weight) !important; + } + + &.ant-select-open, &:hover { + border-color: ${token.colorPrimary} !important; + } + `); + + return { + autocomplete + }; +}); \ No newline at end of file diff --git a/shesha-reactjs/src/components/autocomplete/urlAutocomplete.tsx b/shesha-reactjs/src/components/autocomplete/urlAutocomplete.tsx deleted file mode 100644 index 89631d0f97..0000000000 --- a/shesha-reactjs/src/components/autocomplete/urlAutocomplete.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import React, { - useEffect, - useMemo, - useRef, - useState - } from 'react'; -import { getQueryParams, getUrlWithoutQueryParams } from '@/utils/url'; -import { IAjaxResponseBase } from '@/interfaces/ajaxResponse'; -import { ReadOnlyDisplayFormItem } from './../readOnlyDisplayFormItem'; -import { Select } from 'antd'; -import { useDebouncedCallback } from 'use-debounce'; -import { useGet } from '@/hooks'; -import { - AutocompleteItemDto, - CustomLabeledValue, - ISelectOption, - IUrlAutocompleteProps, - IUrlFetcherQueryParams, -} from './models'; -import { isEqual } from 'lodash'; - -export const UrlAutocomplete = (props: IUrlAutocompleteProps) => { - const { - value, - defaultValue, - placeholder, - onChange, - disabled, - bordered = true, - dataSourceUrl, - style, - size, - mode, - notFoundContent, - queryParams: incomingQueryParams, - getOptionFromFetchedItem, - getLabeledValue, - readOnly, - //readOnlyMultipleMode = 'raw', - disableSearch, - allowFreeText = false, - } = props; - - const urlFetcher = useGet( - decodeURI(getUrlWithoutQueryParams(dataSourceUrl)) || '', - { - lazy: true, - } - ); - - const selectRef = useRef(null); - const previousQueryParams = useRef(null); - - const [autocompleteText, setAutocompleteText] = useState(null); - - const additionalQueryParams = incomingQueryParams || {}; - - const doFetchItems = (term: string, ignoreSelectedValue = false) => { - const selectedValue = - typeof value === 'string' && !ignoreSelectedValue - ? value - : /*: isStringArray(value) - ? value*/ - undefined; - - if (dataSourceUrl) { - const queryParams = { - ...getQueryParams(dataSourceUrl), - term, - selectedValue, - ...additionalQueryParams, - }; - - if (isEqual(queryParams, previousQueryParams.current)) - return; - - previousQueryParams.current = queryParams; - urlFetcher.refetch({ - queryParams, - }); - } - }; - - useEffect(() => { - doFetchItems(null); - }, []); - - useEffect(() => { - if (value && !autocompleteText) { - // Refresh the autocomplete whenever the value changes to - // TODO: Review this by, maybe, passing renderWhenDataIsReady to the dialog form so that items are not fetched may times - doFetchItems(null); - } - }, [value]); - - const getFetchedItems = (): AutocompleteItemDto[] => { - return urlFetcher.data?.result; - }; - - // Refetch when clear because at this stage, only 1 item (which is the previsously selected item) is in the list of options - - const onFocus = () => { - doFetchItems(null, true); - }; - - const handleSelect = () => { - selectRef.current.blur(); - }; - - const debouncedFetchItems = useDebouncedCallback<(value: string) => void>( - (localValue) => { - doFetchItems(localValue); - }, - // delay in ms - 200 - ); - - const wrapValue = (localValue: TValue | TValue[], allOptions: ISelectOption[]): CustomLabeledValue | CustomLabeledValue[] => { - if (!Boolean(localValue)) return undefined; - if (mode === 'multiple') { - return Array.isArray(localValue) - ? (localValue as TValue[]).map>((o) => { - return getLabeledValue(o, allOptions); - }) - : [getLabeledValue(localValue as TValue, allOptions)]; - } else return getLabeledValue(localValue as TValue, allOptions); - }; - - const options = useMemo[]>(() => { - const fetchedData = getFetchedItems() || []; - - const fetchedItems = fetchedData.map>((item) => { - const option = Boolean(getOptionFromFetchedItem) - ? (getOptionFromFetchedItem(item) as ISelectOption) - : (item as ISelectOption); - - return option; - }); - - const selectedItem = wrapValue(value, fetchedItems); - - // Remove items which are already exist in the fetched items. - // Note: we shouldn't process full list and make it unique because by this way we'll hide duplicates received from the back-end - const selectedItems = selectedItem - ? (Array.isArray(selectedItem) ? selectedItem : [selectedItem]).filter( - (i) => fetchedItems.findIndex((fi) => fi.value === i.value) === -1 - ) - : []; - - const result = [...fetchedItems, ...selectedItems]; - - if (autocompleteText && allowFreeText && !value) { - if (fetchedItems.findIndex((fi) => fi.label === autocompleteText) === -1) { - result.push({ label: autocompleteText, value: autocompleteText, data: autocompleteText }); - } - } - - return result; - }, [value, autocompleteText, urlFetcher]); - - const handleSearch = (localValue: string) => { - setAutocompleteText(localValue); - if (localValue) { - debouncedFetchItems(localValue); - } - }; - - const handleChange = (_value: CustomLabeledValue, option: any) => { - if (!Boolean(onChange)) return; - const selectedValue = Boolean(option) - ? Array.isArray(option) - ? (option as ISelectOption[]).map((o) => o.data) - : (option as ISelectOption).data - : undefined; - - if (mode === 'multiple') { - onChange(Array.isArray(selectedValue) ? selectedValue : [selectedValue]); - } else onChange(selectedValue); - }; - - /* - if (readOnly) { - const wrappedValue = wrapValue(value); - - let displayValue: any; - - if (Array.isArray(wrappedValue)) { - displayValue = wrappedValue?.map(({ label, value: keyId }: any) => - readOnlyMultipleMode === 'raw' && typeof label === 'string' ? label : {label} - ); - - if (readOnlyMultipleMode === 'raw') displayValue = (displayValue as any[])?.join(', '); - } else { - displayValue = (wrappedValue as any)?.label; - } - } - */ - const autocompleteValue = wrapValue(value, options); - - if (readOnly) { - return ( - - ); - } - - return ( - | CustomLabeledValue[]> - className="sha-dropdown" - showSearch={!disableSearch} - labelInValue={true} - notFoundContent={notFoundContent} - defaultActiveFirstOption={false} - filterOption={false} - onSearch={handleSearch} - defaultValue={wrapValue(defaultValue, options)} - value={autocompleteValue} - onChange={handleChange} - allowClear={true} - onFocus={onFocus} - loading={urlFetcher?.loading} - placeholder={placeholder} - disabled={disabled} - variant={!bordered ? 'borderless' : undefined } - onSelect={handleSelect} - style={style} - size={size} - ref={selectRef} - mode={value && mode === 'multiple' ? mode : undefined} // When mode is multiple and value is null, the control shows an empty tag - > - {options?.map(({ value: localValue, label, data }) => ( - - {label} - - ))} - - ); -}; diff --git a/shesha-reactjs/src/components/autocompleteTagGroup/index.tsx b/shesha-reactjs/src/components/autocompleteTagGroup/index.tsx index c46c5bd668..edfb602041 100644 --- a/shesha-reactjs/src/components/autocompleteTagGroup/index.tsx +++ b/shesha-reactjs/src/components/autocompleteTagGroup/index.tsx @@ -112,7 +112,6 @@ export const AutocompleteTagGroup: FC = = (props) => { export interface IButtonGroupItemProps { item: IButtonGroupItem; + actualModelContext?: any; actionConfiguration?: IConfigurableActionConfiguration; } export const ButtonGroupItem: FC = ({ item, actionConfiguration }) => { + const { backendUrl, httpHeaders } = useSheshaApplication(); const { styles } = useStyles(); const actualItem = useActualContextData({ ...item, actionConfiguration }); - const { icon, label, tooltip, iconPosition, size, buttonType, borderColor, borderRadius, height, width, backgroundColor, fontSize, fontWeight, color, borderStyle, borderWidth, readOnly, style: itemStyle, block, danger } = actualItem; - const newStyles = { + const { icon, label, tooltip, iconPosition, size, buttonType, borderColor, borderRadius, + height, width, backgroundColor, fontSize, fontWeight, color, borderStyle, borderWidth, + readOnly, block, danger } = actualItem; + + const model = { + ...actualItem, + type: 'button', width: addPx(width), height: addPx(height), backgroundColor: backgroundColor, @@ -49,10 +63,58 @@ export const ButtonGroupItem: FC = ({ item, actionConfigu borderRadius: addPx(borderRadius) }; + + const prevStyles = migratePrevStyles(model, initialValues()); + const dimensions = prevStyles?.dimensions; + const border = prevStyles?.border; + const font = prevStyles?.font; + const shadow = prevStyles?.shadow; + const background = prevStyles?.background; + const styling = JSON.parse(model.stylingBox || '{}'); + + const dimensionsStyles = useMemo(() => getDimensionsStyle(dimensions), [dimensions]); + const jsStyle = useMemo(() => getStyle(model.style), [model.style]); + const borderStyles = useMemo(() => getBorderStyle(border, jsStyle), [border, jsStyle]); + const fontStyles = useMemo(() => getFontStyle(font), [font]); + const [backgroundStyles, setBackgroundStyles] = useState({}); + const shadowStyles = useMemo(() => getShadowStyle(shadow), [shadow]); + const stylingBoxAsCSS = pickStyleFromModel(styling); + + useEffect(() => { + const fetchStyles = async () => { + + const storedImageUrl = background?.storedFile?.id && background?.type === 'storedFile' + ? await fetch(`${backendUrl}/api/StoredFile/Download?id=${background?.storedFile?.id}`, + { headers: { ...httpHeaders, "Content-Type": "application/octet-stream" } }) + .then((response) => { + return response.blob(); + }) + .then((blob) => { + return URL.createObjectURL(blob); + }) : ''; + + const style = getBackgroundStyle(background, jsStyle, storedImageUrl); + setBackgroundStyles(style); + }; + + fetchStyles(); + }, [background, httpHeaders, backendUrl, jsStyle]); + + const newStyles = { + ...dimensionsStyles, + ...(['primary', 'default'].includes(item.buttonType) && borderStyles), + ...fontStyles, + ...(['dashed', 'default'].includes(item.buttonType) && backgroundStyles), + ...(['primary', 'default'].includes(item.buttonType) && shadowStyles), + ...jsStyle, + ...stylingBoxAsCSS, + justifyContent: font?.align, + }; + return ( <> {item.itemSubType === 'button' && ( - <> + @@ -72,7 +134,7 @@ export const ButtonGroupItem: FC = ({ item, actionConfigu )} - + )} {item.itemSubType === 'separator' && (— separator —)} {isDynamicItem(item) && ()} diff --git a/shesha-reactjs/src/components/buttonGroupConfigurator/buttonGroupItemsGroup.tsx b/shesha-reactjs/src/components/buttonGroupConfigurator/buttonGroupItemsGroup.tsx index 64fbb2c288..569a606918 100644 --- a/shesha-reactjs/src/components/buttonGroupConfigurator/buttonGroupItemsGroup.tsx +++ b/shesha-reactjs/src/components/buttonGroupConfigurator/buttonGroupItemsGroup.tsx @@ -5,7 +5,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons'; import ShaIcon, { IconType } from '@/components/shaIcon'; import { useStyles } from '@/components/listEditor/styles/styles'; import { ItemChangeDetails } from '../listEditor'; -import { useActualContextData } from '@/hooks/useActualContextData'; +import { useActualContextData } from '@/hooks'; export interface IContainerRenderArgs { index?: number[]; diff --git a/shesha-reactjs/src/components/buttonGroupConfigurator/buttonGroupSettingsEditor.tsx b/shesha-reactjs/src/components/buttonGroupConfigurator/buttonGroupSettingsEditor.tsx index 3d8a8b268c..80fd75f91e 100644 --- a/shesha-reactjs/src/components/buttonGroupConfigurator/buttonGroupSettingsEditor.tsx +++ b/shesha-reactjs/src/components/buttonGroupConfigurator/buttonGroupSettingsEditor.tsx @@ -8,6 +8,7 @@ import { Alert, Button, Divider } from 'antd'; import { useStyles } from '@/designer-components/_common/styles/listConfiguratorStyles'; import { isGroup } from '@/providers/buttonGroupConfigurator/models'; import { ListEditorWithPropertiesPanel } from '../listEditorWithPropertiesPanel'; +import { initialValues } from './utils'; export interface ButtonGroupSettingsEditorProps { readOnly: boolean; @@ -79,7 +80,8 @@ export const ButtonGroupSettingsEditor: FC = ({ label: `Button ${itemNo}`, itemSubType: 'button', buttonType: 'link', - editMode: 'inherited' + editMode: 'inherited', + ...initialValues() }; return newItem; @@ -93,7 +95,7 @@ export const ButtonGroupSettingsEditor: FC = ({ readOnly={readOnly} header={} itemProperties={(itemProps) => ()} - groupHeader={ButtonGroupEditorHeader} + groupHeader={ButtonGroupEditorHeader} > {({ item, itemOnChange, index, nestedRenderer }) => ( void; title?: ReactNode | string; + size?: SizeType; } interface IButtonGroupConfiguratorProps extends IToolbarSettingsModal { - + size?: SizeType; } export const ButtonGroupConfigurator: FC = ({ value, onChange, readOnly, + size, title = 'Buttons Configuration', }) => { const isSmall = useMedia('(max-width: 480px)'); @@ -43,12 +46,12 @@ export const ButtonGroupConfigurator: FC = ({ return ( - + { + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: 'W_m7doMyCpCYwAYDfRh6I', + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: 's4gmBg31azZC0UjZjpfTm', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'f061d971-8b38-4b82-b192-563259afc159', + parentId: 's4gmBg31azZC0UjZjpfTm', + inputType: 'textField', + propertyName: 'name', + label: 'Group Name', + jsSetting: false, + validate: { + required: true + }, + }) + .addSettingsInputRow({ + id: 'label-tooltip-s4gmBg31azZC0UjZjpfTm', + parentId: 's4gmBg31azZC0UjZjpfTm', + inputs: [ + { + id: "A-qcRVk-qlnGDLtFvK-2X", + type: "textField", + propertyName: "label", + parentId: "root", + label: "Label" + }, + { + id: "rupsZ1fuRwqetjQ0BC5sk", + type: "textArea", + propertyName: "tooltip", + label: "Group Tooltip", + labelAlign: "right", + parentId: "root", + hidden: false, + allowClear: false + } + ] + }) + .addSettingsInputRow({ + id: 'icon-drop-icon-s4gmBg31azZC0UjZjpfTm', + parentId: 's4gmBg31azZC0UjZjpfTm', + inputs: [ + { + id: "XZ_7OvzMkfC3GEzzrduwu", + type: "iconPicker", + propertyName: "icon", + label: "Icon", + labelAlign: "right", + parentId: "root", + hidden: false, + settingsValidationErrors: [] + }, + { + id: "YA_9x3zMkfC3GEzzrduwu", + type: "iconPicker", + propertyName: "downIcon", + label: "Down Icon", + labelAlign: "right", + parentId: "root", + hidden: false, + settingsValidationErrors: [] + } + ], + }) + .addSettingsInput({ + id: "LKVj_90JMwALpmLN_6iZn", + inputType: "dropdown", + propertyName: "buttonType", + label: "Button Type", + labelAlign: "right", + parentId: "root", + hidden: false, + validate: {}, + dropdownOptions: buttonTypes, + jsSetting: false + }) + .addSettingsInputRow({ + id: '12d700d6-ed4d-49d5-9cfd-fe8f0060f3b6', + parentId: 's4gmBg31azZC0UjZjpfTm', + inputs: [ + { + type: 'editModeSelector', + id: 'editMode-s4gmBg31azZC0UjZjpfTm', + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + }, + { + type: 'switch', + id: 'hidden-s4gmBg31azZC0UjZjpfTm', + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .addSettingsInput({ + id: 'hide-when-empty-s4gmBg31azZC0UjZjpfTm', + inputType: 'switch', + label: 'Hide When Empty', + propertyName: 'hideWhenEmpty', + jsSetting: true, + }) + .toJson() + ] + }, + { + key: '2', + title: 'Security', + id: '6Vw9iiDw9d0MD_Rh5cbIn', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: '1adea529-1f0c-4def-bd41-ee166a5dfcd7', + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: '6Vw9iiDw9d0MD_Rh5cbIn' + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/buttonGroupConfigurator/itemSettings.json b/shesha-reactjs/src/components/buttonGroupConfigurator/itemSettings.json deleted file mode 100644 index 08b658b5b9..0000000000 --- a/shesha-reactjs/src/components/buttonGroupConfigurator/itemSettings.json +++ /dev/null @@ -1,466 +0,0 @@ -{ - "components": [ - { - "id": "840aee56-42d2-40ed-a2c6-57abb255fb95", - "type": "dropdown", - "propertyName": "itemSubType", - "label": "Item Type", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "dataSourceType": "values", - "jsSetting": false, - "values": [ - { - "id": "8400a8ec-577d-4468-9347-5601f952b44c", - "label": "Button", - "value": "button" - }, - { - "id": "cc268f50-47ca-4e4e-966f-7f2abfec902f", - "label": "Separator", - "value": "separator" - }, - { - "id": "58B3C738-46C7-4486-BDE6-22D99C8349BE", - "label": "Dynamic item(s)", - "value": "dynamic" - } - ], - "validate": { - "required": true - }, - "version": 2, - "referenceListId": null - }, - { - "id": "345c2181-fb9f-48ed-9631-864ac357807b", - "type": "container", - "propertyName": "cntButton", - "label": "Container2", - "labelAlign": "right", - "parentId": "root", - "hidden": { - "_value": false, - "_code": "return getSettingValue(data?.itemSubType) !== 'button';", - "_mode": "code" - }, - "direction": "vertical", - "justifyContent": "left", - "components": [ - { - "id": "5d4d56fb-d7f8-4835-a529-c4fa93f3596d", - "type": "textField", - "propertyName": "name", - "label": "Name", - "labelAlign": "right", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "jsSetting": false, - "validate": { - "required": true - }, - "version": 1, - "textType": "text" - }, - { - "id": "mwI8bhzkGyQOVpkttIKyc", - "type": "textField", - "propertyName": "label", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "label": "Label" - }, - { - "id": "po1qeQwn48ykEB4EI9J8y", - "type": "textArea", - "propertyName": "tooltip", - "label": "Tooltip", - "labelAlign": "right", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {} - }, - { - "id": "91b404a6-4021-4b0a-b9ef-007167a93075", - "type": "iconPicker", - "propertyName": "icon", - "label": "Icon", - "labelAlign": "right", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "settingsValidationErrors": [], - "version": 0, - "jsSetting": true - }, - { - "type": "dropdown", - "id": "5a6d6b4d-7d8e-4b5d-8b0b-0f9a6c1d1f4b", - "propertyName": "iconPosition", - "label": "Icon Position", - "labelAlign": "right", - "parentId": "345c2181-fb9f-48ed-9631-864ucl57807b", - "hidden": false, - "validate": {}, - "dataSourceType": "values", - "values": [ - { - "id": "f5e5b8e2-0b7b-4f7f-8b1f-0e8d7d9amg1a", - "label": "start", - "value": "start" - }, - { - "id": "c9b1e4f7-6e8d-4f4f-8b1f-0e8d7da45d1a", - "label": "end", - "value": "end" - } - ] - }, - { - "id": "Vl0_92ZM-SeCukk5VlgXm", - "type": "dropdown", - "propertyName": "buttonType", - "label": "Button Type", - "labelAlign": "right", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "hidden": false, - "validate": { - "required": true - }, - "dataSourceType": "values", - "values": [ - { - "id": "c4a96833-8ed7-4085-8848-169d5607136d", - "label": "primary", - "value": "primary" - }, - { - "id": "c6f974da-ad28-44e5-8e4d-50280cf24ae7", - "label": "ghost", - "value": "ghost" - }, - { - "id": "71c0dc14-0473-4748-ae75-a4ed3bd6cffd", - "label": "dashed", - "value": "dashed" - }, - { - "id": "789d5733-2d4f-4969-890f-613e5b4a7d59", - "label": "link", - "value": "link" - }, - { - "id": "36abe636-40b2-476c-95b0-78a50478146b", - "label": "text", - "value": "text" - }, - { - "id": "de08ea36-a831-4373-ab10-ce25fadf80cd", - "label": "default", - "value": "default" - } - ], - "jsSetting": false - }, - { - "id": "oXQyiI2WU5P0E4lVO-uLW", - "type": "checkbox", - "propertyName": "danger", - "label": "Danger", - "labelAlign": "right", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "hidden": false, - "validate": {} - }, - { - "id": "24a8be15-98eb-40f7-99ea-ebb602693e9c", - "type": "editModeSelector", - "propertyName": "editMode", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "label": "Edit mode" - }, - { - "id": "oW5driq-fQt8oyUTeB8rX", - "type": "checkbox", - "propertyName": "hidden", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "label": "Hidden" - }, - { - "id": "F3B46A95-703F-4465-96CA-A58496A5F78C", - "type": "configurableActionConfigurator", - "propertyName": "actionConfiguration", - "label": "Action configuration", - "hidden": false, - "validate": {}, - "settingsValidationErrors": [], - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "version": 0 - }, - { - "id": "6befdd49-41aa-41d6-a29e-76fa00590b75", - "type": "sectionSeparator", - "propertyName": "sectionStyle", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "label": "Style", - "version": 0 - }, - { - "id": "06ab0599-914d-4d2d-875c-765a495472f8", - "type": "codeEditor", - "propertyName": "style", - "label": "Style", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "validate": {}, - "settingsValidationErrors": [], - "description": "A script that returns the style of the element as an object. This should conform to CSSProperties", - "exposedVariables": [ - { - "name": "data", - "description": "Form values", - "type": "object" - } - ], - "version": 0 - }, - { - "id": "8615d12f-6ea0-4b11-a1a1-6088c7160fd9", - "type": "dropdown", - "propertyName": "size", - "parentId": "root", - "label": "Size", - "allowClear": true, - "version": 3, - "dataSourceType": "values", - "values": [ - { - "label": "Small", - "value": "small", - "id": "4f11403c-95fd-4e49-bb60-cb8c25f0f3c3" - }, - { - "label": "Middle", - "value": "middle", - "id": "8f85c476-e632-4fa7-89ad-2be6cfb7f1f1" - }, - { - "label": "Large", - "value": "large", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - } - ] - }, - { - "id": "d498779d-012a-4c6a-82a9-77231001ae28", - "type": "textField", - "propertyName": "width", - "parentId": "root", - "label": "Width" - }, - { - "id": "d498779d-012a-4c6a-82a9-771121245ae28", - "type": "textField", - "propertyName": "height", - "parentId": "root", - "label": "Height" - }, - { - "id": "d4987360d-012a-4c6a-82a9-77231245ae28", - "type": "colorPicker", - "propertyName": "backgroundColor", - "parentId": "root", - "label": "Background Color", - "allowClear": true, - "version": 3 - }, - { - "id": "d498779d-012a-4c6a-82a9-77231234ve28", - "type": "textField", - "propertyName": "fontSize", - "parentId": "root", - "label": "Font Size" - }, - { - "id": "d498779d-012p-4c6a-82a9-7723124uae28", - "type": "colorPicker", - "propertyName": "color", - "parentId": "root", - "label": "Color", - "allowClear": true, - "version": 3 - }, - { - "id": "d498779d-012a-4c6a-82c9-00731245ae28", - "type": "dropdown", - "propertyName": "fontWeight", - "parentId": "root", - "label": "Font Weight", - "description": "Defines the thickness of font characters. Use values like 400 for normal, 100 for thin, 500 for medium, 700 for bold, and 900 for extra bold when working with JS.", - "dataSourceType": "values", - "values": [ - { - "id": "c4a96833-8ed7-4085-8848-169d5607136d", - "label": "Thin", - "value": "100" - }, - { - "id": "c6f974da-ad28-44e5-8e4d-50280cf24ae7", - "label": "Normal", - "value": "400" - }, - { - "id": "71c0dc14-0473-4748-ae75-a4ed3bd6c0fd", - "label": "Medium", - "value": "500" - }, - { - "id": "71c0dc14-0473-4748-ae75-a4ed3bd90ffd", - "label": "Bold", - "value": "700" - }, - { - "id": "71c04514-0473-4748-ae75-00ed3bd6cffd", - "label": "Extra bold", - "value": "900" - } - ] - }, - { - "id": "d498779d-012a-4c6a-82a9-77231245ce33", - "type": "textField", - "propertyName": "borderWidth", - "parentId": "root", - "label": "Border Width" - }, - { - "id": "d498779d-012a-4c6a-82a9-77231245ce28", - "type": "colorPicker", - "propertyName": "borderColor", - "parentId": "root", - "label": "Border Color", - "allowClear": true, - "version": 3 - }, - { - "id": "d498779d-012a-4c6a-10ma9-77231245ae28", - "type": "dropdown", - "propertyName": "borderStyle", - "parentId": "root", - "label": "Border Style", - "dataSourceType": "values", - "values": [ - { - "id": "c4a96833-8ed7-4085-8848-169d5607136d", - "label": "solid", - "value": "solid" - }, - { - "id": "c6f974da-ad28-44e5-8e4d-50280cf24ae7", - "label": "dashed", - "value": "dashed" - }, - { - "id": "71c0dc14-0473-4748-ae75-a4ed3bd6cffd", - "label": "dotted", - "value": "dotted" - } - ] - }, - { - "id": "d498779d-012a-4c6a-82a9-77231245c4r3", - "type": "textField", - "propertyName": "borderRadius", - "parentId": "root", - "label": "Border Radius" - } - ], - "version": 1, - "flexWrap": "wrap", - "componentName": "cntButton", - "settingsValidationErrors": [] - }, - { - "id": "P4lhbrJsly_2yCjgicaql", - "type": "container", - "propertyName": "container2", - "componentName": "container2", - "label": "Container2", - "labelAlign": "right", - "parentId": "root", - "hidden": { - "_value": false, - "_code": "return getSettingValue(data?.itemSubType) !== 'dynamic';", - "_mode": "code" - }, - "isDynamic": false, - "version": 1, - "direction": "vertical", - "justifyContent": "left", - "flexWrap": "wrap", - "components": [ - { - "id": "BobKODSk3NmsjzWl2E8ED", - "type": "dynamicItemsConfigurator", - "propertyName": "dynamicItemsConfiguration", - "componentName": "configurableActionConfigurator1", - "label": "", - "labelAlign": "right", - "parentId": "P4lhbrJsly_2yCjgicaql", - "hidden": false, - "isDynamic": false, - "settingsValidationErrors": [], - "_formFields": [ - "propertyName", - "description", - "customVisibility" - ] - } - ], - "settingsValidationErrors": [] - }, - { - "id": "6Vw9iiDw9d0MD_Rh5cbIn", - "type": "collapsiblePanel", - "propertyName": "pnlSecurity", - "label": "Security", - "labelAlign": "right", - "parentId": "root", - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "mbzE03mJnv_89oUvkmjPs", - "components": [] - }, - "content": { - "id": "SsWPpiJfTbgl4iHlFA_or", - "components": [ - { - "id": "1adea529-1f0c-4def-bd41-ee166a5dfcd7", - "type": "permissionAutocomplete", - "propertyName": "permissions", - "parentId": "SsWPpiJfTbgl4iHlFA_or", - "label": "Permissions", - "validate": {}, - "version": 1 - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - } - ], - "formSettings": { - "isSettingsForm": true, - "layout": "horizontal", - "colon": true, - "labelCol": { - "span": 8 - }, - "wrapperCol": { - "span": 16 - } - } -} \ No newline at end of file diff --git a/shesha-reactjs/src/components/buttonGroupConfigurator/itemSettings.ts b/shesha-reactjs/src/components/buttonGroupConfigurator/itemSettings.ts new file mode 100644 index 0000000000..c8d79c69b5 --- /dev/null +++ b/shesha-reactjs/src/components/buttonGroupConfigurator/itemSettings.ts @@ -0,0 +1,739 @@ +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '@/designer-components/_settings/utils/background/utils'; +import { getBorderInputs, getCornerInputs } from '@/designer-components/_settings/utils/border/utils'; +import { fontTypes, fontWeights, textAlign } from '@/designer-components/_settings/utils/font/utils'; +import { buttonTypes } from '@/designer-components/button/util'; +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { nanoid } from '@/utils/uuid'; + +export const getItemSettings = (data) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const commonButtonSettingsContainerId = nanoid(); + const styleRouterId = nanoid(); + const fontStylePnlId = nanoid(); + const dimensionsStylePnlId = nanoid(); + const borderStylePnlId = nanoid(); + const backgroundStylePnlId = nanoid(); + const shadowStylePnlId = nanoid(); + const stylePnlId = nanoid(); + const customStylePnlId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: "dropdown", + propertyName: "itemSubType", + label: "Item Type", + labelAlign: "right", + parentId: commonTabId, + hidden: false, + jsSetting: false, + dropdownOptions: [ + { + label: "Button", + value: "button" + }, + { + label: "Separator", + value: "separator" + }, + { + label: "Dynamic item(s)", + value: "dynamic" + } + ], + validate: { + "required": true + } + }) + .addContainer({ + id: commonButtonSettingsContainerId, + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.itemSubType) !== "button";', + _mode: 'code', + _value: false + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: commonButtonSettingsContainerId, + inputs: [{ + id: nanoid(), + type: 'textField', + propertyName: 'name', + label: 'Name', + jsSetting: false, + validate: { + required: true + } + }] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonButtonSettingsContainerId, + hidden: data?.itemSubType !== 'button', + inputs: [ + { + id: nanoid(), + type: "textField", + propertyName: "label", + parentId: commonButtonSettingsContainerId, + label: "Caption", + jsSetting: true + }, + { + id: nanoid(), + type: "textArea", + propertyName: "tooltip", + label: "Tooltip", + labelAlign: "right", + parentId: commonButtonSettingsContainerId, + hidden: false, + allowClear: false, + jsSetting: true + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonButtonSettingsContainerId, + inputs: [ + { + id: nanoid(), + type: 'iconPicker', + propertyName: 'icon', + label: 'Icon', + size: 'small', + parentId: commonButtonSettingsContainerId, + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'iconPosition', + label: 'Icon Position', + size: 'small', + jsSetting: true, + type: 'radio', + parentId: commonButtonSettingsContainerId, + buttonGroupOptions: [ + { title: 'Start', value: 'start', icon: 'LeftOutlined' }, + { title: 'End', value: 'end', icon: 'RightOutlined' }, + ], + hidden: { + _code: 'return !getSettingValue(data?.icon);', + _mode: 'code', + _value: false + } as any + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonButtonSettingsContainerId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + parentId: commonButtonSettingsContainerId, + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .addConfigurableActionConfigurator({ + id: nanoid(), + propertyName: 'actionConfiguration', + label: 'Action Configuration', + hideLabel: true, + validate: {}, + settingsValidationErrors: [], + }).toJson() + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.itemSubType) !== "dynamic";', + _mode: 'code', + _value: false + }, + inputs: [{ + id: nanoid(), + type: "dynamicItemsConfigurator", + propertyName: "dynamicItemsConfiguration", + componentName: "configurableActionConfigurator1", + label: "", + hideLabel: true, + labelAlign: "right", + hidden: false, + isDynamic: false, + settingsValidationErrors: [], + _formFields: [ + "propertyName", + "description", + "customVisibility" + ] + }] + }) + .toJson() + ] + }, + { + key: '2', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter', + componentName: 'propertyRouter', + label: 'Property router', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + inline: true, + inputs: [ + { + id: nanoid(), + propertyName: 'buttonType', + label: 'Type', + validate: { + required: true, + }, + type: 'dropdown', + dropdownOptions: buttonTypes, + hidden: { + _code: 'return getSettingValue(data?.itemSubType) === "separator";', + _mode: 'code', + _value: false + } as any, + }, + { + id: nanoid(), + type: 'textField', + propertyName: 'dividerWidth', + label: "thickness", + hidden: { + _code: 'return getSettingValue(data?.itemSubType) !== "separator";', + _mode: 'code', + _value: false + } as any, + }, + { + id: nanoid(), + type: 'colorPicker', + propertyName: 'dividerColor', + label: 'Color', + hidden: { + _code: 'return getSettingValue(data?.itemSubType) !== "separator";', + _mode: 'code', + _value: false + } as any, + } + ] + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + hidden: { + _code: 'return getSettingValue(data?.itemSubType) === "separator";', + _mode: 'code', + _value: false + } as any, + collapsible: 'header', + content: { + id: fontStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: fontStylePnlId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + propertyName: 'font.color', + hideLabel: true, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + hidden: { + _code: 'return getSettingValue(data?.itemSubType) == "separator";', + _mode: 'code', + _value: false + } as any, + collapsible: 'header', + content: { + id: dimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + hidden: { _code: 'return ["dashed","text", "link", "ghost"].includes(getSettingValue(data?.buttonType)) || getSettingValue(data?.itemSubType) === "separator";', _mode: 'code', _value: false } as any, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: borderStylePnlId, + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: borderStylePnlId, + components: getBorderInputs("", false) as any + }) + .addContainer({ + id: nanoid(), + parentId: borderStylePnlId, + components: getCornerInputs("", false) as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { _code: 'return ["text", "link", "ghost", "primary"].includes(getSettingValue(data?.buttonType)) || getSettingValue(data?.itemSubType) === "separator";', _mode: 'code', _value: false } as any, + content: { + id: backgroundStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: backgroundStylePnlId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + defaultValue: "color", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + hidden: { _code: 'return getSettingValue(data?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inline: true, + hidden: { _code: 'return getSettingValue(data?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + hideLabel: true, + propertyName: "background.size", + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + }, + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + hidden: { _code: 'return ["text", "link", "ghost"].includes(getSettingValue(data?.buttonType)) || getSettingValue(data?.itemSubType) === "separator";', _mode: 'code', _value: false } as any, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: shadowStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: shadowStylePnlId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + inputType: 'numberField', + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + inputType: 'numberField', + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + inputType: 'numberField', + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + inputType: 'numberField', + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + hidden: { _code: 'return getSettingValue(data?.itemSubType) === "separator";', _mode: 'code', _value: false } as any, + collapsible: 'header', + content: { + id: stylePnlId, + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + hidden: { _code: 'return getSettingValue(data?.itemSubType) === "separator";', _mode: 'code', _value: false } as any, + parentId: styleRouterId, collapsible: 'header', + content: { + id: customStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson() + ] + }) + .toJson() + ] + }, + { + key: '3', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/buttonGroupConfigurator/properties.tsx b/shesha-reactjs/src/components/buttonGroupConfigurator/properties.tsx index 74d391b0bd..a0eac4af37 100644 --- a/shesha-reactjs/src/components/buttonGroupConfigurator/properties.tsx +++ b/shesha-reactjs/src/components/buttonGroupConfigurator/properties.tsx @@ -1,13 +1,13 @@ import React, { FC, useMemo } from 'react'; import { Empty } from 'antd'; -import itemSettingsJson from './itemSettings.json'; -import itemGroupSettingsJson from './itemGroupSettings.json'; import { FormMarkup } from '@/providers/form/models'; import { useDebouncedCallback } from 'use-debounce'; import { SourceFilesFolderProvider } from '@/providers/sourceFileManager/sourcesFolderProvider'; import { ConfigurableForm } from '@/components/configurableForm'; import { ButtonGroupItemProps } from '@/providers'; import { sheshaStyles } from '@/styles'; +import { getGroupSettings } from './itemGroupSettings'; +import { getItemSettings } from './itemSettings'; export interface IButtonGroupPropertiesProps { item?: ButtonGroupItemProps; @@ -28,12 +28,12 @@ export const ButtonGroupProperties: FC = ({ item, o const editor = useMemo(() => { const emptyEditor = null; if (!item) return emptyEditor; - + const markup = item.itemType === 'item' - ? (itemSettingsJson as FormMarkup) + ? (getItemSettings(item) as FormMarkup) : item.itemType === 'group' - ? (itemGroupSettingsJson as FormMarkup) + ? (getGroupSettings(item) as FormMarkup) : []; return ( diff --git a/shesha-reactjs/src/components/buttonGroupConfigurator/utils.ts b/shesha-reactjs/src/components/buttonGroupConfigurator/utils.ts new file mode 100644 index 0000000000..5022c182e7 --- /dev/null +++ b/shesha-reactjs/src/components/buttonGroupConfigurator/utils.ts @@ -0,0 +1,23 @@ + +export const initialValues = (): any => { + return { + background: { + type: 'color', + repeat: 'no-repeat', + size: 'cover', + position: 'center', + gradient: { direction: 'to right' } + }, + font: { weight: '400', size: 14, align: 'center', type: 'Segoe UI' }, + dimensions: { width: 'auto', height: '32px', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' }, + border: { + radiusType: 'all', + borderType: 'all', + hideBorder: false, + border: { all: { width: '1px', style: 'solid' } }, + radius: { all: 8 } + }, + shadow: { spreadRadius: 0, blurRadius: 0, color: '#000', offsetX: 0, offsetY: 0 }, + stylingBox: '{"paddingLeft":"15","paddingBottom":"4","paddingTop":"4","paddingRight":"15"}', + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/chevron/index.tsx b/shesha-reactjs/src/components/chevron/index.tsx index d4cab3b7ab..f0971a1595 100644 --- a/shesha-reactjs/src/components/chevron/index.tsx +++ b/shesha-reactjs/src/components/chevron/index.tsx @@ -1,22 +1,27 @@ -import { CSSProperties, FC, useEffect, useRef, useState } from "react"; +import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from "react"; import React from "react"; import { Button, Form, FormInstance } from "antd"; import ConfigurableButton from "@/designer-components/button/configurableButton"; -import { useTheme } from "@/index"; +import { pickStyleFromModel, useTheme } from "@/index"; import { useStyles } from "./styles"; -import { fadeColor } from "@/providers/refList/provider/utils"; -import { IChevronButton, IChevronControlProps} from "./models"; +import { fadeColor } from "@/components/refListSelectorDisplay/provider/utils"; +import { IChevronButton, IChevronControlProps } from "./models"; import classNames from "classnames"; -import { addPx } from "@/designer-components/button/util"; +import { addPx } from '@/utils/style'; +import { getFontStyle } from "@/designer-components/_settings/utils/font/utils"; + export const ChevronControl: FC = (props) => { - const { value, activeColor, fontColor, showIcons, colorSource, items, width, height, fontSize } = props; - const { styles } = useStyles({height}); - const [ form ] = Form.useForm(); + const fontStyles = useMemo(() => getFontStyle(props?.font), [props?.font]); + const { value, activeColor, showIcons, colorSource, items, width, height, stylingBox } = props; + const { styles } = useStyles({ height }); + const [form] = Form.useForm(); const { theme } = useTheme(); const [showLeftArrow, setShowLeftArrow] = useState(false); const [showRightArrow, setShowRightArrow] = useState(true); const containerRef = useRef(null); + const stylingBoxJSON = JSON.parse(stylingBox || '{}'); + const stylingBoxCSS = pickStyleFromModel(stylingBoxJSON); const renderButton = (props: IChevronButton, uuid: string, form?: FormInstance) => { function getColor(source: string) { @@ -26,14 +31,13 @@ export const ChevronControl: FC = (props) => { case 'custom': return activeColor ?? theme.application.primaryColor; case 'reflist': - return props.color; + return props?.color ?? theme.application.primaryColor; default: return theme.application.primaryColor; } } - const newStyles: CSSProperties= { - color: fontColor, + const newStyles: CSSProperties = { backgroundColor: props.itemValue === value ? getColor(colorSource) : fadeColor(getColor(colorSource), 70), clipPath: 'polygon(95% 0, 100% 50%, 95% 100%, 0% 100%, 5% 50%, 0% 0%)', textAlign: 'center', @@ -41,17 +45,17 @@ export const ChevronControl: FC = (props) => { outline: 'none', width: addPx(width) ?? '150px', height: addPx(height) ?? '35px', - fontSize: addPx(fontSize) ?? '14px', - borderRadius: '0px' + borderRadius: '0px', + ...fontStyles }; return !props.hidden && ( -
+
= (props) => { useEffect(() => { const container = containerRef.current; const handleScroll = () => { - setShowLeftArrow(container.scrollLeft > 0); - setShowRightArrow( - container.scrollLeft < container.scrollWidth - container.clientWidth - ); + setShowLeftArrow(container.scrollLeft > 0); + setShowRightArrow( + container.scrollLeft < container.scrollWidth - container.clientWidth + ); }; - + container.addEventListener('scroll', handleScroll); handleScroll(); // Call once to set initial state - + return () => container.removeEventListener('scroll', handleScroll); }, []); const scroll = (e, scrollOffset: number) => { e.preventDefault(); containerRef.current.scrollLeft += scrollOffset; - }; + }; return (
{showLeftArrow && ( )}
- {items?.map((item) => { - return renderButton(item, item.id, form); - })} + {items?.map((item) => { + return renderButton(item, item.id, form); + })}
{showRightArrow && ( )}
diff --git a/shesha-reactjs/src/components/chevron/models.ts b/shesha-reactjs/src/components/chevron/models.ts index 6746662fa8..5f831a1cea 100644 --- a/shesha-reactjs/src/components/chevron/models.ts +++ b/shesha-reactjs/src/components/chevron/models.ts @@ -1,3 +1,4 @@ +import { IFontValue } from "@/designer-components/_settings/utils/font/interfaces"; import { IButtonGroupItem, IButtonItem, IConfigurableFormComponent } from "@/providers"; export type RefListGroupItemProps = IRefListItemFormModel | IRefListItemGroup; @@ -16,18 +17,17 @@ export interface IRefListItemGroup extends IRefListGroupItemBase { export interface IChevronProps extends IConfigurableFormComponent { items?: IChevronButton[]; - description: string; + description?: string; image?: string; imageStyle?: boolean; imageSize?: number; referenceList?: any; activeColor?: string; - fontColor?: string; showIcons?: boolean; colorSource?: 'primary' | 'custom' | 'reflist'; width?: number; height?: number; - fontSize?: number; + font?: IFontValue; } export interface IChevronControlProps extends IChevronProps { diff --git a/shesha-reactjs/src/components/chevron/styles.ts b/shesha-reactjs/src/components/chevron/styles.ts index 9770b5a7de..2dd6379416 100644 --- a/shesha-reactjs/src/components/chevron/styles.ts +++ b/shesha-reactjs/src/components/chevron/styles.ts @@ -1,5 +1,5 @@ +import { addPx } from '@/utils/style'; import { createStyles } from '@/styles'; -import { addPx } from '../sectionSeparator/utils'; export const useStyles = createStyles(({ css, cx, prefixCls }, {height}) => { const chevronButton = cx(`${prefixCls}-chevron-btn`, css` diff --git a/shesha-reactjs/src/components/codeEditor/client-side/codeEditorClientSide.tsx b/shesha-reactjs/src/components/codeEditor/client-side/codeEditorClientSide.tsx index ca84a8ae61..c9acf5eb68 100644 --- a/shesha-reactjs/src/components/codeEditor/client-side/codeEditorClientSide.tsx +++ b/shesha-reactjs/src/components/codeEditor/client-side/codeEditorClientSide.tsx @@ -170,10 +170,14 @@ const CodeEditorClientSide: FC = (props) => { } catch (error) { console.error('Failed to create model', { error, uri, content }); } + } else { + const currentValue = model.getValue(); + if (currentValue !== content){ + // content has been modified - sync model + model.setValue(content); + } } - // addExtralibIfMissing(monaco.languages.typescript.javascriptDefaults, content, filePath); - // addExtralibIfMissing(monaco.languages.typescript.typescriptDefaults, content, filePath); return model; }; diff --git a/shesha-reactjs/src/components/codeEditor/hocs/withEnvironment.tsx b/shesha-reactjs/src/components/codeEditor/hocs/withEnvironment.tsx index 27ae9312b6..b2b50dfbca 100644 --- a/shesha-reactjs/src/components/codeEditor/hocs/withEnvironment.tsx +++ b/shesha-reactjs/src/components/codeEditor/hocs/withEnvironment.tsx @@ -45,6 +45,7 @@ export function withEnvironment(WrappedComponent: ComponentType['presets'][number]; type ColorFormat = ColorPickerProps['format']; @@ -16,6 +18,9 @@ export interface IColorPickerProps { allowClear?: boolean; disabledAlpha?: boolean; readOnly?: boolean; + size?: SizeType; + style?: CSSProperties; + defaultValue?: ColorValueType; } const formatColor = (color: Color, format: ColorFormat) => { @@ -29,8 +34,25 @@ const formatColor = (color: Color, format: ColorFormat) => { } }; -export const ColorPicker: FC = ({ value, onChange, title, presets, showText, allowClear, disabledAlpha, readOnly }) => { +/** + * + * @param theme + * @returns a (object) map of theme colors with keys as `primary`, `success`, `warning`, `error`, `info`, `processing` + */ +export const readThemeColor = (theme: IConfigurableTheme) => ({ + 'primary': theme.application?.primaryColor, + 'success': theme.application?.successColor, + 'warning': theme.application?.warningColor, + 'error': theme.application?.errorColor, + 'info': theme.application?.infoColor, + 'processing': theme.application?.processingColor, + 'primaryTextColor': theme?.text?.default, + 'secondaryTextColor': theme?.text?.secondary +}); + +export const ColorPicker: FC = ({ value, onChange, title, presets, showText, allowClear, disabledAlpha, readOnly, size, style, defaultValue }) => { const [format, setFormat] = useState('hex'); + const { theme } = useTheme(); const handleChange = (value: Color) => { const formattedValue = formatColor(value, format); @@ -45,8 +67,28 @@ export const ColorPicker: FC = ({ value, onChange, title, pre event.stopPropagation(); }; + const panelRender = (panel: React.ReactNode) => ( +
+ {title && ( +
+ {title} +
+ + )} + {panel} +
+ ); + return ( = ({ value, onChange, title, pre allowClear={allowClear} disabled={readOnly} onClear={handleClear} - value={value ?? ""} + size={size} + style={style} + value={(readThemeColor(theme)[value as string] ?? value) ?? ""} + defaultValue={readThemeColor(theme)?.[defaultValue as string] ?? defaultValue} onChange={handleChange} presets={presets} - panelRender={(panel) => ( -
- {title && ( -
- {title} -
- - )} - {panel} -
- )} + panelRender={panelRender} /> ); }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/columnItemFilter/index.tsx b/shesha-reactjs/src/components/columnItemFilter/index.tsx index 01bfb9175f..1752e036a0 100644 --- a/shesha-reactjs/src/components/columnItemFilter/index.tsx +++ b/shesha-reactjs/src/components/columnItemFilter/index.tsx @@ -36,11 +36,9 @@ import { type MenuItem = MenuProps['items'][number]; -const { RangePicker: DateRangePicker } = DatePicker; - const allOptions: IDictionary = { date: ['equals', 'between', 'before', 'after'], - datetime: ['equals', 'between', 'before', 'after'], + 'date-time': ['equals', 'between', 'before', 'after'], time: ['equals', 'between', 'before', 'after'], number: ['lessThan', 'greaterThan', 'equals', 'between'], string: ['contains', 'startsWith', 'endsWith', 'equals'], @@ -240,7 +238,7 @@ const DateTimeFilter: FC = ({ id, filter, filterOption, onC }, [filter, format]); return filterOption === 'between' ? ( - + ) : ( = ({ const { styles } = useStyles(); - const errortip = (errors: IModelValidation) =>
    {errors.errors.map((error) =>
  • {error.error}
  • )}
; + const errortip = (errors: IModelValidation) =>
    {errors.errors.map((error, index) =>
  • {error.error}
  • )}
; const tooltipClassName = type === 'info' ? styles.cmoponentErrorInfo diff --git a/shesha-reactjs/src/components/configurableForm/configurableFormRenderer.tsx b/shesha-reactjs/src/components/configurableForm/configurableFormRenderer.tsx index 4b902fa572..62f5d259f3 100644 --- a/shesha-reactjs/src/components/configurableForm/configurableFormRenderer.tsx +++ b/shesha-reactjs/src/components/configurableForm/configurableFormRenderer.tsx @@ -7,17 +7,17 @@ import React, { } from 'react'; import { ComponentsContainerForm } from '../formDesigner/containers/componentsContainerForm'; import { ComponentsContainerProvider } from '@/providers/form/nesting/containerContext'; -import { Button, Form, Result, Spin } from 'antd'; +import { Button, Form, Result } from 'antd'; import { ValidateErrorEntity } from '@/interfaces'; import { IConfigurableFormRendererProps } from './models'; import { ROOT_COMPONENT_KEY } from '@/providers/form/models'; -import { useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerStateSelector } from '@/providers/formDesigner'; import { useSheshaApplication } from '@/providers'; import { useStyles } from './styles/styles'; import Link from 'next/link'; -import { LoadingOutlined } from '@ant-design/icons'; import { useDelayedUpdate } from '@/providers/delayedUpdateProvider'; import { useShaFormInstance } from '@/providers/form/providers/shaFormProvider'; +import { ShaSpin } from '..'; export const ConfigurableFormRenderer: FC> = ({ children, @@ -25,7 +25,6 @@ export const ConfigurableFormRenderer: FC { - props.onValuesChange?.(changedValues, values); + const isDragging = useFormDesignerStateSelector(x => x.isDragging) ?? false; + const onValuesChangeInternal = (_changedValues: any, values: any) => { shaForm.setFormData({ values: values, mergeValues: true }); }; @@ -99,11 +94,7 @@ export const ConfigurableFormRenderer: FC - } - > +
{children} -
+ ); }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/configurableForm/formInfo.tsx b/shesha-reactjs/src/components/configurableForm/formInfo.tsx index 55a14c6f9a..718263a0fb 100644 --- a/shesha-reactjs/src/components/configurableForm/formInfo.tsx +++ b/shesha-reactjs/src/components/configurableForm/formInfo.tsx @@ -47,7 +47,8 @@ export const FormInfo: FC = ({ formProps, onMarkupUpdated, childr }; useEffect(() => { - setPanelShowing(softInfoBlock); + if (displayEditMode) + setPanelShowing(softInfoBlock); }, [softInfoBlock]); if (!formProps?.id) { diff --git a/shesha-reactjs/src/components/configurableForm/formWithFlatMarkup.tsx b/shesha-reactjs/src/components/configurableForm/formWithFlatMarkup.tsx index b840b3cc86..3096870a05 100644 --- a/shesha-reactjs/src/components/configurableForm/formWithFlatMarkup.tsx +++ b/shesha-reactjs/src/components/configurableForm/formWithFlatMarkup.tsx @@ -8,10 +8,10 @@ import { useAppConfigurator } from '@/providers/appConfigurator'; import { IConfigurableFormRuntimeProps } from './models'; import { FormFlatMarkupProvider } from '@/providers/form/providers/formMarkupProvider'; import { ConditionalMetadataProvider } from '@/providers'; -import { useShaForm } from '@/providers/form/store/shaFormInstance'; -import ParentProvider from '@/providers/parentProvider'; +import { IShaFormInstance } from '@/index'; -export type IFormWithFlatMarkupProps = IConfigurableFormRuntimeProps & { +export type IFormWithFlatMarkupProps = Omit & { + shaForm: IShaFormInstance; formFlatMarkup: IFlatComponentsStructure; formSettings: IFormSettings; persistedFormProps?: IPersistedFormProps; @@ -28,16 +28,18 @@ export const FormWithFlatMarkup: FC = (props) => { propertyFilter, actions, sections, - } = props; - - const { form, + formFlatMarkup, + formSettings, + persistedFormProps, + onMarkupUpdated, + shaForm, } = props; - const [shaForm] = useShaForm({ form: props.shaForm }); const { formInfoBlockVisible } = useAppConfigurator(); - const { formFlatMarkup, formSettings, persistedFormProps, onMarkupUpdated } = props; - if (!formFlatMarkup) return null; + + if (!formFlatMarkup) + return null; const formStatusInfo = persistedFormProps?.versionStatus ? ConfigurationItemVersionStatusMap[persistedFormProps.versionStatus] @@ -47,29 +49,24 @@ export const FormWithFlatMarkup: FC = (props) => { return ( - - - - - - - - - + + + + + + + ); }; diff --git a/shesha-reactjs/src/components/configurableForm/formWithRawMarkup.tsx b/shesha-reactjs/src/components/configurableForm/formWithRawMarkup.tsx deleted file mode 100644 index c0b593b74f..0000000000 --- a/shesha-reactjs/src/components/configurableForm/formWithRawMarkup.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { FC } from 'react'; -import { IConfigurableFormRuntimeProps, SheshaFormProps } from './models'; -import { FormMarkup } from '@/interfaces'; -import { FormWithFlatMarkupMemo } from './formWithFlatMarkup'; -import { Form } from 'antd'; -import { useShaForm } from '@/providers/form/store/shaFormInstance'; - -export type IFormWithRawMarkupProps = IConfigurableFormRuntimeProps & { - markup: FormMarkup; - cacheKey?: string; - onMarkupUpdated?: () => void; - isSettingsForm?: boolean; -} & SheshaFormProps; - -export const FormWithRawMarkup: FC = (props) => { - const { markup, cacheKey, isSettingsForm, ...restProps } = props; - const [form] = Form.useForm(props.form); - const [shaForm] = useShaForm({ - form: undefined, - init: (shaForm) => { - shaForm.initByRawMarkup({ - rawMarkup: markup, - cacheKey: cacheKey, - formArguments: undefined, - initialValues: restProps.initialValues, - }); - } - }); - - const { markupLoadingState/*, dataLoadingState*/ } = shaForm; - - //const MarkupErrorRender = markupLoadingError ?? MarkupLoadingError; - return ( - <> - {markupLoadingState.status === 'ready' && ( - { - shaForm.reloadMarkup(); - }} - shaForm={shaForm} - /> - // - )} - - ); -/* - const { components, formSettings } = getComponentsAndSettings(markup); - const { state, form } = useFormByMarkup({ markup: components, key: cacheKey, formSettings, isSettingsForm }); - - return state === 'ready' - ? ( - - ) - : state === 'loading' - ? ( - - ) - : state === 'error' - ? - : null; - */ -}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/configurableForm/index.tsx b/shesha-reactjs/src/components/configurableForm/index.tsx index ccfd1146af..121fe0c83c 100644 --- a/shesha-reactjs/src/components/configurableForm/index.tsx +++ b/shesha-reactjs/src/components/configurableForm/index.tsx @@ -3,16 +3,17 @@ import ConfigurableComponent from '../appConfigurator/configurableComponent'; import EditViewMsg from '../appConfigurator/editViewMsg'; import React, { FC, MutableRefObject, useEffect } from 'react'; import { IConfigurableFormProps, SheshaFormProps } from './models'; -import { Form, FormInstance, Spin } from 'antd'; +import { Form, FormInstance } from 'antd'; import { useAppConfigurator, useShaRouting, useSheshaApplication } from '@/providers'; import { useFormDesignerUrl } from '@/providers/form/hooks'; import { FormWithFlatMarkup } from './formWithFlatMarkup'; import { useShaForm } from '@/providers/form/store/shaFormInstance'; import { MarkupLoadingError } from './markupLoadingError'; -import { LoadingOutlined } from '@ant-design/icons'; -import { ConfigurableFormInstance } from '@/interfaces'; +import { ConfigurableFormInstance, ConfigurableItemIdentifierToString } from '@/interfaces'; import { ShaFormProvider } from '@/providers/form/providers/shaFormProvider'; import { IShaFormInstance } from '@/providers/form/store/interfaces'; +import ParentProvider from '@/providers/parentProvider'; +import { ShaSpin } from '..'; export type ConfigurableFormProps = Omit, 'form' | 'formRef' | 'shaForm'> & { form?: FormInstance; @@ -119,14 +120,13 @@ export const ConfigurableForm: FC = (props) => { }; const { markupLoadingState, dataLoadingState } = shaForm; - const MarkupErrorRender = markupLoadingError ?? MarkupLoadingError; return ( - } + spinIconSize={40} > {(componentState, BlockOverlay) => ( @@ -135,32 +135,36 @@ export const ConfigurableForm: FC = (props) => { - {markupLoadingState.status === 'ready' && ( - { - shaForm.reloadMarkup(); - }} - shaForm={shaForm} - actions={actions} - sections={sections} - /> - )} - {markupLoadingState.status === 'failed' && ( - - )} - {markupLoadingState.status === 'loading' && ( - <> - )} + + {markupLoadingState.status === 'ready' && ( + { + shaForm.reloadMarkup(); + }} + shaForm={shaForm} + actions={actions} + sections={sections} + /> + )} + {markupLoadingState.status === 'failed' && ( + + )} + {markupLoadingState.status === 'loading' && ( + <> + )} +
)} - + ); }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/configurableItemAutocomplete/generic.tsx b/shesha-reactjs/src/components/configurableItemAutocomplete/generic.tsx index af3e379aa7..8c2ec11828 100644 --- a/shesha-reactjs/src/components/configurableItemAutocomplete/generic.tsx +++ b/shesha-reactjs/src/components/configurableItemAutocomplete/generic.tsx @@ -9,6 +9,7 @@ import HelpTextPopover from '@/components/helpTextPopover'; import { ConfigurableItemFullName, ConfigurableItemIdentifier, isConfigurableItemFullName, isConfigurableItemRawId, isEntityMetadata } from '@/interfaces'; import { MetadataProvider, useMetadata } from '@/providers'; import { StandardEntityActions } from '@/interfaces/metadata'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; interface EditorProps { value?: TValue; @@ -22,6 +23,7 @@ export type StandardAutocompleteProps = { readOnly?: boolean; maxResultCount?: number; filter?: object; + size?: SizeType; }; type EditorWithMode = SingleEditorProps | MultipleEditorProps; @@ -182,6 +184,7 @@ export const GenericConfigurableItemAutocompleteInternal = { setAutocompleteText(null); @@ -308,7 +311,7 @@ export const GenericConfigurableItemAutocompleteInternal = ).rawValue : undefined; - if (isMultipleEditor(props)){ + if (isMultipleEditor(props)) { const newValue = Array.isArray(selectedValue) ? selectedValue : [selectedValue]; props.onChange(newValue); } else { @@ -346,6 +349,7 @@ export const GenericConfigurableItemAutocompleteInternal = { diff --git a/shesha-reactjs/src/components/configurableItemAutocomplete/index.tsx b/shesha-reactjs/src/components/configurableItemAutocomplete/index.tsx index cfd0514f9e..823b1746b1 100644 --- a/shesha-reactjs/src/components/configurableItemAutocomplete/index.tsx +++ b/shesha-reactjs/src/components/configurableItemAutocomplete/index.tsx @@ -3,6 +3,7 @@ import { FormAutocomplete } from './formAutocomplete'; import { NotificationAutocomplete } from './notificationAutocomplete'; import { NotificationChannelAutocomplete } from './notificationChannelAutocomplete'; import { ReferenceListAutocomplete } from './referenceListAutocomplete'; +import { RoleAutocomplete } from './roleAutocomplete'; export { GenericConfigItemAutocomplete as ConfigurableItemAutocomplete }; @@ -12,6 +13,7 @@ interface ConfigItemAutocompleteType extends GenericConfigItemAutocompleteType { Notification: typeof NotificationAutocomplete; NotificationChannel: typeof NotificationChannelAutocomplete; ReferenceList: typeof ReferenceListAutocomplete; + Role: typeof RoleAutocomplete; } const autocomplete = GenericConfigItemAutocomplete as ConfigItemAutocompleteType; @@ -19,5 +21,6 @@ autocomplete.Form = FormAutocomplete; autocomplete.Notification = NotificationAutocomplete; autocomplete.NotificationChannel = NotificationChannelAutocomplete; autocomplete.ReferenceList = ReferenceListAutocomplete; +autocomplete.Role = RoleAutocomplete; export { autocomplete as ConfigItemAutocomplete }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/configurableItemAutocomplete/roleAutocomplete.tsx b/shesha-reactjs/src/components/configurableItemAutocomplete/roleAutocomplete.tsx new file mode 100644 index 0000000000..0852efc23a --- /dev/null +++ b/shesha-reactjs/src/components/configurableItemAutocomplete/roleAutocomplete.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react'; +import { FormFullName } from '@/providers/form/models'; +import { GenericConfigItemAutocomplete, ConfigurableItemAutocompleteRuntimeProps, StandardAutocompleteProps } from './generic'; +import React from 'react'; + +export type IRoleAutocompleteRuntimeProps = ConfigurableItemAutocompleteRuntimeProps>; + +const ROLE_ENTITY_TYPE = 'Shesha.Domain.ShaRole'; + +export const RoleAutocomplete: FC = (props) => { + return ( + + ); +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/configurableSidebarMenu/configurator/groupSettings.ts b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/groupSettings.ts new file mode 100644 index 0000000000..c50ca2e960 --- /dev/null +++ b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/groupSettings.ts @@ -0,0 +1,93 @@ +import { DesignerToolbarSettings } from "@/index"; +import { nanoid } from "@/utils/uuid"; +import { FormLayout } from "antd/lib/form/Form"; + +export const getGroupSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const securityTabId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: "textField", + propertyName: "title", + parentId: commonTabId, + label: "Title", + jsSetting: true, + validate: { + required: true + }, + }) + .addSettingsInput({ + id: nanoid(), + inputType: "textArea", + propertyName: "tooltip", + parentId: commonTabId, + label: "Tooltip", + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: "iconPicker", + propertyName: "icon", + parentId: commonTabId, + label: "Icon", + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: "switch", + propertyName: "hidden", + parentId: commonTabId, + label: "Hide", + jsSetting: true, + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'requiredPermissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + jsSetting: true, + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + isSettingsForm: true + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/configurableSidebarMenu/configurator/index.tsx b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/index.tsx index 27c2b911ac..4c94e6410a 100644 --- a/shesha-reactjs/src/components/configurableSidebarMenu/configurator/index.tsx +++ b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/index.tsx @@ -7,6 +7,8 @@ import { SidebarItemProperties } from './itemProperties'; import { SidebarListItemCommon } from './sidebarListItemCommon'; import { Alert } from 'antd'; +type SidebarConfigItem = ISidebarMenuItem & { itemType?: 'item' | 'group' }; + export interface ISidebarConfiguratorProps { readOnly: boolean; value: ISidebarMenuItem[]; @@ -26,12 +28,11 @@ export const SidebarConfigurator: FC = ({ value, onCh return newItem; }; - return ( - - value={value} - onChange={onChange} - initNewItem={makeNewItem} + + value={value as SidebarConfigItem[]} + onChange={onChange as (newValue: SidebarConfigItem[]) => void} + initNewItem={makeNewItem as (items: SidebarConfigItem[]) => SidebarConfigItem} readOnly={readOnly} header={} itemProperties={(itemProps) => ()} diff --git a/shesha-reactjs/src/components/configurableSidebarMenu/configurator/itemProperties.tsx b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/itemProperties.tsx index 8197c76053..5e97fab56c 100644 --- a/shesha-reactjs/src/components/configurableSidebarMenu/configurator/itemProperties.tsx +++ b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/itemProperties.tsx @@ -1,5 +1,3 @@ -import groupSettingsJson from './groupSettings.json'; -import itemSettingsJson from './itemSettings.json'; import React, { FC, useMemo, @@ -11,6 +9,8 @@ import { ISidebarMenuItem, isSidebarGroup } from '@/interfaces/sidebar'; import { SourceFilesFolderProvider } from '@/providers/sourceFileManager/sourcesFolderProvider'; import { sheshaStyles } from '@/styles'; import { ConfigurableForm } from '@/components'; +import { getGroupSettings } from './groupSettings'; +import { getItemSettings } from './itemSettings'; export interface ISidebarItemPropertiesProps { item?: ISidebarMenuItem; @@ -33,8 +33,8 @@ export const SidebarItemProperties: FC = ({ item, o if (!item) return emptyEditor; const markup = isSidebarGroup(item) - ? (groupSettingsJson as FormMarkup) - : (itemSettingsJson as FormMarkup); + ? getGroupSettings(item) as FormMarkup + : (getItemSettings(item) as FormMarkup); return ( @@ -44,7 +44,7 @@ export const SidebarItemProperties: FC = ({ item, o wrapperCol={{ span: 24 }} mode={readOnly ? 'readonly' : 'edit'} markup={markup} - cacheKey={isSidebarGroup(item) ? 'group' : 'item'} + cacheKey={isSidebarGroup(item) ? 'group' : 'button'} initialValues={item} onValuesChange={debouncedSave} className={sheshaStyles.verticalSettingsClass} diff --git a/shesha-reactjs/src/components/configurableSidebarMenu/configurator/itemSettings.ts b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/itemSettings.ts new file mode 100644 index 0000000000..f6d19210fb --- /dev/null +++ b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/itemSettings.ts @@ -0,0 +1,142 @@ +import { DesignerToolbarSettings } from "@/index"; +import { nanoid } from "@/utils/uuid"; +import { FormLayout } from "antd/lib/form/Form"; + +export const getItemSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const securityTabId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: "dropdown", + propertyName: "itemType", + parentId: commonTabId, + label: "Item Type", + size: "small", + dropdownOptions: [ + { + label: "Button", + value: "button" + }, + { + label: "Divider", + value: "divider" + } + ], + validate: { + required: true + }, + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: "textField", + propertyName: "title", + parentId: commonTabId, + label: "Title", + jsSetting: true, + validate: { + required: true + }, + }) + .addContainer({ + id: nanoid(), + parentId: commonTabId, + propertyName: "cntButton", + label: "Container2", + hidden: { + _code: "return getSettingValue(data?.itemType) !== 'button';", + _mode: "code", + _value: false + }, + justifyContent: "left", + direction: "vertical", + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: "textArea", + propertyName: "tooltip", + parentId: commonTabId, + label: "Tooltip", + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: "iconPicker", + propertyName: "icon", + parentId: commonTabId, + label: "Icon", + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: "configurableActionConfigurator", + propertyName: "actionConfiguration", + parentId: commonTabId, + label: "Action Configuration", + allowedActions: ["shesha.common"], + jsSetting: false, + hideLabel: true, + }) + .toJson() + ] + }) + .addSettingsInput({ + id: nanoid(), + inputType: "switch", + propertyName: "hidden", + parentId: commonTabId, + label: "Hide", + jsSetting: true, + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'requiredPermissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + jsSetting: true, + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + isSettingsForm: true + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/configurableSidebarMenu/configurator/sidebarListGroup.tsx b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/sidebarListGroup.tsx index a002876950..0a2397c664 100644 --- a/shesha-reactjs/src/components/configurableSidebarMenu/configurator/sidebarListGroup.tsx +++ b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/sidebarListGroup.tsx @@ -5,7 +5,7 @@ import ShaIcon, { IconType } from '@/components/shaIcon'; import { ISidebarGroup, ISidebarMenuItem } from '@/interfaces/sidebar'; import { useStyles } from '@/components/listEditor/styles/styles'; import { ItemChangeDetails } from '@/components/listEditor'; -import { useActualContextData } from '@/hooks/useActualContextData'; +import { useActualContextData } from '@/hooks'; export interface IContainerRenderArgs { id?: string; diff --git a/shesha-reactjs/src/components/configurableSidebarMenu/configurator/sidebarListItem.tsx b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/sidebarListItem.tsx index 247d866e6c..64956cc679 100644 --- a/shesha-reactjs/src/components/configurableSidebarMenu/configurator/sidebarListItem.tsx +++ b/shesha-reactjs/src/components/configurableSidebarMenu/configurator/sidebarListItem.tsx @@ -4,7 +4,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons'; import ShaIcon, { IconType } from '@/components/shaIcon'; import { ISidebarMenuItem } from '@/interfaces/sidebar'; import { useStyles } from '@/components/listEditor/styles/styles'; -import { useActualContextData } from '@/hooks/useActualContextData'; +import { useActualContextData } from '@/hooks'; const { Text } = Typography; diff --git a/shesha-reactjs/src/components/customErrorBoundary/fallbackComponent.tsx b/shesha-reactjs/src/components/customErrorBoundary/fallbackComponent.tsx index f78005495d..04d6321073 100644 --- a/shesha-reactjs/src/components/customErrorBoundary/fallbackComponent.tsx +++ b/shesha-reactjs/src/components/customErrorBoundary/fallbackComponent.tsx @@ -74,7 +74,7 @@ const CustomErrorBoundaryFallbackComponent: FC; }; diff --git a/shesha-reactjs/src/components/customFile/index.tsx b/shesha-reactjs/src/components/customFile/index.tsx index 1cfa032ef8..85351a7a77 100644 --- a/shesha-reactjs/src/components/customFile/index.tsx +++ b/shesha-reactjs/src/components/customFile/index.tsx @@ -1,7 +1,7 @@ import React, { FC, } from 'react'; import { IUploadFilePayload, IStoredFile } from '@/providers/storedFiles/contexts'; import { StoredFilesRendererBase } from '@/components/'; -import { IInputStyles, useSheshaApplication, useStoredFilesStore } from '@/providers'; +import { IInputStyles, IStyleType, useSheshaApplication, useStoredFilesStore } from '@/providers'; import { layoutType, listType } from '@/designer-components/attachmentsEditor/attachmentsEditor'; export interface ICustomFileProps extends IInputStyles { @@ -9,6 +9,7 @@ export interface ICustomFileProps extends IInputStyles { ownerId?: string; uploadFile?: (payload: IUploadFilePayload) => void; onFileListChanged?: (list: IStoredFile[]) => void; + maxCount?: number; allowAdd?: boolean; allowReplace?: boolean; allowDelete?: boolean; @@ -19,12 +20,14 @@ export interface ICustomFileProps extends IInputStyles { maxHeight?: string; isDragger?: boolean; downloadZip?: boolean; - layout?: layoutType; + filesLayout?: layoutType; listType?: listType; thumbnailWidth?: string; thumbnailHeight?: string; borderRadius?: number; hideFileName?: boolean; + container?: IStyleType; + primaryColor?: string; } export const CustomFile: FC = (props) => { @@ -61,7 +64,7 @@ export const CustomFile: FC = (props) => { isDownloadZipSucceeded={downloadZipSuccess} allowedFileTypes={props?.allowedFileTypes} maxHeight={props?.maxHeight} - layout={props?.layout} + layout={props?.filesLayout} listType={props?.listType} />
diff --git a/shesha-reactjs/src/components/dataList/createModal.tsx b/shesha-reactjs/src/components/dataList/createModal.tsx index 22c8978e0e..c0c6d00d2c 100644 --- a/shesha-reactjs/src/components/dataList/createModal.tsx +++ b/shesha-reactjs/src/components/dataList/createModal.tsx @@ -65,7 +65,7 @@ const CreateModal: FC = ({ - + diff --git a/shesha-reactjs/src/components/dataList/index.tsx b/shesha-reactjs/src/components/dataList/index.tsx index 556cb0651e..6085fef1a8 100644 --- a/shesha-reactjs/src/components/dataList/index.tsx +++ b/shesha-reactjs/src/components/dataList/index.tsx @@ -1,16 +1,16 @@ /* eslint @typescript-eslint/no-use-before-define: 0 */ import { Alert, Checkbox, Collapse, Divider, Typography } from 'antd'; import classNames from 'classnames'; -import React, { FC, useEffect, useState, useRef, MutableRefObject } from 'react'; +import React, { FC, useEffect, useState, useRef, MutableRefObject, CSSProperties } from 'react'; import { useMeasure, usePrevious } from 'react-use'; -import { FormFullName, FormIdentifier, IFormDto, IPersistedFormProps, useAppConfigurator, useConfigurableActionDispatcher } from '@/providers'; +import { FormFullName, FormIdentifier, IFormDto, IPersistedFormProps, useAppConfigurator, useConfigurableActionDispatcher, useShaFormInstance } from '@/providers'; import { useConfigurationItemsLoader } from '@/providers/configurationItemsLoader'; import ConditionalWrap from '@/components/conditionalWrapper'; import FormInfo from '../configurableForm/formInfo'; import ShaSpin from '@/components/shaSpin'; import Show from '@/components/show'; import { GroupLevelInfo, GroupLevels, IDataListProps, NewItemInitializer, Row, RowOrGroup, RowsGroup } from './models'; -import { useAvailableConstantsData, executeScriptSync, getStyle } from '@/providers/form/utils'; +import { useAvailableConstantsData, executeScriptSync, getStyle, isFormFullName } from '@/providers/form/utils'; import { isEqual } from 'lodash'; import { useDeepCompareMemo } from '@/hooks'; import { ValueRenderer } from '@/components/valueRenderer/index'; @@ -21,7 +21,9 @@ import { useMemo } from 'react'; import moment from 'moment'; import { useDeepCompareEffect } from '@/hooks/useDeepCompareEffect'; import { useStyles } from './styles/styles'; -import {EmptyState} from ".."; +import { EmptyState } from ".."; +import AttributeDecorator from '../attributeDecorator'; +import { useFormComponentStyles } from '@/hooks/formComponentHooks'; interface EntityForm { entityType: string; @@ -51,9 +53,7 @@ export const DataList: FC> = ({ entityType, selectedIds, changeSelectedIds, - orientation, - listItemWidth, - customListItemWidth, + orientation = 'horizontal', grouping, groupingMetadata, collapsible, @@ -72,22 +72,16 @@ export const DataList: FC> = ({ noDataSecondaryText, noDataText, cardHeight, - cardMaxWidth = '350px', - cardMinWidth = '350px', showBorder, cardSpacing, + style, + gap, ...props }) => { const { styles } = useStyles(); - //const refreshRef = useRef(0); let skipCache = false; - /*const now = new Date().valueOf(); - if ((now - refreshRef.current) > 5000) { - skipCache = true; - refreshRef.current = now; - }*/ interface IFormIdDictionary { [key: string]: Promise; @@ -104,6 +98,8 @@ export const DataList: FC> = ({ const [content, setContent] = useState(null); const rows = useRef(null); + const shaForm = useShaFormInstance(); + useDeepCompareEffect(() => { entityForms.current = []; entityFormInfo.current = undefined; @@ -146,8 +142,8 @@ export const DataList: FC> = ({ changeSelectedIds( val ? records?.map((item: any) => { - return item?.id; - }) + return item?.id; + }) : [] ); onMultiSelectRows( @@ -174,7 +170,7 @@ export const DataList: FC> = ({ const getFormIdFromExpression = (item): FormFullName => { if (!formIdExpression) return null; - return executeScriptSync(formIdExpression, {...allData, item}); + return executeScriptSync(formIdExpression, { ...allData, item }); }; const { formInfoBlockVisible } = useAppConfigurator(); @@ -183,42 +179,9 @@ export const DataList: FC> = ({ const persistedCreateFormProps = createFormInfo.current?.formConfiguration; - const [measuredRef, measured] = useMeasure(); - const [itemWidthCalc, setItemWidth] = useState({}); - - // TODO: Horisontal orientation works incorrect under Container with Display = `grid` + const [measuredRef] = useMeasure(); - //The below forces the card to use max-width, therefore avoiding the issue of having cards - //with varying sizes. This is only a problem when selection mode is not "none" - - if(orientation === "wrap" && selectionMode !== "none"){ - cardMinWidth = cardMaxWidth; - } - - - useEffect(() => { - if (measured?.width === 0) return; - let res = null; - if(orientation === "horizontal" && listItemWidth !== 'custom'){ - res = ({ width: '100%', minWidth: listItemWidth as unknown as number * 100 + '%' } as React.CSSProperties); - - }else if (orientation === "horizontal" && listItemWidth === "custom") { - res = ({width: `${customListItemWidth}px`} as React.CSSProperties); - - }else if (orientation === 'vertical' || !listItemWidth || (listItemWidth === 'custom' && !customListItemWidth)) { - res = - selectionMode === 'none' - ? ({ width: '100%' } as React.CSSProperties) - : ({ width: 'calc(100% - 0px)' } as React.CSSProperties); - } else { - res = - listItemWidth === 'custom' - ? ({ width: `${customListItemWidth}px` } as React.CSSProperties) - : { width: `${(measured?.width - 40) * listItemWidth - (selectionMode === 'none' ? 0 : 28)}px` }; - } - - setItemWidth(res); - }, [measured?.width, listItemWidth, customListItemWidth, orientation]); + const fcContainerStyles = useFormComponentStyles({ ...props.container ?? {} }); const isReady = (forms: EntityForm[]) => { if (!(!forms || forms.length === 0 || forms.find(x => !x.formConfiguration))) { @@ -239,11 +202,11 @@ export const DataList: FC> = ({ entityForms.current.push(entityForm); if (!entityFormInfo?.current) entityFormInfo.current = entityForm; - } else + } else return !!entityForm.formConfiguration; if (!!entityForm.formId) { - getForm({formId: entityForm.formId, configurationItemMode, skipCache}) + getForm({ formId: entityForm.formId, configurationItemMode, skipCache }) .then(response => { entityForm.formConfiguration = response; isReady(entityForms.current); @@ -253,13 +216,13 @@ export const DataList: FC> = ({ const f = loadedFormId.current[`${entityForm.entityType}_${fType}`] ?? getEntityFormId(entityForm.entityType, fType); - f.then((e) => - getForm({formId: e, configurationItemMode, skipCache}) - .then(response => { - entityForm.formId = e; - entityForm.formConfiguration = response; - isReady(entityForms.current); - }) + f.then((e) => + getForm({ formId: e, configurationItemMode, skipCache }) + .then(response => { + entityForm.formId = e; + entityForm.formConfiguration = response; + isReady(entityForms.current); + }) ); loadedFormId.current[`${entityForm.entityType}_${fType}`] = f; @@ -268,7 +231,7 @@ export const DataList: FC> = ({ }; useDeepCompareEffect(() => { - let isReady = true; + let isReady = true; let fId = createFormId; let className = '$createFormName$'; @@ -305,7 +268,7 @@ export const DataList: FC> = ({ updateRows(); updateContent(); } - }, [records, formId, formType, createFormId, createFormType, entityType, formSelectionMode, canEditInline, canDeleteInline, noDataIcon, noDataSecondaryText, noDataText]); + }, [records, formId, formType, createFormId, createFormType, entityType, formSelectionMode, canEditInline, canDeleteInline, noDataIcon, noDataSecondaryText, noDataText, style, groupStyle, orientation]); const renderSubForm = (item: any, index: number) => { let className = null; @@ -320,7 +283,7 @@ export const DataList: FC> = ({ let entityForm = entityForms.current.find((x) => x.entityType === className && x.formType === fType); - if (!entityForm?.formConfiguration?.markup) + if (!entityForm?.formConfiguration?.markup) return ; const dblClick = () => { @@ -338,28 +301,39 @@ export const DataList: FC> = ({ } else console.error('Action is not configured'); return false; }; - + + const attributes = { + 'data-sha-datalist-item-type': 'subForm', + 'data-sha-parent-form-id': `${shaForm?.form?.id}`, + 'data-sha-parent-form-name': `${shaForm?.form?.module}/${shaForm?.form?.name}`, + 'data-sha-form-id': `${entityForm?.formConfiguration?.id}`, + }; + + if (isFormFullName(entityForm?.formId)) + attributes['data-sha-form-name'] = `${entityForm?.formId.module}/${entityForm?.formId.name}`; + return ( -
- updateAction(index, rowData)} - deleter={() => deleteAction(index, item)} - allowChangeEditMode={inlineEditMode === 'one-by-one'} - editMode={canEditInline && inlineEditMode === 'all-at-once' ? 'update' : 'read'} - autoSave={inlineSaveMode === 'auto'} + +
+ updateAction(index, rowData)} + deleter={() => deleteAction(index, item)} + allowChangeEditMode={inlineEditMode === 'one-by-one'} + editMode={canEditInline && inlineEditMode === 'all-at-once' ? 'update' : 'read'} + autoSave={inlineSaveMode === 'auto'} /> - -
+
+ ); }; @@ -398,7 +372,7 @@ export const DataList: FC> = ({ } parent = g.currentGroup.$childs; }); - parent.push({index: rowIndex, row} as Row); + parent.push({ index: rowIndex, row } as Row); }); return result; } @@ -440,12 +414,33 @@ export const DataList: FC> = ({ }; const renderRow = (item: any, index: number, isLastItem: Boolean) => { - + const stylesAsCSS = style as CSSProperties; + + const hasBorder = () => { + const borderProps = ['border', 'borderWidth', 'borderTop', 'borderBottom', 'borderLeft', 'borderRight']; + return borderProps.some(prop => { + const value = stylesAsCSS?.[prop]; + return value && value !== 'none' && value !== '0' && value !== '0px'; + }) || showBorder; + }; + const selected = selectedRow?.index === index && !(selectedRows?.length > 0) || (selectedRows?.length > 0 && selectedRows?.some(({ id }) => id === item?.id)); + + const itemStyles: CSSProperties = { + ...(stylesAsCSS || {}), + ...(orientation === 'horizontal' && { flexShrink: 0 }), + ...(orientation === 'wrap' && showBorder && { + border: '1px solid #d3d3d3', + borderRadius: '8px' + }), + ...(cardHeight && { height: cardHeight }), + overflow: 'auto' + }; + return ( -
+
( @@ -454,25 +449,30 @@ export const DataList: FC> = ({ checked={selected} onChange={() => { onSelectRowLocal(index, item); - }} - > + }}> {children} )} > -
{ onSelectRowLocal(index, item); }} - style={orientation === 'wrap' ? {minWidth: `${Number(cardMinWidth) ? cardMinWidth+'px' : cardMinWidth}`, maxWidth: `${Number(cardMaxWidth) ? cardMaxWidth+'px' : cardMaxWidth}`, height: `${Number(cardHeight) ? cardHeight+'px' : cardHeight}`, - ...(showBorder && {border: '1px #d3d3d3 solid'})} : itemWidthCalc} + style={itemStyles} > {rows.current?.length > index ? rows.current[index] : null}
-
{' '} - {(orientation !== "wrap" && (!isLastItem) && )} + + {(orientation !== "wrap" && (!isLastItem) && !hasBorder() && gap === undefined && ( + + ))}
); }; @@ -497,26 +497,76 @@ export const DataList: FC> = ({ : {} ); }, [onNewListItemInitializeExecuter, allData.data, allData.globalState, allData.contexts.lastUpdate]); - const updateRows = () => { rows.current = records?.map((item: any, index) => renderSubForm(item, index)); }; const updateContent = () => { - setContent(groups + setContent(groups ? groups?.map((item: RowsGroup, index) => renderGroup(item, index)) - : records?.map((item: any, index) => renderRow(item, index, records?.length - 1 === index)) + : records?.map((item: any, index) => renderRow(item, index, records?.length - 1 === index)) ); }; + + const getContainerStyles = (): CSSProperties => { + const containerStyles: CSSProperties = { + gap: gap !== undefined ? (typeof gap === 'number' ? `${gap}px` : gap) : '0px', + ...fcContainerStyles.jsStyle, + ...fcContainerStyles.stylingBoxAsCSS, + ...fcContainerStyles.dimensionsStyles + + }; + + + const rawItemWidth = + (style as CSSProperties)?.width ?? props.container?.dimensions?.width; + const itemWidth = + rawItemWidth !== undefined + ? typeof rawItemWidth === 'number' + ? `${rawItemWidth}px` + : rawItemWidth + : '300px'; + + switch (orientation) { + case 'horizontal': + return { + ...containerStyles, + display: 'flex', + gridAutoFlow: 'row', + gridAutoColumns: 'max-content', + alignItems: 'start', + overflowX: 'auto', + width: '100%' + }; + + case 'wrap': + return { + ...containerStyles, + display: 'grid', + gridTemplateColumns: `repeat(auto-fill, ${itemWidth})`, + alignItems: 'start' + }; + + case 'vertical': + default: + return { + ...containerStyles, + display: 'grid', + gridTemplateColumns: '1fr', + alignItems: 'stretch' + }; + } + }; + return ( <> {createModalOpen && createFormInfo?.current?.formConfiguration && - setCreateModalOpen(isOpen)} @@ -537,63 +587,43 @@ export const DataList: FC> = ({ - {// Use Configurable Action - /* - - */}
- - -
- - - - - - 0}> - {orientation === "wrap" && -
- {content} -
- } - - - {orientation === "horizontal" && -
- {React.Children.map(content, child => { - return React.cloneElement(child, { style: itemWidthCalc }); - })} -
- } - - {orientation === "vertical" && -
- {React.Children.map(content, child => { - return React.cloneElement(child, { style: itemWidthCalc }); + + +
+ + + + + 0}> + {React.Children.map(content, (child, index) => { + return React.cloneElement(child, { + key: child.key || index, + style: { + ...child.props.style, + overflow: 'visible', + flex: '0 0 100%' + } + }); })} -
- } - - -
- +
+
+
); -}; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/dataList/models.tsx b/shesha-reactjs/src/components/dataList/models.tsx index a4a8afdaa1..ea1f77811a 100644 --- a/shesha-reactjs/src/components/dataList/models.tsx +++ b/shesha-reactjs/src/components/dataList/models.tsx @@ -1,5 +1,5 @@ import { MutableRefObject } from 'react'; -import { FormIdentifier } from '@/interfaces'; +import { FormIdentifier, IStyleType } from '@/interfaces'; import { IConfigurableActionConfiguration } from '@/interfaces/configurableAction'; import { IPropertyMetadata } from '@/interfaces/metadata'; import { ISelectionProps } from '@/providers/dataTable/contexts'; @@ -38,7 +38,7 @@ export interface IDataListProps extends IDataListBaseProps, IDataListActions { noDataIcon?: string; } -export interface IDataListBaseProps { +export interface IDataListBaseProps extends IStyleType { id: string; dataSource?: string; @@ -65,6 +65,8 @@ export interface IDataListBaseProps { cardHeight?: string; cardSpacing?: string; showBorder?: boolean; + gap?: number; + container?: IStyleType; dblClickActionConfiguration?: IConfigurableActionConfiguration; diff --git a/shesha-reactjs/src/components/dataList/styles/styles.ts b/shesha-reactjs/src/components/dataList/styles/styles.ts index 382251c169..eb147373b3 100644 --- a/shesha-reactjs/src/components/dataList/styles/styles.ts +++ b/shesha-reactjs/src/components/dataList/styles/styles.ts @@ -117,7 +117,6 @@ export const useStyles = createStyles(({ css, cx, token, prefixCls }) => { .${shaDatalistWrapParent} { width: 100%; display: grid; - padding: 8px; overflow-wrap: break-word; } diff --git a/shesha-reactjs/src/components/dataTable/cell/dataCell.tsx b/shesha-reactjs/src/components/dataTable/cell/dataCell.tsx index e55705fe68..562aa2e5c3 100644 --- a/shesha-reactjs/src/components/dataTable/cell/dataCell.tsx +++ b/shesha-reactjs/src/components/dataTable/cell/dataCell.tsx @@ -7,8 +7,8 @@ import React, { FC, useRef } from 'react'; import StringCell from './default/stringCell'; import TimeCell from './default/timeCell'; import { CustomErrorBoundary } from '@/components'; -import { DEFAULT_FORM_SETTINGS, FormItemProvider, useForm } from '@/providers'; -import { getActualModel, upgradeComponent, useAvailableConstantsData } from '@/providers/form/utils'; +import { DEFAULT_FORM_SETTINGS, FormItemProvider, IConfigurableFormComponent, useForm } from '@/providers'; +import { upgradeComponent, useAvailableConstantsData } from '@/providers/form/utils'; import { getInjectables } from './utils'; import { IColumnEditorProps, standardCellComponentTypes } from '@/providers/datatableColumnsConfigurator/models'; import { IComponentWrapperProps, IConfigurableCellProps, IDataCellProps } from './interfaces'; @@ -19,6 +19,8 @@ import { useCrud } from '@/providers/crudContext'; import { useDeepCompareMemo } from '@/hooks'; import { useFormDesignerComponents } from '@/providers/form/hooks'; import { editorAdapters, updateModelExcludeFiltered } from '@/components/formComponentSelector/adapters'; +import MultiEntityCell from './default/multiEntityCell'; +import FormComponentMemo from '@/components/formDesigner/formComponent'; export const DefaultDataDisplayCell = (props: IDataCellProps) => { const { columnConfig } = props; @@ -42,11 +44,14 @@ export const DefaultDataDisplayCell = (props: case 'entity': return {...cellProps} />; case 'array': { - return columnConfig.dataFormat === 'reference-list-item' ? ( - {...cellProps} /> - ) : ( - {...props} /> - ); + switch (columnConfig.dataFormat) { + case 'reference-list-item': + return {...cellProps} />; + case 'entity': + return {...cellProps} />; + default: + return {...cellProps} />; + } } case 'string': return {...cellProps} />; @@ -64,7 +69,7 @@ const ComponentWrapper: FC = (props) => { const component = toolboxComponents[customComponent.type]; const injectables = getInjectables(props); - const componentModel = useDeepCompareMemo(() => { + const componentModel: IConfigurableFormComponent = useDeepCompareMemo(() => { // migrate component const model = upgradeComponent( customComponent.settings, @@ -73,25 +78,15 @@ const ComponentWrapper: FC = (props) => { { allComponents: { 'component': customComponent.settings }, componentRelations: {} } ); - // calcualte JS properties - const actualModel = getActualModel( - model, - { - ...allData, - formMode: props.readOnly ? 'readonly' : undefined, // imitate form mode according to cell mode - tableRow: injectables.injectedTableRow - }, - props.readOnly); - let editorModel: IColumnEditorProps = { - ...actualModel, + ...model, ...injectables, id: props.columnConfig.columnId, type: customComponent.type, propertyName: columnConfig.propertyName, label: null, hideLabel: true, - readOnly: actualModel.readOnly === undefined ? props.readOnly : actualModel.readOnly, + readOnly: model.readOnly === undefined ? props.readOnly : model.readOnly, }; const adapter = editorAdapters[customComponent.type]; @@ -116,11 +111,7 @@ const ComponentWrapper: FC = (props) => { {/* set namePrefix = '' to reset subForm prefix */} - + ); diff --git a/shesha-reactjs/src/components/dataTable/cell/default/multiEntityCell.tsx b/shesha-reactjs/src/components/dataTable/cell/default/multiEntityCell.tsx new file mode 100644 index 0000000000..051647dcf5 --- /dev/null +++ b/shesha-reactjs/src/components/dataTable/cell/default/multiEntityCell.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { IDataCellProps } from '../interfaces'; +import ReadOnlyDisplayFormItem from '@/components/readOnlyDisplayFormItem'; +import { useStyles } from '../../styles/styles'; + +export interface IMultiEntityCellProps extends IDataCellProps {} + +export const MultiEntityCell = (props: IMultiEntityCellProps) => { + const { styles } = useStyles(); + const { value } = props; + if (!props.value) return null; + + const values = Array.isArray(value) ? value.map((item) => ({ label: item['_displayName'] })) : [{ label: value['_displayName'] }]; + return ; +}; + +export default MultiEntityCell; \ No newline at end of file diff --git a/shesha-reactjs/src/components/dataTable/cell/formCell/componentsContainerFormCell.tsx b/shesha-reactjs/src/components/dataTable/cell/formCell/componentsContainerFormCell.tsx index 197770ffb3..3cb9c5ea72 100644 --- a/shesha-reactjs/src/components/dataTable/cell/formCell/componentsContainerFormCell.tsx +++ b/shesha-reactjs/src/components/dataTable/cell/formCell/componentsContainerFormCell.tsx @@ -4,7 +4,7 @@ import { IComponentsContainerBaseProps } from '@/interfaces'; import { removeUndefinedProperties } from '@/utils/array'; import { useParent } from '@/providers/parentProvider/index'; import { getAlignmentStyle } from '@/components/formDesigner/containers/util'; -import DynamicComponent from '@/designer-components/dynamicView/dynamicComponent'; +import FormComponent from '@/components/formDesigner/formComponent'; interface IComponentsContainerFormCellProps extends IComponentsContainerBaseProps, ICommonContainerProps { } @@ -21,8 +21,8 @@ export const ComponentsContainerFormCell: FC
{components?.map((model) => { return ( - = ({ formId, children }) => { ? formLoading.error?.code === 404 ? : formLoading.error?.code === 401 || formLoading.error?.code === 403 - ? + ? : <>Form loading error : children(formLoading.form); }; @@ -45,45 +46,80 @@ const ReadFormCell = (props: IFormCellProps {(form) => ( -
- - - - - - - -
+ +
+ + + + + + + +
+
)} ); }; -export const CreateFormCell = (props: IConfigurableCellProps) => { +export interface ICreateFormCellProps extends IConfigurableCellProps { + /** FormId GUID */ + parentFormId?: string; + /** `Module`/`FormName` */ + parentFormName?: string; +} + +export const CreateFormCell = (props: ICreateFormCellProps) => { const { styles } = useStyles(); const styleMinHeight = useMemo(() => { return { minHeight: props.columnConfig.minHeight ?? 0 }; }, [props.columnConfig.minHeight]); + const attributes = { + 'data-sha-datatable-cell-type': 'subForm', + 'data-sha-parent-form-name': `${props.parentFormName}`, + }; + + if (props.parentFormId) + attributes['data-sha-parent-form-id'] = props.parentFormId; + + if (isFormFullName(props.columnConfig.createFormId)) + attributes['data-sha-form-name'] = `${props.columnConfig.createFormId.module}/${props.columnConfig.createFormId.name}`; + else if (typeof props.columnConfig.createFormId === 'string' && props.columnConfig.createFormId) + attributes['data-sha-form-id'] = props.columnConfig.createFormId; + return !props.columnConfig.createFormId ? null : ( {(form) => ( -
- - - - - - - -
+ +
+ + + + + + + +
+
)}
); @@ -95,20 +131,33 @@ const EditFormCell = (props: IFormCellProps : ( {(form) => ( -
- - - - - - - -
+ +
+ + + + + + + +
+
)}
); diff --git a/shesha-reactjs/src/components/dataTable/cell/index.tsx b/shesha-reactjs/src/components/dataTable/cell/index.tsx index ebbb1e1d5f..94e5cbb350 100644 --- a/shesha-reactjs/src/components/dataTable/cell/index.tsx +++ b/shesha-reactjs/src/components/dataTable/cell/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { CellProps, Renderer } from 'react-table'; -import { ITableColumn } from '@/interfaces'; +import { IShaFormInstance, ITableColumn } from '@/interfaces'; import { IPropertyMetadata } from '@/interfaces/metadata'; import { ITableActionColumn, @@ -15,7 +15,8 @@ import FormCell from './formCell/formCell'; export const getCellRenderer = ( column: ITableColumn, - propertyMeta?: IPropertyMetadata + propertyMeta?: IPropertyMetadata, + shaForm?: IShaFormInstance ): Renderer> | undefined => { switch (column.columnType) { case 'data': { @@ -35,7 +36,7 @@ export const getCellRenderer = ( return null; } case 'form': { - const baseProps = { columnConfig: column as ITableFormColumn }; + const baseProps = { columnConfig: column as ITableFormColumn, parentFormId: shaForm?.form?.id, parentFormName: `${(shaForm as any)?.formId?.module}/${(shaForm as any)?.formId?.name}` }; return (cellProps: CellProps) => {...cellProps} {...baseProps} />; } default: { diff --git a/shesha-reactjs/src/components/dataTable/cell/interfaces.ts b/shesha-reactjs/src/components/dataTable/cell/interfaces.ts index 915dd26024..baeda1c8f0 100644 --- a/shesha-reactjs/src/components/dataTable/cell/interfaces.ts +++ b/shesha-reactjs/src/components/dataTable/cell/interfaces.ts @@ -21,7 +21,12 @@ export interface ICommonCellProps extends ICommonCellProps { } -export interface IFormCellProps extends ICommonCellProps { } +export interface IFormCellProps extends ICommonCellProps { + /** FormId GUID */ + parentFormId?: string; + /** `Module`/`FormName` */ + parentFormName?: string; +} export interface IComponentWrapperProps { customComponent: IFieldComponentProps; diff --git a/shesha-reactjs/src/components/dataTable/index.tsx b/shesha-reactjs/src/components/dataTable/index.tsx index b708536bcf..34f41e40b3 100644 --- a/shesha-reactjs/src/components/dataTable/index.tsx +++ b/shesha-reactjs/src/components/dataTable/index.tsx @@ -14,6 +14,7 @@ import { useGlobalState, useHttpClient, useMetadata, + useShaFormInstance, useSheshaApplication, } from '@/providers'; import { DataTableFullInstance, IColumnWidth } from '@/providers/dataTable/contexts'; @@ -214,6 +215,7 @@ export const DataTable: FC> = ({ const httpClient = useHttpClient(); const toolboxComponents = useFormDesignerComponents(); + const shaForm = useShaFormInstance(); const onNewRowInitializeExecuter = useMemo(() => { return props.onNewRowInitialize @@ -350,7 +352,7 @@ export const DataTable: FC> = ({ const width = strictWidth ?? columnItem.width; const cellStyleAccessor = getCellStyleAccessor(columnItem); - const cellRenderer = getCellRenderer(columnItem, columnItem.metadata); + const cellRenderer = getCellRenderer(columnItem, columnItem.metadata, shaForm); const column: DataTableColumn = { ...columnItem, accessor: camelcaseDotNotation(columnItem.accessor), diff --git a/shesha-reactjs/src/components/dataTable/interfaces.ts b/shesha-reactjs/src/components/dataTable/interfaces.ts index 6e5b3b672b..db8afa3553 100644 --- a/shesha-reactjs/src/components/dataTable/interfaces.ts +++ b/shesha-reactjs/src/components/dataTable/interfaces.ts @@ -67,6 +67,7 @@ export interface IShaDataTableInlineEditableProps { onNewRowInitialize?: string; onRowSave?: string; onRowSaveSuccessAction?: IConfigurableActionConfiguration; + onDblClick?: IConfigurableActionConfiguration | ((rowData: any, index?: number) => void); } export interface IShaDataTableProps extends ITableRowDragProps, IShaDataTableInlineEditableProps { @@ -81,7 +82,6 @@ export interface IShaDataTableProps extends ITableRowDragProps, IShaDataTableInl selectedRowIndex?: number; onSelectRow?: (index: number, row: any) => void; onSelectedIdsChanged?: (selectedRowIds: string[]) => void; - onDblClick?: (data: any, index?: number) => void; onMultiRowSelect?: (rows: Array | Row) => void; customTypeEditors?: ITableCustomTypeEditor[]; onRowsChanged?: (rows: object[]) => void; diff --git a/shesha-reactjs/src/components/dataTable/sortingConfigurator/index.tsx b/shesha-reactjs/src/components/dataTable/sortingConfigurator/index.tsx index 26c5b239f6..f3e39db67a 100644 --- a/shesha-reactjs/src/components/dataTable/sortingConfigurator/index.tsx +++ b/shesha-reactjs/src/components/dataTable/sortingConfigurator/index.tsx @@ -1,61 +1,60 @@ import { Space, Select } from 'antd'; import { ListEditor, PropertyAutocomplete } from '@/components/index'; import { ColumnSorting, GroupingItem as SortingItem } from '@/providers/dataTable/interfaces'; -import { MetadataProvider } from '@/providers/index'; import React, { FC } from 'react'; import { getNanoId } from '@/utils/uuid'; +import { ListItem } from '@/components/listEditor/models'; const { Option } = Select; export interface ISortingEditorProps { value?: SortingItem[]; onChange: (newValue?: SortingItem[]) => void; - modelType: string; readOnly?: boolean; + maxItemsCount?: number; } export const SortingEditor: FC = (props) => { - const { value, onChange, modelType, readOnly: editorReadOnly } = props; + const { value, onChange, readOnly: editorReadOnly, maxItemsCount } = props; return ( - - - value={value} - onChange={onChange} - initNewItem={(_items) => ({ id: getNanoId(), propertyName: '', sorting: 'asc' })} - readOnly={editorReadOnly} - > - {({ item, itemOnChange, readOnly }) => { - return ( -
- - { - if (!Array.isArray(value)) - itemOnChange({ ...item, propertyName: value }, undefined); - }} - autoFillProps={false} - size='small' - readOnly={readOnly} - /> - - value={item.sorting} - onChange={(value) => { - itemOnChange({ ...item, sorting: value }, undefined); - }} - style={{ width: '120px' }} - size='small' - disabled={readOnly} - > - - - - -
); - }} - -
+ + value={value} + onChange={onChange} + initNewItem={(_items) => ({ id: getNanoId(), propertyName: '', sorting: 'asc', itemType: 'item' })} + readOnly={editorReadOnly} + maxItemsCount={maxItemsCount} + > + {({ item, itemOnChange, readOnly }) => { + return ( +
+ + { + if (!Array.isArray(value)) + itemOnChange({ ...item, propertyName: value }, undefined); + }} + autoFillProps={false} + size='small' + readOnly={readOnly} + /> + + value={item.sorting} + onChange={(value) => { + itemOnChange({ ...item, sorting: value }, undefined); + }} + style={{ width: '120px' }} + size='small' + disabled={readOnly} + > + + + + +
); + }} + ); }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/dataTable/styles/styles.ts b/shesha-reactjs/src/components/dataTable/styles/styles.ts index 1d02b9e3a5..5ec0007267 100644 --- a/shesha-reactjs/src/components/dataTable/styles/styles.ts +++ b/shesha-reactjs/src/components/dataTable/styles/styles.ts @@ -1,7 +1,11 @@ import { createStyles } from '@/styles'; export const useStyles = createStyles(({ css, cx }) => { - const shaFormCell = cx("sha-form-cell", css` + const shaMultiEntityCell = cx("sha-form-cell", css` + white-space: normal; + `); + + const shaFormCell = cx("sha-form-cell", css` width: 100%; .ant-form-item-control { @@ -27,5 +31,6 @@ export const useStyles = createStyles(({ css, cx }) => { return { shaChildTableErrorContainer: tableErrorContainer, shaFormCell, + shaMultiEntityCell, }; }); \ No newline at end of file diff --git a/shesha-reactjs/src/components/debugPanel/dataContent.tsx b/shesha-reactjs/src/components/debugPanel/dataContent.tsx index e1caacccdc..2835ddf7d1 100644 --- a/shesha-reactjs/src/components/debugPanel/dataContent.tsx +++ b/shesha-reactjs/src/components/debugPanel/dataContent.tsx @@ -2,7 +2,7 @@ import React, { FC, useEffect, useMemo, useState } from "react"; import { DebugDataTree } from "./dataTree"; import { useDataContextManager } from "@/providers/dataContextManager"; import { useGlobalState, useMetadataDispatcher } from "@/providers"; -import { useFormDesignerState } from "@/providers/formDesigner"; +import { useFormDesignerStateSelector } from "@/providers/formDesigner"; import { IModelMetadata } from "@/interfaces/metadata"; import { getFieldNameFromExpression } from "@/providers/form/utils"; @@ -11,9 +11,9 @@ const DebugPanelDataContent: FC = () => { const contextManager = useDataContextManager(); const pageInstance = contextManager.getPageFormInstance(); - const designer = useFormDesignerState(false); + const formSettings = useFormDesignerStateSelector(x => x.formSettings); - const modelType = pageInstance?.formSettings?.modelType ?? designer?.formSettings?.modelType; + const modelType = pageInstance?.formSettings?.modelType ?? formSettings?.modelType; const [formMetadata, setFormMetadata] = useState(null); const metadataDispatcher = useMetadataDispatcher(); useEffect(() => { diff --git a/shesha-reactjs/src/components/debugPanel/dataTree.tsx b/shesha-reactjs/src/components/debugPanel/dataTree.tsx index 2d1614c881..8901809d8b 100644 --- a/shesha-reactjs/src/components/debugPanel/dataTree.tsx +++ b/shesha-reactjs/src/components/debugPanel/dataTree.tsx @@ -83,7 +83,7 @@ export const DebugDataTree: FC = ({editAll, name, data, las const key = pkey + '.' + item; pm = pl?.find(x => toCamelCase(x.path) === item); - if (typeof p[item] === 'object') { + if (p[item] && typeof p[item] === 'object') { const n: DataNode = {title: , key, isLeaf: false }; res.push(n); } else { diff --git a/shesha-reactjs/src/components/debugPanel/model.tsx b/shesha-reactjs/src/components/debugPanel/model.tsx index 0e92904831..19835cfdb9 100644 --- a/shesha-reactjs/src/components/debugPanel/model.tsx +++ b/shesha-reactjs/src/components/debugPanel/model.tsx @@ -4,7 +4,7 @@ export interface IDebugDataTreeProps { name: string; data: any; metadata?: IModelMetadata; - lastUpdated?: string; + lastUpdated?: number; editAll?: boolean; onChange: (propName: string, val: any) => void; onFunctionExecute?: (path: string) => void; diff --git a/shesha-reactjs/src/components/dropdown/dropdown.tsx b/shesha-reactjs/src/components/dropdown/dropdown.tsx index fd95c4d53e..1e98c3ed18 100644 --- a/shesha-reactjs/src/components/dropdown/dropdown.tsx +++ b/shesha-reactjs/src/components/dropdown/dropdown.tsx @@ -1,16 +1,14 @@ import React, { FC, useCallback } from 'react'; -import ReadOnlyDisplayFormItem from '@/components/readOnlyDisplayFormItem'; -import { evaluateString, executeExpression } from '@/providers/form/utils'; +import ReadOnlyDisplayFormItem, { Icon } from '@/components/readOnlyDisplayFormItem'; +import { executeExpression } from '@/providers/form/utils'; import { IDropdownProps, ILabelValue } from './model'; -import { Select } from 'antd'; -import { - useForm, - useFormData, - useGlobalState, -} from '@/providers'; +import { Select, Tag } from 'antd'; import GenericRefListDropDown from '@/components/refListDropDown/genericRefListDropDown'; import { IncomeValueFunc, ISelectOption, OutcomeValueFunc } from '@/components/refListDropDown/models'; import { ReferenceListItemDto } from '@/apis/referenceList'; +import { useStyles } from './style'; +import { getTagStyle } from '@/utils/style'; +import ReflistTag from '../refListDropDown/reflistTag'; export const Dropdown: FC = ({ @@ -18,15 +16,13 @@ export const Dropdown: FC = ({ incomeCustomJs, outcomeCustomJs, labelCustomJs, - dataSourceType, values, onChange, - value: val, - hideBorder, + value, referenceListId, mode, - defaultValue: defaultVal, + defaultValue, disableItemValue = false, ignoredValues = [], disabledValues = [], @@ -34,22 +30,17 @@ export const Dropdown: FC = ({ readOnly, style, size, + showIcon, + solidColor = true, + showItemName, allowClear = true, + displayStyle, + tagStyle }) => { - const { formMode } = useForm(); - const { data: formData } = useFormData(); - const { globalState } = useGlobalState(); - - const selectedMode = mode === 'single' ? undefined : mode; - //quick fix not to default to empty string or null while working with multi-mode - const defaultValue = Array.isArray(defaultVal) - ? defaultVal - : defaultVal ? evaluateString(defaultVal, { formData, formMode, globalState }) || undefined: undefined; + const { styles } = useStyles({ style }); - const value = typeof val === 'string' - ? (evaluateString(val, { formData, formMode, globalState }) ?? undefined) as any - : val; + const selectedMode = mode === 'multiple' || mode === 'tags' ? mode : undefined; const getOptions = (): ILabelValue[] => { return value && typeof value === 'number' ? values?.map((i) => ({ ...i, value: parseInt(i.value, 10) })) : values; @@ -60,7 +51,7 @@ export const Dropdown: FC = ({ return !!value ? value.itemValue : null; } if (valueFormat === 'custom') { - return executeExpression(incomeCustomJs, {...args, value}, null, null ); + return executeExpression(incomeCustomJs, { ...args, value }, null, null); } return value; }, [valueFormat, incomeCustomJs]); @@ -68,11 +59,11 @@ export const Dropdown: FC = ({ const outcomeValueFunc: OutcomeValueFunc = useCallback((value: ReferenceListItemDto, args?: any) => { if (valueFormat === 'listItem') { return !!value - ? {item: value.item, itemValue: value.itemValue} - : null; + ? { item: value.item, itemValue: value.itemValue } + : null; } if (valueFormat === 'custom') { - return executeExpression(outcomeCustomJs, {...args, value}, null, null ); + return executeExpression(outcomeCustomJs, { ...args, value }, null, null); } return !!value ? value.itemValue : null; }, [valueFormat, outcomeCustomJs]); @@ -86,7 +77,10 @@ export const Dropdown: FC = ({ // fix for designer when switch mode value: typeof itemValue === 'object' ? null : itemValue, label: item?.label ?? 'unknown', + color: item?.color, + icon: item?.icon, data: item?.data, + description: item?.description, }; }, [incomeValueFunc]); @@ -110,6 +104,9 @@ export const Dropdown: FC = ({ value: typeof value === 'object' ? null : value, label, data: outcomeValueFunc(fetchedItem, args), + color: fetchedItem?.color, + icon: fetchedItem?.icon, + description: fetchedItem?.description, }; }, [labelCustomJs, outcomeValueFunc, incomeValueFunc]); @@ -119,19 +116,24 @@ export const Dropdown: FC = ({ onChange={onChange} referenceListId={referenceListId} value={value} - variant={hideBorder ? 'borderless' : undefined} + variant={'borderless'} defaultValue={defaultValue} mode={selectedMode} - disabledValues={disableItemValue? disabledValues : [] } + disabledValues={disableItemValue ? disabledValues : []} filters={ignoredValues} placeholder={placeholder} readOnly={readOnly} size={size} - style={style} - allowClear={allowClear} + showIcon={showIcon} + solidColor={solidColor} + showItemName={showItemName} + className={styles.dropdown} + style={{ ...style }} + tagStyle={tagStyle} + allowClear={allowClear} getLabeledValue={getLabeledValue} getOptionFromFetchedItem={getOptionFromFetchedItem} - + displayStyle={displayStyle} incomeValueFunc={incomeValueFunc} outcomeValueFunc={outcomeValueFunc} /> @@ -140,33 +142,104 @@ export const Dropdown: FC = ({ const options = getOptions() || []; - const selectedValue = options.length > 0 ? value || defaultValue : null; + const selectedValue = options.length > 0 ? value ?? defaultValue : null; const getSelectValue = () => { - const selectedValues = Array.isArray(selectedValue) ? selectedValue : [selectedValue]; - return options?.filter(({ value: currentValue }) => selectedValues.indexOf(currentValue) > -1)?.map(x => x.label)?.join(', '); + const selectedValues = Array.isArray(selectedValue) ? selectedValue : [selectedValue]; + return options?.filter(({ value: currentValue }) => selectedValues.indexOf(currentValue) > -1)?.map(({ label }) => ({ label })); }; if (readOnly) { - return ; + return options.find((o) => o.value === x)) : + getSelectValue() : + options.find((o) => o.value === selectedValue)} />; } + const commonSelectProps = { + allowClear, + onChange, + value: selectedValue, + defaultValue, + variant: 'borderless' as 'borderless' | 'filled' | 'outlined', + disabled: readOnly, + mode: selectedMode, + placeholder, + size + }; + + if (displayStyle === 'tags' && mode !== 'multiple') { + return ; + }; + return ( + @@ -146,14 +148,14 @@ export const EndpointsAutocomplete: FC = ({ readOnl dropdownStyle={props?.dropdownStyle} popupMatchSelectWidth={false} > - + ); return mode === 'endpoint' ? ( - + {autocomplete} ) diff --git a/shesha-reactjs/src/components/entityConfigTree/index.tsx b/shesha-reactjs/src/components/entityConfigTree/index.tsx index 8c7fcb8c3d..ebb84f55f5 100644 --- a/shesha-reactjs/src/components/entityConfigTree/index.tsx +++ b/shesha-reactjs/src/components/entityConfigTree/index.tsx @@ -6,14 +6,12 @@ import { Checkbox, Dropdown, MenuProps, - Spin, Tag } from 'antd'; import { ClassOutlined } from '@/icons/classOutlined'; import { DatabaseFilled, EyeInvisibleOutlined, - LoadingOutlined, QuestionCircleOutlined, UserAddOutlined, } from '@ant-design/icons'; @@ -25,6 +23,7 @@ import { ConfigurationItemVersionStatusMap } from '@/utils/configurationFramewor import { useStyles } from './styles/styles'; import SectionSeparator from '../sectionSeparator'; import { useConfigurableFormActions } from '@/providers/form/actions'; +import { ShaSpin } from '..'; type MenuItem = MenuProps['items'][number]; @@ -204,11 +203,7 @@ export const EntityConfigTree: FC = (props) => { ]; return ( - } - > +
@@ -274,7 +269,7 @@ export const EntityConfigTree: FC = (props) => { }} />
- + ); }; diff --git a/shesha-reactjs/src/components/entityPicker/index.tsx b/shesha-reactjs/src/components/entityPicker/index.tsx index 2443e9bd9f..f19998bbc2 100644 --- a/shesha-reactjs/src/components/entityPicker/index.tsx +++ b/shesha-reactjs/src/components/entityPicker/index.tsx @@ -1,7 +1,6 @@ import { EllipsisOutlined } from '@ant-design/icons'; -import { Button, Space, Select, Skeleton } from 'antd'; +import { Button, Select, Skeleton } from 'antd'; import { DefaultOptionType } from 'antd/lib/select'; -import _ from 'lodash'; import React, { useMemo, useRef, useState } from 'react'; import { useEntitySelectionData } from '@/utils/entity'; import ReadOnlyDisplayFormItem from '@/components/readOnlyDisplayFormItem'; @@ -11,7 +10,7 @@ import { useStyles } from './styles/styles'; import { EntityPickerModal } from './modal'; import { getValueByPropertyName } from '@/utils/object'; import { SheshaError } from '@/utils/errors'; - +import { addPx } from '@/utils/style'; const EntityPickerReadOnly = (props: IEntityPickerProps) => { const { entityType, displayEntityKey, value } = props; @@ -63,13 +62,14 @@ const EntityPickerEditable = (props: IEntityPickerProps) => { title = 'Select Item', outcomeValueFunc, incomeValueFunc, - placeholder + placeholder, + dividerStyle } = props; if (!entityType) throw SheshaError.throwPropertyError('entityType'); - const { styles } = useStyles(); + const { styles } = useStyles({ style }); const selectRef = useRef(undefined); const [showModal, setShowModal] = useState(false); @@ -147,47 +147,97 @@ const EntityPickerEditable = (props: IEntityPickerProps) => { if (onChange) onChange(null, null); }; + const { borderBottomLeftRadius, + borderTopLeftRadius, borderTopRightRadius, + borderBottomRightRadius, width, minWidth, + maxWidth, boxShadow, background, backgroundImage, + marginTop, marginRight, marginBottom, + height, minHeight, maxHeight, + marginLeft, paddingTop, paddingRight, paddingBottom, + backgroundSize, backgroundPosition, backgroundRepeat, + paddingLeft, ...restStyle } = style; + + const borderRadii = style?.borderRadius?.toString().split(' '); + return ( -
+
{useButtonPicker ? ( - ) : ( - - -
)}
diff --git a/shesha-reactjs/src/components/entityPicker/modal.tsx b/shesha-reactjs/src/components/entityPicker/modal.tsx index b425e64303..e88c6573f2 100644 --- a/shesha-reactjs/src/components/entityPicker/modal.tsx +++ b/shesha-reactjs/src/components/entityPicker/modal.tsx @@ -1,4 +1,4 @@ -import { DataTable, DataTableProvider, GlobalTableFilter, IAnyObject, TablePager, evaluateDynamicFilters, useDataContextManager, useDataTable, useForm, useGlobalState, useModal, useNestedPropertyMetadatAccessor } from '@/index'; +import { DataTable, DataTableProvider, GlobalTableFilter, IAnyObject, TablePager, evaluateDynamicFilters, useDataContextManagerActions, useDataTable, useForm, useGlobalState, useModal, useNestedPropertyMetadatAccessor } from '@/index'; import React, { useEffect, useState } from 'react'; import { useStyles } from './styles/styles'; import { useMedia } from 'react-use'; @@ -35,7 +35,7 @@ const EntityPickerModalInternal = (props: IEntityPickerModalProps) => { onCloseModal, } = props; - const { styles } = useStyles(); + const { styles } = useStyles({}); const [modalId] = useState(nanoid()); // use generated value because formId was changed. to be reviewed const [state, setState] = useState({ showModal: true }); const hidePickerDialog = () => { @@ -54,8 +54,8 @@ const EntityPickerModalInternal = (props: IEntityPickerModalProps) => { const { globalState } = useGlobalState(); const { formData } = useForm(); - const pageContext = useDataContextManager(false)?.getPageContext(); - + const pageContext = useDataContextManagerActions(false)?.getPageContext(); + useEffect(() => { registerConfigurableColumns(modalId, configurableColumns); }, [configurableColumns]); @@ -72,13 +72,13 @@ const EntityPickerModalInternal = (props: IEntityPickerModalProps) => { onSelect(row); } else { if (isMultiple) { - const values = !!valueId ? (Array.isArray(valueId) ? valueId : [valueId]): []; + const values = !!valueId ? (Array.isArray(valueId) ? valueId : [valueId]) : []; if (!values.includes(row.id)) { - const vs = !!value ? (Array.isArray(value) ? value : [value]): []; + const vs = !!value ? (Array.isArray(value) ? value : [value]) : []; onChange([...vs, outcomeValueFunc(row, {})], null); } } else { - onChange(outcomeValueFunc(row, {}),null); + onChange(outcomeValueFunc(row, {}), null); } } @@ -117,7 +117,7 @@ const EntityPickerModalInternal = (props: IEntityPickerModalProps) => { [ { match: 'data', data: formData }, { match: 'globalState', data: globalState }, - { match: 'pageContext', data: {...pageContext?.getFull()} }, + { match: 'pageContext', data: { ...pageContext?.getFull() } }, ], propertyMetadataAccessor ).then(evaluatedFilters => { @@ -233,7 +233,7 @@ const EntityPickerModalInternal = (props: IEntityPickerModalProps) => { }; export const EntityPickerModal = (props: IEntityPickerModalProps) => { - + return ( { formId?: FormIdentifier; - + hideBorder?: boolean; onChange?: (value: string | string[] | IEntityReferenceDto | IEntityReferenceDto[], data: IAnyObject) => void; onSelect?: (data: IAnyObject) => void; value?: string | string[] | IEntityReferenceDto | IEntityReferenceDto[] | any; @@ -65,4 +66,5 @@ export interface IEntityPickerProps extends Omit { - const pickerEllipsisBtnWidth = "45px"; +export const useStyles = createStyles(({ css, cx, prefixCls, token }, { style }: { style?: CSSProperties }) => { + const pickerEllipsisBtnWidth = "45px"; - const pickerInputGroup = "picker-input-group"; - const pickerInputGroupInput = "picker-input-group-input"; - const pickerInputGroupEllipsis = "picker-input-group-ellipsis"; - const entityPickerModalPagerContainer = "entity-picker-modal-pager-container"; + const pickerInputGroup = "picker-input-group"; + const pickerInputGroupInput = "picker-input-group-input"; + const pickerInputGroupEllipsis = "picker-input-group-ellipsis"; + const entityPickerModalPagerContainer = "entity-picker-modal-pager-container"; - const shaReactTable = "sha-react-table"; - const shaGlobalTableFilter = "sha-global-table-filter"; - + const shaReactTable = "sha-react-table"; + const shaGlobalTableFilter = "sha-global-table-filter"; - const entityPickerContainer = cx("entity-picker-container", css` + const entityPickerContainer = cx("entity-picker-container", css` + width: ${style?.width || '100%'}; .${pickerInputGroup} { .${pickerInputGroupInput} { - width: calc(100% - ${pickerEllipsisBtnWidth}); + width: calc(100% + ${pickerEllipsisBtnWidth}); } - + .${pickerInputGroupEllipsis} { width: ${pickerEllipsisBtnWidth}; } - } + } + .${pickerInputGroupEllipsis} { + &:hover { + border-color: ${token.colorPrimary} !important; + } + } .global-tablefilter { padding-right: unset !important; } `); - const entityPickerModal = cx("entity-picker-modal", css` + const entityPickerModal = cx("entity-picker-modal", css` .${prefixCls}-modal-body { .ant-alert { margin-bottom: 8px; @@ -43,7 +48,8 @@ export const useStyles = createStyles(({ css, cx, prefixCls }) => { .${shaReactTable} { margin: unset; - } + overflow: auto; + ${sheshaStyles.thinScrollbars} .${entityPickerModalPagerContainer} { display: flex; @@ -64,12 +70,56 @@ export const useStyles = createStyles(({ css, cx, prefixCls }) => { } `); - return { - entityPickerContainer, - pickerInputGroup, - pickerInputGroupInput, - pickerInputGroupEllipsis, - entityPickerModalPagerContainer, - entityPickerModal, - }; + const entitySelect = cx("entity-select", css` + --ant-color-text: ${style?.color || '#000'} !important; + width: calc(100% - 32px) !important; + &:hover { + border-color: ${token.colorPrimary} !important; + } + + .ant-select-selector { + overflow: auto !important; + scrollbar-width: thin !important; + -ms-overflow-style: none !important; + &::-webkit-scrollbar { + width: 8px !important; + } + } + + .ant-select-selection-overflow-item-suffix { + display: none !important; + }; + + .ant-select-selector > ant-select-selection-search, ant-select-selection-placeholder { + border-right: 1px solid #d9d9d9; + padding: 0 8px !important; + * { + font-size: ${style?.fontSize || '14px'} !important; + font-weight: ${style?.fontWeight} !important; + color: ${style?.color || '#000'} !important; + font-family: ${style?.fontFamily || 'inherit'} !important; + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } + } + + .ant-select-selection-item { + font-size: ${style?.fontSize || '14px'} !important; + font-weight: ${style?.fontWeight} !important; + color: ${style?.color || '#000'} !important; + font-family: ${style?.fontFamily || 'inherit'} !important; + + } + + `); + + return { + entityPickerContainer, + pickerInputGroup, + pickerInputGroupInput, + pickerInputGroupEllipsis, + entityPickerModalPagerContainer, + entityPickerModal, + entitySelect + }; }); \ No newline at end of file diff --git a/shesha-reactjs/src/components/entityReference/index.tsx b/shesha-reactjs/src/components/entityReference/index.tsx index a524a6f2c7..dac001fc89 100644 --- a/shesha-reactjs/src/components/entityReference/index.tsx +++ b/shesha-reactjs/src/components/entityReference/index.tsx @@ -1,14 +1,10 @@ -import moment from 'moment'; -import React, { FC, useEffect, useMemo, useState } from 'react'; -import { get } from '@/utils/fetchers'; -import { Button, App, Spin } from 'antd'; import { entitiesGet } from '@/apis/entities'; +import { ShaIcon, ShaLink, ValidationErrors } from '@/components'; import { GenericQuickView } from '@/components/quickView'; import { IConfigurableActionConfiguration } from '@/interfaces/configurableAction'; -import { IKeyValue } from '@/interfaces/keyValue'; -import { ShaIcon, ShaLink, ValidationErrors } from '@/components'; import { StandardNodeTypes } from '@/interfaces/formComponent'; -import { useConfigurationItemsLoader } from '@/providers/configurationItemsLoader'; +import { IKeyValue } from '@/interfaces/keyValue'; +import { isPropertiesArray } from '@/interfaces/metadata'; import { ButtonGroupItemProps, FormIdentifier, @@ -19,12 +15,16 @@ import { useMetadataDispatcher, useSheshaApplication, } from '@/providers'; -import { useStyles } from './styles/styles'; -import { isPropertiesArray } from '@/interfaces/metadata'; +import { useConfigurationItemsLoader } from '@/providers/configurationItemsLoader'; import { ModalFooterButtons } from '@/providers/dynamicModal/models'; -import { getStyle, useAvailableConstantsData } from '@/providers/form/utils'; import { getFormApi } from '@/providers/form/formApi'; +import { useAvailableConstantsData } from '@/providers/form/utils'; +import { get } from '@/utils/fetchers'; +import { App, Button, Spin } from 'antd'; +import moment from 'moment'; +import React, { CSSProperties, FC, useEffect, useMemo, useState } from 'react'; import { ShaIconTypes } from '../iconPicker'; +import { innerEntityReferenceButtonBoxStyle, innerEntityReferenceSpanBoxStyle } from '../quickView/utils'; export type EntityReferenceTypes = 'NavigateLink' | 'Quickview' | 'Dialog'; @@ -71,7 +71,7 @@ export interface IEntityReferenceProps { onSuccess?: IConfigurableActionConfiguration; handleFail: boolean; onFail?: IConfigurableActionConfiguration; - style?: string; + style?: CSSProperties; displayType?: 'textTitle' | 'icon' | 'displayProperty'; iconName?: ShaIconTypes; textTitle?: string; @@ -80,7 +80,6 @@ export interface IEntityReferenceProps { export const EntityReference: FC = (props) => { const { executeAction } = useConfigurableActionDispatcher(); const { globalState } = useGlobalState(); - const { styles } = useStyles(); const { notification, message } = App.useApp(); const localForm = useForm(false); @@ -97,7 +96,7 @@ export const EntityReference: FC = (props) => { props.formSelectionMode === 'name' ? props.formIdentifier : null ); const [fetched, setFetched] = useState(false); - const [property, setProperty] = useState([]); + const [properties, setProperties] = useState([]); const [displayText, setDisplayText] = useState((!props.value ? props.placeholder : props.value._displayName) ?? ''); @@ -105,37 +104,64 @@ export const EntityReference: FC = (props) => { const entityType = props.entityType ?? props.value?._className; const formType = props.formType ?? (props.entityReferenceType === 'Quickview' ? 'quickview' : 'details'); - const style = getStyle(props.style, formData); - useEffect(() => { - if ( - !Boolean(formIdentifier) && - props.formSelectionMode === 'dynamic' && - Boolean(entityType) && - Boolean(formType) && - props.entityReferenceType !== 'Quickview' - ) { - getEntityFormId(entityType, formType).then((formid) => { - setFormIdentifier({ name: formid.name, module: formid.module }); - }); - } - }, [formIdentifier, entityType, formType]); + let isSubscribed = true; + + const fetchFormId = async () => { + if ( + !formIdentifier && + props.formSelectionMode === 'dynamic' && + Boolean(entityType) && + Boolean(formType) && + props.entityReferenceType !== 'Quickview' + ) { + try { + const formid = await getEntityFormId(entityType, formType); + if (isSubscribed) { + setFormIdentifier({ name: formid.name, module: formid.module }); + } + } catch (error) { + console.error('Error fetching form ID:', error); + } + } + }; + + fetchFormId(); + + return () => { + isSubscribed = false; + }; + }, [formIdentifier, entityType, formType, props.formSelectionMode, props.entityReferenceType]); useEffect(() => { - if (entityType) { - getMetadata({ modelType: entityType, dataType: null }).then((res) => { - setProperty(isPropertiesArray(res?.properties) ? res.properties : []); - }); - } + let isSubscribed = true; + + const fetchMetadata = async () => { + if (entityType) { + try { + const res = await getMetadata({ modelType: entityType, dataType: null }); + if (isSubscribed) { + setProperties(isPropertiesArray(res?.properties) ? res.properties : []); + } + } catch (error) { + console.error('Error fetching metadata:', error); + } + } + }; + + fetchMetadata(); + + return () => { + isSubscribed = false; + }; }, [entityType]); useEffect(() => { // fetch data only for NavigateLink and Dialog mode. Quickview will fetch data later if (!fetched && props.entityReferenceType !== 'Quickview' && entityId) { - // const queryParams = { id: entityId, - properties: `id ${Boolean(props.displayProperty) ? props.displayProperty : ''}`, + properties: `id ${props.displayProperty ? props.displayProperty : ''}`, }; const fetcher = props.getEntityUrl ? get(props.getEntityUrl, queryParams, { base: backendUrl, headers: httpHeaders }) @@ -143,22 +169,28 @@ export const EntityReference: FC = (props) => { fetcher .then((resp) => { - setDisplayText(resp.result[props.displayProperty] ?? displayText); + setDisplayText(resp.result[props.displayProperty] ?? displayText ?? 'No Display Name'); setFetched(true); }) .catch((reason) => { notification.error({ message: }); }); } - }, [fetched, entityId, props.entityReferenceType, props.getEntityUrl, entityType]); + }, [fetched, entityId, props.entityReferenceType, props.getEntityUrl, entityType, backendUrl, displayText, httpHeaders, notification, props.displayProperty]); useEffect(() => { setFetched(false); - if (!!props?.value?._displayName) setDisplayText(props?.value?._displayName); - }, [entityId, entityType]); + if (props?.value?._displayName) setDisplayText(props?.value?._displayName); + else setDisplayText(!props?.value ? props?.placeholder : ''); + }, [entityId, entityType, props?.placeholder, props?.value]); - /* Dialog */ + useEffect(() => { + if (props.formIdentifier) { + setFormIdentifier(props.formIdentifier); + } + }, [props.formIdentifier]); + /* Dialog */ const dialogExecute = (event: React.MouseEvent) => { event.stopPropagation(); // Don't collapse the CollapsiblePanel when clicked @@ -205,42 +237,37 @@ export const EntityReference: FC = (props) => { }); }; - const renderDisplayByType = () => { - if (props.displayType === 'icon') return ; - - if (props.displayType === 'textTitle') return props.textTitle; - - return displayText; - }; - - const navigationStyling = { ...style, marginTop: style?.marginTop ? style.marginTop : '3px' }; + const displayTextByType = useMemo(() => { + return props.displayType === 'icon' ? ( + + ) : props.displayType === 'textTitle' ? ( + props.textTitle + ) : ( + displayText + ); + }, [props.displayType, props.iconName, props.textTitle, displayText, props.style]); const content = useMemo(() => { - if (!((formIdentifier && renderDisplayByType() && entityId) || props.entityReferenceType === 'Quickview')) + if (!(fetched || props.entityReferenceType === 'Quickview')) return ( - ); if (props.disabled && props.entityReferenceType !== 'Quickview') return ( - + + {displayTextByType} + ); if (props.entityReferenceType === 'NavigateLink') return ( - + + {displayTextByType} + ); if (props.entityReferenceType === 'Quickview') @@ -248,7 +275,7 @@ export const EntityReference: FC = (props) => { = (props) => { ); return ( - ); - }, [formIdentifier, displayText, entityId, props.disabled, property.length]); + }, [props.formIdentifier, displayText, entityId, props.disabled, properties.length, displayTextByType, fetched]); - if (props.formSelectionMode === 'name' && !Boolean(formIdentifier)) + if (props.formSelectionMode === 'name' && !props.formIdentifier) return ( - ); if (!props.value) return ( - ); diff --git a/shesha-reactjs/src/components/entityReference/styles/styles.ts b/shesha-reactjs/src/components/entityReference/styles/styles.ts deleted file mode 100644 index 8bf109610b..0000000000 --- a/shesha-reactjs/src/components/entityReference/styles/styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createStyles } from '@/styles'; - -export const useStyles = createStyles(({ css, cx }) => { - const entityReferenceBtn = cx("entity-reference-btn", css` - padding: unset !important; - `); - return { - entityReferenceBtn, - }; -}); \ No newline at end of file diff --git a/shesha-reactjs/src/components/fileUpload/index.tsx b/shesha-reactjs/src/components/fileUpload/index.tsx index 464d2e9a5e..7943fa4910 100644 --- a/shesha-reactjs/src/components/fileUpload/index.tsx +++ b/shesha-reactjs/src/components/fileUpload/index.tsx @@ -1,21 +1,23 @@ -import React, { FC, useRef } from 'react'; -import { useStoredFile } from '@/providers'; -import { Upload, App, Button } from 'antd'; -import { UploadRequestOption as RcCustomRequestOptions } from 'rc-upload/lib/interface'; import { - InfoCircleOutlined, - SyncOutlined, DeleteOutlined, - LoadingOutlined, + DownloadOutlined, + EyeOutlined, + PictureOutlined, + SyncOutlined, UploadOutlined, } from '@ant-design/icons'; +import { App, Button, Space, Upload } from 'antd'; +import { Image } from 'antd/lib'; import { UploadProps } from 'antd/lib/upload/Upload'; import filesize from 'filesize'; -import { FileVersionsPopup } from './fileVersionsPopup'; +import { UploadRequestOption as RcCustomRequestOptions } from 'rc-upload/lib/interface'; +import React, { FC, useEffect, useRef, useState } from 'react'; +import { listType } from '@/designer-components/attachmentsEditor/attachmentsEditor'; +import { getFileIcon, isImageType } from '@/icons/fileIcons'; +import { useSheshaApplication, useStoredFile, useTheme } from '@/providers'; +import FileVersionsPopup from './fileVersionsPopup'; import { DraggerStub } from './stubs'; import { useStyles } from './styles/styles'; -import classNames from 'classnames'; - const { Dragger } = Upload; export interface IFileUploadProps { @@ -25,21 +27,32 @@ export interface IFileUploadProps { callback?: (...args: any) => any; value?: any; onChange?: any; - /* isStub is used just to fix strange error when the user is reordering components on the form */ isStub?: boolean; allowedFileTypes?: string[]; isDragger?: boolean; + listType?: listType; + thumbnailWidth?: string; + thumbnailHeight?: string; + borderRadius?: number; + hideFileName?: boolean; + styles?: any; + primaryColor?: string; + type?: string; } export const FileUpload: FC = ({ + type, allowUpload = true, allowReplace = true, allowDelete = true, - //uploadMode = 'async', callback, isStub = false, allowedFileTypes = [], isDragger = false, + listType = 'text', + hideFileName = false, + styles: stylesProp, + primaryColor, }) => { const { fileInfo, @@ -47,30 +60,48 @@ export const FileUpload: FC = ({ deleteFile, uploadFile, isInProgress: { uploadFile: isUploading }, - /* - succeeded: { downloadZip: downloadZipSuccess }, - */ } = useStoredFile(); - const { styles } = useStyles(); - const uploadButtonRef = useRef(null); + const { backendUrl, httpHeaders } = useSheshaApplication(); + const props = { + style: stylesProp, + primaryColor, + model: { + layout: listType === 'thumbnail' && !isDragger, + isDragger, + hideFileName, + listType, + }, + }; + const { styles } = useStyles(props); + const { theme } = useTheme(); const uploadDraggerSpanRef = useRef(null); + const hiddenUploadInputRef = useRef(null); const { message } = App.useApp(); + const [imageUrl, setImageUrl] = useState(''); + const [previewOpen, setPreviewOpen] = useState(false); + const [previewImage, setPreviewImage] = useState({ url: '', uid: '', name: '' }); - const onCustomRequest = ({ file /*, onError, onSuccess*/ }: RcCustomRequestOptions) => { - // call action from context - uploadFile({ file: file as File }, callback); - }; + const url = fileInfo?.url ? `${backendUrl}${fileInfo.url}` : ''; + useEffect(() => { + if (fileInfo && url) { + fetch(url, { headers: { ...httpHeaders, 'Content-Type': 'application/octet-stream' } }) + .then((response) => response.blob()) + .then((blob) => URL.createObjectURL(blob)) + .then((url) => setImageUrl(url)); + } + }, [fileInfo]); - const onDownloadClick = (e: React.MouseEvent) => { - e.preventDefault(); - downloadFile({ fileId: fileInfo.id, fileName: fileInfo.name }); + const onCustomRequest = ({ file }: RcCustomRequestOptions) => { + uploadFile({ file: file as File }, callback); }; const onReplaceClick = (e: React.MouseEvent) => { e.preventDefault(); - + e.stopPropagation(); if (!isDragger) { - uploadButtonRef.current.click(); + if (hiddenUploadInputRef.current) { + hiddenUploadInputRef.current.click(); + } } else { if (uploadDraggerSpanRef.current) { uploadDraggerSpanRef.current.click(); @@ -83,26 +114,99 @@ export const FileUpload: FC = ({ deleteFile(); }; - const fileControls = () => { - return ( - - {fileInfo && ( -
- {false && } - - - )} - {allowReplace && ( - - - - )} - {allowDelete && ( - - + const onPreview = () => { + if (fileInfo) { + if (!url) { + message.error('Preview URL not available'); + return; + } + setPreviewImage({ url, uid: fileInfo?.id, name: fileInfo?.name }); + setPreviewOpen(true); + } + }; + + const fileControls = (color: string) => ( + + + + + {allowReplace && ( + + + + )} + {allowDelete && ( + onDeleteClick(e)} style={{ color: color }}> + + + )} + {listType === 'thumbnail' && + (isImageType(fileInfo?.type) ? ( + + - )} - + ) : ( + hideFileName && ( + downloadFile({ fileId: fileInfo?.id, fileName: fileInfo?.name })} + style={{ color: color }} + > + + + ) + ))} + + ); + + const iconRender = (fileInfo) => { + const { type, name } = fileInfo; + if (isImageType(type)) { + if (listType === 'thumbnail' && !isDragger) { + return {name}; + } + } + return getFileIcon(type); + }; + + const styledfileControls = () => + fileInfo && ( +
+ {iconRender(fileInfo)} +
+ {fileControls('#fff')} +
+
+ ); + + const renderFileItem = (file: any) => { + const showThumbnailControls = !isUploading && listType === 'thumbnail'; + const showTextControls = listType === 'text'; + + return ( + ); }; @@ -112,74 +216,115 @@ export const FileUpload: FC = ({ accept: allowedFileTypes?.join(','), multiple: false, fileList: fileInfo ? [fileInfo] : [], + style: !isDragger && stylesProp, customRequest: onCustomRequest, onChange(info) { - if (info.file.status !== 'uploading') { - // - } if (info.file.status === 'done') { - message.success(`${info.file.name} file uploaded successfully`); + message.success(`${info.file.name} uploaded successfully`); } else if (info.file.status === 'error') { - message.error(`${info.file.name} file upload failed.`); + message.error(`${info.file.name} upload failed`); } }, - itemRender: (_originNode, file, _currFileList) => { - return ( -
-
- {isUploading && } - - {file.name} ({filesize(file.size)}) - - - {!isUploading && fileControls()} -
-
- ); - }, + itemRender: (_originNode, file) => renderFileItem(file), }; - const showUploadButton = allowUpload && !fileInfo && !isUploading; - const classes = classNames(styles.shaUpload, { [styles.shaUploadHasFile]: fileInfo || isUploading }); + const showUploadButton = allowUpload && !isUploading; const uploadButton = ( ); const renderStub = () => { if (isDragger) { - return ; + return ( + + + + ); } - return
{uploadButton}
; + return ( + <> +
+ {uploadButton} +
+ {listType === 'thumbnail' && !hideFileName ?
File name
: null} + + ); }; const renderUploader = () => { + const antListType = listType === 'thumbnail' ? 'picture-card' : 'text'; + if (isDragger && allowUpload) { return ( - + - + ); } return ( - - {allowUpload && uploadButton} +
+ + {allowUpload && !fileInfo && uploadButton} - ); +
+ ); }; + return ( + <> + {isStub ? renderStub() : renderUploader()} + {previewOpen && ( + setPreviewOpen(visible), + toolbarRender: (original) => ( +
+ { + downloadFile({ fileId: previewImage?.uid, fileName: previewImage?.name })} + /> + } + {original} +
+ ), + }} + src={imageUrl} + /> + )} - return {isStub ? renderStub() : renderUploader()}; + {/* Hidden file input for replace functionality */} + { + const file = e.target.files?.[0]; + if (file) { + uploadFile({ file }, callback); + } + e.target.value = ''; // Reset for next time + }} + /> + + ); }; export default FileUpload; diff --git a/shesha-reactjs/src/components/fileUpload/stubs.tsx b/shesha-reactjs/src/components/fileUpload/stubs.tsx index 5c2b505cd8..c81547df23 100644 --- a/shesha-reactjs/src/components/fileUpload/stubs.tsx +++ b/shesha-reactjs/src/components/fileUpload/stubs.tsx @@ -1,19 +1,23 @@ -import React, { FC } from 'react'; +import React from 'react'; import { InboxOutlined } from '@ant-design/icons'; -import { useStyles } from './styles/styles'; -export const DraggerStub: FC = () => { - const { styles } = useStyles(); +interface DraggerStubProps { + styles?: any; + type?: string; +} +export const DraggerStub = ({ styles, type }: DraggerStubProps) => { return (
-

- -

-

Click or drag file to this area to upload

-

- Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files -

+

+ +

+

Click or drag file to this area to upload

+

+ {type === 'fileUpload' + ? 'Support for a single file upload. Strictly prohibit from uploading company data or other band files' + : 'Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files'} +

); }; diff --git a/shesha-reactjs/src/components/fileUpload/styles/styles.ts b/shesha-reactjs/src/components/fileUpload/styles/styles.ts index e0a62278f2..4a1a57778f 100644 --- a/shesha-reactjs/src/components/fileUpload/styles/styles.ts +++ b/shesha-reactjs/src/components/fileUpload/styles/styles.ts @@ -1,79 +1,321 @@ import { createStyles } from '@/styles'; -export const useStyles = createStyles(({ css, cx, token, prefixCls }) => { - const shaUpload = "sha-upload"; - const shaUploadHasFile = "sha-upload-has-file"; - const shaUploadListItemError = "sha-upload-list-item-error"; - const shaUploadUploading = "sha-upload-uploading"; - const shaUploadHistoryControl = "sha-upload-history-control"; - const shaUploadReplaceControl = "sha-upload-replace-control"; - const shaUploadRemoveControl = "sha-upload-remove-control"; - const shaUploadListItemInfo = "sha-upload-list-item-info"; - - const antUploadDragIcon = `${prefixCls}-upload-drag-icon`; - const antUploadText = `${prefixCls}-upload-text`; - const antUploadHint = `${prefixCls}-upload-hint`; - - const shaFileUploadContainer = cx("sha-file-upload-container", css` - .${shaUpload} { - &.${shaUploadHasFile} { - .${prefixCls}-upload-select { - position: absolute; - top: -1000px; - left: -1000px; - } - - &.${prefixCls}-upload-drag { - padding: unset; - border: unset; - - .${prefixCls}-upload-btn { - padding: unset; - } - } +export const useStyles = createStyles(({ token, css, cx, prefixCls }, { style, model, primaryColor }) => { + const { + background = 'transparent', + backgroundImage, + borderRadius = '8px', + borderWidth = '0', + borderTopWidth, + width, + minWidth, + maxWidth, + borderBottomWidth, + borderLeftWidth, + borderLeftColor, + borderLeftStyle, + borderRightColor, + borderRightStyle, + borderColor = '#d9d9d9', + borderTopStyle, + borderTopColor, + borderTop, + boxShadow, + borderBottom, + borderBottomColor, + borderBottomStyle, + borderRight, + borderRightWidth, + backgroundColor, + backgroundPosition, + backgroundRepeat, + backgroundSize, + borderStyle = 'solid', + color, + fontFamily = 'Segoe UI', + fontSize = '25px', + fontWeight = '400', + height, + maxHeight, + minHeight, + textAlign = 'center', + } = style || {}; + + const { layout, isDragger, hideFileName, listType } = model; + + const antUploadDragIcon = `${prefixCls}-upload-drag-icon`; + const antUploadText = `${prefixCls}-upload-text`; + const antUploadHint = `${prefixCls}-upload-hint`; + + const storedFilesRendererBtnContainer = 'stored-files-renderer-btn-container'; + const storedFilesRendererNoFiles = 'stored-files-renderer-no-files'; + + const commonBorderStyles = css` + border: ${borderWidth} ${borderStyle} ${borderColor}; + border-top: ${borderTopWidth || borderWidth} ${borderTopStyle || borderStyle} ${borderTopColor || borderColor}; + border-right: ${borderRightWidth || borderWidth} ${borderRightStyle || borderStyle} + ${borderRightColor || borderColor}; + border-left: ${borderLeftWidth || borderWidth} ${borderLeftStyle || borderStyle} ${borderLeftColor || borderColor}; + border-bottom: ${borderBottomWidth || borderWidth} ${borderBottomStyle || borderStyle} + ${borderBottomColor || borderColor}; + box-shadow: ${boxShadow}; + `; + + const commonTextStyles = css` + color: ${color || token.colorPrimary}; + font-family: ${fontFamily}; + font-size: ${fontSize}; + font-weight: ${fontWeight}; + text-align: ${textAlign}; + `; + const shaStoredFilesRenderer = cx( + 'sha-stored-files-renderer', + css` + --thumbnail-width: ${layout ? (width ?? height ?? '54px') : '100%'}; + --thumbnail-height: ${layout ? (height ?? width ?? '54px') : '100%'}; + --ant-border-radius-xs: ${borderRadius} !important; + --ant-border-radius-sm: ${borderRadius} !important; + --ant-border-radius-lg: ${borderRadius} !important; + --ant-button-content-font-size: ${fontSize} !important; + --ant-button-font-weight: ${fontWeight} !important; + --ant-font-family: ${fontFamily} !important; + height: ${height ?? '54px'} !important; + width: ${width ?? '54px'} !important; + max-height: ${maxHeight ?? 'auto'} !important; + min-height: ${minHeight} !important; + max-width: ${maxWidth} !important; + min-width: ${minWidth} !important; + + .ant-upload-select-picture-card { + width: var(--thumbnail-width) !important; + height: var(--thumbnail-height) !important; + background-position: ${backgroundPosition} !important; + background-repeat: ${backgroundRepeat} !important; + background-size: ${backgroundSize} !important; + } + + .ant-upload-list-item { + width: var(--thumbnail-width) !important; + height: calc(var(--thumbnail-height) + 32px) !important; + border-top: ${borderTop} !important; + border-bottom: ${borderBottom} !important; + border-right: ${borderRight} !important; + } + + .ant-upload-list-picture-card { + height: ${hideFileName + ? 'var(--thumbnail-height)' + : `calc(var(--thumbnail-height) + ${fontSize} * 2 + 32px)`} !important; + padding-bottom: 1rem; + } + + .ant-upload-list-item-image { + object-fit: contain !important; + width: 100% !important; + height: 100% !important; + } + + .ant-upload:not(.ant-upload-disabled) { + .icon { + color: ${primaryColor || token.colorPrimary} !important; + } + } + + .ant-upload-list-item { + --ant-line-width: 0px !important; + --ant-padding-xs: 0px !important; + --font-size: ${fontSize} !important; + --ant-font-size: ${fontSize} !important; + border-radius: ${borderRadius} !important; + border: 1px dashed ${borderColor} !important; + display: flex; + + :before { + top: 0; + width: 100% !important; + border-radius: ${borderRadius} !important; + border: 1px dashed ${borderColor} !important; + height: 100% !important; + } + } + + .ant-upload-list-item-thumbnail { + border-radius: ${borderRadius} !important; + padding: 0 !important; + ${commonBorderStyles} + ${style} + } + + .thumbnail-item-name { + ${commonTextStyles} + a { + ${commonTextStyles} + } + .ant-space { + .anticon { + color: ${primaryColor} !important; } - - .${prefixCls}-upload-list { - .${shaUploadListItemError} { - a { - color: ${token.colorError}; - } - } + } + ${listType !== 'thumbnail' && style?.jsStyle} + } + + .thumbnail-stub { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border: 1px ${borderStyle} ${borderColor} !important; + ${style} + } + + .ant-upload-list-text { + ${commonTextStyles} + height: calc(var(--container-height) - 32px) !important; + max-height: calc(var(--container-max-height) - calc(${fontSize} * 4)) !important; + min-height: calc(var(--container-min-height) - 32px) !important; + width: calc(var(--container-width) - 32px) !important; + max-width: calc(var(--container-max-width) - 32px) !important; + min-width: calc(var(--container-min-width) - 32px) !important; + } + + .ant-upload-drag:hover:not(.ant-upload-disabled) { + border-color: ${token.colorPrimary} !important; + } + + .${prefixCls}-upload { + ${layout && !isDragger && 'width: var(--thumbnail-width) !important;'}; + ${layout && !isDragger && 'height: var(--thumbnail-height) !important'}; + border-radius: ${borderRadius} !important; + align-items: center; + + &.${prefixCls}-upload-btn { + .${prefixCls}-upload-drag-icon { + margin: unset; + } + + .ant-upload-select { + align-content: center; } } - - .${shaUploadUploading} { - margin-right: 5px; + } + + .ant-btn { + color: ${primaryColor || token.colorPrimary} !important; + padding: 0; + * { + font-size: ${fontSize} !important; + font-weight: ${fontWeight} !important; + font-family: ${fontFamily} !important; } - - .${shaUploadHistoryControl}, - .${shaUploadReplaceControl}, - .${shaUploadRemoveControl} { - margin-left: 5px; + ${listType === 'thumbnail' && style} + } + + .ant-upload-list-item-container { + background: ${backgroundImage ?? backgroundColor} !important; + width: var(--thumbnail-width) !important; + height: var(--thumbnail-height) !important; + border-radius: ${borderRadius} !important; + border: ${borderWidth} ${listType === 'thumbnail' ? borderStyle : 'none'} ${borderColor} !important; + &.ant-upload-animate-inline-appear, + &.ant-upload-animate-inline-appear-active, + &.ant-upload-animate-inline { + display: none !important; + animation: none !important; + transition: none !important; } + ${listType !== 'thumbnail' && style} + } + ` + ); - .${prefixCls}-upload { - &.${prefixCls}-upload-btn { - padding: 8px 0; - - .${prefixCls}-upload-drag-icon { - margin: unset; - } - } + const antPreviewDownloadIcon = cx( + 'ant-preview-download-icon', + css` + background: #0000001a; + font-size: 24px; + padding: 8px; + border-radius: 100px; + :hover { + color: #fff; + } + ` + ); + + const thumbnailControls = cx( + 'thumbnail-controls', + css` + width: var(--thumbnail-width, 54px) !important; + height: var(--thumbnail-height, 54px) !important; + border-radius: ${borderRadius} !important; + object-fit: cover !important; + display: flex !important; + justify-content: center !important; + ` + ); + + const overlayThumbnailControls = cx( + 'overlay-thumbnail-controls', + css` + position: absolute; + top: 0; + left: 0; + background: rgba(0, 0, 0, 0.6); + height: ${height ?? '54px'} !important; + width: ${width ?? '54px'} !important; + opacity: 0; + border-radius: 8px; + transition: opacity 0.3s ease; + display: flex; + justify-content: center; + align-items: center; + + .ant-space { + display: grid !important; + grid-template-columns: repeat(2, minmax(24px, auto)) !important; + gap: 4px !important; + place-items: center !important; + place-content: center !important; + } + + &:hover { + opacity: 1; + } + ` + ); + + const styledFileControls = cx( + 'styled-file-controls', + css` + ${commonBorderStyles} + ${commonTextStyles} + border-radius: ${borderRadius} !important; + padding: 0 !important; + background: ${background ?? backgroundImage ?? backgroundColor} !important; + width: ${width || '54px'} !important; + height: ${height || '54px'} !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + + .anticon { + img { + object-fit: cover !important; } - `); - return { - shaFileUploadContainer, - shaUpload, - shaUploadHasFile, - shaUploadListItemError, - shaUploadUploading, - shaUploadHistoryControl, - shaUploadReplaceControl, - shaUploadRemoveControl, - shaUploadListItemInfo, - antUploadDragIcon, - antUploadText, - antUploadHint, - }; -}); \ No newline at end of file + } + ${style} + ` + ); + + return { + shaStoredFilesRenderer, + storedFilesRendererBtnContainer, + storedFilesRendererNoFiles, + antUploadDragIcon, + antPreviewDownloadIcon, + thumbnailControls, + overlayThumbnailControls, + antUploadText, + antUploadHint, + styledFileControls, + }; +}); diff --git a/shesha-reactjs/src/components/formComponentSelector/adapters.ts b/shesha-reactjs/src/components/formComponentSelector/adapters.ts index 321d8314bd..38a049d1bf 100644 --- a/shesha-reactjs/src/components/formComponentSelector/adapters.ts +++ b/shesha-reactjs/src/components/formComponentSelector/adapters.ts @@ -1,5 +1,4 @@ import StatusTagComponent from '@/designer-components/statusTag'; -import AutocompleteComponent from '@/designer-components/autocomplete/autocomplete'; import CheckboxComponent from '@/designer-components/checkbox/checkbox'; import DateFieldComponent from '@/designer-components/dateField/dateField'; import DropdownComponent from '@/designer-components/dropdown'; @@ -10,6 +9,7 @@ import RefListStatusComponent from '@/designer-components/refListStatus/index'; import TextFieldComponent from '@/designer-components/textField/textField'; import { TimeFieldComponent } from '@/designer-components/timeField'; import { IDictionary } from '@/interfaces'; +import AutocompleteComponent from '@/designer-components/autocomplete/autocomplete'; type PropertyInclusionPredicate = (name: string) => boolean; @@ -32,11 +32,12 @@ export const getAllExceptPredicate = (names: string[]): PropertyInclusionPredica }; }; + const labelProperties = ['label', 'hideLabel', 'labelAlign', 'description']; const bindingProperties = ['name', 'propertyName', 'defaultValue']; const visibilityProperties = ['hidden', 'disabled', 'readOnly', 'visibility', 'hideBorder', 'editMode']; -const styleProperties = ['style', 'size']; -const allBaseProperties = [...labelProperties, ...bindingProperties, ...visibilityProperties, ...styleProperties]; +const styleProperties = ['propertyRouter1', 'propertyRouter', 'container', 'style', 'size', 'pnlFontStyle', 'pnlShadowStyle', 'stylingBox']; +const allBaseProperties = [...labelProperties, ...bindingProperties, ...visibilityProperties]; export const editorAdapters: IDictionary = { [HtmlComponent.type]: { @@ -46,6 +47,7 @@ export const editorAdapters: IDictionary = { }, [DropdownComponent.type]: { propertiesFilter: getAllExceptPredicate([ + ...styleProperties, ...allBaseProperties, 'mode', //'referenceListId', @@ -60,38 +62,39 @@ export const editorAdapters: IDictionary = { [AutocompleteComponent.type]: { propertiesFilter: getAllExceptPredicate([ ...allBaseProperties, + ...styleProperties, 'mode', 'dataSourceType', 'dataSourceUrl', - 'entityTypeShortAlias', - 'height', - 'width', - 'borderSize', - 'borderRadius', - 'borderColor', - 'borderType', - 'backgroundColor', - 'stylingBox' + 'entityType', + 'editMode', + 'description', + 'tooltip', + 'customSourceUrl', + 'disableSearch', + 'required' ]), }, [CheckboxComponent.type]: { - propertiesFilter: getAllExceptPredicate([...allBaseProperties]), + propertiesFilter: getAllExceptPredicate([...allBaseProperties, ...styleProperties, + ]), }, [TimeFieldComponent.type]: { - propertiesFilter: getAllExceptPredicate([...allBaseProperties, 'range', 'picker']), + propertiesFilter: getAllExceptPredicate([...allBaseProperties, ...styleProperties, 'range', 'picker']), }, [DateFieldComponent.type]: { - propertiesFilter: getAllExceptPredicate([...allBaseProperties, 'range', 'picker']), + propertiesFilter: getAllExceptPredicate([...allBaseProperties, ...styleProperties, 'range', 'picker']), }, [NumberComponent.type]: { - propertiesFilter: getAllExceptPredicate([...allBaseProperties]), + propertiesFilter: getAllExceptPredicate([...allBaseProperties, ...styleProperties]), }, [RefListStatusComponent.type]: { - propertiesFilter: getAllExceptPredicate([...allBaseProperties, 'referenceListId']), + propertiesFilter: getAllExceptPredicate([...allBaseProperties, ...styleProperties, 'referenceListId']), }, [TextFieldComponent.type]: { propertiesFilter: getAllExceptPredicate([ ...allBaseProperties, + ...styleProperties, 'initialValue', 'passEmptyStringByDefault', 'textType', @@ -101,14 +104,10 @@ export const editorAdapters: IDictionary = { 'borderRadius', 'borderColor', 'backgroundColor', - 'fontSize', - 'fontColor', - 'borderType', - 'stylingBox' ]), }, [EntityReferenceComponent.type]: { - propertiesFilter: getAllExceptPredicate([...allBaseProperties]), + propertiesFilter: getAllExceptPredicate([...allBaseProperties, ...styleProperties]), }, [StatusTagComponent.type]: { propertiesFilter: getAllExceptPredicate([]), diff --git a/shesha-reactjs/src/components/formComponentSelector/componentSettingsModal.tsx b/shesha-reactjs/src/components/formComponentSelector/componentSettingsModal.tsx index 91c7378377..4c9e4fa97e 100644 --- a/shesha-reactjs/src/components/formComponentSelector/componentSettingsModal.tsx +++ b/shesha-reactjs/src/components/formComponentSelector/componentSettingsModal.tsx @@ -57,6 +57,7 @@ function ComponentSettingsModal({ {/* */} ({ layoutSettings={{ labelCol: { span: 8 }, wrapperCol: { span: 16 }, - layout: 'horizontal' + layout: 'vertical' }} /> diff --git a/shesha-reactjs/src/components/formComponentSelector/index.tsx b/shesha-reactjs/src/components/formComponentSelector/index.tsx index 180289fd23..dea1be72ba 100644 --- a/shesha-reactjs/src/components/formComponentSelector/index.tsx +++ b/shesha-reactjs/src/components/formComponentSelector/index.tsx @@ -8,6 +8,7 @@ import { upgradeComponent } from '@/providers/form/utils'; import React, { FC, useMemo, useState } from 'react'; import { editorAdapters } from './adapters'; import ComponentSettingsModal from './componentSettingsModal'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; export type ComponentType = 'input' | 'output'; @@ -22,6 +23,7 @@ export interface IFormComponentSelectorProps { value?: ComponentSelectorValue; onChange?: (value?: ComponentSelectorValue) => void; readOnly?: boolean; + size?: SizeType; propertyMeta?: IPropertyMetadata; } @@ -50,7 +52,10 @@ export const FormComponentSelector: FC = (props) => }, [allComponents, componentType]); const options = useMemo(() => { - const result = editors.map((editor) => ({ label: editor.name, value: editor.type })); + const result = editors.map((editor) => ({ + //capitalise the first letter of each word + label: editor?.name?.replace(/\b\w/g, (char) => char.toUpperCase()), + value: editor.type })); if (noSelectionItem) result.splice(0, 0, noSelectionItem); return result; @@ -63,7 +68,7 @@ export const FormComponentSelector: FC = (props) => }, [value?.type]); const canConfigure = Boolean(formComponent); - const selectStyle = { width: canConfigure ? 'calc(100% - 100px)' : '100%' }; + const selectStyle = { width: canConfigure ? 'calc(100% - 100px)' : '100%'}; const getComponentModel = (toolboxComponent: IToolboxComponent) => { if (!toolboxComponent) return null; @@ -125,7 +130,7 @@ export const FormComponentSelector: FC = (props) => }; return ( - + disabled={readOnly} options={options} @@ -133,10 +138,11 @@ export const FormComponentSelector: FC = (props) => value={value?.type} onChange={onSelectChange} onClear={onClear} + size={props.size} allowClear /> {canConfigure && ( - )} diff --git a/shesha-reactjs/src/components/formDesigner/componentPropertiesPanel/componentPropertiesEditor.tsx b/shesha-reactjs/src/components/formDesigner/componentPropertiesPanel/componentPropertiesEditor.tsx index c337ec18c3..1f9ebefca3 100644 --- a/shesha-reactjs/src/components/formDesigner/componentPropertiesPanel/componentPropertiesEditor.tsx +++ b/shesha-reactjs/src/components/formDesigner/componentPropertiesPanel/componentPropertiesEditor.tsx @@ -15,9 +15,10 @@ export interface IComponentPropertiesEditorProps { formRef?: MutableRefObject; propertyFilter?: (name: string) => boolean; layoutSettings?: IFormLayoutSettings; + isInModal?: boolean; } -const getDefaultFactory = (markup: FormMarkup): ISettingsFormFactory => { +const getDefaultFactory = (markup: FormMarkup, isInModal?: boolean): ISettingsFormFactory => { const evaluatedMarkup = typeof markup === 'function' ? markup({}) : markup; @@ -34,21 +35,22 @@ const getDefaultFactory = (markup: FormMarkup): ISettingsFormFactory => { formRef={formRef} propertyFilter={propertyFilter} layoutSettings={layoutSettings} + isInModal={isInModal} /> ); }; }; export const ComponentPropertiesEditor: FC = (props) => { - const { componentModel, readOnly, toolboxComponent } = props; + const { componentModel, readOnly, toolboxComponent, isInModal } = props; const { getCachedComponentEditor } = useFormDesignerActions(); - + const SettingsForm = getCachedComponentEditor(componentModel.type, () => { return toolboxComponent?.settingsFormFactory ? toolboxComponent.settingsFormFactory : toolboxComponent?.settingsFormMarkup - ? getDefaultFactory(toolboxComponent.settingsFormMarkup) + ? getDefaultFactory(toolboxComponent.settingsFormMarkup, isInModal) : null; }); @@ -83,6 +85,7 @@ export const ComponentPropertiesEditor: FC = (p formRef={formRef} propertyFilter={propertyFilter} layoutSettings={layoutSettings} + isInModal={isInModal} /> ) : null; diff --git a/shesha-reactjs/src/components/formDesigner/componentPropertiesTitle.tsx b/shesha-reactjs/src/components/formDesigner/componentPropertiesTitle.tsx index 6fd9abc8d2..c8dfc8cb63 100644 --- a/shesha-reactjs/src/components/formDesigner/componentPropertiesTitle.tsx +++ b/shesha-reactjs/src/components/formDesigner/componentPropertiesTitle.tsx @@ -1,13 +1,14 @@ import React, { FC } from 'react'; import { Button } from 'antd'; import { CopyOutlined, DeleteFilled } from '@ant-design/icons'; -import { useFormDesignerActions, useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerActions, useFormDesignerStateSelector } from '@/providers/formDesigner'; import { useStyles } from './styles/styles'; export interface IProps {} export const ComponentPropertiesTitle: FC = ({}) => { - const { selectedComponentId, readOnly } = useFormDesignerState(); + const selectedComponentId = useFormDesignerStateSelector(x => x.selectedComponentId); + const readOnly = useFormDesignerStateSelector(x => x.readOnly); const { deleteComponent, duplicateComponent } = useFormDesignerActions(); const { styles } = useStyles(); diff --git a/shesha-reactjs/src/components/formDesigner/components/configurableFormItemContext.tsx b/shesha-reactjs/src/components/formDesigner/components/configurableFormItemContext.tsx new file mode 100644 index 0000000000..a599ae4bf7 --- /dev/null +++ b/shesha-reactjs/src/components/formDesigner/components/configurableFormItemContext.tsx @@ -0,0 +1,44 @@ +import React, { FC } from "react"; +import { Form, FormItemProps } from "antd"; +import { IConfigurableFormItemChildFunc } from "./model"; +import { useDataContextManager } from "@/index"; +import { DataBinder } from "@/hocs/dataBinder"; + +interface IConfigurableFormItem_ContextProps { + componentId: string; + formItemProps: FormItemProps; + valuePropName?: string; + propertyName: string; + contextName: string; + readonly children?: IConfigurableFormItemChildFunc; +} + +export const ConfigurableFormItemContext: FC = (props) => { + const { + formItemProps, + valuePropName, + propertyName, + contextName, + children + } = props; + const { getDataContext } = useDataContextManager(); + const context = getDataContext(contextName); + const { getFieldValue } = context ?? {}; + + const value = getFieldValue ? getFieldValue(propertyName) : undefined; + + return ( + + { + const value = val?.target ? val?.target[valuePropName || 'value'] : val; + if (context?.setFieldValue) + context.setFieldValue(propertyName, value); + }} + value={value} + > + {children} + + + ); +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/components/configurableFormItemForm.tsx b/shesha-reactjs/src/components/formDesigner/components/configurableFormItemForm.tsx new file mode 100644 index 0000000000..c6c544e643 --- /dev/null +++ b/shesha-reactjs/src/components/formDesigner/components/configurableFormItemForm.tsx @@ -0,0 +1,26 @@ +import React, { FC } from "react"; +import { Form, FormItemProps } from "antd"; +import { IConfigurableFormItemChildFunc } from "./model"; +import { DataBinder } from "@/hocs/dataBinder"; + +export interface IConfigurableFormItem_FormProps { + formItemProps: FormItemProps; + readonly children?: IConfigurableFormItemChildFunc; + valuePropName?: string; +} + +export const ConfigurableFormItemForm: FC = (props) => { + const { + formItemProps, + children, + valuePropName, + } = props; + + return ( + + + {children} + + + ); +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/components/configurableFormItemLive.tsx b/shesha-reactjs/src/components/formDesigner/components/configurableFormItemLive.tsx new file mode 100644 index 0000000000..9b77203cc8 --- /dev/null +++ b/shesha-reactjs/src/components/formDesigner/components/configurableFormItemLive.tsx @@ -0,0 +1,80 @@ +import React, { FC, useMemo } from 'react'; +import { Form, FormItemProps } from 'antd'; +import { getFieldNameFromExpression, getValidationRules } from '@/providers/form/utils'; +import classNames from 'classnames'; +import { useFormItem, useShaFormInstance } from '@/providers'; +import { IConfigurableFormItemProps } from './model'; +import { ConfigurableFormItemContext } from './configurableFormItemContext'; +import { ConfigurableFormItemForm } from './configurableFormItemForm'; + +export const ConfigurableFormItemLive: FC = ({ + children, + model, + valuePropName, + initialValue, + className, + labelCol, + wrapperCol, +}) => { + const { getPublicFormApi } = useShaFormInstance(); + const getFormData = getPublicFormApi().getFormData; + const formItem = useFormItem(); + const { namePrefix, wrapperCol: formItemWrapperCol, labelCol: formItemlabelCol } = formItem; + + const layout = useMemo(() => { + // Make sure the `wrapperCol` and `labelCol` from `FormItemProver` override the ones from the main form + return { labelCol: formItemlabelCol || labelCol, wrapperCol: formItemWrapperCol || wrapperCol }; + }, [formItemlabelCol, formItemWrapperCol]); + + const { hideLabel, hidden } = model; + if (hidden) return null; + + const propName = namePrefix && !model.initialContext + ? namePrefix + '.' + model.propertyName + : model.propertyName; + + const formItemProps: FormItemProps = { + className: classNames(className), + label: hideLabel ? null : model.label, + labelAlign: model.labelAlign, + hidden: model.hidden, + valuePropName: valuePropName, + initialValue: initialValue, + tooltip: model.description, + rules: model.hidden ? [] : getValidationRules(model, { getFormData }), + labelCol: layout?.labelCol, + wrapperCol: hideLabel ? { span: 24 } : layout?.wrapperCol, + //layout: model.layout, this property appears to have been removed from the Ant component + name: model.context ? undefined : getFieldNameFromExpression(propName), + }; + + if (typeof children === 'function') { + if (model.context) { + return ( + + {children} + + ); + } else { + return ( + + {children} + + ); + } + } else { + // Use standard Form.Item for components without binding support + return ( + {children} + ); + } +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/components/configurableFormItemSetting.tsx b/shesha-reactjs/src/components/formDesigner/components/configurableFormItemSetting.tsx new file mode 100644 index 0000000000..7aee3a8bdc --- /dev/null +++ b/shesha-reactjs/src/components/formDesigner/components/configurableFormItemSetting.tsx @@ -0,0 +1,88 @@ +import React, { FC, cloneElement } from 'react'; +import { Form, FormItemProps } from 'antd'; +import { getFieldNameFromExpression } from '@/providers/form/utils'; +import { getPropertySettingsFromData } from '@/designer-components/_settings/utils'; +import { SettingsControl, useShaFormInstance } from '@/index'; +import { IConfigurableFormItemChildFunc, IConfigurableFormItemProps } from './model'; +import { ConfigurableFormItemLive } from './configurableFormItemLive'; + +export const ConfigurableFormItemSetting: FC = ({ + children, + model, + valuePropName, +}) => { + const { formData } = useShaFormInstance(); + + if (model.hidden) return null; + + const { _mode: mode } = getPropertySettingsFromData(formData, model.propertyName); + + const formProps: FormItemProps = { + name: getFieldNameFromExpression(model.propertyName), + label: model.label, + //style: model.style, + required: model.validate?.required, + tooltip: model.description, + hidden: model.hidden, + }; + + if (typeof children === 'function') { + const childrenFunc = children as IConfigurableFormItemChildFunc; + return ( + + + {(value, onChange, propertyName) => childrenFunc(value, onChange, propertyName)} + + + ); + } + + const childrenElement = children as React.ReactElement; + const readOnly = model.readOnly || childrenElement.props.readOnly || childrenElement.props.disabled; + + return ( + + {(value, onChange) => { + return ( + + {(value, onChange) => { + return cloneElement( + childrenElement, + { + ...childrenElement?.props, + readOnly: readOnly, + disabled: readOnly, + onChange: (...args: any[]) => { + const event = args[0]; + const data = event && event.target && typeof event.target === 'object' && valuePropName in event.target + ? (event.target as HTMLInputElement)[valuePropName] + : event; + onChange(data); + }, + [valuePropName]: value + }); + }} + + ); + }} + + ); +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/components/formItem.tsx b/shesha-reactjs/src/components/formDesigner/components/formItem.tsx index 80e59e1843..6b12e10f3b 100644 --- a/shesha-reactjs/src/components/formDesigner/components/formItem.tsx +++ b/shesha-reactjs/src/components/formDesigner/components/formItem.tsx @@ -1,160 +1,13 @@ -import React, { FC, ReactNode, useMemo } from 'react'; -import { IConfigurableFormComponent } from '@/providers/form/models'; -import { ColProps, Form, FormItemProps } from 'antd'; -import { useForm } from '@/providers/form'; -import { getFieldNameFromExpression, getValidationRules } from '@/providers/form/utils'; -import classNames from 'classnames'; -import { useFormItem } from '@/providers'; -import { DataBinder } from '@/hocs/dataBinder'; -import { useDataContextManagerActions } from '@/providers/dataContextManager'; - -export type IConfigurableFormItemChildFunc = ( - value: any, - onChange: (...args: any[]) => void, - propertyName?: string -) => ReactNode; - - -export interface IConfigurableFormItemProps { - model: IConfigurableFormComponent; - readonly children?: ReactNode | IConfigurableFormItemChildFunc; - className?: string; - valuePropName?: string; - initialValue?: any; - customVisibility?: string; - wrapperCol?: ColProps; - labelCol?: ColProps; -} - -export interface IConfigurableFormItem_FormProps { - formItemProps: FormItemProps; - readonly children?: IConfigurableFormItemChildFunc; - valuePropName?: string; -} -const ConfigurableFormItemForm: FC = (props) => { - const { - formItemProps, - children, - valuePropName, - } = props; - - return ( - - - {children} - - - ); -}; - -interface IConfigurableFormItem_ContextProps { - formItemProps: FormItemProps; - - valuePropName?: string; - propertyName: string; - contextName: string; - readonly children?: IConfigurableFormItemChildFunc; -} - -const ConfigurableFormItemContext: FC = (props) => { - const { - formItemProps, - valuePropName, - propertyName, - contextName, - children - } = props; - - const { getDataContext } = useDataContextManagerActions(); - const context = getDataContext(contextName); - const { getFieldValue } = context ?? {}; - - const value = getFieldValue ? getFieldValue(propertyName) : undefined; - - return ( - - { - const value = !!val?.target ? val?.target[!!valuePropName ? valuePropName : 'value'] : val; - if (!!context?.setFieldValue) - context.setFieldValue(propertyName, value); - }} - value={value} - > - {children} - - - ); +import React, { FC } from 'react'; +import { IConfigurableFormItemProps } from './model'; +import { ConfigurableFormItemSetting } from './configurableFormItemSetting'; +import { ConfigurableFormItemLive } from './configurableFormItemLive'; + +const ConfigurableFormItem: FC = (props) => { + return props.model.jsSetting + ? + : ; }; -const ConfigurableFormItem: FC = ({ - children, - model, - valuePropName, - initialValue, - className, - labelCol, - wrapperCol, -}) => { - const { formData } = useForm(); - - const formItem = useFormItem(); - - const { namePrefix, wrapperCol: formItemWrapperCol, labelCol: formItemlabelCol } = formItem; - - const layout = useMemo(() => { - // Make sure the `wrapperCol` and `labelCol` from `FormItemProver` override the ones from the main form - return { labelCol: formItemlabelCol || labelCol, wrapperCol: formItemWrapperCol || wrapperCol }; - }, [formItemlabelCol, formItemWrapperCol]); - - const propName = namePrefix && !model.initialContext - ? namePrefix + '.' + model.propertyName - : model.propertyName; - - const { hideLabel } = model; - - const formItemProps: FormItemProps = { - className: classNames(className), - label: hideLabel ? null : model.label, - labelAlign: model.labelAlign, - hidden: model.hidden, - valuePropName: valuePropName, - initialValue: initialValue, - tooltip: model.description, - rules: model.hidden ? [] : getValidationRules(model, { formData }), - labelCol: layout?.labelCol, - wrapperCol: hideLabel ? { span: 24 } : layout?.wrapperCol, - name: model.context ? undefined : getFieldNameFromExpression(propName), - }; - - if (typeof children === 'function') { - if (model.context) { - return ( - - {children} - - ); - } else { - return ( - - {children} - - ); - } - } else { - // Use standard Form.Item for components without binding support - return ( - {children} - ); - } -}; export default React.memo(ConfigurableFormItem); \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/components/model.ts b/shesha-reactjs/src/components/formDesigner/components/model.ts new file mode 100644 index 0000000000..65992baa5c --- /dev/null +++ b/shesha-reactjs/src/components/formDesigner/components/model.ts @@ -0,0 +1,21 @@ +import { IConfigurableFormComponent } from "@/index"; +import { ColProps } from "antd"; +import { ReactNode } from "react"; + +export type IConfigurableFormItemChildFunc = ( + value: any, + onChange: (...args: any[]) => void, + propertyName?: string +) => ReactNode; + +export interface IConfigurableFormItemProps { + model: IConfigurableFormComponent; + readonly children?: ReactNode | IConfigurableFormItemChildFunc; + className?: string; + valuePropName?: string; + initialValue?: any; + customVisibility?: string; + wrapperCol?: ColProps; + labelCol?: ColProps; + hidden?: boolean; +} \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/components/utils.ts b/shesha-reactjs/src/components/formDesigner/components/utils.ts index 1fc7ac8d92..910daf9140 100644 --- a/shesha-reactjs/src/components/formDesigner/components/utils.ts +++ b/shesha-reactjs/src/components/formDesigner/components/utils.ts @@ -3,13 +3,14 @@ import { DOMAttributes } from 'react'; import { IAnyObject, IConfigurableFormComponent } from '@/interfaces'; import { IGooglePlacesAutocompleteProps } from '@/components'; import { ISetStatePayload } from '@/providers/globalState/contexts'; -import { CustomLabeledValue } from '@/components/autocomplete'; import { IAddressAndCoords } from '@/components/googlePlacesAutocomplete'; import { IOpenCageResponse } from '../../googlePlacesAutocomplete/models'; import { IFormApi } from '@/providers/form/formApi'; import { HttpClientApi } from '@/publicJsApis/httpClient'; import { IApplicationContext, executeScriptSync } from '@/index'; import { ObservableProxy } from '@/providers/form/observableProxy'; +import { CustomLabeledValue } from '@/components/refListDropDown/models'; +import { TouchableProxy } from '@/providers/form/touchableProxy'; type SetGlobalStateFunc = (payload: ISetStatePayload) => void; @@ -24,27 +25,37 @@ export interface ICustomEventHandler { setGlobalState: (payload: ISetStatePayload) => void; }; +export const addContextData = (context: any, additionalContext: any) => { + // if context is an ObservableProxy + if (context instanceof ObservableProxy || context instanceof TouchableProxy) { + for(const propName in additionalContext) { + if (additionalContext.hasOwnProperty(propName)) { + context.addAccessor(propName, () => additionalContext[propName]); + } + } + return context; + } + // if context is a simple object + return {...context, ...additionalContext}; +}; + export interface ICustomAddressEventHandler extends ICustomEventHandler { onChange: Function; onSelect: (selected: IAddressAndCoords) => Promise; }; -type EventHandlerAttributes = Pick, 'onBlur' | 'onChange' | 'onFocus' | 'onClick'>; +export type EventHandlerAttributes = Pick, 'onBlur' | 'onChange' | 'onFocus' | 'onClick'>; +export interface IEventHandlers extends Pick, 'onBlur' | 'onFocus' | 'onClick'>{ + onChange: (values: object, event?: any) => void; +}; + +/** @deprecated use getAllEventHandlers instead */ export const getEventHandlers = (model: IConfigurableFormComponent, context: IApplicationContext): EventHandlerAttributes => { const onCustomEvent = (event: any, key: string) => { const expression = model?.[key]; if (Boolean(expression)) { - // if context is an ObservableProxy - if (typeof context['addAccessor'] === 'function') { - const ctx = context as any as ObservableProxy; - ctx.addAccessor('event', () => event); - ctx.addAccessor('value', () => event?.currentTarget.value); - return executeScriptSync(expression, context); - } - - // if context is a simple object - return executeScriptSync(expression, {...context, event, value: event?.currentTarget.value}); + return executeScriptSync(expression, addContextData(context, {event, value: event?.currentTarget.value})); } }; @@ -52,9 +63,30 @@ export const getEventHandlers = (model: IConfigurableFormComponent, con onBlur: (event) => onCustomEvent(event, 'onBlurCustom'), onChange: (event) => onCustomEvent(event, 'onChangeCustom'), onFocus: (event) => onCustomEvent(event, 'onFocusCustom'), - onClick: (event) => { - event.stopPropagation(); - }, + onClick: (event) => onCustomEvent(event, 'onClickCustom'), + }; +}; + +export const getAllEventHandlers = (model: IConfigurableFormComponent, context: IApplicationContext): IEventHandlers => { + const onCustomEvent = (event: any, key: string) => { + const expression = model?.[key]; + if (Boolean(expression)) { + return executeScriptSync(expression, addContextData(context, {event, value: event?.currentTarget.value})); + } + }; + + const onChange = (values: object, event: any) => { + const expression = model?.onChangeCustom; + if (Boolean(expression)) { + return executeScriptSync(expression, addContextData(context, { event, ...values })); + } + }; + + return { + onBlur: (event) => onCustomEvent(event, 'onBlurCustom'), + onChange: (values: object, event?: any) => onChange(values, event), + onFocus: (event) => onCustomEvent(event, 'onFocusCustom'), + onClick: (event) => onCustomEvent(event, 'onClickCustom'), }; }; @@ -62,16 +94,7 @@ export const customDateEventHandler = (model: IConfigurableFormComponent, contex onChange: (value: any | null, dateString: string | [string, string]) => { const expression = model?.onChangeCustom; if (Boolean(expression)) { - // if context is an ObservableProxy - if (typeof context['addAccessor'] === 'function') { - const ctx = context as any as ObservableProxy; - ctx.addAccessor('dateString', () => dateString); - ctx.addAccessor('value', () => value); - return executeScriptSync(expression, context); - } - - // if context is a simple object - return executeScriptSync(expression, {...context, dateString, value}); + return executeScriptSync(expression, addContextData(context, {dateString, value})); } }, }); @@ -80,16 +103,7 @@ export const customTimeEventHandler = (model: IConfigurableFormComponent, contex onChange: (value: any | null, timeString: string | [string, string]) => { const expression = model?.onChangeCustom; if (Boolean(expression)) { - // if context is an ObservableProxy - if (typeof context['addAccessor'] === 'function') { - const ctx = context as any as ObservableProxy; - ctx.addAccessor('timeString', () => timeString); - ctx.addAccessor('value', () => value); - return executeScriptSync(expression, context); - } - - // if context is a simple object - return executeScriptSync(expression, {...context, timeString, value}); + return executeScriptSync(expression, addContextData(context, {timeString, value})); } }, }); @@ -98,35 +112,27 @@ export const customDropDownEventHandler = (model: IConfigurableFormComp onChange: (value: CustomLabeledValue, option: any) => { const expression = model?.onChangeCustom; if (Boolean(expression)) { - // if context is an ObservableProxy - if (typeof context['addAccessor'] === 'function') { - const ctx = context as any as ObservableProxy; - ctx.addAccessor('option', () => option); - ctx.addAccessor('value', () => value); - return executeScriptSync(expression, context); - } - - // if context is a simple object - return executeScriptSync(expression, {...context, option, value}); + return executeScriptSync(expression, addContextData(context, {option, value})); } }, }); -export const customOnChangeValueEventHandler = (model: IConfigurableFormComponent, context: IApplicationContext, changeEvent: Function = null) => ({ +export const customOnChangeValueEventHandler = (model: IConfigurableFormComponent, context: IApplicationContext) => ({ onChange: (value: any) => { - if (typeof changeEvent === 'function') - changeEvent(value); const expression = model?.onChangeCustom; if (Boolean(expression)) { - // if context is an ObservableProxy - if (typeof context['addAccessor'] === 'function') { - const ctx = context as any as ObservableProxy; - ctx.addAccessor('value', () => value); - return executeScriptSync(expression, context); - } + return executeScriptSync(expression, addContextData(context, {value})); + } + }, +}); - // if context is a simple object - return executeScriptSync(expression, {...context, value}); +export const customOnClickEventHandler = (model: IConfigurableFormComponent, context: IApplicationContext, clickEvent: Function = null) => ({ + onClick: (value: any) => { + if (typeof clickEvent === 'function') + clickEvent(value); + const expression = model?.onClickCustom; + if (Boolean(expression)) { + return executeScriptSync(expression, addContextData(context, {value})); } }, }); @@ -163,9 +169,10 @@ export const customAddressEventHandler = ( model: IConfigurableFormComponent, context: IApplicationContext, onChangeCustom, - onSelectCustom - ): IGooglePlacesAutocompleteProps => { - + onSelectCustom, + onFocusCustom, +): IGooglePlacesAutocompleteProps => { + const onCustomEvent = (event: any, key: string) => { const expression = model?.[key]; if (Boolean(expression)) { @@ -178,7 +185,7 @@ export const customAddressEventHandler = ( } // if context is a simple object - return executeScriptSync(expression, {...context, event, value: event?.currentTarget.value}); + return executeScriptSync(expression, { ...context, event, value: event?.currentTarget.value }); } }; @@ -187,12 +194,18 @@ export const customAddressEventHandler = ( onCustomEvent(e, 'onChangeCustom'); }; + const onFocus = (e: string) => { + onFocusCustom(e); + onCustomEvent(e, 'onFocusCustom'); + }; + const onGeocodeChange = (event: IAddressAndCoords) => onSelectCustom(event).then((payload) => onCustomEvent({ ...event, ...(payload || {}) }, 'onSelectCustom')); return { onChange, onGeocodeChange, + onFocus }; }; diff --git a/shesha-reactjs/src/components/formDesigner/configurableFormComponent/dragWrapper.tsx b/shesha-reactjs/src/components/formDesigner/configurableFormComponent/dragWrapper.tsx index 80a4437cf2..25be6637ae 100644 --- a/shesha-reactjs/src/components/formDesigner/configurableFormComponent/dragWrapper.tsx +++ b/shesha-reactjs/src/components/formDesigner/configurableFormComponent/dragWrapper.tsx @@ -1,4 +1,4 @@ -import React, { FC, MutableRefObject, PropsWithChildren, useState } from 'react'; +import React, { FC, MutableRefObject, PropsWithChildren, useMemo, useState } from 'react'; import { ShaForm } from '@/providers/form'; import { Button, Tooltip } from 'antd'; import { useFormDesignerState, useFormDesignerActions } from '@/providers/formDesigner'; @@ -20,7 +20,7 @@ export const DragWrapper: FC> = (props) => const componentModel = ShaForm.useComponentModel(props.componentId); - const tooltip = ( + const tooltip = useMemo(() => (
{isDebug && (
@@ -41,7 +41,7 @@ export const DragWrapper: FC> = (props) =>
Component name: {componentModel.componentName}
)}
- ); + ), [componentModel]); const onClick = (event: React.MouseEvent) => { event.stopPropagation(); diff --git a/shesha-reactjs/src/components/formDesigner/configurableFormComponent/index.tsx b/shesha-reactjs/src/components/formDesigner/configurableFormComponent/index.tsx index 9bb8059f10..9f503134bb 100644 --- a/shesha-reactjs/src/components/formDesigner/configurableFormComponent/index.tsx +++ b/shesha-reactjs/src/components/formDesigner/configurableFormComponent/index.tsx @@ -4,6 +4,7 @@ import FormComponent from '../formComponent'; import React, { FC, MutableRefObject, + memo, useMemo, useRef } from 'react'; @@ -29,15 +30,23 @@ import { useFormDesignerComponentGetter } from '@/providers/form/hooks'; export interface IConfigurableFormComponentDesignerProps { componentModel: IConfigurableFormComponent; componentRef: MutableRefObject; + selectedComponentId?: string; + readOnly?: boolean; + settingsPanelRef?: MutableRefObject; + hidden?: boolean; + componentEditMode?: EditMode; } -export const ConfigurableFormComponentDesigner: FC = ({ componentModel, componentRef }) => { +const ConfigurableFormComponentDesignerInner: FC = ({ + componentModel, + componentRef, + selectedComponentId, + readOnly, + settingsPanelRef, + hidden, + componentEditMode +}) => { const { styles } = useStyles(); - const allData = useAvailableConstantsData({ topContextId: 'all' }); - const { - selectedComponentId, - readOnly, - settingsPanelRef, - } = useFormDesignerState(); + const getToolboxComponent = useFormDesignerComponentGetter(); const isSelected = componentModel.id && selectedComponentId === componentModel.id; @@ -45,9 +54,7 @@ export const ConfigurableFormComponentDesigner: FC 0; const hiddenFx = isPropertySettings(componentModel.hidden); - const hidden = getActualPropertyValue(componentModel, allData, 'hidden')?.hidden; const componentEditModeFx = isPropertySettings(componentModel.editMode); - const componentEditMode = getActualPropertyValue(componentModel, allData, 'editMode')?.editMode as EditMode; const actionText1 = (hiddenFx ? 'hidden' : '') + (hiddenFx && componentEditModeFx ? ' and ' : '') + (componentEditModeFx ? 'disabled' : ''); const actionText2 = (hiddenFx ? 'showing' : '') + (hiddenFx && componentEditModeFx ? '/' : '') + (componentEditModeFx ? 'enabled' : ''); @@ -123,21 +130,35 @@ export const ConfigurableFormComponentDesigner: FC = (props) => { + + const allData = useAvailableConstantsData({ topContextId: 'all' }); + const { selectedComponentId, readOnly, settingsPanelRef } = useFormDesignerState(); + const hidden = getActualPropertyValue(props.componentModel, allData, 'hidden')?.hidden; + const componentEditMode = getActualPropertyValue(props.componentModel, allData, 'editMode')?.editMode as EditMode; + + return ; +}; + export interface IConfigurableFormComponentProps { id: string; + model?: IConfigurableFormComponent; } -export const ConfigurableFormComponent: FC = ({ id }) => { +export const ConfigurableFormComponent: FC = ({id, model}) => { const isDrawing = useIsDrawingForm(); const componentRef = useRef(null); - const componentModel = ShaForm.useComponentModel(id); + const componentMarkupModel = ShaForm.useComponentModel(id); + const componentModel = model?.isDynamic ? model : componentMarkupModel; const ComponentRenderer = !isDrawing || componentModel?.isDynamic ? FormComponent : ConfigurableFormComponentDesigner; return ( - - ); + + ); }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/containers/componentsContainer.tsx b/shesha-reactjs/src/components/formDesigner/containers/componentsContainer.tsx index 36860202b5..4d47bd72b9 100644 --- a/shesha-reactjs/src/components/formDesigner/containers/componentsContainer.tsx +++ b/shesha-reactjs/src/components/formDesigner/containers/componentsContainer.tsx @@ -1,16 +1,14 @@ import React, { CSSProperties, FC, ReactNode } from 'react'; import { IConfigurableFormComponent } from '@/providers/form/models'; -import { IComponentsContainerBaseProps } from '@/interfaces'; +import { ICommonContainerProps, IComponentsContainerBaseProps } from '@/interfaces'; import { useComponentContainer } from '@/providers/form/nesting/containerContext'; -import { ICommonContainerProps } from '@/designer-components/container/interfaces'; -export interface IComponentsContainerProps extends IComponentsContainerBaseProps, ICommonContainerProps { +export interface IComponentsContainerProps extends IComponentsContainerBaseProps, Omit { className?: string; render?: (components: JSX.Element[]) => ReactNode; itemsLimit?: number; dynamicComponents?: IConfigurableFormComponent[]; wrapperStyle?: CSSProperties; - style?: CSSProperties; } const ComponentsContainer: FC = (props) => { diff --git a/shesha-reactjs/src/components/formDesigner/containers/componentsContainerDesigner.tsx b/shesha-reactjs/src/components/formDesigner/containers/componentsContainerDesigner.tsx index b70dd9d4b1..3cd1f672d0 100644 --- a/shesha-reactjs/src/components/formDesigner/containers/componentsContainerDesigner.tsx +++ b/shesha-reactjs/src/components/formDesigner/containers/componentsContainerDesigner.tsx @@ -7,7 +7,7 @@ import { IComponentsContainerProps } from './componentsContainer'; import { ItemInterface, ReactSortable } from 'react-sortablejs'; import { TOOLBOX_COMPONENT_DROPPABLE_KEY, TOOLBOX_DATA_ITEM_DROPPABLE_KEY } from '@/providers/form/models'; import { ShaForm } from '@/providers/form'; -import { useFormDesignerActions, useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerActions, useFormDesignerStateSelector } from '@/providers/formDesigner'; import { useStyles } from '../styles/styles'; import { useParent } from '@/providers/parentProvider'; import _ from 'lodash'; @@ -28,7 +28,8 @@ export const ComponentsContainerDesigner: FC x.readOnly); + const hasDragged = useFormDesignerStateSelector(x => x.hasDragged); const { updateChildComponents, addComponent, addDataProperty, startDragging, endDragging } = useFormDesignerActions(); const childIds = ShaForm.useChildComponentIds(containerId.replace(`${parent?.subFormIdPrefix}.`, '')); diff --git a/shesha-reactjs/src/components/formDesigner/containers/componentsContainerDynamic.tsx b/shesha-reactjs/src/components/formDesigner/containers/componentsContainerDynamic.tsx index f4b292c3e8..621d41398c 100644 --- a/shesha-reactjs/src/components/formDesigner/containers/componentsContainerDynamic.tsx +++ b/shesha-reactjs/src/components/formDesigner/containers/componentsContainerDynamic.tsx @@ -1,12 +1,10 @@ -import DynamicComponent from '@/designer-components/dynamicView/dynamicComponent'; -import React, { - FC, - useCallback, -} from 'react'; -import { executeScriptSync } from '@/providers/form/utils'; +import React, { FC } from 'react'; import { getAlignmentStyle } from './util'; import { IComponentsContainerProps } from './componentsContainer'; -import { useFormData, useGlobalState } from '@/providers'; +import { ConfigurableFormComponent } from '../configurableFormComponent'; +import { useStyles } from '../styles/styles'; +import classNames from 'classnames'; +import { useDeepCompareMemo } from '@/hooks'; export interface IComponentsContainerDynamicProps extends Omit, Required> { @@ -14,30 +12,35 @@ export interface IComponentsContainerDynamicProps extends Omit = (props) => { - const { data: formData } = useFormData(); - const { globalState } = useGlobalState(); + const { + dynamicComponents, + direction = 'vertical', + className, + render, + wrapperStyle, + style: incomingStyle, + noDefaultStyling, + } = props; - const executeExpression = useCallback( - (expression: string) => { - if (!expression) return true; - const evaluated = executeScriptSync(expression, { data: formData, globalState }); - return typeof evaluated === 'boolean' ? evaluated : true; - }, - [formData, globalState] - ); + const { styles } = useStyles(); + + const renderComponents = useDeepCompareMemo(() => { + const renderedComponents = dynamicComponents.map((c) => ( + + )); + + return typeof render === 'function' ? render(renderedComponents) : renderedComponents; + }, [dynamicComponents]); - const style = getAlignmentStyle(props); - return ( -
- {props.dynamicComponents - ?.filter(({ customVisibility }) => { - return executeExpression(customVisibility); - }) - ?.map(({ customEnabled, ...model }, idx) => { - const readOnly = !executeExpression(customEnabled) || model.readOnly; + const style = { ...getAlignmentStyle(props), ...incomingStyle }; - return ; - })} + return noDefaultStyling ? ( +
{renderComponents}
+ ) : ( +
+
+ {renderComponents} +
); }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/containers/componentsContainerLive.tsx b/shesha-reactjs/src/components/formDesigner/containers/componentsContainerLive.tsx index 85ac4507cf..7f2c05a654 100644 --- a/shesha-reactjs/src/components/formDesigner/containers/componentsContainerLive.tsx +++ b/shesha-reactjs/src/components/formDesigner/containers/componentsContainerLive.tsx @@ -6,6 +6,7 @@ import { useStyles } from '../styles/styles'; import classNames from 'classnames'; import { getAlignmentStyle } from './util'; import { useParent } from '@/providers/parentProvider'; +import { useDeepCompareMemo } from '@/hooks'; export const ComponentsContainerLive: FC> = (props) => { const { @@ -23,22 +24,22 @@ export const ComponentsContainerLive: FC { + const renderComponents = useDeepCompareMemo(() => { const renderedComponents = components.map((c) => ( )); return typeof render === 'function' ? render(renderedComponents) : renderedComponents; - }; + }, [components]); const style = { ...getAlignmentStyle(props), ...incomingStyle }; return noDefaultStyling ? ( -
{renderComponents()}
+
{renderComponents}
) : (
- {renderComponents()} + {renderComponents}
{children}
diff --git a/shesha-reactjs/src/components/formDesigner/debugPanel/index.tsx b/shesha-reactjs/src/components/formDesigner/debugPanel/index.tsx index 00f1b5c490..a3d0992876 100644 --- a/shesha-reactjs/src/components/formDesigner/debugPanel/index.tsx +++ b/shesha-reactjs/src/components/formDesigner/debugPanel/index.tsx @@ -2,15 +2,17 @@ import React, { FC } from 'react'; import { Col, Divider } from 'antd'; import { CollapsiblePanel } from '@/components'; import { useDataContextManager } from '@/providers/dataContextManager'; +import { useShaFormInstance, useShaFormUpdateDate } from '@/providers/form/providers/shaFormProvider'; export interface DebugPanelProps { formData?: any; } -export const DebugPanel: FC = (props) => { +export const DebugPanel: FC = () => { + useShaFormUpdateDate(); + const formData = useShaFormInstance(false)?.formData; const ctxManager = useDataContextManager(false)?.getRoot(); - const contexts = ctxManager.getDataContexts('full'); return ( @@ -18,11 +20,11 @@ export const DebugPanel: FC = (props) => { -
{JSON.stringify(props.formData, null, 2)}
+
{JSON.stringify(formData, null, 2)}
- {contexts.map((ctx) => - {ctx.name}: {ctx.description} ({ctx.id})} expandIconPosition='start' ghost key={ctx.id}> + {contexts.map((ctx, index) => + {ctx.name}: {ctx.description} ({ctx.id})} expandIconPosition='start' ghost key={index}>
{JSON.stringify(ctx.getData(), null, 2)}
diff --git a/shesha-reactjs/src/components/formDesigner/designerMainArea/index.tsx b/shesha-reactjs/src/components/formDesigner/designerMainArea/index.tsx index c42ecb1f25..01dd129d51 100644 --- a/shesha-reactjs/src/components/formDesigner/designerMainArea/index.tsx +++ b/shesha-reactjs/src/components/formDesigner/designerMainArea/index.tsx @@ -1,31 +1,37 @@ import { ConfigurableFormRenderer, SidebarContainer } from '@/components'; import ConditionalWrap from '@/components/conditionalWrapper'; -import { MetadataProvider, useCanvas, useForm } from '@/providers'; -import { useFormDesignerActions, useFormDesignerState } from '@/providers/formDesigner'; +import { DataContextProvider, MetadataProvider, useCanvas, useShaFormInstance } from '@/providers'; +import { useFormDesignerStateSelector } from '@/providers/formDesigner'; import ParentProvider from '@/providers/parentProvider'; -import React, { FC, useEffect } from 'react'; +import React, { FC, useMemo } from 'react'; import { ComponentPropertiesPanel } from '../componentPropertiesPanel'; import { ComponentPropertiesTitle } from '../componentPropertiesTitle'; import { DebugPanel } from '../debugPanel'; import { useStyles } from '../styles/styles'; import Toolbox from '../toolbox'; +import { SheshaCommonContexts } from '@/providers/dataContextManager/models'; export interface IDesignerMainAreaProps { - } +const rightSidebarProps = { + title: () => , + content: () => , + placeholder: 'Properties', +}; + export const DesignerMainArea: FC = () => { - const { isDebug, readOnly } = useFormDesignerState(); - const { setSelectedComponent } = useFormDesignerActions(); - const { form, formMode, formSettings } = useForm(); + const isDebug = useFormDesignerStateSelector(state => state.isDebug); + const readOnly = useFormDesignerStateSelector(state => state.readOnly); + const formSettings = useFormDesignerStateSelector(state => state.formSettings); + const formMode = useFormDesignerStateSelector(state => state.formMode); + const { antdForm: form } = useShaFormInstance(); const { designerWidth, zoom } = useCanvas(); const { styles } = useStyles(); - useEffect(() => { - if (formMode !== 'designer') { - setSelectedComponent(null); - } - }, [formMode]); + const leftSidebarProps = useMemo(() => + readOnly ? null : { title: 'Builder Widgets', content: () => , placeholder: 'Builder Widgets' } + , [readOnly]); return (
= () => { condition={formMode === 'designer'} wrap={(children) => ( , - placeholder: 'Builder Widgets', - } - } - rightSidebarProps={{ - title: () => , - content: () => , - placeholder: 'Properties', - }} + leftSidebarProps={leftSidebarProps} + rightSidebarProps={rightSidebarProps} > {children} @@ -63,12 +57,16 @@ export const DesignerMainArea: FC = () => { condition={Boolean(formSettings?.modelType)} wrap={(children) => ({children})} > - - - {isDebug && ( - - )} - + + + + {isDebug && ( + + )} + + @@ -76,4 +74,4 @@ export const DesignerMainArea: FC = () => {
); -}; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/formComponent.tsx b/shesha-reactjs/src/components/formDesigner/formComponent.tsx index c1a9bae516..e58a80ffdd 100644 --- a/shesha-reactjs/src/components/formDesigner/formComponent.tsx +++ b/shesha-reactjs/src/components/formDesigner/formComponent.tsx @@ -1,15 +1,18 @@ -import React, { FC, MutableRefObject } from 'react'; +import React, { FC, MutableRefObject, useMemo } from 'react'; import { IConfigurableFormComponent, IToolboxComponent } from '@/interfaces'; -import { useCanvas, useForm, useSheshaApplication } from '@/providers'; +import { useCanvas, useForm, useShaFormInstance, useSheshaApplication } from '@/providers'; import { useFormDesignerComponentGetter } from '@/providers/form/hooks'; import { IModelValidation } from '@/utils/errors'; import { CustomErrorBoundary } from '..'; import ComponentError from '../componentErrors'; -import { useActualContextData } from '@/hooks/useActualContextData'; +import AttributeDecorator from '../attributeDecorator'; +import { IStyleType, isValidGuid, useActualContextData, useCalculatedModel } from '@/index'; +import { useFormComponentStyles } from '@/hooks/formComponentHooks'; +import { useShaFormUpdateDate } from '@/providers/form/providers/shaFormProvider'; export interface IFormComponentProps { componentModel: IConfigurableFormComponent; - componentRef: MutableRefObject; + componentRef?: MutableRefObject; } // skip some properties by default @@ -21,13 +24,17 @@ export const standartActualModelPropertyFilter = (name: string) => { }; export const formComponentActualModelPropertyFilter = (component: IToolboxComponent, name: string) => { - return (component.actualModelPropertyFilter ? component.actualModelPropertyFilter(name) : true) + return (component?.actualModelPropertyFilter ? component.actualModelPropertyFilter(name) : true) && propertiesToSkip.indexOf(name) === -1; }; const FormComponent: FC = ({ componentModel, componentRef }) => { - const formInstance = useForm(); - const { form, isComponentFiltered, formMode } = formInstance; + + useShaFormUpdateDate(); + + const shaApplication = useSheshaApplication(); + const shaForm = useShaFormInstance(); + const { isComponentFiltered } = useForm(); const getToolboxComponent = useFormDesignerComponentGetter(); const { anyOfPermissionsGranted } = useSheshaApplication(); const { activeDevice } = useCanvas(); @@ -38,26 +45,51 @@ const FormComponent: FC = ({ componentModel, componentRef } const toolboxComponent = getToolboxComponent(componentModel.type); - const actualModel = useActualContextData(deviceModel, undefined, undefined, (name: string) => formComponentActualModelPropertyFilter(toolboxComponent, name)); - - if (!toolboxComponent) - return ; + const actualModel = useActualContextData( + deviceModel, + undefined, + undefined, + (name: string) => formComponentActualModelPropertyFilter(toolboxComponent, name), + undefined + ); - actualModel.hidden = formMode !== 'designer' + actualModel.hidden = shaForm.formMode !== 'designer' && ( actualModel.hidden - || !anyOfPermissionsGranted(actualModel?.permissions || []) - || !isComponentFiltered(actualModel)); + || !anyOfPermissionsGranted(actualModel?.permissions || []) + || !isComponentFiltered(actualModel)); - if (!toolboxComponent.isInput && !toolboxComponent.isOutput) + if (!toolboxComponent?.isInput && !toolboxComponent?.isOutput) actualModel.propertyName = undefined; - if (formInstance.formMode === 'designer') { - const validationResult: IModelValidation = {hasErrors: false, errors: []}; - toolboxComponent.validateModel?.(actualModel, (propertyName, error) => { + actualModel.allStyles = useFormComponentStyles(actualModel); + + const calculatedModel = useCalculatedModel(actualModel, toolboxComponent?.useCalculateModel, toolboxComponent?.calculateModel); + + const control = useMemo(() => ( + + ), [actualModel, actualModel.hidden, actualModel.allStyles, calculatedModel]); + + if (!toolboxComponent) + return ; + + if (shaForm.formMode === 'designer') { + const validationResult: IModelValidation = { hasErrors: false, errors: [] }; + if (actualModel?.background?.type === 'storedFile' && actualModel?.background.storedFile?.id && !isValidGuid(actualModel?.background.storedFile.id)) { + validationResult.hasErrors = true; + validationResult.errors.push({ propertyName: 'The provided StoredFileId is invalid', error: 'The provided StoredFileId is invalid' }); + } + toolboxComponent?.validateModel?.(actualModel, (propertyName, error) => { validationResult.hasErrors = true; validationResult.errors.push({ propertyName, error }); }); @@ -65,11 +97,32 @@ const FormComponent: FC = ({ componentModel, componentRef } validationResult.componentId = actualModel.id; validationResult.componentName = actualModel.componentName; validationResult.componentType = actualModel.type; - return ; + return ; + } + } + + if (shaForm.form.settings.isSettingsForm) + return control; + + const attributes = { + 'data-sha-c-id': `${componentModel.id}`, + 'data-sha-c-name': `${componentModel.componentName}`, + 'data-sha-c-type': `${componentModel.type}`, + }; + + if (componentModel.type === 'subForm') { + if ((componentModel as any)?.formSelectionMode !== 'dynamic'){ + attributes['data-sha-c-form-name'] = `${(componentModel as any)?.formId?.module}/${(componentModel as any)?.formId?.name}`; } + attributes['data-sha-parent-form-id'] = `${shaForm.form.id}`; + attributes['data-sha-parent-form-name'] = `${(shaForm as any)?.formId?.module}/${(shaForm as any)?.formId?.name}`; } - return ; + return ( + + {control} + + ); }; const FormCompomnentErrorWrapper: FC = ({ componentModel, componentRef }) => { diff --git a/shesha-reactjs/src/components/formDesigner/formDesignerWrapper.tsx b/shesha-reactjs/src/components/formDesigner/formDesignerWrapper.tsx index c6622303bf..dd13dd9b2f 100644 --- a/shesha-reactjs/src/components/formDesigner/formDesignerWrapper.tsx +++ b/shesha-reactjs/src/components/formDesigner/formDesignerWrapper.tsx @@ -6,7 +6,7 @@ import { Result, Skeleton } from 'antd'; -import { FormDesignerProvider, useFormDesignerState } from '@/providers/formDesigner'; +import { FormDesignerProvider, useFormDesignerStateSelector } from '@/providers/formDesigner'; import { FormIdentifier } from '@/providers/form/models'; import { FormPersisterProvider } from '@/providers/formPersisterProvider'; import { FormPersisterStateConsumer } from '@/providers/formPersisterProvider/contexts'; @@ -21,7 +21,9 @@ export interface IFormProviderWrapperProps extends PropsWithChildren { } const FormProviderWrapperInner: FC> = ({ form, children }) => { - const { formSettings, formFlatMarkup } = useFormDesignerState(); + const formSettings = useFormDesignerStateSelector(x => x.formSettings); + const formFlatMarkup = useFormDesignerStateSelector(x => x.formFlatMarkup); + const [shaForm] = useShaForm({ form: undefined, antdForm: form, diff --git a/shesha-reactjs/src/components/formDesigner/formSettings.json b/shesha-reactjs/src/components/formDesigner/formSettings.json deleted file mode 100644 index 165d698510..0000000000 --- a/shesha-reactjs/src/components/formDesigner/formSettings.json +++ /dev/null @@ -1,848 +0,0 @@ -{ - "components": [ - { - "id": "19OdE-HiKh6VlC53zuYW6CQHF6gvv2", - "type": "tabs", - "propertyName": "custom Name", - "componentName": "tabs1", - "label": "Tabs1", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "tabs": [ - { - "id": "67SvLUmiSKc5iooJGTArldHg31fX2e", - "name": "tbAppearance", - "key": "tbAppearance", - "title": "Appearance", - "editMode": "inherited", - "selectMode": "editable", - "components": [ - { - "id": "077f5a0d-d59e-44b8-bc5b-a06e18438f4d", - "type": "dropdown", - "propertyName": "layout", - "label": "Layout", - "labelAlign": "right", - "parentId": "67SvLUmiSKc5iooJGTArldHg31fX2e", - "hidden": false, - "allowClear": false, - "validate": { - "required": true - }, - "dataSourceType": "values", - "values": [ - { - "id": "3f7bd074-37e6-450c-8106-95e8d8164ada", - "label": "horizontal", - "value": "horizontal" - }, - { - "id": "5709e098-7c1d-4a27-92e5-f25e998c26fd", - "label": "vertical", - "value": "vertical" - } - ], - "version": 6, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "8615d12f-6ea0-4b11-a1a1-6088c7160fd9", - "type": "dropdown", - "propertyName": "size", - "parentId": "67SvLUmiSKc5iooJGTArldHg31fX2e", - "label": "Size", - "allowClear": true, - "values": [ - { - "label": "Small", - "value": "small", - "id": "4f11403c-95fd-4e49-bb60-cb8c25f0f3c3" - }, - { - "label": "Middle", - "value": "middle", - "id": "8f85c476-e632-4fa7-89ad-2be6cfb7f1f1" - }, - { - "label": "Large", - "value": "large", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - } - ], - "dataSourceType": "values", - "version": 6, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "b6bdf196-769f-4bce-b488-9ca50ad16176", - "type": "checkbox", - "propertyName": "colon", - "label": "Colon", - "labelAlign": "right", - "parentId": "67SvLUmiSKc5iooJGTArldHg31fX2e", - "hidden": false, - "description": "Configure the default value of colon for Form.Item. Indicates whether the colon after the label is displayed (only effective when prop layout is horizontal)", - "validate": {}, - "version": 3 - }, - { - "id": "493e12cd-d4ab-4f2a-8c08-3a20ce0f617e", - "type": "numberField", - "propertyName": "labelCol.span", - "label": "Label span", - "labelAlign": "right", - "parentId": "67SvLUmiSKc5iooJGTArldHg31fX2e", - "hidden": false, - "validate": {}, - "description": "Raster number of cells to occupy, 0 corresponds to display: none", - "version": 3 - }, - { - "id": "99c93c99-0005-429e-8f71-e899230f1c6e", - "type": "numberField", - "propertyName": "wrapperCol.span", - "label": "Component span", - "labelAlign": "right", - "parentId": "67SvLUmiSKc5iooJGTArldHg31fX2e", - "hidden": false, - "validate": {}, - "description": "Raster number of cells to occupy, 0 corresponds to display: none", - "version": 3 - } - ] - }, - { - "id": "07lU1-i-ftBrrhUFjJpbFNW5m3XKLB", - "name": "tbData", - "key": "tbData", - "title": "Data", - "editMode": "inherited", - "selectMode": "editable", - "components": [ - { - "id": "ee6a9269-0e8e-41db-b6e0-069036accb19", - "type": "autocomplete", - "propertyName": "modelType", - "label": "Model Type", - "labelAlign": "right", - "parentId": "07lU1-i-ftBrrhUFjJpbFNW5m3XKLB", - "hidden": false, - "dataSourceType": "url", - "dataSourceUrl": "/api/services/app/Metadata/TypeAutocomplete", - "validate": {}, - "useRawValues": true, - "version": 5, - "settingsValidationErrors": [], - "mode": [ - "single" - ] - }, - { - "id": "kqiWeis_L7y_Nvj7-tPuTQos3p5jj6", - "type": "collapsiblePanel", - "propertyName": "panel7", - "componentName": "pnlDataLoad", - "label": "Data Load", - "labelAlign": "right", - "parentId": "07lU1-i-ftBrrhUFjJpbFNW5m3XKLB", - "hidden": false, - "isDynamic": false, - "version": 6, - "expandIconPosition": "end", - "header": { - "id": "zkobUibZMxFEuytX0weUVi1dvhCOXC", - "components": [] - }, - "content": { - "id": "qzAhO4Zbgao4mbi7CPJsAbJ5Hw7brZ", - "components": [ - { - "id": "K8oEz3bwHMYshFz0maoq2O4DJvRFYC", - "type": "radio", - "propertyName": "dataLoaderType", - "componentName": "dataLoaderType", - "label": "Loader Type", - "labelAlign": "right", - "parentId": "qzAhO4Zbgao4mbi7CPJsAbJ5Hw7brZ", - "hidden": false, - "isDynamic": false, - "version": 5, - "dataSourceType": "values", - "direction": "horizontal", - "referenceListId": null, - "items": { - "_mode": "value", - "_code": " const result = [];\r\n const isEntity = Boolean(data?.modelType);\r\n if (isEntity)\r\n result.push({ label: 'Default', value: 'gql' });\r\n \r\n result.push({ label: 'Custom', value: 'custom' });\r\n result.push({ label: 'None', value: 'none' });\r\n return result;", - "_value": [ - { - "label": "Default", - "value": "gql" - }, - { - "label": "Custom", - "value": "custom" - }, - { - "label": "None", - "value": "none" - } - ] - }, - "validate": {} - }, - { - "id": "R-QJrw5yvKwqKFR6wKMrOcMYfVN8AX", - "type": "container", - "propertyName": "container1", - "componentName": "cntGQLLoader", - "label": "Container1", - "labelAlign": "right", - "parentId": "qzAhO4Zbgao4mbi7CPJsAbJ5Hw7brZ", - "hidden": { - "_mode": "code", - "_code": " return data?.dataLoaderType !== 'gql';", - "_value": false - }, - "isDynamic": false, - "version": 4, - "direction": "vertical", - "justifyContent": "left", - "flexWrap": "wrap", - "components": [ - { - "id": "KlJISScqsOA0iLqNx4P01TW_Y9Rye8", - "type": "radio", - "propertyName": "dataLoadersSettings.gql.endpointType", - "componentName": "dataLoadersSettings.gql.endpointType", - "label": "API Endpoint Type", - "labelAlign": "right", - "parentId": "R-QJrw5yvKwqKFR6wKMrOcMYfVN8AX", - "hidden": false, - "isDynamic": false, - "version": 5, - "dataSourceType": "values", - "direction": "horizontal", - "referenceListId": null, - "items": [ - { - "label": "Default", - "value": "default" - }, - { - "label": "Custom: static", - "value": "static" - }, - { - "label": "Custom: dynamic", - "value": "dynamic" - } - ], - "validate": {} - }, - { - "id": "oamRBlEs6O2Jlc2FGL_gY", - "type": "endpointsAutocomplete", - "propertyName": "dataLoadersSettings.gql.staticEndpoint.url", - "label": "Endpoint", - "labelAlign": "right", - "parentId": "R-QJrw5yvKwqKFR6wKMrOcMYfVN8AX", - "hidden": { - "_mode": "code", - "_code": " return data?.dataLoadersSettings?.gql?.endpointType !== 'static';", - "_value": false - }, - "isDynamic": false, - "placeholder": "", - "description": "", - "validate": { - "minLength": "", - "maxLength": "", - "message": "" - }, - "quickviewWidth": "", - "prefix": "GET", - "suffix": "", - "initialValue": "", - "settingsValidationErrors": [], - "version": 3, - "mode": "url", - "httpVerb": "get" - }, - { - "id": "_9lX9iOOPifIBM_dfaIQxsNjK9ZQs7", - "type": "codeEditor", - "propertyName": "dataLoadersSettings.gql.dynamicEndpoint", - "componentName": "dataLoadersSettings.gql.dynamicEndpoint", - "label": "Endpoint", - "labelAlign": "right", - "parentId": "R-QJrw5yvKwqKFR6wKMrOcMYfVN8AX", - "hidden": { - "_mode": "code", - "_code": " return data?.dataLoadersSettings?.gql?.endpointType !== 'dynamic';", - "_value": false - }, - "isDynamic": false, - "mode": "dialog", - "version": 3, - "language": "typescript", - "wrapInTemplate": true, - "validate": {}, - "templateSettings": { - "useAsyncDeclaration": true, - "functionName": "getLoaderEndpoint" - }, - "resultTypeExpression": " return metadataBuilder\n .object(\"IApiEndpoint\")\n .addString(\"url\", \"Endpoint Url\")\n .addString(\"httpVerb\", \"HTTP verb (GET/POST/PUT etc.)\")\n .build();" - }, - { - "id": "63f3360d-1e54-4a79-a888-8cd23b0bedd5", - "type": "propertyAutocomplete", - "propertyName": "dataLoadersSettings.gql.fieldsToFetch", - "label": "Additional fields for fetch", - "mode": "multiple", - "labelAlign": "right", - "hidden": false, - "validate": {}, - "settingsValidationErrors": [], - "description": "A list of fields you want to fetch", - "parentId": "R-QJrw5yvKwqKFR6wKMrOcMYfVN8AX", - "version": 2 - } - ] - }, - { - "id": "rlQNrfmmV5tVYCEjLkn6cxvgo_-Yor", - "type": "container", - "propertyName": "container2", - "componentName": "cntCustomLoader", - "label": "Container2", - "labelAlign": "right", - "parentId": "qzAhO4Zbgao4mbi7CPJsAbJ5Hw7brZ", - "hidden": { - "_mode": "code", - "_code": " return data?.dataLoaderType !== 'custom';", - "_value": false - }, - "isDynamic": false, - "version": 4, - "direction": "vertical", - "justifyContent": "left", - "flexWrap": "wrap", - "components": [ - { - "id": "g90qK0lVMJf64_8auhs-7eQ7KMmCPa", - "type": "codeEditor", - "propertyName": "dataLoadersSettings.custom.onDataLoad", - "label": "On Load", - "labelAlign": "right", - "parentId": "rlQNrfmmV5tVYCEjLkn6cxvgo_-Yor", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "description": "", - "version": 3, - "settingsValidationErrors": [], - "placeholder": "", - "mode": "dialog", - "wrapInTemplate": true, - "templateSettings": { - "useAsyncDeclaration": true, - "functionName": "onLoad" - }, - "availableConstantsExpression": " const { modelType } = data ?? {};\r\n const result = metadataBuilder.object(\"constants\");\r\n const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false;\r\n if (modelType && isEntity){\r\n await result.addEntityAsync(\"data\", \"Form data\", modelType);\r\n await result.addEntityAsync(\"initialValues\", \"Initial values\", modelType);\r\n } else {\r\n result.addObject(\"data\", \"Form data\");\r\n result.addObject(\"initialValues\", \"Initial values\"); \r\n };\r\n result.addObject(\"parentFormValues\", \"Parent form values. The values of the form rendering the dialog.\");\r\n \r\n result.addStandard([\r\n \"shesha:form\",\r\n \"shesha:globalState\", \r\n \"shesha:setGlobalState\",\r\n \"shesha:http\",\r\n \"shesha:message\",\r\n \"shesha:moment\",\r\n \"shesha:pageContext\", \r\n \"shesha:application\",\r\n \"shesha:query\",\r\n ]);\r\n return result.build();", - "language": "typescript", - "resultTypeExpression": " const { modelType } = data ?? {};\n const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false;\n return isEntity\n ? metadataBuilder.entity(modelType)\n : metadataBuilder.anyObject();" - } - ] - }, - { - "id": "NnUT13nyyq2TWKJIgUGPLt2MbTnHNR", - "type": "codeEditor", - "propertyName": "onBeforeDataLoad", - "label": "On Before Data Load", - "labelAlign": "right", - "parentId": "qzAhO4Zbgao4mbi7CPJsAbJ5Hw7brZ", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "description": "", - "version": 3, - "settingsValidationErrors": [], - "placeholder": "", - "mode": "dialog", - "wrapInTemplate": true, - "templateSettings": { - "useAsyncDeclaration": true, - "functionName": "onBeforeDataLoad" - }, - "availableConstantsExpression": " const { modelType } = data ?? {};\r\n const result = metadataBuilder.object(\"constants\");\r\n const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false;\r\n if (modelType && isEntity){\r\n await result.addEntityAsync(\"data\", \"Form data\", modelType);\r\n await result.addEntityAsync(\"initialValues\", \"Initial values\", modelType);\r\n } else {\r\n result.addObject(\"data\", \"Form data\");\r\n result.addObject(\"initialValues\", \"Initial values\"); \r\n };\r\n result.addObject(\"parentFormValues\", \"Parent form values. The values of the form rendering the dialog.\");\r\n \r\n result.addStandard([\r\n \"shesha:form\",\r\n \"shesha:globalState\", \r\n \"shesha:setGlobalState\",\r\n \"shesha:http\",\r\n \"shesha:message\",\r\n \"shesha:moment\",\r\n \"shesha:pageContext\", \r\n \"shesha:application\",\r\n \"shesha:query\",\r\n ]);\r\n return result.build();", - "language": "typescript" - }, - { - "id": "lPFHZVyQTRzqqOWcmt8ENF14CCB4U0", - "type": "codeEditor", - "propertyName": "onAfterDataLoad", - "label": "On After Data Load", - "labelAlign": "right", - "parentId": "qzAhO4Zbgao4mbi7CPJsAbJ5Hw7brZ", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "description": "", - "version": 3, - "settingsValidationErrors": [], - "placeholder": "", - "mode": "dialog", - "wrapInTemplate": true, - "templateSettings": { - "useAsyncDeclaration": true, - "functionName": "onAfterDataLoad" - }, - "availableConstantsExpression": " const { modelType } = data ?? {};\r\n const result = metadataBuilder.object(\"constants\");\r\n const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false;\r\n if (modelType && isEntity){\r\n await result.addEntityAsync(\"data\", \"Form data\", modelType);\r\n await result.addEntityAsync(\"initialValues\", \"Initial values\", modelType);\r\n } else {\r\n result.addObject(\"data\", \"Form data\");\r\n result.addObject(\"initialValues\", \"Initial values\"); \r\n };\r\n result.addObject(\"parentFormValues\", \"Parent form values. The values of the form rendering the dialog.\");\r\n \r\n result.addStandard([\r\n \"shesha:form\",\r\n \"shesha:globalState\", \r\n \"shesha:setGlobalState\",\r\n \"shesha:http\",\r\n \"shesha:message\",\r\n \"shesha:moment\",\r\n \"shesha:pageContext\", \r\n \"shesha:application\",\r\n \"shesha:query\",\r\n ]);\r\n return result.build();", - "language": "typescript" - } - ] - }, - "collapsible": "icon", - "ghost": true - }, - { - "id": "wpWja-q7Yb4spoDFWKBdJ_G0flU1uU", - "type": "collapsiblePanel", - "propertyName": "panel7", - "componentName": "pnlDataSave", - "label": "Data Save", - "labelAlign": "right", - "parentId": "07lU1-i-ftBrrhUFjJpbFNW5m3XKLB", - "hidden": false, - "isDynamic": false, - "version": 6, - "expandIconPosition": "end", - "header": { - "id": "ldBunArfl8IL2Rl0o8-z_ztR4FTTF2", - "components": [] - }, - "content": { - "id": "pqLHqdEIUCNNqJz4RsVl1d8cMtf-Rf", - "components": [ - { - "id": "2T9EkPWlKT916p_1InKQsghFAwsFKU", - "type": "radio", - "propertyName": "dataSubmitterType", - "componentName": "dataSubmitterType", - "label": "Submit Type", - "labelAlign": "right", - "parentId": "pqLHqdEIUCNNqJz4RsVl1d8cMtf-Rf", - "hidden": false, - "isDynamic": false, - "version": 5, - "dataSourceType": "values", - "direction": "horizontal", - "referenceListId": null, - "items": { - "_mode": "value", - "_code": " const result = [];\r\n const isEntity = Boolean(data?.modelType);\r\n if (isEntity)\r\n result.push({ label: 'Default', value: 'gql' });\r\n \r\n result.push({ label: 'Custom', value: 'custom' });\r\n result.push({ label: 'None', value: 'none' });\r\n return result;", - "_value": [ - { - "label": "Default", - "value": "gql" - }, - { - "label": "Custom", - "value": "custom" - }, - { - "label": "None", - "value": "none" - } - ] - }, - "validate": {} - }, - { - "id": "R6ib-RyV4FJqmxziXFWGIbLvXePqaa", - "type": "container", - "propertyName": "container3", - "componentName": "cntGQLSubmitter", - "label": "Container3", - "labelAlign": "right", - "parentId": "pqLHqdEIUCNNqJz4RsVl1d8cMtf-Rf", - "hidden": { - "_mode": "code", - "_code": " return data?.dataSubmitterType !== 'gql';", - "_value": false - }, - "isDynamic": false, - "version": 4, - "direction": "vertical", - "justifyContent": "left", - "flexWrap": "wrap", - "components": [ - { - "id": "Wkj0gNbHDsi6yi39bR5PVosa8zPgFQ", - "type": "radio", - "propertyName": "dataSubmittersSettings.gql.endpointType", - "componentName": "dataSubmittersSettings.gql.endpointType", - "label": "API Endpoint Type", - "labelAlign": "right", - "parentId": "R6ib-RyV4FJqmxziXFWGIbLvXePqaa", - "hidden": false, - "isDynamic": false, - "version": 5, - "dataSourceType": "values", - "direction": "horizontal", - "referenceListId": null, - "items": [ - { - "label": "Default", - "value": "default" - }, - { - "label": "Custom: static", - "value": "static" - }, - { - "label": "Custom: dynamic", - "value": "dynamic" - } - ], - "validate": {} - }, - { - "id": "APelhUKKU5fQhxwzLGAGz5j9MKPEKb", - "type": "endpointsAutocomplete", - "propertyName": "dataSubmittersSettings.gql.staticEndpoint", - "label": "Endpoint", - "labelAlign": "right", - "parentId": "R6ib-RyV4FJqmxziXFWGIbLvXePqaa", - "hidden": { - "_mode": "code", - "_code": " return data?.dataSubmittersSettings?.gql?.endpointType !== 'static';", - "_value": false - }, - "isDynamic": false, - "placeholder": "", - "description": "", - "validate": { - "minLength": "", - "maxLength": "", - "message": "" - }, - "quickviewWidth": "", - "prefix": "", - "suffix": "", - "initialValue": "", - "settingsValidationErrors": [], - "version": 3, - "componentName": "dataSubmittersSettings.gql.staticEndpoint", - "mode": "endpoint", - "availableHttpVerbs": [ - { - "label": "POST", - "value": "post" - }, - { - "label": "PUT", - "value": "put" - }, - { - "label": "PATCH", - "value": "patch" - }, - { - "label": "DELETE", - "value": "delete" - } - ] - }, - { - "id": "fWwNNymArMB1RN2C2Lpl_Mtfy5NZub", - "type": "codeEditor", - "propertyName": "dataSubmittersSettings.gql.dynamicEndpoint", - "componentName": "dataSubmittersSettings.gql.dynamicEndpoint", - "label": "Endpoint", - "labelAlign": "right", - "parentId": "R6ib-RyV4FJqmxziXFWGIbLvXePqaa", - "hidden": { - "_mode": "code", - "_code": " return data?.dataSubmittersSettings?.gql?.endpointType !== 'dynamic';", - "_value": false - }, - "isDynamic": false, - "mode": "dialog", - "version": 3, - "language": "typescript", - "wrapInTemplate": true, - "validate": {}, - "templateSettings": { - "useAsyncDeclaration": true, - "functionName": "getSubmitterEndpoint" - }, - "resultTypeExpression": " return metadataBuilder\n .object(\"IApiEndpoint\")\n .addString(\"url\", \"Endpoint Url\")\n .addString(\"httpVerb\", \"HTTP verb (GET/POST/PUT etc.)\")\n .build();" - }, - { - "id": "IQHewABvh2D1KqOlJjZ8R2YPao905U", - "type": "checkbox", - "propertyName": "dataSubmittersSettings.gql.excludeFormFields", - "componentName": "dataSubmittersSettings.gql.excludeFormFields", - "label": "Exclude '_formFields' in the payload?", - "labelAlign": "right", - "parentId": "R6ib-RyV4FJqmxziXFWGIbLvXePqaa", - "hidden": false, - "isDynamic": false, - "version": 3, - "description": "Whether or not _formFields should be included in the payload. By default it is included.", - "validate": {} - } - ] - }, - { - "id": "yCFlI7N20sWSf_WivyM9B_HKirQD3p", - "type": "codeEditor", - "propertyName": "onPrepareSubmitData", - "label": "Prepare Submit Data", - "labelAlign": "right", - "parentId": "pqLHqdEIUCNNqJz4RsVl1d8cMtf-Rf", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "description": "Here you can modify data before the form submission", - "version": 3, - "settingsValidationErrors": [], - "placeholder": "", - "mode": "dialog", - "wrapInTemplate": true, - "templateSettings": { - "useAsyncDeclaration": true, - "functionName": "onPrepareSubmitData" - }, - "availableConstantsExpression": " const { modelType } = data ?? {};\r\n const result = metadataBuilder.object(\"constants\");\r\n const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false;\r\n if (modelType && isEntity){\r\n await result.addEntityAsync(\"data\", \"Form data\", modelType);\r\n await result.addEntityAsync(\"initialValues\", \"Initial values\", modelType);\r\n } else {\r\n result.addObject(\"data\", \"Form data\");\r\n result.addObject(\"initialValues\", \"Initial values\"); \r\n };\r\n result.addObject(\"parentFormValues\", \"Parent form values. The values of the form rendering the dialog.\");\r\n \r\n result.addStandard([\r\n \"shesha:form\",\r\n \"shesha:globalState\", \r\n \"shesha:setGlobalState\",\r\n \"shesha:http\",\r\n \"shesha:message\",\r\n \"shesha:moment\",\r\n \"shesha:pageContext\", \r\n \"shesha:application\",\r\n \"shesha:query\",\r\n ]);\r\n return result.build();", - "language": "typescript", - "resultTypeExpression": " const { modelType } = data ?? {};\n const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false;\n return isEntity\n ? metadataBuilder.entity(modelType)\n : metadataBuilder.anyObject();" - }, - { - "id": "igFuqz1yjjNWA81gZzEi07CgMD-PYD", - "type": "codeEditor", - "propertyName": "onBeforeSubmit", - "label": "On Before Submit", - "labelAlign": "right", - "parentId": "pqLHqdEIUCNNqJz4RsVl1d8cMtf-Rf", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "description": "This event is called before the form submission. Here you can specify custom page level validation or modify the data before submission", - "version": 3, - "settingsValidationErrors": [], - "placeholder": "", - "mode": "dialog", - "wrapInTemplate": true, - "templateSettings": { - "useAsyncDeclaration": true, - "functionName": "onBeforeSubmit" - }, - "availableConstantsExpression": " const { modelType } = data ?? {};\r\n const result = metadataBuilder.object(\"constants\");\r\n const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false;\r\n if (modelType && isEntity){\r\n await result.addEntityAsync(\"data\", \"Form data\", modelType);\r\n await result.addEntityAsync(\"initialValues\", \"Initial values\", modelType);\r\n } else {\r\n result.addObject(\"data\", \"Form data\");\r\n result.addObject(\"initialValues\", \"Initial values\"); \r\n };\r\n result.addObject(\"parentFormValues\", \"Parent form values. The values of the form rendering the dialog.\");\r\n \r\n result.addStandard([\r\n \"shesha:form\",\r\n \"shesha:globalState\", \r\n \"shesha:setGlobalState\",\r\n \"shesha:http\",\r\n \"shesha:message\",\r\n \"shesha:moment\",\r\n \"shesha:pageContext\", \r\n \"shesha:application\",\r\n \"shesha:query\",\r\n ]);\r\n return result.build();", - "language": "typescript" - }, - { - "id": "BiIVYHEKbcfRrPVP-uC986fJ8aWgIw", - "type": "codeEditor", - "propertyName": "onSubmitSuccess", - "label": "On Submit Success", - "labelAlign": "right", - "parentId": "pqLHqdEIUCNNqJz4RsVl1d8cMtf-Rf", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "description": "", - "version": 3, - "settingsValidationErrors": [], - "placeholder": "", - "mode": "dialog", - "wrapInTemplate": true, - "templateSettings": { - "useAsyncDeclaration": true, - "functionName": "onSubmitSuccess" - }, - "availableConstantsExpression": " const { modelType } = data ?? {};\r\n const result = metadataBuilder.object(\"constants\");\r\n const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false;\r\n if (modelType && isEntity){\r\n await result.addEntityAsync(\"data\", \"Form data\", modelType);\r\n await result.addEntityAsync(\"initialValues\", \"Initial values\", modelType);\r\n } else {\r\n result.addObject(\"data\", \"Form data\");\r\n result.addObject(\"initialValues\", \"Initial values\"); \r\n };\r\n result.addObject(\"parentFormValues\", \"Parent form values. The values of the form rendering the dialog.\");\r\n \r\n result.addStandard([\r\n \"shesha:form\",\r\n \"shesha:globalState\", \r\n \"shesha:setGlobalState\",\r\n \"shesha:http\",\r\n \"shesha:message\",\r\n \"shesha:moment\",\r\n \"shesha:pageContext\", \r\n \"shesha:application\",\r\n \"shesha:query\",\r\n ]);\r\n return result.build();", - "language": "typescript" - }, - { - "id": "fEnxo9MJcqGtytakdcdhY_-NuXYdji", - "type": "codeEditor", - "propertyName": "onSubmitFailed", - "label": "On Submit Failed", - "labelAlign": "right", - "parentId": "pqLHqdEIUCNNqJz4RsVl1d8cMtf-Rf", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "description": "", - "version": 3, - "settingsValidationErrors": [], - "placeholder": "", - "mode": "dialog", - "wrapInTemplate": true, - "templateSettings": { - "useAsyncDeclaration": true, - "functionName": "onSubmitFailed" - }, - "availableConstantsExpression": " const { modelType } = data ?? {};\r\n const result = metadataBuilder.object(\"constants\");\r\n const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false;\r\n if (modelType && isEntity) {\r\n await result.addEntityAsync(\"data\", \"Form data\", modelType);\r\n await result.addEntityAsync(\"initialValues\", \"Initial values\", modelType);\r\n } else {\r\n result.addObject(\"data\", \"Form data\");\r\n result.addObject(\"initialValues\", \"Initial values\");\r\n };\r\n result.addObject(\"parentFormValues\", \"Parent form values. The values of the form rendering the dialog.\");\r\n\r\n result.addStandard([\r\n \"shesha:form\",\r\n \"shesha:globalState\",\r\n \"shesha:setGlobalState\",\r\n \"shesha:http\",\r\n \"shesha:message\",\r\n \"shesha:moment\",\r\n \"shesha:pageContext\",\r\n \"shesha:application\",\r\n \"shesha:query\",\r\n ]);\r\n return result.build();", - "language": "typescript" - } - ] - }, - "collapsible": "icon", - "ghost": true, - "components": [] - }, - { - "id": "vi4RicBJMPFiVpClJBNjsb4JP0FylJ", - "type": "collapsiblePanel", - "propertyName": "panel3", - "componentName": "panel3", - "label": "Events", - "labelAlign": "right", - "parentId": "07lU1-i-ftBrrhUFjJpbFNW5m3XKLB", - "hidden": false, - "isDynamic": false, - "version": 6, - "expandIconPosition": "end", - "header": { - "id": "59UX-XzU-VvczQSxxfzrSjI4TBw3FQ", - "components": [] - }, - "content": { - "id": "W1OKUNrRHQxrE5buLdNzir02XwZsCJ", - "components": [ - { - "id": "5092dd2b-c514-4b06-bfb5-546ce94fab97", - "type": "codeEditor", - "propertyName": "onValuesUpdate", - "label": "On Values Update", - "labelAlign": "right", - "parentId": "W1OKUNrRHQxrE5buLdNzir02XwZsCJ", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "description": "This action will be executed whenever the form updates", - "version": 3, - "settingsValidationErrors": [], - "placeholder": "", - "mode": "dialog", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "onValuesUpdate" - }, - "availableConstantsExpression": " const { modelType } = data ?? {};\r\n const result = metadataBuilder.object(\"constants\");\r\n const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false;\r\n if (modelType && isEntity) {\r\n await result.addEntityAsync(\"data\", \"Form data\", modelType);\r\n await result.addEntityAsync(\"initialValues\", \"Initial values\", modelType);\r\n } else {\r\n result.addObject(\"data\", \"Form data\");\r\n result.addObject(\"initialValues\", \"Initial values\");\r\n };\r\n result.addObject(\"parentFormValues\", \"Parent form values. The values of the form rendering the dialog.\");\r\n\r\n result.addStandard([\r\n \"shesha:form\",\r\n \"shesha:globalState\",\r\n \"shesha:setGlobalState\",\r\n \"shesha:http\",\r\n \"shesha:message\",\r\n \"shesha:moment\",\r\n \"shesha:pageContext\",\r\n \"shesha:application\",\r\n \"shesha:query\",\r\n ]);\r\n return result.build();", - "language": "typescript" - } - ] - }, - "collapsible": "icon", - "ghost": true - } - ] - }, - { - "id": "c9WBkqeaF-svQu3AzPA3RU8UVZMvnj", - "label": "Tab 1", - "title": "Security", - "key": "tbSecurity", - "components": [ - { - "id": "777f5a0d-d59e-44b8-bc5b-a06e18438f4d", - "type": "dropdown", - "propertyName": "access", - "label": "Access", - "labelAlign": "right", - "parentId": "c9WBkqeaF-svQu3AzPA3RU8UVZMvnj", - "hidden": false, - "dataSourceType": "values", - "allowClear": false, - "values": [ - { - "id": "3", - "label": "Any authenticated", - "value": "3" - }, - { - "id": "4", - "label": "Requires permissions", - "value": "4" - }, - { - "id": "5", - "label": "Allow anonymous", - "value": "5" - } - ], - "version": 6, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "1adea529-1f0c-4def-bd41-ee166a5dfcd7", - "type": "permissionAutocomplete", - "propertyName": "permissions", - "parentId": "c9WBkqeaF-svQu3AzPA3RU8UVZMvnj", - "label": "Permissions", - "validate": {}, - "version": 1, - "hidden": { - "_mode": "code", - "_code": "return data?.access != 4;" - } - } - ], - "editMode": "inherited", - "name": "tbSecurity", - "selectMode": "editable" - } - ], - "version": 3, - "tabType": "line" - } - ], - "formSettings": { - "layout": "horizontal", - "colon": true, - "labelCol": { - "span": 5 - }, - "wrapperCol": { - "span": 19 - }, - "uniqueFormId": "", - "permissions": null, - "access": null, - "version": 6, - "dataLoaderType": null, - "dataLoadersSettings": null, - "dataSubmitterType": null, - "dataSubmittersSettings": null, - "onBeforeDataLoad": null, - "onAfterShow": null, - "onBeforeSubmit": null, - "onSubmitSuccess": null, - "onSubmitFailed": null - } -} \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/formSettings.ts b/shesha-reactjs/src/components/formDesigner/formSettings.ts new file mode 100644 index 0000000000..7c9a10e9d0 --- /dev/null +++ b/shesha-reactjs/src/components/formDesigner/formSettings.ts @@ -0,0 +1,557 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/lib/form/Form'; + +export const getSettings = () => { + const searchableTabsId = nanoid(); + const dataTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const dataLoaderTypeId = nanoid(); + + return { + components: new DesignerToolbarSettings() + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'autocomplete', + propertyName: 'modelType', + label: 'Model Type', + parentId: dataTabId, + dataSourceType: 'url', + dataSourceUrl: '/api/services/app/Metadata/TypeAutocomplete', + useRawValues: true, + mode: ['single'], + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDataLoad', + label: 'Data Load', + labelAlign: 'right', + parentId: dataTabId, + ghost: true, + expandIconPosition: 'end', + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'radio', + propertyName: 'dataLoaderType', + label: 'Loader Type', + parentId: 'root', + defaultValue: 'gql', + buttonGroupOptions: { + _mode: 'value', + _code: ` const result = []; + const isEntity = Boolean(data?.modelType); + if (isEntity) + result.push({ title: 'Default', value: 'gql' }); + + result.push({ title: 'Custom', value: 'custom' }); + result.push({ title: 'None', value: 'none' }); + return result;`, + _value: [ + { title: 'Default', value: 'gql' }, + { title: 'Custom', value: 'custom' }, + { title: 'None', value: 'none' } + ] + } as any + }) + .addContainer({ + id: dataLoaderTypeId, + propertyName: "container1", + componentName: "cntGQLLoader", + label: "Container1", + labelAlign: "right", + parentId: "qzAhO4Zbgao4mbi7CPJsAbJ5Hw7brZ", + hidden: { + _mode: "code", + _code: " return data?.dataLoaderType !== 'gql';", + _value: false + }, + isDynamic: false, + version: 4, + direction: "vertical", + justifyContent: "left", + flexWrap: "wrap", + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: 'root', + inputType: 'radio', + propertyName: 'dataLoadersSettings.gql.endpointType', + label: 'API Endpoint Type', + labelAlign: 'right', + buttonGroupOptions: [ + { title: 'Default', value: 'default' }, + { title: 'Custom: static', value: 'static' }, + { title: 'Custom: dynamic', value: 'dynamic' } + ] + }) + .addSettingsInputRow({ + id: 'endpint-additional-fields-to-fetch', + inputs: [ + { + id: nanoid(), + type: "endpointsAutocomplete", + propertyName: "dataLoadersSettings.gql.staticEndpoint.url", + label: "Endpoint", + labelAlign: "right", + parentId: dataLoaderTypeId, + hidden: { + _mode: "code", + _code: " return data?.dataLoadersSettings?.gql?.endpointType !== 'static';", + _value: false + } as any, + isDynamic: false, + prefix: "GET", + mode: "url", + httpVerb: "get" + }, + { + id: nanoid(), + type: "codeEditor", + propertyName: "dataLoadersSettings.gql.dynamicEndpoint", + componentName: "dataLoadersSettings.gql.dynamicEndpoint", + label: "Endpoint", + labelAlign: "right", + parentId: dataLoaderTypeId, + hidden: { + _mode: "code", + _code: " return data?.dataLoadersSettings?.gql?.endpointType !== 'dynamic';", + _value: false + } as any, + isDynamic: false, + mode: "dialog", + version: 3, + language: "typescript", + wrapInTemplate: true, + validate: {}, + templateSettings: { + useAsyncDeclaration: true, + functionName: "getLoaderEndpoint" + }, + resultTypeExpression: " return metadataBuilder\n .object(\"IApiEndpoint\")\n .addString(\"url\", \"Endpoint Url\")\n .addString(\"httpVerb\", \"HTTP verb (GET/POST/PUT etc.)\")\n .build();" + }, + + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataLoaderTypeId, + inputs: [ + { + id: nanoid(), + type: "propertyAutocomplete", + propertyName: "dataLoadersSettings.gql.fieldsToFetch", + label: "Additional fields for fetch", + mode: "multiple", + labelAlign: "right", + hidden: false, + validate: {}, + settingsValidationErrors: [], + tooltip: "A list of fields you want to fetch", + parentId: dataLoaderTypeId, + version: 2 + } + ] + }) + .toJson() + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'root', + hidden: { + _mode: "code", + _code: " return data?.dataLoaderType !== 'custom';", + _value: false + }, + inputs: [ + { + type: 'codeEditor', + id: nanoid(), + propertyName: 'dataLoadersSettings.custom.onDataLoad', + label: 'On Load', + mode: 'dialog', + wrapInTemplate: true, + templateSettings: { + useAsyncDeclaration: true, + functionName: 'onLoad' + }, + language: 'typescript' + } + ] + }) + .addSettingsInputRow({ + id: 'on-beforeload-data-on-after-load-data', + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'onBeforeDataLoad', + label: 'On Before Data Load', + parentId: 'root', + mode: 'dialog', + wrapInTemplate: true, + templateSettings: { + useAsyncDeclaration: true, + functionName: 'onBeforeDataLoad' + }, + language: 'typescript' + }, + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'onAfterDataLoad', + label: 'On After Data Load', + parentId: 'root', + mode: 'dialog', + wrapInTemplate: true, + templateSettings: { + useAsyncDeclaration: true, + functionName: 'onAfterDataLoad' + }, + language: 'typescript' + }] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDataSave', + label: 'Data Save', + labelAlign: 'right', + parentId: dataTabId, + ghost: true, + expandIconPosition: 'end', + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'radio', + propertyName: 'dataSubmitterType', + label: 'Submit Type', + parentId: 'root', + buttonGroupOptions: { + _mode: 'value', + _code: ` const result = []; + const isEntity = Boolean(data?.modelType); + if (isEntity) + result.push({ title: 'Default', value: 'gql' }); + + result.push({ title: 'Custom', value: 'custom' }); + result.push({ title: 'None', value: 'none' }); + return result;`, + _value: [ + { title: 'Default', value: 'gql' }, + { title: 'Custom', value: 'custom' }, + { title: 'None', value: 'none' } + ] + } as any + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'root', + hidden: { _code: 'return getSettingValue(data?.dataSubmitterType) !== "gql";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'radio', + id: nanoid(), + propertyName: 'dataSubmittersSettings.gql.endpointType', + label: 'API Endpoint Type', + labelAlign: 'right', + buttonGroupOptions: [ + { title: 'Default', value: 'default' }, + { title: 'Custom: static', value: 'static' }, + { title: 'Custom: dynamic', value: 'dynamic' } + ] + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'root', + inputs: [ + { + type: 'endpointsAutocomplete', + id: nanoid(), + propertyName: 'dataSubmittersSettings.gql.staticEndpoint', + label: 'Endpoint', + mode: 'endpoint', + availableHttpVerbs: [ + { label: 'POST', value: 'post', id: nanoid() }, + { label: 'PUT', value: 'put', id: nanoid() }, + { label: 'PATCH', value: 'patch', id: nanoid() }, + { label: 'DELETE', value: 'delete', id: nanoid() } + ], + hidden: { _code: 'return getSettingValue(data?.dataSubmittersSettings?.gql?.endpointType) !== "static" || getSettingValue(data?.dataSubmitterType) !== "gql";', _mode: 'code', _value: false } as any, + }, + { + type: 'codeEditor', + id: nanoid(), + propertyName: 'dataSubmittersSettings.gql.dynamicEndpoint', + label: 'Endpoint', + mode: 'dialog', + language: 'typescript', + wrapInTemplate: true, + templateSettings: { + useAsyncDeclaration: true, + functionName: 'getSubmitterEndpoint' + }, + resultTypeExpression: ` return metadataBuilder + .object("IApiEndpoint") + .addString("url", "Endpoint Url") + .addString("httpVerb", "HTTP verb (GET/POST/PUT etc.)") + .build();`, + hidden: { _code: 'return getSettingValue(data?.dataSubmittersSettings?.gql?.endpointType) !== "dynamic" || getSettingValue(data?.dataSubmitterType) !== "gql";', _mode: 'code', _value: false } as any, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'root', + hidden: { _code: 'return getSettingValue(data?.dataSubmitterType) !== "gql";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'dataSubmittersSettings.gql.excludeFormFields', + label: 'Exclude \'_formFields\' in the payload?', + tooltip: 'Whether or not _formFields should be included in the payload. By default it is included.' + } + ] + }) + .addSettingsInputRow({ + id: 'on-prepare-submit-data-on-before-sumbit', + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'onPrepareSubmitData', + label: 'Prepare Submit Data', + parentId: 'root', + tooltip: 'Here you can modify data before the form submission', + mode: 'dialog', + wrapInTemplate: true, + templateSettings: { + useAsyncDeclaration: true, + functionName: 'onPrepareSubmitData' + }, + language: 'typescript', + resultTypeExpression: ` const { modelType } = data ?? {}; + const isEntity = modelType ? await metadataBuilder.isEntityAsync(modelType) : false; + return isEntity + ? metadataBuilder.entity(modelType) + : metadataBuilder.anyObject();` + }, + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'onBeforeSubmit', + label: 'On Before Submit', + parentId: 'root', + tooltip: 'This event is called before the form submission. Here you can specify custom page level validation or modify the data before submission', + mode: 'dialog', + wrapInTemplate: true, + templateSettings: { + useAsyncDeclaration: true, + functionName: 'onBeforeSubmit' + }, + language: 'typescript' + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'onSubmitSuccess', + label: 'On Submit Success', + parentId: 'root', + mode: 'dialog', + wrapInTemplate: true, + templateSettings: { + useAsyncDeclaration: true, + functionName: 'onSubmitSuccess' + }, + language: 'typescript' + }, + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'onSubmitFailed', + label: 'On Submit Failed', + parentId: 'root', + mode: 'dialog', + wrapInTemplate: true, + templateSettings: { + useAsyncDeclaration: true, + functionName: 'onSubmitFailed' + }, + language: 'typescript' + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlEvents', + label: 'Events', + labelAlign: 'right', + parentId: dataTabId, + ghost: true, + expandIconPosition: 'end', + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onValuesUpdate', + label: 'On Values Update', + parentId: 'root', + tooltip: 'This action will be executed whenever the form updates', + mode: 'dialog', + wrapInTemplate: true, + templateSettings: { + functionName: 'onValuesUpdate' + }, + language: 'typescript' + }) + .toJson() + ] + } + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'layout', + label: 'Layout', + parentId: appearanceTabId, + validate: { + required: true + }, + dropdownOptions: [ + { label: 'horizontal', value: 'horizontal' }, + { label: 'vertical', value: 'vertical' } + ] + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'size', + label: 'Size', + parentId: appearanceTabId, + allowClear: true, + dropdownOptions: [ + { label: 'Small', value: 'small' }, + { label: 'Middle', value: 'middle' }, + { label: 'Large', value: 'large' } + ] + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'colon', + label: 'Colon', + parentId: appearanceTabId, + tooltip: 'Configure the default value of colon for Form.Item. Indicates whether the colon after the label is displayed (only effective when prop layout is horizontal)' + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'numberField', + propertyName: 'labelCol.span', + label: 'Label span', + parentId: appearanceTabId, + tooltip: 'Raster number of cells to occupy, 0 corresponds to display: none', + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'numberField', + propertyName: 'wrapperCol.span', + label: 'Component span', + parentId: appearanceTabId, + tooltip: 'Raster number of cells to occupy, 0 corresponds to display: none' + + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'access', + label: 'Access', + parentId: securityTabId, + dropdownOptions: [ + { label: 'Any authenticated', value: '3' }, + { label: 'Requires permissions', value: '4' }, + { label: 'Allow anonymous', value: '5' } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + inputs: [ + { + id: nanoid(), + type: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + parentId: securityTabId, + hidden: { _code: 'return data?.access != 4;', _mode: 'code', _value: false } as any + } + ] + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/formSettingsEditor.tsx b/shesha-reactjs/src/components/formDesigner/formSettingsEditor.tsx index 7c95ca82a7..d951d8fb54 100644 --- a/shesha-reactjs/src/components/formDesigner/formSettingsEditor.tsx +++ b/shesha-reactjs/src/components/formDesigner/formSettingsEditor.tsx @@ -1,15 +1,15 @@ import React, { FC } from 'react'; import { Modal } from 'antd'; import { ConfigurableForm } from '@/components'; -import formSettingsJson from './formSettings.json'; import { FormMarkup } from '@/providers/form/models'; -import { useFormDesignerActions, useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerActions, useFormDesignerStateSelector } from '@/providers/formDesigner'; import { SourceFilesFolderProvider } from '@/providers/sourceFileManager/sourcesFolderProvider'; import { useFormPersister } from '@/providers/formPersisterProvider'; import { useTheme } from '@/index'; import { useShaFormRef } from '@/providers/form/providers/shaFormProvider'; +import { getSettings } from './formSettings'; -const formSettingsMarkup = formSettingsJson as FormMarkup; +const formSettingsMarkup = getSettings() as FormMarkup; export interface IFormSettingsEditorProps { isVisible: boolean; @@ -19,13 +19,13 @@ export interface IFormSettingsEditorProps { export const FormSettingsEditor: FC = ({ isVisible, close, readOnly }) => { const { theme } = useTheme(); - const { formSettings } = useFormDesignerState(); + const formSettings = useFormDesignerStateSelector(x => x.formSettings); const { updateFormSettings } = useFormDesignerActions(); const { formProps } = useFormPersister(); const formRef = useShaFormRef(); - formSettings.labelCol = { span: formSettings?.labelCol?.span || theme.labelSpan }; - formSettings.wrapperCol = { span: formSettings?.wrapperCol?.span || theme.componentSpan }; + formSettings.labelCol = { span: formSettings?.labelCol?.span || theme?.labelSpan }; + formSettings.wrapperCol = { span: formSettings?.wrapperCol?.span || theme?.componentSpan }; const onSave = values => { if (!readOnly) { @@ -40,8 +40,7 @@ export const FormSettingsEditor: FC = ({ isVisible, cl { formRef.current?.submit(); }} @@ -52,11 +51,11 @@ export const FormSettingsEditor: FC = ({ isVisible, cl > { formRef?: MutableRefObject; propertyFilter?: (name: string) => boolean; layoutSettings?: IFormLayoutSettings; + isInModal?: boolean; } function GenericSettingsForm({ @@ -29,6 +30,7 @@ function GenericSettingsForm({ formRef, propertyFilter, layoutSettings = DEFAULT_FORM_LAYOUT_SETTINGS, + isInModal }: IProps) { const [form] = Form.useForm(); @@ -63,7 +65,7 @@ function GenericSettingsForm({ return ( = ({ formId }) => { - return ( - - - - ); -}; - -type InternalFormDesignerType = typeof FormDesigner; -interface IInternalFormDesignerInterface extends InternalFormDesignerType { - Visual: typeof FormDesignerRenderer; - NonVisual: typeof FormProviderWrapper; -} - -const FormDesignerInterface = FormDesigner as IInternalFormDesignerInterface; -FormDesignerInterface.Visual = FormDesignerRenderer; -FormDesignerInterface.NonVisual = FormProviderWrapper; - -export { - FormDesignerInterface as FormDesigner, - type IFormDesignerProps, +import { FormProviderWrapper } from "./formDesignerWrapper"; +import { FormDesignerRenderer } from "./formDesignerRenderer"; +import React, { FC } from "react"; +import { FormIdentifier } from "@/interfaces"; + +interface IFormDesignerProps { + formId: FormIdentifier; +} + +const FormDesigner: FC = ({ formId }) => { + return ( + + + + ); +}; + +type InternalFormDesignerType = typeof FormDesigner; +interface IInternalFormDesignerInterface extends InternalFormDesignerType { + Visual: typeof FormDesignerRenderer; + NonVisual: typeof FormProviderWrapper; +} + +const FormDesignerInterface = FormDesigner as IInternalFormDesignerInterface; +FormDesignerInterface.Visual = FormDesignerRenderer; +FormDesignerInterface.NonVisual = FormProviderWrapper; + +export { + FormDesignerInterface as FormDesigner, + type IFormDesignerProps, }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/quickEdit/quickEditToolbar.tsx b/shesha-reactjs/src/components/formDesigner/quickEdit/quickEditToolbar.tsx index eede96e160..c60b2c4dcc 100644 --- a/shesha-reactjs/src/components/formDesigner/quickEdit/quickEditToolbar.tsx +++ b/shesha-reactjs/src/components/formDesigner/quickEdit/quickEditToolbar.tsx @@ -1,5 +1,5 @@ import { FormConfigurationDto } from '@/providers/form/api'; -import { useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerStateSelector } from '@/providers/formDesigner'; import { App } from 'antd'; import React, { FC } from 'react'; import { CreateNewVersionButton } from '../toolbar/createNewVersionButton'; @@ -19,7 +19,7 @@ export interface IQuickEditToolbarProps { } export const QuickEditToolbar: FC = ({ onUpdated, onNewVersionCreated, renderSource }) => { - const { readOnly } = useFormDesignerState(); + const readOnly = useFormDesignerStateSelector(x => x.readOnly); const { message } = App.useApp(); // TODO: reload current dialog diff --git a/shesha-reactjs/src/components/formDesigner/toolbar/customActions.tsx b/shesha-reactjs/src/components/formDesigner/toolbar/customActions.tsx index 4c320f9dc9..7d3ddc726b 100644 --- a/shesha-reactjs/src/components/formDesigner/toolbar/customActions.tsx +++ b/shesha-reactjs/src/components/formDesigner/toolbar/customActions.tsx @@ -1,5 +1,5 @@ import { useDynamicModals, useHttpClient } from "@/providers"; -import { useFormDesignerState } from "@/providers/formDesigner"; +import { useFormDesignerStateSelector } from "@/providers/formDesigner"; import { useFormPersister } from "@/providers/formPersisterProvider"; import { downloadAsJson } from "@/utils/configurationFramework/actions"; import { MenuOutlined } from "@ant-design/icons"; @@ -18,7 +18,7 @@ export const CustomActions: FC = () => { const { formProps, loadForm } = useFormPersister(); const { open: openModal } = useDynamicModals(); const { message } = App.useApp(); - const { readOnly } = useFormDesignerState(); + const readOnly = useFormDesignerStateSelector(x => x.readOnly); const items: MenuItem[] = [ { @@ -36,6 +36,7 @@ export const CustomActions: FC = () => { openModal({ title: 'Import JSON', formId: { module: 'Shesha', name: 'form-import-json' }, + mode: 'edit', formArguments: { itemId: formProps.id, }, diff --git a/shesha-reactjs/src/components/formDesigner/toolbar/debugButton.tsx b/shesha-reactjs/src/components/formDesigner/toolbar/debugButton.tsx index 283accc1ae..712cb1833c 100644 --- a/shesha-reactjs/src/components/formDesigner/toolbar/debugButton.tsx +++ b/shesha-reactjs/src/components/formDesigner/toolbar/debugButton.tsx @@ -1,14 +1,14 @@ import React, { FC } from 'react'; import { BugOutlined } from '@ant-design/icons'; import { Button } from 'antd'; -import { useFormDesignerState, useFormDesignerActions } from '@/providers/formDesigner'; +import { useFormDesignerActions, useFormDesignerStateSelector } from '@/providers/formDesigner'; export interface IDebugButtonProps { } export const DebugButton: FC = () => { - const { isDebug } = useFormDesignerState(); + const isDebug = useFormDesignerStateSelector(x => x.isDebug); const { setDebugMode } = useFormDesignerActions(); return ( diff --git a/shesha-reactjs/src/components/formDesigner/toolbar/formDesignerToolbar.tsx b/shesha-reactjs/src/components/formDesigner/toolbar/formDesignerToolbar.tsx index d52eca9ba9..93a09f8c99 100644 --- a/shesha-reactjs/src/components/formDesigner/toolbar/formDesignerToolbar.tsx +++ b/shesha-reactjs/src/components/formDesigner/toolbar/formDesignerToolbar.tsx @@ -3,7 +3,7 @@ import { App } from 'antd'; import { CreateNewVersionButton } from './createNewVersionButton'; import { FormConfigurationDto } from '@/providers/form/api'; import { SaveMenu } from './saveMenu'; -import { useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerStateSelector } from '@/providers/formDesigner'; import { useShaRouting, useSheshaApplication } from '@/providers'; import { PublishButton } from './publishButton'; import { DebugButton } from './debugButton'; @@ -19,11 +19,11 @@ export interface IProps { } export const FormDesignerToolbar: FC = () => { const { routes } = useSheshaApplication(); const { router } = useShaRouting(false) ?? {}; - const { readOnly } = useFormDesignerState(); + const readOnly = useFormDesignerStateSelector(x => x.readOnly); + const formSettings = useFormDesignerStateSelector(x => x.formSettings); const { styles } = useStyles(); const { message } = App.useApp(); - const { formSettings } = useFormDesignerState(); const { anyOfPermissionsGranted } = useSheshaApplication(); const isGranted = formSettings?.access !== 4 || anyOfPermissionsGranted(formSettings?.permissions || []); diff --git a/shesha-reactjs/src/components/formDesigner/toolbar/formSettingsButton.tsx b/shesha-reactjs/src/components/formDesigner/toolbar/formSettingsButton.tsx index 295b5454e4..18294edb8b 100644 --- a/shesha-reactjs/src/components/formDesigner/toolbar/formSettingsButton.tsx +++ b/shesha-reactjs/src/components/formDesigner/toolbar/formSettingsButton.tsx @@ -2,7 +2,7 @@ import React, { FC, useState } from 'react'; import { Button } from 'antd'; import { FormSettingsEditor } from '../formSettingsEditor'; import { SettingOutlined } from '@ant-design/icons'; -import { useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerStateSelector } from '@/providers/formDesigner'; export interface IFormSettingsButtonProps { @@ -10,7 +10,7 @@ export interface IFormSettingsButtonProps { export const FormSettingsButton: FC = () => { const [settingsVisible, setSettingsVisible] = useState(false); - const { readOnly } = useFormDesignerState(); + const readOnly = useFormDesignerStateSelector(x => x.readOnly); const onSettingsClick = () => { setSettingsVisible(true); diff --git a/shesha-reactjs/src/components/formDesigner/toolbar/previewButton.tsx b/shesha-reactjs/src/components/formDesigner/toolbar/previewButton.tsx index de08bed7cc..fc11f6d624 100644 --- a/shesha-reactjs/src/components/formDesigner/toolbar/previewButton.tsx +++ b/shesha-reactjs/src/components/formDesigner/toolbar/previewButton.tsx @@ -1,25 +1,29 @@ -import React, { FC } from 'react'; -import { Button } from 'antd'; -import { EyeOutlined } from '@ant-design/icons'; -import { useForm } from '@/providers'; - -export interface IPreviewButtonProps { - -} - -export const PreviewButton: FC = () => { - const { setFormMode, formMode } = useForm(); - - return ( - - ); +import React, { FC } from 'react'; +import { Button } from 'antd'; +import { EyeOutlined } from '@ant-design/icons'; +import { useFormActions } from '@/providers'; +import { useFormDesignerActions, useFormDesignerStateSelector } from '@/providers/formDesigner'; + +export interface IPreviewButtonProps { + +} + +export const PreviewButton: FC = () => { + const { setFormMode } = useFormActions(); + const { setFormMode: setFormDesignerMode } = useFormDesignerActions(); + const formMode = useFormDesignerStateSelector(x => x.formMode); + + return ( + + ); }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/formDesigner/toolbar/saveMenu.tsx b/shesha-reactjs/src/components/formDesigner/toolbar/saveMenu.tsx index c68bb947d5..d60fa03f2f 100644 --- a/shesha-reactjs/src/components/formDesigner/toolbar/saveMenu.tsx +++ b/shesha-reactjs/src/components/formDesigner/toolbar/saveMenu.tsx @@ -14,7 +14,7 @@ import { MenuProps, } from 'antd'; import { FormMarkupWithSettings } from '@/providers/form/models'; -import { useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerStateSelector } from '@/providers/formDesigner'; import { useFormDesignerComponents } from '@/providers/form/hooks'; import { useFormPersister } from '@/providers/formPersisterProvider'; import { useHttpClient } from '@/providers'; @@ -34,7 +34,8 @@ export interface ISaveMenuProps { export const SaveMenu: FC = ({ onSaved }) => { const { loadForm, saveForm, formProps } = useFormPersister(); - const { formFlatMarkup, formSettings } = useFormDesignerState(); + const formFlatMarkup = useFormDesignerStateSelector(x => x.formFlatMarkup); + const formSettings = useFormDesignerStateSelector(x => x.formSettings); const toolboxComponents = useFormDesignerComponents(); const httpClient = useHttpClient(); const { message, modal } = App.useApp(); diff --git a/shesha-reactjs/src/components/formDesigner/toolbox.tsx b/shesha-reactjs/src/components/formDesigner/toolbox.tsx index eabff17719..471da71143 100644 --- a/shesha-reactjs/src/components/formDesigner/toolbox.tsx +++ b/shesha-reactjs/src/components/formDesigner/toolbox.tsx @@ -3,7 +3,7 @@ import { ToolboxComponents } from './toolboxComponents'; import { ToolboxDataSources } from './toolboxDataSources'; import { useStyles } from './styles/styles'; import { Tabs } from 'antd'; -import { useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerStateSelector } from '@/providers/formDesigner'; import { isEntityMetadata, isPropertiesArray } from '@/interfaces/metadata'; import { useMetadata } from '@/providers'; @@ -11,7 +11,7 @@ export interface IProps { } const Toolbox: FC = () => { const { styles } = useStyles(); - const { dataSources: formDs } = useFormDesignerState(); + const formDs = useFormDesignerStateSelector(x => x.dataSources); const currentMeta = useMetadata(false); const builderItems = useMemo(() => { diff --git a/shesha-reactjs/src/components/formDesigner/toolboxComponents.tsx b/shesha-reactjs/src/components/formDesigner/toolboxComponents.tsx index 987bc0445b..bfc17a4308 100644 --- a/shesha-reactjs/src/components/formDesigner/toolboxComponents.tsx +++ b/shesha-reactjs/src/components/formDesigner/toolboxComponents.tsx @@ -6,7 +6,7 @@ import { ItemInterface, ReactSortable } from 'react-sortablejs'; import { TOOLBOX_COMPONENT_DROPPABLE_KEY } from '@/providers/form/models'; import { IToolboxComponentGroup } from '@/interfaces'; import { SearchBox } from './toolboxSearchBox'; -import { useFormDesignerActions, useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerActions, useFormDesignerStateSelector } from '@/providers/formDesigner'; import { useStyles } from './styles/styles'; type PanelType = CollapseProps['items'][number]; @@ -18,7 +18,7 @@ export const ToolboxComponents: FC = () => { const [searchText, setSearchText] = useLocalStorage('shaDesigner.toolbox.components.search', ''); const { styles } = useStyles(); - const { toolboxComponentGroups } = useFormDesignerState(); + const toolboxComponentGroups = useFormDesignerStateSelector(state => state.toolboxComponentGroups); const { startDraggingNewItem, endDraggingNewItem } = useFormDesignerActions(); const filteredGroups = useMemo(() => { diff --git a/shesha-reactjs/src/components/globalTableFilter/index.tsx b/shesha-reactjs/src/components/globalTableFilter/index.tsx index 14b18f0402..c9943c0327 100644 --- a/shesha-reactjs/src/components/globalTableFilter/index.tsx +++ b/shesha-reactjs/src/components/globalTableFilter/index.tsx @@ -6,18 +6,18 @@ import { useDataTable } from '@/providers'; export interface IGlobalTableFilterProps { searchProps?: SearchProps; block?: boolean; + style?: React.CSSProperties; } -export const GlobalTableFilter: FC = ({ searchProps, block }) => { +export const GlobalTableFilter: FC = ({ searchProps, block, style }) => { const { changeQuickSearch, quickSearch, performQuickSearch } = useDataTable(); const srcProps: SearchProps = { - size: 'small', allowClear: true, ...searchProps, }; - return ; + return ; }; export default GlobalTableFilter; diff --git a/shesha-reactjs/src/components/globalTableFilterBase/index.tsx b/shesha-reactjs/src/components/globalTableFilterBase/index.tsx index 9fd330de93..710bd5841d 100644 --- a/shesha-reactjs/src/components/globalTableFilterBase/index.tsx +++ b/shesha-reactjs/src/components/globalTableFilterBase/index.tsx @@ -11,6 +11,7 @@ export interface IGlobalTableFilterBaseProps { quickSearch: string; size?: SizeType; block?: boolean; + style?: React.CSSProperties; } export const GlobalTableFilterBase: FC = ({ @@ -18,14 +19,14 @@ export const GlobalTableFilterBase: FC = ({ changeQuickSearch, performQuickSearch, quickSearch, - size = 'small', block = false, + style, }) => { const { styles } = useStyles({ block }); const srcProps: SearchProps = { - size, allowClear: true, ...searchProps, + size: searchProps?.size || 'small', }; const onSearch = ( @@ -42,18 +43,18 @@ export const GlobalTableFilterBase: FC = ({ }; return ( -
- event?.stopPropagation()} - onSearch={onSearch} - onChange={(e) => { - e?.stopPropagation(); - changeQuickSearch(e.target.value); - }} - onClick={(event) => event?.stopPropagation()} - {...srcProps} - /> +
+ event?.stopPropagation()} + onSearch={onSearch} + onChange={(e) => { + e?.stopPropagation(); + changeQuickSearch(e.target.value); + }} + onClick={(event) => event?.stopPropagation()} + {...srcProps} + />
); }; diff --git a/shesha-reactjs/src/components/googlePlacesAutocomplete/index.tsx b/shesha-reactjs/src/components/googlePlacesAutocomplete/index.tsx index 7cc7504fc8..89aeb7ff05 100644 --- a/shesha-reactjs/src/components/googlePlacesAutocomplete/index.tsx +++ b/shesha-reactjs/src/components/googlePlacesAutocomplete/index.tsx @@ -44,6 +44,8 @@ export interface IGooglePlacesAutocompleteProps { style?: CSSProperties; size?: SizeType; searchOptions?: PropTypes['searchOptions']; + onFocus?: (payload: any) => void; + onBlur?: (payload: string) => void; } const GooglePlacesAutocomplete: FC = ({ @@ -63,6 +65,7 @@ const GooglePlacesAutocomplete: FC = ({ style, size, searchOptions, + onFocus }) => { const { styles } = useStyles(); const [highlightedPlaceId, setHighlightedPlaceId] = useState(''); @@ -182,6 +185,8 @@ const GooglePlacesAutocomplete: FC = ({ }; const onBlur = () => setShowSuggestionsDropdownContainer(false); + + const handleFocus = (event) => onFocus(event); return ( // @ts-ignore = ({ tabIndex={tabIndex} onKeyDown={onKeyDown} onBlur={onBlur} + onFocus={handleFocus} style={style} size={size} /> diff --git a/shesha-reactjs/src/components/iconPicker/index.tsx b/shesha-reactjs/src/components/iconPicker/index.tsx index 5a97eea6ca..20b6a75eb7 100644 --- a/shesha-reactjs/src/components/iconPicker/index.tsx +++ b/shesha-reactjs/src/components/iconPicker/index.tsx @@ -1,213 +1,219 @@ -import React, { FC, ReactNode, useEffect, useMemo, useState } from 'react'; -import { IconBaseProps } from '@ant-design/icons/lib/components/Icon'; -import { FilledIconTypes, FILLED_ICON_GROUPS } from './iconNamesFilled'; -import ShaIcon from '@/components/shaIcon'; -import { Button, Input, Radio, RadioChangeEvent, Modal } from 'antd'; -import { SelectOutlined } from '@ant-design/icons'; -import { SizeType } from 'antd/lib/config-provider/SizeContext'; -import { OutlinedIconTypes, OUTLINED_ICON_GROUPS } from './iconNamesOutlined'; -import SectionSeparator from '@/components/sectionSeparator'; -import { TwoToneIconTypes, TWO_FACED_ICON_GROUPS } from './iconNamesTwoTone'; -import { humanizeString } from '@/utils/string'; -import classNames from 'classnames'; -import { useStyles } from './styles/styles'; - -export type ShaIconTypes = FilledIconTypes | OutlinedIconTypes | TwoToneIconTypes; -type IconModes = 'outlined' | 'filled' | 'twoFaced'; - -interface IIconMode { - label: string; - value: IconModes; - icon?: ShaIconTypes; -} - -const ICON_MODE_OPTIONS: IIconMode[] = [ - { label: 'Outlined', value: 'outlined', icon: 'BorderOutlined' }, - { label: 'Filled', value: 'filled' }, - { label: 'Two Faced', value: 'twoFaced' }, -]; - -const ICON_MODE_GROUPS = { - filled: FILLED_ICON_GROUPS, - outlined: OUTLINED_ICON_GROUPS, - twoFaced: TWO_FACED_ICON_GROUPS, -}; - -export interface IIconPickerProps extends IconBaseProps { - /** The icon name */ - value?: ShaIconTypes; - - /** A callback for when the icon changes */ - onIconChange?: (icon: ReactNode, iconName: ShaIconTypes) => void; - - /** The size of the select button */ - selectBtnSize?: SizeType; - - /** if true, indicates that the picker is readonly */ - readOnly?: boolean; - - twoToneColor?: string; - - defaultValue?: ShaIconTypes; -} - -interface IOption { - mode: IconModes; - group: typeof FILLED_ICON_GROUPS | typeof OUTLINED_ICON_GROUPS | typeof TWO_FACED_ICON_GROUPS; -} - -/** - * A component for selecting icons, usually for form - */ -const IconPicker: FC = ({ - selectBtnSize = 'middle', - value, - onIconChange, - readOnly = false, - defaultValue, - ...props -}) => { - const { styles } = useStyles(); - const [localSelectedIcon, setLocalSelectedIcon] = useState(defaultValue); - const [showModal, setShowModal] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - const [searchOption, setSearchOption] = useState({ - mode: 'outlined', - group: ICON_MODE_GROUPS['outlined'], - }); - - useEffect(() => { - setLocalSelectedIcon(value || defaultValue); - }, [defaultValue, value]); - - const toggleModalVisibility = () => { - if (!readOnly) setShowModal((visible) => !visible); - }; - - const changeIconModes = (e: RadioChangeEvent) => { - const mode = e.target.value as IconModes; - - setSearchOption({ mode, group: ICON_MODE_GROUPS[mode] }); - }; - - const onSearchChange = (event: React.ChangeEvent) => setSearchQuery(event?.target?.value); - - const handleIconSelection = (selected: ShaIconTypes) => { - if (readOnly) return; - setLocalSelectedIcon(selected); - toggleModalVisibility(); - - if (onIconChange) { - onIconChange(, selected); - } - }; - - const onClear = () => { - if (readOnly) return; - setLocalSelectedIcon(null); - toggleModalVisibility(); - - if (onIconChange) { - onIconChange(null, null); - } - }; - - const memoizedActiveGroup = useMemo(() => { - if (searchQuery) { - const activeGroup = searchOption?.group; - const filteredGroup = {}; - const objectKeys = Object.keys(activeGroup); - - for (const key of objectKeys) { - filteredGroup[key] = activeGroup[key].filter((groupItem: string) => - groupItem?.toLowerCase()?.includes(searchQuery?.toLowerCase()) - ); - } - return filteredGroup; - } else { - return searchOption?.group; - } - }, [searchQuery, searchOption?.group]); - - return ( -
-
- {localSelectedIcon ? ( - - - - ) : !readOnly ? ( - - ) : ( - <> - )} -
- - - -
- } - className={styles.shaIconPickerModal} - > -
- - -
- -
-
-
- {Object.keys(memoizedActiveGroup).map((groupKey) => ( -
- {memoizedActiveGroup[groupKey]?.length ? ( -
- -
- ) : null} - -
- {memoizedActiveGroup[groupKey].map((item: ShaIconTypes, index) => ( - handleIconSelection(item)} - key={index} - > - - {item} - - ))} -
-
- ))} -
- -
- ); -}; - -export default IconPicker; +import React, { FC, ReactNode, useEffect, useMemo, useState } from 'react'; +import { IconBaseProps } from '@ant-design/icons/lib/components/Icon'; +import { FilledIconTypes, FILLED_ICON_GROUPS } from './iconNamesFilled'; +import ShaIcon from '@/components/shaIcon'; +import { Button, Input, Radio, RadioChangeEvent, Modal } from 'antd'; +import { SelectOutlined } from '@ant-design/icons'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; +import { OutlinedIconTypes, OUTLINED_ICON_GROUPS } from './iconNamesOutlined'; +import SectionSeparator from '@/components/sectionSeparator'; +import { TwoToneIconTypes, TWO_FACED_ICON_GROUPS } from './iconNamesTwoTone'; +import { humanizeString } from '@/utils/string'; +import classNames from 'classnames'; +import { useStyles } from './styles/styles'; + +export type ShaIconTypes = FilledIconTypes | OutlinedIconTypes | TwoToneIconTypes; +type IconModes = 'outlined' | 'filled' | 'twoFaced'; + +interface IIconMode { + label: string; + value: IconModes; + icon?: ShaIconTypes; +} + +const ICON_MODE_OPTIONS: IIconMode[] = [ + { label: 'Outlined', value: 'outlined', icon: 'BorderOutlined' }, + { label: 'Filled', value: 'filled' }, + { label: 'Two Faced', value: 'twoFaced' }, +]; + +const ICON_MODE_GROUPS = { + filled: FILLED_ICON_GROUPS, + outlined: OUTLINED_ICON_GROUPS, + twoFaced: TWO_FACED_ICON_GROUPS, +}; + +export interface IIconPickerProps extends IconBaseProps { + /** The icon name */ + value?: any; + + /** A callback for when the icon changes */ + onIconChange?: (icon: ReactNode, iconName: ShaIconTypes) => void; + + /** The size of the select button */ + selectBtnSize?: SizeType; + + /** if true, indicates that the picker is readonly */ + readOnly?: boolean; + + twoToneColor?: string; + + iconSize?: number; + + defaultValue?: ShaIconTypes; +} + +interface IOption { + mode: IconModes; + group: typeof FILLED_ICON_GROUPS | typeof OUTLINED_ICON_GROUPS | typeof TWO_FACED_ICON_GROUPS; +} + +/** + * A component for selecting icons, usually for form + */ +const IconPicker: FC = ({ + selectBtnSize = 'middle', + value, + onIconChange, + readOnly = false, + defaultValue, + iconSize, + ...props +}) => { + const { styles } = useStyles(); + const [localSelectedIcon, setLocalSelectedIcon] = useState(defaultValue); + const [showModal, setShowModal] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [searchOption, setSearchOption] = useState({ + mode: 'outlined', + group: ICON_MODE_GROUPS['outlined'], + }); + + useEffect(() => { + setLocalSelectedIcon(typeof value === 'object' ? value?.props?.iconName : value || defaultValue); + onIconChange(, defaultValue); + }, [defaultValue, value]); + + const toggleModalVisibility = () => { + if (!readOnly) setShowModal((visible) => !visible); + }; + + const changeIconModes = (e: RadioChangeEvent) => { + const mode = e.target.value as IconModes; + + setSearchOption({ mode, group: ICON_MODE_GROUPS[mode] }); + }; + + const onSearchChange = (event: React.ChangeEvent) => setSearchQuery(event?.target?.value); + + const handleIconSelection = (selected: ShaIconTypes) => { + if (readOnly) return; + setLocalSelectedIcon(selected); + toggleModalVisibility(); + + if (onIconChange) { + onIconChange(, selected); + } + }; + + const onClear = () => { + if (readOnly) return; + setLocalSelectedIcon(null); + toggleModalVisibility(); + + if (onIconChange) { + onIconChange(null, null); + } + }; + + const memoizedActiveGroup = useMemo(() => { + if (searchQuery) { + const activeGroup = searchOption?.group; + const filteredGroup = {}; + const objectKeys = Object.keys(activeGroup); + + for (const key of objectKeys) { + filteredGroup[key] = activeGroup[key].filter((groupItem: string) => + groupItem?.toLowerCase()?.includes(searchQuery?.toLowerCase()) + ); + } + return filteredGroup; + } else { + return searchOption?.group; + } + }, [searchQuery, searchOption?.group]); + + return ( +
+
+
+ {localSelectedIcon ? ( + + ) : ( + + )} +
+
+ + + +
+ } + className={styles.shaIconPickerModal} + > +
+ +
+ +
+
+
+ {Object.keys(memoizedActiveGroup).map((groupKey) => ( +
+ {memoizedActiveGroup[groupKey]?.length ? ( +
+ +
+ ) : null} + +
+ {memoizedActiveGroup[groupKey].map((item: ShaIconTypes, index) => ( + handleIconSelection(item)} + key={index} + > + + {item} + + ))} +
+
+ ))} +
+
+
+ ); +}; + +export default IconPicker; diff --git a/shesha-reactjs/src/components/index.ts b/shesha-reactjs/src/components/index.ts index 0227c6d4a6..a5b8e3c593 100644 --- a/shesha-reactjs/src/components/index.ts +++ b/shesha-reactjs/src/components/index.ts @@ -14,7 +14,8 @@ export { ConfigurableFormComponent, type IConfigurableFormComponentProps, } from './formDesigner/configurableFormComponent'; -export { default as ConfigurableFormItem, type IConfigurableFormItemProps } from './formDesigner/components/formItem'; +export { default as ConfigurableFormItem } from './formDesigner/components/formItem'; +export { type IConfigurableFormItemProps } from './formDesigner/components/model'; export { default as BooleanDropDown } from './booleanDropDown'; export { SidebarContainer } from './sidebarContainer'; export { default as AutoCompletePlaces } from './googlePlacesAutocomplete'; @@ -91,12 +92,7 @@ export { default as Show, type IShowProps } from './show'; export { CodeEditor } from '@/designer-components/codeEditor/codeEditor'; export { default as ModelConfigurator } from './modelConfigurator'; export { ConfigurableApplicationComponent, type IConfigurableApplicationComponentProps } from './configurableComponent'; -export { - EntityDtoAutocomplete as Autocomplete, - EntityDtoAutocomplete, - RawAutocomplete, - type IAutocompleteProps, -} from './autocomplete'; +export { Autocomplete, type IAutocompleteProps} from './autocomplete'; export { Page } from './page'; export { default as EditableTagGroup, type IEditableTagGroupProps } from './editableTagGroup'; export { default as QuickView, type IQuickViewProps } from './quickView'; @@ -118,4 +114,5 @@ export { ListEditor } from './listEditor'; export { ButtonGroupConfigurator } from './buttonGroupConfigurator'; export { ConfigurableComponentRenderer } from "./configurableComponentRenderer"; export { PermissionAutocomplete, type IPermissionAutocompleteProps } from './permissionAutocomplete'; -export { ConfigItemAutocomplete } from './configurableItemAutocomplete'; \ No newline at end of file +export { ConfigItemAutocomplete } from './configurableItemAutocomplete'; +export { RefListSimpleDropdown as RefListDropdown } from './queryBuilder/widgets/refListDropDown/simpleDropdown'; \ No newline at end of file diff --git a/shesha-reactjs/src/components/kanban/components/renderColumn.tsx b/shesha-reactjs/src/components/kanban/components/renderColumn.tsx index 7010668d0a..9fef3324b6 100644 --- a/shesha-reactjs/src/components/kanban/components/renderColumn.tsx +++ b/shesha-reactjs/src/components/kanban/components/renderColumn.tsx @@ -1,14 +1,13 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { Button, Dropdown, Popconfirm } from 'antd'; +import { getDimensionsStyle } from '@/designer-components/_settings/utils/dimensions/utils'; +import { getFontStyle } from '@/designer-components/_settings/utils/font/utils'; +import { ConfigurableForm, ShaIcon, useAvailableConstantsData, useConfigurableActionDispatcher } from '@/index'; +import { useRefListItemGroupConfigurator } from '@/components/refListSelectorDisplay/provider'; +import { LeftOutlined, MoreOutlined, PlusOutlined, RightOutlined, SettingOutlined } from '@ant-design/icons'; +import { Button, Dropdown, Flex, MenuProps, Popconfirm } from 'antd'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ReactSortable } from 'react-sortablejs'; -import { PlusOutlined, MoreOutlined, RightOutlined, LeftOutlined, SettingFilled } from '@ant-design/icons'; -import { ConfigurableForm, getStyle, IconPicker, useAvailableConstantsData, useConfigurableActionDispatcher, useFormData } from '@/index'; -import { MenuProps } from 'antd'; -import { Flex } from 'antd'; -import { useRefListItemGroupConfigurator } from '@/providers/refList/provider'; -import { useKanbanActions } from '../utils'; import { useStyles } from '../styles/styles'; -import { addPx } from '@/designer-components/button/util'; +import { useKanbanActions } from '../utils'; interface KanbanColumnProps { column: any; @@ -42,17 +41,16 @@ const RenderColumn: React.FC = ({ const [isCollapsed, setIsCollapsed] = useState(collapse); const { updateUserSettings } = useKanbanActions(); const { storeSettings, userSettings } = useRefListItemGroupConfigurator(); - const { styles } = useStyles({ ...props, isCollapsed }); - const { data: formData } = useFormData(); + const dimensionsStyles = useMemo(() => getDimensionsStyle(props.columnStyles.dimensions), [props.columnStyles.dimensions]); + const fontStyles = useMemo(() => getFontStyle(props.font), [props.font]); + const { styles } = useStyles({ ...props, isCollapsed, dimensionsStyles, fontStyles }); const { updateKanban } = useKanbanActions(); const allData = useAvailableConstantsData(); const { executeAction } = useConfigurableActionDispatcher(); - // Initialize collapse state from props useEffect(() => { setIsCollapsed(collapse); }, [collapse]); - // Update user settings and persist to backend const toggleFold = async () => { try { @@ -85,15 +83,13 @@ const RenderColumn: React.FC = ({ }, ].filter(Boolean); - - const onEnd = useCallback( (evt: any, column: any): Promise => { return new Promise((resolve) => { const { to, dragged } = evt; const draggedTask = dragged?.dataset; - const targetColumn = to?.dataset?.targetColumn; - + const targetColumn = to?.dataset?.targetColumn; + if (!targetColumn?.actionConfiguration?.actionName) { resolve(true); // Allow the drag and drop to proceed without action return; @@ -157,13 +153,14 @@ const RenderColumn: React.FC = ({ }); }; + const iconStyles = { ...fontStyles, margin: '0px 8px' }; return ( <> {!column.hidden && (
= ({ } align="center" className={styles.combinedHeaderStyle} - style={{ ...(getStyle(props.headerStyles, formData) || {}) }} + style={props.headerStyles || {}} > - {props.showIcons && column.icon && ( - - )} -

+ {props.showIcons && column.icon && } + {column.item} ({columnTasks.length}) -

+ {props.kanbanReadonly || props.readonly || !(props.allowNewRecord || props.collapsible) ? null : ( - ); + : () => ((!maxItemsCount || (sortableItems?.length ?? 0) < maxItemsCount) && ); return (
diff --git a/shesha-reactjs/src/components/listEditor/styles/styles.ts b/shesha-reactjs/src/components/listEditor/styles/styles.ts index 5150d3d705..0d988aa232 100644 --- a/shesha-reactjs/src/components/listEditor/styles/styles.ts +++ b/shesha-reactjs/src/components/listEditor/styles/styles.ts @@ -34,9 +34,8 @@ export const useStyles = createStyles(({ css, cx, token }) => { padding-bottom: 7px; .${listItem} { position: relative; - padding: 5px; display: flex; - + padding: 5px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 4px; diff --git a/shesha-reactjs/src/components/listEditorWithPropertiesPanel/index.tsx b/shesha-reactjs/src/components/listEditorWithPropertiesPanel/index.tsx index 5cd2701746..dd6f12a04d 100644 --- a/shesha-reactjs/src/components/listEditorWithPropertiesPanel/index.tsx +++ b/shesha-reactjs/src/components/listEditorWithPropertiesPanel/index.tsx @@ -1,72 +1,108 @@ -import React, { useState, useTransition } from 'react'; -import { ListItem, ListItemWithId } from '../listEditor/models'; -import { ListEditorRenderer } from '../listEditorRenderer'; -import { ListEditor, ListEditorChildrenFn, ListEditorSectionRenderingFn } from '../listEditor'; -import { Skeleton } from 'antd'; -import { DefaultGroupHeader } from './defaultGroupHeader'; -import { NoSelection } from './noSelection'; - -export interface ItemPropertiesRendererProps { - item: TItem; - onChange: (newValues: TItem) => void; - readOnly: boolean; -} - -export interface IListEditorWithPropertiesPanelProps { - readOnly: boolean; - value: TItem[]; - onChange: (newValue: TItem[]) => void; - header?: React.ReactNode; - children: ListEditorChildrenFn; - addItemText?: string; - groupHeader?: ListEditorSectionRenderingFn; - initNewItem: (items: TItem[]) => TItem; - itemProperties: (itemProps: ItemPropertiesRendererProps) => React.ReactNode; - noSelectionProperties?: string | React.ReactElement; -} - -export const ListEditorWithPropertiesPanel = ({ value, onChange, readOnly, header, groupHeader, initNewItem, children, itemProperties, noSelectionProperties, addItemText }: IListEditorWithPropertiesPanelProps) => { - const [selectedItem, setSelectedItem] = useState(); - const [isPending, startTransition] = useTransition(); - - const onSelectionChange = (item: TItem) => { - startTransition(() => { - setSelectedItem(item); - }); - }; - - const onItemUpdate = (newValues: TItem) => { - if (!selectedItem || selectedItem.id !== newValues.id) return; - - Object.assign(selectedItem, newValues); - onChange([...value]); - }; - - return ( - - : selectedItem - ? itemProperties({ item: selectedItem, onChange: onItemUpdate, readOnly: readOnly }) - : !noSelectionProperties || typeof (noSelectionProperties) === 'string' - ? - : noSelectionProperties, - }} - > - {header} - - value={value} - onChange={onChange} - initNewItem={initNewItem} - readOnly={readOnly} - selectionType='single' - onSelectionChange={onSelectionChange} - header={groupHeader ?? (groupHeader === undefined ? (headerProps) => () : null)} - > - {children} - - - ); +import React, { useState, useTransition } from 'react'; +import { ListItem, ListItemWithId } from '../listEditor/models'; +import { ListEditorRenderer } from '../listEditorRenderer'; +import { ListEditor, ListEditorChildrenFn, ListEditorSectionRenderingFn } from '../listEditor'; +import { Skeleton } from 'antd'; +import { DefaultGroupHeader } from './defaultGroupHeader'; +import { NoSelection } from './noSelection'; + +export interface ItemPropertiesRendererProps { + item: TItem; + onChange: (newValues: TItem) => void; + readOnly: boolean; +} + +export interface IListEditorWithPropertiesPanelProps { + readOnly: boolean; + value: TItem[]; + onChange: (newValue: TItem[]) => void; + header?: React.ReactNode; + children: ListEditorChildrenFn; + addItemText?: string; + groupHeader?: ListEditorSectionRenderingFn; + initNewItem: (items: TItem[]) => TItem; + itemProperties: (itemProps: ItemPropertiesRendererProps) => React.ReactNode; + noSelectionProperties?: string | React.ReactElement; +} + +/** + * Helper function to recursively find and update an item in nested groups + */ +const findAndUpdateItemRecursively = ( + items: TItem[], + targetId: string, + newValues: TItem +): { updated: boolean; newItems: TItem[] } => { + const newItems = [...items]; + + for (let i = 0; i < newItems.length; i++) { + const item = newItems[i]; + + // Check if this is the item we're looking for + if (item.id === targetId) { + newItems[i] = { ...item, ...newValues }; + return { updated: true, newItems }; + } + + // If this is a group with childItems, search recursively + if (item.itemType === 'group' && item.childItems) { + const result = findAndUpdateItemRecursively(item.childItems, targetId, newValues); + if (result.updated) { + newItems[i] = { ...item, childItems: result.newItems }; + return { updated: true, newItems }; + } + } + } + + return { updated: false, newItems }; +}; + +export const ListEditorWithPropertiesPanel = ({ value, onChange, readOnly, header, groupHeader, initNewItem, children, itemProperties, noSelectionProperties, addItemText }: IListEditorWithPropertiesPanelProps) => { + const [selectedItem, setSelectedItem] = useState(); + const [isPending, startTransition] = useTransition(); + + const onSelectionChange = (item: TItem) => { + startTransition(() => { + setSelectedItem(item); + }); + }; + + const onItemUpdate = (newValues: TItem) => { + if (!selectedItem || selectedItem.id !== newValues.id) return; + + const result = findAndUpdateItemRecursively(value, newValues.id, newValues); + if (result.updated) { + onChange(result.newItems); + } else { + console.warn('item not found'); + } + }; + + return ( + + : selectedItem + ? itemProperties({ item: selectedItem, onChange: onItemUpdate, readOnly: readOnly }) + : !noSelectionProperties || typeof (noSelectionProperties) === 'string' + ? + : noSelectionProperties, + }} + > + {header} + + value={value} + onChange={onChange} + initNewItem={initNewItem} + readOnly={readOnly} + selectionType='single' + onSelectionChange={onSelectionChange} + header={groupHeader ?? (groupHeader === undefined ? (headerProps) => () : null)} + > + {children} + + + ); }; \ No newline at end of file diff --git a/shesha-reactjs/src/components/mainLayout/styles/indexStyles.ts b/shesha-reactjs/src/components/mainLayout/styles/indexStyles.ts index 53bdc15598..0af2e02d3b 100644 --- a/shesha-reactjs/src/components/mainLayout/styles/indexStyles.ts +++ b/shesha-reactjs/src/components/mainLayout/styles/indexStyles.ts @@ -89,6 +89,15 @@ export const GlobalSheshaStyles = createGlobalStyle` } } + .sha-form-settings-editor .ant-form-item .ant-row.ant-form-item-row { + margin-bottom: 0px !important; + .ant-form-item-control-input{ + display: inline !important; + } + } + + + .sha-form-designer { .sha-index-toolbar { max-height: unset !important; @@ -122,6 +131,12 @@ export const GlobalSheshaStyles = createGlobalStyle` body { margin: 0; + div { + .ant-tabs-dropdown { + --ant-tabs-z-index-popup: 2000 !important; + z-index: 2000 !important; + } + } } [hidden] { @@ -139,4 +154,15 @@ export const GlobalSheshaStyles = createGlobalStyle` font-family: ${(p) => p.theme.fontFamily}; line-height: 1; } + .sha-toolbar-btn-configurable { + display: flex; + align-items: center; + span { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + } `; diff --git a/shesha-reactjs/src/components/mainLayout/styles/styles.ts b/shesha-reactjs/src/components/mainLayout/styles/styles.ts index 60c58d5f57..71b13a5232 100644 --- a/shesha-reactjs/src/components/mainLayout/styles/styles.ts +++ b/shesha-reactjs/src/components/mainLayout/styles/styles.ts @@ -156,7 +156,7 @@ export const useStyles = createStyles(({ css, cx, responsive, token, prefixCls } .${prefixCls}-layout-sider-children { width: ${shaSiderExpandedWidth} !important; - +z .logo { margin-top: ${layoutTriggerHeight} !important; // It should use a config } diff --git a/shesha-reactjs/src/components/modelConfigurator/permissionEditor/index.tsx b/shesha-reactjs/src/components/modelConfigurator/permissionEditor/index.tsx index 22008b527a..1710df1f07 100644 --- a/shesha-reactjs/src/components/modelConfigurator/permissionEditor/index.tsx +++ b/shesha-reactjs/src/components/modelConfigurator/permissionEditor/index.tsx @@ -2,7 +2,6 @@ import { ConfigurableForm } from '@/components/configurableForm'; import permissionSettingsMarkup from '../permissionSettings.json'; import React, { FC } from 'react'; import { Form } from 'antd'; -import { FormMarkup } from '@/providers/form/models'; import { isEqual } from 'lodash'; import { PermissionDto } from '@/apis/permission'; @@ -22,7 +21,7 @@ const PermissionEditor: FC = (props) => { labelCol={{ span: 6 }} wrapperCol={{ span: 18 }} mode="edit" - markup={permissionSettingsMarkup as FormMarkup} + markup={permissionSettingsMarkup as any} // convert to any for using JS settings initialValues={props?.value} onValuesChange={(v) => { const d = { ...props?.value, ...v }; diff --git a/shesha-reactjs/src/components/modelConfigurator/permissionSettings.json b/shesha-reactjs/src/components/modelConfigurator/permissionSettings.json index 7c63e2c03d..d1ed35d32e 100644 --- a/shesha-reactjs/src/components/modelConfigurator/permissionSettings.json +++ b/shesha-reactjs/src/components/modelConfigurator/permissionSettings.json @@ -1,212 +1,296 @@ { - "components": [ - { - "id": "XfBj2SDuEkTf-SbtWciWo", - "type": "container", - "propertyName": "container6", - "label": "Container6", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "direction": "vertical", - "justifyContent": "left", - "components": [ - { - "textType": "text", - "id": "U4jgpgbofzzQW3_njc5wZ", - "type": "textField", - "propertyName": "dependency", - "label": "Depends on", - "labelAlign": "right", - "hidden": false, - "isDynamic": false, - "maxLength": null, - "parentId": "XfBj2SDuEkTf-SbtWciWo", - "placeholder": "", - "description": "", - "prefix": "", - "suffix": "", - "initialValue": "", - "disabled": true, - "readOnly": true, - "validate": { - "minLength": "", - "maxLength": "", - "message": "" - }, - "quickviewWidth": "", - "settingsValidationErrors": [] + "components": [ + { + "id": "XfBj2SDuEkTf-SbtWciWo", + "type": "container", + "propertyName": "container6", + "label": "Container6", + "labelAlign": "right", + "parentId": "root", + "hidden": false, + "isDynamic": false, + "editMode": { + "_code": "return !getSettingValue(data.hardcoded);", + "_mode": "code", + "_value": true + }, + "direction": "vertical", + "justifyContent": "left", + "components": [ + { + "alertType": "info", + "id": "sR6cp2tuDImKtgf0Od_1ECzUy-jkSN", + "type": "alert", + "propertyName": "alert1", + "componentName": "alert1", + "label": "Alert1", + "labelAlign": "right", + "parentId": "XfBj2SDuEkTf-SbtWciWo", + "hidden": { + "_mode": "code", + "_code": "return !getSettingValue(data.hardcoded);", + "_value": false }, - { - "textType": "text", - "id": "C-F9oF99z_LKSfSG4-TdG", - "type": "textField", - "propertyName": "category", - "label": "Category", - "labelAlign": "right", - "hidden": {"_code": "return getSettingValue(data.parent);", "_mode": "value", "_value": true}, - "isDynamic": false, - "maxLength": null, - "parentId": "XfBj2SDuEkTf-SbtWciWo", - "placeholder": "", - "description": "", - "prefix": "", - "suffix": "", - "initialValue": "", - "validate": { - "minLength": "", - "maxLength": "", - "message": "" - }, - "quickviewWidth": "", - "settingsValidationErrors": [] + "isDynamic": false, + "version": 2, + "text": "This permission is hardcoded on the backend and cannot be changed" + }, + { + "textType": "text", + "id": "U4jgpgbofzzQW3_njc5wZ", + "type": "textField", + "propertyName": "dependency", + "label": "Depends on", + "labelAlign": "right", + "hidden": false, + "isDynamic": false, + "maxLength": null, + "parentId": "XfBj2SDuEkTf-SbtWciWo", + "placeholder": "", + "description": "", + "prefix": "", + "suffix": "", + "initialValue": "", + "disabled": true, + "readOnly": true, + "validate": { + "minLength": "", + "maxLength": "", + "message": "" }, - { - "id": "j0EAgLiqqB7QdHW-QUJqI", - "type": "dropdown", - "propertyName": "access", - "label": "Access", - "labelAlign": "right", - "parentId": "XfBj2SDuEkTf-SbtWciWo", - "hidden": false, - "isDynamic": false, - "dataSourceType": "values", - "placeholder": "", - "description": "", - "defaultValue": null, - "entityTypeShortAlias": "", - "dataSourceUrl": "", - "validate": {}, - "values": [ - { - "id": "EVQV8Kf_dyUYwd56cb_b8", - "label": "Disabled", - "value": "1" - }, - { - "id": "jbKpqNqiNIKXme_m2Yt4s", - "label": "Inherited", - "value": "2" - }, - { - "id": "EwBWTiQ1biRoO0_MVzq1z", - "label": "Any authenticated", - "value": "3" - }, - { - "id": "w0euzBU5EpNSzFGzXm568", - "label": "Requires permissions", - "value": "4" - }, - { - "id": "TOLZuIjSNfnaruUeeY9AM", - "label": "Allow anonymous", - "value": "5" - } - ], - "settingsValidationErrors": [] + "quickviewWidth": "", + "settingsValidationErrors": [], + "version": 5, + "editMode": "readOnly", + "desktop": {}, + "tablet": {}, + "mobile": {} + }, + { + "textType": "text", + "id": "C-F9oF99z_LKSfSG4-TdG", + "type": "textField", + "propertyName": "category", + "label": "Category", + "labelAlign": "right", + "hidden": { + "_code": "return getSettingValue(data.parent);", + "_mode": "value", + "_value": true }, - { - "id": "WXaDzSHBAKzkbO18zDotx", - "type": "permissionAutocomplete", - "propertyName": "permissions", - "label": "Permissions", - "labelAlign": "right", - "parentId": "XfBj2SDuEkTf-SbtWciWo", - "hidden": {"_code": "return getSettingValue(data?.access) !== 4;", "_mode": "code", "_value": false}, - "isDynamic": false, - "autocompleteUrl": "/api/services/app/permission/autocomplete", - "placeholder": "", - "validate": {}, - "version": 1, - "settingsValidationErrors": [] + "isDynamic": false, + "maxLength": null, + "parentId": "XfBj2SDuEkTf-SbtWciWo", + "placeholder": "", + "description": "", + "prefix": "", + "suffix": "", + "initialValue": "", + "validate": { + "minLength": "", + "maxLength": "", + "message": "" }, - { - "id": "V4ZLolRl8UZK1HHiNvYO_", - "type": "autocompleteTagGroup", - "propertyName": "actualPermissions", - "label": "Actual permissions", - "labelAlign": "right", - "parentId": "XfBj2SDuEkTf-SbtWciWo", - "hidden": {"_code": "return getSettingValue(data?.access) !== 2 || getSettingValue(data?.actualAccess) !== 4;", "_mode": "code", "_value": false}, - "isDynamic": false, - "disabled": true, - "validate": {}, - "autocompleteUrl": "", - "placeholder": "", - "settingsValidationErrors": [] + "quickviewWidth": "", + "settingsValidationErrors": [], + "version": 5, + "desktop": {}, + "tablet": {}, + "mobile": {} + }, + { + "id": "j0EAgLiqqB7QdHW-QUJqI", + "type": "dropdown", + "propertyName": "access", + "label": "Access", + "labelAlign": "right", + "parentId": "XfBj2SDuEkTf-SbtWciWo", + "hidden": false, + "isDynamic": false, + "dataSourceType": "values", + "placeholder": "", + "description": "", + "defaultValue": null, + "entityTypeShortAlias": "", + "dataSourceUrl": "", + "validate": {}, + "values": [ + { + "id": "EVQV8Kf_dyUYwd56cb_b8", + "label": "Disabled", + "value": "1" + }, + { + "id": "jbKpqNqiNIKXme_m2Yt4s", + "label": "Inherited", + "value": "2" + }, + { + "id": "EwBWTiQ1biRoO0_MVzq1z", + "label": "Any authenticated", + "value": "3" + }, + { + "id": "w0euzBU5EpNSzFGzXm568", + "label": "Requires permissions", + "value": "4" + }, + { + "id": "TOLZuIjSNfnaruUeeY9AM", + "label": "Allow anonymous", + "value": "5" + } + ], + "settingsValidationErrors": [], + "version": 7, + "useRawValues": false, + "referenceListId": null, + "valueFormat": "listItem", + "desktop": {}, + "tablet": {}, + "mobile": {} + }, + { + "id": "WXaDzSHBAKzkbO18zDotx", + "type": "permissionAutocomplete", + "propertyName": "permissions", + "label": "Permissions", + "labelAlign": "right", + "parentId": "XfBj2SDuEkTf-SbtWciWo", + "hidden": { + "_code": "return getSettingValue(data?.access) !== 4;", + "_mode": "code", + "_value": false }, - { - "id": "8YKNrq6D42G1Lzrgoizyn", - "type": "dropdown", - "propertyName": "actualAccess", - "label": "Actual access", - "labelAlign": "right", - "parentId": "XfBj2SDuEkTf-SbtWciWo", - "hidden": {"_code": "return !(getSettingValue(data?.access) === 2 && getSettingValue(data?.actualAccess) !== 2 && getSettingValue(data?.actualAccess) !== 4);", "_mode": "code", "_value": false}, - "isDynamic": false, - "dataSourceType": "values", - "placeholder": "", - "description": "", - "disabled": true, - "readOnly": true, - "defaultValue": "", - "entityTypeShortAlias": "", - "dataSourceUrl": "", - "validate": {}, - "values": [ - { - "id": "-R48wPdyuhwtHUuqXA9GZ", - "label": "Disable", - "value": "1" - }, - { - "id": "epifkpaKzi1ph2miyO4bV", - "label": "Inherited", - "value": "2" - }, - { - "id": "Yf-hXzCAaQZw2j-EepLl_", - "label": "Any authenticated", - "value": "3" - }, - { - "id": "bd3Z_Pig0ARstcxdt3cKc", - "label": "Requires permissions", - "value": "4" - }, - { - "id": "poxaZGdF6-RgAKqNBRGJG", - "label": "Allow anonymous", - "value": "5" - } - ], - "settingsValidationErrors": [] - } - ] - } - ], - "formSettings": { - "_formFields": [ "modelType", "postUrl", "putUrl", "deleteUrl", "getUrl", "layout", "size", "colon", "labelCol.span", "wrapperCol.span", "excludeFormFieldsInPayload", "formKeysToPersist", "uniqueFormId", "preparedValues", "onDataLoaded", "onUpdate", "initialValues" ], - "layout": "horizontal", - "colon": true, - "labelCol": { "span": 8 }, - "wrapperCol": { "span": 16 }, - "modelType": "Shesha.Permissions.PermissionedObjectDto", - "postUrl": "", - "putUrl": "", - "deleteUrl": "", - "getUrl": "", - "uniqueFormId": "1", - "onInitialize": "formMode = 'readonly';", + "isDynamic": false, + "autocompleteUrl": "/api/services/app/permission/autocomplete", + "placeholder": "", + "validate": {}, + "version": 1, + "settingsValidationErrors": [] + }, + { + "id": "V4ZLolRl8UZK1HHiNvYO_", + "type": "autocompleteTagGroup", + "propertyName": "actualPermissions", + "label": "Actual permissions", + "labelAlign": "right", + "parentId": "XfBj2SDuEkTf-SbtWciWo", + "hidden": { + "_code": "return getSettingValue(data?.access) !== 2 || getSettingValue(data?.actualAccess) !== 4;", + "_mode": "code", + "_value": false + }, + "isDynamic": false, + "disabled": true, + "validate": {}, + "autocompleteUrl": "", + "placeholder": "", + "settingsValidationErrors": [], + "version": 2 + }, + { + "id": "8YKNrq6D42G1Lzrgoizyn", + "type": "dropdown", + "propertyName": "actualAccess", + "label": "Actual access", + "labelAlign": "right", + "parentId": "XfBj2SDuEkTf-SbtWciWo", + "hidden": { + "_code": "return !(getSettingValue(data?.access) === 2 && getSettingValue(data?.actualAccess) !== 2 && getSettingValue(data?.actualAccess) !== 4);", + "_mode": "code", + "_value": false + }, + "isDynamic": false, + "dataSourceType": "values", + "placeholder": "", + "description": "", + "disabled": true, + "readOnly": true, + "defaultValue": "", + "entityTypeShortAlias": "", + "dataSourceUrl": "", + "validate": {}, + "values": [ + { + "id": "-R48wPdyuhwtHUuqXA9GZ", + "label": "Disable", + "value": "1" + }, + { + "id": "epifkpaKzi1ph2miyO4bV", + "label": "Inherited", + "value": "2" + }, + { + "id": "Yf-hXzCAaQZw2j-EepLl_", + "label": "Any authenticated", + "value": "3" + }, + { + "id": "bd3Z_Pig0ARstcxdt3cKc", + "label": "Requires permissions", + "value": "4" + }, + { + "id": "poxaZGdF6-RgAKqNBRGJG", + "label": "Allow anonymous", + "value": "5" + } + ], + "settingsValidationErrors": [], + "version": 7, + "useRawValues": false, + "referenceListId": null, + "editMode": "readOnly", + "valueFormat": "listItem", + "desktop": {}, + "tablet": {}, + "mobile": {} + } + ], + "version": 6, + "flexWrap": "wrap", + "desktop": {}, + "tablet": {}, + "mobile": {}, + "shadowStyle": "none" + } + ], + "formSettings": { + "_formFields": [ "modelType", "postUrl", "putUrl", "deleteUrl", "getUrl", "layout", "size", "colon", "labelCol.span", "wrapperCol.span", "excludeFormFieldsInPayload", "formKeysToPersist", "uniqueFormId", "preparedValues", "onDataLoaded", "onUpdate", "initialValues" ], + "layout": "horizontal", + "colon": true, + "labelCol": { "span": 8 }, + "wrapperCol": { "span": 16 }, + "modelType": "Shesha.Permissions.PermissionedObjectDto", + "uniqueFormId": "1", + "onInitialize": "formMode = 'readonly';", + "displayName": "DEFAULT_FORM_SETTINGS", + "__docgenInfo": { + "description": "Default form settings", "displayName": "DEFAULT_FORM_SETTINGS", - "__docgenInfo": { - "description": "Default form settings", - "displayName": "DEFAULT_FORM_SETTINGS", - "props": {} - }, - "fieldsToFetch": [], - "formKeysToPersist": [] + "props": {} + }, + "formKeysToPersist": [], + "version": 6, + "dataLoaderType": "gql", + "dataSubmitterType": "gql", + "onBeforeDataLoad": " form.setFieldsValue({...form.formArguments});", + "onAfterDataLoad": null, + "onPrepareSubmitData": null, + "onBeforeSubmit": null, + "onSubmitSuccess": null, + "onSubmitFailed": null, + "dataSubmittersSettings": { + "gql": { + "endpointType": "default", + "dynamicEndpoint": " return data?.id ? form.defaultApiEndpoints.update : form.defaultApiEndpoints.create" + } + }, + "dataLoadersSettings": { + "gql": { "endpointType": "default" }, + "custom": {} } - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer-new/entityProperty.tsx b/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer-new/entityProperty.tsx index d772237235..f45e404d1e 100644 --- a/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer-new/entityProperty.tsx +++ b/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer-new/entityProperty.tsx @@ -19,7 +19,7 @@ export const EntityProperty: FC = props => {
{props.suppress && } {icon && } - {props.name} {props.label && <>({props.label})}: {props.entityType ?? 'udefined'} + {props.name} {props.label && <>({props.label})}: {props.entityType ?? 'undefined'} {props.description && ( diff --git a/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer-new/jsonProperty.tsx b/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer-new/jsonProperty.tsx index c0b109a86d..a5fc75e104 100644 --- a/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer-new/jsonProperty.tsx +++ b/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer-new/jsonProperty.tsx @@ -16,7 +16,7 @@ export const JsonProperty: FC = props => {
{props.suppress && } - {props.name} : {props.entityType ?? 'udefined'} + {props.name} : {props.entityType ?? 'undefined'} {props.description && ( diff --git a/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/arrayEntityProperty.tsx b/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/arrayEntityProperty.tsx index 488c2dab82..5e8d63d5b4 100644 --- a/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/arrayEntityProperty.tsx +++ b/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/arrayEntityProperty.tsx @@ -29,7 +29,7 @@ export const ArrayEntityProperty: FC = props => { {props.suppress && } {icon && } - {props.name} {props.label && <>({props.label})}: Array{'<' + (listType ?? 'udefined') + '>'} + {props.name} {props.label && <>({props.label})}: Array{'<' + (listType ?? 'undefined') + '>'} {props.description && ( diff --git a/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/entityProperty.tsx b/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/entityProperty.tsx index f6b2e2f46b..fab0d405ea 100644 --- a/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/entityProperty.tsx +++ b/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/entityProperty.tsx @@ -25,7 +25,7 @@ export const EntityProperty: FC = props => { {props.suppress && } {icon && } - {props.name} {props.label && <>({props.label})}: {props.entityType ?? 'udefined' } + {props.name} {props.label && <>({props.label})}: {props.entityType ?? 'undefined' } {props.description && ( diff --git a/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/jsonProperty.tsx b/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/jsonProperty.tsx index d72125cff5..c8bc99ba93 100644 --- a/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/jsonProperty.tsx +++ b/shesha-reactjs/src/components/modelConfigurator/propertiesEditor/renderer/jsonProperty.tsx @@ -22,7 +22,7 @@ export const JsonProperty: FC = props => { {props.suppress && } - {props.name} : {props.entityType ?? 'udefined' } + {props.name} : {props.entityType ?? 'undefined' } {props.description && ( diff --git a/shesha-reactjs/src/components/notesRenderer/index.tsx b/shesha-reactjs/src/components/notesRenderer/index.tsx index a2c8d412b9..96e24c3017 100644 --- a/shesha-reactjs/src/components/notesRenderer/index.tsx +++ b/shesha-reactjs/src/components/notesRenderer/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, CSSProperties } from 'react'; +import React, { FC, CSSProperties, useEffect } from 'react'; import { useNotes } from '@/providers'; import NotesRendererBase from '@/components/notesRendererBase'; import { useStyles } from './styles/styles'; @@ -11,14 +11,22 @@ export interface INotesRendererProps { buttonPostion?: 'left' | 'right'; autoSize?: boolean; allowDelete?: boolean; + onCreated?: (payload: Array) => void; } -export const NotesRenderer: FC = ({ autoSize, buttonPostion, showCommentBox = true, allowDelete }) => { +export const NotesRenderer: FC = ({ autoSize, buttonPostion, showCommentBox = true, allowDelete, onCreated }) => { const { notes, deleteNotes, isInProgress, postNotes } = useNotes(); const { styles } = useStyles(); const { fetchNotes: isFetchingNotes, postNotes: isPostingNotes } = isInProgress; + useEffect(() => { + if (onCreated) { + onCreated(notes); + } + }, [isInProgress, notes]); + + return (
= ({ }} + locale={{ emptyText: }} className={`${styles.commentList} scroll scroll-y`} style={{ ...commentListStyles }} itemLayout="horizontal" diff --git a/shesha-reactjs/src/components/panel/index.tsx b/shesha-reactjs/src/components/panel/index.tsx index a0097945c0..f5880f71ab 100644 --- a/shesha-reactjs/src/components/panel/index.tsx +++ b/shesha-reactjs/src/components/panel/index.tsx @@ -1,20 +1,17 @@ import React, { FC } from 'react'; -import { Collapse, Skeleton, theme } from 'antd'; +import { Collapse, Skeleton } from 'antd'; import { CollapseProps } from 'antd/lib/collapse'; import classNames from 'classnames'; -import styled from 'styled-components'; +import { IStyleType } from '@/index'; import { useStyles } from './styles/styles'; -const { useToken } = theme; - export type headerType = 'parent' | 'child' | 'default'; -export interface ICollapsiblePanelProps extends CollapseProps { +export interface ICollapsiblePanelProps extends CollapseProps, Omit { isActive?: boolean; header?: React.ReactNode; className?: string; extraClassName?: string; - style?: React.CSSProperties; showArrow?: boolean; forceRender?: boolean; extra?: React.ReactNode; @@ -30,8 +27,28 @@ export interface ICollapsiblePanelProps extends CollapseProps { primaryColor?: string; dynamicBorderRadius?: number; panelHeadType?: headerType; + headerStyles?: IStyleType; + bodyStyle?: React.CSSProperties; + headerStyle?: React.CSSProperties; + accentStyle?: boolean; + overflowStyle?: React.CSSProperties; } +const defaultHeaderStyle: React.CSSProperties = { + backgroundColor: 'transparent', + paddingLeft: '16px', + paddingRight: '16px', + paddingBottom: '8px', + paddingTop: '8px' +}; + +const defaultBodyStyle: React.CSSProperties = { + paddingLeft: '16px', + paddingBottom: '16px', + paddingTop: '16px', + paddingRight: '16px', + marginBottom: '5px', +}; /** * There was an error * TS4023: Exported variable 'xxx' has or is using name 'zzz' from external module "yyy" but cannot be named. @@ -41,70 +58,41 @@ export interface ICollapsiblePanelProps extends CollapseProps { * */ -const StyledCollapse: any = styled(Collapse) < - Omit ->` - .ant-collapse-header { - visibility: ${({ hideCollapseContent }) => (hideCollapseContent ? 'hidden' : 'visible')}; - border-top: ${({ primaryColor, panelHeadType }) => (panelHeadType === 'parent' ? `3px solid ${primaryColor}` : 'none')}; - border-left: ${({ primaryColor, panelHeadType }) => (panelHeadType === 'child' ? `3px solid ${primaryColor}` : 'none')}; - font-size: ${({ panelHeadType }) => (panelHeadType === 'parent' ? '13px' : '16px')}; - font-weight: 'bold'; - background-color: ${({ headerColor }) => headerColor} !important; - - } - .ant-collapse-content { - .ant-collapse-content-box > .sha-components-container { - background-color: ${({ bodyColor }) => bodyColor}; - } - } -`; - export const CollapsiblePanel: FC> = ({ expandIconPosition = 'end', onChange, header, extra, children, - noContentPadding, loading, className, extraClassName, - style, collapsedByDefault = false, showArrow, collapsible, ghost, - bodyColor = 'unset', - headerColor = 'unset', + bodyStyle = defaultBodyStyle, + headerStyle = defaultHeaderStyle, isSimpleDesign, + noContentPadding, + hideWhenEmpty, hideCollapseContent, - hideWhenEmpty = false, - panelHeadType = 'default', - dynamicBorderRadius, - + accentStyle, + overflowStyle }) => { // Prevent the CollapsiblePanel from collapsing every time you click anywhere on the extra and header const onContainerClick = (event: React.MouseEvent) => event?.stopPropagation(); - const { styles } = useStyles({ borderRadius: dynamicBorderRadius }); - const { token } = useToken(); - - const shaCollapsiblePanelStyle = isSimpleDesign ? {} : styles.shaCollapsiblePanel; + const { styles } = useStyles({ bodyStyle, headerStyle, ghost, isSimpleDesign, noContentPadding, hideWhenEmpty, hideCollapseContent, accentStyle, overflow: overflowStyle }); + const shaCollapsiblePanelStyle = isSimpleDesign ? styles.shaSimpleDesign : styles.shaCollapsiblePanel; return ( - { - const extraMargin = "28px"; - +export const useStyles = createStyles(({ css, cx, token, prefixCls }, { + headerStyle = {} as CSSProperties, + bodyStyle = {} as CSSProperties, + hideCollapseContent, + isSimpleDesign, + ghost, + accentStyle, + overflow +}) => { const noContentPadding = "no-content-padding"; const hideWhenEmpty = "hide-empty"; - const shaCollapsiblePanel = cx("sha-collapsible-panel", css` + const { + borderWidth, + borderStyle, + borderColor, + borderTopWidth, + borderTopStyle, + borderTopColor, + borderBottomWidth, + borderBottomStyle, + borderBottomColor, + borderRightWidth, + borderRightStyle, + borderRightColor, + borderLeftWidth, + borderLeftStyle, + borderLeftColor, + backgroundColor, + backgroundImage, + backgroundSize, + backgroundPosition, + backgroundRepeat, + boxShadow, + width, + height, + minWidth, + minHeight, + maxWidth, + maxHeight, + borderTopLeftRadius = '8px', + borderTopRightRadius = '8px', + borderBottomLeftRadius = '8px', + borderBottomRightRadius = '8px', + marginBottom, + marginTop, + marginLeft, + marginRight, + paddingTop, + paddingBottom, + paddingLeft, + paddingRight + } = bodyStyle; + + const { + backgroundImage: headerBgImage, + backgroundColor: headerBgColor, + height: headerHeight, + minHeight: headerMinHeight, + maxHeight: headerMaxHeight, + color: headerColor = token.colorTextLabel, + fontFamily, + textAlign, + fontSize, + fontWeight, + borderWidth: headerBorderWidth, + borderStyle: headerBorderStyle, + borderColor: headerBorderColor, + borderTopWidth: headerBorderTopWidth, + borderTopStyle: headerBorderTopStyle, + borderTopColor: headerBorderTopColor, + borderBottomWidth: headerBorderBottomWidth, + borderBottomStyle: headerBorderBottomStyle, + borderBottomColor: headerBorderBottomColor, + borderRightWidth: headerBorderRightWidth, + borderRightStyle: headerBorderRightStyle, + borderRightColor: headerBorderRightColor, + borderLeftWidth: headerBorderLeftWidth, + borderLeftStyle: headerBorderLeftStyle, + borderLeftColor: headerBorderLeftColor, + borderRadius: headerBorderRadius, + paddingBottom: headerPaddingBottom = '8px', + paddingTop: headerPaddingTop = '8px', + paddingLeft: headerPaddingLeft = '0px', + paddingRight: headerPaddingRight = '8px', + overflow: headerOverflow, + ...headerRest + } = headerStyle; + + const hasBorder = borderWidth || borderTopWidth || borderBottomWidth || borderLeftWidth || borderRightWidth; + + const shaCollapsiblePanel = cx("ant-collapse-component", css` &.${hideWhenEmpty}:not(:has(.${prefixCls}-collapse-content .${prefixCls}-form-item:not(.${prefixCls}-form-item-hidden))) { display: none; } + --primary-color: ${token.colorPrimary}; + --ant-line-width: ${hasBorder ? '0px' : '1px'} !important; + ${hasBorder && '--ant-line-width: 0px !important;'} + --ant-collapse-header-bg: transparent !important; - &.${prefixCls}-collapse-icon-position-left { - .${prefixCls}-collapse-header-text { - margin-left: ${extraMargin}; - } - } - - &.${prefixCls}-collapse-icon-position-right { - .${prefixCls}-collapse-extra { - margin-right: ${extraMargin}; - } + > .ant-collapse-item { + display: flex; + flex-direction: column; + box-shadow: ${boxShadow}; + border-radius: ${borderTopLeftRadius} ${borderTopRightRadius} ${borderBottomRightRadius} ${borderBottomLeftRadius} !important; + margin-bottom: ${marginBottom}; + margin-top: ${marginTop}; + margin-left: ${marginLeft}; + margin-right: ${marginRight}; + } + + > .ant-collapse-item > .ant-collapse-content { + width: ${width}; + min-width: ${minWidth}; + max-width: ${maxWidth}; + height: ${height}; + min-height: ${minHeight}; + max-height: ${maxHeight}; + background: ${backgroundImage || backgroundColor}; + background-size: ${backgroundSize}; + background-position: ${backgroundPosition}; + background-repeat: ${backgroundRepeat}; + position: relative; + padding-top: ${paddingTop} !important; + padding-bottom: ${paddingBottom} !important; + padding-left: ${paddingLeft} !important; + padding-right: ${paddingRight} !important; + border-radius : 0px 0px ${borderBottomRightRadius} ${borderBottomLeftRadius} !important; + border-top: ${ghost ? 'none' : borderTopWidth || borderWidth} ${borderTopStyle || borderStyle} ${borderTopColor || borderColor}; + border-right: ${ghost ? 'none' : borderRightWidth || borderWidth} ${borderRightStyle || borderStyle} ${borderRightColor || borderColor}; + border-left: ${ghost ? 'none' : borderLeftWidth || borderWidth} ${borderLeftStyle || borderStyle} ${borderLeftColor || borderColor}; + border-bottom: ${ghost ? 'none' : borderBottomWidth || borderWidth} ${borderBottomStyle || borderStyle} ${borderBottomColor || borderColor}; + + > .ant-collapse-content-box { + --ant-collapse-content-padding: 0px !important; + padding: 0px !important; + height: 100%; + width: 100%; + overflow: ${overflow?.overflow ?? 'auto'}; + ${overflow}; } + } + + > .ant-collapse-item > .ant-collapse-header[aria-expanded="false"] { + border-radius: ${isSimpleDesign || ghost ? 0 : borderTopLeftRadius} ${isSimpleDesign || ghost ? 0 : borderTopRightRadius} ${isSimpleDesign || ghost ? 0 : borderBottomRightRadius} ${isSimpleDesign || ghost ? 0 : borderBottomLeftRadius} !important; + } - &.${noContentPadding} { - .${prefixCls}-collapse-content-box { - padding: unset; - padding-top: 16px; - } + > .ant-collapse-item > .ant-collapse-header[aria-expanded="true"] { + border-radius : ${isSimpleDesign || ghost ? '0px' : borderTopLeftRadius ?? '0px'} ${isSimpleDesign || ghost ? '0px' : borderTopRightRadius ?? '0px'} 0px 0px !important; + } + + > .ant-collapse-item > .ant-collapse-header { + ${headerRest} + position: relative; + visibility: ${hideCollapseContent ? 'hidden' : 'visible'}; + background: ${headerBgImage || headerBgColor}; + width: ${width}; + min-width: ${minWidth}; + max-width: ${maxWidth}; + height: ${headerHeight}; + min-height: ${headerMinHeight}; + max-height: ${headerMaxHeight}; + border-top: ${accentStyle ? + `3px` : headerBorderTopWidth || headerBorderWidth} ${headerBorderTopStyle || headerBorderStyle} ${accentStyle ? + 'var(--primary-color)' : headerBorderTopColor || headerBorderColor}; + border-right: ${headerBorderRightWidth || headerBorderWidth} ${headerBorderRightStyle || headerBorderStyle} ${headerBorderRightColor || headerBorderColor}; + border-left: ${headerBorderLeftWidth || headerBorderWidth} ${headerBorderLeftStyle || headerBorderStyle} ${headerBorderLeftColor || headerBorderColor}; + border-bottom: ${headerBorderBottomWidth || headerBorderWidth} ${headerBorderBottomStyle || headerBorderStyle} ${headerBorderBottomColor || headerBorderColor}; + padding-top: ${headerPaddingTop} !important; + padding-right: ${headerPaddingRight} !important; + padding-bottom: ${headerPaddingBottom} !important; + padding-left: ${headerPaddingLeft} !important; + border-radius: ${borderTopLeftRadius} ${borderTopRightRadius}; + + .ant-collapse-header-text { + color: ${headerColor}; + font-family: ${fontFamily}; + text-align: ${textAlign}; + font-size: ${fontSize}; + font-weight: ${fontWeight}; + align-self: center; } - - &:not(.${prefixCls}-collapse-ghost) { - > .${prefixCls}-collapse-item { - > .${prefixCls}-collapse-header { - border-top-left-radius: ${borderRadius ?? token.borderRadiusLG}px; - border-top-right-radius: ${borderRadius ?? token.borderRadiusLG}px; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - background-color: #f0f0f0; - margin:'auto 0px'; - min-height: 50px; - height: auto; - width: auto; - padding: 0; - padding-left:10px; - padding-top:5px; - } - } + + .ant-collapse-extra { + align-self: center; } - - &.${prefixCls}-collapse-ghost { - > .${prefixCls}-collapse-item { - > .${prefixCls}-collapse-header { - padding: 4px 0px; - border-bottom: 2px solid ${token.colorPrimary}; - border-bottom-left-radius: unset; - border-bottom-right-radius: unset; - } - > .${prefixCls}-collapse-content { - > .${prefixCls}-collapse-content-box { - padding: 5px 0; - } - } - } + + .ant-collapse-expand-icon { + align-self: center; + margin-right: 8px; } - - .${prefixCls}-collapse-item { - &.${prefixCls}-collapse-item-active { - } - - .${prefixCls}-collapse-header { - min-height: 25px; - font-size: 15px; - font-weight: 500; - position: relative; - display: flex; - justify-content: space-between; - align-items: center; - .${prefixCls}-collapse-extra { - float: unset; - } - - .${prefixCls}-collapse-header-text { - flex-grow: 1; - margin: auto 0; - margin-left: -5px; - } + } + + &.${prefixCls}-collapse-ghost { + > .ant-collapse-item { + > .ant-collapse-header { + --ant-collapse-header-padding: 5px 0px !important; + border-bottom-left-radius: unset; + border-bottom-right-radius: unset; + border: none; + ${accentStyle && `border-bottom: 2px solid ${token.colorPrimary};`} + ${accentStyle && `border-top: 3px solid var(--primary-color);`} + font-weight: ${fontWeight || '500'}; + } - - .${prefixCls}-collapse-content { - border-top: unset; - background: white; - - &.${noContentPadding} { - padding: unset; - } - - &.${prefixCls}-collapse-content-active { + > .ant-collapse-content { + border: none; + > .ant-collapse-content-box { + padding: 5px 0; } } } - - .${prefixCls}-collapse-header-text { - font-size: 14px; - min-height: 30px; - line-height: 2; - } - - .${prefixCls}-blend-btn { - border: none; - height: 24px; + } + `); + + const shaSimpleDesign = cx(css` + --primary-color: ${token.colorPrimary}; + + > .ant-collapse-item > .ant-collapse-header-text { + color: ${headerColor}; + font-family: ${fontFamily}; + text-align: ${textAlign}; + font-size: ${fontSize}; + font-weight: ${fontWeight}; + } + + &.${prefixCls}-collapse-ghost { + > .ant-collapse-item { + > .ant-collapse-header { + --ant-collapse-header-padding: ${headerStyle?.padding || '12px 16px'} !important; + padding: 12px 16px !important; + font-size: 14px; + } } - - .${prefixCls}-collapse-arrow { - padding-top: 3px !important; - margin-top: -3px !important; - } - .ant-collapse-expand-icon{ - margin-right:10px; - } - `); + } + + > .ant-collapse-item > .${prefixCls}-collapse-content-box { + padding: 5px 0; + width: ${width}; + min-width: ${minWidth}; + max-width: ${maxWidth}; + height: max-content; + min-height: ${minHeight}; + max-height: ${maxHeight}; + overflow: ${overflow ?? 'auto'}; + padding-top: ${paddingTop} !important; + padding-bottom: ${paddingBottom} !important; + padding-left: ${paddingLeft} !important; + padding-right: ${paddingRight} !important; + } + + > .ant-collapse-item > .ant-collapse-header { + visibility: ${hideCollapseContent ? 'hidden' : 'visible'}; + ${accentStyle && `border-top: 3px solid var(--primary-color);`} + font-size: 14px; + height: ${headerHeight}; + min-height: ${headerMinHeight}; + max-height: ${headerMaxHeight} + width: ${width}; + min-width: ${minWidth}; + max-width: ${maxWidth}; + } + `); return { shaCollapsiblePanel, noContentPadding, hideWhenEmpty, + shaSimpleDesign }; }); \ No newline at end of file diff --git a/shesha-reactjs/src/components/passwordInputCombo/index.tsx b/shesha-reactjs/src/components/passwordInputCombo/index.tsx index 1fe6c82022..2644a3e82f 100644 --- a/shesha-reactjs/src/components/passwordInputCombo/index.tsx +++ b/shesha-reactjs/src/components/passwordInputCombo/index.tsx @@ -26,6 +26,7 @@ interface IProps { readonly formProps?: FormProps; readonly formItemProps?: FormItemProps; readonly formItemConfirmProps?: FormItemProps; + readonly style?: React.CSSProperties; } const FormItem = Form.Item; @@ -45,6 +46,7 @@ const PasswordInputCombo: FC = ({ formProps, formItemProps, formItemConfirmProps, + style }) => { useEffect(() => isPasswordOk(isSamePassword(newPassword, repeatPassword, passwordLength)), [ newPassword, @@ -65,7 +67,7 @@ const PasswordInputCombo: FC = ({ trigger="focus" > - + @@ -82,6 +84,7 @@ const PasswordInputCombo: FC = ({ {...inputProps} placeholder={confirmPlaceholder} value={repeatPassword} + style={style} onChange={onConfirmPasswordChange} /> diff --git a/shesha-reactjs/src/components/permissionAutocomplete/index.tsx b/shesha-reactjs/src/components/permissionAutocomplete/index.tsx index 2991300d9b..091e8cea5c 100644 --- a/shesha-reactjs/src/components/permissionAutocomplete/index.tsx +++ b/shesha-reactjs/src/components/permissionAutocomplete/index.tsx @@ -1,16 +1,18 @@ import React, { FC, useState } from 'react'; import { Select } from 'antd'; import { PermissionsTree } from '@/components/permissionsTree'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; export interface IPermissionAutocompleteProps { onChange?: (value: string[]) => void; value?: string[]; readOnly?: boolean; + size?: SizeType; } export const PermissionAutocomplete: FC = (props) => { - const { onChange, value, readOnly } = props; + const { onChange, value, readOnly, size } = props; const [searchText, setSearchText] = useState(''); @@ -23,19 +25,19 @@ export const PermissionAutocomplete: FC = (props) return ( ; + } + + return ( + | CustomLabeledValue[]> + {...commonSelectProps} + style={{ ...style }} + showSearch mode={mode} + {...(displayStyle === 'tags' ? { + labelRender: (props) => { + const option = options.find((o) => o.value === props.value); + return ; + } + } : {})} > - {options?.map(({ value: localValue, label, data, disabled }, index) => ( - + {options?.map(({ value: localValue, label, data, disabled }) => ( + {label} ))} diff --git a/shesha-reactjs/src/components/refListDropDown/models.ts b/shesha-reactjs/src/components/refListDropDown/models.ts index 1153730dbb..761ea98429 100644 --- a/shesha-reactjs/src/components/refListDropDown/models.ts +++ b/shesha-reactjs/src/components/refListDropDown/models.ts @@ -36,11 +36,16 @@ export interface IRefListDropDownProps extends Omit void; } @@ -56,6 +61,9 @@ export interface ISelectOption { label: string | React.ReactNode; data: TValue; disabled?: boolean; + color?: string; + icon?: string; + description?: string; } export type CustomLabeledValue = LabeledValue & { data: TValue }; diff --git a/shesha-reactjs/src/components/refListDropDown/reflistTag.tsx b/shesha-reactjs/src/components/refListDropDown/reflistTag.tsx new file mode 100644 index 0000000000..96bb2f9629 --- /dev/null +++ b/shesha-reactjs/src/components/refListDropDown/reflistTag.tsx @@ -0,0 +1,24 @@ +import { getTagStyle } from '@/utils/style'; +import convertCssColorNameToHex from 'convert-css-color-name-to-hex'; +import { Tag, Tooltip, TooltipProps } from 'antd'; +import React from 'react'; +import { Icon } from '../readOnlyDisplayFormItem'; +function ReflistTag({ value, tooltip, color, icon, showIcon, tagStyle, solidColor, showItemName, label, placement = 'right' }) { + + const memoizedColor = solidColor + ? convertCssColorNameToHex(color ?? '') + : color?.toLowerCase(); + + return ( + + } + style={getTagStyle(tagStyle, !!color)} + >{showItemName && label.toUpperCase()} + + ); +}; + +export default ReflistTag; \ No newline at end of file diff --git a/shesha-reactjs/src/providers/refList/options/configurator.tsx b/shesha-reactjs/src/components/refListSelectorDisplay/options/configurator.tsx similarity index 74% rename from shesha-reactjs/src/providers/refList/options/configurator.tsx rename to shesha-reactjs/src/components/refListSelectorDisplay/options/configurator.tsx index 223266bfb9..9afbd6f836 100644 --- a/shesha-reactjs/src/providers/refList/options/configurator.tsx +++ b/shesha-reactjs/src/components/refListSelectorDisplay/options/configurator.tsx @@ -1,8 +1,8 @@ import { Alert } from 'antd'; import React, { FC, ReactNode } from 'react'; -import { useStyles } from './styles/styles'; -import { useRefListItemGroupConfigurator } from '../provider'; -import RefListItemProperties from './properties'; +import { useStyles } from '@/components/refListSelectorDisplay/options/styles/styles'; +import { useRefListItemGroupConfigurator } from '@/components/refListSelectorDisplay/provider'; +import RefListItemProperties from '@/components/refListSelectorDisplay/options/properties'; export interface IRefListItemGroupConfiguratorProps { render?: ReactNode | (() => ReactNode); diff --git a/shesha-reactjs/src/providers/refList/options/dragHandle.tsx b/shesha-reactjs/src/components/refListSelectorDisplay/options/dragHandle.tsx similarity index 100% rename from shesha-reactjs/src/providers/refList/options/dragHandle.tsx rename to shesha-reactjs/src/components/refListSelectorDisplay/options/dragHandle.tsx diff --git a/shesha-reactjs/src/providers/refList/options/modal.tsx b/shesha-reactjs/src/components/refListSelectorDisplay/options/modal.tsx similarity index 99% rename from shesha-reactjs/src/providers/refList/options/modal.tsx rename to shesha-reactjs/src/components/refListSelectorDisplay/options/modal.tsx index 35b8b6b598..fd4ad9e500 100644 --- a/shesha-reactjs/src/providers/refList/options/modal.tsx +++ b/shesha-reactjs/src/components/refListSelectorDisplay/options/modal.tsx @@ -74,6 +74,7 @@ export const TableViewSelectorSettingsModalInner: FC> = ( props, ) => { + const [modalVisible, setModalVisible] = useState(false); const showModal = () => setModalVisible(true); diff --git a/shesha-reactjs/src/providers/refList/options/properties.tsx b/shesha-reactjs/src/components/refListSelectorDisplay/options/properties.tsx similarity index 94% rename from shesha-reactjs/src/providers/refList/options/properties.tsx rename to shesha-reactjs/src/components/refListSelectorDisplay/options/properties.tsx index ab42b98d35..33c50455dc 100644 --- a/shesha-reactjs/src/providers/refList/options/properties.tsx +++ b/shesha-reactjs/src/components/refListSelectorDisplay/options/properties.tsx @@ -1,7 +1,7 @@ import { Form } from 'antd'; import React, { FC, ReactNode, useEffect, useRef, useState } from 'react'; import { useDebouncedCallback } from 'use-debounce'; -import itemSettingsJson from './refListItemsSettings.json'; +import { getSettings } from './refListItemsSettingsForm'; import { useRefListItemGroupConfigurator } from '../provider'; import { ConfigurableFormInstance, FormMarkup } from '@/interfaces'; import { getComponentModel } from '../provider/utils'; @@ -40,7 +40,7 @@ export const RefListItemProperties: FC = () => { const componentModel = getComponentModel(getItem(selectedItemId)); - const markup = itemSettingsJson as FormMarkup; + const markup = getSettings() as FormMarkup; return ( = (props) => } }; + if (props.items.length === 0) { + return <>No items have been configured; + } + return ( -
+
{ + return { + components: new DesignerToolbarSettings() + .addSearchableTabs({ + id: nanoid(), + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'cfd7d45e-c7e3-4a27-987b-dc525c412448', + propertyName: 'hidden', + label: 'Hide', + parentId: 'b8954bf6-f76d-4139-a850-c99bf06c8b69', + inputType: 'switch', + }) + .toJson() + ] + }, + { + key: 'events', + title: 'Events', + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addConfigurableActionConfigurator({ + id: 'F3B46A95-703F-4465-96CA-A58496A5F78C', + propertyName: 'actionConfiguration', + label: 'Action Configuration', + hidden: false, + jsSetting: false, + parentId: '345c2181-fb9f-48ed-9631-864ac357807b', + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + layout: 'vertical' as FormLayout, + colon: false, + labelCol: { + span: 24 + }, + wrapperCol: { + span: 24 + } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/providers/refList/options/styles/styles.ts b/shesha-reactjs/src/components/refListSelectorDisplay/options/styles/styles.ts similarity index 100% rename from shesha-reactjs/src/providers/refList/options/styles/styles.ts rename to shesha-reactjs/src/components/refListSelectorDisplay/options/styles/styles.ts diff --git a/shesha-reactjs/src/providers/refList/provider/actions.ts b/shesha-reactjs/src/components/refListSelectorDisplay/provider/actions.ts similarity index 100% rename from shesha-reactjs/src/providers/refList/provider/actions.ts rename to shesha-reactjs/src/components/refListSelectorDisplay/provider/actions.ts diff --git a/shesha-reactjs/src/providers/refList/provider/contexts.ts b/shesha-reactjs/src/components/refListSelectorDisplay/provider/contexts.ts similarity index 100% rename from shesha-reactjs/src/providers/refList/provider/contexts.ts rename to shesha-reactjs/src/components/refListSelectorDisplay/provider/contexts.ts diff --git a/shesha-reactjs/src/providers/refList/provider/index.tsx b/shesha-reactjs/src/components/refListSelectorDisplay/provider/index.tsx similarity index 66% rename from shesha-reactjs/src/providers/refList/provider/index.tsx rename to shesha-reactjs/src/components/refListSelectorDisplay/provider/index.tsx index 785051e873..970843ec68 100644 --- a/shesha-reactjs/src/providers/refList/provider/index.tsx +++ b/shesha-reactjs/src/components/refListSelectorDisplay/provider/index.tsx @@ -5,19 +5,18 @@ import { RefListItemGroupConfiguratorStateContext, REF_LIST_ITEM_GROUP_CONTEXT_INITIAL_STATE, IUpdateChildItemsPayload, -} from './contexts'; +} from '@/components/refListSelectorDisplay/provider/contexts'; import { selectItemAction, setItems, storeSettingsAction, updateChildItemsAction, updateItemAction, -} from './actions'; -import { RefListGroupItemProps } from './models'; -import { useGet } from '@/hooks'; -import RefListItemGroupReducer from './reducers'; -import { getItemById, getRefListItems } from './utils'; - +} from '@/components/refListSelectorDisplay/provider/actions'; +import { RefListGroupItemProps } from '@/components/refListSelectorDisplay/provider/models'; +import RefListItemGroupReducer from '@/components/refListSelectorDisplay/provider/reducers'; +import { getItemById } from '@/components/refListSelectorDisplay/provider/utils'; +import { useReferenceListDispatcher } from '@/providers/referenceListDispatcher'; export interface IRefListItemGroupConfiguratorProviderPropsBase { baseUrl?: string; @@ -31,9 +30,9 @@ export interface IRefListItemGroupConfiguratorProviderProps { referenceList?: any; } -const RefListItemGroupConfiguratorProvider: FC> = (props) => { +const RefListSelectorDisplayProvider: FC> = (props) => { const { children, readOnly } = props; - const { refetch } = useGet({ path: '', lazy: true }); + const { getReferenceList } = useReferenceListDispatcher(); const [state, dispatch] = useReducer(RefListItemGroupReducer, { ...REF_LIST_ITEM_GROUP_CONTEXT_INITIAL_STATE, @@ -41,18 +40,14 @@ const RefListItemGroupConfiguratorProvider: FC { - if (props?.items?.length && props.items.some(x =>x.referenceList?.id === props?.referenceList?.id )) return; - refetch(getRefListItems(props.referenceList?.id as string)) - .then((resp) => { - dispatch(setItems(resp.result.items)); - }) - .catch((_e) => { - console.error("LOG:: reference list error", _e); + if (props?.items?.length && props.items.some((x) => x.referenceList === props?.referenceList)) return; + getReferenceList({ + refListId: { module: props?.referenceList?.module, name: props?.referenceList?.name }, + }).promise.then((t) => { + dispatch(setItems(t?.items)); }); - - }, [props?.referenceList?.id]); + }, [props?.referenceList]); const selectItem = (uid: string) => { dispatch(selectItemAction(uid)); @@ -71,12 +66,8 @@ const RefListItemGroupConfiguratorProvider: FC { - - dispatch(storeSettingsAction({ columnId: columnId, isCollapsed: isCollapsed })); - + dispatch(storeSettingsAction({ columnId: columnId, isCollapsed: isCollapsed })); }; - - return ( @@ -109,7 +100,9 @@ function useRefListItemGroupConfiguratorActions() { const context = useContext(RefListItemGroupConfiguratorActionsContext); if (context === undefined) { - throw new Error('useRefListItemGroupConfiguratorActions must be used within a RefListItemGroupConfiguratorProvider'); + throw new Error( + 'useRefListItemGroupConfiguratorActions must be used within a RefListItemGroupConfiguratorProvider' + ); } return context; @@ -119,4 +112,4 @@ function useRefListItemGroupConfigurator() { return { ...useRefListItemGroupConfiguratorState(), ...useRefListItemGroupConfiguratorActions() }; } -export { RefListItemGroupConfiguratorProvider, useRefListItemGroupConfigurator }; +export { RefListSelectorDisplayProvider as RefListItemGroupConfiguratorProvider, useRefListItemGroupConfigurator }; diff --git a/shesha-reactjs/src/providers/refList/provider/models.ts b/shesha-reactjs/src/components/refListSelectorDisplay/provider/models.ts similarity index 100% rename from shesha-reactjs/src/providers/refList/provider/models.ts rename to shesha-reactjs/src/components/refListSelectorDisplay/provider/models.ts diff --git a/shesha-reactjs/src/providers/refList/provider/reducers.ts b/shesha-reactjs/src/components/refListSelectorDisplay/provider/reducers.ts similarity index 88% rename from shesha-reactjs/src/providers/refList/provider/reducers.ts rename to shesha-reactjs/src/components/refListSelectorDisplay/provider/reducers.ts index 8f0f3f446a..8b1ab1dc32 100644 --- a/shesha-reactjs/src/providers/refList/provider/reducers.ts +++ b/shesha-reactjs/src/components/refListSelectorDisplay/provider/reducers.ts @@ -3,12 +3,12 @@ import { ISettingsUpdatePayload, IUpdateChildItemsPayload, IUpdateItemSettingsPayload, - REF_LIST_ITEM_GROUP_CONTEXT_INITIAL_STATE, -} from './contexts'; -import { RefListItemGroupActionEnums } from './actions'; -import { IRefListItemGroup } from './models'; + REF_LIST_ITEM_GROUP_CONTEXT_INITIAL_STATE +} from '@/components/refListSelectorDisplay/provider/contexts'; +import { RefListItemGroupActionEnums } from '@/components/refListSelectorDisplay/provider/actions'; +import { IRefListItemGroup } from '@/components/refListSelectorDisplay/provider/models'; import { handleActions } from 'redux-actions'; -import { getItemPositionById } from './utils'; +import { getItemPositionById } from '@/components/refListSelectorDisplay/provider/utils'; const RefListItemGroupReducer = handleActions( { diff --git a/shesha-reactjs/src/providers/refList/provider/utils.ts b/shesha-reactjs/src/components/refListSelectorDisplay/provider/utils.ts similarity index 96% rename from shesha-reactjs/src/providers/refList/provider/utils.ts rename to shesha-reactjs/src/components/refListSelectorDisplay/provider/utils.ts index 53f8beb0b7..daeb763b0b 100644 --- a/shesha-reactjs/src/providers/refList/provider/utils.ts +++ b/shesha-reactjs/src/components/refListSelectorDisplay/provider/utils.ts @@ -52,7 +52,13 @@ export const getComponentModel = (item: RefListGroupItemProps) => ({ export function fadeColor(color: string, fadePercentage: number): string { // Helper function to parse RGB values from different formats - function getRGBValues(color: string): [number, number, number] { + + //handle simple colors + if (/^[a-zA-Z]+$/.test(color)) { + return color; + } + + function getRGBValues(color: string): [number, number, number] | string { // Handle hex if (color.startsWith('#')) { if (!/^#([0-9A-F]{3}){1,2}$/i.test(color)) { diff --git a/shesha-reactjs/src/components/refListStatus/index.tsx b/shesha-reactjs/src/components/refListStatus/index.tsx index 8298a0c5d1..cbcf87f168 100644 --- a/shesha-reactjs/src/components/refListStatus/index.tsx +++ b/shesha-reactjs/src/components/refListStatus/index.tsx @@ -1,4 +1,3 @@ -import convertCssColorNameToHex from 'convert-css-color-name-to-hex'; import React, { CSSProperties, FC } from 'react'; import RefTag from './tag'; import { Alert, Skeleton } from 'antd'; @@ -23,9 +22,20 @@ const Icon = ({ type, ...rest }) => { }; export const RefListStatus: FC = (props) => { - const { styles } = useStyles(); - const { value, referenceListId, showIcon, solidBackground, showReflistName, style } = props; + const { + value, + referenceListId, + showIcon, + solidBackground, + showReflistName, + style = {}, + } = props; + const { width, height, minHeight, minWidth, maxHeight, maxWidth } = style; + const dimensionsStyles = { width, height, minHeight, minWidth, maxHeight, maxWidth }; + const { fontSize, fontWeight, textAlign, color, backgroundColor, backgroundImage, ...rest } = style; + const fontStyles = { fontSize, fontWeight, textAlign }; + const { styles } = useStyles({ dimensionsStyles, fontStyles }); const listItem = useReferenceListItem(referenceListId?.module, referenceListId?.name, value); if (listItem?.error && !listItem?.loading) { @@ -41,13 +51,9 @@ export const RefListStatus: FC = (props) => { const itemData = listItem?.data; - const memoizedColor = solidBackground - ? convertCssColorNameToHex(itemData?.color ?? '') - : itemData?.color?.toLowerCase(); - const canShowIcon = showIcon && itemData?.icon; - if (typeof itemData?.itemValue === 'undefined' && !listItem?.loading) return null; + if (typeof itemData?.itemValue === 'undefined' && !listItem?.loading) return null; return listItem?.loading ? ( @@ -55,12 +61,14 @@ export const RefListStatus: FC = (props) => {
- - - : null} style={style}> + : null} + style={!solidBackground || !itemData?.color ? style : { ...rest }} + styles={styles} + > {showReflistName && itemData?.item} -
); diff --git a/shesha-reactjs/src/components/refListStatus/styles/styles.ts b/shesha-reactjs/src/components/refListStatus/styles/styles.ts index 67d52d6f5d..fad87d2ee3 100644 --- a/shesha-reactjs/src/components/refListStatus/styles/styles.ts +++ b/shesha-reactjs/src/components/refListStatus/styles/styles.ts @@ -1,6 +1,7 @@ -import { createStyles } from 'antd-style'; +import { createStyles } from '@/styles'; + +export const useStyles = createStyles(({ css, cx }, { dimensionsStyles, fontStyles }) => { -export const useStyles = createStyles(({ css, cx }) => { const shaStatusTag = 'sha-status-tag'; const shaStatusTagContainer = cx( 'sha-status-tag-container', @@ -8,15 +9,22 @@ export const useStyles = createStyles(({ css, cx }) => { display: flex; align-items: center; width: fit-content; + ${dimensionsStyles}; + + > span { + ${dimensionsStyles}; + } .${shaStatusTag} { text-transform: uppercase; display: flex; width: 100%; - justify-content: flex-start; + justify-content: ${fontStyles.textAlign === 'center' ? 'center' : fontStyles.textAlign === 'right' ? 'flex-end' : 'flex-start'}; align-items: center; text-align: center; align-self: center; + ${fontStyles} + .sha-help-icon { cursor: help; diff --git a/shesha-reactjs/src/components/refListStatus/tag.tsx b/shesha-reactjs/src/components/refListStatus/tag.tsx index c36c342798..e0db4c3a8f 100644 --- a/shesha-reactjs/src/components/refListStatus/tag.tsx +++ b/shesha-reactjs/src/components/refListStatus/tag.tsx @@ -1,19 +1,18 @@ import { Tag } from 'antd'; import React, { FC, PropsWithChildren } from 'react'; -import { useStyles } from './styles/styles'; - interface ITagProps { color: string; icon?: React.ReactNode; style?: React.CSSProperties; + styles?: any; } const RefTag: FC> = ({ children, ...props }) => { - const { styles } = useStyles(); + if (!children && !props?.icon) return null; return ( - + {children} ); diff --git a/shesha-reactjs/src/components/referenceListAutocomplete/index.tsx b/shesha-reactjs/src/components/referenceListAutocomplete/index.tsx new file mode 100644 index 0000000000..0fe1e4dbf9 --- /dev/null +++ b/shesha-reactjs/src/components/referenceListAutocomplete/index.tsx @@ -0,0 +1,270 @@ +import { AutoComplete, Empty, Spin, Typography } from 'antd'; +import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; +import { FC } from 'react'; +import { useGet } from '@/hooks'; +import { useDebouncedCallback } from 'use-debounce'; +import { GENERIC_ENTITIES_ENDPOINT, LEGACY_REFERENCE_LISTS_MODULE_NAME } from '@/shesha-constants'; +import { IAbpWrappedGetEntityListResponse, IGenericGetAllPayload } from '@/interfaces/gql'; +import { IReferenceListIdentifier } from '@/interfaces/referenceList'; +import HelpTextPopover from '@/components/helpTextPopover'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; + +export interface IReferenceListAutocompleteRuntimeProps { + value?: IReferenceListIdentifier; + onChange?: (value?: IReferenceListIdentifier) => void; + readOnly?: boolean; + maxResultCount?: number; + size?: SizeType; +} + +interface IOption { + label: string | ReactNode; + value: string; + data: IReferenceListIdentifier; + options?: IOption[]; +} + +const baseItemFilter = [ + { + "==": [{ "var": "isLast" }, true] + } +]; +const getFilter = (term: string) => { + const termFilter = term + ? [ + { + or: [ + { 'in': [term, { 'var': 'name' }] }, + { 'in': [term, { 'var': 'module.name' }] }, + ] + }, + ] + : []; + const filter = { + and: [...baseItemFilter, ...termFilter] + }; + return JSON.stringify(filter); +}; +const REFERENCE_LIST_ENTITY_TYPE = 'Shesha.Framework.ReferenceList'; +const REFERENCE_LIST_PROPERTIES = 'id name module { id name } label description versionNo'; +const getListFetcherQueryParams = (term: string, maxResultCount): IGenericGetAllPayload => { + return { + skipCount: 0, + maxResultCount: maxResultCount ?? 10, + entityType: REFERENCE_LIST_ENTITY_TYPE, + properties: REFERENCE_LIST_PROPERTIES, + quickSearch: null, + filter: getFilter(term), + sorting: 'module.name, name', + }; +}; +const getSelectedValueQueryParams = (value?: IReferenceListIdentifier): IGenericGetAllPayload => { + if (!value) + return null; + + const filters = [ + ...baseItemFilter, + { '==': [{ 'var': 'name' }, value.name] }, + ]; + if (value.module !== undefined) + filters.push({ '==': [{ 'var': 'module.name' }, value.module] }); + + const expression = { and: filters }; + + return expression + ? { + skipCount: 0, + maxResultCount: 1000, + entityType: REFERENCE_LIST_ENTITY_TYPE, + properties: REFERENCE_LIST_PROPERTIES, + filter: JSON.stringify(expression), + } + : null; +}; + +interface IResponseItem { + id: string; + name: string; + label?: string; + description?: string; + versionNo?: number; + module?: { + id: string; + name: string; + }; +} + +interface IConfigurationItemProps { + name: string; + label?: string; + description?: string; + versionNo?: number; +} + +const RefListLabel: FC = ({ name, description, versionNo, label }) => { + const displayLabel = label && label !== name + ? label + : null; + return ( +
+ + {name} {false && versionNo && (version {versionNo})} + + {displayLabel && ( + <>
{displayLabel} + )} +
+ ); +}; + +const getDisplayText = (item: IResponseItem) => { + if (!item) + return null; + const fullName = item.name; + + return item.module + ? `${item.module.name}:${fullName}` + : fullName; +}; + +export const ReferenceListAutocomplete: FC = (props) => { + const selectedValue = useRef(null); + const [autocompleteText, setAutocompleteText] = useState(null); + const { + maxResultCount = 10, + } = props; + + const listFetcher = useGet, any, IGenericGetAllPayload>( + `${GENERIC_ENTITIES_ENDPOINT}/GetAll`, + { + lazy: true, + queryParams: getListFetcherQueryParams(null, maxResultCount), + } + ); + + const valueFetcher = useGet, any, IGenericGetAllPayload>( + `${GENERIC_ENTITIES_ENDPOINT}/GetAll`, + { + lazy: true, + } + ); + useEffect(() => { + if (valueFetcher.data?.success && valueFetcher.data.result) { + const items = valueFetcher.data.result?.items ?? []; + if (items.length === 1) { + const displayText = getDisplayText(items[0]); + setAutocompleteText(displayText); + } else + console.error(items.length > 1 ? "Found more than one form with specified name" : "Reference list not found"); + } + }, [valueFetcher.data?.result]); + + const listItems = listFetcher.data?.result?.items; + const valueItems = valueFetcher.data?.result?.items; + + const fetchedItems = useMemo(() => { + return listItems + ? listItems + : valueItems; + }, [listItems, valueItems]); + + useEffect(() => { + // fetch data if required + const valueFetchParams = getSelectedValueQueryParams(props.value); + if (valueFetchParams) { + const selectedFromList = selectedValue.current && selectedValue.current === props.value; + if (!selectedFromList) + valueFetcher.refetch({ queryParams: valueFetchParams }); + } + }, [props.value]); + + const options = useMemo(() => { + const result: IOption[] = []; + if (fetchedItems) { + fetchedItems.forEach(item => { + const moduleDto = item.module ?? { name: LEGACY_REFERENCE_LISTS_MODULE_NAME, id: '-' }; + + const opt: IOption = { + label: ( + + ), + value: getDisplayText(item), + data: { + name: item.name, + module: item.module?.name, + } + }; + let group = result.find(g => g.value === moduleDto.id); + if (!group) { + group = { + label: moduleDto.name, + value: moduleDto.id, + data: null, + options: [opt], + }; + result.push(group); + } else + group.options.push(opt); + + }); + } + + return result; + }, [fetchedItems]); + + const debouncedFetchItems = useDebouncedCallback<(value: string) => void>( + localValue => { + listFetcher.refetch({ queryParams: getListFetcherQueryParams(localValue, maxResultCount) }); + }, + // delay in ms + 100 + ); + + + const onSearch = (term) => { + debouncedFetchItems(term); + }; + + const onSelect = (_value, option) => { + const listId = (option as IOption)?.data; + selectedValue.current = listId; + if (props.onChange) { + props.onChange(listId); + } + }; + + const onClear = () => { + selectedValue.current = null; + if (props.onChange) { + props.onChange(null); + } + }; + + const loading = listFetcher.loading; + + return ( + : } + style={{ width: '100%' }} + options={options} + onSearch={onSearch} + onSelect={onSelect} + onClear={onClear} + placeholder={valueFetcher.loading ? 'Loading...' : 'Type to search'} + disabled={valueFetcher.loading || props.readOnly} + size={props?.size} + + value={autocompleteText} + onChange={setAutocompleteText} + > + + ); +}; + +export default ReferenceListAutocomplete; \ No newline at end of file diff --git a/shesha-reactjs/src/components/richTextEditor/joditEditor.tsx b/shesha-reactjs/src/components/richTextEditor/joditEditor.tsx index 60e3364cdc..5b006202fa 100644 --- a/shesha-reactjs/src/components/richTextEditor/joditEditor.tsx +++ b/shesha-reactjs/src/components/richTextEditor/joditEditor.tsx @@ -34,17 +34,6 @@ export const JoditEditorWrapper: FC = (props) => { return result; }); - const updatePlaceholder = (newValue: string) => { - const newPlaceholder = getPlaceholder(newValue); - if (fullConfig.placeholder !== newPlaceholder){ - setFullConfig({ ...fullConfig, placeholder: newPlaceholder }); - } - }; - - useEffect(() => { - updatePlaceholder(value); - }, [value]); - const updateConfig = (newConfig: any) => { setFullConfig({ ...defaultOptions, ...newConfig, placeholder: fullConfig.placeholder }); }; diff --git a/shesha-reactjs/src/components/sectionSeparator/index.tsx b/shesha-reactjs/src/components/sectionSeparator/index.tsx index 5be95a3c44..f25d1bdbe8 100644 --- a/shesha-reactjs/src/components/sectionSeparator/index.tsx +++ b/shesha-reactjs/src/components/sectionSeparator/index.tsx @@ -1,9 +1,10 @@ import React, { CSSProperties, FC, ReactNode, useEffect, useRef, useState } from 'react'; import { useStyles } from './styles/styles'; -import { addPx } from './utils'; import Show from '../show'; import { Tooltip } from 'antd'; import { QuestionCircleOutlined } from '@ant-design/icons'; +import { addPx } from '@/utils/style'; +import { titleDefaultStyles } from './utils'; export interface ISectionSeparatorProps { id?: string; @@ -11,38 +12,35 @@ export interface ISectionSeparatorProps { containerStyle?: CSSProperties; titleStyle?: CSSProperties; tooltip?: string; - fontSize?: number; - fontColor?: string; inline?: boolean; - dashed?: boolean; lineColor?: string; lineThickness?: number; lineWidth?: string; lineHeight?: string; titleMargin?: number; + marginBottom?: string | number; labelAlign?: 'left' | 'center' | 'right'; orientation?: 'horizontal' | 'vertical'; - fontWeight?: string; + fontSize?: string | number; + lineType?: string; } export const SectionSeparator: FC = ({ id, labelAlign = 'left', - fontSize = 14, - fontColor = '#000', - fontWeight = '500', inline, - dashed, + lineType = 'solid', lineColor, - lineThickness = 2, + lineThickness, lineWidth, lineHeight, orientation, containerStyle, - titleStyle, + titleStyle = titleDefaultStyles(), tooltip, title, - titleMargin + titleMargin, + marginBottom, }) => { const { styles } = useStyles(); const titleRef = useRef(null); @@ -58,18 +56,17 @@ export const SectionSeparator: FC = ({ const borderStyle: CSSProperties = { '--border-thickness': `${lineThickness ?? 2}px`, - '--border-style': dashed ? 'dashed' : 'solid', + '--border-style': lineType, '--border-color': lineColor || styles.primaryColor, textAlign: labelAlign, - marginBottom: '8px', + marginBottom: marginBottom || '8px', } as CSSProperties; const baseStyle: CSSProperties = { - borderBottom: inline ? `${lineThickness || 2}px ${dashed ? 'dashed' : 'solid'} ${lineColor || styles.primaryColor}` : 'none', + borderBottom: inline ? `${lineThickness || 2}px ${lineType} ${lineColor || styles.primaryColor}` : 'none', }; const getLineStyles = (isLeft: boolean) => { - if ((isLeft && labelAlign === 'left') || (!isLeft && labelAlign === 'right')) { return { width: `calc(${titleMargin || 0}% - ${titleWidth / 2}px)` }; } @@ -90,36 +87,38 @@ export const SectionSeparator: FC = ({ } return ( -
+
- < div ref={titleRef} style={{ ...titleStyle, ...titleMarginStyle, whiteSpace: 'nowrap', color: fontColor, fontSize, fontWeight } - }> +
{title} - < Show when={Boolean(tooltip?.trim())}> + - +
- < div style={{ ...getLineStyles(false), ...baseStyle }}>
+
); }; - return ( - vertical ? (
) : -
+ ) : ( +
-
- {renderTitle()} -
+ }} + key={id} + > +
+ {renderTitle()}
+
); }; diff --git a/shesha-reactjs/src/components/sectionSeparator/utils.ts b/shesha-reactjs/src/components/sectionSeparator/utils.ts index f933cb2a52..7bbfaff613 100644 --- a/shesha-reactjs/src/components/sectionSeparator/utils.ts +++ b/shesha-reactjs/src/components/sectionSeparator/utils.ts @@ -1,9 +1,13 @@ -export const addPx = (value) => { - value = value ?? "100%"; - return /^\d+(\.\d+)?$/.test(value) ? `${value}px` : value; -}; - export const strings = { tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', -}; \ No newline at end of file +}; + +export const titleDefaultStyles = (): React.CSSProperties => { + return { + fontWeight: '500', + fontSize: 14, + color: '#000', + fontFamily: 'Segoe UI', + }; +}; diff --git a/shesha-reactjs/src/components/settingsEditor/provider/index.tsx b/shesha-reactjs/src/components/settingsEditor/provider/index.tsx index 1c873f8d07..67b2d42b07 100644 --- a/shesha-reactjs/src/components/settingsEditor/provider/index.tsx +++ b/shesha-reactjs/src/components/settingsEditor/provider/index.tsx @@ -1,7 +1,7 @@ import { settingsGetValue, settingsUpdateValue } from '@/apis/settings'; import useThunkReducer from '@/hooks/thunkReducer'; import { IAbpWrappedGetEntityListResponse, IGenericGetAllPayload } from '@/interfaces/gql'; -import { useSheshaApplication } from '@/providers'; +import { useSheshaApplication, useTheme } from '@/providers'; import React, { FC, PropsWithChildren, useContext, useEffect } from 'react'; import { ConfigurationItemVersionStatus } from '@/utils/configurationFramework/models'; import * as RestfulShesha from '@/utils/fetchers'; @@ -71,9 +71,13 @@ const SettingsEditorProvider: FC ...SETTINGS_EDITOR_STATE_CONTEXT_INITIAL_STATE, }; const { backendUrl, httpHeaders } = useSheshaApplication(); - const [state, dispatch] = useThunkReducer(settingsEditorReducer, initial); + const { resetToApplicationTheme } = useTheme(); + useEffect(() => { + resetToApplicationTheme(); + }, [state.applications, state.settingSelection]); + const fetchConfigurations = () => { dispatch(fetchConfigurationsAction()); diff --git a/shesha-reactjs/src/components/shaIcon/index.tsx b/shesha-reactjs/src/components/shaIcon/index.tsx index ca357e2d79..93b8c80fb1 100644 --- a/shesha-reactjs/src/components/shaIcon/index.tsx +++ b/shesha-reactjs/src/components/shaIcon/index.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { CSSProperties, FC } from 'react'; import { IconBaseProps } from '@ant-design/icons/lib/components/Icon'; import { FilledIconTypes } from '../iconPicker/iconNamesFilled'; import { OutlinedIconTypes } from '../iconPicker/iconNamesOutlined'; @@ -10,6 +10,7 @@ export type IconType = FilledIconTypes | OutlinedIconTypes | TwoToneIconTypes; export interface IShaIconProps extends IconBaseProps { iconName: IconType; twoToneColor?: string; + style?: CSSProperties; } const ShaIcon: FC = ({ iconName = 'WarningFilled', ...props }) => { diff --git a/shesha-reactjs/src/components/shaLink/index.tsx b/shesha-reactjs/src/components/shaLink/index.tsx index 8dc9d25d86..d6a95c0b1c 100644 --- a/shesha-reactjs/src/components/shaLink/index.tsx +++ b/shesha-reactjs/src/components/shaLink/index.tsx @@ -1,12 +1,8 @@ -import React, { - FC, - PropsWithChildren, - ReactNode, - useMemo - } from 'react'; +import React, { CSSProperties, FC, PropsWithChildren, ReactNode, useMemo } from 'react'; import { Button } from 'antd'; import { FormIdentifier } from '@/interfaces'; import { useShaRouting } from '@/providers/shaRouting'; +import { innerEntityReferenceButtonBoxStyle, innerEntityReferenceSpanBoxStyle } from '../quickView/utils'; export interface IShaLinkProps { linkTo?: string; @@ -22,6 +18,10 @@ export interface IShaLinkProps { displayName?: string; className?: string; + + style?: CSSProperties; + + disabled?: boolean; } export const ShaLink: FC> = ({ @@ -32,12 +32,14 @@ export const ShaLink: FC> = ({ displayName, children, className, + style, + disabled, }) => { const { router, getFormUrl } = useShaRouting(); const paramsStr = useMemo(() => { if (!params) return undefined; - var str = []; + const str = []; for (const key of Object.keys(params)) str.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); return str.join('&'); }, [params]); @@ -53,9 +55,16 @@ export const ShaLink: FC> = ({ const childrenOrDisplayText = children || displayName; return ( - ); }; diff --git a/shesha-reactjs/src/components/shaSpin/index.tsx b/shesha-reactjs/src/components/shaSpin/index.tsx index fe5bf91cd4..76ab187a1d 100644 --- a/shesha-reactjs/src/components/shaSpin/index.tsx +++ b/shesha-reactjs/src/components/shaSpin/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, PropsWithChildren } from 'react'; +import React, { FC, PropsWithChildren, useMemo } from 'react'; import { Spin } from 'antd'; import { LoadingOutlined } from '@ant-design/icons'; @@ -8,15 +8,11 @@ export interface IShaSpin { tip?: string; } -export const ShaSpin: FC> = ({ children, spinning, spinIconSize = 24, tip }) => ( - } - tip={tip} - > +export const ShaSpin: FC> = ({ children, spinning, spinIconSize = 24, tip }) => { + const indicator = useMemo(() => , [spinIconSize]); + return {children} - -); + ; +}; export default ShaSpin; diff --git a/shesha-reactjs/src/components/sidebarContainer/index.tsx b/shesha-reactjs/src/components/sidebarContainer/index.tsx index 9b1758acc0..14fcdcd7ff 100644 --- a/shesha-reactjs/src/components/sidebarContainer/index.tsx +++ b/shesha-reactjs/src/components/sidebarContainer/index.tsx @@ -40,8 +40,6 @@ export const SidebarContainer: FC = ({ [isOpenRight, leftSidebarProps, rightSidebarProps, allowFullCollapse, isOpenLeft] ); - const handleDrag = (newSizes) => setCurrentSizes(newSizes); - const renderSidebar = (side: SidebarPanelPosition) => { const sidebarProps = side === 'left' ? leftSidebarProps : rightSidebarProps; const hideFullCollapse = allowFullCollapse && !sidebarProps?.open; @@ -70,8 +68,7 @@ export const SidebarContainer: FC = ({ gutterSize={8} gutterAlign="center" snapOffset={5} - dragInterval={1} - onDrag={handleDrag} + dragInterval={12} direction="horizontal" cursor="col-resize" className={classNames(styles.sidebarContainerBody)} diff --git a/shesha-reactjs/src/components/sidebarMenu/utils.tsx b/shesha-reactjs/src/components/sidebarMenu/utils.tsx index c5c775351a..c775786a14 100644 --- a/shesha-reactjs/src/components/sidebarMenu/utils.tsx +++ b/shesha-reactjs/src/components/sidebarMenu/utils.tsx @@ -1,10 +1,11 @@ -import { MenuProps } from 'antd'; +import { MenuProps, Tooltip } from 'antd'; import classNames from 'classnames'; import React, { ReactNode } from 'react'; import ShaIcon, { IconType } from '@/components/shaIcon'; import { ISidebarMenuItem, isSidebarButton, isSidebarGroup, SidebarItemType } from '@/interfaces/sidebar'; import { IConfigurableActionConfiguration } from '@/providers/index'; import Link from 'next/link'; +import { QuestionCircleOutlined } from '@ant-design/icons'; type MenuItem = Required['items'][number]; @@ -18,9 +19,10 @@ interface IGetItemArgs { url?: string; navigationType?: string; onClick?: () => void; + tooltip?: string | ReactNode; } -function getItem({ label, key, icon, children, isParent, itemType, onClick, navigationType, url }: IGetItemArgs): MenuItem { +function getItem({ label, key, icon, children, isParent, itemType, onClick, navigationType, url, tooltip }: IGetItemArgs): MenuItem { const clickHandler = (event) => { event.preventDefault(); onClick(); @@ -32,9 +34,25 @@ function getItem({ label, key, icon, children, isParent, itemType, onClick, navi key, icon, children, - label: Boolean(onClick) - ? navigationType === 'url' || navigationType === 'form' ? {label} : {label} - : {label}, + label: (() => { + const baseContent = onClick + ? ((navigationType === 'url' || navigationType === 'form') + ? {label} + : {label}) + : {label}; + + if (!tooltip) return baseContent; + + const tooltipText = typeof tooltip === 'string' ? tooltip : undefined; + return ( + + {baseContent} + + + + + ); + })(), type: itemType === 'divider' ? 'divider' : undefined, } as MenuItem; } @@ -86,6 +104,7 @@ export const sidebarMenuItemToMenuItem = ({ item, onButtonClick, onItemEvaluatio url, navigationType, onClick: actionConfiguration ? () => onButtonClick(id, actionConfiguration) : undefined, + tooltip: item.tooltip, }; if (onItemEvaluation) onItemEvaluation(item); diff --git a/shesha-reactjs/src/components/statistic/index.tsx b/shesha-reactjs/src/components/statistic/index.tsx index 9fb0b0f160..0d8ea04c22 100644 --- a/shesha-reactjs/src/components/statistic/index.tsx +++ b/shesha-reactjs/src/components/statistic/index.tsx @@ -3,11 +3,47 @@ import { Statistic, StatisticProps } from 'antd'; import { useStyles } from './styles/styles'; import classNames from 'classnames'; -export interface IShaStatisticProps extends StatisticProps { } +export interface IShaStatisticProps extends StatisticProps { + onClick?: (event: React.MouseEvent) => void; +} export const ShaStatistic: FC = ({ className, ...rest }) => { - const { styles } = useStyles(); - return ; + const { styles } = useStyles({ + token: { + fontSize: rest.valueStyle?.fontSize, + }, + }); + + const marginStyles = rest.style + ? { + margin: rest.style.margin, + marginRight: rest.style.marginRight, + marginLeft: rest.style.marginLeft, + marginTop: rest.style.marginTop, + marginBottom: rest.style.marginBottom, + } + : {}; + + const restStyle = rest.style ? { ...rest.style } : {}; + if (restStyle) { + delete restStyle.margin; + delete restStyle.marginRight; + delete restStyle.marginLeft; + delete restStyle.marginTop; + delete restStyle.marginBottom; + } + + return ( +
rest.onClick(e) : undefined} + style={{ + ...marginStyles, + }} + > + +
+ ); }; export default ShaStatistic; diff --git a/shesha-reactjs/src/components/statistic/styles/styles.ts b/shesha-reactjs/src/components/statistic/styles/styles.ts index f039480264..f7a0d42561 100644 --- a/shesha-reactjs/src/components/statistic/styles/styles.ts +++ b/shesha-reactjs/src/components/statistic/styles/styles.ts @@ -2,8 +2,6 @@ import { createStyles } from '@/styles'; import { sheshaStyles } from '@/styles'; export const useStyles = createStyles(({ css, cx, prefixCls, iconPrefixCls, token }) => { - const contentFontSize = "45px"; - const shaStatistic = cx("sha-statistic", css` box-shadow: 0 7px 30px -10px rgba(150, 170, 180, 0.5); margin-right: 12px; @@ -24,12 +22,21 @@ export const useStyles = createStyles(({ css, cx, prefixCls, iconPrefixCls, toke } .${iconPrefixCls} { - font-size: ${contentFontSize}; + font-size: ${token?.fontSize}; } .${prefixCls}-statistic-content-value-int { - font-size: ${contentFontSize}; + font-size: ${token?.fontSize}; } + + .container-div-zero-padding-margin { + padding: 0; + margin: 0; + } + + .content-box { + box-sizing: content-box; + } `); return { shaStatistic, diff --git a/shesha-reactjs/src/components/storedFilesRenderer/index.tsx b/shesha-reactjs/src/components/storedFilesRenderer/index.tsx index b44c2d00aa..0f09302563 100644 --- a/shesha-reactjs/src/components/storedFilesRenderer/index.tsx +++ b/shesha-reactjs/src/components/storedFilesRenderer/index.tsx @@ -2,6 +2,7 @@ import React, { FC, ReactNode } from 'react'; import { StoredFilesRendererBase } from '@/components/storedFilesRendererBase'; import { useStoredFilesStore } from '@/providers/storedFiles'; import { ButtonProps } from 'antd'; +import { IStoredFile } from '@/index'; export interface IStoredFilesRendererProps { ownerId?: string; @@ -13,6 +14,7 @@ export interface IStoredFilesRendererProps { accept?: string[]; layout?: 'vertical' | 'horizontal' | 'grid'; listType?: 'text' | 'thumbnail'; + onFileListChanged?: (list: IStoredFile[]) => void; } export const StoredFilesRenderer: FC = ({ @@ -24,6 +26,7 @@ export const StoredFilesRenderer: FC = ({ accept = [], layout, listType, + onFileListChanged, }) => { const { fileList, @@ -52,6 +55,7 @@ export const StoredFilesRenderer: FC = ({ allowedFileTypes={accept} layout={layout} listType={listType} + onFileListChanged={onFileListChanged} // noFilesCaption={noFilesCaption} /> ); diff --git a/shesha-reactjs/src/components/storedFilesRendererBase/index.tsx b/shesha-reactjs/src/components/storedFilesRendererBase/index.tsx index 12afefc0eb..52f39e48c3 100644 --- a/shesha-reactjs/src/components/storedFilesRendererBase/index.tsx +++ b/shesha-reactjs/src/components/storedFilesRendererBase/index.tsx @@ -1,22 +1,26 @@ -import Dragger, { DraggerProps } from 'antd/lib/upload/Dragger'; -import React, { FC, useEffect, useState } from 'react'; +import { DraggerStub } from '@/components/fileUpload/stubs'; +import { layoutType, listType } from '@/designer-components/attachmentsEditor/attachmentsEditor'; +import { useFormComponentStyles } from '@/hooks/formComponentHooks'; +import { getFileIcon, isImageType } from '@/icons/fileIcons'; +import { IInputStyles, IStyleType, useSheshaApplication, ValidationErrors } from '@/index'; +import { IFormComponentStyles } from '@/providers/form/models'; +import { IDownloadFilePayload, IStoredFile, IUploadFilePayload } from '@/providers/storedFiles/contexts'; +import { addPx } from '@/utils/style'; +import { DownloadOutlined, FileZipOutlined, UploadOutlined } from '@ant-design/icons'; import { Alert, + App, Button, ButtonProps, - App, - Upload, Image, + Upload, UploadFile, } from 'antd'; -import { DraggerStub } from '@/components/fileUpload/stubs'; -import { DownloadOutlined, FileZipOutlined, UploadOutlined } from '@ant-design/icons'; -import { IDownloadFilePayload, IStoredFile, IUploadFilePayload } from '@/providers/storedFiles/contexts'; +import Dragger, { DraggerProps } from 'antd/lib/upload/Dragger'; import { RcFile, UploadChangeParam } from 'antd/lib/upload/interface'; +import React, { FC, useEffect, useState } from 'react'; +import { isValidGuid } from '../formDesigner/components/utils'; import { useStyles } from './styles/styles'; -import { getStyle, IInputStyles, pickStyleFromModel, useSheshaApplication } from '@/index'; -import { layoutType, listType } from '@/designer-components/attachmentsEditor/attachmentsEditor'; -import { getFileIcon, isImageType } from '@/icons/fileIcons'; interface IUploaderFileTypes { name: string; type: string; @@ -56,6 +60,9 @@ export interface IStoredFilesRendererBaseProps extends IInputStyles { borderRadius?: number; hideFileName?: boolean; gap?: number; + container?: IStyleType; + primaryColor?: string; + allStyles?: IFormComponentStyles; } export const StoredFilesRendererBase: FC = ({ @@ -76,41 +83,38 @@ export const StoredFilesRendererBase: FC = ({ validFileTypes = [], maxFileLength = 0, isDragger = false, + primaryColor, disabled, isStub = false, allowedFileTypes = [], - maxHeight, downloadZip, allowDelete, layout, listType, - hideFileName, - stylingBox, - style, gap, - borderSize, borderColor, borderType, fontColor, fontSize, width, height, thumbnailHeight, borderRadius, thumbnailWidth + ...rest }) => { + const { message, notification } = App.useApp(); const { httpHeaders } = useSheshaApplication(); + const [previewOpen, setPreviewOpen] = useState(false); + const [previewImage, setPreviewImage] = useState({ url: '', uid: '', name: '' }); + const [imageUrls, setImageUrls] = useState<{ [key: string]: string }>(fileList.reduce((acc, { uid, url }) => ({ ...acc, [uid]: url }), {})); + const model = rest; const hasFiles = !!fileList.length; - const addPx = (value) => /^\d+(\.\d+)?$/.test(value) ? `${value}px` : value; - const styling = JSON.parse(stylingBox || '{}'); - const customStyle = getStyle(style || '{}'); - const stylingBoxAndCSS = pickStyleFromModel(styling); - const jsSstyles = { ...customStyle, ...stylingBoxAndCSS }; + const { dimensionsStyles: containerDimensionsStyles, jsStyle: containerJsStyle, stylingBoxAsCSS } = useFormComponentStyles({ ...model.container }); const { styles } = useStyles({ - borderSize: addPx(borderSize), borderColor, borderType, fontColor, fontSize: addPx(fontSize), width: layout === 'vertical' ? '' : addPx(width), height: layout === 'horizontal' ? '' : addPx(height), maxHeight: addPx(maxHeight), - thumbnailHeight: addPx(thumbnailHeight), borderRadius: addPx(borderRadius), thumbnailWidth: addPx(thumbnailWidth), layout: listType === 'thumbnail' && !isDragger ? layout : false, - hideFileName: hideFileName && listType === 'thumbnail', isDragger, gap: addPx(gap), styles: jsSstyles + containerStyles: { + ...containerDimensionsStyles, + width: layout === 'vertical' ? '' : addPx(containerDimensionsStyles.width), height: layout === 'horizontal' ? '' : addPx(containerDimensionsStyles.height), + ...containerJsStyle, ...stylingBoxAsCSS, + }, + style: model?.allStyles?.fullStyle, model: { gap: addPx(gap), layout: listType === 'thumbnail' && !isDragger, hideFileName: rest.hideFileName && listType === 'thumbnail', isDragger, isStub }, + primaryColor }); - const { message, notification } = App.useApp(); - const [previewOpen, setPreviewOpen] = useState(false); - const [previewImage, setPreviewImage] = useState({ url: '', uid: '', name: '' }); - const [imageUrls, setImageUrls] = useState<{ [key: string]: string }>(fileList.reduce((acc, { uid, url }) => ({ ...acc, [uid]: url }), {})); - const listTypeAndLayout = listType === 'text' || !listType || isDragger ? 'text' : 'picture-card'; const openFilesZipNotification = () => @@ -159,7 +163,6 @@ export const StoredFilesRendererBase: FC = ({ setPreviewOpen(true); }; - const iconRender = (file) => { const { type, uid } = file; @@ -172,6 +175,11 @@ export const StoredFilesRendererBase: FC = ({ return getFileIcon(type); }; + + if (model?.background?.type === 'storedFile' && model?.background.storedFile?.id && !isValidGuid(model?.background.storedFile.id)) { + return ; + } + const props: DraggerProps = { name: '', accept: allowedFileTypes?.join(','), @@ -185,7 +193,9 @@ export const StoredFilesRendererBase: FC = ({ } else if (status === 'error') { message.error(`${info.file.name} file upload failed.`); } - onFileListChanged(info.fileList); + if (onFileListChanged) { + onFileListChanged(info.fileList); + } }, onRemove(file) { deleteFile(file.uid); @@ -241,18 +251,28 @@ export const StoredFilesRendererBase: FC = ({ return (
+ layout === 'vertical' && listTypeAndLayout !== 'text' ? styles.shaStoredFilesRendererVertical : + layout === 'grid' && listTypeAndLayout !== 'text' ? styles.shaStoredFilesRendererGrid : ''}`}> {isStub ? (isDragger - ? - :
{renderUploadContent()}
) + ? + :
+ {renderUploadContent()} + {listType !== 'text' && !rest.hideFileName && + + {'file name'} + } +
) : (props.disabled - ? - : isDragger - ? + ? + : isDragger ? + + + : {!disabled ? renderUploadContent() : null}) } - {previewImage && ( { - const uploadListMaxHeight = "80px"; // @sha-upload-list-max-height +export const useStyles = createStyles(({ token, css, cx, prefixCls }, { style, model, containerStyles, primaryColor }) => { + const { background, backgroundImage, borderRadius: allRadius, borderWidth, borderTopWidth, width, minWidth, maxWidth, + borderBottomWidth, borderLeftWidth, borderLeftColor, borderLeftStyle, borderRightColor, borderRightStyle, borderColor, borderTopStyle, borderTopColor, + borderTop, boxShadow, borderBottom, borderBottomColor, borderBottomStyle, borderRight, borderRightWidth, backgroundColor, backgroundPosition, + backgroundRepeat, backgroundSize, borderStyle, color, fontFamily, fontSize, fontWeight, height, maxHeight, minHeight, textAlign, + borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius, borderTopLeftRadius, + ...rest + } = style; + + const { width: containerWidth, height: containerHeight, + maxHeight: containerMaxHeight, maxWidth: containerMaxWidth, minHeight: containerMinHeight, + minWidth: containerMinWidth, marginTop, marginLeft, marginRight, marginBottom, paddingTop, + paddingLeft, paddingRight, paddingBottom, ...restContainerStyles } = containerStyles; + + const { gap, layout, hideFileName, isDragger } = model; const storedFilesRendererBtnContainer = "stored-files-renderer-btn-container"; const storedFilesRendererNoFiles = "stored-files-renderer-no-files"; + const borderRadius = [ + borderTopRightRadius ?? allRadius ?? '8px', + borderBottomRightRadius ?? allRadius ?? '8px', + borderBottomLeftRadius ?? allRadius ?? '8px', + borderTopLeftRadius ?? allRadius ?? '8px', + ].join(' '); + const antUploadDragIcon = `${prefixCls}-upload-drag-icon`; const shaStoredFilesRenderer = cx("sha-stored-files-renderer", css` - --thumbnail-width: ${thumbnailWidth ?? thumbnailHeight ?? '54px'}; - --thumbnail-height: ${thumbnailHeight ?? thumbnailWidth ?? '54px'}; + --thumbnail-width: ${layout ? (width ?? '54px') : '100%'}; + --thumbnail-height: ${layout ? (height ?? '54px') : '100%'}; --ant-margin-xs: ${gap ?? '8px'} !important; --ant-border-radius-xs: ${borderRadius ?? '8px'} !important; --ant-border-radius-sm: ${borderRadius ?? '8px'} !important; - --ant-border-radius-lg: ${borderRadius ?? '8px'} !important; - --container-width: ${width}; - --container-height: ${height}; - ${styles} - - .sha-stored-files-renderer { - max-height: var(--container-height) !important; - max-width: var(--container-width) !important; - display: inline-block; - } - + --ant-border-radius-lg: ${borderRadius ?? '8px'} !important; + --container-width: ${containerWidth}; + --container-max-width: ${containerMaxWidth}; + --container-min-width: ${containerMinWidth}; + --container-min-height: ${containerMinHeight}; + --container-max-height: ${containerMaxHeight}; + --container-height: ${containerHeight}; + --ant-upload-actions-color: ${token.colorError} !important; + --ant-button-content-font-size: ${fontSize ?? '14px'} !important; + --ant-button-font-weight: ${fontWeight ?? '400'} !important; + --ant-font-family: ${fontFamily ?? 'Segoe UI'} !important; + --ant-button-font-weight: ${fontWeight ?? '400'} !important; + margin-top: ${marginTop}; + margin-left: ${marginLeft}; + margin-right: ${marginRight}; + margin-bottom: ${marginBottom}; + padding-top: ${paddingTop ?? '2px'}; + padding-left: ${paddingLeft ?? '2px'}; + padding-right: ${paddingRight ?? '2px'}; + padding-bottom: ${paddingBottom ?? '2px'}; + ${restContainerStyles} + width: var(--container-width) !important; + max-width: var(--container-max-width) !important;; + min-width: var(--container-min-width) !important; + height: var(--container-height) !important; + max-height: var(--container-max-height) !important; + min-height: var(--container-min-height) !important; + overflow: auto; + scrollbar-width: thin; + &::-webkit-scrollbar { + width: 8px; + background-color: transparent; + } + .ant-upload:not(.ant-upload-disabled) { - .icon { - color: ${token.colorPrimary} !important; - }; + .icon { + color: ${primaryColor ?? token.colorPrimary} !important; + }; } .ant-upload-list-item { @@ -36,21 +79,29 @@ export const useStyles = createStyles(({ token, css, cx, prefixCls }, { borderSi --ant-padding-xs: 0px !important; --font-size: ${fontSize ?? '14px'} !important; --ant-font-size: ${fontSize ?? '14px'} !important; - border-radius: ${borderRadius ?? '8px'} !important; display: flex; + width: ${layout ? (width ?? '54px') + ' !important' : ''}; + height: ${layout ? (height ?? '54px') + ' !important' : ''}; :before { top: 0; width: 100% !important; - border-radius: ${borderRadius ?? '8px'} !important; height: 100% !important; + border-radius: ${borderRadius ?? '8px'} !important; + ${rest} } } .ant-upload-list-item-thumbnail { + background: ${backgroundImage ?? (backgroundColor ?? '#fff')} !important; + border: ${borderWidth} ${borderStyle} ${borderColor}; + border-top: ${borderTopWidth ?? borderWidth} ${borderTopStyle ?? borderStyle} ${borderTopColor ?? borderColor}; + border-right: ${borderRightWidth ?? borderWidth} ${borderRightStyle ?? borderStyle} ${borderRightColor ?? borderColor}; + border-left: ${borderLeftWidth ?? borderWidth} ${borderLeftStyle ?? borderStyle} ${borderLeftColor ?? borderColor}; + border-bottom: ${borderBottomWidth ?? borderWidth} ${borderBottomStyle ?? borderStyle} ${borderBottomColor ?? borderColor}; + box-shadow: ${boxShadow}; border-radius: ${borderRadius ?? '8px'} !important; - padding: 0 !important; - border: ${borderSize ?? 0} ${borderType ?? 'solid'} ${borderColor ?? token.colorPrimary} !important; + ${rest} img { width: var(--thumbnail-width, 54px) !important; @@ -59,7 +110,7 @@ export const useStyles = createStyles(({ token, css, cx, prefixCls }, { borderSi object-fit: cover !important; display: flex !important; justify-content: center !important; - } + } .ant-image .anticon { border-radius: ${borderRadius ?? '8px'} !important; display: block !important; @@ -68,118 +119,182 @@ export const useStyles = createStyles(({ token, css, cx, prefixCls }, { borderSi .ant-upload-list-item-name { display: ${hideFileName ? 'none !important' : 'block'}; - color: ${fontColor ?? token.colorPrimary}; + color: ${color ?? token.colorPrimary}; + font-family: ${fontFamily ?? 'Segoe UI'}; + font-size: ${fontSize ?? '14px'}; + font-weight: ${fontWeight ?? '400'}; + text-align: ${textAlign ?? 'center'}; padding: 0 8px !important; - width: ${(layout && thumbnailWidth) ?? '54px'} !important; + width: ${(layout && width) ?? '54px'} !important; font-size: var(--font-size, 14px) !important; } - .ant-upload-drag:hover:not(.ant-upload-disabled) { + .ant-upload-list-text { + height: calc(var(--container-height) - 32px) !important; + max-height: calc(var(--container-max-height) - calc(${isDragger ? '0px' : fontSize} * 4)) !important; + min-height: calc(var(--container-min-height) - 32px) !important; + width: calc(var(--container-width) - 32px) !important; + max-width: calc(var(--container-max-width) - 32px) !important; + min-width: calc(var(--container-min-width) - 32px) !important; + } + + .ant-upload-drag:hover:not(.ant-upload-disabled) { border-color: ${token.colorPrimary} !important; - } + } - .${prefixCls}-upload { - ${(layout && !isDragger) && 'width: var(--thumbnail-width) !important;'}; - ${(layout && !isDragger) && 'height: var(--thumbnail-height) !important'}; - border-radius: ${borderRadius ?? '8px'} !important; - align-items: center; + .${prefixCls}-upload { + ${(layout && !isDragger) && 'width: var(--thumbnail-width) !important;'}; + ${(layout && !isDragger) && 'height: var(--thumbnail-height) !important'}; + ${rest} + align-items: center; - &.${prefixCls}-upload-btn { - padding: 8px 0; - - .${prefixCls}-upload-drag-icon { - margin: unset; - } - - .${storedFilesRendererNoFiles} { - margin-bottom: 6px; - } - - .ant-upload-select { - align-content: center; - } - } + &.${prefixCls}-upload-btn { + .${prefixCls}-upload-drag-icon { + margin: unset; } - - .${storedFilesRendererBtnContainer} { - display: flex; - margin-top: 4px; - justify-content: flex-end; - } - - .${prefixCls}-upload-list { - padding: 2px !important; - --ant-margin-xs: ${gap ?? '8px'} !important; - overflow-y: auto; - max-height: ${height ?? `calc(${uploadListMaxHeight} + 32px)`}; + + .${storedFilesRendererNoFiles} { + margin-bottom: 6px; } - .ant-upload-list-item-uploading { - display: none; - } - `); - - const antPreviewDownloadIcon = cx("ant-preview-download-icon", css` - background: #0000001A; - font-size: 24px; - padding: 8px; - border-radius: 100px; - :hover { - color: #fff; + .ant-upload-select { + align-content: center; + } } - `); - const shaStoredFilesRendererHorizontal = cx("sha-stored-files-renderer-horizontal", css` - height: max-content; - width: var(--container-width) !important; + } + + .ant-btn { + color: ${primaryColor ?? token.colorPrimary} !important; + padding: 0; + * { + font-size: ${fontSize ?? '14px'} !important; + font-weight: ${fontWeight ?? '400'} !important; + font-family: ${fontFamily ?? 'Segoe UI'} !important; + } + } + .${storedFilesRendererBtnContainer} { + display: flex; + margin-top: 4px; + justify-content: flex-end; + } + .${prefixCls}-upload-list { - display: flex !important; - flex-wrap: nowrap !important; - flex-direction: row !important; - flex-shrink: 0 !important; - overflow-x: auto; - overflow-y: clip !important; - align-items: center !important; + --ant-margin-xs: ${gap ?? '8px'} !important; + padding: 2px; + overflow-y: auto; + display: flex; + flex-direction: column; + scrollbar-width: thin; + &::-webkit-scrollbar { + width: 8px; + background-color: transparent; } + } + + .ant-upload-list-item-uploading { + display: none; + } - .ant-upload-list-item-container { - display: inline-block !important; - width: var(--thumbnail-width) !important; - height: var(--thumbnail-height) !important; - border-radius: ${borderRadius ?? '8px'} !important; - &.ant-upload-animate-inline-appear, - &.ant-upload-animate-inline-appear-active, - &.ant-upload-animate-inline { - display: none !important; - animation: none !important; - transition: none !important; + .ant-upload-list-item-container { + display: inline-block !important; + &.ant-upload-animate-inline-appear, + &.ant-upload-animate-inline-appear-active, + &.ant-upload-animate-inline { + display: none !important; + animation: none !important; + transition: none !important; + } + width: ${layout ? (width ?? '54px') + ' !important' : ''}; + height: ${layout ? (height ?? '54px') + ' !important' : ''}; + } + + .ant-upload-list-item-action { + > .ant-btn-icon { + > .anticon-delete { + color: ${token.colorError} !important; } } - `); + } - const shaStoredFilesRendererVertical = cx("sha-stored-files-renderer-horizontal", css` - width: max-content; - padding: 0 ${borderSize ?? '2px'} !important; + .thumbnail-stub { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + ${rest} + } + + .ant-upload-list-item-name-stub { + position: absolute; + bottom: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + } +`); + + const antPreviewDownloadIcon = cx("ant-preview-download-icon", css` + background: #0000001A; + font-size: 24px; + padding: 8px; + border-radius: 100px; + :hover { + color: #fff; + } +`); + const shaStoredFilesRendererHorizontal = cx("sha-stored-files-renderer-horizontal", css` + height: var(--container-height) !important; + width: var(--container-width) !important; + max-height: var(--container-max-height) !important; + max-width: var(--container-max-width) !important; + min-height: var(--container-min-height) !important; .${prefixCls}-upload-list { - display: flex !important; - flex-direction: column !important; - flex-wrap: nowrap !important; - padding: 0 ${borderSize ?? '2px'} !important; - height: calc(var(--height) - 32px) !important; - } + display: flex !important; + flex-wrap: nowrap !important; + flex-direction: row !important; + flex-shrink: 0 !important; + overflow-x: auto; + overflow-y: clip !important; + align-items: center !important; + height: max-content !important; + width: var(--container-width) !important; + min-width: var(--container-min-width) !important; + max-width: var(--container-max-width) !important; + } .ant-upload-list-item-container { - display: flex !important; - flex-direction: column !important; - justify-content: center !important; - width: var(--thumbnail-width) !important; + display: inline-block !important; + max-width: var(--thumbnail-width) !important; height: var(--thumbnail-height) !important; + border-radius: ${borderRadius ?? '8px'} !important; &.ant-upload-animate-inline-appear, - &.ant-upload-animate-inline-appear-active, - &.ant-upload-animate-inline { - display: none !important; - animation: none !important; - transition: none !important; - } + &.ant-upload-animate-inline-appear-active, + &.ant-upload-animate-inline { + display: none !important; + animation: none !important; + transition: none !important; + } + } +`); + + const shaStoredFilesRendererVertical = cx("sha-stored-files-renderer-vertical", css` + --container-width: max-content !important; + max-width: max-content !important; + width: max-content !important; + min-width: max-content !important; + height: var(--container-height) !important; + max-height: var(--container-max-height) !important; + min-height: var(--container-min-height) !important; + .${prefixCls}-upload-list { + display: flex !important; + flex-direction: column-reverse !important; + flex-wrap: nowrap !important; + padding: 2px ${borderWidth ?? '2px'} !important; + height: var(--container-height) !important; + width: 100% !important; + max-height: calc(var(--container-max-height) - 32px) !important; + min-height: calc(var(--container-min-height) - 32px) !important; } .stored-files-renderer-btn-container { @@ -187,39 +302,57 @@ export const useStyles = createStyles(({ token, css, cx, prefixCls }, { borderSi .ant-btn { padding: 0; } - } + } - `); + .ant-upload-list-item-container { + display: inline-block !important; + width: var(--thumbnail-width) !important; + height: var(--thumbnail-height) !important; + border-radius: ${borderRadius ?? '8px'} !important; + &.ant-upload-animate-inline-appear, + &.ant-upload-animate-inline-appear-active, + &.ant-upload-animate-inline { + display: none !important; + animation: none !important; + transition: none !important; + } + } +`); - const shaStoredFilesRendererGrid = cx("sha-stored-files-renderer-horizontal", css` + const shaStoredFilesRendererGrid = cx("sha-stored-files-renderer-grid", css` max-width: var(--container-width) !important; max-height: var(--container-height) !important; .${prefixCls}-upload-list { align-items: center; padding: 2px; - max-height: calc(var(--container-height) - 32px) !important; - .${prefixCls}-upload-list-item { - width: 100% !important; - height: 100% !important; - } - } + height: var(--container-height) !important; + width: var(--container-width) !important; + max-height: var(--container-max-height) !important; + max-width: var(--container-max-width) !important; + min-height: var(--container-min-height) !important; + min-width: var(--container-min-width) !important; + .${prefixCls}-upload-list-item { + width: 100% !important; + height: 100% !important; + border-radius: ${borderRadius ?? '8px'} !important; + } + } - .ant-upload-list-item-container { - display: flex !important; - flex-direction: column !important; - justify-content: center !important; - width: var(--thumbnail-width) !important; - height: var(--thumbnail-height) !important; - &.ant-upload-animate-inline-appear, - &.ant-upload-animate-inline-appear-active, - &.ant-upload-animate-inline { - display: none !important; - animation: none !important; - transition: none !important; - } - } - `); + .ant-upload-list-item-container { + display: inline-block !important; + width: var(--thumbnail-width) !important; + height: var(--thumbnail-height) !important; + border-radius: ${borderRadius ?? '8px'} !important; + &.ant-upload-animate-inline-appear, + &.ant-upload-animate-inline-appear-active, + &.ant-upload-animate-inline { + display: none !important; + animation: none !important; + transition: none !important; + } + } +`); return { shaStoredFilesRenderer, diff --git a/shesha-reactjs/src/components/tablePager/index.tsx b/shesha-reactjs/src/components/tablePager/index.tsx index 516b241402..aaab65429e 100644 --- a/shesha-reactjs/src/components/tablePager/index.tsx +++ b/shesha-reactjs/src/components/tablePager/index.tsx @@ -1,17 +1,34 @@ -import React, { FC } from 'react'; +import React, { CSSProperties, FC } from 'react'; import { useMediaQuery } from 'react-responsive'; import { DESKTOP_SIZE_QUERY, PHONE_SIZE_QUERY } from '@/shesha-constants/media-queries'; import { useDataTable } from '@/providers'; import TablePaging from './tablePaging'; import TableNoPaging from './tableNoPaging'; +import { IFontValue } from '@/designer-components/_settings/utils/font/interfaces'; +import { IShadowValue } from '@/designer-components/_settings/utils/shadow/interfaces'; +import { IBackgroundValue } from '@/designer-components/_settings/utils/background/interfaces'; +import { IBorderValue } from '@/designer-components/_settings/utils/border/interfaces'; export interface ITablePagerProps { showSizeChanger?: boolean; showTotalItems?: boolean; + font?: IFontValue; + shadow?: IShadowValue; + background?: IBackgroundValue; + border?: IBorderValue; + style?: CSSProperties; } -export const TablePager: FC = ({ showSizeChanger, showTotalItems }) => { - const { pageSizeOptions, currentPage, totalRows, selectedPageSize, setCurrentPage, changePageSize, dataFetchingMode } = useDataTable(); +export const TablePager: FC = ({ showSizeChanger, showTotalItems, style }) => { + const { + pageSizeOptions, + currentPage, + totalRows, + selectedPageSize, + setCurrentPage, + changePageSize, + dataFetchingMode, + } = useDataTable(); const hideSizeChanger = useMediaQuery({ query: DESKTOP_SIZE_QUERY, @@ -21,26 +38,26 @@ export const TablePager: FC = ({ showSizeChanger, showTotalIte query: PHONE_SIZE_QUERY, }); - if (totalRows === undefined || totalRows === null) - return null; + if (totalRows === undefined || totalRows === null) return null; - return dataFetchingMode === 'paging' - ? ( - - ) - : ; + return dataFetchingMode === 'paging' ? ( + + ) : ( + + ); }; export default TablePager; diff --git a/shesha-reactjs/src/components/tablePager/style.ts b/shesha-reactjs/src/components/tablePager/style.ts new file mode 100644 index 0000000000..eaf02e3120 --- /dev/null +++ b/shesha-reactjs/src/components/tablePager/style.ts @@ -0,0 +1,46 @@ +import { createStyles } from '@/styles'; + +export const useStyles = createStyles(({ css, cx }, { style }) => { + const pager = cx("sha-pager", css` + * { + --ant-pagination-item-size-sm: calc(${style?.fontSize} * 1.5px) !important; + --ant-pagination-item-size: calc(${style?.fontSize} * 1.5px) !important; + font-size: ${style?.fontSize} !important; + color: ${style?.color} !important; + font-weight: ${style?.fontWeight} !important; + -ms-overflow-style: none; + scrollbar-width: none; + font-family: ${style?.fontFamily} !important; + .ant-pagination-item-container { + display: flex !important; + align-items: center !important; + } + .ant-pagination-item-ellipsis { + position: relative !important; + } + .ant-pagination-item-link-icon{ + position: absolute !important; + } + .ant-pagination-next button { + font-size: ${style?.fontSize} !important; + } + + .ant-select { + --ant-color-text : ${style?.color} !important; + --ant-font-size : ${style?.fontSize} !important; + --ant-font-family : ${style?.fontFamily} !important; + height: calc(${style?.fontSize} * 1.5); + + .ant-select-selection-item { + height: calc(${style?.fontSize} * 1.5); + line-height: calc(${style?.fontSize} * 1.5) !important; + font-weight: ${style?.fontWeight} !important; + } + } + } + `); + + return { + pager, + }; +}); \ No newline at end of file diff --git a/shesha-reactjs/src/components/tablePager/tableNoPaging.tsx b/shesha-reactjs/src/components/tablePager/tableNoPaging.tsx index 5020f6a2c2..4a96930467 100644 --- a/shesha-reactjs/src/components/tablePager/tableNoPaging.tsx +++ b/shesha-reactjs/src/components/tablePager/tableNoPaging.tsx @@ -1,20 +1,22 @@ -import React, { FC } from 'react'; +import React, { CSSProperties, FC } from 'react'; import { useMedia } from 'react-use'; export interface ITablePagerBaseProps { /** Total number of rows to display on the table */ totalRows: number; + style?: CSSProperties; } export const TableNoPaging: FC = ({ - totalRows + totalRows, + style, }) => { const isWider = useMedia('(min-width: 1202px)'); if (!isWider) return null; return ( - Total {totalRows} items + Total {totalRows} items ); }; diff --git a/shesha-reactjs/src/components/tablePager/tablePaging.tsx b/shesha-reactjs/src/components/tablePager/tablePaging.tsx index a0caff3bb5..8852004528 100644 --- a/shesha-reactjs/src/components/tablePager/tablePaging.tsx +++ b/shesha-reactjs/src/components/tablePager/tablePaging.tsx @@ -1,6 +1,7 @@ import React, { FC, useEffect } from 'react'; import { Pagination } from 'antd'; import { useMedia } from 'react-use'; +import { useStyles } from './style'; export interface ITablePagerBaseProps { /** Whether this component */ @@ -29,6 +30,7 @@ export interface ITablePagerBaseProps { /** A function to change */ changePageSize: (size: number) => void; + style?: any; } export const TablePaging: FC = ({ @@ -41,8 +43,10 @@ export const TablePaging: FC = ({ showTotalItems = true, setCurrentPage, changePageSize, + style, }) => { const isWider = useMedia('(min-width: 1202px)'); + const { styles } = useStyles({ style }); const onPageNumberChange = (page: number, pageSize?: number) => { setCurrentPage(page); @@ -70,9 +74,11 @@ export const TablePaging: FC = ({ return ( `${s}`)} + pageSizeOptions={(pageSizeOptions || []).map((s) => `${s}`)} current={currentPage} pageSize={selectedPageSize} showSizeChanger={showSizeChanger} diff --git a/shesha-reactjs/src/components/validationErrors/index.tsx b/shesha-reactjs/src/components/validationErrors/index.tsx index bc5b3263a2..c4efad29af 100644 --- a/shesha-reactjs/src/components/validationErrors/index.tsx +++ b/shesha-reactjs/src/components/validationErrors/index.tsx @@ -28,6 +28,14 @@ export const ValidationErrors: FC = ({ if (!error) return null; const renderValidationErrors = (props: AlertProps) => { + const widthStyle = props.style?.width && props.style?.marginLeft && props.style?.marginRight + ? { + width: `calc(${props.style.width} - (${props.style.marginLeft} + ${props.style.marginRight}))`, + maxWidth: `calc(${props.style.width} - (${props.style.marginLeft} + ${props.style.marginRight}))`, + minWidth: `calc(${props.style.width} - (${props.style.marginLeft} + ${props.style.marginRight}))` + } + : {}; + if (renderMode === 'alert') { return ( = ({ showIcon closable {...props} + style={{ ...props.style, ...widthStyle }} /> ); } diff --git a/shesha-reactjs/src/components/validationErrors/styles/styles.ts b/shesha-reactjs/src/components/validationErrors/styles/styles.ts index d89b5ff80f..822a55d152 100644 --- a/shesha-reactjs/src/components/validationErrors/styles/styles.ts +++ b/shesha-reactjs/src/components/validationErrors/styles/styles.ts @@ -1,10 +1,7 @@ -import { createStyles } from '@/styles'; -import { sheshaStyles } from '@/styles'; +import { createStyles, sheshaStyles } from '@/styles'; export const useStyles = createStyles(({ css, cx, prefixCls }) => { const shaValidationErrorAlert = cx("sha-validation-error-alert", css` - margin-bottom: ${sheshaStyles.paddingMD}px !important; - padding: ${sheshaStyles.paddingLG}px !important; .${prefixCls}-alert-description { >ul { diff --git a/shesha-reactjs/src/designer-components/_common-migrations/migrateFormLifecycle.ts b/shesha-reactjs/src/designer-components/_common-migrations/migrateFormLifecycle.ts index 245e939ae9..e74cddaf0f 100644 --- a/shesha-reactjs/src/designer-components/_common-migrations/migrateFormLifecycle.ts +++ b/shesha-reactjs/src/designer-components/_common-migrations/migrateFormLifecycle.ts @@ -2,7 +2,9 @@ import { IFormLifecycleSettings, IFormSettings } from "@/interfaces"; import { IKeyValue } from "@/interfaces/keyValue"; import { GqlLoaderSettings } from "@/providers/form/loaders/interfaces"; import { GqlSubmitterSettings } from "@/providers/form/submitters/interfaces"; -import { extractJsFieldFromKeyValue } from "./keyValueUtils"; +import { convertFormMarkupToFlatStructure } from "@/index"; +import { IFormMigrationContext } from "./models"; +import { setValueByPropertyName } from "@/utils/object"; const getPrepareSubmitData = (preparedValues: string): string => { const normalizedPreparedValues = (preparedValues ?? "").trim(); @@ -26,17 +28,25 @@ const getBeforeDataLoad = (onInitialized: string): string => { const getAfterDataLoad = (onDataLoaded: string, initialValues?: IKeyValue[]): string => { if (!initialValues || initialValues.length === 0) return null; - - let result = " const initialValues = {\r\n"; - initialValues.forEach(item => { - if (item.key) { - const value = extractJsFieldFromKeyValue(item.value?.trim()); - const currentPropLine = ` ${item.key}: ${value},\r\n`; - result += currentPropLine; - } - }); - result += " };\r\n"; - result += " form.setFieldsValue(initialValues);"; + + // Convert to JSON + const initialData = {}; + const initValues = [...initialValues, {key: "__shaFormData", value: 0}]; + initValues.forEach(item => { + const value = typeof item.value === "string" && /{(.*?)}/gm.test(item.value) + ? "'" + item.value.replaceAll("{", "' + ").replaceAll("}", " + '") + "'" // replace Mustache syntax if needed + : item.value; + setValueByPropertyName(initialData, item.key, value); + }); + const initialObjString = JSON.stringify(initialData, null, 4) + .replaceAll("\"'' + ", "").replaceAll(" + ''\"", "") + .replaceAll("\"'", "'").replaceAll("'\"", "'") + .replace("\"__shaFormData\": 0", "...form.data"); // add loaded form data + + let result = '\r\n// Migrated from Initial Values and components defaults\r\n'; + result += `const initialData = ${initialObjString};\r\n`; + result += 'form.setFieldsValue(initialData);\r\n'; + result += '// ----------------------------------------------------\r\n\r\n'; const normalizedJs = onDataLoaded?.trim(); if (normalizedJs) @@ -45,6 +55,21 @@ const getAfterDataLoad = (onDataLoaded: string, initialValues?: IKeyValue[]): st return result; }; +export const migrateDefaults = (settings: IFormSettings, context: IFormMigrationContext) => { + const initialData: IKeyValue[] = []; + const flatStructure = convertFormMarkupToFlatStructure(context.form.markup, settings, context.designerComponents); + for(const id in flatStructure.allComponents) { + if (!flatStructure.allComponents.hasOwnProperty(id)) continue; + const component = flatStructure.allComponents[id]; + if (component.defaultValue !== undefined) + initialData.push({ key: component.propertyName, value: component.defaultValue }); + if (component['initialValue'] !== undefined) + initialData.push({ key: component.propertyName, value: component['initialValue'] }); + } + const onAfterDataLoad = getAfterDataLoad(settings.onAfterDataLoad, initialData); + return { ...settings, onAfterDataLoad }; +}; + export const migrateFormLifecycle = (settings: IFormSettings): IFormSettings => { const { fieldsToFetch, diff --git a/shesha-reactjs/src/designer-components/_common-migrations/migrateStyles.ts b/shesha-reactjs/src/designer-components/_common-migrations/migrateStyles.ts new file mode 100644 index 0000000000..b34cdbbe56 --- /dev/null +++ b/shesha-reactjs/src/designer-components/_common-migrations/migrateStyles.ts @@ -0,0 +1,103 @@ +import { nanoid } from "@/utils/uuid"; +import { addPx } from '@/utils/style'; +import { ICommonContainerProps, IConfigurableFormComponent, IInputStyles, IStyleType } from "@/interfaces"; + +type ExtendedType = IInputStyles & Omit & { block?: boolean }; + +export const migrateStyles = (prev: T, defaults?: Omit, screen?: 'desktop' | 'tablet' | 'mobile'): IStyleType => { + const prevStyles: IInputStyles = screen && prev[`${screen}`] ? prev[`${screen}`] : prev; + + const border = (side) => ({ + ...prev?.border?.border?.[side], + width: prevStyles?.borderSize ?? prevStyles?.borderWidth ?? prev?.border?.border?.[side]?.width ?? defaults?.border?.border?.[side]?.width, + style: prevStyles?.borderType ?? prevStyles?.borderStyle ?? prev?.border?.border?.[side]?.style ?? defaults?.border?.border?.[side]?.style, + color: prevStyles?.borderColor ?? prev?.border?.border?.[side]?.color ?? defaults?.border?.border?.[side]?.color + }); + + const heightFromSize = prevStyles?.size === 'small' ? '24px' : prevStyles?.size === 'large' ? '40px' : null; + const fontSizeFromSize = prevStyles?.size === 'small' ? 14 : prevStyles?.size === 'large' ? 16 : null; + const isColor = prevStyles.backgroundType === 'color' || prev.backgroundType === 'color'; + const isBase64 = prevStyles.backgroundDataSource === 'base64' || prev.backgroundDataSource === 'base64'; + const isUrl = prevStyles.backgroundDataSource === 'url' || prev.backgroundDataSource === 'url'; + const isStoredFile = prevStyles.backgroundDataSource === 'storedFileId' || prev.backgroundDataSource === 'storedFileId'; + + const backgroundType = isColor ? 'color' : isBase64 ? 'image' : isUrl ? 'url' : isStoredFile ? 'storedFile' : 'color'; + const backgroundUrl = prevStyles.backgroundUrl || prev.backgroundUrl; + const backgroundBase64 = prevStyles.backgroundBase64 || prev.backgroundBase64; + const backgroundStoredFileId = prevStyles.backgroundStoredFileId || prev.backgroundStoredFileId; + const backgroundColor = prevStyles.backgroundColor || prev.backgroundColor; + const backgroundRepeat = prevStyles.backgroundRepeat || prev.backgroundRepeat; + const backgroundCover = prevStyles.backgroundCover || prev.backgroundCover; + + return { + size: prevStyles?.size, + border: { + hideBorder: prevStyles?.hideBorder || defaults?.hideBorder || false, + radiusType: defaults?.border?.radiusType || 'all', + borderType: defaults?.border?.borderType || 'all', + border: { + all: border('all'), + top: border('top'), + bottom: border('bottom'), + left: border('left'), + right: border('right'), + }, + radius: { + all: prevStyles?.borderRadius || defaults?.border?.radius?.all, + topLeft: defaults?.border?.radius?.topLeft, + topRight: defaults?.border?.radius?.topRight, + bottomLeft: defaults?.border?.radius?.bottomLeft, + bottomRight: defaults?.border?.radius?.bottomRight + }, + }, + background: { + type: backgroundType, + color: backgroundColor || defaults?.background?.color, + repeat: backgroundRepeat || defaults?.background?.repeat || 'no-repeat', + size: backgroundCover || defaults?.background?.size || 'cover', + position: 'center', + gradient: { direction: 'to right', colors: {} }, + url: backgroundUrl || defaults?.background?.url || '', + storedFile: { id: backgroundStoredFileId || null }, + uploadFile: backgroundBase64 ? { uid: nanoid(), name: '', url: backgroundBase64 } : null, + }, + font: { + color: prevStyles?.fontColor || prevStyles.color || defaults?.font?.color, + type: prevStyles?.font?.type || defaults?.font?.type, + align: prevStyles?.font?.align || defaults?.font?.align || 'left', + size: prevStyles?.fontSize as number || fontSizeFromSize || defaults?.font?.size, + weight: prevStyles?.fontWeight as string || defaults?.font?.weight || '400', + }, + dimensions: { + width: prev.block ? '100%' : addPx(prevStyles?.width) ?? addPx(prev?.width) ?? addPx(prev?.dimensions?.width) ?? defaults?.dimensions?.width, + height: addPx(prevStyles?.height) ?? heightFromSize ?? addPx(prev?.dimensions?.height) ?? defaults?.dimensions?.height, + minHeight: addPx(prev?.dimensions?.minHeight) ?? defaults?.dimensions?.minHeight, + maxHeight: addPx(prev?.dimensions?.maxHeight) ?? defaults?.dimensions?.maxHeight, + minWidth: addPx(prev?.dimensions?.minWidth) ?? defaults?.dimensions?.minWidth, + maxWidth: addPx(prev?.dimensions?.maxWidth) ?? defaults?.dimensions?.maxWidth, + }, + shadow: { + offsetX: defaults?.shadow?.offsetX || 0, + offsetY: defaults?.shadow?.offsetY || 0, + color: defaults?.shadow?.color || '#000', + blurRadius: defaults?.shadow?.blurRadius || 0, + spreadRadius: defaults?.shadow?.spreadRadius || 0 + }, + ...(defaults?.display && { display: defaults?.display || 'block' }), + stylingBox: prev?.stylingBox || defaults?.stylingBox || '{}', + }; +}; + +export const migratePrevStyles = (prev: T, defaults?: Omit) => { + + + const result: T = { + ...prev, + desktop: { ...prev.desktop, ...migrateStyles(prev, defaults, 'desktop') }, + tablet: { ...prev.tablet, ...migrateStyles(prev, defaults, 'tablet') }, + mobile: { ...prev.mobile, ...migrateStyles(prev, defaults, 'mobile') }, + }; + + return result; + +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/_common-migrations/migrateVisibility.ts b/shesha-reactjs/src/designer-components/_common-migrations/migrateVisibility.ts index b5887f7768..2853a94c6f 100644 --- a/shesha-reactjs/src/designer-components/_common-migrations/migrateVisibility.ts +++ b/shesha-reactjs/src/designer-components/_common-migrations/migrateVisibility.ts @@ -16,4 +16,4 @@ export const migrateVisibility = (prev: T delete result[visibilityName]; return result; -}; \ No newline at end of file +}; diff --git a/shesha-reactjs/src/designer-components/_common-migrations/models.ts b/shesha-reactjs/src/designer-components/_common-migrations/models.ts new file mode 100644 index 0000000000..6269b3c6ee --- /dev/null +++ b/shesha-reactjs/src/designer-components/_common-migrations/models.ts @@ -0,0 +1,6 @@ +import { IFormDto, IToolboxComponents } from "@/index"; + +export interface IFormMigrationContext { + form: IFormDto; + designerComponents: IToolboxComponents; +} \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/_legacyComponents/divider/index.tsx b/shesha-reactjs/src/designer-components/_legacyComponents/divider/index.tsx index 3dc6404f0d..d6e7fd119c 100644 --- a/shesha-reactjs/src/designer-components/_legacyComponents/divider/index.tsx +++ b/shesha-reactjs/src/designer-components/_legacyComponents/divider/index.tsx @@ -17,6 +17,7 @@ export interface IDividerProps extends IConfigurableFormComponent { const settingsForm = settingsFormJson as FormMarkup; +/** @deprecated: Use Section Separator instead */ const DividerComponent: IToolboxComponent = { type: 'divider', isInput: false, diff --git a/shesha-reactjs/src/designer-components/_settings/components/formItem.tsx b/shesha-reactjs/src/designer-components/_settings/components/formItem.tsx new file mode 100644 index 0000000000..b11992a5ac --- /dev/null +++ b/shesha-reactjs/src/designer-components/_settings/components/formItem.tsx @@ -0,0 +1,74 @@ +import React, { cloneElement, FC, ReactElement, useState } from 'react'; +import { ConfigurableFormItem } from '@/components'; +import SettingsControl from '../settingsControl'; +import { ISettingsFormItemProps } from '../settingsFormItem'; +import { useStyles } from '../styles/styles'; + +const FormItem: FC = (props) => { + const { styles } = useStyles(); + const { name, label, tooltip, required, hidden, jsSetting, children, valuePropName = 'value', layout } = props; + const [hasCode, setHasCode] = useState(false); + + const childElement = children as ReactElement; + const readOnly = props.readOnly || childElement.props.readOnly || childElement.props.disabled; + + const handleChange = (onChange) => (...args: any[]) => { + const event = args[0]; + const data = event && event.target && typeof event.target === 'object' && valuePropName in event.target + ? (event.target as HTMLInputElement)[valuePropName] + : event; + onChange(data); + }; + + const createClonedElement = (value, onChange) => cloneElement( + childElement, + { + ...childElement?.props, + readOnly: readOnly, + size: 'small', + disabled: readOnly, + onChange: handleChange(onChange), + [valuePropName]: value + } + ); + + return ( + {label}
, + type: '', + id: '', + description: tooltip, + validate: { required }, + hidden, + layout, + size: 'small', + + }} + + className='sha-js-label' + > + {(value, onChange) => + !jsSetting ? ( + createClonedElement(value, onChange) + ) : ( + + {(value, onChange) => createClonedElement(value, onChange,)} + + ) + } + + ); +}; + +export default FormItem; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/_settings/settingsCollapsiblePanel.tsx b/shesha-reactjs/src/designer-components/_settings/settingsCollapsiblePanel.tsx index 23d542dcf3..e05fe29263 100644 --- a/shesha-reactjs/src/designer-components/_settings/settingsCollapsiblePanel.tsx +++ b/shesha-reactjs/src/designer-components/_settings/settingsCollapsiblePanel.tsx @@ -8,7 +8,7 @@ interface ISettingsCollapsiblePanelProps extends ICollapsiblePanelProps { } export interface ISettingsCollapsiblePanelActionsContext { registerField: (name: string) => void; } - + export const SettingsCollapsiblePanelActionsContext = createNamedContext(undefined, "SettingsCollapsiblePanelActionsContext"); const SettingsCollapsiblePanel: FC = (props) => { @@ -22,14 +22,14 @@ const SettingsCollapsiblePanel: FC = (props) => const settingsCollapsiblePanelActions: ISettingsCollapsiblePanelActionsContext = { registerField }; - const show = !fields || fields.length === 0 || typeof propertyFilter !== 'function' + const show = !fields || fields.length === 0 || typeof propertyFilter !== 'function' || Boolean(fields.find(x => (propertyFilter(x)))); return ( {show - ? - : null} + ? + : null} ); }; @@ -38,7 +38,7 @@ export function useSettingsPanel(required: Boolean) { const actionsContext = useContext(SettingsCollapsiblePanelActionsContext); if (actionsContext === undefined && required) throw new Error('useSettingsPanel must be used within a SettingsCollapsiblePanel'); - + return actionsContext; } diff --git a/shesha-reactjs/src/designer-components/_settings/settingsComponent.tsx b/shesha-reactjs/src/designer-components/_settings/settingsComponent.tsx index 9e2016141c..d8bbadce8b 100644 --- a/shesha-reactjs/src/designer-components/_settings/settingsComponent.tsx +++ b/shesha-reactjs/src/designer-components/_settings/settingsComponent.tsx @@ -6,14 +6,13 @@ import { IConfigurableFormComponent, ShaForm } from '@/providers'; import { IToolboxComponent } from '@/interfaces'; import { migrateReadOnly } from '../_common-migrations/migrateSettings'; import { SettingOutlined } from '@ant-design/icons'; +import { ICodeExposedVariable } from '@/components/codeVariablesTable'; import { SettingComponentContainer } from './settingContainer/settingComponentContainer'; -import { useActualContextData } from '@/hooks/useActualContextData'; +import { useActualContextData } from '@/hooks'; export interface ISettingsComponentProps extends IConfigurableFormComponent { + exposedVariables?: ICodeExposedVariable[]; components?: IConfigurableFormComponent[]; - availableConstantsExpression?: string; - resultTypeExpression?: string; - useAsyncEvaluation?: boolean; } const SettingsComponent: IToolboxComponent = { @@ -37,28 +36,26 @@ const SettingsComponent: IToolboxComponent = { }, [model.hidden, model?.readOnly, model?.id, actualSourceComponent]); if (model.hidden) return null; - + return ( - - {(value, onChange) => ( - - {(_valueValue, _onChangeValue, propertyName) => { - return ( - - ); - }} - - )} - + + {(value, onChange) => ( + + {(_valueValue, _onChangeValue, propertyName) => { + return ( + + ); + }} + + )} + ); }, settingsFormMarkup: getSettings(), diff --git a/shesha-reactjs/src/designer-components/_settings/settingsControl.tsx b/shesha-reactjs/src/designer-components/_settings/settingsControl.tsx index afea072440..a04229d30c 100644 --- a/shesha-reactjs/src/designer-components/_settings/settingsControl.tsx +++ b/shesha-reactjs/src/designer-components/_settings/settingsControl.tsx @@ -1,22 +1,24 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, ReactNode, useEffect } from 'react'; import { getPropertySettingsFromValue } from './utils'; import { CodeEditor, IPropertySetting, PropertySettingMode } from '@/index'; -import { Button } from 'antd'; import { useStyles } from './styles/styles'; -import { isEqual } from 'lodash'; import { ICodeExposedVariable } from '@/components/codeVariablesTable'; import camelcase from 'camelcase'; import { GetAvailableConstantsFunc, GetResultTypeFunc, ICodeEditorProps } from '../codeEditor/interfaces'; import { CodeEditorWithStandardConstants } from '../codeEditor/codeEditorWithConstants'; import { useConstantsEvaluator } from '../codeEditor/hooks/useConstantsEvaluator'; import { useResultTypeEvaluator } from '../codeEditor/hooks/useResultType'; +import { Button } from 'antd'; +import { CodeOutlined, CodeFilled } from '@ant-design/icons'; -export type SettingsControlChildrenType = (value: any, onChange: (val: any) => void, propertyName: string) => ReactElement; +export type SettingsControlChildrenType = (value: any, onChange: (val: any) => void, propertyName: string) => ReactElement | ReactNode; export interface ISettingsControlProps { propertyName: string; readOnly?: boolean; value?: IPropertySetting; + setHasCode?: (hasCode: boolean) => void; + hasCode?: boolean; mode: PropertySettingMode; onChange?: (value: IPropertySetting) => void; readonly children?: SettingsControlChildrenType; @@ -25,7 +27,7 @@ export interface ISettingsControlProps { useAsyncEvaluation?: boolean; } -const defaultExposedVariables: ICodeExposedVariable[] = [ +export const defaultExposedVariables: ICodeExposedVariable[] = [ { name: "data", description: "Selected form values", type: "object" }, { name: "pageContext", description: "Contexts data of current page", type: "object" }, { name: "contexts", description: "Contexts data", type: "object" }, @@ -41,14 +43,14 @@ const defaultExposedVariables: ICodeExposedVariable[] = [ export const SettingsControl = (props: ISettingsControlProps) => { - const { styles } = useStyles(); - const constantsEvaluator = useConstantsEvaluator({ availableConstantsExpression: props.availableConstantsExpression }); const resultType = useResultTypeEvaluator({ resultTypeExpression: props.resultTypeExpression }); const setting = getPropertySettingsFromValue(props.value); const { _mode: mode, _code: code } = setting; + const { styles } = useStyles(); + const onInternalChange = (value: IPropertySetting, m?: PropertySettingMode) => { const newSetting = { ...value, _mode: (m ?? mode) }; const newValue = !!newSetting._code || newSetting._mode === 'code' ? newSetting : value._value; @@ -56,16 +58,18 @@ export const SettingsControl = (props: ISettingsControlProps props.onChange(newValue); }; + useEffect(() => { + onInternalChange({ ...setting, _mode: mode }, mode); + }, [mode]); + const codeOnChange = (val: any) => { const newValue = { ...setting, _code: val }; onInternalChange(newValue); }; const valueOnChange = (val: any) => { - if (!isEqual(setting?._value, val)) { - const newValue = { ...setting, _value: val }; - onInternalChange(newValue); - } + const newValue = { ...setting, _value: val }; + onInternalChange(newValue); }; const onSwitchMode = () => { @@ -73,6 +77,7 @@ export const SettingsControl = (props: ISettingsControlProps onInternalChange(setting, newMode); }; + const propertyName = !!setting._code || setting._mode === 'code' ? `${props.propertyName}._value` : props.propertyName; const functionName = `get${camelcase(props.propertyName, { pascalCase: true })}`; @@ -85,35 +90,38 @@ export const SettingsControl = (props: ISettingsControlProps propertyName: props.propertyName + 'Code', fileName: props.propertyName, wrapInTemplate: true, - templateSettings: { + templateSettings: { functionName: functionName, useAsyncDeclaration: props.useAsyncEvaluation, }, - exposedVariables: defaultExposedVariables + type: 'text', + label: ' ', + ghost: true, + exposedVariables: defaultExposedVariables, + hidden: !setting._code && props.readOnly, }; const editor = constantsEvaluator - ? - : ; + + ? + : ; return (
-
- {mode === 'code' && editor} - {mode === 'value' && props.children(setting?._value, valueOnChange, propertyName)} -
+ /> + {mode === 'code' && editor} + {mode === 'value' &&
+ {props.children(setting?._value, valueOnChange, propertyName)} +
}
); }; diff --git a/shesha-reactjs/src/designer-components/_settings/settingsForm.tsx b/shesha-reactjs/src/designer-components/_settings/settingsForm.tsx index 72d7462eac..95275ec1d7 100644 --- a/shesha-reactjs/src/designer-components/_settings/settingsForm.tsx +++ b/shesha-reactjs/src/designer-components/_settings/settingsForm.tsx @@ -99,6 +99,7 @@ const SettingsForm = (props: PropsWithChildren {props.children} diff --git a/shesha-reactjs/src/designer-components/_settings/settingsFormItem.tsx b/shesha-reactjs/src/designer-components/_settings/settingsFormItem.tsx index d16cd86206..c6cb7401ff 100644 --- a/shesha-reactjs/src/designer-components/_settings/settingsFormItem.tsx +++ b/shesha-reactjs/src/designer-components/_settings/settingsFormItem.tsx @@ -3,7 +3,7 @@ import React, { FC, ReactElement, useEffect - } from 'react'; +} from 'react'; import SettingsControl, { SettingsControlChildrenType } from './settingsControl'; import { ConfigurableFormItem, IConfigurableFormItemProps } from '@/components'; import { Form, FormItemProps } from 'antd'; @@ -12,7 +12,7 @@ import { useSettingsForm } from './settingsForm'; import { useSettingsPanel } from './settingsCollapsiblePanel'; import { getFieldNameFromExpression } from '@/index'; -interface ISettingsFormItemProps extends Omit { +export interface ISettingsFormItemProps extends Omit { name?: string; label?: string; jsSetting?: boolean; @@ -22,6 +22,9 @@ interface ISettingsFormItemProps extends Omit = (props) => { @@ -32,35 +35,46 @@ const SettingsFormComponent: FC = (props) => { const { _mode: mode } = getPropertySettingsFromData(model, props.name?.toString()); + const formProps: FormItemProps = { + name: getFieldNameFromExpression(props.name), + label: props.label, + style: props.style, + required: props.required, + tooltip: props.tooltip, + hidden: props.hidden, + valuePropName: props.valuePropName, + }; + if (typeof props.children === 'function') { - const children = props.children as SettingsControlChildrenType; - if (!props.jsSetting) { - return - {children} - ; - } - return ( - - - {(value, onChange, propertyName) => children(value, onChange, propertyName)} - - - ); + const children = props.children as SettingsControlChildrenType; + if (!props.jsSetting) { + return + {children} + ; + } + + return ( + + + {(value, onChange, propertyName) => children(value, onChange, propertyName)} + + + ); } if (!props.jsSetting) { - return }>{props.children}; + return {props.children}; } const valuePropName = props.valuePropName ?? 'value'; diff --git a/shesha-reactjs/src/designer-components/_settings/styles/styles.ts b/shesha-reactjs/src/designer-components/_settings/styles/styles.ts index 541dd5ea9a..dc72921961 100644 --- a/shesha-reactjs/src/designer-components/_settings/styles/styles.ts +++ b/shesha-reactjs/src/designer-components/_settings/styles/styles.ts @@ -1,81 +1,94 @@ -import { createStyles } from '@/styles'; -import { sheshaStyles } from '@/styles'; +import { createStyles, sheshaStyles } from '@/styles'; -export const useStyles = createStyles(({ css, cx, responsive, prefixCls }) => { +export const useStyles = createStyles(({ css, cx, responsive, token }) => { + + const contentJs = cx(css` + position: relative; + top: 0px; + `); + + const contentCode = cx(css` + position: relative; + top: 0px; + ${responsive.mobile} { + padding-top: 4px; + } + + `); + + const jsContent = cx(css` + position: relative; + ${responsive.mobile} { + margin-left: 28px; + } + + // special style when inside the sidebar + .sidebar-container & { + margin-right: 0; + } + `); + + const label = cx("properties-label", css` + font-size: 12px; + color: darkslategrey; + font-weight: 500; + position: relative; + + +.ant-form-item-tooltip { + align-self: end !important; + position: relative; + bottom: -2px; + margin-right: 8px; + } + + +.sha-required-mark { + position: relative; + bottom: -8px; + } + `); + + const bindingOptionsBtn = cx(css` + top: -8px; + `); const jsSwitch = cx(css` - &.${prefixCls}-btn { position: absolute; - left: 0; - right: auto; + right: 0; top: 4px; font-size: 12px; height: 20px; - width: 2.5rem; + max-width: 100%; margin-left: 5px; - margin-right: 5px; + margin-right: 0px; + color: ${token.colorPrimary}; display: flex; justify-content: center; align-items: center; ${responsive.mobile} { right: 0; left: auto; - top: -28px; + top: -25px; } // special style when inside the sidebar .sidebar-container & { right: 0; left: auto; - top: -28px; + top: -25px; } .${sheshaStyles.verticalSettingsClass} & { right: 0; left: auto; - top: -28px; + top: -25px; } - } - `); - - const contentJs = cx(css` - position: relative; - top: 0px; - `); - const contentCode = cx(css` - position: relative; - top: 0px; - ${responsive.mobile} { - padding-top: 4px; - } - - // special style when inside the sidebar - .sidebar-container & { - padding-top: 4px; - } - .${sheshaStyles.verticalSettingsClass} & { - padding-top: 4px; - } - `); - const jsContent = cx(css` - margin-left: 64px; - ${responsive.mobile} { - margin-left: 0; - } - - // special style when inside the sidebar - .sidebar-container & { - margin-right: 0; - margin-left: 0; - } - .${sheshaStyles.verticalSettingsClass} & { - margin-right: 0; - margin-left: 0; - } + `); return { - jsSwitch, contentJs, contentCode, jsContent, + label, + bindingOptionsBtn, + jsSwitch }; }); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/_settings/utils.tsx b/shesha-reactjs/src/designer-components/_settings/utils.tsx index 6af4f1bdac..072ca348f1 100644 --- a/shesha-reactjs/src/designer-components/_settings/utils.tsx +++ b/shesha-reactjs/src/designer-components/_settings/utils.tsx @@ -1,5 +1,7 @@ -import { IContainerComponentProps } from "@/designer-components/container/interfaces"; -import { IComponentsDictionary, IConfigurableFormComponent, IPropertySetting, IToolboxComponents } from "@/interfaces"; +import { IContainerComponentProps } from '@/designer-components/container/interfaces'; +import React from 'react'; +import { IComponentsDictionary, IConfigurableFormComponent, IPropertySetting, IToolboxComponents } from '@/interfaces'; +import { useStyles } from './styles/styles'; /** * Checks if the provided data is an instance of IPropertySetting. @@ -7,153 +9,184 @@ import { IComponentsDictionary, IConfigurableFormComponent, IPropertySetting, IT * @param {any} data - The data to be checked * @return {boolean} Indicates whether the data is an instance of IPropertySetting */ -export const isPropertySettings = (data: any): data is IPropertySetting => { - if (!data || typeof data !== 'object') - return false; - - const typed = data as IPropertySetting; - return typed._mode === 'code' || typed._mode === 'value'; +export const isPropertySettings = (data: any): data is IPropertySetting => { + if (!data || typeof data !== 'object') return false; + + const typed = data as IPropertySetting; + return typed._mode === 'code' || typed._mode === 'value'; }; export const getPropertySettingsFromData = (data: any, propName: string): IPropertySetting => { - if (!propName || !data) - return { _mode: 'value', _code: undefined, _value: undefined }; + if (!propName || !data) return { _mode: 'value', _code: undefined, _value: undefined }; - const propNames = propName.split('.'); - let val = data; - propNames.forEach(p => { - val = val?.[p]; - }); + const propNames = propName.split('.'); + let val = data; + propNames.forEach((p) => { + val = val?.[p]; + }); - if (isPropertySettings(val)) - return val; - else - return { _mode: 'value', _code: undefined, _value: val }; + if (isPropertySettings(val)) return val; + else return { _mode: 'value', _code: undefined, _value: val }; }; export const updateSettingsFromValues = (model: T, values: T): T => { - const copy = { ...model }; - Object.keys(values).forEach(k => { - if (isPropertySettings(copy[k]) && !isPropertySettings(values[k])) - copy[k]._value = values[k]; - else - copy[k] = values[k]; - }); - return copy; + const copy = { ...model }; + Object.keys(values).forEach((k) => { + if (isPropertySettings(copy[k]) && !isPropertySettings(values[k])) copy[k]._value = values[k]; + else copy[k] = values[k]; + }); + return copy; }; export const getValueFromPropertySettings = (value: any): any => { - if (isPropertySettings(value)) - return value._value; - else - return value; + if (isPropertySettings(value)) return value._value; + else return value; }; export const getValuesFromSettings = (model: T): T => { const copy = { ...model }; - Object.keys(copy).forEach(k => { - copy[k] = getValueFromPropertySettings(copy[k]); + Object.keys(copy).forEach((k) => { + copy[k] = getValueFromPropertySettings(copy[k]); }); return copy; }; export const getPropertySettingsFromValue = (value: any): IPropertySetting => { - if (!isPropertySettings(value) || !value) - return { _mode: 'value', _code: undefined, _value: value }; - else - return value; + if (!isPropertySettings(value) || !value) return { _mode: 'value', _code: undefined, _value: value }; + else return value; }; /** * Update structure of components to use with Setting component - * + * * @param toolboxComponents List of Toolbox components * @param components Components structure * @returns Updated components structure */ export const updateSettingsComponents = ( + toolboxComponents: IToolboxComponents, + components: IConfigurableFormComponent[] +) => { + const processComponent = (component: IConfigurableFormComponent) => { + const componentRegistration = toolboxComponents[component.type]; + + const newComponent: IConfigurableFormComponent = { ...component, jsSetting: false }; + if ((componentRegistration?.canBeJsSetting && component.jsSetting !== false) || component.jsSetting === true) { + const oldComponent: IConfigurableFormComponent = { ...newComponent }; + + // If should be wrapped as Setting + newComponent.type = 'setting'; + newComponent.id = oldComponent.id + '_setting'; + + // copy `exposedVariables`. NOTE: it's a temporary solution, will be removed later + if (oldComponent['exposedVariables']) newComponent['exposedVariables'] = oldComponent['exposedVariables']; + + // Add source component as a child of Setting component + if (Array.isArray(oldComponent['components']) && oldComponent['components'].length > 0) { + newComponent['components'] = [ + { + ...oldComponent, + components: oldComponent['components'].map((c) => { + return processComponent(c); + }), + parentId: newComponent.id, + } as IContainerComponentProps, + ]; + } else { + newComponent['components'] = [ + { + ...oldComponent, + parentId: newComponent.id, + } as IConfigurableFormComponent, + ]; + } + return newComponent; + } else { + // If should not be wrapped as Setting then check all child containers + + // custom containers + const customContainerNames = componentRegistration?.customContainerNames || []; + customContainerNames.forEach((subContainer) => { + if (Array.isArray(component[subContainer]?.components) && component[subContainer]?.components.length > 0) + newComponent[subContainer].components = component[subContainer]?.components.map((c) => { + return processComponent(c); + }); + }); + + // default container + if (Array.isArray(component['components']) && component['components'].length > 0) + newComponent['components'] = component['components'].map((c) => { + return processComponent(c); + }); + + return newComponent; + } + }; + + return components.map((c) => { + return processComponent(c); + }); +}; + +export const updateJsSettingsForComponents = ( toolboxComponents: IToolboxComponents, components: IConfigurableFormComponent[]) => { - const processComponent = (component: IConfigurableFormComponent) => { - - const componentRegistration = toolboxComponents[component.type]; - - const newComponent: IConfigurableFormComponent = {...component, jsSetting: false}; - - if (componentRegistration?.canBeJsSetting && (component.jsSetting !== false) - || component.jsSetting === true) { - - const oldComponent: IConfigurableFormComponent = {...newComponent}; - - // If should be wrapped as Setting - newComponent.type = 'setting'; - newComponent.id = oldComponent.id + '_setting'; - - // copy `exposedVariables`. NOTE: it's a temporary solution, will be removed later - if (oldComponent['exposedVariables']) - newComponent['exposedVariables'] = oldComponent['exposedVariables']; - - // Add source component as a child of Setting component - if (Array.isArray(oldComponent['components']) && oldComponent['components'].length > 0) { - newComponent['components'] = [{ - ...oldComponent, - components: oldComponent['components'].map(c => { - return processComponent(c); - }), - parentId: newComponent.id - } as IContainerComponentProps]; - } else { - newComponent['components'] = [{ - ...oldComponent, - parentId: newComponent.id - } as IConfigurableFormComponent]; - } - return newComponent; - } else { - // If should not be wrapped as Setting then check all child containers - - // custom containers - const customContainerNames = componentRegistration?.customContainerNames || []; - customContainerNames.forEach(subContainer => { - if (Array.isArray(component[subContainer]?.components) && component[subContainer]?.components.length > 0) - newComponent[subContainer].components = component[subContainer]?.components.map(c => { - return processComponent(c); - }); + const processComponent = (component: IConfigurableFormComponent) => { + const componentRegistration = toolboxComponents[component.type]; + const newComponent: IConfigurableFormComponent = { + ...component, + jsSetting: componentRegistration?.canBeJsSetting && component.jsSetting !== false || component.jsSetting === true + }; + + // Check all child containers + // custom containers + const customContainerNames = componentRegistration?.customContainerNames || []; + customContainerNames.forEach(subContainer => { + if (Array.isArray(component[subContainer]?.components) && component[subContainer]?.components.length > 0) + newComponent[subContainer].components = component[subContainer]?.components.map(c => { + return processComponent(c); }); + }); - // default container - if (Array.isArray(component['components']) && component['components'].length > 0) - newComponent['components'] = component['components'].map(c => { - return processComponent(c); - }); + // default container + if (Array.isArray(component['components']) && component['components'].length > 0) + newComponent['components'] = component['components'].map(c => { + return processComponent(c); + }); + + return newComponent; + }; - return newComponent; - } - }; - return components.map(c => { return processComponent(c); }); }; -export const updateSettingsComponentsDict = ( - toolboxComponents: IToolboxComponents, - components: IComponentsDictionary) => { - const comps: IConfigurableFormComponent[] = []; +export const updateSettingsComponentsDict = ( + toolboxComponents: IToolboxComponents, + components: IComponentsDictionary +) => { + const comps: IConfigurableFormComponent[] = []; - for (const key in components) { - if (components.hasOwnProperty(key)) { - comps.push(components[key]); - } - } + for (const key in components) { + if (components.hasOwnProperty(key)) { + comps.push(components[key]); + } + } - const updComps = updateSettingsComponents(toolboxComponents, comps); + const updComps = updateSettingsComponents(toolboxComponents, comps); - const res: IComponentsDictionary = {}; - updComps.forEach((comp) => { - res[comp.id] = comp; - }); + const res: IComponentsDictionary = {}; + updComps.forEach((comp) => { + res[comp.id] = comp; + }); + + return res; +}; + +export const StyledLabel = ({ label }: { label: string }) => { + const { styles } = useStyles(); - return res; + return {label}; }; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/_settings/utils/CustomDropdown.tsx b/shesha-reactjs/src/designer-components/_settings/utils/CustomDropdown.tsx new file mode 100644 index 0000000000..e58aded34a --- /dev/null +++ b/shesha-reactjs/src/designer-components/_settings/utils/CustomDropdown.tsx @@ -0,0 +1,99 @@ +import React, { FC, useState } from 'react'; +import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'; +import { Button, Divider, Input, Select, Space, Tooltip } from 'antd'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; +import { IDropdownOption } from '@/designer-components/_settings/utils/background/interfaces'; + +interface CustomDropdownProps { + value: string; + options: Array; + readOnly?: boolean; + label?: string | React.ReactNode; + size?: SizeType; + defaultValue?: string; + customTooltip?: string; + onAddCustomOption?: (newOption: string) => void; + onChange?: (value: string) => void; + placeholder?: string; +} + +const CustomDropdown: FC = ({ + value, + options, + readOnly, + label, + placeholder, + defaultValue, + customTooltip, + onChange, + size +}) => { + const [customOption, setCustomOption] = useState(''); + const [customOptions, setCustomOptions] = useState(options); + + const clearInputs = () => { + setCustomOption(''); + }; + + const addCustomOption = () => { + setCustomOptions(() => [...options, customOption]); + clearInputs(); + }; + + + const renderCustomOptionInput = () => ( + <> + + e.stopPropagation()}> + +
+ +
+ + setCustomOption(e.target.value)} + size='small' + prefix={ + + } + onClick={(e) => e.stopPropagation()} + /> + +
+
+
+ +
+ + ); + + return ( + diff --git a/shesha-reactjs/src/designer-components/address/utils.ts b/shesha-reactjs/src/designer-components/address/utils.ts index 52457c23da..28522d11af 100644 --- a/shesha-reactjs/src/designer-components/address/utils.ts +++ b/shesha-reactjs/src/designer-components/address/utils.ts @@ -1,6 +1,7 @@ import { PropTypes } from 'react-places-autocomplete'; import { IEntityReferenceDto } from '@/interfaces'; import { IAddressCompomentProps } from './models'; +import { COUNTRY_CODES } from '@/shesha-constants/country-codes'; export const EXPOSED_VARIABLES = [ { @@ -83,9 +84,13 @@ export const getSearchOptions = (model: IAddressCompomentProps): PropTypes['sear showPriorityBounds, } = model; let result = {} as PropTypes['searchOptions']; - + if (country?.length) { - result = { componentRestrictions: { country } }; + const countryCodes = country.map(countryLabel => { + const foundCountry = COUNTRY_CODES.find(item => item.value === countryLabel); + return foundCountry ? foundCountry.code : countryLabel; + }); + result = { componentRestrictions: { country: countryCodes } }; } try { diff --git a/shesha-reactjs/src/designer-components/alert/index.tsx b/shesha-reactjs/src/designer-components/alert/index.tsx index 613fdd634f..b48bc6c8fe 100644 --- a/shesha-reactjs/src/designer-components/alert/index.tsx +++ b/shesha-reactjs/src/designer-components/alert/index.tsx @@ -3,38 +3,119 @@ import { IToolboxComponent } from '@/interfaces'; import { ExclamationCircleOutlined } from '@ant-design/icons'; import { Alert } from 'antd'; import { evaluateString, getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; -import { useFormData, useGlobalState } from '@/providers'; -import { getSettings } from './settings'; +import { FormMode } from '@/providers'; +import { getSettings } from './settingsForm'; import ShaIcon from '@/components/shaIcon'; import { IAlertComponentProps } from './interfaces'; import { migratePropertyName, migrateCustomFunctions } from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import Marquee from 'react-fast-marquee'; +import parse from 'html-react-parser'; -const AlertComponent: IToolboxComponent = { +const defaultTextForPreview = { + success: { + text: 'Success Alert Preview Text', + description: 'This is a success alert preview text. More information here.' + }, + info: { + text: 'Info Alert Preview Text', + description: 'This is an info alert preview text. More information here.' + }, + warning: { + text: 'Warning Alert Preview Text', + description: 'This is a warning alert preview text. More information here.' + }, + error: { + text: 'Error Alert Preview Text', + description: 'This is an error alert preview text. More information here.' + } +}; + +interface IAlertComponentCalulatedValues { + evaluatedMessage: string; + evaluatedDescription: string; + formMode: FormMode; +} + +const AlertComponent: IToolboxComponent = { type: 'alert', isInput: false, name: 'Alert', icon: , - Factory: ({ model }) => { - const { data: formData } = useFormData(); - const { globalState } = useGlobalState(); + calculateModel: (model, allData) => ({ + evaluatedMessage: evaluateString(model.text, { data: allData.data, globalState: allData.globalState }), + evaluatedDescription: evaluateString(model.description, allData.data), + formMode: allData.form.formMode, + }), + Factory: ({ model, calculatedModel }) => { + const { alertType, showIcon, closable, icon, style } = model; + let { evaluatedMessage, evaluatedDescription, formMode } = calculatedModel; - const { text, alertType, description, showIcon, closable, icon, style } = model; + if (model.hidden) return null; - const evaluatedMessage = evaluateString(text, { data: formData, globalState }); - const evaluatedDescription = evaluateString(description, formData); + if (formMode === 'designer') { + const previewData = defaultTextForPreview[alertType]; + if (!evaluatedMessage || evaluatedMessage?.trim() === '') { + evaluatedMessage = previewData.text; + } + if (!evaluatedDescription || evaluatedDescription?.trim() === '') { + evaluatedDescription = previewData.description; + } + } + + const renderContent = (content: string | React.ReactNode) => { + if (React.isValidElement(content)) { + return React.cloneElement(content as React.ReactElement, { + style: { + ...(content as React.ReactElement).props?.style, + padding: 0, + margin: 0, + lineHeight: 'normal' + } + }); + } + + const contentStr = String(content || ''); + const hasHtmlTags = contentStr.match(/<\/?[a-z][\s\S]*>/i); + + if (hasHtmlTags) { + const parsedContent: any = parse(contentStr); + // If parsed content is a React element, apply our styles + if (React.isValidElement(parsedContent)) { + return React.cloneElement(parsedContent as React.ReactElement, { + className: 'sha-alert-content', + ...(parsedContent as React.ReactElement).props, + style: { + ...(parsedContent as React.ReactElement).props?.style, + padding: 0, + margin: 0, + lineHeight: 'normal' + } + }); + } + return parsedContent; + } + + return {contentStr}; + }; + + const messageContent = renderContent(evaluatedMessage); + const descriptionContent = evaluatedDescription ? renderContent(evaluatedDescription) : null; - if (model.hidden) return null; - return ( } + message={model.marquee ? ( + + {messageContent} + + ) : messageContent} + banner={model.banner} type={alertType} - description={evaluatedDescription} + description={descriptionContent} showIcon={showIcon} - style={getStyle(style, formData)} // Temporary. Make it configurable + style={{ ...getStyle(style, {}), padding: '8px' }} // Temporary. Make it configurable closable={closable} icon={icon ? : null} /> @@ -47,10 +128,10 @@ const AlertComponent: IToolboxComponent = { migrator: (m) => m .add(0, (prev: IAlertComponentProps) => migratePropertyName(migrateCustomFunctions(prev))) .add(1, (prev) => migrateVisibility(prev)) - .add(2, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) + .add(2, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) , settingsFormMarkup: (data) => getSettings(data), validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), }; -export default AlertComponent; +export default AlertComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/alert/interfaces.ts b/shesha-reactjs/src/designer-components/alert/interfaces.ts index 6dd481576b..3f172d2618 100644 --- a/shesha-reactjs/src/designer-components/alert/interfaces.ts +++ b/shesha-reactjs/src/designer-components/alert/interfaces.ts @@ -7,4 +7,6 @@ export interface IAlertComponentProps extends IConfigurableFormComponent { alertType?: 'success' | 'info' | 'warning' | 'error'; closable?: boolean; icon?: string; + banner?: boolean; + marquee?: boolean; } \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/alert/settings.ts b/shesha-reactjs/src/designer-components/alert/settings.ts index 28ea1d920f..6c69978fe4 100644 --- a/shesha-reactjs/src/designer-components/alert/settings.ts +++ b/shesha-reactjs/src/designer-components/alert/settings.ts @@ -12,128 +12,130 @@ export const getSettings = (data: any) => ghost: true, collapsible: 'header', content: { - id:'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addTextField({ - id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', - propertyName: 'componentName', - parentId: 'root', - label: 'Component name', - validate: { required: true }, - jsSetting: false - }) - .addDropdown({ - id: 'f6c3d710-8d98-47fc-9fe2-7c6312e9a03c', - propertyName: 'alertType', - parentId: 'root', - hidden: false, - label: 'Type', - dataSourceType: 'values', - values: [ - { id: '17a865b3-8e28-41de-ab40-1fc49a56b31d', label: 'Success', value: 'success' }, - { id: 'edeb7d32-f942-41cc-a941-07b8882d8faa', label: 'Info', value: 'info' }, - { id: 'df342c95-4dae-49a5-beff-259d0f2ebcb3', label: 'Warning', value: 'warning' }, - { id: '21fc57e5-5e5d-4ae8-83c4-080a15b55176', label: 'Error', value: 'error' }, - ], - validate: { required: true }, - defaultValue: 'info', - }) - .addTextArea({ - id: '277b7ffe-d023-4543-a4b4-ff7f76052867', - propertyName: 'text', - parentId: 'root', - hidden: false, - label: 'Text', - autoSize: false, - showCount: false, - allowClear: false, - validate: { required: true }, - description: 'Accepts {{mustache}} syntax', - }) - .addTextArea({ - id: '8340f638-c466-448e-99cd-fb8c544fe02a', - propertyName: 'description', - parentId: 'root', - hidden: false, - label: 'Description', - autoSize: false, - showCount: false, - description: 'Accepts {{mustache}} syntax', + id: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', + components: [...new DesignerToolbarSettings() + .addTextField({ + id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', + propertyName: 'componentName', + parentId: 'root', + label: 'Component name', + validate: { required: true }, + jsSetting: false + }) + .addDropdown({ + id: 'f6c3d710-8d98-47fc-9fe2-7c6312e9a03c', + propertyName: 'alertType', + parentId: 'root', + hidden: false, + label: 'Type', + dataSourceType: 'values', + values: [ + { id: '17a865b3-8e28-41de-ab40-1fc49a56b31d', label: 'Success', value: 'success' }, + { id: 'edeb7d32-f942-41cc-a941-07b8882d8faa', label: 'Info', value: 'info' }, + { id: 'df342c95-4dae-49a5-beff-259d0f2ebcb3', label: 'Warning', value: 'warning' }, + { id: '21fc57e5-5e5d-4ae8-83c4-080a15b55176', label: 'Error', value: 'error' }, + ], + validate: { required: true }, + defaultValue: 'info', + }) + .addTextArea({ + id: '277b7ffe-d023-4543-a4b4-ff7f76052867', + propertyName: 'text', + parentId: 'root', + hidden: false, + label: 'Text', + autoSize: false, + showCount: false, + allowClear: false, + validate: { required: true }, + description: 'Accepts {{mustache}} syntax', + }) + .addTextArea({ + id: '8340f638-c466-448e-99cd-fb8c544fe02a', + propertyName: 'description', + parentId: 'root', + hidden: false, + label: 'Description', + autoSize: false, + showCount: false, + description: 'Accepts {{mustache}} syntax', + }) + .addCheckbox({ + id: '65aef83a-ea37-480a-9d77-ee4f4e229a70', + propertyName: 'showIcon', + parentId: 'root', + label: 'Show Icon', + }) + .addCheckbox({ + id: '2950EE07-A72F-41E2-B7B1-5E6456D3DAB8', + propertyName: 'closable', + label: 'Closable', + parentId: 'root', + }) + .addIconPicker({ + id: '152f3d72-68fb-43ab-adf6-8cf7d11fe6e1', + propertyName: 'icon', + label: 'Icon', + parentId: 'root', + }) + .addCheckbox({ + id: '148e12c0-41a0-4fa2-8c64-8f6dd5213a3e', + propertyName: 'hidden', + label: 'hide', + parentId: 'root', + }) + .toJson() + ] + } }) - .addCheckbox({ - id: '65aef83a-ea37-480a-9d77-ee4f4e229a70', - propertyName: 'showIcon', - parentId: 'root', - label: 'Show Icon', - }) - .addCheckbox({ - id: '2950EE07-A72F-41E2-B7B1-5E6456D3DAB8', - propertyName: 'closable', - label: 'Closable', - parentId: 'root', - }) - .addIconPicker({ - id: '152f3d72-68fb-43ab-adf6-8cf7d11fe6e1', - propertyName: 'icon', - label: 'Icon', - parentId: 'root', - }) - .addCheckbox({ - id: '148e12c0-41a0-4fa2-8c64-8f6dd5213a3e', - propertyName: 'hidden', - label: 'Hidden', + .addCollapsiblePanel({ + id: '22224bf6-f76d-4139-a850-c99bf06c8b69', + propertyName: 'pnlData', parentId: 'root', - }) - .toJson() - ]} -}) - .addCollapsiblePanel({ - id: '22224bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'pnlData', - parentId: 'root', - label: 'Style', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addCodeEditor({ - id: '987c3de1-b959-4670-96f6-9b1747189a6e', - propertyName: 'style', label: 'Style', + labelAlign: "left", + expandIconPosition: "start", + ghost: true, + collapsible: 'header', + content: { + id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b69', + components: [...new DesignerToolbarSettings() + .addCodeEditor({ + id: '987c3de1-b959-4670-96f6-9b1747189a6e', + propertyName: 'style', + label: 'Style', + parentId: 'root', + mode: 'dialog', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', + propertyName: 'pnlSecurity', parentId: 'root', - mode: 'dialog', + label: 'Security', + labelAlign: "left", + expandIconPosition: "start", + ghost: true, + collapsible: 'header', + content: { + id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', + components: [...new DesignerToolbarSettings() + .addPermissionAutocomplete({ + id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', + propertyName: 'permissions', + label: 'Permissions', + labelAlign: 'right', + parentId: 'root', + hidden: false, + validate: {}, + }).toJson() + ] + } }) - .toJson() - ]} -}) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] -} -}) -.toJson(); + .toJson(); diff --git a/shesha-reactjs/src/designer-components/alert/settingsForm.ts b/shesha-reactjs/src/designer-components/alert/settingsForm.ts new file mode 100644 index 0000000000..e87f564f1a --- /dev/null +++ b/shesha-reactjs/src/designer-components/alert/settingsForm.ts @@ -0,0 +1,225 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { IAlertComponentProps } from './interfaces'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/es/form/Form'; + +export const getSettings = (data: IAlertComponentProps) => { + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: nanoid(), + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'componentName', + label: 'Component Name', + size: 'small', + jsSetting: true, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + propertyName: 'alertType', + label: 'Type', + size: 'small', + jsSetting: true, + dropdownOptions: [ + { label: 'Success', value: 'success' }, + { label: 'Info', value: 'info' }, + { label: 'Warning', value: 'warning' }, + { label: 'Error', value: 'error' } + ] + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textArea', + id: nanoid(), + propertyName: 'text', + label: 'Message', + size: 'small', + tooltip: 'The message to display in the alert. You can use variables and expressions.', + allowClear: true, + jsSetting: true, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textArea', + id: nanoid(), + propertyName: 'description', + label: 'Description', + tooltip: 'Additional information about the alert.', + jsSetting: true, + } + ], + hidden: { _code: 'return getSettingValue(data?.readOnly) || getSettingValue(data?.banner);', _mode: 'code', _value: true } as any, + + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'showIcon', + label: 'Show Icon', + size: 'small', + jsSetting: true, + }, + { + type: 'iconPicker', + id: nanoid(), + propertyName: 'icon', + label: 'Icon', + size: 'small', + jsSetting: true, + hidden: { + _code: 'return !getSettingValue(data?.showIcon);', _mode: 'code', _value: false + } as any + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + size: 'small', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'closable', + label: 'Closable', + size: 'small', + jsSetting: true, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'marquee', + label: 'Marquee', + size: 'small', + tooltip: 'If enabled, the content will scroll horizontally.', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'banner', + label: 'Banner', + size: 'small', + tooltip: 'If enabled, the alert will be displayed as a banner.', + jsSetting: true, + }, + ], + }) + .toJson()] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: appearanceTabId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/attachmentsEditor/attachmentsEditor.tsx b/shesha-reactjs/src/designer-components/attachmentsEditor/attachmentsEditor.tsx index b519a9eef3..5faf8d7aec 100644 --- a/shesha-reactjs/src/designer-components/attachmentsEditor/attachmentsEditor.tsx +++ b/shesha-reactjs/src/designer-components/attachmentsEditor/attachmentsEditor.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { CustomFile } from '@/components'; import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import { IToolboxComponent } from '@/interfaces'; -import { useForm, useFormData, useGlobalState, useHttpClient, useSheshaApplication } from '@/providers'; +import { IStyleType, useForm, useFormData, useGlobalState, useHttpClient, useSheshaApplication } from '@/providers'; import { IConfigurableFormComponent, IInputStyles } from '@/providers/form/models'; import { evaluateValue, @@ -20,6 +20,7 @@ import { migrateVisibility } from '@/designer-components/_common-migrations/migr import { GHOST_PAYLOAD_KEY } from '@/utils/form'; import { getFormApi } from '@/providers/form/formApi'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { containerDefaultStyles, defaultStyles } from './utils'; export type layoutType = 'vertical' | 'horizontal' | 'grid'; export type listType = 'text' | 'thumbnail'; @@ -37,12 +38,14 @@ export interface IAttachmentsEditorProps extends IConfigurableFormComponent, IIn maxHeight?: string; onFileChanged?: string; downloadZip?: boolean; - layout: layoutType; + filesLayout?: layoutType; listType: listType; thumbnailWidth?: string; thumbnailHeight?: string; borderRadius?: number; hideFileName?: boolean; + container?: IStyleType; + primaryColor?: string; } const AttachmentsEditor: IToolboxComponent = { @@ -109,7 +112,7 @@ const AttachmentsEditor: IToolboxComponent = { isDragger={model?.isDragger} onFileListChanged={onFileListChanged} downloadZip={model.downloadZip} - layout={model.layout} + filesLayout={model.filesLayout} listType={model.listType} {...model} ownerId={ownerId} @@ -120,7 +123,7 @@ const AttachmentsEditor: IToolboxComponent = { ); }, - settingsFormMarkup: getSettings(), + settingsFormMarkup: () => getSettings(), validateSettings: (model) => validateConfigurableComponentSettings(getSettings(), model), migrator: (m) => m .add(0, (prev) => { @@ -135,8 +138,9 @@ const AttachmentsEditor: IToolboxComponent = { ownerType: '', ownerName: '', listType: 'text', - layout: 'horizontal', + filesLayout: 'horizontal', hideFileName: true, + editMode: 'inherited' }; }) .add(1, (prev) => migratePropertyName(migrateCustomFunctions(prev))) @@ -147,7 +151,8 @@ const AttachmentsEditor: IToolboxComponent = { ...migrateFormApi.eventsAndProperties(prev), onFileChanged: migrateFormApi.withoutFormData(prev?.onFileChanged), })) - .add(6, (prev) => ({ ...prev, listType: !prev.listType ? 'text' : prev.listType })), + .add(6, (prev) => ({ ...prev, listType: !prev.listType ? 'text' : prev.listType, filesLayout: prev.filesLayout ?? 'horizontal' })) + .add(7, (prev) => ({ ...prev, desktop: { ...defaultStyles(), container: containerDefaultStyles() }, mobile: { ...defaultStyles() }, tablet: { ...defaultStyles() } })), }; export default AttachmentsEditor; diff --git a/shesha-reactjs/src/designer-components/attachmentsEditor/settings.ts b/shesha-reactjs/src/designer-components/attachmentsEditor/settings.ts index b8a78efac9..ccdcd326ee 100644 --- a/shesha-reactjs/src/designer-components/attachmentsEditor/settings.ts +++ b/shesha-reactjs/src/designer-components/attachmentsEditor/settings.ts @@ -1,424 +1,921 @@ import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { FormLayout } from 'antd/es/form/Form'; import { nanoid } from '@/utils/uuid'; -export const getSettings = () => - new DesignerToolbarSettings() - .addCollapsiblePanel({ - id: '11114bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'pnlDisplay', - parentId: 'root', - label: 'Display', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addPropertyAutocomplete({ - id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', - propertyName: 'componentName', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Component name', - validate: { - required: true, - }, - jsSetting: false - }) - .addTextField({ - id: '46d07439-4c18-468c-89e1-60c002ce96c5', - propertyName: 'label', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Label', - }) - .addDropdown({ - id: '57a40a33-7e08-4ce4-9f08-a34d24a83338', - propertyName: 'labelAlign', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Label align', - values: [ - { - label: 'left', - value: 'left', - id: 'f01e54aa-a1a4-4bd6-ba73-c395e48ap8ce', - }, - { - label: 'right', - value: 'right', - id: 'b920ef96-ae27-4a01-bfad-b5b7d07218dr', - }, - ], - dataSourceType: 'values', - }) - .addTextArea({ - id: '2d32fe70-99a0-4825-ae6c-8b933004e119', - propertyName: 'description', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Description', - }) - .addCheckbox({ - id: 'cfd7d45e-c7e3-4a27-987b-dc525c412447', - propertyName: 'isDragger', - parentId: 'root', - label: 'Is Dragger', - description: 'Where the uploader should show a dragger instead of buttom', - hidden: { _code: 'return getSettingValue(data?.listType) === "thumbnail";', _mode: 'code', _value: false } as any, - }) +export const getSettings = () => { - .addDropdown({ - id: 'b920ef96-ae27-4a01-bfad-bob7d07218da', - propertyName: 'listType', - parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'List Type', - hidden: { _code: 'return getSettingValue(data?.isDragger);', _mode: 'code', _value: false } as any, - dataSourceType: 'values', - values: [ - { - label: 'File Name', - value: 'text', - id: 'f01e54aa-a1a4-4bd6-ba3-c395e48af8ce', - }, - { - label: 'Thumbnail', - value: 'thumbnail', - id: 'b920ef9w-ae27-4a01-bfad-b5b7d07218da', - }, - ], - }) - .addDropdown({ - id: 'f01e54aa-a1a4-4bd6-ba73-c39te48af8ce', - propertyName: 'layout', - parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Layout', - dataSourceType: 'values', - hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail" || getSettingValue(data?.isDragger);', _mode: 'code', _value: false } as any, - values: [ - { - label: 'Vertical', - value: 'vertical', - id: 'f01e54aa-a1a4-4bd6-ba73-c395e48af8se', - }, - { - label: 'Horizontal', - value: 'horizontal', - id: 'b920ef96-aeq7-4a01-bfad-b5b7d07218da', - }, - { - label: 'Grid', - value: 'grid', - id: 'b920ef96-ae27-4a01-bfad-b5b7x07218da', - } - ], - }) - .addNumberField({ - id: 'b920ef96-ae27-4a01-bfad-b5b7d07218da', - propertyName: 'gap', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Gap', - description: 'The gap between the thumbnails.', - hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, - }) - .addCheckbox({ - id: 'cfd7d45e-c7e3-4a27-987b-dc525c412448', - propertyName: 'hidden', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Hidden', - }) - .addCheckbox({ - id: 'c6885251-96a6-40ce-99b2-4b5209a9e01c', - propertyName: 'hideLabel', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Hide Label', - }) - .addEditMode({ - id: '24a8be15-98eb-40f7-99ea-ebb602693e9c', - propertyName: 'editMode', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: "Edit mode", - }) - .addCheckbox({ - id: 'b920ef96-ae27-4a01-bfad-b5b7d0xc18da', - label: 'Hide File Name', - propertyName: 'hideFileName', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, - }) - .addCheckbox({ - id: '40024b1c-edd4-4b5d-9c85-1dda6fb8db6c', - propertyName: 'allowAdd', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Allow Add', - validate: {}, - hidden: { _code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false } as any, - }) - .addCheckbox({ - id: '6b3d298a-0e82-4420-ae3c-38bf5a2246d4', - propertyName: 'allowDelete', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Allow Remove', - validate: {}, - hidden: { _code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false } as any, - }) - .addCheckbox({ - id: '332d298a-0e82-4420-ae3c-38bf5a2246d4', - propertyName: 'downloadZip', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Download Zip', - validate: {}, - }) - .addCodeEditor({ - id: '48ff91b3-5fb1-4e1b-a17f-ff86bce22e0b', - propertyName: 'onFileChanged', - label: 'On File List Changed', - labelAlign: 'right', - parentId: 'root', - hidden: false, - description: 'Callback that is triggered when the file is changed.', - validate: {}, - settingsValidationErrors: [], - wrapInTemplate: true, - templateSettings: { - functionName: 'onFileListChanged', - useAsyncDeclaration: true, - }, - }) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: '9b302942-a0a6-4805-ac47-8f45486a69d4', - propertyName: 'pnlFiles', - parentId: 'root', - label: 'Files', - labelAlign: "right", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl02942-a0a6-4805-ac47-8f45486a69d4', - components: [...new DesignerToolbarSettings() - .addPropertyAutocomplete({ - id: '3fe73b1a-04c5-4658-ac0f-cbcbae6b3bd4', - autoFillProps: false, - propertyName: 'ownerName', - parentId: 'pnl02942-a0a6-4805-ac47-8f45486a69d4', - label: 'Owner', - }) - .addTextField({ - id: '1c03863c-880d-4308-8667-c3d996619cb7', - propertyName: 'ownerId', - parentId: 'pnl02942-a0a6-4805-ac47-8f45486a69d4', - label: 'Owner Id', - }) - .addAutocomplete({ - id: '0009bf13-04a3-49d5-a9d8-1b23df20b97c', - propertyName: 'ownerType', - label: 'Owner Type', - labelAlign: 'right', - parentId: 'pnl02942-a0a6-4805-ac47-8f45486a69d4', - hidden: false, - dataSourceType: 'url', - validate: {}, - dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', - settingsValidationErrors: [], - useRawValues: true, - }) - .addTextField({ - id: 'db913b1b-3b25-46c9-afef-21854d917ba7', - propertyName: 'filesCategory', - parentId: 'pnl02942-a0a6-4805-ac47-8f45486a69d4', - label: 'Files Category', - }) - .addEditableTagGroupProps({ - id: nanoid(), - parentId: 'pnl02942-a0a6-4805-ac47-8f45486a69d4', - propertyName: 'allowedFileTypes', - label: 'Allowed File Types', - description: 'File types that can be accepted.', - }).toJson() - ] - } - }) - .addCollapsiblePanel({ - id: 'd675bfe4-ee69-431e-931b-b0e0b9ceee6f', - propertyName: 'pnlValidation', - parentId: 'root', - label: 'Validation', - labelAlign: "right", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6f', - components: [...new DesignerToolbarSettings() - .addCheckbox({ - id: '3be9da3f-f47e-48ae-b4c3-f5cc36e534d9', - propertyName: 'validate.required', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6f', - label: 'Required', - }).toJson() - ] - } - }) - .addCollapsiblePanel({ - id: 'd675bfe4-ee69-431e-931b-b0e0b9ceee6', - propertyName: 'styles', - parentId: 'root', + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const validationTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const styleDimensionsPnlId = nanoid(); + const containerStylePnlId = nanoid(); + const containerDimensionsStylePnlId = nanoid(); + const pnlBorderStyle = nanoid(); + const pnlBackgroundStyle = nanoid(); + const pnlShadowStyleId = nanoid(); + const customStylePnlId = nanoid(); + const pnlFontStyleId = nanoid(); - label: 'Styles', - labelAlign: "right", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - components: [...new DesignerToolbarSettings() - .addCollapsiblePanel({ - id: 'item-styles-1c03863c-880d-4308-8667-c3d996619cb', - propertyName: 'itemStyles', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - label: 'Item Styles', - hideWhenEmpty: true, - content: { - id: 'item-styles-content-880d-4308-c3d996619cb', - components: [...new DesignerToolbarSettings() - .addTextField({ - id: '1c03863c-880d-4308-8667-c3d996619cb3', - propertyName: 'fontSize', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - label: 'Font Size', - hidden: { _code: 'return getSettingValue(data?.hideFileName);', _mode: 'code', _value: false } as any, - }) - .addColorPicker({ - id: '1c03863c-880d-4308-8667-c3d996619cb0', - propertyName: 'fontColor', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - label: 'Color', - hidden: { _code: 'return getSettingValue(data?.hideFileName);', _mode: 'code', _value: false } as any, - allowClear: true + return { + components: new DesignerToolbarSettings() + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'componentName', + label: 'Component name', + inputType: 'propertyAutocomplete', + validate: { required: true }, + jsSetting: false, }) - .addTextField({ - id: '1c03863c-880d-4308-8667-c3d996619cb8', - propertyName: 'thumbnailWidth', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - label: 'Thumbnail Width', - hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + hideLabel: true, + label: 'Label', + hideLabelPropName: 'hideLabel' }) - .addTextField({ - id: '1c03863c-880d-4308-8667-c3d9966197', - propertyName: 'thumbnailHeight', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - label: 'Thumbnail Height', - hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + type: 'textArea', + jsSetting: true + }, + { + id: nanoid(), + propertyName: 'isDragger', + label: 'Is Dragger', + type: 'switch', + jsSetting: true, + description: 'Where the uploader should show a dragger instead of a button', + hidden: { _code: 'return getSettingValue(data?.listType) === "thumbnail";', _mode: 'code', _value: false } as any, + } + ] }) - .addTextField({ - id: '1c03863c-880d-4308-8667-c3d996619cb5', - propertyName: 'borderSize', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - label: 'Border Width', - hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'listType', + label: 'List Type', + type: 'dropdown', + dropdownOptions: [ + { label: 'File name', value: 'text' }, + { label: 'Thumbnail', value: 'thumbnail' }, + ], + hidden: { _code: 'return getSettingValue(data?.isDragger);', _mode: 'code', _value: false } as any, + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'hideFileName', + label: 'Hide File Name', + type: 'switch', + jsSetting: true, + hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + } + ] }) - .addColorPicker({ - id: '1c03863c-880d-4308-8667-c3d996619cb6', - propertyName: 'borderColor', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - label: 'Border Color', - allowClear: true, - hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + type: 'switch', + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + type: 'editModeSelector', + defaultValue: 'inherited', + jsSetting: true, + }, + ], }) - .addDropdown({ - id: '1c03863c-type-4308-8667-c3d996619cb9', - propertyName: 'borderType', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - label: 'Border Type', - hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, - dataSourceType: 'values', - values: [ + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ { - label: 'dashed', - value: 'dashed', - id: 'f01e54aa-a1a4-4bd6-ba73-c395e48af8ce', + id: nanoid(), + propertyName: 'allowAdd', + label: 'Allow Add', + type: 'switch', + jsSetting: true, + hidden: { _code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false } as any, }, { - label: 'solid', - value: 'solid', - id: 'b920ef96-ae27-4a01-bfad-b5b7d07218da', + id: nanoid(), + propertyName: 'allowDelete', + label: 'Allow Remove', + type: 'switch', + hidden: { _code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false } as any, + jsSetting: true, }, ], }) - .addTextField({ - id: '1c03863c-880d-4308-8667-c3d996619cb', - propertyName: 'borderRadius', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - label: 'Border Radius', - hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + .addSettingsInput({ + id: nanoid(), + propertyName: 'downloadZip', + label: 'Download Zip', + inputType: 'switch', + jsSetting: true, + }) + .toJson(), + ], + }, + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'ownerName', + label: 'Owner', + type: 'propertyAutocomplete', + autoFillProps: false, + }, + { + id: nanoid(), + propertyName: 'ownerId', + label: 'Owner ID', + type: 'textField', + jsSetting: true, + }, + ] }) - .toJson()] - } - }) - .addCollapsiblePanel({ - id: 'container-styles-1c03863c-880d-4308-8667-c3d996619cb', - propertyName: 'containerStyles', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6s', - label: 'Container Styles', - content: { - id: 'container-styles-content-880d-4308-c3d996619cb', - components: [...new DesignerToolbarSettings() - .addTextField({ - id: '1c03863c-880d-4308-8667-c3d996619cb1', - propertyName: 'width', - parentId: 'container-styles-content-880d-4308-c3d996619cb', - label: 'Width', - hidden: { _code: 'return getSettingValue(data?.layout) === "vertical";', _mode: 'code', _value: false } as any, + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'ownerType', + label: 'Owner Type', + type: 'autocomplete', + dataSourceType: 'url', + dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', + useRawValues: true, + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'filesCategory', + label: 'Files Category', + type: 'textField', + jsSetting: true, + } + ] }) - .addTextField({ - id: '1c03863c-880d-4308-8667-c3d996619cb2', - propertyName: 'height', - parentId: 'container-styles-content-880d-4308-c3d996619cb', - label: 'Height', - hidden: { _code: 'return getSettingValue(data?.layout) === "horizontal";', _mode: 'code', _value: false } as any, + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'allowedFileTypes', + label: 'Allowed File Types', + type: 'editableTagGroupProps', + description: 'File types that can be accepted.', + jsSetting: true, + } + ] }) - .addStyleBox({ - id: '1c03863c-880d-4308-8567-c3d996619cb3', - propertyName: 'stylingBox', - parentId: 'container-styles-content-880d-4308-c3d996619cb', - jsSetting: false, + .toJson(), + ], + }, + { + key: 'validation', + title: 'Validation', + id: validationTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'validate.required', + label: 'Required', + inputType: 'switch', + jsSetting: true, }) - .addCodeEditor({ - id: '1c03863c-880d-4308-8667-c3d996619cb4', - propertyName: 'style', - label: 'Style', + .toJson(), + ], + }, + { + key: 'events', + title: 'Events', + id: eventsTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onFileChanged', + label: 'On File List Changed', + labelAlign: 'right', + parentId: eventsTabId, + hidden: false, + description: 'Callback that is triggered when the file is changed.', + validate: {}, + settingsValidationErrors: [], + wrapInTemplate: true, + templateSettings: { + functionName: 'onFileListChanged', + useAsyncDeclaration: true, + }, + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: pnlFontStyleId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlFontStyleId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'primaryColor', + label: 'Primary Color', + inputType: 'colorPicker', + jsSetting: true, + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + content: { + id: styleDimensionsPnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleDimensionsPnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + content: { + id: pnlBorderStyle, + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: pnlBorderStyle, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: pnlBorderStyle, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + content: { + id: pnlBackgroundStyle, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: pnlBackgroundStyle, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: [ + { + value: "color", + icon: "FormatPainterOutlined", + title: "Color" + }, + { + value: "gradient", + icon: "BgColorsOutlined", + title: "Gradient" + }, + { + value: "image", + icon: "PictureOutlined", + title: "Image" + }, + { + value: "url", + icon: "LinkOutlined", + title: "URL" + }, + { + value: "storedFile", + icon: "DatabaseOutlined", + title: "Stored File" + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlBackgroundStyle, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlBackgroundStyle, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlBackgroundStyle, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlBackgroundStyle, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlBackgroundStyle, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlBackgroundStyle, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "background.size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlBackgroundStyle, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + content: { + id: pnlShadowStyleId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlShadowStyleId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + content: { + id: customStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: customStylePnlId, + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlContainerStyle', + label: 'Container Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: containerStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: containerStylePnlId, + inputs: [ + { + id: nanoid(), + propertyName: 'filesLayout', + label: 'Layout', + type: 'dropdown', + dropdownOptions: [ + { label: 'Vertical', value: 'vertical' }, + { label: 'Horizontal', value: 'horizontal' }, + { label: 'Grid', value: 'grid' }, + ], + defaultValue: 'horizontal', + jsSetting: true, + hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail" || getSettingValue(data?.isDragger);', _mode: 'code', _value: false } as any, + }, + { + id: nanoid(), + propertyName: 'gap', + label: 'Gap', + type: 'numberField', + description: 'The gap between the thumbnails.', + jsSetting: true, + hidden: { _code: 'return getSettingValue(data?.listType) !== "thumbnail";', _mode: 'code', _value: false } as any, + } + ] + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'containerDimensionsPanel', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: containerDimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: containerDimensionsStylePnlId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.filesLayout) === "vertical";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "container.dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "container.dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "container.dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.filesLayout) === "horizontal";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "container.dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "container.dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "container.dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'containerStylingBoxPanel', + label: 'Margin & Padding', + labelAlign: 'right', + collapsible: 'header', + ghost: true, + parentId: styleRouterId, + content: { + id: 'containerStylingBoxPanel', + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'container.stylingBox', + parentId: 'containerStylingBoxPanel' + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'containerCustomStylePanel', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: 'containerCustomStylePanel', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'container.style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + parentId: 'containerCustomStylePanel' + }) + .toJson() + ] + } + }) + .toJson()] + } + }) + .toJson()] + }).toJson()] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId }) - .toJson()] - } - }) - - .toJson()] - } - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlData', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() + .toJson() + ] + } ] - } - }) - .toJson(); + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/attachmentsEditor/utils.ts b/shesha-reactjs/src/designer-components/attachmentsEditor/utils.ts new file mode 100644 index 0000000000..89febaaaad --- /dev/null +++ b/shesha-reactjs/src/designer-components/attachmentsEditor/utils.ts @@ -0,0 +1,46 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + + return { + border: { hideBorder: false, radiusType: 'all', borderType: 'all', border: { all: { width: '1px', style: 'solid', color: '#d9d9d9' } }, radius: { all: 8 } }, + dimensions: { width: '54px', height: '54px', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' }, + background: { + type: 'color', + color: '#fff', + repeat: 'no-repeat', + size: 'cover', + position: 'center', + gradient: { direction: 'to right', colors: {} }, + url: '', + storedFile: { id: null }, + uploadFile: null, + }, + font: { + type: 'Segoe UI', + align: 'left', + size: 14, + weight: '400', + }, + shadow: { + offsetX: 0, + offsetY: 0, + color: '#000', + blurRadius: 0, + spreadRadius: 0 + }, + }; +}; + +export const containerDefaultStyles = (): IStyleType => { + return { + dimensions: { + width: 'auto', + height: 'auto', + minHeight: '0px', + maxHeight: '140px', + minWidth: '0px', + maxWidth: 'auto', + } + }; +}; diff --git a/shesha-reactjs/src/designer-components/autocomplete/autocomplete.tsx b/shesha-reactjs/src/designer-components/autocomplete/autocomplete.tsx index d8c8bc2d05..0ab88cc0c2 100644 --- a/shesha-reactjs/src/designer-components/autocomplete/autocomplete.tsx +++ b/shesha-reactjs/src/designer-components/autocomplete/autocomplete.tsx @@ -1,38 +1,27 @@ import { FileSearchOutlined } from '@ant-design/icons'; -import React, { CSSProperties, Key } from 'react'; -import { Autocomplete, IAutocompleteProps, ISelectOption } from '@/components/autocomplete'; -import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; -import { customDropDownEventHandler } from '@/components/formDesigner/components/utils'; +import React, { useCallback } from 'react'; import { migrateDynamicExpression } from '@/designer-components/_common-migrations/migrateUseExpression'; -import { useAsyncMemo } from '@/hooks/useAsyncMemo'; import { IToolboxComponent } from '@/interfaces'; import { DataTypes } from '@/interfaces/dataTypes'; -import { useNestedPropertyMetadatAccessor } from '@/providers'; -import { FormMarkup, IInputStyles } from '@/providers/form/models'; +import { IInputStyles } from '@/providers/form/models'; import { - evaluateString, - executeScriptSync, - getStyle, - pickStyleFromModel, - replaceTags, + executeExpression, useAvailableConstantsData, validateConfigurableComponentSettings, } from '@/providers/form/utils'; -import { evaluateDynamicFilters } from '@/utils'; import { IAutocompleteComponentProps } from './interfaces'; -import settingsFormJson from './settingsForm.json'; import { migratePropertyName, migrateCustomFunctions, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; -import { isEntityReferencePropertyMetadata } from '@/interfaces/metadata'; +import { isEntityReferenceArrayPropertyMetadata, isEntityReferencePropertyMetadata } from '@/interfaces/metadata'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; -import { toSizeCssProp } from '@/utils/form'; -import { removeUndefinedProps } from '@/utils/object'; -interface IQueryParams { - // tslint:disable-next-line:typedef-whitespace - [name: string]: Key; -} - -const settingsForm = settingsFormJson as FormMarkup; +import { ConfigurableFormItem } from '@/components'; +import { customDropDownEventHandler } from '@/components/formDesigner/components/utils'; +import { getValueByPropertyName } from '@/utils/object'; +import { FilterSelectedFunc, KayValueFunc, OutcomeValueFunc } from '@/components/autocomplete/models'; +import { Autocomplete } from '@/components/autocomplete'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; const AutocompleteComponent: IToolboxComponent = { type: 'autocomplete', @@ -43,165 +32,44 @@ const AutocompleteComponent: IToolboxComponent = { icon: , dataTypeSupported: ({ dataType }) => dataType === DataTypes.entityReference, Factory: ({ model }) => { - const { queryParams, filter } = model; - const allData = useAvailableConstantsData(); - const propertyMetadataAccessor = useNestedPropertyMetadatAccessor( - model.dataSourceType === 'entitiesList' ? model.entityTypeShortAlias : null - ); - - const dataSourceUrl = model.dataSourceUrl ? replaceTags(model.dataSourceUrl, { data: allData.data }) : model.dataSourceUrl; - - const evaluatedFilters = useAsyncMemo(async () => { - if (model.dataSourceType !== 'entitiesList' || !filter) - return ''; - - const response = await evaluateDynamicFilters( - [{ expression: filter } as any], - [ - { - match: 'data', - data: allData.data, - }, - { - match: 'globalState', - data: allData.globalState, - }, - { - match: 'pageContext', - data: { ...allData.pageContext }, - }, - ], - propertyMetadataAccessor - ); - - if (response.find((f) => f?.unevaluatedExpressions?.length)) return ''; - - return JSON.stringify(response[0]?.expression) || ''; - }, [filter, allData.data, allData.globalState]); - - const getQueryParams = (): IQueryParams => { - const queryParamObj: IQueryParams = {}; - - if (model.dataSourceType === 'url') { - if (queryParams && typeof (queryParams) === 'object') { - if (Array.isArray(queryParams)) { - queryParams.forEach(({ param, value }) => { - const valueAsString = value as string; - if (param?.length && valueAsString.length) { - queryParamObj[param] = /{.*}/i.test(valueAsString) ? evaluateString(valueAsString, { data: allData.data }) : value; - } - }); - } else - Object.assign(queryParamObj, queryParams); - } - } - - if (model.dataSourceType === 'entitiesList') { - if (filter) queryParamObj['filter'] = typeof filter === 'string' ? filter : evaluatedFilters; - } - - return queryParamObj; - }; - - const getFetchedItemData = ( - item: object, - useRawValues: boolean, - value: string = 'value', - displayText: string = 'displayText' - ) => - useRawValues - ? item[value] - : { - id: item[value], - _displayName: item[displayText], - _className: model.entityTypeShortAlias, - }; - - const getOptionFromFetchedItem = (item: object): ISelectOption => { - const { dataSourceType, keyPropName, useRawValues, valuePropName } = model; - - if (dataSourceType === 'url' && keyPropName && valuePropName) { - return { - value: item[keyPropName], - label: item[valuePropName], - data: getFetchedItemData(item, useRawValues, keyPropName, valuePropName), - }; - } - - return { - value: item['value'], - label: item['displayText'], - data: getFetchedItemData(item, useRawValues), - }; - }; - - const getDefaultValue = () => { - try { - if (model?.defaultValue) { - return executeScriptSync(model?.defaultValue, allData); - } - } catch { - return undefined; - } - }; - - const styling = JSON.parse(model.stylingBox || '{}'); - const stylingBoxAsCSS = pickStyleFromModel(styling); - - const additionalStyles: CSSProperties = removeUndefinedProps({ - height: toSizeCssProp(model.height), - width: toSizeCssProp(model.width), - fontWeight: model.fontWeight, - borderWidth: model.borderSize, - borderRadius: model.borderRadius, - borderStyle: model.borderType, - borderColor: model.borderColor, - backgroundColor: model.backgroundColor, - fontSize: model.fontSize, - overflow: 'hidden', //this allows us to retain the borderRadius even when the component is active - ...stylingBoxAsCSS, - }); - const jsStyle = getStyle(model.style, allData.data); - const finalStyle = removeUndefinedProps({ ...jsStyle, ...additionalStyles }); - - const defaultValue = getDefaultValue(); - - const autocompleteProps: IAutocompleteProps = { - className: 'sha-autocomplete', - typeShortAlias: model.entityTypeShortAlias, - entityDisplayProperty: model.entityDisplayProperty, - allowInherited: true /*hardcoded for now*/, - bordered: !model.hideBorder, - dataSourceUrl, - dataSourceType: model.dataSourceType, - mode: model.mode, - placeholder: model.placeholder, - queryParams: getQueryParams(), - readOnly: model.readOnly, - defaultValue, - getOptionFromFetchedItem, - disableSearch: model.disableSearch, - filter: evaluatedFilters, - quickviewEnabled: model.quickviewEnabled, - quickviewFormPath: model.quickviewFormPath, - quickviewDisplayPropertyName: model.quickviewDisplayPropertyName, - quickviewGetEntityUrl: model.quickviewGetEntityUrl, - quickviewWidth: model.quickviewWidth, - subscribedEventNames: model.subscribedEventNames, - style: finalStyle, - size: model.size, - allowFreeText: model.allowFreeText, - }; - - const formProps = defaultValue ? { model, initialValue: defaultValue } : { model }; - - // TODO: implement other types of datasources! - + const keyPropName = model.keyPropName || (model.dataSourceType === 'entitiesList' ? 'id' : 'value'); + const displayPropName = model.displayPropName || (model.dataSourceType === 'entitiesList' ? '_displayName' : 'displayText'); + + const keyValueFunc: KayValueFunc = useCallback( (value: any, args: any) => { + if (model.valueFormat === 'custom' && model.keyValueFunc) + return executeExpression(model.keyValueFunc, {...args, value}, null, null ); + if (model.valueFormat === 'entityReference') + return value?.id; + return typeof(value) === 'object' ? getValueByPropertyName(value, keyPropName) : value; + }, [model.valueFormat, model.keyValueFunc, keyPropName]); + + const outcomeValueFunc: OutcomeValueFunc = useCallback((item: any, args: any) => { + if (model.valueFormat === 'entityReference') + return Boolean(item) + ? {id: item.id, _displayName: item._displayName || getValueByPropertyName(item, displayPropName), _className: model.entityType} + : null; + if (model.valueFormat === 'custom' && model.outcomeValueFunc) + return executeExpression(model.outcomeValueFunc, {...args, item: item}, null, null ); + return typeof(item) === 'object' ? getValueByPropertyName(item, keyPropName) : item; + }, [model.valueFormat, model.outcomeValueFunc, keyPropName, displayPropName, model.entityType]); + + const displayValueFunc: OutcomeValueFunc = useCallback((value: any, args: any) => { + if (model.displayValueFunc) + return executeExpression(model.displayValueFunc, {...args, item: value}, null, null ); + return getValueByPropertyName(value, displayPropName) || 'unknown'; + }, [model.displayValueFunc, displayPropName]); + + const filterKeysFunc: FilterSelectedFunc = useCallback((value: any | any[]) => { + const localValue = value?.length === 1 ? value[0] : value; + return Array.isArray(localValue) + ? { or : localValue.map(x => executeExpression(model.filterKeysFunc, {value: x}, null, null )) } + : executeExpression(model.filterKeysFunc, {value: localValue}, null, null ); + }, [model.filterKeysFunc]); return ( - + {(value, onChange) => { const customEvent = customDropDownEventHandler(model, allData); const onChangeInternal = (...args: any[]) => { @@ -211,18 +79,25 @@ const AutocompleteComponent: IToolboxComponent = { }; - return ( - model.useRawValues ? ( - - ) : ( - - )); + return 0 ? model.grouping[0] : undefined} + keyValueFunc={keyValueFunc} + outcomeValueFunc={outcomeValueFunc} + displayValueFunc={displayValueFunc} + filterKeysFunc={model.filterKeysFunc ? filterKeysFunc : undefined} + style={model?.allStyles?.fullStyle} + size={model?.size ?? 'middle'} + value={value} + onChange={onChangeInternal} + allowFreeText={model.allowFreeText && model.valueFormat === 'simple'} + />; }} ); }, - settingsFormMarkup: settingsForm, - validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings, model), migrator: (m) => m .add(0, (prev) => ({ ...prev, @@ -255,7 +130,7 @@ const AutocompleteComponent: IToolboxComponent = { height: prev.height, hideBorder: prev.hideBorder, borderSize: prev.borderSize, - borderRadius: prev.borderRadius, + borderRadius: prev.borderRadius ?? 8, borderColor: prev.borderColor, fontSize: prev.fontSize, backgroundColor: prev.backgroundColor, @@ -264,16 +139,45 @@ const AutocompleteComponent: IToolboxComponent = { return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; }) + .add(7, (prev) => { + return { + ...prev, + mode: prev.mode || 'single', + entityType: prev.entityType || prev['entityTypeShortAlias'], + valueFormat: prev.dataSourceType === 'entitiesList' + ? prev['useRawValues'] ? 'simple' : 'entityReference' + : 'simple', + displayPropName: prev.dataSourceType === 'entitiesList' + ? prev['entityDisplayProperty'] + : prev['useRawValues'] + ? prev['valuePropName'] || 'displayText' + : prev['valuePropName'], + keyPropName: prev.dataSourceType === 'url' && prev['useRawValues'] ? prev.keyPropName || 'value' : prev.keyPropName, + }; + }) + .add(8, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })) , linkToModelMetadata: (model, propMetadata): IAutocompleteComponentProps => { return { ...model, - //useRawValues: true, dataSourceType: 'entitiesList', - entityTypeShortAlias: isEntityReferencePropertyMetadata(propMetadata) ? propMetadata.entityType : undefined, - mode: undefined, + mode: isEntityReferenceArrayPropertyMetadata(propMetadata) ? 'multiple' : 'single', + entityType: isEntityReferencePropertyMetadata(propMetadata) + ? propMetadata.entityType + : isEntityReferenceArrayPropertyMetadata(propMetadata) + ? propMetadata.entityType + : undefined, + valueFormat: isEntityReferencePropertyMetadata(propMetadata) || isEntityReferenceArrayPropertyMetadata(propMetadata) + ? 'entityReference' + : 'simple', }; }, + actualModelPropertyFilter: (propName) => propName !== 'queryParams', + getFieldsToFetch(propertyName, rawModel) { + return rawModel.valueFormat === 'entityReference' + ? [`${propertyName}.id`, `${propertyName}._className`, `${propertyName}._displayName`] + : [propertyName]; + }, }; -export default AutocompleteComponent; +export default AutocompleteComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/autocomplete/interfaces.ts b/shesha-reactjs/src/designer-components/autocomplete/interfaces.ts index b51403bc17..823f3f9290 100644 --- a/shesha-reactjs/src/designer-components/autocomplete/interfaces.ts +++ b/shesha-reactjs/src/designer-components/autocomplete/interfaces.ts @@ -2,6 +2,7 @@ import { Key } from 'react'; import { AutocompleteDataSourceType } from '@/components/autocomplete'; import { FormIdentifier } from '@/providers'; import { IConfigurableFormComponent } from '@/providers/form/models'; +import { GroupingItem, ISortingItem } from '@/providers/dataTable/interfaces'; interface IQueryParamProp { id: string; @@ -10,28 +11,19 @@ interface IQueryParamProp { } export interface IAutocompleteComponentProps extends IConfigurableFormComponent { - entityTypeShortAlias?: string; - entityDisplayProperty?: string; + entityType?: string; hideBorder?: boolean; dataSourceUrl?: string; dataSourceType: AutocompleteDataSourceType; mode?: 'single' | 'multiple'; - useRawValues: boolean; - queryParams?: IQueryParamProp[]; keyPropName?: string; - valuePropName?: string; filter?: object; disableSearch?: boolean; placeholder?: string; - quickviewEnabled?: boolean; - quickviewFormPath?: FormIdentifier; - quickviewDisplayPropertyName?: string; - quickviewGetEntityUrl?: string; - quickviewWidth?: number; width?: string | number; + height?: string | number; minWidth?: string | number; maxWidth?: string | number; - allowFreeText?: boolean; borderSize?: string | number; borderRadius?: number; borderType?: string; @@ -39,6 +31,23 @@ export interface IAutocompleteComponentProps extends IConfigurableFormComponent fontSize?: string | number; fontWeight?: string | number; stylingBox?: string; - height?: string | number; backgroundColor?: string; + queryParams?: IQueryParamProp[]; + quickviewEnabled?: boolean; + quickviewFormPath?: FormIdentifier; + quickviewDisplayPropertyName?: string; + quickviewGetEntityUrl?: string; + quickviewWidth?: number; + displayPropName?: string; + fields?: string[]; + valueFormat?: 'simple' | 'entityReference' | 'custom'; + keyValueFunc?: string; + displayValueFunc?: string; + outcomeValueFunc?: string; + filterKeysFunc?: string; + sorting?: ISortingItem[]; + grouping?: GroupingItem[]; + allowFreeText?: boolean; + font?: any; + dimensions?: any; } diff --git a/shesha-reactjs/src/designer-components/autocomplete/settingsForm.json b/shesha-reactjs/src/designer-components/autocomplete/settingsForm.json index 56d0c15fb0..43b903f0d8 100644 --- a/shesha-reactjs/src/designer-components/autocomplete/settingsForm.json +++ b/shesha-reactjs/src/designer-components/autocomplete/settingsForm.json @@ -116,9 +116,6 @@ } ], "dataSourceType": "values", - "defaultValue": [ - "single" - ], "version": 6, "referenceListId": null, "valueFormat": "listItem" @@ -253,6 +250,7 @@ "id": "e71e4e52-2886-4569-94b4-f47d58602bc2" } ], + "onChangeCustom": "if (value === 'url' && data?.valueFormat === 'entityReference') form.setFieldValue('valueFormat', 'simple');", "version": 6, "referenceListId": null, "valueFormat": "listItem" @@ -275,43 +273,6 @@ "className": "", "settingsValidationErrors": [], "components": [ - { - "id": "33557039-48e8-4082-9ac7-510388d4dc6e", - "type": "textField", - "propertyName": "dataSourceUrl", - "parentId": "Xp6zDosEy-IrSeDfHHwh-", - "label": "Data Source Url", - "settingsValidationErrors": [], - "version": 5, - "textType": "text", - "desktop": {}, - "tablet": {}, - "mobile": {} - }, - { - "id": "4c6133f1-8886-4527-aa6c-ae0606506a8a", - "type": "textField", - "propertyName": "keyPropName", - "parentId": "Xp6zDosEy-IrSeDfHHwh-", - "label": "Key Prop Name", - "version": 5, - "textType": "text", - "desktop": {}, - "tablet": {}, - "mobile": {} - }, - { - "id": "007e5859-9299-4434-84d9-9f7b0f7228af", - "type": "textField", - "propertyName": "valuePropName", - "parentId": "Xp6zDosEy-IrSeDfHHwh-", - "label": "Value Prop Name", - "version": 5, - "textType": "text", - "desktop": {}, - "tablet": {}, - "mobile": {} - }, { "id": "b395c0e9-dbc1-44f1-8fef-c18a49442871", "type": "labelValueEditor", @@ -354,7 +315,7 @@ { "id": "c1ffda30-8eea-4621-aae7-0af583143df6", "type": "autocomplete", - "propertyName": "entityTypeShortAlias", + "propertyName": "entityType", "label": "Entity Type", "labelAlign": "right", "parentId": "uEFBpy19tApJMiBuFyj9s", @@ -366,27 +327,6 @@ "useRawValues": true, "version": 5 }, - { - "id": "hpm6rN_aj-L_KaG5MLIZt", - "type": "propertyAutocomplete", - "propertyName": "entityDisplayProperty", - "label": "Display Property", - "labelAlign": "right", - "parentId": "uEFBpy19tApJMiBuFyj9s", - "hidden": { - "_code": "return !getSettingValue(data?.entityTypeShortAlias);", - "_mode": "code", - "_value": false - }, - "isDynamic": false, - "placeholder": "", - "description": "Name of the property that should be displayed in the autocomplete. Live empty to use default display property defined on the back-end.", - "validate": {}, - "modelType": "{{data.entityTypeShortAlias}}", - "autoFillProps": false, - "settingsValidationErrors": [], - "version": 2 - }, { "id": "n4enebtmhFgvkP5ukQK1f", "type": "queryBuilder", @@ -394,11 +334,15 @@ "label": "Entity Filter", "labelAlign": "right", "parentId": "uEFBpy19tApJMiBuFyj9s", - "hidden": false, + "hidden": { + "_code": "return !getSettingValue(data?.entityType);", + "_mode": "code", + "_value": false + }, "isDynamic": false, "validate": {}, "settingsValidationErrors": [], - "modelType": "{{data.entityTypeShortAlias}}", + "modelType": "{{data.entityType}}", "fieldsUnavailableHint": "Please select `Entity Type` to be able to configure this filter.", "version": 2 } @@ -407,16 +351,111 @@ "flexWrap": "wrap" }, { - "id": "dde8e245-1555-4a99-a333-4b7c31bcc5f4", - "type": "checkbox", - "propertyName": "useRawValues", - "label": "Use raw values", + "id": "oamRBlEs6O2Jlc2FGL_gY", + "type": "endpointsAutocomplete", + "propertyName": "dataSourceUrl", + "label": { + "_code": "return getSettingValue(data?.dataSourceType) === 'entitiesList' ? 'Custom Source Url' : 'Data Source Url';", + "_mode": "code", + "_value": false + }, + "parentId": "f9pNv-hqF5ukaDytb7xwq", + "isDynamic": false, + "prefix": "GET", + "settingsValidationErrors": [], + "version": 3, + "mode": "url", + "httpVerb": "get" + }, + { + "id": "acb2d566-fe48-43bd-84e0-28b7103354c1", + "type": "dropdown", + "propertyName": "valueFormat", + "parentId": "f9pNv-hqF5ukaDytb7xwq", + "label": "Value format", + "hidden": false, + "values": { + "_code": "return getSettingValue(data?.dataSourceType) === 'entitiesList' ? [{\"label\": \"Simple Id\",\"value\": \"simple\",\"id\": \"1\"},{\"label\": \"Entity reference\",\"value\": \"entityReference\",\"id\": \"2\"},{\"label\": \"Custom\",\"value\": \"custom\",\"id\": \"3\"}] : [{\"label\": \"Simple Id\",\"value\": \"simple\",\"id\": \"1\"},{\"label\": \"Custom\",\"value\": \"custom\",\"id\": \"3\"}];", + "_mode": "code", + "_value": false + }, + "dataSourceType": "values" + }, + { + "id": "222ce49e-3067-4285-a1cb-104d4a8ee570", + "type": "codeEditor", + "propertyName": "outcomeValueFunc", + "label": "Value Function", + "labelAlign": "right", + "parentId": "f9pNv-hqF5ukaDytb7xwq", + "hidden": { "_code": "return getSettingValue(data?.valueFormat) !== 'custom';", "_mode": "code", "_value": false }, + "description": "Return value for item object", + "validate": {}, + "settingsValidationErrors": [], + "exposedVariables": [{ "name": "item", "description": "Item of list", "type": "object" }], + "version": 3, + "language": "typescript", + "placeholder": "", + "wrapInTemplate": true, + "templateSettings": { "functionName": "outcomeValueFunc"}, + "availableConstantsExpression": "return metadataBuilder.object(\"constants\").addObject(\"item\", \"Item of list\").build();" + }, + { + "id": "111ce49e-3067-4285-a1cb-104d4a8ee570", + "type": "codeEditor", + "propertyName": "keyValueFunc", + "label": "Key value Function", + "labelAlign": "right", + "parentId": "f9pNv-hqF5ukaDytb7xwq", + "hidden": { "_code": "return getSettingValue(data?.valueFormat) !== 'custom';", "_mode": "code", "_value": false }, + "description": "Return key from selected value", + "validate": {}, + "settingsValidationErrors": [], + "exposedVariables": [{ "name": "value", "description": "Value of item", "type": "object" }], + "version": 3, + "language": "typescript", + "placeholder": "", + "wrapInTemplate": true, + "templateSettings": { "functionName": "keyValueFunc"}, + "availableConstantsExpression": "return metadataBuilder.object(\"constants\").addObject(\"value\", \"Value of item\").build();" + }, + { + "id": "333ce49e-3067-4285-a1cb-104d4a8ee570", + "type": "codeEditor", + "propertyName": "displayValueFunc", + "label": "Display value Function", "labelAlign": "right", "parentId": "f9pNv-hqF5ukaDytb7xwq", "hidden": false, + "description": "Return display value for item's object", "validate": {}, "settingsValidationErrors": [], - "version": 3 + "exposedVariables": [{ "name": "item", "description": "Item of list", "type": "object" }], + "version": 3, + "language": "typescript", + "placeholder": "", + "wrapInTemplate": true, + "templateSettings": { "functionName": "displayValueFunc"}, + "availableConstantsExpression": "return metadataBuilder.object(\"constants\").addObject(\"item\", \"Item of list\").build();" + }, + { + "id": "444ce49e-3067-4285-a1cb-104d4a8ee570", + "type": "codeEditor", + "propertyName": "filterKeysFunc", + "label": "Filter selected Function", + "labelAlign": "right", + "parentId": "f9pNv-hqF5ukaDytb7xwq", + "hidden": { "_code": "return getSettingValue(data?.valueFormat) !== 'custom';", "_mode": "code", "_value": false }, + "description": "Return filter object (JsonLogic) for selected value(s). Use this settings to configure non-standard values format", + "validate": {}, + "settingsValidationErrors": [], + "exposedVariables": [{ "name": "item", "description": "Item of list", "type": "object" }], + "version": 3, + "language": "typescript", + "placeholder": "", + "wrapInTemplate": true, + "templateSettings": { "functionName": "filterSelectedFunc"}, + "availableConstantsExpression": "return metadataBuilder.object(\"constants\").addObject(\"value\", \"Value of autocomplete\").build();" }, { "id": "Wda2Q9DU5DDExAb7MEEfK", @@ -425,16 +464,86 @@ "label": "Allow Free Text", "labelAlign": "right", "parentId": "f9pNv-hqF5ukaDytb7xwq", - "hidden": { - "_code": "return !(getSettingValue(data?.dataSourceType) === 'url' && getSettingValue(data?.useRawValues) === true);", - "_mode": "code", - "_value": false - }, + "hidden": {"_code": "return getSettingValue(data?.valueFormat) !== 'simple';", "_mode": "code", "_value": false}, "isDynamic": false, "description": "Allow to use free text that is missing on the source", "validate": {}, "settingsValidationErrors": [], "version": 3 + }, + { + "id": "hpm6rN_aj-L_KaG5MLIZt", + "type": "propertyAutocomplete", + "propertyName": "displayPropName", + "label": "Display Property", + "labelAlign": "right", + "parentId": "uEFBpy19tApJMiBuFyj9s", + "hidden": false, + "isDynamic": false, + "placeholder": "", + "description": "Name of the property that should be displayed in the autocomplete. Live empty to use default display property defined on the back-end.", + "validate": {}, + "modelType": "{{data.entityType}}", + "autoFillProps": false, + "settingsValidationErrors": [], + "version": 2 + }, + { + "id": "4c6133f1-8886-4527-aa6c-ae0606506a8a", + "type": "propertyAutocomplete", + "propertyName": "keyPropName", + "parentId": "Xp6zDosEy-IrSeDfHHwh-", + "label": "Key Property Name", + "modelType": "{{data.entityType}}", + "autoFillProps": false, + "settingsValidationErrors": [], + "hidden": {"_code": "return getSettingValue(data?.valueFormat) === 'entityReference';", "_mode": "code", "_value": false}, + "version": 2 + }, + { + "id": "11hpm6rN_aj-L_KaG5MLIZt", + "type": "propertyAutocomplete", + "propertyName": "fields", + "label": "Fields to fetch", + "labelAlign": "right", + "parentId": "f9pNv-hqF5ukaDytb7xwq", + "mode": "multiple", + "validate": {}, + "modelType": "{{data.entityType}}", + "settingsValidationErrors": [], + "hidden": {"_code": "return getSettingValue(data?.dataSourceType) !== 'entitiesList';", "_mode": "code", "_value": false}, + "version": 2 + }, + { + "id": "casrMotX62bQPBNc9D6jK", + "type": "dataSortingEditor", + "propertyName": "sorting", + "componentName": "standardSorting", + "label": "Sort By", + "labelAlign": "right", + "parentId": "f9pNv-hqF5ukaDytb7xwq", + "hidden": { "_code": "return getSettingValue(data?.dataSourceType) !== 'entitiesList';", "_mode": "code", "_value": false }, + "isDynamic": false, + "version": 0, + "modelType": "{{data.entityType}}", + "validate": {}, + "settingsValidationErrors": [] + }, + { + "id": "hWKZzsgtRKKXBy8TdBFMy", + "type": "dataSortingEditor", + "propertyName": "grouping", + "componentName": "grouping", + "label": "Grouping", + "labelAlign": "right", + "maxItemsCount": 1, + "parentId": "root", + "hidden": { "_code": "return getSettingValue(data?.dataSourceType) !== 'entitiesList';", "_mode": "code", "_value": false }, + "isDynamic": false, + "version": 0, + "validate": {}, + "settingsValidationErrors": [], + "modelType": "{{data.entityType}}" } ] }, @@ -620,37 +729,41 @@ }, { "id": "c6ec53eb-3bec-40d6-b1a1-687c5d1c3d1d", - "type": "textField", + "type": "endpointsAutocomplete", "propertyName": "quickviewGetEntityUrl", - "parentId": "SsWPpiJfTbgl4iHlFA_or", "label": "Get Entity Url", - "version": 5, + "parentId": "SsWPpiJfTbgl4iHlFA_or", "hidden": { "_code": "return !getSettingValue(data?.quickviewEnabled);", "_mode": "code", "_value": false - }, - "textType": "text", - "desktop": {}, - "tablet": {}, - "mobile": {} - }, + }, + "isDynamic": false, + "prefix": "GET", + "settingsValidationErrors": [], + "version": 3, + "mode": "url", + "httpVerb": "get" + }, { "id": "b0eeb8cb-d9ad-479e-a731-10d5909a50a2", - "type": "textField", + "type": "propertyAutocomplete", "propertyName": "quickviewDisplayPropertyName", "parentId": "SsWPpiJfTbgl4iHlFA_or", "label": "Display Property Name", - "version": 5, + "labelAlign": "right", "hidden": { "_code": "return !getSettingValue(data?.quickviewEnabled);", "_mode": "code", "_value": false }, - "textType": "text", - "desktop": {}, - "tablet": {}, - "mobile": {} + "isDynamic": false, + "placeholder": "", + "validate": {}, + "modelType": "{{data.entityType}}", + "autoFillProps": false, + "settingsValidationErrors": [], + "version": 2 }, { "id": "d690a0b7-a9d3-4ab2-8843-9dcfbac08adc", @@ -822,7 +935,6 @@ "propertyName": "borderRadius", "label": "Border Radius", "parentId": "G670M5i5T85_M0HrSligg", - "defaultValue": "8", "version": 3 }, { diff --git a/shesha-reactjs/src/designer-components/autocomplete/settingsForm.ts b/shesha-reactjs/src/designer-components/autocomplete/settingsForm.ts new file mode 100644 index 0000000000..b830ca78f1 --- /dev/null +++ b/shesha-reactjs/src/designer-components/autocomplete/settingsForm.ts @@ -0,0 +1,1208 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs } from '../_settings/utils/border/utils'; +import { getCornerInputs } from '../_settings/utils/border/utils'; +import { IAutocompleteComponentProps } from './interfaces'; +import { nanoid } from '@/utils/uuid'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; + +export const getSettings = (data: IAutocompleteComponentProps) => { + + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const validationTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'placeholder', + label: 'Placeholder', + type: 'textField', + size: 'small', + jsSetting: true, + }, + { + id: nanoid(), + type: 'textArea', + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + type: 'editModeSelector', + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'disableSearch', + label: 'Disable Search', + size: 'small', + jsSetting: true, + }) + .toJson() + ] + }, + { + key: '2', + title: 'Data', + id: dataTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'mode', + label: 'Selection Mode', + jsSetting: true, + size: 'small', + dropdownOptions: [ + { value: 'single', label: 'Single' }, + { value: 'multiple', label: 'Multiple' }, + ], + }) + // //check defaultValue + // .addSettingsInput({ + // id: nanoid(), + // inputType: 'codeEditor', + // propertyName: 'defaultValue', + // label: 'Default Value', + // size: 'small', + // jsSetting: true, + // parentId: dataTabId, + // wrapInTemplate: true, + // validate: {}, + // settingsValidationErrors: [], + // templateSettings: { + // "functionName": "getDefaultValue" + // }, + // availableConstantsExpression: " return metadataBuilder\n .object(\"constants\")\n .addAllStandard(\"shesha:selectedRow\").build();" + // }) + .addSettingsInput({ + id: nanoid(), + parentId: dataTabId, + label: "Data Source Type", + propertyName: "dataSourceType", + inputType: "dropdown", + size: "small", + jsSetting: true, + dropdownOptions: [ + { + value: "entitiesList", + label: "Entities list" + }, + { + value: "url", + label: "URL" + }, + ] + }) + .addContainer({ + id: nanoid(), + parentId: dataTabId, + propertyName: 'container1', + label: 'Container', + labelAlign: 'right', + hidden: { + _code: `return getSettingValue(data.dataSourceType) !== 'url';`, + _mode: 'code', + _value: false + }, + direction: 'vertical', + justifyContent: 'left', + settingsValidationErrors: [], + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: dataTabId, + label: "Data Source URL", + propertyName: "dataSourceURL", + inputType: "textField", + size: "small", + jsSetting: true, + }) + + .addSettingsInput({ + id: nanoid(), + parentId: dataTabId, + label: "Key Prop Name", + propertyName: "keyPropName", + inputType: "textField", + size: "small", + jsSetting: true, + }) + + .addSettingsInput({ + id: nanoid(), + parentId: dataTabId, + label: "Value Prop Name", + propertyName: "valuePropName", + inputType: "textField", + size: "small", + jsSetting: true, + }) + + .addSettingsInput({ + id: nanoid(), + inputType: 'labelValueEditor', + parentId: dataTabId, + propertyName: 'queryParams', + label: 'Query Param', + labelName: 'param', + labelTitle: 'Param', + valueName: 'value', + valueTitle: 'Value', + mode: 'dialog', + version: 2 + }) + .toJson() + ] + }) + .addContainer({ + id: nanoid(), + parentId: dataTabId, + propertyName: 'container2', + label: 'Container2', + labelAlign: 'right', + direction: 'vertical', + justifyContent: 'left', + settingsValidationErrors: [], + components: [...new DesignerToolbarSettings() + .addSettingsInput( + { + id: nanoid(), + inputType: 'autocomplete', + propertyName: 'entityType', + label: 'Entity Type', + labelAlign: 'right', + parentId: dataTabId, + hidden: false, + dataSourceType: 'url', + validate: {}, + jsSetting: true, + dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', + useRawValues: true + } + ) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return !getSettingValue(data.entityType);', + _mode: 'code', + _value: false + }, + inputs: [ + { + id: nanoid(), + type: 'queryBuilder', + parentId: dataTabId, + propertyName: 'filter', + label: 'Entity Filter', + isDynamic: true, + validate: {}, + hidden: false, + settingsValidationErrors: [], + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + fieldsUnavailableHint: "Please select `Entity Type` to be able to configure this filter.", + }, + ], + }) + + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: "return getSettingValue(data?.dataSourceType) === 'entitiesList';", + _mode: 'code', + _value: false + }, + inputs: [ + { + id: nanoid(), + parentId: dataTabId, + label: 'Data Source URL', + propertyName: "dataSourceUrl", + type: "endpointsAutocomplete", + size: "small", + jsSetting: true, + mode: "url", + httpVerb: "get", + isDynamic: false, + } + + ], + }) + + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: "return getSettingValue(data?.dataSourceType) === 'url';", + _mode: 'code', + _value: false + }, + inputs: [ + { + id: nanoid(), + parentId: dataTabId, + label: 'Custom Source URL', + propertyName: "dataSourceUrl", + type: "endpointsAutocomplete", + size: "small", + jsSetting: true, + mode: "url", + httpVerb: "get", + isDynamic: false, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: "return getSettingValue(data?.dataSourceType) !== 'entitiesList';", + _mode: 'code', + _value: false + }, + inputs: [ + { + id: nanoid(), + type: 'propertyAutocomplete', + parentId: dataTabId, + propertyName: 'fields', + label: 'Fields to Fetch', + isDynamic: true, + jsSetting: true, + validate: {}, + hidden: false, + mode: 'multiple', + settingsValidationErrors: [], + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + fieldsUnavailableHint: "Please select `Entity Type` to be able to configure this filter.", + }, + + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: "return getSettingValue(data?.dataSourceType) !== 'entitiesList';", + _mode: 'code', + _value: false + }, + inputs: [ + { + id: nanoid(), + type: 'dataSortingEditor', + parentId: dataTabId, + propertyName: 'sorting', + label: 'Sort By', + isDynamic: true, + jsSetting: true, + validate: {}, + hidden: false, + settingsValidationErrors: [], + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + }, + + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: "return getSettingValue(data?.dataSourceType) !== 'entitiesList';", + _mode: 'code', + _value: false + }, + inputs: [ + { + id: nanoid(), + type: 'dataSortingEditor', + parentId: dataTabId, + propertyName: 'grouping', + label: 'Grouping', + isDynamic: true, + jsSetting: true, + validate: {}, + hidden: false, + settingsValidationErrors: [], + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + }, + + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + type: 'dropdown', + propertyName: 'valueFormat', + label: 'Value Format', + hidden: false, + dropdownOptions: { + _code: `return getSettingValue(data?.dataSourceType) === 'entitiesList' ? [{\"label\": \"Simple ID\",\"value\":\"simple\",\"id\": \"1\"}, + {\"label\": \"Entity reference\",\"value\": \"entityReference\",\"id\": \"2\"},{\"label\": \"Custom\",\"value\": \"custom\",\"id\": \"3\"}] + : [{\"label\": \"Simple ID\",\"value\": \"simple\",\"id\": \"1\"},{\"label\": \"Custom\",\"value\": \"custom\",\"id\": \"3\"}];`, + _mode: "code", + _value: false + } as any, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + }, + + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { _code: "return getSettingValue(data?.valueFormat) !== 'custom';", _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'outcomeValueFunc', + label: 'Value Function', + labelAlign: 'right', + description: 'Return value for item object', + mode: 'dialog', + exposedVariables: [{ name: 'item', description: 'Item of list', type: 'object' }], + wrapInTemplate: true, + templateSettings: { functionName: 'outcomeValueFunc' }, + availableConstantsExpression: 'return metadataBuilder.object("constants").addObject("item", "Item of list").build();', + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + }, + + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { _code: "return getSettingValue(data?.valueFormat) !== 'custom';", _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'keyValueFunc', + label: 'Key Value Function', + labelAlign: 'right', + description: 'Return key from selected value', + mode: 'dialog', + exposedVariables: [{ name: 'value', description: 'Value of item', type: 'object' }], + wrapInTemplate: true, + templateSettings: { functionName: 'keyValueFunc' }, + availableConstantsExpression: 'return metadataBuilder.object("constants").addObject("value", "Value of item").build();', + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + } + + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { _code: "return getSettingValue(data?.valueFormat) !== 'custom';", _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'displayValueFunc', + label: 'Display Value Function', + labelAlign: 'right', + hidden: false, + description: "Return display value for item's object", + mode: 'dialog', + exposedVariables: [{ name: 'item', description: 'Item of list', type: 'object' }], + wrapInTemplate: true, + templateSettings: { functionName: 'displayValueFunc' }, + availableConstantsExpression: 'return metadataBuilder.object("constants").addObject("item", "Item of list").build();', + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + } + + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { _code: "return getSettingValue(data?.valueFormat) !== 'custom';", _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'filterKeysFunc', + label: 'Filter Selected Function', + labelAlign: 'right', + description: 'Return filter object (JsonLogic) for selected value(s). Use this settings to configure non-standard values format', + mode: 'dialog', + exposedVariables: [{ name: 'item', description: 'Item of list', type: 'object' }], + wrapInTemplate: true, + templateSettings: { functionName: 'filterSelectedFunc' }, + availableConstantsExpression: 'return metadataBuilder.object("constants").addObject("value", "Value of autocomplete").build();', + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return !getSettingValue(data.entityType);', + _mode: 'code', + _value: false + }, + inputs: [ + { + id: nanoid(), + propertyName: 'displayPropName', + label: 'Display Property', + parentId: dataTabId, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + isDynamic: false, + autoFillProps: false, + settingsValidationErrors: [], + type: 'propertyAutocomplete', + size: 'small', + validate: { + required: true, + } + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'useRawValues', + label: 'Use Raw Values', + labelAlign: 'right', + jsSetting: true, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + hidden: { _code: "return getSettingValue(data?.valueFormat) !== 'simple';", _mode: 'code', _value: false } as any, + type: 'switch', + propertyName: 'allowFreeText', + label: 'Allow Free Text', + labelAlign: 'right', + jsSetting: true, + description: 'Allow to use free text that is missing on the source', + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + } + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'quickviewEnabled', + label: 'Use Quickview', + parentId: dataTabId, + size: 'small', + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlQuickView', + label: 'Quickview', + labelAlign: 'right', + parentId: dataTabId, + ghost: true, + collapsible: 'header', + hidden: { + _code: 'return !getSettingValue(data?.quickviewEnabled);', + _mode: 'code', + _value: false + }, + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: false, + inputs: [ + { + id: nanoid(), + type: 'formAutocomplete', + propertyName: 'quickviewFormPath', + label: 'Form Path', + parentId: dataTabId, + size: 'small', + validate: { + required: false, + } + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: false, + inputs: [ + + { + id: nanoid(), + type: 'endpointsAutocomplete', + propertyName: 'quickviewGetEntityUrl', + parentId: dataTabId, + label: 'Get Entity URL', + size: 'small', + version: 5 + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: false, + inputs: [ + { + id: nanoid(), + propertyName: 'quickviewDisplayPropertyName', + label: 'Display Property', + parentId: dataTabId, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + isDynamic: false, + autoFillProps: false, + settingsValidationErrors: [], + type: 'propertyAutocomplete', + size: 'small', + validate: { + required: true, + } + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: false, + inputs: [ + { + id: nanoid(), + type: 'textField', + propertyName: 'quickviewWidth', + parentId: dataTabId, + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit", + label: 'Width', + size: 'small', + version: 5 + } + ], + }) + .toJson() + ] + } + }) + .toJson() + ] + }) + .toJson() + ] + }, + { + key: '4', + title: 'Validation', + id: validationTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + id: nanoid(), + inputType: 'switch', + propertyName: 'validate.required', + label: 'Required', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: validationTabId + }) + .toJson() + ] + }, + { + key: '5', + title: 'Events', + id: eventsTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onChangeCustom', + label: 'On Change', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on changing of event. (form, event) are exposed', + parentId: eventsTabId, + availableConstantsExpression: "return metadataBuilder\n .object(\"constants\")\n .addAllStandard()\n .addObject(\"value\", \"Component current value\")\n .addObject(\"option\", \"Meta data of component current value\") \t\n .build();" + }) + .toJson() + ] + }, + { + key: '6', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: styleRouterId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "background.size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + }, + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson()] + }).toJson()] + }, + { + key: '7', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/autocomplete/utils.ts b/shesha-reactjs/src/designer-components/autocomplete/utils.ts new file mode 100644 index 0000000000..dc4ceb31aa --- /dev/null +++ b/shesha-reactjs/src/designer-components/autocomplete/utils.ts @@ -0,0 +1,21 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + return { + background: { type: 'color', color: '#fff' }, + font: { weight: '400', size: 14, color: '#000', type: 'Segoe UI' }, + border: { + border: { + all: { width: '1px', style: 'solid', color: '#d9d9d9' }, + top: { width: '1px', style: 'solid', color: '#d9d9d9' }, + bottom: { width: '1px', style: 'solid', color: '#d9d9d9' }, + left: { width: '1px', style: 'solid', color: '#d9d9d9' }, + right: { width: '1px', style: 'solid', color: '#d9d9d9' }, + }, + radius: { all: 8, topLeft: 8, topRight: 8, bottomLeft: 8, bottomRight: 8 }, + borderType: 'all', + radiusType: 'all' + }, + dimensions: { width: '100%', height: 'auto', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/button/button.tsx b/shesha-reactjs/src/designer-components/button/button.tsx index 5cd78c8e6d..b0965d06c1 100644 --- a/shesha-reactjs/src/designer-components/button/button.tsx +++ b/shesha-reactjs/src/designer-components/button/button.tsx @@ -1,9 +1,8 @@ import ConfigurableButton from './configurableButton'; -import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import React from 'react'; import { BorderOutlined } from '@ant-design/icons'; import { getSettings } from './settingsForm'; -import { getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { validateConfigurableComponentSettings } from '@/providers/form/utils'; import { IButtonComponentProps } from './interfaces'; import { IButtonGroupItemBaseV0, migrateV0toV1 } from './migrations/migrate-v1'; import { IToolboxComponent } from '@/interfaces'; @@ -12,10 +11,9 @@ import { migrateCustomFunctions, migratePropertyName, migrateReadOnly } from '@/ import { migrateNavigateAction } from '@/designer-components/_common-migrations/migrate-navigate-action'; import { migrateV1toV2 } from './migrations/migrate-v2'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; -import { useForm, useFormData, useSheshaApplication } from '@/providers'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; -import { addPx } from './util'; -import { removeNullUndefined } from '@/providers/utils'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultStyles } from './util'; export type IActionParameters = [{ key: string; value: string }]; @@ -26,46 +24,26 @@ const ButtonComponent: IToolboxComponent = { icon: , Factory: ({ model, form }) => { const { style, ...restProps } = model; - const { formMode } = useForm(); - const { data } = useFormData(); - const { anyOfPermissionsGranted } = useSheshaApplication(); - - const fieldModel = { - ...restProps, - label: null, - tooltip: null, + const finalStyle = { + ...model.allStyles.dimensionsStyles, + ...(['primary', 'default'].includes(model.buttonType) && model.allStyles.borderStyles), + ...model.allStyles.fontStyles, + ...(['dashed', 'default'].includes(model.buttonType) && model.allStyles.backgroundStyles), + ...(['primary', 'default'].includes(model.buttonType) && model.allStyles.shadowStyles), + ...model.allStyles.stylingBoxAsCSS, + ...model.allStyles.jsStyle, + justifyContent: model.font?.align }; - - const grantedPermission = anyOfPermissionsGranted(restProps?.permissions || []); - - if (!grantedPermission && formMode !== 'designer') { - return null; - } - - const newStyles = { - width: addPx(model.width), - height: addPx(model.height), - backgroundColor: model.backgroundColor, - fontSize: addPx(model.fontSize), - color: model.color, - fontWeight: model.fontWeight, - borderWidth: addPx(model.borderWidth), - borderColor: model.borderColor, - borderStyle: model.borderStyle, - borderRadius: addPx(model.borderRadius) - }; - + return ( - - - + ); }, settingsFormMarkup: data => getSettings(data), @@ -84,7 +62,7 @@ const ButtonComponent: IToolboxComponent = { .add(0, prev => { const buttonModel: IButtonGroupItemBaseV0 = { ...prev, - hidden: prev.hidden as boolean, + hidden: prev.hidden, label: prev.label ?? 'Submit', sortOrder: 0, itemType: 'item', @@ -99,8 +77,13 @@ const ButtonComponent: IToolboxComponent = { .add(5, (prev) => ({ ...prev, actionConfiguration: migrateNavigateAction(prev.actionConfiguration) })) .add(6, (prev) => migrateReadOnly(prev, 'editable')) .add(7, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) - - , + .add(8, (prev) => ({ + ...prev, + desktop: { ...prev.desktop, buttonType: prev.buttonType || 'default' }, + mobile: { ...prev.mobile, buttonType: prev.buttonType || 'default' }, + tablet: { ...prev.tablet, buttonType: prev.buttonType || 'default' } + })) + .add(9, (prev) => ({ ...migratePrevStyles(prev, defaultStyles(prev)) })), }; export default ButtonComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/button/buttonGroup/buttonGroup.tsx b/shesha-reactjs/src/designer-components/button/buttonGroup/buttonGroup.tsx index 5113c6875c..36608c40eb 100644 --- a/shesha-reactjs/src/designer-components/button/buttonGroup/buttonGroup.tsx +++ b/shesha-reactjs/src/designer-components/button/buttonGroup/buttonGroup.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { CSSProperties, FC, useCallback, useEffect, useState } from 'react'; import ShaIcon, { IconType } from '@/components/shaIcon/index'; import { Alert, @@ -18,50 +18,97 @@ import { import { ConfigurableButton } from '../configurableButton'; import { DynamicActionsEvaluator } from '@/providers/dynamicActions/evaluator/index'; import { + getActualModel, getStyle, IApplicationContext, + pickStyleFromModel, useAvailableConstantsData } from '@/providers/form/utils'; import { getButtonGroupMenuItem } from './utils'; import { IButtonGroupProps } from './models'; import { SizeType } from 'antd/lib/config-provider/SizeContext'; +import { useActualContextData, useDeepCompareMemo } from '@/hooks'; import { useSheshaApplication } from '@/providers'; import type { FormInstance, MenuProps } from 'antd'; import { useStyles } from './styles/styles'; import classNames from 'classnames'; -import { addPx } from '../util'; import { removeNullUndefined } from '@/providers/utils'; -import { useActualContextData } from '@/hooks/useActualContextData'; +import { getDimensionsStyle } from '@/designer-components/_settings/utils/dimensions/utils'; +import { getBorderStyle } from '@/designer-components/_settings/utils/border/utils'; +import { getFontStyle } from '@/designer-components/_settings/utils/font/utils'; +import { getShadowStyle } from '@/designer-components/_settings/utils/shadow/utils'; +import { getBackgroundImageUrl, getBackgroundStyle } from '@/designer-components/_settings/utils/background/utils'; +import ValidationErrors from '@/components/validationErrors'; +import { isValidGuid } from '@/components/formDesigner/components/utils'; +import { removeUndefinedProps } from '@/utils/object'; +import { getOverflowStyle } from '@/designer-components/_settings/utils/overflow/util'; import { standartActualModelPropertyFilter } from '@/components/formDesigner/formComponent'; +import { addPx } from '@/utils/style'; type MenuItem = MenuProps['items'][number]; +type PrepareItemFunc = (item: ButtonGroupItemProps, parentReadOnly: boolean) => ButtonGroupItemProps; + type MenuButton = ButtonGroupItemProps & { childItems?: MenuButton[]; + dividerWidth?: string; + dividerColor?: string; }; -const renderButton = (props: ButtonGroupItemProps, uuid: string, appContext: IApplicationContext, form?: FormInstance) => { - - const { width, height, backgroundColor, fontSize, color, fontWeight, borderWidth, borderColor, borderStyle, borderRadius, size, buttonType } = props; - const newStyles = { - width: addPx(width), - height: addPx(height), - backgroundColor: backgroundColor, - fontSize: addPx(fontSize), - color: color, - fontWeight: fontWeight, - borderWidth: addPx(borderWidth), - borderColor: borderColor, - borderStyle: borderStyle, - borderRadius: addPx(borderRadius) - }; +const RenderButton: FC<{ props: ButtonGroupItemProps; uuid: string; appContext: IApplicationContext; form?: FormInstance }> = ({ props, uuid, appContext, form }) => { + const { backendUrl, httpHeaders } = useSheshaApplication(); + const [imageUrl, setImageUrl] = useState(''); + const { size, buttonType, background } = props; + const model = props; + + const dimensions = model?.dimensions; + const border = model?.border; + const font = model?.font; + const shadow = model?.shadow; + const jsStyle = getStyle(model.style, appContext.data); + + const dimensionsStyles = getDimensionsStyle(dimensions); + const borderStyles = getBorderStyle(border, jsStyle); + const fontStyles = getFontStyle(font); + const shadowStyles = getShadowStyle(shadow); + + useEffect(() => { + const fetchImage = async () => { + const url = await getBackgroundImageUrl(background, backendUrl, httpHeaders); + setImageUrl(url); + }; + fetchImage(); + }, [background, backendUrl, httpHeaders]); + + const additionalStyles: CSSProperties = removeUndefinedProps({ + ...dimensionsStyles, + ...fontStyles, + ...(['primary', 'default'].includes(buttonType) && borderStyles), + ...(['primary', 'default'].includes(buttonType) && shadowStyles), + ...(['dashed', 'default'].includes(buttonType) && getBackgroundStyle(background, jsStyle, imageUrl)), + ...jsStyle, + justifyContent: font?.align, + }); + + const finalStyle = removeUndefinedProps({ ...additionalStyles, fontWeight: Number(model?.font?.weight?.split(' - ')[0]) || 400 }); + + if (model?.background?.type === 'storedFile' && model?.background.storedFile?.id && !isValidGuid(model?.background.storedFile.id)) { + return ; + } + + const styling = JSON.parse(model.stylingBox || '{}'); + const stylingBoxAsCSS = pickStyleFromModel(styling); + + const finalStyles = removeUndefinedProps({ + ...finalStyle, ...stylingBoxAsCSS, '--ant-button-padding-block-lg': '0px' + }); return ( ): MenuItem => { const buttonProps = props.itemType === 'item' ? (props as IButtonGroupItem) : null; const isDivider = buttonProps && (buttonProps.itemSubType === 'line' || buttonProps.itemSubType === 'separator'); const childItems = props.childItems && props.childItems.length > 0 - ? props.childItems.filter(getIsVisible)?.map((props) => createMenuItem(props, getIsVisible, appContext, form)) + ? props.childItems.map(x => prepareItem(x, props.readOnly)).filter(getIsVisible)?.map((props) => createMenuItem(props, getIsVisible, appContext, prepareItem, form)) : null; return isDivider - ? { type: 'divider' } + ? { type: 'divider', style: { height: addPx(props.dividerWidth), backgroundColor: props.dividerColor } } : getButtonGroupMenuItem( - renderButton(props, props?.id, appContext, form), + , props.id, props.readOnly, childItems @@ -103,15 +151,17 @@ interface InlineItemBaseProps { interface InlineItemProps extends InlineItemBaseProps { item: ButtonGroupItemProps; + prepareItem: PrepareItemFunc; form?: FormInstance; + styles?: CSSProperties; } const InlineItem: FC = (props) => { - const { item, uuid, getIsVisible, appContext, form } = props; + const { item, uuid, getIsVisible, appContext, prepareItem, form } = props; if (isGroup(item)) { - const menuItems = item.childItems + const menuItems = item.childItems.map(x => prepareItem(x, item.readOnly)) .filter(item => (getIsVisible(item))) - .map(childItem => (createMenuItem({ ...childItem, buttonType: childItem.buttonType ?? 'link' }, getIsVisible, appContext, form))); + .map(childItem => (createMenuItem({ ...childItem, buttonType: childItem.buttonType ?? 'link' }, getIsVisible, appContext, prepareItem, form))); return ( = (props) => { type={item.buttonType} title={item.tooltip} disabled={item.readOnly} - + className={classNames('sha-toolbar-btn sha-toolbar-btn-configurable')} > {item.label ? item.label : undefined} {item.downIcon ? : undefined} @@ -133,12 +183,13 @@ const InlineItem: FC = (props) => { } if (isItem(item)) { + switch (item.itemSubType) { case 'button': - return renderButton(item, uuid, appContext, form); + return ; case 'separator': case 'line': - return ; + return ; default: return null; } @@ -149,11 +200,21 @@ const InlineItem: FC = (props) => { type ItemVisibilityFunc = (item: ButtonGroupItemProps) => boolean; -export const ButtonGroupInner: FC = ({ items, size, spaceSize = 'middle', isInline, form }) => { +export const ButtonGroupInner: FC = (props) => { const { styles } = useStyles(); const allData = useAvailableConstantsData(); const { anyOfPermissionsGranted } = useSheshaApplication(); + // ToDo: AS - review optimization + const preparedItems = props.items?.map((item) => { + // add editMode property if not exists + const preparedItem = { ...item, editMode: typeof item['editMode'] === 'undefined' ? undefined : item['editMode'] }; + return getActualModel(preparedItem, allData, props.readOnly); + }); + const items = useDeepCompareMemo(() => preparedItems, [preparedItems]); + + const { size, spaceSize = 'middle', isInline, readOnly: disabled, form } = props; + const isDesignMode = allData.form?.formMode === 'designer'; const isVisibleBase = (item: ButtonGroupItemProps): boolean => { @@ -189,9 +250,28 @@ export const ButtonGroupInner: FC = ({ items, size, spaceSize return isItem(item) && isVisibleBase(item) || isGroup(item) && isGroupVisible(item, getIsVisible); }; - const filteredItems = items?.filter(getIsVisible); + const prepareItem: PrepareItemFunc = useCallback((item, parentReadOnly) => { + if (item.editMode === undefined) + item.editMode = 'inherited'; // prepare editMode property if not exist for updating inside getActualModel + const result = getActualModel(item, allData, parentReadOnly); + return { ...result }; + }, [allData]); + + const actualItems = useDeepCompareMemo(() => { + return Promise.all(items?.map(async (item) => { + return prepareItem({ ...item }, disabled); + }) || []); + }, [items, allData.contexts.lastUpdate, allData.data, allData.form?.formMode, allData.globalState, allData.selectedRow]); + + const [resolvedItems, setResolvedItems] = useState([]); + + useEffect(() => { + actualItems?.then(setResolvedItems); + }, [actualItems]); - if (items.length === 0 && isDesignMode) + const filteredItems = resolvedItems?.filter(getIsVisible); + + if (resolvedItems.length === 0 && isDesignMode) return ( = ({ items, size, spaceSize /> ); + if (isInline) { return ( - + {filteredItems?.map((item) => - () + () )} ); } else { - const menuItems = filteredItems?.map((props) => createMenuItem(props, getIsVisible, allData, form)); + const menuItems = filteredItems?.map((props) => createMenuItem(props, getIsVisible, allData, prepareItem, form)); + return (
); @@ -226,9 +308,12 @@ export const ButtonGroupInner: FC = ({ items, size, spaceSize }; export const ButtonGroup: FC = (props) => { - const items = useActualContextData(props.items, props.readOnly, null, standartActualModelPropertyFilter ); + const items = useActualContextData(props.items, props.readOnly, null, standartActualModelPropertyFilter); + + const memoizedItems = useDeepCompareMemo(() => items, [items]); + return ( - + {(items) => ()} ); diff --git a/shesha-reactjs/src/designer-components/button/buttonGroup/buttonGroupComponent.tsx b/shesha-reactjs/src/designer-components/button/buttonGroup/buttonGroupComponent.tsx index e9c27e3d40..8714fd1382 100644 --- a/shesha-reactjs/src/designer-components/button/buttonGroup/buttonGroupComponent.tsx +++ b/shesha-reactjs/src/designer-components/button/buttonGroup/buttonGroupComponent.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { ButtonGroup } from './buttonGroup'; import { ButtonGroupItemProps, isGroup, isItem } from '@/providers/buttonGroupConfigurator/models'; -import { ButtonGroupSettingsForm } from './settings'; import { GroupOutlined } from '@ant-design/icons'; import { IButtonGroupComponentProps } from './models'; import { IToolboxComponent } from '@/interfaces'; @@ -11,14 +10,17 @@ import { migrateV0toV1 } from './migrations/migrate-v1'; import { migrateV1toV2 } from './migrations/migrate-v2'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { migrateFormApi } from '@/designer-components/_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { migratePrevStyles, migrateStyles } from '@/designer-components/_common-migrations/migrateStyles'; +import { defaultContainerStyles, defaultStyles } from './utils'; const ButtonGroupComponent: IToolboxComponent = { type: 'buttonGroup', isInput: false, name: 'Button Group', icon: , - Factory: ({ model ,form}) => { - return model.hidden ? null : ; + Factory: ({ model, form }) => { + return model.hidden ? null : ; }, actualModelPropertyFilter: (name) => name !== 'items', // handle items later to use buttonGroup's readOnly setting migrator: (m) => m @@ -53,14 +55,14 @@ const ButtonGroupComponent: IToolboxComponent = { return { ...item }; }; - + newModel.items = prev.items?.map(updateItemDefaults); return newModel; }) .add(6, (prev) => migrateVisibility(prev)) .add(7, (prev) => migrateButtonsNavigateAction(prev)) .add(8, (prev) => { - const newModel = {...prev, editMode: 'editable'} as IButtonGroupComponentProps; + const newModel = { ...prev, editMode: 'editable' } as IButtonGroupComponentProps; const updateItems = (item: ButtonGroupItemProps): ButtonGroupItemProps => { const newItem = migrateReadOnly(item, 'inherited'); @@ -70,9 +72,9 @@ const ButtonGroupComponent: IToolboxComponent = { }; newModel.items = newModel.items.map(updateItems); - return newModel ; + return newModel; }) - .add(9, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) + .add(9, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) .add(10, (prev) => { const setDownIcon = (item: ButtonGroupItemProps): ButtonGroupItemProps => { if (isGroup(item)) { @@ -86,8 +88,20 @@ const ButtonGroupComponent: IToolboxComponent = { items: prev.items.map(setDownIcon), }; }) - , - settingsFormFactory: (props) => (), + .add(11, (prev) => ({ ...migratePrevStyles(prev, defaultContainerStyles(prev)) })) + .add(12, (prev) => { + const newModel = { ...prev }; + const updateItems = (item: ButtonGroupItemProps): ButtonGroupItemProps => { + const newItem = { ...item, ...migrateStyles(item, defaultStyles(item)) }; + if (Array.isArray(newItem['childItems'])) + newItem['childItems'] = newItem['childItems'].map(updateItems); + return newItem; + }; + + newModel.items = newModel.items.map(updateItems); + return newModel; + }), + settingsFormMarkup: (props) => getSettings(props), }; export default ButtonGroupComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/button/buttonGroup/models.ts b/shesha-reactjs/src/designer-components/button/buttonGroup/models.ts index aed6582b61..0ff261c57f 100644 --- a/shesha-reactjs/src/designer-components/button/buttonGroup/models.ts +++ b/shesha-reactjs/src/designer-components/button/buttonGroup/models.ts @@ -1,12 +1,13 @@ import { SizeType } from 'antd/lib/config-provider/SizeContext'; -import { IConfigurableFormComponent } from '@/providers/form/models'; +import { IConfigurableFormComponent, IStyleType } from '@/providers/form/models'; import { ButtonGroupItemProps } from '@/providers/buttonGroupConfigurator/models'; import { FormInstance } from 'antd'; +import { CSSProperties } from 'react'; export interface IButtonGroupComponentProps extends IConfigurableFormComponent, IBaseButtonGroupProps { } -export interface IButtonGroupProps extends IBaseButtonGroupProps { +export interface IButtonGroupProps extends IBaseButtonGroupProps, IStyleType { id: string; readOnly?: boolean; form?: FormInstance; @@ -18,4 +19,5 @@ export interface IBaseButtonGroupProps { spaceSize?: SizeType; isInline?: boolean; noStyles?: boolean; + styles?: CSSProperties; } diff --git a/shesha-reactjs/src/designer-components/button/buttonGroup/settingsForm.ts b/shesha-reactjs/src/designer-components/button/buttonGroup/settingsForm.ts new file mode 100644 index 0000000000..e1bf8baeb2 --- /dev/null +++ b/shesha-reactjs/src/designer-components/button/buttonGroup/settingsForm.ts @@ -0,0 +1,519 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { IButtonGroupComponentProps } from './models'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '@/designer-components/_settings/utils/background/utils'; +import { getBorderInputs, getCornerInputs } from '@/designer-components/_settings/utils/border/utils'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data: IButtonGroupComponentProps) => { + // Generate unique IDs for main sections + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Component Name', + parentId: commonTabId, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + parentId: commonTabId, + type: 'buttonGroupConfigurator', + propertyName: 'items', + label: 'Configure Buttons' + }, + { + id: nanoid(), + parentId: commonTabId, + type: 'switch', + propertyName: 'isInline', + label: 'Is Button Inline', + jsSetting: true + } + ], + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + propertyName: 'spaceSize', + label: 'Gap', + tooltip: 'The size of space between items', + jsSetting: true, + dropdownOptions: [ + { + label: 'Small', + value: 'small', + }, + { + label: 'Middle', + value: 'middle', + }, + { + label: 'Large', + value: 'large', + } + ], + }, + ], + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { _code: 'return getSettingValue(data?.tabType) === "line";', _mode: 'code', _value: false } as any, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: styleRouterId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "background.size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + parentId: styleRouterId, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson()] + }).toJson()] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/button/buttonGroup/styles/styles.ts b/shesha-reactjs/src/designer-components/button/buttonGroup/styles/styles.ts index 872bd6e1ea..c5ed484667 100644 --- a/shesha-reactjs/src/designer-components/button/buttonGroup/styles/styles.ts +++ b/shesha-reactjs/src/designer-components/button/buttonGroup/styles/styles.ts @@ -1,10 +1,11 @@ import { createStyles } from '@/styles'; export const useStyles = createStyles(({ css, cx, prefixCls, token }) => { - const shaResponsiveButtonGroup = "sha-responsive-button-group"; - const shaResponsiveButtonGroupInlineContainer = "sha-responsive-button-group-inline-container"; - const shaButtonMenu = "sha-button-menu"; - const a = css` + const shaResponsiveButtonGroup = "sha-responsive-button-group"; + const shaResponsiveButtonGroupInlineContainer = "sha-responsive-button-group-inline-container"; + const shaButtonMenu = "sha-button-menu"; + const a = css` + .${shaButtonMenu} { .${prefixCls}-menu-submenu.${prefixCls}-menu-submenu-popup & { padding: unset !important; @@ -18,28 +19,28 @@ export const useStyles = createStyles(({ css, cx, prefixCls, token }) => { } `; -/* -// Make sure that all the buttons in the popups are styled like links -.${prefixCls}-menu-submenu.${prefixCls}-menu-submenu-popup { - .sha-button-menu { - padding: unset !important; - button { - color: var(--ant-primary-color) !important; - border-color: transparent !important; - background: transparent !important; - box-shadow: none !important; + /* + // Make sure that all the buttons in the popups are styled like links + .${prefixCls}-menu-submenu.${prefixCls}-menu-submenu-popup { + .sha-button-menu { + padding: unset !important; + button { + color: var(--ant-primary-color) !important; + border-color: transparent !important; + background: transparent !important; + box-shadow: none !important; + } } } -} -*/ + */ - const shaResponsiveButtonGroupContainer = cx("sha-responsive-button-group-container", css` - .${shaResponsiveButtonGroup} { - line-height: unset; - min-height: 30px; - .${prefixCls}-menu-item, - .${prefixCls}-menu-vertical, - .${prefixCls}-menu-submenu, + const shaResponsiveButtonGroupContainer = cx("sha-responsive-button-group-container", css` + .${shaResponsiveButtonGroup} { + line-height: unset; + min-height: 30px; + .${prefixCls}-menu-item, + .${prefixCls}-menu-vertical, + .${prefixCls}-menu-submenu, .${prefixCls}-menu-sub, .${prefixCls}-menu-item-only-child { padding: unset !important; @@ -95,10 +96,10 @@ export const useStyles = createStyles(({ css, cx, prefixCls, token }) => { } } `); - return { - shaResponsiveButtonGroupContainer, - shaResponsiveButtonGroup, - shaResponsiveButtonGroupInlineContainer, - a, - }; - }); \ No newline at end of file + return { + shaResponsiveButtonGroupContainer, + shaResponsiveButtonGroup, + shaResponsiveButtonGroupInlineContainer, + a, + }; +}); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/button/buttonGroup/utils.tsx b/shesha-reactjs/src/designer-components/button/buttonGroup/utils.tsx index 12309da2f9..8a65b9636a 100644 --- a/shesha-reactjs/src/designer-components/button/buttonGroup/utils.tsx +++ b/shesha-reactjs/src/designer-components/button/buttonGroup/utils.tsx @@ -1,8 +1,19 @@ import { MenuProps } from 'antd'; import React from 'react'; +import { IStyleType } from '@/index'; type MenuItem = MenuProps['items'][number]; +/** + * Removes CSS units from a value and returns the numeric value + * @param value - CSS value with units (e.g. '10px', '1.5rem', '50%') + * @returns The numeric value without units + */ +export const removeCssUnit = (value: string | number): number => { + if (typeof value === 'number') return value; + return parseFloat(value); +}; + export function getButtonGroupMenuItem( label: React.ReactNode, key: React.Key, @@ -16,4 +27,47 @@ export function getButtonGroupMenuItem( className: 'sha-button-menu', disabled, } as MenuItem; +}; + +export const defaultStyles = (prev): IStyleType => { + return { + background: { type: 'color', color: prev.backgroundColor, }, + font: { weight: prev.fontWeight ?? '400', size: prev.fontSize ?? 14, type: prev.fontFamily ?? 'Segoe UI', align: 'center' }, + border: { + borderType: 'all', + radiusType: 'all', + border: { + all: { + width: prev.borderWidth ?? '1px', + style: prev.borderStyle ?? 'solid', + color: prev.borderColor ?? '#d9d9d9' + }, + }, + radius: { all: prev.borderRadius ?? 8 } + }, + shadow: { spreadRadius: 0, blurRadius: 0, color: '#000', offsetX: 0, offsetY: 0 }, + dimensions: { + width: prev.block ? '100%' : 'auto', + height: prev.size === 'small' ? '24px' : prev.size === 'middle' ? '32px' : '40px', + minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto' + }, + stylingBox: '{"paddingLeft":"15","paddingBottom":"4","paddingTop":"4","paddingRight":"15"}', + }; +}; + +export const defaultContainerStyles = (prev): IStyleType => { + return { + background: { type: 'color' }, + border: { + border: { all: { width: '1px', style: 'none', color: '#d9d9d9' } }, + radius: { all: 8 }, + hideBorder: false, + borderType: 'all', + }, + shadow: { spreadRadius: 0, blurRadius: 0, color: '#000', offsetX: 0, offsetY: 0 }, + dimensions: { width: prev.block ? '100%' : 'auto', height: 'auto', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' } + }; }; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/button/configurableButton/index.tsx b/shesha-reactjs/src/designer-components/button/configurableButton/index.tsx index 366b458218..82a986d972 100644 --- a/shesha-reactjs/src/designer-components/button/configurableButton/index.tsx +++ b/shesha-reactjs/src/designer-components/button/configurableButton/index.tsx @@ -1,18 +1,17 @@ -import React, { FC, useState } from 'react'; +import React, { FC, useState, CSSProperties } from 'react'; import { Button, FormInstance } from 'antd'; import { ShaIcon, IconType } from '@/components'; import classNames from 'classnames'; import { IButtonItem } from '@/providers/buttonGroupConfigurator/models'; -import { CSSProperties } from 'react'; import { useConfigurableActionDispatcher } from '@/providers/configurableActionsDispatcher'; import { useAvailableConstantsData } from '@/providers/form/utils'; import { isNavigationActionConfiguration, useShaRouting, useTheme } from '@/index'; import { useAsyncMemo } from '@/hooks/useAsyncMemo'; - +import { useStyles } from './style'; export interface IConfigurableButtonProps extends Omit { style?: CSSProperties; form: FormInstance; - dynamicItem?: any; + dynamicItem?: any; } export const ConfigurableButton: FC = props => { @@ -23,6 +22,7 @@ export const ConfigurableButton: FC = props => { const dynamicContext = useActionDynamicContext(actionConfiguration); const { theme } = useTheme(); + const { styles } = useStyles(); const [loading, setLoading] = useState(false); const [isModal, setModal] = useState(false); @@ -74,7 +74,7 @@ export const ConfigurableButton: FC = props => { danger={props.danger} icon={props.icon ? : undefined} iconPosition={props.iconPosition} - className={classNames('sha-toolbar-btn sha-toolbar-btn-configurable')} + className={classNames('sha-toolbar-btn sha-toolbar-btn-configurable', styles.configurableButton)} size={props?.size} disabled={buttonDisabled} style={{ ...props?.style, ...(isSameUrl && { background: theme.application.primaryColor, color: theme.text.default }) }} diff --git a/shesha-reactjs/src/designer-components/button/configurableButton/style.ts b/shesha-reactjs/src/designer-components/button/configurableButton/style.ts new file mode 100644 index 0000000000..c1c1d35bcd --- /dev/null +++ b/shesha-reactjs/src/designer-components/button/configurableButton/style.ts @@ -0,0 +1,21 @@ +import { createStyles } from "@/styles"; + +export const useStyles = createStyles(({ css, cx }) => { + const configurableButton = cx("flex-container", css` + .sha-toolbar-btn-configurable { + display: flex; + align-items: center; + max-width: 100%; + span { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + } + `); + return { + configurableButton, + }; +}); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/button/interfaces.ts b/shesha-reactjs/src/designer-components/button/interfaces.ts index fba1bc335d..64125ed784 100644 --- a/shesha-reactjs/src/designer-components/button/interfaces.ts +++ b/shesha-reactjs/src/designer-components/button/interfaces.ts @@ -1,4 +1,4 @@ -import { IConfigurableFormComponent } from '@/providers/form/models'; +import { IConfigurableFormComponent, IStyleType } from '@/providers/form/models'; import { IButtonItem } from '@/providers/buttonGroupConfigurator/models'; -export interface IButtonComponentProps extends IButtonItem, IConfigurableFormComponent {} \ No newline at end of file +export interface IButtonComponentProps extends IButtonItem, IConfigurableFormComponent, Omit { } \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/button/settingsForm.ts b/shesha-reactjs/src/designer-components/button/settingsForm.ts index f5a3056ddc..0bca7ba177 100644 --- a/shesha-reactjs/src/designer-components/button/settingsForm.ts +++ b/shesha-reactjs/src/designer-components/button/settingsForm.ts @@ -1,349 +1,605 @@ import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { buttonTypes } from './util'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { nanoid } from '@/utils/uuid'; -export const getSettings = (data: any) => - new DesignerToolbarSettings(data) - .addCollapsiblePanel({ - id: '11114bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'pnlDisplay', - parentId: 'root', - label: 'Display', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addTextField({ - id: '5d4d56fb-d7f8-4835-a529-c4fa93f3596d', - propertyName: 'componentName', - label: 'Component name', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: { - required: true, - }, - jsSetting: false - }) - .addTextField({ - id: 'd498779d-012a-4c6a-82a9-77231245ae28', - propertyName: 'label', - parentId: 'root', - label: 'Label', - }) - .addTextArea({ - id: '9b671866-072e-4688-8b48-ddf5e12d70d4', - propertyName: 'tooltip', - label: 'Tooltip', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ac357807b', - hidden: false, - autoSize: false, - showCount: false, - allowClear: false, - validate: {}, - }) - .addIconPicker({ - id: '91b404a6-4021-4b0a-b9ef-007167a93075', - propertyName: 'icon', - label: 'Icon', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ac357807b', - hidden: false, - settingsValidationErrors: [], - }) - .addDropdown({ - id: '5a6d6b4d-7d8e-4b5d-8b0b-0f9a6c1d1f4b', - propertyName: 'iconPosition', - label: 'Icon Position', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ucl57807b', - hidden: false, - validate: {}, - dataSourceType: 'values', - values: [ - { - id: 'f5e5b8e2-0b7b-4f7f-8b1f-0e8d7d9amg1a', - label: 'start', - value: 'start', - }, - { - id: 'c9b1e4f7-6e8d-4f4f-8b1f-0e8d7da45d1a', - label: 'end', - value: 'end', - }, - ], - }) - .addDropdown({ - id: 'be15598e-5c23-40bc-8245-6b5385bb7963', - propertyName: 'buttonType', - label: 'Button Type', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ac357807b', - hidden: false, - validate: { - required: true, - }, - dataSourceType: 'values', - values: [ - { - id: 'c4a96833-8ed7-4085-8848-169d5607136d', - label: 'primary', - value: 'primary', - }, - { - id: 'c6f974da-ad28-44e5-8e4d-50280cf24ae7', - label: 'ghost', - value: 'ghost', - }, - { - id: '71c0dc14-0473-4748-ae75-a4ed3bd6cffd', - label: 'dashed', - value: 'dashed', - }, - { - id: '789d5733-2d4f-4969-890f-613e5b4a7d59', - label: 'link', - value: 'link', - }, - { - id: '36abe636-40b2-476c-95b0-78a50478146b', - label: 'text', - value: 'text', - }, - { - id: 'de08ea36-a831-4373-ab10-ce25fadf80cd', - label: 'default', - value: 'default', - }, - ], - }) - .addCheckbox({ - id: '4e9b886a-6186-4467-a688-639b30a0e06f', - propertyName: 'danger', - label: 'Danger', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ac357807b', - hidden: false, - validate: {}, - }) - .addCheckbox({ - id: 'd25a7d85-6afe-4595-899f-62675fb6c491', - propertyName: 'block', - label: 'Block', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ac357807b', - hidden: false, - validate: {}, - }) - .addCheckbox({ - id: 'cfd7d45e-c7e3-4a27-987b-dc525c412448', - propertyName: 'hidden', - parentId: 'root', - label: 'Hidden', - }) - .addEditMode({ - id: '24a8be15-98eb-40f7-99ea-ebb602693e9c', - propertyName: 'editMode', - parentId: 'root', - label: "Edit mode", - }) - .addConfigurableActionConfigurator({ - id: 'F3B46A95-703F-4465-96CA-A58496A5F78C', - propertyName: 'actionConfiguration', - label: 'Action configuration', - hidden: false, - validate: {}, - settingsValidationErrors: [], - }) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: '22224bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'pnlStyle', - parentId: 'root', - label: 'Style', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addCodeEditor({ - id: '06ab0599-914d-4d2d-875c-765a495472f8', - propertyName: 'style', - label: 'Style', - parentId: 'root', - validate: {}, - settingsValidationErrors: [], - description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', - exposedVariables: [{ name: 'data', description: 'Form values', type: 'object' }], - }) - .addDropdown({ - id: '8615d12f-6ea0-4b11-a1a1-6088c7160fd9', - propertyName: 'size', - parentId: 'root', - label: 'Size', - allowClear: true, - values: [ - { - label: 'Small', - value: 'small', - id: '4f11403c-95fd-4e49-bb60-cb8c25f0f3c3', - }, - { - label: 'Middle', - value: 'middle', - id: '8f85c476-e632-4fa7-89ad-2be6cfb7f1f1', - }, - { - label: 'Large', - value: 'large', - id: 'f01e54aa-a1a4-4bd6-ba73-c395e48af8ce', - }, - ], - dataSourceType: 'values', - }) - .addTextField({ - id: 'd498779d-012a-4c6a-82a9-77231001ae28', - propertyName: 'width', - parentId: 'root', - label: 'Width', - }) - .addTextField({ - id: 'd498779d-012a-4c6a-82a9-771121245ae28', - propertyName: 'height', - parentId: 'root', - label: 'Height', - }) - .addColorPicker({ - id: 'd4987360d-012a-4c6a-82a9-77231245ae28', - propertyName: 'backgroundColor', - parentId: 'root', - label: 'Background Color', - allowClear: true - }) - .addTextField({ - id: 'd498779d-012a-4c6a-82a9-77231234ve28', - propertyName: 'fontSize', - parentId: 'root', - label: 'Font Size', - }) - .addColorPicker({ - id: 'd498779d-012p-4c6a-82a9-7723124uae28', - propertyName: 'color', - parentId: 'root', - label: 'Color', - allowClear: true - }) - .addDropdown({ - id: 'd498779d-012a-4c6a-82c9-00731245ae28', - propertyName: 'fontWeight', - parentId: 'root', - label: 'Font Weight', - dataSourceType: 'values', - description: "Defines the thickness of font characters. Use values like 400 for normal, 100 for thin, 500 for medium, 700 for bold, and 900 for extra bold when working with JS.", - values: [ - { - id: 'c4a96833-8ed7-4085-8848-169d5607136d', - label: 'Thin', - value: '100', - }, - { - id: 'c6f974da-ad28-44e5-8e4d-50280cf24ae7', - label: 'Normal', - value: '400', - }, - { - id: '71c0dc14-0473-4748-ae75-a4ed3bd6c0fd', - label: 'Medium', - value: '500', - }, - { - id: '71c0dc14-0473-4748-ae75-a4ed3bd90ffd', - label: 'Bold', - value: '700', - }, - { - id: '71c04514-0473-4748-ae75-00ed3bd6cffd', - label: 'Extra bold', - value: '900', - } - ], - }) - .addTextField({ - id: 'd498779d-012a-4c6a-82a9-77231245ce33', - propertyName: 'borderWidth', - parentId: 'root', - label: 'Border Width', - }) - .addColorPicker({ - id: 'd498779d-012a-4c6a-82a9-77231245ce28', - propertyName: 'borderColor', - parentId: 'root', - label: 'Border Color', - allowClear: true - }) - .addDropdown({ - id: 'd498779d-012a-4c6a-10ma9-77231245ae28', - propertyName: 'borderStyle', - parentId: 'root', - label: 'Border Style', - dataSourceType: 'values', - values: [ - { - id: 'c4a96833-8ed7-4085-8848-169d5607136d', - label: 'solid', - value: 'solid', - }, - { - id: 'c6f974da-ad28-44e5-8e4d-50280cf24ae7', - label: 'dashed', - value: 'dashed', - }, - { - id: '71c0dc14-0473-4748-ae75-a4ed3bd6cffd', - label: 'dotted', - value: 'dotted', - }, - ], - }) - .addTextField({ - id: 'd498779d-012a-4c6a-82a9-77231245ae26', - propertyName: 'borderRadius', - parentId: 'root', - label: 'Border Radius', - }) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() +export const getSettings = (data) => { + + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'componentName', + label: 'Component Name', + parentId: commonTabId, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'label', + label: 'Caption', + jsSetting: true, + parentId: commonTabId, + }, + { + id: nanoid(), + type: 'textArea', + propertyName: 'tooltip', + label: 'Tooltip', + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + type: 'iconPicker', + propertyName: 'icon', + label: 'Icon', + size: 'small', + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'iconPosition', + label: 'Icon Position', + size: 'small', + jsSetting: true, + type: 'radio', + buttonGroupOptions: [ + { title: 'Start', value: 'start', icon: 'LeftOutlined' }, + { title: 'End', value: 'end', icon: 'RightOutlined' }, + ], + hidden: { + _code: 'return !getSettingValue(data?.icon);', + _mode: 'code', + _value: false + } as any + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .addConfigurableActionConfigurator({ + id: nanoid(), + propertyName: 'actionConfiguration', + label: 'Action Configuration', + validate: {}, + settingsValidationErrors: [], + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'buttonType', + label: 'Type', + defaultValue: 'default', + validate: { + required: true, + }, + inputType: 'dropdown', + dropdownOptions: buttonTypes, + parentId: styleRouterId, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + propertyName: 'font.color', + hideLabel: true, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType) === "link";', _mode: 'code', _value: false } as any, + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + hidden: { _code: 'return ["text", "link", "ghost", "dashed"].includes(getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType));', _mode: 'code', _value: false } as any, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { _code: 'return ["text", "link", "ghost", "primary"].includes(getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType));', _mode: 'code', _value: false } as any, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: styleRouterId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + hideLabel: true, + propertyName: "background.size", + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + }, + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + hidden: { _code: 'return ["text", "link", "ghost"].includes(getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType));', _mode: 'code', _value: false } as any, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + parentId: styleRouterId, + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson()] + }).toJson()] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } ] - } - }) - .toJson(); \ No newline at end of file + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/button/util.ts b/shesha-reactjs/src/designer-components/button/util.ts index 8c14c9facb..0658b74434 100644 --- a/shesha-reactjs/src/designer-components/button/util.ts +++ b/shesha-reactjs/src/designer-components/button/util.ts @@ -1,3 +1,56 @@ -export const addPx = (value) => { - return !value ? null : /^\d+(\.\d+)?$/.test(value) ? `${value}px` : value; -}; \ No newline at end of file +import { IStyleType } from "@/index"; + +export const buttonTypes = [ + { + label: 'Default', + value: 'default', + }, + { + label: 'Primary', + value: 'primary', + }, + { + label: 'Ghost', + value: 'ghost', + }, + { + label: 'Dashed', + value: 'dashed', + }, + { + label: 'Link', + value: 'link', + }, + { + label: 'Text', + value: 'text', + } +]; + +export const defaultStyles = (prev): IStyleType => { + return { + background: { type: 'color' }, + font: { weight: '400', size: 14, type: 'Segoe UI', align: 'center' }, + border: { + border: { all: { width: '1px', style: 'solid', color: '#d9d9d9' } }, + radius: { all: 8 }, + hideBorder: false, + borderType: 'all', + }, + shadow: { + color: '#000000', + offsetX: 0, + offsetY: 0, + blurRadius: 0, + spreadRadius: 0, + }, + dimensions: { + width: prev.block ? '100%' : 'auto', + height: '32px', minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto' + } + }; +}; + diff --git a/shesha-reactjs/src/designer-components/card/index.tsx b/shesha-reactjs/src/designer-components/card/index.tsx index c0f11a088a..34553a5227 100644 --- a/shesha-reactjs/src/designer-components/card/index.tsx +++ b/shesha-reactjs/src/designer-components/card/index.tsx @@ -1,19 +1,25 @@ import ComponentsContainer from '@/components/formDesigner/containers/componentsContainer'; import { IToolboxComponent } from '@/interfaces'; -import { useFormData, useGlobalState } from '@/providers'; +import { useFormData, useGlobalState, useSheshaApplication } from '@/providers'; import { useForm } from '@/providers/form'; -import { getLayoutStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { getLayoutStyle, getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; import ParentProvider from '@/providers/parentProvider/index'; import { nanoid } from '@/utils/uuid'; import { CodeSandboxSquareFilled } from '@ant-design/icons'; import { Card } from 'antd'; -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { ICardComponentProps } from './interfaces'; import { getSettings } from './settingsForm'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; import classNames from 'classnames'; import { useStyles } from './styles'; import { removeComponents } from '../_common-migrations/removeComponents'; +import { getBorderStyle } from '../_settings/utils/border/utils'; +import { getShadowStyle } from '../_settings/utils/shadow/utils'; +import { getBackgroundStyle } from '../_settings/utils/background/utils'; +import { removeNullUndefined } from '@/providers/utils'; +import { defaultStyles } from './utils'; + const CardComponent: IToolboxComponent = { type: 'card', @@ -25,9 +31,45 @@ const CardComponent: IToolboxComponent = { const { formMode } = useForm(); const { globalState } = useGlobalState(); const { styles } = useStyles(); + const { backendUrl, httpHeaders } = useSheshaApplication(); const title = model.hideHeading ? null : model.label; + const border = model?.border; + const shadow = model?.shadow; + const background = model?.background; + const jsStyle = getStyle(model.style, model); + + const borderStyles = useMemo(() => getBorderStyle(border, jsStyle), [border, jsStyle]); + const [backgroundStyles, setBackgroundStyles] = useState({}); + const shadowStyles = useMemo(() => getShadowStyle(shadow), [shadow]); + useEffect(() => { + const fetchStyles = async () => { + + const storedImageUrl = background?.storedFile?.id && background?.type === 'storedFile' + ? await fetch(`${backendUrl}/api/StoredFile/Download?id=${background?.storedFile?.id}`, + { headers: { ...httpHeaders, "Content-Type": "application/octet-stream" } }) + .then((response) => { + return response.blob(); + }) + .then((blob) => { + return URL.createObjectURL(blob); + }) : ''; + + const style = await getBackgroundStyle(background, jsStyle, storedImageUrl); + setBackgroundStyles(style); + }; + + fetchStyles(); + }, [background, background?.gradient?.colors, backendUrl, httpHeaders, jsStyle]); + + const newStyles = { + ...borderStyles, + ...backgroundStyles, + ...shadowStyles, + ...jsStyle + }; + const headerComponents = model?.header?.components ?? []; const extra = @@ -47,7 +89,7 @@ const CardComponent: IToolboxComponent = { className={classNames(model.className, { [styles.hideWhenEmpty]: model.hideWhenEmpty })} title={title} extra={extra} - style={getLayoutStyle(model, { data, globalState })} + style={{ ...removeNullUndefined(newStyles), ...getLayoutStyle(model, { data, globalState }) }} > = { migrator: (m) => m .add(1, (prev) => ({ ...migrateFormApi.properties(prev) })) .add(2, (prev) => removeComponents(prev)) + .add(3, (prev) => ({ ...prev, desktop: { ...defaultStyles(prev) }, mobile: { ...defaultStyles(prev) }, tablet: { ...defaultStyles(prev) } })), + }; export default CardComponent; diff --git a/shesha-reactjs/src/designer-components/card/interfaces.ts b/shesha-reactjs/src/designer-components/card/interfaces.ts index a5171f3d93..aa1fcd677c 100644 --- a/shesha-reactjs/src/designer-components/card/interfaces.ts +++ b/shesha-reactjs/src/designer-components/card/interfaces.ts @@ -1,4 +1,7 @@ import { IConfigurableFormComponent } from '@/providers/form/models'; +import { IBorderValue } from '../_settings/utils/border/interfaces'; +import { IShadowValue } from '../_settings/utils/shadow/interfaces'; +import { IBackgroundValue } from '../_settings/utils/background/interfaces'; interface ICardContent { id: string; @@ -11,4 +14,7 @@ export interface ICardComponentProps extends IConfigurableFormComponent { header?: ICardContent; hideHeading?: boolean; hideWhenEmpty?: boolean; + border?: IBorderValue; + shadow?: IShadowValue; + background?: IBackgroundValue; } diff --git a/shesha-reactjs/src/designer-components/card/settingsForm.ts b/shesha-reactjs/src/designer-components/card/settingsForm.ts index 5817609dc2..269ae9b6bb 100644 --- a/shesha-reactjs/src/designer-components/card/settingsForm.ts +++ b/shesha-reactjs/src/designer-components/card/settingsForm.ts @@ -1,173 +1,409 @@ import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; import { nanoid } from '@/utils/uuid'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; -export const getSettings = (data: any) => - new DesignerToolbarSettings(data) - .addCollapsiblePanel({ - id: nanoid(), - propertyName: 'pnlDisplay', - parentId: 'root', - label: 'Display', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: '11a170e0-ff22-4448-a2f3-0030580ea52b', - components: [ - ...new DesignerToolbarSettings() - .addPropertyAutocomplete({ - id: nanoid(), - propertyName: 'componentName', - parentId: '11a170e0-ff22-4448-a2f3-0030580ea52b', - label: 'Component name', - validate: { - required: true, - }, - jsSetting: false - }) - .addTextField({ - id: nanoid(), - propertyName: 'label', - parentId: '11a170e0-ff22-4448-a2f3-0030580ea52b', - label: 'Heading', - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'hidden', - parentId: '11a170e0-ff22-4448-a2f3-0030580ea52b', - label: 'Hidden', - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'hideHeading', - parentId: '11a170e0-ff22-4448-a2f3-0030580ea52b', - label: 'Hide Heading', - }) - .toJson(), - ], - }, - }) - .addPropertyRouter({ - id: nanoid(), - propertyName: 'propertyRouter1', - componentName: 'propertyRouter1', - label: 'Property router1', - labelAlign: 'right', - parentId: 'root', - hidden: false, - propertyRouteName: { - _mode: "code", - _code: " return contexts.canvasContext?.designerDevice || 'desktop';", - _value: "" - }, - components: [ - ...new DesignerToolbarSettings() - .addCollapsiblePanel({ - id: nanoid(), - propertyName: 'pnlStyle', - parentId: 'root', - label: 'Style', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: '92ad3873-216c-4465-a21f-489f21e9cca5', - components: [ - ...new DesignerToolbarSettings() - .addTextField({ - id: nanoid(), - propertyName: 'className', - componentName: 'className', - parentId: '92ad3873-216c-4465-a21f-489f21e9cca5', - label: 'Custom CSS Class', - textType: 'text', - description: 'Custom CSS Class to add to this component', - }) - .addCodeEditor({ - id: nanoid(), - propertyName: 'style', - parentId: '92ad3873-216c-4465-a21f-489f21e9cca5', - label: 'Style', - description: - 'A script that returns the style of the element as an object. This should conform to CSSProperties', - exposedVariables: [ - { + +export const getSettings = (data: any) => { + // Generate unique IDs for major components + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ id: nanoid(), - name: 'data', - description: 'Form data', - type: 'object', - }, - { + propertyName: 'componentName', + parentId: commonTabId, + label: 'Component Name', + inputType: 'propertyAutocomplete', + validate: { + required: true, + }, + jsSetting: false + }) + .addSettingsInput({ id: nanoid(), - name: 'globalState', - description: 'The global state', - type: 'object', - }, - ], - }) - .addStyleBox({ - id: nanoid(), - propertyName: 'stylingBox', - parentId: '92ad3873-216c-4465-a21f-489f21e9cca5', - validate: {}, - settingsValidationErrors: [], - jsSetting: false, - }) - .toJson(), - ], - }, - }).toJson()]}) - .addCollapsiblePanel({ - id: nanoid(), - propertyName: 'pnlVisibility', - parentId: 'root', - label: 'Visibility', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6f', - components: [ - ...new DesignerToolbarSettings() - .addCheckbox({ - id: nanoid(), - propertyName: 'hideWhenEmpty', - parentId: 'pnl5bfe4-ee69-431e-931b-b0e0b9ceee6f', - label: 'Hide when empty', - labelAlign: 'right', - hidden: false, - isDynamic: false, - description: 'Allows to hide the panel when all components are hidden due to some conditions', - }) - .toJson(), + propertyName: 'label', + parentId: commonTabId, + label: 'Heading', + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'hidden', + parentId: commonTabId, + label: 'Hide', + inputType: 'switch', + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'hideHeading', + parentId: commonTabId, + label: 'Hide Heading', + inputType: 'switch', + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + label: 'Hide When Empty', + propertyName: 'hideWhenEmpty', + defaultValue: false, + jsSetting: true, + }) + .toJson(), + ], + }, + + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + hidden: { _code: 'return ["text", "link", "ghost"].includes(getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType));', _mode: 'code', _value: false } as any, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { _code: 'return ["text", "link", "ghost"].includes(getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType));', _mode: 'code', _value: false } as any, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: styleRouterId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + hideLabel: true, + propertyName: "background.size", + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + }, + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + hidden: { _code: 'return ["text", "link", "ghost"].includes(getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType));', _mode: 'code', _value: false } as any, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + inputType: 'numberField', + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + inputType: 'numberField', + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + inputType: 'numberField', + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + inputType: 'numberField', + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'textField', + propertyName: 'className', + label: 'Custom CSS Class', + }) + .toJson() + ] + } + }) + .toJson() + ] + }) + .toJson() + ] + }, + { + key: '4', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, ], - }, - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] - } - }) - .toJson(); + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/card/utils.ts b/shesha-reactjs/src/designer-components/card/utils.ts new file mode 100644 index 0000000000..ca6783130a --- /dev/null +++ b/shesha-reactjs/src/designer-components/card/utils.ts @@ -0,0 +1,35 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (prev): IStyleType => { + const { size } = prev; + + return { + border: { hideBorder: false, radiusType: 'all', borderType: 'all', border: { all: { width: '1px', style: 'solid', color: '#d9d9d9' } }, radius: { all: 8 } }, + dimensions: { width: '100%', height: size === 'small' ? '22px' : size === 'large' ? '38px' : '30px', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' }, + background: { + type: 'color', + color: '#fff', + repeat: 'no-repeat', + size: 'cover', + position: 'center', + gradient: { direction: 'to right', colors: {} }, + url: '', + storedFile: { id: null }, + uploadFile: null, + }, + font: { + color: '#000', + type: 'Segoe UI', + align: 'left', + size: 14, + weight: '400', + }, + shadow: { + offsetX: 0, + offsetY: 0, + color: '#000', + blurRadius: 0, + spreadRadius: 0 + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/charts/bar.tsx b/shesha-reactjs/src/designer-components/charts/bar.tsx new file mode 100644 index 0000000000..e2ef748785 --- /dev/null +++ b/shesha-reactjs/src/designer-components/charts/bar.tsx @@ -0,0 +1,58 @@ +import { ConfigurableFormItem } from '@/components'; +import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; +import { IToolboxComponent } from '@/interfaces'; +import { BarChartOutlined } from '@ant-design/icons'; +import React from 'react'; +import ChartDataProvider from '../../providers/chartData'; +import ChartControl from './chartControl'; +import ChartControlURL from './chartControlURL'; +import { IChartProps } from './model'; +import { getSettings } from './settingsFormIndividual'; +import { defaultConfigFiller, filterNonNull } from './utils'; + +const BarChartComponent: IToolboxComponent = { + type: 'barChart', + name: 'Bar Chart', + isInput: false, + isOutput: true, + icon: , + Factory: ({ model }) => { + if (model.hidden) return null; + + return ( + + {() => { + return ( + + {model.dataMode === 'url' ? : } + + ); + }} + + ); + }, + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + migrator: (m) => m + .add(0, prev => ({ + chartType: 'bar', + showTitle: false, + showLegend: true, + legendPosition: 'top', + hidden: false, + ...prev, + })) + .add(1, prev => ({ ...prev, hideLabel: true })) + .add(2, prev => ({ ...prev, showBorder: true })) + .add(3, prev => ({ ...prev, isDoughnut: false })) + .add(4, prev => ({ ...prev, showLegend: false, showTitle: true })) + .add(5, prev => ({ + ...defaultConfigFiller, + stacked: false, + ...filterNonNull(prev), + type: prev.type, + id: prev.id + })) +}; + +export default BarChartComponent; diff --git a/shesha-reactjs/src/designer-components/charts/chartControl.tsx b/shesha-reactjs/src/designer-components/charts/chartControl.tsx index bbd42ebc85..7b31b427d4 100644 --- a/shesha-reactjs/src/designer-components/charts/chartControl.tsx +++ b/shesha-reactjs/src/designer-components/charts/chartControl.tsx @@ -1,4 +1,3 @@ -import ConditionalWrap from '@/components/conditionalWrapper'; import { useGet } from '@/hooks'; import { useFormData, useMetadataDispatcher, useNestedPropertyMetadatAccessor } from '@/index'; import { IRefListPropertyMetadata } from '@/interfaces/metadata'; @@ -6,43 +5,33 @@ import { useFormEvaluatedFilter } from '@/providers/dataTable/filters/evaluateFi import { useReferenceListDispatcher } from '@/providers/referenceListDispatcher'; import { toCamelCase } from '@/utils/string'; import { LoadingOutlined } from '@ant-design/icons'; -import { Alert, Button, Flex, Result, Spin } from 'antd'; -import React, { useEffect, useMemo } from 'react'; +import { Alert, Flex, Spin } from 'antd'; +import React, { useEffect } from 'react'; import { useChartDataActionsContext, useChartDataStateContext } from '../../providers/chartData'; -import BarChart from './components/bar'; -import FilterComponent from './components/filterComponent'; -import LineChart from './components/line'; -import PieChart from './components/pie'; -import PolarAreaChart from './components/polarArea'; -import { useBarChartData, useLineChartData, usePieOrPolarAreaChartData, usePivotChartData } from "./hooks"; +import { useProcessedChartData } from "./hooks"; import { IChartData, IChartsProps } from './model'; import useStyles from './styles'; -import { applyFilters, formatDate, getAllProperties, getChartDataRefetchParams } from './utils'; +import { formatDate, getChartDataRefetchParams, getResponsiveStyle, renderChart } from './utils'; + const ChartControl: React.FC = (props) => { - const { chartType, entityType, valueProperty, filters, legendProperty, - axisProperty, simpleOrPivot, filterProperties, allowFilter, - isAxisTimeSeries, timeSeriesFormat, orderBy, orderDirection + const { chartType, entityType, valueProperty, legendProperty, + axisProperty, filterProperties, isAxisTimeSeries, timeSeriesFormat, + orderBy, orderDirection } = props; const { refetch } = useGet({ path: '', lazy: true }); const state = useChartDataStateContext(); const { getMetadata } = useMetadataDispatcher(); const { getReferenceList } = useReferenceListDispatcher(); - const { setData, setIsFilterVisible, setIsLoaded, setFilterdData, setChartFilters, setControlProps } = useChartDataActionsContext(); + const { setData, setIsLoaded, setFilterdData, setControlProps } = useChartDataActionsContext(); const { data: formData } = useFormData(); const { styles, cx } = useStyles(); - useEffect(() => { - setControlProps({ - ...props - }); - }, [props, formData]); - - const memoFilters = useMemo(() => filters, [filters, formData]); + useEffect(() => setControlProps(props), [props, formData]); const propertyMetadataAccessor = useNestedPropertyMetadatAccessor(entityType); - const evaluatedFilters = useFormEvaluatedFilter({ filter: memoFilters, metadataAccessor: propertyMetadataAccessor }); + const evaluatedFilters = useFormEvaluatedFilter({ metadataAccessor: propertyMetadataAccessor }); useEffect(() => { if (!entityType || !valueProperty || !axisProperty) { return; @@ -103,10 +92,7 @@ const ChartControl: React.FC = (props) => { } }, [state.data]); - const lineChartData = useLineChartData(); - const barChartData = useBarChartData(); - const pieOrPolarAreaChartData = usePieOrPolarAreaChartData(); - const pivotChartData = usePivotChartData(); + const data: IChartData = useProcessedChartData(); if (!entityType || !chartType || !valueProperty || !axisProperty) { // Collect the missing properties in an array @@ -129,22 +115,6 @@ const ChartControl: React.FC = (props) => { ); } - const toggleFilterVisibility = () => { - setIsFilterVisible(!state.isFilterVisible); - }; - - const resetFilter = () => { - setChartFilters([]); - setFilterdData(state.data); - }; - - const onFilter = () => { - const afterFilterData = applyFilters(state.data, state.chartFilters); - setFilterdData(afterFilterData); - }; - - let data: IChartData; - if (!state.isLoaded) { return ( @@ -153,75 +123,16 @@ const ChartControl: React.FC = (props) => { ); } - const wrapperDiv = (children: React.ReactNode) => ( -
200 ? props.height : 'auto', - width: props?.width > 300 ? props.width : 'auto', - border: props?.showBorder ? '1px solid #ddd' : 'none' - }}> -
- - - - - 0 ? filterProperties : getAllProperties(state.filteredData)} - isVisible={state.isFilterVisible} - onClose={toggleFilterVisibility} - onFilter={onFilter} - resetFilter={resetFilter} - /> -
- {children} -
- ); - return ( - wrapperDiv(children)} - > - 200 ? props.height : 'auto', - width: props?.width > 300 ? props.width : 'auto', - border: props?.showBorder ? '1px solid #ddd' : 'none' - }}> - { - (() => { - switch (chartType) { - case 'line': - data = simpleOrPivot === 'simple' - ? lineChartData - : pivotChartData; - return ; - case 'bar': - data = simpleOrPivot === 'simple' - ? barChartData - : pivotChartData; - return ; - case 'pie': - data = simpleOrPivot === 'simple' - ? pieOrPolarAreaChartData - : pivotChartData; - return ; - case 'polarArea': - data = simpleOrPivot === 'simple' - ? pieOrPolarAreaChartData - : pivotChartData; - return ; - default: - return ; - } - })() - } - - +
+ {renderChart(chartType, data)} +
); }; diff --git a/shesha-reactjs/src/designer-components/charts/chartControlURL.tsx b/shesha-reactjs/src/designer-components/charts/chartControlURL.tsx index b4bc4ca743..1ec63d8067 100644 --- a/shesha-reactjs/src/designer-components/charts/chartControlURL.tsx +++ b/shesha-reactjs/src/designer-components/charts/chartControlURL.tsx @@ -4,14 +4,10 @@ import { LoadingOutlined } from '@ant-design/icons'; import { Alert, Flex, Result, Spin } from 'antd'; import React, { useEffect } from 'react'; import { useChartDataActionsContext, useChartDataStateContext } from '../../providers/chartData'; -import BarChart from './components/bar'; -import LineChart from './components/line'; -import PieChart from './components/pie'; -import PolarAreaChart from './components/polarArea'; import { useChartURLData } from './hooks'; import { IChartsProps } from './model'; import useStyles from './styles'; -import { getURLChartDataRefetchParams } from './utils'; +import { getResponsiveStyle, getURLChartDataRefetchParams, renderChart } from './utils'; const ChartControlURL: React.FC = (props) => { const { url, chartType } = props; @@ -22,12 +18,7 @@ const ChartControlURL: React.FC = (props) => { const { styles, cx } = useStyles(); - useEffect(() => { - setControlProps({ - ...props - }); - }, [props, formData]); - + useEffect(() => setControlProps(props), [props, formData]); useEffect(() => { if (!url) { return; @@ -39,7 +30,7 @@ const ChartControlURL: React.FC = (props) => { throw new Error('No data returned from the server. Please check the URL and try again.'); } if (!data.result.datasets || !data.result.labels) { - var errors: string[] = []; + let errors: string[] = []; if (!data.result.datasets) { errors.push('No datasets returned from the server. Please check the URL and try again.'); } @@ -63,7 +54,6 @@ const ChartControlURL: React.FC = (props) => { if (!chartType) missingProperties.push("'chartType'"); const descriptionMessage = `Please make sure that you've specified the following properties: ${missingProperties.join(', ')}.`; - return ( = (props) => { if (!state.isLoaded) { return ( - + } /> ); @@ -84,37 +74,20 @@ const ChartControlURL: React.FC = (props) => { if (!state.urlTypeData) { return ( - + ); } return ( - 200 ? props.height : 'auto', - width: props?.width > 300 ? props.width : 'auto', - border: props?.showBorder ? '1px solid #ddd' : 'none' - }}> - { - (() => { - switch (chartType) { - case 'line': - return ; - case 'bar': - return ; - case 'pie': - return ; - case 'polarArea': - return ; - default: - return ; - } - })() - } - +
+ {renderChart(chartType, memoUrlTypeData)} +
); }; diff --git a/shesha-reactjs/src/designer-components/charts/components/bar/index.tsx b/shesha-reactjs/src/designer-components/charts/components/bar/index.tsx index cc550595c0..bda9e66389 100644 --- a/shesha-reactjs/src/designer-components/charts/components/bar/index.tsx +++ b/shesha-reactjs/src/designer-components/charts/components/bar/index.tsx @@ -1,38 +1,47 @@ import { - BarController, BarElement, CategoryScale, - Chart as ChartJS, ChartOptions, + BarController, + BarElement, + CategoryScale, + Chart as ChartJS, + ChartOptions, Legend, - LinearScale, Title, Tooltip + LinearScale, + Title, + Tooltip, } from 'chart.js'; import React from 'react'; import { Bar } from 'react-chartjs-2'; import { useChartDataStateContext } from '../../../../providers/chartData'; import { useGeneratedTitle } from '../../hooks'; import { IChartData, IChartDataProps } from '../../model'; +import { splitTitleIntoLines } from '../../utils'; interface BarChartProps extends IChartDataProps { data: IChartData; } -ChartJS.register( - BarController, - BarElement, - CategoryScale, - LinearScale, - Title, - Tooltip, - Legend -); +ChartJS.register(BarController, BarElement, CategoryScale, LinearScale, Title, Tooltip, Legend); const BarChart: React.FC = ({ data }) => { - const { axisProperty: xProperty, valueProperty: yProperty, aggregationMethod, showLegend, showTitle, legendPosition, showXAxisScale, showXAxisTitle, showYAxisScale, showYAxisTitle, stacked, - } = useChartDataStateContext(); - + const { + axisProperty: xProperty, + valueProperty: yProperty, + aggregationMethod, + showLegend, + showTitle, + legendPosition, + showXAxisScale, + showXAxisTitle, + showYAxisScale, + showYAxisTitle, + stacked, + dataMode, + } = useChartDataStateContext(); + const chartTitle: string = useGeneratedTitle(); - if (!data || !data.datasets || !data.labels) { - if (!data) - throw new Error('BarChart: No data to display. Please check the data source'); + if (!data?.datasets || !data?.labels) { + if (!data) throw new Error('BarChart: No data to display. Please check the data source'); if (!data.datasets || !data.labels) throw new Error('BarChart: No datasets or labels to display. Please check the data source'); @@ -40,36 +49,40 @@ const BarChart: React.FC = ({ data }) => { const options: ChartOptions = { responsive: true, + maintainAspectRatio: true, // Maintain aspect ratio to prevent overflow + aspectRatio: 2, // Width to height ratio (2:1) plugins: { legend: { - display: showLegend ? true : false, + display: !!showLegend, position: legendPosition ?? 'top', }, title: { - display: showTitle && chartTitle.length > 0 ? true : false, - text: chartTitle, + display: !!(showTitle && chartTitle?.length > 0), + text: splitTitleIntoLines(chartTitle), }, }, scales: { x: { title: { - display: showXAxisTitle && xProperty?.trim().length > 0 ? true : false, - text: xProperty?.trim() ?? '' + display: !!(showXAxisTitle && xProperty?.trim().length > 0), + text: xProperty?.trim() ?? '', }, - display: showXAxisScale ? true : false, + display: !!showXAxisScale, stacked: stacked, offset: true, // Ensure the x-axis does not coincide with the y-axis - beginAtZero: false + beginAtZero: false, }, y: { title: { - display: showYAxisTitle && yProperty?.trim().length > 0 ? true : false, - text: `${yProperty?.trim() ?? ''} (${aggregationMethod})`, + display: !!(showYAxisTitle && yProperty?.trim().length > 0), + text: dataMode === 'url' || !aggregationMethod + ? yProperty?.trim() ?? '' + : `${yProperty?.trim() ?? ''} (${aggregationMethod})`, }, - display: showYAxisScale ? true : false, + display: !!showYAxisScale, stacked: stacked, - } - } + }, + }, }; return ; diff --git a/shesha-reactjs/src/designer-components/charts/components/filterComponent/index.tsx b/shesha-reactjs/src/designer-components/charts/components/filterComponent/index.tsx deleted file mode 100644 index d1e996f09e..0000000000 --- a/shesha-reactjs/src/designer-components/charts/components/filterComponent/index.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import React from 'react'; -import { Button, Select, Input, Space, Row, Col, Flex } from 'antd'; -import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; -import { IFilter } from '../../model'; -import useStyles from '../../styles'; - -const { Option } = Select; - -const FilterComponent = ({ - filters, - setFilters, - properties, - isVisible, - onClose, - onFilter, - resetFilter -}: { - filters: IFilter[]; - setFilters: (arg: IFilter[]) => void; - properties: any[]; - isVisible: boolean; - onClose: () => void; - onFilter: () => void; - resetFilter: () => void; -}) => { - const { styles, cx } = useStyles(); - - const addNewCondition = () => { - if (filters.length > 0 && !filters[filters.length - 1]?.property || filters[filters.length - 1]?.property.length === 0) { - return; - } - setFilters([...filters, { property: '', operator: 'contains', value: '' }]); - onFilter(); - }; - - // Handle input change for each filter condition - const handleInputChange = (index: number, field: any, value: string) => { - const newFilters = filters?.map((filter, idx) => - idx === index ? { ...filter, [field]: value } : filter - ); - setFilters(newFilters); - }; - - return ( -
- {filters?.map((filter, index) => ( - 0 ? cx(styles.fullWidth, styles['margin-top-5']) : cx(styles.fullWidth)}> - - - - {/* Select property */} - - - - {/* Select operator */} - - - - {/* Input value */} - {filter.operator === 'is_empty' || filter.operator === 'is_not_empty' ? null : ( - - handleInputChange(index, 'value', e.target.value)} - /> - - )} - {/* Delete Button */} - - - - - - - - -
- ); -}; - -export default FilterComponent; diff --git a/shesha-reactjs/src/designer-components/charts/components/line/index.tsx b/shesha-reactjs/src/designer-components/charts/components/line/index.tsx index e595bea22e..b4fb85319d 100644 --- a/shesha-reactjs/src/designer-components/charts/components/line/index.tsx +++ b/shesha-reactjs/src/designer-components/charts/components/line/index.tsx @@ -1,95 +1,109 @@ import { - CategoryScale, - Chart as ChartJS, - Legend, - LinearScale, - LineController, LineElement, PointElement, - Title, Tooltip + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + LineController, + LineElement, + PointElement, + Title, + Tooltip, } from 'chart.js'; import React, { useEffect } from 'react'; import { Line } from 'react-chartjs-2'; import { useChartDataStateContext } from '../../../../providers/chartData'; import { IChartData, IChartDataProps } from '../../model'; -import { useGeneratedTitle } from "../../hooks"; +import { useGeneratedTitle } from '../../hooks'; +import { splitTitleIntoLines } from '../../utils'; interface ILineChartProps extends IChartDataProps { - data: IChartData; + data: IChartData; } -ChartJS.register( - CategoryScale, - LineController, - LineElement, - PointElement, - LinearScale, - Title, - Tooltip, - Legend -); +ChartJS.register(CategoryScale, LineController, LineElement, PointElement, LinearScale, Title, Tooltip, Legend); const LineChart: React.FC = ({ data }) => { - const { axisProperty: xProperty, valueProperty: yProperty, aggregationMethod, showLegend, showTitle, legendPosition, showXAxisScale, showXAxisTitle, showYAxisScale, showYAxisTitle, tension, strokeColor, dataMode, strokeWidth } = useChartDataStateContext(); + const { + axisProperty: xProperty, + valueProperty: yProperty, + aggregationMethod, + showLegend, + showTitle, + legendPosition, + showXAxisScale, + showXAxisTitle, + showYAxisScale, + showYAxisTitle, + tension, + strokeColor, + dataMode, + strokeWidth, + } = useChartDataStateContext(); - const chartTitle: string = useGeneratedTitle(); + const chartTitle: string = useGeneratedTitle(); - if (!data || !data.datasets || !data.labels) { - if (!data) - throw new Error('LineChart: No data to display. Please check the data source'); + if (!data?.datasets || !data?.labels) { + if (!data) throw new Error('LineChart: No data to display. Please check the data source'); - if (!data.datasets || !data.labels) - throw new Error('LineChart: No datasets or labels to display. Please check the data source'); - } + if (!data.datasets || !data.labels) + throw new Error('LineChart: No datasets or labels to display. Please check the data source'); + } - useEffect(() => { - if (dataMode === 'url') { - data?.datasets?.map((dataset: any) => { - dataset.borderColor = strokeColor || 'black'; - dataset.pointRadius = 5; - dataset.borderWidth = typeof (strokeWidth) === 'number' || strokeWidth > 1 ? strokeWidth : 1; - dataset.tension = tension; - return dataset; - }); - } else { - data?.datasets?.map((dataset: any) => { - dataset.tension = tension; - return dataset; - }); - } - }, [dataMode, data?.datasets, strokeColor, strokeWidth, tension]); + useEffect(() => { + if (dataMode === 'url') { + data?.datasets?.map((dataset: any) => { + dataset.borderColor = strokeColor || 'black'; + dataset.pointRadius = 5; + dataset.borderWidth = typeof strokeWidth === 'number' || strokeWidth > 1 ? strokeWidth : 1; + dataset.tension = tension; + return dataset; + }); + } else { + data?.datasets?.map((dataset: any) => { + dataset.tension = tension; + dataset.borderColor = strokeColor || 'black'; + return dataset; + }); + } + }, [dataMode, data?.datasets, strokeColor, strokeWidth, tension]); - const options: any = { - responsive: true, - plugins: { - legend: { - display: showLegend ? true : false, - position: legendPosition ?? 'top', - }, - title: { - display: showTitle && chartTitle.length > 0 ? true : false, - text: chartTitle, - } + const options: any = { + responsive: true, + maintainAspectRatio: true, // Maintain aspect ratio to prevent overflow + aspectRatio: 2, // Width to height ratio (2:1) + plugins: { + legend: { + display: !!showLegend, + position: legendPosition ?? 'top', + }, + title: { + display: !!(showTitle && chartTitle?.length > 0), + text: splitTitleIntoLines(chartTitle), + }, + }, + scales: { + x: { + title: { + display: !!(showXAxisTitle && xProperty?.trim().length > 0), + text: xProperty?.trim() ?? '', + }, + display: !!showXAxisScale, + offset: true, // Ensure the x-axis does not coincide with the y-axis + }, + y: { + title: { + display: !!(showYAxisTitle && yProperty?.trim().length > 0), + text: dataMode === 'url' || !aggregationMethod + ? yProperty?.trim() ?? '' + : `${yProperty?.trim() ?? ''} (${aggregationMethod})`, }, - scales: { - x: { - title: { - display: showXAxisTitle && xProperty?.trim().length > 0 ? true : false, - text: xProperty?.trim() ?? '' - }, - display: showXAxisScale ? true : false, - offset: true, // Ensure the x-axis does not coincide with the y-axis - }, - y: { - title: { - display: showYAxisTitle && yProperty?.trim().length > 0 ? true : false, - text: `${yProperty?.trim() ?? ''} (${aggregationMethod})`, - }, - display: showYAxisScale ? true : false, - beginAtZero: true - } - } - }; + display: !!showYAxisScale, + beginAtZero: true, + }, + }, + }; - return ; + return ; }; export default LineChart; diff --git a/shesha-reactjs/src/designer-components/charts/components/pie/index.tsx b/shesha-reactjs/src/designer-components/charts/components/pie/index.tsx index df8cd83092..44c94adf01 100644 --- a/shesha-reactjs/src/designer-components/charts/components/pie/index.tsx +++ b/shesha-reactjs/src/designer-components/charts/components/pie/index.tsx @@ -1,5 +1,6 @@ import { - ArcElement, CategoryScale, + ArcElement, + CategoryScale, Chart as ChartJS, ChartOptions, Decimation, @@ -7,17 +8,20 @@ import { Filler, Legend, LinearScale, - LineController, LineElement, + LineController, + LineElement, PieController, PointElement, RadialLinearScale, - Title, Tooltip, + Title, + Tooltip, } from 'chart.js'; import React from 'react'; import { Pie, Doughnut } from 'react-chartjs-2'; import { useChartDataStateContext } from '../../../../providers/chartData'; import { IChartData, IChartDataProps } from '../../model'; -import { useGeneratedTitle } from "../../hooks"; +import { useGeneratedTitle } from '../../hooks'; +import { splitTitleIntoLines } from '../../utils'; interface IPieChartProps extends IChartDataProps { data: IChartData; @@ -41,45 +45,46 @@ ChartJS.register( LineElement, PointElement, LinearScale, - Legend, + Legend ); const PieChart = ({ data }: IPieChartProps) => { - const { showLegend, showTitle, legendPosition, isDoughnut, strokeColor, dataMode, strokeWidth } = useChartDataStateContext(); + const { showLegend, showTitle, legendPosition, isDoughnut, strokeColor, dataMode, strokeWidth } = useChartDataStateContext(); const chartTitle: string = useGeneratedTitle(); - if (!data || !data.datasets || !data.labels) { - if (!data) - throw new Error('PieChart: No data to display. Please check the data source'); + if (!data?.datasets || !data?.labels) { + if (!data) throw new Error('PieChart: No data to display. Please check the data source'); if (!data.datasets || !data.labels) throw new Error('PieChart: No datasets or labels to display. Please check the data source'); } data.datasets.forEach((dataset: { data: any[] }) => { - dataset.data = dataset?.data?.map((item) => item === null || item === undefined ? 'undefined' : item); + dataset.data = dataset?.data?.map((item) => item ?? 'undefined'); }); if (dataMode === 'url') { data?.datasets?.map((dataset: any) => { dataset.borderColor = strokeColor || 'black'; - dataset.borderWidth = typeof (strokeWidth) === 'number' || strokeWidth > 1 ? strokeWidth : 1; + dataset.borderWidth = typeof strokeWidth === 'number' && strokeWidth > 1 ? strokeWidth : 1; dataset.strokeColor = strokeColor || 'black'; return dataset; }); - } + } const options: ChartOptions = { responsive: true, + maintainAspectRatio: true, // Maintain aspect ratio to prevent overflow + aspectRatio: 1, // Square aspect ratio for pie charts plugins: { legend: { - display: showLegend ? true : false, + display: !!showLegend, position: legendPosition ?? 'top', }, title: { - display: showTitle && chartTitle.length > 0, - text: chartTitle, + display: !!(showTitle && chartTitle?.length > 0), + text: splitTitleIntoLines(chartTitle), }, }, layout: { @@ -95,4 +100,4 @@ const PieChart = ({ data }: IPieChartProps) => { return isDoughnut ? : ; }; -export default PieChart; \ No newline at end of file +export default PieChart; diff --git a/shesha-reactjs/src/designer-components/charts/components/polarArea/index.tsx b/shesha-reactjs/src/designer-components/charts/components/polarArea/index.tsx index 9418e69ebb..64ab251f4a 100644 --- a/shesha-reactjs/src/designer-components/charts/components/polarArea/index.tsx +++ b/shesha-reactjs/src/designer-components/charts/components/polarArea/index.tsx @@ -1,50 +1,35 @@ -import { - ArcElement, - Chart as ChartJS, - ChartOptions, - Legend, - RadialLinearScale, - Title, Tooltip, -} from 'chart.js'; +import { ArcElement, Chart as ChartJS, ChartOptions, Legend, RadialLinearScale, Title, Tooltip } from 'chart.js'; import React from 'react'; import { PolarArea } from 'react-chartjs-2'; import { useChartDataStateContext } from '../../../../providers/chartData'; import { IChartData, IChartDataProps } from '../../model'; -import { useGeneratedTitle } from "../../hooks"; +import { useGeneratedTitle } from '../../hooks'; +import { splitTitleIntoLines } from '../../utils'; interface IPolarAreaChartProps extends IChartDataProps { data: IChartData; } -ChartJS.register( - Title, - Tooltip, - Legend, - ArcElement, - RadialLinearScale, -); +ChartJS.register(Title, Tooltip, Legend, ArcElement, RadialLinearScale); const PolarAreaChart = ({ data }: IPolarAreaChartProps) => { const { showTitle, legendPosition, showLegend, strokeColor, dataMode, strokeWidth } = useChartDataStateContext(); const chartTitle: string = useGeneratedTitle(); - if (!data || !data.datasets || !data.labels) { - if (!data) - throw new Error('PolarAreaChart: No data to display. Please check the data source.'); + if (!data) throw new Error('PolarAreaChart: No data to display. Please check the data source.'); - if (!data.datasets || !data.labels) - throw new Error('PolarAreaChart: No datasets or labels to display. Please check the data source.'); - } + if (!data.datasets || !data.labels) + throw new Error('PolarAreaChart: No datasets or labels to display. Please check the data source.'); data.datasets.forEach((dataset: { data: any[] }) => { - dataset.data = dataset?.data?.map((item) => item === null || item === undefined ? 'undefined' : item); + dataset.data = dataset?.data?.map((item) => item ?? 'undefined'); }); if (dataMode === 'url') { data?.datasets?.map((dataset: any) => { dataset.borderColor = strokeColor || 'black'; - dataset.borderWidth = typeof (strokeWidth) === 'number' || strokeWidth > 1 ? strokeWidth : 1; + dataset.borderWidth = typeof strokeWidth === 'number' || strokeWidth > 1 ? strokeWidth : 1; dataset.strokeColor = strokeColor || 'black'; return dataset; }); @@ -52,28 +37,30 @@ const PolarAreaChart = ({ data }: IPolarAreaChartProps) => { const options: ChartOptions = { responsive: true, + maintainAspectRatio: true, // Maintain aspect ratio to prevent overflow + aspectRatio: 1, // Square aspect ratio for polar area charts plugins: { scales: { r: { ticks: { color: 'green', font: { - size: 14 // Make ticks larger for better visibility + size: 14, // Make ticks larger for better visibility }, - backdropColor: 'rgba(255, 255, 255, 0)' // Remove the tick's backdrop + backdropColor: 'rgba(255, 255, 255, 0)', // Remove the tick's backdrop }, grid: { - color: 'rgba(0, 0, 0, 0.2)' // Make grid lines more transparent - } - } + color: 'rgba(0, 0, 0, 0.2)', // Make grid lines more transparent + }, + }, }, legend: { - display: showLegend ? true : false, + display: !!showLegend, position: legendPosition ?? 'top', }, title: { - display: showTitle && chartTitle.length > 0, - text: chartTitle, + display: !!(showTitle && chartTitle?.length > 0), + text: splitTitleIntoLines(chartTitle), }, }, layout: { diff --git a/shesha-reactjs/src/designer-components/charts/hooks.ts b/shesha-reactjs/src/designer-components/charts/hooks.ts index a21dc24d9d..eb4d43172d 100644 --- a/shesha-reactjs/src/designer-components/charts/hooks.ts +++ b/shesha-reactjs/src/designer-components/charts/hooks.ts @@ -1,7 +1,7 @@ -import { useChartDataStateContext } from "@/providers"; -import { useMemo } from "react"; -import { IChartData } from "./model"; -import { aggregateData, aggregateValues, getPredictableColor, getPropertyValue, stringifyValues } from "./utils"; +import { useChartDataStateContext } from '@/providers'; +import { useMemo } from 'react'; +import { IChartData } from './model'; +import { aggregateValues, getPredictableColor, getPropertyValue, stringifyValues } from './utils'; /** * Create title for the chart based on the chart type @@ -16,114 +16,26 @@ import { aggregateData, aggregateValues, getPredictableColor, getPropertyValue, * @returns title for the chart */ export const useGeneratedTitle = (): string => { - const { axisProperty: xProperty, valueProperty: yProperty, aggregationMethod, title, legendProperty, dataMode, entityType, simpleOrPivot } = useChartDataStateContext(); - - const entityTypeArray = dataMode === 'entityType' ? entityType?.split('.') : undefined; - const entityClassName = dataMode === 'entityType' ? entityTypeArray[entityTypeArray?.length - 1] : ''; - return dataMode === 'entityType' ? - (title?.trim().length > 0 ? title : `${entityClassName}: ${xProperty} vs ${yProperty} (${aggregationMethod})${legendProperty && simpleOrPivot === 'pivot' ? `, grouped by ${legendProperty}` : ''}`) : - (title?.trim().length > 0 ? title : ``); -}; - -/** - * Prepare line chart data - * @param data raw data - * @param xProperty x-axis property - * @param yProperty y-axis property - * @param aggregationMethod aggregation method (sum, average, count, min, max) - * @returns prepared line chart data - */ -export const useLineChartData = (): IChartData => { - const { - filteredData: data, xProperty, yProperty, aggregationMethod, strokeColor, strokeWidth - } = useChartDataStateContext(); - const memoData = useMemo(() => stringifyValues(data), [data]); - const aggregatedData = aggregateData(memoData, xProperty, yProperty, aggregationMethod); - - return { - labels: aggregatedData?.map(item => item.x), - datasets: [ - { - label: `${yProperty} (${aggregationMethod}) Over ${xProperty}`, - data: aggregatedData?.map(item => item.y), - borderColor: strokeColor || 'fff', - backgroundColor: getPredictableColor(yProperty), - fill: false, - pointRadius: 5, - borderWidth: typeof (strokeWidth) === 'number' ? strokeWidth : 0, - } - ] - }; -}; - -/** - * Prepare bar chart data - * @param data raw data - * @param xProperty x-axis property - * @param yProperty y-axis property - * @param aggregationMethod aggregation method (sum, average, count, min, max) - * @returns prepared bar chart data - */ -export const useBarChartData = (): IChartData => { - const { - filteredData: data, xProperty, yProperty, aggregationMethod, strokeColor, strokeWidth - } = useChartDataStateContext(); - const memoData = useMemo(() => stringifyValues(data), [data]); - const aggregatedData = aggregateData(memoData, xProperty, yProperty, aggregationMethod); - - return { - labels: aggregatedData?.map(item => item.x), - datasets: [ - { - label: `${yProperty} (${aggregationMethod}) Over ${xProperty}`, - data: aggregatedData?.map(item => item.y), - backgroundColor: aggregatedData?.map(item => getPredictableColor(item.x.toString())), - borderColor: strokeColor || 'fff', - borderWidth: typeof (strokeWidth) === 'number' ? strokeWidth : 0 - } - ] - }; -}; - -/** - * prepare polar area chart data - * @param data raw data - * @param legendProperty legend property - * @param valueProperty value property - * @param strokeColor stroke color - * @param aggregationMethod aggregation method (sum, average, count, min, max) - * @returns prepared polar area chart data - */ -export const usePieOrPolarAreaChartData = (): IChartData => { const { - filteredData: data, legendProperty, valueProperty, aggregationMethod, strokeColor, strokeWidth + axisProperty: xProperty, + valueProperty: yProperty, + aggregationMethod, + title, + legendProperty, + dataMode, + entityType, + simpleOrPivot, } = useChartDataStateContext(); - const memoData = useMemo(() => stringifyValues(data), [data]); - const labels = [...new Set(memoData?.map((item: { [key: string]: any }) => getPropertyValue(item, legendProperty)))]; - - const datasets = [{ - label: `${valueProperty} (${aggregationMethod})`, - data: labels?.map((label: string) => { - const filteredData = memoData?.filter((item: { [key: string]: any }) => getPropertyValue(item, legendProperty) === label); - const values: number[] = filteredData?.map((item: { [key: string]: any }) => getPropertyValue(item, valueProperty) as number); - - // Aggregation logic - if (aggregationMethod === 'sum') return values.reduce((acc, val) => acc + (val || 0), 0); - if (aggregationMethod === 'count') return values.length; - if (aggregationMethod === 'average') return values.reduce((acc, val) => acc + (val || 0), 0) / values.length; - if (aggregationMethod === 'min') return Math.min(...values); - if (aggregationMethod === 'max') return Math.max(...values); - return 0; - }), - backgroundColor: labels?.map((label: string) => getPredictableColor(label)), - borderColor: strokeColor || 'fff', - borderWidth: typeof (strokeWidth) === 'number' ? strokeWidth : 0, - }]; - return { - labels, - datasets - }; + const entityTypeArray = dataMode === 'entityType' ? entityType?.split('.') : undefined; + const entityClassName = dataMode === 'entityType' ? entityTypeArray[entityTypeArray?.length - 1] : ''; + return dataMode === 'entityType' + ? title?.trim().length > 0 + ? title + : `${entityClassName}: ${xProperty} vs ${yProperty} (${aggregationMethod})${legendProperty && simpleOrPivot === 'pivot' ? `, grouped by ${legendProperty}` : ''}` + : title?.trim().length > 0 + ? title + : ``; }; /** @@ -137,45 +49,84 @@ export const usePieOrPolarAreaChartData = (): IChartData => { * @param refLists reference lists for the legend property * @returns prepared pivot chart data */ -export const usePivotChartData = (): IChartData => { +export const useProcessedChartData = (): IChartData => { const { - filteredData: data, axisProperty, legendProperty, valueProperty, strokeColor, aggregationMethod, chartType, strokeWidth, tension + filteredData: data, + axisProperty, + legendProperty, + valueProperty, + strokeColor, + aggregationMethod, + chartType, + strokeWidth, + tension, + simpleOrPivot, } = useChartDataStateContext(); - const memoData = useMemo(() => stringifyValues(data), [data]); - var labels = [...new Set(memoData?.map((item: { [key: string]: any }) => getPropertyValue(item, axisProperty)))]; - const legendItems = [...new Set(memoData?.map((item: { [key: string]: any }) => getPropertyValue(item, legendProperty)))]; + const memoData = useMemo(() => stringifyValues(data ?? []), [data]); + let labels = memoData.length + ? [...new Set(memoData.map((item: { [key: string]: any }) => getPropertyValue(item, axisProperty)))] + : []; + let datasets; - var datasets = legendItems?.map(legend => { - const strLegend = typeof legend === 'string' ? legend : legend + ''; - const barBackgroundColor = getPredictableColor(strLegend); - let colors: string[] = []; - const legendDisplayValue = legend; - return { - label: legendDisplayValue, - data: labels?.map(label => { - const matchingItems = memoData.filter((item: { [key: string]: any }) => { - return getPropertyValue(item, axisProperty) === label && getPropertyValue(item, legendProperty) === legend; - }); - switch (chartType) { - case 'bar': - case 'line': - colors.push(barBackgroundColor); - break; - default: - const strLabel = typeof label === 'string' ? label : legend + ''; - colors.push(getPredictableColor(strLabel)); - break; - } - return matchingItems.length > 0 ? aggregateValues(matchingItems, aggregationMethod, valueProperty) : 0; - }), - fill: false, - borderColor: strokeColor || 'fff', - backgroundColor: colors, - pointRadius: 5, - borderWidth: typeof (strokeWidth) === 'number' ? strokeWidth : 0, - tension: typeof (tension) === 'number' ? tension : 0.0 - }; - }); + if (simpleOrPivot === 'simple' || !legendProperty) { + // Generate different colors for each data point based on the label + const colors = labels.map((label) => getPredictableColor(typeof label === 'string' ? label : label + '')); + + datasets = [ + { + label: `${valueProperty} (${aggregationMethod})`, + data: labels?.map((label) => { + const matchingItems = memoData.filter((item: { [key: string]: any }) => { + return getPropertyValue(item, axisProperty) === label; + }); + return matchingItems.length > 0 ? aggregateValues(matchingItems, aggregationMethod, valueProperty) : 0; + }), + fill: false, + borderColor: strokeColor || '#fff', + backgroundColor: colors, + pointRadius: 5, + borderWidth: typeof strokeWidth === 'number' ? strokeWidth : 0, + tension: typeof tension === 'number' ? tension : 0.0, + }, + ]; + } else { + // Pivot mode - multiple datasets based on legend property + const legendItems = [ + ...new Set(memoData?.map((item: { [key: string]: any }) => getPropertyValue(item, legendProperty))), + ]; + + datasets = legendItems?.map((legend) => { + const strLegend = typeof legend === 'string' ? legend : legend + ''; + const barBackgroundColor = getPredictableColor(strLegend); + let colors: string[] = []; + const legendDisplayValue = legend; + return { + label: legendDisplayValue, + data: labels?.map((label) => { + const matchingItems = memoData.filter((item: { [key: string]: any }) => { + return getPropertyValue(item, axisProperty) === label && getPropertyValue(item, legendProperty) === legend; + }); + switch (chartType) { + case 'bar': + case 'line': + colors.push(barBackgroundColor); + break; + default: + const strLabel = typeof label === 'string' ? label : label + ''; + colors.push(getPredictableColor(strLabel)); + break; + } + return matchingItems.length > 0 ? aggregateValues(matchingItems, aggregationMethod, valueProperty) : 0; + }), + fill: false, + borderColor: strokeColor || '#fff', + backgroundColor: colors, + pointRadius: 5, + borderWidth: typeof strokeWidth === 'number' ? strokeWidth : 0, + tension: typeof tension === 'number' ? tension : 0.0, + }; + }); + } // Ensure dataset labels and data labels are not null or undefined datasets = datasets?.map((dataset) => ({ @@ -196,19 +147,19 @@ export const usePivotChartData = (): IChartData => { * @returns prepared chart data from URL */ export const useChartURLData = () => { - const { - urlTypeData, strokeColor, strokeWidth, tension - } = useChartDataStateContext(); + const { urlTypeData, strokeColor, strokeWidth, tension } = useChartDataStateContext(); + + const memoData = useMemo(() => urlTypeData, [urlTypeData]); return { - labels: urlTypeData?.labels, + labels: memoData?.labels, datasets: urlTypeData?.datasets?.map((dataset: any) => { dataset.borderColor = strokeColor || 'black'; - dataset.borderWidth = typeof (strokeWidth) === 'number' || strokeWidth > 1 ? strokeWidth : 1; + dataset.borderWidth = typeof strokeWidth === 'number' && strokeWidth > 1 ? strokeWidth : 1; dataset.strokeColor = strokeColor || 'black'; - dataset.tension = typeof (tension) === 'number' ? tension : 0.0; + dataset.tension = typeof tension === 'number' ? tension : 0.0; return dataset; - }) - };; -}; \ No newline at end of file + }), + }; +}; diff --git a/shesha-reactjs/src/designer-components/charts/index.tsx b/shesha-reactjs/src/designer-components/charts/index.tsx deleted file mode 100644 index 218c516b33..0000000000 --- a/shesha-reactjs/src/designer-components/charts/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ConfigurableFormItem } from '@/components'; -import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; -import { IToolboxComponent } from '@/interfaces'; -import { BarChartOutlined } from '@ant-design/icons'; -import React from 'react'; -import ChartDataProvider from '../../providers/chartData'; -import ChartControl from './chartControl'; -import ChartControlURL from './chartControlURL'; -import { IChartProps } from './model'; -import { settingsForm } from './settings'; - -const ChartComponent: IToolboxComponent = { - type: 'chart', - name: 'Chart', - isInput: false, - isOutput: true, - icon: , - Factory: ({ model }) => { - return ( - - {() => { - return ( - - {model.dataMode === 'url' ? : } - - ); - }} - - ); - }, - initModel: (model) => ({ - chartType: 'line', showTitle: false, showLegend: true, legendPosition: 'top', ...model - }), - settingsFormMarkup: settingsForm, - validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), - migrator: (m) => m - .add(0, prev => ({ ...prev, hideLabel: true })) - .add(1, prev => ({...prev, showBorder: true})) - .add(2, prev => ({...prev, isDoughnut: false})) -}; - -export default ChartComponent; diff --git a/shesha-reactjs/src/designer-components/charts/line.tsx b/shesha-reactjs/src/designer-components/charts/line.tsx new file mode 100644 index 0000000000..f756d23990 --- /dev/null +++ b/shesha-reactjs/src/designer-components/charts/line.tsx @@ -0,0 +1,57 @@ +import { ConfigurableFormItem } from '@/components'; +import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; +import { IToolboxComponent } from '@/interfaces'; +import { LineChartOutlined } from '@ant-design/icons'; +import React from 'react'; +import ChartDataProvider from '../../providers/chartData'; +import ChartControl from './chartControl'; +import ChartControlURL from './chartControlURL'; +import { IChartProps } from './model'; +import { getSettings } from './settingsFormIndividual'; +import { defaultConfigFiller, filterNonNull } from './utils'; + +const LineChartComponent: IToolboxComponent = { + type: 'lineChart', + name: 'Line Chart', + isInput: false, + isOutput: true, + icon: , + Factory: ({ model }) => { + if (model.hidden) return null; + + return ( + + {() => { + return ( + + {model.dataMode === 'url' ? : } + + ); + }} + + ); + }, + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + migrator: (m) => m + .add(0, prev => ({ + chartType: 'line', + showTitle: false, + showLegend: true, + legendPosition: 'top', + hidden: false, + ...prev, + })) + .add(1, prev => ({ ...prev, hideLabel: true })) + .add(2, prev => ({ ...prev, showBorder: true })) + .add(3, prev => ({ ...prev, isDoughnut: false })) + .add(4, prev => ({ ...prev, showTitle: true, showLegend: false })) + .add(5, prev => ({ + ...defaultConfigFiller, + ...filterNonNull(prev), + type: prev.type, + id: prev.id + })) +}; + +export default LineChartComponent; diff --git a/shesha-reactjs/src/designer-components/charts/model.ts b/shesha-reactjs/src/designer-components/charts/model.ts index d6bee468db..6fb5e494db 100644 --- a/shesha-reactjs/src/designer-components/charts/model.ts +++ b/shesha-reactjs/src/designer-components/charts/model.ts @@ -27,7 +27,6 @@ export interface IChartsProps { showLegend?: boolean; legendPosition?: TLegendPosition; entityType?: string; - filters?: string[]; valueProperty?: string; axisProperty?: string; isAxisTimeSeries?: boolean; @@ -55,7 +54,9 @@ export interface IChartsProps { /** * Chart props, used in the Shesha tool box */ -export interface IChartProps extends IConfigurableFormComponent, IChartsProps { } +export interface IChartProps extends IConfigurableFormComponent, IChartsProps { + hidden?: boolean; + } export interface IChartDataProps extends IChartsProps { labels?: string[]; @@ -92,10 +93,4 @@ export type TOperator = 'starts_with' | 'ends_with' | 'is' | - 'is_not'; - -export interface IFilter { - property: string; - operator: TOperator; - value: string | number; -} \ No newline at end of file + 'is_not'; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/charts/pie.tsx b/shesha-reactjs/src/designer-components/charts/pie.tsx new file mode 100644 index 0000000000..5aff43d727 --- /dev/null +++ b/shesha-reactjs/src/designer-components/charts/pie.tsx @@ -0,0 +1,57 @@ +import { ConfigurableFormItem } from '@/components'; +import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; +import { IToolboxComponent } from '@/interfaces'; +import { PieChartOutlined } from '@ant-design/icons'; +import React from 'react'; +import ChartDataProvider from '../../providers/chartData'; +import ChartControl from './chartControl'; +import ChartControlURL from './chartControlURL'; +import { IChartProps } from './model'; +import { getSettings } from './settingsFormIndividual'; +import { defaultConfigFiller, filterNonNull } from './utils'; + +const PieChartComponent: IToolboxComponent = { + type: 'pieChart', + name: 'Pie Chart', + isInput: false, + isOutput: true, + icon: , + Factory: ({ model }) => { + if (model.hidden) return null; + + return ( + + {() => { + return ( + + {model.dataMode === 'url' ? : } + + ); + }} + + ); + }, + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + migrator: (m) => m + .add(0, prev => ({ + chartType: 'pie', + showTitle: false, + showLegend: true, + legendPosition: 'top', + hidden: false, + ...prev, + })) + .add(1, prev => ({ ...prev, hideLabel: true })) + .add(2, prev => ({ ...prev, showBorder: true })) + .add(3, prev => ({ ...prev, isDoughnut: false })) + .add(4, prev => ({ ...prev, showTitle: true })) + .add(5, prev => ({ + ...defaultConfigFiller, + ...filterNonNull(prev), + type: prev.type, + id: prev.id + })) +}; + +export default PieChartComponent; diff --git a/shesha-reactjs/src/designer-components/charts/polarArea.tsx b/shesha-reactjs/src/designer-components/charts/polarArea.tsx new file mode 100644 index 0000000000..bf1cab8dca --- /dev/null +++ b/shesha-reactjs/src/designer-components/charts/polarArea.tsx @@ -0,0 +1,57 @@ +import { ConfigurableFormItem } from '@/components'; +import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; +import { IToolboxComponent } from '@/interfaces'; +import { RadarChartOutlined } from '@ant-design/icons'; +import React from 'react'; +import ChartDataProvider from '../../providers/chartData'; +import ChartControl from './chartControl'; +import ChartControlURL from './chartControlURL'; +import { IChartProps } from './model'; +import { getSettings } from './settingsFormIndividual'; +import { defaultConfigFiller, filterNonNull } from './utils'; + +const PolarAreaChartComponent: IToolboxComponent = { + type: 'polarAreaChart', + name: 'Polar Area Chart', + isInput: false, + isOutput: true, + icon: , + Factory: ({ model }) => { + if (model.hidden) return null; + + return ( + + {() => { + return ( + + {model.dataMode === 'url' ? : } + + ); + }} + + ); + }, + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + migrator: (m) => m + .add(0, prev => ({ + chartType: 'polarArea', + showTitle: false, + showLegend: true, + legendPosition: 'top', + hidden: false, + ...prev, + })) + .add(1, prev => ({ ...prev, hideLabel: true })) + .add(2, prev => ({ ...prev, showBorder: true })) + .add(3, prev => ({ ...prev, isDoughnut: false })) + .add(4, prev => ({ ...prev, showTitle: true })) + .add(5, prev => ({ + ...defaultConfigFiller, + ...filterNonNull(prev), + type: prev.type, + id: prev.id + })) +}; + +export default PolarAreaChartComponent; diff --git a/shesha-reactjs/src/designer-components/charts/settings.ts b/shesha-reactjs/src/designer-components/charts/settings.ts deleted file mode 100644 index a2982165ab..0000000000 --- a/shesha-reactjs/src/designer-components/charts/settings.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { nanoid } from "@/utils/uuid"; -import { DesignerToolbarSettings } from "../../interfaces"; - -const chartGeneralId = nanoid(); -const chartSettingsId = nanoid(); -const dataSettingsId = nanoid(); -const dataSettingsForUrlId = nanoid(); - -export const settingsForm = new DesignerToolbarSettings() - .addCollapsiblePanel({ - id: chartGeneralId, - propertyName: 'chartGeneral', - parentId: 'root', - label: 'General', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: nanoid(), - components: - [...new DesignerToolbarSettings() - .addTextField({ - id: nanoid(), - propertyName: 'propertyName', - label: 'Property Name', - parentId: 'root', - hidden: false, - validate: { required: false }, - }) - .addTextField({ - id: nanoid(), - propertyName: 'label', - label: 'Label', - parentId: 'root', - hidden: false, - validate: { required: true }, - version: 3 - }) - .addCheckbox({ - id: nanoid(), - label: 'Hide Label', - propertyName: 'hideLabel', - parentId: 'root', - hidden: false, - defaultValue: true, - version: 2, - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'hidden', - label: 'Hidden', - parentId: 'root', - }) - .addDropdown({ - id: nanoid(), - propertyName: 'dataMode', - parentId: 'root', - label: 'Data Source Type', - description: 'The type of data source you want to use for the chart. If you select `URL`, you will have to provide a URL endpoint to the data. If you select `Entity Type`, you will have to select an entity type from the list.', - dataSourceType: 'values', - values: [ - { id: nanoid(), label: 'URL', value: 'url' }, - { id: nanoid(), label: 'Entity Type', value: 'entityType' } - ], - validate: { required: true }, - defaultValue: 'entityType', - }) - .addNumberField({ - id: nanoid(), - propertyName: 'height', - parentId: 'root', - label: 'Height', - description: 'The height (px) of the chart. If not provided, the default height will be used. Minimum height is 201px. For responsiveness, setting the height will automatically set the width to proportionate value.', - stepNumeric: 1, - min: 200, - hidden: { - _code: "return getSettingValue(data?.width) > 300", - _mode: "code", - _value: true - } - }) - .addNumberField({ - id: nanoid(), - propertyName: 'width', - parentId: 'root', - label: 'Width', - description: 'The width (px) of the chart. If not provided, the default width will be used. Minimum width is 301px. For responsiveness, setting the width will automatically set the height to proportionate value.', - stepNumeric: 1, - min: 300, - hidden: { - _code: "return getSettingValue(data?.height) > 200", - _mode: "code", - _value: true - }, - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'showBorder', - label: 'Show Border', - parentId: 'root', - defaultValue: true, - }) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: chartSettingsId, - propertyName: 'chartSettings', - parentId: 'root', - label: 'Chart Settings', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: nanoid(), - components: - [...new DesignerToolbarSettings() - .addDropdown({ - id: nanoid(), - propertyName: 'chartType', - parentId: 'root', - hidden: false, - label: 'Chart Type', - dataSourceType: 'values', - values: [ - { id: nanoid(), label: 'Pie Chart', value: 'pie' }, - { id: nanoid(), label: 'Line Chart', value: 'line' }, - { id: nanoid(), label: 'Bar Chart', value: 'bar' }, - { id: nanoid(), label: 'Polar Area Chart', value: 'polarArea' }, - ], - validate: { required: true }, - defaultValue: 'line', - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'isDoughnut', - label: 'Is Doughnut', - parentId: 'root', - hidden: { - _code: "return getSettingValue(data?.chartType) !== `pie`", - _mode: "code", - _value: true - }, - defaultValue: false, - }) - .addDropdown({ - id: nanoid(), - propertyName: 'simpleOrPivot', - parentId: 'root', - hidden: false, - label: 'Simple / Pivot', - dataSourceType: 'values', - values: [ - { id: nanoid(), label: 'Simple', value: 'simple' }, - { id: nanoid(), label: 'Pivot', value: 'pivot' } - ], - validate: { required: true }, - defaultValue: 'simple', - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'stacked', - label: 'Stacked', - parentId: 'root', - hidden: { - _code: "return !(getSettingValue(data?.chartType) === `bar` && getSettingValue(data?.simpleOrPivot) === `pivot`)", - _mode: "code", - _value: true - }, - defaultValue: true, - }) - .addTextField({ - id: nanoid(), - propertyName: 'title', - parentId: 'root', - hidden: { - _code: "return getSettingValue(data?.showTitle) !== true", - _mode: "code", - _value: false - }, - label: 'Title', - description: 'The title of the chart (if any)', - labelAlign: 'right', - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'showTitle', - label: 'Show Title', - description: 'Show the title of the chart', - parentId: 'root', - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'showLegend', - label: 'Show Legend', - description: 'Show the legend of the chart. Legend is the area that shows the color and what it represents.', - parentId: 'root', - defaultValue: true, - }) - .addDropdown({ - id: nanoid(), - propertyName: 'legendPosition', - parentId: 'root', - hidden: { - _code: "return getSettingValue(data?.showLegend) !== true", - _mode: "code", - _value: true - }, - label: 'Legend Position', - dataSourceType: 'values', - values: [ - { id: nanoid(), label: 'Top', value: 'top' }, - { id: nanoid(), label: 'Bottom', value: 'bottom' }, - { id: nanoid(), label: 'Left', value: 'left' }, - { id: nanoid(), label: 'Right', value: 'right' }, - ], - validate: { required: true }, - defaultValue: 'top', - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'showXAxisScale', - label: 'Show X Axis', - parentId: 'root', - defaultValue: true, - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'showXAxisTitle', - label: 'Show X Axis Title', - parentId: 'root', - defaultValue: true, - hidden: { - _code: "return getSettingValue(data?.showXAxisScale) !== true", - _mode: "code", - _value: true - }, - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'showYAxisScale', - label: 'Show Y Axis', - parentId: 'root', - defaultValue: true, - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'showYAxisTitle', - label: 'Show Y Axis Title', - parentId: 'root', - defaultValue: true, - hidden: { - _code: "return getSettingValue(data?.showYAxisScale) !== true", - _mode: "code", - _value: true - } - }) - .addNumberField({ - id: nanoid(), - propertyName: 'tension', - parentId: chartSettingsId, - label: 'Tension', - defaultValue: 0, - stepNumeric: 0.1, - min: 0, - hidden: { - _code: "return getSettingValue(data?.chartType) !== `line`", - _mode: "code", - _value: true - }, - }) - .addNumberField({ - id: nanoid(), - propertyName: 'strokeWidth', - parentId: chartSettingsId, - label: 'Stroke width', - defaultValue: 0.0, - description: 'The width of the stroke for the elements (bars, lines, etc.) in the c in the chart. Default is 0.0', - stepNumeric: 0.1, - min: 0, - max: 10, - }) - .addColorPicker({ - id: nanoid(), - propertyName: 'strokeColor', - parentId: 'root', - label: 'Stroke Color', - allowClear: true, - }) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: dataSettingsForUrlId, - propertyName: 'dataSettingsForUrl', - parentId: 'root', - label: 'Data Settings (URL)', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - hidden: { - _code: "return getSettingValue(data?.dataMode) !== `url`", - _mode: "code", - _value: true - }, - content: { - id: nanoid(), - components: - [...new DesignerToolbarSettings() - .addTextField({ - id: nanoid(), - propertyName: 'url', - label: 'URL', - description: 'The URL you want to use for the chart', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: { required: true }, - }) - .addTextField({ - id: nanoid(), - propertyName: 'axisProperty', - label: 'Axis label', - labelAlign: 'right', - parentId: 'root', - isDynamic: false, - description: 'Label for the axis property', - validate: { required: false }, - hidden: { - _code: "return getSettingValue(data?.chartType) === `pie` || getSettingValue(data?.chartType) === `polarArea`", - _mode: "code", - _value: true - }, - }) - .addTextField({ - id: nanoid(), - propertyName: 'valueProperty', - label: 'Value axis label', - labelAlign: 'right', - parentId: 'root', - isDynamic: false, - description: 'Label for the value property', - validate: { required: false }, - hidden: { - _code: "return getSettingValue(data?.chartType) === `pie` || getSettingValue(data?.chartType) === `polarArea`", - _mode: "code", - _value: true - }, - }) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: dataSettingsId, - propertyName: 'dataSettings', - parentId: 'root', - label: 'Data Settings', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - hidden: { - _code: "return getSettingValue(data?.dataMode) === `url`", - _mode: "code", - _value: false - }, - content: { - id: nanoid(), - components: - [...new DesignerToolbarSettings() - .addAutocomplete({ - id: nanoid(), - propertyName: 'entityType', - label: 'Entity Type', - description: 'The entity type you want to use for the chart.', - labelAlign: 'right', - parentId: 'root', - hidden: false, - dataSourceType: 'url', - validate: { required: true }, - dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', - settingsValidationErrors: [], - useRawValues: true, - queryParams: null, - }) - .addPropertyAutocomplete({ - id: nanoid(), - propertyName: 'axisProperty', - label: 'Axis Property', - labelAlign: 'right', - parentId: 'root', - isDynamic: false, - description: 'The property to be used on the x-axis.', - validate: { required: true }, - modelType: '{{data.entityType}}', - autoFillProps: false, - settingsValidationErrors: [], - hidden: { - _code: "return getSettingValue(data?.dataMode) === `url`", - _mode: "code", - _value: true - }, - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'isAxisTimeSeries', - label: 'Is Axis Property Time Series?', - description: 'If the x-axis is a time series, check this box.', - parentId: 'root', - defaultValue: false, - validate: { required: true }, - - }) - .addDropdown({ - id: nanoid(), - propertyName: 'timeSeriesFormat', - parentId: 'root', - label: 'Time Series Format', - dataSourceType: 'values', - values: [ - { id: nanoid(), label: 'Day', value: 'day' }, - { id: nanoid(), label: 'Month', value: 'month' }, - { id: nanoid(), label: 'Year', value: 'year' }, - { id: nanoid(), label: 'Day-Month', value: 'day-month' }, - { id: nanoid(), label: 'Day-Month-Year', value: 'day-month-year' }, - { id: nanoid(), label: 'Month-Year', value: 'month-year' }, - ], - validate: { required: true }, - defaultValue: 'day-month-year', - hidden: { - _code: "return getSettingValue(data?.isAxisTimeSeries) !== true", - _mode: "code", - _value: true - }, - }) - .addPropertyAutocomplete({ - id: nanoid(), - propertyName: 'valueProperty', - label: 'Value Property', - labelAlign: 'right', - parentId: 'root', - isDynamic: false, - description: 'This is the property that will be used to calculate the data and hence show on the depenedent y-axis.', - validate: { required: true }, - modelType: '{{data.entityType}}', - autoFillProps: false, - settingsValidationErrors: [], - hidden: { - _code: "return getSettingValue(data?.dataMode) === `url`", - _mode: "code", - _value: true - }, - }) - .addPropertyAutocomplete({ - id: nanoid(), - propertyName: 'legendProperty', - label: 'Legend Property', - labelAlign: 'right', - parentId: 'root', - hidden: { - _code: "return getSettingValue(data?.simpleOrPivot) === `simple`", - _mode: "code", - _value: false - }, - isDynamic: false, - description: 'The properties you want to use on the Legend. This is the property that will be used to group the data for Pivot Charts.', - validate: { required: true }, - modelType: '{{data.entityType}}', - autoFillProps: false, - settingsValidationErrors: [], - }) - .addPropertyAutocomplete({ - id: nanoid(), - propertyName: 'orderBy', - label: 'Order By', - labelAlign: 'right', - parentId: 'root', - hidden: false, - isDynamic: false, - description: 'The properties you want to order the data by. Use the propeties that you have selected for axis, value (and legend).', - modelType: '{{data.entityType}}', - autoFillProps: false, - settingsValidationErrors: [], - }) - .addDropdown({ - id: nanoid(), - propertyName: 'orderDirection', - parentId: 'root', - label: 'Order Direction', - dataSourceType: 'values', - values: [ - { id: nanoid(), label: 'Ascending', value: 'asc' }, - { id: nanoid(), label: 'Descending', value: 'desc' }, - ], - validate: { required: true }, - defaultValue: 'asc', - hidden: { - _code: "return !(getSettingValue(data?.orderBy))", - _mode: "code", - _value: true - }, - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'allowFilter', - label: 'Allow Chart Filter', - parentId: 'root', - description: 'Allow users to filter the chart data directly from the chart.', - defaultValue: false, - }) - .addPropertyAutocomplete({ - id: nanoid(), - propertyName: 'filterProperties', - label: 'Filter Property list', - labelAlign: 'right', - mode: "multiple", - parentId: 'root', - isDynamic: true, - description: 'The properties you want users to filter by. Use the propeties that you have selected for axis, value (and legend).', - modelType: '{{data.entityType}}', - autoFillProps: false, - settingsValidationErrors: [], - hidden: { - _code: "return !(getSettingValue(data?.allowFilter))", - _mode: "code", - _value: true - }, - }) - .addDropdown({ - id: nanoid(), - propertyName: 'aggregationMethod', - parentId: 'root', - label: 'Aggregation Method', - dataSourceType: 'values', - values: [ - { id: nanoid(), label: 'Sum', value: 'sum' }, - { id: nanoid(), label: 'Count', value: 'count' }, - { id: nanoid(), label: 'Average', value: 'average' }, - { id: nanoid(), label: 'Min', value: 'min' }, - { id: nanoid(), label: 'Max', value: 'max' }, - ], - validate: { required: true }, - defaultValue: 'count', - }) - .addQueryBuilder({ - id: 'n4enebtmhFgvkP5ukQK1f', - propertyName: 'filters', - label: 'Entity filter', - labelAlign: 'right', - parentId: 'root', - hidden: false, - isDynamic: false, - validate: {}, - settingsValidationErrors: [], - modelType: '{{data.entityType}}', - fieldsUnavailableHint: 'Please select `Entity Type` to be able to configure this filter.', - }) - .toJson() - ] - } - }) - .toJson(); diff --git a/shesha-reactjs/src/designer-components/charts/settingsFormIndividual.ts b/shesha-reactjs/src/designer-components/charts/settingsFormIndividual.ts new file mode 100644 index 0000000000..3dfefc5183 --- /dev/null +++ b/shesha-reactjs/src/designer-components/charts/settingsFormIndividual.ts @@ -0,0 +1,1068 @@ +import { DesignerToolbarSettings } from '@/index'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/lib/form/Form'; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const dataTabId = nanoid(); + const dataSettingsId = nanoid(); + const dataSettingsForUrlId = nanoid(); + const securityTabId = nanoid(); + + const propertyNameId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: propertyNameId, + propertyName: 'propertyName', + parentId: commonTabId, + label: 'Property Name', + size: 'small', + validate: { + required: true, + }, + styledLabel: true, + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'hidden', + parentId: 'root', + label: 'Hide', + inputType: 'switch', + }) + .addCollapsiblePanel({ + id: nanoid(), + label: 'Quick Settings', + labelAlign: 'right', + description: 'Quick settings to get started with the chart', + parentId: commonTabId, + ghost: true, + collapsible: 'header', + collapsedByDefault: false, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'dataMode', + parentId: commonTabId, + label: 'Data Source Type', + description: + 'The type of data source you want to use for the chart. If you select `URL`, you will have to provide a URL endpoint to the data. If you select `Entity Type`, you will have to select an entity type from the list.', + type: 'dropdown', + allowClear: true, + dropdownOptions: [ + { label: 'URL', value: 'url' }, + { label: 'Entity Type', value: 'entityType' }, + ], + validate: { required: true }, + defaultValue: 'entityType', + }, + { + id: nanoid(), + propertyName: 'simpleOrPivot', + parentId: commonTabId, + label: 'Simple / Pivot', + type: 'dropdown', + allowClear: true, + dropdownOptions: [ + { label: 'Simple', value: 'simple' }, + { label: 'Pivot', value: 'pivot' }, + ], + validate: { required: true }, + defaultValue: 'simple', + hidden: { + _code: 'return getSettingValue(data?.dataMode) === `url`', + _mode: 'code', + _value: false, + } as any, + }, + ] + }) + .addContainer({ + id: dataSettingsForUrlId, + propertyName: 'dataSettingsForUrl', + parentId: commonTabId, + label: 'Data Settings (URL)', + labelAlign: 'left', + hidden: { + _code: 'return getSettingValue(data?.dataMode) !== `url`', + _mode: 'code', + _value: false, + } as any, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'url', + inputType: 'textField', + label: 'URL', + description: 'The URL you want to use for the chart', + labelAlign: 'right', + parentId: dataSettingsForUrlId, + hidden: false, + validate: { required: true }, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'axisProperty', + label: 'Axis label', + inputType: 'textField', + labelAlign: 'right', + parentId: dataSettingsForUrlId, + isDynamic: false, + description: 'Label for the axis property', + validate: { required: false }, + width: '100%', + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'valueProperty', + label: 'Value axis label', + inputType: 'textField', + labelAlign: 'right', + parentId: dataSettingsForUrlId, + isDynamic: false, + description: 'Label for the value property', + validate: { required: false }, + width: '100%', + }) + .toJson(), + ], + }) + .addContainer({ + id: dataSettingsId, + propertyName: 'dataSettings', + parentId: dataTabId, + label: 'Data Settings', + labelAlign: 'left', + hidden: { + _code: 'return getSettingValue(data?.dataMode) === `url`', + _mode: 'code', + _value: false, + } as any, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dataSettingsId, + inline: true, + inputs: [ + { + id: nanoid(), + type: 'autocomplete', + propertyName: 'entityType', + label: 'Entity Type', + description: 'The entity type you want to use.', + labelAlign: 'right', + parentId: dataSettingsId, + hidden: false, + dataSourceType: 'url', + validate: {}, + dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', + settingsValidationErrors: [], + jsSetting: true, + useRawValues: true, + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataSettingsId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.dataMode) === `url`', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'axisProperty', + label: 'Axis Property', + labelAlign: 'right', + parentId: dataSettingsId, + type: 'propertyAutocomplete', + isDynamic: false, + description: 'The property to be used on the x-axis.', + validate: { required: true }, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + autoFillProps: false, + settingsValidationErrors: [], + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataSettingsId, + inline: true, + hidden: { + _code: 'return !getSettingValue(data?.axisProperty)', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'isAxisTimeSeries', + label: 'Is Axis Property Time Series?', + description: 'If the x-axis is a time series, check this box.', + parentId: dataSettingsId, + defaultValue: false, + validate: { required: true }, + width: '100%', + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataSettingsId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.isAxisTimeSeries) !== true', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'timeSeriesFormat', + parentId: dataSettingsId, + label: 'Time Series Format', + type: 'dropdown', + allowClear: true, + dropdownOptions: [ + { label: 'Day', value: 'day' }, + { label: 'Month', value: 'month' }, + { label: 'Year', value: 'year' }, + { label: 'Day-Month', value: 'day-month' }, + { label: 'Day-Month-Year', value: 'day-month-year' }, + { label: 'Month-Year', value: 'month-year' }, + ], + validate: { required: true }, + defaultValue: 'day-month-year', + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataSettingsId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.dataMode) === `url`', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'valueProperty', + label: 'Value Property', + labelAlign: 'right', + parentId: dataSettingsId, + type: 'propertyAutocomplete', + isDynamic: false, + description: 'The property to be used on the x-axis.', + validate: { required: true }, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + autoFillProps: false, + settingsValidationErrors: [], + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataSettingsId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.simpleOrPivot) === `simple`', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'legendProperty', + label: 'Legend Property', + labelAlign: 'right', + parentId: dataSettingsId, + type: 'propertyAutocomplete', + isDynamic: false, + description: + 'The properties you want to use on the Legend. This is the property that will be used to group the data for Pivot Charts.', + validate: { required: true }, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + autoFillProps: false, + settingsValidationErrors: [], + width: '100%', + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'aggregationMethod', + parentId: dataSettingsId, + label: 'Aggregation Method', + inputType: 'dropdown', + allowClear: true, + dropdownOptions: [ + { label: 'Sum', value: 'sum' }, + { label: 'Count', value: 'count' }, + { label: 'Average', value: 'average' }, + { label: 'Min', value: 'min' }, + { label: 'Max', value: 'max' }, + ], + validate: { required: true }, + defaultValue: 'count', + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + label: 'Labels', + labelAlign: 'right', + description: 'Labels for the chart', + parentId: commonTabId, + ghost: true, + collapsible: 'header', + collapsedByDefault: false, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inline: true, + inputs: [ + { + id: nanoid(), + type: 'textField', + propertyName: 'title', + parentId: commonTabId, + label: 'Title', + tooltip: 'The title of the chart (if any), if none then the title will be generated from the entity type.', + description: 'The title of the chart (if any)', + labelAlign: 'right', + width: '100%', + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return !(getSettingValue(data?.chartType) === `pie` || getSettingValue(data?.chartType) === `polarArea` || getSettingValue(data?.simpleOrPivot) === `pivot`)', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'showTitle', + label: 'Show Title', + description: 'Show the title of the chart', + parentId: commonTabId, + defaultValue: true, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'showLegend', + label: 'Show Legend', + description: + 'Show the legend of the chart. Legend is the area that shows the color and what it represents.', + parentId: commonTabId, + defaultValue: false, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.chartType) === `pie` || getSettingValue(data?.chartType) === `polarArea` || getSettingValue(data?.simpleOrPivot) === `pivot`', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'showTitle', + label: 'Show Title', + description: 'Show the title of the chart', + parentId: commonTabId, + defaultValue: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inline: true, + inputs: [ + { + id: nanoid(), + propertyName: 'legendPosition', + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.showLegend) !== true', + _mode: 'code', + _value: true, + } as any, + label: 'Legend Position', + type: 'dropdown', + allowClear: true, + dropdownOptions: [ + { label: 'Top', value: 'top' }, + { label: 'Bottom', value: 'bottom' }, + { label: 'Left', value: 'left' }, + { label: 'Right', value: 'right' }, + ], + validate: { required: true }, + defaultValue: 'top', + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.chartType) === `pie` || getSettingValue(data?.chartType) === `polarArea`', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'showXAxisScale', + label: 'Show X Axis', + parentId: commonTabId, + defaultValue: true, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'showXAxisTitle', + label: 'Show X Axis Title', + parentId: commonTabId, + defaultValue: true, + hidden: { + _code: 'return getSettingValue(data?.showXAxisScale) !== true', + _mode: 'code', + _value: true, + } as any, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.chartType) === `pie` || getSettingValue(data?.chartType) === `polarArea`', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'showYAxisScale', + label: 'Show Y Axis', + parentId: commonTabId, + defaultValue: true, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'showYAxisTitle', + label: 'Show Y Axis Title', + parentId: commonTabId, + defaultValue: true, + hidden: { + _code: 'return getSettingValue(data?.showYAxisScale) !== true', + _mode: 'code', + _value: true, + } as any, + }, + ], + }) + .toJson(), + ] + } + }) + .toJson(), + ], + }, + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'dataMode', + parentId: 'root', + label: 'Data Source Type', + description: + 'The type of data source you want to use for the chart. If you select `URL`, you will have to provide a URL endpoint to the data. If you select `Entity Type`, you will have to select an entity type from the list.', + inputType: 'dropdown', + allowClear: true, + dropdownOptions: [ + { label: 'URL', value: 'url' }, + { label: 'Entity Type', value: 'entityType' }, + ], + validate: { required: true }, + defaultValue: 'entityType', + hidden: false + }) + .addContainer({ + id: dataSettingsForUrlId, + propertyName: 'dataSettingsForUrl', + parentId: dataTabId, + label: 'Data Settings (URL)', + labelAlign: 'left', + hidden: { + _code: 'return getSettingValue(data?.dataMode) !== `url`', + _mode: 'code', + _value: false, + } as any, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'url', + inputType: 'textField', + label: 'URL', + description: 'The URL you want to use for the chart', + labelAlign: 'right', + parentId: dataTabId, + hidden: false, + validate: { required: true }, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'axisProperty', + label: 'Axis label', + inputType: 'textField', + labelAlign: 'right', + parentId: dataTabId, + isDynamic: false, + description: 'Label for the axis property', + validate: { required: false }, + }) + .addSettingsInput( + { + id: nanoid(), + propertyName: 'valueProperty', + label: 'Value Axis Label', + inputType: 'textField', + labelAlign: 'right', + parentId: dataTabId, + isDynamic: false, + description: 'Label for the value property', + validate: { required: false }, + }) + .toJson(), + ], + }) + .addContainer({ + id: dataSettingsId, + propertyName: 'dataSettings', + parentId: dataTabId, + label: 'Data Settings', + labelAlign: 'left', + hidden: { + _code: 'return getSettingValue(data?.dataMode) === `url`', + _mode: 'code', + _value: false, + } as any, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + inputs: [ + { + id: nanoid(), + type: 'autocomplete', + propertyName: 'entityType', + label: 'Entity Type', + description: 'The entity type you want to use.', + labelAlign: 'right', + parentId: dataTabId, + hidden: false, + dataSourceType: 'url', + validate: {}, + dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', + settingsValidationErrors: [], + jsSetting: true, + useRawValues: true, + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.dataMode) === `url`', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'axisProperty', + label: 'Axis Property', + labelAlign: 'right', + parentId: dataTabId, + type: 'propertyAutocomplete', + isDynamic: false, + description: 'The property to be used on the x-axis.', + validate: { required: true }, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false, + } as any, + autoFillProps: false, + settingsValidationErrors: [], + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _code: 'return !getSettingValue(data?.axisProperty)', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'isAxisTimeSeries', + label: 'Is Axis Property Time Series?', + description: 'If the x-axis is a time series, check this box.', + parentId: dataTabId, + defaultValue: false, + validate: { required: true }, + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.isAxisTimeSeries) !== true', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'timeSeriesFormat', + parentId: dataTabId, + label: 'Time Series Format', + type: 'dropdown', + allowClear: true, + dropdownOptions: [ + { label: 'Day', value: 'day' }, + { label: 'Month', value: 'month' }, + { label: 'Year', value: 'year' }, + { label: 'Day-Month', value: 'day-month' }, + { label: 'Day-Month-Year', value: 'day-month-year' }, + { label: 'Month-Year', value: 'month-year' }, + ], + validate: { required: true }, + defaultValue: 'day-month-year', + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.dataMode) === `url`', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'valueProperty', + label: 'Value Property', + labelAlign: 'right', + parentId: dataTabId, + type: 'propertyAutocomplete', + isDynamic: false, + description: 'The property to be used on the x-axis.', + validate: { required: true }, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false, + } as any, + autoFillProps: false, + settingsValidationErrors: [], + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.simpleOrPivot) === `simple`', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'legendProperty', + label: 'Legend Property', + labelAlign: 'right', + parentId: dataTabId, + type: 'propertyAutocomplete', + isDynamic: false, + description: + 'The properties you want to use on the Legend. This is the property that will be used to group the data for Pivot Charts.', + validate: { required: true }, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false, + } as any, + autoFillProps: false, + settingsValidationErrors: [], + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.dataMode) === `url`', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'orderBy', + label: 'Order By', + labelAlign: 'right', + parentId: dataTabId, + type: 'propertyAutocomplete', + isDynamic: false, + description: + 'The properties you want to order the data by. Use the propeties that you have selected for axis, value (and legend).', + validate: { required: false }, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false, + } as any, + autoFillProps: false, + settingsValidationErrors: [], + width: '100%', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + inputs: [ + { + id: nanoid(), + propertyName: 'orderDirection', + parentId: dataTabId, + label: 'Order Direction', + type: 'dropdown', + allowClear: true, + dropdownOptions: [ + { label: 'Ascending', value: 'asc' }, + { label: 'Descending', value: 'desc' }, + ], + validate: { required: true }, + defaultValue: 'asc', + hidden: { + _code: 'return !(getSettingValue(data?.orderBy))', + _mode: 'code', + _value: false, + } as any, + } + ] + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'aggregationMethod', + parentId: dataTabId, + label: 'Aggregation Method', + inputType: 'dropdown', + allowClear: true, + dropdownOptions: [ + { label: 'Sum', value: 'sum' }, + { label: 'Count', value: 'count' }, + { label: 'Average', value: 'average' }, + { label: 'Min', value: 'min' }, + { label: 'Max', value: 'max' }, + ], + validate: { required: true }, + defaultValue: 'count', + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.dataMode) === `url` || !getSettingValue(data?.entityType)', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'filters', + label: 'Entity Filter', + labelAlign: 'right', + parentId: dataTabId, + type: 'queryBuilder', + hidden: false, + isDynamic: false, + validate: {}, + settingsValidationErrors: [], + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false, + } as any, + fieldsUnavailableHint: 'Please select `Entity Type` to be able to configure this filter.', + width: '100%', + }, + ], + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: appearanceTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'width', + parentId: 'root', + label: 'Width', + description: + 'The width (px) of the chart. If not provided, the default width will be used. Minimum width is 300px. For responsiveness, setting the width will automatically set the height to proportionate value.', + type: 'numberField', + step: 1, + min: 300, + }, + { + id: nanoid(), + propertyName: 'height', + parentId: 'root', + label: 'Height', + description: + 'The height (px) of the chart. If not provided, the default height will be used. Minimum height is 200px. For responsiveness, setting the height will automatically set the width to proportionate value.', + type: 'numberField', + step: 1, + min: 200, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: appearanceTabId, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'showBorder', + label: 'Show Border', + parentId: 'root', + defaultValue: true, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'isDoughnut', + label: 'Is Doughnut', + parentId: appearanceTabId, + defaultValue: false, + hidden: { + _code: 'return getSettingValue(data?.chartType) !== `pie`', + _mode: 'code', + _value: false, + } as any, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'stacked', + label: 'Stacked', + parentId: appearanceTabId, + defaultValue: false, + hidden: { + _code: + 'return getSettingValue(data?.chartType) !== `bar` || getSettingValue(data?.simpleOrPivot) !== `pivot`', + _mode: 'code', + _value: false, + } as any, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: appearanceTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'strokeWidth', + parentId: appearanceTabId, + type: 'numberField', + label: 'Stroke Width', + defaultValue: 0.0, + description: + 'The width of the stroke for the elements (bars, lines, etc.) in the c in the chart. Default is 0.0', + step: 0.1, + }, + { + id: nanoid(), + propertyName: 'strokeColor', + parentId: appearanceTabId, + label: 'Stroke Color', + allowClear: true, + type: 'colorPicker', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: appearanceTabId, + inputs: [ + { + id: nanoid(), + type: 'numberField', + propertyName: 'tension', + parentId: appearanceTabId, + label: 'Tension', + defaultValue: 0, + min: 0, + hidden: { + _code: 'return getSettingValue(data?.chartType) !== `line`', + _mode: 'code', + _value: true, + } as any, + } + ] + }) + .toJson(), + ], + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + jsSetting: true, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/charts/styles.ts b/shesha-reactjs/src/designer-components/charts/styles.ts index 4d03db40cd..60898d7c81 100644 --- a/shesha-reactjs/src/designer-components/charts/styles.ts +++ b/shesha-reactjs/src/designer-components/charts/styles.ts @@ -3,7 +3,7 @@ import { createStyles } from 'antd-style'; const useStyles = createStyles(() => ({ chartControlContainer: { padding: 10, position: 'relative', boxSizing: 'border-box' , textAlign: 'center' }, chartControlButtonContainer: { marginTop: 10, gap: 10 }, - chartControlSpinFontSize: { fontSize: 48 }, + chartControlSpinFontSize: { fontSize: 100 }, filterComponentContainer: { marginTop: 10, padding: 10, display: 'block', border: '1px solid #ddd' }, fullWidth: { width: '100%' }, 'margin-top-5': { marginTop: 5 }, @@ -12,6 +12,72 @@ const useStyles = createStyles(() => ({ 'margin-top-10': { marginTop: 10 }, 'margin-bottom-10': { marginBottom: 10 }, 'gap-10': { gap: 10 }, + + // Responsive chart container + responsiveChartContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + maxWidth: '100%', + maxHeight: '100%', + minWidth: '280px', + minHeight: '200px', + transition: 'all 0.3s ease-in-out', + boxSizing: 'border-box', + + // Center the container itself within its parent + margin: '0 auto', + + // Ensure content is centered within the container + textAlign: 'center', + + // Prevent overflow and ensure proper containment + overflow: 'hidden', + + // Chart container should be flexible but contained + '& > div': { + maxWidth: '100%', + maxHeight: '100%', + width: 'auto', + height: 'auto', + }, + + // Mobile responsive + '@media (max-width: 768px)': { + width: '100% !important', + height: 'auto !important', + minHeight: '250px', + padding: '8px', + margin: '0 auto', + }, + + // Tablet responsive + '@media (max-width: 1024px) and (min-width: 769px)': { + maxWidth: '95vw', + maxHeight: '80vh', + margin: '0 auto', + }, + + // Large screen handling + '@media (min-width: 1400px)': { + maxWidth: '1200px', + maxHeight: '800px', + margin: '0 auto', + } + }, + + // Border variants + chartContainerWithBorder: { + border: '1px solid #ddd', + borderRadius: '8px', + padding: '16px', + }, + + chartContainerNoBorder: { + border: 'none', + borderRadius: 0, + padding: '8px', + }, })); export default useStyles; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/charts/utils.ts b/shesha-reactjs/src/designer-components/charts/utils.tsx similarity index 82% rename from shesha-reactjs/src/designer-components/charts/utils.ts rename to shesha-reactjs/src/designer-components/charts/utils.tsx index 7f0618cc5b..4f3bf59175 100644 --- a/shesha-reactjs/src/designer-components/charts/utils.ts +++ b/shesha-reactjs/src/designer-components/charts/utils.tsx @@ -1,4 +1,106 @@ -import { IFilter, TAggregationMethod, TOperator, TOrderDirection, TTimeSeriesFormat } from "./model"; +import React from "react"; +import { IChartData, IChartsProps, TAggregationMethod, TDataMode, TOperator, TOrderDirection, TTimeSeriesFormat } from "./model"; +import LineChart from "./components/line"; +import BarChart from "./components/bar"; +import PieChart from "./components/pie"; +import PolarAreaChart from "./components/polarArea"; +import { Result } from "antd"; + +export const MAX_TITLE_LINE_LENGTH = 14; + +/** + * Function to manage the length of the title, ie if the title is too long, we need to split it into multiple lines + * @param title the title to manage + * @returns the managed title + */ +export const splitTitleIntoLines = (title: string): string | string[] => { + const words = title.split(' '); + const lines = []; + let currentLine = ''; + + if (title.split(' ').length < MAX_TITLE_LINE_LENGTH) { + return title; + } + + for (const word of words) { + if (currentLine.split(' ').length < MAX_TITLE_LINE_LENGTH) { + currentLine += (currentLine ? ' ' : '') + word; + } else { + if (lines.length === 5) { + lines.push("..."); + return lines; + } + lines.push(currentLine); + currentLine = ''; + } + } + + if (currentLine) lines.push(currentLine); + return lines; +}; + +/** + * Function to generate the responsive style for the chart + * @param props.height the height of the chart + * @param props.width the width of the chart + * @returns the responsive style + */ +export const getResponsiveStyle = (props: IChartsProps) => +({ + // Responsive height with fallbacks + height: props?.height + ? `min(${props.height}px, 80vh)` // Use provided height but cap at 80% viewport height + : 'clamp(300px, 50vh, 600px)', // Responsive height between 300px and 600px + + // Responsive width with fallbacks + width: props?.width + ? `min(${props.width}px, 95vw)` // Use provided width but cap at 95% viewport width + : 'clamp(300px, 90vw, 100%)', // Responsive width between 300px and 800px +}); + +/** + * Filter out null and undefined values from an object + * @param obj the object to filter + * @returns the filtered object + */ +export function filterNonNull(obj: T): Partial { + return Object.fromEntries( + Object.entries(obj).filter(([_, v]) => v !== null && v !== undefined) + ) as Partial; +} + +export const renderChart = (chartType: string, data: IChartData) => { + switch (chartType) { + case 'line': + return ; + case 'bar': + return ; + case 'pie': + return ; + case 'polarArea': + return ; + default: + return ; + } +}; + +export const defaultConfigFiller: { + showTitle: boolean; + simpleOrPivot: 'simple' | 'pivot'; + dataMode: TDataMode; + entityType: string; + axisProperty: string; + valueProperty: string; + aggregationMethod: TAggregationMethod; +} = { + showTitle: true, + simpleOrPivot: 'simple', + dataMode: 'entityType', + entityType: 'Shesha.Domain.FormConfiguration', + axisProperty: 'versionStatus', + valueProperty: 'id', + aggregationMethod: 'count', +}; /** * Function to stringify values in an array of objects @@ -82,7 +184,7 @@ export const getChartDataRefetchParams = (entityType: string, dataProperty: stri properties: removePropertyDuplicates((convertNestedPropertiesToObjectFormat([dataProperty, legendProperty, axisProperty]) + ", " + convertNestedPropertiesToObjectFormat(filterProperties)).replace(/\s/g, '')), filter: filters, sorting: orderBy ? `${orderBy} ${orderDirection ?? 'asc'}` : '', - maxResultCount: -1 + }, }; }; @@ -101,6 +203,8 @@ export const getURLChartDataRefetchParams = (url: string) => { * @returns the value of the property */ export function getPropertyValue(obj: { [key: string]: string | number | object }, path: string) { + if (obj === null) return null; + if (obj === undefined) return undefined; if (!path || typeof obj !== 'object') return undefined; const properties = path.split('.'); @@ -313,28 +417,6 @@ export function filterData(preFilteredData: object[], property: string, operator }); }; -/** - * Apply filters to the data - * @param data raw data - * @param filters filters to apply - * @returns filtered data - */ -export function applyFilters( - data: Array, - filters: Array -): Array { - // Start with the unfiltered data - let filteredData = data ? [...data] : []; - - // Apply each filter one by one - filters?.forEach((filter) => { - const { property, operator, value } = filter; - filteredData = filterData(filteredData, property, operator, value); - }); - - return filteredData; -} - /** * from that array we need make a list of all the properties of the objects * @param data the data to get the properties from diff --git a/shesha-reactjs/src/designer-components/checkbox/checkbox.tsx b/shesha-reactjs/src/designer-components/checkbox/checkbox.tsx index 95c295a7bf..d1a8eae4de 100644 --- a/shesha-reactjs/src/designer-components/checkbox/checkbox.tsx +++ b/shesha-reactjs/src/designer-components/checkbox/checkbox.tsx @@ -1,22 +1,35 @@ import React from 'react'; import { IToolboxComponent } from '@/interfaces'; -import { FormMarkup, IInputStyles } from '@/providers/form/models'; import { CheckSquareOutlined } from '@ant-design/icons'; -import { Checkbox } from 'antd'; +import { Checkbox, CheckboxProps } from 'antd'; import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; -import settingsFormJson from './settingsForm.json'; -import { getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { validateConfigurableComponentSettings } from '@/providers/form/utils'; import { DataTypes } from '@/interfaces/dataTypes'; -import { useFormData } from '@/providers'; +import { IInputStyles } from '@/providers'; import ReadOnlyDisplayFormItem from '@/components/readOnlyDisplayFormItem'; import { ICheckboxComponentProps } from './interfaces'; -import { migratePropertyName, migrateCustomFunctions, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; +import { + migratePropertyName, + migrateCustomFunctions, + migrateReadOnly, +} from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { IEventHandlers, getAllEventHandlers } from '@/components/formDesigner/components/utils'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; -const settingsForm = settingsFormJson as FormMarkup; +interface ICheckboxComponentCalulatedValues { + eventHandlers?: IEventHandlers; +} -const CheckboxComponent: IToolboxComponent = { +interface ExtendedCheckboxProps extends CheckboxProps { + onBlur?: (event: React.FocusEvent) => void; + onFocus?: (event: React.FocusEvent) => void; + onChange?: (e: CheckboxChangeEvent) => void; +} + +const CheckboxComponent: IToolboxComponent = { type: 'checkbox', isInput: true, isOutput: true, @@ -24,38 +37,43 @@ const CheckboxComponent: IToolboxComponent = { name: 'Checkbox', icon: , dataTypeSupported: ({ dataType }) => dataType === DataTypes.boolean, - Factory: ({ model }) => { - const { data } = useFormData(); + calculateModel: (model, allData) => ({eventHandlers: getAllEventHandlers(model, allData)}), + Factory: ({ model, calculatedModel }) => { return ( - {(value, onChange) => ( - model.readOnly ? ( - - ) : ( - - ) - )} + {(value, onChange) => { + const events: ExtendedCheckboxProps = { + onBlur: calculatedModel.eventHandlers.onBlur, + onFocus: calculatedModel.eventHandlers.onFocus, + onChange: (e: CheckboxChangeEvent) => { + calculatedModel.eventHandlers.onChange({ value: e.target.checked }, e); + if (typeof onChange === 'function') onChange(e); + } + }; + + return model.readOnly + ? + : + ; + }} ); }, - settingsFormMarkup: settingsForm, - migrator: (m) => m - .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(1, (prev) => migrateVisibility(prev)) - .add(2, (prev) => migrateReadOnly(prev)) - .add(3, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) - .add(4, (prev) => { - const styles: IInputStyles = { - style: prev.style - }; + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + migrator: (m) => + m + .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(1, (prev) => migrateVisibility(prev)) + .add(2, (prev) => migrateReadOnly(prev)) + .add(3, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) + .add(4, (prev) => { + const styles: IInputStyles = { + style: prev.style, + }; - return { ...prev, desktop: {...styles}, tablet: {...styles}, mobile: {...styles} }; - }) - , - validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }), }; export default CheckboxComponent; diff --git a/shesha-reactjs/src/designer-components/checkbox/settingsForm.ts b/shesha-reactjs/src/designer-components/checkbox/settingsForm.ts new file mode 100644 index 0000000000..ca18de2373 --- /dev/null +++ b/shesha-reactjs/src/designer-components/checkbox/settingsForm.ts @@ -0,0 +1,231 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data: any) => { + // Generate unique IDs for major components + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const validationTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textArea', + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + defaultValue: 'inherited', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'defaultValue', + label: 'Default Checked', + jsSetting: true, + }) + .toJson(), + ], + }, + { + key: '2', + title: 'Validation', + id: validationTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'validate.required', + label: 'Required', + inputType: 'switch', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: validationTabId, + }) + .toJson(), + ], + }, + { + key: '3', + title: 'Events', + id: eventsTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onChangeCustom', + label: 'On Change', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on changing of event.', + parentId: eventsTabId, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onFocusCustom', + label: 'On Focus', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on focus of event.', + parentId: eventsTabId, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onBlurCustom', + label: 'On Blur', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on blur of event.', + parentId: eventsTabId, + }) + .toJson(), + ], + }, + { + key: '4', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: '5', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/checkboxGroup/checkboxGroup.tsx b/shesha-reactjs/src/designer-components/checkboxGroup/checkboxGroup.tsx index 5f65237093..c9dbefdae1 100644 --- a/shesha-reactjs/src/designer-components/checkboxGroup/checkboxGroup.tsx +++ b/shesha-reactjs/src/designer-components/checkboxGroup/checkboxGroup.tsx @@ -1,27 +1,32 @@ import { ProfileOutlined } from '@ant-design/icons'; import React from 'react'; -import { useFormData } from '@/providers'; -import { IToolboxComponent } from '@/interfaces'; +import { IConfigurableFormComponent, IToolboxComponent } from '@/interfaces'; import { DataTypes } from '@/interfaces/dataTypes'; -import { FormMarkup } from '@/providers/form/models'; -import { getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { evaluateValue, executeScriptSync, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { IReferenceListIdentifier } from '@/interfaces/referenceList'; import { getLegacyReferenceListIdentifier } from '@/utils/referenceList'; import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import RefListCheckboxGroup from './refListCheckboxGroup'; -import settingsFormJson from './settingsForm.json'; import { ICheckboxGroupProps } from './utils'; -import { migratePropertyName, migrateCustomFunctions, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; +import { + migratePropertyName, + migrateCustomFunctions, + migrateReadOnly, +} from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { IEventHandlers, getAllEventHandlers } from '@/components/formDesigner/components/utils'; -const settingsForm = settingsFormJson as FormMarkup; +interface IEnhancedICheckboxGoupProps extends Omit, IConfigurableFormComponent { +} -interface IEnhancedICheckboxGoupProps extends Omit { - style?: string; +interface ICheckboxGoupComopnentCalulatedValues { + eventHandlers: IEventHandlers; + dataSourceUrl?: string; } -const CheckboxGroupComponent: IToolboxComponent = { +const CheckboxGroupComponent: IToolboxComponent = { type: 'checkboxGroup', isInput: true, isOutput: true, @@ -29,20 +34,42 @@ const CheckboxGroupComponent: IToolboxComponent = { name: 'Checkbox group', icon: , dataTypeSupported: ({ dataType }) => dataType === DataTypes.referenceListItem, - Factory: ({ model }) => { - const { data } = useFormData(); - + calculateModel: (model, allData) => ({ + eventHandlers: getAllEventHandlers(model, allData), + dataSourceUrl: model.dataSourceUrl ? executeScriptSync(model.dataSourceUrl, allData) : model.dataSourceUrl, + defaultValue: evaluateValue(model.defaultValue, allData.data), + }), + Factory: ({ model, calculatedModel }) => { return ( - {(value, onChange) => - - } + {(value, onChange) => { + const customEvents = calculatedModel.eventHandlers; + const onChangeInternal = (e: any) => { + if (e.target) + customEvents.onChange({ value: e.target.value }, e); + else + customEvents.onChange({ value: e }, null); + if (typeof onChange === 'function') onChange(e); + }; + + return ( + + ); + }} ); }, - settingsFormMarkup: settingsForm, - validateSettings: model => validateConfigurableComponentSettings(settingsForm, model), - initModel: model => { + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + initModel: (model) => { const customProps: IEnhancedICheckboxGoupProps = { ...model, dataSourceType: 'values', @@ -51,24 +78,24 @@ const CheckboxGroupComponent: IToolboxComponent = { }; return customProps; }, - migrator: m => m - .add(0, prev => ({ - ...prev, - dataSourceType: prev['dataSourceType'] ?? 'values', - direction: prev['direction'] ?? 'horizontal', - mode: prev['mode'] ?? 'single', - })) - .add(1, prev => { - return { + migrator: (m) => + m + .add(0, (prev) => ({ ...prev, - referenceListId: getLegacyReferenceListIdentifier(prev.referenceListNamespace, prev.referenceListName), - }; - }) - .add(2, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(3, (prev) => migrateVisibility(prev)) - .add(4, (prev) => migrateReadOnly(prev)) - .add(5, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) - , + dataSourceType: prev['dataSourceType'] ?? 'values', + direction: prev['direction'] ?? 'horizontal', + mode: prev['mode'] ?? 'single', + })) + .add(1, (prev) => { + return { + ...prev, + referenceListId: getLegacyReferenceListIdentifier(prev.referenceListNamespace, prev.referenceListName), + }; + }) + .add(2, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(3, (prev) => migrateVisibility(prev)) + .add(4, (prev) => migrateReadOnly(prev)) + .add(5, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })), linkToModelMetadata: (model, metadata): IEnhancedICheckboxGoupProps => { const refListId: IReferenceListIdentifier = metadata.referenceListName ? { module: metadata.referenceListModule, name: metadata.referenceListName } diff --git a/shesha-reactjs/src/designer-components/checkboxGroup/multiCheckbox.tsx b/shesha-reactjs/src/designer-components/checkboxGroup/multiCheckbox.tsx index b082ffee99..f81d22e85b 100644 --- a/shesha-reactjs/src/designer-components/checkboxGroup/multiCheckbox.tsx +++ b/shesha-reactjs/src/designer-components/checkboxGroup/multiCheckbox.tsx @@ -1,67 +1,60 @@ -import { Checkbox, Col, Row } from 'antd'; -import React, { FC, useEffect, useMemo } from 'react'; import { useGet } from '@/hooks'; -import { useFormData, useGlobalState } from '@/providers'; +import { executeScriptSync } from '@/index'; import { useReferenceList } from '@/providers/referenceListDispatcher'; -import { getDataSourceList } from '../radio/utils'; -import { getSpan, ICheckboxGroupProps } from './utils'; import { nanoid } from '@/utils/uuid'; +import { Checkbox } from 'antd'; +import React, { CSSProperties, FC, useEffect, useMemo } from 'react'; +import { getDataSourceList } from '../radio/utils'; +import { ICheckboxGroupProps } from './utils'; const MultiCheckbox: FC = (model) => { - const { data: formData } = useFormData(); - const { globalState } = useGlobalState(); const { items, referenceListId, direction, value, onChange } = model; const { data: refList } = useReferenceList(referenceListId); - - //#region Data source is url - const getEvaluatedUrl = (url: string) => { - if (!url) return ''; - return (() => { - // tslint:disable-next-line:function-constructor - return new Function('data, globalState', url)(formData, globalState); // Pass data, query, globalState - })(); - }; - - const { refetch, data } = useGet({ path: getEvaluatedUrl(model?.dataSourceUrl), lazy: true }); + const { refetch, data } = useGet({ path: model.dataSourceUrl, lazy: true }); useEffect(() => { - if (model?.dataSourceType === 'url' && model?.dataSourceUrl) { + if (model.dataSourceType === 'url' && model.dataSourceUrl) { refetch(); } - }, [model?.dataSourceType, model?.dataSourceUrl]); + }, [model.dataSourceType, model.dataSourceUrl]); const reducedData = useMemo(() => { const list = Array.isArray(data?.result) ? data?.result : data?.result?.items; - if (Array.isArray(list) && model?.reducerFunc) { - return new Function('data', model?.reducerFunc)(list) as []; + if (Array.isArray(list) && model.reducerFunc) { + return executeScriptSync(model.reducerFunc, { data: list }); } return data?.result; - }, [data?.result, model?.reducerFunc]); - //#endregion - - const options = useMemo( - () => { - const list = getDataSourceList(model?.dataSourceType, items, refList?.items, reducedData) || []; - return list.map(item => item.id ? item : { ...item, id: nanoid() }); - }, - [model?.dataSourceType, items, refList?.items, reducedData] - ); - + }, [data?.result, model.reducerFunc]); + + const options = useMemo(() => { + const list = getDataSourceList(model.dataSourceType, items, refList?.items, reducedData) || []; + return list.map((item) => (item.id ? item : { ...item, id: nanoid() })); + }, [model.dataSourceType, items, refList?.items, reducedData]); + + const checkboxGroupStyle: CSSProperties = { + ...model.style, + display: 'flex', + flexDirection: direction === 'vertical' ? 'column' : 'row', + flexWrap: direction === 'vertical' ? 'nowrap' : 'wrap', + gap: '8px', + }; return ( - - - {options.map(({ id, label, value: v }) => ( - - - {label} - - - ))} - - +
model.onFocus?.({ ...e, target: { value: value, ...e.target } })} + onBlur={(e) => model.onBlur?.({ ...e, target: { value: value, ...e.target } })} + > + +
); }; diff --git a/shesha-reactjs/src/designer-components/checkboxGroup/settingsForm.ts b/shesha-reactjs/src/designer-components/checkboxGroup/settingsForm.ts new file mode 100644 index 0000000000..f3edbdd809 --- /dev/null +++ b/shesha-reactjs/src/designer-components/checkboxGroup/settingsForm.ts @@ -0,0 +1,365 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data: any) => { + // Generate unique IDs for major components + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const validationTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const stylePanelId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textArea', + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + defaultValue: 'inherited', + size: 'small', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .toJson(), + ], + }, + { + key: '2', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'mode', + label: 'Mode', + size: 'small', + jsSetting: true, + parentId: dataTabId, + dropdownOptions: [ + { + label: 'Single', + value: 'single', + }, + { + label: 'Multiple', + value: 'multiple', + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'dataSourceType', + label: 'Data Source Type', + size: 'small', + jsSetting: true, + parentId: dataTabId, + dropdownOptions: [ + { + label: 'Values', + value: 'values', + }, + { + label: 'Reference list', + value: 'referenceList', + }, + { + label: 'API URL', + value: 'url', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.dataSourceType) !== "values";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'labelValueEditor', + propertyName: 'items', + parentId: dataTabId, + label: 'Items', + labelTitle: 'Label', + labelName: 'label', + valueTitle: 'Value', + valueName: 'value', + mode: 'dialog', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.dataSourceType) !== "referenceList";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'referenceListAutocomplete', + id: nanoid(), + propertyName: 'referenceListId', + label: 'Reference List', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.dataSourceType) !== "url";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'codeEditor', + id: nanoid(), + propertyName: 'dataSourceUrl', + label: 'Data Source URL', + jsSetting: true, + }, + { + type: 'codeEditor', + id: nanoid(), + propertyName: 'reducerFunc', + label: 'Reducer Function', + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + { + key: '3', + title: 'Validation', + id: validationTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'validate.required', + label: 'Required', + inputType: 'switch', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: validationTabId, + }) + .toJson(), + ], + }, + { + key: '4', + title: 'Events', + id: eventsTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onChangeCustom', + label: 'On Change', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on changing of event.', + parentId: eventsTabId, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onFocusCustom', + label: 'On Focus', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on focus of event.', + parentId: eventsTabId, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onBlurCustom', + label: 'On Blur', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on blur of event.', + parentId: eventsTabId, + }) + .toJson(), + ], + }, + { + key: '5', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'direction', + label: 'Direction', + size: 'small', + jsSetting: true, + parentId: styleRouterId, + defaultValue: 'horizontal', + dropdownOptions: [ + { + label: 'Horizontal', + value: 'horizontal', + }, + { + label: 'Vertical', + value: 'vertical', + }, + ], + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + parentId: stylePanelId, + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: '6', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/checkboxGroup/utils.ts b/shesha-reactjs/src/designer-components/checkboxGroup/utils.ts index a79b12a460..6823b3a0b6 100644 --- a/shesha-reactjs/src/designer-components/checkboxGroup/utils.ts +++ b/shesha-reactjs/src/designer-components/checkboxGroup/utils.ts @@ -1,11 +1,10 @@ import { CSSProperties } from 'react'; import { RadioChangeEvent, SpaceProps } from 'antd'; -import { IConfigurableFormComponent } from '@/providers/form/models'; import { DataSourceType, ILabelValue } from '@/designer-components/dropdown/model'; import { IReferenceListIdentifier } from '@/interfaces/referenceList'; type CheckboxGroupMode = 'single' | 'multiple'; -export interface ICheckboxGroupProps extends Omit { +export interface ICheckboxGroupProps { items?: ILabelValue[]; mode?: CheckboxGroupMode; /** @@ -21,9 +20,13 @@ export interface ICheckboxGroupProps extends Omit | RadioChangeEvent) => void; + onFocus?: (e: any) => void; + onBlur?: (e: any) => void; style?: CSSProperties; dataSourceUrl?: string; reducerFunc?: string; + readOnly?: boolean; + defaultValue?: any | any[]; } export const getSpan = (direction: SpaceProps['direction'], size: number) => diff --git a/shesha-reactjs/src/designer-components/chevron/chevron.tsx b/shesha-reactjs/src/designer-components/chevron/chevron.tsx index c9cb0cee76..a0d647375b 100644 --- a/shesha-reactjs/src/designer-components/chevron/chevron.tsx +++ b/shesha-reactjs/src/designer-components/chevron/chevron.tsx @@ -3,10 +3,12 @@ import { FolderOpenOutlined } from '@ant-design/icons'; import React from 'react'; import { IToolboxComponent } from '@/interfaces'; import { ChevronControl } from '@/components/chevron'; -import { RefListItemGroupConfiguratorProvider } from '@/providers/refList/provider'; -import { ChevronSettingsForm } from './settings'; +import { RefListItemGroupConfiguratorProvider } from '@/components/refListSelectorDisplay/provider'; +import { getSettings } from './settingsForm'; import { ConfigurableFormItem } from '@/components'; import { IChevronProps } from '@/components/chevron/models'; +import { defaultStyles } from './utils'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; const ChevronComponent: IToolboxComponent = { type: 'chevron', @@ -14,9 +16,7 @@ const ChevronComponent: IToolboxComponent = { name: 'Chevron', icon: , Factory: ({ model }) => { - - if (model.hidden) return null; - + if (model.hidden) return null; return ( {value => ( @@ -29,11 +29,9 @@ const ChevronComponent: IToolboxComponent = { ); }, - settingsFormFactory: (props) => { - return ( - - ); - } + settingsFormMarkup: (props) => getSettings(props), + migrator: (m) => m + .add(1, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), }; export default ChevronComponent; diff --git a/shesha-reactjs/src/designer-components/chevron/settings.tsx b/shesha-reactjs/src/designer-components/chevron/settings.tsx index fcac316ff1..f3865480af 100644 --- a/shesha-reactjs/src/designer-components/chevron/settings.tsx +++ b/shesha-reactjs/src/designer-components/chevron/settings.tsx @@ -1,5 +1,5 @@ import { Autocomplete, ColorPicker, Show } from '@/components'; -import RefListItemSelectorSettingsModal from '@/providers/refList/options/modal'; +import RefListItemSelectorSettingsModal from '@/components/refListSelectorDisplay/options/modal'; import { Checkbox, Input, Select } from 'antd'; import React, { FC } from 'react'; import { ContextPropertyAutocomplete } from '@/designer-components/contextPropertyAutocomplete'; @@ -14,8 +14,8 @@ import SettingsCollapsiblePanel from '../_settings/settingsCollapsiblePanel'; interface IChevronSettingsState extends IChevronProps { } -const ChevronSettings: FC> = () => { - const { values, model, onValuesChange } = useSettingsForm(); +export const ChevronSettings: FC> = () => { + const { values, onValuesChange } = useSettingsForm(); const designerModelType = useFormDesignerState(false)?.formSettings?.modelType; const { formSettings } = useForm(); @@ -25,9 +25,10 @@ const ChevronSettings: FC> = () => { @@ -58,8 +59,8 @@ const ChevronSettings: FC> = () => { diff --git a/shesha-reactjs/src/designer-components/chevron/settingsForm.ts b/shesha-reactjs/src/designer-components/chevron/settingsForm.ts new file mode 100644 index 0000000000..c51411a7a3 --- /dev/null +++ b/shesha-reactjs/src/designer-components/chevron/settingsForm.ts @@ -0,0 +1,356 @@ +import { DesignerToolbarSettings } from '@/index'; +import { nanoid } from '@/utils/uuid'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { FormLayout } from 'antd/es/form/Form'; + +const searchableTabsId = nanoid(); +const commonTabId = nanoid(); +const itemsTabId = nanoid(); +const appearanceTabId = nanoid(); +const securityTabId = nanoid(); +const styleRouterId = nanoid(); +const pnlFontStyleId = nanoid(); +const dimensionsStylePnlId = nanoid(); + +export const getSettings = (data: any) => { + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + parentId: commonTabId, + label: 'Property Name', + size: 'small', + validate: { + required: true, + }, + styledLabel: true, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textArea', + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .toJson(), + ], + }, + { + key: 'data', + title: 'Data', + id: itemsTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: itemsTabId, + inputType: 'referenceListAutocomplete', + propertyName: 'referenceList', + label: 'Reference List', + tooltip: 'Make sure to reselect the reference list if any changes are made to its items', + filter: { and: [{ '==': [{ var: 'isLast' }, true] }] }, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'items', + label: 'Items', + labelAlign: 'right', + parentId: itemsTabId, + referenceList: { + _code: 'return getSettingValue(data?.referenceList);', + _mode: 'code', + _value: false, + } as any, + inputType: 'RefListItemSelectorSettingsModal', + }) + .toJson(), + ], + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: pnlFontStyleId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlFontStyleId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + defaultValue: 14, + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: 'Controls text thickness (light, normal, bold, etc.)', + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: dimensionsStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Width', + width: 85, + propertyName: 'width', + icon: 'widthIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + defaultValue: '150px', + }, + { + type: 'textField', + id: nanoid(), + label: 'Height', + width: 85, + propertyName: 'height', + icon: 'heightIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + defaultValue: '35px', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlColorSettings', + label: 'Color Settings', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'colorSource', + label: 'Color Source', + inputType: 'dropdown', + jsSetting: true, + tooltip: 'Hex and RGB colors are supported', + parentId: styleRouterId, + defaultValue: 'primary', + dropdownOptions: [ + { value: 'primary', label: 'Primary color' }, + { value: 'custom', label: 'Custom color' }, + { value: 'reflist', label: 'From reflist item' }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { + _code: 'return getSettingValue(data?.colorSource) !== "custom";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'activeColor', + label: 'Active Color', + type: 'colorPicker', + jsSetting: true, + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'showIcons', + label: 'Show Icons', + inputType: 'switch', + jsSetting: true, + parentId: styleRouterId, + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + tooltip: 'Enter a list of permissions that should be associated with this component', + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/chevron/utils.ts b/shesha-reactjs/src/designer-components/chevron/utils.ts new file mode 100644 index 0000000000..3c65d2673a --- /dev/null +++ b/shesha-reactjs/src/designer-components/chevron/utils.ts @@ -0,0 +1,17 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + + return { + font: { + color: '#000', + type: 'Segoe UI', + align: 'left', + weight: '400', + }, + dimensions: { + width: 'auto', + height: 30 + } + }; +}; diff --git a/shesha-reactjs/src/designer-components/childEntitiesTagGroup/control.tsx b/shesha-reactjs/src/designer-components/childEntitiesTagGroup/control.tsx index 69be73ad11..a7cd963a8a 100644 --- a/shesha-reactjs/src/designer-components/childEntitiesTagGroup/control.tsx +++ b/shesha-reactjs/src/designer-components/childEntitiesTagGroup/control.tsx @@ -1,5 +1,5 @@ import ChildEntitiesTagGroupModal from './modal'; -import React, { FC, useMemo, useState } from 'react'; +import React, { FC, useMemo, useState, useRef } from 'react'; import { Button, Space, App, Modal, Select, Tag } from 'antd'; import { ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { executeScriptSync, useAvailableConstantsData } from '@/providers/form/utils'; @@ -17,26 +17,21 @@ import { getValueByPropertyName } from '@/utils/object'; const { confirm } = Modal; interface IProps { + componentName?: string; model: IChildEntitiesTagGroupProps; onChange?: Function; value?: any; } -interface IState { - activeValue?: IChildEntitiesTagGroupSelectOptions; - open: boolean; -} - -const INIT_STATE: IState = { open: false }; const CONFIRM_DELETE_TITLE = 'Are you sure you want to delete this item?'; const WARNING_BIND_FORM = 'Please bind an appropriate form to this component.'; const ERROR_LABEL = 'Please configure label configuration for this component.'; const ChildEntitiesTagGroupControl: FC = ({ onChange, value, model }) => { const { styles } = useStyles(); - const [state, setState] = useState(INIT_STATE); - const { activeValue, open } = state; - const { deleteConfirmationBody, deleteConfirmationTitle, formId, labelFormat, propertyName } = model; + const [ open, setOpen ] = useState(false); + const activeValue = useRef(null); + const { deleteConfirmationBody, deleteConfirmationTitle, formId, labelFormat, propertyName, componentName } = model; const allData = useAvailableConstantsData(); const parent = useParent(); @@ -69,7 +64,7 @@ const ChildEntitiesTagGroupControl: FC = ({ onChange, value, model }) => }; const options = useDeepCompareMemo(() => { - if (Array.isArray(value)) { + if (Array.isArray(value) && value.length) { const opts: IChildEntitiesTagGroupSelectOptions[] = []; value.forEach(item => { opts.push({ @@ -85,15 +80,15 @@ const ChildEntitiesTagGroupControl: FC = ({ onChange, value, model }) => const onModalChange = (value: any) => { const data = !!value ? getValueByPropertyName(value, propertyName) : undefined; - if (activeValue) { - onChange(options.map(item => item.value === activeValue.value ? data : item.data)); - } else { - onChange([...options.map(item => item.data), data]); - } + const opt = activeValue.current + ? options.map(item => item.value === activeValue.current.value ? data : item.data) + : [...options.map(item => item.data), data]; + onChange(opt); }; const onClickTag = (val: IChildEntitiesTagGroupSelectOptions) => () => { - setState((s) => ({ ...s, activeValue: val, open: true })); + activeValue.current = val; + setOpen(true); }; const onCloseTag = (item: string) => (e: React.MouseEvent) => { @@ -108,7 +103,6 @@ const ChildEntitiesTagGroupControl: FC = ({ onChange, value, model }) => cancelText: 'No', onOk: () => { const opts = options.filter(({ value }) => value !== item); - setState((s) => ({ ...s, options: opts })); onChange(opts.map(item => item.data)); }, }); @@ -116,12 +110,17 @@ const ChildEntitiesTagGroupControl: FC = ({ onChange, value, model }) => const onOpenModal = () => { if (formConfiguration) { - setState((s) => ({ ...s, open: true, activeValue: null })); + setOpen(true); } else { message.warning(WARNING_BIND_FORM); } }; + const onCloseModal = () => { + activeValue.current = null; + setOpen(false); + }; + const isEditable = !model?.readOnly; const tagRender = ({ label, value }) => { @@ -143,8 +142,8 @@ const ChildEntitiesTagGroupControl: FC = ({ onChange, value, model }) => const contextId = [parent?.subFormIdPrefix, propertyName].filter(x => !!x).join('.'); const initData = useMemo(() => { - return new Promise(resolve => resolve({ [propertyName]: activeValue?.data })); - }, [propertyName, activeValue?.data]); + return open ? new Promise(resolve => resolve({ [propertyName]: {...activeValue.current?.data} })) : null; + }, [propertyName, open ]); return ( @@ -157,13 +156,14 @@ const ChildEntitiesTagGroupControl: FC = ({ onChange, value, model }) => type='control' initialData={initData} > - + setState((s) => ({...s, open: false}))} + onToggle={onCloseModal} loading={loading} onChange={onModalChange} /> diff --git a/shesha-reactjs/src/designer-components/childEntitiesTagGroup/index.tsx b/shesha-reactjs/src/designer-components/childEntitiesTagGroup/index.tsx index d3cc408485..950fc081b8 100644 --- a/shesha-reactjs/src/designer-components/childEntitiesTagGroup/index.tsx +++ b/shesha-reactjs/src/designer-components/childEntitiesTagGroup/index.tsx @@ -23,7 +23,7 @@ const ChildEntitiesTagGroup: IToolboxComponent = { return ( {(value, onChange) => - + } ); diff --git a/shesha-reactjs/src/designer-components/childEntitiesTagGroup/modal.tsx b/shesha-reactjs/src/designer-components/childEntitiesTagGroup/modal.tsx index 645443aac3..4676d63cc0 100644 --- a/shesha-reactjs/src/designer-components/childEntitiesTagGroup/modal.tsx +++ b/shesha-reactjs/src/designer-components/childEntitiesTagGroup/modal.tsx @@ -1,7 +1,6 @@ import { Modal, Skeleton } from 'antd'; -import { useDataContext } from '@/providers/dataContextProvider/contexts'; import React, { FC } from 'react'; -import { useAppConfigurator } from '@/providers'; +import { useAppConfigurator, useDataContextManagerActions } from '@/providers'; import { IFormMarkupResponse } from '@/providers/form/api'; import { ConfigurationItemVersionStatusMap } from '@/utils/configurationFramework/models'; import ValidationErrors from '@/components/validationErrors'; @@ -10,6 +9,7 @@ import { IChildEntitiesTagGroupProps } from './models'; import FormInfo from '@/components/configurableForm/formInfo'; interface IProps extends IChildEntitiesTagGroupProps { + contextId: string; formInfo?: IFormMarkupResponse['formConfiguration']; error: IFormMarkupResponse['error'] | any; readOnly?: boolean; @@ -20,6 +20,7 @@ interface IProps extends IChildEntitiesTagGroupProps { } const ChildEntitiesTagGroupModal: FC = ({ + contextId, formInfo, error, readOnly, @@ -31,7 +32,7 @@ const ChildEntitiesTagGroupModal: FC = ({ onChange }) => { const { formInfoBlockVisible } = useAppConfigurator(); - const context = useDataContext(); + const context = useDataContextManagerActions().getDataContext(contextId); const onOk = () => { onChange(context.getData()); diff --git a/shesha-reactjs/src/designer-components/childEntitiesTagGroup/settings.tsx b/shesha-reactjs/src/designer-components/childEntitiesTagGroup/settings.tsx index a1d7b4cf68..58840014a9 100644 --- a/shesha-reactjs/src/designer-components/childEntitiesTagGroup/settings.tsx +++ b/shesha-reactjs/src/designer-components/childEntitiesTagGroup/settings.tsx @@ -34,8 +34,10 @@ const ChildEntitiesTagGroupSettings: FC diff --git a/shesha-reactjs/src/designer-components/codeEditor/codeEditor.tsx b/shesha-reactjs/src/designer-components/codeEditor/codeEditor.tsx index 97674eeed9..62744bdf46 100644 --- a/shesha-reactjs/src/designer-components/codeEditor/codeEditor.tsx +++ b/shesha-reactjs/src/designer-components/codeEditor/codeEditor.tsx @@ -7,7 +7,6 @@ import { App, Button, Modal, - Space, Tabs, Typography } from 'antd'; @@ -50,9 +49,30 @@ export const CodeEditor: FC = ({ } } }; + + const hasValue = value && typeof (value) === 'string' && Boolean(value?.trim()); + const onClear = () => { - setInternalValue(null); - if (props.onChange) props.onChange(null); + if (hasValue) { + modal.confirm({ + title: 'Clear code editor?', + icon: , + content: 'If you clear the code editor, the changes will be lost and the editor will be closed', + okText: 'Yes', + okType: 'danger', + cancelText: 'No', + onOk() { + setInternalValue(null); + setShowDialog(false); + if (props.onChange) props.onChange(null); + } + }); + } + }; + + const onDialogSave = () => { + if (props.onChange) props.onChange(internalValue); + setShowDialog(false); }; const openEditorDialog = () => setShowDialog(true); @@ -76,10 +96,6 @@ export const CodeEditor: FC = ({ setShowDialog(false); } }; - const onDialogSave = () => { - if (props.onChange) props.onChange(internalValue); - setShowDialog(false); - }; const effectiveValue = mode === 'inline' ? value : internalValue; @@ -102,8 +118,7 @@ export const CodeEditor: FC = ({ /> ); - const hasValue = value && typeof (value) === 'string' && Boolean(value?.trim()); - + if (props.hidden) return null; if (mode === 'inline') return renderCodeEditor(); @@ -126,26 +141,26 @@ export const CodeEditor: FC = ({ ] : undefined; + const buttonValue = value?.replace('return', '').replace(/;+$/, "");; + return readOnly && !hasValue ? (No Code) : ( <> - - - - - - + {showDialog && ( = ({ classNames={{ body: styles.codeEditorModalBody }} className={styles.codeEditorModal} width={null} + footer={[ + hasValue && , + , + !readOnly && ( + + ), + ]} > diff --git a/shesha-reactjs/src/designer-components/codeEditor/index.tsx b/shesha-reactjs/src/designer-components/codeEditor/index.tsx index b5c11cbd77..c4aef027c6 100644 --- a/shesha-reactjs/src/designer-components/codeEditor/index.tsx +++ b/shesha-reactjs/src/designer-components/codeEditor/index.tsx @@ -27,7 +27,7 @@ const CodeEditorComponent: IToolboxComponent = { dataType === DataTypes.string && (dataFormat === StringFormats.javascript || dataFormat === StringFormats.json), Factory: ({ model }) => { const editorProps: ICodeEditorProps = { - ...model, + ...model as any, }; const constantsEvaluator = useConstantsEvaluator({ availableConstantsExpression: model.availableConstantsExpression }); diff --git a/shesha-reactjs/src/designer-components/codeEditor/interfaces.ts b/shesha-reactjs/src/designer-components/codeEditor/interfaces.ts index 70d711843b..6591bca65c 100644 --- a/shesha-reactjs/src/designer-components/codeEditor/interfaces.ts +++ b/shesha-reactjs/src/designer-components/codeEditor/interfaces.ts @@ -5,6 +5,7 @@ import { IMetadata, IObjectMetadata } from '@/interfaces/metadata'; import { CodeTemplateSettings } from '@/components/codeEditor/models'; import { Environment, IMetadataBuilder } from '@/publicJsApis/metadataBuilder'; import { IObjectMetadataBuilder as IInternalObjectMetadataBuilder } from '@/utils/metadata/metadataBuilder'; +import { BaseButtonProps } from 'antd/lib/button/button'; export interface IExecutableCodeEditor { fileName?: string; @@ -24,19 +25,22 @@ export interface ICodeEditorProps extends Omit void; mode?: 'inline' | 'dialog'; language?: CodeLanguages; + type?: BaseButtonProps['type']; + className?: string; + ghost?: boolean; availableConstants?: IObjectMetadata | (() => Promise); resultType?: IMetadata | (() => Promise); environment?: Environment; } export type GetAvailableConstantsArgs = { - data: Record; + data: Record; metadataBuilder: IMetadataBuilder; form: IShaFormInstance; }; export type GetAvailableConstantsFunc = (args: GetAvailableConstantsArgs) => Promise; export type GetResultTypeArgs = { - data: Record; + data: Record; metadataBuilder: IMetadataBuilder; form: IShaFormInstance; }; @@ -49,6 +53,6 @@ export interface ICodeEditorComponentProps extends IConfigurableFormComponent, I language?: CodeLanguages; availableConstantsExpression?: string | GetAvailableConstantsFunc; availableConstants?: IObjectMetadata; - + resultTypeExpression?: string | GetResultTypeFunc; } \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/codeEditor/styles.ts b/shesha-reactjs/src/designer-components/codeEditor/styles.ts index c66a95b3a7..b736db745b 100644 --- a/shesha-reactjs/src/designer-components/codeEditor/styles.ts +++ b/shesha-reactjs/src/designer-components/codeEditor/styles.ts @@ -10,9 +10,19 @@ export const useStyles = createStyles(({ css, cx }) => { const codeEditorContainer = cx("sha-code-editor-container", css` height: 70vh; `); + + const button = cx("sha-code-btn", + css` + overflow: hidden; + text-overflow: ellipsis; + display: inline-flex; + align-items: center; + `); + return { codeEditorModal, codeEditorModalBody, codeEditorContainer, + button }; }); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/collapsiblePanel/collapsiblePanelComponent.tsx b/shesha-reactjs/src/designer-components/collapsiblePanel/collapsiblePanelComponent.tsx index a7aabde74e..4859b88bf1 100644 --- a/shesha-reactjs/src/designer-components/collapsiblePanel/collapsiblePanelComponent.tsx +++ b/shesha-reactjs/src/designer-components/collapsiblePanel/collapsiblePanelComponent.tsx @@ -1,28 +1,22 @@ import ComponentsContainer from '@/components/formDesigner/containers/componentsContainer'; -import { CollapsiblePanel, headerType } from '@/components/panel'; +import { CollapsiblePanel } from '@/components/panel'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { IToolboxComponent } from '@/interfaces'; -import { useFormData, useGlobalState } from '@/providers'; +import { useFormData } from '@/providers'; import { useForm } from '@/providers/form'; -import { FormMarkup } from '@/providers/form/models'; -import { evaluateString, pickStyleFromModel, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { evaluateString, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { GroupOutlined } from '@ant-design/icons'; -import { ExpandIconPosition } from 'antd/lib/collapse/Collapse'; import { nanoid } from '@/utils/uuid'; -import React, { createContext, useContext } from 'react'; +import React, { useMemo } from 'react'; import { ICollapsiblePanelComponentProps, ICollapsiblePanelComponentPropsV0 } from './interfaces'; -import settingsFormJson from './settingsForm.json'; -import { executeFunction } from '@/utils'; import ParentProvider from '@/providers/parentProvider/index'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; import { removeComponents } from '../_common-migrations/removeComponents'; - -const settingsForm = settingsFormJson as FormMarkup; - -type PanelContextType = 'parent' | 'child' | undefined; - -const PanelContext = createContext(undefined); +import { getSettings } from './settingsForm'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultHeaderStyles, defaultStyles } from './utils'; +import { useFormComponentStyles } from '@/hooks/formComponentHooks'; const CollapsiblePanelComponent: IToolboxComponent = { type: 'collapsiblePanel', @@ -30,106 +24,80 @@ const CollapsiblePanelComponent: IToolboxComponent, Factory: ({ model }) => { - const { formMode, formSettings } = useForm(); + const { formMode } = useForm(); const { data } = useFormData(); - const { globalState } = useGlobalState(); - const isFormSettings = formSettings?.isSettingsForm; const { label, expandIconPosition, collapsedByDefault, collapsible, + isSimpleDesign, ghost, bodyColor, - headerColor, - isSimpleDesign, hideCollapseContent, hideWhenEmpty, + hasCustomHeader, + isDynamic, + customHeader, + content, + className, + hidden, } = model; - const panelContextState = useContext(PanelContext); - const evaluatedLabel = typeof label === 'string' ? evaluateString(label, data) : label; - - if (model.hidden) return null; - - const styling = JSON.parse(model.stylingBox || '{}'); - - const getPanelStyle = { - ...pickStyleFromModel(styling), - ...(executeFunction(model?.style, { data, globalState }) || {}), - }; + const evaluatedLabel = useMemo(() => ( + typeof label === 'string' ? evaluateString(label, data) : label + ), [label, data]); const headerComponents = model?.header?.components ?? []; - const hasCustomHeader = model?.hasCustomHeader; - - const extra = - ((headerComponents?.length > 0 || formMode === 'designer') && !hasCustomHeader) ? ( - - ) : null; + const headerStyles = useFormComponentStyles({ ...{ ...model.headerStyles, border: ghost ? null : model.headerStyles.border } }).fullStyle; - const panelPosition = !!panelContextState ? 'child' : 'parent'; + const isIconHidden = expandIconPosition === 'hide'; + const extra = ((headerComponents?.length > 0 || formMode === 'designer') && !hasCustomHeader) ? ( + + ) : null; - const headType: headerType = (() => { - if (isFormSettings) { - return 'default'; - } else { - if (panelPosition === 'parent') { - return 'parent'; - } else { - return 'child'; - }; - }; - })(); - - return ( + return hidden ? null : ( - - : - evaluatedLabel - } - expandIconPosition={expandIconPosition !== 'hide' ? (expandIconPosition as ExpandIconPosition) : 'start'} - collapsedByDefault={collapsedByDefault} - extra={extra} - collapsible={collapsible === 'header' ? 'header' : 'icon'} - showArrow={collapsible !== 'disabled' && expandIconPosition !== 'hide'} - ghost={ghost} - dynamicBorderRadius={model?.borderRadius} - style={{ ...getPanelStyle }} - className={model.className} - bodyColor={bodyColor} - headerColor={headerColor} - isSimpleDesign={isSimpleDesign} - panelHeadType={headType} - hideCollapseContent={hideCollapseContent} - hideWhenEmpty={hideWhenEmpty} - > + - - + ) : evaluatedLabel} + expandIconPosition={isIconHidden ? undefined : expandIconPosition} + showArrow={collapsible !== 'disabled' && !isIconHidden} + collapsedByDefault={collapsedByDefault} + extra={extra} + collapsible={collapsible === 'header' ? 'header' : 'icon'} + ghost={ghost} + bodyStyle={model.allStyles.fullStyle} + headerStyle={headerStyles} + className={className} + bodyColor={bodyColor} + isSimpleDesign={isSimpleDesign} + hideCollapseContent={hideCollapseContent} + hideWhenEmpty={hideWhenEmpty} + accentStyle={model?.accentStyle} + overflowStyle={model.allStyles.overflowStyles} + > + + ); }, - initModel: (model) => ({ - ...model, - stylingBox: "{\"marginBottom\":\"5\"}" - }), - settingsFormMarkup: settingsForm, - validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), + settingsFormMarkup: () => getSettings(), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(), model), migrator: (m) => m .add(0, (prev) => { @@ -157,6 +125,7 @@ const CollapsiblePanelComponent: IToolboxComponent(2, (prev) => migratePropertyName(migrateCustomFunctions(prev))) @@ -176,8 +145,25 @@ const CollapsiblePanelComponent: IToolboxComponent(8, (prev) => { + const accentStyle = prev?.overflow === undefined; + + return { + ...prev, accentStyle, desktop: { ...prev.desktop, accentStyle }, + tablet: { ...prev.tablet, accentStyle }, + mobile: { ...prev.mobile, accentStyle } + }; + }) + .add(9, (prev) => { + const newModel = migratePrevStyles(prev, defaultStyles(prev)); + const defaultHeaderStyle = { ...defaultHeaderStyles(prev) }; - , + return { + ...newModel, desktop: { ...newModel.desktop, overflow: prev.overflow ?? 'auto', headerStyles: defaultHeaderStyle }, + tablet: { ...newModel.tablet, overflow: prev.overflow || 'auto', headerStyles: defaultHeaderStyle }, + mobile: { ...newModel.mobile, overflow: prev.overflow || 'auto', headerStyles: defaultHeaderStyle } + }; + }), customContainerNames: ['header', 'content', 'customHeader'], }; diff --git a/shesha-reactjs/src/designer-components/collapsiblePanel/interfaces.ts b/shesha-reactjs/src/designer-components/collapsiblePanel/interfaces.ts index 558a24df4c..77cff7d820 100644 --- a/shesha-reactjs/src/designer-components/collapsiblePanel/interfaces.ts +++ b/shesha-reactjs/src/designer-components/collapsiblePanel/interfaces.ts @@ -1,5 +1,5 @@ import { headerType } from './../../components/panel/index'; -import { IConfigurableFormComponent } from '@/providers/form/models'; +import { IConfigurableFormComponent, IStyleType } from '@/providers/form/models'; import { ExpandIconPosition } from 'antd/lib/collapse/Collapse'; import { CollapsibleType } from 'antd/lib/collapse/CollapsePanel'; @@ -8,13 +8,14 @@ export interface ICollapsiblePanelContent { components?: IConfigurableFormComponent[]; } -export interface ICollapsiblePanelComponentProps extends IConfigurableFormComponent { +export interface ICollapsiblePanelComponentProps extends IConfigurableFormComponent, IStyleType { collapsedByDefault?: boolean; expandIconPosition?: ExpandIconPosition | 'hide'; header?: ICollapsiblePanelContent; content?: ICollapsiblePanelContent; collapsible?: CollapsibleType; ghost?: boolean; + accentStyle?: boolean; hideWhenEmpty?: boolean; className?: string; marginBottom?: string; @@ -24,10 +25,15 @@ export interface ICollapsiblePanelComponentProps extends IConfigurableFormCompon isSimpleDesign?: boolean; hideCollapseContent?: boolean; borderRadius?: number; + noMargin?: boolean; hasCustomHeader?: boolean; customHeader?: ICollapsiblePanelContent; panelHeadType?: headerType; -} + headerStyles?: IStyleType; + desktop?: any; + mobile?: any; + tablet?: any; +}; export interface ICollapsiblePanelComponentPropsV0 extends IConfigurableFormComponent { collapsedByDefault?: boolean; diff --git a/shesha-reactjs/src/designer-components/collapsiblePanel/settingsForm.json b/shesha-reactjs/src/designer-components/collapsiblePanel/settingsForm.json deleted file mode 100644 index 75cc6a97ca..0000000000 --- a/shesha-reactjs/src/designer-components/collapsiblePanel/settingsForm.json +++ /dev/null @@ -1,385 +0,0 @@ -{ - "components": [ - { - "id": "bbbb4bf6-f76d-4139-a850-c99bf06c8b69", - "type": "collapsiblePanel", - "propertyName": "pnlDisplay", - "label": "Display", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "collapsible": "header", - "ghost": true, - "header": { - "id": "FizvoIoyeq1cZMqAgbDLL", - "components": [] - }, - "content": { - "id": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "components": [ - { - "id": "5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4", - "type": "textField", - "propertyName": "componentName", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "label": "Component name", - "validate": { - "required": true - }, - "version": 2, - "textType": "text", - "jsSetting": false - }, - { - "id": "46d07439-4c18-468c-89e1-60c002ce96c5", - "type": "textField", - "propertyName": "label", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "label": "Label", - "version": 2, - "textType": "text" - }, - { - "id": "cfd7d45e-c7e3-4a27-987b-dc525c412558", - "type": "checkbox", - "propertyName": "hasCustomHeader", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "label": "Custom Header", - "version": 1 - }, - { - "id": "57a40a33-7e08-4ce4-9f08-a34d24a83338", - "type": "dropdown", - "propertyName": "expandIconPosition", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "label": "Icon position", - "values": [ - { - "label": "Hide", - "value": "hide", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - "label": "Start", - "value": "start", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - "label": "End", - "value": "end", - "id": "b920ef96-ae27-4a01-bfad-b5b7d07218da" - } - ], - "dataSourceType": "values", - "validate": {}, - "version": 3, - "referenceListId": null - }, - { - "id": "-wshDLo-lK468vwxVVBDMh", - "type": "dropdown", - "propertyName": "collapsible", - "label": "Collapsible", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "values": [ - { - "label": "Header", - "value": "header", - "id": "111e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - "label": "Icon", - "value": "icon", - "id": "2220ef96-ae27-4a01-bfad-b5b7d07218da" - }, - { - "label": "Disabled", - "value": "disabled", - "id": "3330ef96-ae27-4a01-bfad-b5b7d07218da" - } - ], - "dataSourceType": "values", - "validate": {}, - "version": 3, - "referenceListId": null - }, - { - "id": "-wYzLo-lK468vwxVVBDMh", - "type": "checkbox", - "propertyName": "collapsedByDefault", - "label": "Collapsed by default", - "labelAlign": "right", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "hidden": false, - "isDynamic": false, - "description": "", - "validate": {}, - "settingsValidationErrors": [], - "version": 1 - }, - { - "id": "cfd7d45e-c7e3-4a27-987b-dc525c412448", - "type": "checkbox", - "propertyName": "hidden", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "label": "Hidden", - "version": 1 - }, - { - "id": "41924074-296C-4E4D-820A-DC29EC2C7B92", - "type": "checkbox", - "propertyName": "ghost", - "label": "Ghost", - "labelAlign": "right", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "hidden": false, - "isDynamic": false, - "description": "Make the collapse borderless and its background transparent", - "validate": {}, - "settingsValidationErrors": [], - "version": 1 - }, - { - "id": "7e5fc1c1-a804-4f0a-8327-1a92e963e5e1", - "type": "checkbox", - "propertyName": "hideCollapseContent", - "label": "Hide Top Bar", - "labelAlign": "right", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "hidden": false, - "isDynamic": false, - "description": "Hides the collapsible panel", - "validate": {}, - "settingsValidationErrors": [], - "version": 1 - }, - { - "id": "cd7d29fe-63d4-4c43-96c3-d4456f9ef237", - "type": "checkbox", - "propertyName": "isSimpleDesign", - "label": "Simple Design", - "labelAlign": "right", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "hidden": false, - "isDynamic": false, - "description": "Make the collapse borderless, background transparent and simple design", - "validate": {}, - "settingsValidationErrors": [], - "version": 1 - }, - { - "id": "6bac1a1f-67a2-48b5-a11e-b474e30d282e", - "type": "numberField", - "propertyName": "borderRadius", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "label": "Border Radius", - "defaultValue": 8, - "version": 2, - "textType": "number", - "max": 40, - "jsSetting": false - }, - { - "id": "d95693e6-f8dc-4f77-9ec4-68a14612cf34", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "type": "colorPicker", - "propertyName": "headerColor", - "componentName": "headerColor", - "label": "Header Background Color", - "labelAlign": "right", - "hidden": false, - "isDynamic": false, - "version": 2, - "settingsValidationErrors": [], - "title": "", - "fontSize": "", - "allowClear": true, - "showText": true - }, - { - "id": "09f31142-4053-45e6-ab3d-e9b063acbfa7", - "parentId": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "type": "colorPicker", - "propertyName": "bodyColor", - "componentName": "bodyColor", - "label": "Body Background Color", - "labelAlign": "right", - "hidden": false, - "isDynamic": false, - "version": 2, - "settingsValidationErrors": [], - "title": "", - "fontSize": "", - "allowClear": true, - "showText": true - } - ] - } - }, - { - "id": "038adb3d-6fae-450b-afb5-0855e6a30d06", - "type": "collapsiblePanel", - "propertyName": "pnlStyle", - "label": "Style", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "collapsible": "header", - "ghost": true, - "header": { - "id": "FizvoIoyeq1cZMqAgbDLL", - "components": [] - }, - "content": { - "id": "f47668c1-e424-4d18-850e-6d712e48211d", - "components": [ - { - "textType": "text", - "id": "b1M4TO4K1t3pl8pyM2mOK", - "type": "textField", - "propertyName": "className", - "componentName": "className", - "label": "Custom CSS Class", - "labelAlign": "right", - "parentId": "f47668c1-e424-4d18-850e-6d712e48211d", - "hidden": false, - "isDynamic": false, - "version": 2, - "description": "Custom CSS Class to add to this component", - "validate": {}, - "settingsValidationErrors": [] - }, - { - "id": "e029c4ed-6415-412f-be9e-73dddff4037c", - "type": "codeEditor", - "propertyName": "style", - "label": "Style", - "labelAlign": "right", - "parentId": "f47668c1-e424-4d18-850e-6d712e48211d", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "exposedVariables": [ - { - "id": "6034ec34-2b5d-4320-90cd-6ed6a61a0481", - "name": "data", - "description": "Selected form values", - "type": "object" - }, - { - "id": "4008f09b-c4dc-41cd-981c-4b0101d005c3", - "name": "globalState", - "description": "The global state of the application", - "type": "object" - } - ], - "version": 1 - }, - { - "id": "8473d32d-ef70-4310-abef-28c887b940b0", - "type": "styleBox", - "propertyName": "stylingBox", - "parentId": "f47668c1-e424-4d18-850e-6d712e48211d", - "validate": {}, - "settingsValidationErrors": [], - "jsSetting": false - } - ] - } - }, - { - "id": "bbbb960e-77e3-40f2-89cc-f18f94678cce", - "type": "collapsiblePanel", - "propertyName": "pnlVisibility", - "label": "Visibility", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "collapsible": "header", - "ghost": true, - "header": { - "id": "FizvoIoyeq1cZMqAgbDLL", - "components": [] - }, - "content": { - "id": "bc67960e-77e3-40f2-89cc-f18f94678cce", - "components": [ - { - "id": "BC7507ED-ADB6-4D2E-BD37-F5DD51EFF45D", - "type": "checkbox", - "propertyName": "hideWhenEmpty", - "label": "Hide when empty", - "labelAlign": "right", - "parentId": "bc67960e-77e3-40f2-89cc-f18f94678cce", - "hidden": false, - "isDynamic": false, - "description": "Allows to hide the panel when all components are hidden due to some conditions", - "validate": {}, - "settingsValidationErrors": [], - "version": 1 - } - ] - } - }, - { - "id": "6Vw9iiDw9d0MD_Rh5cbIn", - "type": "collapsiblePanel", - "propertyName": "pnlSecurity", - "label": "Security", - "labelAlign": "right", - "parentId": "root", - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "mbzE03mJnv_89oUvkmjPs", - "components": [] - }, - "content": { - "id": "SsWPpiJfTbgl4iHlFA_or", - "components": [ - { - "id": "1adea529-1f0c-4def-bd41-ee166a5dfcd7", - "type": "permissionAutocomplete", - "propertyName": "permissions", - "parentId": "SsWPpiJfTbgl4iHlFA_or", - "label": "Permissions", - "validate": {}, - "version": 1 - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - } - ], - "formSettings": { - "layout": "horizontal", - "colon": true, - "labelCol": { - "span": 5 - }, - "wrapperCol": { - "span": 13 - }, - "displayName": "DEFAULT_FORM_SETTINGS", - "__docgenInfo": { - "description": "Default form settings", - "displayName": "DEFAULT_FORM_SETTINGS", - "props": {} - } - } -} \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/collapsiblePanel/settingsForm.ts b/shesha-reactjs/src/designer-components/collapsiblePanel/settingsForm.ts new file mode 100644 index 0000000000..57adedb4a3 --- /dev/null +++ b/shesha-reactjs/src/designer-components/collapsiblePanel/settingsForm.ts @@ -0,0 +1,1021 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = () => { + // Generate unique IDs for main sections + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + components: new DesignerToolbarSettings() + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + id: commonTabId, + key: '1', + title: 'Common', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + inputType: 'textField', + id: nanoid(), + propertyName: 'componentName', + parentId: commonTabId, + label: 'Component Name', + validate: { + required: true + }, + jsSetting: false + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'label', + parentId: commonTabId, + label: 'Label', + jsSetting: true + }, + { + id: nanoid(), + propertyName: 'hasCustomHeader', + parentId: commonTabId, + label: 'Custom Header', + type: 'switch', + jsSetting: true + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'expandIconPosition', + parentId: commonTabId, + label: 'Icon Position', + type: 'dropdown', + jsSetting: true, + dropdownOptions: [ + { + label: 'Hide', + value: 'hide', + }, + { + label: 'Start', + value: 'start', + }, + { + label: 'End', + value: 'end', + } + ], + validate: {}, + }, + { + id: nanoid(), + propertyName: 'collapsible', + label: 'Collapsible', + type: 'dropdown', + jsSetting: true, + parentId: commonTabId, + dropdownOptions: [ + { + label: 'Header', + value: 'header', + }, + { + label: 'Icon', + value: 'icon', + }, + { + label: 'Disabled', + value: 'disabled', + } + ], + validate: {}, + version: 3 + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + propertyName: 'collapsedByDefault', + labelAlign: 'right', + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'collapsedByDefault', + label: 'Collapsed By Default', + labelAlign: 'right', + type: 'switch', + parentId: commonTabId, + hidden: false, + isDynamic: false, + description: '', + jsSetting: true, + validate: {}, + }, + { + id: nanoid(), + propertyName: 'hideWhenEmpty', + label: 'Hide When Empty', + labelAlign: 'right', + parentId: commonTabId, + type: 'switch', + jsSetting: true, + description: 'Allows to hide the panel when all components are hidden due to some conditions', + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'hidden', + parentId: commonTabId, + label: 'Hide', + type: 'switch', + jsSetting: true + } + ] + }) + .toJson()] + }, + { + key: '2', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + propertyName: 'ghost', + parentId: styleRouterId, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + label: 'Ghost', + propertyName: 'ghost', + type: 'switch', + parentId: styleRouterId, + jsSetting: true + }, + { + id: nanoid(), + propertyName: 'isSimpleDesign', + parentId: styleRouterId, + label: 'Simple Design', + type: 'switch', + jsSetting: true + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'accentStyle', + label: 'Accent', + labelAlign: 'right', + parentId: styleRouterId, + type: 'switch', + jsSetting: true, + description: 'Applies accent styling to panel borders using the primary color', + }, + { + id: nanoid(), + propertyName: 'hideCollapseContent', + label: 'Hide Top Bar', + labelAlign: 'right', + type: 'switch', + parentId: styleRouterId, + description: 'Hides the collapsible panel', + jsSetting: true + } + + ] + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.ghost) || getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.isSimpleDesign);', _mode: 'code', _value: false } as any, + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.ghost) || getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.isSimpleDesign);', _mode: 'code', _value: false } as any, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: styleRouterId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "background.size", + dropdownOptions: [ + { + value: "cover", + label: "Cover" + }, + { + value: "contain", + label: "Contain" + }, + { + value: "auto", + label: "Auto" + } + ], + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + propertyName: "background.position", + dropdownOptions: [ + { + value: "center", + label: "Center" + }, + { + value: "top", + label: "Top" + }, + { + value: "left", + label: "Left" + }, + { + value: "right", + label: "Right" + }, + { + value: "bottom", + label: "Bottom" + }, + { + value: "top left", + label: "Top Left" + }, + { + value: "top right", + label: "Top Right" + }, + { + value: "bottom left", + label: "Bottom Left" + }, + { + value: "bottom right", + label: "Bottom Right" + } + ], + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + hidden: { _code: 'return getSettingValue(data?.ghost) || getSettingValue(data?.isSimpleDesign);', _mode: 'code', _value: false } as any, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'header', + label: 'Header Style', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + collapsedByDefault: true, + parentId: styleRouterId, + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + collapsedByDefault: true, + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Font', + hideLabel: true, + propertyName: 'headerStyles.font.type', + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'headerStyles.font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'headerStyles.font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'headerStyles.font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'headerStyles.font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson()] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensionsStyle', + label: 'Dimensions', + labelAlign: 'right', + collapsedByDefault: true, + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "headerStyles.dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "headerStyles.dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "headerStyles.dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson()] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + collapsedByDefault: true, + ghost: true, + parentId: styleRouterId, + hidden: { + _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.ghost)' + + ' || getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.accentStyle)' + + ' || getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.isSimpleDesign);', + _mode: 'code', _value: false + } as any, + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getBorderInputs('headerStyles', true) as any + }) + .toJson()] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + collapsedByDefault: true, + ghost: true, + hidden: { _code: 'return getSettingValue(data?.ghost) || getSettingValue(data?.isSimpleDesign);', _mode: 'code', _value: false } as any, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: styleRouterId, + label: "Type", + jsSetting: false, + propertyName: "headerStyles.background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: [ + { + value: "color", + icon: "FormatPainterOutlined", + title: "Color" + }, + { + value: "gradient", + icon: "BgColorsOutlined", + title: "Gradient" + }, + { + value: "image", + icon: "PictureOutlined", + title: "Image" + }, + { + value: "url", + icon: "LinkOutlined", + title: "URL" + }, + { + value: "storedFile", + icon: "DatabaseOutlined", + title: "Stored File" + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "headerStyles.background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "headerStyles.background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "headerStyles.background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'headerStyles.background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "headerStyles.background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "headerStyles.background.size", + dropdownOptions: sizeOptions + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + propertyName: "headerStyles.background.position", + dropdownOptions: positionOptions, + }, + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'headerStyles.background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin and Padding', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'headerStyles.stylingBox', + }) + .toJson()] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + collapsedByDefault: true, + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'headerStyles.style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson()] + } + }) + .toJson()] + } + }) + .toJson()] + }).toJson()] + }, + { + key: '3', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/collapsiblePanel/utils.ts b/shesha-reactjs/src/designer-components/collapsiblePanel/utils.ts new file mode 100644 index 0000000000..cd1a035b2f --- /dev/null +++ b/shesha-reactjs/src/designer-components/collapsiblePanel/utils.ts @@ -0,0 +1,55 @@ +import { IStyleType } from "@/index"; +import { ICollapsiblePanelComponentProps } from "./interfaces"; + +export const defaultStyles = (prev: ICollapsiblePanelComponentProps): IStyleType => { + const bodyColor = prev.bodyColor || '#fff'; + const { ghost, borderRadius } = prev; + + return { + background: { type: 'color', color: bodyColor }, + dimensions: { width: 'auto', height: 'auto', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' }, + border: { + radiusType: 'all', borderType: 'custom', + border: { + ...ghost ? { all: { width: '1px', color: '#d9d9d9', style: 'solid' } } + : { + all: { width: '1px', color: '#d9d9d9', style: 'solid' }, + top: { width: '1px', color: '#d9d9d9', style: 'none' }, + bottom: { width: '1px', color: '#d9d9d9', style: 'solid' }, + left: { width: '1px', color: '#d9d9d9', style: 'solid' }, + right: { width: '1px', color: '#d9d9d9', style: 'solid' }, + }, + }, + radius: { all: borderRadius || 8 } + }, + shadow: { blurRadius: 0, color: 'rgba(0, 0, 0, 0.15)', offsetX: 0, offsetY: 0, spreadRadius: 0 }, + stylingBox: '{"marginBottom":"5","paddingLeft":"8","paddingBottom":"8","paddingTop":"8","paddingRight":"8"}' + }; +}; + +export const defaultHeaderStyles = (prev: ICollapsiblePanelComponentProps): IStyleType => { + const headerBgColor = prev?.headerColor || '#fff'; + const { isSimpleDesign, borderRadius } = prev; + + return { + font: { color: '#000', size: 14, weight: isSimpleDesign ? '400' : '500', align: 'left', type: 'Segoe UI' }, + background: { + type: 'color', + color: headerBgColor, + repeat: 'no-repeat', + position: 'center', + size: 'cover', + }, + dimensions: { width: 'auto', height: 'auto', minHeight: '0', maxHeight: 'auto', minWidth: '0', maxWidth: 'auto' }, + border: { + radiusType: 'all', + borderType: 'all', + border: { + all: { width: '1px', color: '#d9d9d9', style: 'solid' }, + }, + radius: { all: borderRadius || 8 } + }, + stylingBox: '{"paddingLeft":"8","paddingBottom":"8","paddingTop":"8","paddingRight":"8"}' + }; +}; + diff --git a/shesha-reactjs/src/designer-components/colorPicker/index.tsx b/shesha-reactjs/src/designer-components/colorPicker/index.tsx index a07dee127e..1383c23c9c 100644 --- a/shesha-reactjs/src/designer-components/colorPicker/index.tsx +++ b/shesha-reactjs/src/designer-components/colorPicker/index.tsx @@ -2,46 +2,52 @@ import ConfigurableFormItem from '@/components/formDesigner/components/formItem' import React from 'react'; import { FormatPainterOutlined } from '@ant-design/icons'; import { IColorPickerComponentProps } from './interfaces'; -import { iconPickerFormSettings } from './settings'; +import { getSettings } from './settingsForm'; import { IToolboxComponent } from '@/interfaces'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { validateConfigurableComponentSettings } from '@/providers/form/utils'; import { ColorPicker } from '@/components'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { IEventHandlers, getAllEventHandlers } from '@/components/formDesigner/components/utils'; -const ColorPickerComponent: IToolboxComponent = { +interface IColorPickerComopnentCalulatedValues { + eventHandlers: IEventHandlers; +} + +const ColorPickerComponent: IToolboxComponent = { type: 'colorPicker', name: 'Color Picker', canBeJsSetting: true, isInput: true, isOutput: true, icon: , - Factory: ({ model }) => { + calculateModel: (model, allData) => ({ + eventHandlers: getAllEventHandlers(model, allData), + }), + Factory: ({ model, calculatedModel }) => { return ( - {(value, onChange) => ( - - )} + {(value, onChange) => { + const customEvents = calculatedModel.eventHandlers; + const onChangeInternal = (colorValue) => { + customEvents.onChange({ value: colorValue }, null); + if (typeof onChange === 'function') onChange(colorValue); + }; + + return ; + }} ); }, - settingsFormMarkup: iconPickerFormSettings, + settingsFormMarkup: (data) => getSettings(data), migrator: (m) => m .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) .add(1, (prev) => migrateVisibility(prev)) .add(2, (prev) => ({ ...prev, allowClear: false, showText: false })) .add(3, (prev) => ({...migrateFormApi.properties(prev)})) , - validateSettings: model => validateConfigurableComponentSettings(iconPickerFormSettings, model), + validateSettings: model => validateConfigurableComponentSettings(getSettings, model), }; -export default ColorPickerComponent; +export default ColorPickerComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/colorPicker/interfaces.ts b/shesha-reactjs/src/designer-components/colorPicker/interfaces.ts index 222d5aa2cc..149277914b 100644 --- a/shesha-reactjs/src/designer-components/colorPicker/interfaces.ts +++ b/shesha-reactjs/src/designer-components/colorPicker/interfaces.ts @@ -5,4 +5,6 @@ export interface IColorPickerComponentProps extends IConfigurableFormComponent { allowClear?: boolean; showText?: boolean; disabledAlpha?: boolean; + tooltip?: string; + stylingBox?: string; } \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/colorPicker/settings.ts b/shesha-reactjs/src/designer-components/colorPicker/settings.ts index ab5fd0e866..ec6612c3dd 100644 --- a/shesha-reactjs/src/designer-components/colorPicker/settings.ts +++ b/shesha-reactjs/src/designer-components/colorPicker/settings.ts @@ -11,9 +11,9 @@ export const iconPickerFormSettings = new DesignerToolbarSettings() ghost: true, collapsible: 'header', content: { - id:'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addContextPropertyAutocomplete({ + id: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', propertyName: 'propertyName', parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', @@ -57,7 +57,7 @@ export const iconPickerFormSettings = new DesignerToolbarSettings() id: 'cfd7d45e-c7e3-4a27-987b-dc525c412448', propertyName: 'hidden', parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Hidden', + label: 'hide', }) .addCheckbox({ id: 'c6885251-96a6-40ce-99b2-4b5209a9e01c', @@ -103,8 +103,8 @@ export const iconPickerFormSettings = new DesignerToolbarSettings() ghost: true, collapsible: 'header', content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() + id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', + components: [...new DesignerToolbarSettings() .addPermissionAutocomplete({ id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', propertyName: 'permissions', @@ -115,6 +115,6 @@ export const iconPickerFormSettings = new DesignerToolbarSettings() validate: {}, }).toJson() ] -} -}) + } + }) .toJson(); diff --git a/shesha-reactjs/src/designer-components/colorPicker/settingsForm.ts b/shesha-reactjs/src/designer-components/colorPicker/settingsForm.ts new file mode 100644 index 0000000000..b5fa2ecb9d --- /dev/null +++ b/shesha-reactjs/src/designer-components/colorPicker/settingsForm.ts @@ -0,0 +1,289 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/lib/form/Form'; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + //common + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + parentId: commonTabId, + label: 'Property Name', + size: 'small', + validate: { + required: true + }, + styledLabel: true, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textArea', + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + defaultValue: 'inherited', + }, + { + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + parentId: styleRouterId, + type: 'switch', + jsSetting: true, + } + ] + }) + .toJson() + ] + }, + //data + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'textField', + propertyName: 'title', + label: 'Title', + parentId: dataTabId, + jsSetting: true, + description: 'Title shown at the top of the color picker', + placeholder: 'Select color', + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'allowClear', + label: 'Allow Clear', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'showText', + label: 'Show Color Code', + tooltip: 'Shows the color value (hex/rgb) alongside the color preview', + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'disabledAlpha', + label: 'Disable Opacity', + type: 'switch', + tooltip: 'Removes the opacity/transparency option from the color picker', + jsSetting: true, + } + ] + }) + .toJson() + ] + }, + //events + { + key: 'events', + title: 'Events', + id: eventsTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onChangeCustom', + label: 'On Change', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on changing of event.', + parentId: eventsTabId + }) + .toJson() + ] + }, + //appearance + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'size', + label: 'Size', + parentId: styleRouterId, + jsSetting: true, + defaultValue: 'middle', + dropdownOptions: [ + { label: 'Small', value: 'small' }, + { label: 'Middle', value: 'middle' }, + { label: 'Large', value: 'large' } + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'marginPadding', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin & Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyles', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + parentId: styleRouterId, + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson() + ] + }) + .toJson() + ] + }, + //security + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + tooltip: 'Enter a list of permissions that should be associated with this component' + }) + .toJson() + ] + }, + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/columns/columns.tsx b/shesha-reactjs/src/designer-components/columns/columns.tsx index e7fd87af24..3e6b857bb8 100644 --- a/shesha-reactjs/src/designer-components/columns/columns.tsx +++ b/shesha-reactjs/src/designer-components/columns/columns.tsx @@ -1,17 +1,26 @@ -import React from 'react'; -import { IToolboxComponent } from '@/interfaces'; -import { SplitCellsOutlined } from '@ant-design/icons'; -import { Row, Col } from 'antd'; -import { IFormComponentContainer, useFormData, useGlobalState } from '@/providers'; -import { nanoid } from '@/utils/uuid'; -import { IColumnsComponentProps } from './interfaces'; -import { getLayoutStyle } from '@/providers/form/utils'; +import { ValidationErrors } from '@/components'; +import { isValidGuid } from '@/components/formDesigner/components/utils'; +import ComponentsContainer from '@/components/formDesigner/containers/componentsContainer'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; -import { ColumnsSettingsForm } from './columnsSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; +import { IToolboxComponent } from '@/interfaces'; +import { IFormComponentContainer, useFormData, useGlobalState, useSheshaApplication } from '@/providers'; +import { getLayoutStyle, getStyle, pickStyleFromModel } from '@/providers/form/utils'; import ParentProvider from '@/providers/parentProvider/index'; -import ComponentsContainer from '@/components/formDesigner/containers/componentsContainer'; +import { removeUndefinedProps } from '@/utils/object'; +import { SplitCellsOutlined } from '@ant-design/icons'; +import { Col, Row } from 'antd'; +import React, { CSSProperties, useEffect, useMemo, useState } from 'react'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; import { removeComponents } from '../_common-migrations/removeComponents'; +import { getBackgroundStyle } from '../_settings/utils/background/utils'; +import { getBorderStyle } from '../_settings/utils/border/utils'; +import { getDimensionsStyle } from '../_settings/utils/dimensions/utils'; +import { getShadowStyle } from '../_settings/utils/shadow/utils'; +import { IColumnsComponentProps, IColumnsInputProps } from './interfaces'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; +import { nanoid } from '@/utils/uuid'; const ColumnsComponent: IToolboxComponent = { type: 'columns', @@ -23,39 +32,80 @@ const ColumnsComponent: IToolboxComponent = { const { globalState } = useGlobalState(); const { columns, gutterX = 0, gutterY = 0 } = model as IColumnsComponentProps; + const { backendUrl, httpHeaders } = useSheshaApplication(); + + const dimensions = model?.dimensions; + const border = model?.border; + const shadow = model?.shadow; + const background = model?.background; + const jsStyle = getStyle(model.style, data); + + const dimensionsStyles = useMemo(() => getDimensionsStyle(dimensions), [dimensions]); + const borderStyles = useMemo(() => getBorderStyle(border, jsStyle), [border]); + const [backgroundStyles, setBackgroundStyles] = useState({}); + const shadowStyles = useMemo(() => getShadowStyle(shadow), [shadow]); + + useEffect(() => { + const fetchStyles = async () => { + const storedImageUrl = background?.storedFile?.id && background?.type === 'storedFile' + ? await fetch(`${backendUrl}/api/StoredFile/Download?id=${background?.storedFile?.id}`, + { headers: { ...httpHeaders, "Content-Type": "application/octet-stream" } }) + .then((response) => { + return response.blob(); + }) + .then((blob) => { + return URL.createObjectURL(blob); + }) : ''; + + const style = await getBackgroundStyle(background, jsStyle, storedImageUrl); + setBackgroundStyles(style); + }; + + fetchStyles(); + }, [background, background?.gradient?.colors, backendUrl, httpHeaders]); + if (model.hidden) return null; + if (model?.background?.type === 'storedFile' && model?.background.storedFile?.id && !isValidGuid(model?.background.storedFile.id)) { + return ; + } + const styling = JSON.parse(model.stylingBox || '{}'); + const stylingBoxAsCSS = pickStyleFromModel(styling); + const additionalStyles: CSSProperties = removeUndefinedProps({ + ...stylingBoxAsCSS, + ...dimensionsStyles, + ...borderStyles, + ...backgroundStyles, + ...shadowStyles + }); + + const finalStyle = removeUndefinedProps({ ...additionalStyles, fontWeight: Number(model?.font?.weight?.split(' - ')[0]) || 400 }); + return ( - - - {columns && - columns.map((col, index) => ( - - - - ))} - - +
+ + + {columns && + columns.map((col, index) => ( + + + + ))} + + +
); }, - migrator: (m) => - m - .add( - 0, - (prev) => migratePropertyName(migrateCustomFunctions(prev)) as IColumnsComponentProps - ) - .add(1, (prev) => migrateVisibility(prev)) - .add(2, (prev) => removeComponents(prev)), initModel: (model) => { const tabsModel: IColumnsComponentProps = { ...model, @@ -71,11 +121,52 @@ const ColumnsComponent: IToolboxComponent = { return tabsModel; }, - settingsFormFactory: (props) => , + migrator: (m) => + m + .add( + 0, + (prev) => migratePropertyName(migrateCustomFunctions(prev)) as IColumnsComponentProps + ) + .add(1, (prev) => migrateVisibility(prev)) + .add(2, (prev) => removeComponents(prev)) + .add(3, (prev) => { + const columns = prev.columns.map(c => ({ + ...c, + components: c.components.map(c => ({ + ...c, + propertyName: c.propertyName || c.id + })) + })); + + return { + ...prev, + columns + }; + }) + .add(4, (prev) => { + const styles: IColumnsInputProps = { + width: prev.width, + height: prev.height, + hideBorder: prev.hideBorder, + borderSize: prev.borderSize, + borderColor: prev.borderColor, + backgroundColor: prev.backgroundColor, + stylingBox: prev.stylingBox, + borderRadius: prev.borderRadius, + border: { + radius: { + all: prev.borderRadius + } + } + }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }) + .add(5, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), + settingsFormMarkup: (data) => getSettings(data), customContainerNames: ['columns'], getContainers: (model) => { return model.columns.map((t) => ({ id: t.id })); }, }; -export default ColumnsComponent; +export default ColumnsComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/columns/columnsList.tsx b/shesha-reactjs/src/designer-components/columns/columnsList.tsx index 61dadc40cf..ad239886cf 100644 --- a/shesha-reactjs/src/designer-components/columns/columnsList.tsx +++ b/shesha-reactjs/src/designer-components/columns/columnsList.tsx @@ -200,7 +200,7 @@ export const ColumnsList: FC = ({ value, onChange, readOnly }) => { dataIndex: 'operations', render: (_, record) => columns.length >= 1 ? ( - handleDeleteTab(record.id)}> + handleDeleteTab(record.id)}> Delete ) : null, @@ -260,7 +260,7 @@ export const ColumnsList: FC = ({ value, onChange, readOnly }) => { return ( - + { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const appearanceId = nanoid(); + const securityId = nanoid(); + const styleRouterId = nanoid(); + const dimensionsStylePnlId = nanoid(); + const borderStylePnlId = nanoid(); + const backgroundStylePnlId = nanoid(); + + const propertyNameId = nanoid(); + const shadowStylePnlId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: propertyNameId, + propertyName: "componentName", + inputType: 'textField', + parentId: commonTabId, + label: "Component Name", + size: "small", + validate: { + "required": true + }, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + labelAlign: 'right', + parentId: appearanceId, + inputType: 'switch', + size: 'small', + jsSetting: true, + }) + .toJson() + ] + }, + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + + id: nanoid(), + inputType: 'columnsList', + propertyName: 'columns', + label: 'Columns', + labelAlign: 'right', + tooltip: 'Configure columns', + parentId: dataTabId, + size: 'small', + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: appearanceId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'gutterX', + label: 'Gutter X', + labelAlign: 'right', + jsSetting: true, + min: 0, + }, + { + type: 'numberField', + id: nanoid(), + propertyName: 'gutterY', + label: 'Gutter Y', + labelAlign: 'right', + jsSetting: true, + min: 0, + }, + ] + }) + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: 'dimensionsStyleCollapsiblePanel', + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: dimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: `width-${styleRouterId}`, + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: `minWidth-${styleRouterId}`, + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: `maxWidth-${styleRouterId}`, + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: `height-${dimensionsStylePnlId}`, + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: `minHeight-${dimensionsStylePnlId}`, + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: `maxHeight-${dimensionsStylePnlId}`, + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: borderStylePnlId, + components: [...new DesignerToolbarSettings() + + .addContainer({ + id: 'borderStyleRow', + parentId: 'borderStylePnl', + components: getBorderInputs() as any + }) + .addContainer({ + id: 'borderRadiusStyleRow', + parentId: 'borderStylePnl', + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: backgroundStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: `${backgroundStylePnlId}-type`, + parentId: backgroundStylePnlId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: [ + { + value: "color", + icon: "FormatPainterOutlined", + title: "Color" + }, + { + value: "gradient", + icon: "BgColorsOutlined", + title: "Gradient" + }, + { + value: "image", + icon: "PictureOutlined", + title: "Image" + }, + { + value: "url", + icon: "LinkOutlined", + title: "URL" + }, + { + value: "storedFile", + icon: "DatabaseOutlined", + title: "Stored File" + } + ], + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-color`, + parentId: backgroundStylePnlId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-gradient`, + parentId: backgroundStylePnlId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-url`, + parentId: backgroundStylePnlId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-image`, + parentId: backgroundStylePnlId, + inputs: [{ + type: 'imageUploader', + id: 'backgroundStyle-image', + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-storedFile`, + parentId: backgroundStylePnlId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-storedFile', + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-controls`, + parentId: backgroundStylePnlId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "background.size", + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + propertyName: "background.position", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-repeat', + parentId: 'backgroundStyleRow', + inputs: [{ + type: 'radio', + id: 'backgroundStyleRow-repeat-radio', + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: shadowStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: shadowStylePnlId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + tooltip: 'OffsetX. The larger the value, the bigger the shadow', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + description: 'OffsetY. The larger the value, the bigger the shadow', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + description: 'Blur. The larger the value, the bigger the blur', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + description: 'Spread. The larger the value, the bigger the spread', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'style', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson(), + ] + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityId, + permissions: data.permissions, + tooltip: "Enter a list of permissions that should be associated with this component" + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/columns/utils.tsx b/shesha-reactjs/src/designer-components/columns/utils.tsx new file mode 100644 index 0000000000..b7d26b8cec --- /dev/null +++ b/shesha-reactjs/src/designer-components/columns/utils.tsx @@ -0,0 +1,25 @@ +import { IColumnsInputProps } from "./interfaces"; + +export const defaultStyles = (): IColumnsInputProps => { + return { + background: { type: 'color', color: '', }, + dimensions: { width: 'auto', height: 'auto', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' }, + border: { + borderType: 'all', + radiusType: 'all', + border: { + all: { width: '0px', color: '#d3d3d3', style: 'solid' }, + bottom: { width: '0px', color: '#d3d3d3', style: 'solid' }, + left: { width: '0px', color: '#d3d3d3', style: 'solid' }, + right: { width: '0px', color: '#d3d3d3', style: 'solid' }, + top: { width: '0px', color: '#d3d3d3', style: 'solid' }, + }, + radius: { all: 0, bottomLeft: 0, bottomRight: 0, topLeft: 0, topRight: 0 }, + hideBorder: false, + }, + borderRadius: 0, + gutterX: 12, + gutterY: 12, + stylingBox: "{\"marginBottom\":\"5\"}" + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/configurableActionsConfigurator/actionArgumensEditor.tsx b/shesha-reactjs/src/designer-components/configurableActionsConfigurator/actionArgumensEditor.tsx index ed80bb18e1..4576325732 100644 --- a/shesha-reactjs/src/designer-components/configurableActionsConfigurator/actionArgumensEditor.tsx +++ b/shesha-reactjs/src/designer-components/configurableActionsConfigurator/actionArgumensEditor.tsx @@ -10,6 +10,7 @@ import { FormMarkup } from '@/providers/form/models'; import GenericArgumentsEditor from './genericArgumentsEditor'; import { IObjectMetadata } from '@/interfaces'; import { getActualActionArguments } from '@/providers/configurableActionsDispatcher'; +import { useStyles } from '../_settings/styles/styles'; export interface IActionArgumentsEditorProps { action: IConfigurableActionDescriptor; @@ -26,9 +27,9 @@ const getDefaultFactory = ( ): IConfigurableActionArgumentsFormFactory => { const { argumentsFormMarkup: markup } = action; return ({ model, onSave, onCancel, onValuesChange, exposedVariables, availableConstants }) => { - - const markupFactory = typeof markup === 'function' - ? (markup as FormMarkupFactory) + + const markupFactory = typeof markup === 'function' + ? (markup as FormMarkupFactory) : () => markup as FormMarkup; const cacheKey = typeof markup !== 'function' ? `${action.ownerUid}-${action.name}-args` @@ -57,6 +58,8 @@ export const ActionArgumentsEditor: FC = ({ exposedVariables, availableConstants, }) => { + const { styles } = useStyles(); + const argumentsEditor = useMemo(() => { const settingsFormFactory = action.argumentsFormFactory ? action.argumentsFormFactory @@ -97,9 +100,7 @@ export const ActionArgumentsEditor: FC = ({ Arguments, children: argumentsEditor }]} /> ); -}; - -export default ActionArgumentsEditor; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/configurableActionsConfigurator/actionSelect.tsx b/shesha-reactjs/src/designer-components/configurableActionsConfigurator/actionSelect.tsx index cb576f4eda..041d0f7b7a 100644 --- a/shesha-reactjs/src/designer-components/configurableActionsConfigurator/actionSelect.tsx +++ b/shesha-reactjs/src/designer-components/configurableActionsConfigurator/actionSelect.tsx @@ -74,6 +74,7 @@ export const ActionSelect: FC = ({ value, onChange, actions, maxHeight: 400, overflow: 'auto', }} + size='small' placeholder="Please select" allowClear //treeDefaultExpandAll diff --git a/shesha-reactjs/src/designer-components/configurableActionsConfigurator/configurator.tsx b/shesha-reactjs/src/designer-components/configurableActionsConfigurator/configurator.tsx index 934a97c6d9..ae4cd93108 100644 --- a/shesha-reactjs/src/designer-components/configurableActionsConfigurator/configurator.tsx +++ b/shesha-reactjs/src/designer-components/configurableActionsConfigurator/configurator.tsx @@ -1,14 +1,20 @@ import React, { FC, useMemo } from 'react'; -import { Collapse, Form, Switch } from 'antd'; +import { Collapse, Form } from 'antd'; import { useForm, useConfigurableActionDispatcher } from '@/providers'; import { IConfigurableActionConfiguration } from '@/interfaces/configurableAction'; -import ActionArgumentsEditor from './actionArgumensEditor'; +import { ActionArgumentsEditor } from './actionArgumensEditor'; import { IConfigurableActionConfiguratorComponentProps } from './interfaces'; import { ICodeExposedVariable } from '@/components/codeVariablesTable'; import { StandardNodeTypes } from '@/interfaces/formComponent'; import { ActionSelect } from './actionSelect'; import { useAvailableStandardConstantsMetadata } from '@/utils/metadata/useAvailableConstants'; import { SourceFilesFolderProvider } from '@/providers/sourceFileManager/sourcesFolderProvider'; +import { StyledLabel } from '../_settings/utils'; +import { SettingInput } from '../settingsInput/settingsInput'; +import { nanoid } from '@/utils/uuid'; +import FormItem from '../_settings/components/formItem'; + +const { Panel } = Collapse; const getActionFullName = (actionOwner: string, actionName: string): string => { return actionName @@ -27,6 +33,9 @@ const parseActionFullName = (fullName: string): IActionIdentifier => { : null; }; +const FORM_ARGUMENTS_FIELD = 'actionArguments'; +const ACTION_FULL_NAME_FIELD = 'actionFullName'; + export const ConfigurableActionConfigurator: FC = props => { const [form] = Form.useForm(); const { formSettings } = useForm(); @@ -49,19 +58,34 @@ export const ConfigurableActionConfigurator: FC { + const hasChangedAction = (changedValues): boolean => { + if (changedValues && changedValues.hasOwnProperty(ACTION_FULL_NAME_FIELD)) { + const { actionFullName } = changedValues; + const prevActionFullName = formValues?.actionFullName; + return prevActionFullName !== actionFullName; + } + return false; + }; + + const onValuesChange = (changedValues, values) => { + const actionChanged = hasChangedAction(changedValues); + if (actionChanged) { + form.setFieldValue(FORM_ARGUMENTS_FIELD, undefined); + } + if (onChange) { - const { actionFullName, ...restProps } = values; + const { actionFullName, actionArguments, ...restProps } = values; const actionId = parseActionFullName(actionFullName); - const formValues = { + const newFormValues = { _type: StandardNodeTypes.ConfigurableActionConfig, actionName: actionId?.actionName, actionOwner: actionId?.actionOwner, + actionArguments: actionChanged ? undefined : actionArguments, ...restProps, }; - onChange(formValues); + onChange(newFormValues); } }; @@ -92,12 +116,12 @@ export const ConfigurableActionConfigurator: FC - + 0 ? filteredActions : actions} readOnly={readOnly}> - + {selectedAction && selectedAction.hasArguments && ( - + - - - + { value?.handleSuccess && ( - - - - ) - }]} - /> + + } key="1"> + + + + + ) } - - - + { value?.handleFail && ( - - - - ) - }]} - /> + + } key="1"> + + + + + ) } @@ -155,7 +165,8 @@ export const ConfigurableActionConfigurator: FC({ }: IProps) { const formRef = useShaFormRef(); - useEffect(() => { - formRef.current?.resetFields(); - }); - return ( ({ ); } -export default GenericArgumentsEditor; +export default GenericArgumentsEditor; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/configurableActionsConfigurator/index.tsx b/shesha-reactjs/src/designer-components/configurableActionsConfigurator/index.tsx index 2203b7b9a0..3a34e36add 100644 --- a/shesha-reactjs/src/designer-components/configurableActionsConfigurator/index.tsx +++ b/shesha-reactjs/src/designer-components/configurableActionsConfigurator/index.tsx @@ -21,7 +21,7 @@ const ConfigurableActionConfiguratorComponent: IToolboxComponent - + ); }, diff --git a/shesha-reactjs/src/designer-components/configurableItemAutocomplete/index.tsx b/shesha-reactjs/src/designer-components/configurableItemAutocomplete/index.tsx index 7870cd8f2c..9c0e7c390b 100644 --- a/shesha-reactjs/src/designer-components/configurableItemAutocomplete/index.tsx +++ b/shesha-reactjs/src/designer-components/configurableItemAutocomplete/index.tsx @@ -6,7 +6,7 @@ import settingsFormJson from './settingsForm.json'; import React from 'react'; import { useAvailableConstantsData, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { IConfigurableItemAutocompleteComponentProps } from './interfaces'; -import { useAsyncMemo } from '@/hooks/useAsyncMemo'; +import { useAsyncDeepCompareMemo } from '@/hooks/useAsyncMemo'; import { evaluateDynamicFilters } from '@/utils'; import { useNestedPropertyMetadatAccessor } from '@/providers'; import { ConfigItemAutocomplete } from '@/components/configurableItemAutocomplete'; @@ -26,7 +26,7 @@ export const ConfigurableItemAutocompleteComponent: IToolboxComponent(async () => { + const evaluatedFilter = useAsyncDeepCompareMemo(async () => { if (!filter) return undefined; @@ -61,7 +61,7 @@ export const ConfigurableItemAutocompleteComponent: IToolboxComponent diff --git a/shesha-reactjs/src/designer-components/container/containerComponent.tsx b/shesha-reactjs/src/designer-components/container/containerComponent.tsx index db62225ae9..6f536b5cd5 100644 --- a/shesha-reactjs/src/designer-components/container/containerComponent.tsx +++ b/shesha-reactjs/src/designer-components/container/containerComponent.tsx @@ -1,38 +1,19 @@ import { GroupOutlined } from '@ant-design/icons'; -import React, { useEffect, useState } from 'react'; -import { ICommonContainerProps, IContainerComponentProps, IToolboxComponent } from '@/interfaces'; +import React from 'react'; +import { IContainerComponentProps, IToolboxComponent } from '@/interfaces'; import { getStyle, getLayoutStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { getSettings } from './settingsForm'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; -import { useFormData, useGlobalState, useSheshaApplication } from '@/providers'; -import { ComponentsContainer, ValidationErrors } from '@/components'; +import { useFormData, useGlobalState } from '@/providers'; +import { ComponentsContainer } from '@/components'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import ParentProvider from '@/providers/parentProvider/index'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; -import { isValidGuid } from '@/components/formDesigner/components/utils'; -import { CSSProperties } from 'styled-components'; -import { toSizeCssProp } from '@/utils/form'; -import { removeUndefinedProperties } from '@/utils/array'; -import { ShadowStyleType } from './interfaces'; - -const basicShadow: CSSProperties = { - boxShadow: "0px 2px 4px 0px rgba(0,0,0,.15)" -}; - -const invertedShadow: CSSProperties = { - boxShadow: "0px -2px 4px 0px rgba(0,0,0,.15)" -}; - -const getShadowStyle = (shadowType?: ShadowStyleType): object=> { - switch (shadowType) { - case 'below': - return basicShadow; - case 'above': - return invertedShadow; - default: - return undefined; - } -}; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultStyles } from './data'; +import { removeUndefinedProps } from '@/utils/object'; +import { addPx } from '@/utils/style'; +import { useStyles } from './styles'; const ContainerComponent: IToolboxComponent = { type: 'container', @@ -42,29 +23,26 @@ const ContainerComponent: IToolboxComponent = { Factory: ({ model }) => { const { data: formData } = useFormData(); const { globalState } = useGlobalState(); - const { backendUrl, httpHeaders } = useSheshaApplication(); - const [fileUrl, setFileUrl] = useState(null); - - useEffect(() => { - if (model.backgroundDataSource === 'storedFileId' && model.backgroundStoredFileId) { - fetch(`${backendUrl}/api/StoredFile/Download?id=${model.backgroundStoredFileId}`, - { headers: { ...httpHeaders, "Content-Type": "application/octet-stream" } }) - .then((response) => { - return response.blob(); - }) - .then((blob) => { - setFileUrl(URL.createObjectURL(blob)); - }); - } - }, [model.backgroundStoredFileId]); - - if (model.backgroundDataSource === 'storedFileId' && model.backgroundStoredFileId && !isValidGuid(model.backgroundStoredFileId)) { - return ; - } + const { styles, cx } = useStyles(); + const { + dimensionsStyles, + borderStyles, + backgroundStyles, + shadowStyles, + stylingBoxAsCSS, + } = model.allStyles; + + const wrapperStyles = removeUndefinedProps({ + ...dimensionsStyles, + ...borderStyles, + ...backgroundStyles, + ...shadowStyles, + ...stylingBoxAsCSS + }); if (model.hidden) return null; - const flexAndGridStyles: ICommonContainerProps = { + const flexAndGridStyles = { display: model.display, flexDirection: model.flexDirection, direction: model.direction, @@ -77,61 +55,25 @@ const ContainerComponent: IToolboxComponent = { noDefaultStyling: model.noDefaultStyling, gridColumnsCount: model.gridColumnsCount, flexWrap: model.flexWrap, - gap: model.gap, + gap: addPx(model.gap), }; - const widthStyles: CSSProperties = { - width: toSizeCssProp(model.width), - minWidth: toSizeCssProp(model.minWidth), - maxWidth: toSizeCssProp(model.maxWidth), - overflow: model.overflow, - }; - - const heightStyles: CSSProperties = { - height: toSizeCssProp(model.height), - minHeight: toSizeCssProp(model.minHeight), - maxHeight: toSizeCssProp(model.maxHeight), - }; - - const borderStyles: CSSProperties = { - borderWidth: toSizeCssProp(model.borderWidth), - borderColor: model.borderColor, - borderStyle: model.borderStyle, - borderRadius: toSizeCssProp(model.borderRadius), - }; - - const val = model.backgroundDataSource === 'storedFileId' - ? fileUrl - : model.backgroundDataSource === 'base64' - ? model.backgroundBase64 - : model.backgroundDataSource === 'url' - ? model.backgroundUrl - : ''; - - const backgroundStyles: CSSProperties = model.backgroundType === 'image' && val - ? { backgroundImage: `url(${val})`, backgroundSize: model.backgroundCover, backgroundRepeat: model.backgroundRepeat } - : model.backgroundType === 'color' - ? { background: model?.backgroundColor } - : {}; - - const renderShadow = getShadowStyle(model.shadowStyle); - - const finalStyle = removeUndefinedProperties({ - ...widthStyles, - ...heightStyles, - ...borderStyles, - ...backgroundStyles, - ...renderShadow, - ...getStyle(model?.style, formData) - }); - return ( @@ -146,9 +88,10 @@ const ContainerComponent: IToolboxComponent = { ...prev, direction: prev['direction'] ?? 'vertical', justifyContent: prev['justifyContent'] ?? 'left', - display: prev['display'] /* ?? 'block'*/, + display: prev['display'], flexWrap: prev['flexWrap'] ?? 'wrap', components: prev['components'] ?? [], + editMode: 'inherited', })) .add(1, (prev) => migratePropertyName(migrateCustomFunctions(prev))) .add(2, (prev) => migrateVisibility(prev)) @@ -173,12 +116,33 @@ const ContainerComponent: IToolboxComponent = { maxHeight: prev.maxHeight, maxWidth: prev.maxWidth }; - return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + const showAdvanced = prev.showAdvanced ?? false; + return { ...prev, showAdvanced: showAdvanced, desktop: { ...styles, showAdvanced }, tablet: { ...styles, showAdvanced }, mobile: { ...styles, showAdvanced } }; }) .add(6, (prev) => { - return { ...prev, shadowStyle: 'none' }; + const flexAndGridStyles = { + display: prev?.display, + flexDirection: prev?.flexDirection, + direction: prev?.direction, + justifyContent: prev?.justifyContent, + alignItems: prev?.alignItems, + alignSelf: prev?.alignSelf, + justifySelf: prev?.justifySelf, + justifyItems: prev?.justifyItems, + textJustify: prev?.textJustify, + noDefaultStyling: prev?.noDefaultStyling, + gridColumnsCount: prev?.gridColumnsCount, + flexWrap: prev?.flexWrap, + gap: prev?.gap || 8, + overflow: prev?.overflow || true, + }; + + return { + ...prev, desktop: { ...prev.desktop, ...flexAndGridStyles }, + tablet: { ...prev.tablet, ...flexAndGridStyles }, mobile: { ...prev.mobile, ...flexAndGridStyles } + }; }) - , + .add(7, (prev) => ({ ...migratePrevStyles(prev, defaultStyles(prev)) })), }; export default ContainerComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/container/data.ts b/shesha-reactjs/src/designer-components/container/data.ts index 8ace8c20be..b987f87711 100644 --- a/shesha-reactjs/src/designer-components/container/data.ts +++ b/shesha-reactjs/src/designer-components/container/data.ts @@ -1,148 +1,218 @@ +import { ICommonContainerProps, IContainerComponentProps, IStyleType } from '@/index'; import { nanoid } from '@/utils/uuid'; export const JUSTIFY_ITEMS = [ - { id: nanoid(), label: 'center', value: 'center' }, - { id: nanoid(), label: 'start', value: 'start' }, - { id: nanoid(), label: 'end', value: 'end' }, - { id: nanoid(), label: 'flex-start', value: 'flex-start' }, - { id: nanoid(), label: 'flex-end', value: 'flex-end' }, - { id: nanoid(), label: 'left', value: 'left' }, - { id: nanoid(), label: 'right', value: 'right' }, - { id: nanoid(), label: 'normal', value: 'normal' }, - { id: nanoid(), label: 'space-between', value: 'space-between' }, - { id: nanoid(), label: 'space-around', value: 'space-around' }, - { id: nanoid(), label: 'space-evenly', value: 'space-evenly' }, - { id: nanoid(), label: 'stretch', value: 'stretch' }, - { id: nanoid(), label: 'safe center', value: 'safe center' }, - { id: nanoid(), label: 'unsafe center', value: 'unsafe center' }, - { id: nanoid(), label: 'inherit', value: 'inherit' }, - { id: nanoid(), label: 'initial', value: 'initial' }, - { id: nanoid(), label: 'revert', value: 'revert' }, - { id: nanoid(), label: 'revert-layer', value: 'revert-layer' }, - { id: nanoid(), label: 'unset', value: 'unset' }, + { id: nanoid(), label: 'Center', value: 'center' }, + { id: nanoid(), label: 'Start', value: 'start' }, + { id: nanoid(), label: 'End', value: 'end' }, + { id: nanoid(), label: 'Flex start', value: 'flex-start' }, + { id: nanoid(), label: 'Flex end', value: 'flex-end' }, + { id: nanoid(), label: 'Left', value: 'left' }, + { id: nanoid(), label: 'Right', value: 'right' }, + { id: nanoid(), label: 'Normal', value: 'normal' }, + { id: nanoid(), label: 'Space between', value: 'space-between' }, + { id: nanoid(), label: 'Space around', value: 'space-around' }, + { id: nanoid(), label: 'Space evenly', value: 'space-evenly' }, + { id: nanoid(), label: 'Stretch', value: 'stretch' }, + { id: nanoid(), label: 'Safe center', value: 'safe center' }, + { id: nanoid(), label: 'Unsafe center', value: 'unsafe center' }, + { id: nanoid(), label: 'Inherit', value: 'inherit' }, + { id: nanoid(), label: 'Initial', value: 'initial' }, + { id: nanoid(), label: 'Revert', value: 'revert' }, + { id: nanoid(), label: 'Revert layer', value: 'revert-layer' }, + { id: nanoid(), label: 'Unset', value: 'unset' }, ]; export const FLEX_DIRECTION = [ - { id: nanoid(), label: 'row', value: 'row' }, - { id: nanoid(), label: 'row-reverse', value: 'row-reverse' }, - { id: nanoid(), label: 'column', value: 'column' }, - { id: nanoid(), label: 'column-reverse', value: 'column-reverse' }, - { id: nanoid(), label: 'inherit', value: 'inherit' }, - { id: nanoid(), label: 'initial', value: 'initial' }, - { id: nanoid(), label: 'revert', value: 'revert' }, - { id: nanoid(), label: 'revert-layer', value: 'revert-layer' }, - { id: nanoid(), label: 'unset', value: 'unset' }, + { id: nanoid(), label: 'Row', value: 'row' }, + { id: nanoid(), label: 'Row reverse', value: 'row-reverse' }, + { id: nanoid(), label: 'Column', value: 'column' }, + { id: nanoid(), label: 'Column reverse', value: 'column-reverse' }, + { id: nanoid(), label: 'Inherit', value: 'inherit' }, + { id: nanoid(), label: 'Initial', value: 'initial' }, + { id: nanoid(), label: 'Revert', value: 'revert' }, + { id: nanoid(), label: 'Revert layer', value: 'revert-layer' }, + { id: nanoid(), label: 'Unset', value: 'unset' }, ]; export const FLEX_WRAP = [ - { id: nanoid(), label: 'nowrap', value: 'nowrap' }, - { id: nanoid(), label: 'wrap', value: 'wrap' }, - { id: nanoid(), label: 'wrap-reverse', value: 'wrap-reverse' }, - { id: nanoid(), label: 'inherit', value: 'inherit' }, - { id: nanoid(), label: 'initial', value: 'initial' }, - { id: nanoid(), label: 'revert', value: 'revert' }, - { id: nanoid(), label: 'revert-layer', value: 'revert-layer' }, - { id: nanoid(), label: 'unset', value: 'unset' }, + { id: nanoid(), label: 'No wrap', value: 'nowrap' }, + { id: nanoid(), label: 'Wrap', value: 'wrap' }, + { id: nanoid(), label: 'Wrap reverse', value: 'wrap-reverse' }, + { id: nanoid(), label: 'Inherit', value: 'inherit' }, + { id: nanoid(), label: 'Initial', value: 'initial' }, + { id: nanoid(), label: 'Revert', value: 'revert' }, + { id: nanoid(), label: 'Revert layer', value: 'revert-layer' }, + { id: nanoid(), label: 'Unset', value: 'unset' }, ]; export const JUSTIFY_CONTENT = [ - { id: nanoid(), label: 'center', value: 'center' }, - { id: nanoid(), label: 'start', value: 'start' }, - { id: nanoid(), label: 'end', value: 'end' }, - { id: nanoid(), label: 'flex-start', value: 'flex-start' }, - { id: nanoid(), label: 'flex-end', value: 'flex-end' }, - { id: nanoid(), label: 'left', value: 'left' }, - { id: nanoid(), label: 'right', value: 'right' }, - { id: nanoid(), label: 'normal', value: 'normal' }, - { id: nanoid(), label: 'space-between', value: 'space-between' }, - { id: nanoid(), label: 'space-around', value: 'space-around' }, - { id: nanoid(), label: 'space-evenly', value: 'space-evenly' }, - { id: nanoid(), label: 'stretch', value: 'stretch' }, - { id: nanoid(), label: 'safe center', value: 'safe center' }, - { id: nanoid(), label: 'unsafe center', value: 'unsafe center' }, + { id: nanoid(), label: 'Center', value: 'center' }, + { id: nanoid(), label: 'Start', value: 'start' }, + { id: nanoid(), label: 'End', value: 'end' }, + { id: nanoid(), label: 'Flex start', value: 'flex-start' }, + { id: nanoid(), label: 'Flex end', value: 'flex-end' }, + { id: nanoid(), label: 'Left', value: 'left' }, + { id: nanoid(), label: 'Right', value: 'right' }, + { id: nanoid(), label: 'Normal', value: 'normal' }, + { id: nanoid(), label: 'Space between', value: 'space-between' }, + { id: nanoid(), label: 'Space around', value: 'space-around' }, + { id: nanoid(), label: 'Space evenly', value: 'space-evenly' }, + { id: nanoid(), label: 'Stretch', value: 'stretch' }, + { id: nanoid(), label: 'Safe center', value: 'safe center' }, + { id: nanoid(), label: 'Unsafe center', value: 'unsafe center' }, { id: nanoid(), label: 'inherit', value: 'inherit' }, - { id: nanoid(), label: 'initial', value: 'initial' }, - { id: nanoid(), label: 'revert', value: 'revert' }, - { id: nanoid(), label: 'revert-layer', value: 'revert-layer' }, - { id: nanoid(), label: 'unset', value: 'unset' }, + { id: nanoid(), label: 'Initial', value: 'initial' }, + { id: nanoid(), label: 'Revert', value: 'revert' }, + { id: nanoid(), label: 'Revert layer', value: 'revert-layer' }, + { id: nanoid(), label: 'Unset', value: 'unset' }, ]; export const JUSTIFY_SELF = [ - { id: nanoid(), label: 'auto', value: 'auto' }, - { id: nanoid(), label: 'normal', value: 'normal' }, - { id: nanoid(), label: 'stretch', value: 'stretch' }, - { id: nanoid(), label: 'center', value: 'center' }, - { id: nanoid(), label: 'start', value: 'start' }, - { id: nanoid(), label: 'end', value: 'end' }, - { id: nanoid(), label: 'flex-start', value: 'flex-start' }, - { id: nanoid(), label: 'flex-end', value: 'flex-end' }, - { id: nanoid(), label: 'self-start', value: 'self-start' }, - { id: nanoid(), label: 'self-end', value: 'self-end' }, - { id: nanoid(), label: 'left', value: 'left' }, - { id: nanoid(), label: 'right', value: 'right' }, - { id: nanoid(), label: 'baseline', value: 'baseline' }, - { id: nanoid(), label: 'first baseline', value: 'first baseline' }, - { id: nanoid(), label: 'last baseline', value: 'last baseline' }, - { id: nanoid(), label: 'safe center', value: 'safe center' }, - { id: nanoid(), label: 'unsafe center', value: 'unsafe center' }, - { id: nanoid(), label: 'inherit', value: 'inherit' }, - { id: nanoid(), label: 'initial', value: 'initial' }, - { id: nanoid(), label: 'revert', value: 'revert' }, - { id: nanoid(), label: 'revert-layer', value: 'revert-layer' }, - { id: nanoid(), label: 'unset', value: 'unset' }, + { id: nanoid(), label: 'Auto', value: 'auto' }, + { id: nanoid(), label: 'Normal', value: 'normal' }, + { id: nanoid(), label: 'Stretch', value: 'stretch' }, + { id: nanoid(), label: 'Center', value: 'center' }, + { id: nanoid(), label: 'Start', value: 'start' }, + { id: nanoid(), label: 'End', value: 'end' }, + { id: nanoid(), label: 'Flex start', value: 'flex-start' }, + { id: nanoid(), label: 'Flex end', value: 'flex-end' }, + { id: nanoid(), label: 'Self start', value: 'self-start' }, + { id: nanoid(), label: 'Self end', value: 'self-end' }, + { id: nanoid(), label: 'Left', value: 'left' }, + { id: nanoid(), label: 'Right', value: 'right' }, + { id: nanoid(), label: 'Baseline', value: 'baseline' }, + { id: nanoid(), label: 'First baseline', value: 'first baseline' }, + { id: nanoid(), label: 'Last baseline', value: 'last baseline' }, + { id: nanoid(), label: 'Safe center', value: 'safe center' }, + { id: nanoid(), label: 'Unsafe center', value: 'unsafe center' }, + { id: nanoid(), label: 'Inherit', value: 'inherit' }, + { id: nanoid(), label: 'Initial', value: 'initial' }, + { id: nanoid(), label: 'Revert', value: 'revert' }, + { id: nanoid(), label: 'Revert layer', value: 'revert-layer' }, + { id: nanoid(), label: 'Unset', value: 'unset' }, ]; export const TEXT_JUSTIFY = [ - { id: nanoid(), label: 'none', value: 'none' }, - { id: nanoid(), label: 'auto', value: 'auto' }, - { id: nanoid(), label: 'inter-word', value: 'inter-word' }, - { id: nanoid(), label: 'inter-character', value: 'inter-character' }, - { id: nanoid(), label: 'distribute', value: 'distribute' }, - { id: nanoid(), label: 'inherit', value: 'inherit' }, - { id: nanoid(), label: 'initial', value: 'initial' }, - { id: nanoid(), label: 'revert', value: 'revert' }, - { id: nanoid(), label: 'revert-layer', value: 'revert-layer' }, - { id: nanoid(), label: 'unset', value: 'unset' }, + { id: nanoid(), label: 'None', value: 'none' }, + { id: nanoid(), label: 'Auto', value: 'auto' }, + { id: nanoid(), label: 'Inter word', value: 'inter-word' }, + { id: nanoid(), label: 'Inter character', value: 'inter-character' }, + { id: nanoid(), label: 'Distribute', value: 'distribute' }, + { id: nanoid(), label: 'Inherit', value: 'inherit' }, + { id: nanoid(), label: 'Initial', value: 'initial' }, + { id: nanoid(), label: 'Revert', value: 'revert' }, + { id: nanoid(), label: 'Revert layer', value: 'revert-layer' }, + { id: nanoid(), label: 'Unset', value: 'unset' }, ]; export const ALIGN_ITEMS = [ - { id: nanoid(), label: 'normal', value: 'normal' }, - { id: nanoid(), label: 'stretch', value: 'stretch' }, - { id: nanoid(), label: 'center', value: 'center' }, - { id: nanoid(), label: 'start', value: 'start' }, - { id: nanoid(), label: 'end', value: 'end' }, - { id: nanoid(), label: 'flex-start', value: 'flex-start' }, - { id: nanoid(), label: 'flex-end', value: 'flex-end' }, - { id: nanoid(), label: 'baseline', value: 'baseline' }, - { id: nanoid(), label: 'first baseline', value: 'first baseline' }, - { id: nanoid(), label: 'last baseline', value: 'last baseline' }, - { id: nanoid(), label: 'safe center', value: 'safe center' }, - { id: nanoid(), label: 'unsafe center', value: 'unsafe center' }, - { id: nanoid(), label: 'inherit', value: 'inherit' }, - { id: nanoid(), label: 'initial', value: 'initial' }, - { id: nanoid(), label: 'revert', value: 'revert' }, - { id: nanoid(), label: 'revert-layer', value: 'revert-layer' }, - { id: nanoid(), label: 'unset', value: 'unset' }, + { id: nanoid(), label: 'Normal', value: 'normal' }, + { id: nanoid(), label: 'Center', value: 'center' }, + { id: nanoid(), label: 'Baseline', value: 'baseline' }, + { id: nanoid(), label: 'Inherit', value: 'inherit' }, + { id: nanoid(), label: 'Initial', value: 'initial' }, + { id: nanoid(), label: 'Revert', value: 'revert' }, + { id: nanoid(), label: 'Revert layer', value: 'revert-layer' }, + { id: nanoid(), label: 'Unset', value: 'unset' }, +]; + +export const ALIGN_ITEMS_GRID = [ + { id: nanoid(), label: 'Stretch', value: 'stretch' }, + { id: nanoid(), label: 'Start', value: 'start' }, + { id: nanoid(), label: 'End', value: 'end' }, + { id: nanoid(), label: 'First baseline', value: 'first baseline' }, + { id: nanoid(), label: 'Last baseline', value: 'last baseline' }, +]; + +export const ALIGN_ITEMS_FLEX = [ + { id: nanoid(), label: 'Flex start', value: 'flex-start' }, + { id: nanoid(), label: 'Flex end', value: 'flex-end' }, ]; export const ALIGN_SELF = [ - { id: nanoid(), label: 'auto', value: 'auto' }, - { id: nanoid(), label: 'normal', value: 'normal' }, - { id: nanoid(), label: 'center', value: 'center' }, - { id: nanoid(), label: 'start', value: 'start' }, - { id: nanoid(), label: 'end', value: 'end' }, - { id: nanoid(), label: 'self-start', value: 'self-start' }, - { id: nanoid(), label: 'self-end', value: 'self-end' }, - { id: nanoid(), label: 'flex-start', value: 'flex-start' }, - { id: nanoid(), label: 'flex-end', value: 'flex-end' }, - { id: nanoid(), label: 'baseline', value: 'baseline' }, - { id: nanoid(), label: 'first baseline', value: 'first baseline' }, - { id: nanoid(), label: 'last baseline', value: 'last baseline' }, - { id: nanoid(), label: 'stretch', value: 'stretch' }, - { id: nanoid(), label: 'safe center', value: 'safe center' }, - { id: nanoid(), label: 'unsafe center', value: 'unsafe center' }, - { id: nanoid(), label: 'inherit', value: 'inherit' }, - { id: nanoid(), label: 'initial', value: 'initial' }, - { id: nanoid(), label: 'revert', value: 'revert' }, - { id: nanoid(), label: 'revert-layer', value: 'revert-layer' }, - { id: nanoid(), label: 'unset', value: 'unset' }, + { id: nanoid(), label: 'Auto', value: 'auto' }, + { id: nanoid(), label: 'Normal', value: 'normal' }, + { id: nanoid(), label: 'Center', value: 'center' }, + { id: nanoid(), label: 'Start', value: 'start' }, + { id: nanoid(), label: 'End', value: 'end' }, + { id: nanoid(), label: 'Self start', value: 'self-start' }, + { id: nanoid(), label: 'Self end', value: 'self-end' }, + { id: nanoid(), label: 'Flex start', value: 'flex-start' }, + { id: nanoid(), label: 'Flex end', value: 'flex-end' }, + { id: nanoid(), label: 'Baseline', value: 'baseline' }, + { id: nanoid(), label: 'First baseline', value: 'first baseline' }, + { id: nanoid(), label: 'Last baseline', value: 'last baseline' }, + { id: nanoid(), label: 'Stretch', value: 'stretch' }, + { id: nanoid(), label: 'Safe center', value: 'safe center' }, + { id: nanoid(), label: 'Unsafe center', value: 'unsafe center' }, + { id: nanoid(), label: 'Inherit', value: 'inherit' }, + { id: nanoid(), label: 'Initial', value: 'initial' }, + { id: nanoid(), label: 'Revert', value: 'revert' }, + { id: nanoid(), label: 'Revert layer', value: 'revert-layer' }, + { id: nanoid(), label: 'Unset', value: 'unset' }, ]; + + +export const defaultStyles = (prev?: IContainerComponentProps): IStyleType & ICommonContainerProps => { + const { + width = 'auto', + height = 'auto', + maxHeight = 'auto', + maxWidth = 'auto', + minHeight = 'auto', + minWidth = '0px', + borderColor = '#d9d9d9', + borderRadius = '8', + borderStyle = 'none', + borderWidth = '1px', + shadowStyle + } = prev || {}; + + + const isBelow = shadowStyle === 'below'; + const isAbove = shadowStyle === 'above'; + + return { + background: { + type: 'color', + color: '', + + }, + dimensions: { + width, + height, + minHeight, + maxHeight, + minWidth, + maxWidth + }, + border: { + radiusType: 'all', + borderType: 'all', + border: { + all: { width: borderWidth, color: borderColor, style: borderStyle as any }, + }, + radius: { all: borderRadius } + }, + shadow: { + blurRadius: isBelow || isAbove ? 4 : 0, + color: '#000000', + offsetX: 0, + offsetY: isAbove ? -2 : isBelow ? 2 : 0, + spreadRadius: 0 + }, + display: prev?.display ?? "block", + direction: prev?.direction ?? "horizontal", + flexWrap: prev?.flexWrap ?? "wrap", + flexDirection: prev?.flexDirection ?? "row", + justifyContent: prev?.justifyContent ?? "left", + alignItems: prev?.alignItems ?? "normal", + alignSelf: prev?.alignSelf ?? "normal", + justifyItems: prev?.justifyItems ?? "normal", + textJustify: prev?.textJustify ?? "auto", + justifySelf: prev?.justifySelf ?? "normal", + noDefaultStyling: prev?.noDefaultStyling ?? false, + gridColumnsCount: prev?.gridColumnsCount ?? null, + gap: prev?.gap ?? '8px' + }; +}; diff --git a/shesha-reactjs/src/designer-components/container/interfaces.ts b/shesha-reactjs/src/designer-components/container/interfaces.ts index 0be63d0ef6..33fc67cb1c 100644 --- a/shesha-reactjs/src/designer-components/container/interfaces.ts +++ b/shesha-reactjs/src/designer-components/container/interfaces.ts @@ -1,5 +1,5 @@ import { ContainerDirection } from '@/components/formDesigner/common/interfaces'; -import { IConfigurableFormComponent } from '@/providers/form/models'; +import { IConfigurableFormComponent, IInputStyles } from '@/providers/form/models'; export type JustifyContent = | 'center' @@ -25,14 +25,14 @@ export type JustifyContent = export type JustifyItems = | 'normal' | 'stretch' - | 'center ' - | 'start ' - | 'end ' + | 'center' + | 'start' + | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' - | 'left ' + | 'left' | 'right' | 'baseline' | 'first baseline' @@ -72,7 +72,7 @@ export type JustifySelf = | 'flex-end; ' | 'self-start' | 'self-end' - | 'left; ' + | 'left' | 'right;' | 'baseline' | 'first baseline' @@ -140,7 +140,7 @@ export type AlignSelf = export type ShadowStyleType = 'none' | 'above' | 'below'; -export interface ICommonContainerProps { +export interface ICommonContainerProps extends Omit { display?: 'block' | 'flex' | 'grid' | 'inline-grid'; direction?: ContainerDirection; flexWrap?: FlexWrap; @@ -165,11 +165,11 @@ export interface ICommonContainerProps { borderColor?: string; borderStyle?: string; borderRadius?: string | number; - overflow?: string; - shadowStyle?: ShadowStyleType; + shadowStyle?: string; + style?: React.CSSProperties; } -export interface IContainerComponentProps extends IConfigurableFormComponent, ICommonContainerProps { +export interface IContainerComponentProps extends IConfigurableFormComponent, Omit { backgroundCover?: 'contain' | 'cover'; backgroundRepeat?: 'repeat' | 'no-repeat' | 'repeat-x' | 'repeat-y' | 'round'; className?: string; @@ -181,4 +181,8 @@ export interface IContainerComponentProps extends IConfigurableFormComponent, IC backgroundUrl?: string; backgroundBase64?: string; backgroundStoredFileId?: string; + desktop?: any; + tablet?: any; + mobile?: any; + showAdvanced?: boolean; } diff --git a/shesha-reactjs/src/designer-components/container/settingsForm.ts b/shesha-reactjs/src/designer-components/container/settingsForm.ts index 0613528713..2e44ff8e86 100644 --- a/shesha-reactjs/src/designer-components/container/settingsForm.ts +++ b/shesha-reactjs/src/designer-components/container/settingsForm.ts @@ -2,734 +2,928 @@ import { nanoid } from '@/utils/uuid'; import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; import { ALIGN_ITEMS, + ALIGN_ITEMS_GRID, ALIGN_SELF, FLEX_DIRECTION, FLEX_WRAP, JUSTIFY_CONTENT, JUSTIFY_ITEMS, - JUSTIFY_SELF, - TEXT_JUSTIFY, + JUSTIFY_SELF } from './data'; -export const getSettings = (data: any) => - new DesignerToolbarSettings(data) - .addCollapsiblePanel({ - id: '11164664-cbc9-4cef-babc-6fbea44cd0ca', - propertyName: 'pnlDisplay', - parentId: 'root', - label: 'Display', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - components: [ - ...new DesignerToolbarSettings() - .addTextField({ - id: '6d39921b-d20e-49cf-bc54-ec584f63be5c', - propertyName: 'componentName', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - label: 'Component name', - validate: { required: true }, - jsSetting: false - }) - .addCheckbox({ - id: 'bf1823d6-dca4-408a-b7d8-5b42eacb076d', - propertyName: 'hidden', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - label: 'Hidden', - }) - .addEditMode({ - id: 'abc823d6-dca4-408a-b7d8-5b42eacb1234', - propertyName: 'editMode', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - label: 'Edit mode', - }) - .addCheckbox({ - id: 'c9900272-11c6-4484-be5b-e48c859f86e4', - propertyName: 'noDefaultStyling', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - label: 'No Default Styling', - description: 'If checked, the default styles and classes of the container will not be applied.', - }) - .addDropdown({ - id: 'dbad943d-d498-454b-b13e-6845b69c8df1', - propertyName: 'display', - label: 'Display', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - allowClear: false, - hidden: false, - dataSourceType: 'values', - values: [ - { - id: '4ca3ef53-be97-457b-bacb-7d0c0e9b535d', - label: 'grid', - value: 'grid', - }, - { - id: '219eff56-3005-4df1-9e52-5bd759a34bc2', - label: 'block', - value: 'block', - }, - { - id: 'f1b3f920-1537-4c2f-86bd-bec683dae879', - label: 'flex', - value: 'flex', - }, - { - id: '4f29e10f-f38f-48f5-b600-0920226493cc', - label: 'inline-grid', - value: 'inline-grid', - }, - ], - description: - 'The display CSS property sets whether an element is treated as a block or inline element and the layout used for its children, such as flow layout, grid or flex.', - validate: { - required: true, - }, - }) - .addDropdown({ - id: '5699c634-286d-4d7e-a804-8405d2cb1721', - propertyName: 'flexDirection', - label: 'Flex Direction', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - hidden: { - _code: "return getSettingValue(data?.display) !== 'flex';", - _mode: 'code', - _value: false, - } as any, - dataSourceType: 'values', - values: FLEX_DIRECTION, - description: - 'The flex-direction CSS property sets how flex items are placed in the flex container defining the main axis and the direction (normal or reversed).', - }) - .addDropdown({ - id: '2706b1e3-83dd-43a9-8800-183ec05f6359', - propertyName: 'flexWrap', - label: 'Flex Wrap', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - hidden: { - _code: "return getSettingValue(data?.display) !== 'flex';", - _mode: 'code', - _value: false, - } as any, - dataSourceType: 'values', - values: FLEX_WRAP, - description: - 'The flex-wrap CSS property sets whether flex items are forced onto one line or can wrap onto multiple lines. If wrapping is allowed, it sets the direction that lines are stacked.', - }) - .addTextField({ - id: 'bccb0c08-6d9e-4257-9c40-129974850f4c', - propertyName: 'gap', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - label: 'Gap', - description: 'Examples of a valid gap include: `10` | `10px` | `20px 20px`', - hidden: { - _code: "return getSettingValue(data?.display) === 'block';", - _mode: 'code', - _value: false, - } as any, - }) - .addNumberField({ - id: 'aae1d544-fe19-4865-9ef7-f885c72d8f61', - propertyName: 'gridColumnsCount', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - label: 'Grid Columns Count', - description: 'Number of columns each grid should have', - hidden: { - _code: - "return getSettingValue(data?.display) !== 'grid' && getSettingValue(data?.display) !== 'inline-grid';", - _mode: 'code', - _value: false, - } as any, - }) - .addDropdown({ - id: '3069d513-09bc-41c1-9f63-e47ecf56fb41', - propertyName: 'direction', - label: 'Direction', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - hidden: true, - dataSourceType: 'values', - values: [ - { - id: 'f6410e20-49ce-45d0-90e3-49a8469a2c30', - label: 'horizontal', - value: 'horizontal', - }, - { - id: 'efd9da05-227c-43fb-b555-140561b6fefc', - label: 'vertical', - value: 'vertical', - }, - ], - validate: { - required: true, - }, - }) - .addDropdown({ - id: '2b17e6cf-0140-47e2-a5a8-f728544b8ef2', - propertyName: 'alignItems', - label: 'Align Items', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - hidden: { - _code: "return getSettingValue(data?.direction) === 'block';", - _mode: 'code', - _value: false, - } as any, - dataSourceType: 'values', - values: ALIGN_ITEMS, - validate: {}, - description: - 'The CSS align-items property sets the align-self value on all direct children as a group. In Flexbox, it controls the alignment of items on the Cross Axis. In Grid Layout, it controls the alignment of items on the Block Axis within their grid area.', - }) - .addDropdown({ - id: 'b9fc3571-5f6e-4bac-8ec9-9313a2e0f695', - propertyName: 'alignSelf', - label: 'Align Self', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - hidden: { - _code: "return getSettingValue(data?.direction) === 'block';", - _mode: 'code', - _value: false, - } as any, - dataSourceType: 'values', - values: ALIGN_SELF, - validate: {}, - description: - "The align-self CSS property overrides a grid or flex item's align-items value. In Grid, it aligns the item inside the grid area. In Flexbox, it aligns the item on the cross axis.", - }) - .addDropdown({ - id: '41e13a88-21b5-4577-adbf-2f4ef09e5ffc', - propertyName: 'justifyContent', - label: 'Justify Content', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - hidden: { - _code: "return getSettingValue(data?.direction) === 'block';", - _mode: 'code', - _value: false, - } as any, - dataSourceType: 'values', - values: JUSTIFY_CONTENT, - validate: {}, - description: - 'The CSS justify-content property defines how the browser distributes space between and around content items along the main-axis of a flex container, and the inline axis of a grid container.', - }) - .addDropdown({ - id: 'daa3b98e-c6bc-4883-ae54-143756579c51', - propertyName: 'justifySelf', - label: 'Justify Self', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - hidden: { - _code: "return getSettingValue(data?.direction) === 'block';", - _mode: 'code', - _value: false, - } as any, - dataSourceType: 'values', - values: JUSTIFY_SELF, - validate: {}, - description: - 'The CSS justify-self property sets the way a box is justified inside its alignment container along the appropriate axis.', - }) - .addDropdown({ - id: 'a089965c-f1ca-45b1-8f2f-2c8355cc83f9', - propertyName: 'textJustify', - label: 'Text Justify', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - hidden: { - _code: "return getSettingValue(data?.direction) === 'block';", - _mode: 'code', - _value: false, - } as any, - dataSourceType: 'values', - values: TEXT_JUSTIFY, - validate: {}, - description: - 'The text-justify CSS property sets what type of justification should be applied to text when text-align: justify; is set on an element.', - }) - .addDropdown({ - id: '2d75b63f-cee9-44b5-afb7-db1d8f05f005', - propertyName: 'justifyItems', - label: 'Justify Items', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - dataSourceType: 'values', - values: JUSTIFY_ITEMS, - validate: {}, - description: - 'The CSS justify-items property defines the default justify-self for all items of the box, giving them all a default way of justifying each box along the appropriate axis.', - }) - .addDropdown({ - id: nanoid(), - propertyName: 'overflow', - label: 'Overflow', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - dataSourceType: 'values', - values: [ - { - label: "Auto", - value: "auto", - id: nanoid() - }, - { - label: "Hidden", - value: "hidden", - id: nanoid() - }, - { - label: "Scroll", - value: "scroll", - id: nanoid() - }, - ], - validate: {}, - description: - 'The CSS justify-items property defines the default justify-self for all items of the box, giving them all a default way of justifying each box along the appropriate axis.', - }) - .addDropdown({ - id: nanoid(), - propertyName: 'shadowStyle', - label: 'Shadow Style', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - dataSourceType: 'values', - values: [ - { - label: "None", - value: "none", - id: nanoid() +import { FormLayout } from 'antd/lib/form/Form'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; + +export const getSettings = (data) => { + // Generate unique IDs for major components + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const displayCollapsiblePanelId = nanoid(); + const fontStylePnlId = nanoid(); + const flexGridPropertiesContainerId = nanoid(); + const buttonDisplayContainerId = nanoid(); + const nonBlockContainerId = nanoid(); + const dimensionsStyleCollapsiblePanelId = nanoid(); + const dimensionsStylePnlId = nanoid(); + const borderStyleCollapsiblePanelId = nanoid(); + const borderStylePnlId = nanoid(); + const backgroundStyleCollapsiblePanelId = nanoid(); + const backgroundStylePnlId = nanoid(); + const shadowStyleCollapsiblePanelId = nanoid(); + const shadowStylePnlId = nanoid(); + const shadowStyleRowId = nanoid(); + const styleCollapsiblePanelId = nanoid(); + const stylePnlId = nanoid(); + const styleBoxPnlId = nanoid(); + const customStyleCollapsiblePanelId = nanoid(); + const stylePnlCustomId = nanoid(); + + const getDisplayType = ' getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.display)'; + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Component Name', + parentId: commonTabId, + size: 'small', + validate: { + required: true, }, - { - label: "Above", - value: "above", - id: nanoid() + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'noDefaultStyling', + label: 'No Default Styling', + parentId: commonTabId, + size: 'small', + jsSetting: true, + }) + .toJson() + ] + }, + { + key: '2', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" }, - { - label: "Below", - value: "below", - id: nanoid() - } + components: [ + ...new DesignerToolbarSettings() + // .addCollapsiblePanel({ + // id: nanoid(), + // propertyName: 'pnlPosition', + // label: 'Position', + // labelAlign: 'right', + // parentId: styleRouterId, + // ghost: true, + // collapsible: 'header', + // content: { + // id: nanoid(), + // components: [...new DesignerToolbarSettings() + // .addSettingsInput({ + // id: nanoid(), + // propertyName: 'position.value', + // label: 'Position', + // parentId: 'positionCollapsiblePanel', + // inputType: 'dropdown', + // description: 'The position CSS property sets how an element is positioned in a document. The top, right, bottom, and left properties determine the final location of positioned elements.', + // validate: { + // required: true, + // }, + // dropdownOptions: [ + // { value: 'relative', label: 'Relative' }, + // { value: 'absolute', label: 'Absolute' } + // ] + // }) + // .addSettingsInputRow( + // getPositionInputs()[0] as any + // ) + // .addSettingsInputRow( + // getPositionInputs()[1] as any + // ) + // .addSettingsInputRow( + // getPositionInputs()[2] as any + // ) + // .addSettingsInputRow( + // getPositionInputs()[3] as any + // ) + // .toJson() + // ] + // } + // }) + .addCollapsiblePanel({ + id: displayCollapsiblePanelId, + propertyName: 'pnlDisplayStyle', + label: 'Display', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: fontStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'display', + label: 'Layout Type', + parentId: displayCollapsiblePanelId, + inputType: 'radio', + description: 'The display CSS property sets whether an element is treated as a block or inline element and the layout used for its children, such as flow layout, grid or flex.', + validate: { + required: true, + }, + buttonGroupOptions: [ + { value: 'block', title: 'Block', icon: 'BorderOutlined' }, + { value: 'grid', title: 'Grid', icon: 'AppstoreOutlined' }, + { value: 'flex', title: 'Flex', icon: 'flex' }, + { value: 'inline-grid', title: 'Inline grid', icon: 'TableOutlined' } + ] + }) + .addContainer({ + id: flexGridPropertiesContainerId, + parentId: displayCollapsiblePanelId, + hidden: { + _code: 'return' + getDisplayType + ' === "block";', + _mode: 'code', + _value: false, + } as any, + components: [ + ...new DesignerToolbarSettings() + .addContainer({ + id: buttonDisplayContainerId, + parentId: displayCollapsiblePanelId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + inline: true, + parentId: buttonDisplayContainerId, + inputs: [ + { + type: 'radio', + id: nanoid(), + label: 'Flex Direction', + hideLabel: true, + propertyName: 'flexDirection', + hidden: { + _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.display) !== "flex";', + _mode: 'code', + _value: false, + } as any, + buttonGroupOptions: [ + { + title: 'Row', + value: 'row', + icon: 'row' + }, + { + title: 'Column', + value: 'column', + icon: 'column' + } + ] + }, + { + id: nanoid(), + type: 'radio', + label: 'Justify Content', + hideLabel: true, + hidden: { + _code: `return (${getDisplayType} == "flex"` + + ' && getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.flexDirection) == "column")' + + ` || ${getDisplayType} == "inline-grid"`, + _mode: 'code', + _value: false, + } as any, + propertyName: 'justifyContent', + buttonGroupOptions: [ + { + title: 'Left', + value: 'left', + icon: 'alignHorizontalLeft' + }, + { + title: 'Center', + value: 'center', + icon: 'alignHorizontalCenter' + }, + { + title: 'Right', + value: 'right', + icon: 'alignHorizontalRight' + } + ] + }, + { + type: 'radio', + id: nanoid(), + label: 'Align Items', + hideLabel: true, + propertyName: 'alignItems', + hidden: { + _code: `return ${getDisplayType} == "flex"` + ' && getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.flexDirection) == "column"', + _mode: 'code', + _value: false, + } as any, + buttonGroupOptions: [ + { + title: 'Start', + value: 'start', + icon: 'alignVerticalTop' + }, + { + title: 'Center', + value: 'center', + icon: 'alignVerticalCenter' + }, + { + title: 'End', + value: 'end', + icon: 'alignVerticalBottom' + } + ] + }, + { + type: 'radio', + id: nanoid(), + label: 'Align Items', + hideLabel: true, + hidden: { + _code: `return ${getDisplayType} !== "flex"` + ' || getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.flexDirection) !== "column"', + _mode: 'code', + _value: false, + } as any, + propertyName: 'alignItems', + buttonGroupOptions: [ + { + title: 'Start', + value: 'start', + icon: 'alignHorizontalLeft' + }, + { + title: 'Center', + value: 'center', + icon: 'alignHorizontalCenter' + }, + { + title: 'End', + value: 'end', + icon: 'alignHorizontalRight' + } + ] + }, + { + type: 'radio', + id: nanoid(), + label: 'Justify Content', + hideLabel: true, + propertyName: 'justifyContent', + hidden: { + _code: `return ${getDisplayType} !== "flex"` + ' || getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.flexDirection) !== "column"', + _mode: 'code', + _value: false, + } as any, + buttonGroupOptions: [ + { + title: 'Start', + value: 'start', + icon: 'alignVerticalTop' + }, + { + title: 'Center', + value: 'center', + icon: 'alignVerticalCenter' + }, + { + title: 'End', + value: 'end', + icon: 'alignVerticalBottom' + } + ] + }, + { + type: 'button', + id: nanoid(), + label: 'Show Advanced', + hideLabel: true, + propertyName: 'showAdvanced', + icon: 'tuneIcon' + } + ] + }) + .toJson() + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: displayCollapsiblePanelId, + hidden: { + _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.display) !== "flex";', + _mode: 'code', + _value: false, + } as any, + inputs: [{ + id: nanoid(), + type: 'textField', + label: 'Gap', + propertyName: 'gap', + description: 'Examples of a valid gap include: `10` | `10px` | `20px 20px`', + }] - ], - validate: {}, - description: - 'Determines visibility and position of the box shadow.', - }) - .toJson(), - ], - }, - }) - .addCollapsiblePanel({ - id: 'pnlc68f29dc-6af4-4ad6-8a30-c71681aca484', - propertyName: 'pnlBorder', - parentId: 'root', - label: 'Border', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl75426727-1739-4812-9eac-9da4d950d311', - components: [...new DesignerToolbarSettings() - .addTextField({ - id: '4ed82d6b-b297-4757-aa3d-6428337d0a18', - propertyName: 'borderWidth', - label: 'Border Width', - labelAlign: 'right', - parentId: 'pnlc68f29dc-6af4-4ad6-8a30-c71681aca484', - validate: {}, - description: - 'Container border width (can px)', - }) - .addDropdown({ - id: nanoid(), - propertyName: 'borderStyle', - label: 'Border Type', - labelAlign: 'right', - dataSourceType: "values", - values: [ - { - label: "Solid", - value: "solid", - id: nanoid() - }, - { - label: "Dashed", - value: "dashed", - id: nanoid() - }, - { - label: "Dotted", - value: "dotted", - id: nanoid() - } - ] - }) - .addColorPicker({ - id: nanoid(), - propertyName: 'borderColor', - label: 'Border Color', - title: 'Choose Border color', - allowClear: true, - showText: true, - }) - .addTextField({ - id: '31598a37-c351-4c3c-8f87-3809d2ebaa85', - propertyName: 'borderRadius', - label: 'Border Radius', - labelAlign: 'right', - parentId: 'pnlc68f29dc-6af4-4ad6-8a30-c71681aca484', - validate: {}, - description: - 'Container corner radius (can px or %)', - }) - .toJson() - ] - } - }) + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: displayCollapsiblePanelId, + hidden: { + _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.display) !== "grid" && getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.display) !== "inline-grid";', + _mode: 'code', + _value: false, + } as any, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Gap', + propertyName: 'gap', + description: 'Examples of a valid gap include: `10` | `10px` | `20px 20px`', + }, + { + type: 'numberField', + id: nanoid(), + propertyName: 'gridColumnsCount', + parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', + label: 'Grid Columns Count', + description: 'Number of columns each grid should have', + }, + ], + }) + .toJson() + ] + }) + .addContainer({ + id: nonBlockContainerId, + parentId: displayCollapsiblePanelId, + hidden: { + _code: `return ${getDisplayType} == "block"` + + '|| !getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.showAdvanced)', + _mode: 'code', + _value: false, + } as any, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: displayCollapsiblePanelId, + inline: false, + hidden: { + _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.display) !== "flex";', + _mode: 'code', + _value: false, + } as any, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Flex Direction', + propertyName: 'flexDirection', + hidden: { + _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.display) !== "flex";', + _mode: 'code', + _value: false, + } as any, + dropdownOptions: FLEX_DIRECTION, + description: 'The flex-direction CSS property sets how flex items are placed in the flex container defining the main axis and the direction (normal or reversed).', + + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Flex Wrap', + propertyName: 'flexWrap', + dropdownOptions: FLEX_WRAP, + hidden: { + _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.flexDirection) !== "row";', + _mode: 'code', + _value: false, + } as any, + description: 'The flex-wrap CSS property sets whether flex items are forced into multiple lines and the direction of that wrapping.' + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: displayCollapsiblePanelId, + inline: false, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Justify Content', + propertyName: 'justifyContent', + dropdownOptions: JUSTIFY_CONTENT + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align Items', + propertyName: 'alignItems', + dropdownOptions: [...ALIGN_ITEMS, ...ALIGN_ITEMS_GRID] + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: displayCollapsiblePanelId, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Align Self', + propertyName: 'alignSelf', + dropdownOptions: ALIGN_SELF + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Justify Items', + propertyName: 'justifyItems', + hidden: { + _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.display) === "flex";', + _mode: 'code', + _value: false, + } as any, + dropdownOptions: JUSTIFY_ITEMS + } + ] + }) + .addSettingsInput({ + inputType: 'dropdown', + id: nanoid(), + label: 'Justify Self', + propertyName: 'justifySelf', + hidden: { + _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.display) === "flex";', + _mode: 'code', + _value: false, + } as any, + dropdownOptions: JUSTIFY_SELF + }) + .toJson() + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: dimensionsStyleCollapsiblePanelId, + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: dimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" - .addCollapsiblePanel({ - id: nanoid(), - propertyName: 'pnlBackground', - parentId: 'root', - label: 'Background', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: nanoid(), - components: [...new DesignerToolbarSettings() - .addDropdown({ - id: nanoid(), - propertyName: 'backgroundType', - label: 'Background Type', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - dataSourceType: "values", - values: [ - { - label: "Color", - value: "color", - id: nanoid() - }, - { - label: "Image", - value: "image", - id: nanoid() - } + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: borderStyleCollapsiblePanelId, + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: borderStylePnlId, + components: [...new DesignerToolbarSettings() + + .addContainer({ + id: nanoid(), + parentId: borderStylePnlId, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: borderStylePnlId, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: backgroundStyleCollapsiblePanelId, + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: backgroundStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: backgroundStylePnlId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + hideLabel: true, + propertyName: "background.size", + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: shadowStyleCollapsiblePanelId, + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: shadowStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: shadowStyleRowId, + parentId: shadowStylePnlId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: styleCollapsiblePanelId, + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: stylePnlId, + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: styleBoxPnlId, + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: customStyleCollapsiblePanelId, + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: stylePnlCustomId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'textField', + propertyName: 'className', + label: 'Custom CSS Class', + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'wrapperStyle', + label: 'Wrapper Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson()] + }).toJson()] + }, + { + key: '5', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson() ] - }) - .addColorPicker({ - id: nanoid(), - propertyName: 'backgroundColor', - label: 'Color', - title: 'Choose Background color', - allowClear: true, - showText: true, - hidden: { - _code: "return getSettingValue(data?.backgroundType) !== 'color';", - _mode: 'code', - _value: false, - } as any - }) - .addDropdown({ - id: nanoid(), - propertyName: 'backgroundDataSource', - label: 'Image Source Type', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - dataSourceType: "values", - values: [ - { - label: "StoredFile", - value: "storedFileId", - id: nanoid() - }, - { - label: "Url", - value: "url", - id: nanoid() - }, - { - label: "Base64", - value: "base64", - id: nanoid() - } - ], - hidden: { - _code: "return getSettingValue(data?.backgroundType) !== 'image';", - _mode: 'code', - _value: false, - } as any - }) - .addDropdown({ - id: nanoid(), - propertyName: 'backgroundCover', - label: 'Background Size', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - dataSourceType: "values", - hidden: { - _code: "return getSettingValue(data?.backgroundType) !== 'image';", - _mode: 'code', - _value: false, - } as any, - values: [ - { - label: "Contain", - value: "contain", - id: nanoid() - }, - { - label: "Cover", - value: "cover", - id: nanoid() - } - ], - }) - .addDropdown({ - id: nanoid(), - propertyName: 'backgroundRepeat', - label: 'Background Repeat', - labelAlign: 'right', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - dataSourceType: "values", - hidden: { - _code: "return getSettingValue(data?.backgroundType) !== 'image';", - _mode: 'code', - _value: false, - } as any, - values: [ - { - label: "Repeat", - value: "repeat", - id: nanoid() - }, - { - label: "No Repeat", - value: "no-repeat", - id: nanoid() - }, { - label: "Repeat-X", - value: "repeat-x", - id: nanoid() - }, - { - label: "Repeat-Y", - value: "repeat-y", - id: nanoid() - }, - { - label: "Round", - value: "round", - id: nanoid() - }, - ], - }) - .addTextField({ - id: nanoid(), - propertyName: 'backgroundUrl', - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - label: 'Background Image URL', - description: 'Add a background image URL', - hidden: { - _code: "return getSettingValue(data?.backgroundDataSource) !== 'url' || getSettingValue(data?.backgroundType) !== 'image';", - _mode: 'code', - _value: false, - } as any, - }) - .addImagePicker({ - id: nanoid(), - propertyName: "backgroundBase64", - parentId: "y9SNusmMM0Wd1Sc_YI1ng", - label: "Upload Image", - hidden: { - _code: "return getSettingValue(data?.backgroundDataSource) !== 'base64' || getSettingValue(data?.backgroundType) !== 'image';", - _mode: "code", - _value: false - } - }) - .addTextField({ - id: nanoid(), - propertyName: "backgroundStoredFileId", - parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - label: "StoredFile ID", - hidden: { - _code: "return getSettingValue(data?.backgroundDataSource) !== 'storedFileId' || getSettingValue(data?.backgroundType) !== 'image';", - _mode: 'code', - _value: false, - } as any, - }) - .toJson() - ] - } - }) - .addPropertyRouter({ - id: nanoid(), - propertyName: 'propertyRouter2', - componentName: 'propertyRouter2', - label: 'Property router2', - labelAlign: 'right', - parentId: 'root', - hidden: false, - propertyRouteName: { - _mode: "code", - _code: " return contexts.canvasContext?.designerDevice || 'desktop';", - _value: "" - }, - components: [ - ...new DesignerToolbarSettings() - .addCollapsiblePanel({ - id: 'db6e32f3-7a8b-4686-a0eb-81b2e36796ef', - propertyName: 'pnlStyle', - parentId: 'root', - label: 'Style', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: 'pnl64664-cbc9-4cef-bdbc-6fbea44cd0ca', - components: [ - ...new DesignerToolbarSettings() - .addTextField({ - id: 'e6be0d18-7f9e-4ee6-ab4a-19dacf3a3fd6', - propertyName: 'width', - label: 'Width', - labelAlign: 'right', - parentId: 'pnld3933cd3-8810-4e29-a434-c84105d46fa2', - validate: {}, - description: - 'Container default width (can be %, px, or vw)', - }) - .addTextField({ - id: '4e024dd1-e7e2-4a0e-8478-e4f6eeba1c94', - propertyName: 'minWidth', - label: 'Min Width', - labelAlign: 'right', - parentId: 'pnld3933cd3-8810-4e29-a434-c84105d46fa2', - validate: {}, - description: - 'Container minimum width (can be %, px, or vw)', - }) - .addTextField({ - id: '747d4a30-02e1-4136-ad78-7126e4d36e29', - propertyName: 'maxWidth', - label: 'Max Width', - labelAlign: 'right', - parentId: 'pnld3933cd3-8810-4e29-a434-c84105d46fa2', - validate: {}, - description: - 'Container maximum width (can be %, px, or vw)', - }) - .addTextField({ - id: '789ef4cf-843d-4cbf-a932-953d313a3293s', - propertyName: 'height', - label: 'Height', - labelAlign: 'right', - parentId: 'pnld3933cd3-8810-4e29-a434-c84105d46fa2', - validate: {}, - description: - 'Container default height (can be %, px, or vw)', - }) - .addTextField({ - id: 'e5b2ef1b-e72b-4477-8794-e1aaa33c0efa', - propertyName: 'minHeight', - label: 'Min Height', - labelAlign: 'right', - parentId: 'pnld3933cd3-8810-4e29-a434-c84105d46fa2', - validate: {}, - description: - 'Container minimum height (can be %, px, or vw)', - }) - .addTextField({ - id: '95f424dc-d307-47d8-bad8-ebe6be282cc0', - propertyName: 'maxHeight', - label: 'Max Height', - labelAlign: 'right', - parentId: 'pnld3933cd3-8810-4e29-a434-c84105d46fa2', - validate: {}, - description: - 'Container maximum height (can be %, px, or vw)', - }) - .addTextField({ - textType: 'text', - id: '648fea09-e548-44b3-9c90-2187ad63fd07', - propertyName: 'className', - label: 'Custom CSS Class', - labelAlign: 'right', - parentId: 'root', - hidden: false, - description: 'Custom CSS Class to add to this component', - validate: {}, - settingsValidationErrors: [], - }) - .addCodeEditor({ - id: '4e319199-d27c-4ed0-9934-fa2cb62745d1', - propertyName: 'style', - label: 'Style', - parentId: 'root', - mode: 'dialog', - description: 'The style that will be applied to the container', - exposedVariables: [{ id: nanoid(), name: 'data', description: 'Form data', type: 'object' }], - }) - .addCodeEditor({ - id: '71b5da4f-9300-41bf-b0c4-9d9f3757d402', - propertyName: 'wrapperStyle', - label: 'Wrapper Style', - parentId: 'root', - mode: 'dialog', - description: 'The style that will be applied to the container wrapper', - exposedVariables: [{ id: nanoid(), name: 'data', description: 'Form data', type: 'object' }], - hidden: { _code: 'return getSettingValue(data?.noDefaultStyling);', _mode: 'code', _value: false } as any, - }) - .addStyleBox({ - id: 'c26c0e0d-f3f6-425f-a8d0-e69f6a4139bd', - propertyName: 'stylingBox', - parentId: 'root', - validate: {}, - settingsValidationErrors: [], - jsSetting: false, - }).toJson(), - ], - }, - }).toJson(), - ] - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() + } ] - } - }) - .toJson(); + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/container/styles.ts b/shesha-reactjs/src/designer-components/container/styles.ts new file mode 100644 index 0000000000..e40b0131be --- /dev/null +++ b/shesha-reactjs/src/designer-components/container/styles.ts @@ -0,0 +1,15 @@ +import { createStyles, sheshaStyles } from '@/styles'; + +export const useStyles = createStyles(({ css, cx }) => { + + const container = cx("sha-container-component", css` + .sha-components-container-inner { + ${sheshaStyles.thinScrollbars} + overflow: auto; + } + `); + + return { + container + }; +}); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/contextPropertyAutocomplete/index.tsx b/shesha-reactjs/src/designer-components/contextPropertyAutocomplete/index.tsx index 64e99aba3f..a160436f60 100644 --- a/shesha-reactjs/src/designer-components/contextPropertyAutocomplete/index.tsx +++ b/shesha-reactjs/src/designer-components/contextPropertyAutocomplete/index.tsx @@ -1,18 +1,19 @@ -import ConditionalWrap from '@/components/conditionalWrapper'; -import React, { FC, useEffect, useState } from 'react'; +import React, { CSSProperties, FC, useCallback, useMemo, useState } from 'react'; import settingsFormJson from './settingsForm.json'; -import { Button, Form, Input } from 'antd'; +import { Button, Input } from 'antd'; import { DataContextSelector } from '@/designer-components/dataContextSelector'; import { FileSearchOutlined } from '@ant-design/icons'; import { FormMarkup } from '@/providers/form/models'; import { getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; -import { IConfigurableFormComponent, MetadataProvider, useForm } from '@/providers'; +import { IConfigurableFormComponent, MetadataProvider } from '@/providers'; import { IToolboxComponent } from '@/interfaces'; import { MetadataType } from '@/providers/metadata/contexts'; import { PropertyAutocomplete } from '@/components/propertyAutocomplete/propertyAutocomplete'; -import { useFormDesignerState } from '@/providers/formDesigner'; +import { useFormDesignerStateSelector } from '@/providers/formDesigner'; import SettingsControl from '../_settings/settingsControl'; import { getValueFromPropertySettings } from '../_settings/utils'; +import { useStyles } from '../_settings/styles/styles'; +import { ConfigurableFormItem } from '@/components'; const settingsForm = settingsFormJson as FormMarkup; @@ -21,12 +22,17 @@ export interface IContextPropertyAutocompleteComponentProps extends IConfigurabl mode?: 'single' | 'multiple'; modelType?: string; autoFillProps?: boolean; + styledLabel?: boolean; } -export interface IContextPropertyAutocompleteProps extends Omit { +export interface IContextPropertyAutocompleteProps extends Omit { + componentName: string; + propertyName: string; + contextName: string; + style?: CSSProperties; + dropdownStyle?: CSSProperties; defaultModelType: string; - formData: any; onValuesChange?: (changedValues: any) => void; } @@ -38,100 +44,130 @@ interface IContextPropertyAutocompleteState { } export const ContextPropertyAutocomplete: FC = (model) => { - const { defaultModelType, readOnly, formData, onValuesChange } = model; - - const [state, setState] = useState(); - - useEffect(() => { - if (!state && formData?.propertyName) - setState({ - mode: !!formData?.context || formData?.propertyName !== formData?.componentName - ? 'context' - : 'formData', - context: formData?.context, - propertyName: formData?.propertyName, - componentName: formData?.componentName - }); - }, [formData]); + const { + defaultModelType, + readOnly, + componentName, + propertyName, + contextName, + style, + dropdownStyle, + onValuesChange + } = model; + + const [state, setState] = useState({ + componentName, + propertyName, + context: contextName, + mode: contextName || propertyName !== componentName ? 'context' : 'formData' + }); + const { styles } = useStyles(); + + // ToDo: AS - remove after full optimization + /*useEffect(() => { + const init = {componentName: undefined, propertyName: undefined, context: undefined, mode: undefined}; + if (componentName && !state?.componentName) + init.componentName = componentName; + if (propertyName && !state?.propertyName) + init.propertyName = propertyName; + if (contextName && !state?.context) + init.context = contextName; + if (!state?.mode && (contextName || propertyName !== componentName)) + init.mode = contextName || propertyName !== componentName ? 'context' : 'formData'; + setState(init); + }, [componentName, propertyName, contextName]);*/ const setContextMode = () => { setState({ ...state, mode: 'context' }); - onValuesChange({ context: state?.context }); + onValuesChange({ context: state?.context, componentName: state?.componentName }); }; const setFormDataMode = () => { setState({ ...state, mode: 'formData' }); - onValuesChange({ context: null }); + onValuesChange({ context: null, componentName: state?.propertyName }); }; const mode = state?.mode ?? 'formData'; - const context = !!state?.context && mode === 'context' ? state?.context : undefined; + const context = state?.context && mode === 'context' ? state?.context : undefined; - const contextlabel = ; - const componentlabel = ; - const propertylabel = ; + const styledLabel = (label: string) => {label}; + const contextlabel = model.styledLabel ? styledLabel("Context") : ; + const componentlabel = model.styledLabel ? styledLabel("Component Name") : ; + const propertylabel = model.styledLabel ? styledLabel("Property Name") : ; const modelType = !context || mode === 'formData' ? defaultModelType : context; const dataType: MetadataType = !context || mode === 'formData' ? 'entity' : 'context'; return ( <> - - - {content}} - > - - { - const changedData = { propertyName: value }; - if (state?.mode === 'formData') - changedData['componentName'] = getValueFromPropertySettings(value); - setState(prev => ({ ...prev, ...changedData } as IContextPropertyAutocompleteState)); - onValuesChange(changedData); + + {(value, onChange) => { + return { + const value = e.target.value; + if (value !== undefined) { + setState(prev => ({ ...prev, componentName: value })); + onChange(value); + } }} - value={formData.propertyName as any} + size={model.size} + />; + }} + + + {(value, onChange) => { + return - {(value, onChange) => { - return ; + value={value} + onChange={(value) => { + onChange(value); + setState({ ...state, context: value }); }} - - - + />; + }} + + + + {(value, onChange) => { + return { + if (value !== undefined) { + const changedData = { propertyName: value }; + if (state?.mode === 'formData') + changedData['componentName'] = getValueFromPropertySettings(value); + if (state.propertyName !== value) { + setState(prev => ({ ...prev, ...changedData } as IContextPropertyAutocompleteState)); + onChange(value); + onValuesChange(changedData); + } + } + }} + value={value} + readOnly={readOnly} + > + {(valueSettings, onChangeSettings) => { + return ; + }} + ; + }} + + @@ -142,29 +178,55 @@ export const ContextPropertyAutocomplete: FC ); }; -const ContextPropertyAutocompleteComponent: IToolboxComponent = { +interface IContextPropertyAutocompleteCalculatedModel { + componentName: string; + propertyName: string; + contextName: string; + style: CSSProperties; + dropdownStyle: CSSProperties; + modelType: string; + setFieldsValue: (values: any) => void; +} + +const emptyObj = {}; + +const ContextPropertyAutocompleteComponent: IToolboxComponent = { type: 'contextPropertyAutocomplete', name: 'Context Property Autocomplete', icon: , isInput: true, isOutput: true, - Factory: ({ model }) => { - const designerModelType = useFormDesignerState(false)?.formSettings?.modelType; - const form = useForm(); - const { formData, formSettings, setFormData } = form; - - if (model.hidden) return null; - - return ( - { + const designerModelType = useFormDesignerStateSelector(x => x.formSettings?.modelType); + const validate = useMemo(() => ({...model.validate, required: false}), [model.validate]); + const onValuesChange = useCallback(values => calculatedModel.setFieldsValue(values), [calculatedModel.setFieldsValue]); + + return model.hidden + ? null + : { - setFormData({ values: { ...values }, mergeValues: true }); - }} + styledLabel={model.styledLabel} + defaultModelType={designerModelType ?? calculatedModel.modelType} + onValuesChange={onValuesChange} /> - ); + ; }, settingsFormMarkup: settingsForm, validateSettings: model => validateConfigurableComponentSettings(settingsForm, model), diff --git a/shesha-reactjs/src/designer-components/dataContextComponent/settings.tsx b/shesha-reactjs/src/designer-components/dataContextComponent/settings.tsx index 38e0c56abb..ac16947cd4 100644 --- a/shesha-reactjs/src/designer-components/dataContextComponent/settings.tsx +++ b/shesha-reactjs/src/designer-components/dataContextComponent/settings.tsx @@ -171,7 +171,6 @@ const DataContextSettings: FC setOpen(false)} width={'50%'} - okButtonProps={{}} > { onValuesChange({ items: value?.map((item) => convertModelItemToPropertyMetadata(item)) }); diff --git a/shesha-reactjs/src/designer-components/dataContextSelector/index.tsx b/shesha-reactjs/src/designer-components/dataContextSelector/index.tsx index 19f11662c7..2447122e8d 100644 --- a/shesha-reactjs/src/designer-components/dataContextSelector/index.tsx +++ b/shesha-reactjs/src/designer-components/dataContextSelector/index.tsx @@ -7,16 +7,18 @@ import { DataTypes, StringFormats } from '@/interfaces/dataTypes'; import { Select } from 'antd'; import { useDataContextManager } from '@/providers/dataContextManager'; import { useDataContext } from '@/providers/dataContextProvider/contexts'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; export interface IDataContextSelectorProps { readOnly?: boolean; value?: TValue; + size?: SizeType; onChange?: (value: TValue) => void; } const DataContextSelector: FC = (props) => { // Use parent DCM because this control usually used in the properties form and has own DCM const dcm = useDataContextManager(); - const { getDataContexts } = dcm.parent ?? dcm.getRoot(); + const { getDataContexts } = dcm.getParent() ?? dcm.getRoot(); const dataContext = useDataContext(false); const dataContexts = getDataContexts(dataContext?.id); @@ -26,7 +28,7 @@ const DataContextSelector: FC = (props) => { }; return ( - {dataContexts.map((item) => { return {item.name}; })} diff --git a/shesha-reactjs/src/designer-components/dataList/dataListComponent.tsx b/shesha-reactjs/src/designer-components/dataList/dataListComponent.tsx index ba8c530cf4..0b1f7e227f 100644 --- a/shesha-reactjs/src/designer-components/dataList/dataListComponent.tsx +++ b/shesha-reactjs/src/designer-components/dataList/dataListComponent.tsx @@ -3,13 +3,15 @@ import { UnorderedListOutlined } from "@ant-design/icons"; import { IToolboxComponent } from "@/interfaces"; import { useDataSources } from '@/providers/dataSourcesProvider'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; -import { DataListSettingsForm } from './dataListSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { IDataListComponentProps } from './model'; import DataListControl, { NotConfiguredWarning } from './dataListControl'; import { useDataTableStore } from '@/providers'; import { migrateNavigateAction } from '@/designer-components/_common-migrations/migrate-navigate-action'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; const DataListComponent: IToolboxComponent = { type: 'datalist', @@ -53,8 +55,10 @@ const DataListComponent: IToolboxComponent = { onNewListItemInitialize: migrateFormApi.full(prev.onNewListItemInitialize), onListItemSave: migrateFormApi.full(prev.onListItemSave) })) + .add(8, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })) + , - settingsFormFactory: (props) => (), +settingsFormMarkup: (data) => getSettings(data), }; export default DataListComponent; diff --git a/shesha-reactjs/src/designer-components/dataList/dataListControl.tsx b/shesha-reactjs/src/designer-components/dataList/dataListControl.tsx index 5366c49876..7ba731799e 100644 --- a/shesha-reactjs/src/designer-components/dataList/dataListControl.tsx +++ b/shesha-reactjs/src/designer-components/dataList/dataListControl.tsx @@ -42,7 +42,8 @@ const DataListControl: FC = (props) => { readOnly, noDataText = "No Data", noDataSecondaryText = "No data is available for this list", - noDataIcon + noDataIcon, + allStyles } = props; const { tableData, @@ -217,6 +218,7 @@ const DataListControl: FC = (props) => { { + + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const appearanceTabId = nanoid(); + const eventsTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const containerStylePnlId = nanoid(); + const containerDimensionsStylePnlId = nanoid(); + + const ROW_SAVE_EXPOSED_VARIABLES = [ + { + id: nanoid(), + name: 'data', + description: 'Current row data', + type: 'object', + }, + { + id: nanoid(), + name: 'formData', + description: 'Form values', + type: 'object', + }, + { + id: nanoid(), + name: 'globalState', + description: 'The global state of the application', + type: 'object', + }, + { + id: nanoid(), + name: 'http', + description: 'axios instance used to make http requests', + type: 'object', + }, + { + id: nanoid(), + name: 'moment', + description: 'The moment.js object', + type: 'object', + } + ].map(item => JSON.stringify(item)); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: nanoid(), + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'textField', + propertyName: "componentName", + parentId: commonTabId, + label: "Component Name", + validate: { + required: true + }, + jsSetting: false, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: "hidden", + parentId: commonTabId, + label: "Hide", + jsSetting: true, + value: 'checked', + }) + .toJson() + ] + }, + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: "formSelectionMode", + parentId: dataTabId, + label: "Form Selection Mode", + jsSetting: false, + dropdownOptions: [ + { label: 'Name', value: 'name' }, + { label: 'Dynamic', value: 'view' }, + { label: 'Expression', value: 'expression' }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.formSelectionMode) !== "view";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'formType', + label: 'Form Type', + parentId: dataTabId, + type: 'formTypeAutocomplete', + jsSetting: true, + width: '100%', + allowClear: true, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.formSelectionMode) !== "name";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'formId', + label: 'Form', + type: 'formAutocomplete', + labelAlign: 'right', + hidden: false, + validate: { + required: true, + }, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.formSelectionMode) !== "expression";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: "formIdExpression", + label: "Form Identifier Expression", + hidden: { _code: 'return getSettingValue(data?.formSelectionMode) !== "expression";', _mode: 'code', _value: false } as any, + jsSetting: false, + description: "Enter code to get form identifier. You must return { name: string; module?: string; version?: number; } object. The global variable data is provided, and allows you to access the data of any form component, by using its API key.", + exposedVariables: [ + `{ name: "item", description: "List item", type: "object" }`, + `{ name: "data", description: "Selected form values", type: "object" }`, + `{ name: "contexts", description: "Contexts data", type: "object" }`, + `{ name: "globalState", description: "The global model of the application", type: "object" }`, + `{ name: "setGlobalState", description: "Setting the global state of the application", type: "function" }`, + `{ name: "formMode", description: "Form mode", type: "object" }`, + `{ name: "form", description: "Form instance", type: "object" }`, + `{ name: "selectedListItem", description: "Selected list item of nearest table (null if not available)", type: "object" }`, + `{ name: "moment", description: "moment", type: "object" }`, + `{ name: "http", description: "axiosHttp", type: "object" }`, + `{ name: "message", description: "message framework", type: "object" }`, + ], + } + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: "selectionMode", + parentId: dataTabId, + label: "Selection Mode", + jsSetting: true, + dropdownOptions: [ + { label: 'None', value: 'none' }, + { label: 'Single', value: 'single' }, + { label: 'Multiple', value: 'multiple' }, + ], + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'addInlineData', + label: 'Can Add Inline', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: 'addInlineDataContent', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'canAddInline', + label: 'Can Add Inline', + inputType: 'dropdown', + parentId: dataTabId, + jsSetting: true, + dropdownOptions: [ + { value: 'yes', label: 'Yes' }, + { value: 'no', label: 'No' }, + { value: 'inherit', label: 'Inherit' }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.canAddInline) === "no" || getSettingValue(data?.formSelectionMode) === "view";', + _mode: 'code', + _value: false + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'createFormId', + label: 'Form', + type: 'formAutocomplete', + labelAlign: 'right', + hidden: false, + jsSetting: true, + validate: { + required: true, + }, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.canAddInline) === "no" || getSettingValue(data?.formSelectionMode) !== "view";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'createFormType', + label: 'Form Type', + type: 'formTypeAutocomplete', + jsSetting: true, + width: '100%', + allowClear: true, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canAddInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'customCreateUrl', + label: 'Custom Create URL', + type: 'endpointsAutocomplete', + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canAddInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [{ + id: nanoid(), + type: 'codeEditor', + propertyName: 'onNewListItemInitialize', + label: 'New List Item Init', + jsSetting: false, + tooltip: 'Custom code to initialize new list items with default values or setup logic', + }, + ], + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canAddInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'modalWidth', + customTooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + label: 'Dialog Width', + parentId: dataTabId, + type: 'customDropdown', + allowClear: true, + jsSetting: true, + customDropdownMode: 'single', + dropdownOptions: [ + { + label: 'Small', + value: '40%', + }, + { + label: 'Medium', + value: '60%', + }, + { + label: 'Large', + value: '80%', + } + ], + width: '60%', + }, + ], + hideLabel: true, + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'editInlineData', + label: 'Can Edit Inline', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: 'editInlineDataContent', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'canEditInline', + label: 'Can Edit Inline', + inputType: 'dropdown', + parentId: dataTabId, + jsSetting: true, + dropdownOptions: [ + { value: 'yes', label: 'Yes' }, + { value: 'no', label: 'No' }, + { value: 'inherit', label: 'Inherit' }, + ], + }) + .addSettingsInputRow({ + parentId: dataTabId, + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canEditInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'inlineEditMode', + label: 'Edit Mode', + type: 'dropdown', + dropdownOptions: [ + { value: 'all-at-once', label: 'All at Once' }, + { value: 'one-by-one', label: 'One by One' }, + ], + }, + { + id: nanoid(), + propertyName: 'inlineSaveMode', + label: 'Save Mode', + type: 'dropdown', + dropdownOptions: [ + { value: 'auto', label: 'Auto' }, + { value: 'manual', label: 'Manual' }, + ], + } + ] + }) + .addSettingsInputRow({ + parentId: dataTabId, + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canEditInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'customUpdateUrl', + label: 'Custom Update URL', + type: 'endpointsAutocomplete', + parentId: dataTabId, + jsSetting: true, + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + parentId: dataTabId, + propertyName: 'deleteInlineData', + label: 'Can Delete Inline', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'canDeleteInline', + label: 'Can Delete Inline', + inputType: 'dropdown', + parentId: dataTabId, + jsSetting: true, + dropdownOptions: [ + { value: 'yes', label: 'Yes' }, + { value: 'no', label: 'No' }, + { value: 'inherit', label: 'Inherit' }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canDeleteInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'customDeleteUrl', + label: 'Custom Delete URL', + type: 'endpointsAutocomplete', + parentId: dataTabId, + jsSetting: true, + } + ] + }).toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + parentId: dataTabId, + propertyName: 'createEditContent', + label: 'Create & Edit Methods', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: 'createEditContentId', + components: [...new DesignerToolbarSettings() + .addConfigurableActionConfigurator({ + id: nanoid(), + propertyName: 'onListItemSaveSuccessAction', + label: 'On List Item Save Action', + hideLabel: true, + description: 'Custom Action configuration executed when saving list items (validation, calculations, etc.)', + }) + .addSettingsInputRow({ + id: nanoid(), + inputs: [{ + id: nanoid(), + type: 'codeEditor', + propertyName: 'onListItemSave', + label: 'On List Item Save', + jsSetting: false, + tooltip: 'Custom business logic executed when saving list items (validation, calculations, etc.)', + } + ], + hideLabel: true, + }) + .toJson() + ] + } + }) + .toJson() + ] + }, + { + key: 'events', + title: 'Events', + id: eventsTabId, + components: [...new DesignerToolbarSettings() + .addConfigurableActionConfigurator({ + id: nanoid(), + propertyName: "dblClickActionConfiguration", + parentId: eventsTabId, + label: "On Double-Click", + jsSetting: false, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'onRowSave', + label: 'On Row Save', + inputType: 'codeEditor', + parentId: eventsTabId, + tooltip: 'Custom business logic to be executed on saving of new/updated row (e.g. custom validation / calculations). This handler should return an object or a Promise.', + hidden: { _code: 'return getSettingValue(data?.canAddInline) === "no" && getSettingValue(data?.canEditInline) === "no";', _mode: 'code', _value: false } as any, + description: 'Allows custom business logic to be executed on saving of new/updated row (e.g. custom validation / calculations).', + exposedVariables: ROW_SAVE_EXPOSED_VARIABLES, + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: styleRouterId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "background.size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + }, + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlContainerStyle', + label: 'Container Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: containerStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: "orientation", + parentId: containerStylePnlId, + label: "Orientation", + jsSetting: true, + defaultValue: 'horizontal', + dropdownOptions: [ + { label: 'Vertical', value: 'vertical' }, + { label: 'Horizontal', value: 'horizontal' }, + { label: 'Wrap', value: 'wrap' }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: containerStylePnlId, + //hidden: { _code: 'return getSettingValue(data?.orientation) !== "wrap";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'gap', + label: 'Gap', + type: 'numberField', + description: 'The gap between the datalist cards.', + jsSetting: true, + } + ] + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'containerDimensionsPanel', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: containerDimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: containerDimensionsStylePnlId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.orientation) === "vertical";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "container.dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit", + defaultValue: '600px', + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "container.dimensions.minWidth", + icon: "minWidthIcon", + defaultValue: 'auto', + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "container.dimensions.maxWidth", + icon: "maxWidthIcon", + defaultValue: 'auto', + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: containerDimensionsStylePnlId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.orientation) === "horizontal";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "container.dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit", + defaultValue: '600px', + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "container.dimensions.minHeight", + icon: "minHeightIcon", + defaultValue: 'auto', + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "container.dimensions.maxHeight", + icon: "maxHeightIcon", + defaultValue: 'auto', + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'containerStylingBoxPanel', + label: 'Margin & Padding', + labelAlign: 'right', + collapsible: 'header', + ghost: true, + parentId: styleRouterId, + content: { + id: 'containerStylingBoxPanel', + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'container.stylingBox', + parentId: 'containerStylingBoxPanel' + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'containerCustomStylePanel', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: 'containerCustomStylePanel', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'container.style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + parentId: 'containerCustomStylePanel' + }) + .toJson() + ] + } + }) + .toJson()] + } + }) + .toJson()] + }) + + //new props + //to remove + + // .addSettingsInputRow({ + // id: nanoid(), + // inputs: [ + // { + // id: nanoid(), + // type: 'textField', + // propertyName: "cardMinWidth", + // label: "Card Minimum Width", + // tooltip: "You can use any unit (%, px, em, etc)", + // hidden: { _code: 'return getSettingValue(data?.orientation) !== "wrap";', _mode: 'code', _value: false } as any, + // jsSetting: false, + // }, + // { + // id: nanoid(), + // type: 'textField', + // propertyName: "cardMaxWidth", + // label: "Card Maximum Width", + // tooltip: "You can use any unit (%, px, em, etc)", + // hidden: { _code: 'return getSettingValue(data?.orientation) !== "wrap";', _mode: 'code', _value: false } as any, + // jsSetting: false, + // } + // ] + // }) + // .addSettingsInputRow({ + // id: nanoid(), + // inputs: [ + // { + // id: nanoid(), + // type: 'textField', + // propertyName: "cardHeight", + // label: "Card Height", + // tooltip: "You can use any unit (%, px, em, etc)", + // hidden: { _code: 'return getSettingValue(data?.orientation) !== "wrap";', _mode: 'code', _value: false } as any, + // jsSetting: false, + // }, + // { + // id: nanoid(), + // type: 'textField', + // propertyName: "gap", + // label: "Gap", + // tooltip: "You can use any unit (%, px, em, etc)", + // hidden: { _code: 'return getSettingValue(data?.orientation) !== "wrap";', _mode: 'code', _value: false } as any, + // jsSetting: false, + // } + // ] + // }) + // .addSettingsInputRow({ + // id: nanoid(), + // inputs: [ + // { + // id: nanoid(), + // type: 'switch', + // propertyName: 'showBorder', + // label: "Show Border", + // hidden: { _code: 'return getSettingValue(data?.orientation) !== "wrap";', _mode: 'code', _value: false } as any, + // jsSetting: false, + // } + // ] + // }) + + + //legacy + // .addSettingsInputRow({ + // id: nanoid(), + // inputs: [{ + // id: nanoid(), + // type: 'customDropdown', + // propertyName: "listItemWidth", + // parentId: appearanceTabId, + // label: "List Item Width", + // hidden: { _code: 'return getSettingValue(data?.orientation) !== "horizontal";', _mode: 'code', _value: false } as any, + // jsSetting: true, + // customDropdownMode: 'single', + // dropdownOptions: [ + // { label: '100%', value: '1' }, + // { label: '50%', value: '0.5' }, + // { label: '33%', value: '0.33' }, + // { label: '25%', value: '0.25' }, + // { label: '(Custom)', value: 'custom' }, + // ], + // }] + // }) + // .addSettingsInputRow({ + // id: nanoid(), + // hidden: { _code: 'return getSettingValue(data?.orientation) !== "horizontal" || getSettingValue(data?.listItemWidth) !== "custom";', _mode: 'code', _value: false } as any, + // inputs: [ + // { + // id: nanoid(), + // type: 'numberField', + // propertyName: "customListItemWidth", + // label: "Custom List Item Width (px)", + // jsSetting: false, + // } + // ] + // }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlGroup', + label: 'Group Styles', + parentId: appearanceTabId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + inputs: [{ + id: nanoid(), + type: "switch", + propertyName: "collapsible", + label: "Collapsible", + labelAlign: "right", + hidden: false, + jsSetting: true, + }, + { + id: nanoid(), + type: "switch", + propertyName: "collapseByDefault", + label: "Collapsible By Default", + labelAlign: "right", + hidden: false, + jsSetting: true, + }], + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + inputs: [{ + id: nanoid(), + type: 'codeEditor', + propertyName: "groupStyle", + parentId: appearanceTabId, + label: "Header Style", + jsSetting: false, + exposedVariables: [ + { name: "data", description: "Selected form values", type: "object" }, + ], + } + ], + hideLabel: true, + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + parentId: appearanceTabId, + propertyName: 'datalistEmptyState', + label: 'Empty State', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'noDataIcon', + label: 'Icon', + inputType: 'iconPicker', + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'noDataText', + label: 'Primary Text', + inputType: 'textField', + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'noDataSecondaryText', + label: 'Secondary Text', + inputType: 'textField', + jsSetting: true, + }).toJson() + ] + } + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: "Permissions", + tooltip: "Enter a list of permissions that should be associated with this component", + jsSetting: true, + }) + .toJson() + ] + }, + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dataList/utils.ts b/shesha-reactjs/src/designer-components/dataList/utils.ts new file mode 100644 index 0000000000..726d3e97b1 --- /dev/null +++ b/shesha-reactjs/src/designer-components/dataList/utils.ts @@ -0,0 +1,21 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + return { + background: { type: 'color', color: '#fff' }, + font: { weight: '400', size: 14, color: '#000', type: 'Segoe UI' }, + border: { + border: { + all: { width: '0px', style: 'solid', color: '#d9d9d9' }, + top: { width: '0px', style: 'solid', color: '#d9d9d9' }, + bottom: { width: '0px', style: 'solid', color: '#d9d9d9' }, + left: { width: '0px', style: 'solid', color: '#d9d9d9' }, + right: { width: '0px', style: 'solid', color: '#d9d9d9' }, + }, + radius: { all: 8, topLeft: 8, topRight: 8, bottomLeft: 8, bottomRight: 8 }, + borderType: 'all', + radiusType: 'all' + }, + dimensions: { width: '100%', height: 'auto', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dataSource/dataSource.tsx b/shesha-reactjs/src/designer-components/dataSource/dataSource.tsx index d560bf92fe..00edf73cdb 100644 --- a/shesha-reactjs/src/designer-components/dataSource/dataSource.tsx +++ b/shesha-reactjs/src/designer-components/dataSource/dataSource.tsx @@ -6,7 +6,7 @@ import { evaluateDynamicFilters } from '@/utils'; import { IDataSourceComponentProps } from './models'; import { MetadataProvider, - useDataContextManager, + useDataContextManagerActions, useDataTableStore, useForm, useGlobalState, @@ -29,7 +29,7 @@ const DataSourceAccessor: FC = ({ id, propertyName: n } = dataSource; const { globalState } = useGlobalState(); - const pageContext = useDataContextManager(false)?.getPageContext(); + const pageContext = useDataContextManagerActions(false)?.getPageContext(); const isDesignMode = formMode === 'designer'; diff --git a/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/advancedFilterButton.tsx b/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/advancedFilterButton.tsx index a48f834b89..2be1e1c17f 100644 --- a/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/advancedFilterButton.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/advancedFilterButton.tsx @@ -1,5 +1,5 @@ import React, { FC, useEffect, useState } from 'react'; -import { Badge, Button } from 'antd'; +import { Badge, Button, Tooltip } from 'antd'; import { FilterFilled, FilterOutlined } from '@ant-design/icons'; import { useDataTableStore, useFormData } from '@/providers'; import { useStyles } from './style'; @@ -16,7 +16,7 @@ export const AdvancedFilterButton: FC = (props) => { const [icon, setIcon] = useState(null); const { data: formData } = useFormData(); - const { styles } = useStyles(); + const { styles } = useStyles(props.styles?.fontSize); const localStyle = getStyle(props.style, formData); @@ -26,8 +26,9 @@ export const AdvancedFilterButton: FC = (props) => { const buttonStyle = { ...localStyle, ...{ color: props.buttonType !== 'primary' && !props.danger ? styles.primaryColor : '' }, - padding: "3px", - border: hasFilters ? `1px solid ${styles.primaryColor}` : 'none', + padding: '3px', + border: props.buttonType === 'link' ? 'none' : hasFilters ? `1px solid ${styles.primaryColor}` : 'none', + ...props.styles, }; const startFilteringColumns = () => setIsInProgressFlag({ isFiltering: true, isSelectingColumns: false }); @@ -68,21 +69,26 @@ export const AdvancedFilterButton: FC = (props) => { size="small" title={filterColumns?.join(' ')} > - + + + - ); }; diff --git a/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/advancedFilterButtonComponent.tsx b/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/advancedFilterButtonComponent.tsx index afdfd870bc..f8a2193e3a 100644 --- a/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/advancedFilterButtonComponent.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/advancedFilterButtonComponent.tsx @@ -1,12 +1,13 @@ -import React from 'react'; +import { migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; +import { migratePrevStyles } from '@/designer-components/_common-migrations/migrateStyles'; +import { IButtonComponentProps } from '@/designer-components/button/interfaces'; import { IToolboxComponent } from '@/interfaces'; -import { FilterOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { validateConfigurableComponentSettings } from '@/providers/form/utils'; -import { getSettings } from './settingsForm'; +import { FilterOutlined } from '@ant-design/icons'; +import React from 'react'; import { AdvancedFilterButton } from './advancedFilterButton'; -import { IButtonComponentProps } from '@/designer-components/button/interfaces'; -import { Show } from '@/components'; -import { Tooltip } from 'antd'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; const AdvancedFilterButtonComponent: IToolboxComponent = { type: 'datatable.filter', @@ -15,14 +16,7 @@ const AdvancedFilterButtonComponent: IToolboxComponent = icon: , Factory: ({ model }) => model.hidden ? null : ( -
- - - - - - -
+ ), initModel: (model) => { return { @@ -31,8 +25,12 @@ const AdvancedFilterButtonComponent: IToolboxComponent = label: '', }; }, - settingsFormMarkup: (context) => getSettings(context), + settingsFormMarkup: (data) => getSettings(data), validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + migrator: m => + m + .add(3, (prev) => migrateReadOnly(prev, 'inherited')) + .add(4, (prev) => ({ ...migratePrevStyles(prev, defaultStyles(prev)) })), }; export default AdvancedFilterButtonComponent; diff --git a/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/settingsForm.ts b/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/settingsForm.ts index 8763e89f27..4b5a7ab8fd 100644 --- a/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/settingsForm.ts +++ b/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/settingsForm.ts @@ -1,217 +1,683 @@ import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { nanoid } from '@/utils/uuid'; +import { + backgroundTypeOptions, + positionOptions, + repeatOptions, + sizeOptions, +} from '@/designer-components/_settings/utils/background/utils'; +import { getBorderInputs, getCornerInputs } from '@/designer-components/_settings/utils/border/utils'; +import { buttonTypes } from '@/designer-components/button/util'; +import { fontTypes, fontWeights } from '@/designer-components/_settings/utils/font/utils'; -export const getSettings = (data: any) => - new DesignerToolbarSettings(data) - .addCollapsiblePanel({ - id: '11114bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'pnlDisplay', - parentId: 'root', - label: 'Display', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addTextField({ - id: '5d4d56fb-d7f8-4835-a529-c4fa93f3596d', - propertyName: 'componentName', - label: 'Component name', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: { - required: true, - }, - jsSetting: false - }) - .addTextField({ - id: 'd498779d-012a-4c6a-82a9-77231245ae28', - propertyName: 'label', - parentId: 'root', - label: 'Label', - }) - .addTextArea({ - id: '9b671866-072e-4688-8b48-ddf5e12d70d4', - propertyName: 'tooltip', - label: 'Tooltip', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ac357807b', - hidden: false, - autoSize: false, - showCount: false, - allowClear: false, - validate: {}, - }) - .addIconPicker({ - id: '91b404a6-4021-4b0a-b9ef-007167a93075', - propertyName: 'icon', - label: 'Icon', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ac357807b', - hidden: false, - settingsValidationErrors: [], - }) - .addDropdown({ - id: 'be15598e-5c23-40bc-8245-6b5385bb7963', - propertyName: 'buttonType', - label: 'Button Type', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ac357807b', - hidden: false, - validate: { - required: true, - }, - dataSourceType: 'values', - values: [ - { - id: 'c4a96833-8ed7-4085-8848-169d5607136d', - label: 'primary', - value: 'primary', - }, - { - id: 'c6f974da-ad28-44e5-8e4d-50280cf24ae7', - label: 'ghost', - value: 'ghost', - }, - { - id: '71c0dc14-0473-4748-ae75-a4ed3bd6cffd', - label: 'dashed', - value: 'dashed', - }, - { - id: '789d5733-2d4f-4969-890f-613e5b4a7d59', - label: 'link', - value: 'link', - }, - { - id: '36abe636-40b2-476c-95b0-78a50478146b', - label: 'text', - value: 'text', - }, - { - id: 'de08ea36-a831-4373-ab10-ce25fadf80cd', - label: 'default', - value: 'default', - }, +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'componentName', + label: 'Component name', + labelAlign: 'right', + parentId: commonTabId, + validate: { + required: true, + }, + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'label', + label: 'Label', + labelAlign: 'right', + parentId: commonTabId, + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'tooltip', + inputType: 'textArea', + label: 'Tooltip', + labelAlign: 'right', + parentId: commonTabId, + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'iconPicker', + propertyName: 'icon', + label: 'Icon', + labelAlign: 'right', + parentId: commonTabId, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'danger', + label: 'Danger', + labelAlign: 'right', + parentId: commonTabId, + hidden: false, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'block', + label: 'Block', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'hidden', + parentId: commonTabId, + label: 'Hide', + labelAlign: 'right', + type: 'switch', + }, + { + id: nanoid(), + type: 'editModeSelector', + propertyName: 'editMode', + parentId: commonTabId, + label: 'Edit Mode', + labelAlign: 'right', + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'configurableActionConfigurator', + propertyName: 'actionConfiguration', + label: 'Action Configuration', + hideLabel: true, + parentId: commonTabId, + validate: {}, + settingsValidationErrors: [], + }) + .toJson(), ], - }) - .addCheckbox({ - id: '4e9b886a-6186-4467-a688-639b30a0e06f', - propertyName: 'danger', - label: 'Danger', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ac357807b', - hidden: false, - validate: {}, - }) - .addCheckbox({ - id: 'd25a7d85-6afe-4595-899f-62675fb6c491', - propertyName: 'block', - label: 'Block', - labelAlign: 'right', - parentId: '345c2181-fb9f-48ed-9631-864ac357807b', - hidden: false, - validate: {}, - }) - .addCheckbox({ - id: 'cfd7d45e-c7e3-4a27-987b-dc525c412448', - propertyName: 'hidden', - parentId: 'root', - label: 'Hidden', - }) - .addEditMode({ - id: '24a8be15-98eb-40f7-99ea-ebb602693e9c', - propertyName: 'editMode', - parentId: 'root', - label: "Edit mode", - }) - .addConfigurableActionConfigurator({ - id: 'F3B46A95-703F-4465-96CA-A58496A5F78C', - propertyName: 'actionConfiguration', - label: 'Action configuration', - hidden: false, - validate: {}, - settingsValidationErrors: [], - }) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: '22224bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'pnlData', - parentId: 'root', - label: 'Style', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addCodeEditor({ - id: '06ab0599-914d-4d2d-875c-765a495472f8', - propertyName: 'style', - label: 'Style', - parentId: 'root', - validate: {}, - settingsValidationErrors: [], - description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', - exposedVariables: [{ name: 'data', description: 'Form values', type: 'object' }], - }) - .addDropdown({ - id: '8615d12f-6ea0-4b11-a1a1-6088c7160fd9', - propertyName: 'size', - parentId: 'root', - label: 'Size', - allowClear: true, - values: [ - { - label: 'Small', - value: 'small', - id: '4f11403c-95fd-4e49-bb60-cb8c25f0f3c3', - }, - { - label: 'Middle', - value: 'middle', - id: '8f85c476-e632-4fa7-89ad-2be6cfb7f1f1', - }, - { - label: 'Large', - value: 'large', - id: 'f01e54aa-a1a4-4bd6-ba73-c395e48af8ce', - }, + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'buttonType', + label: 'Type', + defaultValue: 'link', + validate: { + required: true, + }, + inputType: 'dropdown', + dropdownOptions: buttonTypes, + parentId: styleRouterId, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + hidden: { + _code: + 'return !getSettingValue(data.label);', + _mode: 'code', + _value: false, + } as any, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + hidden: { + _code: + 'return !getSettingValue(data.label);', + _mode: 'code', + _value: false, + } as any, + tooltip: 'Controls text thickness (light, normal, bold, etc.)', + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Width', + width: 85, + propertyName: 'dimensions.width', + icon: 'widthIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minWidth', + icon: 'minWidthIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxWidth', + icon: 'maxWidthIcon', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Height', + width: 85, + propertyName: 'dimensions.height', + icon: 'heightIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minHeight', + icon: 'minHeightIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxHeight', + icon: 'maxHeightIcon', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + hidden: { + _code: + 'return ["text", "link", "ghost", "dashed"].includes(getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType));', + _mode: 'code', + _value: false, + } as any, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getBorderInputs() as any, + }) + .addContainer({ + id: nanoid(), + parentId: styleRouterId, + components: getCornerInputs() as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { + _code: + 'return ["text", "link", "ghost", "primary"].includes(getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType));', + _mode: 'code', + _value: false, + } as any, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: styleRouterId, + label: 'Type', + jsSetting: false, + propertyName: 'background.type', + inputType: 'radio', + tooltip: 'Select a type of background', + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + propertyName: 'background.color', + hideLabel: true, + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'multiColorPicker', + id: nanoid(), + propertyName: 'background.gradient.colors', + label: 'Colors', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', + _mode: 'code', + _value: false, + } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'background.url', + jsSetting: false, + label: 'URL', + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: 'Image', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: 'background.storedFile.id', + label: 'File ID', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: 'Size', + customTooltip: + 'Size of the background image, two space separated values with units e.g "100% 100px"', + hideLabel: true, + propertyName: 'background.size', + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: 'Position', + hideLabel: true, + customTooltip: + 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: 'background.position', + dropdownOptions: positionOptions, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + hidden: { + _code: + 'return ["text", "link", "ghost"].includes(getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType));', + _mode: 'code', + _value: false, + } as any, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + icon: 'offsetHorizontalIcon', + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + parentId: styleRouterId, + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), ], - dataSourceType: 'values', - }) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] - } - }) - .toJson(); + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: '1adea529-1f0c-4def-bd41-ee166a5dfcd7', + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: '6Vw9iiDw9d0MD_Rh5cbIn', + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/style.ts b/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/style.ts index 20a6de6c95..caca3b67c2 100644 --- a/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/style.ts +++ b/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/style.ts @@ -1,7 +1,6 @@ import { createStyles } from "@/styles"; -export const useStyles = createStyles(({ token, cx, css }) => { - +export const useStyles = createStyles(({ token, cx, css }, fontSize: string | number) => { const primaryColor = token.colorPrimary; const secondaryColor = token.colorPrimaryBgHover; const arrowLeft = "scroll-arrow-left"; @@ -34,6 +33,9 @@ export const useStyles = createStyles(({ token, cx, css }) => { `); const button = cx("filter-btn", css` + .ant-btn-icon{ + font-size: ${fontSize} !important; + } &.ant-btn-icon-only { width: max-content; height: max-content; diff --git a/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/utils.ts b/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/utils.ts new file mode 100644 index 0000000000..460acb8ebc --- /dev/null +++ b/shesha-reactjs/src/designer-components/dataTable/advancedFilterButton/utils.ts @@ -0,0 +1,28 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (prev): IStyleType => { + return { + background: { type: 'color' }, + font: { weight: '400', size: 14, type: 'Segoe UI', align: 'center' }, + border: { + border: { all: { width: '1px', style: 'none', color: '#d9d9d9' } }, + radius: { all: 8 }, + hideBorder: true, + borderType: 'all', + }, + shadow: { + color: '#000000', + offsetX: 0, + offsetY: 0, + blurRadius: 0, + spreadRadius: 0, + }, + dimensions: { + width: prev.block ? '100%' : 'auto', + height: '32px', minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto' + } + }; +}; diff --git a/shesha-reactjs/src/designer-components/dataTable/pager/pagerComponent.tsx b/shesha-reactjs/src/designer-components/dataTable/pager/pagerComponent.tsx index 5ba5e2ee6d..852c4033f1 100644 --- a/shesha-reactjs/src/designer-components/dataTable/pager/pagerComponent.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/pager/pagerComponent.tsx @@ -1,17 +1,20 @@ -import { ControlOutlined } from '@ant-design/icons'; import { ITablePagerProps, TablePager } from '@/components'; -import React from 'react'; -import { IToolboxComponent } from '@/interfaces'; -import { IConfigurableFormComponent } from '@/providers/form/models'; -import { validateConfigurableComponentSettings } from '@/providers/form/utils'; -import { getSettings } from './settingsForm'; +import { migrateFormApi } from '@/designer-components/_common-migrations/migrateFormApi1'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; +import { migratePrevStyles } from '@/designer-components/_common-migrations/migrateStyles'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; -import { migrateFormApi } from '@/designer-components/_common-migrations/migrateFormApi1'; import { useDataTableStore } from '@/index'; +import { IToolboxComponent } from '@/interfaces'; +import { IConfigurableFormComponent } from '@/providers/form/models'; +import { validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { removeUndefinedProps } from '@/utils/object'; +import { ControlOutlined } from '@ant-design/icons'; import { Alert } from 'antd'; +import React, { CSSProperties } from 'react'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; -export interface IPagerComponentProps extends ITablePagerProps, IConfigurableFormComponent {} +export interface IPagerComponentProps extends Omit, IConfigurableFormComponent {} const PagerComponent: IToolboxComponent = { type: 'datatable.pager', @@ -19,18 +22,29 @@ const PagerComponent: IToolboxComponent = { name: 'Table Pager', icon: , Factory: ({ model }) => { + const { allStyles } = model; const store = useDataTableStore(false); + const jsStyle = allStyles?.jsStyle; + const fontStyles = allStyles?.fontStyles; + const stylingBoxAsCSS = allStyles?.stylingBoxAsCSS; + + const additionalStyles: CSSProperties = removeUndefinedProps({ + ...stylingBoxAsCSS, + ...fontStyles, + ...jsStyle, + }); + if (model.hidden) return null; - - return ; - return store - ? - : + ) : ( + ; + /> + ); }, initModel: (model: IPagerComponentProps) => { return { @@ -39,15 +53,17 @@ const PagerComponent: IToolboxComponent = { showTotalItems: true, items: [], }; - }, - migrator: m => m - .add(0, prev => ({...prev} as IPagerComponentProps)) - .add(1, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(2, (prev) => migrateVisibility(prev)) - .add(3, (prev) => ({...migrateFormApi.properties(prev)})) - , - settingsFormMarkup: context => getSettings(context), - validateSettings: model => validateConfigurableComponentSettings(getSettings(model), model), + }, + migrator: (m) => + m + .add(0, (prev) => ({ ...prev }) as IPagerComponentProps) + .add(1, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(2, (prev) => migrateVisibility(prev)) + .add(3, (prev) => ({ ...migrateFormApi.properties(prev) })) + .add(4, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), + + settingsFormMarkup: (context) => getSettings(context), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), }; export default PagerComponent; diff --git a/shesha-reactjs/src/designer-components/dataTable/pager/settingsForm.ts b/shesha-reactjs/src/designer-components/dataTable/pager/settingsForm.ts index 6dc356cf9f..9c42190bc7 100644 --- a/shesha-reactjs/src/designer-components/dataTable/pager/settingsForm.ts +++ b/shesha-reactjs/src/designer-components/dataTable/pager/settingsForm.ts @@ -1,95 +1,239 @@ +import { fontTypes, fontWeights, textAlign } from '@/designer-components/_settings/utils/font/utils'; import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; - -export const getSettings = (data: any) => - new DesignerToolbarSettings(data) - .addCollapsiblePanel({ - id: "b8954bf6-f76d-4139-a850-c99bf06c8b69", - propertyName: "separator1", - parentId: "root", - label: "Display", - labelAlign: "right", - expandIconPosition: "start", - ghost: true, - hideWhenEmpty: true, - header: { - id: '3342DA1C-DA07-46F6-8026-E8B9A93F094A', - components: [] - }, - content: { - id: '1BCC52E8-FD3B-4309-AD9B-099CDB729441', - components: new DesignerToolbarSettings() - .addTextField({ - id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', - propertyName: 'componentName', - parentId: 'root', - label: 'Component name', - validate: { required: true }, - jsSetting: false - }) - .toJson() - } -}) - .addCollapsiblePanel({ - id: "8e9069f1-9981-4336-b7af-acd6250a8d2e", - propertyName: "separatorPageSizes", - parentId: "root", - label: 'Page Sizes', - labelAlign: "right", - expandIconPosition: "start", - ghost: true, - hideWhenEmpty: true, - header: { - id: '3342DA1C-DA07-46F6-8026-E8B9A93F094A', - components: [] - }, - content: { - id: '8A3F3517-52D0-4B7F-BDC6-EFA1838A9421', - components: new DesignerToolbarSettings() - .addCheckbox({ - id: 'ff14eada-10f7-4470-8db2-52b543d9d03f', - propertyName: 'showSizeChanger', - parentId: 'root', - label: 'Show Size Changer', - defaultValue: true, - }) - .addCheckbox({ - id: 'b0304429-96b1-40bd-9b36-65197df42470', - propertyName: 'showTotalItems', - parentId: 'root', - label: 'Show Total Items', - defaultValue: true, - }) - .addCheckbox({ - id: 'ff14eada-10f7-4470-8db2-52b543d9d111', - propertyName: 'hidden', - parentId: 'root', - label: 'Hidden', - }) - .toJson() - } -}) -.addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', +import { FormLayout } from 'antd/lib/form/Form'; +export const getSettings = (data: any) => { + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: 'W_m7doMyCpCYwAYDfRh6I', + propertyName: 'settingsTabs', parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] -} -}) -.toJson(); + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: 's4gmBg31azZC0UjZjpfTm', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'palceholder-tooltip-s4gmBg31azZC0UjZjpfTm', + parentId: 's4gmBg31azZC0UjZjpfTm', + inputs: [ + { + id: 'component-name-s4gmBg31azZC0UjZjpfTm', + type: 'textField', + propertyName: 'componentName', + label: 'Component Name', + size: 'large', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: '12d700d6-ed4d-49d5-9cfd-fe8f0060f3b6', + parentId: 's4gmBg31azZC0UjZjpfTm', + inputs: [ + { + id: '12d700d6-ed4d-49d5-9cfd-fe8f0060f3b6', + inputType: 'switch', + type: 'switch', + propertyName: 'showSizeChanger', + label: 'Show Size Changer', + size: 'small', + jsSetting: true, + defaultValue: true, + }, + { + id: '12d700d6-ed4d-49d5-9cfd-fe8f0060f3b6', + type: 'switch', + inputType: 'switch', + propertyName: 'showTotalItems', + label: 'Show Total Items', + size: 'small', + jsSetting: true, + defaultValue: true, + }, + ], + }) + .addSettingsInput({ + id: 'hide-css-412c-8461-4c8d55e5c073', + inputType: 'switch', + propertyName: 'hidden', + hideLabel: false, + label: 'Hide', + jsSetting: true, + }) + .toJson(), + ], + }, + { + key: '4', + title: 'Appearance', + id: 'elgrlievlfwehhh848r8hsdnflsdnclurbd', + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: 'styleRouter', + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: 'elgrlievlfwehhh848r8hsdnflsdnclurbd', + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: 'fontStyleCollapsiblePanel', + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: 'styleRouter', + ghost: true, + collapsible: 'header', + content: { + id: 'fontStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'try26voxhs-HxJ5k5ngYE', + parentId: 'fontStylePnl', + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: 'fontFamily-s4gmBg31azZC0UjZjpfTm', + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: 'fontSize-s4gmBg31azZC0UjZjpfTm', + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: 'fontWeight-s4gmBg31azZC0UjZjpfTm', + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: 'Controls text thickness (light, normal, bold, etc.)', + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: 'fontColor-s4gmBg31azZC0UjZjpfTm', + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: 'fontAlign-s4gmBg31azZC0UjZjpfTm', + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'styleCollapsiblePanel', + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: 'stylePnl-M5-911', + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: 'styleBoxPnl', + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'customStyleCollapsiblePanel', + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: 'stylePnl-M500-911MFR', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'custom-css-412c-8461-4c8d55e5c073', + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: '5', + title: 'Security', + id: '6Vw9iiDw9d0MD_Rh5cbIn', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: '1adea529-1f0c-4def-bd41-ee166a5dfcd7', + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: '6Vw9iiDw9d0MD_Rh5cbIn', + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/dataTable/pager/utils.ts b/shesha-reactjs/src/designer-components/dataTable/pager/utils.ts new file mode 100644 index 0000000000..fb10354c47 --- /dev/null +++ b/shesha-reactjs/src/designer-components/dataTable/pager/utils.ts @@ -0,0 +1,5 @@ +export const defaultStyles = (): any => { + return { + font: { weight: '400', size: 14, color: '#000', type: 'Segoe UI' }, + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dataTable/quickSearch/quickSearchComponent.tsx b/shesha-reactjs/src/designer-components/dataTable/quickSearch/quickSearchComponent.tsx index 552f8280ac..79b632d477 100644 --- a/shesha-reactjs/src/designer-components/dataTable/quickSearch/quickSearchComponent.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/quickSearch/quickSearchComponent.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { CSSProperties, useMemo } from 'react'; import { IConfigurableFormComponent } from '@/providers/form/models'; import { GlobalTableFilter } from '@/components'; import { IToolboxComponent } from '@/interfaces'; @@ -6,13 +6,18 @@ import { migrateCustomFunctions, migratePropertyName } from '@/designer-componen import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { SearchOutlined } from '@ant-design/icons'; import { validateConfigurableComponentSettings } from '@/providers/form/utils'; -import { getSettings } from './settingsForm'; +import { getSettings } from './tabbedSettingsForm'; import { migrateFormApi } from '@/designer-components/_common-migrations/migrateFormApi1'; import { Alert } from 'antd'; import { useDataTableStore } from '@/index'; +import { getDimensionsStyle } from '@/designer-components/_settings/utils/dimensions/utils'; +import { IDimensionsValue } from '@/designer-components/_settings/utils/dimensions/interfaces'; +import { removeUndefinedProps } from '@/utils/object'; +import { migratePrevStyles } from '@/designer-components/_common-migrations/migrateStyles'; export interface IQuickSearchComponentProps extends IConfigurableFormComponent { block?: boolean; + dimensions?: IDimensionsValue; } const QuickSearchComponent: IToolboxComponent = { @@ -20,12 +25,23 @@ const QuickSearchComponent: IToolboxComponent = { isInput: false, name: 'Quick Search', icon: , - Factory: ({ model: { block, hidden } }) => { + Factory: ({ model: { block, hidden, dimensions, size: _size} }) => { const store = useDataTableStore(false); + + const size = useMemo(() => _size, [_size]); + const dimensionsStyles = useMemo(() => getDimensionsStyle(dimensions), [dimensions]); + + const additionalStyles: CSSProperties = removeUndefinedProps({ + ...dimensionsStyles, + }); + const finalStyle = removeUndefinedProps({ ...additionalStyles }); + return hidden ? null : store - ? + ? : = { return { ...model, items: [], + size: 'small', }; }, migrator: (m) => @@ -43,6 +60,7 @@ const QuickSearchComponent: IToolboxComponent = { .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) .add(1, (prev) => migrateVisibility(prev)) .add(2, (prev) => ({...migrateFormApi.properties(prev)})) + .add(3, (prev) => ({ ...migratePrevStyles(prev, { size: 'small' }) })) , settingsFormMarkup: (context) => getSettings(context), validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), diff --git a/shesha-reactjs/src/designer-components/dataTable/quickSearch/tabbedSettingsForm.ts b/shesha-reactjs/src/designer-components/dataTable/quickSearch/tabbedSettingsForm.ts new file mode 100644 index 0000000000..97063ce6cb --- /dev/null +++ b/shesha-reactjs/src/designer-components/dataTable/quickSearch/tabbedSettingsForm.ts @@ -0,0 +1,144 @@ +import { DesignerToolbarSettings } from "@/index"; +import { nanoid } from "@/utils/uuid"; +import { FormLayout } from "antd/lib/form/Form"; + +export const getSettings = (data: any) => { + const commonTabId = nanoid(); + const searchableTabsId = nanoid(); + const appearanceId = nanoid(); + const securityId = nanoid(); + const styleRouterId = nanoid(); + const dimensionsStylePnlId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'block', + parentId: 'root', + label: 'Block', + defaultValue: false, + }).toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: 'dimensionsStyleCollapsiblePanel', + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: dimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: `width-${styleRouterId}`, + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit", + defaultValue: '360px' + }, + { + type: 'textField', + id: `minWidth-${styleRouterId}`, + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + defaultValue: '0px', + }, + { + type: 'textField', + id: `maxWidth-${styleRouterId}`, + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + defaultValue: '100%', + } + ] + }) + .toJson() + ] + } + }) + .toJson(), + ] + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityId + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnProperties.tsx b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnProperties.tsx index b94f3a3659..3e1f414473 100644 --- a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnProperties.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnProperties.tsx @@ -1,7 +1,9 @@ -import columnSettingsJson from './columnSettings.json'; +//import columnSettingsJson from './columnSettings.json'; +//import { getSettings } from './columnSettings'; import React, { FC, useEffect, + useMemo, } from 'react'; import { ConfigurableForm } from '@/components'; import { Form } from 'antd'; @@ -12,27 +14,31 @@ import { useDebouncedCallback } from 'use-debounce'; import { sheshaStyles } from '@/styles'; import { usePrevious } from 'react-use'; import { IMetadataContext } from '@/providers/metadata/contexts'; +import { getColumnSettings } from './columnSettings'; -export interface IColumnPropertiesProps { +export interface IColumnPropertiesProps { item?: ColumnsItemProps; onChange?: (item: ColumnsItemProps) => void; readOnly: boolean; + parentComponentType?: string; metadata?: IMetadataContext; } -export const ColumnProperties: FC = ({ item, onChange, readOnly }) => { +export const ColumnProperties: FC = ({ item, onChange, readOnly, parentComponentType }) => { const [form] = Form.useForm(); const columnType = Form.useWatch('columnType', form); + const columnSettings = useMemo(() => getColumnSettings({ type: parentComponentType }), [parentComponentType]); + const prevColumnType = usePrevious(columnType); useEffect(() => { if (columnType) { - const fromDataToAction=!['action', 'crud-operations'].includes(prevColumnType) && ['action', 'crud-operations'].includes(columnType); - const fromActionToData=['action', 'crud-operations'].includes(prevColumnType) && !['action', 'crud-operations'].includes(columnType); + const fromDataToAction = !['action', 'crud-operations'].includes(prevColumnType) && ['action', 'crud-operations'].includes(columnType); + const fromActionToData = ['action', 'crud-operations'].includes(prevColumnType) && !['action', 'crud-operations'].includes(columnType); - if(fromDataToAction){ + if (fromDataToAction) { form.setFieldsValue({ minWidth: 35, maxWidth: 35 }); - }else if(fromActionToData){ + } else if (fromActionToData) { form.setFieldsValue({ minWidth: 100, maxWidth: 0 }); } } @@ -65,7 +71,7 @@ export const ColumnProperties: FC = ({ item, onChange, r labelCol={{ span: 24 }} wrapperCol={{ span: 24 }} mode={readOnly ? 'readonly' : 'edit'} - markup={columnSettingsJson as FormMarkup} + markup={columnSettings as FormMarkup} form={form} initialValues={item} onValuesChange={debouncedSave} diff --git a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnSettings.json b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnSettings.json index 297312190c..3839cd6b0b 100644 --- a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnSettings.json +++ b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnSettings.json @@ -1,748 +1,355 @@ { "components": [ { - "id": "840aee56-42d2-40ed-a2c6-57abb255fb95", - "type": "dropdown", - "jsSetting": false, - "propertyName": "columnType", - "label": "Type", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "dataSourceType": "values", - "values": [ - { - "id": "cc268f50-47ca-4e4e-966f-7f2abfec902f", - "label": "Action", - "value": "action" - }, - { - "id": "Owj_p4rSgOh3u5WPXv9Ba", - "label": "CRUD Operations", - "value": "crud-operations" - }, - { - "id": "8400a8ec-577d-4468-9347-5601f952b44c", - "label": "Data", - "value": "data" - }, - { - "id": "ffffa8ec-577d-4468-9347-5601f952b44c", - "label": "Form", - "value": "form" - } - ], - "validate": { "required": true }, - "settingsValidationErrors": [], - "version": 7, - "useRawValues": false, - "referenceListId": null, - "placeholder": "", - "description": "", - "defaultValue": "", - "mode": [ "single" ], - "entityTypeShortAlias": "", - "dataSourceUrl": "", - "valueFormat": "listItem", - "desktop": {}, - "tablet": {}, - "mobile": {} - }, - { - "id": "d2ba51c1-13cc-4952-a25c-5b03442097a0", - "type": "container", - "propertyName": "dataContainer", - "label": "Container3", - "labelAlign": "right", - "parentId": "root", - "hidden": { - "_code": "return getSettingValue(data?.columnType) !== 'data';", - "_mode": "code", - "_value": false - }, - "direction": "vertical", - "justifyContent": "left", - "settingsValidationErrors": [], - "components": [ - { - "id": "52f4460d-2e64-4d79-a900-7cd28b0bebd3", - "type": "propertyAutocomplete", - "jsSetting": false, - "propertyName": "propertyName", - "label": "Property Name", - "labelAlign": "right", - "parentId": "d2ba51c1-13cc-4952-a25c-5b03442097a0", - "hidden": false, - "validate": {}, - "settingsValidationErrors": [], - "version": 2 - } - ], - "version": 6, - "flexWrap": "wrap", - "jsSetting": false, - "desktop": {}, - "tablet": {}, - "mobile": {}, - "shadowStyle": "none" - }, - { - "id": "135a51c1-13cc-4952-a25c-5b03442097a0", - "type": "container", - "propertyName": "dataContainerp", - "label": "Container3p", - "labelAlign": "right", - "parentId": "root", - "hidden": { - "_code": "return getSettingValue(data?.columnType) !== 'form';", - "_mode": "code", - "_value": false - }, - "direction": "vertical", - "justifyContent": "left", - "settingsValidationErrors": [], - "components": [ - { - "id": "5314460d-2e64-4d79-a900-7cd28b0bebd3", - "type": "propertyAutocomplete", - "jsSetting": false, - "propertyName": "propertiesNames", - "label": "Properties to fetch", - "labelAlign": "right", - "parentId": "135a51c1-13cc-4952-a25c-5b03442097a0", - "mode": "multiple", - "hidden": false, - "validate": {}, - "settingsValidationErrors": [], - "version": 2 - } - ], - "version": 6, - "flexWrap": "wrap", - "jsSetting": false, - "desktop": {}, - "tablet": {}, - "mobile": {}, - "shadowStyle": "none" - }, - { - "id": "5d4d56fb-d7f8-4835-a529-c4fa93f3596d", - "type": "textField", - "jsSetting": false, - "propertyName": "caption", - "label": "Caption", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "settingsValidationErrors": [], - "version": 5, - "textType": "text", - "desktop": {}, - "tablet": {}, - "mobile": {} - }, - { - "id": "9e5cad3f-b9af-4b19-aebb-d630265f6619", - "type": "textArea", - "jsSetting": false, - "propertyName": "description", - "label": "Tooltip Description", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "settingsValidationErrors": [], - "version": 4, - "desktop": {}, - "tablet": {}, - "mobile": {} - }, - { - "id": "345c2181-fb9f-48ed-9631-864ac357807b", - "type": "container", - "propertyName": "cntButton", - "label": "Container2", - "labelAlign": "right", - "parentId": "root", - "hidden": { - "_code": "return getSettingValue(data?.columnType) !== 'action';", - "_mode": "code", - "_value": false - }, - "direction": "vertical", - "justifyContent": "left", - "components": [ - { - "id": "91b404a6-4021-4b0a-b9ef-007167a93075", - "type": "iconPicker", - "jsSetting": false, - "propertyName": "icon", - "label": "Icon", - "labelAlign": "right", - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "hidden": false, - "settingsValidationErrors": [], - "version": 3 - }, - { - "id": "F3B46A95-703F-4465-96CA-A58496A5F78C", - "type": "configurableActionConfigurator", - "jsSetting": false, - "propertyName": "actionConfiguration", - "label": "Action configuration", - "hidden": false, - "validate": {}, - "settingsValidationErrors": [], - "parentId": "345c2181-fb9f-48ed-9631-864ac357807b", - "version": 1 - } - ], - "settingsValidationErrors": [], - "version": 6, - "flexWrap": "wrap", - "jsSetting": false, - "desktop": {}, - "tablet": {}, - "mobile": {}, - "shadowStyle": "none" - }, - { - "id": "b1803af1-37fb-4f03-b2b6-727b7f17730c", - "type": "numberField", - "jsSetting": false, - "propertyName": "minWidth", - "label": "Min Width", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "validate": {}, - "settingsValidationErrors": [], - "version": 4, - "desktop": {}, - "tablet": {}, - "mobile": {} - }, - { - "id": "65bb484c-ab85-472c-bde7-e447c481ed47", - "type": "numberField", - "jsSetting": false, - "propertyName": "maxWidth", - "label": "Max Width", - "labelAlign": "right", + "id": "searchableTabs1", + "type": "searchableTabs", + "propertyName": "settingsTabs", "parentId": "root", - "hidden": false, - "validate": {}, - "settingsValidationErrors": [], - "version": 4, - "desktop": {}, - "tablet": {}, - "mobile": {} - }, - { - "id": "adc03af1-37fb-4f03-b2b6-727b7f17730c_setting", - "type": "setting", - "propertyName": "minHeight", - "label": "Min Height", - "labelAlign": "right", - "parentId": "root", - "hidden": { - "_code": "return getSettingValue(data?.columnType) !== 'form';", - "_mode": "code", - "_value": false - }, - "validate": {}, - "settingsValidationErrors": [], - "version": 2, - "jsSetting": false, - "components": [ - { - "id": "adc03af1-37fb-4f03-b2b6-727b7f17730c", - "type": "numberField", - "propertyName": "minHeight", - "label": "Min Height", - "labelAlign": "right", - "parentId": "adc03af1-37fb-4f03-b2b6-727b7f17730c_setting", - "hidden": { - "_code": "return getSettingValue(data?.columnType) !== 'form';", - "_mode": "code", - "_value": false - }, - "validate": {}, - "settingsValidationErrors": [], - "version": 4, - "jsSetting": false, - "desktop": {}, - "tablet": {}, - "mobile": {} - } - ] - }, - { - "id": "77ceb915-f4f9-4957-b62f-7a4e4ba00846", - "type": "checkbox", - "jsSetting": false, - "propertyName": "isVisible", - "label": "Is Visible", + "label": "Settings", + "hideLabel": true, "labelAlign": "right", - "parentId": "root", - "hidden": false, - "validate": {}, - "settingsValidationErrors": [], - "version": 4, - "desktop": {}, - "tablet": {}, - "mobile": {} - }, - { - "id": "57a40a33-7e08-4ce4-9f08-a34d24a82338_setting", - "type": "setting", - "propertyName": "anchored", - "parentId": "root", - "label": "Anchored ", - "values": [ + "size": "small", + "tabs": [ { - "label": "Left", - "value": "left", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - "label": "Right", - "value": "right", - "id": "b920ef96-ae27-4a01-bfad-b5b7d07218da" - } - ], - "allowClear": true, - "dataSourceType": "values", - "version": 5, - "useRawValues": false, - "referenceListId": null, - "valueFormat": "listItem", - "jsSetting": false, - "components": [ - { - "id": "57a40a33-7e08-4ce4-9f08-a34d24a82338", - "type": "dropdown", - "propertyName": "anchored", - "parentId": "57a40a33-7e08-4ce4-9f08-a34d24a82338_setting", - "label": "Anchored ", - "values": [ + "key": "common", + "title": "Common", + "id": "commonTab1", + "components": [ { - "label": "Left", - "value": "left", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" + "id": "columnType1", + "type": "settingsInput", + "inputType": "dropdown", + "propertyName": "columnType", + "label": "Type", + "labelAlign": "right", + "dropdownOptions": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "CRUD Operations", + "value": "crud-operations" + }, + { + "label": "Data", + "value": "data" + }, + { + "label": "Form", + "value": "form" + } + ], + "validate": { + "required": true + } }, { - "label": "Right", - "value": "right", - "id": "b920ef96-ae27-4a01-bfad-b5b7d07218da" - } - ], - "allowClear": true, - "dataSourceType": "values", - "version": 7, - "useRawValues": false, - "referenceListId": null, - "valueFormat": "listItem", - "jsSetting": false, - "desktop": {}, - "tablet": {}, - "mobile": {} - } - ] - }, - { - "id": "EhERYkBRv066KGKNaRA7j", - "type": "container", - "propertyName": "container3", - "label": "Container3", - "labelAlign": "right", - "parentId": "root", - "hidden": { - "_code": "return getSettingValue(data?.columnType) !== 'data';", - "_mode": "code", - "_value": false - }, - "isDynamic": false, - "version": 6, - "direction": "vertical", - "justifyContent": "left", - "display": "block", - "flexWrap": "wrap", - "components": [ - { - "id": "FGIcguhDkag6a801GXSgm", - "type": "component-selector", - "name": "displayComponent", - "label": "Display component", - "labelAlign": "right", - "parentId": "EhERYkBRv066KGKNaRA7j", - "hidden": false, - "isDynamic": false, - "version": 2, - "componentType": "output", - "tooltip": "", - "settingsValidationErrors": [], - "description": "", - "noSelectionItemText": "Default", - "noSelectionItemValue": "[default]", - "propertyAccessor": "{{data.propertyName}}", - "componentName": "displayComponent", - "propertyName": "displayComponent", - "jsSetting": false - }, - { - "id": "rYYm0wT-TDnBfqmOMVmIX", - "type": "component-selector", - "name": "editComponent", - "label": "Edit component", - "labelAlign": "right", - "parentId": "EhERYkBRv066KGKNaRA7j", - "hidden": false, - "isDynamic": false, - "version": 2, - "componentType": "input", - "tooltip": "", - "settingsValidationErrors": [], - "description": "", - "noSelectionItemText": "Not editable", - "noSelectionItemValue": "[not-editable]", - "propertyAccessor": "{{data.propertyName}}", - "componentName": "editComponent", - "propertyName": "editComponent", - "jsSetting": false - }, - { - "id": "L2bRSuStgiAmWfd5Z09CR", - "type": "component-selector", - "name": "createComponent", - "label": "Create component", - "labelAlign": "right", - "parentId": "EhERYkBRv066KGKNaRA7j", - "hidden": false, - "isDynamic": false, - "version": 2, - "componentType": "input", - "tooltip": "", - "settingsValidationErrors": [], - "description": "", - "noSelectionItemText": "Not editable", - "noSelectionItemValue": "[not-editable]", - "propertyAccessor": "{{data.propertyName}}", - "componentName": "createComponent", - "propertyName": "createComponent", - "jsSetting": false - } - ], - "gap": "", - "gridColumnsCount": "", - "className": "", - "settingsValidationErrors": [], - "jsSetting": false, - "desktop": { "className": "" }, - "tablet": { "className": "" }, - "mobile": { "className": "" }, - "shadowStyle": "none" - }, - { - "id": "dfaRYkBRv066KGKNaRA7j", - "type": "container", - "propertyName": "container3fp", - "label": "Container3", - "labelAlign": "right", - "parentId": "root", - "hidden": { - "_code": "return getSettingValue(data?.columnType) !== 'form';", - "_mode": "code", - "_value": false - }, - "isDynamic": false, - "version": 6, - "direction": "vertical", - "justifyContent": "left", - "display": "block", - "flexWrap": "wrap", - "components": [ - { - "id": "1FGIcguhDkag6a801GXSgm_setting", - "type": "setting", - "componentName": "displayFormId", - "propertyName": "displayFormId", - "parentId": "dfaRYkBRv066KGKNaRA7j", - "label": "Display form", - "version": 2, - "textType": "text", - "jsSetting": false, - "components": [ + "id": "dataContainer1", + "type": "container", + "propertyName": "dataContainer", + "label": "Data Settings", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'data';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "propertyName1", + "type": "settingsInput", + "inputType": "propertyAutocomplete", + "propertyName": "propertyName", + "label": "Property Name", + "labelAlign": "right" + } + ] + }, + { + "id": "formContainer1", + "type": "container", + "propertyName": "formContainer", + "label": "Form Settings", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'form';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "propertiesNames1", + "type": "settingsInput", + "inputType": "propertyAutocomplete", + "propertyName": "propertiesNames", + "label": "Properties to fetch", + "labelAlign": "right", + "mode": "multiple" + } + ] + }, + { + "id": "caption1", + "type": "settingsInput", + "inputType": "textField", + "propertyName": "caption", + "label": "Caption", + "labelAlign": "right" + }, + { + "id": "description1", + "type": "settingsInput", + "inputType": "textArea", + "propertyName": "description", + "label": "Tooltip Description", + "labelAlign": "right" + }, + { + "id": "actionContainer1", + "type": "container", + "propertyName": "actionContainer", + "label": "Action Settings", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'action';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "icon1", + "type": "settingsInput", + "inputType": "iconPicker", + "propertyName": "icon", + "label": "Icon", + "labelAlign": "right" + }, + { + "id": "actionConfig1", + "type": "settingsInput", + "inputType": "configurableActionConfigurator", + "propertyName": "actionConfiguration", + "label": "Action Configuration" + } + ] + }, + { + "id": "minWidth1", + "type": "settingsInput", + "inputType": "numberField", + "propertyName": "minWidth", + "label": "Min Width", + "labelAlign": "right" + }, + { + "id": "maxWidth1", + "type": "settingsInput", + "inputType": "numberField", + "propertyName": "maxWidth", + "label": "Max Width", + "labelAlign": "right" + }, + { + "id": "minHeight1", + "type": "settingsInputRow", + "propertyName": "minHeight", + "label": "Min Height", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'form';", + "_mode": "code", + "_value": false + }, + "inputs": [ + { + "type": "numberField", + "propertyName": "minHeight", + "label": "Min Height", + "labelAlign": "right" + } + ] + }, + { + "id": "isVisible1", + "type": "settingsInput", + "inputType": "switch", + "propertyName": "isVisible", + "label": "Is Visible", + "labelAlign": "right" + }, { - "id": "1FGIcguhDkag6a801GXSgm", - "type": "formAutocomplete", - "componentName": "displayFormId", - "propertyName": "displayFormId", - "parentId": "1FGIcguhDkag6a801GXSgm_setting", - "label": "Display form", - "version": 2, - "textType": "text", - "jsSetting": false + "id": "anchored1", + "type": "settingsInput", + "inputType": "dropdown", + "propertyName": "anchored", + "label": "Anchored", + "dropdownOptions": [ + { + "label": "Left", + "value": "left" + }, + { + "label": "Right", + "value": "right" + } + ], + "allowClear": true + }, + { + "id": "displayContainer1", + "type": "container", + "propertyName": "displayContainer", + "label": "Display Settings", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'data';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "displayComponent1", + "type": "settingsInput", + "inputType": "componentSelector", + "propertyName": "displayComponent", + "label": "Display component", + "componentType": "output", + "noSelectionItemText": "Default", + "noSelectionItemValue": "[default]" + }, + { + "id": "editComponent1", + "type": "settingsInput", + "inputType": "componentSelector", + "propertyName": "editComponent", + "label": "Edit component", + "componentType": "input", + "noSelectionItemText": "Not editable", + "noSelectionItemValue": "[not-editable]" + }, + { + "id": "createComponent1", + "type": "settingsInput", + "inputType": "componentSelector", + "propertyName": "createComponent", + "label": "Create component", + "componentType": "input", + "noSelectionItemText": "Not editable", + "noSelectionItemValue": "[not-editable]" + } + ] + }, + { + "id": "formDisplayContainer1", + "type": "container", + "propertyName": "formDisplayContainer", + "label": "Form Display Settings", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'form';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "displayFormId1", + "type": "settingsInput", + "inputType": "formAutocomplete", + "propertyName": "displayFormId", + "label": "Display form" + }, + { + "id": "createFormId1", + "type": "settingsInput", + "inputType": "formAutocomplete", + "propertyName": "createFormId", + "label": "Create form" + }, + { + "id": "editFormId1", + "type": "settingsInput", + "inputType": "formAutocomplete", + "propertyName": "editFormId", + "label": "Edit form" + } + ] + }, + { + "id": "allowSorting1", + "type": "settingsInput", + "inputType": "switch", + "propertyName": "allowSorting", + "label": "Allow sorting", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'data';", + "_mode": "code", + "_value": false + } + }, + { + "id": "backgroundColor1", + "type": "settingsInput", + "inputType": "colorPicker", + "propertyName": "backgroundColor", + "label": "Background Color", + "allowClear": true, + "showText": true } ] }, { - "id": "1rYYm0wT-TDnBfqmOMVmIX_setting", - "type": "setting", - "componentName": "createFormId", - "propertyName": "createFormId", - "parentId": "dfaRYkBRv066KGKNaRA7j", - "label": "Create form", - "version": 2, - "textType": "text", - "jsSetting": false, + "key": "visibility", + "title": "Visibility", + "id": "visibilityTab1", "components": [ { - "id": "1rYYm0wT-TDnBfqmOMVmIX", - "type": "formAutocomplete", - "componentName": "createFormId", - "propertyName": "createFormId", - "parentId": "1rYYm0wT-TDnBfqmOMVmIX_setting", - "label": "Create form", - "version": 2, - "textType": "text", - "jsSetting": false + "id": "customVisibility1", + "type": "settingsInput", + "inputType": "codeEditor", + "propertyName": "customVisibility", + "label": "Custom Visibility", + "description": "Enter custom visibility code. You must return true to show the component. The global variable data is provided, and allows you to access the data of any form component, by using its API key.", + "exposedVariables": [ + { + "name": "globalState", + "description": "The global state of the application", + "type": "object" + }, + { + "name": "data", + "description": "Selected form values", + "type": "object" + } + ], + "language": "typescript", + "wrapInTemplate": true, + "templateSettings": { + "functionName": "customVisibility" + } } ] }, { - "id": "1L2bRSuStgiAmWfd5Z09CR_setting", - "type": "setting", - "componentName": "editFormId", - "propertyName": "editFormId", - "parentId": "dfaRYkBRv066KGKNaRA7j", - "label": "Edit form", - "version": 2, - "textType": "text", - "jsSetting": false, + "key": "security", + "title": "Security", + "id": "securityTab1", "components": [ { - "id": "1L2bRSuStgiAmWfd5Z09CR", - "type": "formAutocomplete", - "componentName": "editFormId", - "propertyName": "editFormId", - "parentId": "1L2bRSuStgiAmWfd5Z09CR_setting", - "label": "Edit form", - "version": 2, - "textType": "text", - "jsSetting": false + "id": "permissions1", + "type": "settingsInput", + "inputType": "permissionAutocomplete", + "propertyName": "permissions", + "label": "Permissions" } ] } - ], - "gap": "", - "gridColumnsCount": "", - "className": "", - "settingsValidationErrors": [], - "jsSetting": false, - "desktop": { "className": "" }, - "tablet": { "className": "" }, - "mobile": { "className": "" }, - "shadowStyle": "none" - }, - { - "id": "33IOXJRmPdfU9KLeDUF64", - "type": "checkbox", - "jsSetting": false, - "propertyName": "allowSorting", - "componentName": "allowSorting", - "label": "Allow sorting", - "labelAlign": "right", - "parentId": "root", - "hidden": { - "_code": "return getSettingValue(data?.columnType) !== 'data';", - "_mode": "code", - "_value": false - }, - "isDynamic": false, - "version": 4, - "validate": {}, - "settingsValidationErrors": [], - "desktop": {}, - "tablet": {}, - "mobile": {} - }, - { - "id": "52c604d6-0183-4331-a960-0b9551e88b09", - "type": "sectionSeparator", - "propertyName": "sectionVisibility", - "parentId": "root", - "label": "Visibility", - "version": 3, - "jsSetting": false, - "labelAlign": "left", - "titleMargin": null - }, - { - "id": "eda790c5-3cea-4755-9c75-039eca9318f3", - "type": "codeEditor", - "propertyName": "customVisibility", - "label": "Custom Visibility", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "description": "Enter custom visibility code. You must return true to show the component. The global variable data is provided, and allows you to access the data of any form component, by using its API key.", - "validate": {}, - "settingsValidationErrors": [], - "exposedVariables": [ - { - "name": "globalState", - "description": "The global state of the application", - "type": "object" - }, - { - "name": "data", - "description": "Selected form values", - "type": "object" - } - ], - "version": 4, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { "functionName": "customVisibility" }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\").addStandard([\"shesha:formData\", \"shesha:globalState\"]).build();", - "jsSetting": false, - "environment": "front-end" - }, - { - "id": "WwHwuygM-SKcqDbHVbPNkoMAcsUHGJ_setting", - "type": "setting", - "propertyName": "backgroundColor", - "componentName": "backgroundColor", - "label": "Background color", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 2, - "settingsValidationErrors": [], - "title": "", - "fontSize": "", - "allowClear": true, - "showText": true, - "exposedVariables": [ - { - "id": "RyCiFg3TwQ", - "name": "value", - "description": "Component cell value", - "type": "string | any" - }, - { - "id": "m5URdpeB7x", - "name": "row", - "description": "Current row value", - "type": "object" - } - ], - "jsSetting": false, - "components": [ - { - "id": "WwHwuygM-SKcqDbHVbPNkoMAcsUHGJ", - "type": "colorPicker", - "propertyName": "backgroundColor", - "componentName": "backgroundColor", - "label": "Background color", - "labelAlign": "right", - "parentId": "WwHwuygM-SKcqDbHVbPNkoMAcsUHGJ_setting", - "hidden": false, - "isDynamic": false, - "version": 3, - "settingsValidationErrors": [], - "title": "", - "fontSize": "", - "allowClear": true, - "showText": true, - "exposedVariables": [ - { - "id": "RyCiFg3TwQ", - "name": "value", - "description": "Component cell value", - "type": "string | any" - }, - { - "id": "m5URdpeB7x", - "name": "row", - "description": "Current row value", - "type": "object" - } - ], - "jsSetting": false - } - ], - "availableConstantsExpression": " return metadataBuilder.object('constants').addAllStandard().addAny('value', 'Value of the cell').addAny('row', 'Data of the current row').build();" - }, - { - "id": "52c604d6-0183-4331-a960-0b9551e88b89", - "type": "sectionSeparator", - "propertyName": "sectionSecurity", - "parentId": "root", - "label": "Security", - "version": 3, - "jsSetting": false, - "labelAlign": "left", - "titleMargin": null - }, - { - "id": "1adea529-1f0c-4def-bd41-ee166a5dfcd7_setting", - "type": "setting", - "propertyName": "permissions", - "parentId": "root", - "label": "Permissions", - "validate": {}, - "version": 1, - "jsSetting": false, - "components": [ - { - "id": "1adea529-1f0c-4def-bd41-ee166a5dfcd7", - "type": "permissionAutocomplete", - "propertyName": "permissions", - "parentId": "1adea529-1f0c-4def-bd41-ee166a5dfcd7_setting", - "label": "Permissions", - "validate": {}, - "version": 1, - "jsSetting": false - } ] } ], "formSettings": { "isSettingsForm": true, - "layout": "horizontal", - "colon": true, - "labelCol": { "span": 8 }, - "wrapperCol": { "span": 16 }, - "access": null, - "permissions": null, - "version": 6, - "dataLoaderType": "gql", - "dataSubmitterType": "gql", - "onBeforeDataLoad": " form.setFieldsValue({...form.formArguments});", - "onAfterDataLoad": null, - "onPrepareSubmitData": null, - "onBeforeSubmit": null, - "onSubmitSuccess": null, - "onSubmitFailed": null, - "dataSubmittersSettings": { - "gql": { - "endpointType": "default", - "dynamicEndpoint": " return data?.id ? form.defaultApiEndpoints.update : form.defaultApiEndpoints.create" - } + "layout": "vertical", + "colon": false, + "labelCol": { + "span": 8 }, - "dataLoadersSettings": { - "gql": { "endpointType": "default" }, - "custom": {} + "wrapperCol": { + "span": 16 } } } \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnSettings.ts b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnSettings.ts new file mode 100644 index 0000000000..cf1bf5811d --- /dev/null +++ b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnSettings.ts @@ -0,0 +1,431 @@ +import { DesignerToolbarSettings } from "@/interfaces/toolbarSettings"; + +export const getColumnSettings = (data?: any) => ({ + "components": [ + { + "id": "searchableTabs1", + "type": "searchableTabs", + "propertyName": "settingsTabs", + "parentId": "root", + "label": "Settings", + "hideLabel": true, + "labelAlign": "right", + "size": "small", + "tabs": [ + { + "key": "common", + "title": "Common", + "id": "commonTab1", + "components": [ + { + "id": "columnType1", + "type": "settingsInput", + "inputType": "dropdown", + "propertyName": "columnType", + "label": "Type", + "labelAlign": "right", + "dropdownOptions": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "CRUD Operations", + "value": "crud-operations" + }, + { + "label": "Data", + "value": "data" + }, + { + "label": "Form", + "value": "form" + } + ], + "validate": { + "required": true + } + }, + { + "id": "dataContainer1", + "type": "container", + "propertyName": "dataContainer", + "label": "Data Settings", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'data';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "propertyName1", + "type": "settingsInput", + "inputType": "propertyAutocomplete", + "propertyName": "propertyName", + "label": "Property Name", + "labelAlign": "right" + } + ] + }, + { + "id": "formContainer1", + "type": "container", + "propertyName": "formContainer", + "label": "Form Settings", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'form';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "propertiesNames1", + "type": "settingsInput", + "inputType": "propertyAutocomplete", + "propertyName": "propertiesNames", + "label": "Properties to fetch", + "labelAlign": "right", + "mode": "multiple" + } + ] + }, + { + "id": "caption-tooltip-row", + "type": "settingsInputRow", + "propertyName": "captionTooltipRow", + "label": "Caption & Tooltip", + "labelAlign": "right", + "inputs": [ + { + "id": "caption1", + "type": "textField", + "propertyName": "caption", + "label": "Caption", + "labelAlign": "right" + }, + { + "id": "description1", + "type": "textArea", + "propertyName": "description", + "label": "Tooltip", + "labelAlign": "right" + }, + ] + }, + { + "id": "actionContainer1", + "type": "container", + "propertyName": "actionContainer", + "label": "Action Settings", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'action';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "icon1", + "type": "settingsInput", + "inputType": "iconPicker", + "propertyName": "icon", + "label": "Icon", + "labelAlign": "right" + }, + { + "id": "F3B46A95-703F-4465-96CA-A58496A5F78C", + "type": "configurableActionConfigurator", + "propertyName": "actionConfiguration", + "label": "Action Configuration", + "hidden": false, + "validate": {}, + "settingsValidationErrors": [], + "parentId": "root", + "version": 1 + }, + ] + }, + { + "id": "isVisible-anchored-row", + "type": "settingsInputRow", + "propertyName": "isVisibleAnchoredRow", + "label": "Is Visible & Anchored", + "labelAlign": "right", + "inputs": [ + { + "id": "anchored1", + "type": "radio", + "propertyName": "anchored", + "label": "Anchored", + "jsSetting": true, + "buttonGroupOptions": [ + { + "title": "Left", + "value": "left", + "icon": "LeftOutlined" + }, + { + "title": "Right", + "value": "right", + "icon": "RightOutlined" + } + ], + "allowClear": true + }, + { + "id": "isVisible1", + "type": "switch", + "propertyName": "isVisible", + "label": "Is Visible", + "labelAlign": "right" + } + ] + }, + { + "id": "displayContainer1", + "type": "container", + "propertyName": "displayContainer", + "label": "Display Settings", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'data';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "displayComponent1", + "type": "settingsInput", + "inputType": "componentSelector", + "propertyName": "displayComponent", + "label": "Display Component", + "componentType": "output", + "noSelectionItemText": "Default", + "noSelectionItemValue": "[default]", + "propertyAccessor": "{{data.propertyName}}", + "hidden": data?.type === 'entityPicker' + }, + { + "id": "editComponent1", + "type": "settingsInput", + "inputType": "componentSelector", + "propertyName": "editComponent", + "label": "Edit Component", + "componentType": "input", + "noSelectionItemText": "Not Editable", + "noSelectionItemValue": "[not-editable]", + "propertyAccessor": "{{data.propertyName}}", + "hidden": data?.type === 'entityPicker' + }, + { + "id": "createComponent1", + "type": "settingsInput", + "inputType": "componentSelector", + "propertyName": "createComponent", + "label": "Create Component", + "componentType": "input", + "noSelectionItemText": "Not Editable", + "noSelectionItemValue": "[not-editable]", + "propertyAccessor": "{{data.propertyName}}", + "hidden": data?.type === 'entityPicker' + }, + { + "id": "customVisibility1", + "type": "settingsInput", + "inputType": "codeEditor", + "propertyName": "customVisibility", + "label": "Custom Visibility", + "description": "Enter custom visibility code. You must return true to show the component. The global variable data is provided, and allows you to access the data of any form component, by using its API key.", + "exposedVariables": [ + { + "name": "globalState", + "description": "The global state of the application", + "type": "object" + }, + { + "name": "data", + "description": "Selected form values", + "type": "object" + } + ], + "language": "typescript", + "wrapInTemplate": true, + "templateSettings": { + "functionName": "customVisibility" + } + } + ] + }, + { + "id": "formDisplayContainer1", + "type": "container", + "propertyName": "formDisplayContainer", + "label": "Form Display Settings", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'form';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "displayFormId1", + "type": "settingsInput", + "inputType": "formAutocomplete", + "propertyName": "displayFormId", + "label": "Display form" + }, + { + "id": "createFormId1", + "type": "settingsInput", + "inputType": "formAutocomplete", + "propertyName": "createFormId", + "label": "Create form" + }, + { + "id": "editFormId1", + "type": "settingsInput", + "inputType": "formAutocomplete", + "propertyName": "editFormId", + "label": "Edit form" + } + ] + }, + { + "id": "allowSortingContainer1", + "type": "container", + "propertyName": "allowSortingContainer", + "hidden": { + "_code": "return getSettingValue(data?.columnType) !== 'data';", + "_mode": "code", + "_value": false + }, + "components": [ + { + "id": "allowSorting1", + "type": "settingsInput", + "inputType": "switch", + "propertyName": "allowSorting", + "label": "Allow Sorting", + + },] + } + ] + }, + { + key: 'appearance', + forceRender: true, + title: 'Appearance', + id: 'elgrlievlfwehhh848r8hsdnflsdnclurbd', + components: [...new DesignerToolbarSettings() + + .addCollapsiblePanel({ + id: 'dimensionsStyleCollapsiblePanel', + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: 'styleRouter', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: 'dimensionsStylePnl', + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'dimensionsStyleRowWidth', + parentId: 'dimensionsStylePnl', + inline: true, + inputs: [ + { + "id": "minWidth1", + "type": "numberField", + "propertyName": "minWidth", + "label": "Min Width", + "labelAlign": "right", + }, + { + "id": "maxWidth1", + "type": "numberField", + "propertyName": "maxWidth", + "label": "Max Width", + "labelAlign": "right", + }, + ] + }) + .addSettingsInputRow({ + id: 'dimensionsStyleRowHeight', + parentId: 'dimensionsStylePnl', + inline: true, + hidden: { + _code: "return getSettingValue(data?.columnType) !== 'form';", + _mode: "code", + _value: false + }, + inputs: [ + { + "id": "minHeight1", + "type": "numberField", + "propertyName": "minHeight", + "label": "Min Height", + "labelAlign": "right", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: 'backgroundStyleCollapsiblePanel', + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + hidden: { _code: 'return ["text", "link", "ghost"].includes(getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.buttonType));', _mode: 'code', _value: false } as any, + content: { + id: 'backgroundStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput( + { + "id": "backgroundColor1", + "inputType": "colorPicker", + "propertyName": "backgroundColor", + "label": "Background Color", + "allowClear": true, + "showText": true, + "jsSetting": true, + }) + .toJson() + ], + } + }) + .toJson()] + }, + { + "key": "security", + "title": "Security", + "id": "securityTab1", + "components": [ + { + "id": "permissions1", + "type": "settingsInput", + "inputType": "permissions", + "propertyName": "permissions", + "label": "Permissions" + } + ] + } + ] + } + ], + "formSettings": { + "isSettingsForm": true, + "layout": "vertical", + "colon": false, + "labelCol": { + "span": 8 + }, + "wrapperCol": { + "span": 16 + } + } +}); diff --git a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsConfig.tsx b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsConfig.tsx index 3844e9e397..f58aa0cacc 100644 --- a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsConfig.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsConfig.tsx @@ -2,21 +2,24 @@ import { Button } from 'antd'; import React, { FC, Fragment, useState } from 'react'; import { ColumnsItemProps } from '@/providers/datatableColumnsConfigurator/models'; import { ColumnsEditorModal } from './columnsEditorModal'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; export interface IColumnsConfigProps { value?: ColumnsItemProps[]; onChange?: (value: ColumnsItemProps[]) => void; readOnly?: boolean; + size?: SizeType; + parentComponentType?: string; } -export const ColumnsConfig: FC = ({ value, onChange, readOnly = false }) => { +export const ColumnsConfig: FC = ({ value, onChange, readOnly = false, size, parentComponentType }) => { const [modalVisible, setModalVisible] = useState(false); const toggleModalVisibility = () => setModalVisible((prev) => !prev); return ( - + = ({ value, onChange, readOn value={value} onChange={onChange} readOnly={readOnly} + parentComponentType={parentComponentType} /> ); diff --git a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsConfigurator.tsx b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsConfigurator.tsx index 9712e15162..d233359737 100644 --- a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsConfigurator.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsConfigurator.tsx @@ -9,10 +9,11 @@ import { Column } from './column'; export interface IColumnsConfiguratorProps { readOnly: boolean; value: ColumnsItemProps[]; + parentComponentType?: string; onChange: (newValue: ColumnsItemProps[]) => void; } -export const ColumnsConfigurator: FC = ({ value, onChange, readOnly }) => { +export const ColumnsConfigurator: FC = ({ value, onChange, readOnly, parentComponentType }) => { const makeNewItem = (items: ColumnsItemProps[]): ColumnsItemProps => { const safeItems = items ?? []; const itemsCount = safeItems.filter((i) => i.itemType === 'item').length; @@ -43,7 +44,7 @@ export const ColumnsConfigurator: FC = ({ value, onCh initNewItem={makeNewItem} readOnly={readOnly} header={} - itemProperties={(itemProps) => ()} + itemProperties={(itemProps) => ()} addItemText='Add Column' > {({ item }) => ( diff --git a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsEditorModal.tsx b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsEditorModal.tsx index 0d35eee28e..c1bd9bc10c 100644 --- a/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsEditorModal.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/table/columnsEditor/columnsEditorModal.tsx @@ -9,11 +9,12 @@ export interface IColumnsEditorModal { readOnly: boolean; visible: boolean; hideModal: () => void; + parentComponentType?: string; value?: ColumnsItemProps[]; onChange?: any; } -export const ColumnsEditorModal: FC = ({ onChange, value, visible, hideModal, readOnly }) => { +export const ColumnsEditorModal: FC = ({ onChange, value, visible, hideModal, readOnly, parentComponentType }) => { const isSmall = useMedia('(max-width: 480px)'); const [localValue, setLocalValue] = useState(deepCopyViaJson(value)); @@ -41,6 +42,7 @@ export const ColumnsEditorModal: FC = ({ onChange, value, v diff --git a/shesha-reactjs/src/designer-components/dataTable/table/models.ts b/shesha-reactjs/src/designer-components/dataTable/table/models.ts index ab2b03ad91..5509d74d73 100644 --- a/shesha-reactjs/src/designer-components/dataTable/table/models.ts +++ b/shesha-reactjs/src/designer-components/dataTable/table/models.ts @@ -1,4 +1,5 @@ import { IShaDataTableInlineEditableProps } from '@/components/dataTable/interfaces'; +import { IConfigurableActionConfiguration } from '@/providers'; import { IConfigurableColumnsProps } from '@/providers/datatableColumnsConfigurator/models'; import { IConfigurableFormComponent } from '@/providers/form/models'; @@ -15,6 +16,7 @@ export interface ITableComponentBaseProps extends IShaDataTableInlineEditablePro noDataText?: string; noDataSecondaryText?: string; noDataIcon?: string; + dblClickActionConfiguration?: IConfigurableActionConfiguration; } /** Table component props */ diff --git a/shesha-reactjs/src/designer-components/dataTable/table/tableComponent-settings.tsx b/shesha-reactjs/src/designer-components/dataTable/table/tableComponent-settings.tsx index 1f82818528..158bf6c242 100644 --- a/shesha-reactjs/src/designer-components/dataTable/table/tableComponent-settings.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/table/tableComponent-settings.tsx @@ -395,11 +395,11 @@ const TableSettings: FC> = (props - + - + diff --git a/shesha-reactjs/src/designer-components/dataTable/table/tableComponent.tsx b/shesha-reactjs/src/designer-components/dataTable/table/tableComponent.tsx index 7972a1ea47..2b7ca5b216 100644 --- a/shesha-reactjs/src/designer-components/dataTable/table/tableComponent.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/table/tableComponent.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import TableSettings from './tableComponent-settings'; +import { getSettings } from './tableSettings'; import { Alert } from 'antd'; import { IDataColumnsProps, isActionColumnProps } from '@/providers/datatableColumnsConfigurator/models'; import { ITableComponentProps } from './models'; @@ -14,6 +14,7 @@ import { TableOutlined } from '@ant-design/icons'; import { TableWrapper } from './tableWrapper'; import { useDataTableStore } from '@/providers'; import { migrateFormApi } from '@/designer-components/_common-migrations/migrateFormApi1'; +import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; const TableComponent: IToolboxComponent = { type: 'datatable', @@ -22,6 +23,7 @@ const TableComponent: IToolboxComponent = { icon: , Factory: ({ model }) => { const store = useDataTableStore(false); + if (model.hidden) return null; return store ? ( @@ -40,6 +42,8 @@ const TableComponent: IToolboxComponent = { items: [], }; }, + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings, model), migrator: (m) => m .add(0, (prev) => { @@ -98,8 +102,11 @@ const TableComponent: IToolboxComponent = { onNewRowInitialize: migrateFormApi.full(prev.onNewRowInitialize), onRowSave: migrateFormApi.full(prev.onRowSave) })) - , - settingsFormFactory: (props) => , + .add(11, (prev) => ({ + ...prev, + noDataText: prev.noDataText ?? 'No Data', + noDataSecondaryText: prev.noDataSecondaryText ?? 'No data is available for this table', + })), actualModelPropertyFilter: (name) => name !== 'items', }; diff --git a/shesha-reactjs/src/designer-components/dataTable/table/tableSettings.ts b/shesha-reactjs/src/designer-components/dataTable/table/tableSettings.ts new file mode 100644 index 0000000000..9dfe75137f --- /dev/null +++ b/shesha-reactjs/src/designer-components/dataTable/table/tableSettings.ts @@ -0,0 +1,558 @@ +import { DesignerToolbarSettings } from "@/index"; +import { nanoid } from "@/utils/uuid"; +import { FormLayout } from "antd/lib/form/Form"; +import { ITableComponentProps } from "./models"; + +const NEW_ROW_EXPOSED_VARIABLES = [ + { + id: nanoid(), + name: 'formData', + description: 'Form values', + type: 'object', + }, + { + id: nanoid(), + name: 'globalState', + description: 'The global state of the application', + type: 'object', + }, + { + id: nanoid(), + name: 'http', + description: 'axios instance used to make http requests', + type: 'object', + }, + { + id: nanoid(), + name: 'moment', + description: 'The moment.js object', + type: 'object', + } +].map(item => JSON.stringify(item)); + +const ROW_SAVE_EXPOSED_VARIABLES = [ + { + id: nanoid(), + name: 'data', + description: 'Current row data', + type: 'object', + }, + { + id: nanoid(), + name: 'formData', + description: 'Form values', + type: 'object', + }, + { + id: nanoid(), + name: 'globalState', + description: 'The global state of the application', + type: 'object', + }, + { + id: nanoid(), + name: 'http', + description: 'axios instance used to make http requests', + type: 'object', + }, + { + id: nanoid(), + name: 'moment', + description: 'The moment.js object', + type: 'object', + } +].map(item => JSON.stringify(item)); + +const ENABLE_CRUD_EXPOSED_VARIABLES = [ + { + id: nanoid(), + name: 'formData', + description: 'Form values', + type: 'object', + }, + { + id: nanoid(), + name: 'globalState', + description: 'The global state of the application', + type: 'object', + }, + { + id: nanoid(), + name: 'moment', + description: 'The moment.js object', + type: 'object', + } +].map(item => JSON.stringify(item)); + +export const getSettings = (data: ITableComponentProps) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const crudTabId = nanoid(); + const layoutTabId = nanoid(); + const emptyTableTabId = nanoid(); + const securityTabId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'componentName', + label: 'Component Name', + inputType: 'textField', + validate: { required: true }, + jsSetting: false, + }) + .toJson() + ] + }, + { + key: 'data', + title: 'Data', + id: crudTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'items', + label: data.readOnly ? 'View Columns' : 'Customize Columns', + inputType: 'columnsConfig', + jsSetting: true, + parentId: commonTabId, + }) + .addSettingsInputRow({ + id: nanoid(), + inputs: [ + { + id: nanoid(), + propertyName: 'freezeHeaders', + label: 'Freeze Headers', + type: 'switch', + jsSetting: true, + parentId: commonTabId, + }, + { + id: nanoid(), + propertyName: 'useMultiselect', + label: 'Use Multi-Select', + type: 'switch', + jsSetting: true, + parentId: commonTabId, + } + ] + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'canEditInline', + label: 'Can Edit Inline', + inputType: 'dropdown', + jsSetting: true, + parentId: crudTabId, + dropdownOptions: [ + { value: 'yes', label: 'Yes' }, + { value: 'no', label: 'No' }, + { value: 'inherit', label: 'Inherit' }, + { value: 'js', label: 'Expression' }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + + hidden: { _code: 'return getSettingValue(data?.canEditInline) !== "js";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'canEditInlineExpression', + label: 'Can Edit Inline Expression', + type: 'codeEditor', + parentId: crudTabId, + description: 'Return true to enable inline editing and false to disable.', + exposedVariables: ENABLE_CRUD_EXPOSED_VARIABLES, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canEditInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'inlineEditMode', + label: 'Row Edit Mode', + type: 'dropdown', + parentId: crudTabId, + dropdownOptions: [ + { value: 'one-by-one', label: 'One by one' }, + { value: 'all-at-once', label: 'All at once' }, + ], + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canEditInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'inlineSaveMode', + label: 'Save Mode', + type: 'dropdown', + parentId: crudTabId, + dropdownOptions: [ + { value: 'auto', label: 'Auto' }, + { value: 'manual', label: 'Manual' }, + ], + } + ] + }) + + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canEditInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'customUpdateUrl', + label: 'Custom Update URL', + type: 'endpointsAutocomplete', + parentId: crudTabId, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: false, + inputs: [ + { + id: nanoid(), + propertyName: 'canAddInline', + label: 'Can Add Inline', + type: 'dropdown', + jsSetting: true, + parentId: crudTabId, + dropdownOptions: [ + { value: 'yes', label: 'Yes' }, + { value: 'no', label: 'No' }, + { value: 'inherit', label: 'Inherit' }, + { value: 'js', label: 'Expression' }, + ], + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canAddInline) !== "js";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'canAddInlineExpression', + label: 'Can Add Inline Expression', + type: 'codeEditor', + parentId: crudTabId, + description: 'Return true to enable inline creation of new rows and false to disable.', + exposedVariables: ENABLE_CRUD_EXPOSED_VARIABLES, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canAddInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'newRowCapturePosition', + label: 'New Row Capture Position', + type: 'dropdown', + parentId: crudTabId, + dropdownOptions: [ + { value: 'top', label: 'Top' }, + { value: 'bottom', label: 'Bottom' }, + ], + } + ] + }) + + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return true;', _mode: 'code', _value: true } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'newRowInsertPosition', + label: 'New Row Insert Position', + type: 'dropdown', + parentId: crudTabId, + dropdownOptions: [ + { value: 'top', label: 'Top' }, + { value: 'bottom', label: 'Bottom' }, + ], + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canAddInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'customCreateUrl', + label: 'Custom Create URL', + type: 'endpointsAutocomplete', + parentId: crudTabId, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canAddInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'onNewRowInitialize', + label: 'New Row Init', + type: 'codeEditor', + parentId: crudTabId, + tooltip: 'Allows configurators to specify logic to initialise the object bound to a new row.', + description: 'Specify logic to initialise the object bound to a new row. This handler should return an object or a Promise.', + exposedVariables: NEW_ROW_EXPOSED_VARIABLES, + } + ] + }) + + .addSettingsInput({ + id: nanoid(), + propertyName: 'onRowSave', + label: 'On Row Save', + inputType: 'codeEditor', + parentId: crudTabId, + tooltip: 'Custom business logic to be executed on saving of new/updated row (e.g. custom validation / calculations). This handler should return an object or a Promise.', + hidden: { _code: 'return getSettingValue(data?.canAddInline) === "no" && getSettingValue(data?.canEditInline) === "no";', _mode: 'code', _value: false } as any, + description: 'Allows custom business logic to be executed on saving of new/updated row (e.g. custom validation / calculations).', + exposedVariables: ROW_SAVE_EXPOSED_VARIABLES, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'canDeleteInline', + label: 'Can Delete Inline', + inputType: 'dropdown', + parentId: crudTabId, + jsSetting: true, + dropdownOptions: [ + { value: 'yes', label: 'Yes' }, + { value: 'no', label: 'No' }, + { value: 'inherit', label: 'Inherit' }, + { value: 'js', label: 'Expression' }, + ], + }) + + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canDeleteInline) !== "js";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'canDeleteInlineExpression', + label: 'Can Delete Inline Expression', + type: 'codeEditor', + parentId: crudTabId, + description: 'Return true to enable inline deletion and false to disable.', + exposedVariables: ENABLE_CRUD_EXPOSED_VARIABLES, + } + ] + }) + + .addSettingsInputRow({ + id: nanoid(), + hidden: { _code: 'return getSettingValue(data?.canDeleteInline) === "no";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'customDeleteUrl', + label: 'Custom Delete URL', + type: 'endpointsAutocomplete', + parentId: crudTabId, + } + ] + }) + + + .toJson() + ] + }, + { + key: 'events', + title: 'Events', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addConfigurableActionConfigurator({ + id: nanoid(), + propertyName: "dblClickActionConfiguration", + parentId: 'root', + label: "On Double-Click", + jsSetting: false, + }) + .addConfigurableActionConfigurator({ + id: nanoid(), + propertyName: 'onRowSaveSuccessAction', + label: 'On Row Save Success', + parentId: crudTabId, + description: 'Custom business logic to be executed after successfull saving of new/updated row.', + hideLabel: true, + jsSetting: true, + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: layoutTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + inputs: [ + { + id: nanoid(), + propertyName: 'minHeight', + label: 'Min Height', + type: 'numberField', + parentId: layoutTabId, + tooltip: 'The minimum height of the table (e.g. even when 0 rows). If blank then minimum height is 0.', + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'maxHeight', + label: 'Max Height', + type: 'numberField', + parentId: layoutTabId, + tooltip: 'The maximum height of the table. If left blank should grow to display all rows, otherwise should allow for vertical scrolling.', + jsSetting: true, + } + ] + }) + .addCollapsiblePanel({ + id: 'tableStyles', + propertyName: 'tableStylesCol', + label: 'Customize Styles', + labelAlign: 'right', + ghost: true, + parentId: layoutTabId, + collapsible: 'header', + content: { + id: 'tableStylesCollapsible', + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + inputs: [ + { + id: nanoid(), + propertyName: 'tableStyle', + label: 'Table Style', + type: 'codeEditor', + parentId: layoutTabId, + description: 'The style that will be applied to the table', + exposedVariables: [], + }, + { + id: nanoid(), + propertyName: 'containerStyle', + label: 'Table Container style', + type: 'codeEditor', + parentId: layoutTabId, + description: 'The style that will be applied to the table container/wrapper', + exposedVariables: [], + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: 'tableEmptyState', + propertyName: 'tableEmptyState', + label: 'Empty State', + labelAlign: 'right', + ghost: true, + parentId: layoutTabId, + collapsible: 'header', + content: { + id: 'tableEmptyState', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'noDataIcon', + label: 'Icon', + inputType: 'iconPicker', + parentId: emptyTableTabId, + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'noDataText', + label: 'Primary Text', + inputType: 'textField', + parentId: emptyTableTabId, + jsSetting: true, + defaultValue: 'No Data', + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'noDataSecondaryText', + label: 'Secondary Text', + inputType: 'textField', + parentId: emptyTableTabId, + jsSetting: true, + defaultValue: 'No data is available for this table', + }).toJson() + ] + } + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'permissions', + label: 'Permissions', + inputType: 'permissions', + parentId: securityTabId, + jsSetting: true, + tooltip: 'Enter a list of permissions that should be associated with this component', + }) + .toJson() + ] + }, + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dataTable/table/tableWrapper.tsx b/shesha-reactjs/src/designer-components/dataTable/table/tableWrapper.tsx index 36c129b996..1c61e35a45 100644 --- a/shesha-reactjs/src/designer-components/dataTable/table/tableWrapper.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/table/tableWrapper.tsx @@ -122,6 +122,7 @@ export const TableWrapper: FC = (props) => { customDeleteUrl={props.customDeleteUrl} onRowSave={props.onRowSave} onRowSaveSuccessAction={props.onRowSaveSuccessAction} + onDblClick={props.dblClickActionConfiguration} inlineSaveMode={props.inlineSaveMode} inlineEditMode={props.inlineEditMode} minHeight={props.minHeight} diff --git a/shesha-reactjs/src/designer-components/dataTable/tableContext/models.ts b/shesha-reactjs/src/designer-components/dataTable/tableContext/models.ts index a7fa44e87f..a3bc9d4d0c 100644 --- a/shesha-reactjs/src/designer-components/dataTable/tableContext/models.ts +++ b/shesha-reactjs/src/designer-components/dataTable/tableContext/models.ts @@ -9,7 +9,7 @@ import { import { IConfigurableFormComponent } from '@/providers/form/models'; import { YesNoInherit } from '@/interfaces'; -export interface ITableContextComponentProps extends IConfigurableFormComponent { +export interface ITableContextComponentProps extends Omit { sourceType?: 'Form' | 'Entity' | 'Url'; entityType?: string; endpoint?: string; diff --git a/shesha-reactjs/src/designer-components/dataTable/tableContext/settingsForm.ts b/shesha-reactjs/src/designer-components/dataTable/tableContext/settingsForm.ts new file mode 100644 index 0000000000..45b5e6d406 --- /dev/null +++ b/shesha-reactjs/src/designer-components/dataTable/tableContext/settingsForm.ts @@ -0,0 +1,553 @@ +import { DesignerToolbarSettings } from '@/index'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/lib/form/Form'; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const securityTabId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + parentId: commonTabId, + label: 'Property Name', + size: 'small', + validate: { + required: true, + }, + styledLabel: true, + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'disableRefresh', + parentId: commonTabId, + label: 'Disable Refresh Data', + tooltip: + "Return 'true' if datatableContext is not ready to refresh data (filter data is not ready, etc...)", + readOnly: { + _code: 'return getSettingValue(data?.readOnly);', + _mode: 'code', + _value: false, + } as any, + jsSetting: true, + availableConstantsExpression: + ' return metadataBuilder.object("constants").addAllStandard().build();', + placeholder: '', + language: 'typescript', + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'hidden', + parentId: commonTabId, + label: 'Hide', + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: dataTabId, + labelAlign: 'left', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'sourceType', + parentId: dataTabId, + label: 'Data Source Type', + inputType: 'dropdown', + dropdownOptions: [ + { label: 'URL', value: 'Url' }, + { label: 'Entity', value: 'Entity' }, + { label: 'Form', value: 'Form' }, + ], + allowClear: true, + validate: { required: true }, + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _value: false, + _code: "return getSettingValue(data.sourceType) !== 'Entity';", + _mode: 'code', + } as any, + inputs: [ + { + id: nanoid(), + type: 'autocomplete', + propertyName: 'entityType', + label: 'Entity Type', + description: 'The entity type you want to use.', + labelAlign: 'right', + parentId: dataTabId, + hidden: false, + dataSourceType: 'url', + validate: {}, + dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', + settingsValidationErrors: [], + jsSetting: true, + useRawValues: true, + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _value: false, + _code: + "return getSettingValue(data.sourceType) !== 'Url' && getSettingValue(data.sourceType) !== 'Entity';", + _mode: 'code', + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'endpoint', + label: 'Custom Endpoint', + labelAlign: 'right', + parentId: dataTabId, + type: 'endpointsAutocomplete', + description: 'The endpoint to use to fetch data.', + validate: { + required: { + _code: "return getSettingValue(data.sourceType) === 'Url';", + _mode: 'code', + _value: false, + } as any, + }, + dataSourceType: 'url', + dataSourceUrl: '/api/services/app/Api/Endpoints', + settingsValidationErrors: [], + useRawValues: true, + jsSetting: true, + width: '100%', + placeholder: '', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: false, + readOnly: { + _code: 'return getSettingValue(data?.readOnly);', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'dropdown', + propertyName: 'dataFetchingMode', + label: 'Data Fetching Mode', + labelAlign: 'right', + parentId: dataTabId, + allowClear: true, + dropdownOptions: [ + { + label: 'Paging', + value: 'paging', + }, + { + label: 'Fetch all', + value: 'fetchAll', + }, + ], + validate: { required: true }, + readOnly: { + _code: 'return getSettingValue(data?.readOnly);', + _mode: 'code', + _value: false, + } as any, + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'defaultPageSize', + label: 'Default Page Size', + type: 'dropdown', + allowClear: true, + hidden: { + _value: false, + _code: "return getSettingValue(data.dataFetchingMode) !== 'paging';", + _mode: 'code', + } as any, + dropdownOptions: [ + { + label: '5', + value: `5`, + }, + { + label: '10', + value: `10`, + }, + { + label: '20', + value: `20`, + }, + { + label: '30', + value: `30`, + }, + { + label: '50', + value: `50`, + }, + { + label: '100', + value: `100`, + }, + { + label: '200', + value: `200`, + }, + ], + mode: ['single'], + parentId: dataTabId, + validate: { required: true }, + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _value: false, + _code: + "const sourceType = getSettingValue(data && data.sourceType);\nconst entityType = getSettingValue(data && data.entityType);\n\nreturn !(sourceType === 'Entity' && Boolean(entityType));", + _mode: 'code', + }, + inputs: [ + { + id: nanoid(), + propertyName: 'permanentFilter', + label: 'Permanent Filter', + labelAlign: 'right', + parentId: dataTabId, + type: 'queryBuilder', + hidden: false, + isDynamic: false, + validate: {}, + settingsValidationErrors: [], + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + fieldsUnavailableHint: 'Please select `Entity Type` to be able to configure this filter.', + width: '100%', + jsSetting: false, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _value: false, + _code: + "return !getSettingValue(data?.sourceType) || getSettingValue(data.sourceType) === 'Url' || getSettingValue(data.sourceType) === 'Form';", + _mode: 'code', + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'sortMode', + label: 'Sort Mode', + labelAlign: 'right', + parentId: dataTabId, + type: 'dropdown', + allowClear: true, + dropdownOptions: [ + { + label: 'Standard', + value: 'standard', + }, + { + label: 'Strict', + value: 'strict', + }, + ], + validate: { required: true }, + settingsValidationErrors: [], + jsSetting: true, + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _value: false, + _code: + "return !getSettingValue(data?.sortMode) || getSettingValue(data.sortMode) !== 'strict';", + _mode: 'code', + } as any, + validate: { + required: { + _code: "return getSettingValue(data.sortMode) === 'strict';", + _mode: 'code', + } as any, + }, + inputs: [ + { + id: nanoid(), + propertyName: 'strictSortBy', + componentName: 'strictSortBy', + label: 'Order By', + labelAlign: 'right', + parentId: dataTabId, + type: 'propertyAutocomplete', + isDynamic: false, + description: + 'The properties you want to order the data by. Use the propeties that you have selected for axis, value (and legend).', + validate: { required: false }, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + autoFillProps: false, + settingsValidationErrors: [], + width: '100%', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _value: false, + _code: "return getSettingValue(data.sortMode) !== 'strict';", + _mode: 'code', + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'strictSortOrder', + componentName: 'strictSortOrder', + label: 'Sort Order', + labelAlign: 'right', + type: 'dropdown', + inputType: 'dropdown', + allowClear: true, + validate: { + required: { + _code: "return data.sortMode === 'strict';", + _mode: 'code', + _value: false, + } as any, + }, + dropdownOptions: [ + { + label: 'Ascending', + value: 'asc', + }, + { + label: 'Descending', + value: 'desc', + }, + ], + width: '100%', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _value: false, + _code: + "return !getSettingValue(data?.sortMode) || getSettingValue(data.sortMode) !== 'standard' || getSettingValue(data.sourceType) === 'Url' || getSettingValue(data.sourceType) === 'Form';", + _mode: 'code', + }, + inputs: [ + { + id: nanoid(), + type: 'dataSortingEditor', + propertyName: 'standardSorting', + componentName: 'standardSorting', + label: 'Sort By', + labelAlign: 'right', + parentId: dataTabId, + hidden: { + _value: false, + _code: + "return !getSettingValue(data?.sortMode) || getSettingValue(data.sortMode) !== 'standard';", + _mode: 'code', + } as any, + customVisibility: null, + isDynamic: false, + version: 0, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + validate: {}, + settingsValidationErrors: [], + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _value: false, + _code: + "return !(getSettingValue(data && data.sourceType) === 'Entity' && getSettingValue(data.sortMode) !== 'strict');", + _mode: 'code', + } as any, + inputs: [ + { + id: nanoid(), + type: 'dataSortingEditor', + propertyName: 'grouping', + componentName: 'grouping', + label: 'Grouping', + labelAlign: 'right', + parentId: 'root', + hidden: { + _value: false, + _code: + "return !(getSettingValue(data && data.sourceType) === 'Entity' && getSettingValue(data.sortMode) !== 'strict');\n", + _mode: 'code', + } as any, + isDynamic: false, + version: 0, + validate: {}, + settingsValidationErrors: [], + jsSetting: true, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inline: true, + hidden: { + _value: false, + _code: + "return !getSettingValue(data?.sourceType) || !(getSettingValue(data.sourceType) === 'Entity' && getSettingValue(data.sortMode) === 'strict' || getSettingValue(data.sourceType) === 'Form');", + _mode: 'code', + }, + isDynamic: false, + inputs: [ + { + id: nanoid(), + propertyName: 'allowReordering', + parentId: dataTabId, + type: 'dropdown', + label: 'Allow Reordering', + inputType: 'dropdown', + allowClear: true, + dropdownOptions: [ + { + label: 'Yes', + value: 'yes', + }, + { + label: 'No', + value: 'no', + }, + { + label: 'Inherit', + value: 'inherit', + }, + ], + width: '100%', + validate: { required: true }, + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + jsSetting: true, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/dataTable/tableContext/tableContext.tsx b/shesha-reactjs/src/designer-components/dataTable/tableContext/tableContext.tsx index c8d86310cb..03b03ff39d 100644 --- a/shesha-reactjs/src/designer-components/dataTable/tableContext/tableContext.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/tableContext/tableContext.tsx @@ -18,7 +18,7 @@ export const TableContextInner: FC = (props) => { const { data } = useFormData(); const allData = useAvailableConstantsData(); - const disableRefresh = Boolean(props.disableRefresh) ? executeScriptSync(props.disableRefresh, allData) : false; + const disableRefresh: boolean = Boolean(props.disableRefresh) ? executeScriptSync(props.disableRefresh, allData) : false; const propertyMetadataAccessor = useNestedPropertyMetadatAccessor(props.entityType); const permanentFilter = useFormEvaluatedFilter({ filter: props.permanentFilter, metadataAccessor: propertyMetadataAccessor }); diff --git a/shesha-reactjs/src/designer-components/dataTable/tableContext/tableContextComponent.tsx b/shesha-reactjs/src/designer-components/dataTable/tableContext/tableContextComponent.tsx index d9b253e9d6..8f90991dd6 100644 --- a/shesha-reactjs/src/designer-components/dataTable/tableContext/tableContextComponent.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/tableContext/tableContextComponent.tsx @@ -1,6 +1,4 @@ import React from 'react'; -import settingsFormJson from './settingsForm.json'; -import { FormMarkup } from '@/providers/form/models'; import { IToolboxComponent } from '@/interfaces'; import { LayoutOutlined } from '@ant-design/icons'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; @@ -9,8 +7,7 @@ import { validateConfigurableComponentSettings } from '@/providers/form/utils'; import { TableContext } from './tableContext'; import { ITableContextComponentProps } from './models'; import { migrateFormApi } from '@/designer-components/_common-migrations/migrateFormApi1'; - -const settingsForm = settingsFormJson as FormMarkup; +import { getSettings } from './settingsForm'; const TableContextComponent: IToolboxComponent = { type: 'datatableContext', @@ -23,17 +20,17 @@ const TableContextComponent: IToolboxComponent = { }, migrator: (m) => m - .add(0, (prev) => ({...prev, name: prev['uniqueStateId'] ?? prev['name']})) - .add(1, (prev) => ({...prev, sourceType: 'Entity'})) - .add(2, (prev) => ({...prev, defaultPageSize: 10})) - .add(3, (prev) => ({...prev, dataFetchingMode: 'paging'})) + .add(0, (prev) => ({ ...prev, name: prev['uniqueStateId'] ?? prev['name'] })) + .add(1, (prev) => ({ ...prev, sourceType: 'Entity' })) + .add(2, (prev) => ({ ...prev, defaultPageSize: 10 })) + .add(3, (prev) => ({ ...prev, dataFetchingMode: 'paging' })) .add(4, (prev) => migratePropertyName(migrateCustomFunctions(prev))) .add(5, (prev) => ({ ...prev, sortMode: 'standard', strictSortOrder: 'asc', allowReordering: 'no' })) .add(6, (prev) => migrateVisibility(prev)) - .add(7, (prev) => ({...migrateFormApi.properties(prev)})) + .add(7, (prev) => ({ ...migrateFormApi.properties(prev) })) , - settingsFormMarkup: settingsForm, - validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), getFieldsToFetch: (propertyName, rawModel) => { return rawModel.sourceType === 'Form' ? [propertyName] : []; }, diff --git a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filterItemProperties.tsx b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filterItemProperties.tsx index 2687c3db4e..1afef83392 100644 --- a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filterItemProperties.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filterItemProperties.tsx @@ -1,9 +1,9 @@ import React, { FC, useMemo } from 'react'; import { useDebouncedCallback } from 'use-debounce'; import { ConfigurableForm } from '@/components'; -import { FormMarkup } from '@/providers/form/models'; -import tableViewSettingsJson from './filterItemSettings.json'; + import { ITableViewProps } from '@/providers/dataTable/filters/models'; +import { filtersSettingsForm } from './filterItemSettings'; export type BaseFilterProperties = Omit; @@ -13,7 +13,7 @@ export interface IFilterItemPropertiesProps { readOnly: boolean; } -const tableViewSettingsMarkup = tableViewSettingsJson as FormMarkup; +// const tableViewSettingsMarkup = tableViewSettingsJson as FormMarkup; export const FilterItemProperties: FC = ({ value, onChange, readOnly }) => { const debouncedSave = useDebouncedCallback( @@ -29,7 +29,7 @@ export const FilterItemProperties: FC = ({ value, on labelCol={{ span: 24 }} wrapperCol={{ span: 24 }} mode={readOnly ? 'readonly' : 'edit'} - markup={tableViewSettingsMarkup} + markup={filtersSettingsForm} initialValues={value} onValuesChange={debouncedSave} />); diff --git a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filterItemSettings.ts b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filterItemSettings.ts new file mode 100644 index 0000000000..22bcd85b15 --- /dev/null +++ b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filterItemSettings.ts @@ -0,0 +1,25 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; + +export const filtersSettingsForm = new DesignerToolbarSettings() + .addSettingsInput({ + id: 's4gmBg31azZC0UjZjpfTm', + propertyName: 'name', + label: 'Title', + layout: 'horizontal', + }) + .addSettingsInput({ + id: '03959ffd-cadb-496c-bf6d-b742f7f6edc6', + propertyName: 'tooltip', + inputType: 'textArea', + label: 'Tooltip', + layout: 'horizontal', + allowClear: true, + }) + .addSettingsInput({ + id: '1adea529-1f0c-4def-bd41-ee166a5dfcd7', + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + }) + .toJson(); diff --git a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filtersList.tsx b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filtersList.tsx index e6c5cdcce6..141374e357 100644 --- a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filtersList.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/filtersList.tsx @@ -3,6 +3,7 @@ import { ITableViewProps } from '@/providers/dataTable/filters/models'; import { nanoid } from '@/utils/uuid'; import React, { FC } from 'react'; import { FilterItem } from './filterItem'; +import { ListItem } from '@/components/listEditor/models'; export interface IFiltersListProps { value?: ITableViewProps[]; @@ -26,7 +27,7 @@ export const FiltersList: FC = ({ value, onChange, readOnly } }; return ( - + value={value} onChange={localOnChange} initNewItem={makeNewFilter} diff --git a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/styles.ts b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/styles.ts index ec1937fe90..ab6991a7a2 100644 --- a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/styles.ts +++ b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/filters/styles.ts @@ -5,18 +5,22 @@ export const useStyles = createStyles(({ css, cx }) => { const filterItem = cx("filter-item", css` display: flex; justify-content: space-between; + align-items: center; + position: relative; .${controls} { - transition: opacity 0.2s; + visibility: hidden; opacity: 0; + transition: opacity 0.2s ease-in-out; display: flex; align-items: center; + position: absolute; + right: 0; } - - &:hover { - >.${controls} { - opacity: 1; - } + + &:hover .${controls} { + visibility: visible; + opacity: 1; } `); @@ -24,4 +28,6 @@ export const useStyles = createStyles(({ css, cx }) => { filterItem, controls, }; -}); \ No newline at end of file +}); + + diff --git a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/settingsForm.ts b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/settingsForm.ts new file mode 100644 index 0000000000..4def136dfb --- /dev/null +++ b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/settingsForm.ts @@ -0,0 +1,84 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; + +export const getSettings = (data: any) => { + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: 'W_m7doMyCpCYwAYDfRh6I', + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: 's4gmBg31azZC0UjZjpfTm', + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: 'collapsiblePanel1', + propertyName: 'commonSettings', + label: 'Filters', + labelAlign: 'right', + parentId: 'W_m7doMyCpCYwAYDfRh6I', + ghost: true, + collapsible: 'header', + content: { + id: 'fontStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + inputType: 'filtersList', + id: 's4gmBg31azZC0UjZjpfTm', + propertyName: 'filters', + hideLabel: true, + label: 'Filters', + layout: 'horizontal', + }) + .toJson(), + ], + }, + }) + .addSettingsInput({ + inputType: 'switch', + id: 'hidden-s4gmBg31azZC0UjZjpfTm', + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }) + .toJson(), + ], + }, + { + key: '5', + title: 'Security', + id: '6Vw9iiDw9d0MD_Rh5cbIn', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: '1adea529-1f0c-4def-bd41-ee166a5dfcd7', + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: '6Vw9iiDw9d0MD_Rh5cbIn', + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/tableViewSelectorComponent.tsx b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/tableViewSelectorComponent.tsx index a7073a4cef..9eb9944cba 100644 --- a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/tableViewSelectorComponent.tsx +++ b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/tableViewSelectorComponent.tsx @@ -1,6 +1,6 @@ import _ from 'lodash'; import React from 'react'; -import TableViewSelectorSettings from './tableViewSelectorSettings'; + import { ITableViewSelectorComponentProps } from './models'; import { IToolboxComponent } from '@/interfaces'; import { migrateFilterMustacheExpressions } from '@/designer-components/_common-migrations/migrateUseExpression'; @@ -8,7 +8,8 @@ import { migratePropertyName } from '@/designer-components/_common-migrations/mi import { SelectOutlined } from '@ant-design/icons'; import { TableViewSelector } from './tableViewSelector'; import { Alert } from 'antd'; -import { useDataTableStore } from '@/index'; +import { useDataTableStore, validateConfigurableComponentSettings } from '@/index'; +import { getSettings } from './settingsForm'; const TableViewSelectorComponent: IToolboxComponent = { type: 'tableViewSelector', @@ -38,17 +39,8 @@ const TableViewSelectorComponent: IToolboxComponent migratePropertyName(prev)) , - settingsFormFactory: ({ readOnly, model, onSave, onCancel, onValuesChange }) => { - return ( - - ); - }, + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), }; export default TableViewSelectorComponent; diff --git a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/tableViewSelectorSettings.tsx b/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/tableViewSelectorSettings.tsx deleted file mode 100644 index 9d55e6164b..0000000000 --- a/shesha-reactjs/src/designer-components/dataTable/tableViewSelector/tableViewSelectorSettings.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { Checkbox, Form } from 'antd'; -import { ITableViewSelectorComponentProps } from './models'; -import { SectionSeparator } from '@/components'; -import { PermissionAutocomplete } from '@/components/permissionAutocomplete'; -import { FiltersList } from './filters/filtersList'; - -export interface ITableViewSelectorSettingsProps { - readOnly: boolean; - model: ITableViewSelectorComponentProps; - onSave: (model: ITableViewSelectorComponentProps) => void; - onCancel: () => void; - onValuesChange?: (changedValues: any, values: ITableViewSelectorComponentProps) => void; -} - -function TableViewSelectorSettings(props: ITableViewSelectorSettingsProps) { - const [form] = Form.useForm(); - - const handleValuesChange = (changedValues: any, values: ITableViewSelectorComponentProps) => { - if (props.onValuesChange) { - props.onValuesChange(changedValues, values); - } - }; - - return ( -
- - - - - - - - - - - - ); -} - - - -export default TableViewSelectorSettings; diff --git a/shesha-reactjs/src/designer-components/dateField/dateField.tsx b/shesha-reactjs/src/designer-components/dateField/dateField.tsx index 9ff3c1d7a8..7e025eff9c 100644 --- a/shesha-reactjs/src/designer-components/dateField/dateField.tsx +++ b/shesha-reactjs/src/designer-components/dateField/dateField.tsx @@ -1,13 +1,12 @@ import { CalendarOutlined } from '@ant-design/icons'; -import React, { Fragment } from 'react'; +import React, { Fragment} from 'react'; import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import { customDateEventHandler } from '@/components/formDesigner/components/utils'; import { IToolboxComponent } from '@/interfaces'; import { DataTypes } from '@/interfaces/dataTypes'; -import { FormMarkup, IInputStyles } from '@/providers/form/models'; -import { useAvailableConstantsData, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { IInputStyles } from '@/providers/form/models'; +import {useAvailableConstantsData, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { IDateFieldProps } from './interfaces'; -import settingsFormJson from './settingsForm.json'; import { DATE_TIME_FORMATS, } from './utils'; @@ -15,8 +14,9 @@ import { migratePropertyName, migrateCustomFunctions, migrateReadOnly } from '@/ import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { DatePickerWrapper } from './datePickerWrapper'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; - -const settingsForm = settingsFormJson as FormMarkup; +import { getSettings } from './settingsForm'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultStyles } from '../textField/utils'; const DateField: IToolboxComponent = { type: 'dateField', @@ -46,8 +46,8 @@ const DateField: IToolboxComponent = { ); }, - settingsFormMarkup: settingsForm, - validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), initModel: (model) => { const customModel: IDateFieldProps = { ...model, @@ -75,8 +75,25 @@ const DateField: IToolboxComponent = { style: prev.style }; - return { ...prev, desktop: {...styles}, tablet: {...styles}, mobile: {...styles} }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }) + .add(6, (prev) => { + const styles: IInputStyles = { + size: prev.size, + width: prev.width, + height: prev.height, + hideBorder: prev.hideBorder, + borderSize: prev.borderSize, + borderRadius: prev.borderRadius, + borderColor: prev.borderColor, + fontSize: prev.fontSize, + fontColor: prev.fontColor, + backgroundColor: prev.backgroundColor, + stylingBox: prev.stylingBox, + }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; }) + .add(7, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })) , linkToModelMetadata: (model, metadata): IDateFieldProps => { diff --git a/shesha-reactjs/src/designer-components/dateField/datePickerWrapper.tsx b/shesha-reactjs/src/designer-components/dateField/datePickerWrapper.tsx index c8de939f3c..13de69b72b 100644 --- a/shesha-reactjs/src/designer-components/dateField/datePickerWrapper.tsx +++ b/shesha-reactjs/src/designer-components/dateField/datePickerWrapper.tsx @@ -1,14 +1,14 @@ import { DatePicker } from '@/components/antd'; import moment, { isMoment } from 'moment'; -import React, { FC, useMemo } from 'react'; +import React, { FC, useMemo, useRef} from 'react'; import ReadOnlyDisplayFormItem from '@/components/readOnlyDisplayFormItem'; import { useForm, useGlobalState, useMetadata } from '@/providers'; -import { getStyle } from '@/providers/form/utils'; import { getMoment, getRangeMoment } from '@/utils/date'; import { getDataProperty } from '@/utils/metadata'; import { IDateFieldProps, RangePickerChangeEvent, TimePickerChangeEvent } from './interfaces'; import { DATE_TIME_FORMATS, disabledDate, disabledTime, getFormat } from './utils'; import { asPropertiesArray } from '@/interfaces/metadata'; +import { useStyles } from './style'; const MIDNIGHT_MOMENT = moment('00:00:00', 'HH:mm:ss'); @@ -38,13 +38,17 @@ export const DatePickerWrapper: FC = (props) => { disabledTimeFunc, readOnly, style, + additionalStyles, defaultToMidnight, resolveToUTC, + allStyles, ...rest } = props; const dateFormat = props?.dateFormat || getDataProperty(properties, name) || DATE_TIME_FORMATS.date; const timeFormat = props?.timeFormat || DATE_TIME_FORMATS.time; + const fullStyles = {...allStyles?.fullStyle || {}}; + const { styles } = useStyles({ fullStyles }); const { formData } = useForm(); @@ -64,7 +68,7 @@ export const DatePickerWrapper: FC = (props) => { : !showTime ? newValue.startOf('day') : newValue; - return !resolveToUTC ? val.utc(true) : val.local(true); + return !resolveToUTC ? val?.utc(true) : val.local(true); }; const handleDatePickerChange = (localValue: any | null, dateString: string) => { @@ -87,18 +91,112 @@ export const DatePickerWrapper: FC = (props) => { (onChange as RangePickerChangeEvent)(dates, formatString); }; - const handleOnOk = (value: moment.Moment | null) => handleDatePickerChange(value, value?.format(pickerFormat)); + const prevDatePartRef = useRef(null); + + + const handleCalendarDatePickerChange = (dates) => { + if (!dates) return; + + const getDatePart = (date) => date.format('YYYY-MM-DD'); + + const newDatePart = getDatePart(dates); + const prevDatePart = prevDatePartRef.current; + + let newDate; + + if (newDatePart !== prevDatePart) { + // Date part changed — override time with current system time + const now = moment(); + newDate = dates.clone().set({ + hour: now.hour(), + minute: now.minute(), + second: now.second(), + }); + } else { + // Date part did not change — user changed the time, keep it as is + newDate = dates; + } + + prevDatePartRef.current = newDatePart; + + handleDatePickerChange(newDate, newDate.format(pickerFormat)); + }; - const evaluatedStyle = { width: '100%', ...getStyle(style, formData, globalState) }; + + + const handleOnOk = (value: moment.Moment | null) => handleDatePickerChange(value, value?.format(pickerFormat)); const momentValue = useMemo(() => getMoment(value, pickerFormat), [value, pickerFormat]); const rangeMomentValue = useMemo(() => getRangeMoment(value, pickerFormat), [value, pickerFormat]); const defaultMomentValue = useMemo(() => getRangeMoment(defaultValue, pickerFormat), [defaultValue, pickerFormat]); + + +const prevStartDatePartRef = useRef(null); +const prevEndDatePartRef = useRef(null); + +const handleCalendarRangeChange = (dates) => { + if (!dates) return; + + const [start, end] = dates; + + const getDatePart = (date) => date?.format('YYYY-MM-DD'); + + const startDatePart = getDatePart(start); + const endDatePart = getDatePart(end); + + let newStart = start; + let newEnd = end; + + /*start and end date parts are used to determine if the user has changed the date part of the date + if the date part has changed, we override the time with the current system time + if the date part has not changed, we keep the time as it is */ + if (start) { + const prevStartDatePart = prevStartDatePartRef.current; + if (startDatePart !== prevStartDatePart) { + const nowForStart = moment(); + newStart = start.clone().set({ + hour: nowForStart.hour(), + minute: nowForStart.minute(), + second: nowForStart.second(), + }); + } + prevStartDatePartRef.current = startDatePart; + } + + if (end) { + const prevEndDatePart = prevEndDatePartRef.current; + if (endDatePart !== prevEndDatePart) { + const nowForEnd = moment(); + newEnd = end.clone().set({ + hour: nowForEnd.hour(), + minute: nowForEnd.minute(), + second: nowForEnd.second(), + }); + } + prevEndDatePartRef.current = endDatePart; + } + + const newDates = [newStart, newEnd]; + + handleRangePicker( + newDates, + [ + newStart?.format(pickerFormat), + newEnd?.format(pickerFormat), + ] + ); +}; + + + if (range) { return ( - { + if (dates && showTime && !defaultToMidnight) handleCalendarRangeChange(dates); + }} + className="sha-range-picker" disabledDate={(e) => disabledDate(props, e, formData, globalState)} disabledTime={disabledTime(props, formData, globalState)} onChange={handleRangePicker} @@ -109,7 +207,7 @@ export const DatePickerWrapper: FC = (props) => { picker={picker} showTime={showTime ? (defaultToMidnight ? { defaultValue: [MIDNIGHT_MOMENT, MIDNIGHT_MOMENT] } : true) : false} disabled={readOnly} - style={evaluatedStyle} + style={allStyles.fullStyle} allowClear variant={hideBorder ? 'borderless' : undefined} /> @@ -123,7 +221,7 @@ export const DatePickerWrapper: FC = (props) => { return ( disabledDate(props, e, formData, globalState)} disabledTime={disabledTime(props, formData, globalState)} onChange={handleDatePickerChange} @@ -133,7 +231,10 @@ export const DatePickerWrapper: FC = (props) => { showNow={showNow} picker={picker} format={pickerFormat} - style={evaluatedStyle} + style={allStyles.fullStyle} + onCalendarChange={(dates) => { + if (dates && showTime && !defaultToMidnight) handleCalendarDatePickerChange(dates); + }} {...rest} value={momentValue} allowClear diff --git a/shesha-reactjs/src/designer-components/dateField/interfaces.ts b/shesha-reactjs/src/designer-components/dateField/interfaces.ts index 57900242b8..358eddb871 100644 --- a/shesha-reactjs/src/designer-components/dateField/interfaces.ts +++ b/shesha-reactjs/src/designer-components/dateField/interfaces.ts @@ -1,4 +1,5 @@ -import { IConfigurableFormComponent } from '@/providers/form/models'; +import { IConfigurableFormComponent, IInputStyles } from '@/providers/form/models'; +import { CSSProperties } from 'react'; export type RangeType = 'start' | 'end'; @@ -13,7 +14,7 @@ export type RangePickerChangeEvent = (values: any, formatString: [string, string export type DisabledDateTemplate = 'disabledPastTime' | 'disableFutureTime'; -export interface IDateFieldProps extends IConfigurableFormComponent { +export interface IDateFieldProps extends IConfigurableFormComponent, IInputStyles { dateFormat?: string; value?: any; hideBorder?: boolean; @@ -37,4 +38,5 @@ export interface IDateFieldProps extends IConfigurableFormComponent { disabledTimeMode?: 'none' | 'timeFunctionTemplate' | 'customTimeFunction'; disabledTimeTemplate?: DisabledDateTemplate; disabledTimeFunc?: string; + additionalStyles?: CSSProperties; } diff --git a/shesha-reactjs/src/designer-components/dateField/settingsForm.json b/shesha-reactjs/src/designer-components/dateField/settingsForm.json deleted file mode 100644 index b479487d6b..0000000000 --- a/shesha-reactjs/src/designer-components/dateField/settingsForm.json +++ /dev/null @@ -1,834 +0,0 @@ -{ - "components": [ - { - "id": "YdG427sm65_Ww47MFVdxA", - "type": "collapsiblePanel", - "propertyName": "pnlDisplay", - "label": "Display", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "IIBBWlI-XuHjKQs6d00tO", - "components": [] - }, - "content": { - "id": "VmAusRlLZo3cYRFk88xnf", - "components": [ - { - "id": "5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4", - "type": "contextPropertyAutocomplete", - "propertyName": "propertyName", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Property name", - "validate": { - "required": true - } - }, - { - "id": "46d07439-4c18-468c-89e1-60c002ce96c5", - "type": "textField", - "propertyName": "label", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Label", - "version": 3, - "textType": "text" - }, - { - "id": "563a0f3b-bd6f-4af3-a4dd-23d821e68cb4", - "type": "checkbox", - "propertyName": "range", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Range?", - "version": 2 - }, - { - "id": "e92bcf3c-a70a-4aeb-b6ac-5643eb5b4fe2", - "type": "switch", - "propertyName": "resolveToUTC", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Resolve to UTC", - "description": "If switched on the selected time will be converted to UTC", - "version": 2 - }, - { - "id": "57a40a33-7e08-4ce4-9f08-a34d24a83338", - "type": "dropdown", - "propertyName": "labelAlign", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Label align", - "values": [ - { - "label": "left", - "value": "left", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - "label": "right", - "value": "right", - "id": "b920ef96-ae27-4a01-bfad-b5b7d07218da" - } - ], - "dataSourceType": "values", - "version": 5, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "7bddaaf5-d880-4438-9fb0-159791bc210e", - "type": "dropdown", - "propertyName": "picker", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Picker", - "values": [ - { - "label": "date", - "value": "date", - "id": "e8321f5d-f96d-4101-84d4-6c02ea7fc7e6" - }, - { - "label": "week", - "value": "week", - "id": "d846cd16-d7f1-4501-92ea-ca8c685c4c87" - }, - { - "label": "month", - "value": "month", - "id": "fecc571b-8ae7-4c47-b690-7b0da10e7e1d" - }, - { - "label": "quarter", - "value": "quarter", - "id": "3580b2e6-0cfd-4e74-a497-a256741b8224" - }, - { - "label": "year", - "value": "year", - "id": "2ff6cbb0-3c3e-4530-9a9b-d09c1f04ac3d" - } - ], - "dataSourceType": "values", - "value": "date", - "version": 5, - "referenceListId": null, - "placeholder": "", - "description": "", - "defaultValue": "", - "mode": [ - "single" - ], - "entityTypeShortAlias": "", - "dataSourceUrl": "", - "validate": {}, - "settingsValidationErrors": [], - "valueFormat": "listItem" - }, - { - "id": "fbdb6bb1-5994-4205-86b4-236ca61ae54e", - "type": "textField", - "propertyName": "placeholder", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Placeholder", - "version": 3, - "textType": "text" - }, - { - "id": "2d32fe70-99a0-4825-ae6c-8b933004e119", - "type": "textArea", - "propertyName": "description", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Description", - "version": 2 - }, - { - "id": "cfd7d45e-c7e3-4a27-987b-dc525c412448", - "type": "checkbox", - "propertyName": "hidden", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Hidden", - "version": 2 - }, - { - "id": "c6885251-96a6-40ce-99b2-4b5209a9e01c", - "type": "checkbox", - "propertyName": "hideLabel", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Hide Label", - "version": 2 - }, - { - "id": "24a8be15-98eb-40f7-99ea-ebb602693e9c", - "type": "editModeSelector", - "propertyName": "editMode", - "parentId": "VmAusRlLZo3cYRFk88xnf", - "label": "Edit mode" - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - }, - { - "id": "oP0uMf56z-zs32j-jCAhx", - "type": "collapsiblePanel", - "propertyName": "pnlFormats", - "label": "Formats", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "G3I8ozdzmOKbUhuYbs68k", - "components": [] - }, - "content": { - "id": "95KYYLKf3eLe-Y2WL5M8x", - "components": [ - { - "id": "26ef465a-b1da-464d-a8e7-59211bc4c68a", - "type": "textField", - "propertyName": "dateFormat", - "parentId": "95KYYLKf3eLe-Y2WL5M8x", - "label": "Date Format", - "defaultValue": "DD/MM/YYYY", - "version": 3, - "textType": "text" - }, - { - "id": "aa619384-f071-4e6c-a472-77c16f55a0d9", - "type": "textField", - "propertyName": "timeFormat", - "parentId": "95KYYLKf3eLe-Y2WL5M8x", - "label": "Time Format", - "defaultValue": "HH:mm:ss", - "version": 3, - "textType": "text" - }, - { - "id": "13baec80-15ca-439d-98ab-a339a0952928", - "type": "textField", - "propertyName": "yearFormat", - "parentId": "95KYYLKf3eLe-Y2WL5M8x", - "label": "Year Format", - "defaultValue": "YYYY", - "version": 3, - "textType": "text" - }, - { - "id": "34302bf1a-0b77-4662-bbd1-278da0f87f72", - "type": "textField", - "propertyName": "quarterFormat", - "parentId": "95KYYLKf3eLe-Y2WL5M8x", - "label": "Quarter Format", - "defaultValue": "YYYY-\\QQ", - "version": 3, - "textType": "text" - }, - { - "id": "1ebdb12b-89c6-44f0-a372-5b6a31537150", - "type": "textField", - "propertyName": "monthFormat", - "parentId": "95KYYLKf3eLe-Y2WL5M8x", - "label": "Month Format", - "defaultValue": "YYYY-MM", - "version": 3, - "textType": "text" - }, - { - "id": "07686226-6322-404f-a32a-cae3f98a5602", - "type": "textField", - "propertyName": "weekFormat", - "parentId": "95KYYLKf3eLe-Y2WL5M8x", - "label": "Week Format", - "defaultValue": "YYYY-wo", - "version": 3, - "textType": "text" - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - }, - { - "id": "kp3afwRnhZk2958QCUvFTFeGWOokZY", - "type": "propertyRouter", - "propertyName": "propertyRouter1", - "componentName": "propertyRouter1", - "label": "Property router1", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "propertyRouteName": { - "_mode": "code", - "_code": " return contexts.canvasContext?.designerDevice || 'desktop';", - "_value": "" - }, - "components": [ - { - "id": "gCBUNeJjyMzcs1G2RRAK8", - "type": "collapsiblePanel", - "propertyName": "pnlStyle", - "label": "Style", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "d8nBKM4Ft50LTL1Supx8N", - "components": [] - }, - "content": { - "id": "Fd2QdtLi2FOTaZJZvFz3Q", - "components": [ - { - "id": "c67b7404-d980-437a-a544-74e4e816f6cd", - "type": "checkbox", - "propertyName": "hideBorder", - "label": "Hide border", - "parentId": "Fd2QdtLi2FOTaZJZvFz3Q", - "hidden": false, - "validate": {}, - "version": 2 - }, - { - "id": "06ab0599-914d-4d2d-875c-765a495472f8", - "type": "codeEditor", - "propertyName": "style", - "label": "Style", - "parentId": "Fd2QdtLi2FOTaZJZvFz3Q", - "validate": {}, - "settingsValidationErrors": [], - "description": "A script that returns the style of the element as an object. This should conform to CSSProperties", - "exposedVariables": [ - { - "name": "data", - "description": "Form values", - "type": "object" - }, - { - "name": "globalState", - "description": "The global state", - "type": "object" - } - ], - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "getStyle" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\").addStandard([\"shesha:formData\", \"shesha:globalState\"]).build();" - }, - { - "id": "8615d12f-6ea0-4b11-a1a1-6088c7160fd9", - "type": "dropdown", - "propertyName": "size", - "parentId": "Fd2QdtLi2FOTaZJZvFz3Q", - "label": "Size", - "allowClear": true, - "values": [ - { - "label": "Small", - "value": "small", - "id": "4f11403c-95fd-4e49-bb60-cb8c25f0f3c3" - }, - { - "label": "Middle", - "value": "middle", - "id": "8f85c476-e632-4fa7-89ad-2be6cfb7f1f1" - }, - { - "label": "Large", - "value": "large", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - } - ], - "dataSourceType": "values", - "version": 5, - "referenceListId": null, - "valueFormat": "listItem" - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - }]}, - { - "id": "kE28pclZaV8tpOab8iJyl", - "type": "collapsiblePanel", - "propertyName": "pnlControlVisibility", - "label": "Control Visibility", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "_DiUNfu1EtZNRRpjO7eHB", - "components": [] - }, - "content": { - "id": "nU7UhIr28XCaiEFbzhB0n", - "components": [ - { - "id": "9fa24c62-8c89-4c84-b96f-44fbca26a69b", - "type": "checkbox", - "propertyName": "showTime", - "parentId": "nU7UhIr28XCaiEFbzhB0n", - "label": "Show Time", - "defaultValue": false, - "version": 2 - }, - { - "id": "cae2b4ed-c957-43e1-90b4-8ac491fb9a16", - "type": "checkbox", - "propertyName": "defaultToMidnight", - "parentId": "nU7UhIr28XCaiEFbzhB0n", - "label": "Default time to midnight", - "hidden": { - "_code": "return !getSettingValue(data.showTime);", - "_mode": "code", - "_value": false - }, - "description": "If checked the default time will be '00:00:00', else it will be the current time on the machine", - "defaultValue": true, - "version": 2 - }, - { - "id": "88266ea0-4ba5-49d1-8f13-b34d684989c0", - "type": "checkbox", - "propertyName": "showNow", - "parentId": "nU7UhIr28XCaiEFbzhB0n", - "label": "Show Today/Now", - "defaultValue": false, - "version": 2 - }, - { - "id": "ac88293a-af3b-45a2-a57c-7703d882b473", - "type": "dropdown", - "propertyName": "disabledDateMode", - "label": "Disabled date mode", - "labelAlign": "right", - "parentId": "nU7UhIr28XCaiEFbzhB0n", - "hidden": false, - "dataSourceType": "values", - "defaultValue": "none", - "values": [ - { - "id": "cedee63a-8679-4cc0-97a0-1814afa08b86", - "label": "None", - "value": "none" - }, - { - "id": "db823427-ee53-4c62-9032-911da93dee9a", - "label": "Function template", - "value": "functionTemplate" - }, - { - "id": "3e402036-63ac-434e-a399-c1551fc32890", - "label": "Custom function", - "value": "customFunction" - } - ], - "description": "", - "version": 5, - "referenceListId": null, - "jsSetting": false, - "valueFormat": "listItem" - }, - { - "id": "ebbd2969-f7d5-4e28-9a7c-56695ed09e59", - "type": "dropdown", - "propertyName": "disabledDateTemplate", - "label": "Disabled date templates", - "labelAlign": "right", - "parentId": "nU7UhIr28XCaiEFbzhB0n", - "hidden": { - "_code": "return getSettingValue(data.disabledDateMode) !== 'functionTemplate';", - "_mode": "code", - "_value": false - }, - "dataSourceType": "values", - "values": [ - { - "id": "ec349b3e-ef29-467c-96fb-265320ac37a0", - "label": "Disable past dates", - "value": "return current && current < moment().startOf('day');" - }, - { - "id": "41db4d59-249d-4ba2-8049-db0fd9fbde14", - "label": "Disable future dates", - "value": "return current && current > moment().endOf('day');" - } - ], - "description": "", - "version": 5, - "jsSetting": false, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "a0143f52-5bdd-418a-9e22-89f2c5d4162d", - "type": "codeEditor", - "propertyName": "disabledDateFunc", - "label": "Disabled Date Func", - "labelAlign": "right", - "parentId": "nU7UhIr28XCaiEFbzhB0n", - "hidden": { - "_code": "return getSettingValue(data.disabledDateMode) !== 'customFunction';", - "_mode": "code", - "_value": false - }, - "description": "Enter disabled date code. You must return true to hide the date. Two objects are exposed to work with, namely 'current' and 'moment'. Use these variables to write the code that will hide the dates you want.", - "validate": {}, - "settingsValidationErrors": [], - "exposedVariables": [ - { - "name": "current", - "description": "Component current value", - "type": "FormInstance" - }, - { - "name": "moment", - "description": "Instance of moment.js", - "type": "object" - } - ], - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "isDateDisabled" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\")\r\n .addDate(\"current\", \"Component current value\")\r\n .addStandard([\"shesha:moment\"])\r\n .build();\r\n " - }, - { - "id": "41613cbd-1943-4df7-b2e3-e9842ba2c2b3", - "type": "dropdown", - "propertyName": "disabledTimeMode", - "label": "Disabled time mode", - "labelAlign": "right", - "parentId": "nU7UhIr28XCaiEFbzhB0n", - "hidden": { - "_code": "return !getSettingValue(data.showTime);", - "_mode": "code", - "_value": false - }, - "dataSourceType": "values", - "defaultValue": "none", - "values": [ - { - "id": "e58b9b6a-b7d4-4d71-9791-e81326300fa2", - "label": "None", - "value": "none" - }, - { - "id": "918f4978-acda-4f4b-a834-9f5a47221fbb", - "label": "Function template", - "value": "timeFunctionTemplate" - }, - { - "id": "a069ac62-9181-470b-8573-b55af4f4a97a", - "label": "Custom function", - "value": "customTimeFunction" - } - ], - "description": "", - "version": 5, - "referenceListId": null, - "jsSetting": false, - "valueFormat": "listItem" - }, - { - "id": "692081a5-7e96-401e-90e0-9da5b5bfd836", - "type": "dropdown", - "propertyName": "disabledTimeTemplate", - "label": "Disabled time templates", - "labelAlign": "right", - "parentId": "nU7UhIr28XCaiEFbzhB0n", - "hidden": { - "_code": "return getSettingValue(data.disabledTimeMode) !== 'timeFunctionTemplate';", - "_mode": "code", - "_value": false - }, - "dataSourceType": "values", - "values": [ - { - "id": "ec349b3e-ef29-467c-96fb-265320ac37a0", - "label": "Disabled past times", - "value": "disabledPastTime" - }, - { - "id": "41db4d59-249d-4ba2-8049-db0fd9fbde14", - "label": "Disabled future times", - "value": "disabledFutureTime" - } - ], - "description": "", - "version": 5, - "jsSetting": false, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "32c21b4d-0b55-43be-9d9a-312370769c3b", - "type": "codeEditor", - "propertyName": "disabledTimeFunc", - "label": "Disabled Time Func", - "labelAlign": "right", - "parentId": "nU7UhIr28XCaiEFbzhB0n", - "hidden": { - "_code": "return getSettingValue(data.disabledTimeMode) !== 'customTimeFunction';", - "_mode": "code", - "_value": false - }, - "description": "Enter disabled time code. You must return a lamda function with object of DisabledTimes(disabledHours, disabledMinutes, disabledSeconds). Two objects is exposed to work with, namely 'range', 'moment'. Use these variables to write the code that will hide the dates you want.", - "validate": {}, - "settingsValidationErrors": [], - "exposedVariables": [ - { - "name": "range", - "description": "Creates an array of numbers (positive and/or negative) progressing from start up to, but not including, end. If end is not specified it’s set to start with start then set to 0. If end is less than start a zero-length range is created unless a negative step is specified.", - "type": "function" - }, - { - "name": "moment", - "description": "Instance of moment.js", - "type": "object" - } - ], - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "isTimeDisabled" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\")\r\n .addFunction(\"range\", \"Creates an array of numbers (positive and/or negative) progressing from start up to, but not including, end. If end is not specified it’s set to start with start then set to 0. If end is less than start a zero-length range is created unless a negative step is specified.\")\r\n .addStandard([\"shesha:moment\"])\r\n .build();\r\n " - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - }, - { - "id": "WJlrXoQpXXzHVvk-VGn-z", - "type": "collapsiblePanel", - "propertyName": "pnlEvents", - "label": "Events", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "i8dLAW_jVLivqO6nDxhaD", - "components": [] - }, - "content": { - "id": "_m7e9WBdpkz4CsCrpVx8d", - "components": [ - { - "id": "26347fb3-4c52-4d24-9fb7-54d266b18ea4", - "type": "codeEditor", - "propertyName": "onChangeCustom", - "label": "On Change", - "labelAlign": "right", - "parentId": "_m7e9WBdpkz4CsCrpVx8d", - "hidden": false, - "description": "Enter custom eventhandler on changing of event. (form, value, dateString) are exposed", - "validate": {}, - "settingsValidationErrors": [], - "exposedVariables": [ - { - "name": "data", - "description": "Selected form values", - "type": "object" - }, - { - "name": "dateString", - "description": "Date string of current value", - "type": "string" - }, - { - "name": "form", - "description": "Form instance", - "type": "FormInstance" - }, - { - "name": "formMode", - "description": "The form mode", - "type": "'readonly' | 'edit' | 'designer'" - }, - { - "name": "globalState", - "description": "The global state of the application", - "type": "object" - }, - { - "name": "http", - "description": "axios instance used to make http requests", - "type": "object" - }, - { - "name": "message", - "description": "This is the Ant API for displaying toast messages. See: https://ant.design/components/message/#header", - "type": "object" - }, - { - "name": "moment", - "description": "The moment.js object", - "type": "object" - }, - { - "name": "current", - "description": "Component current value", - "type": "object" - }, - { - "name": "setFormData", - "description": "A function used to update the form data", - "type": "({ values: object, mergeValues: boolean}) => void" - }, - { - "name": "setGlobalState", - "description": "Setting the global state of the application", - "type": "(payload: { key: string, data: any } ) => void" - } - ], - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "onChange" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\")\r\n .addAllStandard()\r\n .addString(\"dateString\", \"Date string value\")\r\n .addDate(\"value\", \"Component current value\")\r\n .build();" - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - }, - { - "id": "UgwgMBSPX2eKVlZKWGtcc", - "type": "collapsiblePanel", - "propertyName": "pnlValidation", - "label": "Validation", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "2S9VA8QkefAxCRQG-Ua3A", - "components": [] - }, - "content": { - "id": "WQ3g0fIWQ9XjijBUSolq6", - "components": [ - { - "id": "3be9da3f-f47e-48ae-b4c3-f5cc36e534d9", - "type": "checkbox", - "propertyName": "validate.required", - "parentId": "WQ3g0fIWQ9XjijBUSolq6", - "label": "Required", - "version": 2 - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [], - "components": [ - { - "id": "WQ3g0fIWQ9XjijBUSolq6", - "components": [ - { - "id": "3be9da3f-f47e-48ae-b4c3-f5cc36e534d9", - "type": "checkbox", - "propertyName": "validate.required", - "parentId": "WQ3g0fIWQ9XjijBUSolq6", - "label": "Required", - "version": 2 - } - ], - "parentId": "UgwgMBSPX2eKVlZKWGtcc" - } - ] - }, - { - "id": "6Vw9iiDw9d0MD_Rh5cbIn", - "type": "collapsiblePanel", - "propertyName": "pnlSecurity", - "label": "Security", - "labelAlign": "right", - "parentId": "root", - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "mbzE03mJnv_89oUvkmjPs", - "components": [] - }, - "content": { - "id": "SsWPpiJfTbgl4iHlFA_or", - "components": [ - { - "id": "1adea529-1f0c-4def-bd41-ee166a5dfcd7", - "type": "permissionAutocomplete", - "propertyName": "permissions", - "parentId": "SsWPpiJfTbgl4iHlFA_or", - "label": "Permissions", - "validate": {}, - "version": 1 - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - } - ], - "formSettings": { - "layout": "horizontal", - "colon": true, - "labelCol": { - "span": 8 - }, - "wrapperCol": { - "span": 16 - }, - "displayName": "DEFAULT_FORM_SETTINGS", - "__docgenInfo": { - "description": "Default form settings", - "displayName": "DEFAULT_FORM_SETTINGS", - "props": {} - } - } -} diff --git a/shesha-reactjs/src/designer-components/dateField/settingsForm.ts b/shesha-reactjs/src/designer-components/dateField/settingsForm.ts new file mode 100644 index 0000000000..63fc02479e --- /dev/null +++ b/shesha-reactjs/src/designer-components/dateField/settingsForm.ts @@ -0,0 +1,890 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs } from '../_settings/utils/border/utils'; +import { getCornerInputs } from '../_settings/utils/border/utils'; +import { IDateFieldProps } from './interfaces'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data: IDateFieldProps) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const eventsTabId = nanoid(); + const validationTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'placeholder', + label: 'Placeholder', + type: 'textField', + size: 'small', + jsSetting: true, + }, + { + id: nanoid(), + type: 'textArea', + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + + { + id: nanoid(), + type: 'switch', + propertyName: 'range', + label: 'Range', + jsSetting: true, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + + + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + type: 'editModeSelector', + propertyName: 'editMode', + defaultValue: 'inherited', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + }, + ], + }) + .toJson() + ] + }, + { + key: '2', + title: 'Data', + id: dataTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'picker', + label: 'Picker', + type: 'dropdown', + size: 'small', + jsSetting: true, + dropdownOptions: [ + { + value: "date", + label: "Date" + }, + { + value: "week", + label: "Week" + }, + { + value: "month", + label: "Month" + }, + { + value: "quarter", + label: "Quarter" + }, + { + value: "year", + label: "Year" + } + ] + }, + { + + id: nanoid(), + type: 'switch', + propertyName: 'resolveToUTC', + label: 'Resolve to UTC', + jsSetting: true, + + }, + + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { _code: 'return getSettingValue(data?.picker) !== "date"', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'showTime', + label: 'Show Time', + size: 'small', + jsSetting: true, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'defaultToMidnight', + label: 'Default time to midnight', + size: 'small', + defaultValue: true, + jsSetting: true, + hidden: { _code: 'return !getSettingValue(data?.showTime);', _mode: 'code', _value: false } as any, + }, + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { _code: 'return getSettingValue(data?.picker) !== "date"', _mode: 'code', _value: false } as any, + inputs: + [ + { + id: nanoid(), + type: 'switch', + propertyName: 'showNow', + label: 'Show Today/Now', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: dataTabId, + + } + ] + + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + type: 'dropdown', + propertyName: 'disabledDateMode', + label: 'Disabled Date Mode', + parentId: dataTabId, + defaultValue: 'none', + dropdownOptions: [ + { value: 'none', label: 'None' }, + { value: 'functionTemplate', label: 'Function template' }, + { value: 'customFunction', label: 'Custom function' }, + ] + }, + { + id: nanoid(), + type: 'dropdown', + propertyName: 'disabledDateTemplate', + label: 'Disabled Date Templates', + parentId: dataTabId, + hidden: { _code: "return getSettingValue(data.disabledDateMode) !== 'functionTemplate'", _mode: 'code', _value: false } as any, + dropdownOptions: [ + { value: "return current && current < moment().startOf('day');", label: 'Disable past dates' }, + { value: "return current && current > moment().endOf('day');", label: 'Disable future dates' } + ] + }, + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'disabledDateFunc', + label: 'Disabled Date Func', + description: "Enter disabled date code. You must return true to hide the date. Two objects are exposed to work with, namely 'current' and 'moment'. Use these variables to write the code that will hide the dates you want.", + hidden: { _code: "return getSettingValue(data.disabledDateMode) !== 'customFunction'", _mode: 'code', _value: false } as any, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + type: 'dropdown', + propertyName: 'disabledTimeMode', + label: 'Disabled Time Mode', + defaultValue: 'none', + dropdownOptions: [ + { value: 'none', label: 'None' }, + { value: 'timeFunctionTemplate', label: 'Function template' }, + { value: 'customTimeFunction', label: 'Custom function' }, + ], + hidden: { + _code: 'return !getSettingValue(data?.showTime);', + _mode: 'code', + _value: false + } as any + }, + { + id: nanoid(), + type: 'dropdown', + propertyName: 'disabledTimeTemplate', + label: 'Disabled Time Templates', + hidden: { + _code: "return getSettingValue(data.disabledTimeMode) !== 'timeFunctionTemplate';", + _mode: 'code', + _value: false + } as any, + dropdownOptions: [ + { value: "disabledPastTime", label: 'Disable past times' }, + { value: "disabledFutureTime", label: 'Disable future times' } + ] + }, + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'disabledTimeFunc', + label: 'Disabled Time Func', + hidden: { _code: "return getSettingValue(data.disabledTimeMode) !== 'customTimeFunction';", _mode: 'code', _value: false } as any, + } + ] + }) + + + + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { _code: 'return getSettingValue(data?.picker) !== "date"', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'dateFormat', + label: 'Date Format', + size: 'small', + jsSetting: true, + type: "textField", + defaultValue: "DD/MM/YYYY", + parentId: dataTabId, + }, { + id: nanoid(), + propertyName: 'timeFormat', + label: 'Time Format', + size: 'small', + jsSetting: true, + type: "textField", + defaultValue: "HH:mm:ss", + parentId: dataTabId, + }, + ] + + }) + + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + + id: nanoid(), + propertyName: 'yearFormat', + label: 'Year Format', + size: 'small', + jsSetting: true, + type: "textField", + defaultValue: "YYYY", + parentId: dataTabId, + hidden: { _code: 'return getSettingValue(data?.picker) !== "year"', _mode: 'code', _value: false } as any, + + }, + { + id: nanoid(), + propertyName: 'quarterFormat', + label: 'Quarter Format', + size: 'small', + jsSetting: true, + type: "textField", + defaultValue: "YYYY-\\QQ", + parentId: dataTabId, + hidden: { _code: 'return getSettingValue(data?.picker) !== "quarter"', _mode: 'code', _value: false } as any, + + }, + { + + id: nanoid(), + propertyName: 'monthFormat', + label: 'Month Format', + size: 'small', + jsSetting: true, + type: "textField", + defaultValue: "YYYY-MM", + parentId: dataTabId, + hidden: { _code: 'return getSettingValue(data?.picker) !== "month"', _mode: 'code', _value: false } as any, + + }, + { + id: nanoid(), + propertyName: 'weekFormat', + label: 'Week Format', + size: 'small', + jsSetting: true, + type: "textField", + defaultValue: "YYYY-wo", + parentId: dataTabId, + hidden: { _code: 'return getSettingValue(data?.picker) !== "week"', _mode: 'code', _value: false } as any, + + } + + ] + + }) + + + .toJson() + ] + }, + { + key: '4', + title: 'Events', + id: eventsTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onChangeCustom', + label: 'On Change', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on changing of event.', + parentId: eventsTabId, + availableConstantsExpression: ' return metadataBuilder.object(\"constants\")\r\n .addAllStandard()\r\n .addString(\"dateString\", \"Date string value\")\r\n .addDate(\"value\", \"Component current value\")\r\n .build();', + }) + .toJson() + ] + }, + { + key: '5', + title: 'Validation', + id: validationTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'validate.required', + label: 'Required', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: validationTabId + }) + .toJson() + ] + }, + { + key: '6', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: nanoid(), + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: nanoid(), + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: nanoid(), + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inline: true, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "background.size", + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + propertyName: "background.position", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson()] + }).toJson()] + }, + { + key: '7', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dateField/style.ts b/shesha-reactjs/src/designer-components/dateField/style.ts new file mode 100644 index 0000000000..f9bedbeaf8 --- /dev/null +++ b/shesha-reactjs/src/designer-components/dateField/style.ts @@ -0,0 +1,30 @@ +import { createStyles } from '@/styles'; +import { CSSProperties } from 'react'; + +export const useStyles = createStyles(({ css, cx }, { fullStyles }: { fullStyles: CSSProperties }) => { + + const dateField = cx( + "sha-dropdown", + css` + --ant-color-text: ${fullStyles.color} !important; + --ant-font-size: ${fullStyles.fontSize} !important; + --ant-font-weight: ${fullStyles.fontWeight} !important; + --ant-text-align: ${fullStyles.textAlign} !important; + + + .ant-picker-input input { + font-weight: var(--ant-font-weight) !important; + text-align: var(--ant-text-align) !important; + } + + .ant-picker-cell-inner { + font-weight: var(--ant-font-weight) !important; + } + ` + ); + + + return { + dateField + }; +}); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/drawer/drawer.tsx b/shesha-reactjs/src/designer-components/drawer/drawer.tsx index fb1cbdea54..4f044d4bd5 100644 --- a/shesha-reactjs/src/designer-components/drawer/drawer.tsx +++ b/shesha-reactjs/src/designer-components/drawer/drawer.tsx @@ -1,33 +1,47 @@ import ComponentsContainer from '@/components/formDesigner/containers/componentsContainer'; import ParentProvider from '@/providers/parentProvider/index'; -import React, { FC, Fragment, useState } from 'react'; -import { - Alert, - Button, - Drawer, - DrawerProps, - Space - } from 'antd'; +import React, { CSSProperties, FC, Fragment, ReactNode, useState } from 'react'; +import { Alert, Button, Drawer, Space } from 'antd'; import { executeScriptSync, useAvailableConstantsData } from '@/providers/form/utils'; import { IConfigurableActionConfiguration } from '@/interfaces/configurableAction'; -import { IDrawerProps } from './models'; -import { - useConfigurableAction, - useConfigurableActionDispatcher, -} from '@/providers/configurableActionsDispatcher'; - -export interface IShaDrawerProps extends Omit, Omit { } +import { useConfigurableAction, useConfigurableActionDispatcher } from '@/providers/configurableActionsDispatcher'; +import { IConfigurableFormComponent } from '@/providers'; +import { IDimensionsValue } from '../_settings/utils/dimensions/interfaces'; +interface IShaDrawer { + id?: string; + componentName?: string; + showFooter?: boolean; + label?: string | ReactNode; + onOkAction?: IConfigurableActionConfiguration; + onCancelAction?: IConfigurableActionConfiguration; + okText?: string; + cancelText?: string; + components?: IConfigurableFormComponent[]; + style?: CSSProperties; + isDynamic?: boolean; + okButtonCustomEnabled?: string; + cancelButtonCustomEnabled?: string; + showHeader?: boolean; + headerStyle?: CSSProperties; + footerStyle?: CSSProperties; + placement?: 'top' | 'right' | 'bottom' | 'left'; + width?: string | number; + readOnly?: boolean; + backgroundStyles?: CSSProperties; + dimensions?: IDimensionsValue; + stylingBoxAsCSS?: CSSProperties; + allStyles?: any; +} interface IShaDrawerState { open?: boolean; } -const ShaDrawer: FC = props => { +const ShaDrawer: FC = (props) => { const { id, placement, - width, - propertyName: name, + componentName: name, readOnly, label, onOkAction, @@ -35,22 +49,47 @@ const ShaDrawer: FC = props => { okText, cancelText, components, + style, isDynamic, okButtonCustomEnabled, cancelButtonCustomEnabled, + showHeader, + headerStyle, + footerStyle, + showFooter, } = props; - const allData = useAvailableConstantsData(); const [state, setState] = useState(); const { executeAction } = useConfigurableActionDispatcher(); + const { + paddingTop, + paddingRight, + paddingBottom, + paddingLeft, + marginRight, + marginLeft, + marginBottom, + marginTop, + backgroundImage, + backgroundSize, + backgroundPosition, + backgroundRepeat, + backgroundColor, + minHeight, + minWidth, + maxHeight, + maxWidth, + width, + height, + ...rest + } = style; - const openDrawer = () => setState(prev => ({ ...prev, open: true })); + const openDrawer = () => setState((prev) => ({ ...prev, open: true })); - const closeDrawer = () => setState(prev => ({ ...prev, open: false })); + const closeDrawer = () => setState((prev) => ({ ...prev, open: false })); const actionOwnerName = `Drawer (${name})`; - /// NAVIGATION const executeActionIfConfigured = (actionConfiguration: IConfigurableActionConfiguration) => { if (!actionConfiguration) { @@ -105,8 +144,12 @@ const ShaDrawer: FC = props => { globalState: allData.globalState, }; - const okButtonDisabled = !!okButtonCustomEnabled ? !executeScriptSync(okButtonCustomEnabled, context) : false; - const cancelButtonDisabled = !!cancelButtonCustomEnabled ? !executeScriptSync(cancelButtonCustomEnabled, context) : false; + const okButtonDisabled = !!okButtonCustomEnabled + ? !executeScriptSync(okButtonCustomEnabled, context) + : false; + const cancelButtonDisabled = !!cancelButtonCustomEnabled + ? !executeScriptSync(cancelButtonCustomEnabled, context) + : false; if (allData.form?.formMode === 'designer') { return ( @@ -126,7 +169,38 @@ const ShaDrawer: FC = props => { open={state?.open} placement={placement} width={width} + height={height} onClose={closeDrawer} + styles={{ + header: { display: showHeader ? 'block' : 'none', ...headerStyle }, + footer: { display: showFooter ? 'block' : 'none', ...footerStyle }, + body: { + backgroundImage, + backgroundSize, + backgroundPosition, + backgroundRepeat, + backgroundColor, + paddingTop, + paddingRight, + paddingBottom, + paddingLeft, + }, + content: { + ...rest, + }, + wrapper: { + width, + height, + minWidth, + maxWidth, + minHeight, + maxHeight, + marginTop, + marginRight, + marginBottom, + marginLeft, + }, + }} title={label} size="large" footer={ @@ -144,7 +218,7 @@ const ShaDrawer: FC = props => { ({ ...c, readOnly: readOnly })) : []} + dynamicComponents={isDynamic ? components?.map((c) => ({ ...c, readOnly: readOnly })) : []} /> diff --git a/shesha-reactjs/src/designer-components/drawer/index.tsx b/shesha-reactjs/src/designer-components/drawer/index.tsx index 22173e3520..69eed8a0ed 100644 --- a/shesha-reactjs/src/designer-components/drawer/index.tsx +++ b/shesha-reactjs/src/designer-components/drawer/index.tsx @@ -1,17 +1,18 @@ -import React from 'react'; +import { migrateNavigateAction } from '@/designer-components/_common-migrations/migrate-navigate-action'; +import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; +import { useFormComponentStyles } from '@/hooks/formComponentHooks'; import { IToolboxComponent } from '@/interfaces'; +import { IInputStyles } from '@/providers'; +import { validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { removeUndefinedProps } from '@/utils/object'; import { SwapOutlined } from '@ant-design/icons'; +import React, { CSSProperties } from 'react'; +import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; import ShaDrawer from './drawer'; import { IDrawerProps } from './models'; -import { getSettings } from './settings'; -import { getLayoutStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; -import { - migrateCustomFunctions, - migratePropertyName, -} from '@/designer-components/_common-migrations/migrateSettings'; -import { useFormData, useGlobalState } from '@/providers'; -import { migrateNavigateAction } from '@/designer-components/_common-migrations/migrate-navigate-action'; -import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; const DrawerComponent: IToolboxComponent = { type: 'drawer', @@ -19,21 +20,87 @@ const DrawerComponent: IToolboxComponent = { name: 'Drawer', icon: , Factory: ({ model }) => { - const { data } = useFormData(); - const { globalState } = useGlobalState(); - const { size, style, ...props } = model; + const { allStyles, style, headerStyles, footerStyles, ...props } = model; + + const { + backgroundStyles: headerBackgroundStyles, + shadowStyles: headerShadowStyles, + jsStyle: headerJsStyle, + } = useFormComponentStyles(headerStyles); + const { + backgroundStyles: footerBackgroundStyles, + shadowStyles: footerShadowStyles, + jsStyle: footerJsStyle, + } = useFormComponentStyles(footerStyles); + + const additionalHeaderStyles: CSSProperties = removeUndefinedProps({ + ...headerShadowStyles, + ...headerBackgroundStyles, + ...headerJsStyle, + }); + + const additionalFooterStyles: CSSProperties = removeUndefinedProps({ + ...footerShadowStyles, + ...footerBackgroundStyles, + ...footerJsStyle, + }); - return ; + return ( + + ); }, settingsFormMarkup: (data) => getSettings(data), - migrator: (m) => m.add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(1, (prev) => ({ - ...prev, - onOkAction: migrateNavigateAction(prev.onOkAction), - onCancelAction: migrateNavigateAction(prev.onCancelAction), - })) - .add(2, (prev) => ({...migrateFormApi.properties(prev)})) - , + migrator: (m) => + m + .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(1, (prev) => ({ + ...prev, + onOkAction: migrateNavigateAction(prev.onOkAction), + onCancelAction: migrateNavigateAction(prev.onCancelAction), + })) + .add(2, (prev) => ({ ...migrateFormApi.properties(prev) })) + .add(3, (prev) => { + const styles: IInputStyles = { + size: prev.size, + width: prev.width, + height: prev.height, + hideBorder: prev.hideBorder, + borderSize: prev.borderSize, + borderRadius: prev.borderRadius, + borderColor: prev.borderColor, + fontSize: prev.fontSize, + fontColor: prev.fontColor, + backgroundColor: prev.backgroundColor, + stylingBox: prev.stylingBox, + }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }) + .add(4, (prev) => ({ + ...migratePrevStyles(prev, defaultStyles()), + desktop: { + ...migratePrevStyles(prev, defaultStyles()).desktop, + headerStyles: defaultStyles(), + footerStyles: defaultStyles(), + }, + tablet: { + ...migratePrevStyles(prev, defaultStyles()).tablet, + headerStyles: defaultStyles(), + footerStyles: defaultStyles(), + }, + mobile: { + ...migratePrevStyles(prev, defaultStyles()).mobile, + headerStyles: defaultStyles(), + footerStyles: defaultStyles(), + }, + })), initModel: (model) => { const customProps: IDrawerProps = { ...model, diff --git a/shesha-reactjs/src/designer-components/drawer/models.ts b/shesha-reactjs/src/designer-components/drawer/models.ts index bcad3cb16c..efc16064cd 100644 --- a/shesha-reactjs/src/designer-components/drawer/models.ts +++ b/shesha-reactjs/src/designer-components/drawer/models.ts @@ -1,8 +1,11 @@ import { IConfigurableActionConfiguration } from '@/interfaces/configurableAction'; import { IConfigurableFormComponent } from '@/interfaces/formDesigner'; +import { IInputStyles } from '@/index'; +import { IBackgroundValue } from '../_settings/utils/background/interfaces'; export interface IDrawerProps extends IConfigurableFormComponent { showFooter?: boolean; + showHeader?: boolean; showOkayBtn?: boolean; onOkAction?: IConfigurableActionConfiguration; @@ -17,6 +20,20 @@ export interface IDrawerProps extends IConfigurableFormComponent { placement?: 'top' | 'right' | 'bottom' | 'left'; height?: string | number; width?: string | number; + hideBorder?: boolean; + stylingBox?: string; + borderSize?: number; + borderRadius?: number; + borderColor?: string; + fontColor?: string; + backgroundColor?: string; + fontSize?: number; + headerStyles?: IDrawerProps; + footerStyles?: any; + background?: IBackgroundValue; + desktop?: IInputStyles; + tablet?: IInputStyles; + mobile?: IInputStyles; components?: IConfigurableFormComponent[]; } diff --git a/shesha-reactjs/src/designer-components/drawer/settings.ts b/shesha-reactjs/src/designer-components/drawer/settings.ts deleted file mode 100644 index 67b7addeb7..0000000000 --- a/shesha-reactjs/src/designer-components/drawer/settings.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { nanoid } from '@/utils/uuid'; -import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; - -export const getSettings = (data: { readOnly?: boolean }) => - new DesignerToolbarSettings(data) - .addCollapsiblePanel({ - id: '11114bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'pnlDisplay', - parentId: 'root', - label: 'Display', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - components: [ - ...new DesignerToolbarSettings() - .addTextField({ - id: nanoid(), - propertyName: 'componentName', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Component name', - validate: { required: true }, - jsSetting: false, - }) - .addDropdown({ - id: nanoid(), - propertyName: 'placement', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - hidden: false, - label: 'Placement', - dataSourceType: 'values', - jsSetting: false, - values: [ - { id: nanoid(), label: 'top', value: 'top' }, - { id: nanoid(), label: 'right', value: 'right' }, - { id: nanoid(), label: 'bottom', value: 'bottom' }, - { id: nanoid(), label: 'left', value: 'left' }, - ], - validate: { required: true }, - }) - .addTextField({ - id: nanoid(), - propertyName: 'width', - label: 'Width', - description: 'Width of the Drawer dialog in % or px', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - placeholder: '70% or 800px', - hidden: { - _code: - 'return getSettingValue(data?.placement) !== "right" && getSettingValue(data?.placement) !== "left";', - _mode: 'code', - _value: false, - } as any, - }) - .addTextField({ - id: nanoid(), - propertyName: 'height', - label: 'Height', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - description: 'Placement is top or bottom, height of the Drawer dialog', - hidden: { - _code: - 'return getSettingValue(data?.placement) !== "bottom" && getSettingValue(data?.placement) !== "top";', - _mode: 'code', - _value: false, - } as any, - placeholder: '70% or 800px', - }) - .toJson(), - ], - }, - }) - .addCollapsiblePanel({ - id: '22224bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'pnlFooter', - parentId: 'root', - label: 'Footer', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b69', - components: [ - ...new DesignerToolbarSettings() - .addCheckbox({ - id: nanoid(), - propertyName: 'showFooter', - label: 'Show Footer', - description: 'Whether Ok and Cancel buttons are shown', - parentId: 'pnl24bf6-f76d-4139-a850-c99bf06c8b69', - jsSetting: false, - }) - .addContainer({ - id: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'containerComponents', - direction: 'vertical', - hidden: { _code: 'return !getSettingValue(data?.showFooter);', _mode: 'code', _value: false } as any, - parentId: 'pnl24bf6-f76d-4139-a850-c99bf06c8b69', - components: new DesignerToolbarSettings() - .addSectionSeparator({ id: nanoid(), propertyName: 'okButtonSeparator', label: 'Ok button' }) - .addConfigurableActionConfigurator({ - id: nanoid(), - propertyName: 'onOkAction', - parentId: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Ok Action', - }) - .addTextField({ - id: nanoid(), - propertyName: 'okText', - parentId: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Ok Text', - description: 'The text that will be displayed on the Ok button', - }) - .addCodeEditor({ - id: nanoid(), - propertyName: 'okButtonCustomEnabled', - parentId: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Custom Enabled', - description: 'Enter custom enabled of the Ok button', - exposedVariables: [ - { - id: '788673a5-5eb9-4a9a-a34b-d8cea9cacb3c', - name: 'data', - description: 'Form data', - type: 'object', - }, - { - id: '65b71112-d412-401f-af15-1d3080f85319', - name: 'globalState', - description: 'The global state', - type: 'object', - }, - ], - }) - - .addSectionSeparator({ - id: nanoid(), - propertyName: 'cancelButtonSeparator', - label: 'Cancel button', - parentId: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', - }) - .addConfigurableActionConfigurator({ - id: nanoid(), - propertyName: 'onCancelAction', - label: 'Ok Cancel', - parentId: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', - }) - .addTextField({ - id: nanoid(), - propertyName: 'cancelText', - label: 'Cancel Text', - description: 'The text that will be displayed on the Cancel button', - parentId: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', - }) - .addCodeEditor({ - id: nanoid(), - propertyName: 'cancelButtonCustomEnabled', - parentId: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Custom Enabled', - description: 'Enter custom enabled of the Cancel button', - exposedVariables: [ - { - id: '788673a5-5eb9-4a9a-a34b-d8cea9cacb3c', - name: 'data', - description: 'Form data', - type: 'object', - }, - { - id: '65b71112-d412-401f-af15-1d3080f85319', - name: 'globalState', - description: 'The global state', - type: 'object', - }, - ], - }) - .toJson(), - }) - .toJson(), - ], - }, - }) - .addCollapsiblePanel({ - id: 'ecd2c3f2-3fe7-459c-8d5e-1e5b89755151', - propertyName: 'pnlStyle', - parentId: 'root', - label: 'Style', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: '1452191c-1b5c-4039-b137-f6b472cc9e89', - components: [ - ...new DesignerToolbarSettings() - .addCodeEditor({ - id: nanoid(), - propertyName: 'style', - parentId: '1452191c-1b5c-4039-b137-f6b472cc9e89', - label: 'Style', - description: - 'A script that returns the style of the element as an object. This should conform to CSSProperties', - exposedVariables: [ - { - id: '788673a5-5eb9-4a9a-a34b-d8cea9cacb3c', - name: 'data', - description: 'Form data', - type: 'object', - }, - { - id: '65b71112-d412-401f-af15-1d3080f85319', - name: 'globalState', - description: 'The global state', - type: 'object', - }, - ], - }) - .addStyleBox({ - id: 'f8a00759-406b-4568-b65c-2eee6aeb3448', - propertyName: 'stylingBox', - parentId: 'root', - validate: {}, - settingsValidationErrors: [], - jsSetting: false, - }) - .toJson(), - ], - }, - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] - } - }) - .toJson(); diff --git a/shesha-reactjs/src/designer-components/drawer/settingsForm.ts b/shesha-reactjs/src/designer-components/drawer/settingsForm.ts new file mode 100644 index 0000000000..8379a15a24 --- /dev/null +++ b/shesha-reactjs/src/designer-components/drawer/settingsForm.ts @@ -0,0 +1,1272 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/lib/form/Form'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { + backgroundTypeOptions, + positionOptions, + repeatOptions, + sizeOptions, +} from '../_settings/utils/background/utils'; + +export const getSettings = (data: any) => { + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: 'W_m7doMyCpCYwAYDfRh6I', + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: 's4gmBg31azZC0UjZjpfTm', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'placeholder-tooltip-s4gmBg31azZC0UjZjpfTm', + parentId: 's4gmBg31azZC0UjZjpfTm', + inputType: 'textField', + propertyName: 'componentName', + label: 'Component Name', + size: 'large', + jsSetting: true, + }) + .addSettingsInputRow({ + id: 'headerTitle-row-s4gmBg31azZC0UjZjpfTm', + parentId: 's4gmBg31azZC0UjZjpfTm', + inputs: [ + { + id: 'headerTitle-input-s4gmBg31azZC0UjZjpfTm', + type: 'textField', + propertyName: 'label', + label: 'Header Title', + size: 'large', + jsSetting: true, + }, + ], + hidden: { + _code: 'return !getSettingValue(data?.showHeader);', + _mode: 'code', + _value: false, + } as any, + }) + .addCollapsiblePanel({ + id: 'placement-panel-s4gmBg31azZC0UjZjpfTm', + propertyName: 'action', + label: 'Actions', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + parentId: 's4gmBg31azZC0UjZjpfTm', + content: { + id: 'placement-content-s4gmBg31azZC0UjZjpfTm', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: '12d700d6-ed4d-49d5-9cfd-fe8f00d6-ed4d-49d5-9cfd-fe8f00w0f3b6', + parentId: 'placement-content-s4gmBg31azZC0UjZjpfTm', + inputs: [ + { + id: '12d700d6-ed4d-49d5-9cfd-fe8f00w0f3b6', + inputType: 'switch', + type: 'switch', + propertyName: 'showHeader', + label: 'Show Header', + size: 'small', + jsSetting: true, + defaultValue: false, + }, + { + id: '12d700d6-ed4d-49d5-9cfd-fe8f0060f3b6', + inputType: 'switch', + type: 'switch', + propertyName: 'showFooter', + label: 'Show Action Buttons', + size: 'small', + jsSetting: true, + defaultValue: false, + }, + ], + }) + .addContainer({ + id: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', + propertyName: 'containerComponents', + direction: 'vertical', + hidden: { + _code: 'return !getSettingValue(data?.showFooter);', + _mode: 'code', + _value: false, + } as any, + parentId: 'placement-content-s4gmBg31azZC0UjZjpfTm', + components: new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'okButtonCollapsiblePanel', + label: 'Ok Button', + parentId: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', + content: { + id: nanoid(), + components: new DesignerToolbarSettings() + .addConfigurableActionConfigurator({ + id: nanoid(), + propertyName: 'onOkAction', + parentId: nanoid(), + label: 'Ok Action', + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'okText', + parentId: nanoid(), + label: 'Ok Text', + description: 'The text that will be displayed on the Ok button', + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'okButtonCustomEnabled', + parentId: nanoid(), + label: 'Custom Enabled', + inputType: 'codeEditor', + description: 'Enter custom enabled of the Ok button', + }) + .toJson(), + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'cancelButtonCollapsiblePanel', + label: 'Cancel Button', + parentId: 'ccc24bf6-f76d-4139-a850-c99bf06c8b69', + content: { + id: nanoid(), + components: new DesignerToolbarSettings() + .addConfigurableActionConfigurator({ + id: nanoid(), + propertyName: 'onCancelAction', + label: 'Ok Cancel', + parentId: nanoid(), + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'cancelText', + label: 'Cancel Text', + description: 'The text that will be displayed on the Cancel button', + parentId: nanoid(), + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'cancelButtonCustomEnabled', + label: 'Custom Enabled', + inputType: 'codeEditor', + description: 'Enter custom enabled of the Cancel button', + parentId: nanoid(), + }) + .toJson(), + }, + }) + .toJson(), + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }, + { + key: '4', + title: 'Appearance', + id: 'elgrlievlfwehhh848r8hsdnflsdnclurbd', + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: 'styleRouter', + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: 'elgrlievlfwehhh848r8hsdnflsdnclurbd', + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'predefinedOrientation', + propertyName: 'placement', + label: 'Slide Direction', + inputType: 'dropdown', + hidden: false, + defaultValue: 'right', + dropdownOptions: [ + { label: 'Top', value: 'top' }, + { label: 'Right', value: 'right' }, + { label: 'Bottom', value: 'bottom' }, + { label: 'Left', value: 'left' }, + ], + validate: { required: true }, + }) + .addCollapsiblePanel({ + id: 'dimensionsStyleCollapsiblePanel', + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: 'styleRouter', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: 'dimensionsStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'dimensionsStyleRowWidth', + parentId: 'dimensionsStylePnl', + inline: true, + inputs: [ + { + type: 'textField', + id: 'width-dimensions-s4gmBg31azZC0UjZjpfTm', + label: 'Width', + width: 85, + propertyName: 'dimensions.width', + icon: 'widthIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minWidth', + icon: 'minWidthIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxWidth', + icon: 'maxWidthIcon', + }, + ], + }) + .addSettingsInputRow({ + id: 'dimensionsStyleRowHeight', + parentId: 'dimensionsStylePnl', + inline: true, + inputs: [ + { + type: 'textField', + id: 'height-dimensions-s4gmBg31azZC0UjZjpfTm', + label: 'Height', + width: 85, + propertyName: 'dimensions.height', + icon: 'heightIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minHeight', + icon: 'minHeightIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxHeight', + icon: 'maxHeightIcon', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'backgroundStyleCollapsiblePanel', + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: 'backgroundStylePnl-main', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'backgroundStyleRow-selectType-main', + parentId: 'backgroundStylePnl-main', + label: 'Type', + jsSetting: false, + propertyName: 'background.type', + inputType: 'radio', + tooltip: 'Select a type of background', + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-color-main', + parentId: 'backgroundStylePnl-main', + inputs: [ + { + type: 'colorPicker', + id: 'backgroundStyleRow-color-picker-main', + label: 'Color', + propertyName: 'background.color', + hideLabel: true, + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'backgroundStyle-gradientColors-row-main', + parentId: 'backgroundStylePnl-main', + inputs: [ + { + type: 'multiColorPicker', + id: 'backgroundStyle-gradientColors-picker-main', + propertyName: 'background.gradient.colors', + label: 'Colors', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', + _mode: 'code', + _value: false, + } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: 'backgroundStyle-url-row-main', + parentId: 'backgroundStylePnl-main', + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-url-field-main', + propertyName: 'background.url', + jsSetting: false, + label: 'URL', + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'backgroundStyle-image-row-main', + parentId: 'backgroundStylePnl-main', + inputs: [ + { + type: 'imageUploader', + id: 'backgroundStyle-image-uploader-main', + propertyName: 'background.uploadFile', + label: 'Image', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-storedFile-main', + parentId: 'backgroundStylePnl-main', + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-storedFile-main', + jsSetting: false, + propertyName: 'background.storedFile.id', + label: 'File ID', + }, + ], + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-controls-main', + parentId: 'backgroundStylePnl-main', + inline: true, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'customDropdown', + id: 'backgroundStyleRow-size-main', + label: 'Size', + hideLabel: true, + propertyName: 'background.size', + customTooltip: + 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'customDropdown', + id: 'backgroundStyleRow-position-main', + label: 'Position', + hideLabel: true, + customTooltip: + 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: 'background.position', + dropdownOptions: positionOptions, + }, + ], + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-repeat-main', + parentId: 'backgroundStylePnl-main', + inputs: [ + { + type: 'radio', + id: 'backgroundStyleRow-repeat-radio-main', + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'borderStyleCollapsiblePanel', + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: 'borderStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: `borderStyleRow-main`, + parentId: 'borderStylePnl', + hidden: { + _code: + 'return !getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.border?.hideBorder);', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'button', + id: 'borderStyleRow-hideBorder-main', + label: 'Border', + hideLabel: true, + propertyName: 'border.hideBorder', + icon: 'EyeOutlined', + iconAlt: 'EyeInvisibleOutlined', + }, + ], + }) + .addContainer({ + id: 'borderStyleRow-container', + parentId: 'borderStylePnl', + components: getBorderInputs() as any, + }) + .addContainer({ + id: 'borderRadiusStyleRow-container', + parentId: 'borderStylePnl', + components: getCornerInputs() as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'shadowStyleCollapsiblePanel', + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: 'shadowStylePnl-main', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'shadowStyleRow-main', + parentId: 'shadowStylePnl-main', + inline: true, + inputs: [ + { + type: 'numberField', + id: 'shadowStyleRow-offsetX-main', + label: 'Offset X', + hideLabel: true, + width: 80, + icon: 'offsetHorizontalIcon', + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: 'shadowStyleRow-offsetY-main', + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: 'shadowStyleRow-blurRadius-main', + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: 'shadowStyleRow-spreadRadius-main', + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: 'shadowStyleRow-color-main', + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'styleCollapsiblePanel', + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: 'stylePnl-M5-911', + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: 'styleBoxPnl', + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'customStyleCollapsiblePanel', + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: 'stylePnl-custom-main', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'custom-css-main', + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + // Header Styles Section - Updated to use headerStyles namespace + .addCollapsiblePanel({ + id: 'headerStyleCollapsiblePanel', + propertyName: 'headerStyles', + label: 'Header Styles', + collapsedByDefault: true, + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + hidden: { + _code: 'return !getSettingValue(data?.showHeader);', + _mode: 'code', + _value: false, + } as any, + content: { + id: 'stylePnl-header-main', + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: 'headerbackgroundStyleCollapsiblePanel', + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: 'stylePnl-header-main', + collapsible: 'header', + content: { + id: 'backgroundStylePnl-header', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'backgroundStyleRow-selectType-header', + parentId: 'backgroundStylePnl-header', + label: 'Type', + jsSetting: false, + propertyName: 'headerStyles.background.type', + inputType: 'radio', + tooltip: 'Select a type of background', + buttonGroupOptions: [ + { + value: 'color', + icon: 'FormatPainterOutlined', + title: 'Color', + }, + { + value: 'gradient', + icon: 'BgColorsOutlined', + title: 'Gradient', + }, + { + value: 'image', + icon: 'PictureOutlined', + title: 'Image', + }, + { + value: 'url', + icon: 'LinkOutlined', + title: 'URL', + }, + { + value: 'storedFile', + icon: 'DatabaseOutlined', + title: 'Stored File', + }, + ], + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-color-header', + parentId: 'backgroundStylePnl-header', + inputs: [ + { + type: 'colorPicker', + id: 'backgroundStyleRow-color-picker-header', + label: 'Color', + propertyName: 'headerStyles.background.color', + hideLabel: true, + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) !== "color";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'backgroundStyle-gradientColors-header', + parentId: 'backgroundStylePnl-header', + inputs: [ + { + type: 'multiColorPicker', + id: 'backgroundStyle-gradientColors-picker-header', + propertyName: 'headerStyles.background.gradient.colors', + label: 'Colors', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) !== "gradient";', + _mode: 'code', + _value: false, + } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: 'backgroundStyle-url-header', + parentId: 'backgroundStylePnl-header', + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-url-field-header', + propertyName: 'headerStyles.background.url', + jsSetting: false, + label: 'URL', + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) !== "url";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'backgroundStyle-image-header', + parentId: 'backgroundStylePnl-header', + inputs: [ + { + type: 'imageUploader', + id: 'backgroundStyle-image-uploader-header', + propertyName: 'headerStyles.background.uploadFile', + label: 'Image', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) !== "image";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-storedFile-header', + parentId: 'backgroundStylePnl-header', + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) !== "storedFile";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-storedFile-header', + jsSetting: false, + propertyName: 'headerStyles.background.storedFile.id', + label: 'File ID', + }, + ], + }) + .addSettingsInputRow({ + id: 'headerBackgroundStyleRow-controls-header', + parentId: 'backgroundStylePnl-header', + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + inline: true, + inputs: [ + { + type: 'customDropdown', + id: 'headerBackgroundStyleRow-size-header', + label: 'Size', + hideLabel: true, + propertyName: 'headerStyles.background.size', + customTooltip: + 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'customDropdown', + id: 'headerBackgroundStyleRow-position-header', + label: 'Position', + hideLabel: true, + customTooltip: + 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: 'headerStyles.background.position', + dropdownOptions: positionOptions, + }, + ], + }) + .addSettingsInputRow({ + id: 'headerBackgroundStyleRow-repeat-header', + parentId: 'backgroundStylePnl-header', + inputs: [ + { + type: 'radio', + id: 'headerBackgroundStyleRow-repeat-radios-header', + label: 'Repeat', + hideLabel: true, + propertyName: 'headerStyles.background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.headerStyles?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'headerShadowStyleCollapsiblePanel', + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: 'stylePnl-header-main', + collapsible: 'header', + content: { + id: 'shadowStylePnl-header', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'shadowStyleRow-header', + parentId: 'shadowStylePnl-header', + inline: true, + inputs: [ + { + type: 'numberField', + id: 'HeadershadowStyleRow-offsetX-header', + label: 'Offset X', + hideLabel: true, + width: 80, + icon: 'offsetHorizontalIcon', + propertyName: 'headerStyles.shadow.offsetX', + }, + { + type: 'numberField', + id: 'HeadershadowStyleRow-offsetY-header', + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'headerStyles.shadow.offsetY', + }, + { + type: 'numberField', + id: 'HeadershadowStyleRow-blurRadius-header', + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'headerStyles.shadow.blurRadius', + }, + { + type: 'numberField', + id: 'HeadershadowStyleRow-spreadRadius-header', + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'headerStyles.shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: 'HeadershadowStyleRow-color-header', + label: 'Color', + hideLabel: true, + propertyName: 'headerStyles.shadow.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addSettingsInput({ + id: 'custom-css-header', + inputType: 'codeEditor', + propertyName: 'headerStyles.style', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + // Footer Styles Section - Updated to use footerStyles namespace + .addCollapsiblePanel({ + id: 'footerStyleCollapsiblePanel', + propertyName: 'footerStyles', + label: 'Footer Styles', + collapsedByDefault: true, + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + hidden: { + _code: 'return !getSettingValue(data?.showFooter);', + _mode: 'code', + _value: false, + } as any, + content: { + id: 'stylePnl-footer-main', + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: 'footerBackgroundStyleCollapsiblePanel', + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: 'stylePnl-footer-main', + collapsible: 'header', + content: { + id: 'backgroundStylePnl-footer', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'backgroundStyleRow-selectType-footer', + parentId: 'backgroundStylePnl-footer', + label: 'Type', + jsSetting: false, + propertyName: 'footerStyles.background.type', + inputType: 'radio', + tooltip: 'Select a type of background', + buttonGroupOptions: [ + { + value: 'color', + icon: 'FormatPainterOutlined', + title: 'Color', + }, + { + value: 'gradient', + icon: 'BgColorsOutlined', + title: 'Gradient', + }, + { + value: 'image', + icon: 'PictureOutlined', + title: 'Image', + }, + { + value: 'url', + icon: 'LinkOutlined', + title: 'URL', + }, + { + value: 'storedFile', + icon: 'DatabaseOutlined', + title: 'Stored File', + }, + ], + }) + .addSettingsInputRow({ + id: 'footerbackgroundStyleRow-color', + parentId: 'backgroundStylePnl-footer', + inputs: [ + { + type: 'colorPicker', + id: 'backgroundStyleRow-color-footer', + label: 'Color', + propertyName: 'footerStyles.background.color', + hideLabel: true, + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.footerStyles?.background?.type) !== "color";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'backgroundStyle-gradientColors-footer', + parentId: 'backgroundStylePnl-footer', + inputs: [ + { + type: 'multiColorPicker', + id: 'backgroundStyle-gradientColors-footer', + propertyName: 'footerStyles.background.gradient.colors', + label: 'Colors', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.footerStyles?.background?.type) !== "gradient";', + _mode: 'code', + _value: false, + } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: 'footerBackgroundStyle-url', + parentId: 'backgroundStylePnl-footer', + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-url-footer', + propertyName: 'footerStyles.background.url', + jsSetting: false, + label: 'URL', + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.footerStyles?.background?.type) !== "url";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'footerbackgroundStyle-image', + parentId: 'backgroundStylePnl-footer', + inputs: [ + { + type: 'imageUploader', + id: 'backgroundStyle-image-footer', + propertyName: 'footerStyles.background.uploadFile', + label: 'Image', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.footerStyles?.background?.type) !== "image";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'footerbackgroundStyleRow-storedFile', + parentId: 'backgroundStylePnl-footer', + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.footerStyles?.background?.type) !== "storedFile";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: 'footerbackgroundStyle-storedFile', + jsSetting: false, + propertyName: 'footerStyles.background.storedFile.id', + label: 'File ID', + }, + ], + }) + .addSettingsInputRow({ + id: 'footerBackgroundStyleRow-controls', + parentId: 'backgroundStylePnl-footer', + inline: true, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.footerStyles?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'customDropdown', + id: 'footerBackgroundStyleRow-size', + label: 'Size', + hideLabel: true, + propertyName: 'footerStyles.background.size', + customTooltip: + 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.footerStyles?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'customDropdown', + id: 'footerBackgroundStyleRow-position', + label: 'Position', + hideLabel: true, + customTooltip: + 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: 'footerStyles.background.position', + dropdownOptions: positionOptions, + }, + ], + }) + .addSettingsInputRow({ + id: 'footerBackgroundStyleRow-repeat', + parentId: 'backgroundStylePnl-footer', + inputs: [ + { + type: 'radio', + id: 'footer-backgroundStyleRow-repeat-radio', + label: 'Repeat', + hideLabel: true, + propertyName: 'footerStyles.background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.footerStyles?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'footerShadowStyleCollapsiblePanel', + propertyName: 'footerpnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: 'stylePnl-footer-main', + collapsible: 'header', + content: { + id: 'footershadowStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'footershadowStyleRow', + parentId: 'footershadowStylePnl', + inline: true, + inputs: [ + { + type: 'numberField', + id: 'footerShadowStyleRow-offsetX', + label: 'Offset X', + hideLabel: true, + width: 80, + icon: 'offsetHorizontalIcon', + propertyName: 'footerStyles.shadow.offsetX', + }, + { + type: 'numberField', + id: 'footerShadowStyleRow-offsetY', + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'footerStyles.shadow.offsetY', + }, + { + type: 'numberField', + id: 'footerShadowStyleRow-blurRadius', + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'footerStyles.shadow.blurRadius', + }, + { + type: 'numberField', + id: 'footerShadowStyleRow-spreadRadius', + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'footerStyles.shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: 'footerShadowStyleRow-color', + label: 'Color', + hideLabel: true, + propertyName: 'footerStyles.shadow.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addSettingsInput({ + id: 'custom-css-footer', + inputType: 'codeEditor', + propertyName: 'footerStyles.style', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/drawer/utils.ts b/shesha-reactjs/src/designer-components/drawer/utils.ts new file mode 100644 index 0000000000..159b549cdb --- /dev/null +++ b/shesha-reactjs/src/designer-components/drawer/utils.ts @@ -0,0 +1,27 @@ +export const defaultStyles = (): any => { + return { + background: { type: 'color', color: '#fff', position: 'center', size: 'cover', repeat: 'no-repeat' }, + font: { weight: '400', size: 14, color: '#000', type: 'Segoe UI' }, + dimensions: { width: '50%', height: '100%', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' }, + border: { + border: { + all: { width: '1px', style: 'solid', color: '#d9d9d9' }, + top: { width: '1px', style: 'solid', color: '#d9d9d9' }, + bottom: { width: '1px', style: 'solid', color: '#d9d9d9' }, + left: { width: '1px', style: 'solid', color: '#d9d9d9' }, + right: { width: '1px', style: 'solid', color: '#d9d9d9' }, + }, + radius: { all: 0, topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0 }, + borderType: 'all', + radiusType: 'all' + }, + style: '', + shadow: { + offsetX: 0, + offsetY: 0, + color: '#000', + blurRadius: 0, + spreadRadius: 0 + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dropdown/index.tsx b/shesha-reactjs/src/designer-components/dropdown/index.tsx index b145370fbd..5959c299d7 100644 --- a/shesha-reactjs/src/designer-components/dropdown/index.tsx +++ b/shesha-reactjs/src/designer-components/dropdown/index.tsx @@ -1,53 +1,77 @@ import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import React from 'react'; -import settingsFormJson from './settingsForm.json'; import { customDropDownEventHandler } from '@/components/formDesigner/components/utils'; import { DataTypes } from '@/interfaces/dataTypes'; import { DownSquareOutlined } from '@ant-design/icons'; -import { FormMarkup, IInputStyles } from '@/providers/form/models'; +import { IInputStyles } from '@/providers/form/models'; import { getLegacyReferenceListIdentifier } from '@/utils/referenceList'; -import { getStyle, useAvailableConstantsData, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { evaluateString, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { IDropdownComponentProps } from './model'; import { IToolboxComponent } from '@/interfaces'; import { migrateCustomFunctions, migratePropertyName, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { Dropdown } from '@/components/dropdown/dropdown'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultStyles, defaultTagStyles } from './utils'; +import { CustomLabeledValue } from '@/components/refListDropDown/models'; +import { useFormComponentStyles } from '@/hooks/formComponentHooks'; -const settingsForm = settingsFormJson as FormMarkup; +interface ITextFieldComponentCalulatedValues { + eventHandlers?: { onChange: (value: CustomLabeledValue, option: any) => any }; + defaultValue?: any; +} -const DropdownComponent: IToolboxComponent = { +const DropdownComponent: IToolboxComponent = { type: 'dropdown', isInput: true, isOutput: true, canBeJsSetting: true, + isHidden: false, name: 'Dropdown', icon: , dataTypeSupported: ({ dataType }) => dataType === DataTypes.referenceListItem, - Factory: ({ model }) => { - const allData = useAvailableConstantsData(); - - const localStyle = getStyle(model.style, allData.data); + calculateModel: (model, allData) => ({ + eventHandlers: customDropDownEventHandler(model, allData), + //quick fix not to default to empty string or null while working with multi-mode + defaultValue: Array.isArray(model.defaultValue) + ? model.defaultValue + : model.defaultValue + ? evaluateString(model.defaultValue, { formData: allData.data, formMode: allData.form.formMode, globalState: allData.globalState }) || undefined + : undefined, + }), + Factory: ({ model, calculatedModel }) => { const initialValue = model?.defaultValue ? { initialValue: model.defaultValue } : {}; + const tagStyle = useFormComponentStyles({ ...model.tag, font: model.font }).fullStyle; return ( {(value, onChange) => { - const customEvent = customDropDownEventHandler(model, allData); + const customEvent = calculatedModel.eventHandlers; const onChangeInternal = (...args: any[]) => { customEvent.onChange(args[0], args[1]); if (typeof onChange === 'function') onChange(...args); }; - return ; + return ; }} ); }, - settingsFormMarkup: settingsForm, - validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), migrator: (m) => m .add(0, (prev) => ({ ...prev, @@ -65,14 +89,15 @@ const DropdownComponent: IToolboxComponent = { .add(4, (prev) => migrateReadOnly(prev)) .add(5, (prev, context) => ({ ...prev, - valueFormat: prev.valueFormat ?? + valueFormat: prev.valueFormat ?? context.isNew + ? 'simple' + : prev['useRawValue'] === true ? 'simple' - : prev['useRawValue'] === true - ? 'simple' - : 'listItem', + : 'listItem', + editMode: prev?.editMode ?? 'inherited' })) - .add(6, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) + .add(6, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) .add(7, (prev) => { const styles: IInputStyles = { size: prev.size, @@ -80,7 +105,40 @@ const DropdownComponent: IToolboxComponent = { style: prev.style, }; - return { ...prev, desktop: {...styles}, tablet: {...styles}, mobile: {...styles} }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }) + .add(8, (prev) => { + const styles: IInputStyles = { + size: prev.size, + width: prev.width, + height: prev.height, + hideBorder: prev.hideBorder, + borderSize: prev.borderSize, + borderRadius: prev.borderRadius, + borderColor: prev.borderColor, + fontSize: prev.fontSize, + fontColor: prev.fontColor, + backgroundColor: prev.backgroundColor, + stylingBox: prev.stylingBox, + }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }) + .add(9, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })) + .add(10, (prev) => { + const newModel = migratePrevStyles(prev, defaultStyles()); + const initTagStyle = { ...defaultTagStyles() }; + + return { + ...newModel, + tag: { ...initTagStyle }, + showItemName: prev.showItemName ?? true, + showIcon: prev.showIcon ?? true, + solidColor: prev.solidColor ?? true, + displayStyle: prev.displayStyle ?? 'text', + desktop: { ...newModel.desktop, tag: { ...initTagStyle } }, + tablet: { ...newModel.tablet, tag: { ...initTagStyle } }, + mobile: { ...newModel.mobile, tag: { ...initTagStyle } } + }; }) , linkToModelMetadata: (model, metadata): IDropdownComponentProps => { @@ -100,4 +158,4 @@ const DropdownComponent: IToolboxComponent = { }, }; -export default DropdownComponent; +export default DropdownComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dropdown/model.ts b/shesha-reactjs/src/designer-components/dropdown/model.ts index 4b4899f0e2..0495ea4780 100644 --- a/shesha-reactjs/src/designer-components/dropdown/model.ts +++ b/shesha-reactjs/src/designer-components/dropdown/model.ts @@ -1,4 +1,4 @@ -import { IConfigurableFormComponent } from '@/providers/form/models'; +import { IConfigurableFormComponent, IInputStyles } from '@/providers/form/models'; import { IDropdownProps } from '@/components/dropdown/model'; export type DataSourceType = 'values' | 'referenceList' | 'url'; @@ -9,5 +9,5 @@ export interface ILabelValue { value: TValue; } -export interface IDropdownComponentProps extends Omit, IConfigurableFormComponent { +export interface IDropdownComponentProps extends Omit, IConfigurableFormComponent,IInputStyles { } diff --git a/shesha-reactjs/src/designer-components/dropdown/settingsForm.json b/shesha-reactjs/src/designer-components/dropdown/settingsForm.json deleted file mode 100644 index c63ebeacc8..0000000000 --- a/shesha-reactjs/src/designer-components/dropdown/settingsForm.json +++ /dev/null @@ -1,739 +0,0 @@ -{ - "components": [ - { - "id": "xjb9Aq4cAijce1v8OhZ0a", - "type": "collapsiblePanel", - "propertyName": "pnlDisplay", - "label": "Display", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "L3eN-eBeygd1JF9CcFTSb", - "components": [] - }, - "content": { - "id": "WngUqinrTrAiWGINTk0z6", - "components": [ - { - "id": "5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4", - "type": "contextPropertyAutocomplete", - "propertyName": "propertyName", - "parentId": "WngUqinrTrAiWGINTk0z6", - "label": "Property name", - "required": true, - "validate": { - "required": true - } - }, - { - "id": "46d07439-4c18-468c-89e1-60c002ce96c5", - "type": "textField", - "propertyName": "label", - "parentId": "WngUqinrTrAiWGINTk0z6", - "label": "Label", - "version": 3, - "textType": "text" - }, - { - "id": "57a40a33-7e08-4ce4-9f08-a34d24a83338", - "type": "dropdown", - "propertyName": "labelAlign", - "parentId": "WngUqinrTrAiWGINTk0z6", - "label": "Label align", - "values": [ - { - "label": "left", - "value": "left", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - "label": "right", - "value": "right", - "id": "b920ef96-ae27-4a01-bfad-b5b7d07218da" - } - ], - "dataSourceType": "values", - "version": 5, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "textType": "text", - "id": "VSSp87LcgvY8-B6nQN93T", - "type": "textField", - "propertyName": "placeholder", - "label": "Placeholder", - "labelAlign": "right", - "parentId": "WngUqinrTrAiWGINTk0z6", - "hidden": false, - "validate": {}, - "settingsValidationErrors": [], - "version": 3 - }, - { - "id": "vdHXORT_t16LbITksJNtm", - "type": "textArea", - "propertyName": "description", - "label": "Description", - "labelAlign": "right", - "parentId": "WngUqinrTrAiWGINTk0z6", - "hidden": false, - "autoSize": false, - "showCount": false, - "allowClear": false, - "validate": {}, - "settingsValidationErrors": [], - "version": 2 - }, - { - "id": "cfd7d45e-c7e3-4a27-987b-dc525c412448", - "type": "checkbox", - "propertyName": "hidden", - "parentId": "WngUqinrTrAiWGINTk0z6", - "label": "Hidden", - "version": 2 - }, - { - "id": "c6885251-96a6-40ce-99b2-4b5209a9e01c", - "type": "checkbox", - "propertyName": "hideLabel", - "parentId": "WngUqinrTrAiWGINTk0z6", - "label": "Hide Label", - "version": 2 - }, - { - "id": "24a8be15-98eb-40f7-99ea-ebb602693e9c", - "type": "editModeSelector", - "propertyName": "editMode", - "parentId": "WngUqinrTrAiWGINTk0z6", - "label": "Edit mode" - }, - { - "id": "c67b7404-d980-437a-a544-74e4e816f6cd", - "type": "checkbox", - "propertyName": "hideBorder", - "label": "Hide border", - "parentId": "WngUqinrTrAiWGINTk0z6", - "hidden": false, - "validate": {}, - "version": 2 - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - }, - { - "id": "vo4MNuVku-MPmfaD-6JZN", - "type": "collapsiblePanel", - "propertyName": "pnlData", - "label": "Data", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "7J3jg3AcmY4j3ssmRvwYK", - "components": [] - }, - "content": { - "id": "PSXX7u5GWT0W8RXnVW3Ao", - "components": [ - { - "id": "fbdb6bb1-5994-4205-86b4-236ca61ae53e", - "type": "numberField", - "propertyName": "defaultValue", - "parentId": "PSXX7u5GWT0W8RXnVW3Ao", - "label": "Default Value", - "validate": {}, - "settingsValidationErrors": [], - "description": "Enter default value of component. (formData, formMode, globalState) are exposed", - "version": 2 - }, - { - "id": "d2648d0e-4664-4fee-ab4a-b14e556d957b", - "type": "dropdown", - "propertyName": "mode", - "parentId": "PSXX7u5GWT0W8RXnVW3Ao", - "label": "Mode", - "values": [ - { - "label": "single", - "value": "single", - "id": "b920ef96-ae27-4a01-bfad-b5b7d07238da" - }, - { - "label": "tags", - "value": "tags", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - "label": "multiple", - "value": "multiple", - "id": "b920ef96-ae27-4a01-bfad-b5b7d07218da" - } - ], - "dataSourceType": "values", - "defaultValue": [ - "single" - ], - "version": 5, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "bf5660d5-cae0-4df9-99f2-cbaf941ae7c7", - "type": "dropdown", - "propertyName": "dataSourceType", - "parentId": "PSXX7u5GWT0W8RXnVW3Ao", - "label": "DataSource type", - "values": [ - { - "id": "ce8f93c6-e43e-42fe-969e-53a50c05cb49", - "label": "Values", - "value": "values" - }, - { - "id": "55400fd2-99eb-4bf8-ae0b-a969b326a4e3", - "label": "Reference List", - "value": "referenceList" - } - ], - "dataSourceType": "values", - "version": 5, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "ELxx5jarWvInJXJF5xHN2", - "type": "container", - "propertyName": "container1", - "label": "Container1", - "labelAlign": "right", - "parentId": "PSXX7u5GWT0W8RXnVW3Ao", - "hidden": { - "_code": "return getSettingValue(data.dataSourceType) !== 'referenceList';", - "_mode": "code", - "_value": false - }, - "direction": "vertical", - "justifyContent": "left", - "settingsValidationErrors": [], - "components": [ - { - "id": "JS9cXXL4J7k7Ncms20CmX", - "type": "referenceListAutocomplete", - "propertyName": "referenceListId", - "label": "Reference List", - "labelAlign": "right", - "parentId": "ELxx5jarWvInJXJF5xHN2", - "hidden": false, - "isDynamic": false, - "version": 2, - "convertToFullId": true, - "placeholder": "", - "description": "", - "validate": { - "message": "" - }, - "settingsValidationErrors": [] - }, - { - "id": "e1xQrrBshDPQOwaaASyqe", - "type": "queryBuilder", - "propertyName": "filter", - "label": "Items Filter", - "labelAlign": "right", - "parentId": "ELxx5jarWvInJXJF5xHN2", - "hidden": false, - "isDynamic": false, - "validate": {}, - "settingsValidationErrors": [], - "modelType": "Shesha.Framework.ReferenceListItem", - "version": 2 - }, - { - "id": "aad660d5-cae0-4df9-99f2-cbaf941ae7c7", - "type": "dropdown", - "propertyName": "valueFormat", - "parentId": "ELxx5jarWvInJXJF5xHN2", - "label": "Value format", - "values": [ - { - "id": "ce8f93c6-e43e-42fe-969e-53a50c05cb49", - "label": "Simple item value", - "value": "simple" - }, - { - "id": "55400fd2-99eb-4bf8-ae0b-a969b326a4e3", - "label": "Reference List item", - "value": "listItem" - }, - { - "id": "14620fd2-99eb-4bf8-ae0b-a969b326a4e3", - "label": "Custom", - "value": "custom" - } - ], - "dataSourceType": "values", - "defaultValue": "simple", - "version": 5, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "16ab0599-914d-4d2d-875c-765a495472f8", - "type": "codeEditor", - "propertyName": "incomeCustomJs", - "label": "Key value", - "parentId": "ELxx5jarWvInJXJF5xHN2", - "validate": {}, - "hidden": { - "_code": "return getSettingValue(data?.valueFormat) !== 'custom';", - "_mode": "code", - "_value": false - }, - "settingsValidationErrors": [], - "description": "Return key from the value", - "exposedVariables": [ - { - "name": "value", - "description": "Field value", - "type": "object" - } - ], - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "getKeyValue" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\").addObject(\"value\", \"Field value\tobject\").build();" - }, - { - "id": "26ab0599-914d-4d2d-875c-765a495472f8", - "type": "codeEditor", - "propertyName": "outcomeCustomJs", - "label": "Custom value", - "parentId": "ELxx5jarWvInJXJF5xHN2", - "validate": {}, - "hidden": { - "_code": "return getSettingValue(data?.valueFormat) !== 'custom';", - "_mode": "code", - "_value": false - }, - "settingsValidationErrors": [], - "description": "Return value that will be stored as field value", - "exposedVariables": [ - { - "name": "value", - "description": "Fetched item", - "type": "ReferenceListItemDto" - } - ], - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "getCustomValue" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\").addObject(\"value\", \"Field value\tobject\").build();" - }, - { - "id": "36ab0599-914d-4d2d-875c-765a495472f8", - "type": "codeEditor", - "propertyName": "labelCustomJs", - "label": "Item custom label", - "parentId": "ELxx5jarWvInJXJF5xHN2", - "validate": {}, - "settingsValidationErrors": [], - "description": "Return label value", - "exposedVariables": [ - { - "name": "value", - "description": "Fetched item", - "type": "ReferenceListItemDto" - } - ], - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "getCustomLabel" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\").addObject(\"value\", \"Field value\tobject\").build();" - }, - { - "id": "3be9da3f-f47e-48ae-b4c3-f5cc36e534x0", - "type": "checkbox", - "propertyName": "disableItemValue", - "description": "Disable reference list from selection", - "parentId": "tGXA95wmOf54IsiPT-WgW", - "label": "Disable item value" - }, - { - "id": "03959ffd-cadb-496c-bf6d-b742f7f6edc9", - "type": "textArea", - "propertyName": "disabledValues", - "parentId": "ELxx5jarWvInJXJF5xHN2", - "label": "Disabled Values", - "autoSize": false, - "showCount": false, - "allowClear": true, - "description": "Pass an array of positive integers to disable specific values. For example: [1, 2, 3].", - "version": 2, - "hidden": { - "_code": "return !getSettingValue(data.disableItemValue);", - "_mode": "code", - "_value": false - } - }, - { - "id": "03959ffd-cadb-496c-bf6d-b742f7f6edc5", - "type": "textArea", - "propertyName": "ignoredValues", - "parentId": "ELxx5jarWvInJXJF5xHN2", - "label": "Ignored Values", - "autoSize": false, - "showCount": false, - "allowClear": true, - "description": "Pass an array of positive integers to ignore specific values. For example: [1, 2, 3].", - "version": 2 - } - ], - "version": 2, - "flexWrap": "wrap" - }, - { - "id": "99629618-d725-4690-b744-97a4b0ae3b19", - "type": "textField", - "propertyName": "dataSourceUrl", - "parentId": "PSXX7u5GWT0W8RXnVW3Ao", - "label": "Data Source Url", - "hidden": { - "_code": "return getSettingValue(data.dataSourceType) !== 'url';", - "_mode": "code", - "_value": false - }, - "version": 3, - "textType": "text" - }, - { - "id": "757ea243-a811-4110-a492-16fa3fbe702d", - "type": "labelValueEditor", - "propertyName": "values", - "parentId": "PSXX7u5GWT0W8RXnVW3Ao", - "label": "Values", - "labelTitle": "Label", - "labelName": "label", - "valueTitle": "Value", - "valueName": "value", - "hidden": { - "_code": "return getSettingValue(data.dataSourceType) !== 'values';", - "_mode": "code", - "_value": false - }, - "version": 2, - "mode": "dialog" - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - }, - { - "id": "75yJPi5tyshcrOt3me9mS", - "type": "collapsiblePanel", - "propertyName": "pnlEvents", - "label": "Events", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "S57wmle4q6DFr4e-EZBRI", - "components": [] - }, - "content": { - "id": "Vd8kaikjalJkbFuqN5U3I", - "components": [ - { - "id": "b9269416-3b78-42c4-934e-3e0dac8c7f01", - "type": "codeEditor", - "propertyName": "onChangeCustom", - "label": "On Change", - "labelAlign": "right", - "parentId": "Vd8kaikjalJkbFuqN5U3I", - "hidden": false, - "description": "Enter custom eventhandler on changing of event. (form, value, option) are exposed", - "validate": {}, - "settingsValidationErrors": [], - "exposedVariables": [ - { - "name": "data", - "description": "Selected form values", - "type": "object" - }, - { - "name": "form", - "description": "Form instance", - "type": "FormInstance" - }, - { - "name": "formMode", - "description": "The form mode", - "type": "'readonly' | 'edit' | 'designer'" - }, - { - "name": "globalState", - "description": "The global state of the application", - "type": "object" - }, - { - "name": "http", - "description": "axios instance used to make http requests", - "type": "object" - }, - { - "name": "message", - "description": "This is the Ant API for displaying toast messages. See: https://ant.design/components/message/#header", - "type": "object" - }, - { - "name": "moment", - "description": "The moment.js object", - "type": "object" - }, - { - "name": "option", - "description": "Meta data of component current value", - "type": "object" - }, - { - "name": "value", - "description": "Component current value", - "type": "string | any" - }, - { - "name": "setFormData", - "description": "A function used to update the form data", - "type": "({ values: object, mergeValues: boolean}) => void" - }, - { - "name": "setGlobalState", - "description": "Setting the global state of the application", - "type": "(payload: { key: string, data: any } ) => void" - } - ], - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "onChange" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\")\r\n .addAllStandard()\r\n .addObject(\"option\", \"Selected option\")\r\n .addObject(\"value\", \"Component current value\")\r\n .build(); " - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - }, - { - "id": "AASvPn3ioEAnbsJqvV5_c", - "type": "collapsiblePanel", - "propertyName": "pnlValidation", - "label": "Validation", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "rE2CibkEdbZsNetuGU3y4", - "components": [] - }, - "content": { - "id": "tGXA95wmOf54IsiPT-WgW", - "components": [ - { - "id": "3be9da3f-f47e-48ae-b4c3-f5cc36e534d9", - "type": "checkbox", - "propertyName": "validate.required", - "parentId": "tGXA95wmOf54IsiPT-WgW", - "label": "Required", - "version": 2 - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - }, - { - "id": "kp3afwRnhZk2958QCUvFTFeGWOokZY", - "type": "propertyRouter", - "propertyName": "propertyRouter1", - "componentName": "propertyRouter1", - "label": "Property router1", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "propertyRouteName": { - "_mode": "code", - "_code": " return contexts.canvasContext?.designerDevice || 'desktop';", - "_value": "" - }, - "components": [ - { - "id": "Mozdwwy7lEfcuptdyLjTw", - "type": "collapsiblePanel", - "propertyName": "pnlStyle", - "label": "Style", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "SQLqOqH5GQNgDUE72HGw5", - "components": [] - }, - "content": { - "id": "eAyxVPfzY3iiPI_FDfs83", - "components": [ - { - "id": "06ab0599-914d-4d2d-875c-765a495472f8", - "type": "codeEditor", - "propertyName": "style", - "label": "Style", - "parentId": "eAyxVPfzY3iiPI_FDfs83", - "validate": {}, - "settingsValidationErrors": [], - "description": "A script that returns the style of the element as an object. This should conform to CSSProperties", - "exposedVariables": [ - { - "name": "data", - "description": "Form values", - "type": "object" - } - ], - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "getStyle" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\").addStandard([\"shesha:formData\", \"shesha:globalState\"]).build();" - }, - { - "id": "8615d12f-6ea0-4b11-a1a1-6088c7160fd9", - "type": "dropdown", - "propertyName": "size", - "parentId": "eAyxVPfzY3iiPI_FDfs83", - "label": "Size", - "allowClear": true, - "values": [ - { - "label": "Small", - "value": "small", - "id": "4f11403c-95fd-4e49-bb60-cb8c25f0f3c3" - }, - { - "label": "Middle", - "value": "middle", - "id": "8f85c476-e632-4fa7-89ad-2be6cfb7f1f1" - }, - { - "label": "Large", - "value": "large", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - } - ], - "dataSourceType": "values", - "version": 5, - "referenceListId": null, - "valueFormat": "listItem" - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - } - ] - }, - { - "id": "6Vw9iiDw9d0MD_Rh5cbIn", - "type": "collapsiblePanel", - "propertyName": "pnlSecurity", - "label": "Security", - "labelAlign": "right", - "parentId": "root", - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "mbzE03mJnv_89oUvkmjPs", - "components": [] - }, - "content": { - "id": "SsWPpiJfTbgl4iHlFA_or", - "components": [ - { - "id": "1adea529-1f0c-4def-bd41-ee166a5dfcd7", - "type": "permissionAutocomplete", - "propertyName": "permissions", - "parentId": "SsWPpiJfTbgl4iHlFA_or", - "label": "Permissions", - "validate": {}, - "version": 1 - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - } - ], - "formSettings": { - "layout": "horizontal", - "colon": true, - "labelCol": { - "span": 8 - }, - "wrapperCol": { - "span": 16 - }, - "displayName": "DEFAULT_FORM_SETTINGS", - "__docgenInfo": { - "description": "Default form settings", - "displayName": "DEFAULT_FORM_SETTINGS", - "props": {} - } - } -} \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dropdown/settingsForm.ts b/shesha-reactjs/src/designer-components/dropdown/settingsForm.ts new file mode 100644 index 0000000000..204cd2cef3 --- /dev/null +++ b/shesha-reactjs/src/designer-components/dropdown/settingsForm.ts @@ -0,0 +1,1267 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { IDropdownComponentProps } from './model'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { nanoid } from '@/utils/uuid'; +import { presetColors } from './utils'; + +export const getSettings = (data: IDropdownComponentProps) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const validationTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const dataContainerId = nanoid(); + + const DefaultTagStyleId = nanoid(); + const tagCustomStyleId = nanoid(); + const tagBorderStyleId = nanoid(); + const tagBackgroundStyleId = nanoid(); + const tagShadowStyleId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'placeholder', + label: 'Placeholder', + type: 'textField', + size: 'small', + jsSetting: true, + }, + { + id: nanoid(), + type: 'textArea', + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + type: 'editModeSelector', + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .toJson() + ] + }, + { + key: '2', + title: 'Data', + id: dataTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.displayStyle) !== "tags";', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'showItemName', + label: 'Show Item Name', + jsSetting: true, + tooltip: 'When checked the DisplayName/RefList Name will be shown.', + layout: 'horizontal', + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'showIcon', + label: 'Show Icon', + size: 'small', + jsSetting: true, + defaultValue: true, + tooltip: 'When checked the icon will display on the left side of the DisplayName', + } + + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'defaultValue', + label: 'Default Value', + size: 'small', + jsSetting: true, + type: "numberField", + }, + { + id: nanoid(), + parentId: dataTabId, + label: "Mode", + propertyName: "mode", + type: "dropdown", + size: "small", + defaultValue: "single", + jsSetting: true, + dropdownOptions: [ + { + value: "single", + label: "Single" + }, + { + value: "multiple", + label: "Multiple" + } + ] + } + ] + }) + .addSettingsInput({ + id: nanoid(), + parentId: dataTabId, + label: "Data Source Type", + propertyName: "dataSourceType", + inputType: "dropdown", + size: "small", + jsSetting: true, + dropdownOptions: [ + { + value: "values", + label: "Values" + }, + { + value: "referenceList", + label: "Reference list" + } + ], + }) + .addContainer({ + id: dataContainerId, + parentId: dataTabId, + propertyName: 'container1', + label: 'Container1', + labelAlign: 'right', + hidden: { + _code: `return getSettingValue(data.dataSourceType) !== 'referenceList';`, + _mode: 'code', + _value: false + }, + direction: 'vertical', + justifyContent: 'left', + settingsValidationErrors: [], + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: dataContainerId, + propertyName: 'referenceListId', + label: 'Reference List', + isDynamic: true, + inputType: 'referenceListAutocomplete', + settingsValidationErrors: [], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'queryBuilder', + parentId: dataContainerId, + propertyName: 'filter', + label: 'Items Filter', + isDynamic: true, + validate: {}, + settingsValidationErrors: [], + modelType: 'Shesha.Framework.ReferenceListItem' + }) + .addSettingsInput({ + id: nanoid(), + parentId: dataContainerId, + label: "Value Format", + propertyName: "valueFormat", + inputType: "dropdown", + size: "small", + jsSetting: true, + defaultValue: 'simple', + dropdownOptions: [ + { + value: "simple", + label: "Simple item value" + }, + { + value: "listItem", + label: "Reference list item" + }, + { + value: "custom", + label: "Custom" + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataContainerId, + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'incomeCustomJs', + label: 'Key Value', + labelAlign: 'right', + tooltip: 'Return key from the value', + parentId: dataContainerId, + hidden: { + "_code": "return getSettingValue(data?.valueFormat) !== 'custom';", + "_mode": "code", + "_value": false + } as any, + }, + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'outcomeCustomJs', + label: 'Custom Value', + labelAlign: 'right', + tooltip: 'Return value that will be stored as field value', + parentId: dataContainerId, + hidden: { + "_code": "return getSettingValue(data?.valueFormat) !== 'custom';", + "_mode": "code", + "_value": false + } as any, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataContainerId, + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'labelCustomJs', + label: 'Item Custom Label', + labelAlign: 'right', + tooltip: 'Return label value', + parentId: dataContainerId, + }, + { + id: nanoid(), + parentId: dataContainerId, + type: 'switch', + propertyName: 'disableItemValue', + tooltip: 'Disable reference list from selection', + label: 'Disable Item Value', + jsSetting: true, + layout: 'horizontal', + version: 'latest' + + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataContainerId, + inputs: [ + { + id: nanoid(), + parentId: dataContainerId, + type: 'textArea', + propertyName: 'disabledValues', + label: 'Disabled Values', + allowClear: true, + jsSetting: true, + hidden: { + _code: "return !getSettingValue(data.disableItemValue);", + _mode: 'code', + _value: false + } as any, + tooltip: 'Pass an array of positive integers to disable specific values. For example: [1, 2, 3].', + } + ] + }) + .addSettingsInput({ + id: nanoid(), + parentId: dataContainerId, + inputType: 'textArea', + propertyName: 'ignoredValues', + label: 'Ignored Values', + allowClear: true, + jsSetting: true, + tooltip: 'Pass an array of positive integers to ignore specific values. For example: [1, 2, 3].', + }) + .toJson() + ] + + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + type: 'customLabelValueEditor', + propertyName: 'values', + jsSetting: true, + label: 'Values', + labelName: 'label', + labelTitle: 'Label', + colorName: 'color', + colorTitle: 'Color', + iconName: 'icon', + iconTitle: 'Icon', + mode: 'inline', + valueName: 'value', + valueTitle: 'Value', + dropdownOptions: presetColors + } + ], + hidden: { + _code: `return getSettingValue(data.dataSourceType) !== 'values';`, + _mode: 'code', + _value: false + }, + }) + .addSettingsInput({ + id: nanoid(), + hidden: { + _code: 'return !getSettingValue(data.disableItemValue);', + _mode: 'code', + _value: false + }, + inputType: 'textArea', + propertyName: 'ignoredValues', + label: 'Ignored Values', + size: 'small', + layout: 'horizontal', + jsSetting: true, + allowClear: true, + tooltip: "Pass an array of positive integers to ignore specific values. For example: [1, 2, 3].", + parentId: dataTabId + }) + .toJson() + ] + }, + { + key: '3', + title: 'Validation', + id: validationTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'validate.required', + label: 'Required', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: validationTabId + }) + .toJson() + ] + }, + { + key: '4', + title: 'Events', + id: eventsTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onChangeCustom', + label: 'On Change', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on changing of event.', + parentId: eventsTabId + }) + .toJson() + ] + }, + { + key: '5', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: styleRouterId, + propertyName: 'displayStyle', + label: 'Display Style', + inputType: 'dropdown', + dropdownOptions: [ + { + value: 'text', + label: 'Plain text' + }, + { + value: 'tags', + label: 'Tags' + } + ] + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: nanoid(), + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: nanoid(), + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: nanoid(), + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "background.size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'defaultTagStyle', + label: 'Custom Tag Styles', + labelAlign: 'right', + ghost: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.displayStyle) !== "tags";', _mode: 'code', _value: false } as any, + collapsible: 'header', + content: { + id: DefaultTagStyleId, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'tagDimensionStyle', + label: 'Dimension', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + parentId: DefaultTagStyleId, + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "tag.dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "tag.dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "tag.dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "tag.dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "tag.dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "tag.dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'tagBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + parentId: DefaultTagStyleId, + content: { + id: tagBorderStyleId, + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: tagBorderStyleId, + components: getBorderInputs('tag') as any + }) + .addContainer({ + id: nanoid(), + parentId: tagBorderStyleId, + components: getCornerInputs('tag') as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'tagBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: DefaultTagStyleId, + collapsible: 'header', + content: { + id: tagBackgroundStyleId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'solidColor', + label: 'Show Solid Color', + size: 'small', + tooltip: 'When checked solid color fills the entire background, when unchecked creates a subtle light background with a prominent colored border.', + }) + .addSettingsInput({ + id: nanoid(), + parentId: tagBackgroundStyleId, + label: "Type", + jsSetting: false, + propertyName: "tag.background.type", + inputType: "radio", + tooltip: "Select a type of background, the default background color for tags when no specific color is assigned. Acts as a fallback background style for uncolored tags.", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: tagBackgroundStyleId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "tag.background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.tag?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: tagBackgroundStyleId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "tag.background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.tag?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: tagBackgroundStyleId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "tag.background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.tag?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: tagBackgroundStyleId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'tag.background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.tag?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: tagBackgroundStyleId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.tag?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "tag.background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: tagBackgroundStyleId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.tag?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "tag.background.size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "tag.background.position", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: tagBackgroundStyleId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'tag.background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.tag?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'tagShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: DefaultTagStyleId, + collapsible: 'header', + content: { + id: tagShadowStyleId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: tagShadowStyleId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'tag.shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'tag.shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'tag.shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'tag.shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'tag.shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'tagCustomStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + parentId: DefaultTagStyleId, + content: { + id: tagCustomStyleId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'tag.style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson() + ] + } + }) + .toJson()] + }).toJson()] + }, + { + key: '6', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dropdown/utils.ts b/shesha-reactjs/src/designer-components/dropdown/utils.ts new file mode 100644 index 0000000000..f9adb3778b --- /dev/null +++ b/shesha-reactjs/src/designer-components/dropdown/utils.ts @@ -0,0 +1,120 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + return { + background: { type: 'color', color: '#fff' }, + font: { + weight: '400', + size: 14, + color: '#000', + type: 'Segoe UI' + }, + border: { + border: { + all: { + width: '1px', + style: 'solid', + color: '#d9d9d9' + } + }, + radius: { all: 8 }, + borderType: 'all', + radiusType: 'all' + }, + dimensions: { + width: '100%', + height: 'auto', + minHeight: '32px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto' + } + }; +}; + +export const defaultTagStyles = (): IStyleType => { + return { + background: { type: 'color', color: '' }, + font: { + weight: '400', + size: 14, + color: '#000', + type: 'Segoe UI' + }, + border: { + border: { + all: { + width: '1px', + style: 'solid', + color: '#d9d9d9' + } + }, + radius: { all: 4 }, + borderType: 'all', + radiusType: 'all' + }, + dimensions: { + width: 'auto', + height: '22px', + minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto' + } + }; +}; + +export const presetColors = [ + { + value: 'success', + label: 'Success' + }, + { + value: 'warning', + label: 'Warning' + }, + { + value: 'error', + label: 'Error' + }, + { + value: 'magenta', + label: 'Magenta' + }, + { + value: 'red', + label: 'Red' + }, + { + value: 'orange', + label: 'Orange' + }, + { + value: 'gold', + label: 'Gold' + }, + { + value: 'lime', + label: 'Lime' + }, + { + value: 'green', + label: 'Green' + }, + { + value: 'cyan', + label: 'Cyan' + }, + { + value: 'blue', + label: 'Blue' + }, + { + value: 'geekblue', + label: 'Geekblue' + }, + { + value: 'purple', + label: 'Purple' + } +]; diff --git a/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/configurator.tsx b/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/configurator.tsx index 4825dd1dc5..56407dbf32 100644 --- a/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/configurator.tsx +++ b/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/configurator.tsx @@ -1,20 +1,21 @@ -import React, { useMemo } from 'react'; -import { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { IDynamicActionsConfiguratorComponentProps } from './interfaces'; import { IDynamicActionsConfiguration } from './models'; import { ProviderSelector } from './providerSelector'; import { Form } from 'antd'; import { useDynamicActionsDispatcher, useForm } from '@/providers'; import { ProviderSettingsEditor } from './providerSettingsEditor'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; export interface IDynamicActionsConfiguratorProps { value?: IDynamicActionsConfiguration; onChange?: (newValue: IDynamicActionsConfiguration) => void; - editorConfig: IDynamicActionsConfiguratorComponentProps; + editorConfig?: IDynamicActionsConfiguratorComponentProps; readOnly?: boolean; + size?: SizeType; } -export const DynamicActionsConfigurator: FC = ({ value, onChange, readOnly }) => { +export const DynamicActionsConfigurator: FC = ({ value, onChange, readOnly, size }) => { const [form] = Form.useForm(); const { formSettings } = useForm(); @@ -42,6 +43,7 @@ export const DynamicActionsConfigurator: FC = colon={formSettings.colon} onValuesChange={onValuesChange} initialValues={value} + size={size} > diff --git a/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/genericSettingsEditor.tsx b/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/genericSettingsEditor.tsx index 62d5badf16..3caf038c39 100644 --- a/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/genericSettingsEditor.tsx +++ b/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/genericSettingsEditor.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { ConfigurableForm } from '@/components'; import { FormMarkup } from '@/providers/form/models'; import { IProviderSettings } from './interfaces'; @@ -22,10 +22,6 @@ export function GenericSettingsEditor({ }: IProps) { const formRef = useShaFormRef(); - useEffect(() => { - formRef.current?.resetFields(); - }); - return ( = ({ if (!settingsEditor) return null; return ( - + + {settingsEditor} + ); }; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/settings.ts b/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/settings.ts index bb30c80be7..8bdd6b01fa 100644 --- a/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/settings.ts +++ b/shesha-reactjs/src/designer-components/dynamicActionsConfigurator/settings.ts @@ -35,7 +35,7 @@ export const configurableActionsConfiguratorSettingsForm = new DesignerToolbarSe id: 'bf1823d6-dca4-408a-b7d8-5b42eacb076d', propertyName: 'hidden', parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - label: 'Hidden', + label: 'hide', }) .addPermissionAutocomplete({ id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', diff --git a/shesha-reactjs/src/designer-components/dynamicView/dynamicComponent.tsx b/shesha-reactjs/src/designer-components/dynamicView/dynamicComponent.tsx index 8ddf22f7d9..67b792fdc7 100644 --- a/shesha-reactjs/src/designer-components/dynamicView/dynamicComponent.tsx +++ b/shesha-reactjs/src/designer-components/dynamicView/dynamicComponent.tsx @@ -1,23 +1,28 @@ -import React, { FC, useRef } from 'react'; +import React, { FC, useMemo, useRef } from 'react'; import { CustomErrorBoundary } from '@/components'; import { IConfigurableFormComponent } from '@/interfaces'; -import { useCanvas, useForm, useSheshaApplication } from '@/index'; +import { useActualContextData, useActualContextExecution, useCalculatedModel, useCanvas, useShaFormInstance, useSheshaApplication } from '@/index'; import { useFormDesignerComponentGetter } from '@/providers/form/hooks'; import { IModelValidation } from '@/utils/errors'; import ComponentError from '@/components/componentErrors'; -import { useActualContextData } from '@/hooks/useActualContextData'; import { formComponentActualModelPropertyFilter } from '@/components/formDesigner/formComponent'; +import AttributeDecorator from '@/components/attributeDecorator'; +import { useShaFormUpdateDate } from '@/providers/form/providers/shaFormProvider'; export interface IConfigurableFormComponentProps { model: IConfigurableFormComponent; } const DynamicComponent: FC = ({ model: componentModel }) => { - const { activeDevice } = useCanvas(); - const formInstance = useForm(); - const { anyOfPermissionsGranted } = useSheshaApplication(); - const { form } = formInstance; + + useShaFormUpdateDate(); + + const shaApplication = useSheshaApplication(); + const shaForm = useShaFormInstance(); + // const { isComponentFiltered } = useForm(); const getToolboxComponent = useFormDesignerComponentGetter(); + const { anyOfPermissionsGranted } = useSheshaApplication(); + const { activeDevice } = useCanvas(); const componentRef = useRef(); const toolboxComponent = getToolboxComponent(componentModel.type); @@ -26,27 +31,47 @@ const DynamicComponent: FC = ({ model: componen ? { ...componentModel, ...componentModel?.[activeDevice] } : componentModel; - const actualModel = useActualContextData(deviceModel, undefined, undefined, (name: string) => formComponentActualModelPropertyFilter(toolboxComponent, name)); - - if (!toolboxComponent) - return ; + const actualModel = useActualContextData( + deviceModel, + undefined, + undefined, + (name: string) => formComponentActualModelPropertyFilter(toolboxComponent, name), + undefined, + ); // TODO: AS review hidden and enabled for SubForm - actualModel.hidden = formInstance.formMode !== 'designer' + actualModel.hidden = shaForm.formMode !== 'designer' && ( actualModel.hidden || !anyOfPermissionsGranted(actualModel?.permissions || [])); - - actualModel.readOnly = actualModel.readOnly; + // || !isComponentFiltered(actualModel)); ToDo: AS - check if needed for dynamic components // binding only input and output components if (!toolboxComponent.isInput && !toolboxComponent.isOutput) actualModel.propertyName = undefined; - if (formInstance.formMode === 'designer') { + actualModel.jsStyle = useActualContextExecution(actualModel.style, null, {}); // use default style if empty or error + + const calculatedModel = useCalculatedModel(actualModel, toolboxComponent?.calculateModel); + + const control = useMemo(() => ( + + ), [actualModel, actualModel.hidden, actualModel.jsStyle, calculatedModel]); + + if (!toolboxComponent) + return ; + + if (shaForm.formMode === 'designer') { const validationResult: IModelValidation = {hasErrors: false, errors: []}; toolboxComponent.validateModel?.(actualModel, (propertyName, error) => { validationResult.hasErrors = true; @@ -60,9 +85,23 @@ const DynamicComponent: FC = ({ model: componen } } + const attributes = { + 'data-sha-c-id': `${componentModel.id}`, + 'data-sha-c-name': `${componentModel.componentName}`, + 'data-sha-c-type': `${componentModel.type}`, + }; + + if (componentModel.type === 'subForm') { + attributes['data-sha-c-form-name'] = `${(componentModel as any)?.formId?.module}/${(componentModel as any)?.formId?.name}`; + attributes['data-sha-parent-form-id'] = `${shaForm.form.id}`; + attributes['data-sha-parent-form-name'] = `${(shaForm as any)?.formId?.module}/${(shaForm as any)?.formId?.name}`; + } + return ( - + + {control} + ); }; diff --git a/shesha-reactjs/src/designer-components/dynamicView/index.tsx b/shesha-reactjs/src/designer-components/dynamicView/index.tsx index cd1d5bd281..dc3f619c9a 100644 --- a/shesha-reactjs/src/designer-components/dynamicView/index.tsx +++ b/shesha-reactjs/src/designer-components/dynamicView/index.tsx @@ -1,28 +1,27 @@ import React from 'react'; import { IToolboxComponent } from '@/interfaces'; -import { FormMarkup, IConfigurableFormComponent } from '@/providers/form/models'; +import { IConfigurableFormComponent } from '@/providers/form/models'; import { AppstoreOutlined } from '@ant-design/icons'; -import settingsFormJson from './settingsForm.json'; import { validateConfigurableComponentSettings } from '@/providers/form/utils'; import DynamicView from './dynamicView'; +import { getSettings } from './settings'; export interface DynamicViewComponentProps extends IConfigurableFormComponent { } -const settingsForm = settingsFormJson as FormMarkup; - +/** @deprecated */ const DynamicViewComponent: IToolboxComponent = { - type: 'dynamicView', - isInput: false, - name: 'Dynamic View', - icon: , - Factory: ({ model }) => { - return ( - - ); - }, - settingsFormMarkup: settingsForm, - validateSettings: model => validateConfigurableComponentSettings(settingsForm, model), - }; + type: 'dynamicView', + isInput: false, + name: 'Dynamic View', + icon: , + Factory: ({ model }) => { + return ( + + ); + }, + settingsFormMarkup: data => getSettings(data), + validateSettings: model => validateConfigurableComponentSettings(getSettings(model), model), +}; - export default DynamicViewComponent; \ No newline at end of file +export default DynamicViewComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/dynamicView/settings.ts b/shesha-reactjs/src/designer-components/dynamicView/settings.ts new file mode 100644 index 0000000000..7cba125a2a --- /dev/null +++ b/shesha-reactjs/src/designer-components/dynamicView/settings.ts @@ -0,0 +1,77 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { DynamicViewComponentProps } from '.'; + +export const getSettings = (data: DynamicViewComponentProps) => { + + return { + + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: 'W_m7doMyCpCYwAYDfRh6I', + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: 's4gmBg31azZC0UjZjpfTm', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: '3be9da3f-f47e-48ae-b4c3-f5cc36e534d9', + propertyName: 'componentName', + label: 'Component name', + inputType: 'textField', + size: 'small', + layout: 'horizontal', + jsSetting: true, + validate: { required: true }, + parentId: 's4gmBg31azZC0UjZjpfTm' + }) + .addSettingsInputRow({ + id: '12d700d6-ed4d-49d5-9cfd-fe8f0060f3b6', + parentId: 's4gmBg31azZC0UjZjpfTm', + inputs: [ + { + id: '5c813b1a-04c5-4678-ac0f-cbcbae6b3bd4', + type: 'switch', + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .toJson() + ] + }, + { + key: '2', + title: 'Security', + id: '6Vw9iiDw9d0MD_Rh5cbIn', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: '1adea529-1f0c-4def-bd41-ee166a5dfcd7', + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: '6Vw9iiDw9d0MD_Rh5cbIn' + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/editModeToggler/settingsForm-old.ts b/shesha-reactjs/src/designer-components/editModeToggler/settingsForm-old.ts new file mode 100644 index 0000000000..9e911d1af7 --- /dev/null +++ b/shesha-reactjs/src/designer-components/editModeToggler/settingsForm-old.ts @@ -0,0 +1,64 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data: any) => + new DesignerToolbarSettings(data) + .addCollapsiblePanel({ + id: nanoid(), + parentId: 'root', + propertyName: 'pnlDisplay', + label: 'Display', + labelAlign: 'left', + expandIconPosition: 'start', + ghost: true, + collapsible: 'header', + content: { + id: 'dfce8149-b595-4686-8778-e93d1b82d1e5', + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property name', + parentId: 'dfce8149-b595-4686-8778-e93d1b82d1e5', + validate: { + required: true, + }, + }) + .addCheckbox({ + id: nanoid(), + propertyName: 'hidden', + label: 'hide', + parentId: 'dfce8149-b595-4686-8778-e93d1b82d1e5', + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlSecurity', + parentId: 'root', + label: 'Security', + labelAlign: 'left', + expandIconPosition: 'start', + ghost: true, + collapsible: 'header', + content: { + id: 'fccb6b17-656d-43c0-8144-0b91c454da1d', + components: [ + ...new DesignerToolbarSettings() + .addPermissionAutocomplete({ + id: nanoid(), + propertyName: 'permissions', + label: 'Permissions', + labelAlign: 'right', + parentId: 'fccb6b17-656d-43c0-8144-0b91c454da1d', + hidden: false, + validate: {}, + }) + .toJson(), + ], + }, + }) + .toJson(); diff --git a/shesha-reactjs/src/designer-components/editModeToggler/settingsForm.ts b/shesha-reactjs/src/designer-components/editModeToggler/settingsForm.ts index 3c764b925b..a34b4611e8 100644 --- a/shesha-reactjs/src/designer-components/editModeToggler/settingsForm.ts +++ b/shesha-reactjs/src/designer-components/editModeToggler/settingsForm.ts @@ -1,64 +1,78 @@ -import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; -import { nanoid } from '@/utils/uuid'; +import { DesignerToolbarSettings } from "@/index"; +import { nanoid } from "@/utils/uuid"; +import { FormLayout } from "antd/lib/form/Form"; -export const getSettings = (data: any) => - new DesignerToolbarSettings(data) - .addCollapsiblePanel({ - id: nanoid(), - parentId: 'root', - propertyName: 'pnlDisplay', - label: 'Display', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: 'dfce8149-b595-4686-8778-e93d1b82d1e5', - components: [ - ...new DesignerToolbarSettings() - .addContextPropertyAutocomplete({ - id: nanoid(), - propertyName: 'propertyName', - label: 'Property name', - parentId: 'dfce8149-b595-4686-8778-e93d1b82d1e5', - validate: { - required: true, - }, - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'hidden', - label: 'Hidden', - parentId: 'dfce8149-b595-4686-8778-e93d1b82d1e5', - }) - .toJson(), - ], - }, - }) - .addCollapsiblePanel({ - id: nanoid(), - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: 'fccb6b17-656d-43c0-8144-0b91c454da1d', - components: [ - ...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: nanoid(), - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'fccb6b17-656d-43c0-8144-0b91c454da1d', - hidden: false, - validate: {}, - }) - .toJson(), - ], - }, - }) - .toJson(); +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const securityId = nanoid(); + + const propertyNameId = nanoid(); + const hiddenId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: propertyNameId, + propertyName: "propertyName", + parentId: commonTabId, + label: "Property Name", + size: "small", + validate: { + "required": true + }, + styledLabel: true, + jsSetting: true, + }) + .addSettingsInput({ + id: hiddenId, + propertyName: 'hidden', + label: 'Hide', + parentId: commonTabId, + inputType: 'switch', + jsSetting: true, + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityId + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/endpointsAutocomplete/settingsForm.json b/shesha-reactjs/src/designer-components/endpointsAutocomplete/settingsForm.json index f46ce4bfdc..644037be0d 100644 --- a/shesha-reactjs/src/designer-components/endpointsAutocomplete/settingsForm.json +++ b/shesha-reactjs/src/designer-components/endpointsAutocomplete/settingsForm.json @@ -178,7 +178,7 @@ "type": "textField", "propertyName": "httpVerb", "componentName": "httpVerb", - "label": "Http Verb", + "label": "HTTP Verb", "labelAlign": "right", "parentId": "root", "hidden": { @@ -231,7 +231,7 @@ "labelAlign": "right", "parentId": "root", "hidden": false, - "description": "Enter custom event handler on changing of event. (form, event) are exposed", + "description": "Enter custom event handler on changing of event.", "validate": {}, "settingsValidationErrors": [], "exposedVariables": [ @@ -293,7 +293,7 @@ "labelAlign": "right", "parentId": "root", "hidden": false, - "description": "Enter custom event handler on focus of event. (form, event) are exposed", + "description": "Enter custom event handler on focus of event.", "validate": {}, "settingsValidationErrors": [], "exposedVariables": [ @@ -354,7 +354,7 @@ "labelAlign": "right", "parentId": "root", "hidden": false, - "description": "Enter custom event handler on blur of event. (form, event) are exposed", + "description": "Enter custom event handler on blur of event.", "validate": {}, "settingsValidationErrors": [], "exposedVariables": [ diff --git a/shesha-reactjs/src/designer-components/entityPicker/index.tsx b/shesha-reactjs/src/designer-components/entityPicker/index.tsx index f7442a8e08..374727e018 100644 --- a/shesha-reactjs/src/designer-components/entityPicker/index.tsx +++ b/shesha-reactjs/src/designer-components/entityPicker/index.tsx @@ -1,27 +1,28 @@ import { EllipsisOutlined } from '@ant-design/icons'; -import React, { useCallback, useMemo } from 'react'; -import { EntityPicker } from '@/components'; +import React, { CSSProperties, useCallback, useMemo } from 'react'; +import { EntityPicker, ValidationErrors } from '@/components'; import { migrateDynamicExpression } from '@/designer-components/_common-migrations/migrateUseExpression'; import { IToolboxComponent } from '@/interfaces'; import { DataTypes } from '@/interfaces/dataTypes'; -import { ButtonGroupItemProps } from '@/providers'; +import { ButtonGroupItemProps, IStyleType } from '@/providers'; import { IConfigurableColumnsProps } from '@/providers/datatableColumnsConfigurator/models'; import { FormIdentifier, IConfigurableFormComponent } from '@/providers/form/models'; -import { executeExpression, getStyle, useAvailableConstantsData, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { executeExpression, useAvailableConstantsData, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { ITableViewProps } from '@/providers/dataTable/filters/models'; import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import { migrateV0toV1 } from './migrations/migrate-v1'; -import { entityPickerSettings } from './settingsForm'; import { migrateCustomFunctions, migratePropertyName, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; import { isEntityReferenceArrayPropertyMetadata, isEntityReferencePropertyMetadata } from '@/interfaces/metadata'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { IncomeValueFunc, OutcomeValueFunc } from '@/components/entityPicker/models'; import { ModalFooterButtons } from '@/providers/dynamicModal/models'; -import { customOnChangeValueEventHandler } from '@/components/formDesigner/components/utils'; +import { customOnChangeValueEventHandler, isValidGuid } from '@/components/formDesigner/components/utils'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; import { getValueByPropertyName } from '@/utils/object'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; -export interface IEntityPickerComponentProps extends IConfigurableFormComponent { +export interface IEntityPickerComponentProps extends IConfigurableFormComponent, IStyleType { placeholder?: string; items: IConfigurableColumnsProps[]; hideBorder?: boolean; @@ -42,6 +43,9 @@ export interface IEntityPickerComponentProps extends IConfigurableFormComponent widthUnits?: string; buttons?: ButtonGroupItemProps[]; footerButtons?: ModalFooterButtons; + dividerWidth?: string; + dividerStyle?: CSSProperties['borderLeftStyle']; + dividerColor?: string; } const EntityPickerComponent: IToolboxComponent = { @@ -51,10 +55,9 @@ const EntityPickerComponent: IToolboxComponent = { name: 'Entity Picker', icon: , dataTypeSupported: ({ dataType }) => dataType === DataTypes.entityReference, - Factory: ({ model }) => { + Factory: ({ model }) => { const allData = useAvailableConstantsData(); - - const { filters, modalWidth, customWidth, widthUnits, style } = model; + const { filters, modalWidth, customWidth, widthUnits } = model; const displayEntityKey = model.displayEntityKey || '_displayName'; @@ -71,12 +74,12 @@ const EntityPickerComponent: IToolboxComponent = { ]; }, [filters]); - const incomeValueFunc: IncomeValueFunc = useCallback( (value: any, args: any) => { + const incomeValueFunc: IncomeValueFunc = useCallback((value: any, args: any) => { if (model.valueFormat === 'entityReference') { return !!value ? value.id : null; } if (model.valueFormat === 'custom') { - return executeExpression(model.incomeCustomJs, {...args, value}, null, null ); + return executeExpression(model.incomeCustomJs, { ...args, value }, null, null); } return value; }, [model.valueFormat, model.incomeCustomJs]); @@ -84,17 +87,22 @@ const EntityPickerComponent: IToolboxComponent = { const outcomeValueFunc: OutcomeValueFunc = useCallback((value: any, args: any) => { if (model.valueFormat === 'entityReference') { return !!value - ? {id: value.id, _displayName: getValueByPropertyName(value, displayEntityKey) ?? value._displayName, _className: model.entityType} + ? { id: value.id, _displayName: getValueByPropertyName(value, displayEntityKey) ?? value._displayName, _className: model.entityType } : null; } if (model.valueFormat === 'custom') { - return executeExpression(model.outcomeCustomJs, {...args, value}, null, null ); + return executeExpression(model.outcomeCustomJs, { ...args, value }, null, null); } return !!value ? value.id : null; }, [model.valueFormat, model.outcomeCustomJs, displayEntityKey, model.entityType]); + if (model?.background?.type === 'storedFile' && model?.background.storedFile?.id && !isValidGuid(model?.background.storedFile.id)) { + return ; + } + const width = modalWidth === 'custom' && customWidth ? `${customWidth}${widthUnits}` : modalWidth; - const computedStyle = getStyle(style, allData.data) ?? {}; + + const finalStyle = model.allStyles.fullStyle; return ( @@ -107,38 +115,39 @@ const EntityPickerComponent: IToolboxComponent = { onChange(...args); }; return ( - - ); + + ); }} ); @@ -173,12 +182,12 @@ const EntityPickerComponent: IToolboxComponent = { .add(6, (prev) => migrateReadOnly(prev)) .add(7, (prev, context) => ({ ...prev, - valueFormat: prev.valueFormat ?? + valueFormat: prev.valueFormat ?? context.isNew + ? 'simple' + : prev['useRawValue'] === true ? 'simple' - : prev['useRawValue'] === true - ? 'simple' - : 'entityReference', + : 'entityReference', })) .add(8, (prev, context) => ({ ...prev, @@ -186,16 +195,17 @@ const EntityPickerComponent: IToolboxComponent = { ? 'default' : prev.footerButtons ?? prev.showModalFooter ? 'default' : 'none', })) - .add(9, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) - , - settingsFormMarkup: entityPickerSettings, - validateSettings: (model) => validateConfigurableComponentSettings(entityPickerSettings, model), + .add(9, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) + .add(10, (prev) => ({ ...prev, desktop: { ...defaultStyles(prev) }, mobile: { ...defaultStyles(prev) }, tablet: { ...defaultStyles(prev) } })), + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), linkToModelMetadata: (model, propMetadata): IEntityPickerComponentProps => { return { ...model, + editMode: 'inherited', entityType: isEntityReferencePropertyMetadata(propMetadata) - ? propMetadata.entityType + ? propMetadata.entityType : isEntityReferenceArrayPropertyMetadata(propMetadata) ? propMetadata.entityType : undefined, @@ -203,17 +213,17 @@ const EntityPickerComponent: IToolboxComponent = { valueFormat: isEntityReferenceArrayPropertyMetadata(propMetadata) ? 'entityReference' : 'simple', }; }, - getFieldsToFetch: (propertyName, rawModel) => { - if (rawModel.valueFormat === 'entityReference') { - return [ - `${propertyName}.id`, - rawModel.displayEntityKey - ? `${propertyName}.${rawModel.displayEntityKey}` - : `${propertyName}._displayName`, - `${propertyName}._className` - ]; - } - return null; + getFieldsToFetch: (propertyName, rawModel) => { + if (rawModel.valueFormat === 'entityReference') { + return [ + `${propertyName}.id`, + rawModel.displayEntityKey + ? `${propertyName}.${rawModel.displayEntityKey}` + : `${propertyName}._displayName`, + `${propertyName}._className` + ]; + } + return null; }, validateModel: (model, addModelError) => { if (!model.entityType) addModelError('entityType', 'Select `Entity Type` on the settings panel'); diff --git a/shesha-reactjs/src/designer-components/entityPicker/settingsForm.ts b/shesha-reactjs/src/designer-components/entityPicker/settingsForm.ts index ad2440dd40..c61a3221a3 100644 --- a/shesha-reactjs/src/designer-components/entityPicker/settingsForm.ts +++ b/shesha-reactjs/src/designer-components/entityPicker/settingsForm.ts @@ -1,539 +1,879 @@ -import { nanoid } from '@/utils/uuid'; import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; -import { EXPOSED_VARIABLES } from './utils'; +import { FormLayout } from 'antd/lib/form/Form'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { nanoid } from '@/utils/uuid'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; -export const entityPickerSettings = new DesignerToolbarSettings() - .addCollapsiblePanel({ - id: '11114bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'pnlDisplay', - parentId: 'root', - label: 'Display', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addContextPropertyAutocomplete({ - id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', - propertyName: 'propertyName', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Property name', - validate: { - required: true, - }, - }) - .addTextField({ - id: '46d07439-4c18-468c-89e1-60c002ce96c5', - propertyName: 'label', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Label', - version: 0, - textType: 'text', - }) - .addDropdown({ - id: '57a40a33-7e08-4ce4-9f08-a34d24a83338', - propertyName: 'labelAlign', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Label align', - values: [ - { - label: 'left', - value: 'left', - id: 'f01e54aa-a1a4-4bd6-ba73-c395e48af8ce', - }, - { - label: 'right', - value: 'right', - id: 'b920ef96-ae27-4a01-bfad-b5b7d07218da', - }, - ], - dataSourceType: 'values', - }) - .addTextArea({ - id: '2d32fe70-99a0-4825-ae6c-8b933004e119', - propertyName: 'description', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Description', - }) - .addTextField({ - id: "fbdb6bb1-5994-4205-86b4-236ca61ae54e", - propertyName: "placeholder", - parentId: "pnl54bf6-f76d-4139-a850-c99bf06c8b69", - label: "Placeholder", - }) - .addDropdown({ - id: '1692d566-fe48-43bd-84e0-28b7103354c1', - propertyName: 'mode', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Mode', - hidden: false, - values: [ - { - label: 'single', - value: 'single', - id: '329a001c-3c42-44b2-8616-a8fde0959323', - }, - { - label: 'multiple', - value: 'multiple', - id: '722eb4ea-9e00-4f4e-addb-e7300fa0c74c', - }, - ], - dataSourceType: 'values', - defaultValue: ['single'], - }) - .addAutocomplete({ - id: '6b0bd9c6-6a53-4a05-9de0-ad1b17eb0018', - propertyName: 'entityType', - label: 'Entity Type', - labelAlign: 'right', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - hidden: false, - dataSourceType: 'url', - validate: {}, - dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', - settingsValidationErrors: [], - useRawValues: true, - queryParams: null, - }) - .addContainer({ - id: "pn154bf6-f76d-4139-a850-c99bf06c8b69", - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'filters', - hidden: { _code: 'return !getSettingValue(data?.entityType);', _mode: 'code', _value: false } as any, - components: new DesignerToolbarSettings() - .addPropertyAutocomplete({ - id: 'hpm6rN_aj-L_KaG5MLIZt', - propertyName: 'displayEntityKey', - label: 'Display Property', - labelAlign: 'right', - parentId: 'pn154bf6-f76d-4139-a850-c99bf06c8b69', - hidden: false, - isDynamic: false, - description: - 'Name of the property that should be displayed in the field. Live empty to use default display property defined on the back-end.', - validate: {}, - modelType: '{{data.entityType}}', - autoFillProps: false, - settingsValidationErrors: [], - }) - .addSectionSeparator({ - id: '14413162-429e-451c-b3e6-b3f0ab6d7b09', - propertyName: 'sectionFilters', - parentId: 'pn154bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Filters', - }) - .addQueryBuilder({ - id: 'n4enebtmhFgvkP5ukQK1f', - propertyName: 'filters', - label: 'Entity Filter', - labelAlign: 'right', - parentId: 'pn154bf6-f76d-4139-a850-c99bf06c8b69', - hidden: false, - isDynamic: false, - validate: {}, - settingsValidationErrors: [], - modelType: '{{data.entityType}}', - fieldsUnavailableHint: 'Please select `Entity Type` to be able to configure this filter.', - }) - .addDropdown({ - id: 'acb2d566-fe48-43bd-84e0-28b7103354c1', - propertyName: 'valueFormat', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Value format', - hidden: false, - values: [ - { - label: 'Simple Id', - value: 'simple', - id: '329a001c-3c42-44b2-8616-a8fde0959323', - }, - { - label: 'Entity reference', - value: 'entityReference', - id: '722eb4ea-9e00-4f4e-addb-e7300fa0c74c', - }, - { - label: 'Custom', - value: 'custom', - id: 'faaeb4ea-9e00-4f4e-addb-e7300fa0c74c', - }, - ], - dataSourceType: 'values', - defaultValue: ['simple'], - }) - .addCodeEditor({ - id: '405b0599-914d-4d2d-875c-765a495472f8', - propertyName: 'incomeCustomJs', - label: 'Id value', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - validate: {}, - settingsValidationErrors: [], - description: "Return string value of Id", - hidden: { _code: 'return getSettingValue(data?.valueFormat) !== "custom";', _mode: 'code', _value: false } as any, - exposedVariables: [{ name: 'value', type: 'object', description: 'Field value' }], - }) - .addCodeEditor({ - id: '81fb0599-914d-4d2d-875c-765a495472f8', - propertyName: 'outcomeCustomJs', - label: 'Custom value', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - validate: {}, - settingsValidationErrors: [], - description: "Return value that will be stored as field value", - hidden: { _code: 'return getSettingValue(data?.valueFormat) !== "custom";', _mode: 'code', _value: false } as any, - exposedVariables: [{ name: 'value', type: 'object', description: 'Selected value' }], - }) - .addColumnsEditor({ - id: '2a6ee3b0-15f1-4521-cc6e-6a1c9d192ce2', - propertyName: 'items', - parentId: 'pn154bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Columns', - items: [], - modelType: '{{data.entityType}}', - }) - .toJson(), - }) - .addSectionSeparator({ - id: '253ee0fd-9916-4728-9f93-16ecc1ca3bb9', - propertyName: 'sectionFilters', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: '', - }) - .addCheckbox({ - id: 'cfd7d45e-c7e3-4a27-987b-dc525c412448', - propertyName: 'hidden', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Hidden', - }) - .addCheckbox({ - id: 'c6885251-96a6-40ce-99b2-4b5209a9e01c', - propertyName: 'hideLabel', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Hide Label', - }) - .addEditMode({ - id: '24a8be15-98eb-40f7-99ea-ebb602693e9c', - propertyName: 'editMode', - parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: "Edit mode", - }) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: '22224bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'pnlData', - parentId: 'root', - label: 'Data', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addCheckbox({ - id: '0cc0b997-f3f7-4a3d-ba36-8590687af9bd', - propertyName: 'allowNewRecord', - parentId: 'root', - label: 'Allow New Record', - }) - .addCollapsiblePanel({ - id: '1234d45e-c7e3-4a27-987b-dc525c412448', - propertyName: 'pnlModalSettings', - label: 'Dialogue settings', - labelAlign: 'right', - parentId: 'pnl24bf6-f76d-4139-a850-c99bf06c8b69', - hidden: { _code: 'return !getSettingValue(data?.allowNewRecord);', _mode: 'code', _value: false } as any, - validate: { - required: false, - }, - content: { - id: '2a5acbcf-cd52-487e-9cd7-09594a04793a', - components: new DesignerToolbarSettings() - .addTextField({ - id: '4b3b0da0-f126-4e37-b5f5-568367dc008f', - propertyName: 'modalTitle', - label: 'Title', - labelAlign: 'right', - parentId: '2a5acbcf-cd52-487e-9cd7-09594a04793a', - hidden: false, +export const getSettings = (data) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const validationTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const dataPanelId = nanoid(); + const fontStylePnlId = nanoid(); + const dimensionsStylePnlId = nanoid(); + const borderStylePnlId = nanoid(); + const backgroundStylePnlId = nanoid(); + const backgroundStyleRowId = nanoid(); + const shadowStylePnlId = nanoid(); + const modalSettingsPnlId = nanoid(); + const dataPanelContainerId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', validate: { required: true, }, - version: 0, - textType: 'text', + jsSetting: true, }) - .addFormAutocomplete({ - id: 'fd3d4ef4-be06-40e9-9815-118754707d0e', - propertyName: 'modalFormId', - label: 'Modal form', - labelAlign: 'right', - parentId: '2a5acbcf-cd52-487e-9cd7-09594a04793a', - hidden: false, - validate: { - required: true, - }, + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, }) - .addDropdown({ + .addSettingsInput({ id: nanoid(), - propertyName: 'footerButtons', - label: 'Buttons type', - dataSourceType: 'values', - values: [ - { id: nanoid(), label: 'Default', value: 'default' }, - { id: nanoid(), label: 'Custom', value: 'custom' }, - { id: nanoid(), label: 'None', value: 'none' }, - ], - defaultValue: 'default', + parentId: commonTabId, + inputType: 'textField', + propertyName: 'placeholder', + label: 'Placeholder', + size: 'small', + jsSetting: true, }) - .addButtons({ + .addSettingsInput({ + inputType: 'textArea', id: nanoid(), - propertyName: 'buttons', - hidden: { _code: 'return !(getSettingValue(data?.footerButtons) === "custom");', _mode: 'code', _value: false }, - label: 'Configure Modal Buttons', + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, }) - .addDropdown({ - id: 'ea60aee4-a7aa-4fd6-a641-638a5a609157', - propertyName: 'submitHttpVerb', - parentId: '2a5acbcf-cd52-487e-9cd7-09594a04793a', - label: 'Submit Http Verb', - values: [ + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ { - label: 'POST', - value: 'POST', - id: '8418606a-d85d-4795-a2ee-4a69fcc656f9', + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + defaultValue: 'inherited', + jsSetting: true, }, { - label: 'PUT', - value: 'PUT', - id: '64bbca8a-2fb1-4448-ab71-3db077233bd2', + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', }, ], - dataSourceType: 'values', - hidden: { _code: 'return !(getSettingValue(data?.showModalFooter) === true || getSettingValue(data?.footerButtons) === "default");', _mode: 'code', _value: false }, - defaultValue: 'POST', }) - .addTextField({ - id: 'e669632e-55e0-46f4-9585-9e81ef0ae174', - propertyName: 'onSuccessRedirectUrl', - parentId: '2a5acbcf-cd52-487e-9cd7-09594a04793a', - label: 'Success Redirect URL', - hidden: { _code: 'return !getSettingValue(data?.showModalFooter);', _mode: 'code', _value: false } as any, - version: 0, - textType: 'text', + .toJson() + ] + }, + { + key: '2', + title: 'Data', + id: dataTabId, + components: [...new DesignerToolbarSettings() + .addContainer({ + id: dataPanelContainerId, + parentId: dataTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dataPanelContainerId, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + propertyName: 'mode', + label: 'Selection Type', + size: 'small', + jsSetting: true, + dropdownOptions: [ + { + label: 'Single', + value: 'single', + }, + { + label: 'Multiple', + value: 'multiple', + }, + ], + }, + { + type: 'autocomplete', + id: nanoid(), + propertyName: 'entityType', + label: 'Entity Type', + jsSetting: true, + dataSourceType: 'url', + validate: {}, + dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', + settingsValidationErrors: [], + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataPanelContainerId, + readOnly: false, + hidden: { _code: 'return !getSettingValue(data?.entityType);', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'propertyAutocomplete', + id: nanoid(), + propertyName: 'displayEntityKey', + label: 'Display Property', + labelAlign: 'right', + hidden: false, + isDynamic: false, + description: 'Name of the property that should be displayed in the field. Live empty to use default display property defined on the back-end.', + validate: {}, + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + autoFillProps: false, + settingsValidationErrors: [], + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataPanelContainerId, + inputs: [ + { + id: nanoid(), + type: 'queryBuilder', + propertyName: 'filters', + label: 'Entity Filter', + labelAlign: 'right', + isDynamic: false, + validate: {}, + settingsValidationErrors: [], + modelType: '{{data.entityType}}', + fieldsUnavailableHint: 'Please select `Entity Type` to be able to configure this filter.', + hidden: { _code: 'return !getSettingValue(data?.entityType);', _mode: 'code', _value: false } as any, + }, + ], + }) + .addSettingsInput({ + inputType: 'dropdown', + id: nanoid(), + propertyName: 'valueFormat', + parentId: dataPanelContainerId, + label: 'Value Format', + jsSetting: true, + dropdownOptions: [ + { + label: 'Simple ID', + value: 'simple', + }, + { + label: 'Entity reference', + value: 'entityReference', + }, + { + label: 'Custom', + value: 'custom', + }, + ], + defaultValue: ['simple'], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataPanelContainerId, + hidden: { _code: 'return getSettingValue(data?.valueFormat) !== "custom";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'codeEditor', + id: nanoid(), + propertyName: 'incomeCustomJs', + label: 'ID Value', + parentId: dataPanelId, + validate: {}, + settingsValidationErrors: [], + description: "Return string value of Id", + }, + { + type: 'codeEditor', + id: nanoid(), + propertyName: 'outcomeCustomJs', + label: 'Custom Value', + parentId: dataPanelId, + validate: {}, + settingsValidationErrors: [], + description: "Return value that will be stored as field value", + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'columnsConfig', + propertyName: 'items', + parentId: dataPanelContainerId, + label: 'Columns', + items: [], + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false + } as any, + parentComponentType: 'entityPicker', + jsSetting: true, + }).toJson()] }) - .addDropdown({ - id: '264903ff-b525-4a6e-893f-d560b219df9d', - propertyName: 'modalWidth', - label: 'Dialog Width (%)', - allowClear: true, - values: [ - { - label: 'Small', - value: '40%', - id: '2f56ae38-e5f3-40ff-9830-bc048736ddb4', - }, - { - label: 'Medium', - value: '60%', - id: '470d820b-7cd7-439c-8e95-1f5b3134f80c', - }, - { - label: 'Large', - value: '80%', - id: '1f2ac3db-3b3f-486c-991f-ad703088ab2d', - }, - { - label: 'Custom', - value: 'custom', - id: 'fde460b0-1f84-4b64-9a6a-e02ba862937d', - }, - ], - dataSourceType: 'values', + .addSettingsInput({ + inputType: 'switch', + id: nanoid(), + propertyName: 'allowNewRecord', + parentId: dataTabId, + label: 'Allow New Record', }) - .addDropdown({ + .addCollapsiblePanel({ id: nanoid(), - propertyName: 'widthUnits', - label: 'Units', - allowClear: true, - values: [ - { - label: 'Percentage (%)', - value: '%', - id: '2f56ae38-e5f3-40ff-9830-bc048736ddb4', - }, - { - label: 'Pixels (px)', - value: 'px', - id: '470d820b-7cd7-439c-8e95-1f5b3134f80c', - }, - ], - dataSourceType: 'values', - hidden: { _code: 'return getSettingValue(data?.modalWidth) !== "custom";', _mode: 'code', _value: false } as any, + propertyName: 'modalSettings', + label: 'Dialog Settings', + labelAlign: 'right', + parentId: dataTabId, + collapsible: 'header', + ghost: true, + hidden: { _code: 'return !getSettingValue(data?.allowNewRecord);', _mode: 'code', _value: false } as any, + content: { + id: modalSettingsPnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: modalSettingsPnlId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'modalTitle', + label: 'Title', + labelAlign: 'right', + parentId: modalSettingsPnlId, + hidden: false, + jsSetting: true, + validate: { + required: true, + } + }, + { + type: 'formAutocomplete', + id: nanoid(), + propertyName: 'modalFormId', + label: 'Modal Form', + labelAlign: 'right', + parentId: modalSettingsPnlId, + hidden: false, + size: 'small', + jsSetting: true, + validate: { + required: true, + } + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + inputs: [{ + id: nanoid(), + propertyName: 'footerButtons', + label: 'Buttons Type', + type: 'dropdown', + width: 120, + jsSetting: true, + dropdownOptions: [ + { label: 'Default', value: 'default' }, + { label: 'Custom', value: 'custom' }, + { label: 'None', value: 'none' }, + ], + defaultValue: 'default', + }, + { + type: 'dropdown', + id: nanoid(), + hidden: { _code: 'return !(getSettingValue(data?.showModalFooter) === true || getSettingValue(data?.footerButtons) === "default");', _mode: 'code', _value: false } as any, + propertyName: 'submitHttpVerb', + label: 'Submit HTTP Verb', + jsSetting: true, + dropdownOptions: [ + { label: 'POST', value: 'POST' }, + { label: 'PUT', value: 'PUT' }, + ], + defaultValue: 'POST', + }, + { + id: nanoid(), + propertyName: 'buttons', + hidden: { _code: 'return !(getSettingValue(data?.footerButtons) === "custom");', _mode: 'code', _value: false } as any, + label: 'Configure Modal Buttons', + type: 'buttonGroupConfigurator', + jsSetting: true, + } + ] + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'modalWidth', + inputType: 'customDropdown', + label: 'Dialog Width', + allowClear: true, + jsSetting: true, + customTooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + customDropdownMode: 'single', + dropdownOptions: [ + { + label: 'Small', + value: '40%', + }, + { + label: 'Medium', + value: '60%', + }, + { + label: 'Large', + value: '80%', + } + ] + }) + .toJson()] + } }) - .addNumberField({ + .toJson() + ] + }, + { + key: '3', + title: 'Validation', + id: validationTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ id: nanoid(), - propertyName: 'customWidth', - label: 'Enter Custom Width', - hidden: { _code: 'return getSettingValue(data?.modalWidth) !== "custom" || !getSettingValue(data?.widthUnits);', _mode: 'code', _value: false } as any, - min: 0, + propertyName: 'validate.required', + label: 'Required', + inputType: 'switch', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: validationTabId }) .toJson() - } - }) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: '2225bfe4-ee69-431e-931b-b0e0b9ceee6f', - propertyName: 'pnlValidation', - parentId: 'root', - label: 'Validation', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl34bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addCheckbox({ - id: '3be9da3f-f47e-48ae-b4c3-f5cc36e534d9', - propertyName: 'validate.required', - parentId: 'pnl34bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Required', - }).toJson() - ] - } - }) - .addCollapsiblePanel({ - id: '3335bfe4-ee69-431e-931b-b0e0b9ceee6f', - propertyName: 'pnlStyle', - parentId: 'root', - label: 'Style', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl44bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addCodeEditor({ - id: '06ab0599-914d-4d2d-875c-765a495472f8', - propertyName: 'style', - label: 'Style', - parentId: 'pnl44bf6-f76d-4139-a850-c99bf06c8b69', - validate: {}, - settingsValidationErrors: [], - description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', - exposedVariables: [ - { - id: nanoid(), - name: 'data', - description: 'Form values', - type: 'object', - }, - ], - }) - .addDropdown({ - id: '8615d12f-6ea0-4b11-a1a1-6088c7160fd9', - propertyName: 'size', - parentId: 'pnl44bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Size', - allowClear: true, - values: [ - { - label: 'Small', - value: 'small', - id: '4f11403c-95fd-4e49-bb60-cb8c25f0f3c3', - }, - { - label: 'Middle', - value: 'middle', - id: '8f85c476-e632-4fa7-89ad-2be6cfb7f1f1', - }, - { - label: 'Large', - value: 'large', - id: 'f01e54aa-a1a4-4bd6-ba73-c395e48af8ce', - }, - ], - dataSourceType: 'values', - }).toJson() - ] - } - }) - .addCollapsiblePanel({ - id: '3335bfe4-ee69-431e-931b-b0e0b9cexe6f', - propertyName: 'pnlEvents', - parentId: 'root', - label: 'Events', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - version: 4, - collapsible: 'header', - content: { - id: '44bf6-f76d-4139-a853-c99bf06c8b69', - components: [...new DesignerToolbarSettings() - .addCodeEditor( + ] + }, + { + key: '4', + title: 'Events', + id: eventsTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onChangeCustom', + label: 'On Change', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on changing of event.', + parentId: eventsTabId + }) + .toJson() + ] + }, + { + key: '5', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: fontStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: fontStylePnlId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + hidden: { _code: 'return getSettingValue(data?.mode) === "multiple";', _mode: 'code', _value: false } as any, + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: dimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: borderStylePnlId, + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: borderStylePnlId, + components: getBorderInputs('', true, true) as any + }) + .addContainer({ + id: nanoid(), + parentId: borderStylePnlId, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: backgroundStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: backgroundStylePnlId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: backgroundStyleRowId, + parentId: backgroundStylePnlId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + hideLabel: true, + propertyName: "background.size", + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + }, + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: shadowStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: shadowStylePnlId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson()] + }).toJson()] + }, { - id: "3cef348b-6bba-4176-93f6-f3a8b21e33c9", - propertyName: "onChangeCustom", - label: "On Change", - labelAlign: "right", - parentId: "44bf6-f76d-4139-a853-c99bf06c8b69", - hidden: false, - description: "Enter custom eventhandler on changing of event. (data, form, event) are exposed", - validate: {}, - settingsValidationErrors: [], - exposedVariables: EXPOSED_VARIABLES, - version: 3, - language: "typescript", - wrapInTemplate: true, - templateSettings: { - functionName: "onChange" - }, - availableConstantsExpression: async ({ metadataBuilder }) => { - return metadataBuilder.object('constants') - .addAllStandard() - .addString("value", "Component current value") - .addObject("event", "Event callback when user input", undefined) - .build(); - }, + key: '6', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] } - ) - .toJson() - ] - } - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } } - }) - .toJson(); + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/entityPicker/utils.ts b/shesha-reactjs/src/designer-components/entityPicker/utils.ts index 2fb70ef882..1fad2d01a9 100644 --- a/shesha-reactjs/src/designer-components/entityPicker/utils.ts +++ b/shesha-reactjs/src/designer-components/entityPicker/utils.ts @@ -1,3 +1,40 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (prev): IStyleType => { + const { size } = prev; + + return { + border: { hideBorder: false, radiusType: 'all', borderType: 'all', border: { all: { width: '1px', style: 'solid', color: '#d9d9d9' } }, radius: { all: 8 } }, + dimensions: { width: '100%', height: size === 'small' ? '22px' : size === 'large' ? '38px' : 'auto', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' }, + background: { + type: 'color', + color: '#fff', + repeat: 'no-repeat', + size: 'cover', + position: 'center', + gradient: { direction: 'to right', colors: {} }, + url: '', + storedFile: { id: null }, + uploadFile: null, + }, + font: { + color: '#000', + type: 'Segoe UI', + align: 'left', + size: 14, + weight: '400', + }, + shadow: { + offsetX: 0, + offsetY: 0, + color: '#000', + blurRadius: 0, + spreadRadius: 0 + }, + }; +}; + + export const EXPOSED_VARIABLES = [ { id: 'd430d31c-9360-4b57-96cc-3c322de31e58', @@ -59,10 +96,10 @@ export const EXPOSED_VARIABLES = [ description: "Setting the global state of the application", type: "(payload: { key: string, data: any } ) => void" }, - { + { id: '5a367dfe-70e4-4521-96ba-bdee1336592h', name: "option", description: "Meta data of component current value", type: "object" }, -]; +]; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/entityReference/entityReference.tsx b/shesha-reactjs/src/designer-components/entityReference/entityReference.tsx index 7fe835ca77..f281b8fb63 100644 --- a/shesha-reactjs/src/designer-components/entityReference/entityReference.tsx +++ b/shesha-reactjs/src/designer-components/entityReference/entityReference.tsx @@ -1,19 +1,30 @@ -import React from 'react'; import { EntityReference, IEntityReferenceProps } from '@/components/entityReference'; import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; +import { ShaIconTypes } from '@/components/iconPicker'; +import { + migrateCustomFunctions, + migratePropertyName, + migrateReadOnly, +} from '@/designer-components/_common-migrations/migrateSettings'; +import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; +import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; import { LinkExternalOutlined } from '@/icons/linkExternalOutlined'; import { IToolboxComponent } from '@/interfaces'; -import { IConfigurableFormComponent } from '@/providers/form/models'; -import { EntityReferenceSettingsForm } from './settings'; -import { migratePropertyName, migrateCustomFunctions, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; import { isEntityReferencePropertyMetadata } from '@/interfaces/metadata'; -import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; +import { IConfigurableFormComponent } from '@/providers/form/models'; +import React from 'react'; import { migrateNavigateAction } from '../_common-migrations/migrate-navigate-action'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; export type IActionParameters = [{ key: string; value: string }]; -export interface IEntityReferenceControlProps extends IEntityReferenceProps, IConfigurableFormComponent {} +export interface IEntityReferenceControlProps extends Omit, IConfigurableFormComponent { + /** @deprecated Use iconName instead */ + icon?: string; +} const EntityReferenceComponent: IToolboxComponent = { type: 'entityReference', @@ -22,50 +33,69 @@ const EntityReferenceComponent: IToolboxComponent isOutput: true, icon: , Factory: ({ model: passedModel }) => { - const { style, hidden, readOnly, ...model } = passedModel; - - if (hidden) - return null; + const { allStyles, hidden, readOnly, ...model } = passedModel; + + if (hidden) return null; return ( {(value) => { - return ; + return ; }} ); }, - settingsFormFactory: (props) => { - return ; - }, - migrator: m => m - .add(0, prev => { - return { + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + migrator: (m) => + m + .add(0, (prev) => { + return { + ...prev, + formSelectionMode: 'name', + entityReferenceType: 'Quickview', + quickviewWidth: 600, + displayProperty: '', + handleFail: false, + handleSuccess: false, + style: prev.style, + stylingBox: prev.stylingBox, + }; + }) + .add(1, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(2, (prev) => migrateVisibility(prev)) + .add(3, (prev) => ({ + ...prev, + onSuccess: migrateNavigateAction(prev.onSuccess), + onFail: migrateNavigateAction(prev.onFail), + })) + .add(4, (prev) => migrateReadOnly(prev, 'editable')) + .add(5, (prev, context) => ({ + ...prev, + footerButtons: context.isNew ? 'default' : (prev.footerButtons ?? prev.showModalFooter) ? 'default' : 'none', + })) + .add(6, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) + .add(7, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })) + .add(8, (prev) => ({ + ...prev, + iconName: (prev?.iconName as ShaIconTypes) ?? (prev?.icon as ShaIconTypes), + })) + .add(9, (prev) => ({ ...prev, - formSelectionMode: 'name', - entityReferenceType: 'Quickview', - quickviewWidth: 600, - displayProperty: '', - handleFail: false, - handleSuccess: false - }; - }) - .add(1, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(2, (prev) => migrateVisibility(prev)) - .add(3, (prev) => ({ - ...prev, - onSuccess: migrateNavigateAction(prev.onSuccess), - onFail: migrateNavigateAction(prev.onFail), - })) - .add(4, (prev) => migrateReadOnly(prev, 'editable')) - .add(5, (prev, context) => ({ - ...prev, - footerButtons: context.isNew - ? 'default' - : prev.footerButtons ?? prev.showModalFooter ? 'default' : 'none', - })) - .add(6, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) - , + style: prev.style, + mobile: { + ...prev.mobile, + style: prev.style, + }, + tablet: { + ...prev.tablet, + style: prev.style, + }, + desktop: { + ...prev.desktop, + style: prev.style, + }, + })), linkToModelMetadata: (model, propMetadata): IEntityReferenceControlProps => { return { ...model, diff --git a/shesha-reactjs/src/designer-components/entityReference/settings.tsx b/shesha-reactjs/src/designer-components/entityReference/settings.tsx index 049dceb667..c8f6ce5e2d 100644 --- a/shesha-reactjs/src/designer-components/entityReference/settings.tsx +++ b/shesha-reactjs/src/designer-components/entityReference/settings.tsx @@ -20,12 +20,12 @@ import { useFormDesignerState } from '@/providers/formDesigner'; import { ButtonGroupConfigurator, IconPicker } from '@/components'; import { PermissionAutocomplete } from '@/components/permissionAutocomplete'; -const formTypes = ['Table', 'Create', 'Edit', 'Details', 'Quickview', 'ListItem', 'Picker']; +export const formTypes = ['Table', 'Create', 'Edit', 'Details', 'Quickview', 'ListItem', 'Picker']; const EntityReferenceSettings: FC> = (props) => { const { readOnly } = props; - const { model, values, onValuesChange } = useSettingsForm(); + const { values, onValuesChange } = useSettingsForm(); const designerModelType = useFormDesignerState(false)?.formSettings?.modelType; const { formSettings } = useForm(); @@ -43,8 +43,10 @@ const EntityReferenceSettings: FC diff --git a/shesha-reactjs/src/designer-components/entityReference/settingsForm.ts b/shesha-reactjs/src/designer-components/entityReference/settingsForm.ts new file mode 100644 index 0000000000..5c12633409 --- /dev/null +++ b/shesha-reactjs/src/designer-components/entityReference/settingsForm.ts @@ -0,0 +1,1194 @@ +import { DesignerToolbarSettings } from '@/index'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/lib/form/Form'; +import { IEntityReferenceControlProps } from './entityReference'; +import { + backgroundTypeOptions, + positionOptions, + repeatOptions, + sizeOptions, +} from '../_settings/utils/background/utils'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; + +export const getSettings = (data: IEntityReferenceControlProps) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const dataTabId = nanoid(); + const securityId = nanoid(); + const styleRouterId = nanoid(); + const propertyNameId = nanoid(); + const hiddenId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: propertyNameId, + propertyName: 'propertyName', + parentId: commonTabId, + label: 'Property Name', + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + styledLabel: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: `placeholder-${commonTabId}`, + propertyName: 'placeholder', + label: 'Placeholder', + size: 'small', + jsSetting: true, + }, + { + type: 'textArea', + id: `tooltip-${commonTabId}`, + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'displayType', + label: 'Display Type', + type: 'dropdown', + allowClear: true, + size: 'small', + jsSetting: true, + dropdownOptions: [ + { value: 'displayProperty', label: 'Display property' }, + { value: 'icon', label: 'Icon' }, + { value: 'textTitle', label: 'Text title' }, + ], + readOnly: { + _code: 'return getSettingValue(data?.readOnly);', + _mode: 'code', + _value: false, + } as any, + parentId: commonTabId, + }, + { + id: nanoid(), + propertyName: 'iconName', + label: 'Icon', + parentId: commonTabId, + type: 'iconPicker', + jsSetting: true, + allowClear: true, + readOnly: { + _code: 'return getSettingValue(data?.readOnly);', + _mode: 'code', + _value: false, + } as any, + hidden: { + _code: 'return getSettingValue(data?.displayType) !== "icon";', + _mode: 'code', + _value: false, + } as any, + }, + { + id: nanoid(), + propertyName: 'textTitle', + label: 'Text Title', + parentId: commonTabId, + type: 'textField', + jsSetting: true, + readOnly: { + _code: 'return getSettingValue(data?.readOnly);', + _mode: 'code', + _value: false, + } as any, + hidden: { + _code: 'return getSettingValue(data?.displayType) !== "textTitle";', + _mode: 'code', + _value: false, + } as any, + }, + ], + }) + .addSettingsInput({ + inputType: 'switch', + id: hiddenId, + propertyName: 'hidden', + parentId: commonTabId, + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }) + .toJson(), + ], + }, + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'autocomplete', + allowClear: true, + propertyName: 'entityType', + label: 'Entity Type', + description: 'The entity type you want to use for the chart.', + labelAlign: 'right', + parentId: dataTabId, + hidden: false, + dataSourceType: 'url', + validate: { required: true }, + dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', + settingsValidationErrors: [], + useRawValues: true, + width: '100%', + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'autocomplete', + allowClear: true, + propertyName: 'getEntityUrl', + label: 'Get Entity URL', + labelAlign: 'right', + parentId: dataTabId, + hidden: false, + dataSourceType: 'url', + validate: { required: true }, + dataSourceUrl: '/api/services/app/Api/Endpoints', + settingsValidationErrors: [], + useRawValues: true, + width: '100%', + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + hidden: { + _code: 'return getSettingValue(data?.displayType) !== "displayProperty";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'displayProperty', + label: 'Display Property', + parentId: dataTabId, + type: 'propertyAutocomplete', + allowClear: true, + jsSetting: true, + width: '100%', + modelType: { + _code: 'return getSettingValue(data?.entityType);', + _mode: 'code', + _value: false, + } as any, + autoFillProps: false, + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'entityReferenceType', + label: 'Entity Reference Type', + parentId: dataTabId, + inputType: 'dropdown', + allowClear: true, + jsSetting: true, + dropdownOptions: [ + { value: 'Quickview', label: 'Quickview' }, + { value: 'NavigateLink', label: 'Navigate link' }, + { value: 'Dialog', label: 'Dialog' }, + ], + defaultValue: 'Quickview', + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'formSelectionMode', + label: 'Form Selection Mode', + parentId: dataTabId, + type: 'dropdown', + allowClear: true, + jsSetting: true, + dropdownOptions: [ + { value: 'name', label: 'Name' }, + { value: 'dynamic', label: 'Dynamic' }, + ], + defaultValue: 'name', + }, + { + id: nanoid(), + propertyName: 'formType', + label: 'Form Type', + parentId: dataTabId, + type: 'formTypeAutocomplete', + jsSetting: true, + width: '100%', + allowClear: true, + readOnly: { + _code: 'return getSettingValue(data?.readOnly);', + _mode: 'code', + _value: false, + } as any, + hidden: { + _code: 'return getSettingValue(data?.formSelectionMode) !== "dynamic";', + _mode: 'code', + _value: false, + } as any, + }, + { + id: nanoid(), + propertyName: 'formIdentifier', + label: 'Form Identifier', + parentId: dataTabId, + type: 'formAutocomplete', + jsSetting: true, + allowClear: true, + readOnly: { + _code: 'return getSettingValue(data?.readOnly);', + _mode: 'code', + _value: false, + } as any, + hidden: { + _code: 'return getSettingValue(data?.formSelectionMode) !== "name";', + _mode: 'code', + _value: false, + } as any, + }, + ], + }) + .addContainer({ + id: nanoid(), + propertyName: 'pnlDialogSettings', + label: 'Dialog Settings', + labelAlign: 'right', + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.entityReferenceType) !== "Dialog";', + _mode: 'code', + _value: false, + } as any, + components: [ + ...new DesignerToolbarSettings() + .addSectionSeparator({ + id: nanoid(), + parentId: dataTabId, + label: 'Dialog Settings', + labelAlign: 'left', + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.entityReferenceType) !== "Dialog";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'modalTitle', + label: 'Modal Title', + parentId: dataTabId, + type: 'textField', + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'footerButtons', + label: 'Footer Buttons', + parentId: dataTabId, + type: 'dropdown', + allowClear: true, + jsSetting: true, + dropdownOptions: [ + { value: 'default', label: 'Default' }, + { value: 'custom', label: 'Custom' }, + { value: 'none', label: 'None' }, + ], + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'buttons', + label: 'Configure Modal Buttons', + parentId: dataTabId, + type: 'buttonGroupConfigurator', + hidden: { + _code: 'return getSettingValue(data?.footerButtons) !== "custom";', + _mode: 'code', + _value: false, + } as any, + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'submitHttpVerb', + label: 'Submit HTTP Verb', + parentId: dataTabId, + type: 'dropdown', + allowClear: true, + dropdownOptions: [ + { value: 'POST', label: 'POST' }, + { value: 'PUT', label: 'PUT' }, + ], + defaultValue: 'POST', + hidden: { + _code: 'return getSettingValue(data?.footerButtons) === "default";', + _mode: 'code', + _value: false, + } as any, + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: appearanceTabId, + readOnly: { + _code: 'return getSettingValue(data?.readOnly);', + _mode: 'code', + _value: false, + } as any, + hidden: { + _code: 'return getSettingValue(data?.entityReferenceType) !== "Dialog";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'additionalProperties', + label: 'Additional Properties', + parentId: dataTabId, + jsSetting: true, + type: 'labelValueEditor', + labelTitle: 'Key', + valueTitle: 'Value', + labelName: 'key', + valueName: 'value', + tooltip: + 'Additional properties you want to be passed when the form gets submitted like parentId in the case where the modal is used in a childTable. ' + + 'Also note you can use Mustache expression like {{id}} for value property. \n\n' + + 'Id initial value is already initialised with {{entityReference.id}} but you can override it', + exposedVariables: [ + { name: 'data', description: 'This form data', type: 'object' }, + { name: 'form', description: 'Form instance', type: 'object' }, + { + name: 'formMode', + description: 'Current form mode', + type: "'designer' | 'edit' | 'readonly'", + }, + { name: 'globalState', description: 'Global state', type: 'object' }, + { + name: 'entityReference.id', + description: 'Id of entity reference entity', + type: 'object', + }, + { name: 'entityReference.entity', description: 'Entity', type: 'object' }, + { name: 'moment', description: 'moment', type: '' }, + { name: 'http', description: 'axiosHttp', type: '' }, + ].map((item) => JSON.stringify(item)), + }, + { + id: nanoid(), + propertyName: 'modalWidth', + label: 'Dialog Width (%)', + parentId: dataTabId, + type: 'dropdown', + allowClear: true, + jsSetting: true, + dropdownOptions: [ + { value: '40%', label: 'Small' }, + { value: '60%', label: 'Medium' }, + { value: '80%', label: 'Large' }, + { value: 'custom', label: 'Custom' }, + ], + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.modalWidth) !== "custom";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + propertyName: 'widthUnits', + label: 'Width Units', + parentId: dataTabId, + type: 'dropdown', + allowClear: true, + jsSetting: true, + dropdownOptions: [ + { value: '%', label: 'Percentage (%)' }, + { value: 'px', label: 'Pixels (px)' }, + ], + width: '100%', + }, + { + id: nanoid(), + propertyName: 'customWidth', + label: 'Custom Width', + parentId: dataTabId, + type: 'textField', + jsSetting: true, + min: 0, + width: '100%', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'handleSuccess', + label: 'Handle Success', + parentId: dataTabId, + type: 'switch', + defaultValue: true, + jsSetting: true, + width: '100%', + }, + ], + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlOnSuccess', + label: 'On Success Handler', + labelAlign: 'left', + parentId: dataTabId, + collapsible: 'header', + ghost: true, + hidden: { + _code: 'return getSettingValue(data?.handleSuccess) !== true;', + _mode: 'code', + _value: false, + } as any, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addConfigurableActionConfigurator({ + id: nanoid(), + propertyName: 'onSuccess', + label: 'On Success', + parentId: dataTabId, + jsSetting: true, + }) + .toJson(), + ], + }, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'handleFail', + label: 'Handle Fail', + parentId: dataTabId, + type: 'switch', + defaultValue: false, + jsSetting: false, + }, + ], + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlOnFail', + label: 'On Fail Handler', + labelAlign: 'left', + parentId: dataTabId, + collapsible: 'header', + ghost: true, + hidden: { + _code: 'return getSettingValue(data?.handleFail) !== true;', + _mode: 'code', + _value: false, + } as any, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addConfigurableActionConfigurator({ + id: nanoid(), + propertyName: 'onFail', + label: 'On Fail', + parentId: dataTabId, + jsSetting: false, + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .addContainer({ + id: nanoid(), + propertyName: 'pnlQuickviewSettings', + label: 'Quickview Settings', + labelAlign: 'right', + parentId: dataTabId, + hidden: { + _code: 'return getSettingValue(data?.entityReferenceType) !== "Quickview";', + _mode: 'code', + _value: false, + } as any, + components: [ + ...new DesignerToolbarSettings() + .addSectionSeparator({ + id: nanoid(), + parentId: dataTabId, + label: 'Quickview Settings', + labelAlign: 'left', + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'quickviewWidth', + label: 'Quickview Width', + parentId: dataTabId, + inputType: 'numberField', + jsSetting: true, + defaultValue: 600, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: 'fontStyleCollapsiblePanel', + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: 'styleRouter', + ghost: true, + collapsible: 'header', + content: { + id: 'fontStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'try26voxhs-HxJ5k5ngYE', + parentId: 'fontStylePnl', + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: 'fontFamily-s4gmBg31azZC0UjZjpfTm', + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: 'fontSize-s4gmBg31azZC0UjZjpfTm', + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: 'fontWeight-s4gmBg31azZC0UjZjpfTm', + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: 'Controls text thickness (light, normal, bold, etc.)', + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: 'fontColor-s4gmBg31azZC0UjZjpfTm', + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: 'fontAlign-s4gmBg31azZC0UjZjpfTm', + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'dimensionsStyleCollapsiblePanel', + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: 'styleRouter', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: 'dimensionsStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'dimensionsStyleRowWidth', + parentId: 'dimensionsStylePnl', + inline: true, + inputs: [ + { + type: 'textField', + id: 'width-s4gmBg31azZC0UjZjpfTm', + label: 'Width', + width: 85, + propertyName: 'dimensions.width', + icon: 'widthIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: 'minWidth-s4gmBg31azZC0UjZjpfTm', + label: 'Min Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minWidth', + icon: 'minWidthIcon', + }, + { + type: 'textField', + id: 'maxWidth-s4gmBg31azZC0UjZjpfTm', + label: 'Max Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxWidth', + icon: 'maxWidthIcon', + }, + ], + }) + .addSettingsInputRow({ + id: 'dimensionsStyleRowHeight', + parentId: 'dimensionsStylePnl', + inline: true, + inputs: [ + { + type: 'textField', + id: 'height-s4gmBg31azZC0UjZjpfTm', + label: 'Height', + width: 85, + propertyName: 'dimensions.height', + icon: 'heightIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: 'minHeight-s4gmBg31azZC0UjZjpfTm', + label: 'Min Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minHeight', + icon: 'minHeightIcon', + }, + { + type: 'textField', + id: 'maxHeight-s4gmBg31azZC0UjZjpfTm', + label: 'Max Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxHeight', + icon: 'maxHeightIcon', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'borderStyleCollapsiblePanel', + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: 'borderStylePnl', + components: [ + ...new DesignerToolbarSettings() + + .addContainer({ + id: 'borderStyleRow', + parentId: 'borderStylePnl', + components: getBorderInputs() as any, + }) + .addContainer({ + id: 'borderRadiusStyleRow', + parentId: 'borderStylePnl', + components: getCornerInputs() as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'backgroundStyleCollapsiblePanel', + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: 'backgroundStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'backgroundStyleRow-selectType', + parentId: 'backgroundStylePnl', + label: 'Type', + jsSetting: false, + propertyName: 'background.type', + inputType: 'radio', + tooltip: 'Select a type of background', + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-color', + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'colorPicker', + id: 'backgroundStyleRow-color', + label: 'Color', + propertyName: 'background.color', + hideLabel: true, + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'backgroundStyle-gradientColors', + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'multiColorPicker', + id: 'backgroundStyle-gradientColors', + propertyName: 'background.gradient.colors', + label: 'Colors', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', + _mode: 'code', + _value: false, + } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: 'backgroundStyle-url', + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-url', + propertyName: 'background.url', + jsSetting: false, + label: 'URL', + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'backgroundStyle-image', + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'imageUploader', + id: 'backgroundStyle-image', + propertyName: 'background.uploadFile', + label: 'Image', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-storedFile', + parentId: 'backgroundStylePnl', + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-storedFile', + jsSetting: false, + propertyName: 'background.storedFile.id', + label: 'File ID', + }, + ], + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-controls', + parentId: 'backgroundStyleRow', + inline: true, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'customDropdown', + id: 'backgroundStyleRow-size', + label: 'Size', + hideLabel: true, + propertyName: 'background.size', + customTooltip: + 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'customDropdown', + id: 'backgroundStyleRow-position', + label: 'Position', + hideLabel: true, + customTooltip: + 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: 'background.position', + dropdownOptions: positionOptions, + }, + ], + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-repeat', + parentId: 'backgroundStyleRow', + inputs: [ + { + type: 'radio', + id: 'backgroundStyleRow-repeat-radio', + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: 'shadowStyleCollapsiblePanel', + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: 'shadowStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'shadowStyleRow', + parentId: 'shadowStylePnl', + inline: true, + inputs: [ + { + type: 'numberField', + id: 'shadowStyleRow-offsetX', + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: 'offsetHorizontalIcon', + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: 'shadowStyleRow-offsetY', + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: 'shadowStyleRow-blurRadius', + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: 'shadowStyleRow-spreadRadius', + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: 'shadowStyleRow-color', + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlLayout', + label: 'Layout', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'pnlLayout', + readOnly: false, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'labelCol', + label: 'Label Col', + size: 'small', + jsSetting: true, + min: 0, + max: 24, + defaultValue: 8, + }, + { + type: 'numberField', + id: nanoid(), + propertyName: 'wrapperCol', + label: 'Wrapper Col', + size: 'small', + jsSetting: true, + min: 0, + max: 24, + defaultValue: 16, + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + readOnly: { + _code: 'return getSettingValue(data?.readOnly);', + _mode: 'code', + _value: false, + } as any, + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + parentId: styleRouterId, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: 'security', + title: 'Security', + id: securityId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityId, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/entityReference/utils.ts b/shesha-reactjs/src/designer-components/entityReference/utils.ts new file mode 100644 index 0000000000..9a2a121e50 --- /dev/null +++ b/shesha-reactjs/src/designer-components/entityReference/utils.ts @@ -0,0 +1,22 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + return { + background: { type: 'color', color: 'transparent' }, + font: { weight: '400', size: 14, type: 'Segoe UI' }, + border: { + border: { + all: { width: '1px', style: 'none', color: '#d9d9d9' }, + top: { width: '1px', style: 'solid', color: '#d9d9d9' }, + bottom: { width: '1px', style: 'solid', color: '#d9d9d9' }, + left: { width: '1px', style: 'solid', color: '#d9d9d9' }, + right: { width: '1px', style: 'solid', color: '#d9d9d9' }, + }, + radius: { all: 8, topLeft: 8, topRight: 8, bottomLeft: 8, bottomRight: 8 }, + borderType: 'all', + radiusType: 'all' + }, + dimensions: { width: '100%', height: '32px', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' }, + stylingBox: '{"paddingLeft":"0","paddingBottom":"0","paddingTop":"0","paddingRight":"0"}', + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/fileUpload/index.tsx b/shesha-reactjs/src/designer-components/fileUpload/index.tsx index 49c36b1325..c340c53f0c 100644 --- a/shesha-reactjs/src/designer-components/fileUpload/index.tsx +++ b/shesha-reactjs/src/designer-components/fileUpload/index.tsx @@ -1,21 +1,35 @@ import { FileAddOutlined } from '@ant-design/icons'; -import React from 'react'; +import React, { CSSProperties, useEffect, useMemo, useState } from 'react'; import { FileUpload } from '@/components'; import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import { IFormItem, IToolboxComponent } from '@/interfaces'; -import { StoredFileProvider, useFormData, useGlobalState, useSheshaApplication } from '@/providers'; +import { IStyleType, StoredFileProvider, useFormData, useGlobalState, useSheshaApplication } from '@/providers'; import { useForm } from '@/providers/form'; import { IConfigurableFormComponent } from '@/providers/form/models'; import { evaluateValue, + getStyle, + pickStyleFromModel, validateConfigurableComponentSettings, } from '@/providers/form/utils'; -import { getSettings } from './settings'; -import { migrateCustomFunctions, migratePropertyName, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; +import { + migrateCustomFunctions, + migratePropertyName, + migrateReadOnly, +} from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; +import { removeUndefinedProps } from '@/utils/object'; +import { getBorderStyle } from '../_settings/utils/border/utils'; +import { getFontStyle } from '../_settings/utils/font/utils'; +import { getShadowStyle } from '../_settings/utils/shadow/utils'; +import { getBackgroundStyle } from '../_settings/utils/background/utils'; +import { listType } from '../attachmentsEditor/attachmentsEditor'; +import { getDimensionsStyle } from '../_settings/utils/dimensions/utils'; -export interface IFileUploadProps extends IConfigurableFormComponent, Omit { +export interface IFileUploadProps extends IConfigurableFormComponent, Omit, IStyleType { ownerId: string; ownerType: string; allowUpload?: boolean; @@ -24,6 +38,11 @@ export interface IFileUploadProps extends IConfigurableFormComponent, Omit = { @@ -33,7 +52,73 @@ const FileUploadComponent: IToolboxComponent = { isInput: true, isOutput: true, Factory: ({ model }) => { - const { backendUrl } = useSheshaApplication(); + const { backendUrl, httpHeaders } = useSheshaApplication(); + + const dimensions = model?.dimensions; + const border = model?.border; + const font = model?.font; + const shadow = model?.shadow; + const background = model?.background; + const jsStyle = getStyle(model?.style, model); + + const dimensionsStyles = useMemo(() => getDimensionsStyle(dimensions), [dimensions]); + const borderStyles = useMemo(() => getBorderStyle(border, jsStyle), [border, jsStyle]); + const fontStyles = useMemo(() => getFontStyle(font), [font]); + const [backgroundStyles, setBackgroundStyles] = useState({}); + const shadowStyles = useMemo(() => getShadowStyle(shadow), [shadow]); + + useEffect(() => { + let objectUrl = ''; + + const fetchStyles = async () => { + let storedImageUrl = ''; + + try { + if (background?.storedFile?.id && background?.type === 'storedFile') { + const response = await fetch(`${backendUrl}/api/StoredFile/Download?id=${background?.storedFile?.id}`, { + headers: { ...httpHeaders, 'Content-Type': 'application/octet-stream' }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch background: ${response.status} ${response.statusText}`); + } + + const blob = await response.blob(); + objectUrl = URL.createObjectURL(blob); + storedImageUrl = objectUrl; + } + } catch (error) { + console.error('Error fetching background image:', error); + } + + const style = getBackgroundStyle(background, jsStyle, storedImageUrl); + setBackgroundStyles(style); + }; + + fetchStyles(); + + // Cleanup function to revoke the object URL when component unmounts or when dependencies change + return () => { + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + }; + }, [background, backendUrl, httpHeaders]); + + const styling = JSON.parse(model.stylingBox || '{}'); + const stylingBoxAsCSS = pickStyleFromModel(styling); + + const additionalStyles: CSSProperties = removeUndefinedProps({ + ...stylingBoxAsCSS, + ...dimensionsStyles, + ...borderStyles, + ...fontStyles, + ...backgroundStyles, + ...shadowStyles, + jsStyle, + }); + + const finalStyle = removeUndefinedProps(additionalStyles); // TODO: refactor and implement a generic way for values evaluation const { formSettings, formMode } = useForm(); @@ -54,18 +139,24 @@ const FileUploadComponent: IToolboxComponent = { baseUrl={backendUrl} ownerId={Boolean(ownerId) ? ownerId : Boolean(data?.id) ? data?.id : ''} ownerType={ - Boolean(model.ownerType) ? model.ownerType : Boolean(formSettings?.modelType) ? formSettings?.modelType : '' + Boolean(model.ownerType) + ? model.ownerType + : Boolean(formSettings?.modelType) + ? formSettings?.modelType + : '' } propertyName={model.propertyName} uploadMode={model.useSync ? 'sync' : 'async'} > ); @@ -73,41 +164,51 @@ const FileUploadComponent: IToolboxComponent = { ); }, - initModel: model => { + initModel: (model) => { const customModel: IFileUploadProps = { ...model, allowReplace: true, allowDelete: true, allowUpload: true, + hideFileName: true, ownerId: '', ownerType: '', isDragger: false, + editMode: 'inherited', }; return customModel; }, - migrator: (m) => m - .add(0, prev => { - return { + migrator: (m) => + m + .add(0, (prev) => { + return { + ...prev, + allowReplace: true, + allowDelete: true, + allowUpload: true, + hideFileName: true, + ownerId: prev['ownerId'], + ownerType: prev['ownerType'], + owner: prev['owner'], + } as IFileUploadProps; + }) + .add(1, (prev, context) => ({ ...prev, useSync: !Boolean(context.formSettings?.modelType) })) + .add(2, (prev) => { + const pn = prev['name'] ?? prev.propertyName; + const model = migratePropertyName(migrateCustomFunctions(prev)); + model.propertyName = pn; + return model; + }) + .add(3, (prev) => migrateVisibility(prev)) + .add(4, (prev) => migrateReadOnly(prev)) + .add(5, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) + .add(6, (prev) => ({ ...prev, - allowReplace: true, - allowDelete: true, - allowUpload: true, - ownerId: prev['ownerId'], - ownerType: prev['ownerType'], - owner: prev['owner'], - } as IFileUploadProps; - }) - .add(1, (prev, context) => ({ ...prev, useSync: !Boolean(context.formSettings?.modelType) })) - .add(2, (prev) => { - const pn = prev['name'] ?? prev.propertyName; - const model = migratePropertyName(migrateCustomFunctions(prev)); - model.propertyName = pn; - return model; - }) - .add(3, (prev) => migrateVisibility(prev)) - .add(4, (prev) => migrateReadOnly(prev)) - .add(5, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) - , + ...defaultStyles(), + desktop: { ...defaultStyles() }, + mobile: { ...defaultStyles() }, + tablet: { ...defaultStyles() }, + })), settingsFormMarkup: getSettings(), validateSettings: (model) => validateConfigurableComponentSettings(getSettings(), model), }; diff --git a/shesha-reactjs/src/designer-components/fileUpload/settings.ts b/shesha-reactjs/src/designer-components/fileUpload/settings.ts index f346b570cc..f0c75d3a91 100644 --- a/shesha-reactjs/src/designer-components/fileUpload/settings.ts +++ b/shesha-reactjs/src/designer-components/fileUpload/settings.ts @@ -13,7 +13,7 @@ export const getSettings = () => ghost: true, collapsible: 'header', content: { - id:'abc54bf6-f76d-4139-a850-c99bf06c8b69', + id: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', components: [...new DesignerToolbarSettings() .addContextPropertyAutocomplete({ id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', @@ -60,12 +60,13 @@ export const getSettings = () => propertyName: 'isDragger', label: 'Is dragger', parentId: 'root', - description: 'Whether the uploader should show a dragger instead of button', }) + description: 'Whether the uploader should show a dragger instead of button', + }) .addCheckbox({ id: 'cfd7d45e-c7e3-4a27-987b-dc525c412448', propertyName: 'hidden', parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Hidden', + label: 'hide', }) .addCheckbox({ id: 'c6885251-96a6-40ce-99b2-4b5209a9e01c', @@ -84,7 +85,7 @@ export const getSettings = () => propertyName: 'allowUpload', label: 'Allow Upload', parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - hidden: {_code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false} as any, + hidden: { _code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false } as any, validate: {}, }) .addCheckbox({ @@ -92,7 +93,7 @@ export const getSettings = () => propertyName: 'allowReplace', label: 'Allow Replace', parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - hidden: {_code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false} as any, + hidden: { _code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false } as any, validate: {}, }) .addCheckbox({ @@ -100,7 +101,7 @@ export const getSettings = () => propertyName: 'allowDelete', label: 'Allow Delete', parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - hidden: {_code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false} as any, + hidden: { _code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false } as any, validate: {}, }).toJson() ] @@ -117,7 +118,7 @@ export const getSettings = () => ghost: true, collapsible: 'header', content: { - id:'abc5bfe4-ee69-431e-931b-b0e0b9ceee6f', + id: 'abc5bfe4-ee69-431e-931b-b0e0b9ceee6f', components: [...new DesignerToolbarSettings() .addCheckbox({ id: 'abc5bfe4-ee69-431e-931b-b0e0b9ceee6f', @@ -139,7 +140,7 @@ export const getSettings = () => ghost: true, collapsible: 'header', content: { - id:'abc8b8f9-ec00-4d0a-9d2a-44a630cb2dcb', + id: 'abc8b8f9-ec00-4d0a-9d2a-44a630cb2dcb', components: [...new DesignerToolbarSettings() .addCheckbox({ id: 'af3d9a3f-f47e-48ae-b4c3-f5cc36e534d9', @@ -161,8 +162,10 @@ export const getSettings = () => labelAlign: 'right', dataSourceType: 'url', dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', - useRawValues: true, - }) + displayPropName: 'displayText', + keyPropName: 'value', + mode: 'single', + }) .addEditableTagGroupProps({ id: nanoid(), propertyName: 'allowedFileTypes', @@ -183,8 +186,8 @@ export const getSettings = () => ghost: true, collapsible: 'header', content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() + id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', + components: [...new DesignerToolbarSettings() .addPermissionAutocomplete({ id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', propertyName: 'permissions', @@ -195,6 +198,6 @@ export const getSettings = () => validate: {}, }).toJson() ] - } - }) - .toJson(); + } + }) + .toJson(); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/fileUpload/settingsForm.ts b/shesha-reactjs/src/designer-components/fileUpload/settingsForm.ts new file mode 100644 index 0000000000..e0397f6040 --- /dev/null +++ b/shesha-reactjs/src/designer-components/fileUpload/settingsForm.ts @@ -0,0 +1,809 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs } from '../_settings/utils/border/utils'; +import { getCornerInputs } from '../_settings/utils/border/utils'; +import { positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { FormLayout } from 'antd/es/form/Form'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = () => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const dataTabId = nanoid(); + const validationTabId = nanoid(); + + return { + components: new DesignerToolbarSettings() + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property name', + validate: { required: true }, + styledLabel: true, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + hideLabel: true, + label: 'Label', + hideLabelPropName: 'hideLabel', + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + inputType: 'textArea', + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'listType', + label: 'List Type', + type: 'dropdown', + defaultValue: 'text', + dropdownOptions: [ + { label: 'File name', value: 'text' }, + { label: 'Thumbnail', value: 'thumbnail' }, + ], + hidden: { + _code: 'return getSettingValue(data?.isDragger);', + _mode: 'code', + _value: false, + } as any, + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'isDragger', + label: 'Is Dragger', + type: 'switch', + description: 'Where the uploader should show a dragger instead of a button', + hidden: { + _code: 'return getSettingValue(data?.listType) === "thumbnail";', + _mode: 'code', + _value: false, + } as any, + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + type: 'editModeSelector', + defaultValue: 'inherited', + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'hideFileName', + label: 'Hide File Name', + type: 'switch', + jsSetting: true, + hidden: { + _code: 'return getSettingValue(data?.listType) !== "thumbnail";', + _mode: 'code', + _value: false, + } as any, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + type: 'switch', + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'allowUpload', + label: 'Allow Upload', + type: 'switch', + defaultValue: true, + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'allowReplace', + label: 'Allow Replace', + type: 'switch', + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'allowDelete', + label: 'Allow Delete', + type: 'switch', + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + { + key: '2', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'ownerId', + label: 'Owner ID', + type: 'textField', + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'ownerType', + label: 'Owner Type', + type: 'autocomplete', + dataSourceType: 'url', + dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', + useRawValues: true, + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'useSync', + label: 'Synchronous Upload', + type: 'switch', + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'allowedFileTypes', + label: 'Allowed File Types', + type: 'editableTagGroupProps', + description: 'File types that can be accepted.', + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + { + key: '3', + title: 'Validation', + id: validationTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'validate.required', + label: 'Required', + inputType: 'switch', + jsSetting: true, + }) + .toJson(), + ], + }, + { + key: '4', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: 'fontStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'fontStylePnl', + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: 'Controls text thickness (light, normal, bold, etc.)', + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + hidden: { + _code: 'return getSettingValue(data.listType) !== "thumbnail";', + _mode: 'code', + _value: false, + } as any, + dropdownOptions: textAlign, + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'primaryColor', + label: 'Primary Color', + inputType: 'colorPicker', + jsSetting: true, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: 'styleRouter', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + hidden: { + _code: 'return getSettingValue(data.listType) !== "thumbnail";', + _mode: 'code', + _value: false, + } as any, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Width', + width: 85, + propertyName: 'dimensions.width', + icon: 'widthIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minWidth', + icon: 'minWidthIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxWidth', + icon: 'maxWidthIcon', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Height', + width: 85, + propertyName: 'dimensions.height', + icon: 'heightIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minHeight', + icon: 'minHeightIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxHeight', + icon: 'maxHeightIcon', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + hidden: { + _code: 'return getSettingValue(data.listType) !== "thumbnail";', + _mode: 'code', + _value: false, + } as any, + content: { + id: 'borderStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { + _code: + 'return !getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.border?.hideBorder);', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'button', + id: nanoid(), + label: 'Border', + hideLabel: true, + propertyName: 'border.hideBorder', + icon: 'EyeOutlined', + iconAlt: 'EyeInvisibleOutlined', + }, + ], + }) + .addContainer({ + id: nanoid(), + parentId: 'borderStylePnl', + components: getBorderInputs() as any, + }) + .addContainer({ + id: nanoid(), + parentId: 'borderStylePnl', + components: getCornerInputs() as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + hidden: { + _code: 'return getSettingValue(data.listType) !== "thumbnail";', + _mode: 'code', + _value: false, + } as any, + content: { + id: 'backgroundStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: styleRouterId, + label: 'Type', + jsSetting: false, + propertyName: 'background.type', + inputType: 'radio', + tooltip: 'Select a type of background', + buttonGroupOptions: [ + { + value: 'color', + icon: 'FormatPainterOutlined', + title: 'Color', + }, + { + value: 'gradient', + icon: 'BgColorsOutlined', + title: 'Gradient', + }, + { + value: 'image', + icon: 'PictureOutlined', + title: 'Image', + }, + { + value: 'url', + icon: 'LinkOutlined', + title: 'URL', + }, + { + value: 'storedFile', + icon: 'DatabaseOutlined', + title: 'Stored File', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'colorPicker', + id: 'backgroundStyleRow-color-picker', + label: 'Color', + propertyName: 'background.color', + hideLabel: true, + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'multiColorPicker', + id: nanoid(), + propertyName: 'background.gradient.colors', + label: 'Colors', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', + _mode: 'code', + _value: false, + } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'background.url', + jsSetting: false, + label: 'URL', + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: 'Image', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: 'background.storedFile.id', + label: 'File ID', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + inline: true, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: 'Size', + hideLabel: true, + propertyName: 'background.size', + customTooltip: + 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: 'Position', + hideLabel: true, + customTooltip: + 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: 'background.position', + dropdownOptions: positionOptions, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: { + _code: 'return getSettingValue(data.listType) !== "thumbnail";', + _mode: 'code', + _value: false, + } as any, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + icon: 'offsetHorizontalIcon', + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: 'shadowStyleRow-color-picker', + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: '5', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/fileUpload/utils.ts b/shesha-reactjs/src/designer-components/fileUpload/utils.ts new file mode 100644 index 0000000000..5daf1ee5b3 --- /dev/null +++ b/shesha-reactjs/src/designer-components/fileUpload/utils.ts @@ -0,0 +1,46 @@ +import { IStyleType } from '@/index'; + +export const defaultStyles = (): IStyleType => { + return { + border: { + hideBorder: false, + radiusType: 'all', + borderType: 'all', + border: { all: { width: '1px', style: 'solid', color: '#d9d9d9' } }, + radius: { all: 8 }, + }, + dimensions: { + width: '54px', + height: '54px', + minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto', + }, + background: { + type: 'color', + color: '', + repeat: 'no-repeat', + size: 'cover', + position: 'center', + gradient: { direction: 'to right', colors: {} }, + url: '', + storedFile: { id: null }, + uploadFile: null, + }, + font: { + color: '#000', + type: 'Segoe UI', + align: 'left', + size: 14, + weight: '400', + }, + shadow: { + offsetX: 0, + offsetY: 0, + color: '#000', + blurRadius: 0, + spreadRadius: 0, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/htmlRender/index.tsx b/shesha-reactjs/src/designer-components/htmlRender/index.tsx index 5aee5a6b85..36d4b5a61c 100644 --- a/shesha-reactjs/src/designer-components/htmlRender/index.tsx +++ b/shesha-reactjs/src/designer-components/htmlRender/index.tsx @@ -1,5 +1,5 @@ import { IToolboxComponent } from '@/interfaces'; -import { executeScriptSync, useAvailableConstantsData, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { executeScriptSync, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { HighlightOutlined } from '@ant-design/icons'; import parse from 'html-react-parser'; import React from 'react'; @@ -7,22 +7,29 @@ import { IHtmlComponentProps } from './interfaces'; import { getSettings } from './settingsForm'; import { ConfigurableFormItem } from '@/components'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { addContextData } from '@/index'; -const HtmlComponent: IToolboxComponent = { +interface IHtmlComponentCalulatedModel { + getContent: (value: any) => string; +} + +const HtmlComponent: IToolboxComponent = { type: 'htmlRender', name: 'HTML Render', icon: , isInput: false, isOutput: true, - Factory: ({ model }) => { - const ctx = useAvailableConstantsData(); - return - {(value) => { - return parse(model?.renderer - ? executeScriptSync(model?.renderer, { ...ctx, value }) || '
' - : '
'); - } - }; + calculateModel: (model, allData) => ({ + getContent: (value: any) => model.renderer + ? executeScriptSync(model.renderer, addContextData(allData, {value})) || '
' + : '
' + }), + Factory: ({ model, calculatedModel }) => { + return
+ + {value => parse(calculatedModel.getContent(value))} + +
; }, settingsFormMarkup: (data) => getSettings(data), validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), diff --git a/shesha-reactjs/src/designer-components/htmlRender/settingsForm.ts b/shesha-reactjs/src/designer-components/htmlRender/settingsForm.ts index 37e7c9506c..74883996b0 100644 --- a/shesha-reactjs/src/designer-components/htmlRender/settingsForm.ts +++ b/shesha-reactjs/src/designer-components/htmlRender/settingsForm.ts @@ -1,112 +1,156 @@ import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; import { nanoid } from '@/utils/uuid'; -export const getSettings = (data: any) => - new DesignerToolbarSettings(data) - .addCollapsiblePanel({ - id: nanoid(), - propertyName: 'pnlDisplay', - parentId: 'root', - label: 'Display', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: '87667bd9-0ba6-4f29-a7d3-aecdac17da2a', - components: [ - ...new DesignerToolbarSettings() - .addContextPropertyAutocomplete({ - id: nanoid(), - propertyName: 'propertyName', - parentId: '87667bd9-0ba6-4f29-a7d3-aecdac17da2a', - label: 'Property name', - validate: { - required: true, - }, - }) - .addCodeEditor({ - id: nanoid(), - propertyName: 'renderer', - parentId: '87667bd9-0ba6-4f29-a7d3-aecdac17da2a', - label: 'Render HTML', - description: 'Enter custom JSX script that will render a component', - exposedVariables: [ - { +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ id: nanoid(), - name: 'data', - description: 'Form data', - type: 'object', - }, - { + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addSettingsInputRow({ id: nanoid(), - name: 'globalState', - description: 'The global state', - type: 'object', - }, - ], - wrapInTemplate: true, - templateSettings: { - "functionName": "renderer" - }, - availableConstantsExpression: async ({ metadataBuilder, data }) => { - const { modelType } = data ?? {}; - const result = metadataBuilder.object("constants"); - if (modelType) { - await result.addEntityAsync("data", "Form data", modelType); - await result.addEntityAsync("initialValues", "Initial values", modelType); - } else { - result.addObject("data", "Form data"); - result.addObject("initialValues", "Initial values"); - }; - - result.addObject("parentFormValues", "Parent form values. The values of the form rendering the dialog."); + parentId: commonTabId, + inputs: [{ + id: nanoid(), + type: 'codeEditor', + propertyName: 'renderer', + parentId: commonTabId, + label: 'Render HTML', + description: 'Enter custom JSX script that will render a component', + exposedVariables: [ + { + id: nanoid(), + name: 'data', + description: 'Form data', + type: 'object', + }.toString(), + { + id: nanoid(), + name: 'globalState', + description: 'The global state', + type: 'object', + }.toString(), + ], + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + } + ], + }) + .toJson(), + ], + }, - result.addStandard([ - "shesha:form", - "shesha:globalState", - "shesha:setGlobalState", - "shesha:http", - "shesha:message", - "shesha:pageContext", - "shesha:contexts", - "shesha:moment", - ]); - return result.build(); - }, - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'hidden', - parentId: '87667bd9-0ba6-4f29-a7d3-aecdac17da2a', - label: 'Hidden', - }) - .toJson(), + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson() + ] + }) + .toJson() + ] + }, + { + key: '4', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, ], - }, - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] - } - }) - .toJson(); + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/iconPicker/iconPickerWrapper.tsx b/shesha-reactjs/src/designer-components/iconPicker/iconPickerWrapper.tsx index fdf2ea6cde..2c5adb72c5 100644 --- a/shesha-reactjs/src/designer-components/iconPicker/iconPickerWrapper.tsx +++ b/shesha-reactjs/src/designer-components/iconPicker/iconPickerWrapper.tsx @@ -1,73 +1,96 @@ import IconPicker, { ShaIconTypes } from '@/components/iconPicker'; -import React, { - CSSProperties, - FC, - ReactNode -} from 'react'; -import { IApplicationContext, pickStyleFromModel } from '@/providers/form/utils'; -import { executeFunction, useFormData, useGlobalState } from '@/index'; +import React, { CSSProperties, FC, ReactNode, useState , useRef, useEffect} from 'react'; +import { IApplicationContext} from '@/providers/form/utils'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; +import { IDimensionsValue } from '../_settings/utils/dimensions/interfaces'; +import { Tooltip } from 'antd'; interface IconPickerWrapperProps { - disabled?: boolean; // todo: move to the model level - applicationContext: IApplicationContext; - value: any; - onChange: (...args: any[]) => void; - readOnly?: boolean; - fontSize?: number; - color?: string; - customColor?: string; - borderWidth?: number; - borderColor?: string; - borderRadius?: number; - backgroundColor?: string; - stylingBox?: string; - defaultValue?: ShaIconTypes; - textAlign?: string; + disabled?: boolean; // todo: move to the model level + applicationContext: IApplicationContext; + value: any; + onChange: (...args: any[]) => void; + readOnly?: boolean; + fontSize?: number; + iconSize?: number; + selectBtnSize?: SizeType; + color?: string; + customColor?: string; + borderWidth?: number; + borderColor?: string; + borderRadius?: number; + backgroundColor?: string; + stylingBox?: string; + defaultValue?: ShaIconTypes; + textAlign?: string; + style?: string; + dimensions?: IDimensionsValue; + description?: string; + fullStyles?: CSSProperties; } export const IconPickerWrapper: FC = (props) => { - const { fontSize, color, readOnly, onChange, borderColor, borderRadius, borderWidth, backgroundColor, stylingBox, defaultValue, value, textAlign } = props; - const { data } = useFormData(); - const { globalState } = useGlobalState(); + const { + color, + readOnly, + onChange, + defaultValue, + textAlign, + selectBtnSize, + fullStyles, + iconSize, + value + } = props; - const onIconChange = (_icon: ReactNode, iconName: ShaIconTypes) => { - if (onChange) onChange(iconName); - }; + const [finalValue, setFinalValue] = useState(null); + const hasSaved = useRef(false); - const stylingBoxJSON = JSON.parse(stylingBox || '{}'); - const style: CSSProperties = { - fontSize: fontSize || 24, - color: color, - marginLeft: (defaultValue) ? '12px' : 'none' //this allows us to correct the icon layout when an icon is selected - }; + const onIconChange = (_icon: ReactNode, iconName: ShaIconTypes) => { + if (onChange) onChange(iconName); + }; - const getIconStyle = { - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - width: Number(fontSize) ? `${fontSize}px` : '25px', - height: Number(fontSize) ? `${fontSize}px` : '25px', - border: `${borderWidth}px solid ${borderColor}`, - borderRadius: `${borderRadius}px`, - backgroundColor: backgroundColor, - ...pickStyleFromModel(stylingBoxJSON), - ...(executeFunction("{}", { data, globalState }) || {}) - }; + const fontSize = parseFloat(String(fullStyles?.fontSize).replace('px', '')); - return ( -
-
- -
+ const style: CSSProperties = { + fontSize: fullStyles?.fontSize || 24, + color: fullStyles?.color, + }; + + + useEffect(() => { + if (value && !hasSaved.current) { + setFinalValue(value); + hasSaved.current = true; + } + }, [value]); + + const iconValue = finalValue ?? defaultValue; + + return ( +
+ +
+
- ); -}; \ No newline at end of file +
+
+ ); +}; diff --git a/shesha-reactjs/src/designer-components/iconPicker/index.tsx b/shesha-reactjs/src/designer-components/iconPicker/index.tsx index de26c59757..fe52b3e6bc 100644 --- a/shesha-reactjs/src/designer-components/iconPicker/index.tsx +++ b/shesha-reactjs/src/designer-components/iconPicker/index.tsx @@ -1,7 +1,6 @@ import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import React from 'react'; import { HeartOutlined } from '@ant-design/icons'; -import { iconPickerFormSettings } from './settings'; import { IconPickerWrapper } from './iconPickerWrapper'; import { IIconPickerComponentProps } from './interfaces'; import { IToolboxComponent } from '@/interfaces'; @@ -11,6 +10,9 @@ import { useAvailableConstantsData } from '@/providers/form/utils'; import { validateConfigurableComponentSettings } from '@/providers/form/utils'; import { legacyColor2Hex } from '@/designer-components/_common-migrations/migrateColor'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultStyles } from './utils'; const IconPickerComponent: IToolboxComponent = { type: 'iconPicker', @@ -20,23 +22,35 @@ const IconPickerComponent: IToolboxComponent = { isOutput: true, canBeJsSetting: true, Factory: ({ model }) => { - const allData = useAvailableConstantsData(); return ( - {(value, onChange) => ()} + {(value, onChange) => ( + + )} ); }, - settingsFormMarkup: iconPickerFormSettings, - validateSettings: model => validateConfigurableComponentSettings(iconPickerFormSettings, model), - migrator: (m) => m - .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(1, (prev) => migrateVisibility(prev)) - .add(2, (prev) => ({ ...prev, color: legacyColor2Hex(prev.color) })) - .add(3, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) - , + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + migrator: (m) => + m + .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(1, (prev) => migrateVisibility(prev)) + .add(2, (prev) => ({ ...prev, color: legacyColor2Hex(prev.color) })) + .add(3, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) + .add(4, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })) + .add(5, (prev) => { + prev.hideLabel = true; + return prev; + }), }; export default IconPickerComponent; diff --git a/shesha-reactjs/src/designer-components/iconPicker/settings.ts b/shesha-reactjs/src/designer-components/iconPicker/settings.ts index 94b0547419..5d1fb16005 100644 --- a/shesha-reactjs/src/designer-components/iconPicker/settings.ts +++ b/shesha-reactjs/src/designer-components/iconPicker/settings.ts @@ -2,88 +2,88 @@ import { nanoid } from '@/utils/uuid'; import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; export const iconPickerFormSettings = new DesignerToolbarSettings() -.addCollapsiblePanel({ - id: '11164664-cbc9-4cef-babc-6fbea44cd0ca', - propertyName: 'pnlDisplay', - parentId: 'root', - label: 'Display', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - components: [ - ...new DesignerToolbarSettings() - .addContextPropertyAutocomplete({ - id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', - propertyName: 'propertyName', + .addCollapsiblePanel({ + id: '11164664-cbc9-4cef-babc-6fbea44cd0ca', + propertyName: 'pnlDisplay', parentId: 'root', - label: 'Property name', - validate: { - required: true, + label: 'Display', + labelAlign: 'left', + expandIconPosition: 'start', + ghost: true, + collapsible: 'header', + content: { + id: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', + propertyName: 'propertyName', + parentId: 'root', + label: 'Property name', + validate: { + required: true, + }, + }) + .addTextField({ + id: '46d07439-4c18-468c-89e1-60c002ce96c5', + propertyName: 'label', + parentId: 'root', + label: 'Label', + }) + .addDropdown({ + id: '57a40a33-7e08-4ce4-9f08-a34d24a83338', + propertyName: 'labelAlign', + parentId: 'root', + label: 'Label align', + values: [ + { + label: 'left', + value: 'left', + id: 'f01e54aa-a1a4-4bd6-ba73-c395e48af8ce', + }, + { + label: 'right', + value: 'right', + id: 'b920ef96-ae27-4a01-bfad-b5b7d07218da', + }, + ], + dataSourceType: 'values', + }) + .addDropdown({ + id: nanoid(), + propertyName: 'textAlign', + parentId: 'root', + label: 'Icon Align', + allowClear: false, + defaultValue: 'start', + values: [ + { + label: 'Left', + value: 'start', + id: '32ebcc5b-6775-4b34-b856-d7ed42f33c3b', + }, + { + label: 'Center', + value: 'center', + id: 'f3622f5e-3dc3-452b-aa57-2273f65b9fdc', + }, + { + label: 'Right', + value: 'end', + id: '3e6a5ac8-bf51-48fb-b5c1-33ba455a1246', + }, + ], + dataSourceType: 'values', + }) + .addTextArea({ + id: '2d32fe70-99a0-4825-ae6c-8b933004e119', + propertyName: 'description', + parentId: 'root', + label: 'Description', + }).toJson() + ], }, }) - .addTextField({ - id: '46d07439-4c18-468c-89e1-60c002ce96c5', - propertyName: 'label', - parentId: 'root', - label: 'Label', - }) - .addDropdown({ - id: '57a40a33-7e08-4ce4-9f08-a34d24a83338', - propertyName: 'labelAlign', - parentId: 'root', - label: 'Label align', - values: [ - { - label: 'left', - value: 'left', - id: 'f01e54aa-a1a4-4bd6-ba73-c395e48af8ce', - }, - { - label: 'right', - value: 'right', - id: 'b920ef96-ae27-4a01-bfad-b5b7d07218da', - }, - ], - dataSourceType: 'values', - }) - .addDropdown({ - id: nanoid(), - propertyName: 'textAlign', - parentId: 'root', - label: 'Icon Align', - allowClear: false, - defaultValue: 'start', - values: [ - { - label: 'Left', - value: 'start', - id: '32ebcc5b-6775-4b34-b856-d7ed42f33c3b', - }, - { - label: 'Center', - value: 'center', - id: 'f3622f5e-3dc3-452b-aa57-2273f65b9fdc', - }, - { - label: 'Right', - value: 'end', - id: '3e6a5ac8-bf51-48fb-b5c1-33ba455a1246', - }, - ], - dataSourceType: 'values', - }) - .addTextArea({ - id: '2d32fe70-99a0-4825-ae6c-8b933004e119', - propertyName: 'description', - parentId: 'root', - label: 'Description', - }).toJson() -], -}, -}) .addCollapsiblePanel({ id: '11164664-cbc9-4cef-babc-6fbea55cd0ca', propertyName: 'separatorColor', @@ -97,121 +97,121 @@ export const iconPickerFormSettings = new DesignerToolbarSettings() id: 'pnl64664-cbc9-4cdf-babc-6fbea44cd0ca', components: [ ...new DesignerToolbarSettings() - .addCheckbox({ - id: nanoid(), - propertyName: 'hidden', - parentId: 'root', - label: 'Hidden', + .addCheckbox({ + id: nanoid(), + propertyName: 'hidden', + parentId: 'root', + label: 'hide', + }) + .addCheckbox({ + id: 'c6885251-96a6-40ce-99b2-4b5209a9e01c', + propertyName: 'hideLabel', + parentId: 'root', + label: 'Hide Label', + }) + .addEditMode({ + id: '24a8be15-98eb-40f7-99ea-ebb602693e9c', + propertyName: 'editMode', + parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', + label: "Edit mode", + }) + .toJson(), + ], + }, }) - .addCheckbox({ - id: 'c6885251-96a6-40ce-99b2-4b5209a9e01c', - propertyName: 'hideLabel', + .addCollapsiblePanel({ + id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5091', + propertyName: 'pnlStyling', parentId: 'root', - label: 'Hide Label', + label: 'Icon Styling', + labelAlign: "left", + expandIconPosition: "start", + ghost: true, + collapsible: 'header', + content: { + id: 'pnl24bf6-f76d-4139-a850-cbf06c8b71', + components: [...new DesignerToolbarSettings() + .addIconPicker({ + id: nanoid(), + propertyName: 'defaultValue', + label: 'Default Icon', + labelAlign: 'left', + jsSetting: true + }) + .addColorPicker({ + id: nanoid(), + propertyName: 'color', + label: 'Color', + title: 'Choose Icon color', + allowClear: true, + showText: true, + }) + .addNumberField({ + id: nanoid(), + propertyName: 'borderWidth', + label: 'Border Width', + }) + .addColorPicker({ + id: nanoid(), + propertyName: 'borderColor', + label: 'Border Color', + title: 'Choose Icon Border color', + allowClear: true, + showText: true, + }) + .addNumberField({ + id: nanoid(), + propertyName: 'borderRadius', + label: 'Border Radius', + }) + .addNumberField({ + id: nanoid(), + propertyName: 'fontSize', + label: 'Size', + defaultValue: 24 + }) + .addColorPicker({ + id: nanoid(), + propertyName: 'backgroundColor', + label: 'Background Color', + title: 'Choose Icon background color', + allowClear: true, + showText: true, + }).addStyleBox({ + id: nanoid(), + propertyName: 'stylingBox', + componentName: 'Styling Box', + parentId: 'root', + validate: {}, + settingsValidationErrors: [], + jsSetting: false, + }).toJson() + ] + } }) - .addEditMode({ - id: '24a8be15-98eb-40f7-99ea-ebb602693e9c', - propertyName: 'editMode', - parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - label: "Edit mode", + .addCollapsiblePanel({ + id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', + propertyName: 'pnlSecurity', + parentId: 'root', + label: 'Security', + labelAlign: "left", + expandIconPosition: "start", + ghost: true, + collapsible: 'header', + content: { + id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', + components: [...new DesignerToolbarSettings() + .addPermissionAutocomplete({ + id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', + propertyName: 'permissions', + label: 'Permissions', + labelAlign: 'right', + parentId: 'root', + hidden: false, + validate: {}, + }).toJson() + ] + } }) - .toJson(), -], -}, -}) -.addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5091', - propertyName: 'pnlStyling', - parentId: 'root', - label: 'Icon Styling', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id:'pnl24bf6-f76d-4139-a850-cbf06c8b71', - components: [...new DesignerToolbarSettings() - .addIconPicker({ - id: nanoid(), - propertyName: 'defaultValue', - label: 'Default Icon', - labelAlign: 'left', - jsSetting: true - }) - .addColorPicker({ - id: nanoid(), - propertyName: 'color', - label: 'Color', - title: 'Choose Icon color', - allowClear: true, - showText: true, - }) - .addNumberField({ - id: nanoid(), - propertyName: 'borderWidth', - label: 'Border Width', - }) - .addColorPicker({ - id: nanoid(), - propertyName: 'borderColor', - label: 'Border Color', - title: 'Choose Icon Border color', - allowClear: true, - showText: true, - }) - .addNumberField({ - id: nanoid(), - propertyName: 'borderRadius', - label: 'Border Radius', - }) - .addNumberField({ - id: nanoid(), - propertyName: 'fontSize', - label: 'Size', - defaultValue: 24 - }) - .addColorPicker({ - id: nanoid(), - propertyName: 'backgroundColor', - label: 'Background Color', - title: 'Choose Icon background color', - allowClear: true, - showText: true, - }).addStyleBox({ - id: nanoid(), - propertyName: 'stylingBox', - componentName: 'Styling Box', - parentId: 'root', - validate: {}, - settingsValidationErrors: [], - jsSetting: false, - }).toJson() - ] -} -}) -.addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] -} -}) -.toJson(); + .toJson(); diff --git a/shesha-reactjs/src/designer-components/iconPicker/settingsForm.ts b/shesha-reactjs/src/designer-components/iconPicker/settingsForm.ts new file mode 100644 index 0000000000..2fbd2a92fe --- /dev/null +++ b/shesha-reactjs/src/designer-components/iconPicker/settingsForm.ts @@ -0,0 +1,243 @@ +import { nanoid } from '@/utils/uuid'; +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { textAlign } from '../_settings/utils/font/utils'; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const securityTabId = nanoid(); + const appearanceTabId = nanoid(); + const styleRouterId = nanoid(); + const fontStylePnlId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: 's4gmBg31azZC0UjZjpfTm', + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: 's4gmBg31azZC0UjZjpfTm', + hideLabel: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'textArea', + propertyName: 'description', + parentId: commonTabId, + label: 'Tooltip', + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + type: 'iconPicker', + propertyName: 'defaultValue', + label: 'Default Icon', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + type: 'editModeSelector', + propertyName: 'editMode', + label: 'Edit Mode', + parentId: commonTabId, + jsSetting: true, + defaultValue: 'inherited', + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + { + key: '3', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Icon Style', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: fontStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: fontStylePnlId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + parentId: securityTabId, + jsSetting: true, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/iconPicker/utils.ts b/shesha-reactjs/src/designer-components/iconPicker/utils.ts new file mode 100644 index 0000000000..f47adeb96e --- /dev/null +++ b/shesha-reactjs/src/designer-components/iconPicker/utils.ts @@ -0,0 +1,34 @@ +import { IStyleType } from '@/index'; + +export const defaultStyles = (): IStyleType => { + return { + background: { type: 'color', color: '#fff' }, + font: { + weight: '400', + size: 24, + color: '#000', + type: 'Segoe UI', + }, + border: { + border: { + all: { + width: 0, + style: 'solid', + color: '#ffffff', + }, + }, + radius: { all: 8 }, + borderType: 'all', + radiusType: 'all', + }, + dimensions: { + width: 'auto', + height: 'auto', + minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto', + + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/image/image.tsx b/shesha-reactjs/src/designer-components/image/image.tsx index 35e7feae4f..ca7f376754 100644 --- a/shesha-reactjs/src/designer-components/image/image.tsx +++ b/shesha-reactjs/src/designer-components/image/image.tsx @@ -6,8 +6,6 @@ import { DeleteOutlined, UploadOutlined } from '@ant-design/icons'; export type ImageSourceType = 'url' | 'storedFile' | 'base64'; export interface IImageFieldProps { - height?: number | string; - width?: number | string; value?: string; onChange?: (newValue: string) => void; readOnly: boolean; @@ -19,7 +17,7 @@ export interface IImageFieldProps { } export const ImageField: FC = (props) => { - const { height, width, imageSource, value, allowPreview = false, styles, onChange } = props; + const { imageSource, value, allowPreview = false, styles, onChange } = props; const readOnly = props?.readOnly || props.imageSource === 'url'; @@ -96,8 +94,8 @@ export const ImageField: FC = (props) => { {props?.alt} diff --git a/shesha-reactjs/src/designer-components/image/index.tsx b/shesha-reactjs/src/designer-components/image/index.tsx index 04a124d057..647b8ce571 100644 --- a/shesha-reactjs/src/designer-components/image/index.tsx +++ b/shesha-reactjs/src/designer-components/image/index.tsx @@ -3,27 +3,30 @@ import { FormMarkup, IConfigurableFormComponent } from '@/providers/form/models' import { FileImageOutlined } from '@ant-design/icons'; import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import settingsFormJson from './settingsForm.json'; -import { evaluateValue, getStyle, pickStyleFromModel, validateConfigurableComponentSettings } from '@/providers/form/utils'; -import React, { CSSProperties } from 'react'; +import { evaluateValue, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import React from 'react'; import { migrateCustomFunctions, migratePropertyName, } from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; -import { StoredFileProvider, useForm, useFormData, useGlobalState, useSheshaApplication } from '@/providers'; +import { IStyleType, StoredFileProvider } from '@/providers'; import { ImageField, ImageSourceType } from './image'; import ConditionalWrap from '@/components/conditionalWrapper'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; import { removeUndefinedProps } from '@/utils/object'; +import { getSettings } from './settingsForm'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultStyles } from './utils'; import { useTheme } from 'antd-style'; - export interface IImageStyleProps { height?: number | string; width?: number | string; objectFit?: 'fill' | 'contain' | 'cover' | 'scale-down' | 'none'; objectPosition?: string; filter?: string; + filterIntensity?: number; borderSize?: number; borderRadius?: number; borderType?: string; @@ -32,7 +35,7 @@ export interface IImageStyleProps { opacity?: number; style?: string; } -export interface IImageProps extends IConfigurableFormComponent, IFormItem, IImageStyleProps { +export interface IImageProps extends IConfigurableFormComponent, IFormItem, IImageStyleProps, IStyleType { url?: string; storedFileId?: string; base64?: string; @@ -43,6 +46,15 @@ export interface IImageProps extends IConfigurableFormComponent, IFormItem, IIma allowPreview?: boolean; allowedFileTypes?: string[]; alt?: string; + opacity?: number; + sepia?: number; + grayscale?: number; + blur?: number; + brightness?: number; + contrast?: number; + saturate?: number; + hueRotate?: number; + invert?: number; } const settingsForm = settingsFormJson as FormMarkup; @@ -53,36 +65,36 @@ const ImageComponent: IToolboxComponent = { icon: , isInput: true, isOutput: true, - Factory: ({ model }) => { - const { data } = useFormData(); - const { formSettings } = useForm(); - const { globalState } = useGlobalState(); - const { backendUrl } = useSheshaApplication(); - const ownerId = evaluateValue(model.ownerId, { data, globalState }); + calculateModel: (model, allData) => ({ + ownerId: evaluateValue(model.ownerId, allData), + dataId: allData.data?.Id, + formModelType: allData.form.formSettings?.modelType, + }), + Factory: ({ model, calculatedModel }) => { const theme = useTheme(); - const styling = JSON.parse(model.stylingBox || '{}'); - const stylingBoxAsCSS = pickStyleFromModel(styling); - - const additionalStyles: CSSProperties = removeUndefinedProps({ + const finalStyles = removeUndefinedProps({ + ...model.allStyles.jsStyle, objectFit: model.objectFit, objectPosition: model.objectPosition, - filter: model.filter, + filter: `${model.filter ? `${model.filter}(${model.filterIntensity || 0}${model.filter === 'blur' ? 'px' : model.filter === 'hue-rotate' ? 'deg' : '%'})` : 'none'}`, borderWidth: model.borderSize || 0, borderRadius: model.borderRadius, borderStyle: model.borderType || 'solid', borderColor: model.borderColor || theme.colorBorder, opacity: model.opacity, - ...stylingBoxAsCSS + ...model.allStyles.dimensionsStyles, + ...model.allStyles.borderStyles, + ...model.allStyles.shadowStyles, + ...model.allStyles.stylingBoxAsCSS }); - const jsStyle = getStyle(model.style, data); - const finalStyle = removeUndefinedProps({ ...jsStyle, ...additionalStyles }); + return ( {(value, onChange) => { const uploadedFileUrl = model.base64 || value; - const readonly = model?.readOnly || model.dataSource === 'base64' && Boolean(model.base64); + const readonly = model.readOnly || model.dataSource === 'base64' && Boolean(model.base64); const val = model.dataSource === 'storedFile' ? model.storedFileId || value?.id || value @@ -96,14 +108,13 @@ const ImageComponent: IToolboxComponent = { value={val} onChange={onChange} fileId={val} - baseUrl={backendUrl} - ownerId={Boolean(ownerId) ? ownerId : Boolean(data?.id) ? data?.id : ''} + ownerId={Boolean(calculatedModel.ownerId) ? calculatedModel.ownerId : Boolean(calculatedModel.dataId) ? calculatedModel.dataId : ''} ownerType={ - Boolean(model.ownerType) ? model.ownerType : Boolean(formSettings?.modelType) ? formSettings?.modelType : '' + Boolean(model.ownerType) ? model.ownerType : Boolean(calculatedModel.formModelType) ? calculatedModel.formModelType : '' } fileCategory={model.fileCategory} propertyName={!model.context ? model.propertyName : null} - //uploadMode={model.useSync ? 'sync' : 'async'} + //uploadMode={model.useSync ? 'sync' : 'async'} > {child} @@ -116,16 +127,14 @@ const ImageComponent: IToolboxComponent = { wrap={fileProvider} > ); @@ -135,7 +144,7 @@ const ImageComponent: IToolboxComponent = { }, initModel: (model) => { const customModel: IImageProps = { - ...model, + ...model }; return customModel; }, @@ -145,8 +154,11 @@ const ImageComponent: IToolboxComponent = { .add(2, (prev) => { return { ...prev, + hideLabel: true, + editMode: 'inherited', url: Boolean(prev.url) ? { _mode: 'code', _code: prev.url } : null, storedFileId: Boolean(prev.storedFileId) ? { _mode: 'code', _code: prev.storedFileId } : null, + border: { radiusType: 'all', borderType: 'all', border: { all: { width: 0, style: 'solid', color: '#d9d9d9' } } }, } as any; }) .add(3, (prev) => ({ ...migrateFormApi.properties(prev) })) @@ -164,8 +176,8 @@ const ImageComponent: IToolboxComponent = { return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; }) - , - settingsFormMarkup: settingsForm, + .add(6, (prev) => ({ ...migratePrevStyles(prev, defaultStyles(prev)) })), + settingsFormMarkup: (data) => getSettings(data), validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), }; diff --git a/shesha-reactjs/src/designer-components/image/settingsForm.ts b/shesha-reactjs/src/designer-components/image/settingsForm.ts new file mode 100644 index 0000000000..0fef2c4b88 --- /dev/null +++ b/shesha-reactjs/src/designer-components/image/settingsForm.ts @@ -0,0 +1,620 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { positionOptions } from '../_settings/utils/background/utils'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const validationTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const dimensionsStylePnlId = nanoid(); + const sizePositionPnlId = nanoid(); + const borderStylePnlId = nanoid(); + const shadowStylePnlId = nanoid(); + + return { + + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + size: 'small', + styledLabel: true, + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + propertyName: 'alt', + label: 'Alt Text', + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'alt', + label: 'Alt Text', + jsSetting: true, + }, + { + id: nanoid(), + type: 'textArea', + propertyName: 'description', + label: 'Tooltip', + parentId: commonTabId, + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + propertyName: 'allowPreview', + label: 'Allow Preview', + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'allowPreview', + label: 'Allow Preview', + jsSetting: true, + }, + { + id: nanoid(), + propertyName: 'allowedFileTypes', + label: 'Allowed File Types', + type: 'editableTagGroupProps', + parentId: commonTabId, + tooltip: 'Enter the file types that are allowed to be uploaded e.g .jpg, .png, .gif', + } + ] + }) + .addSettingsInput({ + id: nanoid(), + parentId: commonTabId, + label: "Image Source Type", + jsSetting: true, + propertyName: "dataSource", + inputType: "radio", + buttonGroupOptions: [ + { + title: "StoredFile", + icon: "DatabaseOutlined", + value: "storedFile", + }, + { + title: "Url", + icon: "LinkOutlined", + value: "url", + }, + { + title: "Base64", + icon: "PictureOutlined", + value: "base64", + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "url", + jsSetting: false, + label: "URL", + }], + hidden: { + _code: "return getSettingValue(data?.dataSource) !== 'url';", + _mode: "code", + _value: false + }, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [{ + id: nanoid(), + type: "imageUploader", + parentId: commonTabId, + label: "Upload Image", + propertyName: "base64", + }], + hidden: { + _code: "return getSettingValue(data?.dataSource) !== 'base64';", + _mode: "code", + _value: false + }, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: "return getSettingValue(data?.dataSource) !== 'storedFile';", + _mode: "code", + _value: false + }, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "storedFileId", + label: "File ID" + } + ] + }) + .toJson() + ] + }, + { + key: '2', + title: 'Validation', + id: validationTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'validate.required', + label: 'Required', + inputType: 'switch', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: validationTabId + }) + .toJson() + ] + }, + { + key: '3', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: dimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlsize-position-', + label: 'Picture Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: sizePositionPnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: sizePositionPnlId, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: "Object Fit", + propertyName: "objectFit", + dropdownOptions: [ + { + value: "cover", + label: "Cover" + }, + { + value: "contain", + label: "Contain" + }, + { + value: "fill", + label: "Fill" + }, + { + value: "auto", + label: "Auto" + } + ], + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Object Position", + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "objectPosition", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: sizePositionPnlId, + inputs: [ + { + id: nanoid(), + parentId: sizePositionPnlId, + type: 'dropdown', + label: 'Filter', + propertyName: 'filter', + dropdownOptions: [ + { + value: 'none', + label: 'None' + }, + { + value: 'grayscale', + label: 'Grayscale' + }, + { + value: 'sepia', + label: 'Sepia' + }, + { + value: 'blur', + label: 'Blur' + }, + { + value: 'brightness', + label: 'Brightness' + }, + { + value: 'contrast', + label: 'Contrast' + }, + { + value: 'hue-rotate', + label: 'Hue Rotate' + }, + { + value: 'invert', + label: 'Invert' + }, + { + value: 'saturate', + label: 'Saturate' + } + ] + }, + { + id: nanoid(), + label: 'Filter Intensity', + propertyName: 'filterIntensity', + type: 'numberField', + } + ] + }) + .addSettingsInput({ + parentId: sizePositionPnlId, + id: nanoid(), + label: 'Opacity', + propertyName: 'opacity', + inputType: 'numberField', + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: borderStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: borderStylePnlId, + hidden: { _code: 'return !getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.border?.hideBorder);', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'button', + id: nanoid(), + label: "Border", + hideLabel: true, + propertyName: "border.hideBorder", + icon: "EyeOutlined", + iconAlt: "EyeInvisibleOutlined" + }, + ] + }) + .addContainer({ + id: nanoid(), + parentId: borderStylePnlId, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: borderStylePnlId, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: shadowStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: shadowStylePnlId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson()] + }).toJson()] + }, + { + key: '5', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/image/utils.ts b/shesha-reactjs/src/designer-components/image/utils.ts new file mode 100644 index 0000000000..cfb6f592ae --- /dev/null +++ b/shesha-reactjs/src/designer-components/image/utils.ts @@ -0,0 +1,14 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (prev): IStyleType => { + const { borderWidth, borderColor, borderRadius, borderType } = prev; + return { + border: { + radiusType: 'all', borderType: 'all', + border: { + all: { width: borderWidth || '1px', style: borderType || 'none', color: borderColor }, + }, + radius: { all: borderRadius } + }, + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/imagePicker/index.tsx b/shesha-reactjs/src/designer-components/imagePicker/index.tsx index 2e4519f439..9e6e7f4162 100644 --- a/shesha-reactjs/src/designer-components/imagePicker/index.tsx +++ b/shesha-reactjs/src/designer-components/imagePicker/index.tsx @@ -15,8 +15,8 @@ interface IImageUploaderProps { readOnly: boolean; } -const ImagePicker = ({ onChange, value, readOnly }: IImageUploaderProps) => { - const [fileList, setFileList] = useState(value ? [{ ...value }] : []); +export const ImagePicker = ({ onChange, value, readOnly }: IImageUploaderProps) => { + const [fileList, setFileList] = useState(typeof value == 'string' ? value : value ? [{ ...value }] : []); const { styles } = useStyles(); const uploadBtnRef = useRef(null); diff --git a/shesha-reactjs/src/designer-components/imagePicker/settings.ts b/shesha-reactjs/src/designer-components/imagePicker/settings.ts index f346b570cc..6f654bac78 100644 --- a/shesha-reactjs/src/designer-components/imagePicker/settings.ts +++ b/shesha-reactjs/src/designer-components/imagePicker/settings.ts @@ -13,7 +13,7 @@ export const getSettings = () => ghost: true, collapsible: 'header', content: { - id:'abc54bf6-f76d-4139-a850-c99bf06c8b69', + id: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', components: [...new DesignerToolbarSettings() .addContextPropertyAutocomplete({ id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', @@ -60,12 +60,13 @@ export const getSettings = () => propertyName: 'isDragger', label: 'Is dragger', parentId: 'root', - description: 'Whether the uploader should show a dragger instead of button', }) + description: 'Whether the uploader should show a dragger instead of button', + }) .addCheckbox({ id: 'cfd7d45e-c7e3-4a27-987b-dc525c412448', propertyName: 'hidden', parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Hidden', + label: 'hide', }) .addCheckbox({ id: 'c6885251-96a6-40ce-99b2-4b5209a9e01c', @@ -84,7 +85,7 @@ export const getSettings = () => propertyName: 'allowUpload', label: 'Allow Upload', parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - hidden: {_code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false} as any, + hidden: { _code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false } as any, validate: {}, }) .addCheckbox({ @@ -92,7 +93,7 @@ export const getSettings = () => propertyName: 'allowReplace', label: 'Allow Replace', parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - hidden: {_code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false} as any, + hidden: { _code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false } as any, validate: {}, }) .addCheckbox({ @@ -100,7 +101,7 @@ export const getSettings = () => propertyName: 'allowDelete', label: 'Allow Delete', parentId: 'abc54bf6-f76d-4139-a850-c99bf06c8b69', - hidden: {_code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false} as any, + hidden: { _code: 'const r = getSettingValue(data?.readOnly); return r === true || r === "readOnly";', _mode: 'code', _value: false } as any, validate: {}, }).toJson() ] @@ -117,7 +118,7 @@ export const getSettings = () => ghost: true, collapsible: 'header', content: { - id:'abc5bfe4-ee69-431e-931b-b0e0b9ceee6f', + id: 'abc5bfe4-ee69-431e-931b-b0e0b9ceee6f', components: [...new DesignerToolbarSettings() .addCheckbox({ id: 'abc5bfe4-ee69-431e-931b-b0e0b9ceee6f', @@ -139,7 +140,7 @@ export const getSettings = () => ghost: true, collapsible: 'header', content: { - id:'abc8b8f9-ec00-4d0a-9d2a-44a630cb2dcb', + id: 'abc8b8f9-ec00-4d0a-9d2a-44a630cb2dcb', components: [...new DesignerToolbarSettings() .addCheckbox({ id: 'af3d9a3f-f47e-48ae-b4c3-f5cc36e534d9', @@ -161,8 +162,10 @@ export const getSettings = () => labelAlign: 'right', dataSourceType: 'url', dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', - useRawValues: true, - }) + displayPropName: 'displayText', + keyPropName: 'value', + mode: 'single', + }) .addEditableTagGroupProps({ id: nanoid(), propertyName: 'allowedFileTypes', @@ -183,8 +186,8 @@ export const getSettings = () => ghost: true, collapsible: 'header', content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() + id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', + components: [...new DesignerToolbarSettings() .addPermissionAutocomplete({ id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', propertyName: 'permissions', @@ -195,6 +198,6 @@ export const getSettings = () => validate: {}, }).toJson() ] - } - }) + } + }) .toJson(); diff --git a/shesha-reactjs/src/designer-components/inputComponent/icons.tsx b/shesha-reactjs/src/designer-components/inputComponent/icons.tsx new file mode 100644 index 0000000000..0ec9cd8378 --- /dev/null +++ b/shesha-reactjs/src/designer-components/inputComponent/icons.tsx @@ -0,0 +1,194 @@ +import React from 'react'; + +export const customIcons = { + blurIcon: , + editIcon: + , + heightIcon: + + + + + + + , + maxHeightIcon: + + + + + + + , + maxWidthIcon: + + + + + + + , + minHeightIcon: + + + + + + + , + minWidthIcon: + + + + + + + , + widthIcon: + + + + + + + , + noRepeatIcon: + + + + + + + , + offsetHorizontalIcon: + + + + + + + , + offsetVerticalIcon: + + + + + + + , + repeatXIcon: + + + + + + + , + repeatYIcon: + + + + + + + , + repeatIcon: + + + + + + + , + spreadIcon: , + inheritIcon: + + + + + + + + + + + + + + , + readonlyIcon: + + + + + + + + , + flex: , + row: + , + column: + , + alignHorizontalLeft: + + , + alignHorizontalRight: + + , + alignHorizontalCenter: + + , + alignVerticalTop: + + , + alignVerticalBottom: + + , + alignVerticalCenter: + + , + tuneIcon: + + +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/inputComponent/index.tsx b/shesha-reactjs/src/designer-components/inputComponent/index.tsx new file mode 100644 index 0000000000..5bf3884423 --- /dev/null +++ b/shesha-reactjs/src/designer-components/inputComponent/index.tsx @@ -0,0 +1,352 @@ +import React, { FC, useCallback, useEffect, useState } from 'react'; +import { Alert, AutoComplete, Button, Input, InputNumber, Radio, Select, Switch } from "antd"; +import { EditableTagGroup, EndpointsAutocomplete, FormComponentSelector, ButtonGroupConfigurator, ColorPicker, FormAutocomplete, LabelValueEditor, PermissionAutocomplete } from '@/components'; +import { PropertyAutocomplete } from '@/components/propertyAutocomplete/propertyAutocomplete'; +import { IObjectMetadata } from '@/interfaces/metadata'; +import { evaluateString, evaluateValue, executeScript, useAvailableConstantsData, useMetadata, useShaFormInstance } from '@/index'; +import { ICodeEditorProps } from '@/designer-components/codeEditor/interfaces'; +import { useMetadataBuilderFactory } from '@/utils/metadata/hooks'; +import camelcase from 'camelcase'; +import { MultiColorInput } from '@/designer-components/multiColorInput'; +import { useStyles } from './styles'; +import { defaultExposedVariables } from '../_settings/settingsControl'; +import CustomDropdown from '../_settings/utils/CustomDropdown'; +import { Autocomplete } from '@/components/autocomplete'; +import { ContextPropertyAutocomplete } from '../contextPropertyAutocomplete'; +import { IDropdownOption, ISettingsInputProps } from '../settingsInput/interfaces'; +import { QueryBuilder } from '../queryBuilder/queryBuilder'; +import { ColumnsConfig } from '../dataTable/table/columnsEditor/columnsConfig'; +import { DynamicActionsConfigurator } from '../dynamicActionsConfigurator/configurator'; +import { ImagePicker } from '../imagePicker'; +import ReferenceListAutocomplete from '@/components/referenceListAutocomplete'; +import { IconPickerWrapper } from '../iconPicker/iconPickerWrapper'; +import ColumnsList from '../columns/columnsList'; +import KeyInformationBarColumnsList from '../keyInformationBar/columnsList'; +import SizableColumnsList from '../sizableColumns/sizableColumnList'; +import { FiltersList } from '../dataTable/tableViewSelector/filters/filtersList'; +import { ItemListConfiguratorModal } from '../itemListConfigurator/itemListConfiguratorModal'; +import { ConfigurableActionConfigurator } from '../configurableActionsConfigurator/configurator'; +import { formTypes } from '../entityReference/settings'; +import { SortingEditor } from '@/components/dataTable/sortingConfigurator'; +import RefListItemSelectorSettingsModal from '@/components/refListSelectorDisplay/options/modal'; +import { FormLayout } from 'antd/es/form/Form'; +import { CustomLabelValueEditorInputs, editModes, getEditor, iconElement } from './utils'; + +const { Password } = Input; + +export const InputComponent: FC> = (props) => { + const { styles } = useStyles(); + + const [formTypesOptions, setFormTypesOptions] = useState<{ value: string }[]>( + formTypes.map((i) => { + return { value: i }; + }) + ); + + const metadataBuilderFactory = useMetadataBuilderFactory(); + const { formData } = useShaFormInstance(); + const { size, className, value, placeholder, type, dropdownOptions, buttonGroupOptions, defaultValue, componentType, tooltipAlt, iconSize, + + propertyName, tooltip: description, onChange, readOnly, label, availableConstantsExpression, noSelectionItemText, noSelectionItemValue, + allowClear, dropdownMode, variant, icon, iconAlt, tooltip, dataSourceType, dataSourceUrl, onAddNewItem, listItemSettingsMarkup, propertyAccessor, referenceList, textType, defaultChecked, showSearch = true } = props; + + const allData = useAvailableConstantsData(); + + const constantsAccessor = useCallback((): Promise => { + if (!availableConstantsExpression?.trim()) + return Promise.reject("AvailableConstantsExpression is mandatory"); + + const metadataBuilder = metadataBuilderFactory(); + + return executeScript(availableConstantsExpression, { data: formData, metadataBuilder }); + }, [availableConstantsExpression, metadataBuilderFactory, formData]); + + const functionName = `get${camelcase(label ?? propertyName, { pascalCase: true })}`; + + const codeEditorProps: ICodeEditorProps = { + readOnly: readOnly, + description: description, + mode: props.mode ?? 'dialog', + language: props.language ?? 'typescript', + fileName: propertyName, + label: label ?? propertyName, + wrapInTemplate: props.wrapInTemplate ?? true, + value: value, + onChange: onChange, + templateSettings: { functionName: functionName }, + exposedVariables: defaultExposedVariables + }; + + const property = propertyAccessor + ? evaluateString(propertyAccessor, { data: formData }) + : null; + + const meta = useMetadata(false); + + useEffect(() => { + if (defaultValue && !value) { + onChange(defaultValue); + } + }, [defaultValue]); + + const propertyMeta = property && meta + ? meta.getPropertyMeta(property) + : null; + + const verb = props.httpVerb ? evaluateValue(props.httpVerb, { data: formData }) : props.httpVerb; + + switch (type) { + case 'tooltip': + return iconElement(icon, null, tooltip, {}, styles); + case 'dataSortingEditor': + return ; + case 'colorPicker': + return ; + case 'dropdown': { + const options = dropdownOptions as IDropdownOption[]; + return onChange(e.target.value)} + readOnly={readOnly} + defaultValue={defaultValue} + variant={variant} + placeholder={placeholder} + style={{ width: props.width ?? "100%" }} + suffix={{iconElement(icon, null, tooltip, {}, styles)}} + value={value} + type={textType} + />; + } +}; + diff --git a/shesha-reactjs/src/designer-components/inputComponent/styles.ts b/shesha-reactjs/src/designer-components/inputComponent/styles.ts new file mode 100644 index 0000000000..f3c07793e8 --- /dev/null +++ b/shesha-reactjs/src/designer-components/inputComponent/styles.ts @@ -0,0 +1,26 @@ +import { createStyles } from '@/styles'; + +export const useStyles = createStyles(({ css, cx }) => { + + const inlineInputs = cx(css` + align-items: end !important; + display: flex; + flex-wrap: wrap; + gap: 0px 8px; + `); + + const rowInputs = cx(css` + display: flex; + flex-wrap: wrap; + gap: 0px 8px; + `); + + const icon = cx(css` + --icon-fill-color: #1C1B1F; + `); + return { + inlineInputs, + rowInputs, + icon + }; +}); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/inputComponent/utils.tsx b/shesha-reactjs/src/designer-components/inputComponent/utils.tsx new file mode 100644 index 0000000000..6bfd6552aa --- /dev/null +++ b/shesha-reactjs/src/designer-components/inputComponent/utils.tsx @@ -0,0 +1,163 @@ +import React from 'react'; +import { Tooltip, Space, Select, Row } from 'antd'; +import { ShaIcon, IconType, SectionSeparator, CodeEditor, ListEditor } from '@/components'; +import { customIcons } from './icons'; +import { startCase } from 'lodash'; +import { CodeEditorWithStandardConstants } from '../codeEditor/codeEditorWithConstants'; +import { ILabelValueEditorProps, ILabelValueItem } from '@/components/labelValueEditor/labelValueEditor'; +import { InputComponent } from '.'; +import { useStyles } from './styles'; +import { getWidth } from '../settingsInput/utils'; +import { IDropdownOption, InputType } from '../settingsInput/interfaces'; +export const iconElement = ( + icon: string | React.ReactNode, + size?: any, + hint?: string, + style?: React.CSSProperties, + styles?: any, + propertyName?: string +) => { + const icons = require('@ant-design/icons'); + + if (typeof icon !== 'string') { + return icon; + } + + if (icons[icon]) { + return ( + + + + ); + } + + if (customIcons[icon]) { + return ( + + {customIcons[icon]} + + ); + } + + if (icon === 'sectionSeparator') { + return ( +
+ + {size} + + + + +
+ ); + } + + return icon; +}; + +export const editModes = [ + { value: 'editable', icon: 'editIcon', title: 'Editable' }, + { value: 'readOnly', icon: 'readonlyIcon', title: 'Read only' }, + { value: 'inherited', icon: 'inheritIcon', title: 'Inherit' } +]; + +export const getEditor = (availableConstantsExpression: string, codeEditorProps: any, constantsAccessor: any) => { + return availableConstantsExpression?.trim() + ? + : ; +}; + +interface InputPropertyEditorProps { + item: Record; + propertyName: string; + itemOnChange: (item: any, index: any) => void; + placeholder?: string; + type: InputType['type']; + dropdownOptions?: IDropdownOption[] | string; + readOnly?: boolean; +} +const InputPropertyEditor = (props: InputPropertyEditorProps) => { + const { item, propertyName, itemOnChange, placeholder, type, dropdownOptions = [], readOnly } = props; + return ( + type === 'dropdown' ? + : + { + itemOnChange({ ...item, [propertyName]: value }, undefined); + }} + dropdownOptions={dropdownOptions} + /> + ); +}; +export const CustomLabelValueEditorInputs = (props: ILabelValueEditorProps) => { + const { styles } = useStyles(); + const { value, onChange, labelName, valueName, readOnly, labelTitle, valueTitle, colorName, iconName, colorTitle, iconTitle, dropdownOptions } = props; + + return + value={value} + onChange={onChange} + initNewItem={(_items) => ({ + [labelName]: '', + [valueName]: '', + [colorName]: '', + [iconName]: '', + }) + } + readOnly={readOnly} + > + {({ item, itemOnChange, readOnly }) => ( +
+ + + + + + + +
) + } + ; +}; diff --git a/shesha-reactjs/src/designer-components/itemListConfigurator/defaultListItem.tsx b/shesha-reactjs/src/designer-components/itemListConfigurator/defaultListItem.tsx index b56ba54b60..cda983cf8c 100644 --- a/shesha-reactjs/src/designer-components/itemListConfigurator/defaultListItem.tsx +++ b/shesha-reactjs/src/designer-components/itemListConfigurator/defaultListItem.tsx @@ -4,7 +4,7 @@ import { IconType, ShaIcon } from '@/components'; import { Tooltip } from 'antd'; import { QuestionCircleOutlined } from '@ant-design/icons'; import { useStyles } from '@/components/listEditor/styles/styles'; -import { useActualContextData } from '@/hooks/useActualContextData'; +import { useActualContextData } from '@/hooks'; export interface IListItemProps { item: DefaultItemRenderingProps; @@ -12,6 +12,7 @@ export interface IListItemProps { export const DefaultListItem: FC = ({ item }) => { const { label, description, icon } = item; + const actualItem = useActualContextData({ label, description, icon }); const { styles } = useStyles(); diff --git a/shesha-reactjs/src/designer-components/itemListConfigurator/interfaces.ts b/shesha-reactjs/src/designer-components/itemListConfigurator/interfaces.ts index 26e375d7c1..ac1d233d67 100644 --- a/shesha-reactjs/src/designer-components/itemListConfigurator/interfaces.ts +++ b/shesha-reactjs/src/designer-components/itemListConfigurator/interfaces.ts @@ -1,12 +1,13 @@ import { ListItemRenderingArgs } from "@/components/listEditor"; -import { FormMarkup } from "@/providers"; +import { FormMarkup, IStyleType } from "@/providers"; export type ItemSettingsMarkupFactory = (item: TItem) => FormMarkup; -export interface DefaultItemRenderingProps { +export interface DefaultItemRenderingProps extends IStyleType { label: string; description?: string; icon?: string; + stylingBox?: string; } export const isDefaultItemRenderingProps = (renderer: any): renderer is DefaultItemRenderingProps => { diff --git a/shesha-reactjs/src/designer-components/itemListConfigurator/itemListConfigurator.tsx b/shesha-reactjs/src/designer-components/itemListConfigurator/itemListConfigurator.tsx index b0b9459a14..1b09f8dab6 100644 --- a/shesha-reactjs/src/designer-components/itemListConfigurator/itemListConfigurator.tsx +++ b/shesha-reactjs/src/designer-components/itemListConfigurator/itemListConfigurator.tsx @@ -18,12 +18,12 @@ export interface IItemListConfiguratorProps { } export const ItemListConfigurator = (props: IItemListConfiguratorProps) => { - const { - value, - onChange, - initNewItem, - readOnly, - settingsMarkupFactory, + const { + value, + onChange, + initNewItem, + readOnly, + settingsMarkupFactory, itemRenderer, header, } = props; diff --git a/shesha-reactjs/src/designer-components/itemListConfigurator/itemListConfiguratorModal.tsx b/shesha-reactjs/src/designer-components/itemListConfigurator/itemListConfiguratorModal.tsx index c40bd57d91..34b624c1e2 100644 --- a/shesha-reactjs/src/designer-components/itemListConfigurator/itemListConfiguratorModal.tsx +++ b/shesha-reactjs/src/designer-components/itemListConfigurator/itemListConfiguratorModal.tsx @@ -6,6 +6,7 @@ import { ItemListConfigurator } from './itemListConfigurator'; import { useMedia } from 'react-use'; import { DefaultItemRenderer, ItemSettingsMarkupFactory } from './interfaces'; import { ListEditorChildrenFn } from '@/components/listEditor'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; export interface ModalSettings { title?: string; @@ -18,6 +19,7 @@ export interface IItemListConfiguratorModalProps { onChange?: (newValue: TItem[]) => void; initNewItem: (items: TItem[]) => TItem; settingsMarkupFactory: ItemSettingsMarkupFactory; + size?: SizeType; buttonText?: string; modalSettings?: ModalSettings; @@ -34,6 +36,7 @@ export const ItemListConfiguratorModal = (props: I itemRenderer, buttonText, modalSettings, + size } = props; const { title, header } = modalSettings ?? {}; @@ -59,7 +62,7 @@ export const ItemListConfiguratorModal = (props: I return ( <> - + extends ItemPropertiesRendererProps { settingsMarkupFactory: ItemSettingsMarkupFactory; @@ -19,14 +19,14 @@ export const PropertiesPanel = (props: IProperties const [form] = Form.useForm(); const formRef = useRef(null); - + const debouncedSave = useDebouncedCallback( - values => { - onChange?.({ ...item, ...values }); - }, - // delay in ms - 300 - ); + values => { + onChange?.({ ...item, ...values }); + }, + // delay in ms + 300 + ); const editor = useMemo(() => { const emptyEditor = null; @@ -35,7 +35,7 @@ export const PropertiesPanel = (props: IProperties const markup = settingsMarkupFactory(item) ?? []; return ( - (props: IProperties }, [item]); return Boolean(item) - ? (<>{editor}) - : (
- -
); + ? (<>{editor}) + : (
+ +
); }; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/kanban/index.tsx b/shesha-reactjs/src/designer-components/kanban/index.tsx index 67d0e4cd8f..09755b0daf 100644 --- a/shesha-reactjs/src/designer-components/kanban/index.tsx +++ b/shesha-reactjs/src/designer-components/kanban/index.tsx @@ -1,12 +1,24 @@ -import { FormOutlined } from '@ant-design/icons'; -import React from 'react'; import { ConfigurableFormItem } from '@/components'; -import { IToolboxComponent, useForm } from '@/index'; -import { Alert } from 'antd'; import KanbanReactComponent from '@/components/kanban'; import { IKanbanProps } from '@/components/kanban/model'; -import { KanbanSettingsForm } from './settings'; -import { RefListItemGroupConfiguratorProvider } from '@/providers/refList/provider'; +import { + getStyle, + IToolboxComponent, + useDataTableStore, + useSheshaApplication, + validateConfigurableComponentSettings, +} from '@/index'; +import { RefListItemGroupConfiguratorProvider } from '@/components/refListSelectorDisplay/provider'; +import { removeUndefinedProps } from '@/utils/object'; +import { FormOutlined } from '@ant-design/icons'; +import { Alert } from 'antd'; +import React, { CSSProperties, useEffect, useMemo, useState } from 'react'; +import { getBackgroundImageUrl, getBackgroundStyle } from '../_settings/utils/background/utils'; +import { getBorderStyle } from '../_settings/utils/border/utils'; +import { getShadowStyle } from '../_settings/utils/shadow/utils'; +import { getSettings } from './settingsForm'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultColumnStyles, defaultStyles } from './utils'; const KanbanComponent: IToolboxComponent = { type: 'kanban', @@ -15,23 +27,60 @@ const KanbanComponent: IToolboxComponent = { icon: , Factory: ({ model }) => { - const form = useForm(); - - if (form.formMode === 'designer' && !model.entityType) { - return ( - - ); - } + const store = useDataTableStore(false); + const data = model; + const { httpHeaders, backendUrl } = useSheshaApplication(); + const { background: columnBackground, border: columnBorder, shadow: columnShadow } = model.columnStyles; + const { shadow, border, background } = model; + const headerStyle = getStyle(model?.headerStyles as string, data); + const columnStyle = getStyle(model?.columnStyle as string, data); + const borderStyles = useMemo(() => getBorderStyle(border, headerStyle), [border, headerStyle]); + const columnBorderStyles = useMemo(() => getBorderStyle(columnBorder, columnStyle), [columnBorder, columnStyle]); + const shadowStyles = useMemo(() => getShadowStyle(shadow), [shadow]); + const columnShadowStyles = useMemo(() => getShadowStyle(columnShadow), [columnShadow]); + + const [backgroundStyles, setBackgroundStyles] = useState({}); + const [columnBackgroundStyle, setColumnBackgroundStyle] = useState({}); + + useEffect(() => { + const fetchStyles = async () => { + const url = await getBackgroundImageUrl(background, backendUrl, httpHeaders); + const style = getBackgroundStyle(background, headerStyle, url); + + setBackgroundStyles((prev) => (JSON.stringify(prev) !== JSON.stringify(style) ? style : prev)); + }; + fetchStyles(); + }, [background, backendUrl, httpHeaders]); + + useEffect(() => { + const fetchStyles = async () => { + const url = await getBackgroundImageUrl(columnBackground, backendUrl, httpHeaders); + const style = getBackgroundStyle(columnBackground, columnStyle, url); + + setColumnBackgroundStyle((prev) => (JSON.stringify(prev) !== JSON.stringify(style) ? style : prev)); + }; + fetchStyles(); + }, [columnBackground, backendUrl, httpHeaders]); + + const additionalColumnStyles: CSSProperties = removeUndefinedProps({ + ...columnBorderStyles, + ...columnBackgroundStyle, + ...columnShadowStyles, + ...columnStyle, + }); + + const additionalHeaderStyles: CSSProperties = removeUndefinedProps({ + ...borderStyles, + ...backgroundStyles, + ...shadowStyles, + ...headerStyle, + }); + return (
{(value) => { - return ( + return store ? ( = { > + ) : ( + ); }} @@ -52,9 +109,20 @@ const KanbanComponent: IToolboxComponent = { ...model, hideLabel: true, }), - settingsFormFactory: (props) => { - return ; - }, + migrator: (m) => + m.add(8, (prev) => { + const newModel = migratePrevStyles(prev, defaultStyles()); + const defaultColumnStyle = defaultColumnStyles(); + + return { + ...newModel, + desktop: { ...newModel.desktop, columnStyles: defaultColumnStyle }, + tablet: { ...newModel.tablet, columnStyles: defaultColumnStyle }, + mobile: { ...newModel.mobile, columnStyles: defaultColumnStyle }, + }; + }), + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), }; export default KanbanComponent; diff --git a/shesha-reactjs/src/designer-components/kanban/settings.tsx b/shesha-reactjs/src/designer-components/kanban/settings.tsx index 681e4b5fc1..c9240f307c 100644 --- a/shesha-reactjs/src/designer-components/kanban/settings.tsx +++ b/shesha-reactjs/src/designer-components/kanban/settings.tsx @@ -1,5 +1,5 @@ import { Autocomplete, CodeEditor, ColorPicker, FormAutocomplete, PropertyAutocomplete, Show } from '@/components'; -import RefListItemSelectorSettingsModal from '@/providers/refList/options/modal'; +import RefListItemSelectorSettingsModal from '@/components/refListSelectorDisplay/options/modal'; import { Checkbox, Input } from 'antd'; import React, { FC } from 'react'; import SettingsForm, { useSettingsForm } from '../_settings/settingsForm'; @@ -10,9 +10,8 @@ import { MetadataProvider } from '@/providers'; import { IKanbanProps } from '@/components/kanban/model'; import { SheshaConstants } from '@/utils/metadata/standardProperties'; import { useAvailableConstantsMetadata } from '@/utils/metadata/useAvailableConstants'; -import { nanoid } from '@/utils/uuid'; -interface IKanbanSettingsState extends IKanbanProps {} +interface IKanbanSettingsState extends IKanbanProps { } const KanbanSettings: FC> = (props) => { const { values } = useSettingsForm(); @@ -36,20 +35,25 @@ const KanbanSettings: FC> = (props) => { - + - - + + - + @@ -57,7 +61,6 @@ const KanbanSettings: FC> = (props) => { - @@ -69,24 +72,19 @@ const KanbanSettings: FC> = (props) => { - - - - - - + + + - - - - - + + + diff --git a/shesha-reactjs/src/designer-components/kanban/settingsForm.ts b/shesha-reactjs/src/designer-components/kanban/settingsForm.ts new file mode 100644 index 0000000000..7d659eb92d --- /dev/null +++ b/shesha-reactjs/src/designer-components/kanban/settingsForm.ts @@ -0,0 +1,1133 @@ +import { IKanbanProps } from '@/components/kanban/model'; +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/lib/form/Form'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { fontTypes, fontWeights } from '../_settings/utils/font/utils'; + +export const getSettings = (data: IKanbanProps) => { + // Generate unique IDs for top-level components + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const columnsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + // Generate IDs for nested panels in appearance tab + const headerStylesPanelId = nanoid(); + const headerStylesContentId = nanoid(); + const columnStylesPanelId = nanoid(); + const columnStylesContentId = nanoid(); + + // Generate IDs for font style panels + const fontStylePanelId = nanoid(); + const fontStyleContentId = nanoid(); + const fontStyleRowId = nanoid(); + + // Generate IDs for background panels + const bgStylePanelId = nanoid(); + const bgStyleContentId = nanoid(); + //const bgStyleRowId = nanoid(); + + // Generate IDs for shadow panels + const shadowStylePanelId = nanoid(); + const shadowStyleContentId = nanoid(); + const shadowStyleRowId = nanoid(); + + // Generate IDs for border panels + const borderStylePanelId = nanoid(); + const borderStyleContentId = nanoid(); + const borderStyleRowId = nanoid(); + const borderRadiusRowId = nanoid(); + + // Generate IDs for custom style panels + const customStylePanelId = nanoid(); + const customStyleContentId = nanoid(); + + // Generate IDs for column style panels + const colDimensionsPanelId = nanoid(); + const colDimensionsContentId = nanoid(); + const colDimensionsWidthRowId = nanoid(); + const colDimensionsHeightRowId = nanoid(); + const colBgPanelId = nanoid(); + const colBgContentId = nanoid(); + const colShadowPanelId = nanoid(); + const colShadowContentId = nanoid(); + const colShadowRowId = nanoid(); + const colBorderPanelId = nanoid(); + const colBorderContentId = nanoid(); + const colBorderStyleId = nanoid(); + const colBorderContainerId = nanoid(); + const colBorderRadiusRowId = nanoid(); + const colMarginPaddingPanelId = nanoid(); + const colMarginPaddingContentId = nanoid(); + const colCustomStylePanelId = nanoid(); + const colCustomStyleContentId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'componentName', + label: 'Component Name', + parentId: commonTabId, + validate: { + required: true, + }, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'formAutocomplete', + id: nanoid(), + propertyName: 'modalFormId', + label: 'Render Form', + jsSetting: true, + validate: { + required: true, + }, + }, + { + type: 'propertyAutocomplete', + id: nanoid(), + propertyName: 'groupingProperty', + label: 'Grouping Property', + jsSetting: true, + validate: { + required: true, + }, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'collapsible', + label: 'Collapsible', + jsSetting: true, + hidden: { + _code: 'return getSettingValue(data?.kanbanReadonly);', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'kanbanReadonly', + label: 'Readonly', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'allowDelete', + label: 'Allow Delete', + jsSetting: true, + hidden: { + _code: 'return getSettingValue(data?.kanbanReadonly);', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'showIcons', + label: 'Show Icons', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'allowNewRecord', + label: 'Allow New Record', + jsSetting: true, + hidden: { + _code: 'return getSettingValue(data?.kanbanReadonly);', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'formAutocomplete', + id: nanoid(), + propertyName: 'createFormId', + label: 'Create Form', + jsSetting: true, + hidden: { + _code: + 'return !getSettingValue(data?.allowNewRecord) || getSettingValue(data?.kanbanReadonly);', + _mode: 'code', + _value: false, + } as any, + validate: { + required: true, + }, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'allowEdit', + label: 'Allow Edit', + jsSetting: true, + hidden: { + _code: 'return getSettingValue(data?.kanbanReadonly);', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'formAutocomplete', + id: nanoid(), + propertyName: 'editFormId', + label: 'Edit Form', + jsSetting: true, + hidden: { + _code: 'return !getSettingValue(data?.allowEdit) || getSettingValue(data?.kanbanReadonly);', + _mode: 'code', + _value: false, + } as any, + validate: { + required: true, + }, + }, + ], + }) + .toJson(), + ], + }, + { + key: 'columns', + title: 'Data', + id: columnsTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: columnsTabId, + inputType: 'referenceListAutocomplete', + propertyName: 'referenceList', + label: 'Reference List', + tooltip: 'Make sure to reselect the reference list if any changes are made to its items', + filter: { and: [{ '==': [{ var: 'isLast' }, true] }] }, + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'items', + label: 'Items', + labelAlign: 'right', + parentId: columnsTabId, + referenceList: { + _code: 'return getSettingValue(data?.referenceList);', + _mode: 'code', + _value: false, + } as any, + inputType: 'RefListItemSelectorSettingsModal', + }) + .toJson(), + ], + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: headerStylesPanelId, + propertyName: 'headerStyles', + label: 'Header Styles', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: headerStylesContentId, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: fontStylePanelId, + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: headerStylesContentId, + ghost: true, + collapsible: 'header', + content: { + id: fontStyleContentId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: fontStyleRowId, + parentId: fontStyleContentId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: 'Controls text thickness (light, normal, bold, etc.)', + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: bgStylePanelId, + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: headerStylesContentId, + collapsible: 'header', + content: { + id: bgStyleContentId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: bgStyleContentId, + label: 'Type', + jsSetting: false, + propertyName: 'background.type', + inputType: 'radio', + tooltip: 'Select a type of background', + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: bgStyleContentId, + inputs: [ + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + propertyName: 'background.color', + hideLabel: true, + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: bgStyleContentId, + inputs: [ + { + type: 'multiColorPicker', + id: nanoid(), + propertyName: 'background.gradient.colors', + label: 'Colors', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', + _mode: 'code', + _value: false, + } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: bgStyleContentId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'background.url', + jsSetting: false, + label: 'URL', + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: bgStyleContentId, + inputs: [ + { + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: 'Image', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: bgStyleContentId, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: 'background.storedFile.id', + label: 'File ID', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "background.size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + }, + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: shadowStylePanelId, + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: headerStylesContentId, + collapsible: 'header', + content: { + id: shadowStyleContentId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: shadowStyleRowId, + parentId: shadowStyleContentId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: 'offsetHorizontalIcon', + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: borderStylePanelId, + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: headerStylesContentId, + collapsible: 'header', + content: { + id: borderStyleContentId, + components: [ + ...new DesignerToolbarSettings() + .addContainer({ + id: borderStyleRowId, + parentId: borderStyleContentId, + propertyName: 'borderContainer', + components: getBorderInputs() as any, + }) + .addContainer({ + id: borderRadiusRowId, + parentId: borderStyleContentId, + propertyName: 'borderRadiusContainer', + components: getCornerInputs() as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: customStylePanelId, + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: headerStylesContentId, + collapsible: 'header', + content: { + id: customStyleContentId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'headerStyles', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: columnStylesPanelId, + propertyName: 'columnStyles', + label: 'Column Styles', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: columnStylesContentId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'gap', + label: 'Gap', + inputType: 'numberField', + tooltip: 'The gap between the columns', + jsSetting: true, + }) + .addCollapsiblePanel({ + id: colDimensionsPanelId, + propertyName: 'pnlcolumnStyles.dimensions', + label: 'Dimensions', + parentId: columnStylesContentId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: colDimensionsContentId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: colDimensionsWidthRowId, + parentId: colDimensionsContentId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Width', + width: 85, + propertyName: 'columnStyles.dimensions.width', + icon: 'widthIcon', + tooltip: + 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Width', + width: 85, + hideLabel: true, + propertyName: 'columnStyles.dimensions.minWidth', + icon: 'minWidthIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Width', + width: 85, + hideLabel: true, + propertyName: 'columnStyles.dimensions.maxWidth', + icon: 'maxWidthIcon', + }, + ], + }) + .addSettingsInputRow({ + id: colDimensionsHeightRowId, + parentId: colDimensionsContentId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Height', + width: 85, + propertyName: 'columnStyles.dimensions.height', + icon: 'heightIcon', + tooltip: + 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Height', + width: 85, + hideLabel: true, + propertyName: 'columnStyles.dimensions.minHeight', + icon: 'minHeightIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Height', + width: 85, + hideLabel: true, + propertyName: 'columnStyles.dimensions.maxHeight', + icon: 'maxHeightIcon', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: colBgPanelId, + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: columnStylesContentId, + collapsible: 'header', + content: { + id: colBgContentId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: colBgContentId, + label: 'Type', + jsSetting: false, + propertyName: 'columnStyles.background.type', + inputType: 'radio', + tooltip: 'Select a type of background', + buttonGroupOptions: backgroundTypeOptions + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: colBgContentId, + inputs: [ + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + propertyName: 'columnStyles.background.color', + hideLabel: true, + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.columnStyles.background?.type) !== "color";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: colBgContentId, + inputs: [ + { + type: 'multiColorPicker', + id: nanoid(), + propertyName: 'columnStyles.background.gradient.colors', + label: 'Colors', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.columnStyles.background?.type) !== "gradient";', + _mode: 'code', + _value: false, + } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: colBgContentId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'columnStyles.background.url', + jsSetting: false, + label: 'URL', + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.columnStyles.background?.type) !== "url";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: colBgContentId, + inputs: [ + { + type: 'imageUploader', + id: nanoid(), + propertyName: 'columnStyles.background.uploadFile', + label: 'Image', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.columnStyles.background?.type) !== "image";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: colBgContentId, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.columnStyles.background?.type) !== "storedFile";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: 'columnStyles.background.storedFile.id', + label: 'File ID', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: colBgContentId, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.columnStyles.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + inline: true, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: 'Size', + hideLabel: true, + propertyName: 'columnStyles.background.size', + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: 'Position', + hideLabel: true, + propertyName: 'columnStyles.background.position', + dropdownOptions: positionOptions, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: colBgContentId, + inputs: [ + { + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'columnStyles.background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.columnStyles.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: colShadowPanelId, + propertyName: 'pnlcolumnStyles.shadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: columnStylesContentId, + collapsible: 'header', + content: { + id: colShadowContentId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: colShadowRowId, + parentId: colShadowContentId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: 'offsetHorizontalIcon', + propertyName: 'columnStyles.shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'columnStyles.shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur radius', + width: 80, + icon: 'blurIcon', + propertyName: 'columnStyles.shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'columnStyles.shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'columnStyles.shadow.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: colBorderPanelId, + propertyName: 'columnStyles.border', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: columnStylesContentId, + collapsible: 'header', + content: { + id: colBorderContentId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: colBorderStyleId, + parentId: colBorderContentId, + hidden: { + _code: + 'return !getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.columnStyles.border?.hideBorder);', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'button', + id: nanoid(), + label: 'Border', + hideLabel: true, + propertyName: 'columnStyles.border.hideBorder', + icon: 'EyeOutlined', + iconAlt: 'EyeInvisibleOutlined', + }, + ], + }) + .addContainer({ + id: colBorderContainerId, + parentId: colBorderContentId, + components: getBorderInputs('columnStyles', true) as any, + }) + .addContainer({ + id: colBorderRadiusRowId, + parentId: colBorderContentId, + components: getCornerInputs('columnStyles', true) as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: colMarginPaddingPanelId, + propertyName: 'columnStyles.stylingBox', + label: 'Margin and Padding', + labelAlign: 'right', + parentId: columnStylesContentId, + collapsible: 'header', + content: { + id: colMarginPaddingContentId, + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'columnStyles.stylingBox', + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: colCustomStylePanelId, + propertyName: 'columncustomStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: columnStylesContentId, + collapsible: 'header', + content: { + id: colCustomStyleContentId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'columnStyle', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + tooltip: 'Enter a list of permissions that should be associated with this component', + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/kanban/utils.ts b/shesha-reactjs/src/designer-components/kanban/utils.ts new file mode 100644 index 0000000000..ee9e295104 --- /dev/null +++ b/shesha-reactjs/src/designer-components/kanban/utils.ts @@ -0,0 +1,52 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + return { + font: { color: '#000', size: 14, weight: '400', type: 'Segoe UI' }, + background: { type: 'color', color: '#fff' }, + dimensions: { + width: 'auto', + height: 'auto', + minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto', + }, + border: { + radiusType: 'all', + borderType: 'custom', + border: { + all: { width: '1px', color: '#d9d9d9', style: 'solid' }, + top: { width: '1px', color: '#d9d9d9', style: 'solid' }, + bottom: { width: '1px', color: '#d9d9d9', style: 'solid' }, + left: { width: '1px', color: '#d9d9d9', style: 'solid' }, + right: { width: '1px', color: '#d9d9d9', style: 'solid' }, + }, + radius: { all: 0 }, + }, + shadow: { blurRadius: 0, color: 'rgba(0, 0, 0, 0.15)', offsetX: 0, offsetY: 0, spreadRadius: 0 }, + stylingBox: '{"marginBottom":"5","paddingLeft":"8","paddingBottom":"8","paddingTop":"8","paddingRight":"8"}', + }; + }; + + export const defaultColumnStyles = (): IStyleType => { + return { + font: { color: '#000', size: 14, weight: '400', type: 'Segoe UI' }, + background: { type: 'color', color: '#00000005' }, + dimensions: { width: '300px', height: '500px', minHeight: '500px', maxHeight: '500px', minWidth: '300px', maxWidth: '300px' }, + shadow: { blurRadius: 0, color: 'rgba(0, 0, 0, 0.15)', offsetX: 0, offsetY: 0, spreadRadius: 0 }, + border: { + radiusType: 'all', + borderType: 'all', + border: { + all: { width: '1px', color: '#d9d9d9', style: 'solid' }, + top: { width: '1px', color: '#d9d9d9', style: 'solid' }, + bottom: { width: '1px', color: '#d9d9d9', style: 'solid' }, + left: { width: '1px', color: '#d9d9d9', style: 'solid' }, + right: { width: '1px', color: '#d9d9d9', style: 'solid' }, + }, + radius: { all: 0 }, + }, + stylingBox: '{"paddingLeft":"8","paddingBottom":"8","paddingTop":"8","paddingRight":"8"}', + }; + }; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/keyInformationBar/columnsList.tsx b/shesha-reactjs/src/designer-components/keyInformationBar/columnsList.tsx index 0a61fec640..dd67ec1e99 100644 --- a/shesha-reactjs/src/designer-components/keyInformationBar/columnsList.tsx +++ b/shesha-reactjs/src/designer-components/keyInformationBar/columnsList.tsx @@ -15,11 +15,13 @@ import { DeleteFilled, MenuOutlined, PlusOutlined, QuestionCircleOutlined } from import { nanoid } from '@/utils/uuid'; import { KeyInfomationBarItemProps } from './interfaces'; import { strings } from '@/components/keyInformationBar/utils'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; export interface IProps { readOnly: boolean; value?: object; onChange?: any; + size?: SizeType; } const EditableContext = React.createContext(null); @@ -149,7 +151,7 @@ const DragHandle = () => { }; -export const ColumnsList: FC = ({ value, onChange, readOnly }) => { +export const ColumnsList: FC = ({ value, onChange, readOnly, size }) => { const columns = value as KeyInfomationBarItemProps[]; const handleDeleteTab = (key: string) => { @@ -222,7 +224,7 @@ export const ColumnsList: FC = ({ value, onChange, readOnly }) => { dataIndex: 'operations', render: (_, record) => columns.length >= 1 ? ( - handleDeleteTab(record.id)}> + handleDeleteTab(record.id)}> ) : null, @@ -282,7 +284,7 @@ export const ColumnsList: FC = ({ value, onChange, readOnly }) => { return ( - + Configure Columns {tooltip()}} diff --git a/shesha-reactjs/src/designer-components/keyInformationBar/index.tsx b/shesha-reactjs/src/designer-components/keyInformationBar/index.tsx index aaafddfc11..dd94d2c784 100644 --- a/shesha-reactjs/src/designer-components/keyInformationBar/index.tsx +++ b/shesha-reactjs/src/designer-components/keyInformationBar/index.tsx @@ -3,12 +3,15 @@ import { IToolboxComponent } from '@/interfaces'; import { BorderLeftOutlined } from '@ant-design/icons'; import { nanoid } from '@/utils/uuid'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; -import { KeyInformationBarSettingsForm } from './settings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import ParentProvider from '@/providers/parentProvider/index'; import { IKeyInformationBarProps } from './interfaces'; import KeyInformationBar from '@/components/keyInformationBar'; import { removeComponents } from '../_common-migrations/removeComponents'; +import { getSettings } from './settingsForm'; +import { validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultStyles } from './utils'; const ColumnsComponent: IToolboxComponent = { @@ -18,6 +21,7 @@ const ColumnsComponent: IToolboxComponent = { icon: , Factory: ({ model }) => { + return ( @@ -31,7 +35,25 @@ const ColumnsComponent: IToolboxComponent = { (prev) => migratePropertyName(migrateCustomFunctions(prev)) as IKeyInformationBarProps ) .add(1, (prev) => migrateVisibility(prev)) - .add(2, (prev) => removeComponents(prev)), + .add(2, (prev) => removeComponents(prev)) + .add(3, (prev) => { + + const prevDividerStyles = { + orientation: prev?.orientation, + dividerWidth: prev?.dividerWidth, + dividerMargin: prev?.dividerMargin, + dividerColor: prev?.dividerColor, + gap: prev?.gap, + }; + + return ({ + ...prev, + desktop: { ...prev.desktop, ...prevDividerStyles }, + mobile: { ...prev.mobile, ...prevDividerStyles }, + tablet: { ...prev.tablet, ...prevDividerStyles } + }); + }) + .add(4, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), initModel: (model) => { const tabsModel: IKeyInformationBarProps = { ...model, @@ -45,14 +67,13 @@ const ColumnsComponent: IToolboxComponent = { components: [], padding: '0px', } - ], - orientation: 'horizontal', - stylingBox: "{\"marginBottom\":\"5\"}" + ] }; return tabsModel; }, - settingsFormFactory: (props) => , + settingsFormMarkup: () => getSettings(), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(), model), customContainerNames: ['columns'], }; diff --git a/shesha-reactjs/src/designer-components/keyInformationBar/interfaces.ts b/shesha-reactjs/src/designer-components/keyInformationBar/interfaces.ts index 3621279f43..a2967a6a3c 100644 --- a/shesha-reactjs/src/designer-components/keyInformationBar/interfaces.ts +++ b/shesha-reactjs/src/designer-components/keyInformationBar/interfaces.ts @@ -1,4 +1,4 @@ -import { IConfigurableFormComponent } from '@/providers/form/models'; +import { IConfigurableFormComponent, IInputStyles } from '@/providers/form/models'; import { AlignItems } from '../container/interfaces'; export interface KeyInfomationBarItemProps { @@ -10,7 +10,7 @@ export interface KeyInfomationBarItemProps { padding?: string; } -export interface IKeyInformationBarProps extends IConfigurableFormComponent { +export interface IKeyInformationBarProps extends IConfigurableFormComponent, IInputStyles { width?: string; height?: string; dividerHeight?: string; diff --git a/shesha-reactjs/src/designer-components/keyInformationBar/settingsForm.ts b/shesha-reactjs/src/designer-components/keyInformationBar/settingsForm.ts new file mode 100644 index 0000000000..e1d0fbea5d --- /dev/null +++ b/shesha-reactjs/src/designer-components/keyInformationBar/settingsForm.ts @@ -0,0 +1,589 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/lib/form/Form'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; + +export const getSettings = () => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dividerTabId = nanoid(); + const styleRouterId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + + return { + components: new DesignerToolbarSettings() + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + + .addSettingsInput( + { + id: nanoid(), + inputType: 'textField', + propertyName: 'componentName', + label: 'Component Name', + parentId: commonTabId, + size: 'small', + validate: { + required: true, + } + } + ) + .addSettingsInputRow({ + id: 'componentName-hide-row', + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'columns', + label: 'Columns', + parentId: commonTabId, + type: 'keyInformationBarColumnsList', + }, + { + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + parentId: commonTabId, + type: 'switch', + jsSetting: true + } + ] + }) + .toJson() + ] + }, + { + key: '4', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + propertyName: 'orientation', + label: 'Orientation', + jsSetting: true, + defaultValue: 'horizontal', + dropdownOptions: [ + { value: 'horizontal', label: 'Horizontal' }, + { value: 'vertical', label: 'Vertical' } + ] + }, + { + id: nanoid(), + propertyName: 'alignItems', + label: 'Align Items', + parentId: commonTabId, + type: 'dropdown', + defaultValue: 'flex-start', + hidden: { _code: 'return getSettingValue(data?.orientation) !== "horizontal";', _mode: 'code', _value: false } as any, + dropdownOptions: [ + { value: 'flex-start', label: 'Flex Start' }, + { value: 'flex-end', label: 'Flex End' }, + { value: 'center', label: 'Center' } + ] + } + ] + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'gap', + label: 'Gap', + parentId: commonTabId, + inputType: 'numberField', + jsSetting: true + }) + .addCollapsiblePanel({ + id: 'dimensionsStyleCollapsiblePanel', + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: 'dimensionsStylePnl', + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'dimensionsStyleRowWidth', + parentId: 'dimensionsStylePnl', + inline: true, + inputs: [ + { + type: 'textField', + id: 'width-s4gmBg31azZC0UjZjpfTm', + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + + }, + { + type: 'textField', + id: 'minWidth-s4gmBg31azZC0UjZjpfTm', + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: 'maxWidth-s4gmBg31azZC0UjZjpfTm', + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: 'dimensionsStyleRowHeight', + parentId: 'dimensionsStylePnl', + inline: true, + inputs: [ + { + type: 'textField', + id: 'height-s4gmBg31azZC0UjZjpfTm', + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: 'minHeight-s4gmBg31azZC0UjZjpfTm', + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: 'maxHeight-s4gmBg31azZC0UjZjpfTm', + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: 'borderStyleCollapsiblePanel', + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: 'borderStylePnl', + components: [...new DesignerToolbarSettings() + + .addContainer({ + id: 'borderStyleRow', + parentId: 'borderStylePnl', + components: getBorderInputs() as any + }) + .addContainer({ + id: 'borderRadiusStyleRow', + parentId: 'borderStylePnl', + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: 'backgroundStyleCollapsiblePanel', + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: 'backgroundStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: "backgroundStyleRow-selectType", + parentId: "backgroundStylePnl", + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: "backgroundStyleRow-color", + parentId: "backgroundStylePnl", + inputs: [{ + type: 'colorPicker', + id: 'backgroundStyleRow-color', + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: "backgroundStyle-gradientColors", + parentId: "backgroundStylePnl", + inputs: [{ + type: 'multiColorPicker', + id: 'backgroundStyle-gradientColors', + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: "backgroundStyle-url", + parentId: "backgroundStylePnl", + inputs: [{ + type: 'textField', + id: 'backgroundStyle-url', + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: "backgroundStyle-image", + parentId: 'backgroundStylePnl', + inputs: [{ + type: 'imageUploader', + id: 'backgroundStyle-image', + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: "backgroundStyleRow-storedFile", + parentId: 'backgroundStylePnl', + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-storedFile', + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: "backgroundStyleRow-controls", + parentId: 'backgroundStyleRow', + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: 'backgroundStyleRow-size', + label: "Size", + hideLabel: true, + propertyName: "background.size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }, + { + type: 'customDropdown', + id: 'backgroundStyleRow-position', + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + }, + ] + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-repeat', + parentId: 'backgroundStyleRow', + inputs: [{ + type: 'radio', + id: 'backgroundStyleRow-repeat-radio', + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: 'shadowStyleCollapsiblePanel', + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: 'shadowStylePnl', + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'shadowStyleRow', + parentId: 'shadowStylePnl', + inline: true, + inputs: [ + { + type: 'numberField', + id: 'shadowStyleRow-offsetX', + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: 'shadowStyleRow-offsetY', + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: 'shadowStyleRow-blurRadius', + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: 'shadowStyleRow-spreadRadius', + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: 'shadowStyleRow-color', + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: 'styleCollapsiblePanel', + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: 'stylePnl-M5-911', + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: 'styleBoxPnl', + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: 'customStyleCollapsiblePanel', + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: 'stylePnl-M500-911MFR', + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: 'custom-css-412c-8461-4c8d55e5c073', + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: dividerTabId, + propertyName: 'divider', + label: 'Divider', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: 'dividerPnl', + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'dividerMargin-height-row', + parentId: dividerTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'dividerMargin', + label: 'Divider Margin', + parentId: dividerTabId, + type: 'textField', + jsSetting: true, + tooltip: 'Sets the margin around the divider' + }, + { + id: nanoid(), + propertyName: 'dividerWidth', + label: 'Divider Width', + parentId: dividerTabId, + type: 'textField', + jsSetting: true, + tooltip: 'Sets the width of the divider', + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.orientation) !== "vertical";', _mode: 'code', _value: false } as any + }, + { + id: nanoid(), + propertyName: 'dividerHeight', + label: 'Divider Height', + parentId: dividerTabId, + type: 'textField', + jsSetting: true, + tooltip: 'Sets the height of the divider', + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.orientation) !== "horizontal";', _mode: 'code', _value: false } as any + }] + }) + .addSettingsInputRow({ + id: 'dividerWidth-thickness-row', + parentId: dividerTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'dividerThickness', + label: 'Divider Thickness', + parentId: dividerTabId, + type: 'textField', + jsSetting: true, + tooltip: 'Sets the thickness of the divider line' + }, + { + id: nanoid(), + propertyName: 'dividerColor', + label: 'Divider Color', + parentId: dividerTabId, + type: 'colorPicker', + jsSetting: true, + allowClear: true + }] + }) + .toJson() + ] + } + + + }) + .toJson()] + }).toJson()] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + tooltip: 'Enter a list of permissions that should be associated with this component', + jsSetting: true, + parentId: securityTabId + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/keyInformationBar/utils.ts b/shesha-reactjs/src/designer-components/keyInformationBar/utils.ts new file mode 100644 index 0000000000..c3fc5b2f53 --- /dev/null +++ b/shesha-reactjs/src/designer-components/keyInformationBar/utils.ts @@ -0,0 +1,48 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + return { + background: { type: 'color', color: '#fff' }, + border: { + border: { + all: { + width: 1, + style: 'none', + color: '#d9d9d9' + }, + top: { + width: 1, + style: 'none', + color: '#d9d9d9' + }, + bottom: { + width: 1, + style: 'none', + color: '#d9d9d9' + }, + left: { + width: 1, + style: 'none', + color: '#d9d9d9' + }, + right: { + width: 1, + style: 'none', + color: '#d9d9d9' + }, + }, + radius: { all: 0 }, + borderType: 'all', + radiusType: 'all' + }, + dimensions: { + width: '100%', + height: 'auto', + minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto' + }, + stylingBox: "{\"marginBottom\":\"5\"}" + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/link/index.tsx b/shesha-reactjs/src/designer-components/link/index.tsx index 5783d9a4bf..2e2749f76e 100644 --- a/shesha-reactjs/src/designer-components/link/index.tsx +++ b/shesha-reactjs/src/designer-components/link/index.tsx @@ -1,10 +1,8 @@ import React, { CSSProperties, ReactNode } from 'react'; import { IToolboxComponent } from '@/interfaces'; -import { FormMarkup, IConfigurableFormComponent, IInputStyles } from '@/providers/form/models'; import { LinkOutlined } from '@ant-design/icons'; -import { evaluateString, getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; -import { useForm, useFormData } from '@/providers'; -import settingsFormJson from './settingsForm.json'; +import { evaluateString, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { IInputStyles, IConfigurableFormComponent } from '@/providers'; import ComponentsContainer from '@/components/formDesigner/containers/componentsContainer'; import { AlignItems, JustifyContent, JustifyItems } from '@/designer-components/container/interfaces'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; @@ -12,6 +10,10 @@ import ConfigurableFormItem from '@/components/formDesigner/components/formItem' import ParentProvider from '@/providers/parentProvider/index'; import { ContainerDirection } from '@/components/formDesigner/common/interfaces'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { IFontValue } from '../_settings/utils/font/interfaces'; +import { defaultStyles } from './utils'; export interface IAlertProps extends IConfigurableFormComponent { text: string; @@ -32,24 +34,22 @@ export interface ILinkProps extends IConfigurableFormComponent { justifyItems?: JustifyItems; className?: string; icon?: ReactNode; + font?: IFontValue; components?: IConfigurableFormComponent[]; } -const settingsForm = settingsFormJson as FormMarkup; const LinkComponent: IToolboxComponent = { type: 'link', isInput: false, name: 'link', icon: , - Factory: ({ model }) => { - const { formMode } = useForm(); - - const { data } = useFormData(); - + calculateModel: (model, allData) => ({ + isDesignerMode: allData.form.formMode === 'designer', + href: evaluateString(model.href, allData.data), + }), + Factory: ({ model, calculatedModel }) => { const { - href: initialHref = '', - content = '', - style, + content = 'Link', target, direction, justifyContent, @@ -58,7 +58,7 @@ const LinkComponent: IToolboxComponent = { justifyItems, hasChildren, } = model; - + // Create link container style with textAlign from fontStyles const linkStyle: CSSProperties = {}; if (direction === 'horizontal' && justifyContent) { @@ -68,28 +68,25 @@ const LinkComponent: IToolboxComponent = { linkStyle['justifyItems'] = justifyItems; } - const isDesignerMode = formMode === 'designer'; - - if (model.hidden) - return null; + if (model.hidden) return null; return ( - + {() => { - - const href = evaluateString(initialHref || model?.href, data); - if (!hasChildren) { return ( - - {content} - + ); } const containerHolder = () => ( = { /> ); - if (isDesignerMode) { + if (calculatedModel.isDesignerMode) { return containerHolder(); } return ( - + {containerHolder()} ); @@ -113,39 +110,40 @@ const LinkComponent: IToolboxComponent = { ); }, - settingsFormMarkup: settingsForm, - validateSettings: model => validateConfigurableComponentSettings(settingsForm, model), + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), initModel: (model: ILinkProps) => { const customProps: ILinkProps = { ...model, direction: 'vertical', target: '_self', justifyContent: 'left', + hideLabel: true, }; return customProps; }, - migrator: (m) => m - .add(0, (prev) => ({...prev} as ILinkProps)) - .add(1, (prev) => { - return { - ...prev, - label: prev.label ?? prev['name'], - href: prev.content, - content: prev['name'], - }; - }) - .add(2, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(3, (prev) => ({...migrateFormApi.properties(prev)})) - .add(4, (prev) => { - const styles: IInputStyles = { - style: prev.style - }; - - return { ...prev, desktop: {...styles}, tablet: {...styles}, mobile: {...styles} }; - }) - , - + migrator: (m) => + m + .add(0, (prev) => ({ ...prev }) as ILinkProps) + .add(1, (prev) => { + return { + ...prev, + label: prev.label ?? prev['name'], + href: prev.content, + content: prev['name'], + }; + }) + .add(2, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(3, (prev) => ({ ...migrateFormApi.properties(prev) })) + .add(4, (prev) => { + const styles: IInputStyles = { + style: prev.style, + }; + + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }) + .add(5, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), }; export default LinkComponent; diff --git a/shesha-reactjs/src/designer-components/link/settingsForm.ts b/shesha-reactjs/src/designer-components/link/settingsForm.ts new file mode 100644 index 0000000000..383d727797 --- /dev/null +++ b/shesha-reactjs/src/designer-components/link/settingsForm.ts @@ -0,0 +1,534 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { fontTypes, fontWeights } from '../_settings/utils/font/utils'; +import { FormLayout } from 'antd/es/form/Form'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const fontStylePnlId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { _code: 'return getSettingValue(data?.hasChildren);', _mode: 'code', _value: false } as any, + inputs: [ + { + id: nanoid(), + type: 'textArea', + propertyName: 'content', + label: 'Content', + size: 'small', + jsSetting: true, + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + parentId: commonTabId, + inputType: 'textField', + propertyName: 'href', + label: 'Href', + size: 'small', + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'hasChildren', + label: 'Has Children', + size: 'small', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + + propertyName: 'target', + label: 'Target', + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + dropdownOptions: [ + { + label: 'Blank', + value: '_blank', + }, + { + label: 'Parent', + value: '_parent', + }, + { + label: 'Self', + value: '_self', + }, + { + label: 'Top', + value: '_top', + }, + ], + }, + ], + }) + .toJson(), + ], + }, + { + key: '4', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'direction-options-row', + parentId: 'styleRouter', + hidden: { + _code: 'return !getSettingValue(data?.hasChildren);', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'dropdown', + id: 'direction-s4gmBg31azZC0UjZjpfTm', + propertyName: 'direction', + label: 'Direction', + jsSetting: true, + layout: 'horizontal', + defaultValue: 'vertical', + dropdownOptions: [ + { + label: 'Horizontal', + value: 'horizontal', + }, + { + label: 'Vertical', + value: 'vertical', + }, + ], + }, + ], + }) + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + hidden: { + _code: 'return getSettingValue(data?.hasChildren);', + _mode: 'code', + _value: false, + } as any, + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: fontStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: fontStylePnlId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: 'Controls text thickness (light, normal, bold, etc.)', + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + + hidden: { + _code: 'return getSettingValue(data?.direction) !== "horizontal"', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + propertyName: 'justifyContent', + label: 'Justify Content', + jsSetting: true, + layout: 'horizontal', + dropdownOptions: [ + { + label: 'Center', + value: 'center', + }, + { + label: 'Flex Start', + value: 'flex-start', + }, + { + label: 'Flex End', + value: 'flex-end', + }, + { + label: 'Left', + value: 'left', + }, + { + label: 'Right', + value: 'right', + }, + { + label: 'Space Between', + value: 'space-between', + }, + { + label: 'Space Around', + value: 'space-around', + }, + { + label: 'Space Evenly', + value: 'space-evenly', + }, + { + label: 'Stretch', + value: 'stretch', + }, + ], + }, + { + type: 'dropdown', + id: nanoid(), + propertyName: 'alignItems', + label: 'Align Items', + jsSetting: true, + layout: 'horizontal', + dropdownOptions: [ + { + label: 'Baseline', + value: 'baseline', + }, + { + label: 'Center', + value: 'center', + }, + { + label: 'End', + value: 'end', + }, + { + label: 'Flex End', + value: 'flex-end', + }, + { + label: 'Flex Start', + value: 'flex-start', + }, + { + label: 'Inherit', + value: 'inherit', + }, + { + label: 'Initial', + value: 'initial', + }, + { + label: 'Normal', + value: 'normal', + }, + { + label: 'Revert', + value: 'revert', + }, + { + label: 'Self End', + value: 'self-end', + }, + { + label: 'Self Start', + value: 'self-start', + }, + { + label: 'Start', + value: 'start', + }, + { + label: 'Stretch', + value: 'stretch', + }, + { + label: 'Unset', + value: 'unset', + }, + ], + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + + hidden: { + _code: 'return !getSettingValue(data?.hasChildren);', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + propertyName: 'justifyItems', + label: 'Justify Items', + hidden: { + _code: 'return getSettingValue(data?.direction) !== "horizontal"', + _mode: 'code', + _value: false, + } as any, + jsSetting: true, + layout: 'horizontal', + dropdownOptions: [ + { + label: 'Baseline', + value: 'baseline', + }, + { + label: 'Center', + value: 'center', + }, + { + label: 'End', + value: 'end', + }, + { + label: 'Flex End', + value: 'flex-end', + }, + { + label: 'Flex Start', + value: 'flex-start', + }, + { + label: 'inherit', + value: 'inherit', + }, + { + label: 'Initial', + value: 'initial', + }, + { + label: 'Left', + value: 'left', + }, + { + label: 'Legacy', + value: 'legacy', + }, + { + label: 'Normal', + value: 'normal', + }, + { + label: 'Revert', + value: 'revert', + }, + { + label: 'Right', + value: 'right', + }, + { + label: 'Self End', + value: 'self-end', + }, + { + label: 'Self Start', + value: 'self-start', + }, + { + label: 'Start', + value: 'start', + }, + { + label: 'Stretch', + value: 'stretch', + }, + { + label: 'Unset', + value: 'unset', + }, + ], + }, + { + type: 'textField', + id: nanoid(), + propertyName: 'className', + label: 'Custom CSS Class', + jsSetting: true, + }, + ], + }) + + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: '5', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/link/utils.ts b/shesha-reactjs/src/designer-components/link/utils.ts new file mode 100644 index 0000000000..7e8047047e --- /dev/null +++ b/shesha-reactjs/src/designer-components/link/utils.ts @@ -0,0 +1,7 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + return { + font: { weight: '400', size: 14, type: 'Segoe UI' }, + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/markdown/index.tsx b/shesha-reactjs/src/designer-components/markdown/index.tsx index 094a872765..5615d8ecbf 100644 --- a/shesha-reactjs/src/designer-components/markdown/index.tsx +++ b/shesha-reactjs/src/designer-components/markdown/index.tsx @@ -1,17 +1,17 @@ -import { EditOutlined } from '@ant-design/icons'; -import React from 'react'; -import { FormMarkup, IInputStyles } from '@/providers/form/models'; -import settingsFormJson from './settingsForm.json'; -import { IToolboxComponent } from '@/interfaces'; -import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; - -import { IMarkdownProps } from './interfaces'; -import Markdown from './markdown'; import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; +import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; +import { IToolboxComponent } from '@/interfaces'; +import { IInputStyles } from '@/providers/form/models'; +import { removeUndefinedProps } from '@/utils/object'; +import { EditOutlined } from '@ant-design/icons'; +import React, { CSSProperties } from 'react'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; - -const settingsForm = settingsFormJson as FormMarkup; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { IMarkdownProps } from './interfaces'; +import Markdown from './markdown'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; const MarkdownComponent: IToolboxComponent = { type: 'markdown', @@ -20,31 +20,53 @@ const MarkdownComponent: IToolboxComponent = { isInput: false, isOutput: true, Factory: ({ model }) => { + const { allStyles, content: contentProp } = model; + + const additionalStyles: CSSProperties = removeUndefinedProps({ + ...allStyles?.stylingBoxAsCSS, + ...allStyles?.borderStyles, + ...allStyles?.fontStyles, + ...allStyles?.backgroundStyles, + ...allStyles?.shadowStyles, + ...allStyles?.jsStyle, + }); + return ( - + {(value) => { - const content = model.content || value; - return ; + const content = contentProp || value; + return ( +
+ +
+ ); }}
); }, - settingsFormMarkup: settingsForm, - validateSettings: model => validateConfigurableComponentSettings(settingsForm, model), - initModel: model => ({ + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + initModel: (model) => ({ ...model, }), - migrator: (m) => m - .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev)) as IMarkdownProps) - .add(1, (prev) => ({...migrateFormApi.properties(prev)})) - .add(2, (prev) => { - const styles: IInputStyles = { - style: prev.style - }; - - return { ...prev, desktop: {...styles}, tablet: {...styles}, mobile: {...styles} }; - }) - , + migrator: (m) => + m + .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev)) as IMarkdownProps) + .add(1, (prev) => ({ ...migrateFormApi.properties(prev) })) + .add(2, (prev) => { + const styles: IInputStyles = { + size: prev.size, + hideBorder: prev.hideBorder, + borderSize: prev.borderSize, + borderRadius: prev.borderRadius, + borderColor: prev.borderColor, + fontSize: prev.fontSize, + fontColor: prev.fontColor, + backgroundColor: prev.backgroundColor, + }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }) + .add(3, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), }; export default MarkdownComponent; diff --git a/shesha-reactjs/src/designer-components/markdown/interfaces.ts b/shesha-reactjs/src/designer-components/markdown/interfaces.ts index af005a7e47..fb77eccbde 100644 --- a/shesha-reactjs/src/designer-components/markdown/interfaces.ts +++ b/shesha-reactjs/src/designer-components/markdown/interfaces.ts @@ -1,7 +1,21 @@ import { IConfigurableFormComponent } from '@/interfaces'; - export interface IMarkdownProps extends IConfigurableFormComponent { content: string; textColor?: string; remarkPlugins?: string[]; + style: string; + fontSize: number; + fontColor: string; + backgroundColor: string; + hideBorder: boolean; + borderSize: number; + borderRadius: number; + borderColor: string; +} + +export interface IMarkdownComponentProps { + style: React.CSSProperties; + content: string; + textColor?: string; + remarkPlugins?: string[]; } diff --git a/shesha-reactjs/src/designer-components/markdown/markdown.tsx b/shesha-reactjs/src/designer-components/markdown/markdown.tsx index 75b575c9b2..9ab5a0fa7e 100644 --- a/shesha-reactjs/src/designer-components/markdown/markdown.tsx +++ b/shesha-reactjs/src/designer-components/markdown/markdown.tsx @@ -2,8 +2,8 @@ import { Alert, Skeleton } from 'antd'; import React, { FC, lazy } from 'react'; import { useFormData, useGlobalState, useSubForm } from '@/providers'; import { useForm } from '@/providers/form'; -import { evaluateString, getStyle } from '@/providers/form/utils'; -import { IMarkdownProps } from './interfaces'; +import { evaluateString } from '@/providers/form/utils'; +import { IMarkdownComponentProps } from './interfaces'; import './styles.module.scss'; // This manually loads github-markdown-css, as per https://raw.githubusercontent.com/sindresorhus/github-markdown-css/gh-pages/github-markdown.css let SyntaxHighlighter; @@ -11,22 +11,22 @@ let dark; let remarkGfm; const ReactMarkdown = lazy(async () => { - import('remark-gfm').then(module => { + import('remark-gfm').then((module) => { remarkGfm = module?.default; }); - import('react-syntax-highlighter').then(module => { + import('react-syntax-highlighter').then((module) => { SyntaxHighlighter = module?.Prism; }); - import('react-syntax-highlighter/dist/esm/styles/prism').then(module => { + import('react-syntax-highlighter/dist/esm/styles/prism').then((module) => { dark = module?.dark; }); return import('react-markdown'); }); -const Markdown: FC = model => { +const Markdown: FC = (model) => { const { formMode } = useForm(); // NOTE: to be replaced with a generic context implementation const { value: subFormData } = useSubForm(false) ?? {}; @@ -47,10 +47,11 @@ const Markdown: FC = model => { ) : ( Loading editor...
}> -
+
{ + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + propertyName: 'content', + label: 'Content', + parentId: commonTabId, + inputs: [ + { + type: 'codeEditor', + id: nanoid(), + propertyName: 'content', + label: 'Content', + parentId: commonTabId, + hideLabel: false, + language: 'markdown', + wrapInTemplate: false, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'hidden', + label: 'Hide', + parentId: commonTabId, + hideLabel: false, + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + { + key: '4', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: nanoid(), + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: 'styleRouter', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'fontStylePnl', + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: 'Controls text thickness (light, normal, bold, etc.)', + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: 'styleRouter', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'dimensionsStylePnl', + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Width', + width: 85, + propertyName: 'dimensions.width', + icon: 'widthIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minWidth', + icon: 'minWidthIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxWidth', + icon: 'maxWidthIcon', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'borderStylePnl', + hidden: { + _code: + 'return !getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.border?.hideBorder);', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'button', + id: nanoid(), + label: 'Border', + hideLabel: true, + propertyName: 'border.hideBorder', + icon: 'EyeOutlined', + iconAlt: 'EyeInvisibleOutlined', + }, + ], + }) + .addContainer({ + id: nanoid(), + parentId: 'borderStylePnl', + components: getBorderInputs() as any, + }) + .addContainer({ + id: nanoid(), + parentId: 'borderRadiusStyleRow', + components: getCornerInputs() as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: 'backgroundStylePnl', + label: 'Type', + jsSetting: false, + propertyName: 'background.type', + inputType: 'radio', + tooltip: 'Select a type of background', + buttonGroupOptions: [ + { + value: 'color', + icon: 'FormatPainterOutlined', + title: 'Color', + }, + { + value: 'gradient', + icon: 'BgColorsOutlined', + title: 'Gradient', + }, + { + value: 'image', + icon: 'PictureOutlined', + title: 'Image', + }, + { + value: 'url', + icon: 'LinkOutlined', + title: 'URL', + }, + { + value: 'storedFile', + icon: 'DatabaseOutlined', + title: 'Stored File', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + propertyName: 'background.color', + hideLabel: true, + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'multiColorPicker', + id: nanoid(), + propertyName: 'background.gradient.colors', + label: 'Colors', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', + _mode: 'code', + _value: false, + } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'background.url', + jsSetting: false, + label: 'URL', + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: 'Image', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: 'background.storedFile.id', + label: 'File ID', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStyleRow', + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + inline: true, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: 'Size', + hideLabel: true, + propertyName: 'background.size', + dropdownOptions: [ + { + value: 'cover', + label: 'Cover', + }, + { + value: 'contain', + label: 'Contain', + }, + { + value: 'auto', + label: 'Auto', + }, + ], + }, + { + type: 'customDropdown', + id: nanoid(), + label: 'Position', + hideLabel: true, + propertyName: 'background.position', + dropdownOptions: [ + { + value: 'center', + label: 'Center', + }, + { + value: 'top', + label: 'Top', + }, + { + value: 'left', + label: 'Left', + }, + { + value: 'right', + label: 'Right', + }, + { + value: 'bottom', + label: 'Bottom', + }, + { + value: 'top left', + label: 'Top Left', + }, + { + value: 'top right', + label: 'Top Right', + }, + { + value: 'bottom left', + label: 'Bottom Left', + }, + { + value: 'bottom right', + label: 'Bottom Right', + }, + ], + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStyleRow', + inputs: [ + { + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'shadowStylePnl', + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + icon: 'offsetHorizontalIcon', + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: '5', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/markdown/utils.ts b/shesha-reactjs/src/designer-components/markdown/utils.ts new file mode 100644 index 0000000000..f84c6d6c89 --- /dev/null +++ b/shesha-reactjs/src/designer-components/markdown/utils.ts @@ -0,0 +1,9 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + return { + background: { type: 'color', color: '#fff' }, + font: { weight: '400', size: 14, color: '#000', type: 'Segoe UI' }, + dimensions: { width: '100%', height: 'auto', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/metadataEditor/metadataEditorModal.tsx b/shesha-reactjs/src/designer-components/metadataEditor/metadataEditorModal.tsx index 0390736ce1..fa21953006 100644 --- a/shesha-reactjs/src/designer-components/metadataEditor/metadataEditorModal.tsx +++ b/shesha-reactjs/src/designer-components/metadataEditor/metadataEditorModal.tsx @@ -6,6 +6,7 @@ import { Item } from '@/components/modelConfigurator/propertiesEditor/renderer-n import { ModelItemProperties } from '@/components/modelConfigurator/propertiesEditor/renderer-new/modelItemProperties'; import { IMetadataEditorProps } from './interfaces'; import { ListEditorRenderer } from '@/components/listEditorRenderer'; +import { ListItem } from '@/components/listEditor/models'; type ItemType = IModelItem; @@ -41,7 +42,7 @@ export const MetadataEditorModal: FC = ({ value, onCh content: , }} > - + value={value} onChange={onChange} initNewItem={makeNewItem} diff --git a/shesha-reactjs/src/designer-components/multiColorInput/index.tsx b/shesha-reactjs/src/designer-components/multiColorInput/index.tsx new file mode 100644 index 0000000000..5a36fde8eb --- /dev/null +++ b/shesha-reactjs/src/designer-components/multiColorInput/index.tsx @@ -0,0 +1,72 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Row, Tag } from 'antd'; +import { nanoid } from '@/utils/uuid'; +import { PlusOutlined } from '@ant-design/icons'; +import { useTheme } from '@/providers'; +import FormItem from 'antd/es/form/FormItem'; +import { removeUndefinedProps } from '@/utils/object'; +import { SettingInput } from '../settingsInput/settingsInput'; +import { gradientDirectionOptions } from '../_settings/utils/background/utils'; +import { InputRow } from '../settingsInputRow'; + +export const MultiColorInput = ({ value = {}, onChange, readOnly, propertyName }) => { + const { theme } = useTheme(); + const [colors, setColors] = useState(value); + + useEffect(() => { + if (!value || Object.entries(value).length === 0) { + const defaultColors = { '1': theme.application.primaryColor, '2': '#fff' }; + onChange(defaultColors); + setColors(defaultColors); + } + }, [value, onChange, theme.application.primaryColor]); + + return ( + <> + + {Object.entries(removeUndefinedProps(colors)).map(([id]) => { + return ( + { + onChange({ ...value, [id]: undefined }); + setColors({ ...value, [id]: undefined }); + }} + > + + + ); + })} + + + + + + + + + ); +}; + diff --git a/shesha-reactjs/src/designer-components/notes/notesComponent.tsx b/shesha-reactjs/src/designer-components/notes/notesComponent.tsx index 45745a3d65..988ac13534 100644 --- a/shesha-reactjs/src/designer-components/notes/notesComponent.tsx +++ b/shesha-reactjs/src/designer-components/notes/notesComponent.tsx @@ -1,15 +1,18 @@ import { IToolboxComponent } from '@/interfaces'; -import { FormMarkup, IConfigurableFormComponent } from '@/providers/form/models'; +import { IConfigurableFormComponent } from '@/providers/form/models'; import { FormOutlined } from '@ant-design/icons'; -import settingsFormJson from './settingsForm.json'; +import { getSettings } from './settingsForm'; import { NotesRenderer } from '@/components'; -import { useFormData } from '@/providers'; -import { evaluateValue, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { useForm, useFormData, useGlobalState, useHttpClient } from '@/providers'; +import { evaluateValue, executeScript, validateConfigurableComponentSettings } from '@/providers/form/utils'; import React from 'react'; import NotesProvider from '@/providers/notes'; import { migrateCustomFunctions, migrateFunctionToProp, migratePropertyName, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getFormApi } from '@/providers/form/formApi'; +import { App } from 'antd'; +import moment from 'moment'; export interface INotesProps extends IConfigurableFormComponent { ownerId: string; @@ -19,44 +22,65 @@ export interface INotesProps extends IConfigurableFormComponent { savePlacement?: 'left' | 'right'; autoSize?: boolean; allowDelete?: boolean; + onCreated?: string; + category?: string; } -const settingsForm = settingsFormJson as FormMarkup; - const NotesComponent: IToolboxComponent = { type: 'notes', isInput: false, name: 'Notes', icon: , Factory: ({ model }) => { + const httpClient = useHttpClient(); + const form = useForm(); const { data } = useFormData(); - - // TODO:: Change to use Mustache - const ownerId = evaluateValue(model.ownerId, { data }); + const { globalState, setState: setGlobalState } = useGlobalState(); + const { message } = App.useApp(); if (model.hidden) return null; + const ownerId = evaluateValue(`${model.ownerId}`, { data: data, globalState }); + + const onCreated = (createdNotes: Array) => { + if (!model.onCreated) + return; + + executeScript(model?.onCreated, { + createdNotes, + data, + form: getFormApi(form), + globalState, + http: httpClient, + message, + moment, + setGlobalState + }); + }; + return ( - + ); }, - validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings, model), initModel: (model) => { const customModel: INotesProps = { ...model, ownerId: '{data.id}', ownerType: '', + hideLabel: true, }; return customModel; }, - settingsFormMarkup: settingsForm, + settingsFormMarkup: (data) => getSettings(data), migrator: (m) => m .add(0, (prev) => migratePropertyName( @@ -71,4 +95,4 @@ const NotesComponent: IToolboxComponent = { , }; -export default NotesComponent; +export default NotesComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/notes/settingsForm.ts b/shesha-reactjs/src/designer-components/notes/settingsForm.ts new file mode 100644 index 0000000000..42d098f2c4 --- /dev/null +++ b/shesha-reactjs/src/designer-components/notes/settingsForm.ts @@ -0,0 +1,221 @@ +import { DesignerToolbarSettings } from "@/index"; +import { nanoid } from "@/utils/uuid"; +import { FormLayout } from "antd/lib/form/Form"; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'componentName', + label: 'Component Name', + jsSetting: false, + validate: { + required: true + } + }, + { + id: nanoid(), + propertyName: 'category', + label: 'Notes Category', + type: 'textField', + tooltip: 'This is used to group notes into categories', + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'autoSize', + label: 'Auto Size', + jsSetting: true + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'allowDelete', + label: 'Allow Delete', + jsSetting: true + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + type: 'editModeSelector', + propertyName: 'editMode', + label: 'Edit Mode', + parentId: commonTabId, + defaultValue: 'inherited', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true + } + ] + }) + .toJson() + ] + }, + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'ownerId', + label: 'Owner ID', + jsSetting: true, + validate: { + required: true + } + }, + { + id: nanoid(), + propertyName: 'ownerType', + type: 'autocomplete', + parentId: nanoid(), + label: 'Owner Type', + labelAlign: 'right', + dataSourceType: 'url', + dataSourceUrl: '/api/services/app/Metadata/EntityTypeAutocomplete', + mode: 'single', + jsSetting: true, + validate: { + required: true + } + } + ] + }) + .toJson() + ] + }, + { + key: 'events', + title: 'Events', + id: eventsTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: eventsTabId, + inputs: [ + { + id: nanoid(), + type: 'codeEditor', + propertyName: 'onCreated', + label: 'On Created', + labelAlign: 'right', + parentId: eventsTabId, + tooltip: 'Triggered after successfully creating a new note (access notes using createdNotes array)', + exposedVariables: [` { name: 'createdNotes', description: 'Created note', type: 'array' },`] + } + ] + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: appearanceTabId, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + propertyName: 'savePlacement', + label: 'Buttons Layout', + tooltip: 'This is used to place the save button (Left, Right).', + defaultValue: 'left', + jsSetting: true, + dropdownOptions: [ + { value: 'left', label: 'Left' }, + { value: 'right', label: 'Right' } + ] + } + ] + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + parentId: securityTabId, + validate: {}, + jsSetting: true + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + layout: 'vertical' as FormLayout, + colon: false, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/numberField/control.tsx b/shesha-reactjs/src/designer-components/numberField/control.tsx deleted file mode 100644 index 9f65272ea2..0000000000 --- a/shesha-reactjs/src/designer-components/numberField/control.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { InputNumber, InputNumberProps } from 'antd'; -import React, { FC } from 'react'; -import { customOnChangeValueEventHandler } from '@/components/formDesigner/components/utils'; -import { getStyle, useAvailableConstantsData } from '@/providers/form/utils'; -import { INumberFieldComponentProps } from './interfaces'; - -interface IProps { - disabled: boolean; - model: INumberFieldComponentProps; - onChange?: Function; - value?: number; -} - -const NumberFieldControl: FC = ({ disabled, model, onChange, value }) => { - const allData = useAvailableConstantsData(); - - const style = model.style; - - const inputProps: InputNumberProps = { - className: 'sha-number-field', - disabled: disabled, - variant: model.hideBorder ? 'borderless' : undefined, - min: model?.min, - max: model?.max, - placeholder: model?.placeholder, - size: model?.size, - style: style ? getStyle(style, allData.data, allData.globalState) : { width: '100%' }, - step: model?.highPrecision ? model?.stepNumeric : model?.stepNumeric, - ...customOnChangeValueEventHandler(model, allData, onChange), - defaultValue: model?.defaultValue, - changeOnWheel: false, - }; - - return ; -}; - -export default NumberFieldControl; diff --git a/shesha-reactjs/src/designer-components/numberField/interfaces.ts b/shesha-reactjs/src/designer-components/numberField/interfaces.ts index 1e5de234a6..900c02c608 100644 --- a/shesha-reactjs/src/designer-components/numberField/interfaces.ts +++ b/shesha-reactjs/src/designer-components/numberField/interfaces.ts @@ -1,6 +1,7 @@ -import { IConfigurableFormComponent } from '@/providers/form/models'; +import { IconType } from '@/components'; +import { IConfigurableFormComponent, IInputStyles, IStyleType } from '@/providers/form/models';; -export interface INumberFieldComponentProps extends IConfigurableFormComponent { +export interface INumberFieldComponentProps extends IConfigurableFormComponent, IInputStyles, IStyleType { hideBorder?: boolean; min?: number; max?: number; @@ -8,4 +9,8 @@ export interface INumberFieldComponentProps extends IConfigurableFormComponent { stepNumeric?: number; stepString?: string; placeholder?: string; + prefix?: string; + suffix?: string; + suffixIcon?: IconType; + prefixIcon?: IconType; } diff --git a/shesha-reactjs/src/designer-components/numberField/numberField.tsx b/shesha-reactjs/src/designer-components/numberField/numberField.tsx index ce08cf7f86..79d6216dc2 100644 --- a/shesha-reactjs/src/designer-components/numberField/numberField.tsx +++ b/shesha-reactjs/src/designer-components/numberField/numberField.tsx @@ -4,10 +4,9 @@ import ConfigurableFormItem from '@/components/formDesigner/components/formItem' import ReadOnlyDisplayFormItem from '@/components/readOnlyDisplayFormItem'; import { IToolboxComponent } from '@/interfaces'; import { DataTypes } from '@/interfaces/dataTypes'; -import { useForm, useGlobalState, useMetadata } from '@/providers'; -import { FormMarkup, IInputStyles } from '@/providers/form/models'; +import { IInputStyles, useMetadata } from '@/providers'; +import { FormMarkup } from '@/providers/form/models'; import { evaluateString, validateConfigurableComponentSettings } from '@/providers/form/utils'; -import NumberFieldControl from './control'; import { INumberFieldComponentProps } from './interfaces'; import settingsFormJson from './settingsForm.json'; import { migratePropertyName, migrateCustomFunctions, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; @@ -16,10 +15,23 @@ import { getDataProperty } from '@/utils/metadata'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { asPropertiesArray } from '@/interfaces/metadata'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { getEventHandlers, customOnChangeValueEventHandler } from '@/components/formDesigner/components/utils'; +import { defaultStyles } from './utils'; +import { useStyles } from './styles'; +import { InputNumber, InputNumberProps } from 'antd'; +import { ShaIcon } from '@/components'; -const settingsForm = settingsFormJson as FormMarkup; +const settingsForm = settingsFormJson as unknown as FormMarkup; +const suffixStyle = { color: 'rgba(0,0,0,.45)' }; -const NumberFieldComponent: IToolboxComponent = { +interface INumberFieldComponentCalulatedValues { + defaultValue?: string; + eventHandlers?: any; +} + +const NumberFieldComponent: IToolboxComponent = { type: 'numberField', isInput: true, isOutput: true, @@ -27,51 +39,88 @@ const NumberFieldComponent: IToolboxComponent = { name: 'Number field', icon: , dataTypeSupported: ({ dataType }) => dataType === DataTypes.number, - Factory: ({ model }) => { + calculateModel: (model, allData) => { + return { + defaultValue: model?.defaultValue + ? evaluateString(model?.defaultValue, { formData: allData.data, formMode: allData.form.formMode, globalState: allData.globalState }) + : undefined, + eventHandlers: {...getEventHandlers(model, allData), ...customOnChangeValueEventHandler(model, allData)}, + }; + }, + Factory: ({ model, calculatedModel }) => { + const { styles } = useStyles({ + fontFamily: model?.font?.type, + fontWeight: model?.font?.weight, + textAlign: model?.font?.align, + color: model?.font?.color, + fontSize: model?.font?.size, + }); + const { properties: metaProperties } = useMetadata(false)?.metadata ?? {}; const properties = asPropertiesArray(metaProperties, []); - const { formMode, formData } = useForm(); - const { globalState } = useGlobalState(); + const inputProps: InputNumberProps = { + className: 'sha-number-field', + disabled: model.readOnly, + variant: model.hideBorder ? 'borderless' : undefined, + min: model?.validate?.minValue ?? 0, + max: model?.validate?.maxValue ?? Number.MAX_SAFE_INTEGER, + placeholder: model?.placeholder, + size: model?.size, + style: model.style ? model.allStyles.jsStyle : { width: '100%' }, + step: model?.highPrecision ? model?.stepNumeric : model?.stepNumeric, + ...calculatedModel.eventHandlers, + defaultValue: calculatedModel.defaultValue, + changeOnWheel: false, + prefix: <>{model.prefix}{model.prefixIcon && }, + suffix: <>{model.suffix}{model.suffixIcon && }, + }; return ( - + {(value, onChange) => { - return model.readOnly ? ( - { + customEvents.onChange(args[0]); + onChange(...args); + }; + return model.readOnly + ? + : - ) : ( - - ); + ; }} ); }, - settingsFormMarkup: settingsForm, + settingsFormMarkup: (data) => getSettings(data), initModel: (model) => ({ ...model, }), - migrator: (m) => m - .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(1, (prev) => migrateVisibility(prev)) - .add(2, (prev) => migrateReadOnly(prev)) - .add(3, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) - .add(4, (prev) => { - const styles: IInputStyles = { - size: prev.size, - hideBorder: prev.hideBorder, - stylingBox: prev.stylingBox, - style: prev.style - }; + migrator: (m) => + m + .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(1, (prev) => migrateVisibility(prev)) + .add(2, (prev) => migrateReadOnly(prev)) + .add(3, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) + .add(4, (prev) => { + const styles: IInputStyles = { + size: prev.size, + hideBorder: prev.hideBorder, + stylingBox: prev.stylingBox, + style: prev.style, + }; + + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }) + .add(5, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), - return { ...prev, desktop: {...styles}, tablet: {...styles}, mobile: {...styles} }; - }) - , validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), linkToModelMetadata: (model, metadata): INumberFieldComponentProps => { return { diff --git a/shesha-reactjs/src/designer-components/numberField/settingsForm.ts b/shesha-reactjs/src/designer-components/numberField/settingsForm.ts new file mode 100644 index 0000000000..37fcbd24d9 --- /dev/null +++ b/shesha-reactjs/src/designer-components/numberField/settingsForm.ts @@ -0,0 +1,813 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { INumberFieldComponentProps } from './interfaces'; +import { FormLayout } from 'antd/lib/form/Form'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { + backgroundTypeOptions, + positionOptions, + repeatOptions, + sizeOptions, +} from '../_settings/utils/background/utils'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data: INumberFieldComponentProps) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const validationTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'placeholder', + label: 'Placeholder', + size: 'small', + jsSetting: true, + }, + { + type: 'textArea', + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + defaultValue: 'inherited', + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'defaultValue', + label: 'Default Value', + tooltip: 'Enter default value of component. (formData, formMode, globalState) are exposed', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'prefix', + label: 'Prefix', + jsSetting: true, + }, + { + type: 'iconPicker', + id: nanoid(), + propertyName: 'prefixIcon', + label: 'Prefix Icon', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'suffix', + label: 'Suffix', + jsSetting: true, + }, + { + type: 'iconPicker', + id: nanoid(), + propertyName: 'suffixIcon', + label: 'Suffix Icon', + jsSetting: true, + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'highPrecision', + label: 'High Precision', + tooltip: 'To support high precision decimals support', + version: 2, + defaultValue: false, + }) + + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.highPrecision)', + _mode: 'code', + _value: false, + }, + inputs: [ + { + id: nanoid(), + type: 'numberField', + propertyName: 'stepNumeric', + parentId: nanoid(), + label: 'Step', + defaultValue: 1, + validate: {}, + settingsValidationErrors: [], + }, + ], + }) + + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return !getSettingValue(data?.highPrecision)', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'textField', + propertyName: 'stepString', + parentId: nanoid(), + label: 'Step', + defaultValue: 0.1, + validate: {}, + settingsValidationErrors: [], + }, + ], + }) + + .toJson(), + ], + }, + { + key: '2', + title: 'Validation', + id: validationTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'validate.required', + label: 'Required', + inputType: 'switch', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: validationTabId, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: validationTabId, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'validate.minValue', + label: 'Min Value', + size: 'small', + jsSetting: true, + }, + { + type: 'numberField', + id: nanoid(), + propertyName: 'validate.maxValue', + label: 'Max Value', + size: 'small', + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + { + key: '3', + title: 'Events', + id: eventsTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onChangeCustom', + label: 'On Change', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on changing of event.', + parentId: eventsTabId, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onFocusCustom', + label: 'On Focus', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on focus of event.', + parentId: eventsTabId, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onBlurCustom', + label: 'On Blur', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on blur of event.', + parentId: eventsTabId, + }) + .toJson(), + ], + }, + { + key: '4', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: nanoid(), + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: 'styleRouter', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'fontStylePnl', + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: 'Controls text thickness (light, normal, bold, etc.)', + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: 'styleRouter', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'dimensionsStylePnl', + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Width', + width: 85, + propertyName: 'dimensions.width', + icon: 'widthIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minWidth', + icon: 'minWidthIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxWidth', + icon: 'maxWidthIcon', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'dimensionsStylePnl', + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Height', + width: 85, + propertyName: 'dimensions.height', + icon: 'heightIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minHeight', + icon: 'minHeightIcon', + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxHeight', + icon: 'maxHeightIcon', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: 'borderStylePnl', + components: getBorderInputs() as any, + }) + .addContainer({ + id: nanoid(), + parentId: 'borderStylePnl', + components: getCornerInputs() as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: 'backgroundStylePnl', + label: 'Type', + jsSetting: false, + propertyName: 'background.type', + inputType: 'radio', + tooltip: 'Select a type of background', + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + propertyName: 'background.color', + hideLabel: true, + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'multiColorPicker', + id: nanoid(), + propertyName: 'background.gradient.colors', + label: 'Colors', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', + _mode: 'code', + _value: false, + } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'background.url', + jsSetting: false, + label: 'URL', + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: 'Image', + jsSetting: false, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', + _mode: 'code', + _value: false, + } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStylePnl', + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: 'background.storedFile.id', + label: 'File ID', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStyleRow', + inline: true, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: 'Size', + hideLabel: true, + propertyName: 'background.size', + customTooltip: + 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: 'Position', + hideLabel: true, + customTooltip: + 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: 'background.position', + dropdownOptions: positionOptions, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStyleRow', + inputs: [ + { + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }, + ], + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', + _mode: 'code', + _value: false, + } as any, + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'shadowStylePnl', + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: 'offsetHorizontalIcon', + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: '5', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/numberField/styles.ts b/shesha-reactjs/src/designer-components/numberField/styles.ts new file mode 100644 index 0000000000..dbb44bad1a --- /dev/null +++ b/shesha-reactjs/src/designer-components/numberField/styles.ts @@ -0,0 +1,21 @@ +import { createStyles } from '@/styles'; + +export const useStyles = createStyles(({ css, cx }, { fontWeight, fontFamily, textAlign, color, fontSize }) => { + const numberField = cx( + 'sha-ant-input-number-input', + css` + + + .ant-input-number-input { + --ant-color-text: ${color} !important; + --ant-font-size: ${fontSize} !important; + font-weight: ${fontWeight} !important; + font-family: ${fontFamily}; + text-align: ${textAlign}; + } + ` + ); + return { + numberField, + }; +}); diff --git a/shesha-reactjs/src/designer-components/numberField/utils.ts b/shesha-reactjs/src/designer-components/numberField/utils.ts new file mode 100644 index 0000000000..2e1c2afd8c --- /dev/null +++ b/shesha-reactjs/src/designer-components/numberField/utils.ts @@ -0,0 +1,33 @@ +import { IStyleType } from '@/index'; + +export const defaultStyles = (): IStyleType => { + return { + background: { type: 'color', color: '#fff' }, + font: { + weight: '400', + size: 14, + color: '#000', + type: 'Segoe UI', + }, + border: { + border: { + all: { + width: 1, + style: 'solid', + color: '#d9d9d9', + }, + }, + radius: { all: 8 }, + borderType: 'all', + radiusType: 'all', + }, + dimensions: { + width: '100%', + height: '32px', + minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto', + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/passwordCombo/index.tsx b/shesha-reactjs/src/designer-components/passwordCombo/index.tsx index 9fdbfb3e95..7647b7549c 100644 --- a/shesha-reactjs/src/designer-components/passwordCombo/index.tsx +++ b/shesha-reactjs/src/designer-components/passwordCombo/index.tsx @@ -1,23 +1,25 @@ -import React, { Fragment } from 'react'; -import settingsFormJson from './settingsForm.json'; +import React from 'react'; import { confirmModel, + defaultStyles, getDefaultModel, getFormItemProps, getInputProps, IPasswordComponentProps - } from './utils'; +} from './utils'; import { DataTypes, StringFormats } from '@/interfaces/dataTypes'; -import { FormMarkup, IInputStyles } from '@/providers/form/models'; import { IToolboxComponent } from '@/interfaces'; import { LockOutlined } from '@ant-design/icons'; import { migrateCustomFunctions, migratePropertyName, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; import { PasswordCombo } from './passwordCombo'; -import { useForm } from '@/providers'; +import { IInputStyles, useForm } from '@/providers'; import { validateConfigurableComponentSettings } from '@/providers/form/utils'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; - -const settingsForm = settingsFormJson as FormMarkup; +import { ValidationErrors } from '@/components'; +import { isValidGuid } from '@/components/formDesigner/components/utils'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { getSettings } from './settingsForm'; +import { useStyles } from './styles'; const PasswordComboComponent: IToolboxComponent = { type: 'passwordCombo', @@ -33,26 +35,34 @@ const PasswordComboComponent: IToolboxComponent = { const options = { hidden: model.hidden, formData }; + const { styles } = useStyles({ fontFamily: model?.font?.type, fontWeight: model?.font?.weight, textAlign: model?.font?.align }); + + + if (model?.background?.type === 'storedFile' && model?.background.storedFile?.id && !isValidGuid(model?.background.storedFile.id)) { + return ; + } + return ( - - - + + ); }, - settingsFormMarkup: settingsForm, - validateSettings: model => validateConfigurableComponentSettings(settingsForm, model), + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), migrator: (m) => m .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) .add(1, (prev) => migrateReadOnly(prev)) - .add(2, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) + .add(2, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) .add(6, (prev) => { const styles: IInputStyles = { size: prev.size, @@ -60,9 +70,9 @@ const PasswordComboComponent: IToolboxComponent = { style: prev.style }; - return { ...prev, desktop: {...styles}, tablet: {...styles}, mobile: {...styles} }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; }) - , + .add(7, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()), editMode: 'inherited' })), }; export default PasswordComboComponent; diff --git a/shesha-reactjs/src/designer-components/passwordCombo/passwordCombo.tsx b/shesha-reactjs/src/designer-components/passwordCombo/passwordCombo.tsx index bcddf3cdd6..06cd275fa1 100644 --- a/shesha-reactjs/src/designer-components/passwordCombo/passwordCombo.tsx +++ b/shesha-reactjs/src/designer-components/passwordCombo/passwordCombo.tsx @@ -2,7 +2,7 @@ import { FormItemProps, FormProps, InputProps } from 'antd'; import React, { FC, useState } from 'react'; import PasswordInputCombo from '@/components/passwordInputCombo'; -interface IProps { +export interface IProps { readonly confirmPlaceholder: string; readonly errorMessage?: string; readonly formItemProps: FormItemProps; @@ -11,6 +11,8 @@ interface IProps { readonly inputProps: InputProps; readonly passwordLength: number; readonly placeholder: string; + readonly style?: React.CSSProperties; + readonly className?: string; } interface IState { @@ -32,6 +34,7 @@ export const PasswordCombo: FC = ({ formItemConfirmProps, formProps, passwordLength, + style }) => { const [state, setState] = useState(INIT_STATE); const { newPassword, repeatPassword } = state; @@ -54,6 +57,7 @@ export const PasswordCombo: FC = ({ passwordLength={passwordLength} errorMessage={errorMessage} isPasswordOk={_e => { /*nop*/ }} + style={style} /> ); }; diff --git a/shesha-reactjs/src/designer-components/passwordCombo/settingsForm.json b/shesha-reactjs/src/designer-components/passwordCombo/settingsForm.json deleted file mode 100644 index e278fe5f35..0000000000 --- a/shesha-reactjs/src/designer-components/passwordCombo/settingsForm.json +++ /dev/null @@ -1,332 +0,0 @@ -{ - - "components": [ - { - "id": "b8954bf6-f76d-4139-a850-c99bf06c8b69", - "type": "collapsiblePanel", - "propertyName": "pnlDisplay", - "label": "Display", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - - "isDynamic": false, - "version": 1, - "expandIconPosition": "start", - "collapsible": "header", - "collapsedByDefault": false, - "ghost": true, - "header": { - "id": "2JkE1UV7XdEQgLMcKGSq3", - "components": [] - }, - "content": { - "id": "pnl54bf6-f76d-4139-a850-c89bf06c8b69", - "components": [ - { - - "id": "f67bf678-f667-4620-8eba-b0fd1cdd3271", - "type": "propertyAutocomplete", - "propertyName": "propertyName", - "parentId": "root", - "label": "Property name", - "validate": { - "required": true - } - }, - { - "id": "f86bdc91-2c07-4214-ba2b-54799ddbbb56", - "type": "textField", - "propertyName": "label", - "parentId": "root", - "label": "Label" - }, - { - "id": "e93ed37c-2bd5-4ced-ac68-73797b516bfc", - "type": "textField", - "propertyName": "confirmLabel", - "parentId": "root", - "label": "Label: Confirmation" - }, - { - "id": "5bb92b3f-ffed-4dca-8c46-887501ad3d87", - "type": "dropdown", - "propertyName": "labelAlign", - "parentId": "root", - "label": "Label align", - "values": [ - { - "label": "left", - "value": "left", - "id": "1641ad88-5ff9-4c06-a272-ccc18c81b2b5" - }, - { - "label": "right", - "value": "right", - "id": "33796247-1cb7-43d4-81b6-59acc02dd18e" - } - ], - "dataSourceType": "values" - }, - { - "id": "1336690b-7917-4d39-a530-ee3025821801", - "type": "textField", - "propertyName": "placeholder", - "parentId": "root", - "label": "Placeholder" - }, - { - "id": "84f127cb-20f4-4af3-a60d-86eadb41fec2", - "type": "textField", - "propertyName": "confirmPlaceholder", - "parentId": "root", - "label": "Placeholder: Confirmation" - }, - { - "id": "605af7f5-9a9e-4ace-a42b-46d6004af9ce", - "type": "textArea", - "propertyName": "description", - "parentId": "root", - "label": "Description" - }, - { - "id": "dff1b08b-2a0f-4182-b71c-385d2c0004d9", - "type": "textArea", - "propertyName": "confirmDescription", - "parentId": "root", - "label": "Description: Confirmation" - }, - { - "id": "2b2c4bcd-5607-4dd4-9899-5fa59bab06d0", - "type": "checkbox", - "propertyName": "hidden", - "parentId": "root", - "label": "Hidden" - }, - { - "id": "0393ea49-2fe9-4dc9-9cd8-ca9221b2aec1", - "type": "checkbox", - "propertyName": "hideLabel", - "parentId": "root", - "label": "Hide Label" - }, - { - "id": "ea219103-9299-499e-86bb-8c4490e1c759", - "type": "editModeSelector", - "propertyName": "editMode", - "parentId": "root", - "label": "Edit mode" - }] - } - }, - { - "id": "07356df9-8bb9-4d8b-bc8f-6919b316a060", - "type": "collapsiblePanel", - "propertyName": "pnlValidation", - "label": "Validation", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - - "isDynamic": false, - "version": 1, - "expandIconPosition": "start", - "collapsible": "header", - "collapsedByDefault": false, - "ghost": true, - "header": { - "id": "2JkE1UV7XdEQgLAcKGSq3", - "components": [] - }, - "content": { - "id": "pnl54bf6-f76d-4139-a850-c99bf06c8b69", - "components": [ - { - "id": "c8ac6cea-17ed-4ba4-abeb-11148d7c8144", - "type": "checkbox", - "propertyName": "validate.required", - "parentId": "root", - "label": "Required" - }, - { - "id": "dfcd525a-5c0a-49e0-9ef6-3175c5185de1", - "type": "numberField", - "propertyName": "minLength", - "parentId": "root", - "label": "Min Password Length", - "validate": {}, - "defaultValue": 4 - }, - { - "id": "cdd57f4e-83d6-46c7-89ac-2c12cace86d0", - "type": "textField", - "propertyName": "message", - "parentId": "root", - "label": "Message", - "validate": {} - }, - { - "id": "db276deb-2c5a-461a-9ea3-9d5d4d75a698", - "type": "codeEditor", - "propertyName": "validate.validator", - "parentId": "root", - "label": "Validator", - "validate": {}, - "labelAlign": "right", - "hidden": false, - "description": "Enter custom validator logic for form.item rules. Returns a Promise", - "settingsValidationErrors": [], - "exposedVariables": [ - { - "name": "rules", - "description": "Validate rules, only support customize validator", - "type": "object" - }, - { "name": "value", "description": "Component current value", "type": "object" }, - { "name": "callback", "description": "Callback called by component", "type": "function" }, - { "name": "data", "description": "Form values", "type": "object" } - ] - } - ] -} - }, - { - "id": "kp3afwRnhZk2958QCUvFTFeGWOokZY", - "type": "propertyRouter", - "propertyName": "propertyRouter1", - "componentName": "propertyRouter1", - "label": "Property router1", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "propertyRouteName": { - "_mode": "code", - "_code": " return contexts.canvasContext?.designerDevice || 'desktop';", - "_value": "" - }, - "components": [ - { - "id": "b8c06332-2e5e-40ad-a5f0-770de616ee3e", - "type": "collapsiblePanel", - "propertyName": "pnlStyle", - "label": "Style", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - - "isDynamic": false, - "version": 1, - "expandIconPosition": "start", - "collapsible": "header", - "collapsedByDefault": false, - "ghost": true, - "header": { - "id": "2JkE1UV7XdEQgLAcKGSq3", - "components": [] - }, - "content": { - "id": "54bf6-f76d-4139-a850-c99bf06c8b69", - "components": [ - { - "id": "bf95c7d8-aa8a-41d2-bf47-b47a7ce99ada", - "type": "checkbox", - "propertyName": "hideBorder", - "label": "Hide border", - "parentId": "root", - "hidden": false, - "validate": {} - }, - { - "id": "77711f71-809f-40d3-bdb1-d5fcd9c9cdef", - "type": "codeEditor", - "propertyName": "style", - "label": "Style", - "parentId": "root", - "validate": {}, - "settingsValidationErrors": [], - "description": "A script that returns the style of the element as an object. This should conform to CSSProperties", - "exposedVariables": [{ "name": "data", "description": "Form values", "type": "object" }] - }, - { - "id": "28242e9f-78d5-4396-859b-d13f88c29101", - "type": "dropdown", - "propertyName": "size", - "parentId": "root", - "label": "Size", - "allowClear": true, - "values": [ - { - "label": "Small", - "value": "small", - "id": "9377eece-22db-4948-9337-6028d16b6c4f" - }, - { - "label": "Middle", - "value": "middle", - "id": "3259a358-36de-49e4-a24f-d10328943771" - }, - { - "label": "Large", - "value": "large", - "id": "54e34b63-e1eb-457c-a5ac-b97ef8c23719" - } - ], - "dataSourceType": "values" - } - ] - } - } - ] - }, - { - "id": "6Vw9iiDw9d0MD_Rh5cbIn", - "type": "collapsiblePanel", - "propertyName": "pnlSecurity", - "label": "Security", - "labelAlign": "right", - "parentId": "root", - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "mbzE03mJnv_89oUvkmjPs", - "components": [] - }, - "content": { - "id": "SsWPpiJfTbgl4iHlFA_or", - "components": [ - { - "id": "1adea529-1f0c-4def-bd41-ee166a5dfcd7", - "type": "permissionAutocomplete", - "propertyName": "permissions", - "parentId": "SsWPpiJfTbgl4iHlFA_or", - "label": "Permissions", - "validate": {}, - "version": 1 - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - } - ], - "formSettings": { - "layout": "horizontal", - "colon": true, - "labelCol": { - "span": 8 - }, - "wrapperCol": { - "span": 16 - }, - "displayName": "DEFAULT_FORM_SETTINGS", - "__docgenInfo": { - "description": "Default form settings", - "displayName": "DEFAULT_FORM_SETTINGS", - "props": {} - } - } -} diff --git a/shesha-reactjs/src/designer-components/passwordCombo/settingsForm.ts b/shesha-reactjs/src/designer-components/passwordCombo/settingsForm.ts new file mode 100644 index 0000000000..8aea79f8c4 --- /dev/null +++ b/shesha-reactjs/src/designer-components/passwordCombo/settingsForm.ts @@ -0,0 +1,646 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { IPasswordComponentProps } from './utils'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data: IPasswordComponentProps) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const validationTabId = nanoid(); + const appearanceTabId = nanoid(); + const styleRouterId = nanoid(); + const fontStylePnlId = nanoid(); + const pnlDimensions = nanoid(); + const borderStylePnl = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.hideLabel);', + _mode: 'code', + _value: false + } as any, + inputs: [{ + type: 'textField', + id: nanoid(), + label: 'Label: Confirmation', + propertyName: 'confirmLabel', + size: 'small', + jsSetting: true, + }] + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'placeholder', + label: 'Placeholder', + size: 'small', + jsSetting: true, + parentId: commonTabId, + }) + .addSettingsInput({ + parentId: commonTabId, + id: nanoid(), + propertyName: 'confirmPlaceholder', + label: 'Placeholder: Confirmation', + size: 'small', + jsSetting: true, + }) + .addSettingsInput({ + inputType: 'textArea', + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + parentId: commonTabId, + }) + .addSettingsInput({ + inputType: 'textArea', + id: nanoid(), + parentId: commonTabId, + propertyName: 'confirmDescription', + label: 'Tooltip: Confirmation', + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + .toJson() + ] + }, + { + key: '2', + title: 'Validation', + id: validationTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'validate.required', + label: 'Required', + inputType: 'switch', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: validationTabId + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: validationTabId, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'minLength', + label: 'Min Length', + size: 'small', + jsSetting: true, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: validationTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'message', + label: 'Message', + size: 'small', + jsSetting: true, + }, + { + type: 'codeEditor', + id: nanoid(), + propertyName: 'validate.validator', + label: 'Validator', + labelAlign: 'right', + tooltip: 'Enter custom validator logic for form.item rules. Returns a Promise', + } + ], + }) + .toJson() + ] + }, + { + key: '3', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: fontStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: fontStylePnlId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: pnlDimensions, + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlDimensions, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: borderStylePnl, + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: borderStylePnl, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: borderStylePnl, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: nanoid(), + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + hideLabel: true, + propertyName: "background.size", + dropdownOptions: sizeOptions, + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + propertyName: "background.position", + dropdownOptions: positionOptions, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: nanoid(), + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson()] + }).toJson()] + }, + { + key: '5', + title: 'Security', + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: nanoid() + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/passwordCombo/styles.ts b/shesha-reactjs/src/designer-components/passwordCombo/styles.ts new file mode 100644 index 0000000000..b4b50b4df0 --- /dev/null +++ b/shesha-reactjs/src/designer-components/passwordCombo/styles.ts @@ -0,0 +1,17 @@ +import { createStyles } from '@/styles'; + +export const useStyles = createStyles(({ css, cx }, { fontWeight, fontFamily, textAlign, color, fontSize }) => { + + const passwordCombo = cx("sha-password-combo", css` + .ant-input { + --ant-color-text: ${color} !important; + --ant-font-size: ${fontSize} !important; + font-weight: ${fontWeight}; + font-family: ${fontFamily}; + text-align: ${textAlign}; + } + `); + return { + passwordCombo, + }; +}); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/passwordCombo/utils.ts b/shesha-reactjs/src/designer-components/passwordCombo/utils.ts index cb9372122d..941f5fccb8 100644 --- a/shesha-reactjs/src/designer-components/passwordCombo/utils.ts +++ b/shesha-reactjs/src/designer-components/passwordCombo/utils.ts @@ -3,14 +3,14 @@ import { FormItemProps, FormProps } from 'antd'; import { getFieldNameFromExpression, getValidationRules } from '@/formDesignerUtils'; import { getStyle } from '@/providers/form/utils'; import { IConfigurableFormComponent } from '@/interfaces'; -import { IFormSettings, SILENT_KEY } from '@/providers/form/models'; +import { IFormSettings, IStyleType, SILENT_KEY } from '@/providers/form/models'; export interface IFormPropOptions { hidden: boolean; formData: any; } -export interface IPasswordComponentProps extends IConfigurableFormComponent { +export interface IPasswordComponentProps extends IConfigurableFormComponent, IStyleType { placeholder?: string; confirmDescription?: string; confirmPlaceholder?: string; @@ -98,4 +98,36 @@ export const getDefaultModel = (m: IPasswordComponentProps) => { } catch { return m; } +}; + +export const defaultStyles = (): any => { + return { + background: { type: 'color', color: '#fff' }, + font: { + weight: '400', + size: 14, + color: '#000', + type: 'Segoe UI' + }, + border: { + border: { + all: { + width: 1, + style: 'solid', + color: '#d9d9d9' + } + }, + radius: { all: 8 }, + selectedBorder: 'all', + selectedCorner: 'all' + }, + dimensions: { + width: '100%', + height: '32px', + minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto' + } + }; }; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/profileDropdown/index.tsx b/shesha-reactjs/src/designer-components/profileDropdown/index.tsx index 7458b69607..660062d608 100644 --- a/shesha-reactjs/src/designer-components/profileDropdown/index.tsx +++ b/shesha-reactjs/src/designer-components/profileDropdown/index.tsx @@ -1,4 +1,6 @@ import { + ConfigurableForm, + FormIdentifier, IButtonGroup, IConfigurableFormComponent, IToolboxComponent, @@ -10,8 +12,8 @@ import { } from '@/index'; import { getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { DownOutlined, UserOutlined } from '@ant-design/icons'; -import { Avatar, Dropdown } from 'antd'; -import React, { useMemo, useState } from 'react'; +import { Avatar, Dropdown, Popover } from 'antd'; +import React, { CSSProperties, useMemo, useState } from 'react'; import { getSettings } from './settingsForm'; import { useStyles } from './styles'; import { getAccountMenuItems, getMenuItem } from './utils'; @@ -22,13 +24,21 @@ import { IResolvedDynamicItem, } from '@/providers/dynamicActions/evaluator/utils'; import { SingleDynamicItemEvaluator } from '@/providers/dynamicActions/evaluator/singleDynamicItemEvaluator'; +import ConditionalWrap from '@/components/conditionalWrapper'; interface IProfileDropdown extends IConfigurableFormComponent { items?: IButtonGroup[]; subText?: string; subTextColor?: string; subTextFontSize?: string; + subTextFontWeight?: string; + subTextFontFamily?: string; + subTextTextAlign?: CSSProperties['textAlign']; subTextStyle?: string; + showUserInfo?: boolean; + popOverTitle?: string; + popOverFormId?: FormIdentifier; + popOverContentStyle?: string; } const ProfileDropdown: IToolboxComponent = { @@ -40,7 +50,19 @@ const ProfileDropdown: IToolboxComponent = { Factory: ({ model }) => { const [numResolved, setNumResolved] = useState(0); - const { subText, subTextColor, subTextFontSize, subTextStyle } = model; + const { + subText, + subTextColor, + subTextFontSize, + subTextFontWeight, + subTextFontFamily, + subTextTextAlign, + subTextStyle, + showUserInfo, + popOverTitle, + popOverFormId, + popOverContentStyle, + } = model; const { styles } = useStyles({ subText, @@ -56,6 +78,9 @@ const ProfileDropdown: IToolboxComponent = { const subTextStyling = { color: subTextColor, fontSize: subTextFontSize, + fontWeight: subTextFontWeight, + fontFamily: subTextFontFamily, + textAlign: subTextTextAlign, ...getStyle(subTextStyle, formData, globalState), }; @@ -84,6 +109,14 @@ const ProfileDropdown: IToolboxComponent = { if (model.hidden) return null; + const popoverContent = popOverFormId ? ( +
+ +
+ ) : ( +
Select Popover Form
+ ); + return (
{subText &&
{subText}
} @@ -93,17 +126,38 @@ const ProfileDropdown: IToolboxComponent = { ))}
- - e.preventDefault()}> - {loginInfo?.fullName} - - + { + return ( + + {children} + + ); + }} + > + + e.preventDefault()}> + {loginInfo?.fullName} + + + } />
); }, settingsFormMarkup: (data) => getSettings(data), + migrator: (m) => m + .add(1, (prev) => ( + { + ...prev, subTextFontWeight: 'normal', + subTextFontFamily: 'Arial', + subTextTextAlign: 'left', + subTextColor: '#000000', + subTextFontSize: '12px', + } + )), validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), }; diff --git a/shesha-reactjs/src/designer-components/profileDropdown/settingsForm.ts b/shesha-reactjs/src/designer-components/profileDropdown/settingsForm.ts index 2f5ff11022..2cdfb2319b 100644 --- a/shesha-reactjs/src/designer-components/profileDropdown/settingsForm.ts +++ b/shesha-reactjs/src/designer-components/profileDropdown/settingsForm.ts @@ -1,118 +1,278 @@ import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; import { nanoid } from '@/utils/uuid'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { FormLayout } from 'antd/es/form/Form'; -export const getSettings = (data: any) => - new DesignerToolbarSettings(data) - .addCollapsiblePanel({ - id: nanoid(), - parentId: 'root', - propertyName: 'pnlDisplay', - label: 'Display', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: 'dfce8149-b595-4686-8778-e93d1b82d1e5', - components: [ - ...new DesignerToolbarSettings() - .addContextPropertyAutocomplete({ - id: nanoid(), - propertyName: 'propertyName', - label: 'Property name', - parentId: 'dfce8149-b595-4686-8778-e93d1b82d1e5', - validate: { - required: true, - }, - }) - .addCheckbox({ - id: nanoid(), - propertyName: 'hidden', - label: 'Hidden', - parentId: 'dfce8149-b595-4686-8778-e93d1b82d1e5', - }) - .addTextField({ - id: nanoid(), - propertyName: 'subText', - label: 'Sub Text', - parentId: 'dfce8149-b595-4686-8778-e93d1b82d1e5', - }) - .addButtons({ - id: nanoid(), - propertyName: 'items', - label: 'Configure Menu Buttons', - parentId: 'dfce8149-b595-4686-8778-e93d1b82d1e5', - }) - .toJson(), - ], - }, - }) - .addCollapsiblePanel({ - id: nanoid(), - propertyName: "pnlStyle", - parentId: "root", - label: "Style", - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: "header", - content: { - id: '64cf99eb-5b1d-4fae-9ad6-015b7bd5bcad', - components: [ - ...new DesignerToolbarSettings() - .addCodeEditor({ - id: nanoid(), - propertyName: "subTextStyle", - label: "Sub Text Style", - parentId: "64cf99eb-5b1d-4fae-9ad6-015b7bd5bcad", - mode: "dialog", - }) - .addColorPicker({ - id: nanoid(), - propertyName: "subTextColor", - label: "Sub Text Color", - title: "Sub Text Color", - allowClear: true, - showText: true, - parentId: "64cf99eb-5b1d-4fae-9ad6-015b7bd5bcad", - }) - .addSlider({ - id: nanoid(), - propertyName: "subTextFontSize", - label: "Sub Text Size", - defaultValue: "12", - min: "1", - max: "100", - parentId: "64cf99eb-5b1d-4fae-9ad6-015b7bd5bcad", - }) - .toJson(), - ], - }, - }) - .addCollapsiblePanel({ - id: nanoid(), - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: 'left', - expandIconPosition: 'start', - ghost: true, - collapsible: 'header', - content: { - id: 'fccb6b17-656d-43c0-8144-0b91c454da1d', - components: [ - ...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: nanoid(), - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'fccb6b17-656d-43c0-8144-0b91c454da1d', - hidden: false, - validate: {}, - }) - .toJson(), - ], - }, - }) - .toJson(); +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const styleTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + parentId: commonTabId, + label: '', + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'textField', + propertyName: 'subText', + label: 'Sub Text', + parentId: commonTabId, + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + propertyName: 'items', + label: 'Configure Menu Buttons', + parentId: commonTabId, + jsSetting: true, + inputs: [ + { + id: nanoid(), + type: 'buttonGroupConfigurator', + propertyName: 'items', + label: 'Configure Menu Buttons', + parentId: commonTabId, + jsSetting: true, + }, + { + id: nanoid(), + type: 'textField', + propertyName: 'popOverTitle', + label: 'Popover Title', + parentId: commonTabId, + hidden: { _code: 'return data?.showUserInfo != true', _mode: 'code', _value: false } as any, + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + propertyName: 'showUserInfo', + label: 'Show User Info', + parentId: commonTabId, + jsSetting: true, + inputs: [ + { + id: nanoid(), + type: 'switch', + propertyName: 'showUserInfo', + label: 'Show User Info', + parentId: commonTabId, + jsSetting: true, + }, + { + id: nanoid(), + type: 'formAutocomplete', + propertyName: 'popOverFormId', + label: 'Popover Form', + parentId: commonTabId, + hidden: { _code: 'return data?.showUserInfo != true', _mode: 'code', _value: false } as any, + jsSetting: true, + } + ] + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'hidden', + label: 'Hide', + parentId: commonTabId, + jsSetting: true, + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: styleTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: styleTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'subTextStyle', + label: 'Sub Text Style', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: 'fontStyleCollapsiblePanel', + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: 'styleRouter', + ghost: true, + collapsible: 'header', + content: { + id: 'fontStylePnl', + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: 'try26voxhs-HxJ5k5ngYE', + parentId: 'fontStylePnl', + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: 'fontFamily-s4gmBg31azZC0UjZjpfTm', + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: 'fontSize-s4gmBg31azZC0UjZjpfTm', + label: 'Size', + propertyName: 'subTextFontSize', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: 'fontWeight-s4gmBg31azZC0UjZjpfTm', + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: 'fontColor-s4gmBg31azZC0UjZjpfTm', + label: 'Color', + hideLabel: true, + propertyName: 'subTextColor', + }, + { + type: 'dropdown', + id: 'fontAlign-s4gmBg31azZC0UjZjpfTm', + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + hidden: { _code: 'return getSettingValue(data?.mode) === "multiple";', _mode: 'code', _value: false } as any, + }, + ], + }) + .toJson() + ] + } + }) + .addSettingsInput({ + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'subTextStyle', + hideLabel: false, + label: 'Custom Style', + description: 'A script that returns the style of the sub text as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'popOverStyle', + label: 'Popover Style', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'popOverContentStyle', + label: 'Content Style', + description: 'A script that returns the style of the popover content as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson() + ] + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + readOnly: { _code: 'return getSettingValue(data?.readOnly);', _mode: 'code', _value: false } as any, + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/progress/index.tsx b/shesha-reactjs/src/designer-components/progress/index.tsx index ca71c6aae3..c9e8c7f019 100644 --- a/shesha-reactjs/src/designer-components/progress/index.tsx +++ b/shesha-reactjs/src/designer-components/progress/index.tsx @@ -1,6 +1,6 @@ import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import React from 'react'; -import { progressSettingsForm } from './settings'; +import { getSettings } from './settings'; import { IConfigurableFormComponent } from '@/providers/form/models'; import { IToolboxComponent } from '@/interfaces'; import { LineOutlined } from '@ant-design/icons'; @@ -8,7 +8,7 @@ import { migrateCustomFunctions, migratePropertyName } from '@/designer-componen import { ProgressProps } from 'antd'; import { ProgressType } from 'antd/lib/progress/progress'; import { ProgressWrapper } from './progressWrapper'; -import { validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; interface IProgressProps @@ -20,6 +20,7 @@ interface IProgressProps strokeColor?: string; lineStrokeColor?: string; circleStrokeColor?: string; + defaultValue?: number; } const ProgressComponent: IToolboxComponent = { @@ -28,6 +29,12 @@ const ProgressComponent: IToolboxComponent = { icon: , isInput: true, isOutput: true, + initModel: (model) => { + return { + ...model, + hideLabel: true, + }; + }, Factory: ({ model }) => { const { progressType, @@ -45,9 +52,16 @@ const ProgressComponent: IToolboxComponent = { gapPosition, strokeWidth, width, + defaultValue, + hidden, + gapDegree, + style } = model; - if (model.hidden) return null; + if (hidden) return null; + + const styles = getStyle(style); + const getEvaluatedSuccessColor = () => { // tslint:disable-next-line:function-constructor @@ -55,22 +69,22 @@ const ProgressComponent: IToolboxComponent = { }; const getEvaluatedStrokeValue = () => { - let color = strokeColor; + let color: string = strokeColor; let isLineOrCircle = false; if (progressType === 'line') { - color = lineStrokeColor; + color = lineStrokeColor?.toString(); isLineOrCircle = true; } if (progressType === 'circle') { - color = circleStrokeColor; + color = circleStrokeColor?.toString(); isLineOrCircle = true; } if (isLineOrCircle) { // tslint:disable-next-line:function-constructor - return new Function(color)(); + return color; } else { return color; } @@ -84,15 +98,12 @@ const ProgressComponent: IToolboxComponent = { return ( {(value) => { - - const perc = percent || value; - return ( = { showInfo={showInfo} strokeLinecap={strokeLinecap} success={getEvaluatedSuccessColor()} + defaultValue={defaultValue} + gapDegree={gapDegree} + style={styles} />); }} ); }, - settingsFormMarkup: progressSettingsForm, - validateSettings: model => validateConfigurableComponentSettings(progressSettingsForm, model), + settingsFormMarkup: data => getSettings(data), + validateSettings: model => validateConfigurableComponentSettings(getSettings, model), migrator: (m) => m .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) .add(1, (prev) => ({...migrateFormApi.properties(prev)})) diff --git a/shesha-reactjs/src/designer-components/progress/settings.ts b/shesha-reactjs/src/designer-components/progress/settings.ts index 217fe3c5bd..9b78fd4a52 100644 --- a/shesha-reactjs/src/designer-components/progress/settings.ts +++ b/shesha-reactjs/src/designer-components/progress/settings.ts @@ -1,269 +1,358 @@ import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { nanoid } from '@/utils/uuid'; -export const progressSettingsForm = new DesignerToolbarSettings() +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const styleRouterId = nanoid(); - .addCollapsiblePanel({ - id: "b8954bf6-f76d-4139-a850-c99bf06c8b69", - propertyName: "separator1", - parentId: "root", - label: "Display", - labelAlign: "right", - expandIconPosition: "start", - ghost: true, - hideWhenEmpty: true, - header: { - id: '3342DA1C-DA07-46F6-8026-E8B9A93F094A', - components: [] - }, - content: { - id: '1BCC52E8-FD3B-4309-AD9B-099CDB729441', - components: new DesignerToolbarSettings() - .addContextPropertyAutocomplete({ - id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', - propertyName: 'propertyName', - parentId: 'root', - label: 'Property name', - validate: { required: true }, - }) - .addTextField({ - id: '001bc9d9-d943-457c-b39a-95d5380bfdbe', - propertyName: 'label', - label: 'Label', - }) - .addCheckbox({ - id: '3f7226c7-7953-4bea-b103-1484340f5929', - propertyName: 'hideLabel', - label: 'Hide label', - }) - .addCodeEditor({ - id: 'a62a1d47-4255-4e0c-abae-ee313dada8d4', - propertyName: 'format', - label: 'Format', - exposedVariables: [ - { - id: '4925a95d-6521-4856-a253-bd9f042a7388', - name: 'percent', - description: 'Progress percentage', - type: 'number', - }, - { - id: '2b8f3030-00aa-4081-85ac-2b5c7692615e', - name: 'successPercent', - description: 'success percentage', - type: 'number', - }, - ], - mode: 'dialog', - description: 'The template function of the content. This function should return string or number', - wrapInTemplate: true, - templateSettings: { - functionName: 'getFormat', + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + defaultValue: false, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + type: 'textArea', + jsSetting: true + }, + { + type: 'numberField', + id: nanoid(), + propertyName: 'percent', + label: 'Percent', + tooltip: 'To set the completion percentage', + min: 0, + max: 100, + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + propertyName: 'progressType', + label: 'Type', + tooltip: 'To specify the type of progress bar', + defaultValue: 'line', + dropdownOptions: [ + { label: 'Line', value: 'line' }, + { label: 'Circle', value: 'circle' }, + { label: 'Dashboard', value: 'dashboard' }, + ], + jsSetting: true, + }, + { + type: 'dropdown', + id: nanoid(), + propertyName: 'status', + label: 'Status', + tooltip: 'To set the status of the Progress', + dropdownOptions: [ + { label: 'Success', value: 'success' }, + { label: 'Exception', value: 'exception' }, + { label: 'Normal', value: 'normal' }, + { label: 'Active', value: 'active' }, + ], + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'showInfo', + label: 'Show Info', + tooltip: 'Whether to display the progress info', + defaultValue: true, + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + } + ] + }) + .toJson(), + ], }, - availableConstants: { - dataType: 'object', - properties: [ - { - path: 'percent', - label: 'percent', - dataType: 'number', - }, - { - path: 'successPercent', - dataType: 'number', - }, - ] - } - }) - .addNumberField({ - id: 'b10d0c9f-d83a-44c5-94e6-0d30f95a36cb', - propertyName: 'percent', - label: 'Percent', - min: 0, - max: 100, - description: 'To set the completion percentage', - }) - .addCheckbox({ - id: '55e685e0-0277-4543-9fc2-1083c2603930', - propertyName: 'showInfo', - label: 'Show Info', - defaultValue: true, - }) - .addDropdown({ - id: 'c8360d69-0da2-4875-8dae-a0a41fb924fc', - propertyName: 'status', - label: 'Status', - description: 'To set the status of the Progress.', - values: [ - { id: 'f1448397-0a7d-42e2-af2e-c9d3b8ae0fdd', label: 'success', value: 'success' }, - { id: '72567015-266d-4c37-9b46-ba633e74a036', label: 'exception', value: 'exception' }, - { id: '4c5e55c5-3ad9-4ed1-8f9a-f1370c85c238', label: 'normal', value: 'normal' }, - { id: 'b028311f-c97a-40e2-81b3-e6b29f377165', label: 'active', value: 'active' }, - ], - dataSourceType: 'values', - }) - .addTextField({ - id: '6e316ddf-183d-4477-a21d-22919292e6df', - propertyName: 'strokeColor', - label: 'Stroke Color', - description: - "The color of progress bar. This will be 'overridden' by the the 'strokeColor' property of 'line' and 'circle' types", - }) - .addDropdown({ - id: 'd8b4769a-4499-42f9-9753-3caba0d44b39', - propertyName: 'strokeLinecap', - label: 'Stroke Linecap', - parentId: 'root', - hidden: false, - dataSourceType: 'values', - defaultValue: 'round', - values: [ - { id: '17a865b3-8e28-41de-ab40-1fc49a56b31d', label: 'round', value: 'round' }, - { id: 'edeb7d32-f942-41cc-a941-07b8882d8faa', label: 'butt', value: 'butt' }, - { id: 'df342c95-4dae-49a5-beff-259d0f2ebcb3', label: 'square', value: 'square' }, - ], - validate: { required: true }, - }) - .addCodeEditor({ - id: '9fa733bf-0cad-4a67-9461-e8df24ea4a13', - propertyName: 'success', - label: 'Success', - description: - 'Configs of successfully progress bar. Returns an object of this format: { percent: number, strokeColor: string }', - mode: 'dialog', - wrapInTemplate: true, - templateSettings: { - functionName: 'getSuccess' + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'progressStyle', + label: 'Progress Style', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'colorPicker', + id: nanoid(), + propertyName: 'strokeColor', + label: 'Stroke Color', + tooltip: "The color of progress bar. This will be overridden by the strokeColor property of 'line' and 'circle' types", + jsSetting: true, + }, + { + type: 'colorPicker', + id: nanoid(), + propertyName: 'trailColor', + label: 'Trail Color', + tooltip: "The color of progress bar background", + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + propertyName: 'strokeLinecap', + label: 'Stroke Linecap', + tooltip: 'To set the style of the progress linecap', + dropdownOptions: [ + { label: 'Round', value: 'round' }, + { label: 'Butt', value: 'butt' }, + { label: 'Square', value: 'square' }, + ], + defaultValue: 'round', + jsSetting: true, + }, + { + type: 'numberField', + id: nanoid(), + propertyName: 'strokeWidth', + label: 'Stroke Width', + tooltip: 'The width of the progress bar, unit: percentage of the canvas width', + defaultValue: 6, + jsSetting: true, + } + ] + }) + .addSettingsInput({ + id: nanoid(), + propertyName: 'width', + label: 'Width', + inputType: 'numberField', + tooltip: 'The canvas width of the circular progress, unit: px', + hidden: { _code: 'return !["circle", "dashboard"].includes(getSettingValue(data?.progressType));', _mode: 'code', _value: false }, + jsSetting: true, + parentId: styleRouterId, + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'typeSpecificSettings', + label: 'Type Specific Settings', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + hideWhenEmpty: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'steps', + label: 'Steps', + inputType: 'numberField', + tooltip: 'The total step count', + hidden: { _code: 'return getSettingValue(data?.progressType) !== "line";', _mode: 'code', _value: false }, + jsSetting: true, + parentId: styleRouterId, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { _code: 'return getSettingValue(data?.progressType) !== "line";', _mode: 'code', _value: false }, + inputs: [ + { + type: 'colorPicker', + id: nanoid(), + propertyName: 'lineStrokeColor', + label: 'Stroke Color', + tooltip: "The color of progress bar", + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: styleRouterId, + hidden: { _code: 'return getSettingValue(data?.progressType) !== "dashboard";', _mode: 'code', _value: false }, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'gapDegree', + label: 'Gap Degree', + tooltip: "The gap degree of half circle, 0 ~ 295", + jsSetting: true, + }, + { + type: 'dropdown', + id: nanoid(), + propertyName: 'gapPosition', + label: 'Gap Position', + tooltip: "The gap position", + dropdownOptions: [ + { label: 'Top', value: 'top' }, + { label: 'Bottom', value: 'bottom' }, + { label: 'Left', value: 'left' }, + { label: 'Right', value: 'right' }, + ], + jsSetting: true, + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson(), + ], + }) + .toJson(), + ], } - }) - .addTextField({ - id: 'd72986e5-e099-4ba7-8ffe-b8e394542524', - propertyName: 'trailColor', - label: 'trailColor', - description: 'The color of unfilled part', - }) - .addDropdown({ - id: '173e3a29-786f-44b8-b569-7f033c543e24', - propertyName: 'progressType', - parentId: 'root', - hidden: false, - label: 'Type', - dataSourceType: 'values', - values: [ - { id: '17a865b3-8e28-41de-ab40-1fc49a56b31d', label: 'Line', value: 'line' }, - { id: 'edeb7d32-f942-41cc-a941-07b8882d8faa', label: 'Circle', value: 'circle' }, - { id: 'df342c95-4dae-49a5-beff-259d0f2ebcb3', label: 'Dashboard', value: 'dashboard' }, - ], - validate: { required: true }, - }) - .addContainer({ - id: 'fdb031ce-250f-4c7f-8ff9-b35ac59e1946', - propertyName: 'lineContainer', - direction: 'vertical', - hidden: { _code: 'return getSettingValue(data?.progressType) !== "line";', _mode: 'code', _value: false } as any, - components: new DesignerToolbarSettings() - .addNumberField({ - id: '7decc517-9c50-433c-a23f-2054b5684a80', - propertyName: 'steps', - label: 'Steps', - description: 'The total step count', - }) - .addCodeEditor({ - id: '7decc517-9c50-433c-a23f-2054b5684a80', - propertyName: 'lineStrokeColor', - label: 'Stroke Width', - description: - 'The color of progress bar, render linear-gradient when passing an object, could accept string[] when has steps. ' + - 'Write the code that returns any of the following: `string | string[] | { from: string; to: string; direction: string }`', - mode: 'dialog', - }) - .toJson(), - }) - .addContainer({ - id: '6a49b8af-da4f-4de4-b834-cab5d83541ce', - propertyName: 'circleContainer', - direction: 'vertical', - hidden: { _code: 'return getSettingValue(data?.progressType) !== "circle";', _mode: 'code', _value: false } as any, - components: new DesignerToolbarSettings() - .addCodeEditor({ - id: 'b6e39afe-2495-4b35-9c0a-661242b4b7c4', - propertyName: 'circleStrokeColor', - label: 'Stroke Color', - description: - 'The color of circular progress, render linear-gradient when passing an object. Write the code that returns: `string | object`', - mode: 'dialog', - }) - .toJson(), - }) - .addContainer({ - id: '73f9b6eb-e251-4286-b0c3-183f675b39fd', - propertyName: 'dashboardContainer', - direction: 'vertical', - hidden: { _code: 'return getSettingValue(data?.progressType) !== "dashboard";', _mode: 'code', _value: false } as any, - components: new DesignerToolbarSettings() - .addNumberField({ - propertyName: 'gapDegree', - label: 'Gap Degree', - id: '25e604fc-5d88-4299-a596-5b51f0724675', - stepNumeric: 0, - stepString: '', - min: 0, - max: 295, - }) - .addDropdown({ - id: 'bdf1f1d3-21b2-4135-b482-72e784ea1d39', - propertyName: 'gapPosition', - parentId: 'root', - hidden: false, - label: 'Gap Position', - dataSourceType: 'values', - values: [ - { id: '2e9bde7b-1a0d-4438-879e-5dd7bb5fdb8e', label: 'top', value: 'top' }, - { id: 'c78eabf3-d0c7-4112-8078-c83c982c75d2', label: 'bottom', value: 'bottom' }, - { id: 'c11bcfff-c65b-49b7-a514-32f1881daa2a', label: 'left', value: 'left' }, - { id: '0e931e4c-d829-4a44-ac61-67a879f51d21', label: 'right', value: 'right' }, - ], - validate: { required: true }, - }) - .toJson(), - }) - .addNumberField({ - id: '6ffeac8a-71e0-4a98-bfe9-a47d3b1d0fa0', - propertyName: 'strokeWidth', - label: 'Stroke Width', - description: 'To set the width of the circular progress, unit: percentage of the canvas width in px', - defaultValue: 6, - }) - .addNumberField({ - id: '1bc6dfc0-6088-4a99-a667-ae6bd6943be4', - propertyName: 'width', - label: 'Width', - description: 'To set the canvas width of the circular progress, unit: px', - hidden: { _code: 'return !["circle", "dashboard"].includes(getSettingValue(data?.progressType));', _mode: 'code', _value: false } as any, - }) - .toJson() - } - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] -} -}).toJson(); + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/propertiesTabs/__tests__/index.test.tsx b/shesha-reactjs/src/designer-components/propertiesTabs/__tests__/index.test.tsx new file mode 100644 index 0000000000..258f904438 --- /dev/null +++ b/shesha-reactjs/src/designer-components/propertiesTabs/__tests__/index.test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +const title = 'Hello React'; + +function SimpleComponent() { + return
{title}
; +} + +describe('SimpleComponent', () => { + test('renders SimpleComponent component', () => { + render(); + }); +}); diff --git a/shesha-reactjs/src/designer-components/propertiesTabs/index.tsx b/shesha-reactjs/src/designer-components/propertiesTabs/index.tsx new file mode 100644 index 0000000000..68c5440052 --- /dev/null +++ b/shesha-reactjs/src/designer-components/propertiesTabs/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { FolderOutlined } from '@ant-design/icons'; +import { IFormComponentContainer } from '@/providers/form/models'; +import { ITabsComponentProps } from './models'; +import { IToolboxComponent } from '@/interfaces'; +import { nanoid } from '@/utils/uuid'; +import { TabSettingsForm } from './settings'; +import SearchableTabsComponent from './searchableTabsComponent'; + +const SearchableTabs: IToolboxComponent = { + type: 'searchableTabs', + isInput: false, + name: 'SearchableTabs', + icon: , + Factory: ({ model }) => { + + return model.hidden ? null : ( + + ); + }, + initModel: (model) => { + const tabsModel: ITabsComponentProps = { + ...model, + propertyName: 'custom Name', + tabs: [{ id: nanoid(), label: 'Tab 1', title: 'Tab 1', key: 'tab1', components: [] }], + }; + return tabsModel; + }, + settingsFormFactory: (props) => , + customContainerNames: ['tabs'], + getContainers: (model) => { + return model.tabs.map((tab) => ({ id: tab.id })); + }, +}; + +export default SearchableTabs; diff --git a/shesha-reactjs/src/designer-components/tabs/itemSettings.json b/shesha-reactjs/src/designer-components/propertiesTabs/itemSettings.json similarity index 94% rename from shesha-reactjs/src/designer-components/tabs/itemSettings.json rename to shesha-reactjs/src/designer-components/propertiesTabs/itemSettings.json index 02309af207..f44953b9e5 100644 --- a/shesha-reactjs/src/designer-components/tabs/itemSettings.json +++ b/shesha-reactjs/src/designer-components/propertiesTabs/itemSettings.json @@ -6,7 +6,7 @@ "propertyName": "name", "label": "Name", "labelAlign": "right", - "jsSetting": false, + "jsSetting": true, "parentId": "root" }, { @@ -127,7 +127,13 @@ "validate": {}, "settingsValidationErrors": [], "description": "A script that returns the style of the element as an object. This should conform to CSSProperties", - "exposedVariables": [{ "name": "data", "description": "Form values", "type": "object" }] + "exposedVariables": [ + { + "name": "data", + "description": "Form values", + "type": "object" + } + ] }, { "id": "1adea529-1f0c-4def-bd41-ee166a5dfcd7", @@ -141,6 +147,8 @@ ], "formSettings": { "layout": "horizontal", + "size": "small", + "jsSettings": true, "isSettingsForm": true, "colon": true, "labelCol": { @@ -150,4 +158,4 @@ "span": 13 } } -} +} \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/propertiesTabs/models.ts b/shesha-reactjs/src/designer-components/propertiesTabs/models.ts new file mode 100644 index 0000000000..a79c098dc8 --- /dev/null +++ b/shesha-reactjs/src/designer-components/propertiesTabs/models.ts @@ -0,0 +1,32 @@ +import { TabPaneProps } from 'antd'; +import { SizeType } from 'antd/lib/config-provider/SizeContext'; +import { EditMode, IConfigurableFormComponent } from '@/interfaces'; + +export interface ITabPaneProps + extends Omit { + id: string; + icon?: string; + key: string; + title: string; + hidden?: boolean; + permissions?: string[]; + components?: IConfigurableFormComponent[]; + childItems?: ITabPaneProps[]; + editMode?: EditMode; + selectMode?: EditMode; + readOnly?: boolean; + + label?: string; + name?: string; + tooltip?: string; +} + +export interface ITabsComponentProps extends IConfigurableFormComponent { + tabs: ITabPaneProps[]; + size?: SizeType; + defaultActiveKey?: string; + tabType?: 'line' | 'card'; + hidden?: boolean; + customVisibility?: string; + position?: 'left' | 'right' | 'top' | 'bottom'; +} diff --git a/shesha-reactjs/src/designer-components/propertiesTabs/searchableTabsComponent.tsx b/shesha-reactjs/src/designer-components/propertiesTabs/searchableTabsComponent.tsx new file mode 100644 index 0000000000..fa709a7250 --- /dev/null +++ b/shesha-reactjs/src/designer-components/propertiesTabs/searchableTabsComponent.tsx @@ -0,0 +1,118 @@ +import React, { useState } from 'react'; +import { Tabs, Input, Empty } from 'antd'; +import ParentProvider from '@/providers/parentProvider'; +import { ComponentsContainer } from '@/components'; +import { useStyles } from './style'; +import { SearchOutlined } from '@ant-design/icons'; +import { filterDynamicComponents } from './utils'; +import { ITabsComponentProps } from './models'; +import { useFormState, useFormActions } from '@/providers/form'; + +interface SearchableTabsProps { + model: ITabsComponentProps; +} + +const SearchableTabs: React.FC = ({ model }) => { + const { tabs } = model; + const [searchQuery, setSearchQuery] = useState(''); + const { styles } = useStyles(); + + const formState = useFormState(false); + const formActions = useFormActions(false); + + const isComponentHidden = (component) => { + if (formState.name === "modalSettings") { + if (component.inputs) { + + const visibleInputs = component.inputs.filter(input => { + if (!input.propertyName) return true; + return formActions.isComponentFiltered(input); + }); + + if (visibleInputs.length === 0) { + return false; + } + + component.inputs = visibleInputs; + + return visibleInputs.length > 0; + } + + return formActions.isComponentFiltered(component); + } else { + return true; + } + }; + + const newFilteredTabs = tabs + .map((tab: any) => { + const filteredComponents = tab.children ?? filterDynamicComponents(tab.components, searchQuery); + + const visibleComponents = Array.isArray(filteredComponents) + ? filteredComponents.filter(comp => isComponentHidden(comp)) + : filteredComponents; + + const hasVisibleComponents = Array.isArray(visibleComponents) + ? visibleComponents.length > 0 + : !!visibleComponents; + + return { + ...tab, + label: tab.label ?? tab.title, + components: visibleComponents, + children: visibleComponents.length === 0 + ? + : + + , + forceRender: true, + hidden: tab.hidden ?? !hasVisibleComponents + }; + }) + .filter(tab => !tab.hidden); + + const handleSearchChange = (e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + }; + + return ( + <> +
+ + } + /> +
+ {newFilteredTabs.length === 0 && searchQuery ? + : + + } + + ); +}; + +export default SearchableTabs; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/tabs/settings.tsx b/shesha-reactjs/src/designer-components/propertiesTabs/settings.tsx similarity index 80% rename from shesha-reactjs/src/designer-components/tabs/settings.tsx rename to shesha-reactjs/src/designer-components/propertiesTabs/settings.tsx index 9cf2808958..79fbfaefc7 100644 --- a/shesha-reactjs/src/designer-components/tabs/settings.tsx +++ b/shesha-reactjs/src/designer-components/propertiesTabs/settings.tsx @@ -2,16 +2,15 @@ import itemSettings from './itemSettings.json'; import React, { FC } from 'react'; import SettingsForm from '@/designer-components/_settings/settingsForm'; import SettingsFormItem from '@/designer-components/_settings/settingsFormItem'; -import StyleBox from '../styleBox/components/box'; import { Alert, Checkbox, Input, Select } from 'antd'; -import { CodeEditor } from '@/components'; +import { CodeEditor, PermissionAutocomplete } from '@/components'; import { FormMarkup } from '@/providers/form/models'; import { ISettingsFormFactoryArgs } from '@/interfaces'; import { ITabPaneProps, ITabsComponentProps } from './models'; import { nanoid } from '@/utils/uuid'; import SettingsCollapsiblePanel from '@/designer-components/_settings/settingsCollapsiblePanel'; -import { PermissionAutocomplete } from '@/components/permissionAutocomplete'; -import { ItemListConfiguratorModal } from '../itemListConfigurator/itemListConfiguratorModal'; +import { ItemListConfiguratorModal } from '@/designer-components/itemListConfigurator/itemListConfiguratorModal'; +import StyleBox from '@/designer-components/styleBox/components/box'; const { Option } = Select; @@ -76,24 +75,24 @@ const TabSettings: FC> = (props) = - - - readOnly={readOnly} - initNewItem={onAddNewItem} - settingsMarkupFactory={() => tabSettingsMarkup} + + + readOnly={readOnly} + initNewItem={onAddNewItem} + settingsMarkupFactory={() => tabSettingsMarkup} itemRenderer={({ item }) => ({ - label: item.title || item.label || item.name, - description: item.tooltip, - icon: item.icon + label: item.title || item.label || item.name, + description: item.tooltip, + icon: item.icon })} - buttonText={readOnly ? "View Tab Panes" : "Configure Tab Panes"} - modalSettings={{ - title: readOnly ? "View Tab Panes" : "Configure Tab Panes", - header: , - }} - > - - + buttonText={readOnly ? "View Tab Panes" : "Configure Tab Panes"} + modalSettings={{ + title: readOnly ? "View Tab Panes" : "Configure Tab Panes", + header: , + }} + > + + diff --git a/shesha-reactjs/src/designer-components/propertiesTabs/style.ts b/shesha-reactjs/src/designer-components/propertiesTabs/style.ts new file mode 100644 index 0000000000..aeefa0b1f3 --- /dev/null +++ b/shesha-reactjs/src/designer-components/propertiesTabs/style.ts @@ -0,0 +1,38 @@ +import { createStyles } from '@/styles'; + +export const useStyles = createStyles(({ css, cx }) => { + const searchField = cx(css` + width: 100%; + background: #fff; + `); + + const content = cx(css` + .ant-tabs-tab, .ant-tabs-nav-operations { + height: 24px; + } + + .ant-tabs-nav { + margin-bottom: 0px; + } + .ant-tabs-tab { + --ant-tabs-card-padding-sm: 0 8px; + } + + .sha-toolbar-btn-configurable, .ant-btn { + display: flex; + align-items: center; + max-width: 100%; + span { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + `); + + return { + searchField, + content + }; +}); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/propertiesTabs/utils.ts b/shesha-reactjs/src/designer-components/propertiesTabs/utils.ts new file mode 100644 index 0000000000..6a5ddea457 --- /dev/null +++ b/shesha-reactjs/src/designer-components/propertiesTabs/utils.ts @@ -0,0 +1,170 @@ +const getHeaderStyles = () => ( + { + font: { + color: "darkslategray", + size: 14, + weight: "500", + align: "left", + type: "Segoe UI" + }, + background: { + type: "color", + color: "#fff" + }, + dimensions: { + width: "auto", + height: "auto", + minHeight: "0", + maxHeight: "auto", + minWidth: "0", + maxWidth: "auto" + }, + border: { + radiusType: "all", + borderType: "custom", + border: { + all: {}, + top: {}, + right: {}, + bottom: { + width: "2px", + style: "solid", + color: "var(--primary-color)" + }, + left: {} + }, + radius: { + all: '0' + } + }, + stylingBox: "{\"paddingLeft\":\"0\",\"paddingBottom\":\"4\",\"paddingTop\":\"4\",\"paddingRight\":\"0\"}" + } +); + +const getBodyStyles = () => ({ + border: { + radiusType: "all", + borderType: "all", + border: { + all: { width: '0px', style: 'none', color: '' }, + top: {}, + right: {}, + bottom: {}, + left: {} + }, + radius: { + all: 0 + } + } +}); + +export const filterDynamicComponents = (components, query) => { + if (!components || !Array.isArray(components)) return []; + + + const lowerCaseQuery = query.toLowerCase(); + + // Helper function to evaluate hidden property + const evaluateHidden = (hidden, directMatch, hasVisibleChildren) => { + return hidden || (!directMatch && !hasVisibleChildren); + }; + + // Helper function to check if text + // matches query + + const matchesQuery = (text) => { + return text?.toLowerCase().includes(lowerCaseQuery); + }; + + const filterResult = components.map(component => { + // Deep clone the component to avoid mutations + const c = { ...component }; + + // Check if component matches query directly + const directMatch = ( + matchesQuery(c.label) || + matchesQuery(c.propertyName) || + (c.propertyName && matchesQuery(c.propertyName.split('.').join(' '))) + ); + + // Handle propertyRouter + if (c.componentName === 'propertyRouter') { + const filteredComponents = filterDynamicComponents(c.components, query); + + return { + ...c, + hidden: filteredComponents.length < 1, + components: filteredComponents + }; + } + + // Handle collapsiblePanel + if (c.type === 'collapsiblePanel') { + const contentComponents = filterDynamicComponents(c.content?.components || [], query); + const hasVisibleChildren = contentComponents.length > 0; + + return { + ...c, + collapsible: 'header', + content: { + ...c.content, + components: contentComponents + }, + ghost: false, + collapsedByDefault: false, + headerStyles: getHeaderStyles(), + allStyles: getBodyStyles(), + border: getBodyStyles().border, + stylingBox: "{\"paddingLeft\":\"4\",\"paddingBottom\":\"4\",\"paddingTop\":\"0\",\"paddingRight\":\"4\",\"marginBottom\":\"5\"}", + hidden: evaluateHidden(c.hidden, directMatch, hasVisibleChildren) + }; + } + + // Handle settingsInputRow + if (c.type === 'settingsInputRow') { + const filteredInputs = c.inputs?.filter(input => + matchesQuery(input.label) || + matchesQuery(input.propertyName) || + (input.propertyName && matchesQuery(input.propertyName.split('.').join(' '))) + ) || []; + + return { + ...c, + inputs: filteredInputs, + hidden: evaluateHidden(c.hidden, directMatch, filteredInputs.length > 0) + }; + } + + // Handle components with nested components + if (c.components) { + const filteredComponents = filterDynamicComponents(c.components, query); + const hasVisibleChildren = filteredComponents.length > 0; + + return { + ...c, + components: filteredComponents, + hidden: evaluateHidden(c.hidden, directMatch, hasVisibleChildren) + }; + } + + // Handle basic component + return { + ...c, + hidden: evaluateHidden(c.hidden, directMatch, false) + }; + }); + + // Filter out null components and handle visibility + return filterResult.filter(c => { + if (!c) return false; + + // Evaluate final hidden state + const hasVisibleChildren = ( + (c.components && c.components.length > 0) || + (c.content?.components && c.content.components.length > 0) || + (c.inputs && c.inputs.length > 0) + ); + + return !c.hidden || hasVisibleChildren; + }); +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/propertyAutocomplete/index.tsx b/shesha-reactjs/src/designer-components/propertyAutocomplete/index.tsx index 54471c0a3a..6bd0648c58 100644 --- a/shesha-reactjs/src/designer-components/propertyAutocomplete/index.tsx +++ b/shesha-reactjs/src/designer-components/propertyAutocomplete/index.tsx @@ -8,7 +8,7 @@ import { FormMarkup } from '@/providers/form/models'; import { getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { IPropertyAutocompleteComponentProps } from './interfaces'; import { IToolboxComponent } from '@/interfaces'; -import { MetadataProvider, useFormData } from '@/providers'; +import { MetadataProvider } from '@/providers'; import { migrateCustomFunctions, migratePropertyName, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; import { PropertyAutocomplete } from '@/components/propertyAutocomplete/propertyAutocomplete'; @@ -20,11 +20,12 @@ export const PropertyAutocompleteComponent: IToolboxComponent, isInput: true, isOutput: true, - Factory: ({ model }) => { - const { data: formData } = useFormData(); - const { modelType: modelTypeExpression } = model; - - const modelType = modelTypeExpression ? evaluateString(modelTypeExpression, { data: formData }) : null; + calculateModel: (model, allData) => ({ + modelType: model.modelType ? evaluateString(model.modelType, { data: allData.data }) : null, + dropdownStyle: getStyle(model.dropdownStyle, allData.data), + }), + Factory: ({ model, calculatedModel }) => { + const modelType = calculatedModel.modelType; return ( id: 'bf1823d6-dca4-408a-b7d8-5b42eacb076d', propertyName: 'hidden', parentId: 'pnl64664-cbc9-4cef-babc-6fbea44cd0ca', - label: 'Hidden', + label: 'hide', }) .addEditMode({ id: 'abc823d6-dca4-408a-b7d8-5b42eacb1234', diff --git a/shesha-reactjs/src/designer-components/queryBuilder/queryBuilder.tsx b/shesha-reactjs/src/designer-components/queryBuilder/queryBuilder.tsx index 3096f0478d..468fd5ffcf 100644 --- a/shesha-reactjs/src/designer-components/queryBuilder/queryBuilder.tsx +++ b/shesha-reactjs/src/designer-components/queryBuilder/queryBuilder.tsx @@ -7,11 +7,11 @@ import { useQueryBuilder } from '@/providers'; export const QueryBuilder: FC = (props) => { const queryBuilder = useQueryBuilder(false); - return queryBuilder ? ( - + return !props.modelType && queryBuilder ? ( + ) : ( - + ); }; diff --git a/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderComponent.tsx b/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderComponent.tsx index 1a08edac28..25c4059202 100644 --- a/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderComponent.tsx +++ b/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderComponent.tsx @@ -6,7 +6,7 @@ import { IQueryBuilderComponentProps } from './interfaces'; import { IToolboxComponent } from '@/interfaces'; import { migrateCustomFunctions, migratePropertyName, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; -import { validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { evaluateString, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { QueryBuilder } from './queryBuilder'; const settingsForm = settingsFormJson as FormMarkup; @@ -17,9 +17,15 @@ const QueryBuilderComponent: IToolboxComponent = { icon: , isInput: true, isOutput: true, + canBeJsSetting: true, //dataTypes: [DataTypes.string], - Factory: ({ model }) => { - return ; + calculateModel: (model, allData) => ({ + modelType: model.modelType ? evaluateString(model.modelType, { data: allData.data }) : null, + }), + Factory: ({ model, calculatedModel }) => { + return model.hidden + ? null + : ; }, settingsFormMarkup: settingsForm, migrator: (m) => m diff --git a/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderField.tsx b/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderField.tsx index bcffecc48b..48682d0f15 100644 --- a/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderField.tsx +++ b/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderField.tsx @@ -32,8 +32,7 @@ export const QueryBuilderField: FC = (props) => { }; const onChange = (result: JsonLogicResult) => { - if (result !== jsonLogicResult) - setJsonLogicResult(result); + if (result !== jsonLogicResult) setJsonLogicResult(result); }; const onExpandClick = () => { @@ -43,7 +42,7 @@ export const QueryBuilderField: FC = (props) => { const hasValue = Boolean(props?.value); return ( - <> +
= (props) => { } items={[ { - key: "1", + key: '1', label: (
); }; diff --git a/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderWithModelType.tsx b/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderWithModelType.tsx index 9bfdcdf284..85e30ec647 100644 --- a/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderWithModelType.tsx +++ b/shesha-reactjs/src/designer-components/queryBuilder/queryBuilderWithModelType.tsx @@ -1,7 +1,6 @@ import React, { FC, PropsWithChildren } from 'react'; import ConditionalWrap from '@/components/conditionalWrapper'; -import { MetadataProvider, useForm } from '@/providers'; -import { evaluateString } from '@/providers/form/utils'; +import { MetadataProvider } from '@/providers'; import { QueryBuilderWrapper } from './queryBuilderWrapper'; export interface IQueryBuilderWithModelType { @@ -9,14 +8,10 @@ export interface IQueryBuilderWithModelType { } export const QueryBuilderWithModelType: FC> = (props) => { - const { formData } = useForm(); - const { modelType: modelTypeExpression } = props; - const modelType = evaluateString(modelTypeExpression, { data: formData }); - return ( {content}} + condition={Boolean(props.modelType)} + wrap={(content) => {content}} > {props.children} diff --git a/shesha-reactjs/src/designer-components/queryBuilder/styles/styles.ts b/shesha-reactjs/src/designer-components/queryBuilder/styles/styles.ts index 66bee0e877..d887800183 100644 --- a/shesha-reactjs/src/designer-components/queryBuilder/styles/styles.ts +++ b/shesha-reactjs/src/designer-components/queryBuilder/styles/styles.ts @@ -30,10 +30,15 @@ export const useStyles = createStyles(({ css, cx, prefixCls }) => { } `); + const shaQueryBuilderMarginTop8 = cx("sha-query-builder-margin-top", css` + margin-top: 8px; + `); + return { shaQueryBuilderField, shaQueryBuilderPlainWrapper, shaQueryBuilder, shaQqueryBuilderPlainWrapperBtnWrapper, + shaQueryBuilderMarginTop8: shaQueryBuilderMarginTop8, }; }); \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/radio/radio.tsx b/shesha-reactjs/src/designer-components/radio/radio.tsx index 570614f915..4c857d022e 100644 --- a/shesha-reactjs/src/designer-components/radio/radio.tsx +++ b/shesha-reactjs/src/designer-components/radio/radio.tsx @@ -1,26 +1,33 @@ import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import RadioGroup from './radioGroup'; import React from 'react'; -import settingsFormJson from './settingsForm.json'; import { CheckCircleOutlined } from '@ant-design/icons'; import { DataTypes } from '@/interfaces/dataTypes'; -import { FormMarkup, IInputStyles } from '@/providers/form/models'; +import { IInputStyles } from '@/providers/form/models'; import { getLegacyReferenceListIdentifier } from '@/utils/referenceList'; -import { getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { evaluateValue, executeScriptSync, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { IRadioProps } from './utils'; import { IToolboxComponent } from '@/interfaces'; -import { migrateCustomFunctions, migratePropertyName, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; +import { + migrateCustomFunctions, + migratePropertyName, + migrateReadOnly, +} from '@/designer-components/_common-migrations/migrateSettings'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; -import { useFormData } from '@/providers'; +import { IConfigurableFormComponent } from '@/providers'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settingsForm'; +import { IEventHandlers, getAllEventHandlers } from '@/components/formDesigner/components/utils'; -const settingsForm = settingsFormJson as FormMarkup; +interface IEnhancedRadioProps extends Omit, IConfigurableFormComponent {} -interface IEnhancedRadioProps extends Omit { - style?: string; +interface IRadioComopnentCalulatedValues { + eventHandlers: IEventHandlers; + dataSourceUrl?: string; + defaultValue?: any; } -const Radio: IToolboxComponent = { +const Radio: IToolboxComponent = { type: 'radio', name: 'Radio', icon: , @@ -28,55 +35,76 @@ const Radio: IToolboxComponent = { isOutput: true, canBeJsSetting: true, dataTypeSupported: ({ dataType }) => dataType === DataTypes.array, - Factory: ({ model }) => { + calculateModel: (model, allData) => ({ + eventHandlers: getAllEventHandlers(model, allData), + dataSourceUrl: model.dataSourceUrl ? executeScriptSync(model.dataSourceUrl, allData) : model.dataSourceUrl, + defaultValue: evaluateValue(model.defaultValue, allData.data), + }), + Factory: ({ model, calculatedModel }) => { const { style, ...restProps } = model; - const { data: formData } = useFormData(); - return ( - {(value, onChange) => } + {(value, onChange) => { + const customEvents = calculatedModel.eventHandlers; + const onChangeInternal = (e: any) => { + if (e.target) customEvents.onChange({ ...e, currentTarget: { value: e.target.value } }); + if (typeof onChange === 'function') onChange(e); + }; + + return ( + + ); + }} ); }, - settingsFormMarkup: settingsForm, - validateSettings: model => validateConfigurableComponentSettings(settingsForm, model), - migrator: m => m - .add(0, prev => ({ - ...prev, - dataSourceType: prev['dataSourceType'] ?? 'values', - direction: prev['direction'] ?? 'horizontal', - })) - .add(1, prev => { - return { + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + migrator: (m) => + m + .add(0, (prev) => ({ ...prev, - referenceListId: getLegacyReferenceListIdentifier(prev.referenceListNamespace, prev.referenceListName), - }; - }) - .add(2, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(3, (prev) => migrateVisibility(prev)) - .add(4, (prev) => migrateReadOnly(prev)) - .add(5, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) - .add(6, (prev) => { - const styles: IInputStyles = { - style: prev.style - }; + dataSourceType: prev['dataSourceType'] ?? 'values', + direction: prev['direction'] ?? 'horizontal', + })) + .add(1, (prev) => { + return { + ...prev, + referenceListId: getLegacyReferenceListIdentifier(prev.referenceListNamespace, prev.referenceListName), + }; + }) + .add(2, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(3, (prev) => migrateVisibility(prev)) + .add(4, (prev) => migrateReadOnly(prev)) + .add(5, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) + .add(6, (prev) => { + const styles: IInputStyles = { + style: prev.style, + }; - return { ...prev, desktop: {...styles}, tablet: {...styles}, mobile: {...styles} }; - }) - , + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }), linkToModelMetadata: (model, metadata): IEnhancedRadioProps => { const isRefList = metadata.dataType === DataTypes.referenceListItem; - + return { ...model, dataSourceType: isRefList ? 'referenceList' : 'values', referenceListId: isRefList ? { - module: metadata.referenceListModule, - name: metadata.referenceListName, - } + module: metadata.referenceListModule, + name: metadata.referenceListName, + } : null, }; }, diff --git a/shesha-reactjs/src/designer-components/radio/radioGroup.tsx b/shesha-reactjs/src/designer-components/radio/radioGroup.tsx index 5e6a94501c..c130b25996 100644 --- a/shesha-reactjs/src/designer-components/radio/radioGroup.tsx +++ b/shesha-reactjs/src/designer-components/radio/radioGroup.tsx @@ -1,70 +1,59 @@ import { Radio, Space } from 'antd'; import React, { FC, useEffect, useMemo } from 'react'; import { useGet } from '@/hooks'; -import { useFormData, useGlobalState } from '@/providers'; import { useReferenceList } from '@/providers/referenceListDispatcher'; import ReadOnlyDisplayFormItem from '@/components/readOnlyDisplayFormItem'; import { getDataSourceList, IRadioProps } from './utils'; -import { evaluateValue } from '@/providers/form/utils'; const RadioGroup: FC = (model) => { - const { data: formData } = useFormData(); - const { globalState } = useGlobalState(); const { referenceListId, items = [], value, onChange, defaultValue } = model; const { data: refListItems } = useReferenceList(referenceListId); //#region Data source is url - const getEvaluatedUrl = (url: string) => { - if (!url) return ''; - return (() => { - // tslint:disable-next-line:function-constructor - return new Function('data, globalState', url)(formData, globalState); // Pass data, query, globalState - })(); - }; - - const { refetch, data } = useGet({ path: getEvaluatedUrl(model?.dataSourceUrl), lazy: true }); + const { refetch, data } = useGet({ path: model.dataSourceUrl, lazy: true }); useEffect(() => { - if (model?.dataSourceType === 'url' && model?.dataSourceUrl) { + if (model.dataSourceType === 'url' && model.dataSourceUrl) { refetch(); } - }, [model?.dataSourceType, model?.dataSourceUrl]); + }, [model.dataSourceType, model.dataSourceUrl]); useEffect(() => { if (defaultValue) { - onChange(evaluateValue(defaultValue, { data: formData, globalState })); + onChange(defaultValue); } }, [defaultValue]); const reducedData = useMemo(() => { const list = Array.isArray(data?.result) ? data?.result : data?.result?.items; - if (Array.isArray(list) && model?.reducerFunc) { - return new Function('data', model?.reducerFunc)(list) as []; + if (Array.isArray(list) && model.reducerFunc) { + return new Function('data', model.reducerFunc)(list) as []; } return data?.result; - }, [data?.result, model?.reducerFunc]); + }, [data?.result, model.reducerFunc]); //#endregion const options = useMemo( - () => getDataSourceList(model?.dataSourceType, items, refListItems?.items, reducedData) || [], - [model?.dataSourceType, items, refListItems?.items, reducedData] + () => getDataSourceList(model.dataSourceType, items, refListItems?.items, reducedData) || [], + [model.dataSourceType, items, refListItems?.items, reducedData] ); - const defaultVal = evaluateValue(defaultValue, { data: formData, globalState }); - const val = !!value ? `${value}` : defaultVal; + const val = value ? `${value}` : defaultValue; const renderCheckGroup = () => ( - + {options?.map((checkItem, index) => ( {checkItem.label} diff --git a/shesha-reactjs/src/designer-components/radio/settingsForm.json b/shesha-reactjs/src/designer-components/radio/settingsForm.json deleted file mode 100644 index 5868fd2fb2..0000000000 --- a/shesha-reactjs/src/designer-components/radio/settingsForm.json +++ /dev/null @@ -1,445 +0,0 @@ -{ - "components": [ - { - "id": "aIMnm2re1exM1M7pEEur9", - "type": "collapsiblePanel", - "propertyName": "pnlDisplay", - "label": "Display", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [], - "header": { - "id": "FizvoIoyeq1cZMqAgbDLL", - "components": [] - }, - "content": { - "id": "y9SNudmMM0Wd1Sc_YI1ng", - "components": [ - { - "id": "5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4", - "type": "contextPropertyAutocomplete", - "propertyName": "propertyName", - "parentId": "y9SNudmMM0Wd1Sc_YI1ng", - "label": "Property name", - "validate": { - "required": true - } - }, - { - "id": "46d07439-4c18-468c-89e1-60c002ce96c5", - "type": "textField", - "propertyName": "label", - "parentId": "y9SNudmMM0Wd1Sc_YI1ng", - "label": "Label", - "version": 3, - "textType": "text" - }, - { - "id": "57a40a33-7e08-4ce4-9f08-a34d24a83338", - "type": "dropdown", - "propertyName": "labelAlign", - "parentId": "y9SNudmMM0Wd1Sc_YI1ng", - "label": "Label align", - "values": [ - { - "label": "left", - "value": "left", - "id": "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - "label": "right", - "value": "right", - "id": "b920ef96-ae27-4a01-bfad-b5b7d07218da" - } - ], - "dataSourceType": "values", - "version": 5, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "2d32fe70-99a0-4825-ae6c-8b933004e119", - "type": "textArea", - "propertyName": "description", - "parentId": "y9SNudmMM0Wd1Sc_YI1ng", - "label": "Description", - "version": 2 - }, - { - "id": "cfd7d45e-c7e3-4a27-987b-dc525c412448", - "type": "checkbox", - "propertyName": "hidden", - "parentId": "y9SNudmMM0Wd1Sc_YI1ng", - "label": "Hidden", - "version": 2 - }, - { - "id": "c6885251-96a6-40ce-99b2-4b5209a9e01c", - "type": "checkbox", - "propertyName": "hideLabel", - "parentId": "y9SNudmMM0Wd1Sc_YI1ng", - "label": "Hide Label", - "version": 2 - }, - { - "id": "24a8be15-98eb-40f7-99ea-ebb602693e9c", - "type": "editModeSelector", - "propertyName": "editMode", - "parentId": "y9SNudmMM0Wd1Sc_YI1ng", - "label": "Edit mode" - } - ] - } - }, - { - "id": "1aIMnm2re1exM1M7pEEur9", - "type": "collapsiblePanel", - "propertyName": "pnlData", - "label": "Data", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [], - "header": { - "id": "1FizvoIoyeq1cZMqAgbDLL", - "components": [] - }, - "content": { - "id": "1y9SNudmMM0Wd1Sc_YI1ng", - "components": [ - { - "id": "6ad2237c-1c35-4b4d-8c71-cd3116aa0c29", - "type": "textField", - "propertyName": "defaultValue", - "parentId": "1y9SNudmMM0Wd1Sc_YI1ng", - "label": "Default Value", - "validate": {}, - "settingsValidationErrors": [], - "version": 3, - "textType": "text" - }, - { - "id": "f93134cb-049c-47ce-8ad4-6193ffe43ecf", - "type": "dropdown", - "jsSetting": false, - "propertyName": "dataSourceType", - "parentId": "1y9SNudmMM0Wd1Sc_YI1ng", - "label": "Data Source Type", - "dataSourceType": "values", - "values": [ - { - "label": "Values", - "value": "values", - "id": "0a2931b3-dbb6-43c1-9b85-358dc637f1df" - }, - { - "label": "Reference List", - "value": "referenceList", - "id": "447171c1-5d69-404b-bf58-1db2c28882a0" - }, - { - "label": "API Url", - "value": "url", - "id": "02501feb-f369-4306-a0fd-50f8c96410f8" - } - ], - "version": 5, - "referenceListId": null, - "valueFormat": "listItem" - }, - { - "id": "366f360b-a37b-4803-a2c2-8637208caa9d", - "type": "codeEditor", - "propertyName": "dataSourceUrl", - "label": "Data Source Url", - "parentId": "1y9SNudmMM0Wd1Sc_YI1ng", - "validate": {}, - "settingsValidationErrors": [], - "description": "Write a code that returns the URL to be used to fetch Data for the Radio.", - "exposedVariables": [ - { - "name": "data", - "description": "Form values", - "type": "object" - }, - { - "name": "globalState", - "description": "The global state", - "type": "object" - } - ], - "hidden": { - "_code": "return getSettingValue(data?.dataSourceType) !== 'url';", - "_mode": "code", - "_value": false - }, - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "getDataSourceUrl" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\").addStandard([\"shesha:formData\", \"shesha:globalState\"]).build();" - }, - { - "id": "c153b326-7ff2-4bb0-af21-45f80e8cd6e0", - "type": "codeEditor", - "propertyName": "reducerFunc", - "label": "Reducer function", - "parentId": "1y9SNudmMM0Wd1Sc_YI1ng", - "validate": {}, - "settingsValidationErrors": [], - "description": "A reducer function for the data returned from the server. The function is responsible for value and label props. The function should return an array of object of this format: { value, label }", - "exposedVariables": [ - { - "name": "data", - "description": "Data returned from the server. This should is an array", - "type": "array" - } - ], - "hidden": { - "_code": "return getSettingValue(data?.dataSourceType) !== 'url';", - "_mode": "code", - "_value": false - }, - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "reducerFunction" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\").addArray(\"data\", \"An array of items returned from the server\").build();" - }, - { - "id": "OBr7mbe9PFn54MX0wjb9L", - "type": "referenceListAutocomplete", - "propertyName": "referenceListId", - "label": "Reference List", - "labelAlign": "right", - "parentId": "1y9SNudmMM0Wd1Sc_YI1ng", - "hidden": { - "_code": "return getSettingValue(data?.dataSourceType) !== 'referenceList';", - "_mode": "code", - "_value": false - }, - "isDynamic": false, - "version": 2, - "convertToFullId": true, - "placeholder": "", - "description": "", - "validate": { - "message": "" - }, - "settingsValidationErrors": [] - }, - { - "id": "58f715b9-624b-4189-812c-6144cafb7405", - "type": "labelValueEditor", - "propertyName": "items", - "parentId": "1y9SNudmMM0Wd1Sc_YI1ng", - "label": "Items", - "labelTitle": "Label", - "labelName": "label", - "valueTitle": "Value", - "valueName": "value", - "hidden": { - "_code": "return getSettingValue(data?.dataSourceType) !== 'values';", - "_mode": "code", - "_value": false - }, - "version": 2, - "mode": "dialog" - }, - { - "id": "341473be-c60a-4cb4-8b9a-d2700c425d4a", - "type": "dropdown", - "propertyName": "direction", - "parentId": "1y9SNudmMM0Wd1Sc_YI1ng", - "label": "Direction", - "dataSourceType": "values", - "values": [ - { - "id": "f4617474-bcc6-40bc-b8b8-2332dbd05e16", - "label": "Vertical", - "value": "vertical" - }, - { - "id": "c625678e-ef11-4e68-95c0-4826e217a0f3", - "label": "Horizontal", - "value": "horizontal" - } - ], - "version": 5, - "referenceListId": null, - "valueFormat": "listItem" - } - ] - } - }, - { - "id": "2aIMnm2re1exM1M7pEEur9", - "type": "collapsiblePanel", - "propertyName": "pnlValidation", - "label": "Validation", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [], - "header": { - "id": "2FizvoIoyeq1cZMqAgbDLL", - "components": [] - }, - "content": { - "id": "2y9SNudmMM0Wd1Sc_YI1ng", - "components": [ - { - "id": "3be9da3f-f47e-48ae-b4c3-f5cc36e534d9", - "type": "checkbox", - "propertyName": "validate.required", - "parentId": "2y9SNudmMM0Wd1Sc_YI1ng", - "label": "Required", - "version": 2 - } - ] - } - }, - { - "id": "kp3afwRnhZk2958QCUvFTFeGWOokZY", - "type": "propertyRouter", - "propertyName": "propertyRouter1", - "componentName": "propertyRouter1", - "label": "Property router1", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "propertyRouteName": { - "_mode": "code", - "_code": " return contexts.canvasContext?.designerDevice || 'desktop';", - "_value": "" - }, - "components": [ - { - "id": "3aIMnm2re1exM1M7pEEur9", - "type": "collapsiblePanel", - "propertyName": "pnlStyle", - "label": "Style", - "labelAlign": "right", - "parentId": "root", - "hidden": false, - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [], - "header": { - "id": "3FizvoIoyeq1cZMqAgbDLL", - "components": [] - }, - "content": { - "id": "3y9SNudmMM0Wd1Sc_YI1ng", - "components": [ - { - "id": "06ab0599-914d-4d2d-875c-765a495472f8", - "type": "codeEditor", - "propertyName": "style", - "label": "Style", - "parentId": "3y9SNudmMM0Wd1Sc_YI1ng", - "validate": {}, - "settingsValidationErrors": [], - "description": "A script that returns the style of the element as an object. This should conform to CSSProperties", - "exposedVariables": [ - { - "name": "data", - "description": "Form values", - "type": "object" - } - ], - "version": 3, - "language": "typescript", - "placeholder": "", - "wrapInTemplate": true, - "templateSettings": { - "functionName": "getStyle" - }, - "availableConstantsExpression": " return metadataBuilder.object(\"constants\").addStandard([\"shesha:formData\", \"shesha:globalState\"]).build();" - } - ] - } - } - ] - }, - { - "id": "6Vw9iiDw9d0MD_Rh5cbIn", - "type": "collapsiblePanel", - "propertyName": "pnlSecurity", - "label": "Security", - "labelAlign": "right", - "parentId": "root", - "isDynamic": false, - "version": 4, - "expandIconPosition": "start", - "header": { - "id": "mbzE03mJnv_89oUvkmjPs", - "components": [] - }, - "content": { - "id": "SsWPpiJfTbgl4iHlFA_or", - "components": [ - { - "id": "1adea529-1f0c-4def-bd41-ee166a5dfcd7", - "type": "permissionAutocomplete", - "propertyName": "permissions", - "parentId": "SsWPpiJfTbgl4iHlFA_or", - "label": "Permissions", - "validate": {}, - "version": 1 - } - ] - }, - "collapsible": "header", - "ghost": true, - "hideWhenEmpty": true, - "settingsValidationErrors": [] - } - ], - "formSettings": { - "layout": "horizontal", - "colon": true, - "labelCol": { - "span": 8 - }, - "wrapperCol": { - "span": 16 - }, - "displayName": "DEFAULT_FORM_SETTINGS", - "__docgenInfo": { - "description": "Default form settings", - "displayName": "DEFAULT_FORM_SETTINGS", - "props": {} - } - } -} \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/radio/settingsForm.ts b/shesha-reactjs/src/designer-components/radio/settingsForm.ts new file mode 100644 index 0000000000..c43b838871 --- /dev/null +++ b/shesha-reactjs/src/designer-components/radio/settingsForm.ts @@ -0,0 +1,355 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const validationTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const customStylePanelId = nanoid(); + const stylePanelId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textArea', + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + defaultValue: 'inherited', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }, + ], + }) + + .toJson(), + ], + }, + { + key: '2', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + inputType: 'textField', + id: nanoid(), + propertyName: 'defaultValue', + label: 'Default Value', + jsSetting: true, + parentId: dataTabId, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'dataSourceType', + label: 'Data Source Type', + size: 'small', + jsSetting: true, + parentId: dataTabId, + dropdownOptions: [ + { + label: 'Values', + value: 'values', + }, + { + label: 'Reference list', + value: 'referenceList', + }, + { + label: 'API URL', + value: 'url', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.dataSourceType) !== "values";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + id: nanoid(), + type: 'labelValueEditor', + propertyName: 'items', + parentId: dataTabId, + label: 'Items', + labelTitle: 'Label', + labelName: 'label', + valueTitle: 'Value', + valueName: 'value', + jsSetting: true, + mode: 'dialog', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.dataSourceType) !== "referenceList";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'referenceListAutocomplete', + id: nanoid(), + propertyName: 'referenceListId', + label: 'Reference List', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + hidden: { + _code: 'return getSettingValue(data?.dataSourceType) !== "url";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'codeEditor', + id: nanoid(), + propertyName: 'dataSourceUrl', + label: 'Data Source URL', + jsSetting: true, + }, + { + type: 'codeEditor', + id: nanoid(), + propertyName: 'reducerFunc', + label: 'Reducer Function', + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + { + key: '3', + title: 'Validation', + id: validationTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + propertyName: 'validate.required', + label: 'Required', + inputType: 'switch', + size: 'small', + layout: 'horizontal', + jsSetting: true, + parentId: validationTabId, + }) + .toJson(), + ], + }, + { + key: '4', + title: 'Events', + id: eventsTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onChangeCustom', + label: 'On Change', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on changing of event.', + parentId: eventsTabId, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onFocusCustom', + label: 'On Focus', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on focus of event.', + parentId: eventsTabId, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onBlurCustom', + label: 'On Blur', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on blur of event.', + parentId: eventsTabId, + }) + .toJson(), + ], + }, + { + key: '5', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'direction', + label: 'Direction', + size: 'small', + jsSetting: true, + parentId: styleRouterId, + defaultValue: 'horizontal', + dropdownOptions: [ + { + label: 'Horizontal', + value: 'horizontal', + }, + { + label: 'Vertical', + value: 'vertical', + }, + ], + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: customStylePanelId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + parentId: stylePanelId, + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: '6', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/radio/utils.ts b/shesha-reactjs/src/designer-components/radio/utils.ts index c73914bc30..0863ef7ac3 100644 --- a/shesha-reactjs/src/designer-components/radio/utils.ts +++ b/shesha-reactjs/src/designer-components/radio/utils.ts @@ -1,12 +1,11 @@ import { RadioChangeEvent, SpaceProps } from 'antd'; -import { IConfigurableFormComponent } from '@/interfaces'; import { nanoid } from '@/utils/uuid'; import { IReferenceListIdentifier } from '@/interfaces/referenceList'; import { CSSProperties } from 'react'; import { ReferenceListItemDto } from '@/apis/referenceList'; import { DataSourceType, ILabelValue } from '@/designer-components/dropdown/model'; -export interface IRadioProps extends Omit { +export interface IRadioProps { items?: ILabelValue[]; /** * @deprecated - use referenceListId instead @@ -21,9 +20,13 @@ export interface IRadioProps extends Omit { direction?: SpaceProps['direction']; value?: any; onChange?: (e: RadioChangeEvent) => void; + onBlur?: (e: any) => void; + onFocus?: (e: any) => void; style?: CSSProperties; dataSourceUrl?: string; reducerFunc?: string; // The function that receives data from the API and returns it in the format { value, label } + readOnly?: boolean; + defaultValue?: any; } export const getDataSourceList = ( diff --git a/shesha-reactjs/src/designer-components/rate/index.tsx b/shesha-reactjs/src/designer-components/rate/index.tsx index 1b08a62f7e..13a746e597 100644 --- a/shesha-reactjs/src/designer-components/rate/index.tsx +++ b/shesha-reactjs/src/designer-components/rate/index.tsx @@ -1,9 +1,9 @@ import _ from 'lodash'; import classNames from 'classnames'; import React from 'react'; -import { customOnChangeValueEventHandler } from '@/components/formDesigner/components/utils'; -import { getSettings } from './settings'; -import { getStyle, useAvailableConstantsData, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { getAllEventHandlers } from '@/components/formDesigner/components/utils'; +import { getSettings } from './settingsForm'; +import { validateConfigurableComponentSettings } from '@/providers/form/utils'; import { IconType } from '@/components/shaIcon'; import { IToolboxComponent } from '@/interfaces'; import { LikeOutlined, StarFilled } from '@ant-design/icons'; @@ -35,23 +35,21 @@ const RateComponent: IToolboxComponent = { icon: , isInput: true, isOutput: true, - Factory: ({ model }) => { - const allData = useAvailableConstantsData(); - - const { allowClear, icon, count, tooltips, className, style, readOnly } = model; - - if (model.hidden) return null; - + calculateModel: (model, allData) => ({ + eventHandlers: getAllEventHandlers(model, allData), + }), + Factory: ({ model, calculatedModel }) => { + const { allowClear, icon, count, tooltips, className, readOnly } = model; const localCount = !_.isNaN(count) ? count : 5; - return ( - + return model.hidden + ? null + : {(value, onChange) => { - const customEvent = customOnChangeValueEventHandler(model, allData); - const onChangeInternal = (...args: any[]) => { - customEvent.onChange(args[0]); - if (typeof onChange === 'function') - onChange(args); + const customEvent = calculatedModel.eventHandlers; + const onChangeInternal = (value: number) => { + customEvent.onChange({value}); + if (typeof onChange === 'function') onChange(value); }; return = { //allowHalf={allowHalf} character={icon ? : } disabled={readOnly} - count={localCount} + count={localCount ?? 5} tooltips={tooltips} className={classNames(className, 'sha-rate')} - style={getStyle(style, allData.data)} // Temporary. Make it configurable + style={model.allStyles.fullStyle} {...customEvent} value={value} onChange={onChangeInternal} />; }} - ); + ; }, settingsFormMarkup: (data) => getSettings(data), validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), @@ -78,6 +76,11 @@ const RateComponent: IToolboxComponent = { .add(1, (prev) => migrateVisibility(prev)) .add(2, (prev) => migrateReadOnly(prev)) .add(3, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) + .add(4, (prev) => { + prev.hideLabel = true; + if (!prev.icon) prev.icon = 'StarFilled'; + return prev; + }) , }; diff --git a/shesha-reactjs/src/designer-components/rate/settings.ts b/shesha-reactjs/src/designer-components/rate/settings.ts index 356f62bc91..338c9014dd 100644 --- a/shesha-reactjs/src/designer-components/rate/settings.ts +++ b/shesha-reactjs/src/designer-components/rate/settings.ts @@ -12,8 +12,8 @@ export const getSettings = (data: any) => ghost: true, collapsible: 'header', content: { - id:'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() + id: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', + components: [...new DesignerToolbarSettings() .addContextPropertyAutocomplete({ id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', propertyName: 'propertyName', @@ -68,7 +68,7 @@ export const getSettings = (data: any) => id: 'cfd7d45e-c7e3-4a27-987b-dc525c412448', propertyName: 'hidden', parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Hidden', + label: 'hide', }) .addCheckbox({ id: 'c6885251-96a6-40ce-99b2-4b5209a9e01c', @@ -95,8 +95,8 @@ export const getSettings = (data: any) => ghost: true, collapsible: 'header', content: { - id:'2pnl54bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() + id: '2pnl54bf6-f76d-4139-a850-c99bf06c8b69', + components: [...new DesignerToolbarSettings() .addCodeEditor({ id: 'b9269416-3b78-42c4-934e-3e0dac8c7f01', propertyName: 'onChangeCustom', @@ -172,7 +172,7 @@ export const getSettings = (data: any) => ], wrapInTemplate: true, templateSettings: { - functionName: 'onChange' + functionName: 'onChange' }, availableConstantsExpression: async ({ metadataBuilder }) => { return metadataBuilder @@ -195,8 +195,8 @@ export const getSettings = (data: any) => ghost: true, collapsible: 'header', content: { - id:'3pnl4bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() + id: '3pnl4bf6-f76d-4139-a850-c99bf06c8b69', + components: [...new DesignerToolbarSettings() .addCodeEditor({ id: '987c3de1-b959-4670-96f6-9b1747189a6e', propertyName: 'style', @@ -227,8 +227,8 @@ export const getSettings = (data: any) => ghost: true, collapsible: 'header', content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() + id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', + components: [...new DesignerToolbarSettings() .addPermissionAutocomplete({ id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', propertyName: 'permissions', @@ -239,6 +239,6 @@ export const getSettings = (data: any) => validate: {}, }).toJson() ] - } - }) + } + }) .toJson(); diff --git a/shesha-reactjs/src/designer-components/rate/settingsForm.ts b/shesha-reactjs/src/designer-components/rate/settingsForm.ts new file mode 100644 index 0000000000..ca373cca96 --- /dev/null +++ b/shesha-reactjs/src/designer-components/rate/settingsForm.ts @@ -0,0 +1,230 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { nanoid } from '@/utils/uuid'; +import { IRateProps } from '.'; + +export const getSettings = (data: IRateProps) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const eventsTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property name', + parentId: commonTabId, + styledLabel: true, + validate: { required: true }, + size: 'small', + jsSetting: true + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + defaultValue: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textArea', + id: nanoid(), + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + hidden: { _code: 'return getSettingValue(data?.hideLabel);', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'count', + label: 'Max Rating', + jsSetting: true, + }, + { + type: 'iconPicker', + id: nanoid(), + propertyName: 'icon', + label: 'Icon', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + defaultValue: 'inherited', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + size: 'small', + defaultValue: 'inherit', + jsSetting: true, + } + ] + }) + .toJson() + ] + }, + { + key: 'events', + title: 'Events', + id: eventsTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onChangeCustom', + label: 'On Change', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on changing of event.', + parentId: eventsTabId + }) + .addSettingsInput({ + id: '88c2d96c-b808-4316-8a36-701b09e5f6c7', + inputType: 'codeEditor', + propertyName: 'onFocusCustom', + label: 'On Focus', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on focus of event.', + parentId: 'Cc47W08MWrKdhoGqFKMI2' + }) + .addSettingsInput({ + id: '4a2b7329-1a89-45d1-a5b0-f66db21744b0', + inputType: 'codeEditor', + propertyName: 'onBlurCustom', + label: 'On Blur', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on blur of event.', + parentId: 'Cc47W08MWrKdhoGqFKMI2' + }) + .addSettingsInput({ + id: '1232d96c-b808-4316-8a36-701b09e5f6c7', + inputType: 'codeEditor', + propertyName: 'onClickCustom', + label: 'On Click', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on click of event.', + parentId: 'Cc47W08MWrKdhoGqFKMI2' + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: 'styleRouter', + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: 'elgrlievlfwehhh848r8hsdnflsdnclurbd', + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: 'customStyleCollapsiblePanel', + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson() + ] + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson() + ] + }, + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/refListStatus/index.tsx b/shesha-reactjs/src/designer-components/refListStatus/index.tsx index 8e90095cc9..f47c25d9df 100644 --- a/shesha-reactjs/src/designer-components/refListStatus/index.tsx +++ b/shesha-reactjs/src/designer-components/refListStatus/index.tsx @@ -4,15 +4,16 @@ import React from 'react'; import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; import { IToolboxComponent } from '@/interfaces'; -import { useForm, useFormData, useGlobalState } from '@/providers'; +import { IInputStyles, useForm } from '@/providers'; import { IRefListStatusPropsV0 } from './migrations/models'; import { IRefListStatusProps } from './models'; -import { RefListStatusSettingsForm } from './settings'; -import { executeCustomExpression, getStyle } from '@/providers/form/utils'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; import { RefListStatus } from '@/components/refListStatus/index'; import { migrateVisibility } from '@/designer-components/_common-migrations/migrateVisibility'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './settings'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultStyles } from './utils'; const RefListStatusComponent: IToolboxComponent = { type: 'refListStatus', @@ -21,17 +22,10 @@ const RefListStatusComponent: IToolboxComponent = { name: 'Reference list status', icon: , Factory: ({ model }) => { - const { formMode, formData: data } = useForm(); + const { formMode } = useForm(); const { solidBackground = true, referenceListId, showReflistName = true } = model; - const { data: formData } = useFormData(); - const { globalState } = useGlobalState(); - - const isVisibleByCondition = executeCustomExpression(model?.customVisibility, true, formData, globalState); - - const style = {...getStyle(model.style, data, globalState)}; - - if (!isVisibleByCondition && formMode !== 'designer') return null; + if (model?.hidden && formMode !== 'designer') return null; if (formMode === 'designer' && !referenceListId) { return ( @@ -43,7 +37,7 @@ const RefListStatusComponent: IToolboxComponent = { /> ); } - + return ( {(value) => { @@ -54,7 +48,8 @@ const RefListStatusComponent: IToolboxComponent = { showIcon={model.showIcon} showReflistName={showReflistName} solidBackground={solidBackground} - style={style} /> + style={model.allStyles?.fullStyle ?? {}} + /> ); }} @@ -90,10 +85,27 @@ const RefListStatusComponent: IToolboxComponent = { }) .add(2, (prev) => migratePropertyName(migrateCustomFunctions(prev))) .add(3, (prev) => migrateVisibility(prev)) - .add(4, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) + .add(4, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) + .add(5, (prev) => { + const styles: IInputStyles = { + size: prev.size, + width: prev.width, + height: prev.height, + hideBorder: prev.hideBorder, + borderSize: prev.borderSize, + borderRadius: prev.borderRadius, + borderColor: prev.borderColor, + fontSize: prev.fontSize, + fontColor: prev.fontColor, + backgroundColor: prev.backgroundColor, + stylingBox: prev.stylingBox, + }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }) + .add(6, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })) , - settingsFormMarkup: RefListStatusSettingsForm, - validateSettings: (model) => validateConfigurableComponentSettings(RefListStatusSettingsForm, model), + settingsFormMarkup: data => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), linkToModelMetadata: (model, metadata): IRefListStatusProps => { return { ...model, diff --git a/shesha-reactjs/src/designer-components/refListStatus/models.ts b/shesha-reactjs/src/designer-components/refListStatus/models.ts index 5dd9031a79..96d35e5772 100644 --- a/shesha-reactjs/src/designer-components/refListStatus/models.ts +++ b/shesha-reactjs/src/designer-components/refListStatus/models.ts @@ -1,8 +1,8 @@ -import { IConfigurableFormComponent } from '@/providers'; +import { IConfigurableFormComponent, IInputStyles } from '@/providers'; import { IFormItem } from '@/interfaces'; import { IReferenceListIdentifier } from '@/interfaces/referenceList'; -export interface IRefListStatusProps extends IConfigurableFormComponent, Omit { +export interface IRefListStatusProps extends IConfigurableFormComponent, Omit ,IInputStyles{ referenceListId: IReferenceListIdentifier; showIcon?: boolean; solidBackground?: boolean; diff --git a/shesha-reactjs/src/designer-components/refListStatus/settings.ts b/shesha-reactjs/src/designer-components/refListStatus/settings.ts index 20fa622e7c..a6e37fdf94 100644 --- a/shesha-reactjs/src/designer-components/refListStatus/settings.ts +++ b/shesha-reactjs/src/designer-components/refListStatus/settings.ts @@ -1,202 +1,601 @@ -import { nanoid } from '@/utils/uuid'; import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { fontTypes, fontWeights, textAlign } from '../_settings/utils/font/utils'; +import { getBorderInputs, getCornerInputs } from '../_settings/utils/border/utils'; +import { IRefListStatusProps } from './models'; +import { backgroundTypeOptions, positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { nanoid } from '@/utils/uuid'; -export const RefListStatusSettingsForm = new DesignerToolbarSettings() - .addCollapsiblePanel({ - id: "b8954bf6-f76d-4139-a850-c99bf06c8b69", - propertyName: "separator1", - parentId: "root", - label: "Display", - labelAlign: "right", - expandIconPosition: "start", - ghost: true, - hideWhenEmpty: true, - header: { - id: '3342DA1C-DA07-46F6-8026-E8B9A93F094A', - components: [] - }, - content: { - id: '1BCC52E8-FD3B-4309-AD9B-099CDB729441', - components: new DesignerToolbarSettings() - .addContextPropertyAutocomplete({ - id: "5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4", - propertyName: "propertyName", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Property name", - validate: { - required: true - } - }) - .addTextField({ - id: "46d07439-4c18-468c-89e1-60c002ce96c5", - propertyName: "label", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Label" - }) - .addDropdown({ - id: "57a40a33-7e08-4ce4-9f08-a34d24a83338", - propertyName: "labelAlign", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Label align", - values: [ - { - label: "left", - value: "left", - id: "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - label: "right", - value: "right", - id: "b920ef96-ae27-4a01-bfad-b5b7d07218da" - } - ], - dataSourceType: "values" - }) - .addTextArea({ - id: "2d32fe70-99a0-4825-ae6c-8b933004e119", - propertyName: "description", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Description", - readOnly: false - }) - .addCheckbox({ - id: "cfd7d45e-c7e3-4a27-987b-dc525c412448", - propertyName: "hidden", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Hidden" - }) - .addCheckbox({ - id: "c6885251-96a6-40ce-99b2-4v5209a9e01c", - propertyName: "hideLabel", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - defaultValue: true, - label: "Hide Label" - }) - .toJson() - } - }) - .addCollapsiblePanel({ - id: nanoid(), - propertyName: "pnlCustomizeStatus", - parentId: "root", - label: "Customize Status", - labelAlign: "right", - expandIconPosition: "start", - ghost: true, - hideWhenEmpty: true, - header: { - id: nanoid(), - components: [] - }, - content: { - id: '5478b8f9-ec00-4d0a-9k2a-44a630cb2dcb', - components: new DesignerToolbarSettings() - .addCheckbox({ - id: "3fg9da3f-f47e-48ae-b4c3-f5cc36f934d9", - propertyName: "showReflistName", - parentId: "5478b8f9-ec00-4d0a-9k2a-44a630cb2dcb", - description: 'When checked the DisplayName/RefList Name will be shown.', - defaultValue: true, - label: "Show Reflist Item Name" - }) - .addCheckbox({ - id: "3be9da3f-f47e-48ae-b4c3-f5cc36f934d9", - propertyName: "showIcon", - parentId: "5478b8f9-ec00-4d0a-9k2a-44a630cb2dcb", - description: 'When checked the icon will display on the left side of the DisplayName', - label: "Show Icon" - }) - .addCheckbox({ - id: "3be9da3f-f47e-49ae-b8c3-f5cc36f164d9", - propertyName: "solidBackground", - parentId: "5478b8f9-ec00-4d0a-9k2a-44a630cb2dcb", - description: 'When checked the component will show a coloured badge and display within it in white font the icon and/or the selected reference list item label.', - defaultValue: true, - label: "Show Solid Background" +export const getSettings = (data: IRefListStatusProps) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const fontStylePnlId = nanoid(); + const dimensionsStylePnlId = nanoid(); + const borderStylePnlId = nanoid(); + const backgroundStylePnlId = nanoid(); + const shadowStylePnlId = nanoid(); - }) - .toJson() - } - }) - .addCollapsiblePanel({ - id: nanoid(), - propertyName: "pnlStyle", - parentId: "root", - label: "Style", - labelAlign: "right", - expandIconPosition: "start", - ghost: true, - hideWhenEmpty: true, - header: { - id: nanoid(), - components: [] - }, - content: { - id: 'D3AA47EB-6047-4009-8AF1-2E36357CAC5D', - components: new DesignerToolbarSettings() - .addCodeEditor({ - id: "06ab0599-914d-4d2d-875c-765a495472f8", - propertyName: "style", - label: "Style", - parentId: "D3AA47EB-6047-4009-8AF1-2E36357CAC5D", - validate: {}, - settingsValidationErrors: [], - description: "A script that returns the style of the element as an object. This should conform to CSSProperties", - exposedVariables: [ - { id: "3tg9da3f-f58e-48ae-b4c3-f5cc36e534d7", name: "data", description: "Form values", type: "object" }, - { id: nanoid(), name: "globalState", description: "The global state of the application", type: "object" } - ] - }) - .toJson() - } - }) - .addCollapsiblePanel({ - id: nanoid(), - propertyName: "pnlRefListSource", - parentId: "root", - label: "RefList Source", - description: 'This section is for binding your status tag to a specific RefList Properties', - labelAlign: "right", - expandIconPosition: "start", - ghost: true, - hideWhenEmpty: true, - header: { - id: nanoid(), - components: [] - }, - content: { - id: 'B30AECE7-4EEB-4E35-813B-F5BE13C7B1B1', - components: new DesignerToolbarSettings() - .addRefListAutocomplete({ - id: nanoid(), - propertyName: 'referenceListId', - label: 'Reference list' - }) - .toJson() + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property Name', + parentId: commonTabId, + styledLabel: true, + size: 'small', + validate: { + required: true, + }, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + type: 'textArea', + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + }) + .toJson() + ] + }, + { + key: '2', + title: 'Data', + id: dataTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + id: nanoid(), + type: 'referenceListAutocomplete', + propertyName: 'referenceListId', + label: 'Reference List', + parentId: dataTabId, + validate: { + required: true, + }, + jsSetting: true, + }, + { + id: nanoid(), + type: 'switch', + propertyName: 'showReflistName', + label: ' Show Reference List Item Name', + tooltip: 'When checked the DisplayName/RefList Name will be shown.', + size: 'small', + defaultValue: true, + parentId: dataTabId + }, + + ], + }) + + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'showIcon', + label: 'Show Icon', + size: 'small', + tooltip: 'When checked the icon will display on the left side of the DisplayName', + }) + .toJson() + ] + }, + { + key: '3', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: fontStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: fontStylePnlId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: "Controls text thickness (light, normal, bold, etc.)", + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Align', + propertyName: 'font.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: dimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + + }, + { + type: 'textField', + id: nanoid(), + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: nanoid(), + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: nanoid(), + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: borderStylePnlId, + components: [...new DesignerToolbarSettings() + .addContainer({ + id: nanoid(), + parentId: borderStylePnlId, + components: getBorderInputs() as any + }) + .addContainer({ + id: nanoid(), + parentId: borderStylePnlId, + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: backgroundStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'solidBackground', + label: 'Show Solid Background', + size: 'small', + defaultValue: true, + tooltip: 'When checked the component will show a coloured badge and display within it in white font the icon and/or the selected reference list item label.', + parentId: backgroundStylePnlId + }) + .addSettingsInput({ + id: nanoid(), + parentId: backgroundStylePnlId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: backgroundTypeOptions, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + inputs: [{ + type: 'imageUploader', + id: nanoid(), + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: backgroundStylePnlId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStyleRow', + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: nanoid(), + label: "Size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + hideLabel: true, + propertyName: "background.size", + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: nanoid(), + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'backgroundStyleRow', + inputs: [{ + type: 'radio', + id: nanoid(), + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: shadowStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: shadowStylePnlId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + tooltip: 'Offset X', + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + tooltip: 'Offset Y', + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + tooltip: 'Blur Radius', + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + tooltip: 'Spread Radius', + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson()] + }).toJson()] + }, + { + key: '4', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId + }) + .toJson() + ] + } + ] + }).toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } } - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] - } - }) - .toJson(); \ No newline at end of file + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/refListStatus/utils.ts b/shesha-reactjs/src/designer-components/refListStatus/utils.ts new file mode 100644 index 0000000000..3b2920dbfa --- /dev/null +++ b/shesha-reactjs/src/designer-components/refListStatus/utils.ts @@ -0,0 +1,32 @@ +export const defaultStyles = (): any => { + return { + background: { type: 'color', color: '' }, + font: { + weight: '400', + size: 12, + color: '#000', + type: 'Segoe UI', + align: 'center' + }, + border: { + border: { + all: { + width: '1px', + style: 'solid', + color: '#d9d9d9' + } + }, + radius: { all: 4 }, + selectedBorder: 'all', + selectedCorner: 'all' + }, + dimensions: { + width: 'auto', + height: '24px', + minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto' + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/richTextEditor/formSettings.ts b/shesha-reactjs/src/designer-components/richTextEditor/formSettings.ts new file mode 100644 index 0000000000..0b461f692f --- /dev/null +++ b/shesha-reactjs/src/designer-components/richTextEditor/formSettings.ts @@ -0,0 +1,668 @@ +import { DesignerToolbarSettings } from '@/index'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/lib/form/Form'; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const optionsCollapsiblePanelId = nanoid(); + const toolbarCollapsiblePanelId = nanoid(); + const displayCollapsiblePanelId = nanoid(); + const advancedCollapsiblePanelId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + parentId: commonTabId, + label: 'Property Name', + size: 'small', + validate: { + required: true, + }, + styledLabel: true, + jsSetting: true, + }) + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'textField', + propertyName: 'placeholder', + label: 'Placeholder', + parentId: commonTabId, + jsSetting: true, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'textArea', + propertyName: 'description', + label: 'Tooltip', + parentId: commonTabId, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + jsSetting: true, + defaultValue: 'inherited' + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + // Toolbar section + .addCollapsiblePanel({ + id: toolbarCollapsiblePanelId, + propertyName: 'toolbarPanel', + label: 'Toolbar', + labelAlign: 'right', + parentId: dataTabId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: toolbarCollapsiblePanelId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'toolbar', + label: 'Show Toolbar', + jsSetting: true, + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'toolbarButtonSize', + label: 'Size Of Icons', + parentId: toolbarCollapsiblePanelId, + jsSetting: true, + dropdownOptions: [ + { label: 'Tiny', value: 'tiny' }, + { label: 'Extra small', value: 'xsmall' }, + { label: 'Middle', value: 'middle' }, + { label: 'Large', value: 'large' }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'disablePlugins', + label: 'Hidden Actions', + parentId: toolbarCollapsiblePanelId, + jsSetting: true, + dropdownMode: 'multiple', + dropdownOptions: [ + { label: 'add-new-line', value: 'add-new-line' }, + { label: 'about', value: 'about' }, + { label: 'focus', value: 'focus' }, + { label: 'class-span', value: 'class-span' }, + { label: 'backspace', value: 'backspace' }, + { label: 'bold', value: 'bold' }, + { label: 'wrap-nodes', value: 'wrap-nodes' }, + { label: 'clean-html', value: 'clean-html' }, + { label: 'copy-format', value: 'copy-format' }, + { label: 'clipboard', value: 'clipboard' }, + { label: 'paste', value: 'paste' }, + { label: 'paste-storage', value: 'paste-storage' }, + { label: 'drag-and-drop', value: 'drag-and-drop' }, + { label: 'drag-and-drop-element', value: 'drag-and-drop-element' }, + { label: 'paste-from-word', value: 'paste-from-word' }, + { label: 'color', value: 'color' }, + { label: 'enter', value: 'enter' }, + { label: 'key-arrow-outside', value: 'key-arrow-outside' }, + { label: 'error-messages', value: 'error-messages' }, + { label: 'font', value: 'font' }, + { label: 'format-block', value: 'format-block' }, + { label: 'fullsize', value: 'fullsize' }, + { label: 'hotkeys', value: 'hotkeys' }, + { label: 'iframe', value: 'iframe' }, + { label: 'indent', value: 'indent' }, + { label: 'hr', value: 'hr' }, + { label: 'inline-popup', value: 'inline-popup' }, + { label: 'justify', value: 'justify' }, + { label: 'line-height', value: 'line-height' }, + { label: 'limit', value: 'limit' }, + { label: 'link', value: 'link' }, + { label: 'mobile', value: 'mobile' }, + { label: 'ordered-list', value: 'ordered-list' }, + { label: 'powered-by-jodit', value: 'powered-by-jodit' }, + { label: 'placeholder', value: 'placeholder' }, + { label: 'redo-undo', value: 'redo-undo' }, + { label: 'resizer', value: 'resizer' }, + { label: 'search', value: 'search' }, + { label: 'select', value: 'select' }, + { label: 'size', value: 'size' }, + { label: 'resize-handler', value: 'resize-handler' }, + { label: 'source', value: 'source' }, + { label: 'stat', value: 'stat' }, + { label: 'sticky', value: 'sticky' }, + { label: 'spellcheck', value: 'spellcheck' }, + { label: 'symbols', value: 'symbols' }, + { label: 'tooltip', value: 'tooltip' }, + { label: 'tab', value: 'tab' }, + { label: 'xpath', value: 'xpath' }, + { label: 'image-properties', value: 'image-properties' }, + { label: 'image-processor', value: 'image-processor' }, + { label: 'image', value: 'image' }, + { label: 'media', value: 'media' }, + { label: 'video', value: 'video' }, + { label: 'file', value: 'file' }, + { label: 'resize-cells', value: 'resize-cells' }, + { label: 'select-cells', value: 'select-cells' }, + { label: 'table-keyboard-navigation', value: 'table-keyboard-navigation' }, + { label: 'table', value: 'table' }, + { label: 'preview', value: 'preview' }, + { label: 'print', value: 'print' }, + ], + }) + .toJson(), + ], + }, + }) + // Options section + .addCollapsiblePanel({ + id: optionsCollapsiblePanelId, + propertyName: 'optionsPanel', + label: 'Options', + labelAlign: 'right', + parentId: dataTabId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: optionsCollapsiblePanelId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'autofocus', + label: 'Auto Focus', + tooltip: "If yes, will automatically be focused (be selected component) when the page loads", + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: optionsCollapsiblePanelId, + inputs: [ + { + type: 'dropdown', + id: nanoid(), + propertyName: 'preset', + label: 'Presets', + tooltip: "Choose a pre-configured toolbar layout", + dropdownOptions: [ + { label: 'None', value: 'null' }, + { label: 'Inline', value: 'inline' }, + ], + } + ], + }) + .toJson(), + ], + }, + }) + // Display section + .addCollapsiblePanel({ + id: displayCollapsiblePanelId, + propertyName: 'displayPanel', + label: 'Display', + labelAlign: 'right', + parentId: dataTabId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'direction', + label: 'Direction', + parentId: displayCollapsiblePanelId, + jsSetting: true, + dropdownOptions: [ + { label: 'Auto', value: '' }, + { label: 'rtl', value: 'rtl' }, + { label: 'ltr', value: 'ltr' }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: displayCollapsiblePanelId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'showCharsCounter', + label: 'Show Characters Counter', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'showWordsCounter', + label: 'Show Words Counter', + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + }) + // Advanced section + .addCollapsiblePanel({ + id: advancedCollapsiblePanelId, + propertyName: 'advancedPanel', + label: 'Advanced', + labelAlign: 'right', + parentId: dataTabId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'enter', + label: 'Element That Will Be Created On Enter', + parentId: advancedCollapsiblePanelId, + dropdownOptions: [ + { label: 'Break (BR)', value: 'BR' }, + { label: 'Paragraph (P)', value: 'P' }, + { label: 'Block (DIV)', value: 'DIV' }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: advancedCollapsiblePanelId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'insertImageAsBase64URI', + label: 'Insert Image As Base64 URI', + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'iframe', + label: 'Iframe Mode', + tooltip: "Isolates the editor content in a separate iframe for better style separation", + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: advancedCollapsiblePanelId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'saveHeightInStorage', + label: 'Remember Height', + tooltip: "Remembers the editor height for the next session", + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'saveModeInStorage', + label: 'Remember Mode', + tooltip: "Remembers the editor mode (WYSIWYG/Source) for the next session", + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: advancedCollapsiblePanelId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'askBeforePasteHTML', + label: 'Ask Before Paste HTML', + tooltip: "If yes, will ask the user to confirm before allowing content copied from a Web page to be pasted in", + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'askBeforePasteFromWord', + label: 'Ask Before Paste From Word/Excel', + tooltip: "If yes, will ask the user to confirm before allowing content from Excel or Word to be pasted in", + }, + ], + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'dropdown', + propertyName: 'theme', + label: 'Theme', + parentId: styleRouterId, + jsSetting: true, + dropdownOptions: [ + { label: 'Default', value: 'default' }, + { label: 'Dark', value: 'dark' }, + ], + }) + .addCollapsiblePanel({ + id: 'dimensionsStyleCollapsiblePanel', + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: 'dimensionsStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'dimensionsStylePnl', + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'autoWidth', + label: 'Auto Width', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'autoHeight', + label: 'Auto Height', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'dimensionsStylePnl', + inline: true, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.autoWidth);', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Width', + width: 85, + propertyName: 'dimensions.width', + icon: 'widthIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minWidth', + icon: 'minWidthIcon', + hidden: { + _code: + 'return !getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.allowResizeX);', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Width', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxWidth', + icon: 'maxWidthIcon', + hidden: { + _code: + 'return !getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.allowResizeX);', + _mode: 'code', + _value: false, + } as any, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: 'dimensionsStylePnl', + inline: true, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.autoHeight);', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Height', + width: 85, + propertyName: 'dimensions.height', + icon: 'heightIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + { + type: 'textField', + id: nanoid(), + label: 'Min Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.minHeight', + icon: 'minHeightIcon', + hidden: { + _code: + 'return !getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.allowResizeY);', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'textField', + id: nanoid(), + label: 'Max Height', + width: 85, + hideLabel: true, + propertyName: 'dimensions.maxHeight', + icon: 'maxHeightIcon', + hidden: { + _code: + 'return !getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.allowResizeY);', + _mode: 'code', + _value: false, + } as any, + }, + ], + }) + .addSettingsInputRow({ + id: 'dimensionsStyleRowResize', + parentId: 'dimensionsStylePnl', + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'allowResizeX', + label: 'Allow Width Resize', + jsSetting: true, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.autoWidth);', + _mode: 'code', + _value: false, + } as any, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'allowResizeY', + label: 'Allow Height Resize', + jsSetting: true, + hidden: { + _code: + 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.autoHeight);', + _mode: 'code', + _value: false, + } as any, + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlStyle', + label: 'Custom Styles', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + parentId: styleRouterId, + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + exposedVariables: [ + `{ "name": "data", "description": "Form values", "type": "object" }`, + ], + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/richTextEditor/index.tsx b/shesha-reactjs/src/designer-components/richTextEditor/index.tsx index 5eb9d6bdc2..36a9508716 100644 --- a/shesha-reactjs/src/designer-components/richTextEditor/index.tsx +++ b/shesha-reactjs/src/designer-components/richTextEditor/index.tsx @@ -8,11 +8,18 @@ import { getStyle } from '@/providers/form/utils'; import { IJoditEditorProps } from '@/components/richTextEditor/joditEditor'; import { IRichTextEditorProps } from './interfaces'; import { IToolboxComponent } from '@/interfaces/formDesigner'; -import { migrateCustomFunctions, migratePropertyName, migrateReadOnly } from '@/designer-components/_common-migrations/migrateSettings'; +import { + migrateCustomFunctions, + migratePropertyName, + migrateReadOnly, +} from '@/designer-components/_common-migrations/migrateSettings'; import { useDeepCompareMemoKeepReference } from '@/hooks'; -import { useFormData } from '@/providers'; +import { useForm, useFormData } from '@/providers'; import { validateConfigurableComponentSettings } from '@/formDesignerUtils'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { getSettings } from './formSettings'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { defaultStyles } from './utils'; const settingsForm = settingsFormJson as FormMarkup; @@ -26,6 +33,11 @@ const RichTextEditorComponent: IToolboxComponent = { isOutput: true, Factory: ({ model }) => { const { data: formData } = useFormData(); + const { allStyles } = model; + const { width, height, minWidth, minHeight, maxWidth, maxHeight } = allStyles?.dimensionsStyles; + + const { formMode } = useForm(); + const config = useDeepCompareMemoKeepReference(() => { const typedConfig: PartialRichTextEditorConfig = { @@ -36,9 +48,9 @@ const RichTextEditorComponent: IToolboxComponent = { theme: typeof model?.theme === 'string' ? model?.theme : 'default', iframe: model?.iframe, direction: model?.direction, - disablePlugins: model?.disablePlugins?.join(',') || '', - ...!model.autoHeight && {height: model?.height}, - ...!model.autoWidth && {width: model?.width}, + disablePlugins: [...(model?.disablePlugins || []), 'spellcheck'].join(','), + ...(!model.autoHeight && { height, minHeight, maxHeight }), + ...(!model.autoWidth && { width, minWidth, maxWidth }), placeholder: model?.placeholder ?? '', readonly: model?.readOnly, style: getStyle(model?.style, formData), @@ -47,25 +59,28 @@ const RichTextEditorComponent: IToolboxComponent = { editHTMLDocumentMode: false, enterBlock: 'div', colorPickerDefaultTab: 'color', - allowResizeX:model?.allowResizeX && !model?.autoWidth, - allowResizeY:model?.allowResizeY && !model?.autoHeight, + allowResizeX: model?.allowResizeX && !model?.autoWidth, + allowResizeY: model?.allowResizeY && !model?.autoHeight, askBeforePasteHTML: model?.askBeforePasteHTML, askBeforePasteFromWord: model?.askBeforePasteFromWord, - autofocus: model?.autofocus, + autofocus: formMode === 'designer' ? false : model?.autofocus, showCharsCounter: model?.showCharsCounter, showWordsCounter: model?.showWordsCounter, }; return typedConfig; }, [model, model.readOnly]); + + const rerenderKey = `${model?.placeholder || ''}-${model?.placeholder || false}`; + return ( - - {(value, onChange) => } + + {(value, onChange) => } ); }, - settingsFormMarkup: settingsForm, - validateSettings: model => validateConfigurableComponentSettings(settingsForm, model), - initModel: model => ({ + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), + initModel: (model) => ({ ...model, showCharsCounter: true, showWordsCounter: true, @@ -80,19 +95,26 @@ const RichTextEditorComponent: IToolboxComponent = { askBeforePasteFromWord: true, disablePlugins: null, }), - migrator: (m) => m - .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(1, (prev) => migrateReadOnly(prev)) - .add(2, (prev) => ({...migrateFormApi.eventsAndProperties(prev)})) - .add(3, (prev) => { - const styles = { - style: prev.style, - theme: prev.theme - }; - - return { ...prev, desktop: {...styles}, tablet: {...styles}, mobile: {...styles} }; - }) - , + migrator: (m) => + m + .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(1, (prev) => migrateReadOnly(prev)) + .add(2, (prev) => ({ ...migrateFormApi.eventsAndProperties(prev) })) + .add(3, (prev) => { + const styles = { + style: prev.style, + theme: prev.theme, + autoHeight: prev.autoHeight ?? true, + autoWidth: prev.autoWidth ?? true, + }; + const resize = { + allowResizeX: true, + allowResizeY: true, + }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles }, ...resize }; + }) + .add(6, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), + }; export default RichTextEditorComponent; diff --git a/shesha-reactjs/src/designer-components/richTextEditor/utils.ts b/shesha-reactjs/src/designer-components/richTextEditor/utils.ts new file mode 100644 index 0000000000..0c52d11b85 --- /dev/null +++ b/shesha-reactjs/src/designer-components/richTextEditor/utils.ts @@ -0,0 +1,7 @@ +import { IStyleType } from "@/index"; + +export const defaultStyles = (): IStyleType => { + return { + dimensions: { width: '100%', height: '500px', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/sectionSeprator/index.tsx b/shesha-reactjs/src/designer-components/sectionSeprator/index.tsx index e52cf8d74e..9b3aed6465 100644 --- a/shesha-reactjs/src/designer-components/sectionSeprator/index.tsx +++ b/shesha-reactjs/src/designer-components/sectionSeprator/index.tsx @@ -1,13 +1,17 @@ -import { IToolboxComponent } from '@/interfaces'; -import { LineOutlined } from '@ant-design/icons'; -import React from 'react'; -import { getStyle, getLayoutStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; -import { useFormData, useGlobalState } from '@/providers'; import SectionSeparator from '@/components/sectionSeparator'; -import { ISectionSeparatorComponentProps } from './interfaces'; -import { getSettings } from './settings'; import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; +import { IToolboxComponent } from '@/interfaces'; +import { useFormData } from '@/providers'; +import { getStyle, pickStyleFromModel, validateConfigurableComponentSettings } from '@/providers/form/utils'; +import { LineOutlined } from '@ant-design/icons'; +import React, { useMemo } from 'react'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { getFontStyle } from '../_settings/utils/font/utils'; +import { ISectionSeparatorComponentProps } from './interfaces'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; +import { getDimensionsStyle } from '../_settings/utils/dimensions/utils'; const SectionSeparatorComponent: IToolboxComponent = { type: 'sectionSeparator', @@ -15,17 +19,43 @@ const SectionSeparatorComponent: IToolboxComponent, Factory: ({ model }) => { + const { lineWidth, lineHeight } = model; const { data: formData } = useFormData(); - const { globalState } = useGlobalState(); + const { lineFont, font } = model; + const fontStyles = useMemo(() => getFontStyle(font), [font]); - if (model.hidden) return null; + const extractedDimensions = { + width: lineWidth, + height: lineHeight, + }; + + const titleAdditionalStyles = { + ...fontStyles, + ...getStyle(model?.titleStyle, formData), + }; + + const containerstyling = JSON.parse(model.containerStylingBox || '{}'); + const containerstylingBoxAsCSS = pickStyleFromModel(containerstyling); + + const containerAdditionalStyles = { + ...containerstylingBoxAsCSS, + ...getStyle(model?.containerStyle, formData), + }; + + const dimensions = getDimensionsStyle(extractedDimensions, containerAdditionalStyles); + const inputProps = { + ...model, + lineThickness: lineFont?.size, + lineColor: lineFont?.color, + }; + if (model.hidden) return null; return ( ); @@ -37,16 +67,35 @@ const SectionSeparatorComponent: IToolboxComponent m - .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) - .add(1, (prev) => ({ ...migrateFormApi.properties(prev) })) - .add(2, (prev) => ({ ...prev, labelAlign: "left" })) - .add(3, (prev) => ({ ...prev, titleMargin: prev['noMargin'] ? 0 : null })) - , + migrator: (m) => + m + .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev))) + .add(1, (prev) => ({ ...migrateFormApi.properties(prev) })) + .add(2, (prev) => ({ ...prev, labelAlign: 'left' })) + .add(3, (prev) => ({ ...prev, titleMargin: prev['noMargin'] ? 0 : null })) + .add(4, (prev) => { + const prevStyles = { + containerStyle: prev.containerStyle, + titleStyle: prev.titleStyle, + lineFont: prev.lineFont, + font: prev.font, + titleStylingBox: prev.titleStylingBox, + containerStylingBox: prev.containerStylingBox, + lineType: prev.dashed ? 'dashed' : 'solid', + }; + + return { + ...prev, + desktop: { ...prev.desktop, ...prevStyles }, + tablet: { ...prev.tablet, ...prevStyles }, + mobile: { ...prev.mobile, ...prevStyles }, + }; + }) + .add(5, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), }; export default SectionSeparatorComponent; diff --git a/shesha-reactjs/src/designer-components/sectionSeprator/interfaces.ts b/shesha-reactjs/src/designer-components/sectionSeprator/interfaces.ts index d5101174cb..a99c55c068 100644 --- a/shesha-reactjs/src/designer-components/sectionSeprator/interfaces.ts +++ b/shesha-reactjs/src/designer-components/sectionSeprator/interfaces.ts @@ -1,6 +1,14 @@ import { IConfigurableFormComponent } from '@/providers/form/models'; +import { IFontValue } from '../_settings/utils/font/interfaces'; export interface ISectionSeparatorComponentProps extends IConfigurableFormComponent { containerStyle?: string; titleStyle?: string; + lineFont?: IFontValue; + font?: IFontValue; + titleStylingBox?: string; + containerStylingBox?: string; + dashed?: boolean; + lineWidth?: string; + lineHeight?: string; } \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/sectionSeprator/settings.ts b/shesha-reactjs/src/designer-components/sectionSeprator/settings.ts deleted file mode 100644 index 71d4642e16..0000000000 --- a/shesha-reactjs/src/designer-components/sectionSeprator/settings.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { strings } from '@/components/sectionSeparator/utils'; -import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; - -export const getSettings = (data: any) => - new DesignerToolbarSettings(data) - .addCollapsiblePanel({ - id: 'b8954bf6-f76d-4139-a850-c99bf06c8b69', - propertyName: 'separator1', - parentId: 'root', - label: 'Display', - labelAlign: 'right', - expandIconPosition: 'start', - ghost: true, - hideWhenEmpty: true, - header: { - id: '3342DA1C-DA07-46F6-8026-E8B9A93F094A', - components: [], - }, - content: { - id: '1BCC52E8-FD3B-4309-AD9B-099CDB729441', - components: new DesignerToolbarSettings() - .addTextField({ - id: '5c813b1a-04c5-4658-ac0f-cbcbae6b3bd4', - propertyName: 'componentName', - parentId: 'root', - label: 'Component name', - validate: { - required: true, - }, - jsSetting: false - }) - .addCheckbox({ - id: '12345e70-99a0-4825-ae6c-8b933004e119', - propertyName: 'hidden', - parentId: 'root', - label: 'Hidden', - }) - .addTextField({ - id: '46d07439-4c18-468c-89e1-60c002ce96c5', - propertyName: 'label', - parentId: 'root', - label: 'Label', - hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: true } as any, - }) - .addCheckbox({ - id: '3b8b9e3f-M5f90-48ae-b4c3-f5cc36f934d9', - propertyName: 'hideLabel', - parentId: 'root', - label: 'Hide Label', - hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: true } as any, - }) - .addDropdown({ - id: "57a40a33-7e08-4ce4-9f08-a34d24a83338", - propertyName: "labelAlign", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Label align", - values: [ - { - label: "left", - value: "left", - id: "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - label: "right", - value: "right", - id: "b920ef96-ae27-4a01-bfad-b5b7d07218da" - }, - { - label: "center", - value: "center", - id: "b920ef96-ae27-4a01-bfad-b5m5wmb70hda" - } - ], - dataSourceType: "values", - defaultValue: "left", - hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: true } as any, - }) - .addColorPicker({ - id: "3b8b9e3f-f47e-48ae-b4c3-f5cc36f934d9", - propertyName: "fontColor", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Font Color", - allowClear: true, - hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: true } as any, - }) - .addNumberField({ - id: "3b8b9e3f-f47e-48ae-b4c3-f5cc36f934a9", - propertyName: "fontSize", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Font Size", - defaultValue: 14, - hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: true } as any, - - }) - .addNumberField({ - id: "3b8b9e3f-f10c-48ae-b4c3-f5cc36f934m5", - propertyName: "titleMargin", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - description: "Distance between text and edge, which should be a number between 0 and 100", - label: "Title Margin", - hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical" || getSettingValue(data?.labelAlign) === "center";', _mode: 'code', _value: true } as any, - min: 0, - max: 100, - }) - .addCheckbox({ - id: "3b8b9e3f-f47e-48ae-b4c3-f5cc36f934m5", - propertyName: "inline", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Inline", - hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: true } as any, - }) - .addTextArea({ - id: '2d32fe70-99a0-4825-ae6c-8b933004e119', - propertyName: 'description', - parentId: 'root', - label: 'Description', - hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: true } as any, - }) - .addDropdown({ - id: "57a40a33-7e08-4ce4-9f08-a34d00783338", - propertyName: "orientation", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Orientation", - values: [ - { - label: "horizontal", - value: "horizontal", - id: "f01e54aa-a1a4-4bd6-ba73-c395e48af8ce" - }, - { - label: "vertical", - value: "vertical", - id: "b920ef96-ae27-4a01-bfad-b5b7d07218da" - } - ], - defaultValue: "horizontal", - dataSourceType: "values" - }) - .addCheckbox({ - id: "57a40a33-7e08-4ce4-9f08-a34d24a3rs3r", - propertyName: "dashed", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Dashed", - }) - .addColorPicker({ - id: "3b8b9e3f-f47e-48ae-b4c3-f5cc36f93aMg", - propertyName: "lineColor", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Line Color", - allowClear: true, - }) - .addNumberField({ - id: "3b8b9e3f-f47e-48ae-b4c3-f90c36f934d9", - propertyName: "lineThickness", - description: "Line Thickness in pixels (px)", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Line Thickness", - }) - .addTextField({ - id: "3b8bv360-f47e-48ae-b4c3-f5cc36f934a9", - propertyName: "lineWidth", - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Line Width", - description: `${strings.tooltip}. This is only applicable when the orientation is horizontal`, - hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: true } as any, - defaultValue: '100%', - }) - .addTextField({ - id: "3b8bv360-f47e-48ae-b4c3-f5coMp6f934a9", - propertyName: "lineHeight", - description: `${strings.tooltip}. This is only applicable when the orientation is vertical`, - parentId: "1BCC52E8-FD3B-4309-AD9B-099CDB729441", - label: "Line Height", - hidden: { _code: 'return getSettingValue(data?.orientation) === "horizontal";', _mode: 'code', _value: true } as any, - }) - .toJson(), - }, - }) - .addCollapsiblePanel({ - id: '6befdd49-41aa-41d6-a29e-76fa00590b75', - propertyName: 'sectionStyle', - parentId: 'root', - label: 'Style', - labelAlign: 'right', - expandIconPosition: 'start', - ghost: true, - hideWhenEmpty: true, - header: { - id: '3342DA1C-DA07-46F6-8026-E8B9A93F094A', - components: [], - }, - content: { - id: 'FC8969C8-227F-4F09-90BF-82795C8E4594', - components: new DesignerToolbarSettings() - .addCodeEditor({ - id: 'f2504ccf-6c03-4ba4-8f89-776ec2f57b3e', - propertyName: 'containerStyle', - label: 'Container Style', - parentId: 'root', - validate: {}, - settingsValidationErrors: [], - description: - 'A script that returns the style of the element as an object. This should conform to CSSProperties', - exposedVariables: [{ name: 'data', description: 'Form values', type: 'object' }], - wrapInTemplate: true, - templateSettings: { - functionName: 'getContainerStyle', - }, - availableConstantsExpression: async ({ metadataBuilder }) => { - return metadataBuilder - .object("constants") - .addStandard(["shesha:formData", "shesha:globalState"]) - .build(); - }, - }) - .addStyleBox({ - id: 'a0e21b98-0cb9-45ef-8b79-c78c884534f4', - propertyName: 'stylingBox', - parentId: 'root', - validate: {}, - settingsValidationErrors: [], - jsSetting: false, - }) - .addCodeEditor({ - id: '77af6050-c35a-470b-9924-b63d6bf355b6', - propertyName: 'titleStyle', - label: 'Title Style', - parentId: 'root', - validate: {}, - settingsValidationErrors: [], - description: - 'A script that returns the style of the element as an object. This should conform to CSSProperties', - exposedVariables: [{ name: 'data', description: 'Form values', type: 'object' }], - wrapInTemplate: true, - hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: true } as any, - templateSettings: { - functionName: 'getTitleStyle', - }, - availableConstantsExpression: async ({ metadataBuilder }) => { - return metadataBuilder - .object("constants") - .addStandard(["shesha:formData", "shesha:globalState"]) - .build(); - }, - }) - .toJson(), - }, - }) - .addCollapsiblePanel({ - id: 'eb91c2f5-592e-4f60-ba1a-f1d2011a5290', - propertyName: 'pnlSecurity', - parentId: 'root', - label: 'Security', - labelAlign: "left", - expandIconPosition: "start", - ghost: true, - collapsible: 'header', - content: { - id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() - .addPermissionAutocomplete({ - id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', - propertyName: 'permissions', - label: 'Permissions', - labelAlign: 'right', - parentId: 'root', - hidden: false, - validate: {}, - }).toJson() - ] - } - }) - .toJson(); diff --git a/shesha-reactjs/src/designer-components/sectionSeprator/settingsForm.ts b/shesha-reactjs/src/designer-components/sectionSeprator/settingsForm.ts new file mode 100644 index 0000000000..97d46c0661 --- /dev/null +++ b/shesha-reactjs/src/designer-components/sectionSeprator/settingsForm.ts @@ -0,0 +1,456 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { FormLayout } from 'antd/lib/form/Form'; +import { fontTypes, fontWeights } from '../_settings/utils/font/utils'; +import { nanoid } from '@/utils/uuid'; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + const fontStylePnlId = nanoid(); + const dimensionsStylePnlId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: '1', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + parentId: commonTabId, + propertyName: 'componentName', + label: 'Component Name', + jsSetting: true, + }) + .addContainer({ + id: nanoid(), + parentId: commonTabId, + hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: false } as any, + components: [ + ...new DesignerToolbarSettings() + .addLabelConfigurator({ + id: nanoid(), + propertyName: 'hideLabel', + label: 'Label', + parentId: commonTabId, + hideLabel: true, + labelAlignOptions: [ + { value: 'left', icon: 'AlignLeftOutlined', title: 'Left' }, + { value: 'center', icon: 'AlignCenterOutlined', title: 'Center' }, + { value: 'right', icon: 'AlignRightOutlined', title: 'Right' }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + parentId: commonTabId, + type: 'textArea', + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: false } as any, + }, + ], + }).toJson(), + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + id: nanoid(), + parentId: commonTabId, + type: 'dropdown', + propertyName: 'orientation', + label: 'Orientation', + jsSetting: true, + dropdownOptions: [ + { + label: 'Horizontal', + value: 'horizontal', + }, + { + label: 'Vertical', + value: 'vertical', + }, + ], + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'inline', + label: 'Inline', + size: 'small', + hidden: { _code: 'return getSettingValue(data?.orientation) === "vertical";', _mode: 'code', _value: false } as any, + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + layout: 'horizontal', + } + ], + }) + .toJson(), + ], + }, + { + key: '4', + title: 'Appearance', + id: appearanceTabId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: 'code', + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: '', + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'lineStyle', + label: 'Line Style', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: 'fontStylePnlline', + inline: true, + propertyName: 'lineFont', + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Thickness', + propertyName: 'lineFont.size', + hideLabel: false, + width: 50, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: false, + propertyName: 'lineFont.color', + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Type', + propertyName: 'lineType', + hideLabel: false, + defaultValue: 'solid', + dropdownOptions: [ + { + label: 'Solid', + value: 'solid', + }, + { + label: 'Dashed', + value: 'dashed', + }, + { + label: 'Dotted', + value: 'dotted', + } + ], + } + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: dimensionsStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.orientation) === "vertical";', + _mode: 'code', + _value: false, + } as any, + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Width', + width: 85, + propertyName: 'lineWidth', + icon: 'widthIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + hidden: { + _code: 'return getSettingValue(data?.orientation) === "horizontal";', + _mode: 'code', + _value: false, + } as any, + + inputs: [ + { + type: 'textField', + id: nanoid(), + label: 'Height', + width: 85, + propertyName: 'lineHeight', + icon: 'heightIcon', + tooltip: 'You can use any unit (%, px, em, etc). px by default if without unit', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'titleStyle', + label: 'Title Style', + labelAlign: 'right', + ghost: true, + collapsedByDefault: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlFontStyle', + label: 'Font', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: fontStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: fontStylePnlId, + inline: true, + propertyName: 'font', + inputs: [ + { + type: 'dropdown', + id: nanoid(), + label: 'Family', + propertyName: 'font.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: nanoid(), + label: 'Size', + propertyName: 'font.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: nanoid(), + label: 'Weight', + propertyName: 'font.weight', + hideLabel: true, + tooltip: 'Controls text thickness (light, normal, bold, etc.)', + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'font.color', + }, + ], + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'titleStyle', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Container Style', + labelAlign: 'right', + ghost: true, + collapsedByDefault: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'containerStylingBox', + }) + .toJson(), + ], + }, + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + content: { + id: nanoid(), + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'containerStyle', + hideLabel: false, + label: 'Style', + description: + 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson(), + ], + }, + }) + .toJson(), + ], + }) + .toJson(), + ], + }, + { + key: '5', + title: 'Security', + id: securityTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/sectionSeprator/utils.ts b/shesha-reactjs/src/designer-components/sectionSeprator/utils.ts new file mode 100644 index 0000000000..a49ab56c38 --- /dev/null +++ b/shesha-reactjs/src/designer-components/sectionSeprator/utils.ts @@ -0,0 +1,6 @@ +export const defaultStyles = (): any => { + return { + font: { weight: '500', size: 14, color: '#000', type: 'Segoe UI' }, + dimensions: { width: '100%', height: '32px', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/settingsEditor/mainMenuEditor.tsx b/shesha-reactjs/src/designer-components/settingsEditor/mainMenuEditor.tsx index d4406d873a..bccb4e0ebf 100644 --- a/shesha-reactjs/src/designer-components/settingsEditor/mainMenuEditor.tsx +++ b/shesha-reactjs/src/designer-components/settingsEditor/mainMenuEditor.tsx @@ -1,16 +1,22 @@ import { IToolboxComponent } from '@/interfaces'; import { EditOutlined } from '@ant-design/icons'; import React, { useEffect, useRef, useState } from 'react'; -import { FormMarkup, IConfigurableFormComponent, SidebarConfigurator, useForm, useSheshaApplication, validateConfigurableComponentSettings } from '@/index'; -import settingsFormJson from './settingsForm.json'; +import { + IConfigurableFormComponent, + SidebarConfigurator, + useForm, + useSheshaApplication, + validateConfigurableComponentSettings, +} from '@/index'; import { IHasVersion, Migrator } from '@/utils/fluentMigrator/migrator'; import { mainMenuMigration } from '@/providers/mainMenu/migrations/migration'; import { useSettingsEditor } from '@/components/settingsEditor/provider'; import { IConfigurableMainMenu, useMainMenu } from '@/providers/mainMenu'; import { deepCopyViaJson } from '@/utils/object'; import { FRONTEND_DEFAULT_APP_KEY } from '@/components/settingsEditor/provider/models'; - -const settingsForm = settingsFormJson as FormMarkup; +import { getSettings } from './settingsForm'; +import { App, Button, Modal } from 'antd'; +import { useMedia } from 'react-use'; export interface IMainMenuEditorComponentProps extends IConfigurableFormComponent { height?: string; @@ -22,22 +28,24 @@ const MainMenuEditorComponent: IToolboxComponent icon: , isInput: true, isOutput: true, - Factory: ({model}) => { + Factory: ({ model }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const isSmall = useMedia('(max-width: 480px)'); + const { message } = App.useApp(); const [menuProps, setMenuProps] = useState(undefined); - const {applicationKey = null} = useSheshaApplication(); - const {selectedApplication = null, saveStatus, editorMode} = useSettingsEditor(false) ?? {}; - const { changeMainMenu } = useMainMenu(); + const { applicationKey = null } = useSheshaApplication(); + const { selectedApplication = null, saveStatus, editorMode, saveSetting } = useSettingsEditor(false) ?? {}; + const { changeMainMenu, saveMainMenu } = useMainMenu(); const form = useForm(); const initialValues = useRef(); - + const updateMenu = (value: IConfigurableMainMenu) => { const migratorInstance = new Migrator(); const fluent = mainMenuMigration(migratorInstance); - const versionedValue = {...value} as IHasVersion; - if (versionedValue.version === undefined) - versionedValue.version = -1; + const versionedValue = { ...value } as IHasVersion; + if (versionedValue.version === undefined) versionedValue.version = -1; const model = fluent.migrator.upgrade(versionedValue, {}); setMenuProps(model); initialValues.current = deepCopyViaJson(model); @@ -48,8 +56,7 @@ const MainMenuEditorComponent: IToolboxComponent changeMainMenu(form.formData); initialValues.current = deepCopyViaJson(form.formData); } - if (saveStatus === 'canceled') - setMenuProps(initialValues.current); + if (saveStatus === 'canceled') setMenuProps(initialValues.current); }, [saveStatus]); useEffect(() => { @@ -59,17 +66,73 @@ const MainMenuEditorComponent: IToolboxComponent }, [form.formData]); const onChange = (changedValue: any) => { - const newData = {...menuProps, items: changedValue}; + const newData = { ...menuProps, items: changedValue }; setMenuProps(newData); - form.setFormData({values: newData, mergeValues: false}); + form.setFormData({ values: newData, mergeValues: false }); + }; + + if (!isModalOpen) { + return ( +
+ +
+ ); + } + + const onCancel = () => { + setIsModalOpen(false); + setMenuProps(initialValues.current); + form.setFormData({ values: initialValues.current, mergeValues: false }); + message.info('Menu configuration not changed!'); + setIsModalOpen(false); + }; + + const onOk = () => { + if (form.formData === undefined) { + message.error('Menu configuration is empty!'); + return; + } + setMenuProps(form.formData); + saveMainMenu(form.formData) + .then(() => { + changeMainMenu(form.formData); + }) + .then(async () => { + if (saveSetting) { + await saveSetting(); + } + }) + .then(() => { + message.success('Menu saved successfully!'); + }) + .catch((error) => { + console.error(error); + message.error('Failed to save menu changes!'); + }) + .finally(() => { + setIsModalOpen(false); + }); }; - return
- -
; + return ( +
+ + + +
+ ); }, - settingsFormMarkup: settingsForm, - validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model), + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), }; export default MainMenuEditorComponent; diff --git a/shesha-reactjs/src/designer-components/settingsEditor/settingsForm.ts b/shesha-reactjs/src/designer-components/settingsEditor/settingsForm.ts new file mode 100644 index 0000000000..d563a7743a --- /dev/null +++ b/shesha-reactjs/src/designer-components/settingsEditor/settingsForm.ts @@ -0,0 +1,99 @@ +import { DesignerToolbarSettings } from '@/interfaces/toolbarSettings'; +import { nanoid } from '@/utils/uuid'; +import { FormLayout } from 'antd/lib/form/Form'; + +export const getSettings = (data) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + label: 'Property name', + styledLabel: true, + parentId: commonTabId, + validate: { required: true }, + jsSetting: true, + size: 'small', + }) + .addSettingsInput({ + id: nanoid(), + parentId: commonTabId, + inputType: 'textArea', + propertyName: 'description', + label: 'Tooltip', + jsSetting: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + size: 'small', + defaultValue: 'inherited', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + size: 'small', + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'height', + label: 'Height', + tooltip: 'Enter height of component.', + jsSetting: true, + }, + ], + }) + .toJson(), + ], + }, + ], + }) + .toJson(), + formSettings: { + colon: true, + layout: 'horizontal' as FormLayout, + labelCol: { span: 8 }, + wrapperCol: { span: 16 }, + displayName: 'DEFAULT_FORM_SETTINGS', + __docgenInfo: { + description: 'Default form settings', + displayName: 'DEFAULT_FORM_SETTINGS', + props: {}, + }, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/settingsEditor/themeEditor.tsx b/shesha-reactjs/src/designer-components/settingsEditor/themeEditor.tsx index ef1a4c4837..bd62c3a697 100644 --- a/shesha-reactjs/src/designer-components/settingsEditor/themeEditor.tsx +++ b/shesha-reactjs/src/designer-components/settingsEditor/themeEditor.tsx @@ -2,10 +2,11 @@ import { IToolboxComponent } from '@/interfaces'; import { EditOutlined } from '@ant-design/icons'; import React, { useEffect, useRef } from 'react'; import { ConfigurableThemeContent } from '@/generic-pages/settings/dynamic-theme/content'; -import { FormMarkup, IConfigurableTheme, useForm, useSheshaApplication, useTheme, validateConfigurableComponentSettings } from '@/index'; +import { FormMarkup, IConfigurableTheme, useShaFormInstance, useSheshaApplication, useTheme, validateConfigurableComponentSettings } from '@/index'; import settingsFormJson from './settingsForm.json'; import { useSettingsEditor } from '@/components/settingsEditor/provider'; -import { FRONTEND_DEFAULT_APP_KEY } from '@/components/settingsEditor/provider/models'; +import { FRONTEND_DEFAULT_APP_KEY, ISettingIdentifier } from '@/components/settingsEditor/provider/models'; +import { useShaFormUpdateDate } from '@/providers/form/providers/shaFormProvider'; const settingsForm = settingsFormJson as FormMarkup; @@ -17,26 +18,48 @@ const ThemeEditorComponent: IToolboxComponent = { isOutput: true, Factory: () => { + useShaFormUpdateDate(); + const {applicationKey = null} = useSheshaApplication(); - const {selectedApplication = null, editorMode} = useSettingsEditor(false) ?? {}; - const { theme, changeTheme } = useTheme(); - const form = useForm(); + const {selectedApplication = null, settingSelection, editorMode, setEditor, saveSettingValue} = useSettingsEditor(false) ?? {}; + const { theme, changeTheme, resetToApplicationTheme } = useTheme(); + const form = useShaFormInstance(); const initialValues = useRef(theme); const localTheme = useRef(); useEffect(() => { - localTheme.current = theme; + setEditor({ + save: () => { + changeTheme(localTheme.current, applicationKey === (selectedApplication?.appKey ?? FRONTEND_DEFAULT_APP_KEY)); + const settingId: ISettingIdentifier = { + name: settingSelection.setting.name, + module: settingSelection.setting.module, + appKey: selectedApplication?.appKey, + }; + + return saveSettingValue(settingId, localTheme.current) + .then(() => { + initialValues.current = localTheme.current; + }) + .catch((error) => { + throw error; + }); + }, + cancel() { + changeTheme(initialValues.current); + }, + }); + initialValues.current = form.formData; // when form is closing restore the latest form initial values return () => { - changeTheme(initialValues.current); + resetToApplicationTheme(); }; }, []); useEffect(() => { if (form.formData) { changeTheme(form.formData); - if (applicationKey === (selectedApplication?.appKey ?? FRONTEND_DEFAULT_APP_KEY)) - initialValues.current = form.formData; + localTheme.current = form.formData; } }, [form.formData]); diff --git a/shesha-reactjs/src/designer-components/settingsInput/index.tsx b/shesha-reactjs/src/designer-components/settingsInput/index.tsx new file mode 100644 index 0000000000..187ac7f6ba --- /dev/null +++ b/shesha-reactjs/src/designer-components/settingsInput/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { IToolboxComponent } from "@/interfaces"; +import { SettingOutlined } from "@ant-design/icons"; +import { SettingInput } from './settingsInput'; +import { ISettingsInputProps } from './interfaces'; + +const SettingsInput: IToolboxComponent = { + type: 'settingsInput', + isInput: true, + isOutput: true, + name: 'SettingsInput', + icon: , + Factory: ({ model }) => { + const { label, dropdownOptions, buttonGroupOptions, hasUnits, propertyName: property, tooltip: description, readOnly } = model; + + + return ( + model.hidden ? null : + + + ); + } +}; + +export default SettingsInput; diff --git a/shesha-reactjs/src/designer-components/settingsInput/interfaces.ts b/shesha-reactjs/src/designer-components/settingsInput/interfaces.ts new file mode 100644 index 0000000000..289c8f0911 --- /dev/null +++ b/shesha-reactjs/src/designer-components/settingsInput/interfaces.ts @@ -0,0 +1,127 @@ +import { AutocompleteDataSourceType } from '@/components/autocomplete'; +import { CodeLanguages } from '../codeEditor/types'; +import { CodeTemplateSettings, ResultType } from '@/components/codeEditor/models'; +import { FormMarkup, IComponentLabelProps, IConfigurableFormComponent, IObjectMetadata } from '@/index'; +import { SizeType } from 'antd/es/config-provider/SizeContext'; +import { IItemListConfiguratorModalProps } from '../itemListConfigurator/itemListConfiguratorModal'; +import { ComponentType } from '@/components/formComponentSelector'; +import { IConfigurableActionConfiguratorComponentProps } from '../configurableActionsConfigurator/interfaces'; +import { ICodeExposedVariable } from '@/components/codeVariablesTable'; +import { GetResultTypeFunc } from '../codeEditor/interfaces'; +import { IHttpVerb } from '@/components/endpointsAutocomplete/endpointsAutocomplete'; +import { ILabelValueEditorProps } from '@/components/labelValueEditor/labelValueEditor'; + +export interface IRadioOption { + value: string | number; + icon?: string | React.ReactNode; + title?: string; + hint?: string; + disabled?: boolean; +} + +export interface IDropdownOption { + label: string | React.ReactNode; + value: string; + icon?: string | React.ReactNode; +} + +export interface InputType { + type: 'colorPicker' | 'dropdown' | 'radio' | 'switch' | 'numberField' | 'button' | 'buttonGroupConfigurator' | 'editableTagGroupProps' | 'dynamicItemsConfigurator' | 'endpointsAutocomplete' + | 'customDropdown' | 'textArea' | 'codeEditor' | 'iconPicker' | 'contextPropertyAutocomplete' | 'textField' | 'queryBuilder' | 'formAutocomplete' | 'referenceListAutocomplete' | 'filtersList' | + 'autocomplete' | 'imageUploader' | 'editModeSelector' | 'permissions' | 'multiColorPicker' | 'propertyAutocomplete' | 'columnsConfig' | 'columnsList' + | 'sizableColumnsConfig' | 'labelValueEditor' | 'componentSelector' | 'itemListConfiguratorModal' | 'dataSortingEditor' | 'tooltip' | 'customLabelValueEditor' + | 'typeAutoComplete' | 'fullIdFormAutocomplete' | 'formTypeAutocomplete' | 'configurableActionConfigurator' | 'RefListItemSelectorSettingsModal' + | 'keyInformationBarColumnsList' | 'Password'; +} + +export interface ISettingsInputProps extends IComponentLabelProps, Omit, Omit { + type: InputType['type']; + label: string; + propertyName: string; + variant?: 'borderless' | 'filled' | 'outlined'; + buttonGroupOptions?: IRadioOption[]; + dropdownOptions?: IDropdownOption[] | string; + readOnly?: boolean; + onChange?: (value: any) => void; + editorConfig?: IConfigurableActionConfiguratorComponentProps; + level?: number; + allowedActions?: string[]; + hasUnits?: boolean; + jsSetting?: boolean; + children?: React.ReactNode; + tooltip?: string; + iconSize?: number; + tooltipAlt?: string; + customTooltip?: string; + prefix?: string; + suffix?: string; + size?: SizeType; + width?: string | number; + hideLabel?: boolean; + layout?: 'horizontal' | 'vertical'; + language?: CodeLanguages; + style?: string; + wrapperCol?: { span: number }; + fileName?: string; + availableConstantsExpression?: string; + ignorePrefixesOnNewItems?: boolean; + resultType?: ResultType; + labelTitle?: string; + labelName?: string; + valueTitle?: string; + valueName?: string; + value?: any; + placeholder?: string; + mode?: any; + availableHttpVerbs?: IHttpVerb[]; + /** Can be any valid number e.g.: 1, 0.1, 3, 3.14 */ + step?: number; + exposedVariables?: string[] | ICodeExposedVariable[]; + dropdownMode?: 'multiple' | 'tags'; + customDropdownMode?: 'single' | 'multiple'; + allowClear?: boolean; + allowSearch?: boolean; + className?: string; + icon?: string | React.ReactNode; + iconAlt?: string | React.ReactNode; + inline?: boolean; + autoSize?: boolean; + inputType?: InputType['type']; + referenceList?: any; + filter?: any; + dataSourceType?: AutocompleteDataSourceType; + dataSourceUrl?: string; + entityType?: string; + useRawValues?: boolean; + modelType?: string; + maxItemsCount?: number; + httpVerb?: string; + min?: number; + max?: number; + showText?: boolean; + fieldsUnavailableHint?: string; + wrapInTemplate?: boolean; + templateSettings?: CodeTemplateSettings; + resultTypeExpression?: string | GetResultTypeFunc; + availableConstants?: IObjectMetadata; + items?: []; + onAddNewItem?: IItemListConfiguratorModalProps['initNewItem']; + listItemSettingsMarkup?: IConfigurableFormComponent[]; + buttonText?: string; + buttonTextReadOnly?: string; + modalSettings?: IItemListConfiguratorModalProps['modalSettings']; + modalReadonlySettings?: IItemListConfiguratorModalProps['modalSettings']; + settingsMarkupFactory?: FormMarkup; + _formFields?: string[]; + autoFillProps?: boolean; + presets?: string[]; + propertyAccessor?: string; + noSelectionItemText?: string; + noSelectionItemValue?: string; + componentType?: ComponentType; + parentComponentType?: string; + textType?: string; + title?: string; + showSearch?: boolean; + defaultChecked?: boolean; +}; diff --git a/shesha-reactjs/src/designer-components/settingsInput/settingsInput.tsx b/shesha-reactjs/src/designer-components/settingsInput/settingsInput.tsx new file mode 100644 index 0000000000..e440e3d217 --- /dev/null +++ b/shesha-reactjs/src/designer-components/settingsInput/settingsInput.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import FormItem from "../_settings/components/formItem"; +import { InputComponent } from '../inputComponent'; +import { ISettingsInputProps } from './interfaces'; +import ConditionalWrap from '@/components/conditionalWrapper'; +import { MetadataProvider } from '@/providers'; +import { evaluateString, useShaFormInstance } from '@/index'; + +export const SettingInput: React.FC = ({ children, label, hideLabel, propertyName: property, type, + buttonGroupOptions, dropdownOptions, readOnly, hasUnits, jsSetting, tooltip, hidden, width, + size, inline, validate, modelType, ...rest }) => { + const { formData } = useShaFormInstance(); + + const evaluatedModelType = typeof modelType === 'string' ? evaluateString(modelType, { data: formData }) : modelType; + const isHidden = typeof hidden === 'string' ? evaluateString(hidden, { data: formData }) : hidden; + + return isHidden ? null : +
+ {content}} + > + + {children || + } + + +
+ ; + +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/settingsInput/utils.tsx b/shesha-reactjs/src/designer-components/settingsInput/utils.tsx new file mode 100644 index 0000000000..3341e91eae --- /dev/null +++ b/shesha-reactjs/src/designer-components/settingsInput/utils.tsx @@ -0,0 +1,12 @@ +export const getWidth = (type: string, width: any) => { + switch (type) { + case 'numberField': return width || 100; + case 'button': return width || 24; + case 'dropdown': return width || 120; + case 'radio': return width; + case 'colorPicker': return width || 24; + case 'iconPicker': return width || 24; + case 'customDropdown': return width || 120; + default: return width || 50; + } +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/settingsInputRow/index.tsx b/shesha-reactjs/src/designer-components/settingsInputRow/index.tsx new file mode 100644 index 0000000000..c56ae8454c --- /dev/null +++ b/shesha-reactjs/src/designer-components/settingsInputRow/index.tsx @@ -0,0 +1,61 @@ +import React from 'react'; + +import { IConfigurableFormComponent, IToolboxComponent } from "@/interfaces"; +import { SettingOutlined } from "@ant-design/icons"; +import { SettingInput } from '../settingsInput/settingsInput'; +import { getWidth } from '../settingsInput/utils'; +import { evaluateString, useShaFormInstance } from '@/index'; +import { useStyles } from '../inputComponent/styles'; +import { ISettingsInputProps } from '../settingsInput/interfaces'; + +export interface ISettingsInputRowProps extends Omit, IInputRowProps { + +} + +export interface IInputRowProps { + inputs?: Array; + readOnly?: boolean; + inline?: boolean; + children?: React.ReactNode; + hidden?: boolean; +} + +export const InputRow: React.FC = ({ inputs, readOnly, children, inline, hidden }) => { + const { styles } = useStyles(); + const { formData } = useShaFormInstance(); + + const isHidden = typeof hidden === 'string' ? evaluateString(hidden, { data: formData }) : hidden; + return isHidden ? null :
+ {inputs?.map((props, i) => { + const { type } = props; + const isHidden = typeof props.hidden === 'string' ? evaluateString(props.hidden, { data: formData }) : props.hidden; + + const width = getWidth(type, props.width); + + return ( +
; +}; + +const SettingsInputRow: IToolboxComponent = { + type: 'settingsInputRow', + isInput: true, + isOutput: true, + name: 'SettingsInputRow', + icon: , + Factory: ({ model }) => { + return model.hidden ? null : ( + + ); + } +}; + +export default SettingsInputRow; diff --git a/shesha-reactjs/src/designer-components/sizableColumns/interfaces.ts b/shesha-reactjs/src/designer-components/sizableColumns/interfaces.ts index 08764ad1d0..ca6bbd1412 100644 --- a/shesha-reactjs/src/designer-components/sizableColumns/interfaces.ts +++ b/shesha-reactjs/src/designer-components/sizableColumns/interfaces.ts @@ -1,4 +1,17 @@ -import { IConfigurableFormComponent } from '@/providers'; +import { IConfigurableFormComponent, IStyleType } from '@/providers'; + +export interface ISizableColumnInputProps extends IStyleType { + borderSize?: string | number; + borderRadius?: number; + borderType?: string; + borderColor?: string; + stylingBox?: string; + height?: string | number; + width?: string | number; + backgroundColor?: string; + hideBorder?: boolean; + columns?: ISizableColumnProps[]; +} export interface ISizableColumnProps { id: string; @@ -6,6 +19,4 @@ export interface ISizableColumnProps { components: IConfigurableFormComponent[]; } -export interface ISizableColumnComponentProps extends IConfigurableFormComponent { - columns: ISizableColumnProps[]; -} +export interface ISizableColumnComponentProps extends IConfigurableFormComponent, ISizableColumnInputProps { } \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/sizableColumns/settingsForm.ts b/shesha-reactjs/src/designer-components/sizableColumns/settingsForm.ts new file mode 100644 index 0000000000..4d7fe101ff --- /dev/null +++ b/shesha-reactjs/src/designer-components/sizableColumns/settingsForm.ts @@ -0,0 +1,513 @@ +import { DesignerToolbarSettings } from "@/index"; +import { nanoid } from "@/utils/uuid"; +import { FormLayout } from "antd/lib/form/Form"; +import { positionOptions, repeatOptions, sizeOptions } from '../_settings/utils/background/utils'; +import { getBorderInputs, getCornerInputs } from "../_settings/utils/border/utils"; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const appearanceId = nanoid(); + const securityId = nanoid(); + const styleRouterId = nanoid(); + const dimensionsStylePnlId = nanoid(); + const borderStylePnlId = nanoid(); + const backgroundStylePnlId = nanoid(); + + const propertyNameId = nanoid(); + const shadowStylePnlId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: propertyNameId, + inputType: 'textField', + propertyName: "componentName", + parentId: commonTabId, + label: "Component Name", + size: "small", + validate: { + "required": true + }, + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'switch', + propertyName: 'hidden', + parentId: commonTabId, + label: 'Hide', + size: 'small', + jsSetting: true, + }) + .toJson() + ] + }, + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'sizableColumnsConfig', + propertyName: 'columns', + label: 'Columns', + labelAlign: 'right', + tooltip: 'Configure columns', + parentId: dataTabId, + size: 'small', + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: 'dimensionsStyleCollapsiblePanel', + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: dimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: `width-${styleRouterId}`, + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: `minWidth-${styleRouterId}`, + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: `maxWidth-${styleRouterId}`, + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: `height-${dimensionsStylePnlId}`, + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: `minHeight-${dimensionsStylePnlId}`, + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: `maxHeight-${dimensionsStylePnlId}`, + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: borderStylePnlId, + components: [...new DesignerToolbarSettings() + + .addContainer({ + id: 'borderStyleRow', + parentId: 'borderStylePnl', + components: getBorderInputs() as any + }) + .addContainer({ + id: 'borderRadiusStyleRow', + parentId: 'borderStylePnl', + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: backgroundStylePnlId, + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: 'styleRouter', + collapsible: 'header', + content: { + id: 'backgroundStylePnl', + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: "backgroundStyleRow-selectType", + parentId: "backgroundStylePnl", + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: [ + { + value: "color", + icon: "FormatPainterOutlined", + title: "Color" + }, + { + value: "gradient", + icon: "BgColorsOutlined", + title: "Gradient" + }, + { + value: "image", + icon: "PictureOutlined", + title: "Image" + }, + { + value: "url", + icon: "LinkOutlined", + title: "URL" + }, + { + value: "storedFile", + icon: "DatabaseOutlined", + title: "Stored File" + } + ], + }) + .addSettingsInputRow({ + id: "backgroundStyleRow-color", + parentId: "backgroundStylePnl", + inputs: [{ + type: 'colorPicker', + id: 'backgroundStyleRow-color', + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: "backgroundStyle-gradientColors", + parentId: "backgroundStylePnl", + inputs: [{ + type: 'multiColorPicker', + id: 'backgroundStyle-gradientColors', + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: "backgroundStyle-url", + parentId: "backgroundStylePnl", + inputs: [{ + type: 'textField', + id: 'backgroundStyle-url', + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: "backgroundStyle-image", + parentId: 'backgroundStylePnl', + inputs: [ + { + type: 'imageUploader', + id: 'backgroundStyle-image', + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: "backgroundStyleRow-storedFile", + parentId: 'backgroundStylePnl', + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-storedFile', + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: "backgroundStyleRow-controls", + parentId: 'backgroundStyleRow', + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: 'backgroundStyleRow-size', + label: "Size", + hideLabel: true, + propertyName: "background.size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: 'backgroundStyleRow-position', + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-repeat', + parentId: 'backgroundStyleRow', + inputs: [{ + type: 'radio', + id: 'backgroundStyleRow-repeat-radio', + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: shadowStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: shadowStylePnlId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'Offset X', + hideLabel: true, + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + tooltip: 'OffsetX. The larger the value, the bigger the shadow', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Offset Y', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + description: 'OffsetY. The larger the value, the bigger the shadow', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + description: 'Blur. The larger the value, the bigger the blur', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + description: 'Spread. The larger the value, the bigger the spread', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'style', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson(), + ] + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityId, + permissions: data.permissions, + tooltip: "Enter a list of permissions that should be associated with this component" + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/sizableColumns/sizableColumnList.tsx b/shesha-reactjs/src/designer-components/sizableColumns/sizableColumnList.tsx index 1d61e39a1c..467bd3719c 100644 --- a/shesha-reactjs/src/designer-components/sizableColumns/sizableColumnList.tsx +++ b/shesha-reactjs/src/designer-components/sizableColumns/sizableColumnList.tsx @@ -160,13 +160,13 @@ export const SizableColumnsList: FC = ({ value, onChange, readOnly }) => const cols = [ !readOnly ? { - title: '', - dataIndex: 'sort', - width: 30, - render: (_text, _record, _index) => { - return ; - }, - } + title: '', + dataIndex: 'sort', + width: 30, + render: (_text, _record, _index) => { + return ; + }, + } : null, { title: 'Size', @@ -176,15 +176,15 @@ export const SizableColumnsList: FC = ({ value, onChange, readOnly }) => }, !readOnly ? { - title: '', - dataIndex: 'operations', - render: (_, record) => - columns.length >= 1 ? ( - handleDeleteTab(record.id)}> - Delete - - ) : null, - } + title: '', + dataIndex: 'operations', + render: (_, record) => + columns.length >= 1 ? ( + handleDeleteTab(record.id)}> + Delete + + ) : null, + } : null, ].filter((c) => Boolean(c)); @@ -235,7 +235,7 @@ export const SizableColumnsList: FC = ({ value, onChange, readOnly }) => return ( - + = { type: 'sizableColumns', @@ -24,14 +32,62 @@ const SizableColumnsComponent: IToolboxComponent = const { columns } = model as ISizableColumnComponentProps; const style = { ...getLayoutStyle(model, { data, globalState }), display: 'flex' }; + const { backendUrl, httpHeaders } = useSheshaApplication(); + + const dimensions = model?.dimensions; + const border = model?.border; + const shadow = model?.shadow; + const background = model?.background; + const jsStyle = getStyle(model.style, data); + + const dimensionsStyles = useMemo(() => getDimensionsStyle(dimensions), [dimensions]); + const borderStyles = useMemo(() => getBorderStyle(border, jsStyle), [border]); + const [backgroundStyles, setBackgroundStyles] = useState({}); + const shadowStyles = useMemo(() => getShadowStyle(shadow), [shadow]); + + useEffect(() => { + const fetchStyles = async () => { + const storedImageUrl = background?.storedFile?.id && background?.type === 'storedFile' + ? await fetch(`${backendUrl}/api/StoredFile/Download?id=${background?.storedFile?.id}`, + { headers: { ...httpHeaders, "Content-Type": "application/octet-stream" } }) + .then((response) => { + return response.blob(); + }) + .then((blob) => { + return URL.createObjectURL(blob); + }) : ''; + + const style = await getBackgroundStyle(background, jsStyle, storedImageUrl); + setBackgroundStyles(style); + }; + + fetchStyles(); + }, [background, background?.gradient?.colors, backendUrl, httpHeaders]); + if (model.hidden) return null; + if (model?.background?.type === 'storedFile' && model?.background.storedFile?.id && !isValidGuid(model?.background.storedFile.id)) { + return ; + } + const styling = JSON.parse(model.stylingBox || '{}'); + const stylingBoxAsCSS = pickStyleFromModel(styling); + const additionalStyles: CSSProperties = removeUndefinedProps({ + ...style, + ...stylingBoxAsCSS, + ...dimensionsStyles, + ...borderStyles, + ...backgroundStyles, + ...shadowStyles + }); + + const finalStyle = removeUndefinedProps({ ...additionalStyles, fontWeight: Number(model?.font?.weight?.split(' - ')[0]) || 400 }); + return ( col.size)} > {columns && @@ -51,24 +107,49 @@ const SizableColumnsComponent: IToolboxComponent = ); }, initModel: (model) => { + const { dimensions, ...rest } = defaultStyles(); const tabsModel: ISizableColumnComponentProps = { ...model, - columns: [ - { id: nanoid(), size: 50, components: [] }, - { id: nanoid(), size: 50, components: [] }, - ], - stylingBox: "{\"marginBottom\":\"5\"}" + ...rest, }; return tabsModel; }, - settingsFormFactory: (props) => , + settingsFormMarkup: (data) => getSettings(data), migrator: (m) => m .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev)) as ISizableColumnComponentProps) .add(1, (prev) => ({ ...migrateFormApi.properties(prev) })) .add(2, (prev) => removeComponents(prev)) - , + .add(3, (prev) => { + const columns = prev.columns.map(c => ({ + ...c, + components: c.components.map(c => ({ + ...c, + propertyName: c.propertyName || c.id + })) + })); + + return { + ...prev, + columns + }; + }) + .add(4, (prev) => { + const styles: ISizableColumnInputProps = { + size: prev.size, + width: prev.width, + height: prev.height, + hideBorder: prev.hideBorder, + borderSize: prev.borderSize, + borderRadius: prev.borderRadius, + borderColor: prev.borderColor, + backgroundColor: prev.backgroundColor, + stylingBox: prev.stylingBox, + }; + return { ...prev, desktop: { ...styles }, tablet: { ...styles }, mobile: { ...styles } }; + }) + .add(5, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), customContainerNames: ['columns'], }; -export default SizableColumnsComponent; +export default SizableColumnsComponent; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/sizableColumns/utils.ts b/shesha-reactjs/src/designer-components/sizableColumns/utils.ts new file mode 100644 index 0000000000..31c375ca74 --- /dev/null +++ b/shesha-reactjs/src/designer-components/sizableColumns/utils.ts @@ -0,0 +1,25 @@ +import { nanoid } from "@/utils/uuid"; +import { ISizableColumnInputProps } from "./interfaces"; + +export const defaultStyles = (): ISizableColumnInputProps => { + return { + dimensions: { width: '100%', height: 'auto', minHeight: '0px', maxHeight: 'auto', minWidth: '0px', maxWidth: 'auto' }, + columns: [ + { id: nanoid(), size: 50, components: [] }, + { id: nanoid(), size: 50, components: [] }, + ], + border: { + border: { + all: { + width: 0, + style: 'solid', + color: '#fff' + } + }, + radius: { all: 0 }, + borderType: 'all', + radiusType: 'all' + }, + stylingBox: "{\"marginBottom\":\"5\"}" + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/slider/index.tsx b/shesha-reactjs/src/designer-components/slider/index.tsx index b9b5bcccce..4e2a143982 100644 --- a/shesha-reactjs/src/designer-components/slider/index.tsx +++ b/shesha-reactjs/src/designer-components/slider/index.tsx @@ -6,7 +6,7 @@ import { useFormData } from '@/providers'; import { getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; import { SlidersFilled } from '@ant-design/icons'; import { ISliderComponentProps } from './interfaces'; -import { settingsFormMarkup } from './settings'; +import { getSettings } from './settingsForm'; const SwitchComponent: IToolboxComponent = { type: 'slider', @@ -23,7 +23,7 @@ const SwitchComponent: IToolboxComponent = { const max = model?.max ? parseInt(model.max, 10) : undefined; return ( - + {(value, onChange) => ( = { onChange={onChange} value={value} style={getStyle(model?.style, formData)} + /> )} @@ -45,8 +46,8 @@ const SwitchComponent: IToolboxComponent = { label: 'Slider', }; }, - settingsFormMarkup, - validateSettings: (model) => validateConfigurableComponentSettings(settingsFormMarkup, model), + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), }; export default SwitchComponent; diff --git a/shesha-reactjs/src/designer-components/slider/settings.ts b/shesha-reactjs/src/designer-components/slider/settings.ts index fe919ff229..1459c7acb9 100644 --- a/shesha-reactjs/src/designer-components/slider/settings.ts +++ b/shesha-reactjs/src/designer-components/slider/settings.ts @@ -29,7 +29,7 @@ export const settingsFormMarkup = new DesignerToolbarSettings() .addCheckbox({ id: nanoid(), propertyName: 'hidden', - label: 'Hidden', + label: 'hide', }) .addCheckbox({ id: nanoid(), diff --git a/shesha-reactjs/src/designer-components/slider/settingsForm.ts b/shesha-reactjs/src/designer-components/slider/settingsForm.ts new file mode 100644 index 0000000000..bd1b29b859 --- /dev/null +++ b/shesha-reactjs/src/designer-components/slider/settingsForm.ts @@ -0,0 +1,191 @@ +import { nanoid } from '@/utils/uuid'; +import { DesignerToolbarSettings } from '@/interfaces'; +import { FormLayout } from 'antd/lib/form/Form'; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const dataTabId = nanoid(); + const appearanceTabId = nanoid(); + const securityTabId = nanoid(); + const styleRouterId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: 'propertyName', + parentId: commonTabId, + label: 'Property name', + size: 'small', + validate: { + required: true + }, + styledLabel: true, + jsSetting: true, + }) + .addLabelConfigurator({ + id: '46d07439-4c18-468c-89e1-60c002ce96c5', + propertyName: 'hideLabel', + label: 'Label', + parentId: 's4gmBg31azZC0UjZjpfTm', + hideLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type:'editModeSelector', + id: nanoid(), + propertyName: 'editMode', + label: 'Edit Mode', + defaultValue: 'inherited', + jsSetting: true, + }, + { + type: 'switch', + id: nanoid(), + propertyName: 'hidden', + label: 'Hide', + jsSetting: true, + }, + ] + }) + .toJson() + ] + }, + { + key: 'data', + title: 'Data', + id: dataTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'defaultValue', + label: 'Default Value', + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dataTabId, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'min', + label: 'Minimum', + jsSetting: true, + }, + { + type: 'numberField', + id: nanoid(), + propertyName: 'max', + label: 'Maximum', + jsSetting: true, + } + ] + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceTabId, + components: [...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceTabId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: "return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'customStyle', + label: 'Custom Styles', + labelAlign: 'right', + parentId: styleRouterId, + ghost: true, + collapsible: 'header', + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + label: 'Style', + parentId: 'root', + mode: 'dialog', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson() + ] + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityTabId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityTabId, + tooltip: 'Enter a list of permissions that should be associated with this component', + jsSetting: true, + }) + .toJson() + ] + } + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; \ No newline at end of file diff --git a/shesha-reactjs/src/designer-components/sortingEditor/index.tsx b/shesha-reactjs/src/designer-components/sortingEditor/index.tsx index 3febf7f40e..1db1fc24f8 100644 --- a/shesha-reactjs/src/designer-components/sortingEditor/index.tsx +++ b/shesha-reactjs/src/designer-components/sortingEditor/index.tsx @@ -7,11 +7,13 @@ import settingsFormJson from './settingsForm.json'; import { validateConfigurableComponentSettings } from '@/providers/form/utils'; import { evaluateString } from '@/providers/form/utils'; import { SortingEditor } from '@/components/dataTable/sortingConfigurator/index'; -import { useFormData } from '@/providers/index'; +import { MetadataProvider } from '@/providers/index'; import { migrateReadOnly } from '../_common-migrations/migrateSettings'; +import ConditionalWrap from '@/components/conditionalWrapper'; export interface ISortingEditorComponentProps extends IConfigurableFormComponent { modelType: string; + maxItemsCount?: number; } const settingsForm = settingsFormJson as FormMarkup; @@ -23,17 +25,17 @@ export const SortingEditorComponent: IToolboxComponent, - Factory: ({ model }) => { - const { data: formData } = useFormData(); - const { modelType: modelTypeExpression } = model; - - const modelType = modelTypeExpression ? evaluateString(modelTypeExpression, { data: formData }) : null; - const readOnly = model.readOnly; - + calculateModel: (model, allData) => ({ modelType: model.modelType ? evaluateString(model.modelType, { data: allData.data }) : null }), + Factory: ({ model, calculatedModel }) => { return ( - - {(value, onChange) => } - + {content}} + > + + {(value, onChange) => } + + ); }, settingsFormMarkup: settingsForm, diff --git a/shesha-reactjs/src/designer-components/statistic/index.tsx b/shesha-reactjs/src/designer-components/statistic/index.tsx index 5120db4ad4..a2e5be6d64 100644 --- a/shesha-reactjs/src/designer-components/statistic/index.tsx +++ b/shesha-reactjs/src/designer-components/statistic/index.tsx @@ -1,23 +1,45 @@ +import { ConfigurableFormItem } from '@/components'; +import { customOnClickEventHandler, getEventHandlers} from '@/components/formDesigner/components/utils'; +import ShaIcon, { IconType } from '@/components/shaIcon'; +import ShaStatistic from '@/components/statistic'; import { IToolboxComponent } from '@/interfaces'; -import { FormMarkup, IConfigurableFormComponent } from '@/providers/form/models'; +import { IInputStyles, useForm } from '@/providers'; +import { IConfigurableFormComponent } from '@/providers/form/models'; +import { + getStyle, + useAvailableConstantsData, + validateConfigurableComponentSettings, +} from '@/providers/form/utils'; +import { removeUndefinedProps } from '@/utils/object'; import { BarChartOutlined } from '@ant-design/icons'; -import settingsFormJson from './settingsForm.json'; -import React from 'react'; -import { getStyle, validateConfigurableComponentSettings } from '@/providers/form/utils'; -import ShaStatistic, { IShaStatisticProps } from '@/components/statistic'; -import ShaIcon from '@/components/shaIcon'; -import { useFormData } from '@/providers'; -import _ from 'lodash'; -import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; -import { migrateCustomFunctions, migratePropertyName } from '@/designer-components/_common-migrations/migrateSettings'; +import React, { useMemo } from 'react'; import { migrateFormApi } from '../_common-migrations/migrateFormApi1'; +import { migratePrevStyles } from '../_common-migrations/migrateStyles'; +import { IFontValue } from '../_settings/utils/font/interfaces'; +import { getFontStyle } from '../_settings/utils/font/utils'; +import { getSettings } from './settingsForm'; +import { defaultStyles } from './utils'; +import { IBorderValue } from '../_settings/utils/border/interfaces'; -const settingsForm = settingsFormJson as FormMarkup; - -interface IStatisticComponentProps - extends Omit, - IConfigurableFormComponent { +interface IStatisticComponentProps extends Omit, IConfigurableFormComponent { + value?: number | string; + precision?: number; + title?: string | number; + placeholder?: string; valueStyle?: string; + titleStyle?: string; + prefix?: string; + suffix?: string; + prefixIcon?: string; + suffixIcon?: string; + titleFont?: IFontValue; + valueFont?: IFontValue; + onClick?: () => void; + onDoubleClick?: () => void; + borderSize?: string | number; + borderRadius?: string | number; + borderType?: string; + borderColor?: string; } const StatisticComponent: IToolboxComponent = { @@ -27,38 +49,139 @@ const StatisticComponent: IToolboxComponent = { isInput: true, isOutput: true, Factory: ({ model: passedModel }) => { - const { style, valueStyle, prefix, suffix, ...model } = passedModel; - const { data: formData } = useFormData(); + const { style, valueStyle, titleStyle, prefix, suffix, prefixIcon, suffixIcon, ...model } = passedModel; + const allData = useAvailableConstantsData(); + const { formMode } = useForm(); + const { allStyles } = model; + const valueFont = model?.valueFont; + const titleFont = model?.titleFont; + const valueStyles = getStyle(valueStyle); + const titleStyles = getStyle(titleStyle); + const valueFontStyles = useMemo(() => getFontStyle(valueFont), [valueFont]); + const titleFontStyles = useMemo(() => getFontStyle(titleFont), [titleFont]); + + const customEvents = getEventHandlers(model, allData); + + if (formMode === 'designer') { + return ( + + {(value, _) => { + const customEvent = customOnClickEventHandler(model, allData); + const onClickInternal = (_: any) => { + customEvent.onClick(value); + }; + return ( + {passedModel?.title}
+ } + prefix={ +
+ {passedModel.prefixIcon && } + {passedModel.prefix ? passedModel.prefix : null} +
+ } + suffix={ +
+ + {passedModel.suffix && passedModel.suffix} + {passedModel.suffixIcon && } + +
+ } + style={removeUndefinedProps({ ...allStyles.fullStyle })} + valueStyle={removeUndefinedProps({ + ...valueFontStyles, + ...valueStyles, + ...(!(value || passedModel?.value) && { opacity: 0.5, color: '#999' }), + })} + {...customEvents} + onClick={onClickInternal} + /> + ); + }} + + ); + } return ( - - {(value) => - : null} - suffix={suffix ? : null} - style={getStyle(style, formData)} - valueStyle={getStyle(valueStyle, formData)} - /> - } + + {(value) => { + const customEvent = customOnClickEventHandler(model, allData); + const onClickInternal = (_: any) => { + customEvent.onClick(value); + }; + return ( + {passedModel?.title}
+ } + prefix={ +
+ {passedModel.prefixIcon && } + {passedModel.prefix ? passedModel.prefix : null} +
+ } + suffix={ +
+ {suffix && suffix} + {suffixIcon && } +
+ } + style={removeUndefinedProps({ ...allStyles.fullStyle })} + valueStyle={removeUndefinedProps({ + ...valueFontStyles, + ...valueStyles, + ...(!(value || passedModel?.value) && { opacity: 0.5, color: '#999' }), + })} + {...customEvents} + onClick={onClickInternal} + /> + ); + }}
); }, - settingsFormMarkup: settingsForm, - validateSettings: model => validateConfigurableComponentSettings(settingsForm, model), - migrator: (m) => m - .add(0, (prev) => migratePropertyName(migrateCustomFunctions(prev)) as IStatisticComponentProps) - .add(1, (prev) => ({...migrateFormApi.properties(prev)})) - .add(2, (prev) => { - const styles = { - style: prev.style, - valueStyle: prev.valueStyle - }; + settingsFormMarkup: (data) => getSettings(data), + validateSettings: (model) => validateConfigurableComponentSettings(getSettings(model), model), + migrator: (m) => + m + .add(1, (prev) => ({ ...migrateFormApi.properties(prev) })) + .add(2, (prev) => { + const styles = { + style: prev?.style, + valueFont: defaultStyles().valueFont, + valueStyle: prev?.valueStyle, + titleFont: defaultStyles().titleFont, + titleStyle: prev?.titleStyle, + hideBorder: prev?.hideBorder, + shadow: defaultStyles().shadow, + border: { + border: defaultStyles().border.border as IBorderValue, + }, + }; - return { ...prev, desktop: {...styles}, tablet: {...styles}, mobile: {...styles} }; - }) - , + return { + ...prev, + placeholder: 'R 1,234.00', + desktop: { ...styles }, + tablet: { ...styles }, + mobile: { ...styles }, + }; + }) + .add(3, (prev) => ({ ...migratePrevStyles(prev, defaultStyles()) })), }; export default StatisticComponent; diff --git a/shesha-reactjs/src/designer-components/statistic/settingsForm.ts b/shesha-reactjs/src/designer-components/statistic/settingsForm.ts new file mode 100644 index 0000000000..363034bc31 --- /dev/null +++ b/shesha-reactjs/src/designer-components/statistic/settingsForm.ts @@ -0,0 +1,737 @@ +import { DesignerToolbarSettings } from "@/index"; +import { nanoid } from "@/utils/uuid"; +import { FormLayout } from "antd/lib/form/Form"; +import { getBorderInputs, getCornerInputs } from "../_settings/utils/border/utils"; +import { fontTypes, fontWeights, textAlign } from "../_settings/utils/font/utils"; +import { repeatOptions, positionOptions, sizeOptions } from "../_settings/utils/background/utils"; + +export const getSettings = (data: any) => { + const searchableTabsId = nanoid(); + const commonTabId = nanoid(); + const appearanceId = nanoid(); + const securityId = nanoid(); + const eventsId = nanoid(); + const styleRouterId = nanoid(); + const pnlFontStyleId = nanoid(); + const dimensionsStylePnlId = nanoid(); + const backgroundStylePnlId = nanoid(); + + const shadowStylePnlId = nanoid(); + + return { + components: new DesignerToolbarSettings(data) + .addSearchableTabs({ + id: searchableTabsId, + propertyName: 'settingsTabs', + parentId: 'root', + label: 'Settings', + hideLabel: true, + labelAlign: 'right', + size: 'small', + tabs: [ + { + key: 'common', + title: 'Common', + id: commonTabId, + components: [...new DesignerToolbarSettings() + .addContextPropertyAutocomplete({ + id: nanoid(), + propertyName: "propertyName", + parentId: commonTabId, + label: "Property Name", + size: "small", + validate: { + "required": true + }, + jsSetting: true, + styledLabel: true, + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'title', + label: 'Title', + jsSetting: true, + value: 'Statistic', + }, + { + type: 'textField', + id: nanoid(), + propertyName: 'placeholder', + label: 'Placeholder', + jsSetting: true, + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'numberField', + id: nanoid(), + propertyName: 'value', + label: 'Value', + jsSetting: true, + description: 'The value to display in the statistic. This value will will only override if there is no binding to the property name.', + }, + { + type: 'numberField', + id: nanoid(), + propertyName: 'precision', + label: 'Precision', + jsSetting: true, + min: 0, + } + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'prefix', + label: 'Prefix', + jsSetting: true, + }, + { + type: 'iconPicker', + id: nanoid(), + propertyName: 'prefixIcon', + label: 'Prefix Icon', + jsSetting: true, + }, + ], + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: commonTabId, + inputs: [ + { + type: 'textField', + id: nanoid(), + propertyName: 'suffix', + label: 'Suffix', + jsSetting: true, + }, + { + type: 'iconPicker', + id: nanoid(), + propertyName: 'suffixIcon', + label: 'Suffix Icon', + jsSetting: true, + }, + ], + }) + .toJson() + ] + }, + { + key: 'events', + title: 'Events', + id: eventsId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'onClickCustom', + label: 'On Click', + labelAlign: 'right', + tooltip: 'Enter custom eventhandler on click of event.', + parentId: eventsId + }) + .toJson() + ] + }, + { + key: 'appearance', + title: 'Appearance', + id: appearanceId, + components: [ + ...new DesignerToolbarSettings() + .addPropertyRouter({ + id: styleRouterId, + propertyName: 'propertyRouter1', + componentName: 'propertyRouter', + label: 'Property router1', + labelAlign: 'right', + parentId: appearanceId, + hidden: false, + propertyRouteName: { + _mode: "code", + _code: " return contexts.canvasContext?.designerDevice || 'desktop';", + _value: "" + }, + components: [ + ...new DesignerToolbarSettings() + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'titleStyle', + label: 'Title Font', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlFontStyleId, + inline: true, + propertyName: 'titleFont', + inputs: [ + { + type: 'dropdown', + id: `fontFamily-${styleRouterId}`, + label: 'Family', + propertyName: 'titleFont.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: `fontSize-${styleRouterId}`, + label: 'Size', + propertyName: 'titleFont.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: `fontWeight-${styleRouterId}`, + label: 'Weight', + propertyName: 'titleFont.weight', + hideLabel: true, + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: `fontColor-${styleRouterId}`, + label: 'Color', + hideLabel: true, + propertyName: 'titleFont.color', + }, + { + type: 'dropdown', + id: `fontAlign-${styleRouterId}`, + label: 'Align', + propertyName: 'titleFont.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'titleStyle', + hideLabel: false, + label: 'Custom Styles', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'valueStyle', + label: 'Value Font', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: pnlFontStyleId, + inline: true, + propertyName: 'valueFont', + label: 'Value Font', + inputs: [ + { + type: 'dropdown', + id: `fontFamily-${styleRouterId}`, + label: 'Family', + propertyName: 'valueFont.type', + hideLabel: true, + dropdownOptions: fontTypes, + }, + { + type: 'numberField', + id: `fontSize-${styleRouterId}`, + label: 'Size', + propertyName: 'valueFont.size', + hideLabel: true, + width: 50, + }, + { + type: 'dropdown', + id: `fontWeight-${styleRouterId}`, + label: 'Weight', + propertyName: 'valueFont.weight', + hideLabel: true, + dropdownOptions: fontWeights, + width: 100, + }, + { + type: 'colorPicker', + id: `fontColor-${styleRouterId}`, + label: 'Color', + hideLabel: true, + propertyName: 'valueFont.color', + }, + { + type: 'dropdown', + id: `fontAlign-${styleRouterId}`, + label: 'Align', + propertyName: 'valueFont.align', + hideLabel: true, + width: 60, + dropdownOptions: textAlign, + }, + ], + }) + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'valueStyle', + hideLabel: false, + label: 'Custom Styles', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: 'dimensionsStyleCollapsiblePanel', + propertyName: 'pnlDimensions', + label: 'Dimensions', + parentId: styleRouterId, + labelAlign: 'right', + ghost: true, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: dimensionsStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: `width-${styleRouterId}`, + label: "Width", + width: 85, + propertyName: "dimensions.width", + icon: "widthIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: `minWidth-${styleRouterId}`, + label: "Min Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.minWidth", + icon: "minWidthIcon", + }, + { + type: 'textField', + id: `maxWidth-${styleRouterId}`, + label: "Max Width", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxWidth", + icon: "maxWidthIcon", + } + ] + }) + .addSettingsInputRow({ + id: nanoid(), + parentId: dimensionsStylePnlId, + inline: true, + inputs: [ + { + type: 'textField', + id: `height-${dimensionsStylePnlId}`, + label: "Height", + width: 85, + propertyName: "dimensions.height", + icon: "heightIcon", + tooltip: "You can use any unit (%, px, em, etc). px by default if without unit" + }, + { + type: 'textField', + id: `minHeight-${dimensionsStylePnlId}`, + label: "Min Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.minHeight", + icon: "minHeightIcon", + }, + { + type: 'textField', + id: `maxHeight-${dimensionsStylePnlId}`, + label: "Max Height", + width: 85, + hideLabel: true, + propertyName: "dimensions.maxHeight", + icon: "maxHeightIcon", + } + ] + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBorderStyle', + label: 'Border', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + hidden: false, + collapsedByDefault: true, + content: { + id: 'borderStylePnl', + components: [...new DesignerToolbarSettings() + .addContainer({ + id: 'borderStyleRow', + parentId: 'borderStylePnl', + components: getBorderInputs() as any + }) + .addContainer({ + id: 'borderRadiusStyleRow', + parentId: 'borderStylePnl', + components: getCornerInputs() as any + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlBackgroundStyle', + label: 'Background', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: backgroundStylePnlId, + components: [ + ...new DesignerToolbarSettings() + .addSettingsInput({ + id: `${backgroundStylePnlId}-type`, + parentId: backgroundStylePnlId, + label: "Type", + jsSetting: false, + propertyName: "background.type", + inputType: "radio", + tooltip: "Select a type of background", + buttonGroupOptions: [ + { + value: "color", + icon: "FormatPainterOutlined", + title: "Color" + }, + { + value: "gradient", + icon: "BgColorsOutlined", + title: "Gradient" + }, + { + value: "image", + icon: "PictureOutlined", + title: "Image" + }, + { + value: "url", + icon: "LinkOutlined", + title: "URL" + }, + { + value: "storedFile", + icon: "DatabaseOutlined", + title: "Stored File" + } + ], + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-color`, + parentId: backgroundStylePnlId, + inputs: [{ + type: 'colorPicker', + id: nanoid(), + label: "Color", + propertyName: "background.color", + hideLabel: true, + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "color";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-gradient`, + parentId: backgroundStylePnlId, + inputs: [{ + type: 'multiColorPicker', + id: nanoid(), + propertyName: "background.gradient.colors", + label: "Colors", + jsSetting: false, + } + ], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "gradient";', _mode: 'code', _value: false } as any, + hideLabel: true, + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-url`, + parentId: backgroundStylePnlId, + inputs: [{ + type: 'textField', + id: nanoid(), + propertyName: "background.url", + jsSetting: false, + label: "URL", + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "url";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-image`, + parentId: backgroundStylePnlId, + inputs: [{ + type: 'imageUploader', + id: 'backgroundStyle-image', + propertyName: 'background.uploadFile', + label: "Image", + jsSetting: false, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "image";', _mode: 'code', _value: false } as any, + }) + .addSettingsInputRow({ + id: `${backgroundStylePnlId}-storedFile`, + parentId: backgroundStylePnlId, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) !== "storedFile";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'textField', + id: 'backgroundStyle-storedFile', + jsSetting: false, + propertyName: "background.storedFile.id", + label: "File ID" + } + ] + }) + .addSettingsInputRow({ + id: "backgroundStyleRow-controls", + parentId: 'backgroundStyleRow', + inline: true, + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + inputs: [ + { + type: 'customDropdown', + id: 'backgroundStyleRow-size', + label: "Size", + hideLabel: true, + propertyName: "background.size", + customTooltip: 'Size of the background image, two space separated values with units e.g "100% 100px"', + dropdownOptions: sizeOptions, + }, + { + type: 'customDropdown', + id: 'backgroundStyleRow-position', + label: "Position", + hideLabel: true, + customTooltip: 'Position of the background image, two space separated values with units e.g "5em 100px"', + propertyName: "background.position", + dropdownOptions: positionOptions, + } + ] + }) + .addSettingsInputRow({ + id: 'backgroundStyleRow-repeat', + parentId: 'backgroundStyleRow', + inputs: [{ + type: 'radio', + id: 'backgroundStyleRow-repeat-radio', + label: 'Repeat', + hideLabel: true, + propertyName: 'background.repeat', + inputType: 'radio', + buttonGroupOptions: repeatOptions, + }], + hidden: { _code: 'return getSettingValue(data[`${contexts.canvasContext?.designerDevice || "desktop"}`]?.background?.type) === "color";', _mode: 'code', _value: false } as any, + }) + .toJson() + ], + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'pnlShadowStyle', + label: 'Shadow', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: shadowStylePnlId, + components: [...new DesignerToolbarSettings() + .addSettingsInputRow({ + id: nanoid(), + parentId: shadowStylePnlId, + inline: true, + inputs: [ + { + type: 'numberField', + id: nanoid(), + label: 'H-Offset', + hideLabel: true, + width: 80, + icon: "offsetHorizontalIcon", + propertyName: 'shadow.offsetX', + tooltip: 'OffsetX. The larger the value, the bigger the shadow', + description: 'OffsetX. The larger the value, the bigger the shadow', + }, + { + type: 'numberField', + id: nanoid(), + label: 'V-Offset', + hideLabel: true, + width: 80, + icon: 'offsetVerticalIcon', + propertyName: 'shadow.offsetY', + description: 'OffsetY. The larger the value, the bigger the shadow', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Blur', + hideLabel: true, + width: 80, + icon: 'blurIcon', + propertyName: 'shadow.blurRadius', + description: 'Blur. The larger the value, the bigger the blur', + }, + { + type: 'numberField', + id: nanoid(), + label: 'Spread', + hideLabel: true, + width: 80, + icon: 'spreadIcon', + propertyName: 'shadow.spreadRadius', + description: 'Spread. The larger the value, the bigger the spread', + }, + { + type: 'colorPicker', + id: nanoid(), + label: 'Color', + hideLabel: true, + propertyName: 'shadow.color', + }, + ], + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'stylingBox', + label: 'Margin & Padding', + labelAlign: 'right', + ghost: true, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addStyleBox({ + id: nanoid(), + label: 'Margin Padding', + hideLabel: true, + propertyName: 'stylingBox', + }) + .toJson() + ] + } + }) + .addCollapsiblePanel({ + id: nanoid(), + propertyName: 'style', + label: 'Custom Styles', + labelAlign: 'right', + ghost: true, + parentId: styleRouterId, + collapsible: 'header', + collapsedByDefault: true, + content: { + id: nanoid(), + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'codeEditor', + propertyName: 'style', + hideLabel: false, + label: 'Style', + description: 'A script that returns the style of the element as an object. This should conform to CSSProperties', + }) + .toJson() + ] + } + }) + .toJson(), + ] + }) + .toJson() + ] + }, + { + key: 'security', + title: 'Security', + id: securityId, + components: [...new DesignerToolbarSettings() + .addSettingsInput({ + id: nanoid(), + inputType: 'permissions', + propertyName: 'permissions', + label: 'Permissions', + size: 'small', + parentId: securityId + }) + .toJson() + ] + }, + ] + }) + .toJson(), + formSettings: { + colon: false, + layout: 'vertical' as FormLayout, + labelCol: { span: 24 }, + wrapperCol: { span: 24 } + } + }; +}; diff --git a/shesha-reactjs/src/designer-components/statistic/utils.ts b/shesha-reactjs/src/designer-components/statistic/utils.ts new file mode 100644 index 0000000000..37fcd5d878 --- /dev/null +++ b/shesha-reactjs/src/designer-components/statistic/utils.ts @@ -0,0 +1,38 @@ +import { IStyleType } from '@/index'; + +export const defaultStyles = (): {} extends IStyleType | null ? any : any => { + return { + background: { type: 'color', color: '#fff' }, + titleFont: { weight: '300', size: 20, color: '#000', type: 'Segoe UI', align: 'center' }, + valueFont: { weight: '300', size: 35, color: '#000', type: 'Segoe UI', align: 'center' }, + border: { + hideBorder: false, + radiusType: 'all', + borderType: 'all', + border: { + all: { width: '1px', style: 'solid', color: '#d9d9d9' }, + top: { width: '1px', style: 'solid', color: '#d9d9d9' }, + bottom: { width: '1px', style: 'solid', color: '#d9d9d9' }, + left: { width: '3px', style: 'solid', color: 'primary' }, + right: { width: '1px', style: 'solid', color: '#d9d9d9' }, + }, + radius: { all: 8 }, + }, + dimensions: { + width: '100%', + height: 'auto', + minHeight: '0px', + maxHeight: 'auto', + minWidth: '0px', + maxWidth: 'auto', + }, + style: { padding: '0px', margin: '0px auto', verticalAlign: 'middle', textAlign: 'center' }, + shadow: { + color: '#96aab480', + offsetX: 0, + offsetY: 7, + blurRadius: 30, + spreadRadius: -10, + }, + }; +}; diff --git a/shesha-reactjs/src/designer-components/statusTag/settings.ts b/shesha-reactjs/src/designer-components/statusTag/settings.ts index ec12fd7bb8..9e8d236025 100644 --- a/shesha-reactjs/src/designer-components/statusTag/settings.ts +++ b/shesha-reactjs/src/designer-components/statusTag/settings.ts @@ -13,8 +13,8 @@ export const getSettings = () => ghost: true, collapsible: 'header', content: { - id:'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - components: [...new DesignerToolbarSettings() + id: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', + components: [...new DesignerToolbarSettings() .addContextPropertyAutocomplete({ id: nanoid(), propertyName: 'propertyName', @@ -42,7 +42,7 @@ export const getSettings = () => id: 'd936689d-8ea3-4f8a-bcc3-d494d27c46fd', propertyName: 'hidden', parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', - label: 'Hidden', + label: 'hide', labelAlign: 'right', validate: {}, }) @@ -73,7 +73,7 @@ export const getSettings = () => parentId: 'pnl54bf6-f76d-4139-a850-c99bf06c8b69', label: 'Value', validate: {}, - hidden: {_code: 'return getSettingValue(data?.valueSource) !== "manual";', _mode: 'code', _value: false} as any, + hidden: { _code: 'return getSettingValue(data?.valueSource) !== "manual";', _mode: 'code', _value: false } as any, }) .addTextField({ id: '821d3a6c-abdb-4f11-b659-e562c75bada9', @@ -112,8 +112,8 @@ export const getSettings = () => ghost: true, collapsible: 'header', content: { - id:'pnl24bf6-f76d-4139-a850-c99bf06c8b71', - components: [...new DesignerToolbarSettings() + id: 'pnl24bf6-f76d-4139-a850-c99bf06c8b71', + components: [...new DesignerToolbarSettings() .addPermissionAutocomplete({ id: '4d81ae9d-d222-4fc1-85b2-4dc3ee6a3721', propertyName: 'permissions', @@ -124,6 +124,6 @@ export const getSettings = () => validate: {}, }).toJson() ] - } - }) + } + }) .toJson(); diff --git a/shesha-reactjs/src/designer-components/styleBox/index.tsx b/shesha-reactjs/src/designer-components/styleBox/index.tsx index 79e7876036..e1bb28ef5a 100644 --- a/shesha-reactjs/src/designer-components/styleBox/index.tsx +++ b/shesha-reactjs/src/designer-components/styleBox/index.tsx @@ -25,7 +25,7 @@ const StyleBox: IToolboxComponent = { Factory: ({ model: passedModel }) => { const { size, ...model } = passedModel; - return ( + return model.hidden ? null : ( {(value, onChange) => } diff --git a/shesha-reactjs/src/designer-components/styleBox/interfaces.ts b/shesha-reactjs/src/designer-components/styleBox/interfaces.ts index 6df972b461..85d0798d8e 100644 --- a/shesha-reactjs/src/designer-components/styleBox/interfaces.ts +++ b/shesha-reactjs/src/designer-components/styleBox/interfaces.ts @@ -1,6 +1,8 @@ import { IConfigurableFormComponent } from '@/providers/form/models'; -export interface IStyleBoxComponentProps extends IConfigurableFormComponent {} +export interface IStyleBoxComponentProps extends IConfigurableFormComponent { + noMargin?: boolean; +} export interface IInputDirection { top?: number; diff --git a/shesha-reactjs/src/designer-components/styleBox/settings.ts b/shesha-reactjs/src/designer-components/styleBox/settings.ts index 715f808ad9..4e7f679b17 100644 --- a/shesha-reactjs/src/designer-components/styleBox/settings.ts +++ b/shesha-reactjs/src/designer-components/styleBox/settings.ts @@ -43,7 +43,7 @@ export const getSettings = () => id: nanoid(), propertyName: 'hidden', parentId: '7d8493e3-141f-4507-a983-7f3ff7f2cf86', - label: 'Hidden', + label: 'hide', labelAlign: 'right', validate: {}, }) diff --git a/shesha-reactjs/src/designer-components/styleBox/styles/styles.ts b/shesha-reactjs/src/designer-components/styleBox/styles/styles.ts index 405355d91e..46ef4760d6 100644 --- a/shesha-reactjs/src/designer-components/styleBox/styles/styles.ts +++ b/shesha-reactjs/src/designer-components/styleBox/styles/styles.ts @@ -23,9 +23,9 @@ export const useStyles = createStyles(({ css, cx, prefixCls }) => { const title = "title"; const shaStyleBox = cx("sha-style-box", css` - height: 200px; + height: 155px; overflow: hidden; - width: 320px; + width: 240px; .${prefixCls}-input { border: none; @@ -33,38 +33,56 @@ export const useStyles = createStyles(({ css, cx, prefixCls }) => { padding: unset; position: absolute; text-align: center; - width: 26px; + width: 25px; + width: 25px; } .${center} { border-radius: 5px; background-color: ${background}; - height: 50px; - left: 45px; + height: 40px; + left: 35px; + height: 40px; + left: 35px; overflow: hidden; position: relative; - top: 35px; - width: 134px; + top: 30px; + width: 90px; + top: 30px; + width: 90px; } .${margTop} { + position: absolute; + position: absolute; top: 2px; - left: 147px; + left: 107px; + left: 107px; } .${margLeft} { + position: absolute; + position: absolute; left: 2px; - top: 89px; + top: 69px; + top: 69px; } .${margBottom} { + position: absolute; + position: absolute; bottom: 2px; - left: 147px; + left: 107px; + left: 107px; } .${margRight} { - left: 292px; - top: 89px; + position: absolute; + left: 205px; + top: 69px; + position: absolute; + left: 205px; + top: 69px; } .${margin} { @@ -75,42 +93,62 @@ export const useStyles = createStyles(({ css, cx, prefixCls }) => { } .${paddTop} { + position: absolute; + position: absolute; top: 2px; - left: 99px; + left: 70px; + left: 70px; } .${paddLeft} { + position: absolute; + position: absolute; left: 2px; - top: 49px; + top: 39px; + top: 39px; } .${paddBottom} { + position: absolute; + position: absolute; bottom: 2px; - left: 99px; + left: 70px; + left: 70px; } .${paddRight} { - left: 196px; - top: 49px; + position: absolute; + left: 130px; + top: 39px; + position: absolute; + left: 130px; + top: 39px; } .${padding} { border-radius: 5px; background-color: #fff; - height: 120px; - left: 48px; + height: 100px; + left: 38px; + height: 100px; + left: 38px; overflow: hidden; position: relative; - top: 40px; - width: 224px; + top: 30px; + width: 160px; + top: 30px; + width: 160px; } .${title} { - font-size: 14px; + font-size: 12px; font-weight: bold; position: absolute; left: 8px; top: 5px; + max-height: 28px; + color: darkslategrey; + font-weight: 500; } input::-webkit-outer-spin-button, @@ -127,6 +165,8 @@ export const useStyles = createStyles(({ css, cx, prefixCls }) => { input:focus-visible { outline: unset; } + + `); return { shaStyleBox, diff --git a/shesha-reactjs/src/designer-components/styleLabel/index.tsx b/shesha-reactjs/src/designer-components/styleLabel/index.tsx new file mode 100644 index 0000000000..a4d66eebb0 --- /dev/null +++ b/shesha-reactjs/src/designer-components/styleLabel/index.tsx @@ -0,0 +1,28 @@ + +import { IToolboxComponent } from '@/interfaces'; +import { ColumnWidthOutlined } from '@ant-design/icons'; +import React from 'react'; +import ConfigurableFormItem from '@/components/formDesigner/components/formItem'; +import LabelConfiguratorComponent from './labelConfigurator'; +import { ILabelComponentProps } from './interfaces'; +import { getSettings } from './settings'; + +const LabelConfigurator: IToolboxComponent = { + type: 'labelConfigurator', + name: 'Label Configurator', + isInput: true, + isOutput: true, + canBeJsSetting: true, + icon: , + Factory: ({ model }) => { + + return ( + + + + ); + }, + settingsFormMarkup: getSettings(), +}; + +export default LabelConfigurator; diff --git a/shesha-reactjs/src/designer-components/styleLabel/interfaces.ts b/shesha-reactjs/src/designer-components/styleLabel/interfaces.ts new file mode 100644 index 0000000000..f6b8647381 --- /dev/null +++ b/shesha-reactjs/src/designer-components/styleLabel/interfaces.ts @@ -0,0 +1,11 @@ +import { IConfigurableFormComponent } from "@/providers"; +import { IRadioOption } from "../settingsInput/interfaces"; + +export interface ILabelComponentProps extends IConfigurableFormComponent { + value?: boolean; + onChange?: Function; + alignPropName?: string; + labelPropName?: string; + hideLabelPropName?: string; + labelAlignOptions?: IRadioOption[]; +} diff --git a/shesha-reactjs/src/designer-components/styleLabel/labelConfigurator.tsx b/shesha-reactjs/src/designer-components/styleLabel/labelConfigurator.tsx new file mode 100644 index 0000000000..7d64fcf141 --- /dev/null +++ b/shesha-reactjs/src/designer-components/styleLabel/labelConfigurator.tsx @@ -0,0 +1,70 @@ +import React, { FC } from 'react'; +import { useStyles } from './styles'; +import { labelAlignOptions } from './utils'; +import { SettingInput } from '../settingsInput/settingsInput'; +import { nanoid } from '@/utils/uuid'; +import { IRadioOption } from '../settingsInput/interfaces'; + +export interface ILabelProps { + readOnly?: boolean; + onChange?: (value: any) => void; + value?: boolean; + label: string | React.ReactNode; + labelAlignOptions?: IRadioOption[]; +} + +const LabelConfiguratorComponent: FC = ({ value, readOnly, label, labelAlignOptions: labelAlign }) => { + const { styles } = useStyles(); + return ( + <> +
+
+