From 4c1a9086523448348027c52c55d7b2cc68ede609 Mon Sep 17 00:00:00 2001 From: Julien Vannier Date: Wed, 1 Apr 2026 18:09:41 +0200 Subject: [PATCH 1/4] Add form logic --- addon/components/utils/account-banner.hbs | 105 ++++++++++++++-------- addon/components/utils/account-banner.ts | 36 +++++++- 2 files changed, 104 insertions(+), 37 deletions(-) diff --git a/addon/components/utils/account-banner.hbs b/addon/components/utils/account-banner.hbs index a16d497b..9beac0f1 100644 --- a/addon/components/utils/account-banner.hbs +++ b/addon/components/utils/account-banner.hbs @@ -21,44 +21,77 @@ {{/if}} {{#if (or @subtitle (has-block "custom-subtitle"))}} -
- {{#if @subtitle}} - - {{@subtitle}} - - {{else if (has-block "custom-subtitle")}} - {{yield to="custom-subtitle"}} - {{/if}} + {{#if @subtitle}} + + <:content> +
+
+ {{#if @selectedAccount}} + + {{@subtitle}} + + {{else}} + + + {{@subtitle}} + + {{/if}} - {{#if this.canSelectItem}} - -
- {{#each @selectableItems as |item|}} - {{#if (has-block "selectable-item")}} - {{yield item this.closeSelectionDropdown to="selectable-item"}} + {{#if this.canSelectItem}} + +
+ {{#each @selectableItems as |item|}} + {{#if (has-block "selectable-item")}} + {{yield item this.closeSelectionDropdown to="selectable-item"}} + {{/if}} + {{/each}} +
+ {{/if}} +
+ {{#if this.isErrored}} + + {{get (form-field-feedback this.formInstance.id "account.select") "value"}} + {{/if}} - {{/each}} -
- {{/if}} -
+
+ + + + {{else if (has-block "custom-subtitle")}} + {{yield to="custom-subtitle"}} + {{/if}} {{/if}} diff --git a/addon/components/utils/account-banner.ts b/addon/components/utils/account-banner.ts index ffc9caf7..f5bec5cc 100644 --- a/addon/components/utils/account-banner.ts +++ b/addon/components/utils/account-banner.ts @@ -2,6 +2,10 @@ import { action } from '@ember/object'; import { isBlank } from '@ember/utils'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { IntlService } from 'ember-intl'; + +import { Feedback, FormInstance } from '@upfluence/oss-components/services/form-manager'; type SkinType = 'success' | 'error' | 'warning'; @@ -27,10 +31,18 @@ interface UtilsAccountBannerArgs { canSelectItem?: boolean; selectableItems?: any[]; + selectedAccount?: any; + + onFormSetup?(form: FormInstance): void; } export default class extends Component { + @service declare intl: IntlService; + + declare formInstance: FormInstance; + @tracked displaySelectableItems: boolean = false; + @tracked isErrored: boolean = false; get disabledClass(): string { return this.args.disabled ? 'account-banner--disabled' : ''; @@ -41,7 +53,7 @@ export default class extends Component { } get borderColorClass(): string { - if (this.args.skin) return `account-banner--${this.args.skin}`; + if (this.args.skin || this.isErrored) return `account-banner--${this.args.skin}`; return ''; } @@ -53,6 +65,14 @@ export default class extends Component { return ((this.args.selectableItems || []).length > 1 && this.args.canSelectItem) ?? false; } + noop(): void {} + + @action + onFormSetup(form: FormInstance): void { + this.formInstance = form; + this.args.onFormSetup?.(form); + } + @action toggleSelectionDropdown(e: MouseEvent): void { if (!this.canSelectItem || this.args.readonly) return; @@ -65,4 +85,18 @@ export default class extends Component { closeSelectionDropdown(): void { this.displaySelectableItems = false; } + + @action + validateAccountSelection(): Feedback | undefined { + if (!this.args.selectedAccount) { + this.isErrored = true; + return { + kind: 'blank', + message: { type: 'error', value: this.intl.t('oss-components.forms.errors.required') } + }; + } + + this.isErrored = false; + return undefined; + } } From 8a0f5846361db2bb77677070887b1899ce5658ae Mon Sep 17 00:00:00 2001 From: Julien Vannier Date: Thu, 2 Apr 2026 15:19:12 +0200 Subject: [PATCH 2/4] Better manage error state --- addon/components/utils/account-banner.hbs | 5 +---- addon/components/utils/account-banner.ts | 8 +++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/addon/components/utils/account-banner.hbs b/addon/components/utils/account-banner.hbs index 9beac0f1..f5553450 100644 --- a/addon/components/utils/account-banner.hbs +++ b/addon/components/utils/account-banner.hbs @@ -25,10 +25,9 @@ <:content> -
+
{{else}} -
{{#each @selectableItems as |item|}} {{#if (has-block "selectable-item")}} diff --git a/addon/components/utils/account-banner.ts b/addon/components/utils/account-banner.ts index f5bec5cc..86b56f9e 100644 --- a/addon/components/utils/account-banner.ts +++ b/addon/components/utils/account-banner.ts @@ -5,7 +5,7 @@ import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { IntlService } from 'ember-intl'; -import { Feedback, FormInstance } from '@upfluence/oss-components/services/form-manager'; +import FormManager, { Feedback, FormInstance } from '@upfluence/oss-components/services/form-manager'; type SkinType = 'success' | 'error' | 'warning'; @@ -38,8 +38,9 @@ interface UtilsAccountBannerArgs { export default class extends Component { @service declare intl: IntlService; + @service declare formManager: FormManager; - declare formInstance: FormInstance; + @tracked declare formInstance: FormInstance; @tracked displaySelectableItems: boolean = false; @tracked isErrored: boolean = false; @@ -53,7 +54,8 @@ export default class extends Component { } get borderColorClass(): string { - if (this.args.skin || this.isErrored) return `account-banner--${this.args.skin}`; + if (this.isErrored && this.formInstance?.getErrors()?.['account.select']) return 'account-banner--error'; + if (this.args.skin) return `account-banner--${this.args.skin}`; return ''; } From 00574e6d30655f9a4d620db4e4c78069a8a46fdd Mon Sep 17 00:00:00 2001 From: Julien Vannier Date: Thu, 2 Apr 2026 17:33:32 +0200 Subject: [PATCH 3/4] Add tests --- addon/components/utils/account-banner.ts | 8 +- .../components/utils/account-banner-test.ts | 99 ++++++++++++++----- 2 files changed, 76 insertions(+), 31 deletions(-) diff --git a/addon/components/utils/account-banner.ts b/addon/components/utils/account-banner.ts index 86b56f9e..3c6f821b 100644 --- a/addon/components/utils/account-banner.ts +++ b/addon/components/utils/account-banner.ts @@ -43,7 +43,10 @@ export default class extends Component { @tracked declare formInstance: FormInstance; @tracked displaySelectableItems: boolean = false; - @tracked isErrored: boolean = false; + + get isErrored(): boolean { + return !this.args.selectedAccount; + } get disabledClass(): string { return this.args.disabled ? 'account-banner--disabled' : ''; @@ -91,14 +94,11 @@ export default class extends Component { @action validateAccountSelection(): Feedback | undefined { if (!this.args.selectedAccount) { - this.isErrored = true; return { kind: 'blank', message: { type: 'error', value: this.intl.t('oss-components.forms.errors.required') } }; } - - this.isErrored = false; return undefined; } } diff --git a/tests/integration/components/utils/account-banner-test.ts b/tests/integration/components/utils/account-banner-test.ts index 4396846f..ab568fd6 100644 --- a/tests/integration/components/utils/account-banner-test.ts +++ b/tests/integration/components/utils/account-banner-test.ts @@ -3,6 +3,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { setupIntl } from 'ember-intl/test-support'; import { click, render, findAll } from '@ember/test-helpers'; +import sinon from 'sinon'; const SELECTABLE_ITEMS = [{ label: 'Account A' }, { label: 'Account B' }, { label: 'Account C' }]; @@ -10,6 +11,20 @@ module('Integration | Component | utils/account-banner', function (hooks) { setupRenderingTest(hooks); setupIntl(hooks); + module('rendering', function () { + test('it renders', async function (assert) { + await render(hbs``); + + assert.dom('.account-banner').exists(); + }); + + test('it forwards HTML attributes', async function (assert) { + await render(hbs``); + + assert.dom('.account-banner[data-test="banner"]').exists(); + }); + }); + module('modifier classes', function () { test('it renders with no modifier classes by default', async function (assert) { await render(hbs``); @@ -57,6 +72,27 @@ module('Integration | Component | utils/account-banner', function (hooks) { }); }); + module('icon / image', function () { + test('it renders @icon', async function (assert) { + await render(hbs``); + + assert.dom('.upf-badge').exists(); + }); + + test('it renders @image as a badge', async function (assert) { + await render(hbs``); + + assert.dom('.icon-in-badge').exists(); + }); + + test('@icon takes precedence over @image', async function (assert) { + await render(hbs``); + + assert.dom('.upf-badge').exists(); + assert.dom('.icon-in-badge').doesNotExist(); + }); + }); + module('title', function () { test('it renders @title', async function (assert) { await render(hbs``); @@ -89,7 +125,7 @@ module('Integration | Component | utils/account-banner', function (hooks) { test('it renders @subtitle', async function (assert) { await render(hbs``); - assert.dom('[data-control-name="account-banner-selected-item-label"]').hasText('subtitle'); + assert.dom('[data-control-name="account-banner-selected-item-label"]').containsText('subtitle'); }); test('it renders custom-subtitle block', async function (assert) { @@ -113,30 +149,36 @@ module('Integration | Component | utils/account-banner', function (hooks) { assert.dom('.custom-sub').doesNotExist(); }); + test('it marks subtitle as required when no @selectedAccount', async function (assert) { + await render(hbs``); + + assert.dom('[data-control-name="account-banner-selected-item-label"]').hasText('subtitle *'); + }); + + test('it does not mark subtitle as required when @selectedAccount is provided', async function (assert) { + await render(hbs``); + + assert.dom('[data-control-name="account-banner-selected-item-label"]').hasText('subtitle'); + }); + test('account-banner__selection is not rendered without subtitle or custom-subtitle', async function (assert) { await render(hbs``); assert.dom('.account-banner__selection').doesNotExist(); }); - }); - module('actions block', function () { - test('it renders the actions block', async function (assert) { - await render(hbs` - - <:actions>action - - `); + test('unique-account class is applied when canSelectItem is false', async function (assert) { + await render(hbs``); - assert.dom('.action').exists(); + assert.dom('.account-banner__selection--unique-account').exists(); }); }); - module('dropdown', function () { + module('selection dropdown', function () { test('chevron is not rendered without canSelectItem', async function (assert) { await render(hbs``); - assert.dom('.fa-chevron-down').doesNotExist(); + assert.dom('.fa-chevron-down, [data-icon="chevron-down"]').doesNotExist(); }); test('chevron is not rendered with only 1 item', async function (assert) { @@ -149,10 +191,12 @@ module('Integration | Component | utils/account-banner', function (hooks) { /> `); - assert.dom('[data-icon="chevron-down"]').doesNotExist(); + assert.dom('.fa-chevron-down, [data-icon="chevron-down"]').doesNotExist(); }); test('chevron is rendered with canSelectItem and multiple items', async function (assert) { + this.set('items', SELECTABLE_ITEMS); + await render(hbs` `); - this.set('items', SELECTABLE_ITEMS); assert.dom('.fa-chevron-down, [data-icon="chevron-down"]').exists(); }); @@ -286,11 +329,15 @@ module('Integration | Component | utils/account-banner', function (hooks) { assert.strictEqual(findAll('.item').length, SELECTABLE_ITEMS.length); }); + }); - test('unique-account class applied when canSelectItem is false', async function (assert) { - await render(hbs``); + module('form setup', function () { + test('@onFormSetup is called when @subtitle is provided', async function (assert) { + this.set('onFormSetup', sinon.stub()); - assert.dom('.account-banner__selection--unique-account').exists(); + await render(hbs``); + + assert.true(this.onFormSetup.calledOnce); }); }); @@ -328,17 +375,15 @@ module('Integration | Component | utils/account-banner', function (hooks) { }); }); - module('image / icon', function () { - test('it renders @image as a badge', async function (assert) { - await render(hbs``); - - assert.dom('.icon-in-badge').exists(); - }); - - test('it renders @icon', async function (assert) { - await render(hbs``); + module('actions block', function () { + test('it renders the actions block', async function (assert) { + await render(hbs` + + <:actions>action + + `); - assert.dom('.upf-badge').exists(); + assert.dom('.action').exists(); }); }); }); From cb89de755a855478043274b9e5b9cbcd8ca2c521 Mon Sep 17 00:00:00 2001 From: Julien Vannier Date: Wed, 8 Apr 2026 17:57:09 +0200 Subject: [PATCH 4/4] Remove the OSS::Form & tests --- addon/components/utils/account-banner.hbs | 106 ++++++++---------- addon/components/utils/account-banner.ts | 41 ++----- .../components/utils/account-banner-test.ts | 49 ++++++-- 3 files changed, 93 insertions(+), 103 deletions(-) diff --git a/addon/components/utils/account-banner.hbs b/addon/components/utils/account-banner.hbs index f5553450..d3792283 100644 --- a/addon/components/utils/account-banner.hbs +++ b/addon/components/utils/account-banner.hbs @@ -22,70 +22,54 @@ {{#if (or @subtitle (has-block "custom-subtitle"))}} {{#if @subtitle}} - - <:content> -
-
+
+ {{#if @required}} + - {{#if @selectedAccount}} - - {{@subtitle}} - - {{else}} - - {{@subtitle}} - - {{/if}} + {{@subtitle}} + + {{else}} + + {{@subtitle}} + + {{/if}} - {{#if this.canSelectItem}} - -
- {{#each @selectableItems as |item|}} - {{#if (has-block "selectable-item")}} - {{yield item this.closeSelectionDropdown to="selectable-item"}} - {{/if}} - {{/each}} -
- {{/if}} + {{#if this.canSelectItem}} + +
+ {{#each @selectableItems as |item|}} + {{#if (has-block "selectable-item")}} + {{yield item this.closeSelectionDropdown to="selectable-item"}} + {{/if}} + {{/each}}
- {{#if this.isErrored}} - - {{get (form-field-feedback this.formInstance.id "account.select") "value"}} - - {{/if}} -
- - - + {{/if}} +
+ {{#if this.feedbackMessage}} + + {{this.feedbackMessage.value}} + + {{/if}} +
{{else if (has-block "custom-subtitle")}} {{yield to="custom-subtitle"}} {{/if}} diff --git a/addon/components/utils/account-banner.ts b/addon/components/utils/account-banner.ts index 3c6f821b..4872b9d8 100644 --- a/addon/components/utils/account-banner.ts +++ b/addon/components/utils/account-banner.ts @@ -2,10 +2,8 @@ import { action } from '@ember/object'; import { isBlank } from '@ember/utils'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { inject as service } from '@ember/service'; -import { IntlService } from 'ember-intl'; -import FormManager, { Feedback, FormInstance } from '@upfluence/oss-components/services/form-manager'; +import { type FeedbackMessage } from '@upfluence/oss-components/components/o-s-s/input-container'; type SkinType = 'success' | 'error' | 'warning'; @@ -31,21 +29,19 @@ interface UtilsAccountBannerArgs { canSelectItem?: boolean; selectableItems?: any[]; - selectedAccount?: any; - - onFormSetup?(form: FormInstance): void; + feedbackMessage?: FeedbackMessage; + required?: boolean; } export default class extends Component { - @service declare intl: IntlService; - @service declare formManager: FormManager; - - @tracked declare formInstance: FormInstance; - @tracked displaySelectableItems: boolean = false; + get feedbackMessage(): FeedbackMessage | undefined { + return this.args.feedbackMessage; + } + get isErrored(): boolean { - return !this.args.selectedAccount; + return this.feedbackMessage?.type === 'error'; } get disabledClass(): string { @@ -57,7 +53,7 @@ export default class extends Component { } get borderColorClass(): string { - if (this.isErrored && this.formInstance?.getErrors()?.['account.select']) return 'account-banner--error'; + if (this.isErrored) return 'account-banner--error'; if (this.args.skin) return `account-banner--${this.args.skin}`; return ''; } @@ -70,14 +66,6 @@ export default class extends Component { return ((this.args.selectableItems || []).length > 1 && this.args.canSelectItem) ?? false; } - noop(): void {} - - @action - onFormSetup(form: FormInstance): void { - this.formInstance = form; - this.args.onFormSetup?.(form); - } - @action toggleSelectionDropdown(e: MouseEvent): void { if (!this.canSelectItem || this.args.readonly) return; @@ -90,15 +78,4 @@ export default class extends Component { closeSelectionDropdown(): void { this.displaySelectableItems = false; } - - @action - validateAccountSelection(): Feedback | undefined { - if (!this.args.selectedAccount) { - return { - kind: 'blank', - message: { type: 'error', value: this.intl.t('oss-components.forms.errors.required') } - }; - } - return undefined; - } } diff --git a/tests/integration/components/utils/account-banner-test.ts b/tests/integration/components/utils/account-banner-test.ts index ab568fd6..63ac852a 100644 --- a/tests/integration/components/utils/account-banner-test.ts +++ b/tests/integration/components/utils/account-banner-test.ts @@ -3,7 +3,6 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { setupIntl } from 'ember-intl/test-support'; import { click, render, findAll } from '@ember/test-helpers'; -import sinon from 'sinon'; const SELECTABLE_ITEMS = [{ label: 'Account A' }, { label: 'Account B' }, { label: 'Account C' }]; @@ -149,14 +148,14 @@ module('Integration | Component | utils/account-banner', function (hooks) { assert.dom('.custom-sub').doesNotExist(); }); - test('it marks subtitle as required when no @selectedAccount', async function (assert) { - await render(hbs``); + test('it marks subtitle as required when @required is true', async function (assert) { + await render(hbs``); assert.dom('[data-control-name="account-banner-selected-item-label"]').hasText('subtitle *'); }); - test('it does not mark subtitle as required when @selectedAccount is provided', async function (assert) { - await render(hbs``); + test('it does not mark subtitle as required when @required is not set', async function (assert) { + await render(hbs``); assert.dom('[data-control-name="account-banner-selected-item-label"]').hasText('subtitle'); }); @@ -331,13 +330,43 @@ module('Integration | Component | utils/account-banner', function (hooks) { }); }); - module('form setup', function () { - test('@onFormSetup is called when @subtitle is provided', async function (assert) { - this.set('onFormSetup', sinon.stub()); + module('feedback message', function () { + test('it does not render any feedback message by default', async function (assert) { + await render(hbs``); + + assert.dom('.account-banner__selection + span').doesNotExist(); + }); + + test('it renders an error feedback message', async function (assert) { + this.set('feedback', { type: 'error', value: 'Required field' }); + await render(hbs``); + + assert.dom('.account-banner__selection + span').exists(); + assert.dom('.account-banner__selection + span').hasClass('font-color-error-500'); + assert.dom('.account-banner__selection + span').hasText('Required field'); + }); + + test('it renders a warning feedback message', async function (assert) { + this.set('feedback', { type: 'warning', value: 'Be careful' }); + await render(hbs``); + + assert.dom('.account-banner__selection + span').exists(); + assert.dom('.account-banner__selection + span').hasClass('font-color-warning-500'); + assert.dom('.account-banner__selection + span').hasText('Be careful'); + }); + + test('it applies error border class when feedbackMessage type is error', async function (assert) { + this.set('feedback', { type: 'error', value: 'Required field' }); + await render(hbs``); - await render(hbs``); + assert.dom('.account-banner').hasClass('account-banner--error'); + }); - assert.true(this.onFormSetup.calledOnce); + test('it does not apply error border class when feedbackMessage type is warning', async function (assert) { + this.set('feedback', { type: 'warning', value: 'Be careful' }); + await render(hbs``); + + assert.dom('.account-banner').hasNoClass('account-banner--error'); }); });