diff --git a/.all-contributorsrc b/.all-contributorsrc index d9a55b4f45d..2b8d8b37254 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3149,6 +3149,15 @@ "contributions": [ "code" ] + }, + { + "login": "vsezima", + "name": "Václav Sezima", + "avatar_url": "https://avatars.githubusercontent.com/u/15254338?v=4", + "profile": "https://github.com/vsezima", + "contributions": [ + "code" + ] } ], "skipCi": true, diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d2ed54a382a..6fd31154ebc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: Bug report -about: Create a report to help us improve +about: Create a bug report to help us improve title: '' labels: 'bug :bug:' assignees: '' @@ -10,6 +10,10 @@ assignees: '' ### Describe the bug +### Orchard Core version + +Add the version of the Orchard Core NuGet packages you use, or the commit hash if you can reproduce this with the source code. + ### To Reproduce Steps to reproduce the behavior: 1. Go to '...' @@ -20,5 +24,5 @@ Steps to reproduce the behavior: ### Expected behavior A clear and concise description of what you expected to happen. -### Screenshots -If applicable, add screenshots to help explain your problem. +### Logs and screenshots +If applicable, add log files, browser console logs, and screenshots (or screen recording videos) to help explain your problem. diff --git a/.github/workflows/close_stale_prs_issues.yml b/.github/workflows/close_stale_prs_issues.yml index b85f9ffbc65..25ccdabea3a 100644 --- a/.github/workflows/close_stale_prs_issues.yml +++ b/.github/workflows/close_stale_prs_issues.yml @@ -23,12 +23,12 @@ jobs: Closing this pull request because it has been stale for very long. If you think this is still relevant, feel free to reopen it. stale-issue-message: > - It seems that this issue didn't really move for quite a while. Is this something you'd like to revisit - any time soon or should we close? Please reply. + It seems that this issue didn't really move for quite a while despite us asking the author for further + feedback. Is this something you'd like to revisit any time soon or should we close? Please reply. stale-issue-label: stale days-before-issue-stale: 15 days-before-issue-close: 7 only-issue-labels: needs author feedback close-issue-message: > - Closing this issue because it has been stale for very long. If you think this is still relevant, - feel free to reopen it. + Closing this issue because it didn't receive further feedback from the author for very long. If you think + this is still relevant, feel free to reopen it with the requested details. diff --git a/.github/workflows/comment_issue_on_triage.yml b/.github/workflows/comment_issue_on_triage.yml index 1ad96dfa4a2..1565b6750a0 100644 --- a/.github/workflows/comment_issue_on_triage.yml +++ b/.github/workflows/comment_issue_on_triage.yml @@ -12,7 +12,8 @@ jobs: runs-on: ubuntu-latest permissions: issues: write - if: github.event.issue.state == 'open' + # Despite the trigger being called "issues", this would still run for setting the milestone of PRs too. + if: github.event.issue.pull_request == null && github.event.issue.state == 'open' steps: - name: Add Comment run: gh issue comment "$NUMBER" --body "$BODY" diff --git a/.github/workflows/community_metrics.yml b/.github/workflows/community_metrics.yml index 720799ab124..5005475e9f0 100644 --- a/.github/workflows/community_metrics.yml +++ b/.github/workflows/community_metrics.yml @@ -70,7 +70,20 @@ jobs: shell: pwsh run: | Get-Content ./community_metrics.md >> $env:GITHUB_STEP_SUMMARY - + + - name: Close Previous Issue + shell: pwsh + run: | + # Without the --repo switch, the GH CLI will try to look it up from the current clone, which doesn't exist + # because we don't otherwise need checkout. + $issues = gh issue list --repo '${{ github.repository }}' --label 'community metrics' --state open --json number --jq '.[].number' + foreach ($issue in $issues) + { + gh issue close $issue --repo '${{ github.repository }}' --comment 'Closing this issue as newer community metrics are available.' + } + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create Issue # v5.0.0 uses: peter-evans/create-issue-from-file@24452a72d85239eacf1468b0f1982a9f3fec4c94 diff --git a/src/OrchardCore.Build/Dependencies.props b/src/OrchardCore.Build/Dependencies.props index a37de23d636..dc5dbc075d7 100644 --- a/src/OrchardCore.Build/Dependencies.props +++ b/src/OrchardCore.Build/Dependencies.props @@ -30,7 +30,7 @@ - + diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeDriver.cs b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeDriver.cs index 780cdce97e4..c374bab5fe0 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeDriver.cs @@ -33,6 +33,7 @@ public override IDisplayResult Edit(LinkAdminNode treeNode) model.LinkText = treeNode.LinkText; model.LinkUrl = treeNode.LinkUrl; model.IconClass = treeNode.IconClass; + model.Target = treeNode.Target; var permissions = await _adminMenuPermissionService.GetPermissionsAsync(); @@ -56,10 +57,11 @@ public override IDisplayResult Edit(LinkAdminNode treeNode) public override async Task UpdateAsync(LinkAdminNode treeNode, IUpdateModel updater) { var model = new LinkAdminNodeViewModel(); - await updater.TryUpdateModelAsync(model, Prefix, x => x.LinkUrl, x => x.LinkText, x => x.IconClass, x => x.SelectedPermissionNames); + await updater.TryUpdateModelAsync(model, Prefix, x => x.LinkUrl, x => x.LinkText, x => x.Target, x => x.IconClass, x => x.SelectedPermissionNames); treeNode.LinkText = model.LinkText; treeNode.LinkUrl = model.LinkUrl; + treeNode.Target = model.Target; treeNode.IconClass = model.IconClass; var selectedPermissions = (model.SelectedPermissionNames == null ? [] : model.SelectedPermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries)); diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeNavigationBuilder.cs b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeNavigationBuilder.cs index 8bd05d0460f..b8f8d3035e6 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeNavigationBuilder.cs +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeNavigationBuilder.cs @@ -59,6 +59,7 @@ public Task BuildNavigationAsync(MenuItem menuItem, NavigationBuilder builder, I // Add the actual link. itemBuilder.Url(nodeLinkUrl); + itemBuilder.Target(node.Target); itemBuilder.Priority(node.Priority); itemBuilder.Position(node.Position); diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeViewModel.cs b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeViewModel.cs index 21564732a86..b2335ee58e7 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeViewModel.cs @@ -12,6 +12,8 @@ public class LinkAdminNodeViewModel [Required] public string LinkUrl { get; set; } + public string Target { get; set; } + public string IconClass { get; set; } public string SelectedPermissionNames { get; set; } diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/Views/Items/LinkAdminNode.Fields.TreeEdit.cshtml b/src/OrchardCore.Modules/OrchardCore.AdminMenu/Views/Items/LinkAdminNode.Fields.TreeEdit.cshtml index 1b12979744b..dd5c12bac83 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/Views/Items/LinkAdminNode.Fields.TreeEdit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/Views/Items/LinkAdminNode.Fields.TreeEdit.cshtml @@ -24,6 +24,18 @@ @T["The url of the link. A link will be shown only if it or one of their children have a url. The url will be relative to the root of the admin site"] +
+ + + + + + + + + @T["The target attribute of the A tag, see more:"] target +
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Cors/Assets/Admin/cors-admin.js b/src/OrchardCore.Modules/OrchardCore.Cors/Assets/Admin/cors-admin.js index 81621d50124..5801be7031e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Cors/Assets/Admin/cors-admin.js +++ b/src/OrchardCore.Modules/OrchardCore.Cors/Assets/Admin/cors-admin.js @@ -51,7 +51,8 @@ var corsApp = new Vue({ allowedHeaders: [], allowAnyHeader: true, allowCredentials: true, - isDefaultPolicy: false + isDefaultPolicy: false, + exposedHeaders: [] }; }, editPolicy: function (policy) { diff --git a/src/OrchardCore.Modules/OrchardCore.Cors/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Cors/Controllers/AdminController.cs index 629ee0e7d6d..e64c41eea4a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Cors/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Cors/Controllers/AdminController.cs @@ -68,7 +68,8 @@ public async Task Index() AllowAnyOrigin = policySetting.AllowAnyOrigin, AllowedOrigins = policySetting.AllowedOrigins, AllowCredentials = policySetting.AllowCredentials, - IsDefaultPolicy = policySetting.IsDefaultPolicy + IsDefaultPolicy = policySetting.IsDefaultPolicy, + ExposedHeaders = policySetting.ExposedHeaders, }; list.Add(policyViewModel); @@ -113,7 +114,8 @@ public async Task IndexPOST() AllowedHeaders = settingViewModel.AllowedHeaders, AllowedMethods = settingViewModel.AllowedMethods, AllowedOrigins = settingViewModel.AllowedOrigins, - IsDefaultPolicy = settingViewModel.IsDefaultPolicy + IsDefaultPolicy = settingViewModel.IsDefaultPolicy, + ExposedHeaders = settingViewModel.ExposedHeaders, }); if (settingViewModel.AllowAnyOrigin && settingViewModel.AllowCredentials) diff --git a/src/OrchardCore.Modules/OrchardCore.Cors/Services/CorsOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Cors/Services/CorsOptionsConfiguration.cs index 08a8d08685e..47700a3dd81 100644 --- a/src/OrchardCore.Modules/OrchardCore.Cors/Services/CorsOptionsConfiguration.cs +++ b/src/OrchardCore.Modules/OrchardCore.Cors/Services/CorsOptionsConfiguration.cs @@ -69,6 +69,11 @@ public void Configure(CorsOptions options) { configurePolicy.DisallowCredentials(); } + + if (corsPolicy.ExposedHeaders?.Length > 0) + { + configurePolicy.WithExposedHeaders(corsPolicy.ExposedHeaders); + } }); if (corsPolicy.IsDefaultPolicy) diff --git a/src/OrchardCore.Modules/OrchardCore.Cors/Settings/CorsPolicySetting.cs b/src/OrchardCore.Modules/OrchardCore.Cors/Settings/CorsPolicySetting.cs index b26c07e0486..270e2405d4e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Cors/Settings/CorsPolicySetting.cs +++ b/src/OrchardCore.Modules/OrchardCore.Cors/Settings/CorsPolicySetting.cs @@ -19,5 +19,7 @@ public class CorsPolicySetting public bool AllowCredentials { get; set; } public bool IsDefaultPolicy { get; set; } + + public string[] ExposedHeaders { get; set; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Cors/ViewModels/CorsPolicyViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Cors/ViewModels/CorsPolicyViewModel.cs index fcff2331da8..e9022f6ae4f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Cors/ViewModels/CorsPolicyViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Cors/ViewModels/CorsPolicyViewModel.cs @@ -19,5 +19,7 @@ public class CorsPolicyViewModel public bool AllowCredentials { get; set; } public bool IsDefaultPolicy { get; set; } + + public string[] ExposedHeaders { get; set; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Cors/Views/Admin/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Cors/Views/Admin/Index.cshtml index 15b9fc0f122..1d0b46e21b0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Cors/Views/Admin/Index.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Cors/Views/Admin/Index.cshtml @@ -138,6 +138,18 @@
+
+
+
@T["Exposed headers"] + @T["Configure which headers should be exposed."] +
+ +
+ @T["Sets response header 'Access-Control-Expose-Headers'."] + +
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Cors/wwwroot/Scripts/cors-admin.js b/src/OrchardCore.Modules/OrchardCore.Cors/wwwroot/Scripts/cors-admin.js index a149ed0b229..a7093e42d87 100644 --- a/src/OrchardCore.Modules/OrchardCore.Cors/wwwroot/Scripts/cors-admin.js +++ b/src/OrchardCore.Modules/OrchardCore.Cors/wwwroot/Scripts/cors-admin.js @@ -59,7 +59,8 @@ var corsApp = new Vue({ allowedHeaders: [], allowAnyHeader: true, allowCredentials: true, - isDefaultPolicy: false + isDefaultPolicy: false, + exposedHeaders: [] }; }, editPolicy: function editPolicy(policy) { diff --git a/src/OrchardCore.Modules/OrchardCore.Cors/wwwroot/Scripts/cors-admin.min.js b/src/OrchardCore.Modules/OrchardCore.Cors/wwwroot/Scripts/cors-admin.min.js index 2fc2cd87dd1..941ec23e41d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Cors/wwwroot/Scripts/cors-admin.min.js +++ b/src/OrchardCore.Modules/OrchardCore.Cors/wwwroot/Scripts/cors-admin.min.js @@ -1 +1 @@ -var optionsList=Vue.component("options-list",{props:["options","optionType","title","subTitle"],template:"#options-list",data:function(){return{newOption:""}},methods:{addOption:function(i){null!==i&&""!==i&&($.inArray(i.toLowerCase(),this.options.map((function(i){return i.toLowerCase()})))<0&&this.options.push(i))},deleteOption:function(i){this.options.splice($.inArray(i,this.options),1)}}}),policyDetails=Vue.component("policy-details",{components:{optionsList:optionsList},props:["policy"],template:"#policy-details"}),corsApp=new Vue({el:"#corsAdmin",components:{policyDetails:policyDetails,optionsList:optionsList},data:{selectedPolicy:null,policies:null,defaultPolicyName:null},updated:function(){this.searchBox()},methods:{newPolicy:function(){this.selectedPolicy={name:"New policy",allowedOrigins:[],allowAnyOrigin:!0,allowedMethods:[],allowAnyMethod:!0,allowedHeaders:[],allowAnyHeader:!0,allowCredentials:!0,isDefaultPolicy:!1}},editPolicy:function(i){this.selectedPolicy=Object.assign({},i),this.selectedPolicy.originalName=this.selectedPolicy.name},deletePolicy:function(i,e){this.selectedPolicy=null;var t=this.policies.filter((function(e){return e.name===i.name}));t.length>0&&this.policies.splice($.inArray(t[0],this.policies),1),e.stopPropagation(),this.save()},updatePolicy:function(i,e){if(i.isDefaultPolicy&&this.policies.forEach((function(i){return i.isDefaultPolicy=!1})),i.originalName){var t=this.policies.findIndex((function(e){return e.name===i.originalName}));this.policies[t]=i}else this.policies.push(i);this.save(),this.back()},save:function(){document.getElementById("corsSettings").value=JSON.stringify(this.policies),document.getElementById("corsForm").submit()},back:function(){this.selectedPolicy=null},searchBox:function(){var i=$("#search-box");i.keypress((function(i){if(13==i.which){var e=$("#corsAdmin > ul > li:visible");return 1==e.length&&(window.location=e.find(".edit").attr("href")),!1}})),i.keyup((function(e){var t=$(this).val().toLowerCase(),o=$("[data-filter-value]");if(27==e.keyCode||""==t)i.val(""),o.toggle(!0),$("#list-alert").addClass("d-none");else{var s=0;o.each((function(){var i=$(this).data("filter-value").toLowerCase().indexOf(t)>-1;$(this).toggle(i),i&&s++})),0==s?$("#list-alert").removeClass("d-none"):$("#list-alert").addClass("d-none")}}))}}}); +var optionsList=Vue.component("options-list",{props:["options","optionType","title","subTitle"],template:"#options-list",data:function(){return{newOption:""}},methods:{addOption:function(i){null!==i&&""!==i&&($.inArray(i.toLowerCase(),this.options.map((function(i){return i.toLowerCase()})))<0&&this.options.push(i))},deleteOption:function(i){this.options.splice($.inArray(i,this.options),1)}}}),policyDetails=Vue.component("policy-details",{components:{optionsList:optionsList},props:["policy"],template:"#policy-details"}),corsApp=new Vue({el:"#corsAdmin",components:{policyDetails:policyDetails,optionsList:optionsList},data:{selectedPolicy:null,policies:null,defaultPolicyName:null},updated:function(){this.searchBox()},methods:{newPolicy:function(){this.selectedPolicy={name:"New policy",allowedOrigins:[],allowAnyOrigin:!0,allowedMethods:[],allowAnyMethod:!0,allowedHeaders:[],allowAnyHeader:!0,allowCredentials:!0,isDefaultPolicy:!1,exposedHeaders:[]}},editPolicy:function(i){this.selectedPolicy=Object.assign({},i),this.selectedPolicy.originalName=this.selectedPolicy.name},deletePolicy:function(i,e){this.selectedPolicy=null;var t=this.policies.filter((function(e){return e.name===i.name}));t.length>0&&this.policies.splice($.inArray(t[0],this.policies),1),e.stopPropagation(),this.save()},updatePolicy:function(i,e){if(i.isDefaultPolicy&&this.policies.forEach((function(i){return i.isDefaultPolicy=!1})),i.originalName){var t=this.policies.findIndex((function(e){return e.name===i.originalName}));this.policies[t]=i}else this.policies.push(i);this.save(),this.back()},save:function(){document.getElementById("corsSettings").value=JSON.stringify(this.policies),document.getElementById("corsForm").submit()},back:function(){this.selectedPolicy=null},searchBox:function(){var i=$("#search-box");i.keypress((function(i){if(13==i.which){var e=$("#corsAdmin > ul > li:visible");return 1==e.length&&(window.location=e.find(".edit").attr("href")),!1}})),i.keyup((function(e){var t=$(this).val().toLowerCase(),o=$("[data-filter-value]");if(27==e.keyCode||""==t)i.val(""),o.toggle(!0),$("#list-alert").addClass("d-none");else{var s=0;o.each((function(){var i=$(this).data("filter-value").toLowerCase().indexOf(t)>-1;$(this).toggle(i),i&&s++})),0==s?$("#list-alert").removeClass("d-none"):$("#list-alert").addClass("d-none")}}))}}}); diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/HtmlMenuItemPartDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/HtmlMenuItemPartDisplayDriver.cs index 9d52962dbf4..56710193123 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/HtmlMenuItemPartDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/HtmlMenuItemPartDisplayDriver.cs @@ -67,6 +67,7 @@ public override IDisplayResult Edit(HtmlMenuItemPart part) { model.Name = part.ContentItem.DisplayText; model.Url = part.Url; + model.Target = part.Target; model.Html = part.Html; model.MenuItemPart = part; }); @@ -81,6 +82,7 @@ public override async Task UpdateAsync(HtmlMenuItemPart part, IU part.ContentItem.DisplayText = model.Name; part.Html = settings.SanitizeHtml ? _htmlSanitizerService.Sanitize(model.Html) : model.Html; part.Url = model.Url; + part.Target = model.Target; var urlToValidate = part.Url; diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/LinkMenuItemPartDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/LinkMenuItemPartDisplayDriver.cs index 7902719c617..b23aadc5cc3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/LinkMenuItemPartDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/LinkMenuItemPartDisplayDriver.cs @@ -59,6 +59,7 @@ public override IDisplayResult Edit(LinkMenuItemPart part) { model.Name = part.ContentItem.DisplayText; model.Url = part.Url; + model.Target = part.Target; model.MenuItemPart = part; }); } @@ -70,6 +71,7 @@ public override async Task UpdateAsync(LinkMenuItemPart part, IU await updater.TryUpdateModelAsync(model, Prefix); part.Url = model.Url; + part.Target = model.Target; part.ContentItem.DisplayText = model.Name; var urlToValidate = part.Url; diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/GraphQL/HtmlMenuItemQueryObjectType.cs b/src/OrchardCore.Modules/OrchardCore.Menu/GraphQL/HtmlMenuItemQueryObjectType.cs index 84f06d541ac..bd57ae30466 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/GraphQL/HtmlMenuItemQueryObjectType.cs +++ b/src/OrchardCore.Modules/OrchardCore.Menu/GraphQL/HtmlMenuItemQueryObjectType.cs @@ -10,6 +10,7 @@ public HtmlMenuItemQueryObjectType() Name = "HtmlMenuItemPart"; Field(x => x.Url, nullable: true); + Field(x => x.Target, nullable: true); Field(x => x.Html, nullable: true); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Models/HtmlMenuItemPart.cs b/src/OrchardCore.Modules/OrchardCore.Menu/Models/HtmlMenuItemPart.cs index 3065e60b50b..cabd9772546 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Models/HtmlMenuItemPart.cs +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Models/HtmlMenuItemPart.cs @@ -9,6 +9,11 @@ public class HtmlMenuItemPart : ContentPart /// public string Url { get; set; } + /// + /// The target of the link to create. + /// + public string Target { get; set; } + /// /// The raw html to display for this link. /// diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Models/LinkMenuItemPart.cs b/src/OrchardCore.Modules/OrchardCore.Menu/Models/LinkMenuItemPart.cs index 90d042abf62..ad255bc864b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Models/LinkMenuItemPart.cs +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Models/LinkMenuItemPart.cs @@ -8,5 +8,10 @@ public class LinkMenuItemPart : ContentPart /// The url of the link to create. /// public string Url { get; set; } + + /// + /// The target of the link to create. + /// + public string Target { get; set; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/ViewModels/HtmlMenuItemPartEditViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Menu/ViewModels/HtmlMenuItemPartEditViewModel.cs index 37f22c337b7..3a24a2c42f3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/ViewModels/HtmlMenuItemPartEditViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Menu/ViewModels/HtmlMenuItemPartEditViewModel.cs @@ -9,6 +9,8 @@ public class HtmlMenuItemPartEditViewModel public string Url { get; set; } + public string Target { get; set; } + public string Html { get; set; } [BindNever] diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/ViewModels/LinkMenuItemPartEditViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Menu/ViewModels/LinkMenuItemPartEditViewModel.cs index 1848b526e77..301701c5b93 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/ViewModels/LinkMenuItemPartEditViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Menu/ViewModels/LinkMenuItemPartEditViewModel.cs @@ -9,6 +9,8 @@ public class LinkMenuItemPartEditViewModel public string Url { get; set; } + public string Target { get; set; } + [BindNever] public LinkMenuItemPart MenuItemPart { get; set; } } diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Views/HtmlMenuItemPart.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Menu/Views/HtmlMenuItemPart.Edit.cshtml index 500faad52db..6c897b4c8b3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Views/HtmlMenuItemPart.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Views/HtmlMenuItemPart.Edit.cshtml @@ -15,6 +15,20 @@
+
+ +
+ + + + + + + + @T["The target attribute of the A tag, see more:"] target +
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Views/LinkMenuItemPart.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Menu/Views/LinkMenuItemPart.Edit.cshtml index 473a1c549bd..92f6ed71005 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Views/LinkMenuItemPart.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Views/LinkMenuItemPart.Edit.cshtml @@ -14,3 +14,17 @@
+ +
+ +
+ + + + + + + + @T["The target attribute of the A tag, see more:"] target +
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink-HtmlMenuItem.cshtml b/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink-HtmlMenuItem.cshtml index 0efbecc2b4b..17ff3aa36b7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink-HtmlMenuItem.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink-HtmlMenuItem.cshtml @@ -17,6 +17,12 @@ } tag.Attributes["href"] = url; + + if (!string.IsNullOrEmpty(htmlMenuItemPart.Target)) + { + tag.Attributes["target"] = htmlMenuItemPart.Target; + } + tag.InnerHtml.AppendHtml(Html.Raw(htmlMenuItemPart.Html)); } @tag diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink-LinkMenuItem.cshtml b/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink-LinkMenuItem.cshtml index b46b647cf0c..fe75f0004e6 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink-LinkMenuItem.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink-LinkMenuItem.cshtml @@ -16,6 +16,7 @@ url = Url.Content(linkMenuItemPart.Url); } + tag.Attributes["target"] = linkMenuItemPart.Target; tag.Attributes["href"] = url; tag.InnerHtml.Append(contentItem.DisplayText); } diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink.cshtml b/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink.cshtml index b6ad29baba5..924cc3c7294 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Views/MenuItemLink.cshtml @@ -1 +1 @@ -@Model.Text +@Model.Text diff --git a/src/OrchardCore.Modules/OrchardCore.Navigation/Views/NavigationItemLink.cshtml b/src/OrchardCore.Modules/OrchardCore.Navigation/Views/NavigationItemLink.cshtml index 7eb3e808daa..651d04e24b2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Navigation/Views/NavigationItemLink.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Navigation/Views/NavigationItemLink.cshtml @@ -5,4 +5,4 @@ Model.Metadata.Alternates.Add("NavigationItemText_Id__" + Model.Id); } -@await DisplayAsync(Model) +@await DisplayAsync(Model) diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Services/ActivityDisplayManager.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Services/ActivityDisplayManager.cs index 1151ae13bb0..0202d3749c6 100644 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/Services/ActivityDisplayManager.cs +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Services/ActivityDisplayManager.cs @@ -23,10 +23,13 @@ public ActivityDisplayManager( IServiceProvider serviceProvider, IShapeFactory shapeFactory, IEnumerable placementProviders, + IEnumerable> displayDrivers, ILogger> displayManagerLogger, ILayoutAccessor layoutAccessor) { - var drivers = workflowOptions.Value.ActivityDisplayDriverTypes.Select(x => serviceProvider.CreateInstance>(x)); + var drivers = workflowOptions.Value.ActivityDisplayDriverTypes + .Select(x => serviceProvider.CreateInstance>(x)) + .Concat(displayDrivers); _displayManager = new DisplayManager(drivers, shapeFactory, placementProviders, displayManagerLogger, layoutAccessor); } diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/MissingActivity.Fields.Design.cshtml b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/MissingActivity.Fields.Design.cshtml index 5fc56525219..e854f095ba2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/MissingActivity.Fields.Design.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/MissingActivity.Fields.Design.cshtml @@ -1,6 +1,6 @@ -@model ActivityViewModel +@model ShapeViewModel

