From 3a6810ec6749f333580d12ca0a6223131dd46682 Mon Sep 17 00:00:00 2001
From: Benedek Farkas <benedek.farkas@lombiq.com>
Date: Wed, 17 Apr 2024 11:52:51 +0200
Subject: [PATCH] 8225: Adding a checkbox to StringFilterForm to control
 whether an empty value should cause the filter to be skipped (#8781)

* Adding a checkbox to StringFilterForm to control whether an empty value should cause the filter to be skipped

* Removing StringOperator.ContainsAnyIfProvided as its now obsolete due to the IgnoreFilterIfValueIsEmpty checkbox setting

* Code styling in StringFilterForm

* Adding missing T-string

* Adding migration step to upgrade from using the ContainsAnyIfProvided operator in StringFilterForm
---
 .../FilterEditors/Forms/StringFilterForm.cs   | 94 +++++++++----------
 .../Modules/Orchard.Projections/Migrations.cs | 29 ++++--
 2 files changed, 65 insertions(+), 58 deletions(-)

diff --git a/src/Orchard.Web/Modules/Orchard.Projections/FilterEditors/Forms/StringFilterForm.cs b/src/Orchard.Web/Modules/Orchard.Projections/FilterEditors/Forms/StringFilterForm.cs
index 59656c4b6..4b88d245c 100644
--- a/src/Orchard.Web/Modules/Orchard.Projections/FilterEditors/Forms/StringFilterForm.cs
+++ b/src/Orchard.Web/Modules/Orchard.Projections/FilterEditors/Forms/StringFilterForm.cs
@@ -7,7 +7,6 @@ using Orchard.Forms.Services;
 using Orchard.Localization;
 
 namespace Orchard.Projections.FilterEditors.Forms {
-
     public class StringFilterForm : IFormProvider {
         public const string FormName = "StringFilter";
 
@@ -20,44 +19,44 @@ namespace Orchard.Projections.FilterEditors.Forms {
         }
 
         public void Describe(DescribeContext context) {
-            Func<IShapeFactory, object> form =
-                shape => {
+            object form(IShapeFactory shape) {
+                var f = Shape.Form(
+                    Id: "StringFilter",
+                    _Operator: Shape.SelectList(
+                        Id: "operator", Name: "Operator",
+                        Title: T("Operator"),
+                        Size: 1,
+                        Multiple: false
+                    ),
+                    _Value: Shape.TextBox(
+                        Id: "value", Name: "Value",
+                        Title: T("Value"),
+                        Classes: new[] { "text medium", "tokenized" },
+                        Description: T("Enter the value the string should be.")
+                    ),
+                    _IgnoreIfEmptyValue: Shape.Checkbox(
+                        Id: "IgnoreFilterIfValueIsEmpty",
+                        Name: "IgnoreFilterIfValueIsEmpty",
+                        Title: T("Ignore filter if value is empty"),
+                        Description: T("When enabled, the filter will not be applied if the provided value is or evaluates to empty."),
+                        Value: "true"
+                    ));
 
-                    var f = Shape.Form(
-                        Id: "StringFilter",
-                        _Operator: Shape.SelectList(
-                            Id: "operator", Name: "Operator",
-                            Title: T("Operator"),
-                            Size: 1,
-                            Multiple: false
-                        ),
-                        _Value: Shape.TextBox(
-                            Id: "value", Name: "Value",
-                            Title: T("Value"),
-                            Classes: new[] { "text medium", "tokenized" },
-                            Description: T("Enter the value the string should be.")
-                            )
-                        );
+                f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Equals), Text = T("Is equal to").Text });
+                f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotEquals), Text = T("Is not equal to").Text });
+                f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Contains), Text = T("Contains").Text });
+                f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.ContainsAny), Text = T("Contains any word").Text });
+                f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.ContainsAll), Text = T("Contains all words").Text });
+                f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Starts), Text = T("Starts with").Text });
+                f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotStarts), Text = T("Does not start with").Text });
+                f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Ends), Text = T("Ends with").Text });
+                f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotEnds), Text = T("Does not end with").Text });
+                f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotContains), Text = T("Does not contain").Text });
 
-                    f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Equals), Text = T("Is equal to").Text });
-                    f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotEquals), Text = T("Is not equal to").Text });
-                    f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Contains), Text = T("Contains").Text });
-                    f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.ContainsAny), Text = T("Contains any word").Text });
-                    f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.ContainsAll), Text = T("Contains all words").Text });
-                    f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Starts), Text = T("Starts with").Text });
-                    f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotStarts), Text = T("Does not start with").Text });
-                    f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Ends), Text = T("Ends with").Text });
-                    f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotEnds), Text = T("Does not end with").Text });
-                    f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotContains), Text = T("Does not contain").Text });
-                    f._Operator.Add(new SelectListItem {
-                        Value = Convert.ToString(StringOperator.ContainsAnyIfProvided),
-                        Text = T("Contains any word (if any is provided)").Text
-                    });
+                return f;
+            }
 
-                    return f;
-                };
-
-            context.Form(FormName, form);
+            context.Form(FormName, (Func<IShapeFactory, object>)form);
 
         }
 
