Skip to content

Commit 32b8fe0

Browse files
authored
feat(surveys): customize response columns (#42000)
## Problem survey users cannot customize the columns when reviewing response data closes #29844 <!-- Who are we building for, what are their needs, why is this important? --> <!-- Does this fix an issue? Uncomment the line below with the issue ID to automatically close it when merged --> <!-- Closes #ISSUE_ID --> ## Changes prev PR added a new universal column config store. this PR updates the column config component to support the new contextKey param, and implements this functionality for surveys! [survey-columns-demo.mp4 <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.com/user-attachments/thumbnails/0d82bc87-c362-49ee-8931-9669e170cdb8.mp4" />](https://app.graphite.com/user-attachments/video/0d82bc87-c362-49ee-8931-9669e170cdb8.mp4) <!-- If there are frontend changes, please include screenshots. --> <!-- If a reference design was involved, include a link to the relevant Figma frame! --> ## How did you test this code? manual testing, unit tests on the column config api <!-- Briefly describe the steps you took. --> <!-- Include automated tests if possible, otherwise describe the manual testing routine. --> <!-- Docs reminder: If this change requires updated docs, please do that! Engineers are the primary people responsible for their documentation. 🙌 --> 👉 _Stay up-to-date with_ [_PostHog coding conventions_](https://posthog.com/docs/contribute/coding-conventions) _for a smoother review._ ## Changelog: (features only) Is this feature complete? <!-- Yes if this is okay to go in the changelog. No if it's still hidden behind a feature flag, or part of a feature that's not complete yet, etc. --> <!-- Removing this section does not mean the changelog bot won't pick it up, because *some people* like to not use the template, so we can't rely on it existing. --> yes
1 parent 71d3428 commit 32b8fe0

File tree

7 files changed

+100
-18
lines changed

7 files changed

+100
-18
lines changed

frontend/src/queries/nodes/DataTable/ColumnConfigurator/ColumnConfigurator.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export function ColumnConfigurator({ query, setQuery }: ColumnConfiguratorProps)
8989
: isGroupsQuery(query.source)
9090
? { type: 'groups', groupTypeIndex: query.source.group_type_index as GroupTypeIndex }
9191
: { type: 'team_columns' },
92+
contextKey: query.contextKey,
9293
}
9394
const { showModal } = useActions(columnConfiguratorLogic(columnConfiguratorLogicProps))
9495