@T["Missing Activity"]

-@T["{0} is no longer available.", Model.Activity.MissingActivityRecord.Name] +@T["{0} is no longer available.", Model.Value.MissingActivityRecord.Name] diff --git a/src/OrchardCore.Themes/TheAdmin/Views/NavigationItemLink-admin.cshtml b/src/OrchardCore.Themes/TheAdmin/Views/NavigationItemLink-admin.cshtml index 090ef8c99d9..08ac661787d 100644 --- a/src/OrchardCore.Themes/TheAdmin/Views/NavigationItemLink-admin.cshtml +++ b/src/OrchardCore.Themes/TheAdmin/Views/NavigationItemLink-admin.cshtml @@ -22,6 +22,11 @@ tag.Attributes["href"] = Model.Href; } + if (!string.IsNullOrEmpty(Model.Target)) + { + tag.Attributes["target"] = Model.Target; + } + // Extract classes that are not icons from 'Model.Classes'. var notIconClasses = ((IEnumerable)Model.Classes) .Where(c => !c.StartsWith(NavigationConstants.CssClassPrefix, StringComparison.OrdinalIgnoreCase)) diff --git a/src/OrchardCore.Themes/TheAgencyTheme/Views/MenuItemLink-HtmlMenuItem.liquid b/src/OrchardCore.Themes/TheAgencyTheme/Views/MenuItemLink-HtmlMenuItem.liquid index b9f0c3672f4..a05e932748e 100644 --- a/src/OrchardCore.Themes/TheAgencyTheme/Views/MenuItemLink-HtmlMenuItem.liquid +++ b/src/OrchardCore.Themes/TheAgencyTheme/Views/MenuItemLink-HtmlMenuItem.liquid @@ -1,2 +1,2 @@ {% assign link = Model.ContentItem.Content.HtmlMenuItemPart %} -{{ link.Html | raw }} \ No newline at end of file +{{ link.Html | raw }} diff --git a/src/OrchardCore.Themes/TheAgencyTheme/Views/MenuItemLink-LinkMenuItem.liquid b/src/OrchardCore.Themes/TheAgencyTheme/Views/MenuItemLink-LinkMenuItem.liquid index f1ed48b2c57..19f41cfa9d6 100644 --- a/src/OrchardCore.Themes/TheAgencyTheme/Views/MenuItemLink-LinkMenuItem.liquid +++ b/src/OrchardCore.Themes/TheAgencyTheme/Views/MenuItemLink-LinkMenuItem.liquid @@ -1,5 +1,5 @@ {% assign link = Model.ContentItem.Content.LinkMenuItemPart %} - diff --git a/src/OrchardCore.Themes/TheBlogTheme/Views/MenuItemLink-HtmlMenuItem.liquid b/src/OrchardCore.Themes/TheBlogTheme/Views/MenuItemLink-HtmlMenuItem.liquid index 296c2e1b9cb..e69aaf13cdb 100644 --- a/src/OrchardCore.Themes/TheBlogTheme/Views/MenuItemLink-HtmlMenuItem.liquid +++ b/src/OrchardCore.Themes/TheBlogTheme/Views/MenuItemLink-HtmlMenuItem.liquid @@ -3,5 +3,5 @@ {% if Model.HasItems %} {{ link.Html | raw }} {% else %} - {{ link.Html | raw }} -{% endif %} \ No newline at end of file + {{ link.Html | raw }} +{% endif %} diff --git a/src/OrchardCore.Themes/TheBlogTheme/Views/MenuItemLink-LinkMenuItem.liquid b/src/OrchardCore.Themes/TheBlogTheme/Views/MenuItemLink-LinkMenuItem.liquid index c52a9fc051a..53bad68c116 100644 --- a/src/OrchardCore.Themes/TheBlogTheme/Views/MenuItemLink-LinkMenuItem.liquid +++ b/src/OrchardCore.Themes/TheBlogTheme/Views/MenuItemLink-LinkMenuItem.liquid @@ -3,5 +3,5 @@ {% if Model.HasItems %} {{ Model.ContentItem.DisplayText }} {% else %} - {{ Model.ContentItem.DisplayText }} + {{ Model.ContentItem.DisplayText }} {% endif %} diff --git a/src/OrchardCore.Themes/TheTheme/Views/MenuItemLink-HtmlMenuItem.cshtml b/src/OrchardCore.Themes/TheTheme/Views/MenuItemLink-HtmlMenuItem.cshtml index 89499bd7631..c9db308828c 100644 --- a/src/OrchardCore.Themes/TheTheme/Views/MenuItemLink-HtmlMenuItem.cshtml +++ b/src/OrchardCore.Themes/TheTheme/Views/MenuItemLink-HtmlMenuItem.cshtml @@ -18,6 +18,12 @@ } tag.Attributes["href"] = url; + + if (!string.IsNullOrEmpty(htmlMenuItemPart.Target)) + { + tag.Attributes["target"] = htmlMenuItemPart.Target; + } + tag.InnerHtml.AppendHtml(Html.Raw(htmlMenuItemPart.Html)); if (Model.Level == 0 && Model.HasItems) diff --git a/src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/FeatureTypeDiscoveryAttribute.cs b/src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/FeatureTypeDiscoveryAttribute.cs new file mode 100644 index 00000000000..4a975f662fc --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/FeatureTypeDiscoveryAttribute.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace OrchardCore.Environment.Extensions; + +/// +/// Configures how the will assign the type to features. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] +public class FeatureTypeDiscoveryAttribute : Attribute +{ + /// + /// Prevents assignment of a public type to the main feature. + /// + /// + /// If SkipExtension is set to true, the type is only added to the same feature + /// as its startup class. + /// + public bool SkipExtension { get; set; } + + /// + /// Ensures a type is only registered with a single feature. + /// + /// + /// If SingleFeatureOnly is set to true, the TypeFeatureProvider will throw + /// an InvalidOperationException if the type gets assigned to more than one feature. + /// + public bool SingleFeatureOnly { get; set; } + + public static FeatureTypeDiscoveryAttribute GetFeatureTypeDiscoveryForType(Type type) + { + return type.GetCustomAttribute(true) + ?? type.GetInterfaces() + .Select(dmType => dmType.GetCustomAttribute(true)) + .FirstOrDefault(featureTypeDiscoveryAttr => featureTypeDiscoveryAttr != null); + } +} diff --git a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Extensions/FieldTypeExtensions.cs b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Extensions/FieldTypeExtensions.cs deleted file mode 100644 index 6bd87f3ec3d..00000000000 --- a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Extensions/FieldTypeExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using GraphQL.Types; - -namespace OrchardCore.ContentManagement.GraphQL; - -public static class FieldTypeExtensions -{ - public static FieldType WithPartCollapsedMetaData(this FieldType fieldType, bool collapsed = true) - => fieldType.WithMetaData("PartCollapsed", collapsed); - - public static FieldType WithPartNameMetaData(this FieldType fieldType, string partName) - => fieldType.WithMetaData("PartName", partName); - - private static FieldType WithMetaData(this FieldType fieldType, string name, object value) - { - // TODO: Understand if locking is the best solution to https://github.com/OrchardCMS/OrchardCore/issues/15308 - lock (fieldType.Metadata) - { - fieldType.Metadata.TryAdd(name, value); - } - - return fieldType; - } -} diff --git a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Extensions/GraphQLTypeExtensions.cs b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Extensions/GraphQLTypeExtensions.cs new file mode 100644 index 00000000000..0c51e230b3e --- /dev/null +++ b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Extensions/GraphQLTypeExtensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using GraphQL.Types; + +namespace OrchardCore.ContentManagement.GraphQL; + +public static class GraphQLTypeExtensions +{ + public static FieldType WithPartCollapsedMetaData(this FieldType fieldType, bool collapsed = true) + => fieldType.WithMetaData("PartCollapsed", collapsed); + + public static FieldType WithPartNameMetaData(this FieldType fieldType, string partName) + => fieldType.WithMetaData("PartName", partName); + + /// + /// Checks if the field exists in the GraphQL type in a case-insensitive way. + /// + /// + /// + /// This is the same as calling but in a case-insensitive way. OC + /// fields may be added with different casings, and we want to avoid collisions even then. + /// + /// + /// See and its corresponding issues for context. + /// + /// + public static bool HasFieldIgnoreCase(this IComplexGraphType graphType, string fieldName) + => graphType.Fields.Any(field => field.Name.Equals(fieldName, StringComparison.OrdinalIgnoreCase)); + + private static FieldType WithMetaData(this FieldType fieldType, string name, object value) + { + // TODO: Understand if locking is the best solution to https://github.com/OrchardCMS/OrchardCore/issues/15308 + lock (fieldType.Metadata) + { + fieldType.Metadata.TryAdd(name, value); + } + + return fieldType; + } +} diff --git a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Types/DynamicContentTypeBuilder.cs b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Types/DynamicContentTypeBuilder.cs index 3dd4ecb7450..61ce13d7121 100644 --- a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Types/DynamicContentTypeBuilder.cs +++ b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Types/DynamicContentTypeBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using GraphQL.Types; @@ -70,7 +71,8 @@ public void Build(ISchema schema, FieldType contentQuery, ContentTypeDefinition if (fieldType != null) { - if (_contentOptions.ShouldSkip(fieldType.Type, fieldType.Name)) + if (_contentOptions.ShouldSkip(fieldType.Type, fieldType.Name) || + contentItemType.HasFieldIgnoreCase(fieldType.Name)) { continue; } @@ -84,45 +86,63 @@ public void Build(ISchema schema, FieldType contentQuery, ContentTypeDefinition else { // Check if another builder has already added a field for this part. - var existingField = contentItemType.GetField(partName.ToFieldName()); - if (existingField != null) + var partFieldName = partName.ToFieldName(); + var partFieldType = contentItemType.GetField(partFieldName); + + if (partFieldType != null) { - // Add content field types. - foreach (var field in part.PartDefinition.Fields) + // Add dynamic content field types to the static part type. + var partContentItemType = schema.AdditionalTypeInstances + .OfType() + .Where(type => type.GetType() == partFieldType.Type) + .FirstOrDefault(); + + if (partContentItemType != null) { - foreach (var fieldProvider in contentFieldProviders) + foreach (var field in part.PartDefinition.Fields) { - var contentFieldType = fieldProvider.GetField(schema, field, part.Name); - - if (contentFieldType != null && !contentItemType.HasField(contentFieldType.Name)) + foreach (var fieldProvider in contentFieldProviders) { - contentItemType.AddField(contentFieldType); - break; + var contentFieldType = fieldProvider.GetField(schema, field, part.Name); + + if (contentFieldType != null) + { + if (_contentOptions.ShouldSkip(contentFieldType.Type, contentFieldType.Name) || + partContentItemType.HasFieldIgnoreCase(contentFieldType.Name)) + { + continue; + } + + + partContentItemType.AddField(contentFieldType); + break; + } } } } - continue; - } - - if (_dynamicPartFields.TryGetValue(partName, out var fieldType)) - { - contentItemType.AddField(fieldType); } else { - var field = contentItemType - .Field(partName.ToFieldName()) - .Description(S["Represents a {0}.", part.PartDefinition.Name]) - .Resolve(context => - { - var nameToResolve = partName; - var typeToResolve = context.FieldDefinition.ResolvedType.GetType().BaseType.GetGenericArguments().First(); + if (_dynamicPartFields.TryGetValue(partName, out var fieldType)) + { + contentItemType.AddField(fieldType); + } + else + { + var field = contentItemType + .Field(partFieldName) + .Description(S["Represents a {0}.", part.PartDefinition.Name]) + .Resolve(context => + { + var nameToResolve = partName; + var typeToResolve = context.FieldDefinition.ResolvedType.GetType().BaseType.GetGenericArguments().First(); - return context.Source.Get(typeToResolve, nameToResolve); - }); + return context.Source.Get(typeToResolve, nameToResolve); + }); - field.Type(new DynamicPartGraphType(part)); - _dynamicPartFields[partName] = field.FieldType; + field.Type(new DynamicPartGraphType(part)); + _dynamicPartFields[partName] = field.FieldType; + } } } } diff --git a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Types/TypedContentTypeBuilder.cs b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Types/TypedContentTypeBuilder.cs index 58d239c2f40..0db71acc602 100644 --- a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Types/TypedContentTypeBuilder.cs +++ b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Types/TypedContentTypeBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using GraphQL; using GraphQL.Resolvers; @@ -40,9 +41,10 @@ public void Build(ISchema schema, FieldType contentQuery, ContentTypeDefinition } var partName = part.Name; + var partFieldName = partName.ToFieldName(); // Check if another builder has already added a field for this part. - if (contentItemType.HasField(partName)) + if (contentItemType.HasField(partFieldName)) { continue; } @@ -58,7 +60,8 @@ public void Build(ISchema schema, FieldType contentQuery, ContentTypeDefinition { foreach (var field in queryGraphType.Fields) { - if (_contentOptions.ShouldSkip(queryGraphType.GetType(), field.Name)) + if (_contentOptions.ShouldSkip(queryGraphType.GetType(), field.Name) || + contentItemType.HasFieldIgnoreCase(field.Name)) { continue; } @@ -94,11 +97,11 @@ public void Build(ISchema schema, FieldType contentQuery, ContentTypeDefinition { var field = new FieldType { - Name = partName.ToFieldName(), + Name = partFieldName, Type = queryGraphType.GetType(), Description = queryGraphType.Description, }; - contentItemType.Field(partName.ToFieldName(), queryGraphType.GetType()) + contentItemType.Field(partFieldName, queryGraphType.GetType()) .Description(queryGraphType.Description) .Resolve(context => { @@ -135,7 +138,7 @@ public void Build(ISchema schema, FieldType contentQuery, ContentTypeDefinition whereInput.AddField(new FieldType { Type = inputGraphTypeResolved.GetType(), - Name = partName.ToFieldName(), + Name = partFieldName, Description = inputGraphTypeResolved.Description }.WithPartNameMetaData(partName)); } diff --git a/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/IDataMigration.cs b/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/IDataMigration.cs index 57c4be8bf39..ca5516d256c 100644 --- a/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/IDataMigration.cs +++ b/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/IDataMigration.cs @@ -1,3 +1,4 @@ +using OrchardCore.Environment.Extensions; using YesSql.Sql; namespace OrchardCore.Data.Migration @@ -5,6 +6,7 @@ namespace OrchardCore.Data.Migration /// /// Represents a contract for a database migration. /// + [FeatureTypeDiscovery(SingleFeatureOnly = true, SkipExtension = true)] public interface IDataMigration { /// diff --git a/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs b/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs index a9ce8703cfc..70c8b92bcb8 100644 --- a/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs +++ b/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs @@ -276,7 +276,7 @@ private static async Task InvokeMethodAsync(MethodInfo method, IDataMigrati private IDataMigration[] GetDataMigrations(string featureId) { var migrations = _dataMigrations - .Where(dm => _typeFeatureProvider.GetFeaturesForDependency(dm.GetType()).Any(feature => feature.Id == featureId)) + .Where(dm => _typeFeatureProvider.GetFeatureForDependency(dm.GetType()).Id == featureId) .ToArray(); return migrations; diff --git a/src/OrchardCore/OrchardCore.Navigation.Core/MenuItem.cs b/src/OrchardCore/OrchardCore.Navigation.Core/MenuItem.cs index d5d28575d32..28e1f39d783 100644 --- a/src/OrchardCore/OrchardCore.Navigation.Core/MenuItem.cs +++ b/src/OrchardCore/OrchardCore.Navigation.Core/MenuItem.cs @@ -36,6 +36,11 @@ public MenuItem() /// public string Href { get; set; } + /// + /// The html target of the menu item. + /// + public string Target { get; set; } + /// /// The optional url the menu item should link to. /// diff --git a/src/OrchardCore/OrchardCore.Navigation.Core/NavigationHelper.cs b/src/OrchardCore/OrchardCore.Navigation.Core/NavigationHelper.cs index eaed037233d..2228fc7662f 100644 --- a/src/OrchardCore/OrchardCore.Navigation.Core/NavigationHelper.cs +++ b/src/OrchardCore/OrchardCore.Navigation.Core/NavigationHelper.cs @@ -68,6 +68,7 @@ private static async Task BuildMenuItemShapeAsync(dynamic shapeFactory, var menuItemShape = (await shapeFactory.NavigationItem()) .Text(menuItem.Text) .Href(menuItem.Href) + .Target(menuItem.Target) .Url(menuItem.Url) .LinkToFirstChild(menuItem.LinkToFirstChild) .RouteValues(menuItem.RouteValues) @@ -155,7 +156,7 @@ private static dynamic GetHighestPrioritySelectedMenuItem(dynamic parentShape) { dynamic result = null; - var tempStack = new Stack([parentShape]); + var tempStack = new Stack(new dynamic[] { parentShape }); while (tempStack.Count > 0) { diff --git a/src/OrchardCore/OrchardCore.Navigation.Core/NavigationItemBuilder.cs b/src/OrchardCore/OrchardCore.Navigation.Core/NavigationItemBuilder.cs index 22b47ca9ce2..25c2b0f959a 100644 --- a/src/OrchardCore/OrchardCore.Navigation.Core/NavigationItemBuilder.cs +++ b/src/OrchardCore/OrchardCore.Navigation.Core/NavigationItemBuilder.cs @@ -46,6 +46,13 @@ public NavigationItemBuilder Url(string url) return this; } + public NavigationItemBuilder Target(string target) + { + _item.Target = target; + + return this; + } + public NavigationItemBuilder Culture(string culture) { _item.Culture = culture; diff --git a/src/OrchardCore/OrchardCore.Navigation.Core/NavigationManager.cs b/src/OrchardCore/OrchardCore.Navigation.Core/NavigationManager.cs index a810d5767cb..d1ebefa9d7e 100644 --- a/src/OrchardCore/OrchardCore.Navigation.Core/NavigationManager.cs +++ b/src/OrchardCore/OrchardCore.Navigation.Core/NavigationManager.cs @@ -110,6 +110,7 @@ private static void Merge(List items) source.RouteValues = cursor.RouteValues; source.Text = cursor.Text; source.Url = cursor.Url; + source.Target = cursor.Target; source.Permissions.Clear(); source.Permissions.AddRange(cursor.Permissions); @@ -133,6 +134,7 @@ private static void Merge(List items) source.RouteValues = cursor.RouteValues; source.Text = cursor.Text; source.Url = cursor.Url; + source.Target = cursor.Target; source.Permissions.Clear(); source.Permissions.AddRange(cursor.Permissions); diff --git a/src/OrchardCore/OrchardCore.ResourceManagement.Abstractions/ResourceRequiredContext.cs b/src/OrchardCore/OrchardCore.ResourceManagement.Abstractions/ResourceRequiredContext.cs index cb1f809c50a..d9c41af76d6 100644 --- a/src/OrchardCore/OrchardCore.ResourceManagement.Abstractions/ResourceRequiredContext.cs +++ b/src/OrchardCore/OrchardCore.ResourceManagement.Abstractions/ResourceRequiredContext.cs @@ -33,16 +33,13 @@ public void WriteTo(TextWriter writer, string appPath) tagBuilder.WriteTo(writer, NullHtmlEncoder.Default); - if (!string.IsNullOrEmpty(Settings.Condition)) + if (Settings.Condition == NotIE) + { + writer.Write(""); + } + else { - if (Settings.Condition == NotIE) - { - writer.Write(""); - } - else - { - writer.Write(""); - } + writer.Write(""); } } } diff --git a/src/OrchardCore/OrchardCore/Extensions/Features/TypeFeatureProvider.cs b/src/OrchardCore/OrchardCore/Extensions/Features/TypeFeatureProvider.cs index 4acf71f17a1..6bea06f9815 100644 --- a/src/OrchardCore/OrchardCore/Extensions/Features/TypeFeatureProvider.cs +++ b/src/OrchardCore/OrchardCore/Extensions/Features/TypeFeatureProvider.cs @@ -49,7 +49,12 @@ public IEnumerable GetTypesForFeature(IFeatureInfo feature) public void TryAdd(Type type, IFeatureInfo feature) { - _features.AddOrUpdate(type, (key, value) => [value], (key, features, value) => features.Contains(value) ? features : features.Append(value).ToArray(), feature); + var features = _features.AddOrUpdate(type, (key, value) => [value], (key, features, value) => features.Contains(value) ? features : features.Append(value).ToArray(), feature); + + if (features.Count() > 1 && (FeatureTypeDiscoveryAttribute.GetFeatureTypeDiscoveryForType(type)?.SingleFeatureOnly ?? false)) + { + throw new InvalidOperationException($"The type {type} can only be assigned to a single feature. Make sure the type is not added to DI by multiple startup classes."); + } } } } diff --git a/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs b/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs index e5be3cca4e9..348c3d013c3 100644 --- a/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs +++ b/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs @@ -180,9 +180,16 @@ private void PopulateTypeFeatureProvider(ITypeFeatureProvider typeFeatureProvide // Features can have no types. if (typesByFeature.TryGetValue(feature.Id, out var featureTypes)) { + // This is adding the types to the main feature for backward compatibility. + // In the future we could stop doing it as we don't expect this to be necessary, and remove the FeatureTypeDiscovery attribute. foreach (var type in featureTypes) { - typeFeatureProvider.TryAdd(type, feature); + // If the attribute is present then we explicitly ignore the backward compatibility and skip the registration + // in the main feature. + if (!SkipExtensionFeatureRegistration(type)) + { + typeFeatureProvider.TryAdd(type, feature); + } } } } @@ -228,5 +235,10 @@ private static bool IsComponentType(Type type) { return type.IsClass && !type.IsAbstract && type.IsPublic; } + + private static bool SkipExtensionFeatureRegistration(Type type) + { + return FeatureTypeDiscoveryAttribute.GetFeatureTypeDiscoveryForType(type)?.SkipExtension ?? false; + } } } diff --git a/src/docs/community/contributors/README.md b/src/docs/community/contributors/README.md index bc708142556..64393f592fa 100644 --- a/src/docs/community/contributors/README.md +++ b/src/docs/community/contributors/README.md @@ -1,7 +1,7 @@ # Contributors ✨ -[![All Contributors](https://img.shields.io/badge/all_contributors-340-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-341-orange.svg?style=flat-square)](#contributors-) Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key))! @@ -472,6 +472,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d sobotama
sobotama

