Skip to content

Commit

Permalink
Chat: data layer integration
Browse files Browse the repository at this point in the history
  • Loading branch information
EugeniyKiyashko committed Sep 30, 2024
1 parent 396fa5a commit 349f24e
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 4 deletions.
45 changes: 41 additions & 4 deletions packages/devextreme/js/__internal/ui/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import registerComponent from '@js/core/component_registrator';
import Guid from '@js/core/guid';
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
import { isDefined } from '@js/core/utils/type';
import type { Options as DataSourceOptions } from '@js/data/data_source';
import { DataSource } from '@js/data/data_source/data_source';
import DataHelperMixin from '@js/data_helper';
import type { Message, MessageSendEvent, Properties as ChatProperties } from '@js/ui/chat';
import type { OptionChanged } from '@ts/core/widget/types';
import Widget from '@ts/core/widget/widget';
Expand Down Expand Up @@ -45,9 +49,27 @@ class Chat extends Widget<Properties> {
_init(): void {
super._init();

// @ts-expect-error
this._initDataController();

// @ts-expect-error
this._refreshDataSource();

this._createMessageSendAction();
}

_dataSourceLoadErrorHandler(): void {
this.option('items', []);
}

_dataSourceChangedHandler(newItems: Message[]): void {
this.option('items', newItems.slice());
}

_dataSourceOptions(): DataSourceOptions {
return { paginate: false };
}

_initMarkup(): void {
$(this.element()).addClass(CHAT_CLASS);

Expand Down Expand Up @@ -171,9 +193,12 @@ class Chat extends Widget<Properties> {
break;
}
case 'items':
case 'dataSource':
this._messageList.option(name, value);
break;
case 'dataSource':
// @ts-expect-error
this._refreshDataSource();
break;
case 'onMessageSend':
this._createMessageSendAction();
break;
Expand All @@ -183,14 +208,26 @@ class Chat extends Widget<Properties> {
}

renderMessage(message: Message = {}): void {
const { items } = this.option();
// @ts-expect-error
const dataSource = this.getDataSource();

const newItems = [...items ?? [], message];
if (!isDefined(dataSource)) {
const { items } = this.option();

this.option('items', newItems);
const newItems = [...items ?? [], message];
this.option('items', newItems);
return;
}

dataSource.store().insert(message).done(() => {
dataSource.load();
});
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Chat as any).include(DataHelperMixin);

registerComponent('dxChat', Chat);

export default Chat;
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import Chat from 'ui/chat';
import MessageList from '__internal/ui/chat/messagelist';
import MessageBox from '__internal/ui/chat/messagebox';
import keyboardMock from '../../../helpers/keyboardMock.js';
import { DataSource } from 'data/data_source/data_source';

import { isRenderer } from 'core/utils/type';

import config from 'core/config';
import ArrayStore from 'data/array_store';

const CHAT_HEADER_TEXT_CLASS = 'dx-chat-header-text';
const CHAT_MESSAGEGROUP_CLASS = 'dx-chat-messagegroup';
Expand All @@ -16,6 +18,7 @@ const CHAT_MESSAGEBUBBLE_CLASS = 'dx-chat-messagebubble';
const CHAT_MESSAGEBOX_CLASS = 'dx-chat-messagebox';
const CHAT_MESSAGEBOX_BUTTON_CLASS = 'dx-chat-messagebox-button';
const CHAT_MESSAGEBOX_TEXTAREA_CLASS = 'dx-chat-messagebox-textarea';
const CHAT_MESSAGELIST_EMPTY_VIEW_CLASS = 'dx-chat-messagelist-empty-view';

const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input';

Expand Down Expand Up @@ -395,6 +398,152 @@ QUnit.module('Chat', moduleConfig, () => {

assert.strictEqual(activeElement, this.$input.get(0));
});

QUnit.test('getDataSource() should return null when dataSource is not defined', function(assert) {
this.reinit({
items: []
});

assert.strictEqual(this.instance.getDataSource(), null);
});

QUnit.test('getDataSource() should return the dataSource object when dataSource is passed', function(assert) {
this.reinit({
dataSource: [{ text: 'message_text' }]
});

assert.ok(this.instance.getDataSource() instanceof DataSource);
});
});

