diff --git a/addon/components/filters-picker.hbs b/addon/components/filters-picker.hbs index c2f1491e..caa43b5d 100644 --- a/addon/components/filters-picker.hbs +++ b/addon/components/filters-picker.hbs @@ -33,8 +33,9 @@ {{#unless filter.noFilterLabel}} {{/unless}} + {{! An ExtensionComponent filter comes from an extension's own engine. }} {{component - filter.filterComponent + (if (is-object filter.filterComponent) (lazy-engine-component filter.filterComponent) filter.filterComponent) value=filter.filterValue filter=filter param=(or filter.filterParam filter.valuePath) diff --git a/addon/components/layout/resource/action-buttons.hbs b/addon/components/layout/resource/action-buttons.hbs new file mode 100644 index 00000000..1b2e7dcb --- /dev/null +++ b/addon/components/layout/resource/action-buttons.hbs @@ -0,0 +1,95 @@ +{{#each @buttons as |actionButton|}} + {{#if actionButton.items}} + +
+ {{#each actionButton.items as |item|}} + {{#if item.separator}} +
+ {{else}} + + {{/if}} + {{/each}} +
+
+ {{else if actionButton.component}} + {{#let (if (is-object actionButton.component) (lazy-engine-component actionButton.component) actionButton.component) as |buttonComponent|}} + {{#if buttonComponent}} + {{component + buttonComponent + icon=actionButton.icon + iconPrefix=actionButton.iconPrefix + iconComponent=actionButton.iconComponent + size=actionButton.size + text=actionButton.text + options=actionButton.options + items=actionButton.items + permission=actionButton.permission + isLoading=actionButton.isLoading + renderInPlace=actionButton.renderInPlace + triggerClass=actionButton.triggerClass + disabled=actionButton.disabled + onClick=actionButton.onClick + fn=actionButton.fn + perform=actionButton.perform + onSelect=actionButton.onSelect + onChange=actionButton.onChange + }} + {{/if}} + {{/let}} + {{else}} + `, class extends Component {}), + }); + }); + + test('a cell component from an extension engine renders in the table', async function (assert) { + this.set('columns', [{ id: 'score', label: 'Score', valuePath: 'status', cellComponent: { engine: '@acme/engine', path: 'cell/score' } }]); + + await render(hbs``); + + assert.dom('.acme-cell').hasText('Ada: active'); + }); + + test('a filter component from an extension engine renders in the filters picker', async function (assert) { + this.set('columns', [ + { id: 'score', label: 'Score', valuePath: 'status', filterable: true, filterParam: 'score', filterComponent: { engine: '@acme/engine', path: 'filter/score' } }, + ]); + + await render(hbs``); + await click('.ember-basic-dropdown-trigger'); + + assert.dom('.acme-filter').hasAttribute('placeholder', 'Score'); + }); + + test('a button component from an extension engine renders among the action buttons', async function (assert) { + this.set('buttons', [ + { id: 'sync', text: 'Sync', component: { engine: '@acme/engine', path: 'button/sync' } }, + { id: 'plain', text: 'Plain', component: 'button' }, + ]); + + await render(hbs``); + + assert.dom('.acme-button').hasText('Sync'); + assert.ok( + findAll('button').some((button) => button.textContent.includes('Plain')), + 'a string component still renders directly' + ); + }); + }); + + module('Layout::Resource::ActionButtons', function () { + test('a dropdown item without a handler renders and does nothing', async function (assert) { + this.set('buttons', [{ id: 'more', icon: 'ellipsis-h', items: [{ label: 'Nothing yet' }] }]); + + await render(hbs``); + await clickFound(assert, buttonWithIcon('ellipsis'), 'the dropdown trigger'); + await click('.next-dd-item'); + + assert.ok(true, 'clicking it does not throw'); + }); + + test('disabled dropdown items are marked disabled', async function (assert) { + this.set('buttons', [{ id: 'more', icon: 'ellipsis-h', items: [{ label: 'Void', disabled: true }] }]); + + await render(hbs``); + await clickFound(assert, buttonWithIcon('ellipsis'), 'the dropdown trigger'); + + assert.dom('.next-dd-item').hasClass('disabled'); + }); + }); +}); diff --git a/tests/unit/utils/resource-view-test.js b/tests/unit/utils/resource-view-test.js new file mode 100644 index 00000000..acba9064 --- /dev/null +++ b/tests/unit/utils/resource-view-test.js @@ -0,0 +1,68 @@ +import { module, test } from 'qunit'; +import lookupResourceView, { mergeHeaderButtons } from '@fleetbase/ember-ui/utils/resource-view'; +import { ResourceViewStub } from 'dummy/tests/helpers/resource-view-stubs'; + +module('Unit | Utility | resource-view', function () { + test('lookupResourceView returns the service, or null when there is none', function (assert) { + const service = {}; + assert.strictEqual(lookupResourceView({ lookup: () => service }), service); + assert.strictEqual(lookupResourceView({ lookup: () => undefined }), null); + assert.strictEqual(lookupResourceView(null), null); + assert.strictEqual( + lookupResourceView({ + lookup() { + throw new Error('module missing'); + }, + }), + null, + 'a lookup that throws is treated as no service' + ); + }); + + test('mergeHeaderButtons leaves the buttons alone without a service or registry', function (assert) { + const buttons = [{ id: 'edit' }]; + assert.strictEqual(mergeHeaderButtons(null, 'ledger:details:invoice', buttons), buttons); + assert.strictEqual(mergeHeaderButtons(new ResourceViewStub(), null, buttons), buttons); + assert.deepEqual(mergeHeaderButtons(null, null, undefined), []); + }); + + test('mergeHeaderButtons merges actions, and menu items only when asked', function (assert) { + const views = new ResourceViewStub(); + views.add('ledger:details:invoice:actions', { id: 'print' }); + views.add('ledger:details:invoice:menu', { id: 'acme' }); + + assert.deepEqual( + mergeHeaderButtons(views, 'ledger:details:invoice', [{ id: 'edit' }]).map((b) => b.id), + ['edit', 'print'], + 'table toolbars take no menu' + ); + + const dropdown = { id: 'more', items: [{ id: 'void' }] }; + const merged = mergeHeaderButtons(views, 'ledger:details:invoice', [{ id: 'edit' }, dropdown], {}, { withMenu: true }); + assert.deepEqual( + merged.map((b) => b.id), + ['edit', 'more', 'print'] + ); + assert.deepEqual( + merged[1].items.map((i) => i.id), + ['void', 'acme'] + ); + assert.deepEqual( + dropdown.items.map((i) => i.id), + ['void'], + 'the given dropdown is not modified' + ); + + const appended = mergeHeaderButtons(views, 'ledger:details:invoice', [], {}, { withMenu: true }); + assert.deepEqual( + appended.map((b) => b.id), + ['print', 'registered-menu'] + ); + assert.deepEqual( + appended[1].items.map((i) => i.id), + ['acme'] + ); + + assert.deepEqual(mergeHeaderButtons(new ResourceViewStub(), 'ledger:details:invoice', [], {}, { withMenu: true }), [], 'no dropdown is added for an empty menu'); + }); +});