Skip to content

Commit f6db675

Browse files
Chat: the Storybook story was added for Editing feature (DevExpress#29767)
Co-authored-by: EugeniyKiyashko <[email protected]>
1 parent cb48072 commit f6db675

File tree

2 files changed

+213
-9
lines changed

2 files changed

+213
-9
lines changed

apps/react-storybook/stories/chat/Chat.stories.tsx

Lines changed: 205 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useCallback, useEffect, useMemo } from 'react';
22
import { Chat, ChatTypes } from 'devextreme-react/chat'
3-
import { Button } from "devextreme-react";
3+
import { Button, Toast } from 'devextreme-react';
44
import type { Meta, StoryObj } from '@storybook/react';
55
import DataSource from 'devextreme/data/data_source';
66
import CustomStore from 'devextreme/data/custom_store';
@@ -17,10 +17,12 @@ import {
1717
regenerationMessage,
1818
assistant,
1919
} from './data';
20-
import { Popup } from 'devextreme-react/popup';
20+
import { Popup, ToolbarItem } from 'devextreme-react/popup';
2121
import HTMLReactParser from 'html-react-parser';
2222

2323
import './styles.css';
24+
import { Guid } from 'devextreme-react/cjs/common';
25+
import { Message } from 'devextreme/artifacts/npm/devextreme/ui/chat';
2426

