Skip to content

Implement OneToManyDyadCensus and Anonymisation Interfaces #235

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

Open
wants to merge 71 commits into
base: schema-8
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
d843880
set up blank interfaces
jthrilly Dec 5, 2024
1d546f1
first draught
jthrilly Dec 5, 2024
faf9c65
make Node components compatible with framer motion
jthrilly Dec 5, 2024
8142510
tidy up
jthrilly Dec 5, 2024
df4e0c4
improve edgeExists
jthrilly Dec 5, 2024
af9a94b
ts linting
jthrilly Dec 5, 2024
81311fe
knip
jthrilly Dec 5, 2024
26d319c
create encrypted background
jthrilly Dec 6, 2024
9b79713
further UI work on anonymisation interface
jthrilly Dec 6, 2024
1c86e50
initial port of @codaco/shared-consts
jthrilly Dec 10, 2024
8ffafb1
ignore eslint during build
jthrilly Dec 10, 2024
5d0215a
WIP fix selectors
jthrilly Dec 12, 2024
4068362
rename protocolUID (and protocolUid) => protocolId to match schema an…
jthrilly Dec 12, 2024
23adc4c
WIP extensive rework of types for redux
jthrilly Dec 12, 2024
df0674a
WIP addNode
jthrilly Dec 12, 2024
21a1146
bring changes from schema-8
jthrilly Jan 16, 2025
7808532
merge schema 8 changes;implement withoutDate utility
jthrilly Jan 16, 2025
903e36d
bring in thunk middleware change
jthrilly Jan 16, 2025
3258e66
add date serislisation to test function
jthrilly Jan 17, 2025
a0f9c66
remove last uses of Date in session state
jthrilly Jan 19, 2025
f387e52
improve network and session reducers;reimplement removeNode;rename re…
jthrilly Jan 19, 2025
9380914
further improve session and network reducers
jthrilly Jan 19, 2025
6d7f121
implement registerBeforeNext on anonymisation interface
jthrilly Jan 19, 2025
db96166
Significant refactpr of the session and network stores
jthrilly Jan 19, 2025
7308280
working proof of concept (again)
jthrilly Jan 20, 2025
27617d2
fix issue with re-rendering no one to many dyad census
jthrilly Jan 20, 2025
1b37765
implement new session sync system using route handlers
jthrilly Jan 20, 2025
f3cc7a4
WIP defer to query state for navigation
jthrilly Jan 20, 2025
d785ae2
resolve issue with state resetting
jthrilly Jan 20, 2025
0f50559
fix out of bounds issue with skip map
jthrilly Jan 20, 2025
f457145
Merge pull request #270 from complexdatacollective/riing-navigation-f…
jthrilly Jan 20, 2025
f52ace6
WIP passphrase prompt component
jthrilly Jan 22, 2025
9fd1d3e
implement prompt dialog and state for showing prompter
jthrilly Jan 24, 2025
15e1427
dont dispatch events if they arent needed
jthrilly Jan 28, 2025
11e8cfe
implement passphrase verification
jthrilly Feb 7, 2025
51a13db
delay showing tooltip to fix bug with positioning
jthrilly Feb 7, 2025
ea8323a
implement invalid passphrase workflow
jthrilly Feb 7, 2025
d211953
Merge pull request #277 from complexdatacollective/passphrase-prompter
jthrilly Feb 7, 2025
49e1fec
merge with schema 8 brranch
jthrilly Feb 11, 2025
1bc341a
make alert variant consistent
jthrilly Feb 26, 2025
d13b327
merge with schema 8 branch
jthrilly Mar 6, 2025
600f740
linting session selectors
jthrilly Mar 14, 2025
b807de7
add types to processEntityVariables
jthrilly Mar 14, 2025
2294c60
port changes from https://github.com/complexdatacollective/Fresco/pul…
jthrilly Mar 14, 2025
f3eafc9
updated types for createGraphML; use document fragments to simplify b…
jthrilly Mar 19, 2025
3b65766
further type fixes
jthrilly Mar 19, 2025
28f1dba
fix tests
jthrilly Mar 21, 2025
956b952
complete fixing network-exporter tests
jthrilly Mar 21, 2025
ea1af98
WIP field validation tests
jthrilly Mar 21, 2025
0c05ecd
field-validation tests passing
jthrilly Mar 25, 2025
61d6133
Merge pull request #318 from complexdatacollective/dombuilder
jthrilly Mar 25, 2025
05f9672
add types to network query
jthrilly Mar 26, 2025
b2cb8ca
linting
jthrilly Mar 26, 2025
b399d43
update to published package versions
jthrilly Mar 31, 2025
96777e9
transpile @codado/shared-consts
jthrilly Mar 31, 2025
11bb659
wip fixing build errors
jthrilly Mar 31, 2025
6018818
building and linting
jthrilly Apr 1, 2025
9d3c5dc
update form component; correctly pass server payload into state; fix …
jthrilly Apr 4, 2025
b41bddb
use current step
jthrilly Apr 8, 2025
8313f18
fix bug caused by skip logic getting stale data;refactor fake progres…
jthrilly Apr 8, 2025
b1bc6d3
broken implementation of ego form skip fix
jthrilly Apr 9, 2025
67f09cb
fix issue caused by incorrectly memoising selector
jthrilly Apr 9, 2025
b1557b9
Merge pull request #351 from complexdatacollective/broken
jthrilly Apr 9, 2025
0bf0354
fix required() tests and add additional coverage
jthrilly Apr 9, 2025
be4dd76
implement useId in form fields; remove prop selectors
jthrilly Apr 15, 2025
869259b
fix issues with OneToManyDyadCensus animations; disable node creation…
jthrilly Apr 27, 2025
7857307
convert createSorter to typescript
jthrilly Apr 27, 2025
5a9a5e1
implement sorting
jthrilly Apr 27, 2025
dbd7620
reinstate missing createSorter test
jthrilly Apr 27, 2025
6377795
fix ts issues in createSorter
jthrilly Apr 27, 2025
ee2d04e
attempt to handle scrolling
jthrilly Apr 27, 2025
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
7 changes: 3 additions & 4 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const path = require('path');

