main
vue 107 lines 3.56 KB
Raw
1 <template>
2 <n-drawer v-model:show="show" :width="720" placement="right" display-directive="show">
3 <n-drawer-content closable>
4 <template #header>
5 <div class="flex items-center gap-2">
6 <Icon name="carbon:integration" :size="18" />
7 <span>Shuffle apps</span>
8 <span v-if="integration" class="text-secondary"></span>
9 <span v-if="integration" class="text-secondary">{{ integration.display_name }}</span>
10 </div>
11 </template>
12
13 <div class="flex flex-col gap-3">
14 <div class="text-secondary text-sm">
15 Browse Shuffle's catalog of 3,000+ integrations and authenticate the ones this org needs. Apps you
16 authenticate here become available in the route form's app picker — Shuffle handles the OAuth flow;
17 CoPilot just stores the resulting Org-Id and references it in routes.
18 </div>
19
20 <div v-if="loadingToken" class="text-tertiary py-6 text-center text-sm">
21 Fetching org auth token from Shuffle…
22 </div>
23
24 <div v-else-if="error" class="text-error py-6 text-center text-sm">
25 {{ error }}
26 </div>
27
28 <ShuffleMCPEmbed
29 v-else-if="orgAuthToken"
30 :auth-token="orgAuthToken"
31 placeholder="Find a Shuffle app to authenticate…"
32 inline
33 layout="grid"
34 :grid-columns="3"
35 prevent-default
36 />
37 </div>
38 </n-drawer-content>
39 </n-drawer>
40 </template>
41
42 <script setup lang="ts">
43 import type { ApiError } from "@/types/common"
44 import type { ShuffleIntegration } from "@/types/notifications.d"
45 import { NDrawer, NDrawerContent } from "naive-ui"
46 import { ref, watch } from "vue"
47 import Api from "@/api"
48 import Icon from "@/components/common/Icon.vue"
49 import ShuffleMCPEmbed from "@/components/shuffle/ShuffleMCPEmbed.vue"
50 import { getApiErrorMessage } from "@/utils"
51
52 // Per-integration "Manage apps" drawer. Opened from the Shuffle
53 // integration list when an admin wants to authenticate a new Shuffle
54 // app for that customer's org. The actual auth UI is the embedded
55 // ShuffleMCP picker; this drawer is responsible for:
56 // 1. Fetching the org auth token via Shuffle's existing connector
57 // endpoint (already exposed on CoPilot's backend)
58 // 2. Passing the token into the embed
59 // 3. Surfacing a confirmation message when the admin picks an app
60
61 const props = defineProps<{
62 integration: ShuffleIntegration | null
63 }>()
64
65 const show = defineModel<boolean>("show", { required: true, default: false })
66
67 const orgAuthToken = ref<string | null>(null)
68 const loadingToken = ref(false)
69 const error = ref<string | null>(null)
70
71 async function loadOrgToken(orgId: string) {
72 if (orgAuthToken.value) return
73
74 loadingToken.value = true
75 error.value = null
76
77 try {
78 const res = await Api.shuffle.getOrganization(orgId)
79 if (res.data.success) {
80 orgAuthToken.value = res.data.data?.org_auth?.token ?? null
81 if (!orgAuthToken.value) {
82 error.value =
83 "Shuffle returned an org without an org_auth.token. Check the Shuffle connector config or contact Shuffle support."
84 }
85 } else {
86 error.value = res.data.message || "Failed to fetch org auth token"
87 }
88 } catch (err) {
89 error.value = getApiErrorMessage(err as ApiError) || "Failed to fetch org auth token"
90 } finally {
91 loadingToken.value = false
92 }
93 }
94
95 // Re-fetch the org token whenever the drawer opens or switches to a
96 // different integration. Cleared when closing so reopening on a
97 // different integration doesn't briefly flash stale data.
98 watch(
99 [show, () => props.integration?.id],
100 ([showValue, integrationId]) => {
101 if (showValue && integrationId && props.integration?.shuffle_org_id) {
102 loadOrgToken(props.integration.shuffle_org_id)
103 }
104 },
105 { immediate: true }
106 )
107 </script>