diff --git a/app/javascript/react-components/src/components/ChatwootConversation.jsx b/app/javascript/react-components/src/components/ChatwootConversation.jsx new file mode 100644 index 000000000..6af23cfc1 --- /dev/null +++ b/app/javascript/react-components/src/components/ChatwootConversation.jsx @@ -0,0 +1,64 @@ +import { useState, useCallback } from 'react'; +import { useChatwoot } from './ChatwootProvider'; +import { ChatwootMessageListWrapper } from './ChatwootMessageListWrapper'; + +export const ChatwootConversation = ({ + conversationId, + className = '', + style = {}, + onError = null, + onLoad = null, + ...otherProps +}) => { + // Ensure we're inside a ChatwootProvider + useChatwoot(); // This will throw if not in Provider context + + const [isLoaded, setIsLoaded] = useState(false); + const [error, setError] = useState(null); + + // Validate required props + if (!conversationId) { + throw new Error('ChatwootConversation: conversationId is required'); + } + + const handleLoad = useCallback(() => { + setIsLoaded(true); + setError(null); + onLoad?.(); + }, [onLoad]); + + const handleError = useCallback((err) => { + setError(err.message); + setIsLoaded(false); + onError?.(err); + }, [onError]); + + return ( +
+ +
+ ); +}; + +// Export props interface for TypeScript users (future) +// export interface ChatwootConversationProps { +// conversationId: number | string; +// className?: string; +// style?: React.CSSProperties; +// onError?: (error: Error) => void; +// onLoad?: () => void; +// } diff --git a/app/javascript/react-components/src/components/ChatwootMessageListWrapper.jsx b/app/javascript/react-components/src/components/ChatwootMessageListWrapper.jsx new file mode 100644 index 000000000..3ec324312 --- /dev/null +++ b/app/javascript/react-components/src/components/ChatwootMessageListWrapper.jsx @@ -0,0 +1,59 @@ +import { useRef, useEffect } from 'react'; + +export const ChatwootMessageListWrapper = ({ + conversationId, + className = '', + style = {}, + onError = null, + onLoad = null, + ...otherProps +}) => { + const elementRef = useRef(); + + // Update Web Component props when React props change + useEffect(() => { + if (!elementRef.current) return; + + const element = elementRef.current; + + // Set conversation ID on the Web Component + element.conversationId = conversationId; + + }, [conversationId]); + + // Handle Web Component events + useEffect(() => { + if (!elementRef.current) return; + + const element = elementRef.current; + + const handleLoad = () => { + onLoad?.(); + }; + + const handleError = (event) => { + const errorMessage = event.detail?.message || 'Unknown error occurred'; + onError?.(new Error(errorMessage)); + }; + + // Listen for custom events from the Web Component + element.addEventListener('chatwoot:loaded', handleLoad); + element.addEventListener('chatwoot:error', handleError); + + return () => { + element.removeEventListener('chatwoot:loaded', handleLoad); + element.removeEventListener('chatwoot:error', handleError); + }; + }, [onLoad, onError]); + + // Render the Web Component + // Global setup is handled by ChatwootProvider + return ( + + ); +}; diff --git a/app/javascript/react-components/src/components/ChatwootProvider.jsx b/app/javascript/react-components/src/components/ChatwootProvider.jsx new file mode 100644 index 000000000..c5e9de482 --- /dev/null +++ b/app/javascript/react-components/src/components/ChatwootProvider.jsx @@ -0,0 +1,127 @@ +import { createContext, useContext, useEffect, useRef } from 'react'; +import { registerVueWebComponents } from '../vue-components/registerWebComponents'; +import store from '../../../dashboard/store'; +import constants from '../../../dashboard/constants/globals'; +import axios from 'axios'; +import createAxios from '../../../ui/axios'; +import commonHelpers from '../../../dashboard/helper/commons'; +import vueActionCable from '../../../dashboard/helper/actionCable'; + +const ChatwootContext = createContext(); + +export const ChatwootProvider = ({ + baseURL, + userId, + userToken, + websocketURL, + pubsubToken, + children +}) => { + const isInitialized = useRef(false); + const originalGlobals = useRef({}); + + // Validate required props + if (!baseURL) { + throw new Error('ChatwootProvider: baseURL is required'); + } + if (!userToken) { + throw new Error('ChatwootProvider: userToken is required'); + } + + // Configuration object passed to all child components + const config = { + baseURL: baseURL.replace(/\/$/, ''), // Remove trailing slash + userId, + userToken, + websocketURL: websocketURL || `${baseURL.replace('http', 'ws')}/cable`, + pubsubToken: pubsubToken || userToken, // Fallback to userToken if pubsubToken not provided + }; + + useEffect(() => { + if (isInitialized.current) return; + + initializeChatwootGlobals(); + isInitialized.current = true; + + // Cleanup on unmount + return () => { + cleanupChatwootGlobals(); + }; + }, [config.baseURL, config.userToken, config.websocketURL, config.pubsubToken]); + + function initializeChatwootGlobals() { + // Store original globals for cleanup + storeOriginalGlobals(); + + // Register Web Components + registerVueWebComponents(); + + // Set up global variables that Vue components expect + window.__WOOT_API_HOST__ = config.baseURL; + window.__WOOT_ACCESS_TOKEN__ = config.userToken; + window.__WEBSOCKET_URL__ = config.websocketURL; + window.__PUBSUB_TOKEN__ = config.pubsubToken; + window.__WOOT_USER_ID__ = config.userId ? Number(config.userId) : undefined; + window.__WOOT_ISOLATED_SHELL__ = true; + + // Initialize common helpers + commonHelpers(); + + // Set up global objects + window.__CHATWOOT_STORE__ = store; + window.WootConstants = constants; + window.axios = createAxios(axios); + + // Initialize user in store and ActionCable + store.dispatch('setUser').then(() => { + vueActionCable.init(store, config.pubsubToken); + }); + } + + function storeOriginalGlobals() { + originalGlobals.current = { + __WOOT_API_HOST__: window.__WOOT_API_HOST__, + __WOOT_ACCESS_TOKEN__: window.__WOOT_ACCESS_TOKEN__, + __WEBSOCKET_URL__: window.__WEBSOCKET_URL__, + __PUBSUB_TOKEN__: window.__PUBSUB_TOKEN__, + __WOOT_USER_ID__: window.__WOOT_USER_ID__, + __WOOT_ISOLATED_SHELL__: window.__WOOT_ISOLATED_SHELL__, + __CHATWOOT_STORE__: window.__CHATWOOT_STORE__, + WootConstants: window.WootConstants, + axios: window.axios + }; + } + + function cleanupChatwootGlobals() { + // Restore original globals + Object.entries(originalGlobals.current).forEach(([key, value]) => { + if (value !== undefined) { + window[key] = value; + } else { + delete window[key]; + } + }); + + // Disconnect ActionCable + if (vueActionCable.connection) { + vueActionCable.connection.disconnect(); + } + } + + return ( + + {children} + + ); +}; + +export const useChatwoot = () => { + const context = useContext(ChatwootContext); + if (!context) { + throw new Error('useChatwoot must be used within ChatwootProvider'); + } + return context; +}; + +// For backwards compatibility and explicit configuration access +export const useChawootConfig = useChatwoot; diff --git a/app/javascript/react-components/src/index.jsx b/app/javascript/react-components/src/index.jsx index c2f0dcd63..e655f0385 100644 --- a/app/javascript/react-components/src/index.jsx +++ b/app/javascript/react-components/src/index.jsx @@ -1,3 +1,12 @@ // Export all React components for the Chatwoot React Components library + +// Main API components +export { ChatwootProvider, useChatwoot } from './components/ChatwootProvider'; +export { ChatwootConversation } from './components/ChatwootConversation'; + +// For testing/demo purposes export { HelloWorld } from './components/HelloWorld'; -export { VueWebComponentWrapper } from './components/VueWebComponentWrapper'; \ No newline at end of file +export { VueWebComponentWrapper } from './components/VueWebComponentWrapper'; + +// Lower-level components (advanced usage) +export { ChatwootMessageListWrapper } from './components/ChatwootMessageListWrapper'; \ No newline at end of file diff --git a/app/javascript/react-components/src/vue-components/ChatwootMessageListWebComponent.vue b/app/javascript/react-components/src/vue-components/ChatwootMessageListWebComponent.vue new file mode 100644 index 000000000..9dd40a931 --- /dev/null +++ b/app/javascript/react-components/src/vue-components/ChatwootMessageListWebComponent.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/app/javascript/react-components/src/vue-components/registerWebComponents.js b/app/javascript/react-components/src/vue-components/registerWebComponents.js index 7d6837966..f58589dc2 100644 --- a/app/javascript/react-components/src/vue-components/registerWebComponents.js +++ b/app/javascript/react-components/src/vue-components/registerWebComponents.js @@ -1,8 +1,12 @@ import { defineCustomElement } from 'vue'; import VueHelloWorld from './VueHelloWorld.vue'; +import ChatwootMessageListWebComponent from './ChatwootMessageListWebComponent.vue'; -// Convert Vue component to Web Component +// Convert Vue components to Web Components const VueHelloWorldElement = defineCustomElement(VueHelloWorld); +const ChatwootMessageListElement = defineCustomElement( + ChatwootMessageListWebComponent +); // Register Web Components export const registerVueWebComponents = () => { @@ -12,7 +16,13 @@ export const registerVueWebComponents = () => { // eslint-disable-next-line no-console console.log('✅ Registered vue-hello-world Web Component'); } + + if (!customElements.get('chatwoot-message-list')) { + customElements.define('chatwoot-message-list', ChatwootMessageListElement); + // eslint-disable-next-line no-console + console.log('✅ Registered chatwoot-message-list Web Component'); + } }; // Export for manual registration if needed -export { VueHelloWorldElement }; +export { VueHelloWorldElement, ChatwootMessageListElement };