Quellcode durchsuchen

Added IProblem<> to replace IIssues interface
Added Mappings to AutoEntityUnionTable
Added Format operator to ComplexColumns

frogsoftware vor 8 Monaten
Ursprung
Commit
b842184c6b

+ 8 - 2
InABox.Core/Aggregate.cs

@@ -103,7 +103,7 @@ namespace InABox.Core
             Field = CoreUtils.GetFullPropertyName(expression, ".");
         }
 
-        internal ComplexFormulaFieldNode(string field)
+        public ComplexFormulaFieldNode(string field)
         {
             Field = field;
         }
@@ -810,8 +810,14 @@ namespace InABox.Core
         /// Take the maximum of all values.
         /// </summary>
         Maximum,
+        
         [Obsolete]
-        Constant
+        Constant,
+        
+        /// <summary>
+        /// Formats all the operands using the first string
+        /// </summary>
+        Format
     }
 
     public interface IFormula<TType, TProp>

+ 47 - 0
InABox.Core/AutoEntity/AutoEntityUnionGenerator.cs

@@ -29,11 +29,30 @@ namespace InABox.Core
         }
     }
     
+    public interface IAutoEntityUnionMapping
+    {
+        IComplexColumn Source { get; }
+        IColumn Target { get; }
+    }
+    
+    public class AutoEntityUnionMapping : IAutoEntityUnionMapping
+    {
+        public IComplexColumn Source { get; private set; }
+        public IColumn Target { get; private set; }
+
+        public AutoEntityUnionMapping(IComplexColumn source, IColumn target)
+        {
+            Source = source;
+            Target = target;
+        }
+    }
+    
     public interface IAutoEntityUnionTable
     {
         Type Entity { get; }
         IFilter? Filter { get; }
         AutoEntityUnionConstant[] Constants { get; }
+        AutoEntityUnionMapping[] Mappings { get; }
     }
     
     public class AutoEntityUnionTable<TInterface,TEntity> : IAutoEntityUnionTable
@@ -44,6 +63,9 @@ namespace InABox.Core
         
         private List<AutoEntityUnionConstant> _constants = new List<AutoEntityUnionConstant>();
         public AutoEntityUnionConstant[] Constants => _constants.ToArray();
+        
+        private List<AutoEntityUnionMapping> _mappings = new List<AutoEntityUnionMapping>();
+        public AutoEntityUnionMapping[] Mappings => _mappings.ToArray();
 
         public AutoEntityUnionTable(Filter<TEntity>? filter)
         {
@@ -56,6 +78,31 @@ namespace InABox.Core
             return this;
         }
         
+        public AutoEntityUnionTable<TInterface, TEntity> AliasField<TType>(Expression<Func<TInterface, object?>> target, Expression<Func<TEntity, TType>> source)
+        {
+            var _tgt = new Column<TInterface>(target);
+            var _node = new ComplexFormulaFieldNode<TEntity, TType>(source);
+            var _src = new ComplexColumn<TEntity, TType>(_tgt.Property, _node); 
+            
+            _mappings.Add(new AutoEntityUnionMapping(_src, _tgt));
+            return this;
+        }
+        
+        public AutoEntityUnionTable<TInterface, TEntity> AliasField(Expression<Func<TInterface, object?>> target, string formatString, params Expression<Func<TEntity, object?>>[] fields)
+        {
+            var _tgt = new Column<TInterface>(target);
+            var _o = fields.Select(x => new ComplexFormulaFieldNode<TEntity, object?>(x));
+            var _o2 = _o.OfType<IComplexFormulaNode<TEntity, object?>>().ToList()
+                .Prepend(new ComplexFormulaConstantNode<TEntity, object>(formatString))
+                .ToArray();
+            var _n = new ComplexFormulaFormulaNode<TEntity, object?>(_o2, FormulaOperator.Format);
+            //var _node = new ComplexFormulaFieldNode<TEntity, TType>(source);
+            var _src = new ComplexColumn<TEntity, object?>(_tgt.Property, _n); 
+            
+            _mappings.Add(new AutoEntityUnionMapping(_src, _tgt));
+            return this;
+        }
+        
     }
     
     public abstract class AutoEntityUnionGenerator<TInterface> : IAutoEntityUnionGenerator

