UNPKG

@quartal/bridge-client

Version:

Universal client library for embedding applications with URL-configurable transport support (iframe, postMessage) and framework adapters for Angular and Vue

715 lines (594 loc) 19.6 kB
// Example: Generic Vue child client composable using QuartalVueAdapter // This example shows how to implement a child client service that can be embedded in parent applications // Based on quartal-invoicing-ui app.service.ts but made generic for Vue 3 applications import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import { useRouter } from 'vue-router' import { ChildClient, QuartalVueAdapter, FastAction, FastActionCallbackResponse, ParentDataResponse, ClearStorageRequest } from '@quartal/client' // Global state for the child client (similar to Angular app.service.ts) let globalAccountingClient: ChildClient | null = null let globalVueAdapter: QuartalVueAdapter | null = null // Define interfaces for type safety interface AppSettings { customActions?: any[]; customEntities?: any[]; customTabs?: any; customTables?: any[]; [key: string]: any; } interface HttpStatusService { hasPendingRequest(): any; // Observable-like } // At the application level (main.ts) export function setupGlobalChildClient() { // This should be called once when the app starts // if it detects it's running in an iframe context if (window !== window.parent) { console.log('Vue child app detected iframe context') // Listen for beforeunload to clean up if needed window.addEventListener('beforeunload', () => { // Clean up child client on page unload if (globalAccountingClient) { globalAccountingClient.destroy() globalAccountingClient = null } if (globalVueAdapter) { globalVueAdapter.destroy() globalVueAdapter = null } }) } } // Vue useChildClient composable - equivalent to Angular app.service.ts export function useChildClient( storageService: StorageService, httpStatusService?: HttpStatusService ) { const router = useRouter() // Reactive state const isConnected = ref(false) const isInIframe = ref(window !== window.parent) const isInitialized = ref(false) // App settings management const appSettings = ref<AppSettings | null>(null) // Fast actions management const fastAction = ref<FastAction | null>(null) const fastActionCallback = ref<FastActionCallbackResponse | null>(null) // Parent data management const parentData = ref<ParentDataResponse | null>(null) // Custom entities management const customEntities = ref<any>(null) // User notifications (optional) const userNotifications = ref<any>(null) // Fast actions array const fastActions = ref<FastAction[]>([]) // Initialize child client (equivalent to Angular's initializeChildClient) const initializeChildClient = () => { if (globalVueAdapter || globalAccountingClient) { console.log('Child client already initialized') return } if (!isInIframe.value) { console.log('Running as standalone app, no child client needed') // Set default app settings for standalone mode const defaultSettings = getDefaultAppSettings() setAppSettings(defaultSettings) return } try { // Create Vue adapter with services globalVueAdapter = new QuartalVueAdapter( router, storageService ) // Create child client globalAccountingClient = globalVueAdapter.createChildClient({ debug: false, trace: false, appPrefix: 'MyApp', autoConnect: true }, { // Called when parent client sends initialization data onInited: (data: any) => { console.log('Child client inited with data:', data) handleInitData(data) }, // Parent requests URL change in child onOpenUrl: (url: string) => { console.log('Parent requested URL change:', url) handleUrlChange(url) }, // Parent requests navigation with redirect onRedirect: (url: string[]) => { console.log('Parent requested redirect:', url) handleRedirect(url) }, // Parent sends action to execute onOpenAction: (data: any) => { console.log('Parent sent action:', data) handleAction(data) }, // Parent requests cache clear onClearCache: (data: any) => { console.log('Parent requested cache clear:', data) handleClearCache(data) }, // Parent requests storage clear onClearStorage: (request: ClearStorageRequest) => { console.log('Parent requested storage clear:', request) handleClearStorage(request) }, // Parent requests logout onLogout: () => { console.log('Parent requested logout') handleLogout() }, // Parent sends data onFetchedData: (data: any) => { console.log('Parent sent data:', data) setParentData(data) } }) console.log('Child client initialized successfully') isConnected.value = true } catch (error) { console.error('Failed to initialize child client:', error) isConnected.value = false } } // Handle initialization data from parent const handleInitData = (data: any) => { // Get app settings from parent or use defaults const settings: AppSettings = globalAccountingClient?.getAppSettings() || getDefaultAppSettings() // Merge custom data from parent settings.customActions = data.customActions || [] settings.customEntities = data.customEntities || [] settings.customTabs = data.customTabs || {} settings.customTables = data.customTables || [] setAppSettings(settings) console.log('App settings updated:', settings) // Initialize event listeners after client is ready initializeEventListeners() } // Initialize event listeners (similar to Angular service) const initializeEventListeners = () => { if (!globalAccountingClient || !isInIframe.value || isInitialized.value) { return } isInitialized.value = true let firstNavigation = true // Track navigation changes and send to parent router.afterEach((to) => { // Skip first navigation to avoid redundant initial URL change if (!firstNavigation) { const url = to.fullPath // Skip certain URLs that shouldn't be sent to parent if (shouldSkipUrlChange(url)) { return } globalAccountingClient!.sendUrlChange(url) } else { firstNavigation = false } }) // Track pending HTTP requests (if service provided) if (httpStatusService?.hasPendingRequest) { // Assuming it's an observable-like object httpStatusService.hasPendingRequest().subscribe?.((pending: boolean) => { if (globalAccountingClient && isInIframe.value) { globalAccountingClient.sendPendingRequest(pending) } }) } // Track storage changes and sync to parent if (storageService.dataUpdated$?.subscribe) { storageService.dataUpdated$.subscribe(({ key, value }: { key: string; value: any }) => { console.log('Storage updated:', key, value) // Skip sensitive data that shouldn't be synced if (shouldSkipStorageSync(key)) { return } if (globalAccountingClient && isInIframe.value) { globalAccountingClient.sendDataChanged(key, JSON.stringify(value)) } }) } // Track fast action callbacks watch(fastActionCallback, (callback) => { if (callback && globalAccountingClient && isInIframe.value) { globalAccountingClient.sendFastActionCallback(callback) } }) // Track user notifications watch(userNotifications, (notifications) => { if (notifications && globalAccountingClient && isInIframe.value) { globalAccountingClient.sendUserNotifications(notifications) } }) // Initialize user-specific data initUserSpecificData() // Signal to parent that child is ready setInitialized(true) } // Handle URL change from parent const handleUrlChange = (url: string) => { if (!url) { router.push('/') return } if (url.includes('?')) { // Handle URLs with query parameters const [path, queryString] = url.split('?') const queryParams = new URLSearchParams(queryString) const query: any = {} queryParams.forEach((value, key) => { query[key] = value }) router.push({ path, query }) } else { router.push(url) } // Update iframe size after navigation updateIframeSize() } // Handle redirect from parent const handleRedirect = (route: string[]) => { // Clear cache before redirect handleClearCache(null) // Navigate with redirect pattern to ensure clean state router.push('/redirect').then(() => { router.push(route.join('/')) }).catch(console.error) } // Handle action from parent const handleAction = (data: any) => { if (data.action) { fastAction.value = data } } // Handle cache clear from parent const handleClearCache = (data: any) => { console.log('Clearing application caches...', data) // Clear any caches your app uses clearApplicationCaches() } // Handle storage clear from parent const handleClearStorage = (request: ClearStorageRequest) => { if (request.dataTables) { // Clear DataTables storage for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i) if (key && key.startsWith('DataTables_')) { localStorage.removeItem(key) } } } if (request.everything) { // Clear all local storage (be careful with this) localStorage.clear() } // Add your own storage clearing logic here clearApplicationStorage(request) } // Handle logout from parent const handleLogout = () => { localStorage.removeItem('idToken') localStorage.removeItem('accessToken') // Clear any other authentication data clearAuthenticationData() // Navigate to login or home router.push('/') } // Public API methods const setAppSettings = (settings: AppSettings) => { if (!settings) return appSettings.value = settings } const setFastAction = (action: FastAction) => { if (action) { console.log('Setting fast action:', action) fastAction.value = action } } const setFastActionCallback = (callback: FastActionCallbackResponse) => { if (callback) { console.log('Setting fast action callback:', callback) fastActionCallback.value = callback } } const setParentData = (data: ParentDataResponse) => { if (data) { console.log('Setting parent data:', data) parentData.value = data } } const setUserNotifications = (notifications: any) => { if (notifications) { userNotifications.value = notifications } } const setInitialized = (initialized: boolean) => { console.log('Setting initialized state:', initialized) if (globalAccountingClient) { globalAccountingClient.sendInited({ inited: initialized, version: getAppVersion() }) } } // Update iframe size after content changes const updateIframeSize = () => { if (!isInIframe.value) return // Initial size update setTimeout(() => { const height = document.body.scrollHeight setIframeSize(height) }) // Secondary update for dynamic content setTimeout(() => { const newHeight = document.body.scrollHeight setIframeSize(newHeight) }, 400) } const setIframeSize = (height: number) => { if (globalAccountingClient && isInIframe.value) { globalAccountingClient.sendHeightChange(height) } } // Send dialog state changes to parent const sendDialogState = (isOpen: boolean) => { if (globalAccountingClient && isInIframe.value) { globalAccountingClient.sendDialogChanged(isOpen) } } // Send fast actions to parent const sendFastActions = (actions: FastAction[]) => { if (globalAccountingClient && isInIframe.value) { globalAccountingClient.sendFastActions(actions) } } // Send pending request state to parent const sendPendingRequest = (pending: boolean) => { if (globalAccountingClient && isInIframe.value) { globalAccountingClient.sendPendingRequest(pending) } } // Get client instance const getChildClient = () => globalAccountingClient const getVueAdapter = () => globalVueAdapter // Computed properties for reactive state const clientStatus = computed(() => ({ isInitialized: !!globalAccountingClient, isConnected: isConnected.value, isInIframe: isInIframe.value })) // Enhanced cleanup function const cleanup = () => { console.log('Cleaning up useChildClient') // Clean up child client if (globalAccountingClient) { globalAccountingClient.destroy() globalAccountingClient = null } if (globalVueAdapter) { globalVueAdapter.destroy() globalVueAdapter = null } // Reset reactive state isConnected.value = false } // Helper functions - override these in your implementation const getDefaultAppSettings = (): AppSettings => { return { customActions: [], customEntities: [], customTabs: {}, customTables: [] } } const getAppVersion = (): string => { return '1.0.0' // Replace with your app version } const shouldSkipUrlChange = (url: string): boolean => { // Skip URLs that shouldn't be reported to parent const skipUrls = ['/redirect', '/not-authenticated', '/empty', '/login'] return skipUrls.some(skipUrl => url.includes(skipUrl)) } const shouldSkipStorageSync = (key: string): boolean => { // Skip sensitive data that shouldn't be synced to parent const skipKeys = ['idToken', 'accessToken', 'refreshToken'] return skipKeys.includes(key) } const clearApplicationCaches = () => { // Implement your app-specific cache clearing logic console.log('Clearing application-specific caches...') } const clearApplicationStorage = (request: ClearStorageRequest) => { // Implement your app-specific storage clearing logic console.log('Clearing application-specific storage...', request) } const clearAuthenticationData = () => { // Clear your app-specific authentication data console.log('Clearing authentication data...') } const initUserSpecificData = () => { // Initialize user-specific data after client is ready // Example: Load user notifications, fast actions, etc. setTimeout(() => { loadUserNotifications() loadFastActions() }, 500) } const loadUserNotifications = () => { // Load and send user notifications if applicable console.log('Loading user notifications...') } const loadFastActions = () => { // Load and send fast actions if applicable console.log('Loading fast actions...') // Example: // getFastActions().then(actions => { // fastActions.value = actions // sendFastActions(actions) // }) } // Lifecycle hooks onMounted(() => { initializeChildClient() }) onUnmounted(() => { cleanup() }) return { // State isConnected: computed(() => isConnected.value), isInIframe: computed(() => isInIframe.value), clientStatus, appSettings: computed(() => appSettings.value), fastAction: computed(() => fastAction.value), fastActionCallback: computed(() => fastActionCallback.value), parentData: computed(() => parentData.value), customEntities: computed(() => customEntities.value), userNotifications: computed(() => userNotifications.value), fastActions: computed(() => fastActions.value), // Methods initializeChildClient, getChildClient, getVueAdapter, cleanup, // Public API setAppSettings, setFastAction, setFastActionCallback, setParentData, setUserNotifications, updateIframeSize, sendDialogState, sendFastActions, sendPendingRequest } } // Usage example in your Vue app: /* // In your main.ts import { createApp } from 'vue' import App from './App.vue' import { setupGlobalChildClient } from './composables/useChildClient' const app = createApp(App) // Setup global child client detection setupGlobalChildClient() app.mount('#app') // In your main App.vue component <template> <div class="app"> <div v-if="!isConnected && isInIframe" class="loading"> Connecting to parent application... </div> <div v-if="isConnected || !isInIframe"> <!-- Your app content --> <router-view /> </div> <!-- Debug info (remove in production) --> <div v-if="clientStatus" class="debug-info"> Client Status: {{ clientStatus }} </div> </div> </template> <script setup> import { useChildClient } from './composables/useChildClient' import { storageService, httpStatusService } from './services' const { isConnected, isInIframe, clientStatus, appSettings, fastAction, setFastActionCallback, updateIframeSize, sendDialogState } = useChildClient(storageService, httpStatusService) // Listen for app settings changes watch(appSettings, (settings) => { if (settings) { console.log('App settings updated:', settings) // Update your app based on settings } }) // Listen for fast actions watch(fastAction, (action) => { if (action) { console.log('Fast action received:', action) // Handle the action in your app handleFastAction(action) } }) // Example: Handle fast action const handleFastAction = (action) => { switch (action.type) { case 'refresh': // Refresh data break case 'save': // Save current state break default: console.log('Unknown action:', action) } // Send callback to parent setFastActionCallback({ success: true, data: { message: 'Action completed successfully' } }) } // Example: Track content changes const onContentChanged = () => { // Update iframe size when content changes updateIframeSize() } // Example: Track modal/dialog state const onDialogOpen = () => { sendDialogState(true) } const onDialogClose = () => { sendDialogState(false) } </script> // In a component that needs to interact with parent <template> <div> <button @click="sendMessageToParent">Send Message</button> <button @click="notifyContentChange">Content Changed</button> </div> </template> <script setup> import { useChildClient } from '@/composables/useChildClient' const { setFastActionCallback, updateIframeSize, isInIframe } = useChildClient( storageService ) const sendMessageToParent = () => { if (isInIframe.value) { setFastActionCallback({ success: true, data: { message: 'Hello from Vue child!' } }) } } const notifyContentChange = () => { updateIframeSize() } </script> // Advanced usage with custom services export class CustomStorageService implements StorageService { dataUpdated$ = reactive({ subscribe: (callback) => { ... } }) // Your storage implementation } // Usage with custom services const customStorageService = new CustomStorageService() const { ... } = useChildClient(customStorageService) */