Skip to content

Commit

Permalink
Release v1.31.0 (#810)
Browse files Browse the repository at this point in the history
### Features
1. Allow CC recipients in postman action

### UI changes
1. Show connected pipes in Tiles list
2. Allow users to upload secret key in FormSG connection step
3. Show confirmation when deleting tile
4. UI improvements for tiles (auto focus inputs, button size, etc)

### Others
1. Add tracking of users' last logged in
  • Loading branch information
pregnantboy authored Dec 5, 2024
2 parents eb25889 + db01e24 commit 16551e1
Show file tree
Hide file tree
Showing 48 changed files with 1,295 additions and 149 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,5 @@
"tsconfig-paths": "^4.2.0",
"type-fest": "4.10.3"
},
"version": "1.30.0"
"version": "1.31.0"
}
3 changes: 2 additions & 1 deletion packages/backend/src/apps/formsg/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ const auth: IUserAddedConnectionAuth = {
{
key: 'privateKey',
label: 'Form Secret Key',
type: 'string' as const,
type: 'dragdrop' as const,
required: true,
readOnly: false,
value: null,
description:
'This is the key you downloaded/saved when you created the form',
placeholder: 'Enter or drop your Secret Key here to continue',
clickToCopy: false,
autoComplete: 'off' as const,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,25 @@ describe('send transactional email', () => {
})
})

it('should send to CC recipients', async () => {
const recipients = ['[email protected]', '[email protected]']
const ccRecipients = ['[email protected]', '[email protected]']
$.step.parameters.destinationEmail = recipients.join(',')
$.step.parameters.destinationEmailCc = ccRecipients.join(',')
await expect(sendTransactionalEmail.run($)).resolves.not.toThrow()
expect($.setActionItem).toHaveBeenCalledWith({
raw: {
status: ['ACCEPTED', 'ACCEPTED'],
recipient: recipients,
subject: 'test subject',
body: 'test body',
cc: ccRecipients,
from: 'jack',
reply_to: '[email protected]',
},
})
})

it('should throw partial step error if one succeeds while the rest are blacklists', async () => {
const recipients = ['[email protected]', '[email protected]']
$.step.parameters.destinationEmail = recipients.join(',')
Expand Down Expand Up @@ -444,6 +463,7 @@ describe('send transactional email', () => {
recipient: recipients,
},
})
$.execution.testRun = false
await expect(sendTransactionalEmail.run($)).resolves.not.toThrow()
expect($.http.post).toBeCalledTimes(4)
expect($.setActionItem).toHaveBeenCalledWith({
Expand All @@ -457,4 +477,42 @@ describe('send transactional email', () => {
},
})
})