+ 30 - 0
InABox.Core/Entity.cs

@@ -37,6 +37,36 @@ namespace InABox.Core
         double Tax { get; set; }
         double IncTax { get; set; }
     }
+
+    public interface IProblem
+    {
+        string[] Notes { get; set; }
+        
+        DateTime Resolved { get; set; }
+    }
+    
+    public abstract class Problem : EnclosedEntity, IProblem
+    {
+        [NotesEditor]
+        [EditorSequence(1)]
+        [Caption("Notes", IncludePath = false)]
+        public string[] Notes { get; set; }
+        
+        [TimestampEditor]
+        [EditorSequence(999)]
+        [Caption("Resolved", IncludePath = false)]
+        public DateTime Resolved { get; set; }
+    }
+
+    public interface IProblems
+    {
+        IProblem Problem { get; }
+    }
+    
+    public interface IProblems<T>: IProblems where T : Problem
+    {
+        new T Problem { get; set; }
+    }
     
     public interface IIssues
     {

+ 9 - 0
InABox.Core/Security/AutoSecurityDescriptor.cs

@@ -95,6 +95,15 @@ namespace InABox.Core
 
         public bool Value => (typeof(TEntity).GetCustomAttribute<AutoEntity>() == null);
     }
+    
+    public class CanManageProblems<TEntity> : IAutoSecurityAction<TEntity>
+    {
+        public string Prefix => "Manage";
+
+        public string Postfix => "Problem";
+
+        public bool Value => (typeof(TEntity).GetCustomAttribute<AutoEntity>() == null);
+    }
 
     public class AutoSecurityDescriptor<TEntity, TAction> : ISecurityDescriptor
         where TEntity : Entity where TAction : IAutoSecurityAction<TEntity>, new()

+ 11 - 0
InABox.Core/Security/Security.cs

@@ -246,5 +246,16 @@ namespace InABox.Core
         {
             return ClientFactory.IsSupported<TEntity>() && IsAllowed<AutoSecurityDescriptor<TEntity, CanManageIssues<TEntity>>>();
         }
+        
+        public static bool CanManageProblems(Type TEntity)
+        {
+            return ClientFactory.IsSupported(TEntity)
+                   && IsAllowed(typeof(AutoSecurityDescriptor<,>).MakeGenericType(TEntity, typeof(CanManageProblems<>).MakeGenericType(TEntity)));
+        }
+
+        public static bool CanManageProblems<TEntity>() where TEntity : Entity, IProblems, new()
+        {
+            return ClientFactory.IsSupported<TEntity>() && IsAllowed<AutoSecurityDescriptor<TEntity, CanManageProblems<TEntity>>>();
+        }
     }
 }

+ 0 - 1
InABox.Database/DbFactory.cs

@@ -95,7 +95,6 @@ public static class DbFactory
         {
             throw new Exception("Database migration failed. Aborting startup");
         }
-        
 
         //Load up your custom properties here!
         // Can't use clients (b/c we're inside the database layer already

+ 44 - 15
inabox.database.sqlite/SQLiteProvider.cs

