Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 59 additions & 50 deletions CHANGELOG.md

Large diffs are not rendered by default.

11 changes: 2 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,13 @@ Notable features include:
- 19 different form field types, including attachments, tables, email and mobile
- Verified email and mobile phone fields via integrations with Twilio and AWS SES
- Automatic emailing of submissions for forms built with Email Mode
- End-to-end encryption for forms built with Storage Mode
- Encryption for data collected on forms built with Storage Mode
- (Singapore government agencies only) Citizen authentication with [SingPass](https://www.singpass.gov.sg/singpass/common/aboutus)
- (Singapore government agencies only) Citizen authentication with [sgID](https://www.id.gov.sg/)
- (Singapore government agencies only) Corporate authentication with [CorpPass](https://www.corppass.gov.sg/corppass/common/aboutus)
- (Singapore government agencies only) Automatic prefill of verified data with [MyInfo](https://www.singpass.gov.sg/myinfo/common/aboutus)
- Webhooks functionality via the official [FormSG JavaScript SDK](https://github.com/opengovsg/formsg-sdk) and contributor-supported [FormSG Ruby SDK](https://github.com/opengovsg/formsg-ruby-sdk)

The current product roadmap includes:

- (in progress) Frontend rewrite from [AngularJS](https://angularjs.org/) to [React](https://reactjs.org/)
- Enabling payments on forms
- Electronic signatures
- Notifications to form admins for Storage mode submissions
- Integration with vault.gov.sg
- Variable amount and Itemised payments on forms with [stripe](https://stripe.com) integration

## Local Development (Docker)

Expand Down
7 changes: 7 additions & 0 deletions __tests__/unit/backend/helpers/jest-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
IPopulatedForm,
ISubmissionSchema,
IUserSchema,
UserApiToken,
} from 'src/types'

/**
Expand Down Expand Up @@ -88,18 +89,21 @@ const insertUser = async ({
userId,
mailDomain = 'test.gov.sg',
mailName = 'test',
apiToken,
}: {
agencyId: ObjectID
userId?: ObjectID
mailName?: string
mailDomain?: string
apiToken?: UserApiToken
}): Promise<IUserSchema> => {
const User = getUserModel(mongoose)

return User.create({
email: `${mailName}@${mailDomain}`,
_id: userId,
agency: agencyId,
apiToken: apiToken,
})
}

Expand All @@ -116,13 +120,15 @@ const insertFormCollectionReqs = async ({
shortName = 'govtest',
flags,
betaFlags,
apiToken,
}: {
userId?: ObjectID
mailName?: string
mailDomain?: string
shortName?: string
flags?: { lastSeenFeatureUpdateVersion: number }
betaFlags?: IUserSchema['betaFlags']
apiToken?: UserApiToken
} = {}): Promise<{
agency: AgencyDocument
user: IUserSchema
Expand All @@ -137,6 +143,7 @@ const insertFormCollectionReqs = async ({
agency: agency._id,
flags,
betaFlags,
apiToken,
})

return { agency, user }
Expand Down
4 changes: 2 additions & 2 deletions frontend/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 frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "form-frontend",
"version": "6.69.0",
"version": "6.70.0",
"homepage": ".",
"private": true,
"dependencies": {
Expand Down
54 changes: 39 additions & 15 deletions frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from 'react'
import { Inspector, InspectParams } from 'react-dev-inspector'
import { HelmetProvider } from 'react-helmet-async'
import { QueryClient, QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'
Expand Down Expand Up @@ -40,18 +42,40 @@ datadogLogs.init({
sampleRate: 100,
})

export const App = (): JSX.Element => (
<HelmetProvider>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<AppHelmet />
<BrowserRouter>
<ChakraProvider theme={theme} resetCSS>
<AuthProvider>
<AppRouter />
</AuthProvider>
</ChakraProvider>
</BrowserRouter>
</QueryClientProvider>
</HelmetProvider>
)
export const App = (): JSX.Element => {
const isDev = process.env.NODE_ENV === 'development'

return (
<>
{isDev && (
<Inspector
// props see docs:
// https://github.com/zthxxx/react-dev-inspector#inspector-component-props
keys={['control', 'shift', 'c']}
disableLaunchEditor={true}
onClickElement={({ codeInfo }: InspectParams) => {
if (!codeInfo?.absolutePath) return
const { absolutePath, lineNumber, columnNumber } = codeInfo
// you can change the url protocol if you are using in Web IDE
window.open(
`vscode://file/${absolutePath}:${lineNumber}:${columnNumber}`,
)
}}
/>
)}
<HelmetProvider>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<AppHelmet />
<BrowserRouter>
<ChakraProvider theme={theme} resetCSS>
<AuthProvider>
<AppRouter />
</AuthProvider>
</ChakraProvider>
</BrowserRouter>
</QueryClientProvider>
</HelmetProvider>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export interface SingleSelectProviderProps<
children: React.ReactNode
/** Color scheme of component */
colorScheme?: ThemeColorScheme
/** Variant of component */
variant?: 'clear'
fullWidth?: boolean
}
export const SingleSelectProvider = ({
Expand All @@ -60,6 +62,7 @@ export const SingleSelectProvider = ({
inputAria,
colorScheme,
comboboxProps = {},
variant,
fullWidth = false,
}: SingleSelectProviderProps): JSX.Element => {
const { items, getItemByValue } = useItems({ rawItems })
Expand Down Expand Up @@ -222,6 +225,7 @@ export const SingleSelectProvider = ({
const styles = useMultiStyleConfig('SingleSelect', {
isClearable,
colorScheme,
variant,
})

const virtualListHeight = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const SelectCombobox = forwardRef<HTMLInputElement>(
align="center"
zIndex={2}
aria-hidden
sx={styles.inputStack}
>
{selectedItemMeta.icon ? (
<Icon
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/features/admin-form/common/AdminFormPageService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
EndPageUpdateDto,
PaymentsProductUpdateDto,
PaymentsUpdateDto,
StartPageUpdateDto,
} from '~shared/types'
Expand Down Expand Up @@ -58,3 +59,20 @@ export const updateFormPayments = async (
newPayments,
).then(({ data }) => data)
}

/**
* Updates the payments for the given form referenced by its id
*
* @param formId the id of the form to update payments for
* @param newPayments the new payment to replace with
* @returns the updated payment on success
*/
export const updateFormPaymentProducts = async (
formId: string,
products: PaymentsProductUpdateDto,
): Promise<PaymentsProductUpdateDto> => {
return ApiService.put<PaymentsProductUpdateDto>(
`${ADMIN_FORM_ENDPOINT}/${formId}/payments/products`,
products,
).then(({ data }) => data)
}
30 changes: 30 additions & 0 deletions frontend/src/features/admin-form/common/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
EndPageUpdateDto,
FormPermission,
FormPermissionsDto,
PaymentsProductUpdateDto,
PaymentsUpdateDto,
StartPageUpdateDto,
} from '~shared/types/form/form'
Expand Down Expand Up @@ -35,6 +36,7 @@ import { useCollaboratorWizard } from './components/CollaboratorModal/Collaborat
import { permissionsToRole } from './components/CollaboratorModal/utils'
import {
updateFormEndPage,
updateFormPaymentProducts,
updateFormPayments,
updateFormStartPage,
} from './AdminFormPageService'
Expand Down Expand Up @@ -422,10 +424,38 @@ export const useMutateFormPage = () => {
},
)

const paymentsProductMutation = useMutation(
(products: PaymentsProductUpdateDto) =>
updateFormPaymentProducts(formId, products),
{
onSuccess: (newData) => {
toast.closeAll()
queryClient.setQueryData<AdminStorageFormDto | undefined>(
adminFormKeys.products(formId, newData),
(oldData) =>
oldData
? {
...oldData,
payments_field: {
...oldData.payments_field,
products: newData,
},
}
: undefined,
)
toast({
description: 'Payments product was updated.',
})
},
onError: handleError,
},
)

return {
startPageMutation,
endPageMutation,
paymentsMutation,
paymentsProductMutation,
}
}

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/features/admin-form/common/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo } from 'react'
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'
import { useParams } from 'react-router-dom'

import { Product } from '~shared/types'
import { AdminFormDto, PreviewFormViewDto } from '~shared/types/form/form'

import { ApiError } from '~typings/core'
Expand Down Expand Up @@ -29,6 +30,8 @@ export const adminFormKeys = {
[...adminFormKeys.id(id), 'previewForm'] as const,
viewFormTemplate: (id: string) =>
[...adminFormKeys.id(id), 'viewFormTemplate'] as const,
products: (id: string, products: Product[]) =>
[...adminFormKeys.id(id), 'products', ...products] as const,
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useUser } from '~features/user/queries'

import { useCreateTabForm } from '../../../builder-and-design/useCreateTabForm'
import { CreatePageDrawerCloseButton } from '../../../common'
import { FieldListTabIndex } from '../../constants'

import {
BasicFieldPanel,
Expand All @@ -36,6 +37,30 @@ export const FieldListDrawer = (): JSX.Element => {
const displayPayments =
user?.betaFlags?.payment || flags?.has(featureFlags.payment)

const tabsDataList = [
{
header: 'Basic',
component: BasicFieldPanel,
isHidden: false,
isDisabled: isLoading,
key: FieldListTabIndex.Basic,
},
{
header: 'MyInfo',
component: MyInfoFieldPanel,
isHidden: false,
isDisabled: isLoading,
key: FieldListTabIndex.MyInfo,
},
{
header: 'Payments',
component: PaymentsInputPanel,
isHidden: !displayPayments,
isDisabled: isLoading,
key: FieldListTabIndex.Payments,
},
].filter((tab) => !tab.isHidden)

return (
<Tabs
pos="relative"
Expand All @@ -54,24 +79,20 @@ export const FieldListDrawer = (): JSX.Element => {
<CreatePageDrawerCloseButton />
</Flex>
<TabList mx="-0.25rem" w="100%">
<Tab isDisabled={isLoading}>Basic</Tab>
<Tab isDisabled={isLoading}>MyInfo</Tab>
{displayPayments && <Tab isDisabled={isLoading}>Payments</Tab>}
{tabsDataList.map((tab) => (
<Tab key={tab.key} isDisabled={tab.isDisabled}>
{tab.header}
</Tab>
))}
</TabList>
<Divider w="auto" mx="-1.5rem" />
</Box>
<TabPanels pb="1rem" flex={1} overflowY="auto">
<TabPanel>
<BasicFieldPanel />
</TabPanel>
<TabPanel>
<MyInfoFieldPanel />
</TabPanel>
{displayPayments && (
<TabPanel>
<PaymentsInputPanel />
{tabsDataList.map((tab) => (
<TabPanel key={tab.key}>
<tab.component />
</TabPanel>
)}
))}
</TabPanels>
</Tabs>
)
Expand Down
Loading