QUnit.module('Data Layer Integration', moduleConfig, () => {
QUnit.test('Should render empty view container if dataSource is empty', function(assert) {
this.reinit({
dataSource: {
store: new ArrayStore([])
}
});

assert.strictEqual(this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`).length, 1);
});

QUnit.test('Should remove or render empty view container after dataSource is updated at runtime', function(assert) {
this.instance.option('dataSource', {
store: new ArrayStore([{}]),
});

assert.strictEqual(this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`).length, 0);

this.instance.option('dataSource', {
store: new ArrayStore([])
});

assert.strictEqual(this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`).length, 1);
});

QUnit.test('Items should synchronize with dataSource when declared as an array', function(assert) {
const messages = [{ text: 'message_1' }, { text: 'message_2' }];
this.reinit({
dataSource: messages,
});

assert.deepEqual(this.instance.option('items'), messages);
});

QUnit.test('items option should be updated after calling renderMessage(newMessage)', function(assert) {
const messages = [{ text: 'message_1' }, { text: 'message_2' }];
this.reinit({
items: messages,
});

const newMessage = { text: 'message_3' };
this.instance.renderMessage(newMessage);

assert.deepEqual(this.instance.option('items'), [...messages, newMessage]);
assert.deepEqual(this.instance.option('dataSource'), null);
});

QUnit.test('dataSource option should be updated after calling renderMessage(newMessage)', function(assert) {
const messages = [{ text: 'message_1' }, { text: 'message_2' }];
this.reinit({
dataSource: messages,
});

const newMessage = { text: 'message_3' };
this.instance.renderMessage(newMessage);

const expectedData = [...messages, newMessage];
assert.deepEqual(this.instance.option('items'), expectedData);
assert.deepEqual(this.instance.option('dataSource'), expectedData);
});

QUnit.test('Items should synchronize with DataSource store', function(assert) {
const messages = [{ text: 'message_1' }, { text: 'message_2' }];

this.reinit({
dataSource: new DataSource({
store: new ArrayStore({
data: messages,
}),
})
});

assert.deepEqual(this.instance.option('items'), messages);
});

QUnit.test('Items should synchronize with DataSource store after adding new message', function(assert) {
const messages = [{ text: 'message_1' }, { text: 'message_2' }];

this.reinit({
dataSource: new DataSource({
store: new ArrayStore({
data: [...messages],
}),
})
});

const newMessage = { text: 'message_3' };
this.instance.renderMessage(newMessage);

assert.deepEqual(this.instance.option('items'), [...messages, newMessage]);
});

QUnit.test('Items should synchronize with dataSource when declared as a store', function(assert) {
const messages = [{ text: 'message_1' }, { text: 'message_2' }];
this.reinit({
dataSource: new ArrayStore(messages),
});

assert.deepEqual(this.instance.option('items'), messages);
});

QUnit.test('DataSource pagination is false by default', function(assert) {
this.instance.option('dataSource', {
store: new ArrayStore([{}]),
});

assert.strictEqual(this.instance.getDataSource().paginate(), false);
});

QUnit.test('should handle dataSource loading error', function(assert) {
const deferred = $.Deferred();
const messages = [{ text: 'message_1' }, { text: 'message_2' }];
this.reinit({
dataSource: messages
});

this.instance.option({
dataSource: {
load() {
return deferred.promise();
}
},
});

deferred.reject();

assert.strictEqual(this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`).length, 1, 'empty view container was rendered on loading failure');
});
});
});

Expand Down

0 comments on commit 349f24e

Please sign in to comment.