diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 6bb3833c1..0d1fef11d 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -6,6 +6,7 @@ module.exports = { parser: '@typescript-eslint/parser', 'extends': [ 'eslint:recommended', + 'plugin:react-hooks/recommended', // "plugin:react/recommended", // TODO: enable once ], 'ignorePatterns': ['**/api/**/*.js'], diff --git a/frontend/package.json b/frontend/package.json index fc6dff0cc..7fc10716c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -68,6 +68,7 @@ "@faker-js/faker": "^6.1.2", "@playwright/test": "^1.36.2", "@vitejs/plugin-react-refresh": "^1.3.6", + "eslint-plugin-react-hooks": "^4.6.0", "expect-playwright": "^0.8.0", "npm-run-all": "^4.1.5", "typescript": "^4.7.4", diff --git a/frontend/src/components/incorrect-user.tsx b/frontend/src/components/incorrect-user.tsx index 928c2359e..39fe3d8cc 100644 --- a/frontend/src/components/incorrect-user.tsx +++ b/frontend/src/components/incorrect-user.tsx @@ -1,17 +1,19 @@ import { React } from '@common' import { OXColoredStripe } from '@components' -import { loginURL } from '@lib' +import { loginURL, useEnvironment } from '@lib' export interface IncorrectUserProps { desiredRole?: string } export const IncorrectUser:React.FC = ({ desiredRole }) => { + const env = useEnvironment() + return (

Looks like you‘re not logged in{desiredRole ? ` as a ${desiredRole}` : ''}.

-

Please log in before using this site

+

Please log in before using this site

) diff --git a/frontend/src/components/multi-session-bar.tsx b/frontend/src/components/multi-session-bar.tsx index aff77c5ac..a0be5c6c0 100644 --- a/frontend/src/components/multi-session-bar.tsx +++ b/frontend/src/components/multi-session-bar.tsx @@ -12,6 +12,9 @@ export const MultiSessionBar: FC<{ study: ParticipantStudy }> = ({ study }) => { const [first, last] = study.stages const perc = (filter(study.stages, 'isCompleted').length / study.stages.length) * 100 + + // TODO Come back to this + // eslint-disable-next-line react-hooks/rules-of-hooks const duration = useMemo(() => { const d = last.availableAfterDays || 0 if (d === 0) return 'immediately' diff --git a/frontend/src/components/navbar/account-links.tsx b/frontend/src/components/navbar/account-links.tsx index e7d55ccfd..c801bd797 100644 --- a/frontend/src/components/navbar/account-links.tsx +++ b/frontend/src/components/navbar/account-links.tsx @@ -19,7 +19,7 @@ export default function AccountLinks() { {!env.isImpersonating && - { + { logout().then(() => refetch()) }}> diff --git a/frontend/src/lib/environment-provider.tsx b/frontend/src/lib/environment-provider.tsx index da965d439..621ae380b 100644 --- a/frontend/src/lib/environment-provider.tsx +++ b/frontend/src/lib/environment-provider.tsx @@ -1,7 +1,6 @@ import { React } from '@common' import { UserInfo } from '@models' import { ErrorPage, IncorrectUser, LoadingAnimation } from '@components' - import { ENV } from './env' import { useApi } from './api-config' import { useQuery } from 'react-query'; @@ -57,10 +56,9 @@ export const useCurrentResearcher = () => { } export const useUserInfo = () => { - console.log('inside useUserInfo') + const env = useEnvironment() return useQuery('fetchUserInfo', () => { - console.log('inside use query') - return fetchUserInfo() + return fetchUserInfo(env) }) } @@ -72,40 +70,37 @@ export const useUserPreferences = () => { }) } -export const locationOrigin = () => { - const env = useEnvironment() - console.log(env) +export const locationOrigin = (env: Environment) => { if (env.accountsEnvName === 'production') { return `https://openstax.org`; } return `https://${env.accountsEnvName}.openstax.org`; } -export const loginURL = () => { - const url = accountsUrl() +export const loginURL = (env: Environment) => { + const url = accountsUrl(env) if (ENV.IS_DEV_MODE) return url return `${url}/login/?r=${encodeURIComponent(window.location.href)}` } -export const logoutURL = () => { +export const logoutURL = (env: Environment) => { if (ENV.IS_DEV_MODE) return '/dev/user'; - const homepage = encodeURIComponent(`${locationOrigin()}/kinetic`); - return `${accountsUrl()}/signout?r=${homepage}`; + const homepage = encodeURIComponent(`${locationOrigin(env)}/kinetic`); + return `${accountsUrl(env)}/signout?r=${homepage}`; } -export const accountsUrl = (): string => { +export const accountsUrl = (env: Environment): string => { if (ENV.IS_DEV_MODE) return '/dev/user' - return `${locationOrigin()}/accounts`; + return `${locationOrigin(env)}/accounts`; } -export const accountsApiUrl = (): string => { +export const accountsApiUrl = (env: Environment): string => { if (ENV.IS_DEV_MODE) return `${ENV.API_ADDRESS}/development/user/api/user` - return `${accountsUrl()}/api/user` + return `${accountsUrl(env)}/api/user` } -export const fetchUserInfo = async (): Promise => { - console.log('fetching user info') - const resp = await fetch(`${accountsApiUrl()}`, { credentials: 'include' }) +export const fetchUserInfo = async (env: Environment): Promise => { + const resp = await fetch(`${accountsApiUrl(env)}`, { credentials: 'include' }) return await resp.json() } diff --git a/frontend/src/screens/analysis/runs-table.tsx b/frontend/src/screens/analysis/runs-table.tsx index b697926ae..12c26bf51 100644 --- a/frontend/src/screens/analysis/runs-table.tsx +++ b/frontend/src/screens/analysis/runs-table.tsx @@ -131,6 +131,8 @@ const StatusIcon = styled(Icon)({ }) const useRunsTable = (analysis: Analysis) => { + const api = useApi() + const [sorting, setSorting] = React.useState([{ id: 'startedAt', desc: true, @@ -181,7 +183,6 @@ const useRunsTable = (analysis: Analysis) => { size: 375, enableSorting: false, cell: ({ row: { original: run } }: { row: { original: AnalysisRun } }) => { - const api = useApi() const canDownload = hasRunSucceeded(run) const cancelRun = () => { api.updateAnalysisRun({ diff --git a/frontend/src/screens/researcher/account/researcher-account-form.tsx b/frontend/src/screens/researcher/account/researcher-account-form.tsx index 2f0e0e1ee..673146e9e 100644 --- a/frontend/src/screens/researcher/account/researcher-account-form.tsx +++ b/frontend/src/screens/researcher/account/researcher-account-form.tsx @@ -53,17 +53,18 @@ const StyledForm = styled(Form)(({ readOnly }) => ({ export const ResearcherAccountForm: React.FC<{className?: string}> = ({ className }) => { const api = useApi() const [researcher, setResearcher] = useState(useCurrentResearcher()) + const [institution, setInstitution] = useState(researcher?.institution) const { refetch: refetchEnv } = useFetchEnvironment() const { data: userInfo, refetch: refetchUser } = useUserInfo() + if (!researcher) { return null } + // Default to OpenStax accounts first/last name if blank researcher.firstName = researcher.firstName || userInfo?.first_name researcher.lastName = researcher.lastName || userInfo?.last_name - const [institution, setInstitution] = useState(researcher.institution) - const saveResearcher = async (researcher: Researcher) => { try { if (!researcher.id) { diff --git a/frontend/src/screens/researcher/account/researcher-account-page.tsx b/frontend/src/screens/researcher/account/researcher-account-page.tsx index f2efe6304..c4782012f 100644 --- a/frontend/src/screens/researcher/account/researcher-account-page.tsx +++ b/frontend/src/screens/researcher/account/researcher-account-page.tsx @@ -1,6 +1,6 @@ import { Box, Col, Footer, Form, HelpLink, Icon, Modal, ResourceLinks, Tooltip, TopNavBar } from '@components'; import { React, styled, useState } from '@common'; -import { accountsUrl, useApi, useCurrentResearcher } from '@lib'; +import { accountsUrl, useApi, useCurrentResearcher, useEnvironment } from '@lib'; import { colors } from '@theme'; import { Researcher } from '@api'; import CustomerSupportImage from '../../../components/customer-support-image'; @@ -11,6 +11,7 @@ import { ResearcherAccountForm } from './researcher-account-form'; export default function ResearcherAccountPage() { const researcher = useCurrentResearcher() + const env = useEnvironment() if (!researcher) { return null @@ -23,7 +24,7 @@ export default function ResearcherAccountPage() {

My Account

- + Update Email & Password @@ -122,6 +123,9 @@ export const IRB = () => { const Avatar: React.FC = () => { const api = useApi() const [researcher, setResearcher] = useState(useCurrentResearcher()) + const [avatar, setAvatar] = useState() + const [isShowingModal, setShowingModal] = useState(false) + if (!researcher) { return null } @@ -133,8 +137,7 @@ const Avatar: React.FC = () => { height: 125, width: 125, }) - const [avatar, setAvatar] = useState() - const [isShowingModal, setShowingModal] = useState(false) + const onHide = () => setShowingModal(false) const saveResearcher = async (researcher: Researcher) => { @@ -149,7 +152,6 @@ const Avatar: React.FC = () => { onHide() } - return ( setShowingModal(true)} direction='column' align='center' gap='large' css={{ cursor: 'pointer' }} > diff --git a/frontend/src/screens/researcher/studies/create/edit-study.tsx b/frontend/src/screens/researcher/studies/create/edit-study.tsx index 589c6bcad..36e982639 100644 --- a/frontend/src/screens/researcher/studies/create/edit-study.tsx +++ b/frontend/src/screens/researcher/studies/create/edit-study.tsx @@ -1,4 +1,4 @@ -import { Box, React, useEffect, useMemo, useNavigate, useParams, useState, Yup } from '@common' +import { Box, React, useMemo, useNavigate, useParams, useState, Yup } from '@common' import { useApi, useQueryParam } from '@lib'; import { isDraft, useFetchStudy } from '@models'; import { @@ -24,6 +24,7 @@ import { colors } from '@theme'; import { ReviewStudy, SubmitStudyModal } from './forms/review-study'; import { noop } from 'lodash-es'; import { useLocalstorageState } from 'rooks'; +import { Navigate } from 'react-router-dom'; const buildValidationSchema = (allOtherStudies: Study[]) => { return Yup.object().shape({ @@ -46,7 +47,6 @@ const getFormDefaults = (study: Study, step: StudyStep) => { } export default function EditStudy() { - const nav = useNavigate() const id = useParams<{ id: string }>().id const { loading, study, setStudy, allStudies } = useFetchStudy(id || 'new') @@ -55,10 +55,7 @@ export default function EditStudy() { } if (!study) { - useEffect(() => { - nav('/studies') - }, []) - return <> + return } return ( diff --git a/frontend/src/screens/researcher/studies/create/forms/additional-sessions.tsx b/frontend/src/screens/researcher/studies/create/forms/additional-sessions.tsx index 09a2333c7..98336c7ad 100644 --- a/frontend/src/screens/researcher/studies/create/forms/additional-sessions.tsx +++ b/frontend/src/screens/researcher/studies/create/forms/additional-sessions.tsx @@ -66,9 +66,10 @@ const AdditionalSession: FC<{ onDelete: (index: number) => void, session: Stage | NewStage }> = ({ index, onDelete, session }) => { + const { register, getValues } = useFormContext() + // don't show the first session if (index === 0) return null - const { register, getValues } = useFormContext() const prevStagePoints = getValues(`stages.${index - 1}.points`) return ( diff --git a/frontend/src/screens/researcher/studies/overview/edit-submitted-study.tsx b/frontend/src/screens/researcher/studies/overview/edit-submitted-study.tsx index 7cb0e2590..d3219fafa 100644 --- a/frontend/src/screens/researcher/studies/overview/edit-submitted-study.tsx +++ b/frontend/src/screens/researcher/studies/overview/edit-submitted-study.tsx @@ -277,11 +277,12 @@ const ShareStudy: FC<{study: Study}> = () => { } const ClosingCriteria: FC<{study: Study}> = ({ study }) => { + const { watch, setValue, getValues, trigger } = useFormContext() + const firstStage = getFirstStage(study) if (!firstStage) { return null } - const { watch, setValue, getValues, trigger } = useFormContext() return ( diff --git a/frontend/src/screens/researcher/studies/overview/study-overview.tsx b/frontend/src/screens/researcher/studies/overview/study-overview.tsx index 8319c76aa..0bb735c0c 100644 --- a/frontend/src/screens/researcher/studies/overview/study-overview.tsx +++ b/frontend/src/screens/researcher/studies/overview/study-overview.tsx @@ -1,4 +1,4 @@ -import { Box, React, useEffect, useNavigate, useParams } from '@common'; +import { Box, React, useNavigate, useParams } from '@common'; import { Study } from '@api'; import { Col, CollapsibleSection, ExitButton, LoadingAnimation, Page, ResearcherButton } from '@components'; import { getStudyLead, getStudyPi, isReadyForLaunch, isWaiting, useFetchStudy } from '@models'; @@ -8,33 +8,24 @@ import { FinalizeStudy } from './finalize-study'; import Waiting from '@images/study-creation/waiting.svg' import { EditSubmittedStudy } from './edit-submitted-study'; import { useQueryParam } from '@lib'; -import { Link } from 'react-router-dom'; +import { Link, Navigate } from 'react-router-dom'; export default function StudyOverview() { - const nav = useNavigate() const id = useParams<{ id: string }>().id + const { loading, study } = useFetchStudy(id!) if (!id) { - useEffect(() => { - nav('/studies') - }, []) - return <> + return } - const { loading, study } = useFetchStudy(id) - if (loading) { return } if (!study) { - useEffect(() => { - nav('/studies') - }, []) - return <> + return } - return ( diff --git a/frontend/src/screens/study-landing.tsx b/frontend/src/screens/study-landing.tsx index 94efce3dd..cbbbad32d 100644 --- a/frontend/src/screens/study-landing.tsx +++ b/frontend/src/screens/study-landing.tsx @@ -109,17 +109,7 @@ export default function UsersStudies() { const abort = useQueryParam('abort') == 'true' const md = useQueryParam('md') || {} - if (!user) { - return - } - const onNav = () => { - if (isIframed()) { - sendMessageToParent({ closeStudyModal: true }) - } else { - nav('/studies') - } - } useEffect(() => { let isPreview = false try { @@ -141,6 +131,19 @@ export default function UsersStudies() { .catch(setError) }, []) + if (!user) { + return + } + + const onNav = () => { + if (isIframed()) { + sendMessageToParent({ closeStudyModal: true }) + } else { + nav('/studies') + } + } + + if (error) { return } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1fd25307d..eab3c9d34 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2076,6 +2076,11 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" +eslint-plugin-react-hooks@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + eslint-plugin-react@^7.29.4: version "7.33.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.0.tgz#6c356fb0862fec2cd1b04426c669ea746e9b6eb3"