Skip to content

Commit 162e0cf

Browse files
authored
Release v1.56.0 (#1328)
- chore: handle FormSG signature field - chore: remove for each feature flags - chore: hex encode GatherSG field names with special characters - feat: collaborators - fix: collaborators cannot add Excel step ## Pre-deploy checklist - [x] Run migration
2 parents 6af3a7e + d86d733 commit 162e0cf

File tree

162 files changed

+8256
-865
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

162 files changed

+8256
-865
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"@graphql-tools/schema": "10.0.25",
3838
"@graphql-tools/utils": "10.9.1",
3939
"@launchdarkly/node-server-sdk": "9.0.4",
40-
"@opengovsg/formsg-sdk": "0.13.0",
40+
"@opengovsg/formsg-sdk": "0.16.1",
4141
"@opengovsg/sgid-client": "2.3.0",
4242
"@plumber/types": "file:../types",
4343
"@taskforcesh/bullmq-pro": "7.7.1",
@@ -111,5 +111,5 @@
111111
"tsconfig-paths": "^4.2.0",
112112
"type-fest": "4.10.3"
113113
},
114-
"version": "1.55.2"
114+
"version": "1.56.0"
115115
}

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,4 +894,66 @@ describe('decrypt form response', () => {
894894
)
895895
})
896896
})
897+
898+
describe('signature field', () => {
899+
it('should handle signature field with signature captured', async () => {
900+
$.flow.hasFileProcessingActions = false
901+
mocks.cryptoDecrypt.mockReturnValueOnce({
902+
responses: [
903+
{
904+
_id: 'signatureField',
905+
fieldType: 'signature',
906+
question: 'Please sign here',
907+
answerArray: [1.1, 1.2, 1.3],
908+
},
909+
],
910+
})
911+
912+
await expect(decryptFormResponse($)).resolves.toEqual(
913+
SUCCESS_DECRYPT_RESPONSE,
914+
)
915+
expect($.request.body).toEqual(
916+
expect.objectContaining({
917+
fields: {
918+
signatureField: {
919+
fieldType: 'signature',
920+
question: 'Please sign here',
921+
answer: 'Signature captured',
922+
order: 1,
923+
},
924+
},
925+
}),
926+
)
927+
})
928+
929+
it('should handle signature field without signature', async () => {
930+
$.flow.hasFileProcessingActions = false
931+
mocks.cryptoDecrypt.mockReturnValueOnce({
932+
responses: [
933+
{
934+
_id: 'signatureField',
935+
fieldType: 'signature',
936+
question: 'Please sign here',
937+
answerArray: [],
938+
},
939+
],
940+
})
941+
942+
await expect(decryptFormResponse($)).resolves.toEqual(
943+
SUCCESS_DECRYPT_RESPONSE,
944+
)
945+
expect($.request.body).toEqual(
946+
expect.objectContaining({
947+
fields: {
948+
signatureField: {
949+
fieldType: 'signature',
950+
question: 'Please sign here',
951+
answer: '',
952+
order: 1,
953+
},
954+
},
955+
}),
956+
)
957+
})
958+
})
897959
})

packages/backend/src/apps/formsg/__tests__/triggers/new-submission.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,15 @@ describe('new submission trigger for answer array fields', () => {
543543
order: 5,
544544
answerArray: ['51', 'BRAS BASAH ROAD', '', '', '189554'], // Some empty fields
545545
},
546+
signatureField: {
547+
question: 'Sign here',
548+
fieldType: 'signature',
549+
order: 6,
550+
answerArray: [
551+
'draw',
552+
'[[123.45, 123.45, 0.5], [543.21, 543.21, 0.5]]',
553+
],
554+
},
546555
},
547556
},
548557
} as unknown as IExecutionStep
@@ -622,5 +631,16 @@ describe('new submission trigger for answer array fields', () => {
622631
)
623632
})
624633
})
634+
635+
it('Signature type: includes all metadata for signature fields but it is hidden', async () => {
636+
const metadata = await trigger.getDataOutMetadata(executionStep)
637+
const signatureMetadata = metadata.fields.signatureField
638+
.answerArray as IDataOutMetadatum[]
639+
expect(signatureMetadata).toHaveLength(2)
640+
signatureMetadata.forEach((metadata, index) => {
641+
expect(metadata.isHidden).toEqual(true)
642+
expect(metadata.label).toEqual(`6.${index + 1}. Sign here`)
643+
})
644+
})
625645
})
626646
})

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ export async function decryptFormResponse(
136136
)
137137
} else {
138138
rest.answerArray = (rest.answerArray as string[]).map((answer) =>
139-
answer.replaceAll('\u0000', ''),
139+
typeof answer === 'string'
140+
? answer.replaceAll('\u0000', '')
141+
: answer,
140142
)
141143
}
142144
}
@@ -165,6 +167,13 @@ export async function decryptFormResponse(
165167
rest.answerArray = processLocalAddress(rest.answerArray as string[])
166168
}
167169

170+
// Add signature status to the response regardless of whether the user has signed or not (optional field)
171+
if (rest.fieldType === 'signature') {
172+
const isSignaturePresent = (rest.answerArray as string[])?.length > 0
173+
rest.answer = isSignaturePresent ? 'Signature captured' : ''
174+
delete rest.answerArray // we dont need to store it
175+
}
176+
168177
// Note: the order may not be sequential; fields (e.g. NRIC) can be
169178
// omitted from the output.
170179
// Note: FormSG uses dot notation for field ids for MyInfo children

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ function buildQuestionMetadatum(fieldData: IJSONObject): IDataOutMetadatum {
2424
question.label = 'Header'
2525
}
2626

