From 4e579b968e0ab6f5d375ebf971237345245afa87 Mon Sep 17 00:00:00 2001 From: scottdurow Date: Mon, 19 Sep 2016 12:03:11 +0100 Subject: [PATCH] Resolves #111 --- .../Client/ContactEditor/Model/Entities.cs | 3 + .../ViewModels/ContactsEditorViewModel.cs | 34 +++- .../ViewModels/ObservableEntities.cs | 5 + .../ContactEditor/Views/ContactEditorView.cs | 28 +-- .../sparkle_/css/2013/sparkle.css | 27 ++- .../sparkle_/html/form.templates.htm | 2 +- .../WebResources/dev1_/html/ContactEditor.htm | 37 +++- .../sparkle_/css/2013/sparkle.css | 26 ++- .../sparkle_/html/form.templates.htm | 2 +- SparkleXrmSource/SparkleXrm/Sdk/Entity.cs | 8 +- .../CustomBinding/XrmLookupBinding.cs | 177 ++++++++++++++---- .../GridEditor/XrmLookupEditor.cs | 151 ++++++++++++--- .../SparkleXrmUI/ResourceStrings.cs | 18 ++ .../SparkleXrmUI/SparkleXrmUI.csproj | 1 + 14 files changed, 433 insertions(+), 86 deletions(-) create mode 100644 SparkleXrmSource/SparkleXrmUI/ResourceStrings.cs diff --git a/SparkleXrmSource/Client/ContactEditor/Model/Entities.cs b/SparkleXrmSource/Client/ContactEditor/Model/Entities.cs index 1bbe5523..68ca89a5 100644 --- a/SparkleXrmSource/Client/ContactEditor/Model/Entities.cs +++ b/SparkleXrmSource/Client/ContactEditor/Model/Entities.cs @@ -47,6 +47,9 @@ public Contact() [ScriptName("ownerid")] public EntityReference OwnerId; + [ScriptName("parentcustomerid")] + public EntityReference ParentCustomerId; + } } diff --git a/SparkleXrmSource/Client/ContactEditor/ViewModels/ContactsEditorViewModel.cs b/SparkleXrmSource/Client/ContactEditor/ViewModels/ContactsEditorViewModel.cs index c4023cef..f41ac63d 100644 --- a/SparkleXrmSource/Client/ContactEditor/ViewModels/ContactsEditorViewModel.cs +++ b/SparkleXrmSource/Client/ContactEditor/ViewModels/ContactsEditorViewModel.cs @@ -11,6 +11,8 @@ using SparkleXrm; using Client.ContactEditor.ViewModels; using Client.ContactEditor.Model; +using Xrm; +using System.Runtime.CompilerServices; namespace Client.ContactEditor.ViewModels { @@ -229,7 +231,6 @@ public Action SaveCommand() public void TransactionCurrencySearchCommand(string term, Action callback) { // Get the option set values - string fetchXml = @" @@ -255,6 +256,14 @@ public void TransactionCurrencySearchCommand(string term, Action callback) { @@ -287,6 +296,29 @@ public void OwnerSearchCommand(string term, Action callback) } } + [PreserveCase] + public void AccountSearchCommand(string term, Action callback) + { + string fetchXml = @" + + + + + + + + + + "; + + fetchXml = string.Format(fetchXml, XmlHelper.Encode(term)); + OrganizationServiceProxy.BeginRetrieveMultiple(fetchXml, delegate(object result) + { + EntityCollection fetchResult = OrganizationServiceProxy.EndRetrieveMultiple(result, typeof(Entity)); + callback(fetchResult); + }); + } + private void SearchRecords(string term, Action callback, string entityType, string entityNameAttribute) { string fetchXml = @" diff --git a/SparkleXrmSource/Client/ContactEditor/ViewModels/ObservableEntities.cs b/SparkleXrmSource/Client/ContactEditor/ViewModels/ObservableEntities.cs index ef8b9cdd..e6cb0a16 100644 --- a/SparkleXrmSource/Client/ContactEditor/ViewModels/ObservableEntities.cs +++ b/SparkleXrmSource/Client/ContactEditor/ViewModels/ObservableEntities.cs @@ -34,6 +34,9 @@ public partial class ObservableContact [ScriptName("transactioncurrencyid")] public Observable TransactionCurrencyId = Knockout.Observable(); + [ScriptName("parentcustomerid")] + public Observable ParentCustomerId = Knockout.Observable(); + [ScriptName("creditlimit")] public Observable CreditLimit = Knockout.Observable(); @@ -53,6 +56,7 @@ public void SetValue(Contact value) AccountRoleCode.SetValue(_value.AccountRoleCode.Value); NumberOfChildren.SetValue(_value.NumberOfChildren.Value); TransactionCurrencyId.SetValue(_value.TransactionCurrencyId); + ParentCustomerId.SetValue(_value.ParentCustomerId); CreditLimit.SetValue(_value.CreditLimit); } @@ -67,6 +71,7 @@ public Contact Commit() _value.TransactionCurrencyId = TransactionCurrencyId.GetValue(); _value.CreditLimit = CreditLimit.GetValue(); _value.EntityState = EntityStates.Changed; + _value.ParentCustomerId = ParentCustomerId.GetValue(); return _value; } diff --git a/SparkleXrmSource/Client/ContactEditor/Views/ContactEditorView.cs b/SparkleXrmSource/Client/ContactEditor/Views/ContactEditorView.cs index 69656692..613d4591 100644 --- a/SparkleXrmSource/Client/ContactEditor/Views/ContactEditorView.cs +++ b/SparkleXrmSource/Client/ContactEditor/Views/ContactEditorView.cs @@ -23,6 +23,7 @@ using SparkleXrm.CustomBinding; using Client.ContactEditor.ViewModels; using Xrm; +using Client.ContactEditor.Model; namespace Client.Views { @@ -36,7 +37,7 @@ public static void init() OrganizationServiceProxy.GetUserSettings(); // Data Bind Grid - List columns = GridDataViewBinder.ParseLayout(",entityState,20,First Name,firstname,200,Last Name,lastname,200,Birth Date,birthdate,200,Account Role Code,accountrolecode,200,Number of Children,numberofchildren,100,Currency,transactioncurrencyid,200,Credit Limit,creditlimit,100,Gender,gendercode,100,Owner,ownerid,100"); + List columns = GridDataViewBinder.ParseLayout(",entityState,20,First Name,firstname,200,Last Name,lastname,200,Birth Date,birthdate,200,Account Role Code,accountrolecode,200,Number of Children,numberofchildren,100,Currency,transactioncurrencyid,200,Credit Limit,creditlimit,100,Gender,gendercode,100,Owner,ownerid,100,Parent Customer,parentcustomerid,100"); // Set Column formatters and editors columns[0].Formatter = delegate(int row, int cell, object value, Column columnDef, object dataContext) @@ -62,21 +63,30 @@ public static void init() // Currency Column XrmLookupEditor.BindColumn(columns[6], vm.TransactionCurrencySearchCommand, "transactioncurrencyid", "currencyname", ""); - + // Credit Limit Column XrmMoneyEditor.BindColumn(columns[7], -10000, 10000); // Another optionset XrmOptionSetEditor.BindColumn(columns[8], "contact", columns[8].Field, true); + // Owner Column + XrmLookupEditorOptions options = (XrmLookupEditorOptions)XrmLookupEditor.BindColumn(columns[9], vm.OwnerSearchCommand, "id", "name", "").Options; + options.showFooter = true; + - // OWner Column - XrmLookupEditor.BindColumn(columns[9], vm.OwnerSearchCommand, "id", "name", ""); + // Account Column + XrmLookupEditorOptions accountLookupOptions = (XrmLookupEditorOptions)XrmLookupEditor.BindColumn(columns[10], vm.AccountSearchCommand, "id", "name", "").Options; + accountLookupOptions.showFooter = true; + accountLookupOptions.footerButton = new XrmLookupEditorButton(); + accountLookupOptions.footerButton.Label = "Add New"; + accountLookupOptions.footerButton.Image = "/_imgs/add_10.png"; + accountLookupOptions.footerButton.OnClick = vm.AddNewAccountInLine; // Create Grid GridDataViewBinder contactGridDataBinder = new GridDataViewBinder(); Grid contactsGrid = contactGridDataBinder.DataBindXrmGrid(vm.Contacts, columns, "container", "pager",true,false); - //contactGridDataBinder.BindClickHandler(contactsGrid); + // Data Bind ViewBase.RegisterViewModel(vm); @@ -86,13 +96,5 @@ public static void init() }, 0); } - - - - - - - - } } diff --git a/SparkleXrmSource/CrmPackage/WebResources/sparkle_/css/2013/sparkle.css b/SparkleXrmSource/CrmPackage/WebResources/sparkle_/css/2013/sparkle.css index 86849326..874a69b4 100644 --- a/SparkleXrmSource/CrmPackage/WebResources/sparkle_/css/2013/sparkle.css +++ b/SparkleXrmSource/CrmPackage/WebResources/sparkle_/css/2013/sparkle.css @@ -906,7 +906,31 @@ TEXTAREA.sparkle-Input { width:150px; text-overflow:ellipsis; } - +.sparkle-menu-footer { + position: absolute; + border-width: 1px; + border-style: solid; + border-color: rgb(160, 160, 160) rgb(128, 128, 128) rgb(128, 128, 128); + margin: 0px; + padding: 2px; + display: block; + background: white; + height: 16px; + min-width: 100px; +} +.sparkle-xrm .ui-autocomplete { + min-width:100px !important; +} +.sparkle-menu-footer-right { + position: absolute; + right: 4px; + top: 1px; +} +.sparkle-menu-footer-left { + position: absolute; + left: 4px; + top: 1px; +} .sparkle-menu-item-label { } @@ -1030,7 +1054,6 @@ TEXTAREA.sparkle-Input { width: 16px; height: 16px; overflow: hidden; - display:block; } diff --git a/SparkleXrmSource/CrmPackage/WebResources/sparkle_/html/form.templates.htm b/SparkleXrmSource/CrmPackage/WebResources/sparkle_/html/form.templates.htm index dda9dd32..7003088d 100644 --- a/SparkleXrmSource/CrmPackage/WebResources/sparkle_/html/form.templates.htm +++ b/SparkleXrmSource/CrmPackage/WebResources/sparkle_/html/form.templates.htm @@ -93,7 +93,7 @@

