diff --git a/apps/builder/package.json b/apps/builder/package.json index 0e628d512d..2df6818086 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -19,6 +19,8 @@ "ts-check": "tsc --noEmit" }, "dependencies": { + "@chatscope/chat-ui-kit-react": "^1.10.1", + "@chatscope/chat-ui-kit-styles": "^1.4.0", "@codemirror/autocomplete": "^6.4.0", "@codemirror/commands": "^6.1.3", "@codemirror/lang-html": "^6.4.0", @@ -31,6 +33,15 @@ "@codemirror/search": "^6.2.3", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.7.2", + "@editorjs/checklist": "^1.5.0", + "@editorjs/code": "^2.8.0", + "@editorjs/editorjs": "^2.27.2", + "@editorjs/embed": "^2.5.3", + "@editorjs/header": "^2.7.0", + "@editorjs/image": "^2.8.1", + "@editorjs/inline-code": "^1.4.0", + "@editorjs/list": "^1.8.0", + "@editorjs/marker": "^1.3.0", "@emotion/cache": "^11.10.5", "@fingerprintjs/fingerprintjs": "^3.4.0", "@illa-design/react": "workspace:*", @@ -56,6 +67,8 @@ "dayjs": "^1.11.7", "deep-diff": "^1.0.2", "downloadjs": "^1.4.7", + "editorjs-html": "^3.4.3", + "editorjs-md-parser": "^0.0.3", "events": "^3.3.0", "fnv-plus": "^1.3.1", "framer-motion": "^7.6.12", @@ -91,6 +104,7 @@ "react-slick": "^0.29.0", "react-use": "^17.4.0", "react-use-measure": "^2.1.1", + "react-virtuoso": "^4.3.11", "redux-logger": "^3.0.6", "rehype-raw": "^6.1.1", "rehype-sanitize": "^5.0.1", diff --git a/apps/builder/src/assets/chat/delete.svg b/apps/builder/src/assets/chat/delete.svg new file mode 100644 index 0000000000..46115135f2 --- /dev/null +++ b/apps/builder/src/assets/chat/delete.svg @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/apps/builder/src/assets/chat/empty.svg b/apps/builder/src/assets/chat/empty.svg new file mode 100644 index 0000000000..c69a1d65c5 --- /dev/null +++ b/apps/builder/src/assets/chat/empty.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/apps/builder/src/assets/chat/replay.svg b/apps/builder/src/assets/chat/replay.svg new file mode 100644 index 0000000000..b93f82c411 --- /dev/null +++ b/apps/builder/src/assets/chat/replay.svg @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/apps/builder/src/assets/widgetCover/chat.svg b/apps/builder/src/assets/widgetCover/chat.svg new file mode 100644 index 0000000000..b41a712170 --- /dev/null +++ b/apps/builder/src/assets/widgetCover/chat.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/builder/src/assets/widgetCover/richText.svg b/apps/builder/src/assets/widgetCover/richText.svg new file mode 100644 index 0000000000..127bbca822 --- /dev/null +++ b/apps/builder/src/assets/widgetCover/richText.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/builder/src/page/App/components/PanelSetters/ChartSetter/chartDatasetsSetter/listItem.tsx b/apps/builder/src/page/App/components/PanelSetters/ChartSetter/chartDatasetsSetter/listItem.tsx index da5c8d35ca..57ed07d8d5 100644 --- a/apps/builder/src/page/App/components/PanelSetters/ChartSetter/chartDatasetsSetter/listItem.tsx +++ b/apps/builder/src/page/App/components/PanelSetters/ChartSetter/chartDatasetsSetter/listItem.tsx @@ -1,10 +1,12 @@ import { FC, useCallback, useContext, useState } from "react" +import { useTranslation } from "react-i18next" import { EyeOffIcon, EyeOnIcon, Trigger, globalColor, illaPrefix, + useModal, } from "@illa-design/react" import { ReactComponent as DeleteIcon } from "@/assets/delete-dataset-icon.svg" import { DatasetsContext } from "@/page/App/components/PanelSetters/ChartSetter/chartDatasetsSetter/datasetsContext" @@ -123,6 +125,8 @@ export const ColorArea: FC = ({ color }) => { export const ListItem: FC = (props) => { const { color, isHidden, datasetName, datasetMethod, index } = props const [modalVisible, setModalVisible] = useState(false) + const modal = useModal() + const { t } = useTranslation() const handleCloseModal = useCallback(() => { setModalVisible(false) @@ -135,6 +139,25 @@ export const ListItem: FC = (props) => { handleHiddenDataset, handleDeleteDataSet, } = useContext(DatasetsContext) + + const handleDeleteClick = useCallback(() => { + modal.show({ + id: "deleteDatasetItem", + title: t("editor.component.delete_title", { + displayName: datasetName, + }), + children: t("editor.component.delete_content"), + cancelText: t("editor.component.cancel"), + okText: t("editor.component.delete"), + okButtonProps: { + colorScheme: "red", + }, + onOk: () => { + handleDeleteDataSet(index) + }, + }) + }, [datasetName, handleDeleteDataSet, index, modal, t]) + return ( = (props) => { css={baseIconStyle} onClick={(e) => { e.stopPropagation() - handleDeleteDataSet(index) + handleDeleteClick() }} /> diff --git a/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/actionMenu.tsx b/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/actionMenu.tsx index 6aff107ce2..6c2bde6084 100644 --- a/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/actionMenu.tsx +++ b/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/actionMenu.tsx @@ -1,15 +1,34 @@ -import { FC, useContext } from "react" +import { FC, useCallback, useContext } from "react" import { useTranslation } from "react-i18next" -import { DropList, DropListItem } from "@illa-design/react" +import { DropList, DropListItem, useModal } from "@illa-design/react" import { OptionListSetterContext } from "@/page/App/components/PanelSetters/OptionListSetter/context/optionListContext" import { ActionMenuProps } from "@/page/App/components/PanelSetters/OptionListSetter/interface" export const ActionMenu: FC = (props) => { - const { index, handleCloseMode } = props + const { index, label, handleCloseMode } = props const { handleCopyOptionItem, handleDeleteOptionItem } = useContext( OptionListSetterContext, ) const { t } = useTranslation() + const modal = useModal() + const handleDeleteClick = useCallback(() => { + modal.show({ + id: "deleteOptionItem", + title: t("editor.component.delete_title", { + displayName: label, + }), + children: t("editor.component.delete_content"), + cancelText: t("editor.component.cancel"), + okText: t("editor.component.delete"), + okButtonProps: { + colorScheme: "red", + }, + onOk: () => { + handleDeleteOptionItem(index) + handleCloseMode() + }, + }) + }, [handleCloseMode, handleDeleteOptionItem, index, label, modal, t]) return ( @@ -29,10 +48,7 @@ export const ActionMenu: FC = (props) => { "editor.inspect.setter_content.option_list.action_menu.delete", )} deleted - onClick={() => { - handleDeleteOptionItem(index) - handleCloseMode() - }} + onClick={handleDeleteClick} /> ) diff --git a/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/interface.ts b/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/interface.ts index b5f2d7049f..a272a44b94 100644 --- a/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/interface.ts +++ b/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/interface.ts @@ -25,6 +25,7 @@ export interface DragIconAndLabelProps { export interface MoreProps { index: number + label?: string } export interface OptionListSetterProps extends BaseSetter { @@ -43,6 +44,7 @@ export interface DragItem { export interface ActionMenuProps { index: number + label?: string handleCloseMode: () => void } diff --git a/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/listItem.tsx b/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/listItem.tsx index 2f76bf54d4..b69c0c1214 100644 --- a/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/listItem.tsx +++ b/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/listItem.tsx @@ -11,7 +11,7 @@ export const ListItem: FC = (props) => {
- +
) diff --git a/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/more.tsx b/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/more.tsx index dfdf23bfc4..c8223a1c66 100644 --- a/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/more.tsx +++ b/apps/builder/src/page/App/components/PanelSetters/OptionListSetter/more.tsx @@ -4,7 +4,7 @@ import { ActionMenu } from "@/page/App/components/PanelSetters/OptionListSetter/ import { MoreProps } from "@/page/App/components/PanelSetters/OptionListSetter/interface" export const More: FC = (props) => { - const { index } = props + const { index, label } = props const [actionMenuVisible, setActionMenuVisible] = useState(false) const handleCloseActionMenu = useCallback(() => { @@ -14,7 +14,11 @@ export const More: FC = (props) => { + } trigger="click" position="bottom-end" diff --git a/apps/builder/src/page/App/components/ScaleSquare/constant/widget.ts b/apps/builder/src/page/App/components/ScaleSquare/constant/widget.ts index 4181860475..5a750a4763 100644 --- a/apps/builder/src/page/App/components/ScaleSquare/constant/widget.ts +++ b/apps/builder/src/page/App/components/ScaleSquare/constant/widget.ts @@ -4,3 +4,5 @@ export const CONTAINER_PADDING = 4 export const DEFAULT_MIN_COLUMN = 1 export const LIKE_CONTAINER_WIDGET_PADDING = 4 export const LIST_ITEM_MARGIN_TOP = 8 +export const LABEL_TOP_UNIT_HEIGHT = 32 +export const VALIDATE_MESSAGE_HEIGHT = 33 diff --git a/apps/builder/src/types/richText.d.ts b/apps/builder/src/types/richText.d.ts new file mode 100644 index 0000000000..d00b6471d2 --- /dev/null +++ b/apps/builder/src/types/richText.d.ts @@ -0,0 +1,28 @@ +declare module "@editorjs/*" { + export default class editorPlugin { + constructor(config) + render() + normalizeData(data) + setLevel(level): void + merge(data): void + validate(blockData): boolean + save(toolsContent) + static get conversionConfig(): { export: string; import: string } + static get sanitize() + static get isReadOnlySupported(): boolean + get data() + set data(data) + getTag(): HTMLElement + get currentLevel() + get defaultLevel() + get levels() + static get toolbox(): { + icon: string + title: string + } + } +} + +declare module "editorjs-md-parser" { + export const MDfromBlocks: (blocks: any) => Promise +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/baseChat.tsx b/apps/builder/src/widgetLibrary/ChatWidget/baseChat.tsx new file mode 100644 index 0000000000..b0c7348c46 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/baseChat.tsx @@ -0,0 +1,40 @@ +import { forwardRef } from "react" +import { Virtuoso, VirtuosoHandle } from "react-virtuoso" +import { BaseChatProps } from "@/widgetLibrary/ChatWidget/interface" +import { MessageItem } from "@/widgetLibrary/ChatWidget/messageItem" +import { Receiving } from "@/widgetLibrary/ChatWidget/receiving" + +export const BaseChat = forwardRef( + (props, ref) => { + const { value = [], leftMessageColor, showAvatar } = props + return ( +
+ ( + <> + {data.loading ? ( + + ) : ( + + )} + + )} + /> +
+ ) + }, +) +BaseChat.displayName = "BaseChat" diff --git a/apps/builder/src/widgetLibrary/ChatWidget/chat.tsx b/apps/builder/src/widgetLibrary/ChatWidget/chat.tsx new file mode 100644 index 0000000000..957232d9cc --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/chat.tsx @@ -0,0 +1,174 @@ +import { Resizable, ResizeCallback, ResizeStartCallback } from "re-resizable" +import { FC, useCallback, useEffect, useMemo } from "react" +import { useDispatch } from "react-redux" +import { executionActions } from "@/redux/currentApp/executionTree/executionSlice" +import { BaseChat } from "@/widgetLibrary/ChatWidget/baseChat" +import { + ChatWidgetProps, + MessageContent, +} from "@/widgetLibrary/ChatWidget/interface" +import { + chatContainerStyle, + footerStyle, + resizeLineStyle, +} from "@/widgetLibrary/ChatWidget/style" +import { useSizeChange } from "@/widgetLibrary/ChatWidget/useSizeChange" +import { + addOrDelLoading, + formatEventOptions, +} from "@/widgetLibrary/ChatWidget/utils" +import { RenderChildrenCanvas } from "@/widgetLibrary/PublicSector/RenderChildrenCanvas" +import { TooltipWrapper } from "@/widgetLibrary/PublicSector/TooltipWrapper" + +export const ChatWidget: FC = (props) => { + const { + columnNumber, + displayName, + value = [], + tooltipText, + receiving = false, + footerHeight = 50, + childrenNode, + mappedOption, + showFooter, + backgroundColor = "white", + triggerEventHandler, + handleUpdateOriginalDSLMultiAttr, + handleUpdateMultiExecutionResult, + } = props + const { messageListRef, containerRef, handleOnSizeChange } = useSizeChange( + value.length, + ) + const dispatch = useDispatch() + + const messageList = useMemo(() => { + return formatEventOptions(mappedOption) + }, [mappedOption]) + + const handleUpdateValue = useCallback( + (messageList: MessageContent[]) => { + if (!messageList || !Array.isArray(messageList)) { + return + } + handleUpdateMultiExecutionResult([ + { + displayName, + value: { + value: messageList, + }, + }, + ]) + }, + [displayName, handleUpdateMultiExecutionResult], + ) + + const handleResizeStart: ResizeStartCallback = (e) => { + e.preventDefault() + e.stopPropagation() + dispatch(executionActions.setResizingNodeIDsReducer([displayName])) + } + + const handleOnResizeStop: ResizeCallback = useCallback( + (e, dir, elementRef, delta) => { + const { height } = delta + handleUpdateOriginalDSLMultiAttr({ + footerHeight: footerHeight + height, + }) + dispatch(executionActions.setResizingNodeIDsReducer([])) + }, + [dispatch, footerHeight, handleUpdateOriginalDSLMultiAttr], + ) + + const handleOnResize: ResizeCallback = useCallback(() => { + handleOnSizeChange() + }, [handleOnSizeChange]) + + + const handleOnSelect = useCallback( + (message: MessageContent) => { + return new Promise((resolve) => { + handleUpdateMultiExecutionResult([ + { + displayName, + value: { + selectedMessage: message, + }, + }, + ]) + resolve(true) + }).then(() => { + triggerEventHandler("select") + }) + }, + [displayName, handleUpdateMultiExecutionResult, triggerEventHandler], + ) + const handleOnReplay = useCallback( + (message: MessageContent) => { + handleOnSelect(message).then(() => { + triggerEventHandler("replay") + }) + }, + [handleOnSelect, triggerEventHandler], + ) + + const handleOnDelete = useCallback( + (message: MessageContent) => { + handleOnSelect(message).then(() => { + triggerEventHandler("delete") + }) + }, + [handleOnSelect, triggerEventHandler], + ) + + useEffect(() => { + handleUpdateValue(messageList) + }, [handleUpdateValue, messageList]) + + useEffect(() => { + addOrDelLoading(receiving, value, handleUpdateValue) + }, [handleUpdateValue, receiving, value]) + + return ( + +
+
+ +
+ {showFooter && ( + +
+
+ {}} + /> +
+ + )} +
+ + ) +} + +ChatWidget.displayName = "ChatWidget" +export default ChatWidget diff --git a/apps/builder/src/widgetLibrary/ChatWidget/eventHandlerConfig.ts b/apps/builder/src/widgetLibrary/ChatWidget/eventHandlerConfig.ts new file mode 100644 index 0000000000..809ed2f6cc --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/eventHandlerConfig.ts @@ -0,0 +1,26 @@ +import i18n from "@/i18n/config" +import { EventHandlerConfig } from "@/widgetLibrary/interface" + +export const CHAT_EVENT_HANDLER_CONFIG: EventHandlerConfig = { + events: [ + { + label: i18n.t( + "editor.inspect.setter_content.widget_action_type_name.select", + ), + value: "select", + }, + { + label: i18n.t( + "editor.inspect.setter_content.widget_action_type_name.replay", + ), + value: "replay", + }, + { + label: i18n.t( + "editor.inspect.setter_content.widget_action_type_name.delete", + ), + value: "delete", + }, + ], + methods: [], +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/index.ts b/apps/builder/src/widgetLibrary/ChatWidget/index.ts new file mode 100644 index 0000000000..acbcd201da --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/index.ts @@ -0,0 +1,3 @@ +export { CHAT_PANEL_CONFIG } from "./panelConfig" +export { CHAT_WIDGET_CONFIG } from "./widgetConfig" +export { CHAT_EVENT_HANDLER_CONFIG } from "./eventHandlerConfig" diff --git a/apps/builder/src/widgetLibrary/ChatWidget/interface.ts b/apps/builder/src/widgetLibrary/ChatWidget/interface.ts new file mode 100644 index 0000000000..bf7289e3ce --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/interface.ts @@ -0,0 +1,61 @@ +import { TooltipWrapperProps } from "@/widgetLibrary/PublicSector/TooltipWrapper/interface" +import { BaseWidgetProps } from "@/widgetLibrary/interface" + +export type MessageType = "text" | "image" | "video" | "audio" + +export type Pluralize = { + [K in keyof T as `${string & K}s`]: T[K][] +} + +export interface IMessageItem extends BaseChatProps { + message: MessageContent +} + +export interface MessageContent { + messageId?: string + message?: string + sendTime?: string + replyMessageId?: string + senderName?: string + senderId?: string + senderAvatar?: string + messageType?: MessageType + loading?: boolean +} + +export interface BaseChatProps extends BaseWidgetProps { + value?: MessageContent[] + currentSenderId?: string + leftMessageColor?: string + rightMessageColor?: string + receiving?: boolean + selectedMessage?: MessageContent + timeFormat?: string + toolbarReplay?: boolean + toolbarDelete?: boolean + showAvatar?: boolean + showName?: boolean + showSendTime?: boolean + showFooter?: boolean + backgroundColor?: string + handleOnReplay?: (message: MessageContent) => void + handleOnDelete?: (message: MessageContent) => void +} + +export interface ChatWidgetProps + extends BaseChatProps, + Pick { + footerHeight?: number + mappedOption?: Pluralize + columnNumber: number +} + +export interface MessageSpecProps { + content?: string + isReply?: boolean + isOwnMessage?: boolean + leftMessageColor?: string + rightMessageColor?: string +} + +export const SendMessageProps = {} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/messageItem.tsx b/apps/builder/src/widgetLibrary/ChatWidget/messageItem.tsx new file mode 100644 index 0000000000..001b030fc0 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/messageItem.tsx @@ -0,0 +1,101 @@ +import { Avatar, Message } from "@chatscope/chat-ui-kit-react" +import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css" +import dayjs from "dayjs" +import { FC, useRef } from "react" +import { Trigger } from "@illa-design/react" +import { IMessageItem } from "@/widgetLibrary/ChatWidget/interface" +import { Options } from "@/widgetLibrary/ChatWidget/options" +import { ReplayMessage } from "./messageItems/replayMessage" +import { SendMessage } from "./messageItems/sendMessage" +import { + messageContentStyle, + messageHeaderNameStyle, + messageHeaderStyle, + messageHeaderTimeStyle, +} from "./style" + +export const MessageItem: FC = (props) => { + const { + message, + currentSenderId = "", + leftMessageColor = "grayBlue", + rightMessageColor = "blue", + value = [], + timeFormat, + toolbarReplay, + toolbarDelete, + showAvatar, + showName, + showSendTime, + } = props + const { + message: content = "", + senderAvatar, + senderId, + senderName, + messageId, + sendTime, + messageType = "text", + replyMessageId, + } = message + + const isOwnMessage = !!currentSenderId && senderId === currentSenderId + const messageItemRef = useRef(null) + + return ( + <> + + {showAvatar && } + +
+ {showName && {senderName}} + {showSendTime && ( + + {dayjs(sendTime).format(timeFormat)} + + )} +
+
+ + } + colorScheme="transparent" + disabled={!toolbarDelete && !toolbarReplay} + position={isOwnMessage ? "left-start" : "right-start"} + showArrow={false} + autoFitPosition={false} + withoutPadding + trigger="hover" + withoutShadow + > + + + + {value.length && replyMessageId && ( + + + + )} +
+ + ) +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/messageItems/audioMessage.tsx b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/audioMessage.tsx new file mode 100644 index 0000000000..1fdce4a6fa --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/audioMessage.tsx @@ -0,0 +1,49 @@ +import { FC, useState } from "react" +import { useTranslation } from "react-i18next" +import ReactPlayer from "react-player" +import { Loading } from "@illa-design/react" +import { MessageSpecProps } from "../interface" +import { audioStyle, audioWrapperStyle, loadingStyle } from "../style" + +export const AudioMessage: FC = (props) => { + const { content, isReply } = props + const { t } = useTranslation() + const [loading, setLoading] = useState(true) + const [_error, setError] = useState(false) + + if (content === "") { + return
{t("widget.audio.no_audio")}
+ } + + return ( +
+ {loading ? ( +
+ +
+ ) : null} + { + setLoading(false) + setError(false) + }} + onError={() => { + setLoading(false) + setError(true) + }} + /> +
+ ) +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/messageItems/imageMessage.tsx b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/imageMessage.tsx new file mode 100644 index 0000000000..578cfeef2c --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/imageMessage.tsx @@ -0,0 +1,17 @@ +import { FC } from "react" +import { MessageSpecProps } from "../interface" +import { replayImageStyle } from "../style" + +export const ImageMessage: FC = ({ content, isReply }) => { + return ( + <> + {isReply ? ( +
+ +
+ ) : ( + + )} + + ) +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/messageItems/replayMessage.tsx b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/replayMessage.tsx new file mode 100644 index 0000000000..2e509354fd --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/replayMessage.tsx @@ -0,0 +1,27 @@ +import { FC } from "react" +import { MessageContent } from "../interface" +import { replayMessageStyle, replayNameStyle } from "../style" +import { AudioMessage } from "./audioMessage" +import { ImageMessage } from "./imageMessage" +import { TextMessage } from "./textMessage" +import { VideoMessage } from "./videoMessage" + +export const ReplayMessage: FC<{ + messageId: string + value: MessageContent[] +}> = ({ messageId, value }) => { + const message = value.find((item) => item.messageId === messageId) + if (!message) return null + const { messageType, message: content, senderName } = message + return ( +
+
+ {senderName}: + {messageType === "text" && } + {messageType === "image" && } + {messageType === "audio" && } + {messageType === "video" && } +
+
+ ) +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/messageItems/sendMessage.tsx b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/sendMessage.tsx new file mode 100644 index 0000000000..6e8835e642 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/sendMessage.tsx @@ -0,0 +1,36 @@ +import { forwardRef } from "react" +import { MessageSpecProps, MessageType } from "../interface" +import { messageItemContainerStyle } from "../style" +import { AudioMessage } from "./audioMessage" +import { ImageMessage } from "./imageMessage" +import { TextMessage } from "./textMessage" +import { VideoMessage } from "./videoMessage" + +export const SendMessage = forwardRef< + HTMLDivElement, + MessageSpecProps & { messageType: MessageType } +>((props, ref) => { + const { + messageType, + content, + isOwnMessage, + leftMessageColor, + rightMessageColor, + } = props + return ( +
+ {messageType === "text" && ( + + )} + {messageType === "image" && } + {messageType === "audio" && } + {messageType === "video" && } +
+ ) +}) +SendMessage.displayName = "SendMessage" diff --git a/apps/builder/src/widgetLibrary/ChatWidget/messageItems/textMessage.tsx b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/textMessage.tsx new file mode 100644 index 0000000000..165876a403 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/textMessage.tsx @@ -0,0 +1,39 @@ +import { FC } from "react" +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" +import { Paragraph } from "@illa-design/react" +import { MessageSpecProps } from "../interface" +import { messageTextStyle, replayTextStyle } from "../style" + +export const TextMessage: FC = ({ + content, + isReply, + isOwnMessage = false, + leftMessageColor = "grayBlue", + rightMessageColor = "blue", +}) => { + return ( + <> +
+ {children}, + }} + > + {content ?? ""} + +
+ + ) +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/messageItems/videoMessage.tsx b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/videoMessage.tsx new file mode 100644 index 0000000000..5e4ba1b1b8 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/messageItems/videoMessage.tsx @@ -0,0 +1,49 @@ +import { FC, useState } from "react" +import { useTranslation } from "react-i18next" +import ReactPlayer from "react-player" +import { Loading } from "@illa-design/react" +import { MessageSpecProps } from "../interface" +import { loadingStyle, videoStyle } from "../style" + +export const VideoMessage: FC = (props) => { + const { content, isReply } = props + const { t } = useTranslation() + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) + + if (content === "") { + return
{t("widget.video.empty")}
+ } + + return ( +
+ {loading ? ( +
+ +
+ ) : error ? ( +
{t("widget.video.fail")}
+ ) : null} + + +
+ } + width="100%" + height="100%" + url={content} + draggable={false} + onReady={() => { + setLoading(false) + setError(false) + }} + onError={() => { + setLoading(false) + setError(true) + }} + /> +
+ ) +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/options.tsx b/apps/builder/src/widgetLibrary/ChatWidget/options.tsx new file mode 100644 index 0000000000..e7bfddfd77 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/options.tsx @@ -0,0 +1,36 @@ +import { FC } from "react" +import { ReactComponent as DeleteSvg } from "@/assets/chat/delete.svg" +import { ReactComponent as ReplaySvg } from "@/assets/chat/replay.svg" +import { IMessageItem } from "./interface" +import { optionsStyle } from "./style" + +export const Options: FC = ({ + message, + toolbarDelete, + handleOnReplay, + toolbarReplay, + handleOnDelete, +}) => { + return ( +
+ {toolbarReplay && ( +
{ + handleOnReplay && handleOnReplay(message) + }} + > + +
+ )} + {toolbarDelete && ( +
{ + handleOnDelete && handleOnDelete(message) + }} + > + +
+ )} +
+ ) +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/panelConfig.tsx b/apps/builder/src/widgetLibrary/ChatWidget/panelConfig.tsx new file mode 100644 index 0000000000..f5f83bc6a4 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/panelConfig.tsx @@ -0,0 +1,335 @@ +import { ReactComponent as RadioIcon } from "@/assets/radius-icon.svg" +import { ReactComponent as StrokeWidthIcon } from "@/assets/stroke-width-icon.svg" +import i18n from "@/i18n/config" +import { PanelConfig } from "@/page/App/components/InspectPanel/interface" +import { VALIDATION_TYPES } from "@/utils/validationFactory" +import { CHAT_EVENT_HANDLER_CONFIG } from "@/widgetLibrary/ChatWidget/eventHandlerConfig" +import { generatorEventHandlerConfig } from "@/widgetLibrary/PublicSector/utils/generatorEventHandlerConfig" + +const baseWidgetName = "chat" +export const CHAT_PANEL_CONFIG: PanelConfig[] = [ + { + id: `${baseWidgetName}-data`, + groupName: i18n.t("editor.inspect.setter_group.MESSAGE"), //TODO, + children: [ + { + id: `${baseWidgetName}-data-source`, + labelName: i18n.t("editor.inspect.setter_label.data_source"), + attrName: "dataSources", + setterType: "INPUT_SETTER", + expectedType: VALIDATION_TYPES.ARRAY, + }, + { + id: `${baseWidgetName}-option-mapped-event`, + labelName: i18n.t("editor.inspect.setter_label.mapped_option"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.map_data_option"), + useCustomLayout: true, + attrName: "mappedOption", + setterType: "OPTION_MAPPED_SETTER", + childrenSetter: [ + { + id: `${baseWidgetName}-message-message_id`, + labelName: i18n.t("editor.inspect.setter_label.message_id"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.message_id"), + attrName: "messageIds", + placeholder: "{{item.messageId}}", + isSetterSingleRow: true, + setterType: "OPTION_MAPPED_INPUT_SETTER", + expectedType: VALIDATION_TYPES.ARRAY, + }, + { + id: `${baseWidgetName}-message-message`, + labelName: i18n.t("editor.inspect.setter_label.message"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.message"), + attrName: "messages", + placeholder: "{{item.messages}}", + isSetterSingleRow: true, + setterType: "OPTION_MAPPED_INPUT_SETTER", + expectedType: VALIDATION_TYPES.ARRAY, + }, + { + id: `${baseWidgetName}-message-message_type`, + labelName: i18n.t("editor.inspect.setter_label.message_type"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.message_type"), + attrName: "messageTypes", + placeholder: "{{item.messageTypes}}", + isSetterSingleRow: true, + setterType: "OPTION_MAPPED_INPUT_SETTER", + expectedType: VALIDATION_TYPES.ARRAY, + }, + { + id: `${baseWidgetName}-message-replayMessageId`, + labelName: i18n.t("editor.inspect.setter_label.replayMessageId"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.replayMessageId"), + attrName: "replyMessageIds", + placeholder: "{{item.replayMessageIds}}", + isSetterSingleRow: true, + setterType: "OPTION_MAPPED_INPUT_SETTER", + expectedType: VALIDATION_TYPES.ARRAY, + }, + { + id: `${baseWidgetName}-message-sendTime`, + labelName: i18n.t("editor.inspect.setter_label.sendTime"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.sendTime"), + attrName: "sendTimes", + placeholder: "{{item.sendTimes}}", + isSetterSingleRow: true, + setterType: "OPTION_MAPPED_INPUT_SETTER", + expectedType: VALIDATION_TYPES.ARRAY, + }, + { + id: `${baseWidgetName}-message-senderName`, + labelName: i18n.t("editor.inspect.setter_label.senderName"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.senderName"), + attrName: "senderNames", + placeholder: "{{item.senderNames}}", + isSetterSingleRow: true, + setterType: "OPTION_MAPPED_INPUT_SETTER", + expectedType: VALIDATION_TYPES.ARRAY, + }, + { + id: `${baseWidgetName}-message-senderId`, + labelName: i18n.t("editor.inspect.setter_label.senderId"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.senderId"), + attrName: "senderIds", + placeholder: "{{item.senderIds}}", + isSetterSingleRow: true, + setterType: "OPTION_MAPPED_INPUT_SETTER", + expectedType: VALIDATION_TYPES.ARRAY, + }, + { + id: `${baseWidgetName}-message-senderAvatar`, + labelName: i18n.t("editor.inspect.setter_label.senderAvatar"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.senderAvatar"), + attrName: "senderAvatars", + placeholder: "{{item.senderAvatars}}", + isSetterSingleRow: true, + setterType: "OPTION_MAPPED_INPUT_SETTER", + expectedType: VALIDATION_TYPES.ARRAY, + }, + ], + }, + { + id: `${baseWidgetName}-message-timeFormat`, + labelName: i18n.t("editor.inspect.setter_label.timeFormat"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.sendtimeFormatTime"), + isSetterSingleRow: true, + attrName: "timeFormat", + setterType: "INPUT_SETTER", + expectedType: VALIDATION_TYPES.STRING, + }, + { + id: `${baseWidgetName}-message-currentSenderId`, + labelName: i18n.t("editor.inspect.setter_label.currentSenderId"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.currentSenderId"), + isSetterSingleRow: true, + attrName: "currentSenderId", + setterType: "INPUT_SETTER", + expectedType: VALIDATION_TYPES.STRING, + }, + ], + }, + { + id: `${baseWidgetName}-interaction`, + groupName: i18n.t("editor.inspect.setter_group.interaction"), + children: [ + { + ...generatorEventHandlerConfig( + baseWidgetName, + CHAT_EVENT_HANDLER_CONFIG.events, + ), + }, + { + id: `${baseWidgetName}-message-receiving`, + labelName: i18n.t("editor.inspect.setter_label.receiving"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.receiving"), + attrName: "receiving", + isSetterSingleRow: true, + setterType: "INPUT_SETTER", + expectedType: VALIDATION_TYPES.BOOLEAN, + }, + ], + }, + { + id: `${baseWidgetName}-toolbar`, + groupName: i18n.t("editor.inspect.setter_group.Toolbar"), + children: [ + { + id: `${baseWidgetName}-toolbar-replay`, + labelName: i18n.t("editor.inspect.setter_label.toolbar-replay"), + labelDesc: i18n.t("editor.inspect.setter_tips.toolbar-replay"), + attrName: "toolbarReplay", + setterType: "DYNAMIC_SWITCH_SETTER", + placeholder: "false", + useCustomLayout: true, + openDynamic: true, + expectedType: VALIDATION_TYPES.BOOLEAN, + }, + { + id: `${baseWidgetName}-toolbar-delete`, + labelName: i18n.t("editor.inspect.setter_label.toolbar-delete"), + labelDesc: i18n.t("editor.inspect.setter_tips.toolbar-delete"), + attrName: "toolbarDelete", + setterType: "DYNAMIC_SWITCH_SETTER", + placeholder: "false", + useCustomLayout: true, + openDynamic: true, + expectedType: VALIDATION_TYPES.BOOLEAN, + }, + ], + }, + { + id: `${baseWidgetName}-layout`, + groupName: i18n.t("editor.inspect.setter_group.layout"), + children: [ + { + id: `${baseWidgetName}-layout-showAvatar`, + labelName: i18n.t("editor.inspect.setter_label.showAvatar"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.showAvatar"), + setterType: "DYNAMIC_SWITCH_SETTER", + attrName: "showAvatar", + placeholder: "false", + useCustomLayout: true, + openDynamic: true, + expectedType: VALIDATION_TYPES.BOOLEAN, + }, + { + id: `${baseWidgetName}-layout-showName`, + labelName: i18n.t("editor.inspect.setter_label.showName"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.showName"), + setterType: "DYNAMIC_SWITCH_SETTER", + attrName: "showName", + placeholder: "false", + useCustomLayout: true, + openDynamic: true, + expectedType: VALIDATION_TYPES.BOOLEAN, + }, + { + id: `${baseWidgetName}-layout-showSendTime`, + labelName: i18n.t("editor.inspect.setter_label.showSendTime"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.showSendTime"), + setterType: "DYNAMIC_SWITCH_SETTER", + attrName: "showSendTime", + placeholder: "false", + useCustomLayout: true, + openDynamic: true, + expectedType: VALIDATION_TYPES.BOOLEAN, + }, + { + id: `${baseWidgetName}-layout-showFooter`, + labelName: i18n.t("editor.inspect.setter_label.showFooter"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.showFooter"), + setterType: "DYNAMIC_SWITCH_SETTER", + attrName: "showFooter", + placeholder: "false", + useCustomLayout: true, + openDynamic: true, + expectedType: VALIDATION_TYPES.BOOLEAN, + }, + ], + }, + { + id: `${baseWidgetName}-style`, + groupName: i18n.t("editor.inspect.setter_group.style"), + children: [ + { + id: `${baseWidgetName}-colors`, + setterType: "LIST_SETTER", + labelName: i18n.t("editor.inspect.setter_label.colors"), + attrName: "styles", + useCustomLayout: true, + childrenSetter: [ + { + id: `${baseWidgetName}-colors-color`, + labelName: i18n.t("editor.inspect.setter_label.theme_color"), + attrName: "backgroundColor", + setterType: "COLOR_PICKER_SETTER", + defaultValue: "blue", + }, + { + id: `${baseWidgetName}-colors-message-left-color`, + labelName: i18n.t("editor.inspect.setter_label.leftMessageColor"), + labelDesc: i18n.t("editor.inspect.setter_tips.leftMessageColor"), + attrName: "leftMessageColor", + setterType: "COLOR_PICKER_SETTER", + defaultValue: "grayBlue", + }, + { + id: `${baseWidgetName}-colors-right-message-color`, + labelName: i18n.t("editor.inspect.setter_label.rightMessageColor"), + labelDesc: i18n.t("editor.inspect.setter_tips.rightMessageColor"), + attrName: "rightMessageColor", + setterType: "COLOR_PICKER_SETTER", + defaultValue: "blue", + }, + { + id: `${baseWidgetName}-styles-color`, + setterType: "LIST_SETTER", + labelName: i18n.t("editor.inspect.setter_label.border"), + attrName: "border", + useCustomLayout: true, + childrenSetter: [ + { + id: `${baseWidgetName}-style-border`, + labelName: i18n.t("editor.inspect.setter_label.color"), + attrName: "borderColor", + setterType: "COLOR_PICKER_SETTER", + defaultValue: "#ffffffff", + }, + { + id: `${baseWidgetName}-style-radius`, + labelName: i18n.t("editor.inspect.setter_label.radius"), + attrName: "radius", + setterType: "EDITABLE_INPUT_WITH_MEASURE_SETTER", + icon: , + defaultValue: "4px", + }, + { + id: `${baseWidgetName}-style-border-width`, + labelName: i18n.t("editor.inspect.setter_label.width"), + attrName: "borderWidth", + icon: , + setterType: "EDITABLE_INPUT_WITH_MEASURE_SETTER", + defaultValue: "1px", + }, + ], + }, + { + id: `${baseWidgetName}-styles-style`, + setterType: "LIST_SETTER", + labelName: i18n.t("editor.inspect.setter_label.style"), + attrName: "style", + useCustomLayout: true, + childrenSetter: [ + { + id: `${baseWidgetName}-style-shadow`, + labelName: i18n.t("editor.inspect.setter_label.shadow.shadow"), + attrName: "shadow", + setterType: "SHADOW_SELECT_SETTER", + defaultValue: "small", + options: [ + { + label: i18n.t("editor.inspect.setter_option.shadow.none"), + value: "none", + }, + { + label: i18n.t("editor.inspect.setter_option.shadow.large"), + value: "large", + }, + { + label: i18n.t("editor.inspect.setter_option.shadow.medium"), + value: "medium", + }, + { + label: i18n.t("editor.inspect.setter_option.shadow.small"), + value: "small", + }, + ], + }, + ], + }, + ], + }, + ], + }, +] diff --git a/apps/builder/src/widgetLibrary/ChatWidget/receiving.tsx b/apps/builder/src/widgetLibrary/ChatWidget/receiving.tsx new file mode 100644 index 0000000000..b6375bc165 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/receiving.tsx @@ -0,0 +1,19 @@ +import { FC } from "react" +import { + receivingAvatarStyle, + receivingContainerStyle, + receivingStyle, +} from "@/widgetLibrary/ChatWidget/style" + +export const Receiving: FC<{ + leftMessageColor?: string + showAvatar?: boolean +}> = (props) => { + const { leftMessageColor = "grayBlue", showAvatar } = props + return ( +
+ {showAvatar &&
} +
+
+ ) +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/style.ts b/apps/builder/src/widgetLibrary/ChatWidget/style.ts new file mode 100644 index 0000000000..488720c63c --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/style.ts @@ -0,0 +1,275 @@ +import { css } from "@emotion/react" +import { getColor } from "@illa-design/react" + +export const resizeLineStyle = css` + width: 100%; + height: 1px; + background-color: #e5e5e5; + cursor: row-resize; + position: absolute; +` +export const footerStyle = css` + width: 100%; + height: 100%; + // overflow: hidden; + display: flex; + flex-direction: row; +` +export const chatContainerStyle = (backgroundColor: string) => css` + height: 100%; + display: flex; + overflow: hidden; + flex-direction: column; + background-color: ${getColor(backgroundColor, "01")}; +` + +export const messageHeaderStyle = (isOwn: boolean) => { + return css` + width: 100%; + display: flex; + flex-direction: ${isOwn ? "row-reverse" : "row"}; + gap: 8px; + justify-content: flex-start; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 4px; + ` +} +export const messageHeaderNameStyle = css` + font-weight: 500; + font-size: 14px; + line-height: 22px; + color: ${getColor("grayBlue", "02")}; +` + +export const messageHeaderTimeStyle = css` + font-weight: 400; + font-size: 12px; + line-height: 20px; + color: ${getColor("grayBlue", "03")}; +` +export const messageContentStyle = (isOwn: boolean) => css` + .cs-message__content-wrapper { + margin-top: 12px; + } + .cs-message__footer { + margin: 0; + } + .cs-message__content { + height: auto; + width: auto; + border-radius: 0; + padding: 0; + background-color: transparent !important; + } + .cs-message__avatar { + margin: ${isOwn ? "12px 40px 0 16px" : "12px 16px 0 40px"} !important; + width: 32px; + .cs-avatar { + height: 32px; + width: 32px; + min-height: 32px; + min-width: 32px; + } + } + .cs-message__custom-content { + padding-bottom: 8px; + } +` +export const messageItemContainerStyle = css` + display: flex; + flex-shrink: 1; +` +export const messageTextStyle = ( + isOwn: boolean, + leftMessageColor: string, + rightMessageColor: string, +) => { + const backgroundColor = isOwn + ? getColor(rightMessageColor, "03") + : getColor(leftMessageColor, "09") + const color = isOwn ? getColor("white", "01") : getColor("grayBlue", "02") + + return css` + background-color: ${backgroundColor + ? backgroundColor + : getColor(leftMessageColor, "07")}; + color: ${color}!important; + padding: 8px 12px; + border-radius: 8px; + max-width: 344px; + word-wrap: break-word; + span, + a { + color: ${color}; + } + ` +} + +export const receivingStyle = (leftMessageColor: string) => { + const backgroundColor = getColor(leftMessageColor, "09") + return css` + padding: 8px 12px; + border-radius: 8px; + width: 40px; + height: 33px; + background-color: ${backgroundColor + ? backgroundColor + : getColor(leftMessageColor, "07")}; + color: ${getColor("grayBlue", "02")}!important; + &:before { + content: ""; + animation: dotAnimate 1s infinite; + } + @keyframes dotAnimate { + 0%, + 100% { + content: ""; + } + 25% { + content: "."; + } + 50% { + content: ".."; + } + 75% { + content: "..."; + } + } + ` +} + +export const contentContainerStyle = css` + display: flex; + flex-direction: row; + gap: 10px; + align-items: end; +` +export const sendingStyle = css` + height: 16px; + width: 16px; + animation: sendAnimate 1s ease-in-out infinite; + @keyframes sendAnimate { + 0% { + transform: rotate(0deg); + } + 25% { + transform: rotate(90deg); + } + 50% { + transform: rotate(180deg); + } + 75% { + transform: rotate(270deg); + } + 100% { + transform: rotate(360deg); + } + } +` + +export const replayMessageStyle = css` + & > div { + max-width: 344px; + display: flex; + flex-direction: column; + flex-shrink: 1; + padding: 8px 12px; + gap: 4px; + border: 1px solid ${getColor("grayBlue", "08")}; + border-radius: 8px; + } +` +export const replayImageStyle = css` + width: 200px; + height: 100px; + background-color: ${getColor("grayBlue", "09")}; + img { + width: 100%; + height: 100%; + object-fit: scale-down; + } +` + +export const replayNameStyle = css` + font-weight: 400; + font-size: 14px; + line-height: 22px; + color: ${getColor("grayBlue", "02")}; +` +export const replayTextStyle = css` + max-height: 200px; + overflow-y: auto; + word-wrap: break-word; +` + +export const receivingContainerStyle = css` + display: flex; + flex-direction: row; + gap: 16px; + padding: 8px 0; +` + +export const receivingAvatarStyle = css` + height: 32px; + width: 32px; + background-color: ${getColor("grayBlue", "09")}; + border-radius: 50%; +` + +export const optionsStyle = (isAllShow: boolean) => { + return css` + background-color: ${getColor("white", "01")}; + font-weight: 400; + font-size: 14px; + line-height: 22px; + display: flex; + border-radius: 4px; + flex-direction: row; + border: 1px solid ${getColor("grayBlue", "08")}; + & div { + padding: 6px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + } + & div:last-of-type { + border-left: ${isAllShow + ? `1px solid ${getColor("grayBlue", "08")}` + : "none"}; + } + & div:hover { + background-color: ${getColor("grayBlue", "08")}; + } + ` +} + +export const loadingStyle = css` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: ${getColor("gray", "01")}; // ?? + color: ${getColor("black", "01")}; +` +export const audioWrapperStyle = css` + & > audio { + &::-webkit-media-controls { + justify-content: center; + } + } +` + +export const audioStyle = (isReplay?: boolean) => css` + height: 50px; + width: ${isReplay ? 320 : 300}px; +` + +export const videoStyle = (isReplay?: boolean) => css` + height: 150px; + width: ${isReplay ? 320 : 300}px; +` diff --git a/apps/builder/src/widgetLibrary/ChatWidget/useSizeChange.ts b/apps/builder/src/widgetLibrary/ChatWidget/useSizeChange.ts new file mode 100644 index 0000000000..755088c1d9 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/useSizeChange.ts @@ -0,0 +1,61 @@ +import { useCallback, useEffect, useRef } from "react" +import { VirtuosoHandle } from "react-virtuoso" + +export const useSizeChange = (length: number) => { + const containerRef = useRef(null) + const messageListRef = useRef(null) + const containerHeight = useRef(0) + + const setContainerHight = useCallback((height: number) => { + containerHeight.current = height + }, []) + + const handleOnSizeChange = useCallback(() => { + Promise.resolve().then(() => { + messageListRef.current?.scrollToIndex({ + index: length ? length - 1 : 0, + }) + }) + }, [length]) + useEffect(() => { + if (containerRef.current) { + containerHeight.current = containerRef.current.clientHeight + } + }, [containerRef]) + + useEffect(() => { + let observe: HTMLDivElement | null + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + if (entry.target === containerRef.current) { + const { height } = entry.contentRect + if (height !== containerHeight.current) { + handleOnSizeChange() + setContainerHight(height) + } + } + } + }) + + if (containerRef.current) { + resizeObserver.observe(containerRef.current) + observe = containerRef.current + } + + return () => { + if (observe) { + resizeObserver.unobserve(observe) + observe = null + } + } + }) + + useEffect(() => { + handleOnSizeChange() + }, [handleOnSizeChange, length]) + return { + messageListRef, + containerRef, + handleOnSizeChange, + } +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/utils.ts b/apps/builder/src/widgetLibrary/ChatWidget/utils.ts new file mode 100644 index 0000000000..c2ec7531a9 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/utils.ts @@ -0,0 +1,75 @@ +import DefaultAvatar from "@/assets/chat/empty.svg" +import { MessageContent, Pluralize } from "@/widgetLibrary/ChatWidget/interface" + +export const formatEventOptions = ( + mappedOption: Pluralize = { + messageIds: [], + messages: [], + messageTypes: [], + senderAvatars: [], + replyMessageIds: [], + senderIds: [], + senderNames: [], + sendTimes: [], + }, +) => { + const messageId = mappedOption.messageIds ?? [] + const message = mappedOption.messages ?? [] + const messageType = mappedOption.messageTypes ?? [] + const senderAvatar = mappedOption.senderAvatars ?? [] + const replyMessageId = mappedOption.replyMessageIds ?? [] + const senderId = mappedOption.senderIds ?? [] + const senderName = mappedOption.senderNames ?? [] + const sendTime = mappedOption.sendTimes ?? [] + const maxLength = Math.max( + messageId.length, + message.length, + messageType.length, + senderAvatar.length, + replyMessageId.length, + senderId.length, + senderName.length, + sendTime.length, + ) + const messageList: MessageContent[] = [] + for (let i = 0; i < maxLength; i++) { + const messageIdItem = messageId[i] || `${i + 1}` + const messageItem = message[i] || "" + const messageTypeItem = messageType[i] || "text" + const senderAvatarItem = senderAvatar[i] || DefaultAvatar + const replyMessageIdtem = replyMessageId[i] || "" + const senderIdItem = senderId[i] || "" + const senderNameItem = senderName[i] ?? "" + const sendTimeItem = sendTime[i] ?? "" + messageList.push({ + messageId: messageIdItem, + message: safeNodeValue(messageItem), + messageType: messageTypeItem, + senderAvatar: senderAvatarItem, + replyMessageId: replyMessageIdtem, + senderId: senderIdItem, + senderName: safeNodeValue(senderNameItem), + sendTime: safeNodeValue(sendTimeItem), + }) + } + return messageList +} +const safeNodeValue = (value: unknown) => { + return typeof value === "string" ? value : "" +} + +export const addOrDelLoading = ( + receiving: boolean, + messageList: MessageContent[], + updateLoading: (messageList: MessageContent[]) => void, +) => { + if (!messageList.length) return + const loadingIndex = messageList.findIndex((item) => item.loading === true) + if (receiving && loadingIndex === -1) { + updateLoading([...messageList, { loading: true }]) + } else if (!receiving && loadingIndex !== -1) { + const messageListCopy = [...messageList] + messageListCopy.splice(loadingIndex, 1) + updateLoading(messageListCopy) + } +} diff --git a/apps/builder/src/widgetLibrary/ChatWidget/widgetConfig.tsx b/apps/builder/src/widgetLibrary/ChatWidget/widgetConfig.tsx new file mode 100644 index 0000000000..efd3665262 --- /dev/null +++ b/apps/builder/src/widgetLibrary/ChatWidget/widgetConfig.tsx @@ -0,0 +1,101 @@ +import { ReactComponent as ChatWidgetIcon } from "@/assets/widgetCover/chat.svg" +import i18n from "@/i18n/config" +import { BasicContainerConfig } from "@/widgetLibrary/BasicContainer/BasicContainer" +import { BUTTON_WIDGET_CONFIG } from "@/widgetLibrary/ButtonWidget" +import { INPUT_WIDGET_CONFIG } from "@/widgetLibrary/InputWidget" +import { RESIZE_DIRECTION, WidgetConfig } from "@/widgetLibrary/interface" + +export const CHAT_WIDGET_CONFIG: WidgetConfig = { + version: 0, + type: "CHAT_WIDGET", + displayName: "chat", + widgetName: i18n.t("widget.chat.name"), + icon: , + keywords: ["chat", "聊天"], + sessionType: "PRESENTATION", + resizeDirection: RESIZE_DIRECTION.ALL, + w: 18, + h: 50, + childrenNode: [ + { + ...BasicContainerConfig, + childrenNode: [ + { + ...INPUT_WIDGET_CONFIG, + w: 28, + h: 5, + x: 0, + y: 0, + defaults: { + ...INPUT_WIDGET_CONFIG.defaults, + labelHidden: true, + showVisibleButton: false, + value: "", + }, + }, + { + ...BUTTON_WIDGET_CONFIG, + w: 3, + h: 5, + x: 29, + y: 0, + defaults: { + ...BUTTON_WIDGET_CONFIG.defaults, + text: "Send", + }, + }, + ], + }, + ], + defaults: { + dataSources: `{{[ + { + messageId: "001", + message: "What is ILLA Cloud?", + sendTime: "2022-12-31 23:59:59", + replyMessageId: "", + senderName: "James", + senderId: "1", + senderAvatar: + "https://images.pexels.com/photos/6045220/pexels-photo-6045220.jpeg?auto=compress&cs=tinysrgb&w=800", + messageType: "text", + }, + { + messageId: "002", + message: "ILLA Cloud is a low-code platform for developers to build internal tools in minutes.", + replyMessageId: "001", + sendTime: "2023-01-01 00:00:00", + senderName: "Tom", + senderId: "2", + senderAvatar: + "https://images.pexels.com/photos/1888403/pexels-photo-1888403.jpeg?auto=compress&cs=tinysrgb&w=800", + messageType: "text", + }, + ]}}`, + backgroundColor: "white", + leftMessageColor: "grayBlue", + rightMessageColor: "blue", + enableMessageSelection: "{{true}}", + toolbarReplay: "{{true}}", + toolbarDelete: "{{true}}", + showAvatar: "{{true}}", + showName: "{{true}}", + showSendTime: "{{true}}", + showFooter: "{{true}}", + timeFormat: "YYYY-MM-DD hh:mm:ss", + selectionType: "rightClick", + selectedMessage: { + messageId: "", + message: "", + sendTime: "", + replyMessageId: "", + senderName: "", + senderId: "", + senderAvatar: "", + messageType: "", + }, + radius: "4px", + borderWidth: "1px", + shadow: "small", + }, +} diff --git a/apps/builder/src/widgetLibrary/CheckboxGroupWidget/checkboxGroup.tsx b/apps/builder/src/widgetLibrary/CheckboxGroupWidget/checkboxGroup.tsx index cd9d7a114b..e4fedcc456 100644 --- a/apps/builder/src/widgetLibrary/CheckboxGroupWidget/checkboxGroup.tsx +++ b/apps/builder/src/widgetLibrary/CheckboxGroupWidget/checkboxGroup.tsx @@ -169,15 +169,17 @@ export const CheckboxWidget: FC = (props) => { />
-
- -
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/DateRangeWidget/dateRange.tsx b/apps/builder/src/widgetLibrary/DateRangeWidget/dateRange.tsx index 3302134b1f..113cf12dcc 100644 --- a/apps/builder/src/widgetLibrary/DateRangeWidget/dateRange.tsx +++ b/apps/builder/src/widgetLibrary/DateRangeWidget/dateRange.tsx @@ -203,15 +203,17 @@ export const DateRangeWidget: FC = (props) => { />
-
- -
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/DateTimeWidget/dateTime.tsx b/apps/builder/src/widgetLibrary/DateTimeWidget/dateTime.tsx index 43846ac64c..76cbd5f8ed 100644 --- a/apps/builder/src/widgetLibrary/DateTimeWidget/dateTime.tsx +++ b/apps/builder/src/widgetLibrary/DateTimeWidget/dateTime.tsx @@ -185,15 +185,17 @@ export const DateTimeWidget: FC = (props) => { /> -
- -
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/DateWidget/date.tsx b/apps/builder/src/widgetLibrary/DateWidget/date.tsx index c2be2697e4..cd16027861 100644 --- a/apps/builder/src/widgetLibrary/DateWidget/date.tsx +++ b/apps/builder/src/widgetLibrary/DateWidget/date.tsx @@ -183,15 +183,17 @@ export const DateWidget: FC = (props) => { /> -
- -
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/EditableWidget/editableText.tsx b/apps/builder/src/widgetLibrary/EditableWidget/editableText.tsx index fa2178a4a5..eb629f19db 100644 --- a/apps/builder/src/widgetLibrary/EditableWidget/editableText.tsx +++ b/apps/builder/src/widgetLibrary/EditableWidget/editableText.tsx @@ -205,15 +205,17 @@ export const EditableTextWidget: FC = (props) => { /> -
- -
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/EventCalendarWidget/eventCalendar.tsx b/apps/builder/src/widgetLibrary/EventCalendarWidget/eventCalendar.tsx index 7b11fd7743..dbee501909 100644 --- a/apps/builder/src/widgetLibrary/EventCalendarWidget/eventCalendar.tsx +++ b/apps/builder/src/widgetLibrary/EventCalendarWidget/eventCalendar.tsx @@ -206,8 +206,6 @@ export const EventCalendarWidget: FC = (props) => { resizeMsg, triggerEventHandler, handleUpdateMultiExecutionResult, - updateComponentRuntimeProps, - deleteComponentRuntimeProps, } = props const currentDefaultDate = useMemo( @@ -222,55 +220,6 @@ export const EventCalendarWidget: FC = (props) => { return formatEventOptions(eventConfigureMode, manualOptions, mappedOption) }, [eventConfigureMode, manualOptions, mappedOption]) - useEffect(() => { - updateComponentRuntimeProps?.({ - addEvent: (event: Event) => { - if ( - !event || - typeof event !== "object" || - !event.title || - !event.start || - !event.end || - !event.id - ) - return - const eventItems = eventList.filter((item) => item.id !== event.id) - handleUpdateMultiExecutionResult([ - { - displayName, - value: { - eventList: [...eventItems, event], - addEventValue: event, - }, - }, - ]) - }, - deleteEvent: (eventId: string | number) => { - const deleteEventValue = eventList.find((item) => item.id === eventId) - if (!deleteEventValue) return - const eventItems = eventList.filter((item) => item.id !== eventId) - handleUpdateMultiExecutionResult([ - { - displayName, - value: { - eventList: eventItems, - deleteEventValue, - }, - }, - ]) - }, - }) - return () => { - deleteComponentRuntimeProps() - } - }, [ - displayName, - eventList, - updateComponentRuntimeProps, - deleteComponentRuntimeProps, - handleUpdateMultiExecutionResult, - ]) - useEffect(() => { handleUpdateMultiExecutionResult([ { diff --git a/apps/builder/src/widgetLibrary/EventCalendarWidget/eventHandlerConfig.ts b/apps/builder/src/widgetLibrary/EventCalendarWidget/eventHandlerConfig.ts index 169e656547..3cb1d62a34 100644 --- a/apps/builder/src/widgetLibrary/EventCalendarWidget/eventHandlerConfig.ts +++ b/apps/builder/src/widgetLibrary/EventCalendarWidget/eventHandlerConfig.ts @@ -22,5 +22,5 @@ export const EVENT_CALENDAR_EVENT_HANDLER_CONFIG: EventHandlerConfig = { value: "DragOrClickNoEventArea", }, ], - methods: ["addEvent", "deleteEvent"], + methods: [], } diff --git a/apps/builder/src/widgetLibrary/InputWidget/input.tsx b/apps/builder/src/widgetLibrary/InputWidget/input.tsx index 82460060e2..6f2d65d48a 100644 --- a/apps/builder/src/widgetLibrary/InputWidget/input.tsx +++ b/apps/builder/src/widgetLibrary/InputWidget/input.tsx @@ -286,15 +286,17 @@ export const InputWidget: FC = (props) => { -
- -
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/MultiselectWidget/multiselect.tsx b/apps/builder/src/widgetLibrary/MultiselectWidget/multiselect.tsx index 75ef564f1c..0a70ed994d 100644 --- a/apps/builder/src/widgetLibrary/MultiselectWidget/multiselect.tsx +++ b/apps/builder/src/widgetLibrary/MultiselectWidget/multiselect.tsx @@ -210,15 +210,17 @@ export const MultiselectWidget: FC = (props) => { /> -
- -
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/NumberInputWidget/numberInput.tsx b/apps/builder/src/widgetLibrary/NumberInputWidget/numberInput.tsx index 100885da77..9cd12d222f 100644 --- a/apps/builder/src/widgetLibrary/NumberInputWidget/numberInput.tsx +++ b/apps/builder/src/widgetLibrary/NumberInputWidget/numberInput.tsx @@ -216,15 +216,17 @@ export const NumberInputWidget: FC = (props) => { /> -
- -
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/RadioGroupWidget/radioGroup.tsx b/apps/builder/src/widgetLibrary/RadioGroupWidget/radioGroup.tsx index 0aa1d323d7..f9d98c7567 100644 --- a/apps/builder/src/widgetLibrary/RadioGroupWidget/radioGroup.tsx +++ b/apps/builder/src/widgetLibrary/RadioGroupWidget/radioGroup.tsx @@ -166,15 +166,17 @@ export const RadioGroupWidget: FC = (props) => { /> -
- -
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/RateWidget/interface.tsx b/apps/builder/src/widgetLibrary/RateWidget/interface.tsx index c6320894c8..32851b4465 100644 --- a/apps/builder/src/widgetLibrary/RateWidget/interface.tsx +++ b/apps/builder/src/widgetLibrary/RateWidget/interface.tsx @@ -1,4 +1,5 @@ import { RateProps } from "@illa-design/react" +import { ValidateMessageOldProps } from "@/widgetLibrary/PublicSector/InvalidMessage/interface" import LabelProps from "@/widgetLibrary/PublicSector/Label/interface" import { TooltipWrapperProps } from "@/widgetLibrary/PublicSector/TooltipWrapper/interface" import { BaseWidgetProps } from "@/widgetLibrary/interface" @@ -12,7 +13,6 @@ export interface WrappedRateProps loading?: boolean icon?: "star" | "heart" maxCount?: RateProps["count"] - handleUpdateDsl: (value: any) => void handleOnChange?: () => void } @@ -20,4 +20,7 @@ export interface RateWidgetProps extends WrappedRateProps, BaseWidgetProps, LabelProps, - TooltipWrapperProps {} + TooltipWrapperProps, + ValidateMessageOldProps { + validateMessage: string +} diff --git a/apps/builder/src/widgetLibrary/RateWidget/rate.tsx b/apps/builder/src/widgetLibrary/RateWidget/rate.tsx index 423151626a..b9f5b4372b 100644 --- a/apps/builder/src/widgetLibrary/RateWidget/rate.tsx +++ b/apps/builder/src/widgetLibrary/RateWidget/rate.tsx @@ -1,10 +1,16 @@ -import { FC, useCallback, useEffect } from "react" +import { FC, useCallback, useEffect, useRef } from "react" import { Rate } from "@illa-design/react" import { AutoHeightContainer } from "@/widgetLibrary/PublicSector/AutoHeightContainer" import { Label } from "@/widgetLibrary/PublicSector/Label" import { TooltipWrapper } from "@/widgetLibrary/PublicSector/TooltipWrapper" -import { applyLabelAndComponentWrapperStyle } from "@/widgetLibrary/PublicSector/TransformWidgetWrapper/style" +import { + applyLabelAndComponentWrapperStyle, + applyValidateMessageWrapperStyle, +} from "@/widgetLibrary/PublicSector/TransformWidgetWrapper/style" +import { InvalidMessage } from "../PublicSector/InvalidMessage" +import { handleValidateCheck } from "../PublicSector/InvalidMessage/utils" import { RateWidgetProps, WrappedRateProps } from "./interface" +import { rateStyle } from "./style" export const WrappedRate: FC = (props) => { const { @@ -15,12 +21,12 @@ export const WrappedRate: FC = (props) => { readonly, allowHalf, maxCount, - handleUpdateDsl, handleOnChange, } = props return ( = (props) => { readonly={readonly} allowClear={allowClear} value={value} - onChange={(value) => { - handleUpdateDsl({ value }) - handleOnChange?.() - }} + onChange={handleOnChange} /> ) } @@ -53,10 +56,42 @@ export const RateWidget: FC = (props) => { required, labelHidden, tooltipText, + hideValidationMessage, + validateMessage, + customRule, + value, updateComponentHeight, triggerEventHandler, } = props + const triggerRef = useRef(null) + const getValidateMessage = useCallback( + (value?: unknown) => { + if (!hideValidationMessage) { + const message = handleValidateCheck({ + value, + required, + customRule, + }) + const showMessage = message && message.length > 0 + return showMessage ? message : "" + } + return "" + }, + [customRule, hideValidationMessage, required], + ) + + const handleValidate = useCallback( + (value?: unknown) => { + const message = getValidateMessage(value) + handleUpdateDsl({ + validateMessage: message, + }) + return message + }, + [getValidateMessage, handleUpdateDsl], + ) + useEffect(() => { updateComponentRuntimeProps({ setValue: (value: number) => { @@ -65,8 +100,14 @@ export const RateWidget: FC = (props) => { clearValue: () => { handleUpdateDsl({ value: 0 }) }, - validate: () => {}, - clearValidation: () => {}, + validate: () => { + return handleValidate(value) + }, + clearValidation: () => { + handleUpdateDsl({ + validateMessage: "", + }) + }, }) return () => { deleteComponentRuntimeProps() @@ -75,16 +116,30 @@ export const RateWidget: FC = (props) => { updateComponentRuntimeProps, handleUpdateDsl, deleteComponentRuntimeProps, + handleValidate, + value, ]) - const handleOnChange = useCallback(() => { - triggerEventHandler("change") - }, [triggerEventHandler]) + const handleOnChange = useCallback( + (value?: string) => { + new Promise((resolve) => { + const validateMessage = getValidateMessage(value) + handleUpdateDsl({ value, validateMessage }) + resolve(true) + }).then(() => { + triggerEventHandler("change") + }) + }, + [getValidateMessage, handleUpdateDsl, triggerEventHandler], + ) return ( -
+
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/RateWidget/style.ts b/apps/builder/src/widgetLibrary/RateWidget/style.ts index cd7121686d..285f272d3d 100644 --- a/apps/builder/src/widgetLibrary/RateWidget/style.ts +++ b/apps/builder/src/widgetLibrary/RateWidget/style.ts @@ -1,7 +1,5 @@ import { css } from "@emotion/react" -export const inputContainerCss = css` - display: inline-flex; - flex-direction: column; - width: 100%; +export const rateStyle = css` + overflow: hidden; ` diff --git a/apps/builder/src/widgetLibrary/RichTextWidget/eventHandlerConfig.ts b/apps/builder/src/widgetLibrary/RichTextWidget/eventHandlerConfig.ts new file mode 100644 index 0000000000..26f53e9353 --- /dev/null +++ b/apps/builder/src/widgetLibrary/RichTextWidget/eventHandlerConfig.ts @@ -0,0 +1,14 @@ +import i18n from "@/i18n/config" +import { EventHandlerConfig } from "@/widgetLibrary/interface" + +export const RICH_TEXT_EVENT_HANDLER_CONFIG: EventHandlerConfig = { + events: [ + { + label: i18n.t( + "editor.inspect.setter_content.widget_action_type_name.change", + ), + value: "change", + }, + ], + methods: ["focus"], +} diff --git a/apps/builder/src/widgetLibrary/RichTextWidget/index.ts b/apps/builder/src/widgetLibrary/RichTextWidget/index.ts new file mode 100644 index 0000000000..dc59b8d882 --- /dev/null +++ b/apps/builder/src/widgetLibrary/RichTextWidget/index.ts @@ -0,0 +1,3 @@ +export { RICH_TEXT_PANEL_CONFIG } from "./panelConfig" +export { RICH_TEXT_WIDGET_CONFIG } from "./widgetConfig" +export { RICH_TEXT_EVENT_HANDLER_CONFIG } from "./eventHandlerConfig" diff --git a/apps/builder/src/widgetLibrary/RichTextWidget/interface.ts b/apps/builder/src/widgetLibrary/RichTextWidget/interface.ts new file mode 100644 index 0000000000..974201d431 --- /dev/null +++ b/apps/builder/src/widgetLibrary/RichTextWidget/interface.ts @@ -0,0 +1,16 @@ +import { TooltipWrapperProps } from "@/widgetLibrary/PublicSector/TooltipWrapper/interface" +import { BaseWidgetProps } from "@/widgetLibrary/interface" + +export interface BaseRichTextProps extends BaseWidgetProps { + defaultText?: string + handleOnChange: (value: unknown) => void + handleMdValue: (value: unknown) => void +} + +export interface RichTextWidgetProps + extends BaseRichTextProps, + Pick {} + +export interface ICustomRef { + focus: () => void +} diff --git a/apps/builder/src/widgetLibrary/RichTextWidget/panelConfig.tsx b/apps/builder/src/widgetLibrary/RichTextWidget/panelConfig.tsx new file mode 100644 index 0000000000..421f1cccb3 --- /dev/null +++ b/apps/builder/src/widgetLibrary/RichTextWidget/panelConfig.tsx @@ -0,0 +1,140 @@ +import { ReactComponent as RadioIcon } from "@/assets/radius-icon.svg" +import { ReactComponent as StrokeWidthIcon } from "@/assets/stroke-width-icon.svg" +import i18n from "@/i18n/config" +import { PanelConfig } from "@/page/App/components/InspectPanel/interface" +import { VALIDATION_TYPES } from "@/utils/validationFactory" +import { generatorEventHandlerConfig } from "@/widgetLibrary/PublicSector/utils/generatorEventHandlerConfig" +import { RICH_TEXT_EVENT_HANDLER_CONFIG } from "@/widgetLibrary/RichTextWidget/eventHandlerConfig" + +const baseWidgetName = "richText" +export const RICH_TEXT_PANEL_CONFIG: PanelConfig[] = [ + { + id: `${baseWidgetName}-basic`, + groupName: i18n.t("editor.inspect.setter_group.basic"), + children: [ + { + id: `${baseWidgetName}-basic-default-text`, + labelName: i18n.t("editor.inspect.setter_label.rich_text.default_text"), + labelDesc: i18n.t("editor.inspect.setter_hover.rich_text.default_text"), + attrName: "defaultText", + expectedType: VALIDATION_TYPES.STRING, + setterType: "INPUT_SETTER", + }, + ], + }, + { + id: `${baseWidgetName}-interaction`, + groupName: i18n.t("editor.inspect.setter_group.interaction"), + children: [ + { + ...generatorEventHandlerConfig( + baseWidgetName, + RICH_TEXT_EVENT_HANDLER_CONFIG.events, + ), + }, + ], + }, + { + id: `${baseWidgetName}-Adornments`, + groupName: i18n.t("editor.inspect.setter_group.adornments"), + children: [ + { + id: `${baseWidgetName}-adornments-tooltip`, + labelName: i18n.t("editor.inspect.setter_label.tooltip"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.tooltip"), + attrName: "tooltipText", + setterType: "INPUT_SETTER", + expectedType: VALIDATION_TYPES.STRING, + }, + ], + }, + { + id: `${baseWidgetName}-layout`, + groupName: i18n.t("editor.inspect.setter_group.layout"), + children: [ + { + id: `${baseWidgetName}-layout-hidden`, + labelName: i18n.t("editor.inspect.setter_label.hidden"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.hidden"), + setterType: "DYNAMIC_SWITCH_SETTER", + attrName: "hidden", + placeholder: "false", + useCustomLayout: true, + openDynamic: true, + expectedType: VALIDATION_TYPES.BOOLEAN, + }, + ], + }, + { + id: `${baseWidgetName}-styles`, + groupName: i18n.t("editor.inspect.setter_group.style"), + children: [ + { + id: `${baseWidgetName}-styles-color`, + setterType: "LIST_SETTER", + labelName: i18n.t("editor.inspect.setter_label.border"), + attrName: "border", + useCustomLayout: true, + childrenSetter: [ + { + id: `${baseWidgetName}-style-border`, + labelName: i18n.t("editor.inspect.setter_label.color"), + attrName: "borderColor", + setterType: "COLOR_PICKER_SETTER", + defaultValue: "#ffffffff", + }, + { + id: `${baseWidgetName}-style-radius`, + labelName: i18n.t("editor.inspect.setter_label.radius"), + attrName: "radius", + setterType: "EDITABLE_INPUT_WITH_MEASURE_SETTER", + icon: , + defaultValue: "4px", + }, + { + id: `${baseWidgetName}-style-border-width`, + labelName: i18n.t("editor.inspect.setter_label.width"), + attrName: "borderWidth", + icon: , + setterType: "EDITABLE_INPUT_WITH_MEASURE_SETTER", + defaultValue: "1px", + }, + ], + }, + { + id: `${baseWidgetName}-styles-style`, + setterType: "LIST_SETTER", + labelName: i18n.t("editor.inspect.setter_label.style"), + attrName: "style", + useCustomLayout: true, + childrenSetter: [ + { + id: `${baseWidgetName}-style-shadow`, + labelName: i18n.t("editor.inspect.setter_label.shadow.shadow"), + attrName: "shadow", + setterType: "SHADOW_SELECT_SETTER", + defaultValue: "small", + options: [ + { + label: i18n.t("editor.inspect.setter_option.shadow.none"), + value: "none", + }, + { + label: i18n.t("editor.inspect.setter_option.shadow.large"), + value: "large", + }, + { + label: i18n.t("editor.inspect.setter_option.shadow.medium"), + value: "medium", + }, + { + label: i18n.t("editor.inspect.setter_option.shadow.small"), + value: "small", + }, + ], + }, + ], + }, + ], + }, +] diff --git a/apps/builder/src/widgetLibrary/RichTextWidget/richText.tsx b/apps/builder/src/widgetLibrary/RichTextWidget/richText.tsx new file mode 100644 index 0000000000..26be354492 --- /dev/null +++ b/apps/builder/src/widgetLibrary/RichTextWidget/richText.tsx @@ -0,0 +1,112 @@ +import { FC, forwardRef, useCallback, useEffect, useRef } from "react" +import { TooltipWrapper } from "@/widgetLibrary/PublicSector/TooltipWrapper" +import { + BaseRichTextProps, + ICustomRef, + RichTextWidgetProps, +} from "@/widgetLibrary/RichTextWidget/interface" +import { + containerStyle, + editorContainerStyle, + editorStyle, +} from "@/widgetLibrary/RichTextWidget/style" +import { useInitConfig } from "@/widgetLibrary/RichTextWidget/useInitConfig" + +const WrappedRichText = forwardRef( + (props, ref) => { + const { defaultText, handleOnChange, handleMdValue } = props + useInitConfig(defaultText ?? "", handleOnChange, handleMdValue, ref) + return ( +
+
e.stopPropagation()} + css={editorStyle} + /> +
+ ) + }, +) + +WrappedRichText.displayName = "WrappedRichText" + +export const RichTextWidget: FC = (props) => { + const { + displayName, + tooltipText, + deleteComponentRuntimeProps, + updateComponentRuntimeProps, + triggerEventHandler, + handleUpdateMultiExecutionResult, + } = props + + const editorRef = useRef(null) + const containerRef = useRef(null) + + useEffect(() => { + updateComponentRuntimeProps({ + focus: () => { + editorRef.current?.focus() + }, + }) + return () => { + deleteComponentRuntimeProps() + } + }, [ + updateComponentRuntimeProps, + deleteComponentRuntimeProps, + handleUpdateMultiExecutionResult, + displayName, + ]) + + const handleOnChange = useCallback( + (value: unknown) => { + if (typeof value !== "string") return + new Promise((resolve) => { + handleUpdateMultiExecutionResult([ + { + displayName, + value: { + value, + }, + }, + ]) + resolve(value) + }).then(() => { + triggerEventHandler("change") + }) + }, + [displayName, handleUpdateMultiExecutionResult, triggerEventHandler], + ) + + const handleMdValue = useCallback( + (value: unknown) => { + if (typeof value !== "string") return + handleUpdateMultiExecutionResult([ + { + displayName, + value: { + markdownValue: value, + }, + }, + ]) + }, + [displayName, handleUpdateMultiExecutionResult], + ) + + return ( + +
+ +
+
+ ) +} + +RichTextWidget.displayName = "RichTextWidget" +export default RichTextWidget diff --git a/apps/builder/src/widgetLibrary/RichTextWidget/style.ts b/apps/builder/src/widgetLibrary/RichTextWidget/style.ts new file mode 100644 index 0000000000..c7b2e91f67 --- /dev/null +++ b/apps/builder/src/widgetLibrary/RichTextWidget/style.ts @@ -0,0 +1,38 @@ +import { css } from "@emotion/react" + +export const containerStyle = css` + height: 100%; + width: 100%; + overflow: hidden; +` +export const editorContainerStyle = css` + height: 100%; + width: 100%; + padding: 24px 60px; + overflow: auto; +` +export const editorStyle = css` + width: 100%; + .codex-editor__redactor { + padding-bottom: 0 !important; + } + .ce-block__content { + width: 100%; + margin: 0; + max-width: 100%; + & * { + cursor: text; + } + } + .ce-toolbar__content { + width: 100%; + max-width: 100%; + } + .tc-add-column, + .tc-add-row { + display: none; + } + .tc-row:after { + height: 0px; + } +` diff --git a/apps/builder/src/widgetLibrary/RichTextWidget/useInitConfig.ts b/apps/builder/src/widgetLibrary/RichTextWidget/useInitConfig.ts new file mode 100644 index 0000000000..261a718273 --- /dev/null +++ b/apps/builder/src/widgetLibrary/RichTextWidget/useInitConfig.ts @@ -0,0 +1,127 @@ +import CheckList from "@editorjs/checklist" +import Code from "@editorjs/code" +import EditorJS from "@editorjs/editorjs" +import Embed from "@editorjs/embed" +import Header from "@editorjs/header" +import Image from "@editorjs/image" +import InlineCode from "@editorjs/inline-code" +import List from "@editorjs/list" +import Marker from "@editorjs/marker" +import parser from "editorjs-html" +import { MDfromBlocks } from "editorjs-md-parser" +import { ForwardedRef, useEffect, useImperativeHandle, useRef } from "react" +import { ICustomRef } from "@/widgetLibrary/RichTextWidget/interface" +import { handleImageUpload, parseCheckList } from "./utils" + +export const useInitConfig = ( + defaultText: string, + handleOnChange: (value: unknown) => void, + handleMdValue: (value: unknown) => void, + ref: ForwardedRef, +) => { + const editorRef = useRef(null) + + useImperativeHandle(ref, () => ({ + focus: () => { + editorRef.current?.focus(true) + }, + })) + + useEffect(() => { + if (!editorRef.current) { + const editorInstance = new EditorJS({ + holder: "editor-container", + tools: { + header: { + class: Header, + inlineToolbar: true, + }, + code: Code, + list: { + class: List, + inlineToolbar: true, + }, + checklist: { + class: CheckList, + inlineToolbar: true, + }, + image: { + class: Image, + config: { + uploader: { + uploadByFile(file: Blob) { + return handleImageUpload(file).then((data) => { + return { + success: 1, + file: { + url: data, + }, + } + }) + }, + }, + }, + }, + marker: { + class: Marker, + inlineToolbar: true, + }, + inlineCode: { + class: InlineCode, + inlineToolbar: true, + }, + embed: Embed, + }, + data: { + blocks: [ + { + type: "paragraph", + data: { + text: defaultText, + level: 1, + }, + }, + ], + }, + onChange: async (_) => { + const blocks = await _.saver.save() + const htmlParser = parser({ + checklist: parseCheckList, + }) + try { + const html = htmlParser.parse(blocks).join("\n") + editorRef.current?.emit("change", html) + MDfromBlocks(blocks.blocks).then((md) => { + editorRef.current?.emit("mdValue", md) + }) + } catch (e) {} + }, + }) + editorRef.current = editorInstance + } + + return () => { + if (editorRef.current && editorRef.current.destroy) { + editorRef.current.destroy() + editorRef.current = null + } + } + }, [defaultText]) + + useEffect(() => { + if (editorRef.current) { + editorRef.current.isReady.then(() => { + editorRef.current?.on("change", handleOnChange) + editorRef.current?.on("mdValue", handleMdValue) + }) + } + return () => { + if (editorRef.current) { + editorRef.current.isReady.then(() => { + editorRef.current?.off("change", handleOnChange) + editorRef.current?.off("mdValue", handleMdValue) + }) + } + } + }, [handleMdValue, handleOnChange]) +} diff --git a/apps/builder/src/widgetLibrary/RichTextWidget/utils.ts b/apps/builder/src/widgetLibrary/RichTextWidget/utils.ts new file mode 100644 index 0000000000..ea95fabdba --- /dev/null +++ b/apps/builder/src/widgetLibrary/RichTextWidget/utils.ts @@ -0,0 +1,32 @@ +import { OutputBlockData } from "@editorjs/editorjs" + +export const parseCheckList = (block: OutputBlockData) => { + const items = block.data.items + if (Array.isArray(items) && items.length) { + const checkListHtml: string[] = [] + items.forEach((item: { checked: boolean; text: string }, index) => { + const { checked, text } = item + checkListHtml.push(` +
+ + +
`) + }) + return checkListHtml.join("\n") + } +} + +export const handleImageUpload = (file: Blob) => { + return new Promise(function (resolve, reject) { + var reader = new FileReader() + reader.onload = function () { + return resolve(reader.result) + } + reader.onerror = reject + reader.readAsDataURL(file) + }) +} diff --git a/apps/builder/src/widgetLibrary/RichTextWidget/widgetConfig.tsx b/apps/builder/src/widgetLibrary/RichTextWidget/widgetConfig.tsx new file mode 100644 index 0000000000..7b752e8d27 --- /dev/null +++ b/apps/builder/src/widgetLibrary/RichTextWidget/widgetConfig.tsx @@ -0,0 +1,26 @@ +import { ReactComponent as RichTextWidgetIcon } from "@/assets/widgetCover/richText.svg" +import i18n from "@/i18n/config" +import { RESIZE_DIRECTION, WidgetConfig } from "@/widgetLibrary/interface" + +export const RICH_TEXT_WIDGET_CONFIG: WidgetConfig = { + version: 0, + type: "RICH_TEXT_WIDGET", + displayName: "richText", + widgetName: i18n.t("widget.rich_text_editor.name"), + icon: , + resizeDirection: RESIZE_DIRECTION.ALL, + keywords: ["Rich Text", "富文本"], + sessionType: "INPUTS", + w: 20, + h: 55, + defaults: { + defaultText: i18n.t( + "editor.inspect.setter_default_value.rich_text.default_text", + ), + hidden: false, + radius: "4px", + borderWidth: "1px", + shadow: "small", + markdownValue: "", + }, +} diff --git a/apps/builder/src/widgetLibrary/SelectWidget/select.tsx b/apps/builder/src/widgetLibrary/SelectWidget/select.tsx index 9fdec2b429..36af9b4279 100644 --- a/apps/builder/src/widgetLibrary/SelectWidget/select.tsx +++ b/apps/builder/src/widgetLibrary/SelectWidget/select.tsx @@ -192,15 +192,17 @@ export const SelectWidget: FC = (props) => { />
-
- -
+ {!hideValidationMessage && ( +
+ +
+ )} ) } diff --git a/apps/builder/src/widgetLibrary/StepsWidget/interface.ts b/apps/builder/src/widgetLibrary/StepsWidget/interface.ts index 8d3b546de3..b8f43fb5c4 100644 --- a/apps/builder/src/widgetLibrary/StepsWidget/interface.ts +++ b/apps/builder/src/widgetLibrary/StepsWidget/interface.ts @@ -18,6 +18,7 @@ export interface WrappedStepsProps current?: number currentIndex?: number defaultStep?: string + disabled?: boolean handleStepsChange?: (current: number) => void } diff --git a/apps/builder/src/widgetLibrary/StepsWidget/panelConfig.tsx b/apps/builder/src/widgetLibrary/StepsWidget/panelConfig.tsx index 704cd7d648..1d54e9a318 100644 --- a/apps/builder/src/widgetLibrary/StepsWidget/panelConfig.tsx +++ b/apps/builder/src/widgetLibrary/StepsWidget/panelConfig.tsx @@ -208,6 +208,23 @@ export const STEPS_PANEL_CONFIG: PanelConfig[] = [ }, ], }, + { + id: `${baseWidgetName}-interaction`, + groupName: i18n.t("editor.inspect.setter_group.interaction"), + children: [ + { + id: `${baseWidgetName}-interaction-disabled`, + labelName: i18n.t("editor.inspect.setter_label.disabled"), + labelDesc: i18n.t("editor.inspect.setter_tooltip.disabled"), + placeholder: "{{false}}", + attrName: "disabled", + setterType: "INPUT_SETTER", + expectedType: VALIDATION_TYPES.BOOLEAN, + bindAttrName: ["submit"], + shown: (value) => !value, + }, + ], + }, { id: `${baseWidgetName}-layout`, groupName: i18n.t("editor.inspect.setter_group.layout"), diff --git a/apps/builder/src/widgetLibrary/StepsWidget/steps.tsx b/apps/builder/src/widgetLibrary/StepsWidget/steps.tsx index ac964a8317..b50c10eb88 100644 --- a/apps/builder/src/widgetLibrary/StepsWidget/steps.tsx +++ b/apps/builder/src/widgetLibrary/StepsWidget/steps.tsx @@ -38,6 +38,7 @@ export const StepsWidget: FC = (props) => { currentIndex, linkWidgetDisplayName, defaultStep, + disabled, updateComponentHeight, handleUpdateMultiExecutionResult, handleUpdateOriginalDSLOtherMultiAttrNotUseUnDoRedo, @@ -80,7 +81,11 @@ export const StepsWidget: FC = (props) => { */ const { items, uniqueOptions } = useMemo(() => { if (!isLinkedContainer) { - return formatStepsData(optionConfigureMode, formatOptionConfigData) + return formatStepsData( + optionConfigureMode, + formatOptionConfigData, + disabled, + ) } const results = transformedContainerList.map((item, index) => { const { label, value, caption, tooltip = "" } = item @@ -88,6 +93,7 @@ export const StepsWidget: FC = (props) => { return { title: getStepItemTitle(titleContent, tooltip), description: caption, + disabled, } }) return { @@ -99,6 +105,7 @@ export const StepsWidget: FC = (props) => { transformedContainerList, optionConfigureMode, formatOptionConfigData, + disabled, ]) const handleUpdateMultiExecutionResults = useCallback( diff --git a/apps/builder/src/widgetLibrary/StepsWidget/util.tsx b/apps/builder/src/widgetLibrary/StepsWidget/util.tsx index 3a7c2ac055..35118c8f93 100644 --- a/apps/builder/src/widgetLibrary/StepsWidget/util.tsx +++ b/apps/builder/src/widgetLibrary/StepsWidget/util.tsx @@ -89,6 +89,7 @@ export const getStepItemTitle = (title: any, tooltip?: any) => { export const formatStepsData = ( optionConfigureMode: "dynamic" | "static", formatOptionConfigData: StepsOptionsType[] | StepsMappedOptionType, + disabled?: boolean, ) => { if (optionConfigureMode === "static") { const uniqueManualOptions = formatOptionConfigData as StepsOptionsType[] @@ -98,6 +99,7 @@ export const formatStepsData = ( return { title: getStepItemTitle(titleContent, tooltip), description: caption, + disabled, } }) return { diff --git a/apps/builder/src/widgetLibrary/StepsWidget/widgetConfig.tsx b/apps/builder/src/widgetLibrary/StepsWidget/widgetConfig.tsx index 023151283d..660e1f9441 100644 --- a/apps/builder/src/widgetLibrary/StepsWidget/widgetConfig.tsx +++ b/apps/builder/src/widgetLibrary/StepsWidget/widgetConfig.tsx @@ -33,5 +33,6 @@ export const STEPS_WIDGET_CONFIG: WidgetConfig = { currentIndex: 0, dataSources: `{{${JSON.stringify(originData, null, 2)}}}`, resizeDirection: RESIZE_DIRECTION.HORIZONTAL, + disabled: false, }, } diff --git a/apps/builder/src/widgetLibrary/TextAreaWidget/interface.tsx b/apps/builder/src/widgetLibrary/TextAreaWidget/interface.tsx index 04df593c4d..d77ff78a71 100644 --- a/apps/builder/src/widgetLibrary/TextAreaWidget/interface.tsx +++ b/apps/builder/src/widgetLibrary/TextAreaWidget/interface.tsx @@ -9,7 +9,8 @@ export interface WrappedTextareaProps TextAreaProps, "placeholder" | "disabled" | "readOnly" | "maxLength" | "colorScheme" >, - BaseWidgetProps { + BaseWidgetProps, + LabelProps { showCharacterCount?: TextAreaProps["showWordLimit"] value?: string allowClear?: TextAreaProps["allowClear"] @@ -27,12 +28,12 @@ export interface WrappedTextareaProps dynamicMinHeight?: number dynamicMaxHeight?: number getValidateMessage: (value: string) => string + showValidationMessage: boolean } export interface TextareaWidgetProps extends WrappedTextareaProps, BaseWidgetProps, - LabelProps, TooltipWrapperProps, ValidateMessageOldProps { validateMessage: string diff --git a/apps/builder/src/widgetLibrary/TextAreaWidget/style.ts b/apps/builder/src/widgetLibrary/TextAreaWidget/style.ts index 269f7c1118..57f856e80a 100644 --- a/apps/builder/src/widgetLibrary/TextAreaWidget/style.ts +++ b/apps/builder/src/widgetLibrary/TextAreaWidget/style.ts @@ -1,16 +1,27 @@ import { css } from "@emotion/react" +import { VALIDATE_MESSAGE_HEIGHT } from "@/page/App/components/ScaleSquare/constant/widget" export const getTextareaContentContainerStyle = ( labelPosition: "left" | "right" | "top" = "left", - height: number, + showValidationMessage: boolean, ) => { return css` display: flex; - flex: 1; + height: ${showValidationMessage + ? `calc(100% - ${VALIDATE_MESSAGE_HEIGHT})px` + : "100%"}; flex-direction: ${labelPosition === "top" ? "column" : "row"}; - min-height: calc(100% - ${height}px); & textarea { resize: none; } ` } + +export const textAreaStyle = css` + flex: 1; + width: 100%; + overflow-y: auto; + & > textarea:hover { + z-index: 0; + } +` diff --git a/apps/builder/src/widgetLibrary/TextAreaWidget/textArea.tsx b/apps/builder/src/widgetLibrary/TextAreaWidget/textArea.tsx index 9398b2bcd0..54a49357d5 100644 --- a/apps/builder/src/widgetLibrary/TextAreaWidget/textArea.tsx +++ b/apps/builder/src/widgetLibrary/TextAreaWidget/textArea.tsx @@ -2,6 +2,10 @@ import { FC, forwardRef, useCallback, useEffect, useMemo, useRef } from "react" import useMeasure from "react-use-measure" import { TextArea } from "@illa-design/react" import { UNIT_HEIGHT } from "@/page/App/components/DotPanel/constant/canvas" +import { + LABEL_TOP_UNIT_HEIGHT, + VALIDATE_MESSAGE_HEIGHT, +} from "@/page/App/components/ScaleSquare/constant/widget" import { AutoHeightContainer } from "@/widgetLibrary/PublicSector/AutoHeightContainer" import { InvalidMessage } from "@/widgetLibrary/PublicSector/InvalidMessage" import { handleValidateCheck } from "@/widgetLibrary/PublicSector/InvalidMessage/utils" @@ -15,7 +19,10 @@ import { TextareaWidgetProps, WrappedTextareaProps, } from "@/widgetLibrary/TextAreaWidget/interface" -import { getTextareaContentContainerStyle } from "@/widgetLibrary/TextAreaWidget/style" +import { + getTextareaContentContainerStyle, + textAreaStyle, +} from "@/widgetLibrary/TextAreaWidget/style" export const WrappedTextarea = forwardRef< HTMLTextAreaElement, @@ -38,8 +45,10 @@ export const WrappedTextarea = forwardRef< handleUpdateMultiExecutionResult, getValidateMessage, dynamicHeight, - dynamicMinHeight, - dynamicMaxHeight, + dynamicMinHeight = 0, + dynamicMaxHeight = Infinity, + labelPosition, + showValidationMessage, } = props const handleClear = () => handleUpdateDsl({ value: "" }) @@ -70,24 +79,46 @@ export const WrappedTextarea = forwardRef< ], ) - const limitedStyle = - dynamicHeight === "limited" - ? { - minH: - dynamicMinHeight !== undefined - ? `${dynamicMinHeight}px` - : undefined, - maxH: - dynamicMaxHeight !== undefined - ? `${dynamicMaxHeight}px` - : undefined, - } - : {} + const limitedStyle = useMemo(() => { + const limitLinePosition = + labelPosition === "top" ? LABEL_TOP_UNIT_HEIGHT : UNIT_HEIGHT + const minH = `${ + showValidationMessage + ? dynamicMinHeight - limitLinePosition - VALIDATE_MESSAGE_HEIGHT + : dynamicMinHeight - limitLinePosition + }px` + const maxH = `${ + showValidationMessage + ? dynamicMaxHeight - limitLinePosition - VALIDATE_MESSAGE_HEIGHT + : dynamicMaxHeight - limitLinePosition + }px` + if (dynamicHeight === "limited") { + return { + minH, + maxH, + h: "auto", + } + } + return { + minH: showValidationMessage + ? `calc(100% - ${VALIDATE_MESSAGE_HEIGHT})px` + : "100%", + maxH: showValidationMessage + ? `calc(100% - ${VALIDATE_MESSAGE_HEIGHT})px` + : "100%", + h: "auto", + } + }, [ + dynamicHeight, + dynamicMaxHeight, + dynamicMinHeight, + labelPosition, + showValidationMessage, + ]) return (