From 174db6571425972a40ac69e744d10b10334f65e4 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 17 Jan 2019 13:33:44 +0100 Subject: [PATCH] 3908 - Query build returns v8 valid statement - and examples of returned items --- .../src/common/services/editor.service.js | 37 +++--- .../querybuilder/querybuilder.html | 70 +++++----- .../Editors/TemplateQueryController.cs | 121 ++++++------------ .../Models/TemplateQuery/QueryCondition.cs | 104 +++++++-------- .../Models/TemplateQuery/QueryResultModel.cs | 2 +- src/Umbraco.Web/PublishedContentExtensions.cs | 65 +--------- 6 files changed, 138 insertions(+), 261 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index 721efe251c..2427b6249c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -4,10 +4,10 @@ * * @description * Added in Umbraco 8.0. Application-wide service for handling infinite editing. - * * - * - * + * + * + *

Open a build-in infinite editor (media picker)

Markup example

@@ -97,7 +97,7 @@ When building a custom infinite editor view you can use the same components as a
                 hide-icon="true"
                 hide-description="true">
             
-        
+
             
                 
                     
@@ -166,7 +166,7 @@ When building a custom infinite editor view you can use the same components as a
 
         let editorsKeyboardShorcuts = [];
         var editors = [];
-        
+
         /**
          * @ngdoc method
          * @name umbraco.services.editorService#getEditors
@@ -220,7 +220,7 @@ When building a custom infinite editor view you can use the same components as a
                 editors: editors,
                 editor: editor
             };
-            
+
             eventsService.emit("appState.editors.open", args);
         }
 
@@ -245,7 +245,7 @@ When building a custom infinite editor view you can use the same components as a
 
             // emit event to let components know an editor has been removed
             eventsService.emit("appState.editors.close", args);
-            
+
             // delay required to map the properties to the correct editor due
             // to another delay in the closing animation of the editor
             $timeout(function() {
@@ -287,7 +287,7 @@ When building a custom infinite editor view you can use the same components as a
          * @param {Boolean} editor.create Create new content item
          * @param {Function} editor.submit Callback function when the publish and close button is clicked. Returns the editor model object
          * @param {Function} editor.close Callback function when the close button is clicked.
-         * 
+         *
          * @returns {Object} editor object
          */
         function contentEditor(editor) {
@@ -302,12 +302,12 @@ When building a custom infinite editor view you can use the same components as a
          *
          * @description
          * Opens a content picker in infinite editing, the submit callback returns an array of selected items
-         * 
+         *
          * @param {Object} editor rendering options
          * @param {Boolean} editor.multiPicker Pick one or multiple items
          * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object
          * @param {Function} editor.close Callback function when the close button is clicked.
-         * 
+         *
          * @returns {Object} editor object
          */
         function contentPicker(editor) {
@@ -523,7 +523,6 @@ When building a custom infinite editor view you can use the same components as a
          */
         function queryBuilder(editor) {
             editor.view = "views/common/infiniteeditors/querybuilder/querybuilder.html";
-            editor.size = "small";
             open(editor);
         }
 
@@ -699,7 +698,7 @@ When building a custom infinite editor view you can use the same components as a
          *
          * @description
          * Opens the section picker in infinite editing, the submit callback returns an array of the selected items
-         * 
+         *
          * @param {Object} editor rendering options
          * @param {Array} editor.availableItems Array of available items.
          * @param {Array} editor.selectedItems Array of selected items. When passed in the selected items will be filtered from the available items.
@@ -721,7 +720,7 @@ When building a custom infinite editor view you can use the same components as a
          *
          * @description
          * Opens a macro picker in infinite editing, the submit callback returns an array of the selected items
-         * 
+         *
          * @param {Callback} editor.submit Submits the editor.
          * @param {Callback} editor.close Closes the editor.
          * @returns {Object} editor object
@@ -739,7 +738,7 @@ When building a custom infinite editor view you can use the same components as a
          *
          * @description
          * Opens a member group picker in infinite editing.
-         * 
+         *
          * @param {Object} editor rendering options
          * @param {Object} editor.multiPicker Pick one or multiple items.
          * @param {Callback} editor.submit Submits the editor.
@@ -753,22 +752,22 @@ When building a custom infinite editor view you can use the same components as a
         }
 
         ///////////////////////
-        
+
         /**
          * @ngdoc method
          * @name umbraco.services.editorService#storeKeyboardShortcuts
          * @methodOf umbraco.services.editorService
          *
          * @description
-         * Internal method to keep track of keyboard shortcuts registered 
+         * Internal method to keep track of keyboard shortcuts registered
          * to each editor so they can be rebound when an editor closes
-         * 
+         *
          */
         function unbindKeyboardShortcuts() {
             const shortcuts = angular.copy(keyboardService.keyboardEvent);
             editorsKeyboardShorcuts.push(shortcuts);
 
-            // unbind the current shortcuts because we only want to 
+            // unbind the current shortcuts because we only want to
             // shortcuts from the newly opened editor working
             for (let [key, value] of Object.entries(shortcuts)) {
                 keyboardService.unbind(key);
@@ -782,7 +781,7 @@ When building a custom infinite editor view you can use the same components as a
          *
          * @description
          * Internal method to rebind keyboard shortcuts for the editor in focus
-         * 
+         *
          */
         function rebindKeyboardShortcuts() {
             // find the shortcuts from the previous editor
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html
index 725871337d..2d2263c19f 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html
+++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html
@@ -9,7 +9,7 @@
             hide-icon="true"
             hide-description="true">
 		
-		
+
 		
 
 			
@@ -19,14 +19,13 @@
 
 						
- + I want - +
- +
- + from + label="{{vm.query.source.name}}"> - +
- +
- + where and - +
@@ -83,14 +79,13 @@ - +
- +
- + - + - + - - + - + - + - +
- +
- + order by - +
- +
- +
- -
{{model.result.resultCount}} items, returned in {{model.result.executionTime}} ms
- + +
{{model.result.resultCount}} items, returned in N/A ms
+ - +
{{model.result.queryExpression}}
- +
@@ -203,7 +197,7 @@ label-key="general_submit" action="vm.submit(model)"> - + diff --git a/src/Umbraco.Web/Editors/TemplateQueryController.cs b/src/Umbraco.Web/Editors/TemplateQueryController.cs index 00e124cb29..58470e1f3e 100644 --- a/src/Umbraco.Web/Editors/TemplateQueryController.cs +++ b/src/Umbraco.Web/Editors/TemplateQueryController.cs @@ -59,24 +59,19 @@ namespace Umbraco.Web.Editors public QueryResultModel PostTemplateQuery(QueryModel model) { - + var queryResult = new QueryResultModel(); var sb = new StringBuilder(); - var indention = Environment.NewLine + "\t\t\t\t\t\t"; + var indention = Environment.NewLine + " "; sb.Append("Model.Root()"); - //fixme: This timer thing is not correct, it's definitely not timing the resulting query, the timer really isn't important and might as well be removed - var timer = new Stopwatch(); - - timer.Start(); - var currentPage = Umbraco.ContentAtRoot().FirstOrDefault(); - timer.Stop(); var pointerNode = currentPage; + // adjust the "FROM" if (model != null && model.Source != null && model.Source.Id > 0) { @@ -84,26 +79,22 @@ namespace Umbraco.Web.Editors if (targetNode != null) { - var aliases = this.GetChildContentTypeAliases(targetNode, currentPage).Reverse(); + var path = this.GetPathOfContents(targetNode, currentPage).Reverse(); - foreach (var contentTypeAlias in aliases) + foreach (var content in path) { - timer.Start(); - - pointerNode = pointerNode.FirstChild(x => x.ContentType.Alias == contentTypeAlias); + pointerNode = pointerNode.FirstChild(x => x.Key == content.Key); if (pointerNode == null) break; - - timer.Stop(); - - sb.AppendFormat(".FirstChild(\"{0}\")", contentTypeAlias); + sb.Append(indention); + sb.AppendFormat(".FirstChild(Guid.Parse(\"{0}\"))", content.Key); } if (pointerNode == null || pointerNode.Id != model.Source.Id) { - // we did not find the path + // we did not find the path, This will happen if the chosen source is not a descendants sb.Clear(); - sb.AppendFormat("Umbraco.Content({0})", model.Source.Id); + sb.AppendFormat("Umbraco.Content(Guid.Parse(\"{0}\"))",targetNode.Key); pointerNode = targetNode; } } @@ -111,31 +102,21 @@ namespace Umbraco.Web.Editors // TYPE to return if filtered by type IEnumerable contents; + sb.Append(indention); if (model != null && model.ContentType != null && string.IsNullOrEmpty(model.ContentType.Alias) == false) { - timer.Start(); - contents = pointerNode.Children.OfTypes(new[] { model.ContentType.Alias }); + contents = pointerNode.Children(model.ContentType.Alias); - timer.Stop(); - // TODO change to .Children({0}) sb.AppendFormat(".Children(\"{0}\")", model.ContentType.Alias); } else { - timer.Start(); - contents = pointerNode.Children; - timer.Stop(); + contents = pointerNode.Children(); sb.Append(".Children()"); } - //setup 2 clauses, 1 for returning, 1 for testing - var clause = string.Empty; - var tokenizedClause = string.Empty; - // WHERE - var token = 0; - if (model != null) { model.Filters = model.Filters.Where(x => x.ConstraintValue != null); @@ -145,56 +126,25 @@ namespace Umbraco.Web.Editors if (string.IsNullOrEmpty(condition.ConstraintValue)) continue; //x is passed in as the parameter alias for the linq where statement clause - var operation = condition.BuildCondition("x"); - var tokenizedOperation = condition.BuildTokenizedCondition(token); + var operation = condition.BuildCondition("x", contents, Properties); - clause = string.IsNullOrEmpty(clause) ? operation : string.Concat(new[] { clause, " && ", operation }); - tokenizedClause = string.IsNullOrEmpty(tokenizedClause) ? tokenizedOperation : string.Concat(new[] { tokenizedClause, " && ", tokenizedOperation }); - - token++; - } - - if (string.IsNullOrEmpty(clause) == false) - { - timer.Start(); - - //trial-run the tokenized clause to time the execution - //for review - this uses a tonized query rather then the normal linq query. - // fixme - that cannot work anymore now that we have killed dynamic support - //contents = contents.AsQueryable().Where(clause, model.Filters.Select(this.GetConstraintValue).ToArray()); - throw new NotImplementedException(); - - contents = contents.Where(x => x.IsVisible()); - - timer.Stop(); - - //the query to output to the editor - sb.Append(indention); - sb.Append(".Where(x => x.IsVisible())"); + contents = contents.Where(operation.Compile()); sb.Append(indention); - sb.AppendFormat(".Where(x => {0})", clause); + sb.AppendFormat(".Where({0})", operation); } - else - { - timer.Start(); - contents = contents.Where(x => x.IsVisible()); - timer.Stop(); + contents = contents.Where(x => x.IsVisible()); + + //the query to output to the editor + sb.Append(indention); + sb.Append(".Where(x => x.IsVisible())"); - sb.Append(indention); - sb.Append(".Where(x => x.IsVisible())"); - } if (model.Sort != null && string.IsNullOrEmpty(model.Sort.Property.Alias) == false) { - timer.Start(); - contents = this.SortByDefaultPropertyValue(contents, model.Sort); - - timer.Stop(); - sb.Append(indention); if (model.Sort.Direction == "ascending") @@ -209,26 +159,29 @@ namespace Umbraco.Web.Editors if (model.Take > 0) { - timer.Start(); - contents = contents.Take(model.Take); - - timer.Stop(); - sb.Append(indention); sb.AppendFormat(".Take({0})", model.Take); } } + + // Timing should be fairly correct, due to the fact that all the linq statements are yield returned. + Stopwatch timer = new Stopwatch(); + timer.Start(); + var results = contents.ToArray(); + timer.Stop(); + queryResult.QueryExpression = sb.ToString(); + queryResult.ResultCount = results.Count(); queryResult.ExecutionTime = timer.ElapsedMilliseconds; - queryResult.ResultCount = contents.Count(); - queryResult.SampleResults = contents.Take(20).Select(x => new TemplateQueryResult() + queryResult.SampleResults = results.Take(20).Select(x => new TemplateQueryResult() { Icon = "icon-file", Name = x.Name }); + return queryResult; } @@ -273,20 +226,20 @@ namespace Umbraco.Web.Editors } } - private IEnumerable GetChildContentTypeAliases(IPublishedContent targetNode, IPublishedContent current) + private IEnumerable GetPathOfContents(IPublishedContent targetNode, IPublishedContent current) { - var aliases = new List(); + var contents = new List(); - if (targetNode == null || targetNode.Id == current.Id) return aliases; + if (targetNode == null || targetNode.Id == current.Id) return contents; if (targetNode.Id != current.Id) { - aliases.Add(targetNode.ContentType.Alias); + contents.Add(targetNode); } - aliases.AddRange(this.GetChildContentTypeAliases(targetNode.Parent, current)); + contents.AddRange(this.GetPathOfContents(targetNode.Parent, current)); - return aliases; + return contents; } /// diff --git a/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs b/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs index 9c5f2c80c0..009b568556 100644 --- a/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs +++ b/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs @@ -1,97 +1,81 @@ -namespace Umbraco.Web.Models.TemplateQuery +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.Models.TemplateQuery { public class QueryCondition { - public PropertyModel Property { get; set; } public OperatorTerm Term { get; set; } public string ConstraintValue { get; set; } } - internal static class QueryConditionExtensions { + private static Lazy StringContainsMethodInfo => + new Lazy(() => typeof(string).GetMethod("Contains", new[] {typeof(string)})); - public static string BuildTokenizedCondition(this QueryCondition condition, int token) + public static Expression> BuildCondition(this QueryCondition condition, + string parameterAlias, IEnumerable contents, IEnumerable properties) { - return condition.BuildConditionString(string.Empty, token); - } - - public static string BuildCondition(this QueryCondition condition, string parameterAlias) - { - return condition.BuildConditionString(parameterAlias + "."); - } - - private static string BuildConditionString(this QueryCondition condition, string prefix, int token = -1) - { - - - - var operand = string.Empty; - var value = string.Empty; - var constraintValue = string.Empty; - - - //if a token is used, use a token placeholder, otherwise, use the actual value - if(token >= 0){ - constraintValue = string.Format("@{0}", token); - }else { - - //modify the format of the constraint value - switch (condition.Property.Type) - { - case "string": - constraintValue = string.Format("\"{0}\"", condition.ConstraintValue); - break; - case "datetime": - constraintValue = string.Format("DateTime.Parse(\"{0}\")", condition.ConstraintValue); - break; - default: - constraintValue = condition.ConstraintValue; - break; - } - + object constraintValue; + switch (condition.Property.Type) + { + case "string": + constraintValue = condition.ConstraintValue; + break; + case "datetime": + constraintValue = DateTime.Parse(condition.ConstraintValue); + break; + default: + constraintValue = Convert.ChangeType(condition.ConstraintValue, typeof(int)); + break; } + var parameterExpression = Expression.Parameter(typeof(IPublishedContent), parameterAlias); + var propertyExpression = Expression.Property(parameterExpression, condition.Property.Alias); + + var valueExpression = Expression.Constant(constraintValue); + Expression bodyExpression; switch (condition.Term.Operator) { - case Operator.Equals: - operand = " == "; - break; case Operator.NotEquals: - operand = " != "; + bodyExpression = Expression.NotEqual(propertyExpression, valueExpression); break; case Operator.GreaterThan: - operand = " > "; + bodyExpression = Expression.GreaterThan(propertyExpression, valueExpression); break; case Operator.GreaterThanEqualTo: - operand = " >= "; + bodyExpression = Expression.GreaterThanOrEqual(propertyExpression, valueExpression); break; case Operator.LessThan: - operand = " < "; + bodyExpression = Expression.LessThan(propertyExpression, valueExpression); break; case Operator.LessThanEqualTo: - operand = " <= "; + bodyExpression = Expression.LessThanOrEqual(propertyExpression, valueExpression); break; case Operator.Contains: - value = string.Format("{0}{1}.Contains({2})", prefix, condition.Property.Alias, constraintValue); + bodyExpression = Expression.Call(propertyExpression, StringContainsMethodInfo.Value, + valueExpression); break; case Operator.NotContains: - value = string.Format("!{0}{1}.Contains({2})", prefix, condition.Property.Alias, constraintValue); + var tempExpression = Expression.Call(propertyExpression, StringContainsMethodInfo.Value, + valueExpression); + bodyExpression = Expression.Equal(tempExpression, Expression.Constant(false)); break; - default : - operand = " == "; + default: + case Operator.Equals: + bodyExpression = Expression.Equal(propertyExpression, valueExpression); break; } + var predicate = + Expression.Lambda>(bodyExpression.Reduce(), parameterExpression); - if (string.IsNullOrEmpty(value) == false) - return value; - - - - return string.Format("{0}{1}{2}{3}", prefix, condition.Property.Alias, operand, constraintValue); + return predicate; } - } } diff --git a/src/Umbraco.Web/Models/TemplateQuery/QueryResultModel.cs b/src/Umbraco.Web/Models/TemplateQuery/QueryResultModel.cs index a1bda3c4ae..46556dc75e 100644 --- a/src/Umbraco.Web/Models/TemplateQuery/QueryResultModel.cs +++ b/src/Umbraco.Web/Models/TemplateQuery/QueryResultModel.cs @@ -9,7 +9,7 @@ namespace Umbraco.Web.Models.TemplateQuery public string QueryExpression { get; set; } public IEnumerable SampleResults { get; set; } public int ResultCount { get; set; } - public double ExecutionTime { get; set; } + public long ExecutionTime { get; set; } public int Take { get; set; } } } diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index d4494c5f91..c4be7869cd 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -942,65 +942,6 @@ namespace Umbraco.Web #endregion - #region Axes: following-sibling, preceding-sibling, following, preceding + pseudo-axes up, down, next, previous - - // up pseudo-axe ~ ancestors - // bogus, kept for backward compatibility but we should get rid of it - // better use ancestors - - public static IPublishedContent Up(this IPublishedContent content) - { - return content.Parent; - } - - public static IPublishedContent Up(this IPublishedContent content, int number) - { - if (number < 0) - throw new ArgumentOutOfRangeException(nameof(number), "Must be greater than, or equal to, zero."); - return number == 0 ? content : content.EnumerateAncestors(false).Skip(number).FirstOrDefault(); - } - - public static IPublishedContent Up(this IPublishedContent content, string contentTypeAlias) - { - return string.IsNullOrEmpty(contentTypeAlias) - ? content.Parent - : content.Ancestor(contentTypeAlias); - } - - // down pseudo-axe ~ children (not descendants) - // bogus, kept for backward compatibility but we should get rid of it - // better use descendants - - public static IPublishedContent Down(this IPublishedContent content) - { - return content.Children.FirstOrDefault(); - } - - public static IPublishedContent Down(this IPublishedContent content, int number) - { - if (number < 0) - throw new ArgumentOutOfRangeException(nameof(number), "Must be greater than, or equal to, zero."); - if (number == 0) return content; - - content = content.Children.FirstOrDefault(); - while (content != null && --number > 0) - content = content.Children.FirstOrDefault(); - - return content; - } - - public static IPublishedContent Down(this IPublishedContent content, string contentTypeAlias) - { - if (string.IsNullOrEmpty(contentTypeAlias)) - return content.Children.FirstOrDefault(); - - // note: this is what legacy did, but with a broken Descendant - // so fixing Descendant will change how it works... - return content.Descendant(contentTypeAlias); - } - - #endregion - #region Axes: parent // Parent is native @@ -1034,6 +975,7 @@ namespace Umbraco.Web public static IEnumerable Children(this IPublishedContent content) { if (content == null) throw new ArgumentNullException(nameof(content)); + return content.Children; } @@ -1098,6 +1040,11 @@ namespace Umbraco.Web return content.Children(predicate).FirstOrDefault(); } + public static IPublishedContent FirstChild(this IPublishedContent content, Guid uniqueId) + { + return content.Children(x=>x.Key == uniqueId).FirstOrDefault(); + } + public static T FirstChild(this IPublishedContent content) where T : class, IPublishedContent {