Skip to content

Commit 179c1dd

Browse files
feat: update typeform feedback to be an in app dialog (#12961)
## Summary The current typeform feedback link takes the user out of the application, resulting in a worse user experience. This updates the feedback form to be embedded in a dialog within the app. ## Changes - **What**: - Extract TypeformEmbed from existing popovers - Add new dialog content that uses this to display embedded form - Update feedback button to show dialog ## Screenshots (if applicable) <img width="1501" height="1315" alt="image" src="https://github.com/user-attachments/assets/07205aec-7863-4be0-a71e-24f5ed9b7766" /> --------- Co-authored-by: Austin Mroz <austin@comfy.org>
1 parent d791351 commit 179c1dd

19 files changed

Lines changed: 435 additions & 113 deletions

src/components/sidebar/SidebarHelpCenterIcon.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
ref="feedbackRef"
2626
data-testid="feedback-embed"
2727
data-tf-auto-resize
28-
:data-tf-widget="typeformId"
28+
:data-tf-widget="APP_MODE_FEEDBACK_TYPEFORM_ID"
2929
/>
3030
</Popover>
3131
<SidebarIcon
@@ -68,7 +68,7 @@ const sidebarOnLeft = computed(
6868
)
6969
7070
const feedbackRef = useTemplateRef<HTMLDivElement>('feedbackRef')
71-
const { typeformError, isValidTypeformId, typeformId } = useTypeformEmbed(
71+
const { typeformError, isValidTypeformId } = useTypeformEmbed(
7272
feedbackRef,
7373
APP_MODE_FEEDBACK_TYPEFORM_ID
7474
)

src/components/topbar/WorkflowTabs.test.ts

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { render, screen } from '@testing-library/vue'
22
import userEvent from '@testing-library/user-event'
3-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
44
import { defineComponent, h, reactive } from 'vue'
55
import { createI18n } from 'vue-i18n'
66

@@ -36,7 +36,15 @@ vi.mock('@/platform/settings/settingStore', () => ({
3636
}))
3737

3838
vi.mock('@/composables/auth/useCurrentUser', () => ({
39-
useCurrentUser: () => ({ isLoggedIn: { value: false } })
39+
useCurrentUser: () => ({
40+
isLoggedIn: { value: false },
41+
userEmail: { value: undefined }
42+
})
43+
}))
44+
45+
const openFeedbackDialog = vi.hoisted(() => vi.fn())
46+
vi.mock('@/platform/support/feedbackDialog', () => ({
47+
openFeedbackDialog
4048
}))
4149

4250
vi.mock('@/composables/useFeatureFlags', () => ({
@@ -132,44 +140,28 @@ function renderComponent() {
132140
}
133141

134142
describe('WorkflowTabs feedback button', () => {
135-
let openSpy: ReturnType<typeof vi.spyOn>
136-
137143
beforeEach(() => {
138144
distribution.isCloud = false
139145
distribution.isDesktop = false
140146
distribution.isNightly = false
141147
tabBarLayout.value = 'Default'
142-
openSpy = vi.spyOn(window, 'open').mockReturnValue(null)
143-
})
144-
145-
afterEach(() => {
146-
openSpy.mockRestore()
148+
openFeedbackDialog.mockReset()
147149
})
148150

149-
it('opens the Typeform survey tagged with topbar source on Cloud', async () => {
151+
it('opens the feedback dialog tagged with topbar source when clicked', async () => {
150152
distribution.isCloud = true
151153
const { user } = renderComponent()
152154

153155
await user.click(screen.getByRole('button', { name: 'Feedback' }))
154156

155-
expect(openSpy).toHaveBeenCalledWith(
156-
'https://form.typeform.com/to/q7azbWPi#distribution=ccloud&source=topbar',
157-
'_blank',
158-
'noopener,noreferrer'
159-
)
157+
expect(openFeedbackDialog).toHaveBeenCalledWith('topbar')
160158
})
161159

162-
it('opens the Typeform survey tagged with topbar source on Nightly', async () => {
160+
it('renders the feedback button on Nightly', () => {
163161
distribution.isNightly = true
164-
const { user } = renderComponent()
165-
166-
await user.click(screen.getByRole('button', { name: 'Feedback' }))
162+
renderComponent()
167163

168-
expect(openSpy).toHaveBeenCalledWith(
169-
'https://form.typeform.com/to/q7azbWPi#distribution=oss-nightly&source=topbar',
170-
'_blank',
171-
'noopener,noreferrer'
172-
)
164+
expect(screen.getByRole('button', { name: 'Feedback' })).toBeInTheDocument()
173165
})
174166

175167
it('does not render the feedback button on non-Cloud/non-Nightly builds', () => {

src/components/topbar/WorkflowTabs.vue

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ import { useFeatureFlags } from '@/composables/useFeatureFlags'
120120
import { useWorkflowStatusDismissal } from '@/composables/useWorkflowStatusDismissal'
121121
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
122122
import { useSettingStore } from '@/platform/settings/settingStore'
123-
import { buildFeedbackTypeformUrl } from '@/platform/support/config'
123+
import { openFeedbackDialog } from '@/platform/support/feedbackDialog'
124124
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
125125
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
126126
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
@@ -157,11 +157,7 @@ const isIntegratedTabBar = computed(
157157
const showCurrentUser = computed(() => isCloud || isLoggedIn.value)
158158
159159
function openFeedback() {
160-
window.open(
161-
buildFeedbackTypeformUrl('topbar'),
162-
'_blank',
163-
'noopener,noreferrer'
164-
)
160+
openFeedbackDialog('topbar')
165161
}
166162
167163
const containerRef = ref<HTMLElement | null>(null)

src/extensions/core/cloudFeedbackTopbarButton.test.ts

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
22

33
import type { ActionBarButton } from '@/types/comfy'
44

5-
const distribution = vi.hoisted(() => ({ isCloud: false, isNightly: false }))
6-
75
const tabBarLayout = vi.hoisted(() => ({ value: 'Default' }))
86
const registerExtension = vi.hoisted(() => vi.fn())
7+
const openFeedbackDialog = vi.hoisted(() => vi.fn())
98

109
vi.mock('@/i18n', () => ({
1110
t: (key: string) => key
@@ -24,28 +23,15 @@ vi.mock('@/services/extensionService', () => ({
2423
})
2524
}))
2625

27-
vi.mock('@/platform/distribution/types', () => ({
28-
get isCloud() {
29-
return distribution.isCloud
30-
},
31-
get isNightly() {
32-
return distribution.isNightly
33-
}
26+
vi.mock('@/platform/support/feedbackDialog', () => ({
27+
openFeedbackDialog
3428
}))
3529

3630
describe('cloudFeedbackTopbarButton', () => {
37-
let openSpy: ReturnType<typeof vi.spyOn>
38-
3931
beforeEach(() => {
4032
vi.resetModules()
4133
registerExtension.mockReset()
42-
distribution.isCloud = false
43-
distribution.isNightly = false
44-
openSpy = vi.spyOn(window, 'open').mockReturnValue(null)
45-
})
46-
47-
afterEach(() => {
48-
openSpy.mockRestore()
34+
openFeedbackDialog.mockReset()
4935
})
5036

5137
function getRegisteredButtons(): ActionBarButton[] {
@@ -56,22 +42,15 @@ describe('cloudFeedbackTopbarButton', () => {
5642
return extension.actionBarButtons
5743
}
5844

59-
it('opens the Typeform survey tagged with action-bar source on Cloud', async () => {
45+
it('opens the feedback survey tagged with the action-bar source', async () => {
6046
tabBarLayout.value = 'Legacy'
61-
distribution.isCloud = true
6247
await import('./cloudFeedbackTopbarButton')
6348

6449
const buttons = getRegisteredButtons()
6550
expect(buttons).toHaveLength(1)
6651
buttons[0].onClick?.()
6752

68-
expect(openSpy).toHaveBeenCalledTimes(1)
69-
const [url, target, features] = openSpy.mock.calls[0]
70-
expect(url).toBe(
71-
'https://form.typeform.com/to/q7azbWPi#distribution=ccloud&source=action-bar'
72-
)
73-
expect(target).toBe('_blank')
74-
expect(features).toBe('noopener,noreferrer')
53+
expect(openFeedbackDialog).toHaveBeenCalledWith('action-bar')
7554
})
7655

7756
it('only registers the action bar button when the tab bar is Legacy', async () => {

src/extensions/core/cloudFeedbackTopbarButton.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { t } from '@/i18n'
22
import { useSettingStore } from '@/platform/settings/settingStore'
3-
import { buildFeedbackTypeformUrl } from '@/platform/support/config'
3+
import { openFeedbackDialog } from '@/platform/support/feedbackDialog'
44
import { useExtensionService } from '@/services/extensionService'
55
import type { ActionBarButton } from '@/types/comfy'
66

@@ -9,13 +9,7 @@ const buttons: ActionBarButton[] = [
99
icon: 'icon-[lucide--message-square-text]',
1010
label: t('actionbar.feedback'),
1111
tooltip: t('actionbar.feedbackTooltip'),
12-
onClick: () => {
13-
window.open(
14-
buildFeedbackTypeformUrl('action-bar'),
15-
'_blank',
16-
'noopener,noreferrer'
17-
)
18-
}
12+
onClick: () => openFeedbackDialog('action-bar')
1913
}
2014
]
2115

src/locales/en/main.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3182,13 +3182,18 @@
31823182
"description": "You've been using this feature. Would you take a moment to share your feedback?",
31833183
"accept": "Sure, I'll help!",
31843184
"notNow": "Not Now",
3185-
"dontAskAgain": "Don't Ask Again",
3186-
"loadError": "Failed to load survey. Please try again later."
3185+
"dontAskAgain": "Don't Ask Again"
31873186
},
31883187
"errorPanelSurvey": {
31893188
"ctaText": "How's the new error panel?",
31903189
"ctaButton": "Give feedback"
31913190
},
3191+
"feedback": {
3192+
"title": "Share Feedback"
3193+
},
3194+
"typeform": {
3195+
"loadError": "Failed to load the form. Please try again later."
3196+
},
31923197
"cloudOnboarding": {
31933198
"skipToCloudApp": "Skip to the cloud app",
31943199
"survey": {

src/platform/support/config.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,40 @@ describe('buildFeedbackTypeformUrl', () => {
5050
expect(url.hash).toBe('#distribution=ccloud&source=topbar')
5151
})
5252
})
53+
54+
describe('buildFeedbackHiddenFields', () => {
55+
beforeEach(() => {
56+
distribution.isCloud = false
57+
distribution.isNightly = false
58+
})
59+
60+
async function build(
61+
source: 'topbar' | 'action-bar' | 'help-center',
62+
extraTags?: Record<string, string>
63+
) {
64+
vi.resetModules()
65+
const { buildFeedbackHiddenFields } = await import('./config')
66+
return buildFeedbackHiddenFields(source, extraTags)
67+
}
68+
69+
it('reflects the build distribution', async () => {
70+
distribution.isNightly = true
71+
expect(await build('action-bar')).toBe(
72+
'distribution=oss-nightly,source=action-bar'
73+
)
74+
})
75+
76+
it('appends extra tags after the base segmentation tags', async () => {
77+
distribution.isCloud = true
78+
expect(await build('topbar', { email: 'user@example.com' })).toBe(
79+
'distribution=ccloud,source=topbar,email=user@example.com'
80+
)
81+
})
82+
83+
it('escapes commas in values so they survive the data-tf-hidden parser', async () => {
84+
distribution.isCloud = true
85+
expect(await build('topbar', { email: 'a,b@example.com' })).toBe(
86+
'distribution=ccloud,source=topbar,email=a\\,b@example.com'
87+
)
88+
})
89+
})

src/platform/support/config.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,40 @@ function getDistribution(): 'ccloud' | 'oss-nightly' | 'oss' {
2525
}
2626

2727
const SUPPORT_BASE_URL = 'https://support.comfy.org/hc/en-us/requests/new'
28-
const FEEDBACK_TYPEFORM_BASE_URL = 'https://form.typeform.com/to/q7azbWPi'
28+
29+
export type FeedbackSource = 'topbar' | 'action-bar' | 'help-center'
30+
31+
export const FEEDBACK_TYPEFORM_ID = 'q7azbWPi'
32+
33+
const FEEDBACK_TYPEFORM_BASE_URL = `https://form.typeform.com/to/${FEEDBACK_TYPEFORM_ID}`
34+
35+
/** Shared by the URL and embed builders so their segmentation tags can't drift. */
36+
function getFeedbackTags(source: FeedbackSource): Record<string, string> {
37+
return { distribution: getDistribution(), source }
38+
}
2939

3040
/**
3141
* Builds the feedback Typeform URL tagged with the current build distribution
3242
* and the UI source that opened it. Tags are passed via the URL fragment
3343
* (Typeform's hidden-field convention) so survey responses can be segmented
3444
* by distribution (cloud / oss-nightly / oss) and entry point.
3545
*/
36-
export function buildFeedbackTypeformUrl(
37-
source: 'topbar' | 'action-bar' | 'help-center'
38-
): string {
39-
const params = new URLSearchParams({
40-
distribution: getDistribution(),
41-
source
42-
})
46+
export function buildFeedbackTypeformUrl(source: FeedbackSource): string {
47+
const params = new URLSearchParams(getFeedbackTags(source))
4348
return `${FEEDBACK_TYPEFORM_BASE_URL}#${params.toString()}`
4449
}
4550

51+
export function buildFeedbackHiddenFields(
52+
source: FeedbackSource,
53+
extraTags: Record<string, string> = {}
54+
): string {
55+
// Typeform's `data-tf-hidden` parser (transformRecord) splits on `,` and
56+
// unescapes `\,`, so a comma in a value is the only delimiter that needs escaping.
57+
return Object.entries({ ...getFeedbackTags(source), ...extraTags })
58+
.map(([key, value]) => `${key}=${value.replace(/,/g, '\\,')}`)
59+
.join(',')
60+
}
61+
4662
/**
4763
* Builds the support URL with optional user information for pre-filling.
4864
* Users without login information will still get a valid support URL without pre-fill.

0 commit comments

Comments
 (0)