From 3e5238620947b379252abbc3c57d0e6e0f8abf85 Mon Sep 17 00:00:00 2001 From: bookwormsuf Date: Wed, 17 Dec 2025 19:29:24 +0800 Subject: [PATCH 1/6] navigation update for results page --- .../Pagination/PaginationMobile.tsx | 8 +- .../responses/ChartsPage/ChartsPage.tsx | 66 ++++++--- .../UnlockedChartsContainer.tsx | 8 +- .../components/EmptyChartsContainer.tsx | 13 +- .../EmptyFeedback/EmptyFeedback.tsx | 9 +- .../responses/FeedbackPage/FeedbackPage.tsx | 51 +++---- .../responses/FormResultsLayout.tsx | 99 +++++++++++-- .../IndividualResponsePage.tsx | 8 +- .../ResponsesPage/ResponsesLayout.tsx | 9 +- .../common/EmptyResponses/EmptyResponses.tsx | 9 +- .../common/ResponsesTabWrapper.tsx | 12 +- .../storage/StorageResponsesTab.tsx | 27 ++-- .../UnlockedResponses/DownloadButton.tsx | 2 +- .../ResponsesTable/ResponsesTable.tsx | 110 +++++++------- .../UnlockedResponses/UnlockedResponses.tsx | 136 ++++++++++++----- .../FormResultsNavbar/FormResultsNavbar.tsx | 138 ++++++++++-------- .../FormResultsNavbar/ResultsTab.tsx | 37 +++++ .../SecretKeyVerification.tsx | 11 +- .../admin-form/settings/SettingsPage.tsx | 5 +- 19 files changed, 505 insertions(+), 253 deletions(-) create mode 100644 frontend/src/features/admin-form/responses/components/FormResultsNavbar/ResultsTab.tsx diff --git a/frontend/src/components/Pagination/PaginationMobile.tsx b/frontend/src/components/Pagination/PaginationMobile.tsx index 662a488fe2..3b0a9274ca 100644 --- a/frontend/src/components/Pagination/PaginationMobile.tsx +++ b/frontend/src/components/Pagination/PaginationMobile.tsx @@ -47,7 +47,13 @@ export const PaginationMobile = ({ onClick={handlePageBack} icon={} /> - + {t('components.pagination.paginationMobile.currentPageCount', { currentPage, totalPageCount, diff --git a/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx b/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx index 90ee8b7581..caee0d6de9 100644 --- a/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx +++ b/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx @@ -1,6 +1,14 @@ import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' -import { Box, Container, Divider, Stack } from '@chakra-ui/react' +import { + Box, + Container, + Divider, + Flex, + Skeleton, + Stack, + Text, +} from '@chakra-ui/react' import { useFeatureValue } from '@growthbook/growthbook-react' import { featureFlags } from '~shared/constants' @@ -23,7 +31,11 @@ import UnlockedCharts from './UnlockedCharts' export const ChartsPage = (): JSX.Element => { const { t } = useTranslation() const { data: form, isLoading } = useAdminForm() - const { totalResponsesCount, secretKey } = useStorageResponsesContext() + const { + totalResponsesCount, + secretKey, + isLoading: isResponsesLoading, + } = useStorageResponsesContext() const { pathname } = useLocation() const chartsMaxResponseCount = useFeatureValue( featureFlags.chartsMaxResponseCount, @@ -91,24 +103,38 @@ export const ChartsPage = (): JSX.Element => { ) : ( <> - } - ctaText={t( - 'features.adminForm.responses.charts.chartsPage.secretKeyVerification.ctaText', - )} - label={t( - 'features.adminForm.responses.charts.chartsPage.secretKeyVerification.label', - )} - /> - - - - - - - - + + + } + ctaText={t( + 'features.adminForm.responses.charts.chartsPage.secretKeyVerification.ctaText', + )} + label={t( + 'features.adminForm.responses.charts.chartsPage.secretKeyVerification.label', + )} + /> + + + + + + + + + + + + ) } diff --git a/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/UnlockedChartsContainer.tsx b/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/UnlockedChartsContainer.tsx index 2036b81bd8..b6868b59b3 100644 --- a/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/UnlockedChartsContainer.tsx +++ b/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/UnlockedChartsContainer.tsx @@ -134,7 +134,11 @@ export const UnlockedChartsContainer = () => { .filter(isNonEmpty) return ( - <> + { )} /> )} - + ) } diff --git a/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/components/EmptyChartsContainer.tsx b/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/components/EmptyChartsContainer.tsx index 7acae7c99d..a5c271668b 100644 --- a/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/components/EmptyChartsContainer.tsx +++ b/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/components/EmptyChartsContainer.tsx @@ -1,5 +1,4 @@ -import { Box, Container, Divider, Stack, Text } from '@chakra-ui/react' - +import { Box, Divider, Flex, Stack, Text } from '@chakra-ui/react' import { ChartsSvgr } from '../assets/svgr/ChartsSvgr' import { ChartsSupportedFieldsInfoBox } from './ChartsSupportedFieldsInfoBox' @@ -12,7 +11,13 @@ export const EmptyChartsContainer = ({ subtitle: string }): JSX.Element => { return ( - + {title} @@ -26,6 +31,6 @@ export const EmptyChartsContainer = ({ - + ) } diff --git a/frontend/src/features/admin-form/responses/FeedbackPage/EmptyFeedback/EmptyFeedback.tsx b/frontend/src/features/admin-form/responses/FeedbackPage/EmptyFeedback/EmptyFeedback.tsx index 00c57eaa9b..7089e6a120 100644 --- a/frontend/src/features/admin-form/responses/FeedbackPage/EmptyFeedback/EmptyFeedback.tsx +++ b/frontend/src/features/admin-form/responses/FeedbackPage/EmptyFeedback/EmptyFeedback.tsx @@ -10,7 +10,14 @@ export const EmptyFeedback = (): JSX.Element => { const { t } = useTranslation() return ( - + {t('features.adminForm.feedback.emptyFeedback.noFeedbackYet')} diff --git a/frontend/src/features/admin-form/responses/FeedbackPage/FeedbackPage.tsx b/frontend/src/features/admin-form/responses/FeedbackPage/FeedbackPage.tsx index 1745edca85..f27582b871 100644 --- a/frontend/src/features/admin-form/responses/FeedbackPage/FeedbackPage.tsx +++ b/frontend/src/features/admin-form/responses/FeedbackPage/FeedbackPage.tsx @@ -2,15 +2,7 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { UseMutationResult } from 'react-query' import { useParams } from 'react-router-dom' -import { - Box, - ButtonGroup, - Container, - Flex, - Grid, - Icon, - Text, -} from '@chakra-ui/react' +import { Box, ButtonGroup, Flex, Grid, Icon, Text } from '@chakra-ui/react' import { ProcessedFeedbackMeta, ProcessedIssueMeta } from '~shared/types' @@ -188,29 +180,24 @@ export const FeedbackPage = (): JSX.Element => { if (issueProps.count === 0 && reviewProps.count === 0) { return } - return ( - - - + {currentFeedbackType === FeedbackType.Issues ? ( { translations={reviewProps.translations} /> )} - - + + + {' '} - + + {' '} { isMobile={isMobile} /> - + {' '} { onPageChange={setCurrentPage} /> - + ) } diff --git a/frontend/src/features/admin-form/responses/FormResultsLayout.tsx b/frontend/src/features/admin-form/responses/FormResultsLayout.tsx index 326bf8e7d5..381f343b16 100644 --- a/frontend/src/features/admin-form/responses/FormResultsLayout.tsx +++ b/frontend/src/features/admin-form/responses/FormResultsLayout.tsx @@ -1,16 +1,97 @@ -import { Outlet } from 'react-router-dom' -import { Flex } from '@chakra-ui/react' +import { useCallback } from 'react' +import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom' +import { + Box, + Flex, + Spacer, + TabList, + TabPanel, + TabPanels, + Tabs, +} from '@chakra-ui/react' + +import { + ACTIVE_ADMINFORM_RESULTS_ROUTE_REGEX, + ADMINFORM_RESULTS_SUBROUTE, + ADMINFORM_ROUTE, + RESULTS_CHARTS_SUBROUTE, + RESULTS_FEEDBACK_SUBROUTE, + RESULTS_RESPONSES_SUBROUTE, +} from '~constants/routes' +import { useDraggable } from '~hooks/useDraggable' import { FormResultsNavbar } from './components/FormResultsNavbar' -/** - * Page for rendering subroutes via `Outlet` component for admin form result pages. - */ export const FormResultsLayout = (): JSX.Element => { + const { ref, onMouseDown } = useDraggable() + const { formId } = useParams() + const navigate = useNavigate() + const { pathname } = useLocation() + + if (!formId) throw new Error('No formId provided') + + const checkTabActive = useCallback( + (to: string) => { + const match = pathname.match(ACTIVE_ADMINFORM_RESULTS_ROUTE_REGEX) + return (match?.[2] ?? '/') === `/${to}` + }, + [pathname], + ) + + const tabConfig = [ + { path: RESULTS_RESPONSES_SUBROUTE }, + { path: RESULTS_FEEDBACK_SUBROUTE }, + { path: RESULTS_CHARTS_SUBROUTE }, + ] + + const tabIndex = tabConfig.findIndex((tab) => checkTabActive(tab.path)) + + const handleTabChange = (index: number) => { + const subRoute = tabConfig[index].path + const path = subRoute + ? `${ADMINFORM_ROUTE}/${formId}/${ADMINFORM_RESULTS_SUBROUTE}/${subRoute}` + : `${ADMINFORM_ROUTE}/${formId}/${ADMINFORM_RESULTS_SUBROUTE}` + navigate(path) + } + return ( - - - - + + {' '} + + + + + + + + + + ) } diff --git a/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx b/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx index 5623c79bb4..0cb7a49a1c 100644 --- a/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx +++ b/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx @@ -201,9 +201,13 @@ export const IndividualResponsePage = (): JSX.Element => { const workflowNumTotalSteps = data?.mrf?.workflowNumTotalSteps return ( - + + {' '} - { return ( - - - - - + + + ) } diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/common/EmptyResponses/EmptyResponses.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/common/EmptyResponses/EmptyResponses.tsx index b98368d860..042442cc0a 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/common/EmptyResponses/EmptyResponses.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/common/EmptyResponses/EmptyResponses.tsx @@ -9,7 +9,14 @@ import { EmptyResponsesSvgr } from './EmptyResponsesSvgr' export function EmptyResponses(): JSX.Element { const { t } = useTranslation() return ( - + {t('features.adminForm.responses.responsesPage.emptyResponses.title')} diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/common/ResponsesTabWrapper.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/common/ResponsesTabWrapper.tsx index fcaefe466b..e9f3faf96f 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/common/ResponsesTabWrapper.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/common/ResponsesTabWrapper.tsx @@ -1,4 +1,4 @@ -import { Box, Container } from '@chakra-ui/react' +import { Box } from '@chakra-ui/react' export const ResponsesTabWrapper = ({ children, @@ -6,18 +6,16 @@ export const ResponsesTabWrapper = ({ children: React.ReactNode }): JSX.Element => { return ( - - + {children} - + ) } diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/StorageResponsesTab.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/storage/StorageResponsesTab.tsx index 1ca7102cff..c2680b292d 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/StorageResponsesTab.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/StorageResponsesTab.tsx @@ -1,4 +1,5 @@ import { useTranslation } from 'react-i18next' +import { Container, Flex } from '@chakra-ui/react' import { FormActivationSvg } from '~features/admin-form/settings/components/FormActivationSvg' @@ -19,14 +20,22 @@ export const StorageResponsesTab = (): JSX.Element => { return secretKey ? ( ) : ( - } - ctaText={t( - 'features.adminForm.responses.responsesPage.storage.storageResponsesTab.secretKeyVerification.ctaText', - )} - label={t( - 'features.adminForm.responses.responsesPage.storage.storageResponsesTab.secretKeyVerification.label', - )} - /> + + + } + ctaText={t( + 'features.adminForm.responses.responsesPage.storage.storageResponsesTab.secretKeyVerification.ctaText', + )} + label={t( + 'features.adminForm.responses.responsesPage.storage.storageResponsesTab.secretKeyVerification.label', + )} + /> + + ) } diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadButton.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadButton.tsx index fd8d66299e..9a576f6ba1 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadButton.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadButton.tsx @@ -196,7 +196,7 @@ export const DownloadButton = (): JSX.Element => { )} - + {({ isOpen }) => ( <> diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx index b8a2a50138..6e76369463 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx @@ -108,27 +108,44 @@ function NotApprovedBadge() { ) } +// Column width presets - shirt sizes for table columns +const COLUMN_SIZES = { + xs: { width: 80, minWidth: 60, maxWidth: 100 }, // Tiny columns like "#" + sm: { width: 120, minWidth: 100, maxWidth: 150 }, // Small columns like "Status" + md: { width: 180, minWidth: 120, maxWidth: 250 }, // Medium columns like "Email" + lg: { width: 250, minWidth: 200, maxWidth: 350 }, // Large columns like "Timestamp" + xl: { width: 300, minWidth: 200, maxWidth: 400 }, // Extra large like "Response ID" +} + +// Helper function to create column width config +const columnWidth = ( + size: keyof typeof COLUMN_SIZES, + options?: { + disableResizing?: boolean + customWidth?: number + minWidth?: number + maxWidth?: number + }, +) => ({ + ...COLUMN_SIZES[size], + ...options, + ...(options?.customWidth && { width: options.customWidth }), +}) const BASE_RESPONSE_TABLE_COLUMNS: Column[] = [ { Header: '#', accessor: 'number', - width: 80, // width is used for both the flex-basis and flex-grow - minWidth: 80, // minWidth is only used as a limit for resizing - maxWidth: 100, // maxWidth is only used as a limit for resizing + ...columnWidth('xs'), }, { Header: 'Response ID', accessor: 'refNo', - width: 300, - minWidth: 300, - maxWidth: 300, + ...columnWidth('xl'), }, { Header: 'Timestamp', accessor: 'submissionTime', - width: 250, - minWidth: 250, - disableResizing: true, + ...columnWidth('lg', { disableResizing: true }), }, ] @@ -141,8 +158,7 @@ const PAYMENT_COLUMNS: Column[] = [ } return payments.email }, - minWidth: 250, - width: 250, + ...columnWidth('md'), }, { @@ -153,8 +169,7 @@ const PAYMENT_COLUMNS: Column[] = [ } return `${centsToDollars(payments.paymentAmt)}` }, - minWidth: 150, - width: 150, + ...columnWidth('sm'), }, { @@ -169,15 +184,13 @@ const PAYMENT_COLUMNS: Column[] = [ return `${centsToDollars(payments.transactionFee)}` }, - minWidth: 150, - width: 150, + ...columnWidth('sm', { customWidth: 100 }), }, { Header: 'Net Amount (S$)', // (amt they receive in bank) accessor: ({ payments }) => getNetAmount(payments), - minWidth: 150, - width: 150, + ...columnWidth('sm'), }, { @@ -188,9 +201,7 @@ const PAYMENT_COLUMNS: Column[] = [ } return payments.payoutDate }, - minWidth: 200, - width: 200, - disableResizing: true, + ...columnWidth('md', { customWidth: 150, disableResizing: true }), }, ] @@ -198,16 +209,12 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column[] = [ { Header: '#', accessor: 'number', - width: 80, - minWidth: 80, - maxWidth: 100, + ...columnWidth('xs', { customWidth: 60, minWidth: 40 }), }, { Header: 'Response ID', accessor: 'refNo', - width: 240, - minWidth: 240, - maxWidth: 240, + ...columnWidth('md', { customWidth: 160, minWidth: 100, maxWidth: 240 }), }, { Header: MRF_WORKFLOW_STATUS_LABEL, @@ -228,9 +235,7 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column[] = [ return } }, - width: 160, - minWidth: 160, - maxWidth: 160, + ...columnWidth('sm', { minWidth: 90, maxWidth: 160 }), }, { Header: MRF_PENDING_RESPONSE_AT_LABEL, @@ -251,25 +256,12 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column[] = [ workflowNumTotalSteps, }) }, - width: 180, - minWidth: 180, - maxWidth: 180, + ...columnWidth('md', { customWidth: 140, maxWidth: 180 }), }, { Header: MRF_RESPONSE_TIMESTAMP_LABEL, accessor: 'submissionTime', - // TODO(FRM-1933): using submissionTime as we are undecided on showing first submission vs lastSubmittedAt - // accessor: ({ mrf }) => - // mrf?.lastSubmittedAt - // ? formatInTimeZone( - // mrf.lastSubmittedAt, - // 'Asia/Singapore', - // 'do MMM yyyy, hh:mm:ss a', - // ) - // : '', - width: 240, - minWidth: 240, - maxWidth: 240, + ...columnWidth('md', { minWidth: 140, maxWidth: 240 }), }, { Header: MRF_REMINDERS_LABEL, @@ -283,11 +275,9 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column[] = [ ) : null }, - minWidth: 160, - width: 160, + ...columnWidth('md'), }, ] - const PAYMENT_RESPONSE_TABLE_COLUMNS = BASE_RESPONSE_TABLE_COLUMNS.concat(PAYMENT_COLUMNS) @@ -379,6 +369,8 @@ export const ResponsesTable = () => { variant="solid" colorScheme="secondary" {...getTableProps()} + minW="fit-content" // Let table be its natural width based on columns + w="100%" > {headerGroups.map((headerGroup) => ( @@ -395,6 +387,9 @@ export const ResponsesTable = () => { pos="relative" {...column.getHeaderProps()} key={column.getHeaderProps().key} + minW={0} // Allow header to shrink but not overlap + flexShrink={0} // Prevent headers from shrinking below their minWidth + overflow="hidden" // Prevent content from overflowing > {column.render('Header')} @@ -404,7 +399,7 @@ export const ResponsesTable = () => { justify="center" top={0} right={0} - zIndex={1} + zIndex={2} transitionProperty="background" transitionDuration="normal" pos="absolute" @@ -444,12 +439,9 @@ export const ResponsesTable = () => { handleRowClick(row.values.refNo, row.values.number) } cursor="pointer" - _hover={{ - bg: 'primary.100', - }} - _active={{ - bg: 'primary.200', - }} + minWidth="100%" + display="flex" + role="group" // ✨ Add this to make parent hoverable > {row.cells.map((cell) => { return ( @@ -459,6 +451,16 @@ export const ResponsesTable = () => { key={cell.getCellProps().key} display="flex" alignItems="center" + minW={0} + flexShrink={0} + overflow="hidden" + _groupHover={{ + bg: 'primary.100', // ✨ Hover on row affects all cells + }} + _active={{ + bg: 'primary.200', + }} + transition="background 0.15s ease" // ✨ Smooth transition > {cell.render('Cell')} diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/UnlockedResponses.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/UnlockedResponses.tsx index 847f85b5bd..207c141aea 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/UnlockedResponses.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/UnlockedResponses.tsx @@ -37,51 +37,77 @@ export const UnlockedResponses = (): JSX.Element => { const { dateRange, setDateRange } = useStorageResponsesContext() return ( - - + {/* On small screens: stacked (3 rows), On larger screens: horizontal */} + - - - - - {countToUse?.toLocaleString()} - {' '} - {t( - submissionId - ? 'features.adminForm.responses.responsesPage.storage.unlockedResponses.unlockedResponses.resultsFound' - : 'features.adminForm.responses.responsesPage.storage.unlockedResponses.unlockedResponses.responsesToDate', - { count: countToUse ?? 0 }, - )} - - - + + + + + {countToUse?.toLocaleString()} + {' '} + {t( + submissionId + ? 'features.adminForm.responses.responsesPage.storage.unlockedResponses.unlockedResponses.resultsFound' + : 'features.adminForm.responses.responsesPage.storage.unlockedResponses.unlockedResponses.responsesToDate', + { count: countToUse ?? 0 }, + )} + + + - - + + + + {/* Combined: Works on both mobile and desktop */} { /> - - - + + - - + { - const { ref, onMouseDown } = useDraggable() +import { ResultsTab } from './ResultsTab' - const { data: form } = useAdminForm() +interface TabEntry { + label: string + icon: IconType + path: string + showBadge?: boolean + badgeText?: string +} +export const FormResultsNavbar = (): JSX.Element => { + const { formId } = useParams() + const navigate = useNavigate() + const { data: form } = useAdminForm() const { pathname } = useLocation() + const { t } = useTranslation() + + if (!formId) throw new Error('No formId provided') const checkTabActive = useCallback( (to: string) => { @@ -34,65 +48,69 @@ export const FormResultsNavbar = (): JSX.Element => { [pathname], ) - const isChartsEnabled = useFeatureValue('charts', false) // disabled by default + const isChartsEnabled = useFeatureValue('charts', false) const isFormEncryptMode = form?.responseMode === FormResponseMode.Encrypt const shouldShowCharts = isFormEncryptMode && isChartsEnabled - const { t } = useTranslation() + // Temporary debug - remove after checking + console.log('Charts Debug:', { + isChartsEnabled, + isFormEncryptMode, + formResponseMode: form?.responseMode, + shouldShowCharts, + }) + + const tabConfig: TabEntry[] = [ + { + label: t('features.common.responses'), + icon: BiTable, + path: RESULTS_RESPONSES_SUBROUTE, + }, + { + label: t('features.common.feedback'), + icon: BiCommentDetail, + path: RESULTS_FEEDBACK_SUBROUTE, + }, + ...(shouldShowCharts + ? [ + { + label: t('features.common.charts'), + icon: BiBarChartAlt2, + path: RESULTS_CHARTS_SUBROUTE, + showBadge: true, + badgeText: t('features.common.beta'), + }, + ] + : []), + ] + + const handleTabChange = (index: number) => { + const subRoute = tabConfig[index].path + const path = subRoute + ? `${ADMINFORM_ROUTE}/${formId}/${ADMINFORM_RESULTS_SUBROUTE}/${subRoute}` + : `${ADMINFORM_ROUTE}/${formId}/${ADMINFORM_RESULTS_SUBROUTE}` + navigate(path) + } + const tabIndex = tabConfig.findIndex((tab) => checkTabActive(tab.path)) return ( - - - - {t('features.common.responses')} - - - {t('features.common.feedback')} - - {shouldShowCharts ? ( - - {t('features.common.charts')} - - {t('features.common.beta')} - - - ) : null} - - + {tabConfig.map((tab) => ( + + ))} + ) } diff --git a/frontend/src/features/admin-form/responses/components/FormResultsNavbar/ResultsTab.tsx b/frontend/src/features/admin-form/responses/components/FormResultsNavbar/ResultsTab.tsx new file mode 100644 index 0000000000..0ad0b828fb --- /dev/null +++ b/frontend/src/features/admin-form/responses/components/FormResultsNavbar/ResultsTab.tsx @@ -0,0 +1,37 @@ +import { As, Box, Icon, Tab } from '@chakra-ui/react' + +import Badge from '~components/Badge' + +export interface ResultsTabProps { + label: string + icon: As + showBadge?: boolean + badgeText?: string +} + +export const ResultsTab = ({ + label, + icon, + showBadge = false, + badgeText, +}: ResultsTabProps): JSX.Element => { + return ( + + + + {label} + + {showBadge ? ( + + {badgeText} + + ) : null} + + ) +} diff --git a/frontend/src/features/admin-form/responses/components/SecretKeyVerification/SecretKeyVerification.tsx b/frontend/src/features/admin-form/responses/components/SecretKeyVerification/SecretKeyVerification.tsx index 7b13ce9cce..03ea0bde7e 100644 --- a/frontend/src/features/admin-form/responses/components/SecretKeyVerification/SecretKeyVerification.tsx +++ b/frontend/src/features/admin-form/responses/components/SecretKeyVerification/SecretKeyVerification.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next' -import { Container, Skeleton, Stack, Text } from '@chakra-ui/react' +import { Box, Skeleton, Stack, Text } from '@chakra-ui/react' import SecretKeyVerificationInput from '~components/SecretKeyVerificationInput' @@ -10,11 +10,13 @@ export const SecretKeyVerification = ({ ctaText, label, hideResponseCount, + noPadding, }: { heroSvg: JSX.Element ctaText: string label: string hideResponseCount?: boolean + noPadding?: boolean }): JSX.Element => { const { setSecretKey, formPublicKey, isLoading, totalResponsesCount } = useStorageResponsesContext() @@ -22,7 +24,10 @@ export const SecretKeyVerification = ({ const { t } = useTranslation() return ( - + {heroSvg} {!hideResponseCount ? ( @@ -51,6 +56,6 @@ export const SecretKeyVerification = ({ buttonText={ctaText} /> - + ) } diff --git a/frontend/src/features/admin-form/settings/SettingsPage.tsx b/frontend/src/features/admin-form/settings/SettingsPage.tsx index a28d63c6fe..d946295650 100644 --- a/frontend/src/features/admin-form/settings/SettingsPage.tsx +++ b/frontend/src/features/admin-form/settings/SettingsPage.tsx @@ -146,7 +146,8 @@ export const SettingsPage = (): JSX.Element => { orientation="vertical" variant="line" py={{ base: '2.5rem', lg: '3.125rem' }} - px={{ base: '1.5rem', md: '1.75rem', lg: '2rem' }} + pl={{ base: 0, md: '1.75rem', lg: '2rem' }} + pr={{ base: '1.5rem', md: '1.75rem', lg: '2rem' }} index={tabIndex === -1 ? 0 : tabIndex} onChange={(index) => { handleTabChange(index) @@ -175,7 +176,7 @@ export const SettingsPage = (): JSX.Element => { From 4169abec4df56a8309dab3e9de18188ee1707164 Mon Sep 17 00:00:00 2001 From: bookwormsuf Date: Wed, 17 Dec 2025 20:04:14 +0800 Subject: [PATCH 2/6] linting fixes --- .../UnlockedCharts/components/EmptyChartsContainer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/components/EmptyChartsContainer.tsx b/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/components/EmptyChartsContainer.tsx index a5c271668b..4b05f6d376 100644 --- a/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/components/EmptyChartsContainer.tsx +++ b/frontend/src/features/admin-form/responses/ChartsPage/UnlockedCharts/components/EmptyChartsContainer.tsx @@ -1,4 +1,5 @@ import { Box, Divider, Flex, Stack, Text } from '@chakra-ui/react' + import { ChartsSvgr } from '../assets/svgr/ChartsSvgr' import { ChartsSupportedFieldsInfoBox } from './ChartsSupportedFieldsInfoBox' From aa2ab43764bce0ad5aea8912d7e2d6856ac8bb60 Mon Sep 17 00:00:00 2001 From: bookwormsuf Date: Wed, 17 Dec 2025 20:53:00 +0800 Subject: [PATCH 3/6] linting fixes + tried to fix nested scrolling on charts --- .../responses/FormResultsLayout.tsx | 10 +------ .../UnlockedResponses/UnlockedResponses.tsx | 7 +---- .../FormResultsNavbar/FormResultsNavbar.tsx | 28 ++----------------- 3 files changed, 4 insertions(+), 41 deletions(-) diff --git a/frontend/src/features/admin-form/responses/FormResultsLayout.tsx b/frontend/src/features/admin-form/responses/FormResultsLayout.tsx index 381f343b16..ece008156a 100644 --- a/frontend/src/features/admin-form/responses/FormResultsLayout.tsx +++ b/frontend/src/features/admin-form/responses/FormResultsLayout.tsx @@ -1,14 +1,6 @@ import { useCallback } from 'react' import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom' -import { - Box, - Flex, - Spacer, - TabList, - TabPanel, - TabPanels, - Tabs, -} from '@chakra-ui/react' +import { Box, Flex, Spacer, Tabs } from '@chakra-ui/react' import { ACTIVE_ADMINFORM_RESULTS_ROUTE_REGEX, diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/UnlockedResponses.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/UnlockedResponses.tsx index 207c141aea..3067fb8b95 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/UnlockedResponses.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/UnlockedResponses.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { Box, Flex, Grid, Skeleton, Stack, Text } from '@chakra-ui/react' +import { Box, Flex, Skeleton, Stack, Text } from '@chakra-ui/react' import { DateRangePicker, @@ -39,13 +39,11 @@ export const UnlockedResponses = (): JSX.Element => { return ( {/* On small screens: stacked (3 rows), On larger screens: horizontal */} { { const { formId } = useParams() - const navigate = useNavigate() const { data: form } = useAdminForm() - const { pathname } = useLocation() const { t } = useTranslation() if (!formId) throw new Error('No formId provided') - const checkTabActive = useCallback( - (to: string) => { - const match = pathname.match(ACTIVE_ADMINFORM_RESULTS_ROUTE_REGEX) - return (match?.[2] ?? '/') === `/${to}` - }, - [pathname], - ) - const isChartsEnabled = useFeatureValue('charts', false) const isFormEncryptMode = form?.responseMode === FormResponseMode.Encrypt const shouldShowCharts = isFormEncryptMode && isChartsEnabled @@ -84,15 +69,6 @@ export const FormResultsNavbar = (): JSX.Element => { : []), ] - const handleTabChange = (index: number) => { - const subRoute = tabConfig[index].path - const path = subRoute - ? `${ADMINFORM_ROUTE}/${formId}/${ADMINFORM_RESULTS_SUBROUTE}/${subRoute}` - : `${ADMINFORM_ROUTE}/${formId}/${ADMINFORM_RESULTS_SUBROUTE}` - navigate(path) - } - const tabIndex = tabConfig.findIndex((tab) => checkTabActive(tab.path)) - return ( Date: Thu, 18 Dec 2025 14:06:22 +0800 Subject: [PATCH 4/6] Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../src/features/admin-form/responses/ChartsPage/ChartsPage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx b/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx index caee0d6de9..c017d6551d 100644 --- a/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx +++ b/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx @@ -5,9 +5,7 @@ import { Container, Divider, Flex, - Skeleton, Stack, - Text, } from '@chakra-ui/react' import { useFeatureValue } from '@growthbook/growthbook-react' From 029ec455057487ad222b4122f20abff25d1e7b15 Mon Sep 17 00:00:00 2001 From: bookwormsuf Date: Thu, 18 Dec 2025 14:09:37 +0800 Subject: [PATCH 5/6] Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../src/features/admin-form/responses/ChartsPage/ChartsPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx b/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx index c017d6551d..2b0e9eb14b 100644 --- a/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx +++ b/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx @@ -32,7 +32,6 @@ export const ChartsPage = (): JSX.Element => { const { totalResponsesCount, secretKey, - isLoading: isResponsesLoading, } = useStorageResponsesContext() const { pathname } = useLocation() const chartsMaxResponseCount = useFeatureValue( From c1773e7ba6aff9b7c12af393a6f0454d19d0a1b0 Mon Sep 17 00:00:00 2001 From: bookwormsuf Date: Thu, 18 Dec 2025 14:19:42 +0800 Subject: [PATCH 6/6] linting fixes --- .../admin-form/responses/ChartsPage/ChartsPage.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx b/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx index 2b0e9eb14b..240eb58b9d 100644 --- a/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx +++ b/frontend/src/features/admin-form/responses/ChartsPage/ChartsPage.tsx @@ -1,12 +1,6 @@ import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' -import { - Box, - Container, - Divider, - Flex, - Stack, -} from '@chakra-ui/react' +import { Box, Container, Divider, Flex, Stack } from '@chakra-ui/react' import { useFeatureValue } from '@growthbook/growthbook-react' import { featureFlags } from '~shared/constants' @@ -29,10 +23,7 @@ import UnlockedCharts from './UnlockedCharts' export const ChartsPage = (): JSX.Element => { const { t } = useTranslation() const { data: form, isLoading } = useAdminForm() - const { - totalResponsesCount, - secretKey, - } = useStorageResponsesContext() + const { totalResponsesCount, secretKey } = useStorageResponsesContext() const { pathname } = useLocation() const chartsMaxResponseCount = useFeatureValue( featureFlags.chartsMaxResponseCount,