From 1a26fc2762dee366d2ce7ccf24e173cdc761c70c Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 20 Mar 2025 10:52:51 +0100 Subject: [PATCH] fix(editor): Add smart decimals directive (#14054) --- .../utils/src/number/smartDecimal.test.ts | 35 +++ .../@n8n/utils/src/number/smartDecimal.ts | 13 + .../src/directives/n8n-smart-decimal.test.ts | 245 ++++++++++++++++++ .../src/directives/n8n-smart-decimal.ts | 37 +++ 4 files changed, 330 insertions(+) create mode 100644 packages/@n8n/utils/src/number/smartDecimal.test.ts create mode 100644 packages/@n8n/utils/src/number/smartDecimal.ts create mode 100644 packages/frontend/@n8n/design-system/src/directives/n8n-smart-decimal.test.ts create mode 100644 packages/frontend/@n8n/design-system/src/directives/n8n-smart-decimal.ts diff --git a/packages/@n8n/utils/src/number/smartDecimal.test.ts b/packages/@n8n/utils/src/number/smartDecimal.test.ts new file mode 100644 index 0000000000..5a6e1cde44 --- /dev/null +++ b/packages/@n8n/utils/src/number/smartDecimal.test.ts @@ -0,0 +1,35 @@ +import { smartDecimal } from './smartDecimal'; + +describe('smartDecimal', () => { + it('should return the same value if it is an integer', () => { + expect(smartDecimal(42)).toBe(42); + }); + + it('should return the same value if it has only one decimal place', () => { + expect(smartDecimal(42.5)).toBe(42.5); + }); + + it('should round to two decimal places by default', () => { + expect(smartDecimal(42.567)).toBe(42.57); + }); + + it('should round to the specified number of decimal places', () => { + expect(smartDecimal(42.567, 1)).toBe(42.6); + }); + + it('should handle negative numbers correctly', () => { + expect(smartDecimal(-42.567, 2)).toBe(-42.57); + }); + + it('should handle zero correctly', () => { + expect(smartDecimal(0)).toBe(0); + }); + + it('should handle very small numbers correctly', () => { + expect(smartDecimal(0.000567, 5)).toBe(0.00057); + }); + + it('should round to two decimal if it is smaller than the given one', () => { + expect(smartDecimal(42.56, 3)).toBe(42.56); + }); +}); diff --git a/packages/@n8n/utils/src/number/smartDecimal.ts b/packages/@n8n/utils/src/number/smartDecimal.ts new file mode 100644 index 0000000000..2682e570ec --- /dev/null +++ b/packages/@n8n/utils/src/number/smartDecimal.ts @@ -0,0 +1,13 @@ +export const smartDecimal = (value: number, decimals = 2): number => { + // Check if integer + if (Number.isInteger(value)) { + return value; + } + + // Check if it has only one decimal place + if (value.toString().split('.')[1].length <= decimals) { + return value; + } + + return Number(value.toFixed(decimals)); +}; diff --git a/packages/frontend/@n8n/design-system/src/directives/n8n-smart-decimal.test.ts b/packages/frontend/@n8n/design-system/src/directives/n8n-smart-decimal.test.ts new file mode 100644 index 0000000000..2071ddcf2b --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/directives/n8n-smart-decimal.test.ts @@ -0,0 +1,245 @@ +import { render } from '@testing-library/vue'; + +import { n8nSmartDecimal } from './n8n-smart-decimal'; + +describe('Directive n8n-truncate', () => { + it('should leave number as is without decimals', async () => { + const { html } = render( + { + props: { + text: { + type: String, + }, + }, + template: '

{{text}}

', + }, + { + props: { + text: '42', + }, + global: { + directives: { + n8nSmartDecimal, + }, + }, + }, + ); + expect(html()).toBe('

42

'); + }); + + it('should leave number as is without decimals with binding arg', async () => { + const { html } = render( + { + props: { + text: { + type: String, + }, + }, + template: '

{{text}}

', + }, + { + props: { + text: '42', + }, + global: { + directives: { + n8nSmartDecimal, + }, + }, + }, + ); + expect(html()).toBe('

42

'); + }); + + it('should leave the number with 1 decimal', async () => { + const { html } = render( + { + props: { + text: { + type: String, + }, + }, + template: '

{{text}}

', + }, + { + props: { + text: '42.1', + }, + global: { + directives: { + n8nSmartDecimal, + }, + }, + }, + ); + expect(html()).toBe('

42.1

'); + }); + + it('should format number to 2 decimal places by default', async () => { + const { html } = render( + { + props: { + text: { + type: String, + }, + }, + template: '

{{text}}

', + }, + { + props: { + text: '42.123456', + }, + global: { + directives: { + n8nSmartDecimal, + }, + }, + }, + ); + expect(html()).toBe('

42.12

'); + }); + + it('should format number to 1 decimal place', async () => { + const { html } = render( + { + props: { + text: { + type: String, + }, + }, + template: '

{{text}}

', + }, + { + props: { + text: '42.123456', + }, + global: { + directives: { + n8nSmartDecimal, + }, + }, + }, + ); + expect(html()).toBe('

42.1

'); + }); + + it('should format number to 3 decimal places', async () => { + const { html } = render( + { + props: { + text: { + type: String, + }, + }, + template: '

{{text}}

', + }, + { + props: { + text: '42.123456', + }, + global: { + directives: { + n8nSmartDecimal, + }, + }, + }, + ); + expect(html()).toBe('

42.123

'); + }); + + it('should handle negative numbers correctly', () => { + const { html } = render( + { + props: { + text: { + type: String, + }, + }, + template: '

{{text}}

', + }, + { + props: { + text: '-42.123456', + }, + global: { + directives: { + n8nSmartDecimal, + }, + }, + }, + ); + expect(html()).toBe('

-42.12

'); + }); + + it('should handle zero correctly', () => { + const { html } = render( + { + props: { + text: { + type: String, + }, + }, + template: '

{{text}}

', + }, + { + props: { + text: '0', + }, + global: { + directives: { + n8nSmartDecimal, + }, + }, + }, + ); + expect(html()).toBe('

0

'); + }); + + it('should handle very small numbers correctly', () => { + const { html } = render( + { + props: { + text: { + type: String, + }, + }, + template: '

{{text}}

', + }, + { + props: { + text: '0.000567', + }, + global: { + directives: { + n8nSmartDecimal, + }, + }, + }, + ); + expect(html()).toBe('

0.00057

'); + }); + + it('should format number to 2 decimal places if that has fewer decimals than the desired', async () => { + const { html } = render( + { + props: { + text: { + type: String, + }, + }, + template: '

{{text}}

', + }, + { + props: { + text: '42.12', + }, + global: { + directives: { + n8nSmartDecimal, + }, + }, + }, + ); + expect(html()).toBe('

42.12

'); + }); +}); diff --git a/packages/frontend/@n8n/design-system/src/directives/n8n-smart-decimal.ts b/packages/frontend/@n8n/design-system/src/directives/n8n-smart-decimal.ts new file mode 100644 index 0000000000..13f4e72b89 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/directives/n8n-smart-decimal.ts @@ -0,0 +1,37 @@ +import { smartDecimal } from '@n8n/utils/number/smartDecimal'; +import type { DirectiveBinding, FunctionDirective } from 'vue'; + +/** + * Custom directive `n8nSmartDecimal` to format numbers with smart decimal places. + * + * Usage: + * In your Vue template, use the directive `v-n8n-smart-decimal` passing the number and optionally the decimal places. + * + * Example: + *

42.567

+ * + * Compiles to:

42.57

+ * + * Example with specified decimal places: + *

42.56789

+ * + * Compiles to:

42.5679

+ * + * Function Shorthand: + * https://vuejs.org/guide/reusability/custom-directives#function-shorthand + * + * Hint: Do not use it on components + * https://vuejs.org/guide/reusability/custom-directives#usage-on-components + */ + +export const n8nSmartDecimal: FunctionDirective = ( + el: HTMLElement, + binding: DirectiveBinding, +) => { + const value = parseFloat(el.textContent ?? ''); + if (!isNaN(value)) { + const decimals = isNaN(Number(binding.arg)) ? undefined : Number(binding.arg); + const formattedValue = smartDecimal(value, decimals); + el.textContent = formattedValue.toString(); + } +};