Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,8 @@ export interface ConversationsSettings {
widget_identification_form_title?: string
widget_identification_form_description?: string
widget_placeholder_text?: string
widget_ticket_recovery_text?: string
widget_ticket_recovery_link_text?: string
widget_position?: 'bottom_left' | 'bottom_right' | 'top_left' | 'top_right'
slack_enabled?: boolean
slack_team_id?: string | null
Expand Down
3 changes: 3 additions & 0 deletions posthog/models/remote_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ def build_config(self, bypass_recordings_quota_cache: bool = False) -> dict[str,
"identificationFormDescription": conv_settings.get("widget_identification_form_description")
or "Please provide your details so we can help you better.",
"placeholderText": conv_settings.get("widget_placeholder_text") or "Type your message...",
"ticketRecoveryText": conv_settings.get("widget_ticket_recovery_text")
or "Don't see your previous tickets?",
"ticketRecoveryLinkText": conv_settings.get("widget_ticket_recovery_link_text") or "Recover them here",
"widgetPosition": conv_settings.get("widget_position") or "bottom_right",
}
else:
Expand Down
6 changes: 6 additions & 0 deletions posthog/models/test/test_remote_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ def test_conversations_enabled_with_defaults(self):
== "Please provide your details so we can help you better."
)
assert self.remote_config.config["conversations"]["placeholderText"] == "Type your message..."
assert self.remote_config.config["conversations"]["ticketRecoveryText"] == "Don't see your previous tickets?"
assert self.remote_config.config["conversations"]["ticketRecoveryLinkText"] == "Recover them here"

def test_conversations_enabled_with_custom_config(self):
self.team.conversations_enabled = True
Expand All @@ -209,6 +211,8 @@ def test_conversations_enabled_with_custom_config(self):
"widget_identification_form_title": "Let's get started",
"widget_identification_form_description": "Tell us about yourself",
"widget_placeholder_text": "Ask away...",
"widget_ticket_recovery_text": "Missing your tickets?",
"widget_ticket_recovery_link_text": "Restore access",
}
self.team.save()
self.sync_remote_config()
Expand All @@ -223,6 +227,8 @@ def test_conversations_enabled_with_custom_config(self):
assert self.remote_config.config["conversations"]["identificationFormTitle"] == "Let's get started"
assert self.remote_config.config["conversations"]["identificationFormDescription"] == "Tell us about yourself"
assert self.remote_config.config["conversations"]["placeholderText"] == "Ask away..."
assert self.remote_config.config["conversations"]["ticketRecoveryText"] == "Missing your tickets?"
assert self.remote_config.config["conversations"]["ticketRecoveryLinkText"] == "Restore access"

def test_conversations_disabled_returns_false(self):
self.team.conversations_enabled = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { SceneSection } from '~/layout/scenes/components/SceneSection'
import { supportSettingsLogic } from './supportSettingsLogic'

