Skip to content

Commit 5a8a377

Browse files
committed
chore: refactor table data check
1 parent fa132cc commit 5a8a377

File tree

3 files changed

+96
-55
lines changed

3 files changed

+96
-55
lines changed

packages/backend/src/apps/formsg/common/process-table-field.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function convertTableAnswerArrayToTableObject(
3030
// make sure that column names do not contain commas
3131
columnNamesArray.length === answerArray[0].length
3232
? columnNamesArray.map(createColumn)
33-
: answerArray[0].map((_, index) => createColumn(`Column ${index + 1}`))
33+
: answerArray[0].map((_, index) => createColumn(`Col ${index + 1}`))
3434

3535
/**
3636
* NOTE: we do not show table rows that do not have any data

packages/backend/src/apps/formsg/triggers/new-submission/get-data-out-metadata.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,13 @@ function buildAnswerArrayForTable(
176176
function buildTableMetadatum(fieldData: IJSONObject): IDataOutMetadata {
177177
// old execution steps may not have an answer field for their table
178178
if (!fieldData.answer) {
179-
return null
179+
return {
180+
label: '',
181+
order: null,
182+
type: 'table',
183+
displayedValue: '',
184+
value: {},
185+
}
180186
}
181187

182188
const tableObject = JSON.parse(fieldData.answer as string)

packages/frontend/src/components/FlowStepTestController/utils.tsx

Lines changed: 88 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,55 @@ import { Variable } from '@/helpers/variables'
77

88
import { simpleSubstitute, VariableInfoMap } from '../RichTextEditor/utils'
99

10+
const FOR_EACH_INPUT_SOURCE = {
11+
M365_EXCEL: 'm365-excel',
12+
TILES: 'tiles',
13+
STRING_ARRAY: 'string-array',
14+
FORMSG_TABLE: 'formsg-table',
15+
} as const
16+
17+
const STEP_ID_REGEX =
18+
/step\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
19+
20+
type InputSource =
21+
(typeof FOR_EACH_INPUT_SOURCE)[keyof typeof FOR_EACH_INPUT_SOURCE]
22+
23+
interface TableData {
24+
rows: Array<{
25+
data: Record<string, string | number>
26+
rowId?: string
27+
}>
28+
columns?: Array<{
29+
id: string
30+
name: string
31+
value: string
32+
}>
33+
inputSource?: InputSource
34+
}
35+
36+
const hasInputSource = (data: unknown): data is { inputSource: InputSource } =>
37+
typeof data === 'object' && data !== null && 'inputSource' in data
38+
39+
// Utility function to detect input source from test data
40+
const getInputSource = (data: unknown): InputSource | null => {
41+
if (typeof data === 'string') {
42+
try {
43+
const parsed = JSON.parse(data)
44+
return hasInputSource(parsed)
45+
? parsed.inputSource
46+
: FOR_EACH_INPUT_SOURCE.FORMSG_TABLE
47+
} catch {
48+
return null
49+
}
50+
}
51+
52+
if (hasInputSource(data)) {
53+
return data.inputSource
54+
}
55+
56+
return null
57+
}
58+
1059
// guardrail to not show the test result in the event that the app no longer exists
1160
// and users were shown the EmptyFlowStepHeader to "add" a new step.
1261
// it should already be handled by the ErrorFlowStepHeader, but just in case
@@ -24,19 +73,19 @@ export function isSameAppAndAppKey(
2473
)
2574
}
2675

27-
interface TableData {
28-
columns?: { name: string }[]
29-
rows?: unknown[]
76+
const getTableData = (
77+
data: unknown,
78+
inputSource?: InputSource | null,
79+
): TableData => {
80+
if (
81+
inputSource === FOR_EACH_INPUT_SOURCE.FORMSG_TABLE &&
82+
typeof data === 'string'
83+
) {
84+
return JSON.parse(data) as TableData
85+
}
86+
return data as TableData
3087
}
3188

32-
const STEP_ID_REGEX =
33-
/step\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
34-
35-
const getTableData = (data: unknown, isFormSgTable: boolean): TableData =>
36-
isFormSgTable && typeof data === 'string'
37-
? (JSON.parse(data as string) as TableData)
38-
: (data as TableData)
39-
4089
const deepCompare = (a: any, b: any, varInfoMap: VariableInfoMap): boolean => {
4190
if (a === b) {
4291
return true
@@ -139,69 +188,55 @@ export const matchParamsToDataIn = (
139188
}
140189

141190
const match = String(paramValue).match(STEP_ID_REGEX)
142-
let isFormSgTable = false
143-
let searchKey = match?.[0]
144-
145-
// FormSG dataOut structure is different from our own apps
146-
// the individual columns are stored in fields.answerArray,
147-
// while the items object is stored as fields.answer
148-
if (
149-
String(paramValue).includes('fields') &&
150-
String(paramValue).includes('answer')
151-
) {
152-
searchKey = String(paramValue).replace(
153-
/\{\{(.*)answer(.*)\}\}/,
154-
'$1answerArray.0$2',
155-
)
156-
isFormSgTable = true
157-
}
191+
const inputSource = getInputSource(lastTest)
192+
const isFormSgTable = inputSource === FOR_EACH_INPUT_SOURCE.FORMSG_TABLE
158193

159-
if (!searchKey) {
160-
return false
161-
}
194+
const searchKey = isFormSgTable
195+
? String(paramValue).replace(/\{\{(.*)answer\}\}/, '$1answerArray.0')
196+
: match?.[0]
162197

163-
const tableData = getTableData(lastTest, isFormSgTable)
164-
const varRowsFound = varInfoMap.get(
165-
`{{${searchKey}.rowsFound}}`,
166-
)?.testRunValue
167-
168-
if (
169-
!isFormSgTable && // form sg table output will not have rowsFound
170-
Number(varRowsFound) !== Number(tableData.rows?.length)
171-
) {
198+
if (!searchKey) {
172199
return false
173200
}
174201

202+
const tableData = getTableData(lastTest, inputSource)
175203
const lastTestColumns = tableData.columns?.map((c) => c.name) ?? []
176204
const mapKey = isFormSgTable ? searchKey : `${searchKey}.data`
177205
const varInfo = Array.from(varInfoMap.entries())
178206
.filter(([key]) => key.includes(mapKey))
179207
.map(([, value]) => value)
180-
181208
const varColumns = new Set(varInfo.map((item) => item.label))
182209

183-
/**
184-
* FormSG table special case:
185-
* - FormSG table columns are stored like:
186-
* ["Response 6, Row 1 Column 1", "Response 6, Row 1 Column 2", "Response 6, Row 1 Column 3"]
187-
*
188-
* - Our variables are stored like:
189-
* ["Column 1", "Column 2", "Column 3"]
190-
*
191-
* since there is no safe way to parse the FormSG table columns properly,
192-
* we do a best-effort comparison to just check that the columns are present.
193-
*/
194210
if (isFormSgTable) {
211+
/**
212+
* FormSG table special case:
213+
* - FormSG table columns are stored like:
214+
* ["Response 6, Row 1 Col 1", "Response 6, Row 1 Col 2", "Response 6, Row 1 Col 3"]
215+
*
216+
* - Our variables are stored like:
217+
* ["Col 1", "Col 2", "Col 3"]
218+
*
219+
* since there is no safe way to parse the FormSG table columns properly,
220+
* we do a best-effort comparison to just check that the columns are present.
221+
*/
195222
if (tableData.rows?.length === 0) {
196223
return true
197224
}
198225

199226
return lastTestColumns.every((testCol) =>
200227
Array.from(varColumns).some((varCol) => varCol.includes(testCol)),
201228
)
202-
}
229+
} else {
230+
const varRowsFound = varInfoMap.get(
231+
`{{${searchKey}.rowsFound}}`,
232+
)?.testRunValue
203233

204-
return lastTestColumns.every((label) => varColumns.has(label))
234+
if (Number(varRowsFound) !== Number(tableData.rows?.length)) {
235+
return false
236+
}
237+
238+
return lastTestColumns.every((label) => varColumns.has(label))
239+
}
205240
}
206241

207242
// Handle arrays and objects using deep comparison

0 commit comments

Comments
 (0)