Skip to content

Commit

Permalink
Merge pull request #5843 from beyondessential/release-2024-33
Browse files Browse the repository at this point in the history
Release 2024-33
  • Loading branch information
avaek authored Aug 18, 2024
2 parents 41f71ac + 8ca0b7e commit 33701a8
Show file tree
Hide file tree
Showing 100 changed files with 1,887 additions and 778 deletions.
1 change: 1 addition & 0 deletions packages/admin-panel/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ REACT_APP_CLIENT_BASIC_AUTH_HEADER=
REACT_APP_VIZ_BUILDER_API_URL=
SKIP_PREFLIGHT_CHECK=
PARSE_LINK_HEADER_MAXLEN=
REACT_APP_DATATRAK_WEB_URL=
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const useEntities = search =>
['entities', search],
async () => {
const endpoint = stringifyQuery(undefined, `entities`, {
columns: JSON.stringify(['name', 'code', 'id']),
columns: JSON.stringify(['name', 'code', 'id', 'country_code']),
filter: JSON.stringify({
name: { comparator: 'ilike', comparisonValue: `%${search}%`, castAs: 'text' },
}),
Expand Down
26 changes: 26 additions & 0 deletions packages/admin-panel/src/api/mutations/useEditSurveyResponse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import { useMutation, useQueryClient } from 'react-query';
import { useApiContext } from '../../utilities/ApiProvider';

export const useEditSurveyResponse = (surveyResponseId, updatedSurveyResponse) => {
const queryClient = useQueryClient();
const api = useApiContext();
return useMutation(
[`surveyResponseEdit`, surveyResponseId, updatedSurveyResponse],
() => {
return api.put(`surveyResponses/${surveyResponseId}`, null, updatedSurveyResponse);
},
{
throwOnError: true,
onSuccess: async () => {
// invalidate the survey response data
await queryClient.invalidateQueries(['surveyResubmitData', surveyResponseId]);
return 'completed';
},
},
);
};

This file was deleted.

2 changes: 1 addition & 1 deletion packages/admin-panel/src/importExport/ExportButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import React from 'react';
import PropTypes from 'prop-types';
import ExportIcon from '@material-ui/icons/GetApp';
import { ExportIcon } from '../icons';
import { makeSubstitutionsInString } from '../utilities';
import { useApiContext } from '../utilities/ApiProvider';
import { ColumnActionButton } from '../table/columnTypes/ColumnActionButton';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,45 @@
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import { getBrowserTimeZone } from '@tupaia/utils';
import moment from 'moment';
import { ApprovalStatus } from '@tupaia/types';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { SurveyResponsesExportModal } from '../../importExport';
import { getPluralForm } from '../../pages/resources/resourceName';
import { OutdatedFilter } from '../../table/columnTypes/columnFilters';

const RESOURCE_NAME = { singular: 'survey response' };

// Don't include not_required as an editable option because it can lead to
// mis-matches between surveys and survey responses
export const APPROVAL_STATUS_TYPES = Object.values(ApprovalStatus).map(type => ({
label: type,
value: type,
}));
const GREEN = '#47CA80';
const GREY = '#898989';

const Pill = styled.span`
background-color: ${({ $color }) => {
return `${$color}33`; // slightly transparent
}};
border-radius: 1.5rem;
padding: 0.3rem 0.9rem;
color: ${({ $color }) => $color};
.cell-content:has(&) > div {
overflow: visible;
}
`;

const ResponseStatusPill = ({ value }) => {
const text = value ? 'Outdated' : 'Current';
const color = value ? GREY : GREEN;
return <Pill $color={color}>{text}</Pill>;
};

ResponseStatusPill.propTypes = {
value: PropTypes.bool,
};

ResponseStatusPill.defaultProps = {
value: false,
};

const surveyName = {
Header: 'Survey',
Expand Down Expand Up @@ -56,9 +80,13 @@ const dateOfData = {
},
};

const approvalStatus = {
Header: 'Approval status',
source: 'approval_status',
const responseStatus = {
Header: 'Response status',
source: 'outdated',
Filter: OutdatedFilter,
width: 180,
// eslint-disable-next-line react/prop-types
Cell: ({ value }) => <ResponseStatusPill value={value} />,
};

const entityName = {
Expand All @@ -80,7 +108,7 @@ export const SURVEY_RESPONSE_COLUMNS = [
assessorName,
date,
dateOfData,
approvalStatus,
responseStatus,
{
Header: 'Export',
type: 'export',
Expand Down
191 changes: 85 additions & 106 deletions packages/admin-panel/src/surveyResponse/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,38 @@

import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Button } from '@tupaia/ui-components';
import { Divider } from '@material-ui/core';
import { useGetExistingData } from './useGetExistingData';
import { ModalContentProvider, ModalFooter } from '../widgets';
import { useResubmitSurveyResponse } from '../api/mutations/useResubmitSurveyResponse';
import { MODAL_STATUS } from './constants';
import { SurveyScreens } from './SurveyScreens';
import { useEditSurveyResponse } from '../api/mutations/useEditSurveyResponse';
import { ResponseFields } from './ResponseFields';

const ButtonGroup = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
`;

export const Form = ({ surveyResponseId, onDismiss, onAfterMutate }) => {
const [surveyResubmission, setSurveyResubmission] = useState({});
const [filesByQuestionCode, setFilesByQuestionCode] = useState({});
const isUnchanged = Object.keys(surveyResubmission).length === 0;
const [resubmitStatus, setResubmitStatus] = useState(MODAL_STATUS.INITIAL);
const [editedData, setEditedData] = useState({});
const isUnchanged = Object.keys(editedData).length === 0;

const [selectedEntity, setSelectedEntity] = useState({});
const [resubmitError, setResubmitError] = useState(null);

const useResubmitResponse = () => {
// Swap filesByQuestionCode to filesByUniqueFileName.
// Tracking by question code allows us to manage files easier e.g. don't have to worry about tracking them in deletions
// And the API endpoint needs them by uniqueFileName
const filesByUniqueFileName = {};
for (const [questionCode, file] of Object.entries(filesByQuestionCode)) {
const uniqueFileName = surveyResubmission.answers[questionCode];
filesByUniqueFileName[uniqueFileName] = file;
}
return useResubmitSurveyResponse(surveyResponseId, surveyResubmission, filesByUniqueFileName);
};
const { mutateAsync: resubmitResponse } = useResubmitResponse();

const handleResubmit = useCallback(async () => {
setResubmitStatus(MODAL_STATUS.LOADING);
try {
await resubmitResponse();
} catch (e) {
setResubmitStatus(MODAL_STATUS.ERROR);
setResubmitError(e);
return;
}
setResubmitStatus(MODAL_STATUS.SUCCESS);
onAfterMutate();
});
const {
mutateAsync: editResponse,
isLoading,
isError,
error: editError,
reset, // reset the mutation state so we can dismiss the error
isSuccess,
} = useEditSurveyResponse(surveyResponseId, editedData);

const { data, isLoading: isFetching, error: fetchError } = useGetExistingData(surveyResponseId);

const existingAndNewFields = { ...data?.surveyResponse, ...editedData };

useEffect(() => {
if (!data) {
setSelectedEntity({});
Expand All @@ -59,88 +45,81 @@ export const Form = ({ surveyResponseId, onDismiss, onAfterMutate }) => {
}
}, [data]);

const handleDismissError = () => {
setResubmitStatus(MODAL_STATUS.INITIAL);
setResubmitError(null);
const resubmitSurveyResponse = async () => {
await editResponse();
onAfterMutate();
};

const onSetFormFile = (questionCode, file) => {
setFilesByQuestionCode({ ...filesByQuestionCode, [questionCode]: file });
const getDatatrakBaseUrl = () => {
if (import.meta.env.REACT_APP_DATATRAK_WEB_URL)
return import.meta.env.REACT_APP_DATATRAK_WEB_URL;
const { origin } = window.location;
if (origin.includes('localhost')) return 'https://dev-datatrak.tupaia.org';
return origin.replace('admin', 'datatrak');
};

const renderButtons = useCallback(() => {
switch (resubmitStatus) {
case MODAL_STATUS.LOADING:
return <></>;
case MODAL_STATUS.ERROR:
return (
<>
<Button variant="outlined" onClick={() => handleDismissError()}>
Dismiss
</Button>
</>
);
case MODAL_STATUS.SUCCESS:
return (
<>
<Button onClick={onDismiss}>Close</Button>
</>
);
case MODAL_STATUS.INITIAL:
default:
return (
<>
<Button variant="outlined" onClick={onDismiss}>
Cancel
</Button>
<Button
id="form-button-resubmit"
type="submit"
onClick={() => handleResubmit()}
disabled={isFetching || isUnchanged}
>
Resubmit
</Button>
</>
);
const resubmitResponseAndRedirect = async () => {
// If the response has been changed, resubmit it before redirecting
if (!isUnchanged) {
await editResponse();
onAfterMutate();
}
}, [resubmitStatus, isFetching, isUnchanged]);
const { country_code: updatedCountryCode } = selectedEntity;
const { survey, primaryEntity } = data;
const countryCodeToUse = updatedCountryCode || primaryEntity.country_code;
const datatrakBaseUrl = getDatatrakBaseUrl();
const url = `${datatrakBaseUrl}/survey/${countryCodeToUse}/${survey.code}/resubmit/${surveyResponseId}`;
// Open the URL in a new tab, so the user can resubmit the response in Datatrak
window.open(url, '_blank');
};

const existingAndNewFields = { ...data?.surveyResponse, ...surveyResubmission };
const isResubmitting = resubmitStatus === MODAL_STATUS.LOADING;
const isResubmitSuccess = resubmitStatus === MODAL_STATUS.SUCCESS;
const renderButtons = useCallback(() => {
if (isLoading) return null;
if (isError)
return (
<Button variant="outlined" onClick={reset}>
Dismiss
</Button>
);
if (isSuccess) return <Button onClick={onDismiss}>Close</Button>;
return (
<ButtonGroup>
<Button
id="form-button-resubmit"
onClick={resubmitSurveyResponse}
variant="outlined"
disabled={isFetching || isUnchanged}
color="primary"
>
Save and close
</Button>
<div>
<Button variant="outlined" onClick={onDismiss}>
Cancel
</Button>
<Button id="form-button-next" onClick={resubmitResponseAndRedirect} disabled={isFetching}>
Next
</Button>
</div>
</ButtonGroup>
);
}, [isFetching, isUnchanged, isLoading, isError, isSuccess]);

return (
<>
<ModalContentProvider
isLoading={isFetching || isResubmitting}
error={fetchError || resubmitError}
>
{!isFetching && !isResubmitSuccess && (
<>
<ResponseFields
selectedEntity={selectedEntity}
surveyName={data?.survey.name}
fields={existingAndNewFields}
onChange={(field, updatedField) =>
setSurveyResubmission({ ...surveyResubmission, [field]: updatedField })
}
setSelectedEntity={setSelectedEntity}
/>
<Divider />
<SurveyScreens
onChange={(field, updatedField) =>
setSurveyResubmission({ ...surveyResubmission, [field]: updatedField })
}
onSetFormFile={onSetFormFile}
survey={data?.survey}
existingAnswers={data?.answers}
selectedEntity={selectedEntity}
fields={existingAndNewFields}
/>
</>
<ModalContentProvider isLoading={isFetching || isLoading} error={fetchError || editError}>
{!isFetching && !isSuccess && (
<ResponseFields
selectedEntity={selectedEntity}
surveyName={data?.survey.name}
fields={existingAndNewFields}
onChange={(field, updatedField) =>
setEditedData({ ...editedData, [field]: updatedField })
}
setSelectedEntity={setSelectedEntity}
/>
)}
{isResubmitSuccess && 'The survey response has been successfully submitted.'}
{isSuccess && 'The survey response has been successfully submitted.'}
</ModalContentProvider>
<ModalFooter>{renderButtons()}</ModalFooter>
</>
Expand Down
Loading

0 comments on commit 33701a8

Please sign in to comment.