diff --git a/src/components/EditQuestionnaireModal/index.tsx b/src/components/EditQuestionnaireModal/index.tsx index f13c173..6ec4fd1 100644 --- a/src/components/EditQuestionnaireModal/index.tsx +++ b/src/components/EditQuestionnaireModal/index.tsx @@ -28,9 +28,9 @@ import { QuestionnaireCreateInput, QuestionnaireMetadataQuery, QuestionnaireMetadataQueryVariables, - QuestionnarePriorityLevelTypeEnum, - QuestionnareEnumeratorSkillTypeEnum, - QuestionnareDataCollectionMethodTypeEnum, + QuestionnairePriorityLevelTypeEnum, + QuestionnaireEnumeratorSkillTypeEnum, + QuestionnaireDataCollectionMethodTypeEnum, } from '#generated/types'; import { @@ -105,19 +105,19 @@ const QUESTIONNAIRE_DETAIL = gql` const QUESTIONNAIRE_METADATA = gql` query QuestionnaireMetadata { - questionnarePriorityLevelTypeOptions: __type(name: "QuestionnarePriorityLevelTypeEnum") { + questionnairePriorityLevelTypeOptions: __type(name: "QuestionnairePriorityLevelTypeEnum") { enumValues { name description } } - questionnareEnumeratorSkillTypeOptions: __type(name: "QuestionnareEnumeratorSkillTypeEnum") { + questionnaireEnumeratorSkillTypeOptions: __type(name: "QuestionnaireEnumeratorSkillTypeEnum") { enumValues { name description } } - questionnareDataCollectionMethodTypeOptions: __type(name: "QuestionnareDataCollectionMethodTypeEnum") { + questionnaireDataCollectionMethodTypeOptions: __type(name: "QuestionnaireDataCollectionMethodTypeEnum") { enumValues { name description @@ -213,11 +213,12 @@ function EditQuestionnaireModal(props: Props) { QUESTIONNAIRE_METADATA, ); - const priorityLevelOptions = metadataResponse?.questionnarePriorityLevelTypeOptions?.enumValues; + const priorityLevelOptions = metadataResponse + ?.questionnairePriorityLevelTypeOptions?.enumValues; const enumeratorSkillOptions = metadataResponse - ?.questionnareEnumeratorSkillTypeOptions?.enumValues; + ?.questionnaireEnumeratorSkillTypeOptions?.enumValues; const dataCollectionMethods = metadataResponse - ?.questionnareDataCollectionMethodTypeOptions?.enumValues; + ?.questionnaireDataCollectionMethodTypeOptions?.enumValues; const [ triggerQuestionnaireCreate, @@ -360,7 +361,7 @@ function EditQuestionnaireModal(props: Props) { onChange={setFieldValue} value={formValue?.priorityLevel} error={fieldError?.priorityLevel} - options={priorityLevelOptions as EnumOptions} + options={priorityLevelOptions as EnumOptions} keySelector={enumKeySelector} labelSelector={enumLabelSelector} /> @@ -370,7 +371,9 @@ function EditQuestionnaireModal(props: Props) { onChange={setFieldValue} value={formValue?.enumeratorSkill} error={fieldError?.enumeratorSkill} - options={enumeratorSkillOptions as EnumOptions} + options={ + enumeratorSkillOptions as EnumOptions + } keySelector={enumKeySelector} labelSelector={enumLabelSelector} /> @@ -381,7 +384,7 @@ function EditQuestionnaireModal(props: Props) { value={formValue?.dataCollectionMethod} error={fieldError?.dataCollectionMethod} options={ - dataCollectionMethods as EnumOptions + dataCollectionMethods as EnumOptions } keySelector={enumKeySelector} labelSelector={enumLabelSelector} diff --git a/src/views/About/index.module.css b/src/views/About/index.module.css index f022db8..d4d6d41 100644 --- a/src/views/About/index.module.css +++ b/src/views/About/index.module.css @@ -1,8 +1,8 @@ .about { display: flex; flex-direction: column; - overflow-y: auto; height: 100vh; + overflow-y: auto; --color-headers: var(--dui-color-secondary); --color-cells: #dad4f1; @@ -32,12 +32,12 @@ gap: var(--dui-spacing-small); .parent { + border: var(--dui-color-separator) solid var(--dui-width-separator-thin); + background-color: #f5f5f5; padding: var(--dui-spacing-medium); width: 100%; text-align: center; font-weight: var(--dui-font-weight-bold); - background-color: #f5f5f5; - border: var(--dui-color-separator) solid var(--dui-width-separator-thin); } .sub-pillars{ display: flex; @@ -58,10 +58,10 @@ } .cell-header { + border: var(--dui-color-separator) solid var(--dui-width-separator-thin); + background-color: #f5f5f5; padding: 0 var(--dui-spacing-medium); font-weight: var(--dui-font-weight-bold); - background-color: #f5f5f5; - border: var(--dui-color-separator) solid var(--dui-width-separator-thin); } .cell { @@ -75,10 +75,10 @@ flex-grow: 1; border: none; border-radius: 0; - text-align: center; background-color: var(--color-cells); - color: var(--dui-color-primary); min-height: 1.6rem; + text-align: center; + color: var(--dui-color-primary); >* { justify-content: center; @@ -91,8 +91,8 @@ display: flex; flex-direction: column; flex-shrink: 0; - background-color: #fafafa; border: var(--dui-width-separator-thin) solid var(--dui-color-separator); + background-color: #fafafa; padding: var(--dui-spacing-large) var(--dui-spacing-extra-large); width: 30vw; max-width: 30rem; diff --git a/src/views/Home/QuestionnaireItem/index.module.css b/src/views/Home/QuestionnaireItem/index.module.css index 7daa44f..c361d29 100644 --- a/src/views/Home/QuestionnaireItem/index.module.css +++ b/src/views/Home/QuestionnaireItem/index.module.css @@ -7,3 +7,18 @@ width: 100%; } } +.modal-body{ + display: flex; + flex-direction: column; + justify-content: space-between; + gap: var(--dui-spacing-medium); + + .buttons { + display: flex; + gap: var(--dui-spacing-medium); + } + + .iframe { + flex-grow: 1; + } +} diff --git a/src/views/Home/QuestionnaireItem/index.tsx b/src/views/Home/QuestionnaireItem/index.tsx index bb5f202..3b80f8a 100644 --- a/src/views/Home/QuestionnaireItem/index.tsx +++ b/src/views/Home/QuestionnaireItem/index.tsx @@ -1,23 +1,41 @@ -import { useCallback } from 'react'; +import { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import { generatePath, } from 'react-router-dom'; import { IoEllipsisVertical, + IoDownloadOutline, + IoEyeOutline, + IoArrowRedoSharp, } from 'react-icons/io5'; import { isNotDefined, isDefined, } from '@togglecorp/fujs'; -import { gql, useMutation } from '@apollo/client'; import { - Header, + gql, + useMutation, + useQuery, +} from '@apollo/client'; +import { + AlertContext, + Button, + ButtonLikeLink, DateOutput, - TextOutput, - QuickActionDropdownMenu, DropdownMenuItem, - useConfirmation, + Header, + Modal, + QuickActionDropdownMenu, + QuickActionLink, + TextOutput, useAlert, + useConfirmation, useModalState, } from '@the-deep/deep-ui'; @@ -27,6 +45,10 @@ import { QuestionnairesForProjectQuery, DeleteQuestionnaireMutation, DeleteQuestionnaireMutationVariables, + ExportQuestionnaireMutation, + ExportQuestionnaireMutationVariables, + ExportDetailsQuery, + ExportDetailsQueryVariables, } from '#generated/types'; import styles from './index.module.css'; @@ -47,7 +69,67 @@ const DELETE_QUESTIONNAIRE = gql` } `; +const EXPORT_QUESTIONNAIRE = gql` + mutation ExportQuestionnaire ( + $projectId: ID!, + $questionnaireId: ID!, + ) { + private { + projectScope(pk: $projectId) { + createQuestionnaireExport( + data: { + questionnaire: $questionnaireId + } + ) { + errors + ok + result { + enketoPreviewUrl + id + questionnaireId + status + statusDisplay + } + } + } + } + } +`; + +const EXPORT_DETAILS = gql` + query ExportDetails ( + $projectId: ID!, + $exportId: ID!, + ) { + private { + projectScope(pk: $projectId) { + questionnaireExport(pk: $exportId) { + endedAt + enketoPreviewUrl + exportedAt + id + questionnaireId + status + startedAt + statusDisplay + xmlFile { + name + url + } + xlsxFile { + name + url + } + } + } + } + } +`; + +const DOWNLOAD_ALERT_NAME = 'questionnaire-export-download'; + type QuestionnaireType = NonNullable['projectScope']>['questionnaires']>['items']>[number]; + interface Props { questionnaireItem: QuestionnaireType; onQuestionnaireDeleteSuccess: () => void; @@ -63,12 +145,31 @@ function QuestionnaireItem(props: Props) { const alert = useAlert(); + const { + addAlert, + removeAlert, + } = useContext(AlertContext); + + const [ + exportModalShown, + showExportModal, + hideExportModal, + ] = useModalState(false); + + const [ + enketoPreviewModalShown, + showEnketoPreviewModal, + hideEnketoPreviewModal, + ] = useModalState(false); + const [ questionnaireModalShown, showQuestionnaireModal, hideQuestionnaireModal, ] = useModalState(false); + const [exportIdToDownload, setExportIdToDownload] = useState(); + const questionsEditLink = generatePath( wrappedRoutes.questionnaireEdit.absolutePath, { @@ -77,6 +178,124 @@ function QuestionnaireItem(props: Props) { }, ); + const [ + triggerQuestionnaireExport, + ] = useMutation( + EXPORT_QUESTIONNAIRE, + { + onCompleted: (response) => { + const exportResponse = response?.private?.projectScope?.createQuestionnaireExport; + if (!exportResponse?.ok) { + alert.show( + 'Some error occured during export.', + { variant: 'error' }, + ); + setExportIdToDownload(undefined); + } + if (exportResponse?.ok && exportResponse?.result?.id) { + setExportIdToDownload(exportResponse.result.id); + addAlert({ + variant: 'info', + duration: Infinity, + name: DOWNLOAD_ALERT_NAME, + children: 'Please wait while the export is being prepared.', + }); + } + }, + onError: () => { + setExportIdToDownload(undefined); + alert.show( + 'Some error occured during export.', + { variant: 'error' }, + ); + }, + }, + ); + + const handleQuestionnaireExport = useCallback(() => { + if (isNotDefined(projectId)) { + return; + } + triggerQuestionnaireExport({ + variables: { + projectId, + questionnaireId: questionnaireItem.id, + }, + }); + }, [ + projectId, + questionnaireItem.id, + triggerQuestionnaireExport, + ]); + + const exportVariables = useMemo(() => { + if (isNotDefined(projectId) || isNotDefined(exportIdToDownload)) { + return undefined; + } + return { + projectId, + exportId: exportIdToDownload, + }; + }, [ + projectId, + exportIdToDownload, + ]); + + const { + data: exportDetailsResponse, + startPolling, + stopPolling, + } = useQuery( + EXPORT_DETAILS, + { + skip: isNotDefined(exportVariables), + variables: exportVariables, + onCompleted: (response) => { + const exportResponse = response.private.projectScope?.questionnaireExport; + if (isNotDefined(exportResponse)) { + setExportIdToDownload(undefined); + removeAlert(DOWNLOAD_ALERT_NAME); + hideExportModal(); + alert.show( + 'There was some issue creating the export.', + { variant: 'error' }, + ); + } + if (exportResponse?.status === 'SUCCESS') { + showExportModal(); + removeAlert(DOWNLOAD_ALERT_NAME); + } else if (exportResponse?.status === 'FAILURE') { + hideExportModal(); + removeAlert(DOWNLOAD_ALERT_NAME); + alert.show( + 'There was some issue creating the export.', + { variant: 'error' }, + ); + } + }, + }, + ); + + useEffect( + () => { + const shouldPoll = exportIdToDownload + && exportDetailsResponse?.private?.projectScope?.questionnaireExport?.status !== 'SUCCESS' + && exportDetailsResponse?.private?.projectScope?.questionnaireExport?.status !== 'FAILURE'; + + if (shouldPoll) { + startPolling(5000); + } else { + stopPolling(); + } + }, + [ + exportDetailsResponse, + exportIdToDownload, + startPolling, + stopPolling, + ], + ); + const [ triggerQuestionnaireDelete, { loading: questionnaireDeletePending }, @@ -135,6 +354,13 @@ function QuestionnaireItem(props: Props) { message: 'Are you sure you want to delete this questionnaire?', }); + const handleModalClose = useCallback(() => { + hideExportModal(); + setExportIdToDownload(undefined); + }, [ + hideExportModal, + ]); + return (
Edit questions + + Export Questionnaire + )} + {exportModalShown && ( + + Preview the form here: + + Download form in XML and XLSX here: +
+ } + > + XML + + } + > + XLSX + +
+
+ )} + {enketoPreviewModalShown && ( + + + + )} + size="large" + > +