2527
const meta: Meta<typeof Chat> = {
2628
title: 'Components/Chat',
@@ -99,7 +101,7 @@ export const Overview: Story = {
99101
focusStateEnabled,
100102
}) => {
101103
const [messages, setMessages] = useState(items);
102-
104+
103105
const onMessageEntered = useCallback(({ message }) => {
104106
const updatedMessages = [...messages, message];
105107

@@ -344,6 +346,9 @@ export const PopupIntegration: Story = {
344346
visible={true}
345347
showCloseButton={false}
346348
title="Chat title"
349+
wrapperAttr={{
350+
class: 'chat-popup-integration'
351+
}}
347352
position={{
348353
my: 'right bottom',
349354
at: 'right bottom',
@@ -588,3 +593,200 @@ export const AIBotIntegration: Story = {
588593
);
589594
}
590595
}
596+
597+
export const Editing: Story = {
598+
args: {
599+
user: firstAuthor,
600+
activeStateEnabled: true,
601+
hoverStateEnabled: true,
602+
focusStateEnabled: true,
603+
width: "600px",
604+
height: "600px",
605+
allowDeleting: true,
606+
allowUpdating: true,
607+
useCustomMessageRender: false,
608+
cancelMessageEditingStart: false,
609+
cancelMessageDeleting: false,
610+
allowOnlyLatinTextOnEdit: false,
611+
},
612+
argTypes: {
613+
user: {
614+
control: 'select',
615+
options: [firstAuthor.name, secondAuthor.name],
616+
mapping: {
617+
[firstAuthor.name]: firstAuthor,
618+
[secondAuthor.name]: secondAuthor,
619+
},
620+
defaultValue: firstAuthor.name,
621+
},
622+
useCustomMessageRender: {
623+
control: 'boolean',
624+
name: 'Use Custom Message Renderer',
625+
},
626+
cancelMessageEditingStart: {
627+
control: 'boolean',
628+
name: 'Emulate Message Editing Cancellation',
629+
},
630+
cancelMessageDeleting: {
631+
control: 'boolean',
632+
name: 'Emulate Message Deleting Cancellation',
633+
},
634+
allowOnlyLatinTextOnEdit: {
635+
control: 'boolean',
636+
name: 'Allow only latin letters in message editing',
637+
}
638+
},
639+
render: ({
640+
width,
641+
height,
642+
user,
643+
activeStateEnabled,
644+
hoverStateEnabled,
645+
focusStateEnabled,
646+
allowDeleting,
647+
allowUpdating,
648+
useCustomMessageRender,
649+
cancelMessageEditingStart,
650+
cancelMessageDeleting,
651+
allowOnlyLatinTextOnEdit,
652+
}) => {
653+
const messages = useMemo(() => {
654+
const initial = initialMessages.map(item => ({
655+
...item,
656+
id: `dx-${new Guid()}`,
657+
}));
658+
initial[4].isDeleted = true;
659+
initial[5].isDeleted = true;
660+
initial[6].isDeleted = true;
661+
return initial;
662+
}, [initialMessages]);
663+
664+
const [toastConfig, setToastConfig] = useState({
665+
visible: false,
666+
message: '',
667+
});
668+
669+
const messagesRef = React.useRef([...messages]);
670+
671+
const dataSource = useMemo(() => new DataSource({
672+
store: new CustomStore({
673+
load: () => new Promise(resolve => setTimeout(() => resolve([...messagesRef.current]), 500)),
674+
insert: (message) => {
675+
message.id = `dx-${new Guid()}`;
676+
if (message.author.id === user?.id) {
677+
message.author = {
678+
...message.author,
679+
...user,
680+
}
681+
}
682+
messagesRef.current.push(message);
683+
return new Promise<void>(resolve => setTimeout(resolve, 200));
684+
},
685+
key: 'id',
686+
}),
687+
paginate: false,
688+
}), [user]);
689+
690+
const onUndoClick = useCallback((message: Message) => {
691+
const store = dataSource.store();
692+
store.push([{ type: 'update', key: message.id, data: { isDeleted: false } }]);
693+
}, [dataSource]);
694+
695+
const messageRender = useCallback(({ message }) => {
696+
if(message.isDeleted === true) {
697+
return (
698+
<div
699+
className="dx-chat-messagebubble-content dx-chat-messagebubble-deleted"
700+
style={{
701+
display: 'flex',
702+
gap: 4,
703+
alignItems: 'center',
704+
fontStyle: 'italic',
705+
}}
706+
>
707+
<div className="dx-icon dx-icon-cursorprohibition"></div>
708+
<div>This message was deleted</div>
709+
{ user?.id === message.author.id &&
710+
<a href="#" onClick={(e) => {
711+
e.preventDefault();
712+
onUndoClick(message);
713+
}}>Undo</a>
714+
}
715+
</div>
716+
);
717+
}
718+
719+
return <div>{message.text}</div>;
720+
}, [user]);
721+
722+
const showToast = useCallback((message: string) => {
723+
setToastConfig({
724+
visible: true,
725+
message,
726+
});
727+
}, []);
728+
729+
const onToastHiding = useCallback(() => {
730+
setToastConfig({
731+
visible: false,
732+
message: '',
733+
});
734+
}, []);
735+
736+
const validateMessage = (message: string) => message.match(/^[a-zA-Z0-9.,!? ]+$/);
737+
738+
return (
739+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
740+
<Chat
741+
editing={{ allowDeleting, allowUpdating }}
742+
width={width}
743+
height={height}
744+
dataSource={dataSource}
745+
reloadOnChange={false}
746+
user={user}
747+
onMessageEntered={(e) => {
748+
e.component.getDataSource().store().push([{ type: 'insert', data: e.message }]);
749+
}}
750+
onMessageEditingStart={async (e) => {
751+
if (cancelMessageEditingStart) {
752+
showToast('Message editing not allowed');
753+
e.cancel = true;
754+
}
755+
}}
756+
onMessageEditCanceled={() => {
757+
showToast('Message editing is canceled');
758+
}}
759+
onMessageDeleting={(e) => {
760+
if (cancelMessageDeleting) {
761+
showToast('Message deleting was canceled');
762+
e.cancel = true;
763+
}
764+
}}
765+
onMessageDeleted={(e) => {
766+
e.component.getDataSource().store().push([{ type: 'update', key: e.message.id, data: { isDeleted: true } }]);
767+
}}
768+
onMessageUpdating={async (e) => {
769+
if (allowOnlyLatinTextOnEdit) {
770+
if (!validateMessage(e.text ?? '')) {
771+
showToast('Only latin allowed in message');
772+
e.cancel = true;
773+
}
774+
}
775+
}}
776+
onMessageUpdated={(e) => {
777+
e.component.getDataSource().store().push([{ type: 'update', key: e.message.id, data: { text: e.text, isEdited: true } }]);
778+
}}
779+
activeStateEnabled={activeStateEnabled}
780+
focusStateEnabled={focusStateEnabled}
781+
hoverStateEnabled={hoverStateEnabled}
782+
{...(useCustomMessageRender && { messageRender })}
783+
/>
784+
<Toast
785+
{...toastConfig}
786+
onHiding={onToastHiding}
787+
displayTime={600}
788+
/>
789+
</div>
790+
);
791+
}
792+
};
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
.chat-popup-wrapper .dx-popup-content {
2-
padding: 0;
3-
}
4-
5-
.chat-popup-wrapper .dx-chat {
6-
border: none;
1+
.chat-popup-integration {
2+
&.dx-popup-wrapper .dx-popup-content {
3+
padding: 0;
4+
}
5+
6+
&.dx-popup-wrapper .dx-chat {
7+
border: none;
8+
}
79
}

0 commit comments

Comments
 (0)