export function WidgetSection(): JSX.Element {
const { currentTeam } = useValues(teamLogic)
const { currentTeam, currentTeamLoading } = useValues(teamLogic)
const { updateCurrentTeam } = useActions(teamLogic)
const {
generateNewToken,
Expand All @@ -32,13 +32,19 @@ export function WidgetSection(): JSX.Element {
saveIdentificationFormDescription,
setPlaceholderTextValue,
savePlaceholderText,
setTicketRecoveryTextValue,
saveTicketRecoveryText,
setTicketRecoveryLinkTextValue,
saveTicketRecoveryLinkText,
} = useActions(supportSettingsLogic)
const {
widgetEnabledLoading,
greetingInputValue,
identificationFormTitleValue,
identificationFormDescriptionValue,
placeholderTextValue,
ticketRecoveryTextValue,
ticketRecoveryLinkTextValue,
} = useValues(supportSettingsLogic)

return (
Expand Down Expand Up @@ -190,6 +196,70 @@ export function WidgetSection(): JSX.Element {
</LemonButton>
</div>
</div>
<LemonDivider />
<div className="flex items-center gap-4 py-2 justify-between">
<div className="w-40 shrink-0">
<label className="font-medium">Ticket recovery text</label>
<p className="text-xs text-muted-alt mb-2">
Only shown to visitors who aren't using identity verification.
</p>
Comment thread
posthog[bot] marked this conversation as resolved.
</div>
<div className="flex gap-2 flex-1">
Comment thread
posthog[bot] marked this conversation as resolved.
<LemonInput
value={
ticketRecoveryTextValue ??
currentTeam?.conversations_settings?.widget_ticket_recovery_text ??
"Don't see your previous tickets?"
}
placeholder="Enter ticket recovery text"
onChange={setTicketRecoveryTextValue}
fullWidth
/>
<LemonButton
type="primary"
onClick={saveTicketRecoveryText}
loading={currentTeamLoading}
disabledReason={
!ticketRecoveryTextValue ? 'Enter ticket recovery text' : undefined
}
>
Save
</LemonButton>
Comment thread
posthog[bot] marked this conversation as resolved.
</div>
</div>
<LemonDivider />
<div className="flex items-center gap-4 py-2 justify-between">
<div className="w-40 shrink-0">
<label className="font-medium">Ticket recovery link text</label>
<p className="text-xs text-muted-alt mb-2">
The clickable part of the ticket recovery message.
</p>
</div>
<div className="flex gap-2 flex-1">
<LemonInput
value={
ticketRecoveryLinkTextValue ??
currentTeam?.conversations_settings?.widget_ticket_recovery_link_text ??
'Recover them here'
}
placeholder="Enter ticket recovery link text"
onChange={setTicketRecoveryLinkTextValue}
fullWidth
/>
<LemonButton
type="primary"
onClick={saveTicketRecoveryLinkText}
loading={currentTeamLoading}
disabledReason={
!ticketRecoveryLinkTextValue
? 'Enter ticket recovery link text'
: undefined
}
>
Save
</LemonButton>
</div>
</div>
</LemonCard>
</SceneSection>
<SceneSection title="Identification form" className="mt-8" titleSize="sm">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ export interface supportSettingsLogicValues {
name: string
}[]
teamsTeamsLoading: boolean
ticketRecoveryLinkTextValue: string | null
ticketRecoveryTextValue: string | null
widgetEnabledLoading: boolean
}

Expand Down Expand Up @@ -382,6 +384,12 @@ export interface supportSettingsLogicActions {
saveSlackTicketEmoji: () => {
value: true
}
saveTicketRecoveryLinkText: () => {
value: true
}
saveTicketRecoveryText: () => {
value: true
}
sendTestEmail: (configId: string) => {
configId: string
}
Expand Down Expand Up @@ -494,6 +502,12 @@ export interface supportSettingsLogicActions {
status: 'error' | 'idle' | 'installed' | 'installing' | 'needs_org_catalog'
teamId: string | null
}
setTicketRecoveryLinkTextValue: (value: string | null) => {
value: string | null
}
setTicketRecoveryTextValue: (value: string | null) => {
value: string | null
}
setWidgetEnabledLoading: (loading: boolean) => {
loading: boolean
}
Expand Down Expand Up @@ -600,6 +614,10 @@ export const supportSettingsLogic = kea<supportSettingsLogicType>([
saveIdentificationFormDescription: true,
setPlaceholderTextValue: (value: string | null) => ({ value }),
savePlaceholderText: true,
setTicketRecoveryTextValue: (value: string | null) => ({ value }),
saveTicketRecoveryText: true,
setTicketRecoveryLinkTextValue: (value: string | null) => ({ value }),
saveTicketRecoveryLinkText: true,
// Notification recipients
setNotificationRecipients: (users: UserBasicType[]) => ({ users }),
// Slack channel settings (SupportHog)
Expand Down Expand Up @@ -738,6 +756,18 @@ export const supportSettingsLogic = kea<supportSettingsLogicType>([
setPlaceholderTextValue: (_, { value }) => value,
},
],
ticketRecoveryTextValue: [
null as string | null,
{
setTicketRecoveryTextValue: (_, { value }) => value,
},
],
ticketRecoveryLinkTextValue: [
null as string | null,
{
setTicketRecoveryLinkTextValue: (_, { value }) => value,
},
],
// Email multi-config state
emailConfigs: [
[] as EmailConfigStatus[],
Expand Down Expand Up @@ -1277,6 +1307,30 @@ export const supportSettingsLogic = kea<supportSettingsLogicType>([
},
})
},
saveTicketRecoveryText: () => {
const trimmedValue = values.ticketRecoveryTextValue?.trim()
if (!trimmedValue) {
return
}
actions.updateCurrentTeam({
conversations_settings: {
...values.currentTeam?.conversations_settings,
widget_ticket_recovery_text: trimmedValue,
},
})
},
saveTicketRecoveryLinkText: () => {
const trimmedValue = values.ticketRecoveryLinkTextValue?.trim()
if (!trimmedValue) {
return
}
actions.updateCurrentTeam({
conversations_settings: {
...values.currentTeam?.conversations_settings,
widget_ticket_recovery_link_text: trimmedValue,
},
})
},
setNotificationRecipients: ({ users }) => {
actions.updateCurrentTeam({
conversations_settings: {
Expand Down Expand Up @@ -1647,6 +1701,8 @@ export const supportSettingsLogic = kea<supportSettingsLogicType>([
actions.setIdentificationFormTitleValue(null)
actions.setIdentificationFormDescriptionValue(null)
Comment on lines 1701 to 1702

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unrelated updates erase recovery drafts

When a user enters either recovery-text draft and then changes another widget setting such as color, position, enabled state, or required email, that setting's updateCurrentTeamSuccess resets both draft values to null, causing the inputs to revert to their persisted text and silently discard the user's edits.

Prompt To Fix With AI
This is a comment left during a code review.
Path: products/conversations/frontend/scenes/settings/supportSettingsLogic.ts
Line: 1701-1702

Comment:
**Unrelated updates erase recovery drafts**

When a user enters either recovery-text draft and then changes another widget setting such as color, position, enabled state, or required email, that setting's `updateCurrentTeamSuccess` resets both draft values to `null`, causing the inputs to revert to their persisted text and silently discard the user's edits.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed this is a real issue, and it predates this PR. The updateCurrentTeamSuccess handler clears every draft input (greeting, placeholder, both identification form fields, both recovery fields, and the Slack copy fields) to null after any successful team update. Because each input falls back to its saved value when the draft is null, an unsaved draft is silently dropped the moment another setting saves, including the color/position/enabled/require-email controls that save immediately when toggled. This PR only added the two recovery fields to that existing reset list, following the same save-per-field pattern as greeting and placeholder. A proper fix resets only the field that was actually saved, but the current save flow sends the whole settings object every time, so the success handler can't tell which field changed without restructuring how saves work — a change that should apply consistently to all the copy fields, not just the two recovery ones. That's a design decision beyond this PR's scope, so I'm escalating rather than making a recovery-only patch that would behave differently from its sibling fields. What a maintainer needs to decide: whether to redesign the draft-reset so unrelated saves keep in-progress drafts (e.g. each save clears only its own draft), applied across all widget copy fields, ideally alongside the concurrent-save handling from the related thread. Note the loading-guard fix on the recovery Save buttons does not address this, since a toggle elsewhere still triggers the reset.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This is a real, pre-existing issue in the shared copy-field save flow, and fixing only the recovery fields would create inconsistent behavior with greeting, placeholder, identification, and Slack copy. Keeping it as a follow-up for a coordinated draft-reset redesign is the right scope decision; the recovery Save-button loading guard does not address resets triggered by unrelated immediate-save controls.

actions.setPlaceholderTextValue(null)
actions.setTicketRecoveryTextValue(null)
actions.setTicketRecoveryLinkTextValue(null)
actions.setSlackTicketEmojiValue(null)
actions.setSlackBotIconUrlValue(null)
actions.setSlackBotDisplayNameValue(null)
Expand Down
Loading