Przeglądaj źródła

Merge remote-tracking branch 'origin/kenric' into frank

frogsoftware 11 miesięcy temu
rodzic
commit
53ae1a5b18

+ 1 - 1
InABox.Core/Client/Client.cs

@@ -83,7 +83,7 @@ namespace InABox.Clients
                 row?.FillObject(entity);
             }
         }
-        public static void EnsureColumns<TEntity>(IList<TEntity> entities, Columns<TEntity> columns)
+        public static void EnsureColumns<TEntity>(ICollection<TEntity> entities, Columns<TEntity> columns)
             where TEntity : Entity, IRemotable, IPersistent, new()
         {
             var newColumns = Columns.None<TEntity>()

+ 17 - 7
InABox.Core/CoreExpression.cs

@@ -8,6 +8,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Text.RegularExpressions;
+using Expressive.Exceptions;
 
 namespace InABox.Core
 {
@@ -118,7 +119,9 @@ namespace InABox.Core
 
         public static List<string> GetModelVariables(Type modelType)
         {
-            return CoreUtils.PropertyList(modelType, x => true).Select(x => x.Name).ToList();
+            var props = DatabaseSchema.Properties(modelType).Select(x => x.Name).ToList();
+            props.Sort();
+            return props;
         }
         public static List<string> GetModelVariables<TModel>() where TModel : IExpressionModel
             => GetModelVariables(typeof(TModel));
@@ -162,8 +165,8 @@ namespace InABox.Core
             }
             return default;
         }
-        [return: MaybeNull]
-        public TReturn Evaluate(TModel model)
+
+        public Result<TReturn, Exception> Evaluate(TModel model)
         {
             var values = new Dictionary<string, object?>();
             foreach(var variable in ReferencedVariables)
@@ -171,12 +174,19 @@ namespace InABox.Core
                 values[variable] = CoreUtils.GetPropertyValue(model, variable);
             }
 
-            var result = base.Evaluate(values);
-            if(result is TReturn ret)
+            try
             {
-                return ret;
+                var result = base.Evaluate(values);
+                if(result is TReturn ret)
+                {
+                    return Result.Ok(ret);
+                }
+                return Result.Ok<TReturn>(default);
+            }
+            catch (Exception e)
+            {
+                return Result.Error(e);
             }
-            return default;
         }
     }
 }

+ 3 - 3
InABox.Core/CoreUtils.cs

@@ -2718,10 +2718,10 @@ namespace InABox.Core
             return enumerable.ToArray<T>();
         }
 