- +
- + + + + +
diff --git a/SparkleXrmSource/DebugWeb/WebResources/dev1_/html/ContactEditor.htm b/SparkleXrmSource/DebugWeb/WebResources/dev1_/html/ContactEditor.htm index 5ec80040..67ca1411 100644 --- a/SparkleXrmSource/DebugWeb/WebResources/dev1_/html/ContactEditor.htm +++ b/SparkleXrmSource/DebugWeb/WebResources/dev1_/html/ContactEditor.htm @@ -84,13 +84,48 @@
diff --git a/SparkleXrmSource/DebugWeb/WebResources/sparkle_/css/2013/sparkle.css b/SparkleXrmSource/DebugWeb/WebResources/sparkle_/css/2013/sparkle.css index b3e7888d..874a69b4 100644 --- a/SparkleXrmSource/DebugWeb/WebResources/sparkle_/css/2013/sparkle.css +++ b/SparkleXrmSource/DebugWeb/WebResources/sparkle_/css/2013/sparkle.css @@ -906,7 +906,31 @@ TEXTAREA.sparkle-Input { width:150px; text-overflow:ellipsis; } - +.sparkle-menu-footer { + position: absolute; + border-width: 1px; + border-style: solid; + border-color: rgb(160, 160, 160) rgb(128, 128, 128) rgb(128, 128, 128); + margin: 0px; + padding: 2px; + display: block; + background: white; + height: 16px; + min-width: 100px; +} +.sparkle-xrm .ui-autocomplete { + min-width:100px !important; +} +.sparkle-menu-footer-right { + position: absolute; + right: 4px; + top: 1px; +} +.sparkle-menu-footer-left { + position: absolute; + left: 4px; + top: 1px; +} .sparkle-menu-item-label { } diff --git a/SparkleXrmSource/DebugWeb/WebResources/sparkle_/html/form.templates.htm b/SparkleXrmSource/DebugWeb/WebResources/sparkle_/html/form.templates.htm index dda9dd32..7003088d 100644 --- a/SparkleXrmSource/DebugWeb/WebResources/sparkle_/html/form.templates.htm +++ b/SparkleXrmSource/DebugWeb/WebResources/sparkle_/html/form.templates.htm @@ -93,7 +93,7 @@

