Skip to content

Commit

Permalink
style(datatrakWeb): RN-1453: Mobile survey workflow (#5981)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>

* Update SurveyDisplayName.tsx

---------

Co-authored-by: Salman <[email protected]>
Co-authored-by: Andrew <[email protected]>
Co-authored-by: alexd-bes <[email protected]>
  • Loading branch information
4 people authored Nov 24, 2024
1 parent 5ea7097 commit a3591e6
Show file tree
Hide file tree
Showing 41 changed files with 617 additions and 185 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const COLUMNS = [
type: 'export',
actionConfig: {
exportEndpoint: 'dataTable',
fileName: '{code}.xlsx',
fileName: '{code}.json',
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import xlsx from 'xlsx';

import { isEqual } from 'lodash';
import {
DatabaseError,
UploadError,
Expand All @@ -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)) {
Expand All @@ -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<void>}
*
* @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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/data-api/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
};
21 changes: 13 additions & 8 deletions packages/datatrak-web/src/components/CancelConfirmModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
`;

Expand All @@ -56,10 +59,12 @@ export const CancelConfirmModal = ({
<Heading>{headingText}</Heading>
<Typography align="center">{bodyText}</Typography>
<ButtonWrapper>
<ModalButton variant="outlined" to={confirmLink} onClick={onClose}>
<ModalButton onClick={onClose} variant="outlined">
{cancelText}
</ModalButton>
<ModalButton to={confirmLink} onClick={onClose}>
{confirmText}
</ModalButton>
<ModalButton onClick={onClose}>{cancelText}</ModalButton>
</ButtonWrapper>
</Wrapper>
</Modal>
Expand Down
4 changes: 2 additions & 2 deletions packages/datatrak-web/src/components/Icons/ArrowLeftIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<SvgIcon
width="20"
Expand All @@ -17,7 +17,7 @@ export const ArrowLeftIcon = (props: SvgIconProps) => {
>
<path
d="M4.28954 9.99976C4.28954 9.68727 4.41516 9.38789 4.63954 9.16727L13.5883 0.343591C14.0545 -0.114531 14.8095 -0.114531 15.2752 0.343592C15.7408 0.802963 15.7408 1.54983 15.2752 2.00733L7.16891 9.99977L15.2758 17.991C15.7414 18.4509 15.7414 19.1966 15.2758 19.6559C14.8102 20.1147 14.0552 20.1147 13.5889 19.6559L4.63891 10.8323C4.41516 10.6104 4.28954 10.311 4.28954 9.99976Z"
fill="#2E2F33"
fill={htmlColor}
/>
</SvgIcon>
);
Expand Down
Loading

0 comments on commit a3591e6

Please sign in to comment.