-        public static U[] ToArray<T, U>(this T[] from, Func<T, U> mapFunc)
+        public static U[] ToArray<T, U>(this IList<T> from, Func<T, U> mapFunc)
         {
-            var to = new U[from.Length];
-            for(int i = 0; i < from.Length; ++i)
+            var to = new U[from.Count];
+            for(int i = 0; i < from.Count; ++i)
             {
                 to[i] = mapFunc(from[i]);
             }

+ 2 - 4
InABox.Core/DatabaseSchema/DatabaseSchema.cs

@@ -99,9 +99,7 @@ namespace InABox.Core
             {
                 var properties = CoreUtils.PropertyList(
                     type,
-                    x => !x.PropertyType.IsInterface &&
-                        (x.DeclaringType.IsSubclassOf(typeof(BaseObject))
-                        || x.DeclaringType.IsSubclassOf(typeof(BaseEditor)))
+                    x => !x.PropertyType.IsInterface && x.DeclaringType != typeof(BaseObject)
                 );
 
                 var subObjects = new List<Tuple<Type, string>>();
@@ -295,7 +293,7 @@ namespace InABox.Core
             {
                 var props = _properties.GetValueOrDefault(type);
                 var hasprops = props?.Any(x => x.Value is StandardProperty) == true;
-                if (!hasprops && type.IsSubclassOf(typeof(BaseObject)))
+                if (!hasprops)
                 {
                     RegisterProperties(type);
                     return _properties.GetValueOrDefault(type);

+ 4 - 0
InABox.Core/Filter.cs

@@ -368,6 +368,10 @@ namespace InABox.Core
             result.Value = value;
             return result;
         }
+
+        public static Filter<T> All<T>() => new Filter<T>().All();
+
+        public static Filter<T> None<T>() => new Filter<T>().None();
     }
 
     public interface IFilter2<T>

+ 3 - 0
InABox.Core/ILookupDefinition.cs

@@ -50,6 +50,9 @@ namespace InABox.Core
         where TChild : class
     { }
 
+    /// <summary>
+    /// Define a lookup definition for a given property. The generator must derive from <see cref="LookupDefinitionGenerator{TLookup, TEntity}"/>.
+    /// </summary>
     public class LookupDefinitionAttribute : Attribute
     {
         public Type Generator { get; set; }

+ 72 - 15
InABox.Core/Postable/PostExceptions.cs

@@ -5,24 +5,15 @@ using System.Text;
 
 namespace InABox.Core.Postable
 {
-    public class PostException : Exception
-    {
-        public PostException() : base() { }
+    #region BaseExceptions
 
-        public PostException(string message) : base(message) { }
-    }
-
-    public class EmptyPostException : PostException
+    public class BasePosterException : Exception
     {
-        public EmptyPostException() { }
-    }
+        public BasePosterException() : base() { }
 
-    public class PostFailedMessageException : PostException
-    {
-        public PostFailedMessageException(string message): base(message) { }
+        public BasePosterException(string message) : base(message) { }
     }
-
-    public class MissingSettingException : PostException
+    public class MissingSettingException : BasePosterException
     {
         public Type SettingsType { get; }
 
@@ -40,7 +31,7 @@ namespace InABox.Core.Postable
         public MissingSettingException(Expression<Func<T, object?>> setting) : base(typeof(T), CoreUtils.GetFullPropertyName(setting, ".")) { }
     }
 
-    public class MissingSettingsException : PostException
+    public class MissingSettingsException : BasePosterException
     {
         public Type PostableType { get; }
 
@@ -49,6 +40,28 @@ namespace InABox.Core.Postable
             PostableType = postableType;
         }
     }
+
+    #endregion
+
+    #region PostExceptions
+
+    public class PostException : BasePosterException
+    {
+        public PostException() : base() { }
+
+        public PostException(string message) : base(message) { }
+    }
+
+    public class EmptyPostException : PostException
+    {
+        public EmptyPostException() { }
+    }
+
+    public class PostFailedMessageException : PostException
+    {
+        public PostFailedMessageException(string message): base(message) { }
+    }
+
     public class RepostedException : PostException
     {
         public RepostedException() : base("Cannot process an item twice.")
@@ -61,4 +74,48 @@ namespace InABox.Core.Postable
         {
         }
     }
+
+    #endregion
+
+    #region PullExceptions
+
+    public class PullException : BasePosterException
+    {
+        public PullException() : base() { }
+
+        public PullException(string message) : base(message) { }
+    }
+
+    public class PullCancelledException : PullException
+    {
+        public PullCancelledException(): base("Import cancelled") { }
+    }
+
+    public class PullFailedMessageException : PullException
+    {
+        public PullFailedMessageException(string message): base(message) { }
+    }
+
+    public class InvalidPullerException : PullException
+    {
+        public Type PostableType { get; set; }
+
+        public Type? PosterType { get; set; }
+
+        public InvalidPullerException(Type postableType)
+            : base($"Cannot import {postableType.Name}")
+        {
+            PostableType = postableType;
+            PosterType = null;
+        }
+
+        public InvalidPullerException(Type postableType, Type posterType)
+            : base($"Cannot import {postableType.Name} with {posterType.GetCaptionOrNull(true) ?? posterType.Name}")
+        {
+            PostableType = postableType;
+            PosterType = posterType;
+        }
+    }
+
+    #endregion
 }

+ 44 - 0
InABox.Core/Postable/PostResult.cs

@@ -74,4 +74,48 @@ namespace InABox.Core
             fragmentsList.Add(fragment);
         }
     }