- +
diff --git a/SparkleXrmSource/SparkleXrm/Sdk/Entity.cs b/SparkleXrmSource/SparkleXrm/Sdk/Entity.cs index b5ca59b9..ca512a2c 100644 --- a/SparkleXrmSource/SparkleXrm/Sdk/Entity.cs +++ b/SparkleXrmSource/SparkleXrm/Sdk/Entity.cs @@ -226,7 +226,7 @@ public static int SortDelegate(string attributeName, Entity a, Entity b) typeName = l.GetType().Name; else if (r != null) typeName = r.GetType().Name; - + if (l != r) { switch (typeName.ToLowerCase()) @@ -240,7 +240,11 @@ public static int SortDelegate(string attributeName, Entity a, Entity b) result = 1; break; case "date": - if ((bool)Script.Literal("{0}<{1}", l, r)) + if (l == null) + result = -1; + else if (r == null) + result = 1; + else if ((bool)Script.Literal("{0}<{1}", l, r)) result = -1; else result = 1; diff --git a/SparkleXrmSource/SparkleXrmUI/CustomBinding/XrmLookupBinding.cs b/SparkleXrmSource/SparkleXrmUI/CustomBinding/XrmLookupBinding.cs index 178d5680..db7ddf4d 100644 --- a/SparkleXrmSource/SparkleXrmUI/CustomBinding/XrmLookupBinding.cs +++ b/SparkleXrmSource/SparkleXrmUI/CustomBinding/XrmLookupBinding.cs @@ -2,6 +2,7 @@ // using jQueryApi; +using jQueryApi.UI; using jQueryApi.UI.Widgets; using KnockoutApi; using SparkleXrm.GridEditor; @@ -19,12 +20,13 @@ static XrmLookupBinding() if ((string)Script.Literal("typeof(ko)") != "undefined") { Knockout.BindingHandlers["lookup"] = new XrmLookupBinding(); - //ValidationApi.MakeBindingHandlerValidatable("lookup"); } } public override void Init(System.Html.Element element, Func valueAccessor, Func allBindingsAccessor, object viewModel, object context) { + XrmLookupEditorButton footerButton = (XrmLookupEditorButton)allBindingsAccessor()["footerButton"]; + bool showFooter = (bool)allBindingsAccessor()["showFooter"]; jQueryObject container = jQuery.FromElement(element); jQueryObject inputField = container.Find(".sparkle-input-lookup-part"); jQueryObject selectButton = container.Find(".sparkle-input-lookup-button-part"); @@ -32,16 +34,11 @@ public override void Init(System.Html.Element element, Func valueAccesso AutoCompleteOptions options = new AutoCompleteOptions(); options.MinLength = 100000; // Don't enable type down - use search button (or return) options.Delay = 0; - options.Position = new Dictionary("collision", "fit"); - + options.Position = new Dictionary("collision", "fit"); bool justSelected = false; - - // Set the value when selected - options.Select = delegate(jQueryEvent e, AutoCompleteSelectEvent uiEvent) + int totalRecordsReturned = 0; + Action setValue = delegate(AutoCompleteItem item,bool setFocus) { - - // Note we assume that the binding has added an array of string items - AutoCompleteItem item = (AutoCompleteItem)uiEvent.Item; if (_value == null) _value = new EntityReference(null, null, null); string value = item.Label; inputField.Value(value); @@ -49,12 +46,49 @@ public override void Init(System.Html.Element element, Func valueAccesso _value.Name = item.Label; _value.LogicalName = (string)item.Data; justSelected = true; - TrySetObservable(valueAccessor, inputField, _value); - Script.Literal("return false;"); + TrySetObservable(valueAccessor, inputField, _value, setFocus); + }; + + // Set the value when selected + options.Select = delegate(jQueryEvent e, AutoCompleteSelectEvent uiEvent) + { + // Note we assume that the binding has added an array of string items + AutoCompleteItem item = (AutoCompleteItem)uiEvent.Item; + string data = ((string)item.Data); + if (data == "footerlink" || data==null) + { + footerButton.OnClick(item); + e.PreventDefault(); + e.StopImmediatePropagation(); + Script.Literal("return false;"); + } + else + { + setValue(item, true); + Script.Literal("return false;"); + } + }; + + options.Open = delegate(jQueryEvent e, jQueryObject o) + { + if (showFooter && totalRecordsReturned>0) + { + WidgetObject menu = (WidgetObject)Script.Literal("{0}.autocomplete({1})", inputField, "widget"); + XrmLookupEditor.AddFooter(menu, totalRecordsReturned); + } }; - + options.Close = delegate(jQueryEvent e, jQueryObject o) + { + + WidgetObject menu = (WidgetObject)Script.Literal("{0}.autocomplete({1})", inputField, "widget"); + jQueryObject footer = menu.Next(); + if (footer.Length > 0 || footer.HasClass("sparkle-menu-footer")) + { + footer.Hide(); + } + }; // Get the query command Action> queryCommand = (Action>)((object)allBindingsAccessor()["queryCommand"]); string nameAttribute = ((string)allBindingsAccessor()["nameAttribute"]); @@ -75,9 +109,11 @@ public override void Init(System.Html.Element element, Func valueAccesso { Action queryCallBack = delegate(EntityCollection fetchResult) { - AutoCompleteItem[] results = new AutoCompleteItem[fetchResult.Entities.Count]; - - for (int i = 0; i < fetchResult.Entities.Count; i++) + int recordsFound = fetchResult.Entities.Count; + bool noRecordsFound = recordsFound == 0; + AutoCompleteItem[] results = new AutoCompleteItem[recordsFound + (footerButton!=null ? 1 : 0) + (noRecordsFound ? 1 : 0)]; + + for (int i = 0; i < recordsFound; i++) { results[i] = new AutoCompleteItem(); results[i].Label = (string)fetchResult.Entities[i].GetAttributeValue(nameAttribute); @@ -95,7 +131,32 @@ public override void Init(System.Html.Element element, Func valueAccesso results[i].Image = MetadataCache.GetSmallIconUrl(typeCodeName); } - + if (fetchResult.TotalRecordCount > fetchResult.Entities.Count) + { + totalRecordsReturned = fetchResult.TotalRecordCount; + } + else + { + totalRecordsReturned = fetchResult.Entities.Count; + } + int itemsCount = recordsFound; + if (noRecordsFound) + { + AutoCompleteItem noRecordsItem = new AutoCompleteItem(); + noRecordsItem.Label = SparkleResourceStrings.NoRecordsFound; + results[itemsCount] = noRecordsItem; + itemsCount++; + } + if (footerButton != null) + { + // Add the add new + AutoCompleteItem addNewLink = new AutoCompleteItem(); + addNewLink.Label = footerButton.Label; + addNewLink.Image = footerButton.Image; + addNewLink.ColumnValues = null; + addNewLink.Data = "footerlink"; + results[itemsCount] = addNewLink; + } response(results); // Disable it now so typing doesn't trigger a search @@ -105,9 +166,7 @@ public override void Init(System.Html.Element element, Func valueAccesso }; // Call the function with the correct 'this' context - Script.Literal("{0}.call({1}.$parent,{2},{3})",queryCommand, context, request.Term, queryCallBack); - - + Script.Literal("{0}.call({1}.$parent,{2},{3})",queryCommand, context, request.Term, queryCallBack); }; options.Source = queryDelegate; @@ -121,7 +180,18 @@ public override void Init(System.Html.Element element, Func valueAccesso // Set render template ((RenderItemDelegate)Script.Literal("{0}.data('ui-autocomplete')", inputField))._renderItem = delegate(object ul, AutoCompleteItem item) { - string html = "" + item.Label + "
"; + if (item.Data==null) + { + return (object)jQuery.Select("
  • " + item.Label + "
  • ").AppendTo((jQueryObject)ul); + } + + string html = "
    "; + if (item.Image!=null) + { + html += @""; + } + html += @"" + item.Label + "
    "; + if (item.ColumnValues != null && item.ColumnValues.Length > 0) { foreach (string value in item.ColumnValues) @@ -141,22 +211,56 @@ public override void Init(System.Html.Element element, Func valueAccesso enableOption.MinLength = 0; inputField.Focus(); inputField.Plugin().AutoComplete(enableOption); - inputField.Plugin().AutoComplete(AutoCompleteMethod.Search); }); // handle the field changing inputField.Change(delegate(jQueryEvent e) { - if (inputField.GetValue() != _value.Name) - TrySetObservable(valueAccessor, inputField, null); + string inputValue = inputField.GetValue(); + if (inputValue != _value.Name) + { + // The name is different from the name of the lookup reference + // search to see if we can auto resolve it + TrySetObservable(valueAccessor, inputField, null,false); + AutoCompleteRequest lookup = new AutoCompleteRequest(); + lookup.Term = inputValue; + Action lookupResults = delegate(AutoCompleteItem[] results) + { + int selectableItems =0; + // If there is only one, then auto-set + if (results != null) + { + foreach (AutoCompleteItem item in results) + { + if (isItemSelectable(item)) + { + selectableItems++; + } + if (selectableItems > 2) + break; + } + } + + if (selectableItems == 1) + { + // There is only a single value so set it now + setValue(results[0], false); + } + else + { + inputField.Value(String.Empty); + } + }; + + queryDelegate(lookup, lookupResults); + } }); Action disposeCallBack = delegate() { if ((bool)Script.Literal("$({0}).data('ui-autocomplete')!=undefined", inputField)) { - //inputField.Plugin().AutoComplete(AutoCompleteMethod.Destroy); Script.Literal("$({0}).autocomplete(\"destroy\")", inputField); } }; @@ -165,13 +269,9 @@ public override void Init(System.Html.Element element, Func valueAccesso Script.Literal("ko.utils.domNodeDisposal.addDisposeCallback({0}, {1})", element, (object)disposeCallBack); Knockout.BindingHandlers["validationCore"].Init(element, valueAccessor, allBindingsAccessor, null, null); - - // Bind return to searching inputField.Keydown(delegate(jQueryEvent e) { - - if (e.Which == 13 && !justSelected) // Return pressed - but we want to do a search not move to the next cell { selectButton.Click(); @@ -188,14 +288,17 @@ public override void Init(System.Html.Element element, Func valueAccesso e.PreventDefault(); e.StopPropagation(); break; - } justSelected = false; }); //Script.Literal("return { controlsDescendantBindings: true };"); } - + private static bool isItemSelectable(AutoCompleteItem item) + { + string data = (string)item.Data; + return data != null && data != "footerlink"; + } /// /// Extracts additional column display values from the search result /// The name attribute can now be a column separated list of attribute logical names @@ -238,8 +341,7 @@ internal static void GetExtraColumns(string[] columnAttributes, EntityCollection default: value = attributeValue.ToString(); break; - } - + } } } if (value != null && value.Length>0) @@ -252,9 +354,8 @@ internal static void GetExtraColumns(string[] columnAttributes, EntityCollection } } - private static void TrySetObservable(Func valueAccessor, jQueryObject inputField, EntityReference value) + private static void TrySetObservable(Func valueAccessor, jQueryObject inputField, EntityReference value, bool setFocus) { - Observable observable = (Observable)valueAccessor(); bool isValid = true; observable.SetValue(value); @@ -264,17 +365,14 @@ private static void TrySetObservable(Func valueAccessor, jQueryObject in isValid = ((IValidatedObservable)observable).IsValid() == true; } - if (isValid) + if (isValid && setFocus) { // Ensure the field is reinitialised inputField.Blur(); inputField.Focus(); - } - } - public override void Update(System.Html.Element element, Func valueAccessor, Func allBindingsAccessor, object viewModel, object context) { jQueryObject container = jQuery.FromElement(element); @@ -283,8 +381,7 @@ public override void Update(System.Html.Element element, Func valueAcces string displayName = ""; if (value != null) displayName = value.Name; - inputField.Value(displayName); - + inputField.Value(displayName); } } } diff --git a/SparkleXrmSource/SparkleXrmUI/GridEditor/XrmLookupEditor.cs b/SparkleXrmSource/SparkleXrmUI/GridEditor/XrmLookupEditor.cs index ad73e563..61b4153b 100644 --- a/SparkleXrmSource/SparkleXrmUI/GridEditor/XrmLookupEditor.cs +++ b/SparkleXrmSource/SparkleXrmUI/GridEditor/XrmLookupEditor.cs @@ -2,12 +2,15 @@ // using jQueryApi; +using jQueryApi.UI; using jQueryApi.UI.Widgets; using Slick; using SparkleXrm.CustomBinding; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; +using Xrm; using Xrm.Sdk; using Xrm.Sdk.Metadata; @@ -22,8 +25,18 @@ public class XrmLookupEditorOptions public string typeCodeAttribute; public string[] columns; public bool showImage = true; + public bool showFooter = false; + public XrmLookupEditorButton footerButton = null; + public bool useQuickCreate = false; + + } + public class XrmLookupEditorButton + { + public string Label = ""; + public string Tooltip = ""; + public Action OnClick; + public string Image = null; } - public class XrmLookupEditor : GridEditorBase { public static EditorFactory LookupEditor; @@ -58,13 +71,14 @@ public static string Formatter(int row, int cell, object value, Column columnDef private bool _searchOpen = false; private EntityReference _value = new EntityReference(null, null, String.Empty); private EntityReference _originalValue = new EntityReference(null, null, String.Empty); - + private int totalRecordsReturned; + public XrmLookupEditor(EditorArguments args) : base(args) { XrmLookupEditor self = this; _args = args; - _container = jQuery.FromHtml("
    "); + _container = jQuery.FromHtml("
    "); _container.AppendTo(_args.Container); jQueryObject inputField = _container.Find(".sparkle-input-inline"); @@ -78,6 +92,7 @@ public XrmLookupEditor(EditorArguments args) : base(args) options.Position = new Dictionary("collision", "fit"); options.MinLength = 100000; options.Delay = 0; // TODO- set to something that makes sense + XrmLookupEditorOptions editorOptions = (XrmLookupEditorOptions)args.Column.Options; bool justSelected = false; options.Select = delegate(jQueryEvent e, AutoCompleteSelectEvent uiEvent) @@ -86,16 +101,25 @@ public XrmLookupEditor(EditorArguments args) : base(args) // Note we assume that the binding has added an array of string items AutoCompleteItem item = (AutoCompleteItem)uiEvent.Item; - string value = item.Label; - _input.Value(value); - _value.Id = ((EntityReference)item.Value).Id; - _value.Name = ((EntityReference)item.Value).Name; - _value.LogicalName = ((EntityReference)item.Value).LogicalName; - justSelected = true; + EntityReference itemRef = (EntityReference)item.Value; + if (itemRef.LogicalName == "footerlink") + { + XrmLookupEditorButton button = editorOptions.footerButton; + button.OnClick(item); + } + else + { + string value = item.Label; + _input.Value(value); + _value.Id = itemRef.Id; + _value.Name = itemRef.Name; + _value.LogicalName = ((EntityReference)item.Value).LogicalName; + justSelected = true; + } Script.Literal("return false;"); }; - // + options.Focus = delegate(jQueryEvent e, AutoCompleteFocusEvent uiEvent) { // Prevent the value being updated in the text box as we scroll through the results @@ -105,13 +129,23 @@ public XrmLookupEditor(EditorArguments args) : base(args) options.Open = delegate(jQueryEvent e, jQueryObject o) { self._searchOpen = true; + if (editorOptions.showFooter && totalRecordsReturned>0) + { + WidgetObject menu = (WidgetObject)Script.Literal("{0}.autocomplete({1})", _input, "widget"); + AddFooter(menu,totalRecordsReturned); + } }; options.Close = delegate(jQueryEvent e, jQueryObject o) { self._searchOpen = false; + WidgetObject menu = (WidgetObject)Script.Literal("{0}.autocomplete({1})", _input, "widget"); + jQueryObject footer = menu.Next(); + if (footer.Length > 0 || footer.HasClass("sparkle-menu-footer")) + { + footer.Hide(); + } }; - XrmLookupEditorOptions editorOptions = (XrmLookupEditorOptions)args.Column.Options; // If there multiple names, add them to the columnAttributes string[] columns = editorOptions.nameAttribute.Split(","); @@ -125,13 +159,26 @@ public XrmLookupEditor(EditorArguments args) : base(args) // wire up source to CRM search Action> queryDelegate = delegate(AutoCompleteRequest request, Action response) { - // Get the option set values editorOptions.queryCommand(request.Term, delegate(EntityCollection fetchResult) { - AutoCompleteItem[] results = new AutoCompleteItem[fetchResult.Entities.Count]; + if (fetchResult.TotalRecordCount > fetchResult.Entities.Count) + { + totalRecordsReturned = fetchResult.TotalRecordCount; + } + else + { + totalRecordsReturned = fetchResult.Entities.Count; + } + + int recordsFound = fetchResult.Entities.Count; + bool noRecordsFound = recordsFound == 0; + XrmLookupEditorButton button = editorOptions.footerButton; + bool footerButton = (button != null); + + AutoCompleteItem[] results = new AutoCompleteItem[recordsFound + (footerButton ? 1 : 0) + (noRecordsFound ? 1 :0) ]; - for (int i = 0; i < fetchResult.Entities.Count; i++) + for (int i = 0; i < recordsFound; i++) { results[i] = new AutoCompleteItem(); results[i].Label = (string)fetchResult.Entities[i].GetAttributeValue(editorOptions.nameAttribute); @@ -155,6 +202,25 @@ public XrmLookupEditor(EditorArguments args) : base(args) } } + int itemsCount = recordsFound; + if (noRecordsFound) + { + AutoCompleteItem noRecordsItem = new AutoCompleteItem(); + noRecordsItem.Label = SparkleResourceStrings.NoRecordsFound; + results[itemsCount] = noRecordsItem; + itemsCount++; + } + + if (footerButton) + { + // Add the add new + AutoCompleteItem addNewLink = new AutoCompleteItem(); + addNewLink.Label = button.Label; + addNewLink.Image = button.Image; + addNewLink.ColumnValues = null; + addNewLink.Value = new Entity("footerlink"); + results[itemsCount] = addNewLink; + } response(results); // Disable it now so typing doesn't trigger a search @@ -163,15 +229,18 @@ public XrmLookupEditor(EditorArguments args) : base(args) _autoComplete.AutoComplete(disableOption); }); - - - }; options.Source = queryDelegate; inputField = _autoComplete.AutoComplete(options); - ((RenderItemDelegate)Script.Literal("{0}.data('ui-autocomplete')", inputField))._renderItem = delegate(object ul, AutoCompleteItem item) + RenderItemDelegate autoCompleteDelegates = ((RenderItemDelegate)Script.Literal("{0}.data('ui-autocomplete')", inputField)); + autoCompleteDelegates._renderItem = delegate(object ul, AutoCompleteItem item) { + if(item.Value==item.Label) + { + return (object)jQuery.Select("
  • "+item.Label+"
  • ").AppendTo((jQueryObject)ul); + } + string itemHtml = ""; // Allow for no image by passing false to 'ShowImage' on the XrmLookupEditorOptions options if (item.Image != null) @@ -189,7 +258,7 @@ public XrmLookupEditor(EditorArguments args) : base(args) itemHtml += ""; return (object)jQuery.Select("
  • ").Append(itemHtml).AppendTo((jQueryObject)ul); }; - + // Add the click binding to show the drop down selectButton.Click(delegate(jQueryEvent e) { @@ -222,13 +291,13 @@ public XrmLookupEditor(EditorArguments args) : base(args) { switch (e.Which) { + case 9: case 13: // Return case 38: // Up - don't navigate - but use the dropdown to select search results case 40: // Down - don't navigate - but use the dropdown to select search results e.PreventDefault(); e.StopPropagation(); break; - } } else @@ -247,6 +316,39 @@ public XrmLookupEditor(EditorArguments args) : base(args) } + public static void AddFooter(WidgetObject menu, int recordCount) + { + jQueryObject footer = menu.Next(); + if (footer.Length == 0 || !footer.HasClass("sparkle-menu-footer")) + { + footer = jQuery.FromHtml(""); + menu.Parent().Append(footer); + } + + if (footer != null) + { + footer.Html(""); + jQueryObject footerContent = jQuery.FromHtml(""); + jQueryObject footerLeft = jQuery.FromHtml(""); + jQueryObject footerRight = jQuery.FromHtml(""); + footerContent.Append(footerLeft); + footerContent.Append(footerRight); + footerLeft.Append(String.Format(SparkleResourceStrings.LookupFooter, recordCount)); + footer.Append(footerContent); + } + + jQueryPosition pos = menu.Position(); + int height = menu.GetHeight(); + int width = menu.GetWidth(); + if (footer != null && footer.Length > 0) + { + footer.Show(); + footer.CSS("top", (pos.Top + height + 4).ToString() + "px"); + footer.CSS("left", (pos.Left).ToString() + "px"); + footer.Width(width); + } + } + public override void Destroy() { _input.Plugin().AutoComplete(AutoCompleteMethod.Close); @@ -257,15 +359,15 @@ public override void Destroy() } public override void Show() - { + { } public override void Hide() - { + { } public override void Position(jQueryPosition position) - { + { } public override void Focus() @@ -340,7 +442,8 @@ public static Column BindReadOnlyColumn(Column column,string typeCodeAttribute) public class RenderItemDelegate : jQueryObject { public Func _renderItem; - + public Func, object> _renderMenu; + public Action _resizeMenu; } [Imported] [IgnoreNamespace] diff --git a/SparkleXrmSource/SparkleXrmUI/ResourceStrings.cs b/SparkleXrmSource/SparkleXrmUI/ResourceStrings.cs new file mode 100644 index 00000000..bf1903e2 --- /dev/null +++ b/SparkleXrmSource/SparkleXrmUI/ResourceStrings.cs @@ -0,0 +1,18 @@ +// ResourceStrings.cs +// + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SparkleXrm +{ + [IgnoreNamespace] + public class SparkleResourceStrings + { + [PreserveCase] + public static string LookupFooter = "{0} Result(s)"; + [PreserveCase] + public static string NoRecordsFound = "No records found."; + } +} diff --git a/SparkleXrmSource/SparkleXrmUI/SparkleXrmUI.csproj b/SparkleXrmSource/SparkleXrmUI/SparkleXrmUI.csproj index ff7d51a5..97d6e245 100644 --- a/SparkleXrmSource/SparkleXrmUI/SparkleXrmUI.csproj +++ b/SparkleXrmSource/SparkleXrmUI/SparkleXrmUI.csproj @@ -72,6 +72,7 @@ +