Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement registerBeforeNext and onComplete functionalities #44

Merged
merged 34 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
60e6076
implemented similar registerBeforeNext and onComplete functions
mrkarimoff Jan 4, 2024
bfe7fdb
updated onComplte function
mrkarimoff Jan 8, 2024
b87fd63
finished basic implementation of onComplete and registerBeforeNext
mrkarimoff Jan 8, 2024
94ef4f0
update storing beforeNextFunctions
mrkarimoff Jan 8, 2024
bc85e36
fix beforeNext invocation condition
mrkarimoff Jan 8, 2024
3123d8f
Merge branch 'main' into feature/registerbeforenext-oncomplete
jthrilly Jan 11, 2024
def37c9
Merge branch 'fix/promptid-selectors' into feature/registerbeforenext…
jthrilly Jan 11, 2024
8ac5b9a
several fixes for errors where selectors are briefly null during stag…
jthrilly Jan 11, 2024
94fc619
move useNavigationHelpers to its own file
jthrilly Jan 11, 2024
ea73871
Merge branch 'main' into feature/registerbeforenext-oncomplete
jthrilly Jan 12, 2024
159eca0
Merge branch 'feature/registerbeforenext-oncomplete' into fix/stage-t…
jthrilly Jan 12, 2024
31bf2cb
Merge pull request #49 from complexdatacollective/fix/stage-transition
jthrilly Jan 12, 2024
1d60acf
working prototype of new beforeNextFunction handler
jthrilly Jan 12, 2024
b805e82
merge
jthrilly Jan 12, 2024
83f83cb
functioning
jthrilly Jan 12, 2024
c969c1c
deregister handler when stage changes
jthrilly Jan 12, 2024
e390055
resolve issues caused by flash of old stage
jthrilly Jan 16, 2024
cff6c63
update uses of registerBeforeNext in all interfaces
jthrilly Jan 16, 2024
339e627
fix namegeneratorroster data fetching issue when variables arent defi…
jthrilly Jan 16, 2024
805dece
temporarily disable useReadyForNextStage
jthrilly Jan 17, 2024
4b1b26e
reinstage useReadyNextStage on EgoForm
jthrilly Jan 17, 2024
19b4e51
implement simple loading state for <video> element
jthrilly Jan 17, 2024
353d2a9
small fixes to onboarding
jthrilly Jan 19, 2024
a5d0c07
add trackEvent to tRPC routes
jthrilly Jan 19, 2024
8240d40
add rawError to protocol import reducer
jthrilly Jan 19, 2024
ee3d0d2
Merge pull request #52 from complexdatacollective/fix/asset-loading-s…
jthrilly Jan 19, 2024
e2fb998
fix bug with slidesform and new register before handler
jthrilly Jan 19, 2024
e4b0d9e
linting
jthrilly Jan 19, 2024
04ac35b
Merge branch 'main' into feature/registerbeforenext-oncomplete
jthrilly Jan 19, 2024
d8d27b6
fix tRPC request type
jthrilly Jan 19, 2024
e3490ca
fix type error in ServerSync
jthrilly Jan 19, 2024
ecb3190
remove res from tRPC route handler
jthrilly Jan 19, 2024
05f1108
add typing for registerBeforeNext and @ts-expect-error for CurrentInt…
jthrilly Jan 19, 2024
4b35073
third pass at linting registerBeforeNext
jthrilly Jan 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions app/(dashboard)/dashboard/_components/ProtocolUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import { AnimatePresence, motion } from 'framer-motion';
import JobCard from '~/components/ProtocolImport/JobCard';
import { useCallback } from 'react';