+
+    public enum PullResultType
+    {
+        New,
+        Linked,
+        Updated
+    }
+
+    public class PullResultItem<TPostable>
+        where TPostable : IPostable
+    {
+        public PullResultType Type { get; set; }
+
+        public TPostable Item { get; set; }
+
+        public PullResultItem(PullResultType type, TPostable item)
+        {
+            Type = type;
+            Item = item;
+        }
+    }
+
+    public interface IPullResult<TPostable>
+        where TPostable : IPostable
+    {
+        public IEnumerable<PullResultItem<TPostable>> PulledEntities { get; }
+    }
+
+    public class PullResult<TPostable> : IPullResult<TPostable>
+        where TPostable : IPostable
+    {
+        private List<PullResultItem<TPostable>> posts = new List<PullResultItem<TPostable>>();
+
+        /// <summary>
+        /// All successful or failed <typeparamref name="TPostable"/>s.
+        /// </summary>
+        public IEnumerable<PullResultItem<TPostable>> PulledEntities => posts;
+
+        public void AddEntity(PullResultType type, TPostable post)
+        {
+            post.Post();
+            posts.Add(new PullResultItem<TPostable>(type, post));
+        }
+    }
 }

+ 5 - 0
InABox.Core/Postable/PostableSettings.cs

@@ -30,5 +30,10 @@ namespace InABox.Core
         [EditorSequence(4)]
         [CheckBoxEditor]
         public bool ShowClearButton { get; set; } = true;
+
+        [EditorSequence(5)]
+        [CheckBoxEditor]
+        [Caption("Show Import Button")]
+        public bool ShowPullButton { get; set; } = true;
     }
 }

+ 39 - 0
InABox.Core/Postable/PosterEngine.cs

@@ -195,4 +195,43 @@ namespace InABox.Core
             }
         }
     }
+
+    public interface IPullerEngine<TPostable>
+        where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
+    {
+        IPullResult<TPostable>? Pull();
+    }
+
+    public interface IPullerEngine<TPostable, TPoster> : IPullerEngine<TPostable>
+        where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
+    {
+        IPullResult<TPostable> DoPull();
+
+        IPullResult<TPostable>? IPullerEngine<TPostable>.Pull()
+        {
+            try
+            {
+                var result = DoPull();
+
+                return result;
+            }
+            catch (PullCancelledException)
+            {
+                throw;
+            }
+            catch (MissingSettingException)
+            {
+                throw;
+            }
+            catch (PullFailedMessageException)
+            {
+                throw;
+            }
+            catch(Exception e)
+            {
+                Logger.Send(LogType.Error, "", $"Post Failed: {CoreUtils.FormatException(e)}");
+                throw;
+            }
+        }
+    }
 }

+ 84 - 19
InABox.Core/Postable/PosterUtils.cs

@@ -23,10 +23,15 @@ namespace InABox.Core
             return settings;
         }
 
+        private static PostableSettings LoadPostableSettings(Type T)
+        {
+            return FixPostableSettings(T, new GlobalConfiguration<PostableSettings>(T.Name).Load());
+        }
+
         public static PostableSettings LoadPostableSettings<T>()
             where T : Entity, IPostable
         {
-            return FixPostableSettings(typeof(T), new GlobalConfiguration<PostableSettings>(typeof(T).Name).Load());
+            return LoadPostableSettings(typeof(T));
         }
 
         public static void SavePostableSettings<T>(PostableSettings settings)
@@ -140,6 +145,10 @@ namespace InABox.Core
             return GetPosters().Where(x => TPoster.IsAssignableFrom(x)).FirstOrDefault()
                 ?? throw new Exception($"No poster of type {TPoster}.");
         }
+        public static Type? GetPoster(string posterType)
+        {
+            return GetPosters()?.FirstOrDefault(x => x.EntityName() == posterType)!;
+        }
 
         private static EngineType[] GetPosterEngines()
         {
@@ -149,43 +158,71 @@ namespace InABox.Core
                     && !x.IsAbstract
                     && x.GetTypeInfo().GenericTypeParameters.Length == 1
                     && x.HasInterface(typeof(IPosterEngine<,,>))
-            ).Select(x => new EngineType
+            ).Select(x =>
             {
-                Engine = x,
-                Entity = x.GetInterfaceDefinition(typeof(IPosterEngine<,,>))!.GenericTypeArguments[0],
-                Poster = x.GetInterfaceDefinition(typeof(IPosterEngine<,,>))!.GenericTypeArguments[1].GetGenericTypeDefinition()
+                var poster = x.GetInterfaceDefinition(typeof(IPosterEngine<,,>))!.GenericTypeArguments[1];
+                return new EngineType
+                {
+                    Engine = x,
+                    Entity = x.GetInterfaceDefinition(typeof(IPosterEngine<,,>))!.GenericTypeArguments[0],
+                    Poster = poster.IsGenericType ? poster.GetGenericTypeDefinition() : poster
+                };
             }).ToArray();
             return _posterEngines;
         }
 
         /// <summary>