💻 rpedu
rpedu

💻 tonywoo
tonywoo

💻 + Václav Sezima
Václav Sezima

💻 diff --git a/src/docs/guides/contributing/managing-issues.md b/src/docs/guides/contributing/managing-issues.md index ddee911bf96..cf484444482 100644 --- a/src/docs/guides/contributing/managing-issues.md +++ b/src/docs/guides/contributing/managing-issues.md @@ -22,8 +22,8 @@ Once your issue is triaged, one of the following things will happen: This is what [issue milestones](https://github.com/OrchardCMS/OrchardCore/milestones) mean: -- The next patch version (`1.2.something`, e.g. if the current version is `1.2.3`, then `1.2.4`) indicates the highest priority for serious regressions and other urgent bug fixes that we intend to fix ASAP and publish in a patch release. -- The next minor version (`1.something`, e.g. `1.3` if the current version if `1.2.0`) is for less urgent bug fixes and feature requests that we still think should be addressed in the next planned release. Regressions since the last release found by those from the community who live on the edge and use the [preview releases](../../getting-started/preview-package-source.md) are marked as such too. +- The next patch version (e.g. if the current version is `1.2.3`, then `1.2.4`) indicates the highest priority for serious regressions and other urgent bug fixes that we intend to fix ASAP and publish in a patch release. +- The next minor version (e.g. `1.3` if the current version is `1.2.0`) is for less urgent bug fixes and feature requests that we still think should be addressed in the next planned release. Regressions since the last release found by those from the community who live on the edge and use the [preview releases](../../getting-started/preview-package-source.md) are marked as such too. - Some later minor version (literally `1.x` if the current version is `1.anything`) is for issues that we intend to address eventually, maybe. - The `backlog` milestone is for everything else that we think is a valid request, but we won't work on it any time soon. @@ -32,6 +32,8 @@ This is what [issue milestones](https://github.com/OrchardCMS/OrchardCore/milest Some tips on issue management: - An issue should be about a concrete task, some change in Orchard Core or how we run the project. If it's a question or discussion, then [convert it into a discussion](https://docs.github.com/en/discussions/managing-discussions-for-your-community/moderating-discussions#converting-an-issue-to-a-discussion). +- If you asked the author something and the issue should be closed if they don't reply, add the `needs author feedback` label. This will automatically mark the issue as stale after 15 days, and then close it after another 7. +- You can list all issues to be triaged [here](https://github.com/OrchardCMS/OrchardCore/issues?q=is%3Aopen+is%3Aissue+no%3Amilestone+-label%3A%22needs+author+feedback%22+-label%3A%22community+metrics%22+sort%3Acreated-asc). - Set the milestone according to the above logic, or close the issue with a comment elaborating the reason. - Add further labels for categorization (external contributors can't add labels). E.g.: - Add "good first issue" if the issue looks suitable for a novice contributor. @@ -39,4 +41,3 @@ Some tips on issue management: - Add module/feature set-related labels, like "Media" or "OpenId". - Add "security" for security issues. - Change the issue's title if it contains errors or is unclear/incorrect. -- If you asked the author something and the issue should be closed if they don't reply, add the `needs author feedback` label. This will automatically mark the issue as stale after 15 days, and then close it after another 7. diff --git a/src/docs/releases/2.0.0.md b/src/docs/releases/2.0.0.md index 2ae05224a01..6f971f317d7 100644 --- a/src/docs/releases/2.0.0.md +++ b/src/docs/releases/2.0.0.md @@ -246,6 +246,10 @@ Here are the updated signatures: These adjustments ensure compatibility and adherence to the latest conventions within the `SectionDisplayDriver` class. +The GraphQL schema may change because fields are now always added to the correct part. Previously, additional fields may have been added to the parent content item type directly. + +You may have to adjust your GraphQL queries in that case. + ## Change Logs ### Azure AI Search Module @@ -310,6 +314,10 @@ The `OrchardCore.Email` module has undergone a refactoring process with no break - If you were using the `OrchardCore_Email` configuration key to set up the SMTP provider for all tenants, please update the configuration key to `OrchardCore_Email_Smtp`. The `OrchardCore_Email` key continues to work but will be deprecated in a future release. - A new email provider was added to allow you to send email using Azure Communication Services Email. Click [here](../reference/modules/Email.Azure/README.md) to read more about the ACS module. +### Menu + +`Menus` and `AdminMenus` now support specifying the target property. + ### Admin Menu The admin menu has undergone performance enhancements, and new helpers have been added. When incorporating `INavigationProvider` in your project, you can now utilize `NavigationHelper.IsAdminMenu(name)` instead of the previous approach using `string.Equals(name, "admin", StringComparison.OrdinalIgnoreCase)`. Moreover, when passing route values to an action, it is advised to store them in a constant variable. An illustrative example is provided below. diff --git a/src/docs/requirements.txt b/src/docs/requirements.txt index 5f515b9617d..9e5cd2982e9 100644 --- a/src/docs/requirements.txt +++ b/src/docs/requirements.txt @@ -1,5 +1,5 @@ mkdocs>=1.6.0 -mkdocs-material>=9.5.25 +mkdocs-material>=9.5.26 mkdocs-git-authors-plugin>=0.9.0 mkdocs-git-revision-date-localized-plugin>=1.2.6 pymdown-extensions>=10.8.1 diff --git a/src/docs/resources/libraries/README.md b/src/docs/resources/libraries/README.md index 584e3cdb9b1..512c5cecb23 100644 --- a/src/docs/resources/libraries/README.md +++ b/src/docs/resources/libraries/README.md @@ -22,7 +22,7 @@ The below table lists the different .NET libraries used in Orchard Core: | [Irony.Core](https://github.com/daxnet/irony) | A modified version of the Irony project with .NET Core support | 1.0.7 | [MIT](https://github.com/daxnet/irony/blob/master/LICENSE) | | [Jint](https://github.com/sebastienros/jint) | Javascript Interpreter for .NET. | 3.1.2 | [MIT](https://github.com/sebastienros/jint/blob/dev/LICENSE) | | [JsonPath.NET](https://github.com/gregsdennis/json-everything/tree/master/JsonPath) | A string syntax for selecting and extracting JSON values from within a given JSON value. | 1.1.0 | [MIT](https://github.com/gregsdennis/json-everything/blob/master/LICENSE) | -| [libphonenumber-csharp](https://github.com/twcclegg/libphonenumber-csharp) | .NET library for parsing, formatting, and validating international phone numbers | 8.13.37 | [Apache-2.0](https://github.com/twcclegg/libphonenumber-csharp/blob/main/LICENSE) | +| [libphonenumber-csharp](https://github.com/twcclegg/libphonenumber-csharp) | .NET library for parsing, formatting, and validating international phone numbers | 8.13.38 | [Apache-2.0](https://github.com/twcclegg/libphonenumber-csharp/blob/main/LICENSE) | | [Lorem.NET for netstandard](https://github.com/trichards57/Lorem.Universal.NET) | A .NET library for all things random! | 4.0.80 | [MIT](https://github.com/trichards57/Lorem.Universal.NET/blob/master/license.md) | | [Lucene.Net](https://github.com/apache/lucenenet) | .NET full-text search engine. | 4.8.0-beta00016 | [Apache-2.0](https://github.com/apache/lucenenet/blob/master/LICENSE.txt) | | [MailKit](https://github.com/jstedfast/MailKit) | A cross-platform .NET library for IMAP, POP3, and SMTP. | 4.6.0 | [MIT](https://github.com/jstedfast/MailKit/blob/master/LICENSE) | diff --git a/test/OrchardCore.Benchmarks/RuleBenchmark.cs b/test/OrchardCore.Benchmarks/RuleBenchmark.cs index 22173f0e71b..da716353937 100644 --- a/test/OrchardCore.Benchmarks/RuleBenchmark.cs +++ b/test/OrchardCore.Benchmarks/RuleBenchmark.cs @@ -61,16 +61,10 @@ static RuleBenchmark() [Benchmark(Baseline = true)] #pragma warning disable CA1822 // Mark members as static - public void EvaluateIsHomepageWithJavascript() - { - Convert.ToBoolean(_engine.Evaluate(_scope, "isHomepage()")); - } + public void EvaluateIsHomepageWithJavascript() => _engine.Evaluate(_scope, "isHomepage()"); [Benchmark] - public async Task EvaluateIsHomepageWithRule() - { - await _ruleService.EvaluateAsync(_rule); - } + public async Task EvaluateIsHomepageWithRule() => await _ruleService.EvaluateAsync(_rule); #pragma warning restore CA1822 // Mark members as static } } diff --git a/test/OrchardCore.Tests/Data/ContentItemTests.cs b/test/OrchardCore.Tests/Data/ContentItemTests.cs index f7dd3f35cd5..8d61337943c 100644 --- a/test/OrchardCore.Tests/Data/ContentItemTests.cs +++ b/test/OrchardCore.Tests/Data/ContentItemTests.cs @@ -45,19 +45,6 @@ public void ShouldSerializeParts() Assert.Equal("test", (string)contentItem2.Content.MyPart.Text); } - [Fact] - public void ShouldUpdateContent() - { - var contentItem = CreateContentItemWithMyPart(); - - var json = JConvert.SerializeObject(contentItem); - - var contentItem2 = JConvert.DeserializeObject(json); - - Assert.NotNull(contentItem2.Content.MyPart); - Assert.Equal("test", (string)contentItem2.Content.MyPart.Text); - } - [Fact] public void ShouldAlterPart() { diff --git a/test/OrchardCore.Tests/Email/EmailTests.cs b/test/OrchardCore.Tests/Email/EmailTests.cs index 8f9816876e5..f376c30d50b 100644 --- a/test/OrchardCore.Tests/Email/EmailTests.cs +++ b/test/OrchardCore.Tests/Email/EmailTests.cs @@ -7,7 +7,7 @@ namespace OrchardCore.Tests.Email public class EmailTests { [Fact] - public async Task SendEmail_WithToHeader() + public async Task SendEmail_UsesDefaultSender() { // Arrange var message = new MailMessage @@ -73,20 +73,6 @@ public async Task SendEmail_WithDisplayName() await SendEmailAsync(message, "Your Name "); } - [Fact] - public async Task SendEmail_UsesDefaultSender() - { - var message = new MailMessage - { - To = "info@oc.com", - Subject = "Test", - Body = "Test Message" - }; - var content = await SendEmailAsync(message, "Your Name "); - - Assert.Contains("From: Your Name ", content); - } - [Fact] public async Task SendEmail_UsesCustomSender() {