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()));
+ });
});