@@ -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
2934function 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
6777function 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
7889function 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 (
0 commit comments