@@ -888,7 +888,7 @@ public class SQLiteProviderFactory : IProviderFactory
                 foreach (var table in union.Tables)
                 {
 
-                    var columns = Columns.None(table.Entity);
+                    var columns = new List<IBaseColumn>();
                     var constants = CheckDefaultColumns(union);
                     
                     var interfacefields = new Dictionary<string, string>();
@@ -899,20 +899,26 @@ public class SQLiteProviderFactory : IProviderFactory
 
                     foreach (var field in interfacefields.Keys)
                     {
-                        if (entityfields.ContainsKey(field))
-                            columns.Add(field);
+                        var mapping = table.Mappings.FirstOrDefault(x => String.Equals(x.Target.Property, field));
+                        if (mapping != null)
+                            columns.Add(mapping.Source);
                         else
                         {
                             var constant = table.Constants.FirstOrDefault(x => String.Equals(x.Mapping.Property, field));
                             if (constant != null)
                                 constants[field] = constant.Value;
                             else
-                                constants[field] = null;
+                            {
+                                if (entityfields.ContainsKey(field))
+                                    columns.Add(Column.Create(type,field));
+                                else
+                                    constants[field] = null;
+                            }
                         }
                     }
 
                     var query = MainProvider.PrepareSelectNonGeneric(table.Entity, new SQLiteCommand(), 'A',
-                        table.Filter, columns.Columns(), null,
+                        table.Filter, columns, null,
                         null, constants, null, union.Distinct, false);
                     
                     queries.Add(query);
@@ -2050,7 +2056,15 @@ public class SQLiteProvider : IProvider
 //        foreach (var modifier in attribute.Modifiers)
 //            if (!fieldmap.ContainsKey(modifier))
 //                throw new Exception(string.Format("{0}.{1} -> {2} does not exist", columnname, attribute.GetType().Name, modifier));
-        
+
+        if (attribute.Operator == FormulaOperator.Format)
+        {
+            var fmt = fieldmap.TryGetValue(attribute.Value, out var _value) ? _value : attribute.Value;
+            var others = attribute.Modifiers.Select(x => fieldmap.TryGetValue(x, out var _value) ? _value : $"\"\"");
+            var result = $"printf(\"{fmt}\", {string.Join(", ", others)}";
+            return result;
+        }
+
         if (attribute.Operator == FormulaOperator.Add)
             return string.Format("(IFNULL({0},0.00) + {1})", 
                 fieldmap.TryGetValue(attribute.Value, out var _value) ? _value : attribute.Value,
@@ -2234,18 +2248,24 @@ public class SQLiteProvider : IProvider
                 foreach (var field in formula.GetOperands())
                 {
                     var operand = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, field, useparams);
-                    if(op == FormulaOperator.Divide)
-                    {
+                    if (op == FormulaOperator.Divide)
                         operands.Add($"IFNULL({operand}, 1.00)");
-                    }
+                    else if (op == FormulaOperator.Format)
+                        operands.Add(operand);
                     else
-                    {
                         operands.Add($"IFNULL({operand}, 0.00)");
-                    }
                 }
 
                 switch (op)
                 {
+                    
+                    case FormulaOperator.Format:
+                        var fmt = operands.First();
+                        var others = operands.Skip(1);
+                        var result = $"printf({fmt}, {string.Join(", ", others)})";
+                        return result;
+                    
+                    
                     case FormulaOperator.Add:
                         if(formula.TResult == typeof(decimal))
                         {
@@ -2695,9 +2715,18 @@ public class SQLiteProvider : IProvider
         Count
     }
 
-    public string PrepareSelectNonGeneric(Type T, SQLiteCommand command, char prefix,
-        IFilter? filter, IEnumerable<IBaseColumn>? columns, ISortOrder? sort,
-        Dictionary<string, string>? aggregates, Dictionary<string, object?>? constants, CoreRange? range, bool distinct, bool useparams)
+    public string PrepareSelectNonGeneric(
+        Type T, 
+        SQLiteCommand command, 
+        char prefix,
+        IFilter? filter, 
+        IEnumerable<IBaseColumn>? columns, 
+        ISortOrder? sort,
+        Dictionary<string, string>? aggregates, 
+        Dictionary<string, object?>? constants, 
+        CoreRange? range, 
+        bool distinct, 
+        bool useparams)
     {
 
         var fieldmap = new Dictionary<string, string>();
@@ -2748,7 +2777,7 @@ public class SQLiteProvider : IProvider
                         string.Format("{0} as [{1}]", _col, column.Name);
                 }
                 else
-                    combined[constants != null ? column.Name : String.Format("{0:D8}", combined.Keys.Count)] = string.Format("{0} as [{1}]", value, column);
+                    combined[constants != null ? column.Name : String.Format("{0:D8}", combined.Keys.Count)] = string.Format("{0} as [{1}]", value, column.Name);
             }
 
         if (constants != null)

+ 167 - 140
inabox.wpf/DynamicGrid/Columns/DynamicIssuesColumn.cs

@@ -11,143 +11,170 @@ using InABox.WPF;
 
 namespace InABox.DynamicGrid;
 
-public class DynamicIssuesColumn<TIssues> : DynamicImageColumn
-    where TIssues : Entity, IRemotable, IPersistent, IIssues, new()
-{
-    private static readonly BitmapImage _warning = Wpf.Resources.warning.AsBitmapImage();
-
-    private readonly IDynamicGrid _parent;
-
-    public string IssuesProperty;
-    public Func<CoreRow[], TIssues[]> LoadIssues;
-
-    public DynamicIssuesColumn(IDynamicGrid parent, string issuesProperty, Func<CoreRow[], TIssues[]> loadIssues)
-    {
-        Image = IssuesImage;
-        Filters = new[] { "Active Issues", "No Issues" };
-        FilterRecord = DoFilterIssues;
-        ContextMenu = CreateIssuesMenu;
-        ToolTip = CreateIssuesToolTip;
-        _parent = parent;
-        Position = DynamicActionColumnPosition.Start;
-
-        IssuesProperty = issuesProperty;
-        LoadIssues = loadIssues;
-    }
-
-    public DynamicIssuesColumn(IDynamicGrid parent):
-        this(parent, CoreUtils.GetFullPropertyName<TIssues, string>(x => x.Issues, ""), (rows) => rows.ToObjects<TIssues>().ToArray())
-    {
-    }
-
-    private string? GetIssues(CoreRow? row)
-    {
-        return row?.Get<string>(IssuesProperty);
-    }
-
-    private BitmapImage? IssuesImage(CoreRow? row)
-    {
-        if (GetIssues(row).IsNullOrWhiteSpace())
-            return null;
-        return _warning;
-    }
-
-    private FrameworkElement? CreateIssuesToolTip(DynamicActionColumn column, CoreRow? row)
-    {
-        var text = GetIssues(row);
-        if (text.IsNullOrWhiteSpace())
-            text = "No Issues Found";
-        return TextToolTip(text);
-    }
-
-    private bool DoFilterIssues(CoreRow row, string[] filter)
-    {
-        var noissues = GetIssues(row).IsNullOrWhiteSpace();
-        if (filter.Contains("No Issues") && noissues)
-            return true;
-        if (filter.Contains("Active Issues") && !noissues)
-            return true;
-        return false;
-    }
-
-    private MenuItem CreateMenu(string caption, RoutedEventHandler click)
-    {
-        var item = new MenuItem();
-        item.Header = caption;
-        item.Click += click;
-        return item;
-    }
-
-    private ContextMenu? CreateIssuesMenu(CoreRow[]? rows)
-    {
-        if (!Security.CanManageIssues<TIssues>())
-            return null;
-
-        var issues = rows?.Select(GetIssues).Where(x => !string.IsNullOrWhiteSpace(x)).Any();
-        var result = new ContextMenu();
-
-        if (issues != true)
-        {
-            result.Items.Add(CreateMenu("Create Issue", (o, e) => EditIssues(rows)));
-        }
-        else
-        {
-            result.Items.Add(CreateMenu("Update Issues", (o, e) => EditIssues(rows)));
-            result.Items.Add(CreateMenu("Clear Issues", (o, e) => ClearIssues(rows)));
-        }
-
-        return result;
-    }
-
-    private void ClearIssues(CoreRow[]? rows)
-    {
-        if (rows is null)
-        {
-            return;
-        }
-        if (MessageBox.Show("This will clear the flagged issues for these items!\n\nAre you sure you wish to continue?", "Confirm",
-                MessageBoxButton.YesNo) == MessageBoxResult.Yes)
-        {
-            var _updates = LoadIssues(rows).ToArray();
-            foreach (var update in _updates)
-            {
-                update.Issues = "";
-            }
-            using (new WaitCursor())
-            {
-                Client.Save(_updates, "Clearing Issues", (o, e) => { });
-            }
-
-            // False here to prevent Refreshing and losing the selected row record
-            foreach (var row in rows)
-                _parent.UpdateRow(row, IssuesProperty, "");
-        }
-    }
-
-    private void EditIssues(CoreRow[]? rows)
-    {
-        var objects = LoadIssues(rows ?? Array.Empty<CoreRow>());
-
-        var map = new Dictionary<CoreRow, TIssues>();
-        if (rows is not null)
-        {
-            var i = 0;
-            foreach (var row in rows)
-            {
-                map[row] = objects[i];
-                ++i;
-            }
-        }
-
-        if (new DynamicIssuesEditor(map.Values.ToArray()).ShowDialog() == true)
-        {
-            using (new WaitCursor())
-            {
-                Client.Save(map.Values, "Updating Issues", (o, e) => { });
-            }
-
-            foreach (var row in map.Keys)
-                _parent.UpdateRow(row, IssuesProperty, map[row].Issues);
-        }
-    }
-}
+// public interface IDynamicIssuesColumn
+// {
+//     Func<IIssues[], FrameworkElement?>? CustomiseEditor { get; set; }
+//     Action<IIssues[], FrameworkElement?>? IssuesUpdated { get; set; }
+// }
+//
+// public class DynamicIssuesColumn<TIssues> : DynamicImageColumn, IDynamicIssuesColumn
+//     where TIssues : Entity, IRemotable, IPersistent, IIssues, new()
+// {
+//     private static readonly BitmapImage _warning = Wpf.Resources.warning.AsBitmapImage();
+//     private static readonly BitmapImage _graywarning = Wpf.Resources.warning.AsGrayScale().AsBitmapImage();
+//
+//     private readonly IDynamicGrid _parent;
+//
+//     public string IssuesProperty;
+//     public string IssuesResolvedProperty;
+//     public Func<CoreRow[], TIssues[]> LoadIssues;
+//
+//     public Func<IIssues[], FrameworkElement?>? CustomiseEditor { get; set; }
+//     
+//     public Action<IIssues[], FrameworkElement?>? IssuesUpdated { get; set; }
+//
+//     public DynamicIssuesColumn(IDynamicGrid parent, string issuesProperty, string issuesResolvedProperty, Func<CoreRow[], TIssues[]> loadIssues)
+//     {
+//         Image = IssuesImage;
+//         Filters = new[] { "Active Issues", "No Issues" };
+//         FilterRecord = DoFilterIssues;
+//         ContextMenu = CreateIssuesMenu;
+//         ToolTip = CreateIssuesToolTip;
+//         _parent = parent;
+//         Position = DynamicActionColumnPosition.Start;
+//
+//         IssuesProperty = issuesProperty;
+//         IssuesResolvedProperty = issuesResolvedProperty;
+//         LoadIssues = loadIssues;
+//     }
+//
+//     public DynamicIssuesColumn(IDynamicGrid parent):
+//         this(parent, CoreUtils.GetFullPropertyName<TIssues, string>(x => x.Issues, ""), CoreUtils.GetFullPropertyName<TIssues, DateTime>(x => x.IssuesResolved, ""), (rows) => rows.ToObjects<TIssues>().ToArray())
+//     {
+//     }
+//
+//     private string? GetIssues(CoreRow? row)
+//     {
+//         return row?.Get<string>(IssuesProperty);
+//     }
+//     
+//     private DateTime GetIssuesResolved(CoreRow? row)
+//     {
+//         return row?.Get<DateTime>(IssuesResolvedProperty) ?? DateTime.MinValue;
+//     }
+//
+//     private BitmapImage? IssuesImage(CoreRow? row)
+//     {
+//         if (GetIssues(row).IsNullOrWhiteSpace())
+//             return null;
+//         return GetIssuesResolved(row).IsEmpty()
+//             ? _warning
+//             : _graywarning;
+//     }
+//
+//     private FrameworkElement? CreateIssuesToolTip(DynamicActionColumn column, CoreRow? row)
+//     {
+//         var text = GetIssues(row);
+//         if (text.IsNullOrWhiteSpace())
+//             text = "No Issues Found";
+//         return TextToolTip(text);
+//     }
+//
+//     private bool DoFilterIssues(CoreRow row, string[] filter)
+//     {
+//         var noissues = GetIssues(row).IsNullOrWhiteSpace();
+//         if (filter.Contains("No Issues") && noissues)
+//             return true;
+//         if (filter.Contains("Active Issues") && !noissues)
+//             return true;
+//         return false;
+//     }
+//
+//     private MenuItem CreateMenu(string caption, RoutedEventHandler click)
+//     {
+//         var item = new MenuItem();
+//         item.Header = caption;
+//         item.Click += click;
+//         return item;
+//     }
+//
+//     private ContextMenu? CreateIssuesMenu(CoreRow[]? rows)
+//     {
+//         if (!Security.CanManageIssues<TIssues>())
+//             return null;
+//
+//         var issues = rows?.Select(GetIssues).Where(x => !string.IsNullOrWhiteSpace(x)).Any();
+//         var result = new ContextMenu();
+//
+//         if (issues != true)
+//         {
+//             result.Items.Add(CreateMenu("Create Issue", (o, e) => EditIssues(rows)));
+//         }
+//         else
+//         {
+//             result.Items.Add(CreateMenu("Update Issues", (o, e) => EditIssues(rows)));
+//             result.Items.Add(CreateMenu("Clear Issues", (o, e) => ClearIssues(rows)));
+//         }
+//
+//         return result;
+//     }
+//
+//     private void ClearIssues(CoreRow[]? rows)
+//     {
+//         if (rows is null)
+//         {
+//             return;
+//         }
+//         if (MessageBox.Show("This will clear the flagged issues for these items!\n\nAre you sure you wish to continue?", "Confirm",
+//                 MessageBoxButton.YesNo) == MessageBoxResult.Yes)
+//         {
+//             var _updates = LoadIssues(rows).ToArray();
+//             foreach (var update in _updates)
+//             {
+//                 update.Issues = "";
+//             }
+//             using (new WaitCursor())
+//             {
+//                 Client.Save(_updates, "Clearing Issues", (o, e) => { });
+//             }
+//
+//             // False here to prevent Refreshing and losing the selected row record
+//             foreach (var row in rows)
+//                 _parent.UpdateRow(row, IssuesProperty, "");
+//         }
+//     }
+//
+//     private void EditIssues(CoreRow[]? rows)
+//     {
+//         var _objects = LoadIssues(rows ?? Array.Empty<CoreRow>());
+//
+//         var _map = new Dictionary<CoreRow, TIssues>();
+//         if (rows is not null)
+//         {
+//             var i = 0;
+//             foreach (var row in rows)
+//             {
+//                 _map[row] = _objects[i];
+//                 ++i;
+//             }
+//         }
+//
+//         var _values = _map.Values.OfType<IIssues>().ToArray();
+//         var _editor = new DynamicIssuesEditor(_values);
+//         var _custom = CustomiseEditor?.Invoke(_values);
+//         if (_custom != null)
+//             _editor.SetCustom(_custom);
+//         if (_editor.ShowDialog() == true)
+//         {
+//             using (new WaitCursor())
+//             {
+//                 if (_custom != null)
+//                     IssuesUpdated?.Invoke(_values, _custom);
+//                 Client.Save(_map.Values, "Updating Issues", (o, e) => { });
+//             }
+//
+//             foreach (var _row in _map.Keys)
+//                 _parent.UpdateRow(_row, IssuesProperty, _map[_row].Issues);
+//         }
+//     }
+// }

+ 145 - 0
inabox.wpf/DynamicGrid/Columns/DynamicProblemsColumn.cs

@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Imaging;
+using InABox.Clients;
+using InABox.Core;
+using InABox.WPF;
+
+namespace InABox.DynamicGrid;
+
+public class DynamicProblemsColumn<TEntity> : DynamicImageColumn
+    where TEntity : Entity, IRemotable, IPersistent, IProblems, new()
+{
+    private static readonly BitmapImage _warning = Wpf.Resources.warning.AsBitmapImage();
+    private static readonly BitmapImage _graywarning = Wpf.Resources.warning.AsGrayScale().AsBitmapImage();
+
+    private readonly IDynamicGrid _parent;
+
+    public Func<IIssues[], FrameworkElement?>? CustomiseEditor { get; set; }
+    
+    public Action<IIssues[], FrameworkElement?>? IssuesUpdated { get; set; }
+
+    public DynamicProblemsColumn(IDynamicGrid parent)
+    {
+        Image = IssuesImage;
+        Filters = new[] { "Active Issues", "No Issues" };
+        FilterRecord = DoFilterIssues;
+        ContextMenu = CreateIssuesMenu;
+        ToolTip = CreateIssuesToolTip;
+        _parent = parent;
+        Position = DynamicActionColumnPosition.Start;
+    }
+    
+    private BitmapImage? IssuesImage(CoreRow? row)
+    {
+        if (row?.Get<TEntity,string[]>(x=>x.Problem.Notes)?.Any() != true)
+            return null;
+        return row?.Get<TEntity,DateTime>(x=>x.Problem.Resolved).IsEmpty() == true
+            ? _warning
+            : _graywarning;
+    }
+
+    private FrameworkElement? CreateIssuesToolTip(DynamicActionColumn column, CoreRow? row)
+    {
+        var text = row?.Get<TEntity, string[]>(x => x.Problem.Notes) ?? new string[] { };
+        return TextToolTip(string.Join("\n==================================", text));
+    }
+
+    private bool DoFilterIssues(CoreRow row, string[] filter)
+    {
+        var noissues = row?.Get<TEntity, string[]>(x => x.Problem.Notes)?.Any() != true;
+        if (filter.Contains("No Issues") && noissues)
+            return true;
+        if (filter.Contains("Active Issues") && !noissues)
+            return true;
+        return false;
+    }
+
+    private MenuItem CreateMenu(string caption, RoutedEventHandler click)
+    {
+        var item = new MenuItem();
+        item.Header = caption;
+        item.Click += click;
+        return item;
+    }
+
+    private ContextMenu? CreateIssuesMenu(CoreRow[]? rows)
+    {
+        if (!Security.CanManageProblems<TEntity>())
+            return null;
+
+        var issues = rows?.FirstOrDefault()?.Get<TEntity, string[]>(x => x.Problem.Notes)?.Any() == true;
+        var result = new ContextMenu();
+
+        if (issues != true)
+        {
+            result.Items.Add(CreateMenu("Create Issue", (o, e) => EditIssues(rows)));
+        }
+        else
+        {
+            result.Items.Add(CreateMenu("Update Issues", (o, e) => EditIssues(rows)));
+            if (Security.CanManageProblems<TEntity>())
+            {
+                result.Items.Add(new Separator());
+                result.Items.Add(CreateMenu("Mark as Resolved", (o, e) => ResolveIssues(rows)));
+            }
+        }
+
+        return result;
+    }
+
+    private void ResolveIssues(CoreRow[]? rows)
+    {
+        var row = rows?.FirstOrDefault();
+        if (row == null)
+            return;
+        
+        var entity = row.ToObject<TEntity>();
+        entity.Problem.Resolved = DateTime.Now;
+        using (new WaitCursor())
+            Client.Save(entity, "Resolving Issues", (o, e) => { });
+        
+        // False here to prevent Refreshing and losing the selected row record
+        _parent.UpdateRow<TEntity, DateTime>(row, x=>x.Problem.Resolved, entity.Problem.Resolved);
+        
+        // if (MessageBox.Show("This will clear the flagged issues for these items!\n\nAre you sure you wish to continue?", "Confirm",
+        //         MessageBoxButton.YesNo) == MessageBoxResult.Yes)
+        // {
+        //     var _updates = LoadIssues(rows).ToArray();
+        //     foreach (var update in _updates)
+        //     {
+        //         update.Issues = "";
+        //     }
+        //     using (new WaitCursor())
+        //     {
+        //         Client.Save(_updates, "Clearing Issues", (o, e) => { });
+        //     }
+        //
+        //     // False here to prevent Refreshing and losing the selected row record
+        //     foreach (var row in rows)
+        //         _parent.UpdateRow(row, IssuesProperty, "");
+        // }
+    }
+
+    private void EditIssues(CoreRow[]? rows)
+    {
+        
+        var row = rows?.FirstOrDefault();
+        if (row == null)
+            return;
+        
+        var entity = row.ToObject<TEntity>();
+        
+        var _grid = DynamicGridUtils.CreateDynamicGrid(typeof(DynamicItemsListGrid<>), entity.Problem.GetType());
+        if (_grid.EditItems(new object[] { entity.Problem }))
+        {
+            using (new WaitCursor())
+                Client.Save(entity, "Resolving Issues", (o, e) => { });
+            row.Table.LoadRow(entity);
+            _parent.InvalidateRow(row);
+        }
+    }
+}

+ 28 - 4
inabox.wpf/DynamicGrid/DynamicDataGrid.cs

@@ -68,16 +68,40 @@ public class DynamicDataGrid<TEntity> : DynamicGrid<TEntity>, IDynamicDataGrid w
 
         //HiddenColumns.Add(x => x.ID);
 
-        if (typeof(TEntity).GetInterfaces().Contains(typeof(IIssues)))
+        // if (typeof(TEntity).GetInterfaces().Contains(typeof(IIssues)))
+        // {
+        //     HiddenColumns.Add(x => (x as IIssues)!.Issues);
+        //     HiddenColumns.Add(x=>(x as IIssues)!.IssuesResolved);
+        //     var coltype = typeof(DynamicIssuesColumn<>).MakeGenericType(typeof(TEntity));
+        //     var column = Activator.CreateInstance(coltype, this);
+        //     var dic = (column as IDynamicIssuesColumn)!;
+        //     dic.CustomiseEditor = (v) => DoCustomiseIssuesEditor(v.OfType<TEntity>().ToArray());
+        //     dic.IssuesUpdated = (v, c) => DoUpdateIssues(v.OfType<TEntity>().ToArray(), c);
+        //     var dac = (column as DynamicActionColumn)!;
+        //     ActionColumns.Add(dac);
+        // }
+        
+        
+        if (typeof(TEntity).GetInterfaces().Contains(typeof(IProblems)))
         {
-            HiddenColumns.Add(x => (x as IIssues)!.Issues);
-            var coltype = typeof(DynamicIssuesColumn<>).MakeGenericType(typeof(TEntity));
-            ActionColumns.Add((Activator.CreateInstance(coltype, this) as DynamicActionColumn)!);
+            HiddenColumns.Add(x => (x as IProblems)!.Problem.Notes);
+            HiddenColumns.Add(x=>(x as IProblems)!.Problem.Resolved);
+            var coltype = typeof(DynamicProblemsColumn<>).MakeGenericType(typeof(TEntity));
+            var column = Activator.CreateInstance(coltype, this);
+            //var dic = (column as IDynamicIssuesColumn)!;
+            //dic.CustomiseEditor = (v) => DoCustomiseIssuesEditor(v.OfType<TEntity>().ToArray());
+            //dic.IssuesUpdated = (v, c) => DoUpdateIssues(v.OfType<TEntity>().ToArray(), c);
+            var dac = (column as DynamicActionColumn)!;
+            ActionColumns.Add(dac);
         }
 
         SetupFilterColumns();
     }
 