export default function ProtocolUploader() {
const { importProtocols, jobs, cancelJob, cancelAllJobs } =
useProtocolImport();
export default function ProtocolUploader({
handleProtocolUploaded,
}: {
handleProtocolUploaded?: () => void;
}) {
const { importProtocols, jobs, cancelJob, cancelAllJobs } = useProtocolImport(
handleProtocolUploaded,
);

const { getRootProps, getInputProps, open } = useDropzone({
// Disable automatic opening of file dialog - we do it manually to allow for
Expand Down
139 changes: 56 additions & 83 deletions app/(interview)/interview/_components/InterviewShell.tsx
Original file line number Diff line number Diff line change
@@ -1,120 +1,93 @@
'use client';

import { isEqual } from 'lodash';
import { useQueryState } from 'next-usequerystate';
import { useEffect, useState } from 'react';
import { Provider, useSelector } from 'react-redux';
import usePrevious from '~/hooks/usePrevious';
import { Provider } from 'react-redux';
import DialogManager from '~/lib/interviewer/components/DialogManager';
import ProtocolScreen from '~/lib/interviewer/containers/ProtocolScreen';
import {
SET_SERVER_SESSION,
type SetServerSessionAction,
} from '~/lib/interviewer/ducks/modules/setServerSession';
import { getActiveSession } from '~/lib/interviewer/selectors/session';
import { store } from '~/lib/interviewer/store';
import { api } from '~/trpc/client';
import { useRouter } from 'next/navigation';

// The job of ServerSync is to listen to actions in the redux store, and to sync
// data with the server.
const ServerSync = ({ interviewId }: { interviewId: string }) => {
const [init, setInit] = useState(false);
// Current stage
const currentSession = useSelector(getActiveSession);
const prevCurrentSession = usePrevious(currentSession);
const { mutate: syncSessionWithServer } = api.interview.sync.useMutation();

useEffect(() => {
if (!init) {
setInit(true);
return;
}

if (
isEqual(currentSession, prevCurrentSession) ||
!currentSession ||
!prevCurrentSession
) {
return;
}

// check if current stage index is null (happens when hot reloading)
if (currentSession.currentStep === null) {
// eslint-disable-next-line no-console
console.log('⚠️ Current stage index is null. Skipping sync.');
return;
}

// eslint-disable-next-line no-console
console.log(`⬆️ Syncing session with server...`);
syncSessionWithServer({
id: interviewId,
network: currentSession.network,
currentStep: currentSession.currentStep,
});
}, [
currentSession,
prevCurrentSession,
interviewId,
syncSessionWithServer,
init,
]);

return null;
};
import ServerSync from './ServerSync';
import { parseAsInteger, useQueryState } from 'nuqs';
import { useEffect, useState } from 'react';
import { Spinner } from '~/lib/ui/components';

// The job of interview shell is to receive the server-side session and protocol
// and create a redux store with that data.
// Eventually it will handle syncing this data back.
const InterviewShell = ({ interviewID }: { interviewID: string }) => {
const [currentStage, setCurrentStage] = useQueryState('stage');
const router = useRouter();
const [initialized, setInitialized] = useState(false);

const [currentStage, setCurrentStage] = useQueryState(
'stage',
parseAsInteger,
);

const { isLoading } = api.interview.get.byId.useQuery(
const { isLoading, data: serverData } = api.interview.get.byId.useQuery(
{ id: interviewID },
{
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
onSuccess: async (data) => {
if (!data) {
return;
}
if (data.finishTime) {
router.push('/interview/finished');
}
},
);

const { protocol, ...serverSession } = data;
useEffect(() => {
if (initialized || !serverData) {
return;
}

// eslint-disable-next-line no-console
console.log(
'✅ Received server session. Setting current stage, and initializing redux store...',
);
// If the interview is finished, redirect to the finish page
if (serverData.finishTime) {
router.push('/interview/finished');
return;
}

if (!currentStage) {
await setCurrentStage(serverSession.currentStep.toString());
}
const { protocol, ...serverSession } = serverData;

// If we have a current stage in the URL bar, and it is different from the
// server session, set the server session to the current stage.
//
// If we don't have a current stage in the URL bar, set it to the server
// session, and set the URL bar to the server session.
if (currentStage === null) {
void setCurrentStage(serverSession.currentStep);
} else if (currentStage !== serverSession.currentStep) {
serverSession.currentStep = currentStage;
}

store.dispatch<SetServerSessionAction>({
type: SET_SERVER_SESSION,
payload: {
protocol,
session: serverSession,
},
});
// If there's no current stage in the URL bar, set it.
store.dispatch<SetServerSessionAction>({
type: SET_SERVER_SESSION,
payload: {
protocol,
session: serverSession,
},
},
);
});

setInitialized(true);
}, [
serverData,
currentStage,
setCurrentStage,
router,
initialized,
setInitialized,
]);

if (isLoading) {
return 'Second loading stage...';
return <Spinner />;
}

return (
<Provider store={store}>
<ServerSync interviewId={interviewID} />
<ProtocolScreen />
<ServerSync interviewId={interviewID}>
<ProtocolScreen />
</ServerSync>
<DialogManager />
</Provider>
);
Expand Down
61 changes: 61 additions & 0 deletions app/(interview)/interview/_components/ServerSync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import { isEqual } from 'lodash';
import { type ReactNode, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import usePrevious from '~/hooks/usePrevious';
import { getActiveSession } from '~/lib/interviewer/selectors/session';
import { api } from '~/trpc/client';

// The job of ServerSync is to listen to actions in the redux store, and to sync
// data with the server.
const ServerSync = ({
interviewId,
children,
}: {
interviewId: string;
children: ReactNode;
}) => {
const [init, setInit] = useState(false);
// Current stage
const currentSession = useSelector(getActiveSession);
const prevCurrentSession = usePrevious(currentSession);
const { mutate: syncSessionWithServer } = api.interview.sync.useMutation();

useEffect(() => {
if (!init) {
setInit(true);
return;
}

if (
isEqual(currentSession, prevCurrentSession) ||
!currentSession ||
!prevCurrentSession
) {
return;
}

// eslint-disable-next-line no-console
console.log(`⬆️ Syncing session with server...`);
syncSessionWithServer({
id: interviewId,
network: currentSession.network,
currentStep: currentSession.currentStep ?? 0,
});
}, [
currentSession,
prevCurrentSession,
interviewId,
syncSessionWithServer,
init,
]);

if (!init) {
return <div>Sync Loading (no init)...</div>;
}

return children;
};

export default ServerSync;
12 changes: 7 additions & 5 deletions app/(interview)/interview/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default async function Page({
searchParams,
}: {
searchParams: {
identifier?: string;
participantIdentifier?: string;
};
}) {
const activeProtocol = await api.protocol.active.get.query();
Expand Down Expand Up @@ -45,10 +45,11 @@ export default async function Page({
// Anonymous recruitment is enabled

// Use the identifier from the URL, or generate a new one
const identifier = searchParams.identifier ?? faker.string.uuid();
const participantIdentifier =
searchParams.participantIdentifier ?? faker.string.uuid();

// Validate the identifier
const isValid = participantIdentifierSchema.parse(identifier);
const isValid = participantIdentifierSchema.parse(participantIdentifier);

if (!isValid) {
return (
Expand All @@ -60,8 +61,9 @@ export default async function Page({
}

// Create the interview
const { createdInterviewId, error } =
await api.interview.create.mutate(identifier);
const { createdInterviewId, error } = await api.interview.create.mutate(
participantIdentifier,
);

if (error) {
throw new Error('An error occurred while creating the interview');
Expand Down
Loading