feat: setup vue component
This commit is contained in:
@@ -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;
|
||||
@@ -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';
|
||||
+154
@@ -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>
|
||||
+12
-2
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user