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..c540dd80 --- /dev/null +++ b/addon/components/hyper-table-v2/cell-renderers/amount.ts @@ -0,0 +1,21 @@ +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; +} + +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())); + }); });