it('should send to all recipients in test runs', async () => {
const recipients = [
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
]
$.step.parameters.destinationEmail = recipients.join(',')
$.getLastExecutionStep = vi.fn().mockResolvedValueOnce({
status: 'success',
errorDetails: 'error error',
dataOut: {
status: [
'BLACKLISTED',
'ACCEPTED',
'INTERMITTENT-ERROR',
'ERROR',
'RATE-LIMITED',
],
recipient: recipients,
},
})
$.execution.testRun = true
await expect(sendTransactionalEmail.run($)).resolves.not.toThrow()
expect($.http.post).toBeCalledTimes(5)
expect($.setActionItem).toHaveBeenCalledWith({
raw: {
status: ['ACCEPTED', 'ACCEPTED', 'ACCEPTED', 'ACCEPTED', 'ACCEPTED'],
recipient: recipients,
subject: 'test subject',
body: 'test body',
from: 'jack',
reply_to: '[email protected]',
},
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,30 @@ describe('postman transactional email schema zod validation', () => {
assert(result.success === true)
expect(result.data.body).toBe('hello<br>hihi')
})

it('should validate multiple valid CC emails', () => {
validPayload.destinationEmailCc =
'[email protected], [email protected],[email protected]'
const result = transactionalEmailSchema.safeParse(validPayload)
assert(result.success === true) // using assert here for type assertion
expect(result.data.destinationEmailCc).toEqual([
'[email protected]',
'[email protected]',
'[email protected]',
])
})

const invalidCcRecipients: string[][] = [
['invalid-cc-recipient'],
['invalid-cc-recipient2', '[email protected]'],
]
it.each(invalidCcRecipients)(
'should fail if invalid CC recipient',
(...ccRecipients: string[]) => {
validPayload.destinationEmailCc = ccRecipients.join(',')
const result = transactionalEmailSchema.safeParse(validPayload)
assert(result.success === false)
expect(result.error?.errors[0].message).toEqual('Invalid CC emails')
},
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ const action: IRawAction = {
subject,
body,
destinationEmail,
destinationEmailCc,
senderName,
replyTo,
attachments = [],
} = $.step.parameters
const result = transactionalEmailSchema.safeParse({
destinationEmail,
destinationEmailCc,
senderName,
subject,
body,
Expand Down Expand Up @@ -94,7 +96,10 @@ const action: IRawAction = {
lastExecutionStep?.dataOut,
)
const isPartialRetry =
prevDataOutParseResult.success && lastExecutionStep.errorDetails
prevDataOutParseResult.success &&
lastExecutionStep.errorDetails &&
// Don't do partial retry in test runs! always send to all recipients
!$.execution.testRun

if (isPartialRetry) {
const { status, recipient } = prevDataOutParseResult.data
Expand All @@ -112,6 +117,7 @@ const action: IRawAction = {
/(<p\s?((style=")([a-zA-Z0-9:;.\s()\-,]*)("))?>)\s*(<\/p>)/g,
'<p style="margin: 0">&nbsp;</p>',
),
ccList: result.data.destinationEmailCc,
replyTo: result.data.replyTo,
senderName: result.data.senderName,
attachments: attachmentFiles,
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/apps/postman/common/email-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ interface Email {
senderName: string
attachments?: { fileName: string; data: Uint8Array }[]
replyTo?: string
ccList?: string[]
}

interface PostmanPromiseFulfilled {
Expand Down Expand Up @@ -88,6 +89,9 @@ export async function sendTransactionalEmails(
`${email.senderName} <${appConfig.postman.fromAddress}>`,
)
requestData.append('disable_tracking', 'true')
if (email.ccList?.length > 0) {
requestData.append('cc', JSON.stringify(email.ccList))
}

if (email.replyTo) {
requestData.append('reply_to', email.replyTo)
Expand Down Expand Up @@ -122,6 +126,7 @@ export async function sendTransactionalEmails(
subject,
from,
reply_to,
...(email.ccList?.length && { cc: email.ccList }),
},
} satisfies PostmanPromiseFulfilled
} catch (e) {
Expand Down
45 changes: 32 additions & 13 deletions packages/backend/src/apps/postman/common/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ function recipientStringToArray(value: string) {
return uniq(recipientArray)
}

function validateEmails(value: string, ctx: z.RefinementCtx, msg: string) {
const recipients = recipientStringToArray(value)
if (recipients.some((recipient) => !validator.validate(recipient))) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: msg,
})
}
return recipients
}

export const transactionalEmailFields: IField[] = [
{
label: 'Email subject',
Expand All @@ -36,23 +47,32 @@ export const transactionalEmailFields: IField[] = [
type: 'string' as const,
required: true,
description:
'To send to multiple recipients, comma-separate email addresses. Emails will be sent to each email address separately.',
'Enter the email addresses of the main recipients, separated by commas.\nEach recipient will receive an individual email.',
variables: true,
},
{
label: 'CC recipient email(s)',
key: 'destinationEmailCc',
type: 'string' as const,
required: false,
description:
'Enter the email addresses to CC, separated by commas.\nCC recipients will receive a copy of the email for each main recipient.',
variables: true,
},
{
label: 'Sender name',
key: 'senderName',
type: 'string' as const,
required: true,
description: 'For e.g., HR department',
description: 'For e.g., HR department.',
variables: true,
},
{
label: 'Reply-To email',
key: 'replyTo',
type: 'string' as const,
required: false,
description: 'If left blank, this will default to your email address',
description: 'If left blank, this will default to your email address.',
variables: true,
},
{
Expand All @@ -74,16 +94,15 @@ export const transactionalEmailSchema = z.object({
.min(1, { message: 'Empty body' })
// for backward-compatibility with content produced by the old editor
.transform((v) => v.replace(/\n/g, '<br>')),
destinationEmail: z.string().transform((value, ctx) => {
const recipients = recipientStringToArray(value)
if (recipients.some((recipient) => !validator.validate(recipient))) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Invalid recipient emails',
})
}
return recipients
}),
destinationEmail: z
.string()
.transform((value, ctx) =>
validateEmails(value, ctx, 'Invalid recipient emails'),
),
destinationEmailCc: z
.string()
.transform((value, ctx) => validateEmails(value, ctx, 'Invalid CC emails'))
.optional(),
replyTo: z.preprocess((value) => {
if (typeof value !== 'string') {
return value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,14 @@ describe('tiles create row action', () => {
$.user = viewer
await expect(createRowAction.run($)).rejects.toThrow(StepError)
})

it('should throw correct error if Tile deleted', async () => {
$.user = editor
await TableMetadata.query()
.patch({
deletedAt: new Date().toISOString(),
})
.where({ id: $.step.parameters.tableId })
await expect(createRowAction.run($)).rejects.toThrow(StepError)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
generateMockTableRowData,
} from '@/graphql/__tests__/mutations/tiles/table.mock'
import { createTableRow, TableRowFilter } from '@/models/dynamodb/table-row'
import TableColumnMetadata from '@/models/table-column-metadata'
import TableMetadata from '@/models/table-metadata'
import User from '@/models/user'
import Context from '@/types/express/context'
Expand Down Expand Up @@ -91,4 +92,19 @@ describe('tiles create row action', () => {
$.user = viewer
await expect(findSingleRowAction.run($)).rejects.toThrow(StepError)
})

it('should throw correct error if Tile deleted', async () => {
$.user = editor
await TableMetadata.query()
.patch({
deletedAt: new Date().toISOString(),
})
.where({ id: $.step.parameters.tableId })
await TableColumnMetadata.query()
.patch({
deletedAt: new Date().toISOString(),
})
.where({ table_id: $.step.parameters.tableId })
await expect(findSingleRowAction.run($)).rejects.toThrow(StepError)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
generateMockTableRowData,
} from '@/graphql/__tests__/mutations/tiles/table.mock'
import { createTableRow } from '@/models/dynamodb/table-row'
import TableColumnMetadata from '@/models/table-column-metadata'
import TableMetadata from '@/models/table-metadata'
import User from '@/models/user'
import Context from '@/types/express/context'
Expand Down Expand Up @@ -92,4 +93,24 @@ describe('tiles create row action', () => {
$.user = viewer
await expect(updateRowAction.run($)).rejects.toThrow(StepError)
})

it('should throw error if no tableId', async () => {
$.step.parameters.tableId = ''
await expect(updateRowAction.run($)).rejects.toThrow(StepError)
})

it('should throw correct error if Tile deleted', async () => {
$.user = editor
await TableMetadata.query()
.patch({
deletedAt: new Date().toISOString(),
})
.where({ id: $.step.parameters.tableId })
await TableColumnMetadata.query()
.patch({
deletedAt: new Date().toISOString(),
})
.where({ table_id: $.step.parameters.tableId })
await expect(updateRowAction.run($)).rejects.toThrow(StepError)
})
})
Loading

0 comments on commit 16551e1

Please sign in to comment.