From a3591e62c82179f1d0257077775e758d97199fc6 Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Mon, 25 Nov 2024 09:08:34 +1300 Subject: [PATCH] style(datatrakWeb): RN-1453: Mobile survey workflow (#5981) * mobile survey header * custom icons * Update SurveyMobilePaginator.tsx * wip * fix(tupaiaWeb): RN-1512: Fix broken arithmetic visuals (#5980) Update utils.ts * survey select page * country selector * side menu * db(entityTypes): MAUI-4763: Add new entity types (pacmossi_insecticide_test) for PacMOSSI project (#5986) * pacmossi_insecticide_test entity type * schema and model update * copy and share * header style * Update dataTables.js * sticky header * tweak styles * style tweaks * Update ResultsList.tsx * refactor list * select list * mobile menu * toast styles * fix(datatrak): RN-1450: Fix mobile tooltips (#5971) * Update Tooltip.tsx * set delay on BaseTooltip --------- Co-authored-by: Andrew * Update index.ts * tweaks * pr suggestions * Update CancelConfirmModal.tsx * tweak(datatrak): RN-1451: DataTrak request country access form (#6000) Update RequestCountryAccessForm.tsx * tweak(adminPanel): RN-1419: Don't delete survey screens and components on import unless they have changed (#5885) * Working check * Handle object equality with stringify * Fix object equality check * Remove console log --------- Co-authored-by: Andrew * Update SurveyDisplayName.tsx --------- Co-authored-by: Salman <114740396+hrazasalman@users.noreply.github.com> Co-authored-by: Andrew Co-authored-by: alexd-bes <129009580+alexd-bes@users.noreply.github.com> --- .../src/routes/visualisations/dataTables.js | 2 +- .../importSurveys/importSurveyQuestions.js | 215 +++++++++++++----- packages/data-api/src/utils.ts | 1 + ...secticideTestEntityType-modifies-schema.js | 29 +++ .../src/components/CancelConfirmModal.tsx | 21 +- .../src/components/Icons/ArrowLeftIcon.tsx | 4 +- .../src/components/Icons/ShareIcon.tsx | 26 +++ .../src/components/Icons/index.ts | 1 + .../datatrak-web/src/components/Toast.tsx | 36 ++- .../src/components/TopProgressBar.tsx | 4 + .../EntitySelector/EntitySelector.tsx | 9 +- .../features/EntitySelector/SearchField.tsx | 32 +-- .../Survey/Components/CopySurveyUrlButton.tsx | 26 ++- ...veyToolbar.tsx => DesktopSurveyHeader.tsx} | 20 +- .../Survey/Components/MobileSurveyHeader.tsx | 43 ++++ .../Survey/Components/MobileSurveyMenu.tsx | 98 ++++++++ .../Survey/Components/SurveyDisplayName.tsx | 36 +++ .../SurveySideMenu/SideMenuButton.tsx | 6 +- .../SurveySideMenu/SurveySideMenu.tsx | 26 ++- .../src/features/Survey/Components/index.ts | 4 +- .../features/Survey/Screens/SurveyScreen.tsx | 12 +- .../src/features/Survey/SurveyLayout.tsx | 6 +- .../datatrak-web/src/features/Survey/index.ts | 7 +- packages/datatrak-web/src/features/index.ts | 2 +- .../src/layout/ScrollableBody.tsx | 2 +- .../src/layout/ScrollableLayout.tsx | 22 -- .../datatrak-web/src/layout/TasksLayout.tsx | 8 +- packages/datatrak-web/src/layout/index.ts | 2 +- packages/datatrak-web/src/theme/theme.ts | 5 + packages/datatrak-web/src/utils/index.ts | 2 +- packages/datatrak-web/src/utils/toast.ts | 14 ++ .../RequestCountryAccessForm.tsx | 23 +- .../datatrak-web/src/views/SurveyPage.tsx | 21 +- .../ExportDashboard/ExportConfig.tsx | 8 +- .../ExportSettings/ExportDescriptionInput.tsx | 6 +- .../ExportSettings/ExportSettingLabel.tsx | 2 +- packages/types/src/schemas/schemas.ts | 1 + packages/types/src/types/models.ts | 1 + .../src/components/Inputs/InputLabel.tsx | 7 +- .../ui-components/src/components/Tooltip.tsx | 4 +- .../src/components/TooltipIconButton.tsx | 8 +- 41 files changed, 617 insertions(+), 185 deletions(-) create mode 100644 packages/database/src/migrations/20241104002014-addPacmossiInsecticideTestEntityType-modifies-schema.js create mode 100644 packages/datatrak-web/src/components/Icons/ShareIcon.tsx rename packages/datatrak-web/src/features/Survey/Components/{SurveyToolbar.tsx => DesktopSurveyHeader.tsx} (73%) create mode 100644 packages/datatrak-web/src/features/Survey/Components/MobileSurveyHeader.tsx create mode 100644 packages/datatrak-web/src/features/Survey/Components/MobileSurveyMenu.tsx create mode 100644 packages/datatrak-web/src/features/Survey/Components/SurveyDisplayName.tsx delete mode 100644 packages/datatrak-web/src/layout/ScrollableLayout.tsx diff --git a/packages/admin-panel/src/routes/visualisations/dataTables.js b/packages/admin-panel/src/routes/visualisations/dataTables.js index d545d445d1..7e22ffe0f6 100644 --- a/packages/admin-panel/src/routes/visualisations/dataTables.js +++ b/packages/admin-panel/src/routes/visualisations/dataTables.js @@ -67,7 +67,7 @@ const COLUMNS = [ type: 'export', actionConfig: { exportEndpoint: 'dataTable', - fileName: '{code}.xlsx', + fileName: '{code}.json', }, }, { diff --git a/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js b/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js index 0d494e83d4..db45ad6644 100644 --- a/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js +++ b/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js @@ -4,7 +4,7 @@ */ import xlsx from 'xlsx'; - +import { isEqual } from 'lodash'; import { DatabaseError, UploadError, @@ -27,6 +27,12 @@ import { caseAndSpaceInsensitiveEquals, convertCellToJson } from './utilities'; const QUESTION_TYPE_LIST = Object.values(ANSWER_TYPES); const VIS_CRITERIA_CONJUNCTION = '_conjunction'; +const objectsAreEqual = (a, b) => { + // If one is falsy and the other is truthy, they are not equal + if (!!a !== !!b) return false; + return isEqual(a, b); +}; + const validateQuestionExistence = rows => { const isQuestionRow = ({ type }) => QUESTION_TYPE_LIST.includes(type); if (!rows || !rows.some(isQuestionRow)) { @@ -35,6 +41,130 @@ const validateQuestionExistence = rows => { return true; }; +/** + * + * @param {object} models + * @param {string} screenId + * @param {string} questionId + * @param {number} componentNumber + * @param {object} questionObject + * + * @returns {Promise} + * + * @description Checks if the screen component already exists, if it does, it updates the changed values, otherwise it creates a new one + */ +const updateOrCreateSurveyScreenComponent = async ( + models, + screenId, + questionId, + componentNumber, + questionObject, +) => { + const existingScreenComponent = await models.surveyScreenComponent.findOne({ + screen_id: screenId, + question_id: questionId, + component_number: componentNumber, + }); + const { + questionLabel = null, + detailLabel = null, + visibilityCriteria = null, + validationCriteria = null, + type, + } = questionObject; + + const validationCriteriaObject = convertCellToJson( + validationCriteria, + processValidationCriteriaValue, + ); + // Create a new survey screen component to display this question + const visibilityCriteriaObject = convertCellToJson(visibilityCriteria, splitStringOnComma); + + const processedVisibilityCriteria = {}; + + await Promise.all( + Object.entries(visibilityCriteriaObject).map(async ([questionCode, answers]) => { + if (questionCode === VIS_CRITERIA_CONJUNCTION) { + // This is the special _conjunction key, extract the 'and' or the 'or' from answers, + // i.e. { conjunction: ['and'] } -> { conjunction: 'and' } + const [conjunctionType] = answers; + processedVisibilityCriteria[VIS_CRITERIA_CONJUNCTION] = conjunctionType; + } else if (questionCode === 'hidden') { + processedVisibilityCriteria.hidden = answers[0] === 'true'; + } else { + const relatedQuestion = await models.question.findOne({ + code: questionCode, + }); + if (!relatedQuestion) { + throw new ImportValidationError( + `Question with code ${questionCode} does not exist`, + excelRowNumber, + 'visibilityCriteria', + tabName, + ); + } + const { id: questionId } = relatedQuestion; + processedVisibilityCriteria[questionId] = answers; + } + }), + ); + + // If the question is a task, set it to hidden always + if (type === ANSWER_TYPES.TASK && !processedVisibilityCriteria.hidden) { + processedVisibilityCriteria.hidden = true; + } + + // If the screen component already exists, update only the changed values, otherwise create a new one + if (existingScreenComponent) { + const changes = {}; + if ( + !objectsAreEqual( + existingScreenComponent.visibility_criteria + ? JSON.parse(existingScreenComponent.visibility_criteria) + : {}, + processedVisibilityCriteria, + ) + ) { + changes.visibility_criteria = JSON.stringify(processedVisibilityCriteria); + } + + if ( + !objectsAreEqual( + existingScreenComponent.validation_criteria + ? JSON.parse(existingScreenComponent.validation_criteria) + : {}, + validationCriteriaObject, + ) + ) { + changes.validation_criteria = JSON.stringify(validationCriteriaObject); + } + + if (questionLabel !== existingScreenComponent.question_label) { + changes.question_label = questionLabel; + } + + if (detailLabel !== existingScreenComponent.detail_label) { + changes.detail_label = detailLabel; + } + + if (Object.keys(changes).length > 0) { + await models.surveyScreenComponent.update({ id: existingScreenComponent.id }, changes); + } + return existingScreenComponent; + } + const newSurveyScreenComponent = await models.surveyScreenComponent.create({ + screen_id: screenId, + question_id: questionId, + component_number: componentNumber, + visibility_criteria: JSON.stringify(processedVisibilityCriteria), + validation_criteria: JSON.stringify(validationCriteriaObject), + question_label: questionLabel, + detail_label: detailLabel, + }); + + return newSurveyScreenComponent; +}; + const updateOrCreateDataElementInGroup = async ( models, dataElementCode, @@ -100,11 +230,16 @@ export async function importSurveysQuestions({ models, file, survey, dataGroup, await dataGroup.deleteSurveyDateElement(); await dataGroup.upsertSurveyDateElement(); - // Delete all existing survey screens and components that were attached to this survey - await deleteScreensForSurvey(models, survey.id); const rows = xlsx.utils.sheet_to_json(sheet); validateQuestionExistence(rows); + const questions = await survey.questions(); + + // If the questions have changed order or had questions added/removed, delete all screens from the survey and re-create them + if (rows.map(({ code }) => code).join(',') !== questions.map(({ code }) => code).join(',')) { + await deleteScreensForSurvey(models, survey.id); + } + // Add all questions to the survey, creating screens, components and questions as required let currentScreen; let currentSurveyScreenComponent; @@ -172,15 +307,11 @@ export async function importSurveysQuestions({ models, file, survey, dataGroup, type, name, text, - questionLabel, detail, - detailLabel, options, optionLabels, optionColors, newScreen, - visibilityCriteria, - validationCriteria, optionSet, hook, } = questionObject; @@ -222,66 +353,30 @@ export async function importSurveysQuestions({ models, file, survey, dataGroup, if (!currentScreen || shouldStartNewScreen) { // Spreadsheet indicates this question starts a new screen // Create a new survey screen - currentScreen = await models.surveyScreen.create({ + const params = { survey_id: survey.id, screen_number: currentScreen ? currentScreen.screen_number + 1 : 1, // Next screen - }); + }; + const existingScreen = await models.surveyScreen.findOne(params); + if (existingScreen) { + currentScreen = existingScreen; + } else { + currentScreen = await models.surveyScreen.create(params); + } // Clear existing survey screen component currentSurveyScreenComponent = undefined; } - // Create a new survey screen component to display this question - const visibilityCriteriaObject = await convertCellToJson( - visibilityCriteria, - splitStringOnComma, + const componentNumber = currentSurveyScreenComponent + ? currentSurveyScreenComponent.component_number + 1 + : 1; + currentSurveyScreenComponent = await updateOrCreateSurveyScreenComponent( + models, + currentScreen.id, + question.id, + componentNumber, + questionObject, ); - const processedVisibilityCriteria = {}; - await Promise.all( - Object.entries(visibilityCriteriaObject).map(async ([questionCode, answers]) => { - if (questionCode === VIS_CRITERIA_CONJUNCTION) { - // This is the special _conjunction key, extract the 'and' or the 'or' from answers, - // i.e. { conjunction: ['and'] } -> { conjunction: 'and' } - const [conjunctionType] = answers; - processedVisibilityCriteria[VIS_CRITERIA_CONJUNCTION] = conjunctionType; - } else if (questionCode === 'hidden') { - processedVisibilityCriteria.hidden = answers[0] === 'true'; - } else { - const relatedQuestion = await models.question.findOne({ - code: questionCode, - }); - if (!relatedQuestion) { - throw new ImportValidationError( - `Question with code ${questionCode} does not exist`, - excelRowNumber, - 'visibilityCriteria', - tabName, - ); - } - const { id: questionId } = relatedQuestion; - processedVisibilityCriteria[questionId] = answers; - } - }), - ); - - // If the question is a task, set it to hidden always - if (type === ANSWER_TYPES.TASK && !processedVisibilityCriteria.hidden) { - processedVisibilityCriteria.hidden = true; - } - - currentSurveyScreenComponent = await models.surveyScreenComponent.create({ - screen_id: currentScreen.id, - question_id: question.id, - component_number: currentSurveyScreenComponent - ? currentSurveyScreenComponent.component_number + 1 - : 1, - visibility_criteria: JSON.stringify(processedVisibilityCriteria), - validation_criteria: JSON.stringify( - convertCellToJson(validationCriteria, processValidationCriteriaValue), - ), - question_label: questionLabel, - detail_label: detailLabel, - }); - const componentId = currentSurveyScreenComponent.id; await configImporter.add(rowIndex, componentId, constructImportValidationError); } catch (e) { diff --git a/packages/data-api/src/utils.ts b/packages/data-api/src/utils.ts index 06b42c251b..f74c84e29c 100644 --- a/packages/data-api/src/utils.ts +++ b/packages/data-api/src/utils.ts @@ -25,6 +25,7 @@ export const sanitizeAnalyticsTableValue = (value: string, type: string) => { switch (type) { case 'Binary': case 'Checkbox': + case 'Arithmetic': case 'Number': { const sanitizedValue = parseFloat(value); return Number.isNaN(sanitizedValue) ? '' : sanitizedValue; diff --git a/packages/database/src/migrations/20241104002014-addPacmossiInsecticideTestEntityType-modifies-schema.js b/packages/database/src/migrations/20241104002014-addPacmossiInsecticideTestEntityType-modifies-schema.js new file mode 100644 index 0000000000..960ed4034b --- /dev/null +++ b/packages/database/src/migrations/20241104002014-addPacmossiInsecticideTestEntityType-modifies-schema.js @@ -0,0 +1,29 @@ +'use strict'; + +var dbm; +var type; +var seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +exports.up = function (db) { + return db.runSql( + `ALTER TYPE public.entity_type ADD VALUE IF NOT EXISTS 'pacmossi_insecticide_test'`, + ); +}; + +exports.down = function (db) { + return null; +}; + +exports._meta = { + version: 1, +}; diff --git a/packages/datatrak-web/src/components/CancelConfirmModal.tsx b/packages/datatrak-web/src/components/CancelConfirmModal.tsx index fb4b2b5a21..a1b39d54a6 100644 --- a/packages/datatrak-web/src/components/CancelConfirmModal.tsx +++ b/packages/datatrak-web/src/components/CancelConfirmModal.tsx @@ -15,11 +15,15 @@ const Wrapper = styled.div` padding: 1rem 2rem; } `; + const ButtonWrapper = styled.div` display: flex; - flex-direction: column; + flex-direction: column-reverse; width: 100%; - padding-top: 1.3rem; + max-width: 20rem; + margin: 1.5rem auto 0; + gap: 1rem; + ${({ theme }) => theme.breakpoints.up('sm')} { flex-direction: row; justify-content: center; @@ -34,10 +38,9 @@ const Heading = styled(Typography).attrs({ `; const ModalButton = styled(Button)` - ${({ theme }) => theme.breakpoints.down('xs')} { - & + & { - margin: 1rem 0 0 0; - } + &.MuiButtonBase-root.MuiButton-root { + flex: 1; + margin: 0; } `; @@ -56,10 +59,12 @@ export const CancelConfirmModal = ({ {headingText} {bodyText} - + + {cancelText} + + {confirmText} - {cancelText} diff --git a/packages/datatrak-web/src/components/Icons/ArrowLeftIcon.tsx b/packages/datatrak-web/src/components/Icons/ArrowLeftIcon.tsx index 511288c7fa..a86043c400 100644 --- a/packages/datatrak-web/src/components/Icons/ArrowLeftIcon.tsx +++ b/packages/datatrak-web/src/components/Icons/ArrowLeftIcon.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { SvgIcon, SvgIconProps } from '@material-ui/core'; -export const ArrowLeftIcon = (props: SvgIconProps) => { +export const ArrowLeftIcon = ({ htmlColor = 'currentColor', ...props }: SvgIconProps) => { return ( { > ); diff --git a/packages/datatrak-web/src/components/Icons/ShareIcon.tsx b/packages/datatrak-web/src/components/Icons/ShareIcon.tsx new file mode 100644 index 0000000000..1b7f8fabbe --- /dev/null +++ b/packages/datatrak-web/src/components/Icons/ShareIcon.tsx @@ -0,0 +1,26 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ +import React from 'react'; +import { SvgIcon, SvgIconProps } from '@material-ui/core'; + +export const ShareIcon = ({ htmlColor = 'currentColor', ...props }: SvgIconProps) => { + return ( + + + + ); +}; diff --git a/packages/datatrak-web/src/components/Icons/index.ts b/packages/datatrak-web/src/components/Icons/index.ts index 3841949d04..96e3fce786 100644 --- a/packages/datatrak-web/src/components/Icons/index.ts +++ b/packages/datatrak-web/src/components/Icons/index.ts @@ -15,3 +15,4 @@ export { CopyIcon } from './CopyIcon'; export { TaskIcon } from './TaskIcon'; export { CommentIcon } from './CommentIcon'; export { ArrowLeftIcon } from './ArrowLeftIcon'; +export { ShareIcon } from './ShareIcon'; diff --git a/packages/datatrak-web/src/components/Toast.tsx b/packages/datatrak-web/src/components/Toast.tsx index 1434f22ace..43ddfc97d7 100644 --- a/packages/datatrak-web/src/components/Toast.tsx +++ b/packages/datatrak-web/src/components/Toast.tsx @@ -13,6 +13,7 @@ const Wrapper = styled(SnackbarContent)` background-color: white; border-radius: 0.625rem; max-width: 87vw; + min-height: 2.25rem; @media screen and (min-width: 24rem) { width: 21rem; } @@ -66,18 +67,31 @@ const CloseButton = styled(IconButton)<{ } `; -const Message = styled(Typography)` +const Message = styled(Typography)<{ + $variant: CustomContentProps['variant']; +}>` font-size: 0.875rem; flex: 1; word-break: break-word; + text-align: ${({ $variant }) => ($variant === 'info' ? 'center' : 'left')}; + color: ${({ theme, $variant }) => { + if ($variant === 'error') { + return theme.palette.error.main; + } + if ($variant === 'info') { + return theme.palette.info.main; + } + return theme.palette.text.primary; + }}; `; interface ToastProps extends CustomContentProps { Icon?: OptionsObject['Icon']; + hideCloseButton?: boolean; } export const Toast = React.forwardRef((props, ref) => { - const { id, Icon, message, variant, ...notistackProps } = props; + const { id, Icon, message, variant, hideCloseButton = false, ...notistackProps } = props; return ( @@ -87,14 +101,16 @@ export const Toast = React.forwardRef((props, ref) = )} - {message} - closeSnackbar(id)} - $variant={variant} - title="Close toast message" - > - - + {message} + {!hideCloseButton && ( + closeSnackbar(id)} + $variant={variant} + title="Close toast message" + > + + + )} ); diff --git a/packages/datatrak-web/src/components/TopProgressBar.tsx b/packages/datatrak-web/src/components/TopProgressBar.tsx index f99f2c6eb8..2de41b52b0 100644 --- a/packages/datatrak-web/src/components/TopProgressBar.tsx +++ b/packages/datatrak-web/src/components/TopProgressBar.tsx @@ -19,6 +19,10 @@ const ProgressBar = styled.div<{ align-items: flex-start; height: 0.75rem; width: ${({ $progress }) => `${$progress}`}%; + + ${({ theme }) => theme.breakpoints.down('xs')} { + height: 0.4rem; + } `; interface ProgressPercentage { diff --git a/packages/datatrak-web/src/features/EntitySelector/EntitySelector.tsx b/packages/datatrak-web/src/features/EntitySelector/EntitySelector.tsx index 1fd84010ce..27547cd973 100644 --- a/packages/datatrak-web/src/features/EntitySelector/EntitySelector.tsx +++ b/packages/datatrak-web/src/features/EntitySelector/EntitySelector.tsx @@ -44,9 +44,8 @@ const useSearchResults = (searchValue, filter, projectCode, disableSearch = fals interface EntitySelectorProps { id: string; - label?: string | null; - detailLabel?: string | null; name?: string | null; + label?: string | null; required?: boolean | null; controllerProps: { onChange: (value: string) => void; @@ -73,9 +72,8 @@ interface EntitySelectorProps { export const EntitySelector = ({ id, - label, - detailLabel, name, + label, required, controllerProps: { onChange, value, ref, invalid }, projectCode, @@ -145,8 +143,6 @@ export const EntitySelector = ({ ) : ( ` font-size: 1.2em; } + .MuiInputAdornment-positionStart { + margin-right: 0.2rem; + } + &&&& { .MuiInputBase-input::placeholder { - color: ${({ theme }) => theme.palette.text.tertiary}; + color: ${({ theme }) => theme.palette.text.hint}; + } + } + + ${({ theme }) => theme.breakpoints.down('md')} { + .MuiOutlinedInput-notchedOutline { + border: none; + } + .MuiInputBase-root { + border-radius: 6.25rem; } } `; @@ -59,23 +72,11 @@ type SearchFieldProps = TextFieldProps & { onChangeSearch: (value: string) => void; isDirty: boolean; invalid: boolean; - detailLabel?: string; required?: boolean; }; export const SearchField = React.forwardRef((props, ref) => { - const { - name, - label, - id, - searchValue, - onChangeSearch, - isDirty, - invalid, - detailLabel, - required, - inputProps, - } = props; + const { name, id, searchValue, onChangeSearch, isDirty, invalid, required, inputProps } = props; const displayValue = isDirty ? searchValue : ''; @@ -90,14 +91,13 @@ export const SearchField = React.forwardRef((p return ( { +export const useCopySurveyUrl = ({ toastOptions = {} }: { toastOptions: OptionsObject }) => { const params = useParams(); const path = generatePath(ROUTES.SURVEY, params); const link = `${window.location.origin}${path}`; - const copyPageUrl = () => { + return () => { try { navigator.clipboard.writeText(link); - successToast('Page URL copied to clipboard'); + infoToast('Page URL copied to clipboard', { + persist: false, + TransitionProps: { appear: true }, + hideCloseButton: true, + ...toastOptions, + }); } catch (err) { console.warn('Failed to copy page url: ', err); } }; +}; + +export const CopySurveyUrlButton = () => { + const copyPageUrl = useCopySurveyUrl({ + toastOptions: { + anchorOrigin: { + vertical: 'top', + horizontal: 'right', + }, + }, + }); return ( { } arrow enterDelay={500} + enterTouchDelay={500} > + + ); +}; diff --git a/packages/datatrak-web/src/features/Survey/Components/SurveyDisplayName.tsx b/packages/datatrak-web/src/features/Survey/Components/SurveyDisplayName.tsx new file mode 100644 index 0000000000..677d3e7294 --- /dev/null +++ b/packages/datatrak-web/src/features/Survey/Components/SurveyDisplayName.tsx @@ -0,0 +1,36 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import styled from 'styled-components'; +import { useEntityByCode, useSurvey } from '../../../api'; + +const CountryName = styled.span` + padding-left: 0.3rem; + font-weight: ${({ theme }) => theme.typography.fontWeightRegular}; +`; + +const maxSurveyNameLength = 50; + +export const SurveyDisplayName = () => { + const { surveyCode, countryCode } = useParams(); + const { data: survey } = useSurvey(surveyCode); + const { data: country } = useEntityByCode(countryCode!); + + if (!survey?.name) return null; + + const surveyName = + survey.name.length > maxSurveyNameLength + ? `${survey.name.slice(0, maxSurveyNameLength)}...` + : survey.name; + const countryName = country?.name || ''; + + return ( + <> + {surveyName} + {| {countryName}} + + ); +}; diff --git a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SideMenuButton.tsx b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SideMenuButton.tsx index e241b38e8d..b477899313 100644 --- a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SideMenuButton.tsx +++ b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SideMenuButton.tsx @@ -21,10 +21,8 @@ const MenuButton = styled(Button).attrs({ background-color: ${({ theme }) => theme.palette.primary.dark}; color: ${({ theme }) => theme.palette.background.paper}; } - ${({ theme }) => theme.breakpoints.down('sm')} { - position: absolute; - z-index: 1; - margin-top: 0.5rem; + ${({ theme }) => theme.breakpoints.down('md')} { + display: none; } `; diff --git a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx index 0422ca8017..f6fc802209 100644 --- a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx +++ b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx @@ -3,7 +3,7 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ import React from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { To, Link as RouterLink } from 'react-router-dom'; import { useFormContext } from 'react-hook-form'; import { Drawer as BaseDrawer, ListItem, List, ButtonProps } from '@material-ui/core'; @@ -12,6 +12,8 @@ import { getSurveyScreenNumber } from '../../utils'; import { useSurveyRouting } from '../../useSurveyRouting'; import { SideMenuButton } from './SideMenuButton'; import { useSurveyForm } from '../../SurveyContext'; +import { StickyMobileHeader, MobileHeaderWrapper } from '../../../../layout'; +import { SurveyDisplayName } from '../SurveyDisplayName'; export const SIDE_MENU_WIDTH = '20rem'; @@ -30,6 +32,12 @@ const Drawer = styled(BaseDrawer).attrs({ border-right: none; height: 100%; } + + ${MobileHeaderWrapper} { + padding-inline-start: 1rem; + min-height: 4.375rem; + border-bottom: 1px solid ${({ theme }) => theme.palette.divider}; + } ${({ theme }) => theme.breakpoints.up('md')} { .MuiPaper-root { background-color: transparent; @@ -39,6 +47,9 @@ const Drawer = styled(BaseDrawer).attrs({ const SurveyMenuContent = styled(List)` padding: 0 0.5rem; + ${({ theme }) => theme.breakpoints.down('md')} { + padding: 0; + } `; const SurveyMenuItem = styled(ListItem).attrs({ @@ -77,6 +88,18 @@ const SurveyMenuItem = styled(ListItem).attrs({ overflow: hidden; } } + + ${({ theme }) => theme.breakpoints.down('md')} { + border-bottom: 1px solid ${({ theme }) => theme.palette.divider}; + ${({ $active }) => + $active && + css` + background-color: #f4f9ff; + `} + &:hover { + background-color: initial; + } + } `; const SurveyScreenTitle = styled.span` @@ -137,6 +160,7 @@ export const SurveySideMenu = () => { onClose={toggleSideMenu} variant={isMobile ? 'temporary' : 'persistent'} > + } />
diff --git a/packages/datatrak-web/src/features/Survey/Components/index.ts b/packages/datatrak-web/src/features/Survey/Components/index.ts index b4b17c01ef..fa00400cb8 100644 --- a/packages/datatrak-web/src/features/Survey/Components/index.ts +++ b/packages/datatrak-web/src/features/Survey/Components/index.ts @@ -5,8 +5,10 @@ export { SurveyQuestion } from './SurveyQuestion'; export { SurveyQuestionGroup } from './SurveyQuestionGroup'; -export { SurveyToolbar } from './SurveyToolbar'; +export { DesktopSurveyHeader } from './DesktopSurveyHeader'; +export { MobileSurveyHeader } from './MobileSurveyHeader'; export * from './SurveySideMenu'; export { SurveyReviewSection } from './SurveyReviewSection'; export { SurveyPaginator } from './SurveyPaginator'; +export { MobileSurveyMenu } from './MobileSurveyMenu.tsx'; export { SurveySuccess } from './SurveySuccess'; diff --git a/packages/datatrak-web/src/features/Survey/Screens/SurveyScreen.tsx b/packages/datatrak-web/src/features/Survey/Screens/SurveyScreen.tsx index 5b7c27a905..9e96463cbb 100644 --- a/packages/datatrak-web/src/features/Survey/Screens/SurveyScreen.tsx +++ b/packages/datatrak-web/src/features/Survey/Screens/SurveyScreen.tsx @@ -8,8 +8,14 @@ import styled from 'styled-components'; import { Typography } from '@material-ui/core'; import { QuestionType } from '@tupaia/types'; import { useSurveyForm } from '../SurveyContext'; -import { SurveyPaginator, SurveyQuestionGroup } from '../Components'; +import { + SurveyPaginator, + SurveyQuestionGroup, + MobileSurveyHeader, + MobileSurveyMenu, +} from '../Components'; import { ScrollableBody } from '../../../layout'; +import { useIsMobile } from '../../../utils'; const ScreenHeader = styled.div<{ $centered?: boolean; @@ -45,6 +51,7 @@ export const SurveyScreen = () => { screenDetail: instructionDetail, activeScreen, } = useSurveyForm(); + const isMobile = useIsMobile(); const pageHasOnlyInstructions = activeScreen.every( question => question.type === QuestionType.Instruction, @@ -55,6 +62,7 @@ export const SurveyScreen = () => { return ( <> + {isMobile && } {/* * If the first question on the active screen is an instruction, then display it in full @@ -69,7 +77,7 @@ export const SurveyScreen = () => { - + {isMobile ? : } ); }; diff --git a/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx b/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx index 2fab8f6c14..4041104f8b 100644 --- a/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx +++ b/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx @@ -35,7 +35,6 @@ const ScrollableLayout = styled.div<{ `; const Paper = styled(MuiPaper).attrs({ - variant: 'outlined', elevation: 0, })` flex: 1; @@ -45,8 +44,13 @@ const Paper = styled(MuiPaper).attrs({ position: relative; display: flex; flex-direction: column; + background-color: ${({ theme }) => theme.palette.background.default}; + border: none; border-radius: 0; + ${({ theme }) => theme.breakpoints.up('md')} { + border: 1px solid ${({ theme }) => theme.palette.divider}; + background-color: ${({ theme }) => theme.palette.background.paper}; margin-left: 1rem; border-radius: 4px; } diff --git a/packages/datatrak-web/src/features/Survey/index.ts b/packages/datatrak-web/src/features/Survey/index.ts index 75084e7ebb..46033de9da 100644 --- a/packages/datatrak-web/src/features/Survey/index.ts +++ b/packages/datatrak-web/src/features/Survey/index.ts @@ -6,6 +6,11 @@ export * from './Screens'; export { SurveyContext, useSurveyForm, getArithmeticDisplayAnswer } from './SurveyContext'; export { SurveyLayout } from './SurveyLayout'; -export { SurveyToolbar, SurveySideMenu, SurveyReviewSection } from './Components'; +export { + DesktopSurveyHeader, + MobileSurveyHeader, + SurveySideMenu, + SurveyReviewSection, +} from './Components'; export * from './utils'; export { useValidationResolver } from './useValidationResolver'; diff --git a/packages/datatrak-web/src/features/index.ts b/packages/datatrak-web/src/features/index.ts index 00f69a3929..6acaa747d4 100644 --- a/packages/datatrak-web/src/features/index.ts +++ b/packages/datatrak-web/src/features/index.ts @@ -4,6 +4,7 @@ */ export { + DesktopSurveyHeader, SurveyScreen, SurveySuccessScreen, SurveyReviewScreen, @@ -14,7 +15,6 @@ export { SurveySideMenu, useValidationResolver, SurveyResubmitSuccessScreen, - SurveyToolbar, } from './Survey'; export { RequestProjectAccess } from './RequestProjectAccess'; export { MobileAppPrompt } from './MobileAppPrompt'; diff --git a/packages/datatrak-web/src/layout/ScrollableBody.tsx b/packages/datatrak-web/src/layout/ScrollableBody.tsx index 8c82f5d654..3fc584cd8b 100644 --- a/packages/datatrak-web/src/layout/ScrollableBody.tsx +++ b/packages/datatrak-web/src/layout/ScrollableBody.tsx @@ -12,7 +12,7 @@ export const ScrollableBody = styled.div<{ height: 100%; flex: 1; overflow-y: auto; - padding: ${({ $hasSidebar }) => ($hasSidebar ? '1rem 1rem 1rem 3rem' : '1rem')}; + padding: 1rem; ${({ theme }) => theme.breakpoints.up('sm')} { padding: ${({ $hasSidebar }) => ($hasSidebar ? '1rem 1rem 1rem 5rem' : '1rem 2rem')}; } diff --git a/packages/datatrak-web/src/layout/ScrollableLayout.tsx b/packages/datatrak-web/src/layout/ScrollableLayout.tsx deleted file mode 100644 index 0bde32ead4..0000000000 --- a/packages/datatrak-web/src/layout/ScrollableLayout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ -import React from 'react'; -import { Outlet } from 'react-router-dom'; -import styled from 'styled-components'; -import { HEADER_HEIGHT, TITLE_BAR_HEIGHT } from '../constants'; - -export const HeaderLessFullHeightContainer = styled.div` - height: calc(100vh - ${HEADER_HEIGHT} - ${TITLE_BAR_HEIGHT}); - display: flex; - flex-direction: column; -`; - -export const ScrollableLayout = () => { - return ( - - - - ); -}; diff --git a/packages/datatrak-web/src/layout/TasksLayout.tsx b/packages/datatrak-web/src/layout/TasksLayout.tsx index 78f11a699c..b5cfa301d0 100644 --- a/packages/datatrak-web/src/layout/TasksLayout.tsx +++ b/packages/datatrak-web/src/layout/TasksLayout.tsx @@ -6,7 +6,13 @@ import React from 'react'; import { Outlet } from 'react-router'; import styled from 'styled-components'; import { PageContainer as BasePageContainer } from '../components'; -import { HeaderLessFullHeightContainer } from './ScrollableLayout'; +import { HEADER_HEIGHT, TITLE_BAR_HEIGHT } from '../constants'; + +const HeaderLessFullHeightContainer = styled.div` + height: calc(100vh - ${HEADER_HEIGHT} - ${TITLE_BAR_HEIGHT}); + display: flex; + flex-direction: column; +`; const PageContainer = styled(BasePageContainer)` display: flex; diff --git a/packages/datatrak-web/src/layout/index.ts b/packages/datatrak-web/src/layout/index.ts index c82352261b..5baa7551dd 100644 --- a/packages/datatrak-web/src/layout/index.ts +++ b/packages/datatrak-web/src/layout/index.ts @@ -8,4 +8,4 @@ export { MainPageLayout } from './MainPageLayout'; export { CentredLayout } from './CentredLayout'; export { ScrollableBody } from './ScrollableBody'; export { TasksLayout, TasksContentWrapper } from './TasksLayout'; -export { StickyMobileHeader } from './StickyMobileHeader'; +export { StickyMobileHeader, MobileHeaderWrapper } from './StickyMobileHeader'; diff --git a/packages/datatrak-web/src/theme/theme.ts b/packages/datatrak-web/src/theme/theme.ts index 44d73156b9..e0c57a2dd1 100755 --- a/packages/datatrak-web/src/theme/theme.ts +++ b/packages/datatrak-web/src/theme/theme.ts @@ -24,11 +24,16 @@ export const theme = createMuiTheme({ text: { primary: '#2E2F33', // dark text color secondary: '#898989', // light grey text color + hint: '#B8B8B8', }, success: { main: '#25D366', light: '#25D36622', }, + info: { + main: '#004167', + light: '#E6ECF0', + }, error: { main: '#F76853', light: '#F7685333', diff --git a/packages/datatrak-web/src/utils/index.ts b/packages/datatrak-web/src/utils/index.ts index 53065adcf4..228a25ab35 100644 --- a/packages/datatrak-web/src/utils/index.ts +++ b/packages/datatrak-web/src/utils/index.ts @@ -3,7 +3,7 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ -export { errorToast, successToast } from './toast'; +export { errorToast, successToast, infoToast } from './toast'; export { useIsMobileMediaQuery as useIsMobile } from './useIsMobileMediaQuery'; export * from './date'; export * from './detectDevice'; diff --git a/packages/datatrak-web/src/utils/toast.ts b/packages/datatrak-web/src/utils/toast.ts index 55f9a8db6d..1369410b58 100644 --- a/packages/datatrak-web/src/utils/toast.ts +++ b/packages/datatrak-web/src/utils/toast.ts @@ -18,3 +18,17 @@ export const errorToast = (message: string) => { hideIconVariant: true, }); }; + +interface ToastOptions extends OptionsObject { + Icon?: OptionsObject['Icon']; + hideCloseButton?: boolean; +} + +export const infoToast = (message: string, options?: ToastOptions) => { + enqueueSnackbar(message, { + variant: 'info', + autoHideDuration: 2000, + hideIconVariant: true, + ...options, + }); +}; diff --git a/packages/datatrak-web/src/views/AccountSettingsPage/RequestCountryAccessSection/RequestCountryAccessForm.tsx b/packages/datatrak-web/src/views/AccountSettingsPage/RequestCountryAccessSection/RequestCountryAccessForm.tsx index d89570a156..b2fc76cc73 100644 --- a/packages/datatrak-web/src/views/AccountSettingsPage/RequestCountryAccessSection/RequestCountryAccessForm.tsx +++ b/packages/datatrak-web/src/views/AccountSettingsPage/RequestCountryAccessSection/RequestCountryAccessForm.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { useForm } from 'react-hook-form'; import { UseQueryResult } from '@tanstack/react-query'; -import { FormLabel, useMediaQuery, useTheme } from '@material-ui/core'; +import { FormLabel, Typography, useMediaQuery, useTheme } from '@material-ui/core'; import { Entity, ProjectCountryAccessListRequest, ProjectResponse } from '@tupaia/types'; import { Form, FormInput, TextField } from '@tupaia/ui-components'; import { useRequestProjectAccess } from '../../../api'; @@ -87,6 +87,23 @@ const StyledFormInput = styled(FormInput).attrs({ } `; +const Message = styled(Typography)` + font-size: 0.875rem; + font-weight: 400; + text-align: left; + margin-block-end: 1rem; + width: 100%; + + ${({ theme }) => theme.breakpoints.up('md')} { + text-align: center; + margin: 0; + font-weight: 500; + font-size: 1.1125rem; + flex: 1; + align-self: center; + } +`; + interface RequestCountryAccessFormProps { countryAccessList: UseQueryResult; project?: ProjectResponse | null; @@ -157,6 +174,10 @@ export const RequestCountryAccessForm = ({ }); } + if (hasAccessToEveryCountry) { + return You have access to all available countries within this project.; + } + return ( diff --git a/packages/datatrak-web/src/views/SurveyPage.tsx b/packages/datatrak-web/src/views/SurveyPage.tsx index 2be8816e9a..cc822d0ce1 100644 --- a/packages/datatrak-web/src/views/SurveyPage.tsx +++ b/packages/datatrak-web/src/views/SurveyPage.tsx @@ -8,10 +8,15 @@ import styled from 'styled-components'; import { useForm, FormProvider } from 'react-hook-form'; import { useCurrentUserContext, useEditUser, useEntityByCode, useSurvey } from '../api'; import { CancelConfirmModal } from '../components'; -import { SurveyToolbar, useSurveyForm, useValidationResolver, SurveyContext } from '../features'; +import { + DesktopSurveyHeader, + useSurveyForm, + useValidationResolver, + SurveyContext, +} from '../features'; import { SurveyParams } from '../types'; import { HEADER_HEIGHT, TITLE_BAR_HEIGHT } from '../constants'; -import { successToast } from '../utils'; +import { successToast, useIsMobile } from '../utils'; // wrap the entire page so that other content can be centered etc const PageWrapper = styled.div` display: flex; @@ -34,12 +39,14 @@ const SurveyScreenContainer = styled.div<{ display: flex; overflow: ${({ $scrollable }) => ($scrollable ? 'auto' : 'hidden')}; align-items: flex-start; - height: ${({ $hasToolbar }) => - $hasToolbar - ? `calc(100vh - ${HEADER_HEIGHT} - ${TITLE_BAR_HEIGHT})` - : `calc(100vh - ${HEADER_HEIGHT})`}; + + height: 100vh; width: 100%; ${({ theme }) => theme.breakpoints.up('md')} { + height: ${({ $hasToolbar }) => + $hasToolbar + ? `calc(100vh - ${HEADER_HEIGHT} - ${TITLE_BAR_HEIGHT})` + : `calc(100vh - ${HEADER_HEIGHT})`}; margin-left: -1.25rem; padding-top: ${({ $scrollable }) => ($scrollable ? '0' : '2rem')}; padding-bottom: 2rem; @@ -99,7 +106,7 @@ const SurveyPageInner = () => { return ( - + {!useIsMobile() ? : null} {/* Use a key to render a different survey screen component for every screen number. This is so that the screen can be easily initialised with the form data. See https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes */} diff --git a/packages/tupaia-web/src/features/Dashboard/ExportDashboard/ExportConfig.tsx b/packages/tupaia-web/src/features/Dashboard/ExportDashboard/ExportConfig.tsx index 693fb0f44c..258dc2ea9c 100644 --- a/packages/tupaia-web/src/features/Dashboard/ExportDashboard/ExportConfig.tsx +++ b/packages/tupaia-web/src/features/Dashboard/ExportDashboard/ExportConfig.tsx @@ -22,7 +22,7 @@ import { Preview } from './Preview'; import { ExportDescriptionInput } from '../../ExportSettings/ExportDescriptionInput'; const ButtonGroup = styled.div` - padding-top: 2.5rem; + padding: 1rem 0; width: 100%; display: flex; justify-content: flex-end; @@ -32,7 +32,7 @@ const Wrapper = styled.div` display: flex; flex-direction: column; flex-grow: 1; - width 100%; + width: 100%; align-items: start; section + section { margin-top: 1.5rem; @@ -105,9 +105,9 @@ const ExportSettingsInstructionsContainer = styled.div` `; const ExportSettingsWrapper = styled.div` - padding-block-end: 2rem; + padding-block-end: 0.8rem; & + & { - padding-block-start: 1.5rem; + padding-block-start: 1rem; border-top: 0.1rem solid ${({ theme }) => theme.palette.text.secondary}; } &:last-child { diff --git a/packages/tupaia-web/src/features/ExportSettings/ExportDescriptionInput.tsx b/packages/tupaia-web/src/features/ExportSettings/ExportDescriptionInput.tsx index 3673ac5f59..34734cab9f 100644 --- a/packages/tupaia-web/src/features/ExportSettings/ExportDescriptionInput.tsx +++ b/packages/tupaia-web/src/features/ExportSettings/ExportDescriptionInput.tsx @@ -17,7 +17,7 @@ const Wrapper = styled.div` const ExportDescriptionInputArea = styled((props: OutlinedTextFieldProps) => ( ))` - margin-block-start: 1rem; + margin-block-start: 0.6rem; .MuiInputBase-root { border: 1px solid ${({ theme }) => theme.palette.text.secondary}; @@ -44,7 +44,7 @@ const ExportDescription = styled.div<{ display: flex; align-self: end; justify-content: space-between; - margin-top: 0.313rem; + margin-top: 0.3rem; color: ${({ error, theme }) => (error ? theme.palette.error.main : theme.palette.text.secondary)}; font-size: 0.75rem; `; @@ -61,7 +61,7 @@ export const ExportDescriptionInput = () => { theme.palette.text.primary}; padding-inline-start: 0; - font-size: 1.125rem; + font-size: 1rem; font-weight: ${({ theme }) => theme.typography.fontWeightMedium}; `; diff --git a/packages/types/src/schemas/schemas.ts b/packages/types/src/schemas/schemas.ts index 9a82832635..4536718d2d 100644 --- a/packages/types/src/schemas/schemas.ts +++ b/packages/types/src/schemas/schemas.ts @@ -84574,6 +84574,7 @@ export const EntityTypeEnumSchema = { "msupply_store", "nursing_zone", "pacmossi_district", + "pacmossi_insecticide_test", "pacmossi_spraying_site", "pacmossi_village", "postcode", diff --git a/packages/types/src/types/models.ts b/packages/types/src/types/models.ts index 07a1aa0e63..af2026a629 100644 --- a/packages/types/src/types/models.ts +++ b/packages/types/src/types/models.ts @@ -1866,6 +1866,7 @@ export enum EntityTypeEnum { 'pacmossi_district' = 'pacmossi_district', 'pacmossi_village' = 'pacmossi_village', 'pacmossi_spraying_site' = 'pacmossi_spraying_site', + 'pacmossi_insecticide_test' = 'pacmossi_insecticide_test', } export enum DataTableType { 'analytics' = 'analytics', diff --git a/packages/ui-components/src/components/Inputs/InputLabel.tsx b/packages/ui-components/src/components/Inputs/InputLabel.tsx index b7b797597b..eb88e7a45a 100644 --- a/packages/ui-components/src/components/Inputs/InputLabel.tsx +++ b/packages/ui-components/src/components/Inputs/InputLabel.tsx @@ -27,10 +27,13 @@ const TooltipWrapper = styled.span` height: 100%; width: 100%; } + &:hover, &:focus { - svg { - fill: ${props => props.theme.palette.primary.main}; + ${({ theme }) => theme.breakpoints.up('md')} { + svg { + fill: ${props => props.theme.palette.primary.main}; + } } } `; diff --git a/packages/ui-components/src/components/Tooltip.tsx b/packages/ui-components/src/components/Tooltip.tsx index 3e30ec0843..1e5a31aec6 100644 --- a/packages/ui-components/src/components/Tooltip.tsx +++ b/packages/ui-components/src/components/Tooltip.tsx @@ -14,14 +14,14 @@ const TOOLTIP_COLOR = '#002d47'; // For placement options @see https://material-ui.com/api/tooltip export const Tooltip = styled( - ({ className, placement = 'top', enterDelay, ...props }: TooltipProps) => ( + ({ className, placement = 'top', enterDelay, enterTouchDelay = 10, ...props }: TooltipProps) => ( ), diff --git a/packages/ui-components/src/components/TooltipIconButton.tsx b/packages/ui-components/src/components/TooltipIconButton.tsx index 3d119498da..ce479be8a7 100644 --- a/packages/ui-components/src/components/TooltipIconButton.tsx +++ b/packages/ui-components/src/components/TooltipIconButton.tsx @@ -23,8 +23,10 @@ const TooltipWrapper = styled.span` } &:hover, &:focus { - svg { - fill: ${props => props.theme.palette.primary.main}; + ${({ theme }) => theme.breakpoints.up('md')} { + svg { + fill: ${props => props.theme.palette.primary.main}; + } } } `; @@ -48,7 +50,7 @@ interface TooltipIconButtonProps { export const TooltipIconButton = ({ tooltip, Icon = InfoOutlined }: TooltipIconButtonProps) => { return ( - +