+    // protected virtual void DoUpdateIssues(TEntity[] toArray, FrameworkElement? frameworkElement) { }
+    //
+    // protected virtual FrameworkElement? DoCustomiseIssuesEditor(TEntity[] values) => null;
+
     protected override void Init()
     {
         FilterComponent = new(this,

+ 1 - 0
inabox.wpf/DynamicGrid/DynamicIssuesEditor.xaml

@@ -28,6 +28,7 @@
         </DockPanel>
         <Button x:Name="ClearIssues" Grid.Row="2" Grid.Column="0" Width="80" Padding="5" Margin="5,0,0,5"
                 Content="Clear" Click="ClearIssues_Click" TabIndex="3" Visibility="Collapsed" />
+        <DockPanel x:Name="Custom" Grid.Row="2" Grid.Column="1" Margin="5,0,5,5" />
         <Button x:Name="OK" Grid.Row="2" Grid.Column="2" Width="80" Padding="5" Margin="0,0,5,5" Content="OK"
                 Click="OK_Click" TabIndex="4" />
         <Button x:Name="Cancel" Grid.Row="2" Grid.Column="3" Width="80" Padding="5" Margin="0,0,5,5" Content="Cancel"

+ 6 - 0
inabox.wpf/DynamicGrid/DynamicIssuesEditor.xaml.cs

@@ -27,6 +27,12 @@ namespace InABox.DynamicGrid
             ClearIssues.Visibility = allowclear ? Visibility.Visible : Visibility.Collapsed;
         }
 
+        public void SetCustom(FrameworkElement custom)
+        {
+            Custom.Children.Clear();
+            Custom.Children.Add(custom);
+        }
+
         private void ReloadHistory()
         {
             var issues = _items.Select(x => x.Issues).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToArray();

+ 2 - 0
inabox.wpf/DynamicGrid/IDynamicGrid.cs

@@ -103,6 +103,8 @@ public interface IDynamicGrid
     void UpdateRow<TType>(CoreRow row, string column, TType value, bool refresh = true);
 
     void UpdateRow<T, TType>(CoreRow row, Expression<Func<T, TType>> column, TType value, bool refresh = true);
+
+    void InvalidateRow(CoreRow row);
 }
 
 public interface IDynamicGrid<T> : IDynamicGrid