@@ -65,6 +64,11 @@ namespace Orchard.Projections.FilterEditors.Forms {
             var op = (StringOperator)Enum.Parse(typeof(StringOperator), Convert.ToString(formState.Operator));
             object value = Convert.ToString(formState.Value);
 
+            if (bool.TryParse(formState.IgnoreFilterIfValueIsEmpty?.ToString() ?? "", out bool ignoreIfEmpty)
+                && ignoreIfEmpty
+                && string.IsNullOrWhiteSpace(value as string))
+                return (ex) => { };
+
             switch (op) {
                 case StringOperator.Equals:
                     return x => x.Eq(property, value);
@@ -92,14 +96,6 @@ namespace Orchard.Projections.FilterEditors.Forms {
                     return y => y.Not(x => x.Like(property, Convert.ToString(value), HqlMatchMode.End));
                 case StringOperator.NotContains:
                     return y => y.Not(x => x.Like(property, Convert.ToString(value), HqlMatchMode.Anywhere));
-                case StringOperator.ContainsAnyIfProvided:
-                    if (string.IsNullOrWhiteSpace((string)value))
-                        return x => x.IsNotEmpty("Id"); // basically, return every possible ContentItem
-                    var values3 = Convert.ToString(value)
-                        .Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
-                    var predicates3 = values3.Skip(1)
-                        .Select<string, Action<IHqlExpressionFactory>>(x => y => y.Like(property, x, HqlMatchMode.Anywhere)).ToArray();
-                    return x => x.Disjunction(y => y.Like(property, values3[0], HqlMatchMode.Anywhere), predicates3);
                 default:
                     throw new ArgumentOutOfRangeException();
             }
@@ -130,11 +126,6 @@ namespace Orchard.Projections.FilterEditors.Forms {
                     return T("{0} does not end with '{1}'", fieldName, value);
                 case StringOperator.NotContains:
                     return T("{0} does not contain '{1}'", fieldName, value);
-                case StringOperator.ContainsAnyIfProvided:
-                    return T("{0} contains any of '{1}' (or '{1}' is empty)",
-                        fieldName,
-                        new LocalizedString(string.Join("', '",
-                            value.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))));
                 default:
                     throw new ArgumentOutOfRangeException();
             }
@@ -151,7 +142,6 @@ namespace Orchard.Projections.FilterEditors.Forms {
         NotStarts,
         Ends,
         NotEnds,
-        NotContains,
-        ContainsAnyIfProvided
+        NotContains
     }
-}
\ No newline at end of file
+}
diff --git a/src/Orchard.Web/Modules/Orchard.Projections/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Projections/Migrations.cs
index 2c751846a..cf0b6f57e 100644
--- a/src/Orchard.Web/Modules/Orchard.Projections/Migrations.cs
+++ b/src/Orchard.Web/Modules/Orchard.Projections/Migrations.cs
@@ -1,7 +1,6 @@
 using System;
 using System.Data;
 using System.Linq;
-using Orchard.ContentManagement;
 using Orchard.ContentManagement.MetaData;
 using Orchard.Core.Common.Models;
 using Orchard.Core.Contents.Extensions;
@@ -15,13 +14,16 @@ namespace Orchard.Projections {
     public class Migrations : DataMigrationImpl {
         private readonly IRepository<MemberBindingRecord> _memberBindingRepository;
         private readonly IRepository<LayoutRecord> _layoutRepository;
-
+        private readonly IRepository<FilterRecord> _filterRepository;
 
         public Migrations(
             IRepository<MemberBindingRecord> memberBindingRepository,
-            IRepository<LayoutRecord> layoutRepository) {
+            IRepository<LayoutRecord> layoutRepository,
+            IRepository<FilterRecord> filterRepository) {
             _memberBindingRepository = memberBindingRepository;
             _layoutRepository = layoutRepository;
+            _filterRepository = filterRepository;
+
             T = NullLocalizer.Instance;
         }
 
@@ -359,15 +361,30 @@ namespace Orchard.Projections {
         }
 
         public int UpdateFrom5() {
-            SchemaBuilder.AlterTable("LayoutRecord", t => t.AddColumn<string>("GUIdentifier",
-                     column => column.WithLength(68)));
+            SchemaBuilder.AlterTable("LayoutRecord", t => t
+                .AddColumn<string>("GUIdentifier", column => column.WithLength(68)));
 
             var layoutRecords = _layoutRepository.Table.Where(l => l.GUIdentifier == null || l.GUIdentifier == "").ToList();
             foreach (var layout in layoutRecords) {
-               layout.GUIdentifier = Guid.NewGuid().ToString();
+                layout.GUIdentifier = Guid.NewGuid().ToString();
             }
 
             return 6;
         }
+
+        public int UpdateFrom6() {
+            // This casts a somewhat wide net, but filters can't be queried by the form they are using and different
+            // types of filters can (and do) use StringFilterForm. However, the "Operator" parameter's value being
+            // "ContainsAnyIfProvided" is very specific.
+            var formStateToReplace = "<Operator>ContainsAnyIfProvided</Operator>";
+            var filterRecordsToUpdate = _filterRepository.Table.Where(f => f.State.Contains(formStateToReplace)).ToList();
+            foreach (var filter in filterRecordsToUpdate) {
+                filter.State = filter.State.Replace(
+                    formStateToReplace,
+                    "<Operator>ContainsAny</Operator><IgnoreFilterIfValueIsEmpty>true</IgnoreFilterIfValueIsEmpty>");
+            }
+
+            return 7;
+        }
     }
 }
\ No newline at end of file