| 1 | <template> |
| 2 | <div ref="container" /> |
| 3 | </template> |
| 4 | |
| 5 | <script setup lang="ts"> |
| 6 | // Vue wrapper around `<AppDetailDrawer>` (a React component from |
| 7 | // `@shuffleio/shuffle-mcps`). Same manual `react-dom/client` mount |
| 8 | // pattern as ShuffleMCPEmbed / TryMcpEmbed. |
| 9 | // |
| 10 | // AppDetailDrawer is the all-in-one panel for a single app: header |
| 11 | // + auth card (inline form for API-key apps, redirect handoff for |
| 12 | // OAuth apps) + MCP chat + Singul actions playground. We use it after |
| 13 | // picking an app from <ShuffleMCP preventDefault> so the user lands |
| 14 | // on a CoPilot-orchestrated drawer instead of a top-level redirect. |
| 15 | |
| 16 | import type { Root } from "react-dom/client" |
| 17 | import { API_CONFIG, AppDetailDrawer } from "@shuffleio/shuffle-mcps" |
| 18 | import { storeToRefs } from "pinia" |
| 19 | import { createElement } from "react" |
| 20 | import { createRoot } from "react-dom/client" |
| 21 | import { onBeforeUnmount, onMounted, ref, watch } from "vue" |
| 22 | import { useThemeStore } from "@/stores/theme" |
| 23 | import { fetchShuffleConnectorCredentials } from "@/utils/shuffle/shuffleConnectorCredentials" |
| 24 | import { MuiProvider } from "@/utils/shuffle/shuffleMuiTheme" |
| 25 | |
| 26 | interface Props { |
| 27 | appName: string | null |
| 28 | width?: number |
| 29 | anchor?: "left" | "right" |
| 30 | } |
| 31 | |
| 32 | const props = withDefaults(defineProps<Props>(), { |
| 33 | width: 720, |
| 34 | anchor: "right" |
| 35 | }) |
| 36 | |
| 37 | const emit = defineEmits<{ |
| 38 | (e: "refresh"): void |
| 39 | }>() |
| 40 | |
| 41 | const show = defineModel<boolean>("show", { required: true, default: false }) |
| 42 | |
| 43 | const container = ref<HTMLElement | null>(null) |
| 44 | let root: Root | null = null |
| 45 | |
| 46 | const themeStore = useThemeStore() |
| 47 | const { isThemeDark } = storeToRefs(themeStore) |
| 48 | |
| 49 | function render() { |
| 50 | if (!root) return |
| 51 | root.render( |
| 52 | createElement( |
| 53 | MuiProvider as never, |
| 54 | { isDark: isThemeDark.value }, |
| 55 | createElement(AppDetailDrawer as never, { |
| 56 | open: show.value, |
| 57 | onClose: () => (show.value = false), |
| 58 | appName: props.appName, |
| 59 | anchor: props.anchor, |
| 60 | width: props.width, |
| 61 | onRefresh: () => emit("refresh") |
| 62 | }) |
| 63 | ) |
| 64 | ) |
| 65 | } |
| 66 | |
| 67 | watch([show, () => props.appName, () => props.width, () => props.anchor, isThemeDark], () => render()) |
| 68 | |
| 69 | onBeforeUnmount(() => { |
| 70 | if (root) { |
| 71 | root.unmount() |
| 72 | root = null |
| 73 | } |
| 74 | }) |
| 75 | |
| 76 | onMounted(async () => { |
| 77 | if (!container.value) return |
| 78 | root = createRoot(container.value) |
| 79 | // Same gating as the other embeds — wait for connector creds before |
| 80 | // the first render so the drawer's internal fetches go through the |
| 81 | // proxy with the right Bearer token. |
| 82 | const creds = await fetchShuffleConnectorCredentials() |
| 83 | if (creds?.api_key) { |
| 84 | API_CONFIG.setApiKey(creds.api_key) |
| 85 | } |
| 86 | render() |
| 87 | }) |
| 88 | </script> |