diff --git a/addon/components/o-s-s/banner.ts b/addon/components/o-s-s/banner.ts index fa083c166..f7009db3c 100644 --- a/addon/components/o-s-s/banner.ts +++ b/addon/components/o-s-s/banner.ts @@ -1,6 +1,8 @@ import { isBlank } from '@ember/utils'; import Component from '@glimmer/component'; -import { FEEDBACK_TYPES, type FeedbackMessage } from './input-container'; + +import type { FeedbackMessage } from '@upfluence/oss-components/types'; +import { ALLOWED_FEEDBACK_MESSAGE_TYPES } from '@upfluence/oss-components/utils'; type SizeType = 'sm' | 'md' | 'lg'; @@ -35,7 +37,7 @@ export default class OSSBanner extends Component { } get feedbackMessage(): FeedbackMessage | undefined { - if (this.args.feedbackMessage && FEEDBACK_TYPES.includes(this.args.feedbackMessage.type)) { + if (this.args.feedbackMessage && ALLOWED_FEEDBACK_MESSAGE_TYPES.includes(this.args.feedbackMessage.type)) { return this.args.feedbackMessage; } return undefined; diff --git a/addon/components/o-s-s/country-selector.hbs b/addon/components/o-s-s/country-selector.hbs index cb513fd28..43756bfef 100644 --- a/addon/components/o-s-s/country-selector.hbs +++ b/addon/components/o-s-s/country-selector.hbs @@ -1,6 +1,6 @@
{{/in-element}} {{/if}} + + {{#if this.feedbackMessage.value}} + + {{this.feedbackMessage.value}} + + {{/if}}
\ No newline at end of file diff --git a/addon/components/o-s-s/country-selector.stories.js b/addon/components/o-s-s/country-selector.stories.js index cd06bb367..2342af7aa 100644 --- a/addon/components/o-s-s/country-selector.stories.js +++ b/addon/components/o-s-s/country-selector.stories.js @@ -98,6 +98,17 @@ export default { }, control: { type: 'text' } }, + feedbackMessage: { + description: + 'A feedback message to display below the country selector. The message object contains a type and value. Allowed types are: error, warning, success. The type determines the color of the message and applies a CSS class to the upload area.', + table: { + type: { + summary: 'FeedbackMessage' + }, + defaultValue: { summary: 'undefined' } + }, + control: { type: 'object' } + }, onChange: { type: { required: true }, description: 'A callback that sends the selected country/province object to the parent component', @@ -122,6 +133,7 @@ export default { const defaultArgs = { sourceList: partialCountries, value: undefined, + feedbackMessage: undefined, onChange: action('onChange') }; @@ -147,3 +159,11 @@ PrefilledUsage.args = { ...defaultArgs, ...{ value: 'FR' } }; + +export const WithFeedbackMessage = Template.bind({}); +WithFeedbackMessage.args = { + ...defaultArgs, + ...{ + feedbackMessage: { type: 'error', value: 'This is an error message' } + } +}; diff --git a/addon/components/o-s-s/country-selector.ts b/addon/components/o-s-s/country-selector.ts index 59f63b65a..ac71d830f 100644 --- a/addon/components/o-s-s/country-selector.ts +++ b/addon/components/o-s-s/country-selector.ts @@ -1,14 +1,16 @@ import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; import { assert } from '@ember/debug'; import { inject as service } from '@ember/service'; import { isEmpty } from '@ember/utils'; - +import { scheduleOnce } from '@ember/runloop'; +import { tracked } from '@glimmer/tracking'; import type { IntlService } from 'ember-intl'; -import BaseDropdown, { type BaseDropdownArgs } from './private/base-dropdown'; -import { scheduleOnce } from '@ember/runloop'; +import { ALLOWED_FEEDBACK_MESSAGE_TYPES } from '@upfluence/oss-components/utils'; import attachDropdown from '@upfluence/oss-components/utils/attach-dropdown'; +import type { FeedbackMessage } from '@upfluence/oss-components/types'; + +import BaseDropdown, { type BaseDropdownArgs } from './private/base-dropdown'; type Item = { name: string; @@ -49,6 +51,20 @@ export default class OSSCountrySelector extends BaseDropdown + @value={{@value}} + @placeholder={{this.placeholder}} + @errorMessage={{this.errorMessage}} + @feedbackMessage={{@feedbackMessage}} + @onChange={{this.validateInput}} + ...attributes +/> \ No newline at end of file diff --git a/addon/components/o-s-s/email-input.stories.js b/addon/components/o-s-s/email-input.stories.js index 6ecb11c7c..01c1bcd9c 100644 --- a/addon/components/o-s-s/email-input.stories.js +++ b/addon/components/o-s-s/email-input.stories.js @@ -25,6 +25,16 @@ export default { }, control: { type: 'text' } }, + feedbackMessage: { + description: 'A success, warning or error message that will be displayed below the input-group.', + table: { + type: { + summary: '{ type: string, value: string }' + }, + defaultValue: { summary: 'undefined' } + }, + control: { type: 'object' } + }, errorMessage: { description: 'Error message that is displayed when the email pattern is invalid', table: { @@ -73,6 +83,7 @@ const defaultArgs = { value: 'john.doe@example.com', placeholder: 'foo@bar.org', errorMessage: undefined, + feedbackMessage: undefined, validateFormat: false, validates: action('validates') }; @@ -80,7 +91,7 @@ const defaultArgs = { const Template = (args) => ({ template: hbs` + @validates={{this.validates}} @errorMessage={{this.errorMessage}} @feedbackMessage={{this.feedbackMessage}} /> `, context: args }); diff --git a/addon/components/o-s-s/email-input.ts b/addon/components/o-s-s/email-input.ts index 34727f43c..4815262ec 100644 --- a/addon/components/o-s-s/email-input.ts +++ b/addon/components/o-s-s/email-input.ts @@ -3,10 +3,12 @@ import { tracked } from '@glimmer/tracking'; import { assert } from '@ember/debug'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; +import type { FeedbackMessage } from '@upfluence/oss-components/types'; interface OSSEmailInputArgs { value: string | null; placeholder?: string; + feedbackMessage?: FeedbackMessage; errorMessage?: string; validateFormat?: boolean; validates?(isPassing: boolean): void; diff --git a/addon/components/o-s-s/input-container.hbs b/addon/components/o-s-s/input-container.hbs index a8aeb7259..c1ae0dfb9 100644 --- a/addon/components/o-s-s/input-container.hbs +++ b/addon/components/o-s-s/input-container.hbs @@ -33,7 +33,7 @@ {{#if @errorMessage}} {{@errorMessage}} - {{else if this.feedbackMessage}} + {{else if this.feedbackMessage.value}} {{#unless (eq this.feedbackMessage.type "error")}} diff --git a/addon/components/o-s-s/input-container.stories.js b/addon/components/o-s-s/input-container.stories.js index 3772b69d4..a02453601 100644 --- a/addon/components/o-s-s/input-container.stories.js +++ b/addon/components/o-s-s/input-container.stories.js @@ -113,6 +113,7 @@ const defaultArgs = { disabled: false, type: undefined, placeholder: 'this is the placeholder', + feedbackMessage: undefined, errorMessage: undefined, autocomplete: undefined, onChange: action('onChange') @@ -121,7 +122,7 @@ const defaultArgs = { const DefaultUsageTemplate = (args) => ({ template: hbs` + @feedbackMessage={{this.feedbackMessage}} @errorMessage={{this.errorMessage}} @onChange={{this.onChange}} @autocomplete={{this.autocomplete}} /> `, context: args }); diff --git a/addon/components/o-s-s/input-container.ts b/addon/components/o-s-s/input-container.ts index b8d6f9dc1..b2ce5e87f 100644 --- a/addon/components/o-s-s/input-container.ts +++ b/addon/components/o-s-s/input-container.ts @@ -2,13 +2,10 @@ import { action } from '@ember/object'; import { next } from '@ember/runloop'; import Component from '@glimmer/component'; -export const FEEDBACK_TYPES = ['error', 'warning', 'success'] as const; -export type FeedbackType = (typeof FEEDBACK_TYPES)[number]; +import type { FeedbackMessage } from '@upfluence/oss-components/types'; +import { ALLOWED_FEEDBACK_MESSAGE_TYPES } from '@upfluence/oss-components/utils'; -export type FeedbackMessage = { - type: FeedbackType; - value: string; -}; +export type { FeedbackMessage }; export interface OSSInputContainerArgs { value?: string; @@ -26,7 +23,7 @@ export const AutocompleteValues = ['on', 'off']; export default class OSSInputContainer extends Component { get feedbackMessage(): FeedbackMessage | undefined { - if (this.args.feedbackMessage && FEEDBACK_TYPES.includes(this.args.feedbackMessage.type)) { + if (this.args.feedbackMessage && ALLOWED_FEEDBACK_MESSAGE_TYPES.includes(this.args.feedbackMessage.type)) { return this.args.feedbackMessage; } diff --git a/addon/components/o-s-s/upload-area.hbs b/addon/components/o-s-s/upload-area.hbs index 7f0888d40..ba7984628 100644 --- a/addon/components/o-s-s/upload-area.hbs +++ b/addon/components/o-s-s/upload-area.hbs @@ -9,7 +9,7 @@ {{on "click" this.triggerFileBrowser}} {{on "mouseenter" this._mouseEnter}} {{on "mouseleave" this._mouseLeave}} - {{did-insert this.init}} + {{did-insert this.onInit}} ...attributes >
@@ -70,4 +70,9 @@ {{/if}} + {{#if this.feedbackMessage.value}} + + {{this.feedbackMessage.value}} + + {{/if}}
\ No newline at end of file diff --git a/addon/components/o-s-s/upload-area.stories.js b/addon/components/o-s-s/upload-area.stories.js index e546f79f0..7700d9e22 100644 --- a/addon/components/o-s-s/upload-area.stories.js +++ b/addon/components/o-s-s/upload-area.stories.js @@ -1,6 +1,6 @@ import { hbs } from 'ember-cli-htmlbars'; import { action } from '@storybook/addon-actions'; -import { MockUploader } from 'dummy/controllers/application'; +import { MockUploader } from 'dummy/controllers/extra'; const PrivacyTypes = ['public', 'private']; const SizeTypes = ['md', 'lg']; @@ -112,7 +112,21 @@ export default { defaultValue: { summary: false } } }, + feedbackMessage: { + description: + 'A feedback message to display below the upload area. The message object contains a type and value.
' + + 'Allowed types are: error, warning, success.
' + + 'The type determines the color of the message and applies a CSS class to the upload area.', + table: { + type: { + summary: 'FeedbackMessage' + }, + defaultValue: { summary: 'undefined' } + }, + control: { type: 'object' } + }, onUploadSuccess: { + type: { required: true }, description: 'Action called when the file is upload with success. This action has two definitions:
' + '- onUploadSuccess(artifact: FileArtifact): void (single mode)
' + @@ -214,8 +228,8 @@ const DefaultUsageTemplate = (args) => ({
+ @size={{this.size}} @multiple={{this.multiple}} @feedbackMessage={{this.feedbackMessage}} + @onUploadSuccess={{this.onUploadSuccess}} @onFileDeletion={{this.onFileDeletion}} />
`, context: args @@ -224,6 +238,14 @@ const DefaultUsageTemplate = (args) => ({ export const Default = DefaultUsageTemplate.bind({}); Default.args = defaultArgs; +export const WithFeedbackMessage = DefaultUsageTemplate.bind({}); +WithFeedbackMessage.args = { + ...defaultArgs, + ...{ + feedbackMessage: { type: 'error', value: 'File size exceeds the maximum allowed limit' } + } +}; + export const SingleWithArtifact = DefaultUsageTemplate.bind({}); SingleWithArtifact.args = { ...defaultArgs, diff --git a/addon/components/o-s-s/upload-area.ts b/addon/components/o-s-s/upload-area.ts index 1b543b1ff..418401b2d 100644 --- a/addon/components/o-s-s/upload-area.ts +++ b/addon/components/o-s-s/upload-area.ts @@ -14,6 +14,9 @@ import { type FailedUploadResponse, FilePrivacy } from '@upfluence/oss-components/types/uploader'; +import type { FeedbackMessage } from '@upfluence/oss-components/components/o-s-s/input-container'; +import { isBlank } from '@ember/utils'; +import { ALLOWED_FEEDBACK_MESSAGE_TYPES } from '@upfluence/oss-components/utils'; interface OSSUploadAreaArgs { uploader: Uploader; @@ -26,6 +29,7 @@ interface OSSUploadAreaArgs { size?: 'lg' | 'md'; multiple?: boolean; displayPreview?: boolean; + feedbackMessage?: FeedbackMessage; onUploadSuccess(artifact: FileArtifact): void; onUploadFailure?(error: FailedUploadResponse): void; @@ -52,6 +56,7 @@ export default class OSSUploadArea extends Component { @tracked dragging: boolean = false; @tracked hover: boolean = false; @tracked alreadyTriggerAnimation: boolean = false; + @tracked localFeedbackMessage?: FeedbackMessage; constructor(owner: unknown, args: OSSUploadAreaArgs) { super(owner, args); @@ -72,6 +77,10 @@ export default class OSSUploadArea extends Component { classes.push('oss-upload-area--disabled'); } + if (this.feedbackMessage?.type && ALLOWED_FEEDBACK_MESSAGE_TYPES.includes(this.feedbackMessage.type)) { + classes.push(`oss-upload-area--${this.feedbackMessage?.type}`); + } + if (this.dragging) { classes.push('oss-upload-area--dragging'); } @@ -112,8 +121,16 @@ export default class OSSUploadArea extends Component { return this.multiple || this.selectedFiles.length === 0; } + get hasFeedbackMessageValue(): boolean { + return !isBlank(this.args.feedbackMessage?.value); + } + + get feedbackMessage(): FeedbackMessage | undefined { + return this.args.feedbackMessage ?? this.localFeedbackMessage; + } + @action - init(element: HTMLElement): void { + onInit(element: HTMLElement): void { this._DOMElement = element; } @@ -198,7 +215,7 @@ export default class OSSUploadArea extends Component { if (this.multiple) { this.selectedFiles.splice(index, 1); - this.selectedFiles = this.selectedFiles; + this.selectedFiles = [...this.selectedFiles]; this.args.onFileDeletion?.(index); } else { this.selectedFiles = []; @@ -225,7 +242,9 @@ export default class OSSUploadArea extends Component { } private _handleFileUpload(file: File): void { + if (this.args.disabled) return; this.args.onHandleFileUpload?.(); + this.localFeedbackMessage = undefined; if (this._validateFile(file)) { if (this.args.onDryRun) { this.args.onDryRun(file); @@ -234,7 +253,7 @@ export default class OSSUploadArea extends Component { if (this.editingFileIndex !== undefined) { this.selectedFiles[this.editingFileIndex] = file; - this.selectedFiles = this.selectedFiles; + this.selectedFiles = [...this.selectedFiles]; this.editingFileIndex = undefined; } else { this.selectedFiles = [...this.selectedFiles, ...[file]]; @@ -264,6 +283,11 @@ export default class OSSUploadArea extends Component { intlArgs.max_filesize = v.rule.value; } + this.localFeedbackMessage = { + type: 'error', + value: this.intl.t(`oss-components.upload-area.errors.${v.rule.type}.feedback`, intlArgs) + }; + this.toast.error( this.intl.t(`oss-components.upload-area.errors.${v.rule.type}.description`, intlArgs), this.intl.t(`oss-components.upload-area.errors.${v.rule.type}.title`) diff --git a/addon/components/o-s-s/upload-item.hbs b/addon/components/o-s-s/upload-item.hbs index 931f712be..efad16b30 100644 --- a/addon/components/o-s-s/upload-item.hbs +++ b/addon/components/o-s-s/upload-item.hbs @@ -1,7 +1,6 @@
{{#if this.shouldDisplayPreview}} -
+
{{else}} {{/if}} @@ -31,29 +30,40 @@
{{else}} {{#if this.error}} - - {{t "oss-components.upload-area.errors.try_again"}} + + + {{t "oss-components.upload-area.errors.try_again"}} {{else}} + {{on "click" @onEdition}} + {{enable-tooltip title=(t "oss-components.upload-area.tooltips.edit") placement="top"}} + /> + {{on "click" (redirect-to url=this.fileUrl target="_blank")}} + {{enable-tooltip title=(t "oss-components.upload-area.tooltips.view")}} + /> {{/if}} + {{on "click" @onDeletion}} + {{enable-tooltip title=(t "oss-components.upload-area.tooltips.delete") placement="top"}} + /> {{/if}} - + \ No newline at end of file diff --git a/addon/types.ts b/addon/types.ts new file mode 100644 index 000000000..fbd6cb17c --- /dev/null +++ b/addon/types.ts @@ -0,0 +1,5 @@ +export type FeedbackMessageType = 'error' | 'warning' | 'success'; +export type FeedbackMessage = { + type: FeedbackMessageType; + value?: string; +}; diff --git a/addon/utils/index.ts b/addon/utils/index.ts index 3e70170d0..7522512ed 100644 --- a/addon/utils/index.ts +++ b/addon/utils/index.ts @@ -1,3 +1,7 @@ +import type { FeedbackMessageType } from '@upfluence/oss-components/types'; + export function isSafeString(arg: any): boolean { return arg?.constructor?.name === 'SafeString'; } + +export const ALLOWED_FEEDBACK_MESSAGE_TYPES: FeedbackMessageType[] = ['error', 'warning', 'success']; diff --git a/app/styles/country-selector.less b/app/styles/country-selector.less index 1423c4e31..a978f756e 100644 --- a/app/styles/country-selector.less +++ b/app/styles/country-selector.less @@ -45,4 +45,25 @@ color: var(--color-gray-900); } } + + &--success { + .yielded-input input, + .upf-input { + .upf-input--success; + } + } + + &--warning { + .yielded-input input, + .upf-input { + .upf-input--warning; + } + } + + &--error { + .yielded-input input, + .upf-input { + .upf-input--errored; + } + } } diff --git a/app/styles/molecules/upload-area.less b/app/styles/molecules/upload-area.less index dcdbe18aa..f660bbd4b 100644 --- a/app/styles/molecules/upload-area.less +++ b/app/styles/molecules/upload-area.less @@ -11,7 +11,7 @@ border: 1px dashed var(--color-border-default); border-radius: var(--border-radius-lg); - .oss-upload-area__illustration { + &__illustration { position: relative; img { @@ -148,6 +148,18 @@ background-color: var(--color-gray-200); border-color: var(--color-gray-300); } + + &--error { + border-color: var(--color-error-500); + } + + &--warning { + border-color: var(--color-warning-500); + } + + &--success { + border-color: var(--color-success-500); + } } .oss-upload-item { diff --git a/tests/dummy/app/templates/extra.hbs b/tests/dummy/app/templates/extra.hbs index 82194365d..21cc68e24 100644 --- a/tests/dummy/app/templates/extra.hbs +++ b/tests/dummy/app/templates/extra.hbs @@ -97,7 +97,6 @@ @subtitle="JPG, PNG, PDF (Max 800x400px - 2MB)" @onUploadSuccess={{this.onUploadSuccess}} /> - - +
+ + + + +
diff --git a/tests/dummy/app/templates/input.hbs b/tests/dummy/app/templates/input.hbs index c4b0f9db4..8b8c5aca0 100644 --- a/tests/dummy/app/templates/input.hbs +++ b/tests/dummy/app/templates/input.hbs @@ -567,7 +567,7 @@ Country selector
-
+
Country {{#if (gt this.selectedCountry.provinces.length 0)}} -
+
Province
{{/if}} + +
+ Country with error + +
+ + {{#if (gt this.selectedCountry.provinces.length 0)}} +
+ Province with error + +
+ {{/if}}
diff --git a/tests/integration/components/o-s-s/banner-test.ts b/tests/integration/components/o-s-s/banner-test.ts index 8df9d7c5b..404cc37aa 100644 --- a/tests/integration/components/o-s-s/banner-test.ts +++ b/tests/integration/components/o-s-s/banner-test.ts @@ -2,7 +2,9 @@ import { hbs } from 'ember-cli-htmlbars'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; -import { FEEDBACK_TYPES, type FeedbackType } from '@upfluence/oss-components/components/o-s-s/input-container'; + +import type { FeedbackMessageType } from '@upfluence/oss-components/types'; +import { ALLOWED_FEEDBACK_MESSAGE_TYPES } from '@upfluence/oss-components/utils'; module('Integration | Component | o-s-s/banner', function (hooks) { setupRenderingTest(hooks); @@ -188,7 +190,7 @@ module('Integration | Component | o-s-s/banner', function (hooks) { assert.dom('.upf-banner--feedback').hasText('This is a feedback message'); }); - FEEDBACK_TYPES.forEach((type: FeedbackType) => { + ALLOWED_FEEDBACK_MESSAGE_TYPES.forEach((type: FeedbackMessageType) => { test(`When feedback type is ${type}, the border has the corresponding class`, async function (assert) { this.feedbackMessage = { type: type, diff --git a/tests/integration/components/o-s-s/country-selector-test.ts b/tests/integration/components/o-s-s/country-selector-test.ts index 4fde6e7e2..82c9bf4cd 100644 --- a/tests/integration/components/o-s-s/country-selector-test.ts +++ b/tests/integration/components/o-s-s/country-selector-test.ts @@ -6,10 +6,12 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { setupIntl } from 'ember-intl/test-support'; import { click, findAll, render } from '@ember/test-helpers'; -import { countries } from '@upfluence/oss-components/utils/country-codes'; import { set } from '@ember/object'; import triggerKeyEvent from '@ember/test-helpers/dom/trigger-key-event'; +import { ALLOWED_FEEDBACK_MESSAGE_TYPES } from '@upfluence/oss-components/utils'; +import { countries } from '@upfluence/oss-components/utils/country-codes'; + module('Integration | Component | o-s-s/country-selector', function (hooks) { setupRenderingTest(hooks); setupIntl(hooks); @@ -231,4 +233,110 @@ module('Integration | Component | o-s-s/country-selector', function (hooks) { await settled(); }); }); + + module('@feedbackMessageMessage argument', () => { + hooks.beforeEach(function () { + this.feedbackMessage = { type: 'error', value: 'This is an error message' }; + }); + + module('when @feedbackMessage is provided', () => { + test('no error message is displayed', async function (assert) { + await render(hbs``); + assert.dom('[data-control-name="country-selector-feedback-message"]').doesNotExist(); + }); + + test('no error border is displayed', async function (assert) { + await render(hbs``); + assert.dom('.country-selector-container').doesNotHaveClass('country-selector-container--error'); + }); + }); + + module('when @feedbackMessage is null', () => { + test('no error message is displayed', async function (assert) { + this.feedbackMessage = null; + await render( + hbs`` + ); + assert.dom('[data-control-name="country-selector-feedback-message"]').doesNotExist(); + }); + + test('no border is displayed', async function (assert) { + this.feedbackMessage = null; + await render( + hbs`` + ); + assert.dom('.country-selector-container').doesNotHaveClass('country-selector-container--error'); + }); + }); + + test('when @feedbackMessage changes from a message to null, the error message is removed', async function (assert) { + await render( + hbs`` + ); + assert.dom('.country-selector-container').hasClass('country-selector-container--error'); + assert.dom('[data-control-name="country-selector-feedback-message"]').hasText('This is an error message'); + + set(this, 'feedbackMessage', null); + await settled(); + + assert.dom('.country-selector-container').doesNotHaveClass('country-selector-container--error'); + assert.dom('[data-control-name="country-selector-feedback-message"]').doesNotExist(); + }); + + test('when @feedbackMessage changes from null to a message, the error message is displayed', async function (assert) { + this.feedbackMessage = null; + await render( + hbs`` + ); + + assert.dom('.country-selector-container').doesNotHaveClass('country-selector-container--error'); + assert.dom('[data-control-name="country-selector-feedback-message"]').doesNotExist(); + + set(this, 'feedbackMessage', { type: 'error', value: 'This is an error message' }); + await settled(); + + assert.dom('.country-selector-container').hasClass('country-selector-container--error'); + assert.dom('[data-control-name="country-selector-feedback-message"]').hasText('This is an error message'); + }); + + ALLOWED_FEEDBACK_MESSAGE_TYPES.forEach((type) => { + module(`for ${type} type`, () => { + module('when @feedbackMessage is provided', () => { + test(`the correct message is displayed`, async function (assert) { + this.feedbackMessage = { type: type, value: `This is an ${type} message` }; + await render( + hbs`` + ); + assert.dom('[data-control-name="country-selector-feedback-message"]').hasText(`This is an ${type} message`); + }); + + test(`the correct border is displayed`, async function (assert) { + this.feedbackMessage = { type: type, value: `This is an ${type} message` }; + await render( + hbs`` + ); + assert.dom('.country-selector-container').hasClass(`country-selector-container--${type}`); + }); + }); + + module('when @feedbackMessage has undefined value', () => { + test('no message is displayed', async function (assert) { + this.feedbackMessage = { type }; + await render( + hbs`` + ); + assert.dom('[data-control-name="country-selector-feedback-message"]').doesNotExist(); + }); + + test('the correct border is displayed', async function (assert) { + this.feedbackMessage = { type }; + await render( + hbs`` + ); + assert.dom('.country-selector-container').hasClass(`country-selector-container--${type}`); + }); + }); + }); + }); + }); }); diff --git a/tests/integration/components/o-s-s/email-input-test.ts b/tests/integration/components/o-s-s/email-input-test.ts index 44d92fbd2..e394a6b70 100644 --- a/tests/integration/components/o-s-s/email-input-test.ts +++ b/tests/integration/components/o-s-s/email-input-test.ts @@ -23,6 +23,27 @@ module('Integration | Component | o-s-s/email-input', function (hooks) { assert.dom('.text-color-error').hasText('This is the error message'); }); + ['error', 'warning', 'success'].forEach((type) => { + test(`it properly displays the feedback message when the @feedbackMessage type is ${type} and has a value`, async function (assert) { + this.feedbackMessage = { type, value: `This is a ${type} message` }; + + await render(hbs``); + + assert.dom('.oss-input-container').hasClass(`oss-input-container--${type}`); + assert.dom(`.oss-input-container + .font-color-${type}-500`).exists(); + assert.dom(`.oss-input-container + .font-color-${type}-500`).hasText(`This is a ${type} message`); + }); + + test(`it only displays the input in the feedback style when the @feedbackMessage type is ${type} w/ no value`, async function (assert) { + this.feedbackMessage = { type, value: undefined }; + + await render(hbs``); + + assert.dom('.oss-input-container').hasClass(`oss-input-container--${type}`); + assert.dom(`.oss-input-container + .font-color-${type}-500`).doesNotExist(); + }); + }); + test('If the email regex isnt matched, then the error message is displayed', async function (assert) { this.value = ''; await render(hbs``); diff --git a/tests/integration/components/o-s-s/input-container-test.js b/tests/integration/components/o-s-s/input-container-test.ts similarity index 87% rename from tests/integration/components/o-s-s/input-container-test.js rename to tests/integration/components/o-s-s/input-container-test.ts index feb309186..2a73d9c50 100644 --- a/tests/integration/components/o-s-s/input-container-test.js +++ b/tests/integration/components/o-s-s/input-container-test.ts @@ -1,7 +1,7 @@ import { hbs } from 'ember-cli-htmlbars'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, find, typeIn, triggerEvent } from '@ember/test-helpers'; +import { render, typeIn, triggerEvent } from '@ember/test-helpers'; import sinon from 'sinon'; module('Integration | Component | o-s-s/input-container', function (hooks) { @@ -45,19 +45,17 @@ module('Integration | Component | o-s-s/input-container', function (hooks) { }); module('Component Parameters', (hooks) => { - let onValueChange; - hooks.beforeEach(function () { - onValueChange = sinon.fake.returns(true); - this.set('value', 'testValue'); - this.set('placeholder', 'Type your text'); - this.set('onValueChange', onValueChange); - this.set('autocomplete', undefined); + this.value = 'testValue'; + this.placeholder = 'Type your text'; + this.onChange = sinon.stub(); + this.autocomplete = undefined; }); + async function renderComponentWithParameters() { await render(hbs` `); } @@ -69,26 +67,22 @@ module('Integration | Component | o-s-s/input-container', function (hooks) { test('Passing a @placeholder parameter works', async function (assert) { await renderComponentWithParameters(); - let inputElement = find('.upf-input'); - assert.equal(inputElement.getAttribute('placeholder'), 'Type your text'); + assert.dom('.upf-input').hasAttribute('placeholder', 'Type your text'); }); test('Passing an @onChange method works and is triggered on input changes', async function (assert) { await renderComponentWithParameters(); - let inputElement = find('.upf-input'); - await typeIn(inputElement, 'a'); - assert.ok(onValueChange.called); + await typeIn('.upf-input', 's', { delay: 0 }); + assert.ok(this.onChange.calledOnceWithExactly('testValues')); }); test('Passing an @onChange method works and is triggered on copy event', async function (assert) { - this.onChange = sinon.stub(); - await render(hbs``); + await renderComponentWithParameters(); - assert.ok(this.onChange.notCalled); await triggerEvent('.oss-input-container input', 'paste', { - clipboardData: { getData: (format) => `clipboardFormat/${format}` } + clipboardData: { getData: (format: any) => `clipboardFormat/${format}` } }); - assert.ok(this.onChange.calledWith('clipboardFormat/Text')); + assert.ok(this.onChange.calledOnceWithExactly('testValueclipboardFormat/Text')); }); test('Not passing an @autocomplete parameter defaults to "on" state', async function (assert) { @@ -185,8 +179,7 @@ module('Integration | Component | o-s-s/input-container', function (hooks) { test('passing data-control-name works', async function (assert) { await render(hbs``); - let inputWrapper = find('.oss-input-container'); - assert.equal(inputWrapper.getAttribute('data-control-name'), 'firstname-input'); + assert.dom('.oss-input-container').hasAttribute('data-control-name', 'firstname-input'); }); }); }); diff --git a/tests/integration/components/o-s-s/upload-area-test.ts b/tests/integration/components/o-s-s/upload-area-test.ts index 6f7c7aa8d..8ab7d256d 100644 --- a/tests/integration/components/o-s-s/upload-area-test.ts +++ b/tests/integration/components/o-s-s/upload-area-test.ts @@ -7,12 +7,18 @@ import sinon from 'sinon'; import MockUploader from '@upfluence/oss-components/test-support/services/uploader'; import { setupToast } from '@upfluence/oss-components/test-support'; +import { ALLOWED_FEEDBACK_MESSAGE_TYPES } from '@upfluence/oss-components/utils'; const file = new File( [new Blob(['iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='])], '1px.png', { type: 'image/png' } ); +const PDFfile = new File( + [new Blob(['iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='])], + '1px.pdf', + { type: 'pdf' } +); module('Integration | Component | o-s-s/upload-area', function (hooks) { setupRenderingTest(hooks); @@ -217,20 +223,93 @@ module('Integration | Component | o-s-s/upload-area', function (hooks) { }); assert.ok( - this.toastErrorStub.calledWith( - this.intl.t(`oss-components.upload-area.errors.filetype.description`), - this.intl.t(`oss-components.upload-area.errors.filetype.title`) - ) + this.toastErrorStub + .getCall(0) + .calledWithExactly( + this.intl.t('oss-components.upload-area.errors.filesize.description', { max_filesize: '1B' }), + this.intl.t('oss-components.upload-area.errors.filesize.title') + ) ); - assert.ok( - this.toastErrorStub.calledWith( - this.intl.t('oss-components.upload-area.errors.filesize.description', { max_filesize: '1B' }), - this.intl.t('oss-components.upload-area.errors.filesize.title') - ) + this.toastErrorStub + .getCall(1) + .calledWithExactly( + this.intl.t(`oss-components.upload-area.errors.filetype.description`), + this.intl.t(`oss-components.upload-area.errors.filetype.title`) + ) ); }); + test('for filesize rules, it renders the correct local feedback message', async function (assert) { + this.validationRules = [{ type: 'filesize', value: '1B' }]; + + await render(hbs` + + `); + await triggerEvent('.oss-upload-area', 'drop', { + dataTransfer: { files: [this.file] } + }); + + assert.dom('.oss-upload-area').hasClass('oss-upload-area--error'); + assert + .dom(`.oss-upload-area-container .font-color-error-500`) + .hasText(this.intl.t('oss-components.upload-area.errors.filesize.feedback', { max_filesize: '1B' })); + }); + + test('for filetype rules, it renders the correct local feedback message', async function (assert) { + this.validationRules = [{ type: 'filetype', value: ['pdf'] }]; + + await render(hbs` + + `); + await triggerEvent('.oss-upload-area', 'drop', { + dataTransfer: { files: [this.file] } + }); + + assert.dom('.oss-upload-area').hasClass('oss-upload-area--error'); + assert + .dom(`.oss-upload-area-container .font-color-error-500`) + .hasText(this.intl.t('oss-components.upload-area.errors.filetype.feedback')); + }); + + test('the local feedback is removed after new upload', async function (assert) { + this.validationRules = [{ type: 'filetype', value: ['pdf'] }]; + + await render(hbs` + + `); + await triggerEvent('.oss-upload-area', 'drop', { dataTransfer: { files: [file] } }); + await triggerEvent('.oss-upload-area', 'drop', { dataTransfer: { files: [PDFfile] } }); + await waitFor('[data-control-name="upload-item-remove-button"]'); + await click('[data-control-name="upload-item-remove-button"]'); + + assert.dom('.oss-upload-area').doesNotHaveClass('oss-upload-area--error'); + assert.dom(`.oss-upload-area-container .font-color-error-500`).doesNotExist(); + }); + + test('the local feedback is removed after new upload in multiple mode', async function (assert) { + this.validationRules = [{ type: 'filetype', value: ['pdf'] }]; + + await render(hbs` + + `); + await triggerEvent('.oss-upload-area', 'drop', { dataTransfer: { files: [file] } }); + await triggerEvent('.oss-upload-area', 'drop', { dataTransfer: { files: [PDFfile] } }); + + assert.dom('.oss-upload-area').doesNotHaveClass('oss-upload-area--error'); + assert.dom(`.oss-upload-area-container .font-color-error-500`).doesNotExist(); + }); + test('if onDryRun is passed, the uploaded file is passed to it if validated and no upload item is displayed', async function (assert) { this.onDryRun = sinon.stub(); @@ -376,4 +455,156 @@ module('Integration | Component | o-s-s/upload-area', function (hooks) { assert.dom('.oss-upload-item').exists({ count: 1 }); }); }); + + module('for @onHandleFileUpload', function (hooks) { + hooks.beforeEach(function () { + this.onHandleFileUpload = sinon.stub(); + }); + + test('it is called when a file is dropped', async function (assert) { + await render(hbs` + + `); + await triggerEvent('.oss-upload-area', 'drop', { dataTransfer: { files: [file] } }); + + assert.true(this.onHandleFileUpload.calledOnceWithExactly()); + }); + + test('it is called when a file is selected via the file input', async function (assert) { + await render(hbs` + + `); + + const fileInput: HTMLInputElement = document.querySelector('.oss-upload-area-container input[type="file"]')!; + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + fileInput.files = dataTransfer.files; + + await triggerEvent(fileInput, 'change'); + + assert.true(this.onHandleFileUpload.calledOnceWithExactly()); + }); + + test('it is called before validation occurs', async function (assert) { + this.validationRules = [{ type: 'filetype', value: ['pdf'] }]; + + await render(hbs` + + `); + await triggerEvent('.oss-upload-area', 'drop', { dataTransfer: { files: [file] } }); + + assert.true(this.onHandleFileUpload.calledOnceWithExactly()); + }); + + test('it is called in multiple mode for each file upload', async function (assert) { + await render(hbs` + + `); + await triggerEvent('.oss-upload-area', 'drop', { dataTransfer: { files: [file] } }); + await triggerEvent('.oss-upload-area', 'drop', { dataTransfer: { files: [file] } }); + + assert.true(this.onHandleFileUpload.calledTwice); + }); + + test('it is not called when component is disabled', async function (assert) { + await render(hbs` + + `); + await triggerEvent('.oss-upload-area', 'drop', { dataTransfer: { files: [file] } }); + + assert.true(this.onHandleFileUpload.notCalled); + }); + }); + + module('for @feedbackMessage', function () { + test('it does not display feedback message when not provided', async function (assert) { + await render(hbs` + + `); + + assert.dom('.oss-upload-area-container .font-color-error-500').doesNotExist(); + assert.dom('.oss-upload-area-container .font-color-warning-500').doesNotExist(); + assert.dom('.oss-upload-area-container .font-color-success-500').doesNotExist(); + }); + + test('it does not display feedback message when value is empty', async function (assert) { + this.feedbackMessage = { type: 'error', value: '' }; + + await render(hbs` + + `); + + assert.dom('.oss-upload-area-container .font-color-error-500').doesNotExist(); + }); + + ALLOWED_FEEDBACK_MESSAGE_TYPES.forEach((type) => { + test(`it displays ${type} feedback message with proper styling`, async function (assert) { + this.feedbackMessage = { type, value: `This is an ${type} message` }; + + await render(hbs` + + `); + + assert.dom('.oss-upload-area').hasClass(`oss-upload-area--${type}`); + assert.dom(`.oss-upload-area-container .font-color-${type}-500`).hasText(`This is an ${type} message`); + }); + + test('after internal error, the correct feedback message takes precedence over this one', async function (assert) { + this.feedbackMessage = { type, value: `This is an ${type} message` }; + this.validationRules = [{ type: 'filetype', value: ['pdf'] }]; + + await render(hbs` + + `); + await triggerEvent('.oss-upload-area', 'drop', { + dataTransfer: { files: [this.file] } + }); + + assert.dom('.oss-upload-area').hasClass(`oss-upload-area--${type}`); + assert.dom(`.oss-upload-area-container .font-color-${type}-500`).hasText(`This is an ${type} message`); + }); + }); + + test('it does not apply type class when feedback message type is invalid', async function (assert) { + this.feedbackMessage = { type: 'invalid', value: 'Some message' }; + + await render(hbs` + + `); + + assert.dom('.oss-upload-area').hasNoClass('oss-upload-area--invalid'); + assert.dom('.oss-upload-area').hasNoClass('oss-upload-area--error'); + assert.dom('.oss-upload-area').hasNoClass('oss-upload-area--warning'); + assert.dom('.oss-upload-area').hasNoClass('oss-upload-area--success'); + }); + }); }); diff --git a/tests/integration/components/o-s-s/upload-item-test.ts b/tests/integration/components/o-s-s/upload-item-test.ts index 211cdfba1..ba2624cdb 100644 --- a/tests/integration/components/o-s-s/upload-item-test.ts +++ b/tests/integration/components/o-s-s/upload-item-test.ts @@ -94,21 +94,7 @@ module('Integration | Component | o-s-s/upload-item', function (hooks) { @onUploadSuccess={{this.onUploadSuccess}} /> `); - assert.dom('[data-control-name="upload-item-filesize]').doesNotExist(); - }); - - test('clicking the view button opens the file url', async function (assert) { - const windowOpenStub = sinon.stub(window, 'open'); - await render(hbs` - - `); - await click('[data-control-name="upload-item-view-button"]'); - assert.ok(windowOpenStub.calledOnceWithExactly(this.file.url, '_blank')); - windowOpenStub.restore(); + assert.dom('[data-control-name="upload-item-filesize"]').doesNotExist(); }); }); @@ -227,28 +213,87 @@ module('Integration | Component | o-s-s/upload-item', function (hooks) { }); module('common actions', function () { - test('clicking the edit button triggers the onEdition action', async function (assert) { - await render(hbs` - - `); - await click('[data-control-name="upload-item-edit-button"]'); - assert.ok(this.onEdition.calledOnce); + module('for edition button', () => { + test('clicking on it triggers the onEdition action', async function (assert) { + await render(hbs` + + `); + await click('[data-control-name="upload-item-edit-button"]'); + assert.ok(this.onEdition.calledOnce); + }); + + test('it renders the correct tooltip', async function (assert) { + await render(hbs` + + `); + await assert + .tooltip('[data-control-name="upload-item-edit-button"]') + .hasTitle(this.intl.t('oss-components.upload-area.tooltips.edit')); + }); }); - test('clicking the remove button triggers the onDeletion action', async function (assert) { - await render(hbs` - - `); - await click('[data-control-name="upload-item-remove-button"]'); - assert.ok(this.onFileDeletion.calledOnce); + module('for view button', () => { + test('clicking on it opens the file url', async function (assert) { + const windowOpenStub = sinon.stub(window, 'open'); + await render(hbs` + + `); + await click('[data-control-name="upload-item-view-button"]'); + assert.ok(windowOpenStub.calledOnceWithExactly(this.file.url, '_blank')); + windowOpenStub.restore(); + }); + + test('it renders the correct tooltip', async function (assert) { + await render(hbs` + + `); + await assert + .tooltip('[data-control-name="upload-item-view-button"]') + .hasTitle(this.intl.t('oss-components.upload-area.tooltips.view')); + }); + }); + + module('for delete button', () => { + test('clicking on it triggers the onDeletion action', async function (assert) { + await render(hbs` + + `); + await click('[data-control-name="upload-item-remove-button"]'); + assert.ok(this.onFileDeletion.calledOnce); + }); + + test('it renders the correct tooltip', async function (assert) { + await render(hbs` + + `); + await assert + .tooltip('[data-control-name="upload-item-remove-button"]') + .hasTitle(this.intl.t('oss-components.upload-area.tooltips.delete')); + }); }); }); }); diff --git a/translations/en-us.yaml b/translations/en-us.yaml index 8068680e5..3bd7841ef 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -46,9 +46,15 @@ oss-components: filetype: title: Unsupported format description: The uploaded file format is not allowed + feedback: Invalid format. Please upload a correctly formatted file. filesize: title: File size too large description: 'The maximum file size is {max_filesize}' + feedback: Invalid size. The maximum file size is {max_filesize}. + tooltips: + edit: Edit + view: View + delete: Delete url-input: default_format_error: This is not a valid URL. copy: