Skip to content

Commit

Permalink
feat(adminPanel): RN-1228: Link surveys to Datatrak Web (#5671)
Browse files Browse the repository at this point in the history
* Make links

* Use projectId

* Update user preferences if project id is in url

* Add comment

* Allow country codes to be fetched for surveys

* Link directly to survey

* Default to DL and alphabetise the country codes

* Change tooltip text

* Update copy

* Hide button for surveys with no countries

---------

Co-authored-by: Andrew <vanbeekandrew@gmail.com>
  • Loading branch information
alexd-bes and avaek authored Jul 25, 2024
1 parent 439f16b commit 5210b7e
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 10 deletions.
25 changes: 25 additions & 0 deletions packages/admin-panel/src/routes/surveys/surveys.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { SurveyEditFields } from '../../surveys/SurveyEditFields';
import { QUESTION_FIELDS as BASE_QUESTION_FIELDS } from './questions';
import { EditSurveyPage } from '../../pages/resources';

const { REACT_APP_DATATRAK_WEB_URL } = import.meta.env;

const RESOURCE_NAME = { singular: 'survey' };

const PERIOD_GRANULARITIES = [
Expand Down Expand Up @@ -175,6 +177,16 @@ const SURVEY_COLUMNS = [
SURVEY_FIELDS.project,
SURVEY_FIELDS.name,
SURVEY_FIELDS.code,
{
Header: 'Project ID',
source: 'project.id',
show: false,
},
{
Header: 'countries',
source: 'countryCodes',
show: false,
},
{
Header: 'Permission group',
source: 'permission_group.name',
Expand All @@ -183,6 +195,19 @@ const SURVEY_COLUMNS = [
Header: 'Survey group',
source: 'survey_group.name',
},
{
Header: 'View',
type: 'externalLink',
actionConfig: {
generateUrl: row => {
const { code, countryCodes } = row;
if (!countryCodes || !countryCodes.some(countryCode => !!countryCode)) return null;
const countryCodeToUse = countryCodes.includes('DL') ? 'DL' : countryCodes[0];
return `${REACT_APP_DATATRAK_WEB_URL}/survey/${countryCodeToUse}/${code}/1`;
},
title: 'View in DataTrak',
},
},
{
Header: 'Export',
type: 'export',
Expand Down
39 changes: 39 additions & 0 deletions packages/admin-panel/src/table/columnTypes/ExternalLinkButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@material-ui/core';
import { OpenInNewRounded } from '@material-ui/icons';
import { ColumnActionButton } from './ColumnActionButton';
import { makeSubstitutionsInString } from '../../utilities';

export const ExternalLinkButton = ({ actionConfig, row }) => {
const getUrl = () => {
if (actionConfig.generateUrl) {
return actionConfig.generateUrl(row.original);
}
return makeSubstitutionsInString(actionConfig.url, row.original);
};
const fullUrl = getUrl();
if (!fullUrl) return null;

return (
<ColumnActionButton
className="link-button"
title={actionConfig.title}
component={Link}
href={fullUrl}
target="_blank"
>
<OpenInNewRounded />
</ColumnActionButton>
);
};

ExternalLinkButton.propTypes = {
actionConfig: PropTypes.object.isRequired,
row: PropTypes.object.isRequired,
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { BulkEditButton } from './BulkEditButton';
import { TestDatabaseConnectionButton } from './TestDatabaseConnectionButton';
import { QrCodeButton } from './QrCodeButton';
import { ResubmitSurveyResponseButton } from './ResubmitSurveyResponseButton';
import { ExternalLinkButton } from './ExternalLinkButton';

const generateCustomCell = (CustomCell, actionConfig, reduxId) => props =>
<CustomCell actionConfig={actionConfig} reduxId={reduxId} {...props} />;
Expand All @@ -40,6 +41,7 @@ const CUSTOM_CELL_COMPONENTS = {
testDatabaseConnection: TestDatabaseConnectionButton,
qrCode: QrCodeButton,
resubmitSurveyResponse: ResubmitSurveyResponseButton,
externalLink: ExternalLinkButton,
};

const BUTTON_COLUMN_TYPES = [
Expand All @@ -51,6 +53,7 @@ const BUTTON_COLUMN_TYPES = [
'qrCode',
'testDatabaseConnection',
'bulkEdit',
'externalLink',
'sync',
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

const extractParams = template =>
[...template.matchAll(/\{(\w+)\}/gi)].map(matchArray => matchArray[1]);
[...template.matchAll(/(?<=\{)(.*?)(?=\})/gi)].map(matchArray => matchArray[1]);

export const makeSubstitutionsInString = (template, variables) => {
const params = extractParams(template);
Expand Down
27 changes: 26 additions & 1 deletion packages/central-server/src/apiV2/surveys/GETSurveys.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { processColumns } from '../GETHandler/helpers';

const SURVEY_QUESTIONS_COLUMN = 'surveyQuestions';
const COUNTRY_NAMES_COLUMN = 'countryNames';
const COUNTRY_CODES_COLUMN = 'countryCodes';

export class GETSurveys extends GETHandler {
permissionsFilteredInternally = true;
Expand All @@ -44,11 +45,13 @@ export class GETSurveys extends GETHandler {

// 2. Add countryNames
const countryNames = await this.getSurveyCountryNames([surveyId]);
const countryCodes = await this.getSurveyCountryCodes([surveyId]);

return {
...survey,
surveyQuestions: surveyQuestionsValues[surveyId],
countryNames: countryNames[surveyId],
countryCodes: countryCodes[surveyId],
};
}

Expand All @@ -65,10 +68,16 @@ export class GETSurveys extends GETHandler {
records.filter(record => record.id).map(record => record.id),
);

// 3. Add countryCodes
const countryCodes = await this.getSurveyCountryCodes(
records.filter(record => record.id).map(record => record.id),
);

return records.map(record => ({
...record,
surveyQuestions: surveyQuestionsValues[record.id],
countryNames: countryNames[record.id],
countryCodes: countryCodes[record.id],
}));
}

Expand Down Expand Up @@ -104,9 +113,10 @@ export class GETSurveys extends GETHandler {
// If we've requested specific columns, we allow skipping these fields by not requesting them
this.includeQuestions = parsedColumns.includes(SURVEY_QUESTIONS_COLUMN);
this.includeCountryNames = parsedColumns.includes(COUNTRY_NAMES_COLUMN);
this.includeCountryCodes = parsedColumns.includes(COUNTRY_CODES_COLUMN);

const unprocessedColumns = parsedColumns.filter(
col => ![SURVEY_QUESTIONS_COLUMN, COUNTRY_NAMES_COLUMN].includes(col),
col => ![SURVEY_QUESTIONS_COLUMN, COUNTRY_NAMES_COLUMN, COUNTRY_CODES_COLUMN].includes(col),
);
return processColumns(this.models, unprocessedColumns, this.recordType);
}
Expand Down Expand Up @@ -162,6 +172,21 @@ export class GETSurveys extends GETHandler {
);
return Object.fromEntries(rows.map(row => [row.id, row.country_names]));
}

async getSurveyCountryCodes(surveyIds) {
if (surveyIds.length === 0 || !this.includeCountryCodes) return {};
const rows = await this.database.executeSql(
`
SELECT survey.id, array_agg(country.code) as country_codes
FROM survey
LEFT JOIN country ON (country.id = any(survey.country_ids))
WHERE survey.id in (${surveyIds.map(() => '?').join(',')})
GROUP BY survey.id;
`,
surveyIds,
);
return Object.fromEntries(rows.map(row => [row.id, row.country_codes.sort()]));
}
}

const getAggregatedQuestions = rawResults => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import { useSearchParams } from 'react-router-dom';
import styled from 'styled-components';
import { DialogActions, Paper, Typography } from '@material-ui/core';
import { SpinningLoader } from '@tupaia/ui-components';
Expand Down Expand Up @@ -102,6 +103,8 @@ const sortAlphanumerically = (a: ListItemType, b: ListItemType) => {
export const SurveySelectPage = () => {
const navigate = useNavigate();
const [selectedSurvey, setSelectedSurvey] = useState<ListItemType | null>(null);
const [urlSearchParams] = useSearchParams();
const urlProjectId = urlSearchParams.get('projectId');
const {
countries,
selectedCountry,
Expand All @@ -112,7 +115,7 @@ export const SurveySelectPage = () => {
const navigateToSurvey = () => {
navigate(`/survey/${selectedCountry?.code}/${selectedSurvey?.value}`);
};
const { mutate: updateUser, isLoading: isUpdatingUser } = useEditUser(navigateToSurvey);
const { mutateAsync: updateUser, isLoading: isUpdatingUser } = useEditUser();
const user = useCurrentUserContext();

const { data: surveys, isLoading } = useProjectSurveys(user.projectId, selectedCountry?.name);
Expand Down Expand Up @@ -162,7 +165,12 @@ export const SurveySelectPage = () => {
const handleSelectSurvey = () => {
if (countryHasUpdated) {
// update user with new country. If the user goes 'back' and doesn't select a survey, and does not yet have a country selected, that's okay because it will be set whenever they next select a survey
updateUser({ countryId: selectedCountry?.id });
updateUser(
{ countryId: selectedCountry?.id },
{
onSuccess: navigateToSurvey,
},
);
} else navigateToSurvey();
};

Expand All @@ -173,19 +181,34 @@ export const SurveySelectPage = () => {
}
}, [JSON.stringify(surveys)]);

const showLoader = isLoading || isLoadingCountries || isUpdatingUser;
useEffect(() => {
const updateUserProject = async () => {
if (urlProjectId && user.projectId !== urlProjectId) {
updateUser({ projectId: urlProjectId });
}
};
updateUserProject();
}, [urlProjectId]);

const showLoader =
isLoading ||
isLoadingCountries ||
isUpdatingUser ||
(urlProjectId && urlProjectId !== user?.projectId); // in this case the user will be updating and all surveys etc will be reloaded, so showing a loader when this is the case means a more seamless experience
return (
<Container>
<HeaderWrapper>
<div>
<Typography variant="h1">Select survey</Typography>
<Subheader>Select a survey from the list below</Subheader>
</div>
<SurveyCountrySelector
countries={countries}
selectedCountry={selectedCountry}
onChangeCountry={updateSelectedCountry}
/>
{!showLoader && (
<SurveyCountrySelector
countries={countries}
selectedCountry={selectedCountry}
onChangeCountry={updateSelectedCountry}
/>
)}
</HeaderWrapper>
{showLoader ? (
<LoadingContainer>
Expand Down

0 comments on commit 5210b7e

Please sign in to comment.