-        /// Get the <see cref="IPosterEngine{TPostable,TPoster,TSettings}"/> for <typeparamref name="T"/>
-        /// based on the current <see cref="PostableSettings"/> for <typeparamref name="T"/>.
+        /// Get the <see cref="IPosterEngine{TPostable,TPoster,TSettings}"/> for <paramref name="T"/>
+        /// based on the current <see cref="PostableSettings"/> for <paramref name="T"/>.
         /// </summary>
-        /// <typeparam name="T"></typeparam>
         /// <returns></returns>
-        public static Type GetEngine<T>()
-            where T : Entity, IPostable, IRemotable, IPersistent, new()
+        public static Result<Type, Exception> GetEngine(Type T)
         {
-            var settings = LoadPostableSettings<T>();
+            var settings = LoadPostableSettings(T);
             if (string.IsNullOrWhiteSpace(settings.PosterType))
             {
-                throw new MissingSettingsException(typeof(T));
+                return Result.Error<Exception>(new MissingSettingsException(T));
+            }
+            var poster = GetPoster(settings.PosterType);
+            if(poster is null)
+            {
+                return Result.Error(new Exception($"No poster of type {settings.PosterType}."));
             }
-            var poster = GetPosters()?.FirstOrDefault(x => x.EntityName() == settings.PosterType)!;
 
-            var engines = GetPosterEngines().Where(x => poster.HasInterface(x.Poster)).ToList();
+            var engines = GetPosterEngines().Where(x =>
+            {
+                return x.Poster.IsInterface ? poster.HasInterface(x.Poster) : poster.IsSubclassOfRawGeneric(x.Poster);
+            }).ToList();
             if (!engines.Any())
             {
-                throw new Exception("No poster for the given settings.");
+                return Result.Error(new Exception("No poster for the given settings"));
             }
             else if(engines.Count == 1)
             {
-                return engines[0].Engine.MakeGenericType(typeof(T));
+                return Result.Ok(engines[0].Engine.MakeGenericType(T));
             }
             else
             {
-                return engines.Single(x => x.Entity == typeof(T)).Engine.MakeGenericType(typeof(T));
+                return Result.Ok(engines.Single(x => x.Entity == T).Engine.MakeGenericType(T));
+            }
+        }
+
+        /// <summary>
+        /// Get the <see cref="IPosterEngine{TPostable,TPoster,TSettings}"/> for <typeparamref name="T"/>
+        /// based on the current <see cref="PostableSettings"/> for <typeparamref name="T"/>.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <returns></returns>
+        public static Type GetEngine<T>()
+            where T : Entity, IPostable, IRemotable, IPersistent, new()
+        {
+            if (GetEngine(typeof(T)).Get(out var engineType, out var e))
+            {
+                return engineType;
+            }
+            else
+            {
+                throw e;
             }
         }
 
@@ -226,7 +263,7 @@ namespace InABox.Core
             {
                 return false;
             }
