From 2d8ede51c20289540e0da1a29f8dc3e6f540f7d6 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 29 Apr 2026 11:44:31 +0200 Subject: [PATCH 1/2] feat(preview): add channel preview support --- src/components/BundlePreviewFrame.vue | 17 +- .../app/[app].channel.[channel].preview.vue | 195 ++++++++++++++++++ src/pages/app/[app].channel.[channel].vue | 15 ++ supabase/functions/_backend/files/preview.ts | 105 ++++++++-- .../functions/shared/preview-subdomain.ts | 59 +++++- tests/preview-response-headers.unit.test.ts | 29 +++ tests/preview-subdomain.unit.test.ts | 24 ++- 7 files changed, 414 insertions(+), 30 deletions(-) create mode 100644 src/pages/app/[app].channel.[channel].preview.vue create mode 100644 tests/preview-response-headers.unit.test.ts diff --git a/src/components/BundlePreviewFrame.vue b/src/components/BundlePreviewFrame.vue index 66c1d1ebe0..e35e5cb152 100644 --- a/src/components/BundlePreviewFrame.vue +++ b/src/components/BundlePreviewFrame.vue @@ -4,11 +4,12 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import IconExternalLink from '~icons/lucide/external-link' import IconSmartphone from '~icons/lucide/smartphone' -import { buildPreviewSubdomain } from '../../shared/preview-subdomain.ts' +import { buildChannelPreviewSubdomain, buildPreviewSubdomain } from '../../shared/preview-subdomain.ts' const props = defineProps<{ appId: string - versionId: number + versionId?: number + channelId?: number }>() const { t } = useI18n() @@ -53,7 +54,17 @@ const currentDevice = computed(() => devices[selectedDevice.value]) // Build the preview URL using a reversible preview subdomain format. const previewUrl = computed(() => { try { - const subdomain = buildPreviewSubdomain(props.appId, props.versionId) + const hasVersionId = typeof props.versionId === 'number' + const hasChannelId = typeof props.channelId === 'number' + + if (hasVersionId === hasChannelId) { + console.error('BundlePreviewFrame requires exactly one preview target') + return null + } + + const subdomain = hasChannelId + ? buildChannelPreviewSubdomain(props.appId, props.channelId as number) + : buildPreviewSubdomain(props.appId, props.versionId as number) // Extract base domain from current host, default to capgo.app for localhost // Preserve environment segments (e.g., 'dev' in console.dev.capgo.app) const hostname = window.location.hostname diff --git a/src/pages/app/[app].channel.[channel].preview.vue b/src/pages/app/[app].channel.[channel].preview.vue new file mode 100644 index 0000000000..d4cfe9c08e --- /dev/null +++ b/src/pages/app/[app].channel.[channel].preview.vue @@ -0,0 +1,195 @@ + + + + + +meta: + layout: app + diff --git a/src/pages/app/[app].channel.[channel].vue b/src/pages/app/[app].channel.[channel].vue index 62c93ef1e8..c6100916b1 100644 --- a/src/pages/app/[app].channel.[channel].vue +++ b/src/pages/app/[app].channel.[channel].vue @@ -10,6 +10,7 @@ import { toast } from 'vue-sonner' import IconCopy from '~icons/heroicons/clipboard-document-check' import IconCode from '~icons/heroicons/code-bracket' import Settings from '~icons/heroicons/cog-8-tooth' +import IconEye from '~icons/heroicons/eye' import IconInformation from '~icons/heroicons/information-circle' import IconSearch from '~icons/ic/round-search?raw' import IconAlertCircle from '~icons/lucide/alert-circle' @@ -83,6 +84,12 @@ function openBundle() { router.push(`/app/${route.params.app}/bundle/${channel.value.version.id}`) } +function openPreview() { + if (!channel.value) + return + router.push(`/app/${route.params.app}/channel/${id.value}/preview`) +} + async function getChannel(force = false) { if (!id.value) return @@ -669,6 +676,14 @@ async function copyCurlCommand() {
{{ channel.version.name }} +
+
+ +

+ {{ t('app-not-found') }} +

+

+ {{ t('app-not-found-description') }} +

+ +
+

diff --git a/src/pages/app/[app].channel.[channel].vue b/src/pages/app/[app].channel.[channel].vue index c6100916b1..b3d4e0b4df 100644 --- a/src/pages/app/[app].channel.[channel].vue +++ b/src/pages/app/[app].channel.[channel].vue @@ -678,11 +678,12 @@ async function copyCurlCommand() { {{ channel.version.name }}