/** @type {import("eslint").Linter.Config} */
const config = {
overrides: [
Expand All @@ -10,13 +8,13 @@ const config = {
],
files: ['*.ts', '*.tsx'],
parserOptions: {
project: path.join(__dirname, 'tsconfig.json'),
project: true,
},
},
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: path.join(__dirname, 'tsconfig.json'),
project: true,
},
plugins: ['@typescript-eslint'],
extends: [
Expand All @@ -35,6 +33,7 @@ const config = {
],
rules: {
'@next/next/no-img-element': 'off',
'import/no-cycle': 'error',
'import/no-anonymous-default-export': 'off',
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'no-process-env': 'error',
Expand Down
2 changes: 1 addition & 1 deletion actions/activityFeed.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use server';

import type { Activity, ActivityType } from '~/components/DataTable/types';
import { safeRevalidateTag } from '~/lib/cache';
import type { Activity, ActivityType } from '~/lib/data-table/types';
import { prisma } from '~/utils/db';

export async function addEvent(
Expand Down
59 changes: 16 additions & 43 deletions actions/interviews.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use server';

import { type NcNetwork } from '@codaco/shared-consts';
import { createId } from '@paralleldrive/cuid2';
import { Prisma, type Interview, type Protocol } from '@prisma/client';
import { type Interview } from '@prisma/client';
import { cookies } from 'next/headers';
import trackEvent from '~/lib/analytics';
import { safeRevalidateTag } from '~/lib/cache';
import type { InstalledProtocols } from '~/lib/interviewer/store';
import { initialNetwork } from '~/lib/interviewer/ducks/modules/session';
import { formatExportableSessions } from '~/lib/network-exporters/formatters/formatExportableSessions';
import archive from '~/lib/network-exporters/formatters/session/archive';
import { generateOutputFiles } from '~/lib/network-exporters/formatters/session/generateOutputFiles';
Expand All @@ -18,13 +19,11 @@ import type {
FormattedSession,
} from '~/lib/network-exporters/utils/types';
import { getAppSetting } from '~/queries/appSettings';
import { getInterviewsForExport } from '~/queries/interviews';
import type {
CreateInterview,
DeleteInterviews,
SyncInterview,
} from '~/schemas/interviews';
import { type NcNetwork } from '~/schemas/network-canvas';
import {
getInterviewsForExport,
type GetInterviewsForExportReturnType,
} from '~/queries/interviews';
import type { CreateInterview, DeleteInterviews } from '~/schemas/interviews';
import { requireApiAuth } from '~/utils/auth';
import { prisma } from '~/utils/db';
import { ensureError } from '~/utils/ensureError';
Expand Down Expand Up @@ -86,26 +85,29 @@ export const updateExportTime = async (interviewIds: Interview['id'][]) => {
}
};

export type ExportedProtocol =
Awaited<GetInterviewsForExportReturnType>[number]['protocol'];

export const prepareExportData = async (interviewIds: Interview['id'][]) => {
await requireApiAuth();

const interviewsSessions = await getInterviewsForExport(interviewIds);

const protocolsMap = new Map<string, Protocol>();
const protocolsMap = new Map<string, ExportedProtocol>();
interviewsSessions.forEach((session) => {
protocolsMap.set(session.protocol.hash, session.protocol);
});

const formattedProtocols: InstalledProtocols =
Object.fromEntries(protocolsMap);
const formattedProtocols = Object.fromEntries(protocolsMap);

const formattedSessions = formatExportableSessions(interviewsSessions);

return { formattedSessions, formattedProtocols };
};

export const exportSessions = async (
formattedSessions: FormattedSession[],
formattedProtocols: InstalledProtocols,
formattedProtocols: Record<string, ExportedProtocol>,
interviewIds: Interview['id'][],
exportOptions: ExportOptions,
): Promise<ExportReturn> => {
Expand Down Expand Up @@ -200,7 +202,7 @@ export async function createInterview(data: CreateInterview) {
id: true,
},
data: {
network: Prisma.JsonNull,
network: initialNetwork,
participant: participantStatement,
protocol: {
connect: {
Expand Down Expand Up @@ -248,35 +250,6 @@ export async function createInterview(data: CreateInterview) {
}
}

export async function syncInterview(data: SyncInterview) {
const { id, network, currentStep, stageMetadata } = data;

try {
await prisma.interview.update({
where: {
id,
},
data: {
network,
currentStep,
stageMetadata,
lastUpdated: new Date(),
},
});

safeRevalidateTag(`getInterviewById-${id}`);

// eslint-disable-next-line no-console
console.log(`🚀 Interview synced with server! (${id})`);
return { success: true };
} catch (error) {
const message = ensureError(error).message;
return { success: false, error: message };
}
}

export type SyncInterviewType = typeof syncInterview;

export async function finishInterview(interviewId: Interview['id']) {
try {
const updatedInterview = await prisma.interview.update({
Expand Down
16 changes: 5 additions & 11 deletions actions/protocols.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use server';

import { type Protocol } from '@codaco/shared-consts';
import { Prisma } from '@prisma/client';
import { safeRevalidateTag } from 'lib/cache';
import { hash } from 'ohash';
Expand Down Expand Up @@ -125,31 +124,26 @@ export async function insertProtocol(
) {
await requireApiAuth();

const {
protocol: inputProtocol,
protocolName,
newAssets,
existingAssetIds,
} = protocolInsertSchema.parse(input);

const protocol = inputProtocol as Protocol;
const { protocol, protocolName, newAssets, existingAssetIds } =
protocolInsertSchema.parse(input);

try {
const protocolHash = hash(protocol);

await prisma.protocol.create({
data: {
hash: protocolHash,
lastModified: protocol.lastModified,
lastModified: protocol.lastModified ?? new Date(),
name: protocolName,
schemaVersion: protocol.schemaVersion,
stages: protocol.stages as unknown as Prisma.JsonArray, // The Stage interface needs to be changed to be a type: https://www.totaltypescript.com/type-vs-interface-which-should-you-use#index-signatures-in-types-vs-interfaces
stages: protocol.stages,
codebook: protocol.codebook,
description: protocol.description,
assets: {
create: newAssets,
connect: existingAssetIds.map((assetId) => ({ assetId })),
},
experiments: protocol.experiments ?? Prisma.JsonNull,
},
});

Expand Down
27 changes: 18 additions & 9 deletions app/(interview)/interview/[interviewId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { cookies } from 'next/headers';
import { notFound, redirect } from 'next/navigation';
import { syncInterview } from '~/actions/interviews';
import SuperJSON from 'superjson';
import FeedbackBanner from '~/components/Feedback/FeedbackBanner';
import { getAppSetting } from '~/queries/appSettings';
import { getInterviewById } from '~/queries/interviews';
import {
getInterviewById,
type GetInterviewByIdQuery,
} from '~/queries/interviews';
import { getServerSession } from '~/utils/auth';
import InterviewShell from '../_components/InterviewShell';

export const dynamic = 'force-dynamic';

export default async function Page({
params,
}: {
Expand All @@ -18,32 +23,36 @@ export default async function Page({
return 'No interview id found';
}

const interview = await getInterviewById(interviewId);
const session = await getServerSession();
const rawInterview = await getInterviewById(interviewId);

// If the interview is not found, redirect to the 404 page
if (!interview) {
if (!rawInterview) {
notFound();
}

const interview =
SuperJSON.parse<NonNullable<GetInterviewByIdQuery>>(rawInterview);
const session = await getServerSession();

// if limitInterviews is enabled
// Check cookies for interview already completed for this user for this protocol
// and redirect to finished page
const limitInterviews = await getAppSetting('limitInterviews');

if (limitInterviews && cookies().get(interview?.protocol?.id ?? '')) {
if (limitInterviews && cookies().get(interview.protocol.id)) {
redirect('/interview/finished');
}

// If the interview is finished, redirect to the finish page
if (interview?.finishTime) {
// If the interview is finished, redirect to the finish page, unless we are
// logged in as an admin
if (!session && interview?.finishTime) {
redirect('/interview/finished');
}

return (
<>
{session && <FeedbackBanner />}
<InterviewShell interview={interview} syncInterview={syncInterview} />
<InterviewShell rawPayload={rawInterview} />
</>
);
}
71 changes: 71 additions & 0 deletions app/(interview)/interview/[interviewId]/sync/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { NcNetworkSchema } from '@codaco/shared-consts';
import { NextResponse, type NextRequest } from 'next/server';
import { z } from 'zod';
import { safeRevalidateTag } from '~/lib/cache';
import { StageMetadataSchema } from '~/lib/interviewer/ducks/modules/session';
import { prisma } from '~/utils/db';
import { ensureError } from '~/utils/ensureError';

/**
* Handle post requests from the client to store the current interview state.
*/
const routeHandler = async (
request: NextRequest,
{ params }: { params: { interviewId: string } },
) => {
const interviewId = params.interviewId;

const rawRequest = await request.json();

const Schema = z.object({
id: z.string(),
network: NcNetworkSchema,
currentStep: z.number(),
stageMetadata: StageMetadataSchema.optional(),
lastUpdated: z.string(),
});

const validatedRequest = Schema.safeParse(rawRequest);

if (!validatedRequest.success) {
return NextResponse.json(
{
error: validatedRequest.error,
},
{ status: 400 },
);
}

const { network, currentStep, stageMetadata, lastUpdated } =
validatedRequest.data;

try {
await prisma.interview.update({
where: {
id: interviewId,
},
data: {
network,
currentStep,
stageMetadata: stageMetadata ?? undefined,
lastUpdated: new Date(lastUpdated),
},
});

safeRevalidateTag(`getInterviewById-${interviewId}`);

// eslint-disable-next-line no-console
console.log(`🚀 Interview synced with server! (${interviewId})`);
return NextResponse.json({ success: true });
} catch (error) {
const message = ensureError(error).message;
return NextResponse.json(
{
error: message,
},
{ status: 500 },
);
}
};

export { routeHandler as POST };
Loading