27-
if (fieldData.fieldType === 'attachment') {
27+
if (
28+
fieldData.fieldType === 'attachment' ||
29+
fieldData.fieldType === 'signature'
30+
) {
2831
question['isHidden'] = true
2932
}
3033

@@ -69,8 +72,8 @@ function isAnswerArrayValid(fieldData: IJSONObject): boolean {
6972
if (!fieldData.answerArray) {
7073
return false
7174
}
72-
// strict check for only table, checkbox and address variables
73-
const answerArrayFields = ['table', 'checkbox', 'address']
75+
// strict check for only table, checkbox, address and signature variables
76+
const answerArrayFields = ['table', 'checkbox', 'address', 'signature']
7477
return answerArrayFields.includes(fieldData.fieldType as string)
7578
}
7679

@@ -86,6 +89,20 @@ function buildAnswerArrayForAddress(fieldData: IJSONObject): IDataOutMetadata {
8689
}))
8790
}
8891

92+
// Hide all the labels for the signature field
93+
function buildAnswerArrayForSignature(
94+
fieldData: IJSONObject,
95+
): IDataOutMetadata {
96+
const { order, question } = fieldData
97+
const answerArray = fieldData.answerArray as IJSONArray
98+
return answerArray.map((_, index) => ({
99+
type: 'text',
100+
label: `${order}.${index + 1}. ${question}`,
101+
order: order ? `${order}.${index + 1}` : null,
102+
isHidden: true,
103+
}))
104+
}
105+
89106
function buildAnswerArrayForCheckbox(
90107
fieldData: IJSONObject,
91108
): IDataOutMetadatum {
@@ -209,6 +226,8 @@ function buildAnswerArrayMetadatum(
209226
return buildAnswerArrayForTable(fieldData)
210227
case 'address':
211228
return buildAnswerArrayForAddress(fieldData)
229+
case 'signature':
230+
return buildAnswerArrayForSignature(fieldData)
212231
default:
213232
logger.warn(`Answer array unknown fieldtype: ${fieldType}`, {
214233
fieldType,

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ async function getMockData($: IGlobalVariable) {
113113
const formFields = formDetails.form.form_fields as Array<FormField>
114114
for (let i = 0; i < formFields.length; i++) {
115115
if (data.responses[formFields[i]._id]) {
116+
const fieldType = data.responses[formFields[i]._id].fieldType
116117
// forcefully include all checkbox options in the correct order
117-
if (data.responses[formFields[i]._id].fieldType === 'checkbox') {
118+
if (fieldType === 'checkbox') {
118119
data.responses[formFields[i]._id].answerArray =
119120
formFields[i].fieldOptions
120121
// include the others option if available
@@ -125,34 +126,38 @@ async function getMockData($: IGlobalVariable) {
125126
}
126127
}
127128

129+
if (fieldType === 'signature') {
130+
data.responses[formFields[i]._id].answer = 'Signature captured' // mock this to always be present regardless of whether the user has signed or not
131+
}
132+
128133
// formsg payload doesnt contain this anyways, so we dont return in mock data
129-
if (data.responses[formFields[i]._id].fieldType === 'statement') {
134+
if (fieldType === 'statement') {
130135
delete data.responses[formFields[i]._id]
131136
continue
132137
}
133138

134-
if (data.responses[formFields[i]._id].fieldType === 'address') {
139+
if (fieldType === 'address') {
135140
data.responses[formFields[i]._id].answerArray =
136141
generateMockAddressData()
137142
}
138143

139-
if (data.responses[formFields[i]._id].fieldType === 'attachment') {
144+
if (fieldType === 'attachment') {
140145
data.responses[formFields[i]._id].answer = MOCK_ATTACHMENT_FILE_PATH
141146
}
142147

143-
if (data.responses[formFields[i]._id].fieldType === 'nric') {
148+
if (fieldType === 'nric') {
144149
data.responses[formFields[i]._id].answer = filterNric(
145150
$,
146151
data.responses[formFields[i]._id].answer,
147152
)
148153
}
149154

150-
if (data.responses[formFields[i]._id].fieldType === 'email') {
155+
if (fieldType === 'email') {
151156
data.responses[formFields[i]._id].answer = $.user.email
152157
}
153158

154159
// add a stringified version of the table data to the mock data
155-
if (data.responses[formFields[i]._id].fieldType === 'table') {
160+
if (fieldType === 'table') {
156161
const answerArray = data.responses[formFields[i]._id]
157162
.answerArray as string[][]
158163
const question = data.responses[formFields[i]._id].question
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { processFields } from '../../auth/decrypt-response'
4+
import { HEX_ENCODED_FIELD_PREFIX } from '../../common/constants'
5+
6+
describe('processFields', () => {
7+
it('should process fields with whitespace, -, _', () => {
8+
const fields = {
9+
name: 'John Doe',
10+
11+
'field-email': '[email protected]',
12+
field_email: '[email protected]',
13+
'field email': '[email protected]',
14+
}
15+
const processedFields = processFields(fields)
16+
expect(processedFields).toEqual(fields)
17+
})
18+
19+
it.each([
20+
'field / Email',
21+
'field.email',
22+
'field&email',
23+
'field%email',
24+
'field$email',
25+
'field@email',
26+
'field^email',
27+
'field!email',
28+
'field(email)',
29+
])('should hex encode fields with special characters', (field: string) => {
30+
const fields = {
31+
name: 'John Doe',
32+
[field]: '[email protected]',
33+
}
34+
const hexEncodedField = `${HEX_ENCODED_FIELD_PREFIX}${Buffer.from(
35+
field,
36+
).toString('hex')}`
37+
const processedFields = processFields(fields)
38+
expect(processedFields).toEqual({
39+
name: 'John Doe',
40+
[hexEncodedField]: '[email protected]',
41+
})
42+
})
43+
})

0 commit comments

Comments
 (0)