-            var poster = GetPosters()?.FirstOrDefault(x => x.EntityName() == settings.PosterType);
+            var poster = GetPoster(settings.PosterType);
             if (poster is null)
             {
                 return false;
@@ -235,7 +272,7 @@ namespace InABox.Core
             if(iautoRefresh != null)
             {
                 var autoRefresher = Activator.CreateInstance(iautoRefresh.GenericTypeArguments[1]) as IAutoRefresher<T>;
-                if (autoRefresher != null && autoRefresher.ShouldRepost(item))
+                if (autoRefresher != null && autoRefresher.ShouldRepost(item!))
                 {
                     postable.PostedStatus = PostedStatus.RequiresRepost;
                     return true;
@@ -266,6 +303,34 @@ namespace InABox.Core
             return CreateEngine<T>().Process(model);
         }
 
+        /// <summary>
+        /// Import <typeparamref name="T"/> with the currently set <see cref="IPosterEngine{TPostable, TPoster, TSettings}"/>.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <returns><see langword="null"/> if post was unsuccessful.</returns>
+        /// <exception cref="InvalidPullerException">If the engine is not a <see cref="IPullerEngine{TPostable,TPoster}"/></exception>
+        public static IPullResult<T>? Pull<T>()
+            where T : Entity, IPostable, IRemotable, IPersistent, new()
+        {
+            var engine = CreateEngine<T>();
+            if(engine is IPullerEngine<T> puller)
+            {
+                return puller.Pull();
+            }
+            else
+            {
+                var intDef = engine.GetType().GetInterfaceDefinition(typeof(IPosterEngine<,,>));
+                if(intDef != null)
+                {
+                    throw new InvalidPullerException(typeof(T), intDef.GenericTypeArguments[1]);
+                }
+                else
+                {
+                    throw new InvalidPullerException(typeof(T));
+                }
+            }
+        }
+
         #endregion
     }
 }

+ 1 - 1
InABox.Database/Stores/Store.cs

@@ -402,7 +402,7 @@ namespace InABox.Database
                     if (string.IsNullOrWhiteSpace(code))
                         throw new NullCodeException(typeof(T), key);
 
-                    var expr = CoreUtils.GetPropertyExpression<T, object>(key); //CoreUtils.GetMemberExpression(typeof(T),key)
+                    var expr = CoreUtils.GetPropertyExpression<T, object?>(key); //CoreUtils.GetMemberExpression(typeof(T),key)
                     codes = codes == null ? new Filter<T>(expr).IsEqualTo(code) : codes.Or(expr).IsEqualTo(code);
                     columns.Add(key);
                 }

+ 14 - 9
InABox.Poster.MYOB/MYOBPosterEngine.cs

@@ -139,20 +139,21 @@ public static partial class MYOBPosterEngine
     private static partial Regex CodeRegex();
 }
 
-public abstract class MYOBPosterEngine<TPostable, TSettings> :
-    BasePosterEngine<TPostable, IMYOBPoster<TPostable, TSettings>, TSettings>,
-    IGlobalSettingsPosterEngine<IMYOBPoster<TPostable, TSettings>, MYOBGlobalPosterSettings>
+public abstract class MYOBPosterEngine<TPostable, TPoster, TSettings> :
+    BasePosterEngine<TPostable, TPoster, TSettings>,
+    IGlobalSettingsPosterEngine<TPoster, MYOBGlobalPosterSettings>
 
     where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
     where TSettings : MYOBPosterSettings, new()
