From 6464bf120354d7120c0d249a59064e752a1fea13 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Tue, 28 Oct 2025 17:05:26 +0200 Subject: [PATCH 1/3] Chat FU: Integrate FileView component --- .../chat/layout/chat-fileview/_index.scss | 4 +- .../chat/layout/chat-fileview/_mixins.scss | 4 + .../scss/widgets/fluent/chat/_index.scss | 4 +- .../scss/widgets/fluent/chat/_sizes.scss | 1 + .../scss/widgets/generic/chat/_index.scss | 4 +- .../scss/widgets/generic/chat/_sizes.scss | 1 + .../scss/widgets/material/chat/_index.scss | 4 +- .../scss/widgets/material/chat/_sizes.scss | 1 + .../devextreme/js/__internal/ui/chat/chat.ts | 21 +++++ .../js/__internal/ui/chat/file_view/file.ts | 4 +- .../__internal/ui/chat/file_view/file_view.ts | 1 - .../js/__internal/ui/chat/messagebubble.ts | 75 +++++++++++++++--- .../js/__internal/ui/chat/messagegroup.ts | 13 +++- .../js/__internal/ui/chat/messagelist.ts | 11 ++- .../chatParts/chat.tests.js | 47 +++++++++++ .../chatParts/messageBubble.markup.tests.js | 10 ++- .../chatParts/messageBubble.tests.js | 78 ++++++++++++++++++- 17 files changed, 252 insertions(+), 31 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_index.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_index.scss index f0befbed030f..dab10e4c6c92 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_index.scss @@ -39,7 +39,5 @@ } .dx-chat-file-view { - display: flex; - flex-wrap: wrap; - width: 100%; + display: grid; } diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss index 349c4a98bed9..ef763717a639 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss @@ -49,8 +49,12 @@ @mixin chat-fileview( $chat-file-view-items-gap, + $chat-file-view-margin-top, + $chat-file-container-width, ) { .dx-chat-file-view { gap: $chat-file-view-items-gap; + margin-top: $chat-file-view-margin-top; + grid-template-columns: repeat(auto-fit, $chat-file-container-width); } } diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss index 749b370d5f8b..11b105fcb626 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss @@ -29,7 +29,9 @@ $chat-file-file-size-max-width, ); @include chat-fileview( - $chat-file-view-items-gap + $chat-file-view-items-gap, + $chat-file-view-margin-top, + $chat-file-container-width, ); @include chat-alertlist( $chat-alertlist-color, diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss index 5e92a27e4bfa..03decd9372fa 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss @@ -74,6 +74,7 @@ $chat-file-secondary-font-size: 10px !default; $chat-file-base-section-gap: 4px !default; $chat-file-file-size-max-width: 4em !default; $chat-file-view-items-gap: 8px !default; +$chat-file-view-margin-top: 8px !default; @if $size == "default" { $chat-bubble-border-radius: $fluent-base-border-radius * 3 !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss index 66bc2f90a9e4..1099fb5f63c1 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss @@ -32,7 +32,9 @@ $chat-file-file-size-max-width, ); @include chat-fileview( - $chat-file-view-items-gap + $chat-file-view-items-gap, + $chat-file-view-margin-top, + $chat-file-container-width, ); @include chat-alertlist( $chat-alertlist-color, diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss index 0e9b87040363..52ad54c779ae 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss @@ -73,6 +73,7 @@ $chat-file-secondary-font-size: 10px !default; $chat-file-base-section-gap: 4px !default; $chat-file-file-size-max-width: 4em !default; $chat-file-view-items-gap: 8px !default; +$chat-file-view-margin-top: 8px !default; @if $size == "default" { $chat-bubble-padding: 8px 12px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss index d72c0a9c1e3d..38c2926c06a3 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss @@ -30,7 +30,9 @@ $chat-file-file-size-max-width, ); @include chat-fileview( - $chat-file-view-items-gap + $chat-file-view-items-gap, + $chat-file-view-margin-top, + $chat-file-container-width, ); @include chat-alertlist( $chat-alertlist-color, diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss index ef2ea1ce0e80..cf34d9aa2f91 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss @@ -71,6 +71,7 @@ $chat-file-secondary-font-size: 10px !default; $chat-file-base-section-gap: 4px !default; $chat-file-file-size-max-width: 4em !default; $chat-file-view-items-gap: 8px !default; +$chat-file-view-margin-top: 8px !default; @if $size == "default" { $chat-bubble-border-radius: 8px !default; diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index d80840f7e3d0..77fd22f74b56 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -8,6 +8,7 @@ import { isDefined } from '@js/core/utils/type'; import DataHelperMixin from '@js/data_helper'; import type dxChat from '@js/ui/chat'; import type { + AttachmentDownloadEvent, Message, MessageDeletedEvent, MessageDeletingEvent, @@ -73,6 +74,8 @@ class Chat extends Widget { _messageUpdatedAction?: (e: Partial) => void; + _attachmentDownloadAction?: (e: Partial) => void; + _getDefaultOptions(): Properties { return { ...super._getDefaultOptions(), @@ -125,6 +128,7 @@ class Chat extends Widget { this._createMessageUpdatedAction(); this._createTypingStartAction(); this._createTypingEndAction(); + this._createAttachmentDownloadAction(); } _dataSourceLoadErrorHandler(): void { @@ -224,6 +228,9 @@ class Chat extends Widget { onEscapeKeyPressed: () => { this.focus(); }, + onAttachmentDownload: (e) => { + this._attachmentDownloadHandler(e); + }, }; return options; @@ -521,6 +528,13 @@ class Chat extends Widget { ); } + _createAttachmentDownloadAction(): void { + this._attachmentDownloadAction = this._createActionByOption( + 'onAttachmentDownload', + { excludeValidators: ['disabled'] }, + ); + } + _messageEnteredHandler(e: MessageBoxMessageEnteredEvent): void { const { text, event } = e; const { user } = this.option(); @@ -560,6 +574,10 @@ class Chat extends Widget { this._typingEndAction?.({ user }); } + _attachmentDownloadHandler(e: AttachmentDownloadEvent): void { + this._attachmentDownloadAction?.(e); + } + _focusTarget(): dxElementWrapper { const $input = $(this.element()).find(`.${TEXTEDITOR_INPUT_CLASS}`); @@ -622,6 +640,9 @@ class Chat extends Widget { case 'onTypingEnd': this._createTypingEndAction(); break; + case 'onAttachmentDownload': + this._createAttachmentDownloadAction(); + break; case 'showDayHeaders': case 'showAvatar': case 'showUserName': diff --git a/packages/devextreme/js/__internal/ui/chat/file_view/file.ts b/packages/devextreme/js/__internal/ui/chat/file_view/file.ts index 4324b2896fa5..9676a3a8510e 100644 --- a/packages/devextreme/js/__internal/ui/chat/file_view/file.ts +++ b/packages/devextreme/js/__internal/ui/chat/file_view/file.ts @@ -28,8 +28,8 @@ export type Properties = DOMComponentProperties & { export const CHAT_FILE_CLASS = 'dx-chat-file'; const CHAT_FILE_ICON_CONTAINER_CLASS = 'dx-chat-file-icon-container'; -const CHAT_FILE_NAME_CLASS = 'dx-chat-file-name'; -const CHAT_FILE_SIZE_CLASS = 'dx-chat-file-size'; +export const CHAT_FILE_NAME_CLASS = 'dx-chat-file-name'; +export const CHAT_FILE_SIZE_CLASS = 'dx-chat-file-size'; const CHAT_FILE_DOWNLOAD_BUTTON_CLASS = 'dx-chat-file-download-button'; class File extends DOMComponent { diff --git a/packages/devextreme/js/__internal/ui/chat/file_view/file_view.ts b/packages/devextreme/js/__internal/ui/chat/file_view/file_view.ts index 7a56cf6f03fe..eca5ac27abfa 100644 --- a/packages/devextreme/js/__internal/ui/chat/file_view/file_view.ts +++ b/packages/devextreme/js/__internal/ui/chat/file_view/file_view.ts @@ -20,7 +20,6 @@ export interface FileViewProperties extends DOMComponentProperties { } export const CHAT_FILE_VIEW_CLASS = 'dx-chat-file-view'; -export const CHAT_FILE_VIEW_ITEM_CLASS = 'dx-chat-file-view-item'; class FileView extends DOMComponent { private _fileInstances: File[] = []; diff --git a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts index 8d92ddc9837c..75f1498a51ff 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts @@ -1,15 +1,19 @@ import messageLocalization from '@js/common/core/localization/message'; import { getPublicElement } from '@js/core/element'; +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; -import type { Message } from '@js/ui/chat'; +import type { Attachment, AttachmentDownloadEvent, Message } from '@js/ui/chat'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; import { ICON_CLASS } from '@ts/core/utils/m_icon'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; +import FileView from './file_view/file_view'; + export const CHAT_MESSAGEBUBBLE_CLASS = 'dx-chat-messagebubble'; export const CHAT_MESSAGEBUBBLE_DELETED_CLASS = 'dx-chat-messagebubble-deleted'; export const CHAT_MESSAGEBUBBLE_CONTENT_CLASS = 'dx-chat-messagebubble-content'; +export const CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS = 'dx-chat-messagebubble-attachments'; export const CHAT_MESSAGEBUBBLE_ICON_PROHIBITION_CLASS = `${ICON_CLASS}-cursorprohibition`; export const CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS = 'dx-has-image'; export const CHAT_MESSAGEBUBBLE_IMAGE_CLASS = 'dx-chat-messagebubble-image'; @@ -23,10 +27,16 @@ export interface Properties extends WidgetOptions { isEdited?: boolean; src?: string; alt?: string; + attachments?: Attachment[]; + onAttachmentDownload?: (e: AttachmentDownloadEvent) => void; template?: ((message: Message, container: Element) => void) | null; } class MessageBubble extends Widget { + _$content!: dxElementWrapper; + + _$attachmentsContainer!: dxElementWrapper; + _getDefaultOptions(): Properties { return { ...super._getDefaultOptions(), @@ -42,13 +52,25 @@ class MessageBubble extends Widget { $element.addClass(CHAT_MESSAGEBUBBLE_CLASS); - $('
') - .addClass(CHAT_MESSAGEBUBBLE_CONTENT_CLASS) - .appendTo($element); - super._initMarkup(); + this._renderContentContainer(); + this._renderAttachmentsContainer(); + this._updateContent(); + this._renderAttachments(); + } + + _renderContentContainer(): void { + this._$content = $('
') + .addClass(CHAT_MESSAGEBUBBLE_CONTENT_CLASS) + .appendTo(this.$element()); + } + + _renderAttachmentsContainer(): void { + this._$attachmentsContainer = $('
') + .addClass(CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS) + .appendTo(this.$element()); } _updateContent(): void { @@ -62,14 +84,14 @@ class MessageBubble extends Widget { } = this.option(); this.$element().removeClass(CHAT_MESSAGEBUBBLE_DELETED_CLASS); + this.$element().removeClass(CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS); - const $bubbleContainer = $(this.element()).find(`.${CHAT_MESSAGEBUBBLE_CONTENT_CLASS}`); - $bubbleContainer.empty(); + this._$content.empty(); if (template) { template({ type, text, src, alt, - }, getPublicElement($bubbleContainer)); + }, getPublicElement(this._$content)); return; } @@ -84,7 +106,7 @@ class MessageBubble extends Widget { const deletedMessage = $('
') .text(messageLocalization.format('dxChat-deletedMessageText')); - $bubbleContainer + this._$content .append(icon) .append(deletedMessage); @@ -98,11 +120,37 @@ class MessageBubble extends Widget { .attr('src', src ?? '') .attr('alt', alt ?? messageLocalization.format('dxChat-defaultImageAlt')) .addClass(CHAT_MESSAGEBUBBLE_IMAGE_CLASS) - .appendTo($bubbleContainer); + .appendTo(this._$content); break; case 'text': default: - $bubbleContainer.text(text ?? ''); + this._$content.text(text ?? ''); + } + } + + _renderAttachments(): void { + const { + isDeleted, + attachments, + activeStateEnabled, + focusStateEnabled, + hoverStateEnabled, + onAttachmentDownload, + } = this.option(); + + this._$attachmentsContainer.empty(); + + if (attachments?.length && !isDeleted) { + const $fileView = $('
'); + this._createComponent($fileView, FileView, { + files: attachments, + onDownload: onAttachmentDownload, + activeStateEnabled, + focusStateEnabled, + hoverStateEnabled, + }); + + this._$attachmentsContainer.append($fileView); } } @@ -123,6 +171,7 @@ class MessageBubble extends Widget { case 'isDeleted': this._updateMessageData(name, value); this._updateContent(); + this._renderAttachments(); break; case 'template': this._updateContent(); @@ -130,6 +179,10 @@ class MessageBubble extends Widget { case 'isEdited': this._updateMessageData(name, value); break; + case 'onAttachmentDownload': + case 'attachments': + this._renderAttachments(); + break; default: super._optionChanged(args); } diff --git a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts index a31a9dd8af7c..5ea8f9bebac8 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts @@ -5,7 +5,9 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import dateSerialization from '@js/core/utils/date_serialization'; import { isDate, isDefined } from '@js/core/utils/type'; -import type { ImageMessage, Message, TextMessage } from '@js/ui/chat'; +import type { + AttachmentDownloadEvent, ImageMessage, Message, TextMessage, +} from '@js/ui/chat'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; @@ -38,8 +40,9 @@ export interface Properties extends WidgetOptions { showAvatar: boolean; showUserName: boolean; showMessageTimestamp: boolean; - messageTemplate?: MessageTemplate; messageTimestampFormat?: Format; + messageTemplate?: MessageTemplate; + onAttachmentDownload?: (e: AttachmentDownloadEvent) => void; } class MessageGroup extends Widget { @@ -120,13 +123,15 @@ class MessageGroup extends Widget { } _getMessageBubbleOptions(message: Message): MessageBubbleProperties { + const { messageTemplate, onAttachmentDownload } = this.option(); + const options: MessageBubbleProperties = { isDeleted: message.isDeleted, type: message.type, + attachments: message.attachments, + onAttachmentDownload, }; - const { messageTemplate } = this.option(); - if (message.type === 'image') { options.alt = (message as ImageMessage).alt; options.src = (message as ImageMessage).src; diff --git a/packages/devextreme/js/__internal/ui/chat/messagelist.ts b/packages/devextreme/js/__internal/ui/chat/messagelist.ts index 71eb4c09b648..6479a1c28214 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagelist.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagelist.ts @@ -13,7 +13,9 @@ import { isElementInDom } from '@js/core/utils/dom'; import { getHeight } from '@js/core/utils/size'; import { isDate, isDefined } from '@js/core/utils/type'; import type { DxEvent } from '@js/events'; -import type { Message, TextMessage, User } from '@js/ui/chat'; +import type { + AttachmentDownloadEvent, Message, TextMessage, User, +} from '@js/ui/chat'; import type { Item as ContextMenuItem } from '@js/ui/context_menu'; import type dxContextMenu from '@js/ui/context_menu'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; @@ -102,6 +104,7 @@ export interface Properties extends WidgetOptions { showAvatar: boolean; showUserName: boolean; showMessageTimestamp: boolean; + onAttachmentDownload?: (e: AttachmentDownloadEvent) => void; onMessageEditingStart?: (e: MessageEditingEvent) => () => void; onMessageDeleting?: (e: MessageDeletingEvent) => void; onEscapeKeyPressed?: (e: KeyboardEvent) => void; @@ -273,8 +276,9 @@ class MessageList extends Widget { showAvatar, showUserName, showMessageTimestamp, - messageTemplate, messageTimestampFormat, + messageTemplate, + onAttachmentDownload, } = this.option(); const $messageGroup = $('
').appendTo(this._$content); @@ -285,8 +289,9 @@ class MessageList extends Widget { showAvatar, showUserName, showMessageTimestamp, - messageTemplate, messageTimestampFormat, + messageTemplate, + onAttachmentDownload, }); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js index 401a9ef5265f..3ffe9e1906a6 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js @@ -36,6 +36,8 @@ import { import { CHAT_CONFIRMATION_POPUP_WRAPPER_CLASS } from '__internal/ui/chat/confirmationpopup'; import { POPUP_CLASS } from '__internal/ui/popup/m_popup'; import { BUTTON_CLASS } from '__internal/ui/button/button'; +import { CHAT_FILE_CLASS } from '__internal/ui/chat/file_view/file'; + import MessageBubble from '__internal/ui/chat/messagebubble'; import ChatTextArea from '__internal/ui/chat/message_box/chat_text_area'; @@ -2246,6 +2248,51 @@ QUnit.module('Chat', () => { assert.strictEqual(onTypingEnd.callCount, 1); }); }); + + QUnit.module('onAttachmentDownload', { + beforeEach: function() { + moduleConfig.beforeEach.apply(this, arguments); + + this.getDownloadButton = () => this.$element.find(`.${CHAT_FILE_CLASS}`).find(`.${BUTTON_CLASS}`); + this.dataSourceWithAttachments = [ + { + attachments: [ + { + name: 'test.txt', + size: 1024, + }, + ], + } + ]; + }, + }, () => { + QUnit.test('should be called with correct arguments', function(assert) { + assert.expect(4); + + this.reinit({ + dataSource: this.dataSourceWithAttachments, + onAttachmentDownload: ({ component, element, attachment }) => { + assert.strictEqual(component, this.instance, 'component field is correct'); + assert.strictEqual(isRenderer(element), !!config().useJQuery, 'element is correct'); + assert.strictEqual($(element).is(this.$element), true, 'element field is correct'); + assert.deepEqual(attachment, this.dataSourceWithAttachments[0].attachments[0], 'attachment field is correct'); + }, + }); + + + this.getDownloadButton().trigger('dxclick'); + }); + + QUnit.test('should be possible to change at runtime', function(assert) { + const onAttachmentDownload = sinon.spy(); + + this.instance.option({ onAttachmentDownload, dataSource: this.dataSourceWithAttachments }); + + this.getDownloadButton().trigger('dxclick'); + + assert.strictEqual(onAttachmentDownload.callCount, 1); + }); + }); }); QUnit.module('renderMessage', moduleConfig, () => { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js index 658a0bf6a97a..ca404eb5472c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js @@ -6,7 +6,8 @@ import MessageBubble, { CHAT_MESSAGEBUBBLE_CONTENT_CLASS, CHAT_MESSAGEBUBBLE_DELETED_CLASS, CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS, - CHAT_MESSAGEBUBBLE_IMAGE_CLASS + CHAT_MESSAGEBUBBLE_IMAGE_CLASS, + CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS } from '__internal/ui/chat/messagebubble'; const moduleConfig = { @@ -39,6 +40,13 @@ QUnit.module('MessageBubble', moduleConfig, () => { assert.strictEqual($content.parent().is(this.$element), true, 'content element is direct child of root element'); }); + QUnit.test('root element should have a child attachments element with correct class', function(assert) { + const $content = this.$element.find(`.${CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS}`); + + assert.strictEqual($content.length, 1, 'attachments element exist'); + assert.strictEqual($content.parent().is(this.$element), true, 'attachments element is direct child of root element'); + }); + QUnit.test('root element should switch classes when deletion and undo happens', function(assert) { this.instance.option('isDeleted', true); assert.strictEqual(this.$element.hasClass(CHAT_MESSAGEBUBBLE_DELETED_CLASS), true, 'root element has deleted class'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js index 987c519ea2b7..50be4a912e97 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js @@ -2,10 +2,13 @@ import $ from 'jquery'; import messageLocalization from 'common/core/localization/message'; import MessageBubble, { - MESSAGE_DATA_KEY + MESSAGE_DATA_KEY, + CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS, + CHAT_MESSAGEBUBBLE_CONTENT_CLASS, } from '__internal/ui/chat/messagebubble'; - -const CHAT_MESSAGEBUBBLE_CONTENT_CLASS = 'dx-chat-messagebubble-content'; +import { BUTTON_CLASS } from '__internal/ui/button/button'; +import { CHAT_FILE_VIEW_CLASS } from '__internal/ui/chat/file_view/file_view'; +import { CHAT_FILE_CLASS, CHAT_FILE_NAME_CLASS, CHAT_FILE_SIZE_CLASS } from '__internal/ui/chat/file_view/file'; const moduleConfig = { beforeEach: function() { @@ -13,6 +16,8 @@ const moduleConfig = { this.instance = new MessageBubble($('#component'), options); this.$element = $(this.instance.$element()); this.$content = this.$element.find(`.${CHAT_MESSAGEBUBBLE_CONTENT_CLASS}`); + this.$attachments = this.$element.find(`.${CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS}`); + this.getDownloadButton = () => this.$element.find(`.${BUTTON_CLASS}`); }; this.reinit = (options) => { @@ -159,6 +164,73 @@ QUnit.module('MessageBubble', moduleConfig, () => { assert.strictEqual($bubbleContentChild.prop('tagName'), 'H1', 'content tag is correct'); assert.strictEqual($bubbleContentChild.text(), 'template text: text', 'content text is correct'); }); + + QUnit.test('should render FileView when attachments passed', function(assert) { + this.reinit({ attachments: [{ name: 'text.txt', size: 1024 }] }); + + const $fileView = this.$attachments.find(`.${CHAT_FILE_VIEW_CLASS}`); + const $file = $fileView.find(`.${CHAT_FILE_CLASS}`); + const $fileName = $file.find(`.${CHAT_FILE_NAME_CLASS}`); + const $fileSize = $file.find(`.${CHAT_FILE_SIZE_CLASS}`); + + assert.ok($fileView.length, 'FileView is rendered inside attachments container'); + assert.strictEqual($fileName.text(), 'text.txt', 'name rendered correctly'); + assert.strictEqual($fileSize.text(), '1 KB', 'size rendered correctly'); + }); + + QUnit.test('should not render FileView when no attachments passed', function(assert) { + this.reinit({ attachments: [] }); + + assert.strictEqual(this.$attachments.children().length, 0, 'attachments container is empty'); + }); + + QUnit.test('should not render FileView when isDeleted is true', function(assert) { + this.reinit({ + isDeleted: true, + attachments: [{ name: 'text.txt', size: 1024 }], + }); + + assert.strictEqual(this.$attachments.children().length, 0, 'no attachments rendered for deleted message'); + }); + + QUnit.test('should render attachments in runtime', function(assert) { + const attachments = [{ name: 'text.txt', size: 1024 }]; + let $fileView = this.$attachments.find(`.${CHAT_FILE_VIEW_CLASS}`); + + assert.strictEqual($fileView.length, 0, 'FileView is empty initially'); + + this.instance.option({ attachments }); + + $fileView = this.$attachments.find(`.${CHAT_FILE_VIEW_CLASS}`); + + assert.strictEqual($fileView.length, 1, 'FileView is rendered inside attachments container'); + }); + + QUnit.test('should pass onAttachmentDownload to FileView', function(assert) { + const onAttachmentDownload = sinon.spy(); + + this.reinit({ + attachments: [{ name: 'text.txt', size: 1024 }], + onAttachmentDownload, + }); + + this.getDownloadButton().trigger('dxclick'); + + assert.strictEqual(onAttachmentDownload.callCount, 1); + }); + + QUnit.test('should set onAttachmentDownload to FileView in runtime', function(assert) { + const onAttachmentDownload = sinon.spy(); + + this.reinit({ + attachments: [{ name: 'text.txt', size: 1024 }], + }); + + this.instance.option({ onAttachmentDownload }); + this.getDownloadButton().trigger('dxclick'); + + assert.strictEqual(onAttachmentDownload.callCount, 1); + }); }); }); From e31861d353d399d44fc1118dcf653fc1f96793fe Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Wed, 29 Oct 2025 18:36:44 +0200 Subject: [PATCH 2/3] Chat FU: Add display flex to message bubble & refactor --- .../chat/layout/chat-fileview/_mixins.scss | 2 - .../layout/chat-messagebubble/_index.scss | 2 + .../layout/chat-messagebubble/_mixins.scss | 2 + .../scss/widgets/fluent/chat/_index.scss | 2 +- .../scss/widgets/fluent/chat/_sizes.scss | 2 +- .../scss/widgets/generic/chat/_index.scss | 2 +- .../scss/widgets/generic/chat/_sizes.scss | 2 +- .../scss/widgets/material/chat/_index.scss | 2 +- .../scss/widgets/material/chat/_sizes.scss | 2 +- .../js/__internal/ui/chat/messagebubble.ts | 47 ++++++++++++------- .../js/__internal/ui/chat/messagegroup.ts | 10 ++-- .../chatParts/chat.tests.js | 2 +- .../chatParts/messageBubble.markup.tests.js | 8 ++-- .../chatParts/messageBubble.tests.js | 38 ++++++++++++--- 14 files changed, 82 insertions(+), 41 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss index ef763717a639..71ae8d4df5b5 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss @@ -49,12 +49,10 @@ @mixin chat-fileview( $chat-file-view-items-gap, - $chat-file-view-margin-top, $chat-file-container-width, ) { .dx-chat-file-view { gap: $chat-file-view-items-gap; - margin-top: $chat-file-view-margin-top; grid-template-columns: repeat(auto-fit, $chat-file-container-width); } } diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_index.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_index.scss index c4db59f8da99..6f6115f8efc0 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_index.scss @@ -9,6 +9,8 @@ $chat-messagebubble-max-height-image: 400px !default; } .dx-chat-messagebubble { + display: flex; + flex-direction: column; width: auto; word-break: break-word; white-space: break-spaces; diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_mixins.scss index 43b4d51abdfe..247cb72c3e61 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_mixins.scss @@ -8,6 +8,7 @@ $bubble-container-gap, $bubble-delete-gap, $bubble-delete-icon-size, + $chat-bubble-gap, ) { .dx-chat-messagegroup-content { row-gap: $bubble-container-gap; @@ -16,6 +17,7 @@ .dx-chat-messagebubble { padding: $padding; border-radius: $border-radius; + gap: $chat-bubble-gap; &.dx-chat-messagebubble-deleted { .dx-chat-messagebubble-content { diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss index 11b105fcb626..922af1025ab5 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss @@ -30,7 +30,6 @@ ); @include chat-fileview( $chat-file-view-items-gap, - $chat-file-view-margin-top, $chat-file-container-width, ); @include chat-alertlist( @@ -58,6 +57,7 @@ $chat-bubble-container-gap, $chat-bubble-delete-gap, $chat-bubble-delete-icon-size, + $chat-bubble-gap, ); @include chat-messagegroup( $chat-messagegroup-gap, diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss index 03decd9372fa..9d2868a5583a 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss @@ -74,7 +74,7 @@ $chat-file-secondary-font-size: 10px !default; $chat-file-base-section-gap: 4px !default; $chat-file-file-size-max-width: 4em !default; $chat-file-view-items-gap: 8px !default; -$chat-file-view-margin-top: 8px !default; +$chat-bubble-gap: 8px !default; @if $size == "default" { $chat-bubble-border-radius: $fluent-base-border-radius * 3 !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss index 1099fb5f63c1..9cb8276009cc 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss @@ -33,7 +33,6 @@ ); @include chat-fileview( $chat-file-view-items-gap, - $chat-file-view-margin-top, $chat-file-container-width, ); @include chat-alertlist( @@ -61,6 +60,7 @@ $chat-bubble-container-gap, $chat-bubble-delete-gap, $chat-bubble-delete-icon-size, + $chat-bubble-gap, ); @include chat-messagegroup( $chat-messagegroup-gap, diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss index 52ad54c779ae..cb25962b4634 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss @@ -73,7 +73,7 @@ $chat-file-secondary-font-size: 10px !default; $chat-file-base-section-gap: 4px !default; $chat-file-file-size-max-width: 4em !default; $chat-file-view-items-gap: 8px !default; -$chat-file-view-margin-top: 8px !default; +$chat-bubble-gap: 8px !default; @if $size == "default" { $chat-bubble-padding: 8px 12px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss index 38c2926c06a3..ab6d3169655f 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss @@ -31,7 +31,6 @@ ); @include chat-fileview( $chat-file-view-items-gap, - $chat-file-view-margin-top, $chat-file-container-width, ); @include chat-alertlist( @@ -59,6 +58,7 @@ $chat-bubble-container-gap, $chat-bubble-delete-gap, $chat-bubble-delete-icon-size, + $chat-bubble-gap, ); @include chat-messagegroup( $chat-messagegroup-gap, diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss index cf34d9aa2f91..57b06038b9ba 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss @@ -71,7 +71,7 @@ $chat-file-secondary-font-size: 10px !default; $chat-file-base-section-gap: 4px !default; $chat-file-file-size-max-width: 4em !default; $chat-file-view-items-gap: 8px !default; -$chat-file-view-margin-top: 8px !default; +$chat-bubble-gap: 8px !default; @if $size == "default" { $chat-bubble-border-radius: 8px !default; diff --git a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts index 75f1498a51ff..740b518caa34 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts @@ -7,13 +7,11 @@ import type { WidgetOptions } from '@js/ui/widget/ui.widget'; import { ICON_CLASS } from '@ts/core/utils/m_icon'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; - -import FileView from './file_view/file_view'; +import FileView from '@ts/ui/chat/file_view/file_view'; export const CHAT_MESSAGEBUBBLE_CLASS = 'dx-chat-messagebubble'; export const CHAT_MESSAGEBUBBLE_DELETED_CLASS = 'dx-chat-messagebubble-deleted'; export const CHAT_MESSAGEBUBBLE_CONTENT_CLASS = 'dx-chat-messagebubble-content'; -export const CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS = 'dx-chat-messagebubble-attachments'; export const CHAT_MESSAGEBUBBLE_ICON_PROHIBITION_CLASS = `${ICON_CLASS}-cursorprohibition`; export const CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS = 'dx-has-image'; export const CHAT_MESSAGEBUBBLE_IMAGE_CLASS = 'dx-chat-messagebubble-image'; @@ -35,7 +33,7 @@ export interface Properties extends WidgetOptions { class MessageBubble extends Widget { _$content!: dxElementWrapper; - _$attachmentsContainer!: dxElementWrapper; + _$attachments?: dxElementWrapper; _getDefaultOptions(): Properties { return { @@ -68,9 +66,14 @@ class MessageBubble extends Widget { } _renderAttachmentsContainer(): void { - this._$attachmentsContainer = $('
') - .addClass(CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS) - .appendTo(this.$element()); + const { attachments, isDeleted } = this.option(); + + this._$attachments?.remove(); + this._$attachments = undefined; + + if (attachments?.length && !isDeleted) { + this._$attachments = $('
').appendTo(this.$element()); + } } _updateContent(): void { @@ -83,8 +86,9 @@ class MessageBubble extends Widget { isDeleted = false, } = this.option(); - this.$element().removeClass(CHAT_MESSAGEBUBBLE_DELETED_CLASS); - this.$element().removeClass(CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS); + this.$element() + .removeClass(CHAT_MESSAGEBUBBLE_DELETED_CLASS) + .removeClass(CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS); this._$content.empty(); @@ -130,7 +134,6 @@ class MessageBubble extends Widget { _renderAttachments(): void { const { - isDeleted, attachments, activeStateEnabled, focusStateEnabled, @@ -138,19 +141,20 @@ class MessageBubble extends Widget { onAttachmentDownload, } = this.option(); - this._$attachmentsContainer.empty(); + if (!this._$attachments) { + return; + } - if (attachments?.length && !isDeleted) { - const $fileView = $('
'); - this._createComponent($fileView, FileView, { - files: attachments, - onDownload: onAttachmentDownload, + this._$attachments.empty(); + + if (attachments?.length) { + this._createComponent(this._$attachments, FileView, { activeStateEnabled, focusStateEnabled, hoverStateEnabled, + files: attachments, + onDownload: onAttachmentDownload, }); - - this._$attachmentsContainer.append($fileView); } } @@ -171,6 +175,12 @@ class MessageBubble extends Widget { case 'isDeleted': this._updateMessageData(name, value); this._updateContent(); + this._renderAttachmentsContainer(); + this._renderAttachments(); + break; + case 'type': + this._updateContent(); + this._renderAttachmentsContainer(); this._renderAttachments(); break; case 'template': @@ -181,6 +191,7 @@ class MessageBubble extends Widget { break; case 'onAttachmentDownload': case 'attachments': + this._renderAttachmentsContainer(); this._renderAttachments(); break; default: diff --git a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts index 5ea8f9bebac8..d01d3ff3f758 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts @@ -125,14 +125,16 @@ class MessageGroup extends Widget { _getMessageBubbleOptions(message: Message): MessageBubbleProperties { const { messageTemplate, onAttachmentDownload } = this.option(); + const { isDeleted, type, attachments } = message; + const options: MessageBubbleProperties = { - isDeleted: message.isDeleted, - type: message.type, - attachments: message.attachments, + isDeleted, + type, + attachments, onAttachmentDownload, }; - if (message.type === 'image') { + if (type === 'image') { options.alt = (message as ImageMessage).alt; options.src = (message as ImageMessage).src; } else { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js index 3ffe9e1906a6..9eba6119613c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js @@ -2253,7 +2253,7 @@ QUnit.module('Chat', () => { beforeEach: function() { moduleConfig.beforeEach.apply(this, arguments); - this.getDownloadButton = () => this.$element.find(`.${CHAT_FILE_CLASS}`).find(`.${BUTTON_CLASS}`); + this.getDownloadButton = () => this.$element.find(`.${CHAT_FILE_CLASS} .${BUTTON_CLASS}`); this.dataSourceWithAttachments = [ { attachments: [ diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js index ca404eb5472c..4a95d0a003e5 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js @@ -7,9 +7,10 @@ import MessageBubble, { CHAT_MESSAGEBUBBLE_DELETED_CLASS, CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS, CHAT_MESSAGEBUBBLE_IMAGE_CLASS, - CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS } from '__internal/ui/chat/messagebubble'; +import { CHAT_FILE_VIEW_CLASS } from '__internal/ui/chat/file_view/file_view'; + const moduleConfig = { beforeEach: function() { const init = (options = {}) => { @@ -40,8 +41,9 @@ QUnit.module('MessageBubble', moduleConfig, () => { assert.strictEqual($content.parent().is(this.$element), true, 'content element is direct child of root element'); }); - QUnit.test('root element should have a child attachments element with correct class', function(assert) { - const $content = this.$element.find(`.${CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS}`); + QUnit.test('root element should have a child attachments element with correct class if attachments provided', function(assert) { + this.reinit({ attachments: [{ name: 'text.txt', size: 1024 }] }); + const $content = this.$element.find(`.${CHAT_FILE_VIEW_CLASS}`); assert.strictEqual($content.length, 1, 'attachments element exist'); assert.strictEqual($content.parent().is(this.$element), true, 'attachments element is direct child of root element'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js index 50be4a912e97..b1432578667d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js @@ -3,8 +3,8 @@ import messageLocalization from 'common/core/localization/message'; import MessageBubble, { MESSAGE_DATA_KEY, - CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS, CHAT_MESSAGEBUBBLE_CONTENT_CLASS, + CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS } from '__internal/ui/chat/messagebubble'; import { BUTTON_CLASS } from '__internal/ui/button/button'; import { CHAT_FILE_VIEW_CLASS } from '__internal/ui/chat/file_view/file_view'; @@ -16,7 +16,7 @@ const moduleConfig = { this.instance = new MessageBubble($('#component'), options); this.$element = $(this.instance.$element()); this.$content = this.$element.find(`.${CHAT_MESSAGEBUBBLE_CONTENT_CLASS}`); - this.$attachments = this.$element.find(`.${CHAT_MESSAGEBUBBLE_ATTACHMENTS_CLASS}`); + this.$getAttachments = () => this.$element.find(`.${CHAT_FILE_VIEW_CLASS}`); this.getDownloadButton = () => this.$element.find(`.${BUTTON_CLASS}`); }; @@ -165,10 +165,28 @@ QUnit.module('MessageBubble', moduleConfig, () => { assert.strictEqual($bubbleContentChild.text(), 'template text: text', 'content text is correct'); }); + QUnit.test('should remove image class when message type changes from image to text', function(assert) { + const imageSrc = 'test.png'; + + this.reinit({ type: 'image', src: imageSrc }); + + assert.ok( + this.$element.hasClass(CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS), + 'initially has image class' + ); + + this.instance.option({ type: 'text' }); + + assert.notOk( + this.$element.hasClass(CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS), + 'image class is removed after changing type to text' + ); + }); + QUnit.test('should render FileView when attachments passed', function(assert) { this.reinit({ attachments: [{ name: 'text.txt', size: 1024 }] }); - const $fileView = this.$attachments.find(`.${CHAT_FILE_VIEW_CLASS}`); + const $fileView = this.$getAttachments(); const $file = $fileView.find(`.${CHAT_FILE_CLASS}`); const $fileName = $file.find(`.${CHAT_FILE_NAME_CLASS}`); const $fileSize = $file.find(`.${CHAT_FILE_SIZE_CLASS}`); @@ -178,10 +196,16 @@ QUnit.module('MessageBubble', moduleConfig, () => { assert.strictEqual($fileSize.text(), '1 KB', 'size rendered correctly'); }); + QUnit.test('should not render attachments container when no attachments passed', function(assert) { + this.reinit({ attachments: [] }); + + assert.strictEqual(this.$getAttachments().length, 0, 'attachments container is empty'); + }); + QUnit.test('should not render FileView when no attachments passed', function(assert) { this.reinit({ attachments: [] }); - assert.strictEqual(this.$attachments.children().length, 0, 'attachments container is empty'); + assert.strictEqual(this.$getAttachments().children().length, 0, 'attachments container is empty'); }); QUnit.test('should not render FileView when isDeleted is true', function(assert) { @@ -190,18 +214,18 @@ QUnit.module('MessageBubble', moduleConfig, () => { attachments: [{ name: 'text.txt', size: 1024 }], }); - assert.strictEqual(this.$attachments.children().length, 0, 'no attachments rendered for deleted message'); + assert.strictEqual(this.$getAttachments().children().length, 0, 'no attachments rendered for deleted message'); }); QUnit.test('should render attachments in runtime', function(assert) { const attachments = [{ name: 'text.txt', size: 1024 }]; - let $fileView = this.$attachments.find(`.${CHAT_FILE_VIEW_CLASS}`); + let $fileView = this.$getAttachments(); assert.strictEqual($fileView.length, 0, 'FileView is empty initially'); this.instance.option({ attachments }); - $fileView = this.$attachments.find(`.${CHAT_FILE_VIEW_CLASS}`); + $fileView = this.$getAttachments(); assert.strictEqual($fileView.length, 1, 'FileView is rendered inside attachments container'); }); From 447c0ecb08e9d163bdda1f332cc2980b52a30001 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Thu, 30 Oct 2025 13:30:25 +0200 Subject: [PATCH 3/3] Chat FU: Remove _attachmentDownloadHandler & rename _renderAttachmentsElement --- packages/devextreme/js/__internal/ui/chat/chat.ts | 6 +----- .../devextreme/js/__internal/ui/chat/messagebubble.ts | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index 77fd22f74b56..3cad42ca431c 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -229,7 +229,7 @@ class Chat extends Widget { this.focus(); }, onAttachmentDownload: (e) => { - this._attachmentDownloadHandler(e); + this._attachmentDownloadAction?.(e); }, }; @@ -574,10 +574,6 @@ class Chat extends Widget { this._typingEndAction?.({ user }); } - _attachmentDownloadHandler(e: AttachmentDownloadEvent): void { - this._attachmentDownloadAction?.(e); - } - _focusTarget(): dxElementWrapper { const $input = $(this.element()).find(`.${TEXTEDITOR_INPUT_CLASS}`); diff --git a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts index 740b518caa34..3bd98a3e4fbc 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts @@ -53,7 +53,7 @@ class MessageBubble extends Widget { super._initMarkup(); this._renderContentContainer(); - this._renderAttachmentsContainer(); + this._renderAttachmentsElement(); this._updateContent(); this._renderAttachments(); @@ -65,7 +65,7 @@ class MessageBubble extends Widget { .appendTo(this.$element()); } - _renderAttachmentsContainer(): void { + _renderAttachmentsElement(): void { const { attachments, isDeleted } = this.option(); this._$attachments?.remove(); @@ -175,12 +175,12 @@ class MessageBubble extends Widget { case 'isDeleted': this._updateMessageData(name, value); this._updateContent(); - this._renderAttachmentsContainer(); + this._renderAttachmentsElement(); this._renderAttachments(); break; case 'type': this._updateContent(); - this._renderAttachmentsContainer(); + this._renderAttachmentsElement(); this._renderAttachments(); break; case 'template': @@ -191,7 +191,7 @@ class MessageBubble extends Widget { break; case 'onAttachmentDownload': case 'attachments': - this._renderAttachmentsContainer(); + this._renderAttachmentsElement(); this._renderAttachments(); break; default: