From eded2fb19ce9b484082bd5c66dd2efce4000e5b2 Mon Sep 17 00:00:00 2001 From: JacobLef Date: Tue, 18 Nov 2025 17:08:39 -0500 Subject: [PATCH 1/7] Add export route and service documents (blank). --- packages/api/src/routes/export.route.ts | 0 packages/api/src/services/export.service.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/api/src/routes/export.route.ts create mode 100644 packages/api/src/services/export.service.ts diff --git a/packages/api/src/routes/export.route.ts b/packages/api/src/routes/export.route.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/api/src/services/export.service.ts b/packages/api/src/services/export.service.ts new file mode 100644 index 00000000..e69de29b From c9d68eb181fb6630201c5a730c9958a0e658685f Mon Sep 17 00:00:00 2001 From: JacobLef Date: Fri, 28 Nov 2025 19:06:29 -0500 Subject: [PATCH 2/7] Add exporting types for the client's export structure as our internal types differ. --- packages/api/src/routes/export.route.ts | 0 packages/api/src/routes/studies.route.ts | 6 +++ packages/api/src/services/export.service.ts | 0 packages/api/src/services/study.service.ts | 20 ++++++++ packages/shared/types/client-export/types.ts | 52 ++++++++++++++++++++ 5 files changed, 78 insertions(+) delete mode 100644 packages/api/src/routes/export.route.ts delete mode 100644 packages/api/src/services/export.service.ts create mode 100644 packages/shared/types/client-export/types.ts diff --git a/packages/api/src/routes/export.route.ts b/packages/api/src/routes/export.route.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/api/src/routes/studies.route.ts b/packages/api/src/routes/studies.route.ts index 3d6d1442..bafc0402 100644 --- a/packages/api/src/routes/studies.route.ts +++ b/packages/api/src/routes/studies.route.ts @@ -107,4 +107,10 @@ router.post( authRoute((req, user) => studyService.duplicateStudy(user, req.params.id)) ); +router.get( + "/:id/export", + isAuthenticated, + authRoute((req, user) => studyService.exportStudy(user, req.params.id)) +); + export default router; diff --git a/packages/api/src/services/export.service.ts b/packages/api/src/services/export.service.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/api/src/services/study.service.ts b/packages/api/src/services/study.service.ts index 6a9d9409..cc4a0cf7 100644 --- a/packages/api/src/services/study.service.ts +++ b/packages/api/src/services/study.service.ts @@ -432,3 +432,23 @@ export const duplicateStudy = async ( return [201, newStudy._id]; }; + +/* +export const exportStudy = async ( + user: HydratedDocument, + studyId: string +): APIResponse => { + const study = await Study.findOne({ _id: studyId, owner: user._id }) + if (!study) throw new HttpError(404, "Study not found!"); + + const studies = extractStudies(study); + const { conditions, protocol_key, session_ids } = extractVariantInfo(studies) + const batteries = extractBatteries(conditions) + const stages = extractStages(batteries) + return [201, combine(studies, conditions, batteries, stages)]; +}; + +const exportStudies = async( + +): +*/ diff --git a/packages/shared/types/client-export/types.ts b/packages/shared/types/client-export/types.ts new file mode 100644 index 00000000..943ad23d --- /dev/null +++ b/packages/shared/types/client-export/types.ts @@ -0,0 +1,52 @@ +export interface ClientProtocolExport { + Version: number; + Protocols: Record; + Sessions: ClientSession[]; + SessionElements: ClientSessionElement[]; +} + +export interface ClientBatteryExport { + Type: string; + Name: string; + Description: string; + Version: number; + Stages: ClientStage[]; +} + +export interface ClientProtocol { + Name: string; + Sessions: number[]; +} + +export interface ClientSession { + Id: number; + Elements: number[]; +} + +export type ClientSessionElement = ClientBatteryElement | ClientEndElement; + +export interface ClientBatteryElement { + Id: number; + Type: "Battery"; + Battery: string; +} + +export interface ClientEndElement { + Id: number; + Type: "End"; + Instructions: string; + Lockout: number; + ShowQuitButton: boolean; + Password: string; +} + +export type ClientStagePrecursor = + | { Type: "None" } + | { Type: "Script Conditional"; Script: string }; + +export interface ClientStage { + Type: string; + StageLabel?: string; + "Stage Precursor"?: ClientStagePrecursor; + [key: string]: unknown; +} From c4580f273019d075b1a8f45ccfddd68d4fcaf14a Mon Sep 17 00:00:00 2001 From: jolinhuang224 Date: Sun, 7 Dec 2025 05:43:06 -0500 Subject: [PATCH 3/7] export & frontend --- packages/api/src/routes/studies.route.ts | 25 +- packages/api/src/services/export.service.ts | 246 ++++++++++++++++++ packages/api/src/services/study.service.ts | 24 +- packages/shared/types/client-export/types.ts | 5 + packages/ui/src/api/studies.ts | 16 ++ .../components/StudyDetailsSideBar.vue | 10 + .../components/StudyPanel.vue | 46 +++- 7 files changed, 349 insertions(+), 23 deletions(-) create mode 100644 packages/api/src/services/export.service.ts diff --git a/packages/api/src/routes/studies.route.ts b/packages/api/src/routes/studies.route.ts index bafc0402..69f5cd6b 100644 --- a/packages/api/src/routes/studies.route.ts +++ b/packages/api/src/routes/studies.route.ts @@ -1,4 +1,6 @@ -import { Router } from "express"; +import { Router, Request, Response, NextFunction } from "express"; +import type { HydratedDocument } from "mongoose"; +import type { IUser } from "@seitz/shared"; import { isAuthenticated } from "../middleware/auth"; import * as studyService from "../services/study.service"; @@ -110,7 +112,26 @@ router.post( router.get( "/:id/export", isAuthenticated, - authRoute((req, user) => studyService.exportStudy(user, req.params.id)) + async (req: Request, res: Response, next: NextFunction) => { + try { + const user = req.user as unknown as HydratedDocument; + const [status, exportObj] = await studyService.exportStudy( + user, + req.params.id + ); + if (!exportObj) return res.sendStatus(status); + + const filename = `study-${req.params.name}-export.json`; + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename}"` + ); + res.setHeader("Content-Type", "application/json; charset=utf-8"); + res.status(status).send(JSON.stringify(exportObj, null, 2)); + } catch (err) { + next(err); + } + } ); export default router; diff --git a/packages/api/src/services/export.service.ts b/packages/api/src/services/export.service.ts new file mode 100644 index 00000000..5b17add0 --- /dev/null +++ b/packages/api/src/services/export.service.ts @@ -0,0 +1,246 @@ +import { Study } from "../models"; // uses your existing Study model +import type { IStudyVariant } from "@seitz/shared"; + +// Lightweight local types to avoid `any` and satisfy eslint rules. +type LooseRecord = Record; +type StageLike = { + type?: string; + stageLabel?: string; + stagePrecursor?: unknown; +} & LooseRecord; +type BaseBatteryLike = { + stages?: StageLike[]; + name?: string; + description?: string; + version?: number; +} & LooseRecord; +type TaskRefLike = { + _id?: unknown; + name?: string; + battery?: { name?: string } & LooseRecord; +} & LooseRecord; +type TaskInstanceLike = { task?: TaskRefLike } & LooseRecord; +type SessionLike = { tasks?: TaskInstanceLike[] } & LooseRecord; +type VariantLike = { + serverCode?: string; + _id?: unknown; + name?: string; + sessions?: SessionLike[]; +} & LooseRecord; + +/** + * Build condition export + * Returns an object that looks like: { StudyId, StudyName, Conditions: [{ ProtocolKey, StudyName }, ...] } + */ +export async function buildStudyConditionExport(studyId: string) { + // load study and ensure ownership (or allow admin) + // If you already have an ownership check in another service, reuse it. + const study = await Study.findById(studyId) + .populate({ + path: "variants.sessions.tasks.task", + populate: { path: "battery" }, + }) + .lean() + .exec(); + + if (!study) throw new Error("Study not found"); + + // mapping -> list of conditions (one per variant) + const conditions = (study.variants || []).map((v: IStudyVariant) => ({ + ProtocolKey: v.serverCode || "", + StudyName: study.name || "", + })); + + return { + StudyId: study._id, + StudyName: study.name, + Conditions: conditions, + }; +} + +/** + * build full client export for study + */ +export async function buildFullClientExport(studyId: string) { + const study = await Study.findById(studyId) + .populate({ path: "batteries", populate: { path: "battery" } }) + .populate({ + path: "variants.sessions.tasks.task", + populate: { path: "battery" }, + }) + .lean() + .exec(); + + if (!study) throw new Error("Study not found"); + + const studyName = study.name || ""; + + const batteryMap = new Map(); + + if (Array.isArray(study.batteries)) { + for (const b of study.batteries) { + // b may already be populated + const id = + (b as unknown as LooseRecord)._id?.toString?.() ?? String(b as unknown); + if (!batteryMap.has(id)) batteryMap.set(id, b as unknown); + } + } + + // scan tasks in variants for referenced batteries + if (Array.isArray(study.variants)) { + for (const variant of (study.variants || []) as unknown as VariantLike[]) { + for (const session of variant.sessions || []) { + for (const t of session.tasks || []) { + const task = (t as TaskInstanceLike).task as TaskRefLike | undefined; + const idKey = task?._id ? String(task._id) : String(task ?? ""); + if (task && !batteryMap.has(idKey)) { + batteryMap.set(idKey, task as unknown); + } + } + } + } + } + + // Build Batteries export as an array + const Batteries = Array.from(batteryMap.values()).map((b) => { + // b may be a CustomizedBattery (with .battery populated) or a Battery + const baseBattery = (b as unknown as LooseRecord).battery ?? b; + const base = baseBattery as BaseBatteryLike; + + const stages = (base.stages || []).map((s) => { + const stage = s as StageLike; + const mapped: Record = { ...(stage as LooseRecord) }; + // normalize keys + if (stage.type) { + mapped.Type = stage.type; + delete (mapped as LooseRecord).type; + } + if (stage.stageLabel) { + mapped.StageLabel = stage.stageLabel; + delete (mapped as LooseRecord).stageLabel; + } + // TODO: stage precursor doesnt exist right now so placeholder is gonna be none, but should we add? + if (!mapped["Stage Precursor"]) { + if ((stage as LooseRecord).stagePrecursor) { + mapped["Stage Precursor"] = (stage as LooseRecord).stagePrecursor; + } else { + mapped["Stage Precursor"] = { Type: "None" }; + } + } + return mapped; + }); + + //TODO: our batteries right now also don't have a type field, temp making custom + return { + Type: "Custom", + Name: (b as LooseRecord).name ?? base.name ?? "", + Description: base.description ?? "", + Version: (base.version as number) || 0, + Stages: stages, + }; + }); + + // Conditions: one entry per variant + const Conditions = ((study.variants || []) as unknown as VariantLike[]).map( + (v) => ({ + ProtocolKey: v.serverCode || "", + StudyName: studyName, + }) + ); + + // Protocols: build a ClientProtocolExport-like structure + const Sessions: unknown[] = []; + const SessionElements: unknown[] = []; + const Protocols: Record = {}; + + let nextSessionId = 1; + let nextElementId = 1; + + for (const variant of (study.variants || []) as unknown as VariantLike[]) { + const protocolSessions: number[] = []; + + for (const session of variant.sessions || []) { + const sessionId = nextSessionId++; + const elementIds: number[] = []; + + for (const elem of session.tasks || []) { + const elId = nextElementId++; + elementIds.push(elId); + + // If element is a battery element + const task = (elem as TaskInstanceLike).task as TaskRefLike | undefined; + + // Use the customized battery name (if present) or fallback to referenced battery name + const batteryName = + task?.name ?? task?.battery?.name ?? String(task?._id ?? task); + + SessionElements.push({ + Id: elId, + Type: "Battery", + Battery: batteryName, + }); + } + + Sessions.push({ Id: sessionId, Elements: elementIds }); + protocolSessions.push(sessionId); + } + + //TODO: keeping protocolkey as variant server id for now + const protocolKey = variant.serverCode ?? String(variant._id ?? ""); + Protocols[protocolKey] = { + Name: variant.name || "", + Sessions: protocolSessions, + }; + } + + //TODO: where to get version? keeping 1 for now + const ProtocolExport = { + Version: 1, + Protocols, + Sessions, + SessionElements, + }; + + return { + studyName, + Batteries, + Conditions, + Protocols: ProtocolExport, + }; +} + +// Export study files to disk. This is a small helper used by maintenance scripts +// to write the raw study JSON and the client export. +import fs from "fs/promises"; +import path from "path"; + +export async function exportStudyToDisk(studyId: string, outDir: string) { + const study = await Study.findById(studyId).lean().exec(); + if (!study) throw new Error("Study not found"); + + await fs.mkdir(outDir, { recursive: true }); + + const studyDirName = `${(study.name || "study").replace( + /[^a-z0-9-_.]/gi, + "_" + )}_${studyId}`.slice(0, 140); + const studyDir = path.join(outDir, studyDirName); + await fs.mkdir(studyDir, { recursive: true }); + + // write raw study + await fs.writeFile( + path.join(studyDir, "study.json"), + JSON.stringify(study, null, 2), + "utf-8" + ); + + // build and write client export + const clientExport = await buildFullClientExport(studyId); + await fs.writeFile( + path.join(studyDir, "clientExport.json"), + JSON.stringify(clientExport, null, 2), + "utf-8" + ); + + return studyDir; +} diff --git a/packages/api/src/services/study.service.ts b/packages/api/src/services/study.service.ts index cc4a0cf7..e8a63e8d 100644 --- a/packages/api/src/services/study.service.ts +++ b/packages/api/src/services/study.service.ts @@ -1,6 +1,7 @@ import HttpError from "../types/errors"; import { CustomizedBattery, Study } from "../models"; import RedisService from "./redis.service"; +import { buildFullClientExport } from "./export.service"; import type { HydratedDocument, Types } from "mongoose"; import type { @@ -433,22 +434,29 @@ export const duplicateStudy = async ( return [201, newStudy._id]; }; -/* export const exportStudy = async ( user: HydratedDocument, studyId: string -): APIResponse => { - const study = await Study.findOne({ _id: studyId, owner: user._id }) +): APIResponse => { + const study = await Study.findOne({ _id: studyId, owner: user._id }); if (!study) throw new HttpError(404, "Study not found!"); + /* const studies = extractStudies(study); const { conditions, protocol_key, session_ids } = extractVariantInfo(studies) const batteries = extractBatteries(conditions) const stages = extractStages(batteries) return [201, combine(studies, conditions, batteries, stages)]; -}; + */ -const exportStudies = async( - -): -*/ + try { + const exportObj = await buildFullClientExport(studyId); + return [200, exportObj]; + } catch (error) { + console.error("Error exporting study:", error); + throw new HttpError( + 500, + "An internal error occured while exporting the study" + ); + } +}; diff --git a/packages/shared/types/client-export/types.ts b/packages/shared/types/client-export/types.ts index 943ad23d..94eabe43 100644 --- a/packages/shared/types/client-export/types.ts +++ b/packages/shared/types/client-export/types.ts @@ -50,3 +50,8 @@ export interface ClientStage { "Stage Precursor"?: ClientStagePrecursor; [key: string]: unknown; } + +export interface ClientConditionExport { + ProtocolKey: string; + StudyName: string; +} diff --git a/packages/ui/src/api/studies.ts b/packages/ui/src/api/studies.ts index dbc0e141..ecffc618 100644 --- a/packages/ui/src/api/studies.ts +++ b/packages/ui/src/api/studies.ts @@ -63,6 +63,21 @@ async function duplicateStudy(id: string) { return response.data; } +// Download export file for a study (calls /export/studies/:id) +async function downloadStudyExport(id: string) { + const resp = await axiosInstance.get(`/studies/${id}/export`, { + responseType: "blob", + }); + const url = window.URL.createObjectURL(resp.data); + const a = document.createElement("a"); + a.href = url; + a.download = `study-${id}-export.json`; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); +} + export interface VariantFromQuery { _id: string; name: string; @@ -91,4 +106,5 @@ export default { createStudy, fetchRecentStudies, duplicateStudy, + downloadStudyExport, }; diff --git a/packages/ui/src/pages/MyStudiesPage/components/StudyDetailsSideBar.vue b/packages/ui/src/pages/MyStudiesPage/components/StudyDetailsSideBar.vue index 17e66089..25a1920c 100644 --- a/packages/ui/src/pages/MyStudiesPage/components/StudyDetailsSideBar.vue +++ b/packages/ui/src/pages/MyStudiesPage/components/StudyDetailsSideBar.vue @@ -37,6 +37,15 @@ const fetchStudy = async () => { } }; +const downloadExport = async () => { + if (!props.studyId) return; + try { + await studiesAPI.downloadStudyExport(props.studyId); + } catch (err) { + console.error("Failed to download export:", err); + } +}; + watch( () => props.studyId, (newId) => { @@ -133,6 +142,7 @@ onUnmounted(() => { > Edit Study + Export
diff --git a/packages/ui/src/pages/StudyBuilderPage/components/StudyPanel.vue b/packages/ui/src/pages/StudyBuilderPage/components/StudyPanel.vue index b9e46f44..04f2c6e3 100644 --- a/packages/ui/src/pages/StudyBuilderPage/components/StudyPanel.vue +++ b/packages/ui/src/pages/StudyBuilderPage/components/StudyPanel.vue @@ -1,5 +1,6 @@ From 62fbde873529a5a0283e0c2e778ae246a55de019 Mon Sep 17 00:00:00 2001 From: Ananya Rath Date: Thu, 18 Dec 2025 02:09:48 -0500 Subject: [PATCH 4/7] cleaning comments --- packages/api/src/services/export.service.ts | 36 ++++----------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/packages/api/src/services/export.service.ts b/packages/api/src/services/export.service.ts index 5b17add0..006c1862 100644 --- a/packages/api/src/services/export.service.ts +++ b/packages/api/src/services/export.service.ts @@ -1,7 +1,7 @@ -import { Study } from "../models"; // uses your existing Study model +import { Study } from "../models"; import type { IStudyVariant } from "@seitz/shared"; -// Lightweight local types to avoid `any` and satisfy eslint rules. +// Local types used to maintain type safety and accomadate flexibility type LooseRecord = Record; type StageLike = { type?: string; @@ -28,13 +28,7 @@ type VariantLike = { sessions?: SessionLike[]; } & LooseRecord; -/** - * Build condition export - * Returns an object that looks like: { StudyId, StudyName, Conditions: [{ ProtocolKey, StudyName }, ...] } - */ export async function buildStudyConditionExport(studyId: string) { - // load study and ensure ownership (or allow admin) - // If you already have an ownership check in another service, reuse it. const study = await Study.findById(studyId) .populate({ path: "variants.sessions.tasks.task", @@ -45,7 +39,6 @@ export async function buildStudyConditionExport(studyId: string) { if (!study) throw new Error("Study not found"); - // mapping -> list of conditions (one per variant) const conditions = (study.variants || []).map((v: IStudyVariant) => ({ ProtocolKey: v.serverCode || "", StudyName: study.name || "", @@ -58,9 +51,6 @@ export async function buildStudyConditionExport(studyId: string) { }; } -/** - * build full client export for study - */ export async function buildFullClientExport(studyId: string) { const study = await Study.findById(studyId) .populate({ path: "batteries", populate: { path: "battery" } }) @@ -79,14 +69,12 @@ export async function buildFullClientExport(studyId: string) { if (Array.isArray(study.batteries)) { for (const b of study.batteries) { - // b may already be populated const id = (b as unknown as LooseRecord)._id?.toString?.() ?? String(b as unknown); if (!batteryMap.has(id)) batteryMap.set(id, b as unknown); } } - // scan tasks in variants for referenced batteries if (Array.isArray(study.variants)) { for (const variant of (study.variants || []) as unknown as VariantLike[]) { for (const session of variant.sessions || []) { @@ -101,16 +89,13 @@ export async function buildFullClientExport(studyId: string) { } } - // Build Batteries export as an array const Batteries = Array.from(batteryMap.values()).map((b) => { - // b may be a CustomizedBattery (with .battery populated) or a Battery const baseBattery = (b as unknown as LooseRecord).battery ?? b; const base = baseBattery as BaseBatteryLike; const stages = (base.stages || []).map((s) => { const stage = s as StageLike; const mapped: Record = { ...(stage as LooseRecord) }; - // normalize keys if (stage.type) { mapped.Type = stage.type; delete (mapped as LooseRecord).type; @@ -119,7 +104,7 @@ export async function buildFullClientExport(studyId: string) { mapped.StageLabel = stage.stageLabel; delete (mapped as LooseRecord).stageLabel; } - // TODO: stage precursor doesnt exist right now so placeholder is gonna be none, but should we add? + // Placeholder for stage precursor if (!mapped["Stage Precursor"]) { if ((stage as LooseRecord).stagePrecursor) { mapped["Stage Precursor"] = (stage as LooseRecord).stagePrecursor; @@ -130,7 +115,7 @@ export async function buildFullClientExport(studyId: string) { return mapped; }); - //TODO: our batteries right now also don't have a type field, temp making custom + // Placeholder for type field return { Type: "Custom", Name: (b as LooseRecord).name ?? base.name ?? "", @@ -140,7 +125,6 @@ export async function buildFullClientExport(studyId: string) { }; }); - // Conditions: one entry per variant const Conditions = ((study.variants || []) as unknown as VariantLike[]).map( (v) => ({ ProtocolKey: v.serverCode || "", @@ -148,7 +132,6 @@ export async function buildFullClientExport(studyId: string) { }) ); - // Protocols: build a ClientProtocolExport-like structure const Sessions: unknown[] = []; const SessionElements: unknown[] = []; const Protocols: Record = {}; @@ -167,10 +150,8 @@ export async function buildFullClientExport(studyId: string) { const elId = nextElementId++; elementIds.push(elId); - // If element is a battery element const task = (elem as TaskInstanceLike).task as TaskRefLike | undefined; - // Use the customized battery name (if present) or fallback to referenced battery name const batteryName = task?.name ?? task?.battery?.name ?? String(task?._id ?? task); @@ -185,7 +166,7 @@ export async function buildFullClientExport(studyId: string) { protocolSessions.push(sessionId); } - //TODO: keeping protocolkey as variant server id for now + // keeping protocolkey as variant server id const protocolKey = variant.serverCode ?? String(variant._id ?? ""); Protocols[protocolKey] = { Name: variant.name || "", @@ -193,9 +174,8 @@ export async function buildFullClientExport(studyId: string) { }; } - //TODO: where to get version? keeping 1 for now const ProtocolExport = { - Version: 1, + Version: 1, // placeholder since we do not have versioning Protocols, Sessions, SessionElements, @@ -209,8 +189,6 @@ export async function buildFullClientExport(studyId: string) { }; } -// Export study files to disk. This is a small helper used by maintenance scripts -// to write the raw study JSON and the client export. import fs from "fs/promises"; import path from "path"; @@ -227,14 +205,12 @@ export async function exportStudyToDisk(studyId: string, outDir: string) { const studyDir = path.join(outDir, studyDirName); await fs.mkdir(studyDir, { recursive: true }); - // write raw study await fs.writeFile( path.join(studyDir, "study.json"), JSON.stringify(study, null, 2), "utf-8" ); - // build and write client export const clientExport = await buildFullClientExport(studyId); await fs.writeFile( path.join(studyDir, "clientExport.json"), From 8c889e91dd76c18362be5fc606be11c948b49780 Mon Sep 17 00:00:00 2001 From: Ananya Rath Date: Thu, 18 Dec 2025 02:18:41 -0500 Subject: [PATCH 5/7] refactor --- packages/api/src/services/study.service.ts | 11 +---------- packages/ui/src/api/studies.ts | 1 - 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/api/src/services/study.service.ts b/packages/api/src/services/study.service.ts index e8a63e8d..e64b26c6 100644 --- a/packages/api/src/services/study.service.ts +++ b/packages/api/src/services/study.service.ts @@ -439,16 +439,7 @@ export const exportStudy = async ( studyId: string ): APIResponse => { const study = await Study.findOne({ _id: studyId, owner: user._id }); - if (!study) throw new HttpError(404, "Study not found!"); - - /* - const studies = extractStudies(study); - const { conditions, protocol_key, session_ids } = extractVariantInfo(studies) - const batteries = extractBatteries(conditions) - const stages = extractStages(batteries) - return [201, combine(studies, conditions, batteries, stages)]; - */ - + if (!study) throw new HttpError(404, "Study not found."); try { const exportObj = await buildFullClientExport(studyId); return [200, exportObj]; diff --git a/packages/ui/src/api/studies.ts b/packages/ui/src/api/studies.ts index ecffc618..e46697cd 100644 --- a/packages/ui/src/api/studies.ts +++ b/packages/ui/src/api/studies.ts @@ -63,7 +63,6 @@ async function duplicateStudy(id: string) { return response.data; } -// Download export file for a study (calls /export/studies/:id) async function downloadStudyExport(id: string) { const resp = await axiosInstance.get(`/studies/${id}/export`, { responseType: "blob", From 30c088503b69a483999b51b38d30eb734c97224a Mon Sep 17 00:00:00 2001 From: Ananya Rath Date: Thu, 18 Dec 2025 02:26:19 -0500 Subject: [PATCH 6/7] lint errors --- .../ui/src/pages/StudyBuilderPage/components/StudyPanel.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/src/pages/StudyBuilderPage/components/StudyPanel.vue b/packages/ui/src/pages/StudyBuilderPage/components/StudyPanel.vue index a888fa3c..5f5d1e26 100644 --- a/packages/ui/src/pages/StudyBuilderPage/components/StudyPanel.vue +++ b/packages/ui/src/pages/StudyBuilderPage/components/StudyPanel.vue @@ -5,14 +5,12 @@ import AppButton from "@/components/ui/AppButton.vue"; import SessionCard from "./SessionCard.vue"; import Draggable from "vuedraggable"; import StudyServerCode from "./StudyServerCode.vue"; -import { ref, watch } from "vue"; +import { ref } from "vue"; import { ArrowRight, ArrowLeft, Plus, Delete } from "@element-plus/icons-vue"; -import { useRoute } from "vue-router"; import authAPI from "@/api/auth"; import { useQueryClient, useQuery } from "@tanstack/vue-query"; const studyBuilderStore = useStudyBuilderStore(); -const route = useRoute(); const currentVariantIndex = ref(0); const queryClient = useQueryClient(); const { data: currentUser } = useQuery({ From d9c49aa44131531a4416cf4a318e0821a7b1dc03 Mon Sep 17 00:00:00 2001 From: Ananya Rath Date: Thu, 18 Dec 2025 13:24:36 -0500 Subject: [PATCH 7/7] merge conflict changes --- .../components/StudyDetailsSideBar.vue | 16 ++++---- .../components/StudyPanel.vue | 40 ++++++++++++++++++- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/pages/MyStudiesPage/components/StudyDetailsSideBar.vue b/packages/ui/src/pages/MyStudiesPage/components/StudyDetailsSideBar.vue index a939d84d..25a1920c 100644 --- a/packages/ui/src/pages/MyStudiesPage/components/StudyDetailsSideBar.vue +++ b/packages/ui/src/pages/MyStudiesPage/components/StudyDetailsSideBar.vue @@ -126,18 +126,16 @@ onUnmounted(() => { class="fixed right-0 top-0 h-full w-[370px] bg-white shadow-2xl flex flex-col" @click.stop > -
-
+
+

{{ study?.name || "Study Name" }}

- - Edit Study - +

+ {{ study?.description || "" }} +

[studyBuilderStore.variants, studyBuilderStore.currentVariantId], + () => { + if (!studyBuilderStore.currentVariantId) return; + const idx = studyBuilderStore.variants.findIndex( + (v) => v._id === studyBuilderStore.currentVariantId + ); + if (idx !== -1 && idx !== currentVariantIndex.value) { + currentVariantIndex.value = idx; + } + }, + { immediate: true, deep: true } +); + +// Align with the desired variantId from the route (if any) +watch( + () => [studyBuilderStore.variants.length, route.query.variantId], + () => { + const queriedVariant = Array.isArray(route.query.variantId) + ? route.query.variantId[0] + : typeof route.query.variantId === "string" + ? route.query.variantId + : undefined; + if (!queriedVariant) return; + const exists = studyBuilderStore.variants.some( + (v) => v._id === queriedVariant + ); + if (!exists) return; + if (studyBuilderStore.currentVariantId !== queriedVariant) { + studyBuilderStore.switchVariant(queriedVariant); + } + }, + { immediate: true } +); + const downloadExport = async () => { const id = studyBuilderStore.studyId; if (!id) return;