Skip to content

Commit d6aca17

Browse files
committed
chore: hide formsg questions and re-label some fields
1 parent b439397 commit d6aca17

File tree

7 files changed

+183
-14
lines changed

7 files changed

+183
-14
lines changed

packages/backend/src/apps/formsg/auth/decrypt-form-response.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ export async function decryptFormResponse(
135135
}
136136
}
137137

138+
// delete metadata from fields
139+
delete rest['isHeader']
140+
138141
if (rest.fieldType === 'nric' && !!rest.answer) {
139142
const filteredAnswer = filterNric($, rest.answer)
140143
if (!filteredAnswer) {

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

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ function buildQuestionMetadatum(fieldData: IJSONObject): IDataOutMetadatum {
1717
type: 'text',
1818
label: fieldData.order ? `Question ${fieldData.order}` : null,
1919
order: fieldData.order ? (fieldData.order as number) : null,
20+
isCollapsedByDefault: true,
21+
}
22+
23+
if (fieldData.fieldType === 'section') {
24+
question.label = 'Header'
2025
}
2126

2227
if (fieldData.fieldType === 'attachment') {
@@ -28,7 +33,6 @@ function buildQuestionMetadatum(fieldData: IJSONObject): IDataOutMetadatum {
2833

2934
function buildAnswerMetadatum(fieldData: IJSONObject): IDataOutMetadatum {
3035
const answer: IDataOutMetadatum = {
31-
label: fieldData.order ? `Response ${fieldData.order}` : null,
3236
order: fieldData.order ? (fieldData.order as number) + 0.1 : null,
3337
}
3438

@@ -47,9 +51,15 @@ function buildAnswerMetadatum(fieldData: IJSONObject): IDataOutMetadatum {
4751
parseS3Id(fieldData.answer as string)?.objectName ??
4852
(fieldData.answer as string)
4953
break
54+
// Headers will only have empty answers
55+
case 'section':
56+
answer.isHidden = true
57+
break
5058
default:
5159
answer['type'] = 'text'
52-
answer['label'] = fieldData.order ? `Response ${fieldData.order}` : null
60+
answer['label'] = fieldData.order
61+
? `${fieldData.order}. ${fieldData.question}`
62+
: null
5363
}
5464

5565
return answer
@@ -65,25 +75,58 @@ function isAnswerArrayValid(fieldData: IJSONObject): boolean {
6575
}
6676

6777
function buildAnswerArrayForAddress(fieldData: IJSONObject): IDataOutMetadata {
68-
const { order } = fieldData
78+
const { order, question } = fieldData
6979

7080
// NOTE: we return all labels as there are some optional fields in FormSG
81+
// the label is shown before the question, since it will be truncated in the variable pill
7182
return ADDRESS_LABELS.map((label, index) => ({
7283
type: 'text',
73-
label: `Response ${order}, ${label}`,
84+
label: `${order}.${index + 1}. ${label} - ${question}`,
7485
order: order ? `${order}.${index + 1}` : null,
7586
}))
7687
}
7788

7889
function buildAnswerArrayForCheckbox(
7990
fieldData: IJSONObject,
8091
): IDataOutMetadatum {
92+
const { order, question } = fieldData
8193
// NOTE: checkbox answerArray will not be further splitted,
8294
// handled specifically in variables.ts and compute-parameters.ts
8395
return {
8496
type: 'array',
85-
label: `Response ${fieldData.order}`,
86-
order: fieldData.order ? (fieldData.order as number) + 0.1 : null,
97+
label: `${order}. ${question}`,
98+
order: order ? (order as number) + 0.1 : null,
99+
}
100+
}
101+
102+
function extractLastTopLevelBracketContent(questionText: string): {
103+
content: string
104+
prefix: string
105+
} {
106+
let depth = 0
107+
let start = -1
108+
let lastValid = ''
109+
let prefix = ''
110+
111+
for (let i = 0; i < questionText.length; i++) {
112+
if (questionText[i] === '(') {
113+
if (depth === 0) {
114+
start = i
115+
prefix = questionText.slice(0, i).trim()
116+
}
117+
depth++
118+
} else if (questionText[i] === ')') {
119+
depth--
120+
if (depth === 0 && start !== -1) {
121+
lastValid = questionText.slice(start + 1, i)
122+
start = -1 // reset
123+
}
124+
}
125+
}
126+
127+
return {
128+
content: lastValid,
129+
prefix,
87130
}
88131
}
89132

@@ -92,17 +135,33 @@ function buildAnswerArrayForTable(
92135
): IDataOutMetadatum[][] {
93136
const answerArray = [] as IDataOutMetadatum[][]
94137
const array = fieldData.answerArray as IJSONArray
138+
const { order, question } = fieldData
139+
// questions are give in this format:
140+
// Table Question (Column 1, Column 2, Column 3)
141+
// step 1: find the match open bracket for the last pair of brackets
142+
const { prefix: topLevelQuestion, content: columnNames } =
143+
extractLastTopLevelBracketContent(question as string)
144+
const columnNamesArray = columnNames.split(',').map((name) => name.trim())
145+
95146
for (let i = 0; i < array.length; i++) {
96147
const option = array[i]
97148
const nestedAnswerArray = [] as IDataOutMetadatum[]
98149
const optionArray = option as IJSONArray
99150
for (let j = 0; j < optionArray.length; j++) {
151+
// If the column names contain commas, we can't simply split by comma
152+
// so we gotta just display the entire question name
153+
// Also, making an API call to forms to retrieve the column names takes too long
154+
// and won't work if the form is closed
155+
let label = `${order}. ${question} Row ${i + 1} Col ${j + 1}`
156+
if (columnNamesArray.length === optionArray.length) {
157+
label = `${order}. Row ${i + 1} ${
158+
columnNamesArray[j]
159+
} - ${topLevelQuestion}`
160+
}
100161
nestedAnswerArray.push({
101162
type: 'text',
102-
label: fieldData.order
103-
? `Response ${fieldData.order}, Row ${i + 1} Column ${j + 1}`
104-
: null,
105-
order: fieldData.order ? (fieldData.order as number) : null,
163+
label,
164+
order: order ? (order as number) + 0.1 : null,
106165
})
107166
}
108167
answerArray.push(nestedAnswerArray)
@@ -215,14 +274,54 @@ async function getDataOutMetadata(
215274
}
216275

217276
const fieldMetadata: IDataOutMetadata = Object.create(null)
218-
for (const [fieldId, fieldData] of Object.entries(data.fields)) {
277+
278+
const fields = Object.entries(data.fields).sort((a, b) => {
279+
const orderA = a[1].order
280+
const orderB = b[1].order
281+
if (orderA === null) {
282+
return 1
283+
}
284+
return orderA - orderB
285+
})
286+
287+
/**
288+
* This is a hack to match the question number to the form as closely as possible.
289+
* In formsg, the headers are not numbered, so we need to exclude them from the question number.
290+
* But we also need to keep track of the headers between questions, so we can order them correctly.
291+
* The regenerated order will be like so:
292+
* Example given form:
293+
* Header 0.9991
294+
* Header 0.9992
295+
* Question 1
296+
* Question 2
297+
* Sub Heading 2.9991
298+
* Question 3
299+
* Header 3.9991
300+
* Sub Heading 3.9992
301+
* Question 4
302+
* Sub Heading 4.9991
303+
* Question 4
304+
*/
305+
let questionOrder = 0
306+
let headerOrderBetweenQuestions = 0
307+
for (const [fieldId, fieldData] of fields) {
308+
if (fieldData.fieldType !== 'section') {
309+
// reset order between questions (altho not necessary)
310+
headerOrderBetweenQuestions = 0
311+
questionOrder++
312+
fieldData.order = questionOrder
313+
} else {
314+
headerOrderBetweenQuestions++
315+
fieldData.order = questionOrder + +`0.999${headerOrderBetweenQuestions}`
316+
}
219317
fieldMetadata[fieldId] = {
220318
question: buildQuestionMetadatum(fieldData),
221319
answer: buildAnswerMetadatum(fieldData),
222320
fieldType: { isHidden: true },
223321
order: { isHidden: true },
224322
myInfo: { attr: { isHidden: true } },
225323
isVisible: { isHidden: true },
324+
isHeader: { isHidden: true },
226325
}
227326
if (isAnswerArrayValid(fieldData)) {
228327
fieldMetadata[fieldId].answerArray = buildAnswerArrayMetadatum(

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ async function getMockData($: IGlobalVariable) {
118118
}
119119
}
120120

121+
// formsg payload doesnt contain this anyways, so we dont return in mock data
122+
if (data.responses[formFields[i]._id].fieldType === 'statement') {
123+
delete data.responses[formFields[i]._id]
124+
}
125+
121126
if (data.responses[formFields[i]._id].fieldType === 'address') {
122127
data.responses[formFields[i]._id].answerArray =
123128
generateMockAddressData()

packages/frontend/src/components/RichTextEditor/VariableBadge.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@ export const VariableBadge = ({ node }: { node: Node }) => {
3838
>
3939
<Text
4040
isTruncated
41+
maxW="20ch"
4142
color="base.content.strong"
4243
mr={isEmpty ? undefined : '0.25rem'}
4344
>
4445
{node.attrs.label}
4546
</Text>
4647
{!isEmpty && (
47-
<Text isTruncated maxW="50ch" color="base.content.medium">
48+
<Text isTruncated maxW="40ch" color="base.content.medium">
4849
{value}
4950
</Text>
5051
)}

packages/frontend/src/components/VariablesList/index.tsx

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { TDataOutMetadatumType } from '@plumber/types'
22

33
import { useMemo } from 'react'
44
import {
5+
Accordion,
6+
AccordionButton,
7+
AccordionIcon,
8+
AccordionItem,
9+
AccordionPanel,
510
Box,
611
type SystemStyleObject,
712
Tag,
@@ -126,6 +131,24 @@ interface VariablesListProps {
126131
export default function VariablesList(props: VariablesListProps) {
127132
const { variables, onClick } = props
128133

134+
// Separate variables into default and collapsed categories
135+
const { defaultVariables, collapsedVariables } = useMemo(() => {
136+
if (!variables) {
137+
return { defaultVariables: [], collapsedVariables: [] }
138+
}
139+
140+
const defaultVariables: Variable[] = []
141+
const collapsedVariables: Variable[] = []
142+
for (const variable of variables) {
143+
if (variable.isCollapsedByDefault) {
144+
collapsedVariables.push(variable)
145+
} else {
146+
defaultVariables.push(variable)
147+
}
148+
}
149+
return { defaultVariables, collapsedVariables }
150+
}, [variables])
151+
129152
if (!variables || variables.length === 0) {
130153
return <></>
131154
}
@@ -138,14 +161,42 @@ export default function VariablesList(props: VariablesListProps) {
138161
p={onClick ? undefined : '1rem'}
139162
sx={props.customStyles}
140163
>
141-
{variables.map((variable, index) => (
164+
{defaultVariables.map((variable, index) => (
142165
<VariableItem
143166
key={`variable-${variable.name}-${index}`}
144167
variable={variable}
145168
onClick={onClick}
146-
isLast={index === variables.length - 1}
169+
isLast={index === defaultVariables.length - 1}
147170
/>
148171
))}
172+
{collapsedVariables.length > 0 && (
173+
<Accordion allowMultiple border="transparent" py={2}>
174+
<AccordionItem p={0}>
175+
{({ isExpanded }) => (
176+
<>
177+
<AccordionPanel p={0} borderTop="1px solid #EDEDED">
178+
{collapsedVariables.map((variable, index) => (
179+
<VariableItem
180+
key={`variable-${variable.name}-${index}`}
181+
variable={variable}
182+
onClick={onClick}
183+
isLast={index === collapsedVariables.length - 1}
184+
/>
185+
))}
186+
</AccordionPanel>
187+
<AccordionButton
188+
flex={1}
189+
justifyContent="center"
190+
borderRadius="md"
191+
>
192+
<Box as="span">{isExpanded ? 'Show less' : 'Show more'}</Box>
193+
<AccordionIcon />
194+
</AccordionButton>
195+
</>
196+
)}
197+
</AccordionItem>
198+
</Accordion>
199+
)}
149200
</Box>
150201
)
151202
}

packages/frontend/src/helpers/variables.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface Variable {
2525
type: TDataOutMetadatumType | null
2626
order: number | null
2727
displayedValue: string | null
28+
isCollapsedByDefault: boolean
2829
/**
2930
* CAVEAT: not _just_ a name; it contains the lodash.get path for dataOut. Do
3031
* not clobber unless you know what you're doing!
@@ -69,6 +70,7 @@ const process = (
6970
type = null,
7071
order = null,
7172
displayedValue = null,
73+
isCollapsedByDefault = false,
7274
} = metadata
7375

7476
if (isHidden) {
@@ -84,6 +86,7 @@ const process = (
8486
displayedValue,
8587
type,
8688
order,
89+
isCollapsedByDefault,
8790
},
8891
]
8992
}
@@ -106,6 +109,7 @@ const process = (
106109
displayedValue,
107110
type,
108111
order,
112+
isCollapsedByDefault,
109113
},
110114
]
111115
: data.flatMap((item, index) => {

packages/types/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ export interface IDataOutMetadatum {
9393
* variables with the same `order` or undefined `order` are sorted.
9494
*/
9595
order?: number
96+
97+
/**
98+
* Indicates whether this field should only be shown when the user expands
99+
* the "Show more" section in the variable list.
100+
*/
101+
isCollapsedByDefault?: boolean
96102
}
97103

98104
export interface IDataOutMetadata {

0 commit comments

Comments
 (0)