feat: setup vue component

This commit is contained in:
Shivam Mishra
2025-06-10 14:48:37 +05:30
parent 6840108f30
commit d8a1fdbce5
6 changed files with 426 additions and 3 deletions
@@ -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 (
<div
className={`chatwoot-conversation ${className}`}
style={{
height: '100%',
width: '100%',
position: 'relative',
...style
}}
>
<ChatwootMessageListWrapper
conversationId={conversationId}
onLoad={handleLoad}
onError={handleError}
className="h-full w-full"
{...otherProps}
/>
</div>
);
};
// Export props interface for TypeScript users (future)
// export interface ChatwootConversationProps {
// conversationId: number | string;
// className?: string;
// style?: React.CSSProperties;
// onError?: (error: Error) => void;
// onLoad?: () => void;
// }
@@ -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 (
<chatwoot-message-list
ref={elementRef}
className={className}
style={style}
{...otherProps}
/>
);
};
@@ -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 (
<ChatwootContext.Provider value={config}>
{children}
</ChatwootContext.Provider>
);
};
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;
+10 -1
View File
@@ -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';
export { VueWebComponentWrapper } from './components/VueWebComponentWrapper';
// Lower-level components (advanced usage)
export { ChatwootMessageListWrapper } from './components/ChatwootMessageListWrapper';
@@ -0,0 +1,154 @@
<script setup>
import { ref, onMounted, watch } from 'vue';
import MessageList from '../../../ui/MessageList.vue';
// Props that become Web Component attributes
const props = defineProps({
conversationId: {
type: [String, Number],
required: true,
},
});
// Internal state
const isInitialized = ref(false);
const error = ref(null);
async function waitForGlobalInitialization() {
// Wait for global Chatwoot objects to be available
let attempts = 0;
const maxAttempts = 50; // 5 seconds max wait
while (attempts < maxAttempts) {
// eslint-disable-next-line no-underscore-dangle
if (window.__CHATWOOT_STORE__ && window.WootConstants) {
return;
}
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => {
// eslint-disable-next-line no-promise-executor-return
return setTimeout(resolve, 100);
});
attempts += 1;
}
throw new Error('Chatwoot global initialization timed out');
}
function updateConversationId() {
if (props.conversationId) {
// eslint-disable-next-line no-underscore-dangle
window.__WOOT_CONVERSATION_ID__ = Number(props.conversationId);
}
}
onMounted(async () => {
try {
// Wait for global initialization (done by Provider)
await waitForGlobalInitialization();
isInitialized.value = true;
} catch (err) {
// eslint-disable-next-line no-console
console.error('Failed to initialize MessageList:', err);
error.value = err.message;
}
});
// Watch for conversation ID changes
watch(
() => props.conversationId,
() => {
if (isInitialized.value) {
updateConversationId();
}
},
{
immediate: true,
}
);
</script>
<template>
<div class="chatwoot-message-list-container">
<!-- Loading state -->
<div v-if="!isInitialized && !error" class="chatwoot-loading">
<div class="flex items-center justify-center h-full">
<div class="text-center">
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"
/>
<!-- eslint-disable-next-line vue/no-bare-strings-in-template -->
<p class="text-sm text-gray-600">Loading conversation...</p>
</div>
</div>
</div>
<!-- Error state -->
<div v-else-if="error" class="chatwoot-error">
<div class="flex items-center justify-center h-full">
<div class="text-center p-4">
<div class="text-red-500 mb-2">
<svg
class="w-8 h-8 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 15.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<!-- eslint-disable-next-line vue/no-bare-strings-in-template -->
<p class="text-sm text-gray-600">Failed to load conversation</p>
<p class="text-xs text-gray-400 mt-1">{{ error }}</p>
</div>
</div>
</div>
<!-- Main MessageList component -->
<div v-else-if="isInitialized" class="chatwoot-message-list">
<MessageList />
</div>
</div>
</template>
<style>
/* Import all necessary styles for the MessageList */
@import '../../../dashboard/assets/scss/app.scss';
@import 'vue-multiselect/dist/vue-multiselect.css';
@import 'floating-vue/dist/style.css';
.chatwoot-message-list-container {
height: 100%;
width: 100%;
position: relative;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
}
.chatwoot-loading,
.chatwoot-error {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.chatwoot-message-list {
height: 100%;
width: 100%;
}
/* Ensure proper containment */
.chatwoot-message-list-container {
contain: layout style;
}
</style>
@@ -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 };