frontend/src/queries/nodes/DataTable/ColumnConfigurator/columnConfiguratorLogic.tsx

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { actions, connect, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea'
1+
import { actions, afterMount, connect, kea, key, listeners, path, props, propsChanged, reducers, selectors } from 'kea'
2+
import { loaders } from 'kea-loaders'
23

34
import api from 'lib/api'
45
import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast'
@@ -16,6 +17,7 @@ export interface ColumnConfiguratorLogicProps {
1617
columns: string[]
1718
setColumns: (columns: string[]) => void
1819
isPersistent?: boolean
20+
contextKey?: string
1921
context?: {
2022
type: 'event_definition' | 'groups' | 'team_columns'
2123
eventDefinitionId?: string
@@ -46,6 +48,25 @@ export const columnConfiguratorLogic = kea<columnConfiguratorLogicType>([
4648
(context: NonNullable<ColumnConfiguratorLogicProps['context']>) => context,
4749
],
4850
})),
51+
loaders(({ props }) => ({
52+
savedColumnConfiguration: [
53+
null as { id: string; columns: string[] } | null,
54+
{
55+
loadSavedColumnConfiguration: async (): Promise<{ id: string; columns: string[] } | null> => {
56+
if (!props.contextKey) {
57+
return null
58+
}
59+
const response = await api.get(
60+
`api/environments/${teamLogic.values.currentTeamId}/column_configurations/?context_key=${props.contextKey}`
61+
)
62+
if (response.results && response.results.length > 0) {
63+
return { id: response.results[0].id, columns: response.results[0].columns }
64+
}
65+
return null
66+
},
67+
},
68+
],
69+
})),
4970
reducers(({ props }) => ({
5071
saveAsDefault: [
5172
false,
@@ -83,13 +104,45 @@ export const columnConfiguratorLogic = kea<columnConfiguratorLogicType>([
83104
}
84105
}),
85106
listeners(({ actions, values, props }) => ({
107+
loadSavedColumnConfigurationSuccess: ({ savedColumnConfiguration }) => {
108+
if (savedColumnConfiguration) {
109+
props.setColumns(savedColumnConfiguration.columns)
110+
}
111+
},
86112
save: async () => {
87-
actions.reportDataTableColumnsUpdated(props.context?.type ?? 'live_events')
113+
actions.reportDataTableColumnsUpdated(props.contextKey ?? props.context?.type ?? 'live_events')
88114
if (!props.isPersistent || !values.saveAsDefault) {
89115
props.setColumns(values.columns)
90116
return
91117
}
92118

119+
if (props.contextKey) {
120+
try {
121+
if (values.savedColumnConfiguration?.id) {
122+
await api.update(
123+
`api/environments/${teamLogic.values.currentTeamId}/column_configurations/${values.savedColumnConfiguration.id}`,
124+
{ columns: values.columns }
125+
)
126+
} else {
127+
const response = await api.create(
128+
`api/environments/${teamLogic.values.currentTeamId}/column_configurations/`,
129+
{
130+
context_key: props.contextKey,
131+
columns: values.columns,
132+
}
133+
)
134+
actions.loadSavedColumnConfigurationSuccess({ id: response.id, columns: response.columns })
135+
}
136+
137+
lemonToast.success('Default columns saved')
138+
} catch (error: any) {
139+
console.error('Error saving column configuration:', error)
140+
lemonToast.error(error.detail || 'Failed to save column configuration')
141+
}
142+
props.setColumns(values.columns)
143+
return
144+
}
145+
93146
if (props.context?.type === 'groups' && typeof props.context.groupTypeIndex === 'number') {
94147
try {
95148
actions.setDefaultColumns({
@@ -123,4 +176,9 @@ export const columnConfiguratorLogic = kea<columnConfiguratorLogicType>([
123176
props.setColumns(values.columns)
124177
},
125178
})),
179+
afterMount(({ actions, props }) => {
180+
if (props.contextKey) {
181+
actions.loadSavedColumnConfiguration()
182+
}
183+
}),
126184
])

frontend/src/queries/nodes/DataTable/dataTableLogic.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ export const dataTableLogic = kea<dataTableLogicType>([
195195
columnsInQuery,
196196
featureFlags,
197197
context
198-
): RequiredExcept<Omit<DataTableNode, 'response'>, 'version' | 'tags' | 'defaultColumns'> => {
198+
): RequiredExcept<
199+
Omit<DataTableNode, 'response'>,
200+
'version' | 'tags' | 'defaultColumns' | 'contextKey'
201+
> => {
199202
const { kind, columns: _columns, source, ...rest } = query
200203
const showIfFull = !!query.full
201204
const flagQueryRunningTimeEnabled = !!featureFlags[FEATURE_FLAGS.QUERY_RUNNING_TIME]

frontend/src/queries/schema.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8338,6 +8338,10 @@
83388338
"$ref": "#/definitions/DataTableNodeViewPropsContext",
83398339
"description": "Context for the table, used by components like ColumnConfigurator"
83408340
},
8341+
"contextKey": {
8342+
"description": "Context key for universal column configuration (e.g., \"survey:123\")",
8343+
"type": "string"
8344+
},
83418345
"defaultColumns": {
83428346
"description": "Default columns to use when resetting column configuration",
83438347
"items": {
@@ -25688,6 +25692,10 @@
2568825692
"$ref": "#/definitions/DataTableNodeViewPropsContext",
2568925693
"description": "Context for the table, used by components like ColumnConfigurator"
2569025694
},
25695+
"contextKey": {
25696+
"description": "Context key for universal column configuration (e.g., \"survey:123\")",
25697+
"type": "string"
25698+
},
2569125699
"defaultColumns": {
2569225700
"description": "Default columns to use when resetting column configuration",
2569325701
"items": {

frontend/src/queries/schema/schema-general.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,8 @@ interface DataTableNodeViewProps {
10691069
showColumnConfigurator?: boolean
10701070
/** Show a button to configure and persist the table's default columns if possible */
10711071
showPersistentColumnConfigurator?: boolean
1072+
/** Context key for universal column configuration (e.g., "survey:123") */
1073+
contextKey?: string
10721074
/** Shows a list of saved queries */
10731075
showSavedQueries?: boolean
10741076
/** Show saved filters feature for this table (requires uniqueKey) */

frontend/src/scenes/surveys/surveyLogic.tsx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,25 +1539,26 @@ export const surveyLogic = kea<surveyLogicType>([
15391539
where.push(archivedResponsesFilter.substring(4))
15401540
}
15411541

1542+
const defaultColumns = [
1543+
'*',
1544+
...survey.questions.map((q, i) => {
1545+
if (q.type === SurveyQuestionType.MultipleChoice) {
1546+
return `arrayStringConcat(${getSurveyResponse(q, i)}, ', ') -- ${getExpressionCommentForQuestion(q, i)}`
1547+
}
1548+
return `${getSurveyResponse(q, i)} -- ${getExpressionCommentForQuestion(q, i)}`
1549+
}),
1550+
'timestamp',
1551+
'person',
1552+
`coalesce(JSONExtractString(properties, '$lib_version')) -- Library Version`,
1553+
`coalesce(JSONExtractString(properties, '$lib')) -- Library`,
1554+
`coalesce(JSONExtractString(properties, '$current_url')) -- URL`,
1555+
]
1556+
15421557
return {
15431558
kind: NodeKind.DataTableNode,
15441559
source: {
15451560
kind: NodeKind.EventsQuery,
1546-
select: [
1547-
'*',
1548-
...survey.questions.map((q, i) => {
1549-
if (q.type === SurveyQuestionType.MultipleChoice) {
1550-
return `arrayStringConcat(${getSurveyResponse(q, i)}, ', ') -- ${getExpressionCommentForQuestion(q, i)}`
1551-
}
1552-
// Use the new condition that checks both formats
1553-
return `${getSurveyResponse(q, i)} -- ${getExpressionCommentForQuestion(q, i)}`
1554-
}),
1555-
'timestamp',
1556-
'person',
1557-
`coalesce(JSONExtractString(properties, '$lib_version')) -- Library Version`,
1558-
`coalesce(JSONExtractString(properties, '$lib')) -- Library`,
1559-
`coalesce(JSONExtractString(properties, '$current_url')) -- URL`,
1560-
],
1561+
select: defaultColumns,
15611562
orderBy: ['timestamp DESC'],
15621563
where,
15631564
after: dateRange?.date_from || startDate,
@@ -1572,13 +1573,16 @@ export const surveyLogic = kea<surveyLogicType>([
15721573
...propertyFilters,
15731574
],
15741575
},
1576+
defaultColumns,
15751577
propertiesViaUrl: true,
15761578
showExport: true,
15771579
showReload: true,
15781580
showRecordingColumn: true,
15791581
showEventFilter: false,
15801582
showPropertyFilter: false,
15811583
showTimings: false,
1584+
showPersistentColumnConfigurator: true,
1585+
contextKey: `survey:${survey.id}`,
15821586
}
15831587
},
15841588
],

posthog/schema.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5108,6 +5108,9 @@ class SavedInsightNode(BaseModel):
51085108
context: Optional[DataTableNodeViewPropsContext] = Field(
51095109
default=None, description="Context for the table, used by components like ColumnConfigurator"
51105110
)
5111+
contextKey: Optional[str] = Field(
5112+
default=None, description='Context key for universal column configuration (e.g., "survey:123")'
5113+
)
51115114
defaultColumns: Optional[list[str]] = Field(
51125115
default=None, description="Default columns to use when resetting column configuration"
51135116
)
@@ -15898,6 +15901,9 @@ class DataTableNode(BaseModel):
1589815901
context: Optional[DataTableNodeViewPropsContext] = Field(
1589915902
default=None, description="Context for the table, used by components like ColumnConfigurator"
1590015903
)
15904+
contextKey: Optional[str] = Field(
15905+
default=None, description='Context key for universal column configuration (e.g., "survey:123")'
15906+
)
1590115907
defaultColumns: Optional[list[str]] = Field(
1590215908
default=None, description="Default columns to use when resetting column configuration"
1590315909
)

0 commit comments

Comments
 (0)