Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: release v6.91.0 #6918

Merged
merged 5 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@ All notable changes to this project will be documented in this file. Dates are d

Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).

#### [v6.91.0](https://github.com/opengovsg/FormSG/compare/v6.90.0...v6.91.0)

- fix(markdown): refine regex to handle newlines after indentation groups [`#6917`](https://github.com/opengovsg/FormSG/pull/6917)
- fix: omit isVisible property from webhook response [`#6907`](https://github.com/opengovsg/FormSG/pull/6907)
- feat: charts [`#6790`](https://github.com/opengovsg/FormSG/pull/6790)
- build: merge release 6.90.0 to develop [`#6914`](https://github.com/opengovsg/FormSG/pull/6914)
- build: release v6.90.0 [`#6913`](https://github.com/opengovsg/FormSG/pull/6913)

#### [v6.90.0](https://github.com/opengovsg/FormSG/compare/v6.89.2...v6.90.0)

> 20 November 2023

- chore: drop fallback to encrypt mode entirely [`#6912`](https://github.com/opengovsg/FormSG/pull/6912)
- fix(deps): bump type-fest from 4.7.1 to 4.8.1 in /shared [`#6911`](https://github.com/opengovsg/FormSG/pull/6911)
- build: merge Release 6.89.2 into develop [`#6910`](https://github.com/opengovsg/FormSG/pull/6910)
- chore: Revert remove eb shift frontend feature flags (#6869) [`#6909`](https://github.com/opengovsg/FormSG/pull/6909)
- build: merge release v6.89.1 into develop [`#6905`](https://github.com/opengovsg/FormSG/pull/6905)
- chore: bump version to v6.90.0 [`c03692e`](https://github.com/opengovsg/FormSG/commit/c03692e3d9aa64afa8007dffecfd9871542f4759)

#### [v6.89.2](https://github.com/opengovsg/FormSG/compare/v6.89.0...v6.89.2)

Expand Down
393 changes: 391 additions & 2 deletions frontend/package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "form-frontend",
"version": "6.90.0",
"version": "6.91.0",
"homepage": ".",
"private": true,
"dependencies": {
Expand All @@ -15,6 +15,7 @@
"@stablelib/base64": "^1.0.1",
"@stripe/react-stripe-js": "^1.15.0",
"@stripe/stripe-js": "^1.44.1",
"@types/stopword": "^2.0.1",
"axios": "^1.6.2",
"broadcast-channel": "^4.13.0",
"browser-image-compression": "^2.0.2",
Expand Down Expand Up @@ -50,6 +51,7 @@
"react-dom": "^17.0.2",
"react-dropzone": "^11.4.2",
"react-focus-lock": "^2.7.1",
"react-google-charts": "^4.0.1",
"react-helmet-async": "^1.2.3",
"react-hook-form": "^7.28.0",
"react-i18next": "^11.16.7",
Expand All @@ -67,11 +69,13 @@
"react-use-scrollspy": "^3.0.2",
"react-virtuoso": "^2.14.0",
"react-waypoint": "^10.1.0",
"react-wordcloud": "^1.2.7",
"remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1",
"rooks": "^5.11.0",
"simplur": "^3.0.1",
"spark-md5": "^3.0.2",
"stopword": "^2.0.8",
"stripe": "^11.1.0",
"timezone-mock": "^1.3.6",
"type-fest": "^2.8.0",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/app/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
PAYMENT_PAGE_SUBROUTE,
PRIVACY_POLICY_ROUTE,
PUBLICFORM_ROUTE,
RESULTS_CHARTS_SUBROUTE,
RESULTS_FEEDBACK_SUBROUTE,
TOU_ROUTE,
USE_TEMPLATE_REDIRECT_SUBROUTE,
Expand All @@ -35,6 +36,7 @@ import {
ResponsesLayout,
ResponsesPage,
} from '~features/admin-form/responses'
import { ChartsPage } from '~features/admin-form/responses/ChartsPage/ChartsPage'
import { SettingsPage } from '~features/admin-form/settings/SettingsPage'
import { SelectProfilePage } from '~features/login'
import { FormPaymentPage } from '~features/public-form/components/FormPaymentPage/FormPaymentPage'
Expand Down Expand Up @@ -171,6 +173,9 @@ export const AppRouter = (): JSX.Element => {
path={RESULTS_FEEDBACK_SUBROUTE}
element={<FeedbackPage />}
/>
<Route path={RESULTS_CHARTS_SUBROUTE} element={<ResponsesLayout />}>
<Route index element={<ChartsPage />} />
</Route>
</Route>
</Route>
<Route
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/components/DateRangePicker/helpers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { format, isValid } from 'date-fns'

import { DateString } from '~shared/types'

import { DateRangeValue } from '~components/Calendar'

export const dateStringToDatePickerValue = (range: DateString[]) => {
const [start, end] = range
// Convert to Date objects
const startDate = new Date(start)
const endDate = new Date(end)
const result: (Date | null)[] = [null, null]
// Check if dates are valid
if (isValid(startDate)) {
result[0] = startDate
}
if (isValid(endDate)) {
result[1] = endDate
}
return result as DateRangeValue
}

export const datePickerValueToDateString = (range: DateRangeValue) => {
const [start, end] = range
const result: DateString[] = []
if (start) {
result.push(format(start, 'yyyy-MM-dd') as DateString)
}
if (end) {
result.push(format(end, 'yyyy-MM-dd') as DateString)
}
return result
}
1 change: 1 addition & 0 deletions frontend/src/components/DateRangePicker/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './DateRangePicker'
export * as dateRangePickerHelper from './helpers'
20 changes: 19 additions & 1 deletion frontend/src/components/MarkdownText/MarkdownText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,25 @@ export const MarkdownText = ({
const processedRawString = useMemo(() => {
// Create new line nodes for every new line in raw string so new lines gets rendered.
if (multilineBreaks) {
return children.replace(/\n/gi, '&nbsp; \n')
/**
* Matching new lines that are not preceded by a token that indents.
*
* (?<!{regex}): negative lookbehind to ensure that the following regex does not match
*
* (-|\d+\.|\*): matching character tokens that indents
* -: "-"
* *: "*",
* \d+ : "1.", "2.", etc.
*
* \s: whitespace following the token, indentation groups must start with token followed by a whitespace character
*
* .*: any character, any number of times, this is the actual text content of the line
*
* \n: new line character
*
* \n: the new line character that we will want markdown to render as a new line
*/
return children.replace(/(?<!(-|\d+\.|\*)\s.*\n)\n/gi, '&nbsp; \n')
}
return children
}, [children, multilineBreaks])
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/constants/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const LOCAL_STORAGE_EVENT = 'local-storage'
* Key to store whether a user has seen the rollout announcements before.
*/
export const ROLLOUT_ANNOUNCEMENT_KEY_PREFIX =
'has-seen-rollout-announcement-20231116-'
'has-seen-rollout-announcement-20231121-'

/**
* Key to store whether the admin has seen the feature tour in localStorage.
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ export const ACTIVE_ADMINFORM_BUILDER_ROUTE_REGEX = new RegExp(
/** Responses tab has no subroute, its the index results route. */
export const RESULTS_RESPONSES_SUBROUTE = ''
export const RESULTS_FEEDBACK_SUBROUTE = 'feedback'
export const RESULTS_CHARTS_SUBROUTE = 'charts'

export const ACTIVE_ADMINFORM_RESULTS_ROUTE_REGEX = new RegExp(
`${ADMINFORM_ROUTE}/([a-fA-F0-9]{24})/${ADMINFORM_RESULTS_SUBROUTE}(/${RESULTS_FEEDBACK_SUBROUTE})?/?`,
`${ADMINFORM_ROUTE}/([a-fA-F0-9]{24})/${ADMINFORM_RESULTS_SUBROUTE}(/${RESULTS_FEEDBACK_SUBROUTE}|/${RESULTS_CHARTS_SUBROUTE})?/?`,
'i',
)
export const PAYMENT_PAGE_SUBROUTE = 'payment/:paymentId'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { DateString } from '~shared/types'
import {
FormSubmissionMetadataQueryDto,
StorageModeChartsDto,
StorageModeSubmissionDto,
StorageModeSubmissionMetadataList,
SubmissionCountQueryDto,
Expand Down Expand Up @@ -100,3 +102,61 @@ export const getDecryptedSubmissionById = async ({
responses: processedContent,
}
}

const getAllEncryptedSubmission = async ({
formId,
startDate,
endDate,
}: {
formId: string
startDate?: DateString
endDate?: DateString
}): Promise<StorageModeChartsDto[]> => {
const queryUrl = `${ADMIN_FORM_ENDPOINT}/${formId}/submissions`
if (startDate && endDate) {
return ApiService.get(queryUrl, {
params: {
startDate,
endDate,
},
}).then(({ data }) => data)
}
return ApiService.get(queryUrl).then(({ data }) => data)
}

type DecryptedContent = NonNullable<ReturnType<typeof formsgSdk.crypto.decrypt>>
export type DecryptedSubmission = DecryptedContent & {
submissionTime: string
}

export const getAllDecryptedSubmission = async ({
formId,
secretKey,
startDate,
endDate,
}: {
formId: string
secretKey?: string
startDate?: DateString
endDate?: DateString
}): Promise<DecryptedSubmission[]> => {
if (!secretKey) return []

const allEncryptedData = await getAllEncryptedSubmission({
formId,
startDate,
endDate,
})

return allEncryptedData.map((encryptedData) => {
const decryptedContent = formsgSdk.crypto.decrypt(secretKey, {
encryptedContent: encryptedData.encryptedContent,
verifiedContent: encryptedData.verifiedContent,
version: encryptedData.version,
})

if (!decryptedContent) throw new Error('Could not decrypt the response')

return { ...decryptedContent, submissionTime: encryptedData.created }
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useLocation } from 'react-router-dom'
import { Box, Container, Divider, Stack } from '@chakra-ui/react'

import { FormResponseMode } from '~shared/types/form'

import { ACTIVE_ADMINFORM_RESULTS_ROUTE_REGEX } from '~constants/routes'
import { useToast } from '~hooks/useToast'

import { useAdminForm } from '~features/admin-form/common/queries'

import { SecretKeyVerification } from '../components/SecretKeyVerification'
import { ResponsesPageSkeleton } from '../ResponsesPage/ResponsesPageSkeleton'
import { useStorageResponsesContext } from '../ResponsesPage/storage'

import { ChartsSvgr } from './UnlockedCharts/assets/svgr/ChartsSvgr'
import { ChartsSupportedFieldsInfoBox } from './UnlockedCharts/components/ChartsSupportedFieldsInfoBox'
import { EmptyChartsContainer } from './UnlockedCharts/components/EmptyChartsContainer'
import UnlockedCharts from './UnlockedCharts'

export const ChartsPage = (): JSX.Element => {
const { data: form, isLoading } = useAdminForm()
const { totalResponsesCount, secretKey } = useStorageResponsesContext()
const { pathname } = useLocation()

const toast = useToast({ status: 'danger' })

if (isLoading) return <ResponsesPageSkeleton />

if (!form) {
toast({
description:
'There was an error retrieving your form. Please try again later.',
})
return <ResponsesPageSkeleton />
}

// Charts is not available for Email response
// Since there's no entry to the charts page for Email mode we should
// forcefully redirect the user to the responses page
// we need to redirect to one level up, i.e., '../'
if (form.responseMode === FormResponseMode.Email) {
/**
* 0: "/admin/form/<form_id>/results/charts"
* 1: "<form_id>"
* 2: "/charts"
*/
const match = pathname.match(ACTIVE_ADMINFORM_RESULTS_ROUTE_REGEX)
const subroute = match?.[2]
if (subroute) {
const pathnameWithoutSubroute = pathname.replace(subroute, '')
window.location.replace(pathnameWithoutSubroute)
}
return <></>
}

if (totalResponsesCount === 0) {
return (
<EmptyChartsContainer
title="No charts generated yet."
subtitle="Charts will be generated when you receive responses on your form."
/>
)
}

return secretKey ? (
<UnlockedCharts />
) : (
<>
<SecretKeyVerification
hideResponseCount
heroSvg={<ChartsSvgr />}
ctaText="View charts"
label="Enter or upload Secret Key to view charts"
/>
<Container p={0} maxW="42.5rem">
<Box mt="2rem" mb="0.5rem">
<Divider />
</Box>
<Stack>
<ChartsSupportedFieldsInfoBox />
</Stack>
</Container>
</>
)
}
Loading
Loading