diff --git a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx index 1ef3cabc99..f1c309e959 100644 --- a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx +++ b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx @@ -54,7 +54,7 @@ class ErrorDetail extends Component { const { response } = error; let message = ''; - if (response.data) { + if (response?.data) { message = typeof response.data === 'string' ? response.data @@ -64,8 +64,8 @@ class ErrorDetail extends Component { return ( - {response.config.method.toUpperCase()} {response.config.url}{' '} - {response.status} + {response?.config?.method.toUpperCase()} {response?.config?.url}{' '} + {response?.status} {message} diff --git a/awx/ui_next/src/screens/Template/TemplateSurvey.jsx b/awx/ui_next/src/screens/Template/TemplateSurvey.jsx index 9c24b29439..bb19f04df4 100644 --- a/awx/ui_next/src/screens/Template/TemplateSurvey.jsx +++ b/awx/ui_next/src/screens/Template/TemplateSurvey.jsx @@ -1,56 +1,104 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Switch, Route } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; import { JobTemplatesAPI } from '@api'; +import ContentError from '@components/ContentError'; +import AlertModal from '@components/AlertModal'; +import ErrorDetail from '@components/ErrorDetail'; +import useRequest, { useDismissableError } from '@util/useRequest'; import SurveyList from './shared/SurveyList'; -export default function TemplateSurvey({ template }) { - const [survey, setSurvey] = useState(null); +function TemplateSurvey({ template, i18n }) { const [surveyEnabled, setSurveyEnabled] = useState(template.survey_enabled); - useEffect(() => { - (async () => { + const { + result: survey, + request: fetchSurvey, + isLoading, + error: loadingError, + setValue: setSurvey, + } = useRequest( + useCallback(async () => { const { data } = await JobTemplatesAPI.readSurvey(template.id); - setSurvey(data); - })(); - }, [template.id]); + return data; + }, [template.id]) + ); + useEffect(() => { + fetchSurvey(); + }, [fetchSurvey]); - const updateSurvey = async newQuestions => { - await JobTemplatesAPI.updateSurvey(template.id, { - ...survey, - spec: newQuestions, - }); - setSurvey({ - ...survey, - spec: newQuestions, - }); - }; + const { + request: updateSurvey, + isLoading: isUpdateLoading, + error: updateError, + } = useRequest( + useCallback( + async updatedSurvey => { + await JobTemplatesAPI.updateSurvey(template.id, updatedSurvey); + setSurvey(updatedSurvey); + }, + [template.id, setSurvey] + ) + ); - const deleteSurvey = async () => { - await JobTemplatesAPI.destroySurvey(template.id); - setSurvey(null); - }; + const { + request: deleteSurvey, + isLoading: isDeleteLoading, + error: deleteError, + } = useRequest( + useCallback(async () => { + await JobTemplatesAPI.destroySurvey(template.id); + setSurvey(null); + }, [template.id, setSurvey]) + ); - const toggleSurvey = async () => { - await JobTemplatesAPI.update(template.id, { - survey_enabled: !surveyEnabled, - }); - setSurveyEnabled(!surveyEnabled); - }; + const { + request: toggleSurvey, + isLoading: isToggleLoading, + error: toggleError, + } = useRequest( + useCallback(async () => { + await JobTemplatesAPI.update(template.id, { + survey_enabled: !surveyEnabled, + }); + setSurveyEnabled(!surveyEnabled); + }, [template.id, surveyEnabled]) + ); - // TODO - // if (contentError) { - // return ; + const { error, dismissError } = useDismissableError( + updateError || deleteError || toggleError + ); + + if (loadingError) { + return ; + } return ( - - - - - + <> + + + updateSurvey({ ...survey, spec })} + deleteSurvey={deleteSurvey} + /> + + + {error && ( + + {i18n._(t`Failed to update survey.`)} + + + )} + ); } + +export default withI18n()(TemplateSurvey); diff --git a/awx/ui_next/src/screens/Template/shared/SurveyList.jsx b/awx/ui_next/src/screens/Template/shared/SurveyList.jsx index d13ad3a2b7..f608d4caae 100644 --- a/awx/ui_next/src/screens/Template/shared/SurveyList.jsx +++ b/awx/ui_next/src/screens/Template/shared/SurveyList.jsx @@ -1,22 +1,17 @@ -import React, { useEffect, useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import useRequest from '@util/useRequest'; import { Button } from '@patternfly/react-core'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; import ErrorDetail from '@components/ErrorDetail'; -import { JobTemplatesAPI } from '@api'; import ContentEmpty from '@components/ContentEmpty'; import AlertModal from '@components/AlertModal'; import SurveyListItem from './SurveyListItem'; import SurveyToolbar from './SurveyToolbar'; -// survey.name -// survey.description -// survey.spec function SurveyList({ survey, surveyEnabled, @@ -27,7 +22,6 @@ function SurveyList({ }) { const questions = survey?.spec || []; const [selected, setSelected] = useState([]); - // const [showError, setShowError] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const isAllSelected = diff --git a/awx/ui_next/src/util/useRequest.js b/awx/ui_next/src/util/useRequest.js index 906f3160be..9f0ea0c91f 100644 --- a/awx/ui_next/src/util/useRequest.js +++ b/awx/ui_next/src/util/useRequest.js @@ -8,11 +8,13 @@ import { /* * The useRequest hook accepts a request function and returns an object with - * four values: + * six values: * request: a function to call to invoke the request * result: the value returned from the request function (once invoked) * isLoading: boolean state indicating whether the request is in active/in flight * error: any caught error resulting from the request + * setValue: setter to explicitly set the result value + * isInitialized: set to true once the result is initially fetched * * The hook also accepts an optional second parameter which is a default * value to set as result before the first time the request is made. @@ -34,23 +36,42 @@ export default function useRequest(makeRequest, initialValue) { result, error, isLoading, - request: useCallback(async () => { - setIsLoading(true); - try { - const response = await makeRequest(); - if (isMounted.current) { - setResult(response); + request: useCallback( + async (...args) => { + setIsLoading(true); + try { + const response = await makeRequest(...args); + if (isMounted.current) { + setResult(response); + } + } catch (err) { + if (isMounted.current) { + setError(err); + } + } finally { + if (isMounted.current) { + setIsLoading(false); + } } - } catch (err) { - if (isMounted.current) { - setError(err); - } - } finally { - if (isMounted.current) { - setIsLoading(false); - } - } - }, [makeRequest]), + }, + [makeRequest] + ), + setValue: setResult, + }; +} + +export function useDismissableError(error) { + const [showError, setShowError] = useState(false); + + useEffect(() => { + if (error) { + setShowError(true); + } + }, [error]); + + return { + error: showError && error, + dismissError: () => setShowError(false), }; } @@ -60,14 +81,8 @@ export function useDeleteItems( ) { const location = useLocation(); const history = useHistory(); - const [showError, setShowError] = useState(false); - const { error, isLoading, request } = useRequest(makeRequest, null); - - useEffect(() => { - if (error) { - setShowError(true); - } - }, [error]); + const { requestError, isLoading, request } = useRequest(makeRequest, null); + const { error, dismissError } = useDismissableError(requestError); const deleteItems = async () => { await request(); @@ -91,7 +106,7 @@ export function useDeleteItems( return { isLoading, deleteItems, - deletionError: showError && error, - clearDeletionError: () => setShowError(false), + deletionError: error, + clearDeletionError: dismissError, }; }