From 826f89a9d5875e3a24727ce501e428779985cbc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Mon, 18 Mar 2024 12:29:29 +0100 Subject: [PATCH 1/2] Avoid exceptions in JS translations, add support First, Last --- .../JavascriptTranslatableMethodCollection.cs | 22 +++++++++---- .../Scripts/tests/arrayHelper.test.ts | 12 ------- .../Scripts/translations/arrayHelper.ts | 32 ++----------------- .../Scripts/translations/dictionaryHelper.ts | 8 ++--- .../Binding/JavascriptCompilationTests.cs | 12 +++---- 5 files changed, 26 insertions(+), 60 deletions(-) diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index d53d1636ce..fbbf532109 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -562,18 +562,24 @@ string GetDelegateReturnTypeHash(Type type) AddMethodTranslator(() => ImmutableArrayExtensions.ElementAtOrDefault(default(ImmutableArray), 0), new GenericMethodCompiler((args, method) => BuildIndexer(args[1], args[2], method))); - AddMethodTranslator(() => Enumerable.Empty().FirstOrDefault(), new GenericMethodCompiler((args, m) => BuildIndexer(args[1], new JsLiteral(0), m))); - AddMethodTranslator(() => ImmutableArrayExtensions.FirstOrDefault(default(ImmutableArray)), new GenericMethodCompiler((args, m) => BuildIndexer(args[1], new JsLiteral(0), m))); + var firstOrDefault = new GenericMethodCompiler((args, m) => BuildIndexer(args[1], new JsLiteral(0), m).WithAnnotation(MayBeNullAnnotation.Instance)); + AddMethodTranslator(() => Enumerable.Empty().FirstOrDefault(), firstOrDefault); + AddMethodTranslator(() => Enumerable.Empty().First(), firstOrDefault); + AddMethodTranslator(() => ImmutableArrayExtensions.FirstOrDefault(default(ImmutableArray)), firstOrDefault); + AddMethodTranslator(() => ImmutableArrayExtensions.First(default(ImmutableArray)), firstOrDefault); + var firstOrDefaultPred = new GenericMethodCompiler(args => - new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("firstOrDefault").Invoke(args[1], args[2]).WithAnnotation(MayBeNullAnnotation.Instance)); + args[1].Member("find").Invoke(args[2]).WithAnnotation(MayBeNullAnnotation.Instance)); AddMethodTranslator(() => Enumerable.Empty().FirstOrDefault(_ => true), firstOrDefaultPred); + AddMethodTranslator(() => Enumerable.Empty().First(_ => true), firstOrDefaultPred); AddMethodTranslator(() => ImmutableArrayExtensions.FirstOrDefault(default(ImmutableArray), _ => true), firstOrDefaultPred); + AddMethodTranslator(() => ImmutableArrayExtensions.First(default(ImmutableArray), _ => true), firstOrDefaultPred); var lastOrDefault = new GenericMethodCompiler(args => args[1].Member("at").Invoke(new JsLiteral(-1)).WithAnnotation(MayBeNullAnnotation.Instance)); AddMethodTranslator(() => Enumerable.Empty().LastOrDefault(), lastOrDefault); AddMethodTranslator(() => ImmutableArrayExtensions.LastOrDefault(default(ImmutableArray)), lastOrDefault); var lastOrDefaultPred = new GenericMethodCompiler(args => - new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("lastOrDefault").Invoke(args[1], args[2]).WithAnnotation(MayBeNullAnnotation.Instance)); + args[1].Member("findLast").Invoke(args[2]).WithAnnotation(MayBeNullAnnotation.Instance)); AddMethodTranslator(() => Enumerable.Empty().LastOrDefault(_ => false), lastOrDefaultPred); AddMethodTranslator(() => ImmutableArrayExtensions.LastOrDefault(default(ImmutableArray), _ => false), lastOrDefaultPred); @@ -631,12 +637,16 @@ private void AddDefaultNumericEnumerableTranslations() if (m.Name is "Max" or "Min" && parameters.Length == 1 && itemType.UnwrapNullableType().IsNumericType()) { AddMethodTranslator(m, new GenericMethodCompiler(args => - new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member(m.Name is "Min" ? "min" : "max").Invoke(args[1], selectIdentityFunc.Clone(), new JsLiteral(!itemType.IsNullable())))); + new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member(m.Name is "Min" ? "min" : "max").Invoke(args[1], selectIdentityFunc.Clone()) + .WithAnnotation(MayBeNullAnnotation.Instance) + )); } else if (m.Name is "Max" or "Min" && parameters.Length == 2 && selectorResultType?.UnwrapNullableType().IsNumericType() == true) { AddMethodTranslator(m, new GenericMethodCompiler(args => - new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member(m.Name is "Min" ? "min" : "max").Invoke(args[1], args[2], new JsLiteral(!selectorResultType.IsNullable())))); + new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member(m.Name is "Min" ? "min" : "max").Invoke(args[1], args[2]) + .WithAnnotation(MayBeNullAnnotation.Instance) + )); } else if (m.Name is "Sum" && parameters.Length == 1 && itemType.UnwrapNullableType().IsNumericType()) diff --git a/src/Framework/Framework/Resources/Scripts/tests/arrayHelper.test.ts b/src/Framework/Framework/Resources/Scripts/tests/arrayHelper.test.ts index 8e6f231373..d42dc26972 100644 --- a/src/Framework/Framework/Resources/Scripts/tests/arrayHelper.test.ts +++ b/src/Framework/Framework/Resources/Scripts/tests/arrayHelper.test.ts @@ -140,18 +140,6 @@ test("ListExtensions::RemoveLast", () => { expect(vm.Array()[3]().Id()).toBe(4); }) -test("Enumerable::FirstOrDefault", () => { - prepareArray(); - expect(arrayHelper.firstOrDefault(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) % 2 !== 0 }).Id()).toBe(1); - expect(arrayHelper.firstOrDefault(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) % 2 === 0 }).Id()).toBe(2); -}) - -test("Enumerable::LastOrDefault", () => { - prepareArray(); - expect(arrayHelper.lastOrDefault(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) % 2 !== 0 }).Id()).toBe(5); - expect(arrayHelper.lastOrDefault(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) % 2 === 0 }).Id()).toBe(4); -}) - test("Enumerable::Max", () => { prepareArray(); expect(arrayHelper.max(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, true)).toBe(5); diff --git a/src/Framework/Framework/Resources/Scripts/translations/arrayHelper.ts b/src/Framework/Framework/Resources/Scripts/translations/arrayHelper.ts index 27eab10c82..16d312b4ab 100644 --- a/src/Framework/Framework/Resources/Scripts/translations/arrayHelper.ts +++ b/src/Framework/Framework/Resources/Scripts/translations/arrayHelper.ts @@ -7,10 +7,8 @@ export { clear, distinct, contains, - firstOrDefault, insert, insertRange, - lastOrDefault, max, min, orderBy, @@ -70,16 +68,6 @@ function contains(array: T[], value: T): boolean { return array.map(e => ko.unwrap(e)).includes(value); } -function firstOrDefault(array: T[], predicate: (s: T) => boolean): T | null { - for (const item of array) { - const itemUnwrapped = ko.unwrap(item) - if (predicate(itemUnwrapped)) { - return itemUnwrapped - } - } - return null; -} - function insert(observable: any, index: number, element: T): void { let array = Array.from(observable.state); array.splice(index, 0, element); @@ -92,21 +80,8 @@ function insertRange(observable: any, index: number, elements: T[]): void { observable.setState(array); } -function lastOrDefault(array: T[], predicate: (s: T) => boolean): T | null { - for (let i = array.length - 1; i >= 0; i--) { - const itemUnwrapped = ko.unwrap(array[i]) - if (predicate(itemUnwrapped)) { - return itemUnwrapped - } - } - return null; -} - -function max(array: T[], selector: (item: T) => number, throwIfEmpty: boolean): number | null { +function max(array: T[], selector: (item: T) => number): number | null { if (array.length === 0) { - if (throwIfEmpty) { - throw new Error("Source is empty! Max operation cannot be performed."); - } return null; } @@ -119,11 +94,8 @@ function max(array: T[], selector: (item: T) => number, throwIfEmpty: boolean return max; } -function min(array: T[], selector: (item: T) => number, throwIfEmpty: boolean): number | null { +function min(array: T[], selector: (item: T) => number): number | null { if (array.length === 0) { - if (throwIfEmpty) { - throw new Error("Source is empty! Min operation cannot be performed."); - } return null; } diff --git a/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts b/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts index d19e7671b7..a48459256d 100644 --- a/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts +++ b/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts @@ -8,14 +8,10 @@ export function containsKey(dictionary: Dictionary, iden return getKeyValueIndex(dictionary, identifier) !== null; } -export function getItem(dictionary: Dictionary, identifier: Key, defaultValue?: Value): Value { +export function getItem(dictionary: Dictionary, identifier: Key, defaultValue?: Value): Value | undefined { const index = getKeyValueIndex(dictionary, identifier); if (index === null) { - if (defaultValue !== undefined) { - return defaultValue; - } else { - throw Error("Provided key \"" + identifier + "\" is not present in the dictionary!"); - } + return defaultValue; } return ko.unwrap(ko.unwrap(dictionary[index]).Value); diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index 561e1f7921..d387558513 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -719,7 +719,7 @@ public void JsTranslator_EnumerableFirstOrDefault(string binding) public void JsTranslator_EnumerableFirstOrDefaultParametrized(string binding) { var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) }); - Assert.AreEqual("dotvvm.translations.array.firstOrDefault(LongArray(),(item)=>ko.unwrap(item)>0)", result); + Assert.AreEqual("LongArray().find((item)=>ko.unwrap(item)>0)", result); } [TestMethod] @@ -770,7 +770,7 @@ public void JsTranslator_EnumerableLastOrDefault(string binding) public void JsTranslator_EnumerableLastOrDefaultParametrized(string binding) { var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) }); - Assert.AreEqual("dotvvm.translations.array.lastOrDefault(LongArray(),(item)=>ko.unwrap(item)>0)", result); + Assert.AreEqual("LongArray().findLast((item)=>ko.unwrap(item)>0)", result); } [TestMethod] @@ -806,7 +806,7 @@ public void JsTranslator_EnumerableDistinct_NonPrimitiveTypesThrows(string bindi public void JsTranslator_EnumerableMax(string binding, string property, bool nullable) { var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestArraysViewModel) }); - Assert.AreEqual($"dotvvm.translations.array.max({property}(),(arg)=>ko.unwrap(arg),{(!nullable).ToString().ToLowerInvariant()})", result); + Assert.AreEqual($"dotvvm.translations.array.max({property}(),(arg)=>ko.unwrap(arg))", result); } [TestMethod] @@ -833,7 +833,7 @@ public void JsTranslator_EnumerableMax(string binding, string property, bool nul public void JsTranslator_EnumerableMax_WithSelector(string binding, string property, bool nullable) { var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestArraysViewModel) }); - Assert.AreEqual($"dotvvm.translations.array.max({property}(),(item)=>-ko.unwrap(item),{(!nullable).ToString().ToLowerInvariant()})", result); + Assert.AreEqual($"dotvvm.translations.array.max({property}(),(item)=>-ko.unwrap(item))", result); } [TestMethod] @@ -860,7 +860,7 @@ public void JsTranslator_EnumerableMax_WithSelector(string binding, string prope public void JsTranslator_EnumerableMin(string binding, string property, bool nullable) { var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestArraysViewModel) }); - Assert.AreEqual($"dotvvm.translations.array.min({property}(),(arg)=>ko.unwrap(arg),{(!nullable).ToString().ToLowerInvariant()})", result); + Assert.AreEqual($"dotvvm.translations.array.min({property}(),(arg)=>ko.unwrap(arg))", result); } [TestMethod] @@ -887,7 +887,7 @@ public void JsTranslator_EnumerableMin(string binding, string property, bool nul public void JsTranslator_EnumerableMin_WithSelector(string binding, string property, bool nullable) { var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestArraysViewModel) }); - Assert.AreEqual($"dotvvm.translations.array.min({property}(),(item)=>-ko.unwrap(item),{(!nullable).ToString().ToLowerInvariant()})", result); + Assert.AreEqual($"dotvvm.translations.array.min({property}(),(item)=>-ko.unwrap(item))", result); } [DataRow("Int32Array")] From 72c1cb8186ddecaa2f33bbd4dccd441d22135061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Mon, 18 Mar 2024 12:47:22 +0100 Subject: [PATCH 2/2] JS: Fix array sorting of enums and nulls --- .../Resources/Scripts/metadata/enums.ts | 5 +- .../Scripts/tests/arrayHelper.test.ts | 62 ++++++++++++++++--- .../Scripts/tests/stateManagement.data.ts | 14 +++++ .../Scripts/translations/sortingHelper.ts | 32 +++++----- .../Resources/Scripts/utils/isNumber.ts | 2 +- 5 files changed, 87 insertions(+), 28 deletions(-) diff --git a/src/Framework/Framework/Resources/Scripts/metadata/enums.ts b/src/Framework/Framework/Resources/Scripts/metadata/enums.ts index c145e81ad6..6b339c29c5 100644 --- a/src/Framework/Framework/Resources/Scripts/metadata/enums.ts +++ b/src/Framework/Framework/Resources/Scripts/metadata/enums.ts @@ -2,8 +2,11 @@ import { CoerceError } from "../shared-classes" import { isNumber } from "../utils/isNumber" import { keys } from "../utils/objects" -export function enumStringToInt(value: number | string, type: EnumTypeMetadata): number | null { +export function enumStringToInt(value: number | string | null | undefined, type: EnumTypeMetadata): number | null { // if it's number already, just return it + if (value == null) { + return null + } if (isNumber(value)) { return +value } diff --git a/src/Framework/Framework/Resources/Scripts/tests/arrayHelper.test.ts b/src/Framework/Framework/Resources/Scripts/tests/arrayHelper.test.ts index d42dc26972..b1f69c8f89 100644 --- a/src/Framework/Framework/Resources/Scripts/tests/arrayHelper.test.ts +++ b/src/Framework/Framework/Resources/Scripts/tests/arrayHelper.test.ts @@ -142,29 +142,25 @@ test("ListExtensions::RemoveLast", () => { test("Enumerable::Max", () => { prepareArray(); - expect(arrayHelper.max(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, true)).toBe(5); - expect(arrayHelper.max(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, false)).toBe(5); + expect(arrayHelper.max(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) })).toBe(5); arrayHelper.clear(vm.Array); s.doUpdateNow(); - expect(() => arrayHelper.max(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, true)).toThrowError("Source is empty! Max operation cannot be performed."); - expect(arrayHelper.max(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, false)).toBe(null); + expect(arrayHelper.max(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) })).toBe(null); }) test("Enumerable::Min", () => { prepareArray(); - expect(arrayHelper.min(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, true)).toBe(1); - expect(arrayHelper.min(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, false)).toBe(1); + expect(arrayHelper.min(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) })).toBe(1); arrayHelper.clear(vm.Array); s.doUpdateNow(); - expect(() => arrayHelper.min(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, true)).toThrowError("Source is empty! Min operation cannot be performed."); - expect(arrayHelper.min(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, false)).toBe(null); + expect(arrayHelper.min(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) })).toBe(null); }) test("Enumerable::OrderBy", () => { prepareArray(); - const result = arrayHelper.orderBy(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, "t2"); + const result = arrayHelper.orderBy(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, null); expect(result.length).toBe(5); expect(result[0]().Id()).toBe(1); expect(result[1]().Id()).toBe(2); @@ -175,7 +171,7 @@ test("Enumerable::OrderBy", () => { test("Enumerable::OrderByDescending", () => { prepareArray(); - const result = arrayHelper.orderByDesc(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, "t2"); + const result = arrayHelper.orderByDesc(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) }, null); expect(result.length).toBe(5); expect(result[0]().Id()).toBe(5); expect(result[1]().Id()).toBe(4); @@ -184,6 +180,52 @@ test("Enumerable::OrderByDescending", () => { expect(result[4]().Id()).toBe(1); }) +test("Enumerable::OrderBy with nulls", () => { + vm.Array.setState([ { Id: -1 }, { Id: 4 }, { Id: 0 }, { Id: 3 } ]) + s.doUpdateNow() + const result = arrayHelper.orderBy(vm.Array(), function (arg: any) { return ko.unwrap(ko.unwrap(arg).Id) || null }, null); + expect(result.length).toBe(4); + expect(result[0]().Id()).toBe(0); + expect(result[1]().Id()).toBe(-1); + expect(result[2]().Id()).toBe(3); + expect(result[3]().Id()).toBe(4); +}) + +test("Enumerable::OrderBy with enums", () => { + vm.Enums.setState([ 'D', 'A', 0, 'A, D', 'B', 123432 ]) + s.doUpdateNow() + const result = arrayHelper.orderBy(vm.Enums(), function (arg: any) { return ko.unwrap(arg) }, "e1"); + expect(result.length).toBe(6); + expect(result[0]()).toBe(0); + expect(result[1]()).toBe('A'); + expect(result[2]()).toBe('B'); + expect(result[3]()).toBe('D'); + expect(result[4]()).toBe('A,D'); + expect(result[5]()).toBe(123432); +}) + +test("Enumerable::OrderBy stable", () => { + prepareArray() + const result = arrayHelper.orderBy(vm.Array(), function (arg: any) { return 1 }, null); + expect(result.length).toBe(5); + expect(result[0]().Id()).toBe(1); + expect(result[1]().Id()).toBe(2); + expect(result[2]().Id()).toBe(3); + expect(result[3]().Id()).toBe(4); + expect(result[4]().Id()).toBe(5); +}) +test("Enumerable::OrderByDescending stable", () => { + prepareArray() + const result = arrayHelper.orderBy(vm.Array(), function (arg: any) { return 1 }, null); + expect(result.length).toBe(5); + expect(result[0]().Id()).toBe(1); + expect(result[1]().Id()).toBe(2); + expect(result[2]().Id()).toBe(3); + expect(result[3]().Id()).toBe(4); + expect(result[4]().Id()).toBe(5); +}) + + function prepareArray() { arrayHelper.clear(vm.Array); arrayHelper.add(vm.Array, { Id: 1 }); diff --git a/src/Framework/Framework/Resources/Scripts/tests/stateManagement.data.ts b/src/Framework/Framework/Resources/Scripts/tests/stateManagement.data.ts index bfb98e08c6..95e55e9a48 100644 --- a/src/Framework/Framework/Resources/Scripts/tests/stateManagement.data.ts +++ b/src/Framework/Framework/Resources/Scripts/tests/stateManagement.data.ts @@ -9,6 +9,7 @@ initDotvvm({ $type: "t2", Id: 1 }], + Enums: [ "A", "B", "A, B" ], ArrayWillBe: null, Inner: { $type: "t3", @@ -33,6 +34,9 @@ initDotvvm({ "t2" ] }, + Enums: { + type: [ "e1" ] + }, ArrayWillBe: { type: [ "t5" @@ -101,6 +105,16 @@ initDotvvm({ type: "String" } } + }, + e1: { + type: "enum", + isFlags: true, + values: { + "A": 1, + "B": 2, + "C": 4, + "D": 8, + } } } }) diff --git a/src/Framework/Framework/Resources/Scripts/translations/sortingHelper.ts b/src/Framework/Framework/Resources/Scripts/translations/sortingHelper.ts index 2afef0b914..015aad0efb 100644 --- a/src/Framework/Framework/Resources/Scripts/translations/sortingHelper.ts +++ b/src/Framework/Framework/Resources/Scripts/translations/sortingHelper.ts @@ -1,32 +1,31 @@ -import { getTypeInfo } from "../metadata/typeMap"; +import { enumStringToInt } from "../metadata/enums"; +import { getTypeInfo } from "../metadata/typeMap"; type ElementType = string | number | boolean; -export const orderBy = (array: T[], selector: (item: T) => ElementType, typeId: string) => +export const orderBy = (array: T[], selector: (item: T) => ElementType, typeId: string | null) => orderByImpl(array, selector, getComparer(typeId, true)) -export const orderByDesc = (array: T[], selector: (item: T) => ElementType, typeId: string) => +export const orderByDesc = (array: T[], selector: (item: T) => ElementType, typeId: string | null) => orderByImpl(array, selector, getComparer(typeId, false)) function orderByImpl(array: T[], selector: (item: T) => ElementType, compare: (first: ElementType, second: ElementType) => number): T[] { if ((!array || array.length < 2)) return array; - return array - .map((item, index) => ({ item, index })) - .sort((first, second) => compare(ko.unwrap(selector(first.item)), ko.unwrap(selector(second.item))) || first.index - second.index) - .map(({ item }) => item); + // JS sort is stable: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#sort_stability + array = Array.from(array) + array.sort((first, second) => compare(ko.unwrap(selector(first)), ko.unwrap(selector(second)))) + return array; } function getComparer(typeId: string | null, ascending: boolean): (first: ElementType, second: ElementType) => number { - let metadataInfo = (typeId !== null) ? getTypeInfo(typeId) : null; - if (metadataInfo !== null && metadataInfo.type === "enum") { + const metadataInfo = (typeId != null) ? getTypeInfo(typeId) : null; + if (metadataInfo?.type === "enum") { // Enums should be compared based on their underlying primitive values // This is the same behaviour as used by .NET - let enumMetadataInfo = metadataInfo as EnumTypeMetadata; + const enumMetadataInfo = metadataInfo as EnumTypeMetadata; return function (first: ElementType, second: ElementType) { - let firstNumeric = (typeof (first) === "number") ? first : enumMetadataInfo.values[first as string]; - let secondNumeric = (typeof (second) === "number") ? second : enumMetadataInfo.values[second as string]; - return defaultPrimitivesComparer(firstNumeric, secondNumeric, ascending); + return defaultPrimitivesComparer(enumStringToInt(first as any, enumMetadataInfo), enumStringToInt(second as any, enumMetadataInfo), ascending); } } else { @@ -37,7 +36,8 @@ function getComparer(typeId: string | null, ascending: boolean): (first: Element } } -const defaultPrimitivesComparer = (first: ElementType, second: ElementType, ascending: boolean) => { - let comparision = (first < second) ? -1 : (first == second) ? 0 : 1; - return (ascending) ? comparision : comparision * -1; +const defaultPrimitivesComparer = (first: ElementType | null | undefined, second: ElementType | null | undefined, ascending: boolean) => { + // nulls are first in ascending order in .NET + const comparison = (first == second) ? 0 : (first == null || second != null && first < second) ? -1 : 1; + return (ascending) ? comparison : comparison * -1; } diff --git a/src/Framework/Framework/Resources/Scripts/utils/isNumber.ts b/src/Framework/Framework/Resources/Scripts/utils/isNumber.ts index c7bd2467e4..4bcc76ef94 100644 --- a/src/Framework/Framework/Resources/Scripts/utils/isNumber.ts +++ b/src/Framework/Framework/Resources/Scripts/utils/isNumber.ts @@ -3,7 +3,7 @@ export function isNumber(value: any): boolean { // Number(value) returns NaN for anything which is not a number, except for empty/whitespace string which is converted to 0 🤦 // parseFloat correctly fails for empty string, but parseFloat("10dddd") returns 10, not NaN - // so value is number of Number(value) and parseFloat(value) return the same number + // so value is number if Number(value) and parseFloat(value) return the same number // if both return NaN, === is false, finally this "feature" is useful for something :D return parseFloat(value) === Number(value); }