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