+    where TPoster : class, IMYOBPoster<TPostable, TSettings>
 {
     private MYOBGlobalPosterSettings GetGlobalSettings() =>
-        (this as IGlobalSettingsPosterEngine<IMYOBPoster<TPostable, TSettings>, MYOBGlobalPosterSettings>).GetGlobalSettings();
+        (this as IGlobalSettingsPosterEngine<TPoster, MYOBGlobalPosterSettings>).GetGlobalSettings();
 
     private void SaveGlobalSettings(MYOBGlobalPosterSettings settings) =>
-        (this as IGlobalSettingsPosterEngine<IMYOBPoster<TPostable, TSettings>, MYOBGlobalPosterSettings>).SaveGlobalSettings(settings);
+        (this as IGlobalSettingsPosterEngine<TPoster, MYOBGlobalPosterSettings>).SaveGlobalSettings(settings);
 
-    protected override IMYOBPoster<TPostable, TSettings> CreatePoster()
+    protected override TPoster CreatePoster()
     {
         var poster = base.CreatePoster();
         poster.Script = GetScriptDocument();
@@ -163,8 +164,8 @@ public abstract class MYOBPosterEngine<TPostable, TSettings> :
     {
         return Poster.BeforePost(model);
     }
-    
-    protected override IPostResult<TPostable> DoProcess(IDataModel<TPostable> model)
+
+    protected void LoadConnectionData()
     {
         var data = MYOBPosterEngine.GetConnectionData();
 
@@ -231,7 +232,11 @@ public abstract class MYOBPosterEngine<TPostable, TSettings> :
             // data.ActiveCompanyFile = data.CompanyFileService.Get(companyFile, fileCredentials);
         }
         Poster.ConnectionData = data;
-
+    }
+    
+    protected override IPostResult<TPostable> DoProcess(IDataModel<TPostable> model)
+    {
+        LoadConnectionData();
         return Poster.Process(model);
     }
 

+ 11 - 10
inabox.wpf/DynamicGrid/DynamicContentDialog.xaml

@@ -4,18 +4,19 @@
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:local="clr-namespace:InABox.DynamicGrid"
-        mc:Ignorable="d" Height="450" Width="800">
-    <Window.Resources>
-        <Style TargetType="Button">
-            <Setter Property="Width" Value="80"/>
-            <Setter Property="Height" Value="35"/>
-            <Setter Property="Margin" Value="5,5,0,0"/>
-        </Style>
-    </Window.Resources>
+        mc:Ignorable="d" Height="450" Width="800"
+        x:Name="Window">
     <DockPanel Margin="5">
         <DockPanel x:Name="Buttons" DockPanel.Dock="Bottom" LastChildFill="False">
-            <Button x:Name="Cancel" DockPanel.Dock="Right" Content="Cancel" Click="Cancel_OnClick"/>
-            <Button x:Name="OK" DockPanel.Dock="Right" Content="OK" Click="OK_OnClick"/>
+            <Button x:Name="CancelButton" Click="CancelButton_Click"
+                    Content="Cancel"
+                    Margin="5,5,0,0" Padding="5" MinWidth="60"
+                    DockPanel.Dock="Right"/>
+            <Button x:Name="OKButton" Click="OKButton_Click"
+                    Content="OK"
+                    Margin="5,5,0,0" Padding="5" MinWidth="60"
+                    DockPanel.Dock="Right"
+                    IsEnabled="{Binding ElementName=Window,Path=CanSave}"/>
         </DockPanel>
         <ContentPresenter x:Name="Presenter" DockPanel.Dock="Top" />
     </DockPanel>

+ 28 - 22
inabox.wpf/DynamicGrid/DynamicContentDialog.xaml.cs

@@ -1,32 +1,38 @@
 using System.Windows;
 using NPOI.OpenXmlFormats.Spreadsheet;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+public partial class DynamicContentDialog : Window
 {
-    public partial class DynamicContentDialog : Window
+    public static readonly DependencyProperty CanSaveProperty = DependencyProperty.Register(nameof(CanSave), typeof(bool), typeof(DynamicContentDialog));
+
+    public bool ButtonsVisible
     {
-        
-        public bool ButtonsVisible
-        {
-            get => Buttons.Visibility == Visibility.Visible;
-            set => Buttons.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
-        }
+        get => Buttons.Visibility == Visibility.Visible;
+        set => Buttons.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
+    }
 
-        public DynamicContentDialog(FrameworkElement element, bool buttonsvisible = true)
-        {
-            InitializeComponent();
-            ButtonsVisible = buttonsvisible;
-            Presenter.Content = element;
-        }
+    public bool CanSave
+    {
+        get => (bool)GetValue(CanSaveProperty);
+        set => SetValue(CanSaveProperty, value);
+    }
 
-        private void OK_OnClick(object sender, RoutedEventArgs e)
-        {
-            DialogResult = true;
-        }
+    public DynamicContentDialog(FrameworkElement element, bool buttonsvisible = true)
+    {
+        InitializeComponent();
+        ButtonsVisible = buttonsvisible;
+        Presenter.Content = element;
+    }
 
-        private void Cancel_OnClick(object sender, RoutedEventArgs e)
-        {
-            DialogResult = false;
-        }
+    private void OKButton_Click(object sender, RoutedEventArgs e)
+    {
+        DialogResult = true;
+    }
+
+    private void CancelButton_Click(object sender, RoutedEventArgs e)
+    {
+        DialogResult = false;
     }
 }

+ 33 - 16
inabox.wpf/DynamicGrid/Editors/DocumentEditor/DocumentEditorControl.cs

@@ -7,6 +7,7 @@ using System.Windows.Controls;
 using System.Windows.Media;
 using InABox.Core;
 using InABox.Wpf;
+using InABox.Wpf.Editors;
 using InABox.WPF;
 using Microsoft.Win32;
 
@@ -51,10 +52,11 @@ namespace InABox.DynamicGrid
                 HorizontalAlignment = HorizontalAlignment.Stretch
             };
             //Grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(25, GridUnitType.Pixel) });
