From 9c628ab83a20d6ad0e1d32dcb48790efda449cf5 Mon Sep 17 00:00:00 2001 From: Julien Vannier Date: Wed, 5 Aug 2026 15:05:02 +0200 Subject: [PATCH 1/2] Promote OrderValue CellRenderer as Hypertable money type --- README.md | 2 + .../hyper-table-v2/cell-renderers/amount.hbs | 7 ++ .../hyper-table-v2/cell-renderers/amount.ts | 22 ++++++ addon/core/rendering-resolver.ts | 5 ++ .../hyper-table-v2/cell-renderers/amount.js | 1 + .../cell-renderers/amount-test.ts | 72 +++++++++++++++++++ tests/unit/core/rendering-resolver-test.ts | 22 ++++++ 7 files changed, 131 insertions(+) create mode 100644 addon/components/hyper-table-v2/cell-renderers/amount.hbs create mode 100644 addon/components/hyper-table-v2/cell-renderers/amount.ts create mode 100644 app/components/hyper-table-v2/cell-renderers/amount.js create mode 100644 tests/integration/components/hyper-table-v2/cell-renderers/amount-test.ts diff --git a/README.md b/README.md index fd5862e9..e6509232 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ If no RenderingResolver is provided, Hypertable uses a default resolver with bui - `integer` type: properly formats numbers in cells and brings range-based filtering - `timestamp` type: properly displays dates and brings calendar-based filtering +- `money` type: properly formats amounts expressed in cents and brings range-based filtering - a default `text` renderer to display textual content and filter using a typeahead input The Rendering Resolver extends BaseRenderingResolver from `@upfluence/hypertable/core/rendering-resolver` and determines which component should be used to render each cell, filter, and header according to column key. @@ -361,6 +362,7 @@ Hypertable includes built-in renderers for common data types: - **text** - Basic text with ellipsis and tooltip - **numeric** - Formatted numbers - **date** - Formatted dates +- **amount** - Formatted monetary amounts, expecting a `{ cents, currency }` value #### Filter Renderers diff --git a/addon/components/hyper-table-v2/cell-renderers/amount.hbs b/addon/components/hyper-table-v2/cell-renderers/amount.hbs new file mode 100644 index 00000000..e32de600 --- /dev/null +++ b/addon/components/hyper-table-v2/cell-renderers/amount.hbs @@ -0,0 +1,7 @@ +
+ {{#if this.value}} + {{format-money this.value.cents this.value.currency "cents"}} + {{else}} + — + {{/if}} +
\ No newline at end of file diff --git a/addon/components/hyper-table-v2/cell-renderers/amount.ts b/addon/components/hyper-table-v2/cell-renderers/amount.ts new file mode 100644 index 00000000..3d392387 --- /dev/null +++ b/addon/components/hyper-table-v2/cell-renderers/amount.ts @@ -0,0 +1,22 @@ +import Component from '@glimmer/component'; + +import TableHandler from '@upfluence/hypertable/core/handler'; +import { Column, Row } from '@upfluence/hypertable/core/interfaces'; + +interface HyperTableV2CellRenderersAmountArgs { + handler: TableHandler; + column: Column; + row: Row; + extra?: { [key: string]: any }; +} + +export type Amount = { + cents: number; + currency: string; +}; + +export default class HyperTableV2CellRenderersAmount extends Component { + get value(): Amount | undefined { + return this.args.row[this.args.column.definition.key]; + } +} diff --git a/addon/core/rendering-resolver.ts b/addon/core/rendering-resolver.ts index e6152d6b..2ccdd602 100644 --- a/addon/core/rendering-resolver.ts +++ b/addon/core/rendering-resolver.ts @@ -1,6 +1,7 @@ import { ensureSafeComponent } from '@embroider/util'; import GlimmerComponent from '@glimmer/component'; +import AmountCellRenderer from '@upfluence/hypertable/components/hyper-table-v2/cell-renderers/amount'; import DateCellRenderer from '@upfluence/hypertable/components/hyper-table-v2/cell-renderers/date'; import NumericCellRenderer from '@upfluence/hypertable/components/hyper-table-v2/cell-renderers/numeric'; import TextCellRenderer from '@upfluence/hypertable/components/hyper-table-v2/cell-renderers/text'; @@ -21,6 +22,10 @@ const rendererMatchers: { [key: string]: RendererDictionaryItem } = { cell: NumericCellRenderer, filter: NumericFilteringRenderer }, + money: { + cell: AmountCellRenderer, + filter: NumericFilteringRenderer + }, timestamp: { cell: DateCellRenderer, filter: DateFilteringRenderer diff --git a/app/components/hyper-table-v2/cell-renderers/amount.js b/app/components/hyper-table-v2/cell-renderers/amount.js new file mode 100644 index 00000000..e0500834 --- /dev/null +++ b/app/components/hyper-table-v2/cell-renderers/amount.js @@ -0,0 +1 @@ +export { default } from '@upfluence/hypertable/components/hyper-table-v2/cell-renderers/amount'; diff --git a/tests/integration/components/hyper-table-v2/cell-renderers/amount-test.ts b/tests/integration/components/hyper-table-v2/cell-renderers/amount-test.ts new file mode 100644 index 00000000..32e86ce3 --- /dev/null +++ b/tests/integration/components/hyper-table-v2/cell-renderers/amount-test.ts @@ -0,0 +1,72 @@ +import { render, type TestContext } from '@ember/test-helpers'; + +import { setupRenderingTest } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import { module, test } from 'qunit'; + +import TableHandler from '@upfluence/hypertable/core/handler'; +import { TableManager, RowsFetcher } from '@upfluence/hypertable/test-support'; +import { buildColumn } from '@upfluence/hypertable/test-support/table-manager'; + +const CELL_SELECTOR = '.fx-row.fx-1.fx-malign-end'; + +module('Integration | Component | hyper-table-v2/cell-renderers/amount', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function (this: TestContext) { + this.tableManager = new TableManager(); + this.rowsFetcher = new RowsFetcher(); + this.handler = new TableHandler(this, this.tableManager, this.rowsFetcher); + this.column = buildColumn('amount', { type: 'money' }); + }); + + test('it renders the amount converted from cents', async function (this: TestContext, assert) { + this.row = { amount: { cents: 10000, currency: 'USD' } }; + + await render( + hbs`` + ); + + assert.dom(CELL_SELECTOR).hasText('$100'); + }); + + test('it renders the amount with the currency of the value', async function (this: TestContext, assert) { + this.row = { amount: { cents: 10000, currency: 'EUR' } }; + + await render( + hbs`` + ); + + assert.dom(CELL_SELECTOR).hasText('€100'); + }); + + test('it renders a zero amount', async function (this: TestContext, assert) { + this.row = { amount: { cents: 0, currency: 'USD' } }; + + await render( + hbs`` + ); + + assert.dom(CELL_SELECTOR).hasText('$0'); + }); + + test('it renders the empty state when the value is null', async function (this: TestContext, assert) { + this.row = { amount: null }; + + await render( + hbs`` + ); + + assert.dom(CELL_SELECTOR).hasText('—'); + }); + + test('it renders the empty state when the key is missing from the row', async function (this: TestContext, assert) { + this.row = {}; + + await render( + hbs`` + ); + + assert.dom(CELL_SELECTOR).hasText('—'); + }); +}); diff --git a/tests/unit/core/rendering-resolver-test.ts b/tests/unit/core/rendering-resolver-test.ts index eb319ffb..1663ab2f 100644 --- a/tests/unit/core/rendering-resolver-test.ts +++ b/tests/unit/core/rendering-resolver-test.ts @@ -4,6 +4,8 @@ import { ensureSafeComponent } from '@embroider/util'; import { setupApplicationTest } from 'ember-qunit'; import { module, test } from 'qunit'; +import AmountCellRenderer from '@upfluence/hypertable/components/hyper-table-v2/cell-renderers/amount'; +import NumericFilteringRenderer from '@upfluence/hypertable/components/hyper-table-v2/filtering-renderers/numeric'; import BaseHeaderRenderer from '@upfluence/hypertable/components/hyper-table-v2/header-renderers/base'; import BaseRenderingResolver from '@upfluence/hypertable/core/rendering-resolver'; import { buildColumnDefinition } from '@upfluence/hypertable/test-support/table-manager'; @@ -23,4 +25,24 @@ module('Unit | core/rendering-resolver', function (hooks) { assert.deepEqual(resolved.component, ensureSafeComponent(BaseHeaderRenderer, getContext())); assert.equal(resolved.args, undefined); }); + + test('it returns the amount cell rendering component for a money column', async function (assert: Assert) { + const renderingResolver = new BaseRenderingResolver(getContext()); + const resolved = await renderingResolver.lookupCellComponent(buildColumnDefinition('amount', { type: 'money' })); + assert.deepEqual(resolved.component, ensureSafeComponent(AmountCellRenderer, getContext())); + }); + + test('it returns the numeric filtering component for a money column', async function (assert: Assert) { + const renderingResolver = new BaseRenderingResolver(getContext()); + const resolved = await renderingResolver.lookupFilteringComponent( + buildColumnDefinition('amount', { type: 'money' }) + ); + assert.deepEqual(resolved.component, ensureSafeComponent(NumericFilteringRenderer, getContext())); + }); + + test('it returns the base header rendering component for a money column', async function (assert: Assert) { + const renderingResolver = new BaseRenderingResolver(getContext()); + const resolved = await renderingResolver.lookupHeaderComponent(buildColumnDefinition('amount', { type: 'money' })); + assert.deepEqual(resolved.component, ensureSafeComponent(BaseHeaderRenderer, getContext())); + }); }); From 6088ea11c9ca9b7f131ea442c214d77b723054e0 Mon Sep 17 00:00:00 2001 From: Julien Vannier Date: Wed, 5 Aug 2026 16:41:57 +0200 Subject: [PATCH 2/2] PR feedback --- addon/components/hyper-table-v2/cell-renderers/amount.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/addon/components/hyper-table-v2/cell-renderers/amount.ts b/addon/components/hyper-table-v2/cell-renderers/amount.ts index 3d392387..c540dd80 100644 --- a/addon/components/hyper-table-v2/cell-renderers/amount.ts +++ b/addon/components/hyper-table-v2/cell-renderers/amount.ts @@ -7,7 +7,6 @@ interface HyperTableV2CellRenderersAmountArgs { handler: TableHandler; column: Column; row: Row; - extra?: { [key: string]: any }; } export type Amount = {