mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 13:32:11 +00:00
Compare commits
1 Commits
glary/fix-
...
feat/pylon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e52354b23a |
@@ -80,19 +80,23 @@ export class HelpCenterHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept the Zendesk support URL so it never actually loads in the
|
||||
* new tab opened by the Contact Support command.
|
||||
* Intercept the Pylon support URL (and the legacy Zendesk one for safety)
|
||||
* so it never actually loads in the new tab opened by the Contact Support
|
||||
* command.
|
||||
*/
|
||||
async stubSupportPage(): Promise<void> {
|
||||
await this.page
|
||||
.context()
|
||||
.route('https://support.comfy.org/**', (route: Route) =>
|
||||
for (const pattern of [
|
||||
'https://portal.usepylon.com/**',
|
||||
'https://support.comfy.org/**'
|
||||
]) {
|
||||
await this.page.context().route(pattern, (route: Route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/html',
|
||||
body: '<html></html>'
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -103,14 +103,14 @@ test.describe('Settings', () => {
|
||||
})
|
||||
|
||||
test.describe('Support', () => {
|
||||
test('Should open external zendesk link with OSS tag', async ({
|
||||
test('Should open Pylon question form with OSS environment tag', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
// Prevent loading the external page
|
||||
await comfyPage.page
|
||||
.context()
|
||||
.route('https://support.comfy.org/**', (route) =>
|
||||
.route('https://portal.usepylon.com/**', (route) =>
|
||||
route.fulfill({ body: '<html></html>', contentType: 'text/html' })
|
||||
)
|
||||
|
||||
@@ -119,8 +119,9 @@ test.describe('Support', () => {
|
||||
const popup = await popupPromise
|
||||
|
||||
const url = new URL(popup.url())
|
||||
expect(url.hostname).toBe('support.comfy.org')
|
||||
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
|
||||
expect(url.hostname).toBe('portal.usepylon.com')
|
||||
expect(url.pathname).toBe('/comfy-org/forms/question')
|
||||
expect(url.searchParams.get('comfy_environment')).toBe('oss')
|
||||
|
||||
await popup.close()
|
||||
})
|
||||
|
||||
@@ -122,9 +122,15 @@ test.describe('Error dialog', () => {
|
||||
await popup.close()
|
||||
})
|
||||
|
||||
test('Should open contact support when "Help Fix This" is clicked', async ({
|
||||
test('Should open the Pylon bug-report form when "Help Fix This" is clicked', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page
|
||||
.context()
|
||||
.route('https://portal.usepylon.com/**', (route) =>
|
||||
route.fulfill({ body: '<html></html>', contentType: 'text/html' })
|
||||
)
|
||||
|
||||
const errorDialog = await triggerConfigureError(comfyPage)
|
||||
await expect(errorDialog).toBeVisible()
|
||||
|
||||
@@ -133,7 +139,9 @@ test.describe('Error dialog', () => {
|
||||
)
|
||||
|
||||
const url = new URL(popup.url())
|
||||
expect(url.hostname).toBe('support.comfy.org')
|
||||
expect(url.hostname).toBe('portal.usepylon.com')
|
||||
expect(url.pathname).toBe('/comfy-org/forms/report-a-bug')
|
||||
expect(url.searchParams.get('product_area')).toBe('Workflow Error')
|
||||
|
||||
await popup.close()
|
||||
})
|
||||
|
||||
@@ -99,26 +99,28 @@ test.describe('Help Center', () => {
|
||||
expect(url.pathname).toBe('/Comfy-Org/ComfyUI')
|
||||
})
|
||||
|
||||
test('Help & Support item opens the Zendesk support form with OSS tag', async ({
|
||||
test('Help & Support item opens the Pylon question form tagged as OSS', async ({
|
||||
helpCenter
|
||||
}) => {
|
||||
const url = await waitForPopup(helpCenter.page, () =>
|
||||
helpCenter.menuItem('help').click()
|
||||
)
|
||||
|
||||
expect(url.hostname).toBe('support.comfy.org')
|
||||
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
|
||||
expect(url.hostname).toBe('portal.usepylon.com')
|
||||
expect(url.pathname).toBe('/comfy-org/forms/question')
|
||||
expect(url.searchParams.get('comfy_environment')).toBe('oss')
|
||||
})
|
||||
|
||||
test('Give Feedback item opens Contact Support in OSS mode', async ({
|
||||
test('Give Feedback item opens the Pylon question form in OSS mode', async ({
|
||||
helpCenter
|
||||
}) => {
|
||||
const url = await waitForPopup(helpCenter.page, () =>
|
||||
helpCenter.menuItem('feedback').click()
|
||||
)
|
||||
|
||||
expect(url.hostname).toBe('support.comfy.org')
|
||||
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
|
||||
expect(url.hostname).toBe('portal.usepylon.com')
|
||||
expect(url.pathname).toBe('/comfy-org/forms/question')
|
||||
expect(url.searchParams.get('comfy_environment')).toBe('oss')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -70,10 +70,11 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { SupportForm } from '@/platform/support/config'
|
||||
import { useSupportContext } from '@/platform/support/useSupportContext'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { generateErrorReport } from '@/utils/errorReportUtil'
|
||||
import type { ErrorReportData } from '@/utils/errorReportUtil'
|
||||
@@ -114,16 +115,18 @@ const title = computed<string>(
|
||||
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
|
||||
)
|
||||
|
||||
const { openSupport } = useSupportContext()
|
||||
|
||||
/**
|
||||
* Open contact support flow from error dialog and track telemetry.
|
||||
*/
|
||||
const showContactSupport = async () => {
|
||||
const showContactSupport = () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'help_feedback',
|
||||
is_external: true,
|
||||
source: 'error_dialog'
|
||||
})
|
||||
await useCommandStore().execute('Comfy.ContactSupport')
|
||||
openSupport(SupportForm.Bug, { productArea: 'Workflow Error' })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -118,9 +118,10 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { SupportForm } from '@/platform/support/config'
|
||||
import { useSupportContext } from '@/platform/support/useSupportContext'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
@@ -135,7 +136,7 @@ const { buildDocsUrl, docsPaths } = useExternalLink()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useAuthStore()
|
||||
const authActions = useAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const { openSupport } = useSupportContext()
|
||||
const telemetry = useTelemetry()
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
const loading = computed(() => authStore.loading)
|
||||
@@ -168,13 +169,13 @@ const handleCreditsHistoryClick = async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
}
|
||||
|
||||
const handleMessageSupport = async () => {
|
||||
const handleMessageSupport = () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'help_feedback',
|
||||
is_external: true,
|
||||
source: 'credits_panel'
|
||||
})
|
||||
await commandStore.execute('Comfy.ContactSupport')
|
||||
openSupport(SupportForm.Billing, { productArea: 'Credits' })
|
||||
}
|
||||
|
||||
const handleFaqClick = () => {
|
||||
|
||||
@@ -47,6 +47,13 @@ vi.mock('@/stores/commandStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
const mockOpenSupport = vi.fn()
|
||||
vi.mock('@/platform/support/useSupportContext', () => ({
|
||||
useSupportContext: vi.fn(() => ({
|
||||
openSupport: mockOpenSupport
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useExternalLink', () => ({
|
||||
useExternalLink: vi.fn(() => ({
|
||||
staticUrls: {
|
||||
@@ -297,7 +304,7 @@ describe('ErrorNodeCard.vue', () => {
|
||||
openSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('executes ContactSupport command when Get Help button is clicked', async () => {
|
||||
it('opens the Pylon bug-report form when Get Help button is clicked', async () => {
|
||||
const { user } = renderCard(makeRuntimeErrorCard())
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -308,7 +315,9 @@ describe('ErrorNodeCard.vue', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Get Help/ }))
|
||||
|
||||
expect(mockExecuteCommand).toHaveBeenCalledWith('Comfy.ContactSupport')
|
||||
expect(mockOpenSupport).toHaveBeenCalledWith('report-a-bug', {
|
||||
productArea: 'Workflow Error'
|
||||
})
|
||||
expect(mockTrackHelpResourceClicked).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
resource_type: 'help_feedback',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useErrorActions } from './useErrorActions'
|
||||
const mocks = vi.hoisted(() => ({
|
||||
trackUiButtonClicked: vi.fn(),
|
||||
trackHelpResourceClicked: vi.fn(),
|
||||
execute: vi.fn(),
|
||||
openSupport: vi.fn(),
|
||||
telemetry: null as {
|
||||
trackUiButtonClicked: ReturnType<typeof vi.fn>
|
||||
trackHelpResourceClicked: ReturnType<typeof vi.fn>
|
||||
@@ -15,9 +15,9 @@ const mocks = vi.hoisted(() => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => ({
|
||||
execute: mocks.execute
|
||||
vi.mock('@/platform/support/useSupportContext', () => ({
|
||||
useSupportContext: () => ({
|
||||
openSupport: mocks.openSupport
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('useErrorActions', () => {
|
||||
}
|
||||
mocks.trackUiButtonClicked.mockReset()
|
||||
mocks.trackHelpResourceClicked.mockReset()
|
||||
mocks.execute.mockReset()
|
||||
mocks.openSupport.mockReset()
|
||||
windowOpenSpy = vi
|
||||
.spyOn(window, 'open')
|
||||
.mockImplementation(() => null as unknown as Window)
|
||||
@@ -83,36 +83,31 @@ describe('useErrorActions', () => {
|
||||
})
|
||||
|
||||
describe('contactSupport', () => {
|
||||
it('tracks the help resource click and executes the contact support command', () => {
|
||||
mocks.execute.mockReturnValue('executed')
|
||||
it('tracks the help resource click and opens the Pylon bug-report form', () => {
|
||||
const { contactSupport } = useErrorActions()
|
||||
|
||||
const result = contactSupport()
|
||||
contactSupport()
|
||||
|
||||
expect(mocks.trackHelpResourceClicked).toHaveBeenCalledWith({
|
||||
resource_type: 'help_feedback',
|
||||
is_external: true,
|
||||
source: 'error_dialog'
|
||||
})
|
||||
expect(mocks.execute).toHaveBeenCalledWith('Comfy.ContactSupport')
|
||||
expect(result).toBe('executed')
|
||||
expect(mocks.openSupport).toHaveBeenCalledWith('report-a-bug', {
|
||||
productArea: 'Workflow Error'
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the execute promise when the command is async', async () => {
|
||||
mocks.execute.mockResolvedValue('done')
|
||||
const { contactSupport } = useErrorActions()
|
||||
|
||||
await expect(contactSupport()).resolves.toBe('done')
|
||||
})
|
||||
|
||||
it('still executes the command when telemetry is unavailable', () => {
|
||||
it('still opens the support form when telemetry is unavailable', () => {
|
||||
mocks.telemetry = null
|
||||
const { contactSupport } = useErrorActions()
|
||||
|
||||
void contactSupport()
|
||||
contactSupport()
|
||||
|
||||
expect(mocks.trackHelpResourceClicked).not.toHaveBeenCalled()
|
||||
expect(mocks.execute).toHaveBeenCalledWith('Comfy.ContactSupport')
|
||||
expect(mocks.openSupport).toHaveBeenCalledWith('report-a-bug', {
|
||||
productArea: 'Workflow Error'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { SupportForm } from '@/platform/support/config'
|
||||
import { useSupportContext } from '@/platform/support/useSupportContext'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
|
||||
export function useErrorActions() {
|
||||
const telemetry = useTelemetry()
|
||||
const commandStore = useCommandStore()
|
||||
const { openSupport } = useSupportContext()
|
||||
const { staticUrls } = useExternalLink()
|
||||
|
||||
function openGitHubIssues() {
|
||||
@@ -20,7 +21,7 @@ export function useErrorActions() {
|
||||
is_external: true,
|
||||
source: 'error_dialog'
|
||||
})
|
||||
return commandStore.execute('Comfy.ContactSupport')
|
||||
openSupport(SupportForm.Bug, { productArea: 'Workflow Error' })
|
||||
}
|
||||
|
||||
function findOnGitHub(errorMessage: string) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useSubgraphOperations } from '@/composables/graph/useSubgraphOperations'
|
||||
@@ -22,7 +21,8 @@ import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
||||
import { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { buildSupportUrl } from '@/platform/support/config'
|
||||
import { SupportForm } from '@/platform/support/config'
|
||||
import { useSupportContext } from '@/platform/support/useSupportContext'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { ExecutionTriggerSource } from '@/platform/telemetry/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
@@ -862,12 +862,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Contact Support',
|
||||
versionAdded: '1.17.8',
|
||||
function: () => {
|
||||
const { userEmail, resolvedUserInfo } = useCurrentUser()
|
||||
const supportUrl = buildSupportUrl({
|
||||
userEmail: userEmail.value,
|
||||
userId: resolvedUserInfo.value?.id
|
||||
})
|
||||
window.open(supportUrl, '_blank', 'noopener,noreferrer')
|
||||
useSupportContext().openSupport(SupportForm.Question)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<p class="mb-5 text-center text-sm text-gray-600">
|
||||
{{ $t('cloudOnboarding.authTimeout.helpText') }}
|
||||
<a
|
||||
href="https://support.comfy.org"
|
||||
:href="supportUrl"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -75,6 +75,7 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { SupportForm, buildSupportUrl } from '@/platform/support/config'
|
||||
|
||||
interface Props {
|
||||
errorMessage?: string
|
||||
@@ -86,6 +87,10 @@ const router = useRouter()
|
||||
const { logout } = useAuthActions()
|
||||
const showTechnicalDetails = ref(false)
|
||||
|
||||
const supportUrl = buildSupportUrl(SupportForm.Question, {
|
||||
productArea: 'Cloud Onboarding'
|
||||
})
|
||||
|
||||
const handleRestart = async () => {
|
||||
await logout()
|
||||
await router.replace({ name: 'cloud-login' })
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
{{ t('cloudWaitlist_questionsText') }}
|
||||
<a
|
||||
href="https://support.comfy.org"
|
||||
:href="supportUrl"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -144,6 +144,7 @@ import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useFreeTierOnboarding } from '@/platform/cloud/onboarding/composables/useFreeTierOnboarding'
|
||||
import { getSafePreviousFullPath } from '@/platform/cloud/onboarding/utils/previousFullPath'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { SupportForm, buildSupportUrl } from '@/platform/support/config'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { SignUpData } from '@/schemas/signInSchema'
|
||||
@@ -168,6 +169,10 @@ const {
|
||||
} = useFreeTierOnboarding()
|
||||
const googleSsoBlockedReason = getGoogleSsoBlockedReason()
|
||||
|
||||
const supportUrl = buildSupportUrl(SupportForm.Question, {
|
||||
productArea: 'Cloud Onboarding'
|
||||
})
|
||||
|
||||
const navigateToLogin = async () => {
|
||||
await router.push({ name: 'cloud-login', query: route.query })
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{{ t('auth.login.privacyLink') }}
|
||||
</a>
|
||||
<a
|
||||
href="https://support.comfy.org"
|
||||
:href="supportUrl"
|
||||
class="cursor-pointer text-sm text-gray-600 no-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -28,5 +28,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { SupportForm, buildSupportUrl } from '@/platform/support/config'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const supportUrl = buildSupportUrl(SupportForm.Question, {
|
||||
productArea: 'Cloud Onboarding'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -155,8 +155,9 @@ import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeB
|
||||
import SubscriptionBenefits from '@/platform/cloud/subscription/components/SubscriptionBenefits.vue'
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { SupportForm } from '@/platform/support/config'
|
||||
import { useSupportContext } from '@/platform/support/useSupportContext'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import type { SubscriptionDialogReason } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
|
||||
const { onClose, reason, onChooseTeam } = defineProps<{
|
||||
@@ -184,7 +185,7 @@ const formattedMonthlyPrice = new Intl.NumberFormat(
|
||||
maximumFractionDigits: 0
|
||||
}
|
||||
).format(MONTHLY_SUBSCRIPTION_PRICE)
|
||||
const commandStore = useCommandStore()
|
||||
const { openSupport } = useSupportContext()
|
||||
const telemetry = useTelemetry()
|
||||
|
||||
// Always show custom pricing table for cloud subscriptions
|
||||
@@ -215,13 +216,13 @@ const handleClose = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleContactUs = async () => {
|
||||
const handleContactUs = () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'help_feedback',
|
||||
is_external: true,
|
||||
source: 'subscription'
|
||||
})
|
||||
await commandStore.execute('Comfy.ContactSupport')
|
||||
openSupport(SupportForm.Billing, { productArea: 'Billing' })
|
||||
}
|
||||
|
||||
const handleViewEnterprise = () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useSubscriptionActions } from '@/platform/cloud/subscription/composable
|
||||
const mockFetchBalance = vi.fn()
|
||||
const mockFetchStatus = vi.fn()
|
||||
const mockShowTopUpCreditsDialog = vi.fn()
|
||||
const mockExecute = vi.fn()
|
||||
const mockOpenSupport = vi.fn()
|
||||
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: () => ({
|
||||
@@ -32,9 +32,9 @@ vi.mock('@/services/dialogService', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => ({
|
||||
execute: mockExecute
|
||||
vi.mock('@/platform/support/useSupportContext', () => ({
|
||||
useSupportContext: () => ({
|
||||
openSupport: mockOpenSupport
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -59,26 +59,28 @@ describe('useSubscriptionActions', () => {
|
||||
})
|
||||
|
||||
describe('handleMessageSupport', () => {
|
||||
it('should execute support command and manage loading state', async () => {
|
||||
it('opens the Pylon billing form and resets loading state', () => {
|
||||
const { handleMessageSupport, isLoadingSupport } =
|
||||
useSubscriptionActions()
|
||||
|
||||
expect(isLoadingSupport.value).toBe(false)
|
||||
|
||||
const promise = handleMessageSupport()
|
||||
expect(isLoadingSupport.value).toBe(true)
|
||||
handleMessageSupport()
|
||||
|
||||
await promise
|
||||
expect(mockExecute).toHaveBeenCalledWith('Comfy.ContactSupport')
|
||||
expect(mockOpenSupport).toHaveBeenCalledWith('billing-refund-issue', {
|
||||
productArea: 'Billing'
|
||||
})
|
||||
expect(isLoadingSupport.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
mockExecute.mockRejectedValueOnce(new Error('Command failed'))
|
||||
it('handles errors gracefully', () => {
|
||||
mockOpenSupport.mockImplementationOnce(() => {
|
||||
throw new Error('open failed')
|
||||
})
|
||||
const { handleMessageSupport, isLoadingSupport } =
|
||||
useSubscriptionActions()
|
||||
|
||||
await handleMessageSupport()
|
||||
handleMessageSupport()
|
||||
expect(isLoadingSupport.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,9 +3,10 @@ import { onMounted, ref } from 'vue'
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { SupportForm } from '@/platform/support/config'
|
||||
import { useSupportContext } from '@/platform/support/useSupportContext'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
/**
|
||||
* Composable for handling subscription panel actions and loading states
|
||||
@@ -13,7 +14,7 @@ import { useCommandStore } from '@/stores/commandStore'
|
||||
export function useSubscriptionActions() {
|
||||
const dialogService = useDialogService()
|
||||
const authActions = useAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const { openSupport } = useSupportContext()
|
||||
const telemetry = useTelemetry()
|
||||
const { fetchStatus } = useBillingContext()
|
||||
|
||||
@@ -27,7 +28,7 @@ export function useSubscriptionActions() {
|
||||
void dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
const handleMessageSupport = async () => {
|
||||
const handleMessageSupport = () => {
|
||||
try {
|
||||
isLoadingSupport.value = true
|
||||
if (isCloud) {
|
||||
@@ -37,7 +38,7 @@ export function useSubscriptionActions() {
|
||||
source: 'subscription'
|
||||
})
|
||||
}
|
||||
await commandStore.execute('Comfy.ContactSupport')
|
||||
openSupport(SupportForm.Billing, { productArea: 'Billing' })
|
||||
} catch (error) {
|
||||
console.error('[useSubscriptionActions] Error contacting support:', error)
|
||||
} finally {
|
||||
|
||||
@@ -50,3 +50,184 @@ describe('buildFeedbackTypeformUrl', () => {
|
||||
expect(url.hash).toBe('#distribution=ccloud&source=topbar')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSupportUrl', () => {
|
||||
const ORIGINAL_UA = navigator.userAgent
|
||||
|
||||
beforeEach(() => {
|
||||
distribution.isCloud = false
|
||||
distribution.isNightly = false
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value: ORIGINAL_UA,
|
||||
configurable: true
|
||||
})
|
||||
})
|
||||
|
||||
function setUserAgent(value: string) {
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value,
|
||||
configurable: true
|
||||
})
|
||||
}
|
||||
|
||||
async function importModule() {
|
||||
vi.resetModules()
|
||||
return import('./config')
|
||||
}
|
||||
|
||||
it('defaults to the question form when no form is provided', async () => {
|
||||
const { buildSupportUrl } = await importModule()
|
||||
const url = new URL(buildSupportUrl())
|
||||
expect(url.hostname).toBe('portal.usepylon.com')
|
||||
expect(url.pathname).toBe('/comfy-org/forms/question')
|
||||
})
|
||||
|
||||
it('routes to the requested form slug', async () => {
|
||||
const { buildSupportUrl, SupportForm } = await importModule()
|
||||
const url = new URL(buildSupportUrl(SupportForm.Billing))
|
||||
expect(url.pathname).toBe('/comfy-org/forms/billing-refund-issue')
|
||||
})
|
||||
|
||||
it('encodes spaces as %20 (not "+") in the query string', async () => {
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/131.0.0.0'
|
||||
)
|
||||
const { buildSupportUrl, SupportForm } = await importModule()
|
||||
const raw = buildSupportUrl(SupportForm.Bug, {
|
||||
userEmail: 'user@example.com',
|
||||
os: 'macOS 14.5'
|
||||
})
|
||||
expect(raw).toContain('comfy_os=macOS%2014.5')
|
||||
expect(raw).not.toContain('+')
|
||||
})
|
||||
|
||||
it('omits fields with empty or null values', async () => {
|
||||
const { buildSupportUrl, SupportForm } = await importModule()
|
||||
const url = new URL(
|
||||
buildSupportUrl(SupportForm.Question, {
|
||||
userEmail: '',
|
||||
userId: null,
|
||||
os: undefined,
|
||||
version: '1.45.0'
|
||||
})
|
||||
)
|
||||
expect(url.searchParams.has('email')).toBe(false)
|
||||
expect(url.searchParams.has('comfy_cloud_user_id')).toBe(false)
|
||||
expect(url.searchParams.has('comfy_os')).toBe(false)
|
||||
expect(url.searchParams.get('comfy_version')).toBe('1.45.0')
|
||||
})
|
||||
|
||||
it('tags Cloud builds with comfy_environment=ccloud', async () => {
|
||||
distribution.isCloud = true
|
||||
const { buildSupportUrl } = await importModule()
|
||||
const url = new URL(buildSupportUrl())
|
||||
expect(url.searchParams.get('comfy_environment')).toBe('ccloud')
|
||||
})
|
||||
|
||||
it('tags Nightly builds with comfy_environment=oss-nightly', async () => {
|
||||
distribution.isNightly = true
|
||||
const { buildSupportUrl } = await importModule()
|
||||
const url = new URL(buildSupportUrl())
|
||||
expect(url.searchParams.get('comfy_environment')).toBe('oss-nightly')
|
||||
})
|
||||
|
||||
it('detects Chrome from the user agent', async () => {
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
||||
)
|
||||
const { buildSupportUrl } = await importModule()
|
||||
const url = new URL(buildSupportUrl())
|
||||
expect(url.searchParams.get('browser')).toBe('Chrome 131')
|
||||
})
|
||||
|
||||
it('detects Firefox from the user agent', async () => {
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0'
|
||||
)
|
||||
const { buildSupportUrl } = await importModule()
|
||||
const url = new URL(buildSupportUrl())
|
||||
expect(url.searchParams.get('browser')).toBe('Firefox 121')
|
||||
})
|
||||
|
||||
it('detects Edge before falling through to Chrome', async () => {
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0'
|
||||
)
|
||||
const { buildSupportUrl } = await importModule()
|
||||
const url = new URL(buildSupportUrl())
|
||||
expect(url.searchParams.get('browser')).toBe('Edge 131')
|
||||
})
|
||||
|
||||
it('forwards a product area override to the prefill', async () => {
|
||||
const { buildSupportUrl, SupportForm } = await importModule()
|
||||
const url = new URL(
|
||||
buildSupportUrl(SupportForm.Billing, { productArea: 'Billing' })
|
||||
)
|
||||
expect(url.searchParams.get('product_area')).toBe('Billing')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeOsName', () => {
|
||||
const ORIGINAL_UA = navigator.userAgent
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value: ORIGINAL_UA,
|
||||
configurable: true
|
||||
})
|
||||
})
|
||||
|
||||
function setUserAgent(value: string) {
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value,
|
||||
configurable: true
|
||||
})
|
||||
}
|
||||
|
||||
async function importModule() {
|
||||
vi.resetModules()
|
||||
return import('./config')
|
||||
}
|
||||
|
||||
it('promotes "darwin" to the UA-detected macOS version', async () => {
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5_0) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36'
|
||||
)
|
||||
const { normalizeOsName } = await importModule()
|
||||
expect(normalizeOsName('darwin')).toBe('macOS 14.5.0')
|
||||
})
|
||||
|
||||
it('promotes "win32" to the UA-detected Windows version', async () => {
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36'
|
||||
)
|
||||
const { normalizeOsName } = await importModule()
|
||||
expect(normalizeOsName('win32')).toBe('Windows 10/11')
|
||||
})
|
||||
|
||||
it('promotes "linux" to "Linux" when UA reports Linux', async () => {
|
||||
setUserAgent('Mozilla/5.0 (X11; Linux x86_64) Firefox/121.0')
|
||||
const { normalizeOsName } = await importModule()
|
||||
expect(normalizeOsName('linux')).toBe('Linux')
|
||||
})
|
||||
|
||||
it('keeps a descriptive value untouched', async () => {
|
||||
const { normalizeOsName } = await importModule()
|
||||
expect(normalizeOsName('Ubuntu 22.04')).toBe('Ubuntu 22.04')
|
||||
})
|
||||
|
||||
it('falls back to UA detection when the input is empty', async () => {
|
||||
setUserAgent(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/131.0.0.0 Safari/537.36'
|
||||
)
|
||||
const { normalizeOsName } = await importModule()
|
||||
expect(normalizeOsName(null)).toBe('macOS 10.15.7')
|
||||
expect(normalizeOsName('')).toBe('macOS 10.15.7')
|
||||
})
|
||||
|
||||
it('falls back to the kernel name when UA detection cannot resolve', async () => {
|
||||
setUserAgent('SomeWeirdBot/1.0')
|
||||
const { normalizeOsName } = await importModule()
|
||||
expect(normalizeOsName('darwin')).toBe('darwin')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,70 +1,189 @@
|
||||
import { isCloud, isNightly } from '@/platform/distribution/types'
|
||||
|
||||
/**
|
||||
* Zendesk ticket form field IDs.
|
||||
* Slug of a Pylon form under https://portal.usepylon.com/comfy-org/forms/.
|
||||
* The form slug determines which ticket form opens and which fields are shown.
|
||||
*/
|
||||
const ZENDESK_FIELDS = {
|
||||
/** Distribution tag (cloud vs OSS) */
|
||||
DISTRIBUTION: 'tf_42243568391700',
|
||||
/** User email (anonymous requester) */
|
||||
ANONYMOUS_EMAIL: 'tf_anonymous_requester_email',
|
||||
/** User email (authenticated) */
|
||||
EMAIL: 'tf_40029135130388',
|
||||
/** User ID */
|
||||
USER_ID: 'tf_42515251051412'
|
||||
export const SupportForm = {
|
||||
Billing: 'billing-refund-issue',
|
||||
Bug: 'report-a-bug',
|
||||
FeatureRequest: 'feature-request',
|
||||
PartnerNode: 'partner-node-issue',
|
||||
Question: 'question'
|
||||
} as const
|
||||
export type SupportForm = (typeof SupportForm)[keyof typeof SupportForm]
|
||||
|
||||
/**
|
||||
* Gets the distribution identifier for tracking.
|
||||
* Helps distinguish feedback from different build types.
|
||||
* Pylon custom-field slugs (URL keys) configured for the comfy-org workspace.
|
||||
* Pylon prefill uses the slug — not the field UUID — as the URL key.
|
||||
*/
|
||||
function getDistribution(): 'ccloud' | 'oss-nightly' | 'oss' {
|
||||
const PYLON_FIELDS = {
|
||||
EMAIL: 'email',
|
||||
BROWSER: 'browser',
|
||||
COMFY_CLOUD_USER_ID: 'comfy_cloud_user_id',
|
||||
COMFY_ENVIRONMENT: 'comfy_environment',
|
||||
COMFY_OS: 'comfy_os',
|
||||
COMFY_VERSION: 'comfy_version',
|
||||
PRODUCT_AREA: 'product_area'
|
||||
} as const
|
||||
|
||||
const PYLON_FORMS_BASE_URL = 'https://portal.usepylon.com/comfy-org/forms/'
|
||||
const FEEDBACK_TYPEFORM_BASE_URL = 'https://form.typeform.com/to/q7azbWPi'
|
||||
|
||||
/**
|
||||
* Build environment tag for distinguishing tickets by build type.
|
||||
*/
|
||||
function getEnvironment(): 'ccloud' | 'oss-nightly' | 'oss' {
|
||||
if (isCloud) return 'ccloud'
|
||||
if (isNightly) return 'oss-nightly'
|
||||
return 'oss'
|
||||
}
|
||||
|
||||
const SUPPORT_BASE_URL = 'https://support.comfy.org/hc/en-us/requests/new'
|
||||
const FEEDBACK_TYPEFORM_BASE_URL = 'https://form.typeform.com/to/q7azbWPi'
|
||||
|
||||
/**
|
||||
* Builds the feedback Typeform URL tagged with the current build distribution
|
||||
* Builds the feedback Typeform URL tagged with the current build environment
|
||||
* and the UI source that opened it. Tags are passed via the URL fragment
|
||||
* (Typeform's hidden-field convention) so survey responses can be segmented
|
||||
* by distribution (cloud / oss-nightly / oss) and entry point.
|
||||
* by environment (cloud / oss-nightly / oss) and entry point.
|
||||
*/
|
||||
export function buildFeedbackTypeformUrl(
|
||||
source: 'topbar' | 'action-bar' | 'help-center'
|
||||
): string {
|
||||
const params = new URLSearchParams({
|
||||
distribution: getDistribution(),
|
||||
distribution: getEnvironment(),
|
||||
source
|
||||
})
|
||||
return `${FEEDBACK_TYPEFORM_BASE_URL}#${params.toString()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the support URL with optional user information for pre-filling.
|
||||
* Users without login information will still get a valid support URL without pre-fill.
|
||||
*
|
||||
* @param params - User information to pre-fill in the support form
|
||||
* @returns Complete Zendesk support URL with query parameters
|
||||
*/
|
||||
export function buildSupportUrl(params?: {
|
||||
export interface SupportPrefill {
|
||||
/** Authenticated user's email (for Cloud / API-key users). */
|
||||
userEmail?: string | null
|
||||
/** Cloud user id, when available. */
|
||||
userId?: string | null
|
||||
}): string {
|
||||
const searchParams = new URLSearchParams({
|
||||
[ZENDESK_FIELDS.DISTRIBUTION]: getDistribution()
|
||||
})
|
||||
|
||||
if (params?.userEmail) {
|
||||
searchParams.append(ZENDESK_FIELDS.ANONYMOUS_EMAIL, params.userEmail)
|
||||
searchParams.append(ZENDESK_FIELDS.EMAIL, params.userEmail)
|
||||
}
|
||||
if (params?.userId) {
|
||||
searchParams.append(ZENDESK_FIELDS.USER_ID, params.userId)
|
||||
}
|
||||
|
||||
return `${SUPPORT_BASE_URL}?${searchParams.toString()}`
|
||||
/** Operating system string (e.g. "macOS 14.5"). */
|
||||
os?: string | null
|
||||
/** ComfyUI frontend version. */
|
||||
version?: string | null
|
||||
/** Product area this ticket belongs to (e.g. "Billing", "Cloud"). */
|
||||
productArea?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a single `slug=value` pair. Skips empty values so the resulting URL
|
||||
* stays clean. We use `encodeURIComponent` (not `URLSearchParams`) so spaces
|
||||
* become `%20` rather than `+`, matching the Pylon prefill spec.
|
||||
*/
|
||||
function encodePair(
|
||||
slug: string,
|
||||
value: string | null | undefined
|
||||
): string | null {
|
||||
if (value === null || value === undefined || value === '') return null
|
||||
return `${encodeURIComponent(slug)}=${encodeURIComponent(value)}`
|
||||
}
|
||||
|
||||
function detectBrowser(): string | null {
|
||||
if (typeof navigator === 'undefined') return null
|
||||
const ua = navigator.userAgent
|
||||
// Order matters: Edge / Opera identify themselves as Chrome too.
|
||||
const matchers: { name: string; pattern: RegExp }[] = [
|
||||
{ name: 'Edge', pattern: /Edg\/([\d.]+)/ },
|
||||
{ name: 'Opera', pattern: /OPR\/([\d.]+)/ },
|
||||
{ name: 'Chrome', pattern: /Chrome\/([\d.]+)/ },
|
||||
{ name: 'Firefox', pattern: /Firefox\/([\d.]+)/ },
|
||||
{ name: 'Safari', pattern: /Version\/([\d.]+).*Safari/ }
|
||||
]
|
||||
for (const { name, pattern } of matchers) {
|
||||
const match = ua.match(pattern)
|
||||
if (match) return `${name} ${match[1].split('.')[0]}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a user-friendly OS string from the browser. Preferred over backend
|
||||
* platform names like `darwin` / `win32` because those are kernel identifiers,
|
||||
* not what users (or support agents) recognize. Modern browsers freeze the
|
||||
* macOS / Windows minor version in the UA string, so we only report the
|
||||
* family — that's still more useful than `darwin`.
|
||||
*/
|
||||
export function detectOS(): string | null {
|
||||
if (typeof navigator === 'undefined') return null
|
||||
const ua = navigator.userAgent
|
||||
|
||||
if (/iPad|iPhone|iPod/.test(ua)) {
|
||||
const iOS = ua.match(/OS (\d+)[._](\d+)(?:[._](\d+))?/)
|
||||
return iOS ? `iOS ${iOS[1]}.${iOS[2]}${iOS[3] ? `.${iOS[3]}` : ''}` : 'iOS'
|
||||
}
|
||||
if (/Android/.test(ua)) {
|
||||
const android = ua.match(/Android (\d+(?:\.\d+)*)/)
|
||||
return android ? `Android ${android[1]}` : 'Android'
|
||||
}
|
||||
if (/Mac OS X|Macintosh/.test(ua)) {
|
||||
const mac = ua.match(/Mac OS X (\d+)[._](\d+)(?:[._](\d+))?/)
|
||||
if (!mac) return 'macOS'
|
||||
return `macOS ${mac[1]}.${mac[2]}${mac[3] ? `.${mac[3]}` : ''}`
|
||||
}
|
||||
if (/Windows NT/.test(ua)) {
|
||||
const win = ua.match(/Windows NT (\d+\.\d+)/)
|
||||
const winMap: Record<string, string> = {
|
||||
'10.0': 'Windows 10/11',
|
||||
'6.3': 'Windows 8.1',
|
||||
'6.2': 'Windows 8',
|
||||
'6.1': 'Windows 7'
|
||||
}
|
||||
return win ? (winMap[win[1]] ?? `Windows NT ${win[1]}`) : 'Windows'
|
||||
}
|
||||
if (/CrOS/.test(ua)) return 'ChromeOS'
|
||||
if (/Linux/.test(ua)) return 'Linux'
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend (`systemStats.system.os`) reports the Python platform identifier
|
||||
* for OSS / Desktop, which is the kernel name (`darwin`, `linux`, `win32`).
|
||||
* Promote those to the UA-detected version so the Pylon ticket shows
|
||||
* "macOS 14.5" instead of "darwin".
|
||||
*/
|
||||
export function normalizeOsName(
|
||||
rawOs: string | null | undefined
|
||||
): string | null {
|
||||
const uaOs = detectOS()
|
||||
if (!rawOs) return uaOs
|
||||
const lower = rawOs.toLowerCase().trim()
|
||||
if (lower === 'darwin' || lower === 'linux' || lower === 'win32') {
|
||||
return uaOs ?? rawOs
|
||||
}
|
||||
return rawOs
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the Pylon prefill URL for a given form, omitting empty fields.
|
||||
* Users without prefill data still get a valid URL that opens the same form —
|
||||
* Pylon will collect those values from the user manually.
|
||||
*
|
||||
* @param form - Which Pylon form to open
|
||||
* @param prefill - Field values to pre-populate
|
||||
* @returns Complete Pylon form URL
|
||||
*/
|
||||
export function buildSupportUrl(
|
||||
form: SupportForm = SupportForm.Question,
|
||||
prefill: SupportPrefill = {}
|
||||
): string {
|
||||
const pairs: string[] = []
|
||||
const push = (slug: string, value: string | null | undefined) => {
|
||||
const pair = encodePair(slug, value)
|
||||
if (pair) pairs.push(pair)
|
||||
}
|
||||
|
||||
push(PYLON_FIELDS.EMAIL, prefill.userEmail)
|
||||
push(PYLON_FIELDS.COMFY_CLOUD_USER_ID, prefill.userId)
|
||||
push(PYLON_FIELDS.COMFY_ENVIRONMENT, getEnvironment())
|
||||
push(PYLON_FIELDS.COMFY_VERSION, prefill.version)
|
||||
push(PYLON_FIELDS.COMFY_OS, prefill.os)
|
||||
push(PYLON_FIELDS.BROWSER, detectBrowser())
|
||||
push(PYLON_FIELDS.PRODUCT_AREA, prefill.productArea)
|
||||
|
||||
const query = pairs.join('&')
|
||||
return `${PYLON_FORMS_BASE_URL}${form}${query ? `?${query}` : ''}`
|
||||
}
|
||||
|
||||
52
src/platform/support/useSupportContext.ts
Normal file
52
src/platform/support/useSupportContext.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import {
|
||||
SupportForm,
|
||||
buildSupportUrl,
|
||||
normalizeOsName
|
||||
} from '@/platform/support/config'
|
||||
import type { SupportPrefill } from '@/platform/support/config'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
/**
|
||||
* Resolves Pylon prefill data from the current user session + system stats and
|
||||
* exposes a single `openSupport(form, extras?)` action that opens the best-fit
|
||||
* Pylon form in a new tab.
|
||||
*
|
||||
* Resolution is deferred until `openSupport`/`buildPrefill` is actually called
|
||||
* — call sites that never invoke them don't pay the cost of (or fail because
|
||||
* of) booting Firebase auth at component setup time.
|
||||
*/
|
||||
export function useSupportContext() {
|
||||
const buildPrefill = (extra?: Partial<SupportPrefill>): SupportPrefill => {
|
||||
const { userEmail, resolvedUserInfo } = useCurrentUser()
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
return {
|
||||
userEmail: userEmail.value ?? null,
|
||||
userId: resolvedUserInfo.value?.id ?? null,
|
||||
os: normalizeOsName(systemStatsStore.systemStats?.system?.os),
|
||||
version: __COMFYUI_FRONTEND_VERSION__,
|
||||
...extra
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a Pylon support form pre-filled with the user's context. Any field
|
||||
* we can't resolve is omitted from the URL — the form still opens.
|
||||
*
|
||||
* @param form - Which Pylon form best matches the entry-point. Defaults to
|
||||
* the generic "Question" form.
|
||||
* @param extra - Per-callsite overrides (e.g. `productArea: 'Billing'`).
|
||||
*/
|
||||
const openSupport = (
|
||||
form: SupportForm = SupportForm.Question,
|
||||
extra?: Partial<SupportPrefill>
|
||||
): void => {
|
||||
const url = buildSupportUrl(form, buildPrefill(extra))
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
return {
|
||||
buildPrefill,
|
||||
openSupport
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { buildSupportUrl } from '@/platform/support/config'
|
||||
import { SupportForm, buildSupportUrl } from '@/platform/support/config'
|
||||
import { useSupportContext } from '@/platform/support/useSupportContext'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
|
||||
@@ -25,7 +26,11 @@ const { copyToClipboard } = useCopyToClipboard()
|
||||
const guideUrl = buildDocsUrl('troubleshooting/overview', {
|
||||
includeLocale: true
|
||||
})
|
||||
const supportUrl = buildSupportUrl()
|
||||
const { buildPrefill } = useSupportContext()
|
||||
const supportUrl = buildSupportUrl(
|
||||
SupportForm.Bug,
|
||||
buildPrefill({ productArea: 'Linear Mode' })
|
||||
)
|
||||
|
||||
const inputNodeIds = computed(() => {
|
||||
const ids = new Set()
|
||||
|
||||
Reference in New Issue
Block a user