-            Grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
-            Grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(50, GridUnitType.Pixel) });
-            Grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(50, GridUnitType.Pixel) });
-            Grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(50, GridUnitType.Pixel) });
+            Grid.AddColumn(GridUnitType.Star);
+            Grid.AddColumn(50);
+            Grid.AddColumn(50);
+            Grid.AddColumn(50);
+            Grid.AddColumn(60);
 
             Editor = new TextBox
             {
@@ -65,9 +67,7 @@ namespace InABox.DynamicGrid
                 IsEnabled = false
             };
             //Editor.LostFocus += (o, e) => CheckChanged();
-            Editor.SetValue(Grid.ColumnProperty, 0);
-            Editor.SetValue(Grid.RowProperty, 0);
-            Grid.Children.Add(Editor);
+            Grid.AddChild(Editor, 0, 0);
 
             var Select = new Button
             {
@@ -78,10 +78,8 @@ namespace InABox.DynamicGrid
                 Content = "Select",
                 Focusable = false
             };
-            Select.SetValue(Grid.ColumnProperty, 1);
-            Select.SetValue(Grid.RowProperty, 0);
             Select.Click += Select_Click;
-            Grid.Children.Add(Select);
+            Grid.AddChild(Select, 0, 1);
 
             var Clear = new Button
             {
@@ -92,10 +90,8 @@ namespace InABox.DynamicGrid
                 Content = "Clear",
                 Focusable = false
             };
-            Clear.SetValue(Grid.ColumnProperty, 2);
-            Clear.SetValue(Grid.RowProperty, 0);
             Clear.Click += Clear_Click;
-            Grid.Children.Add(Clear);
+            Grid.AddChild(Clear, 0, 2);
 
             var View = new Button
             {
@@ -106,14 +102,35 @@ namespace InABox.DynamicGrid
                 Content = "View",
                 Focusable = false
             };
-            View.SetValue(Grid.ColumnProperty, 3);
-            View.SetValue(Grid.RowProperty, 0);
             View.Click += View_Click;
-            Grid.Children.Add(View);
+            Grid.AddChild(View, 0, 3);
+
+            var rename = new Button
+            {
+                VerticalAlignment = VerticalAlignment.Stretch,
+                VerticalContentAlignment = VerticalAlignment.Center,
+                HorizontalAlignment = HorizontalAlignment.Stretch,
+                Margin = new Thickness(5, 1, 0, 1),
+                Content = "Rename",
+                Focusable = false
+            };
+            rename.Click += Rename_Click;
+            Grid.AddChild(rename, 0, 4);
 
             return Grid;
         }
 
+        private void Rename_Click(object sender, RoutedEventArgs e)
+        {
+            var name = _document.FileName;
+            if(TextEdit.Execute("Enter filename:", ref name))
+            {
+                _document.FileName = name;
+                Editor.Text = _document.FileName;
+                Host.SaveDocument(_document);
+            }
+        }
+
         private void Select_Click(object sender, RoutedEventArgs e)
         {
             var dlg = new OpenFileDialog();

+ 14 - 0
inabox.wpf/Grids/PostableSettingsGrid.cs

@@ -45,6 +45,20 @@ public class PostableSettingsGrid : DynamicItemsListGrid<PostableSettings>
 
             combo.Buttons = [settingsButton, globalSettingsButton];
         }
+        else if(column.ColumnName == nameof(PostableSettings.ShowPullButton))
+        {
+            var entityType = CoreUtils.GetEntityOrNull(settings.PostableType);
+            var visible = false;
+            if (entityType is not null)
+            {
+                var engine = PosterUtils.GetEngine(entityType);
+                if(engine.Get(out var eType, out var _error))
+                {
+                    visible = eType.HasInterface(typeof(IPullerEngine<>));
+                }
+            }
+            editor.Editable = visible ? Editable.Enabled : Editable.Hidden;
+        }
     }
 
     private void ViewGlobalSettings(object editor, object? item)