From deeef6cb15f1e47d6259cd6508db3f7debb95102 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 13:05:21 +0200 Subject: [PATCH 01/19] more files --- .../add/experimental/morphology/page.tsx | 20 + .../explore/interactive/add/layout.tsx | 7 + .../interactive-navigation-menu.tsx | 44 +- .../ContributeLayout/index.tsx | 47 + .../contribute/_components/atoms/index.ts | 170 +++ .../circuit-details/circuit-details.tsx | 22 + .../_components/circuit-details/index.ts | 1 + .../circuit-name/circuit-name.module.css | 50 + .../circuit-preview.module.css | 14 + .../circuit-preview/circuit-preview.tsx | 25 + .../_components/circuit-preview/index.ts | 1 + .../contribute/_components/components.tsx | 964 ++++++++++++++++++ .../_components/file-viewer/index.tsx | 81 ++ .../contribute/_components/hooks/circuit.ts | 88 ++ .../_components/hooks/config-atom.ts | 30 + .../contribute/_components/hooks/schema.ts | 157 +++ .../contribute/_components/section.tsx | 221 ++++ .../_components/simulation-files/index.tsx | 198 ++++ .../_components/simulation-status/index.tsx | 50 + .../_components/tabs-selector/index.ts | 1 + .../tabs-selector/tabs-selector.tsx | 43 + .../contribute/_components/tooltip/index.ts | 1 + .../_components/tooltip/tooltip.module.css | 39 + .../_components/tooltip/tooltip.tsx | 20 + src/features/contribute/_components/utils.ts | 73 ++ src/features/contribute/error-registry.ts | 7 + src/features/contribute/index.tsx | 616 +++++++++++ .../contribute/small-microcircuit.module.css | 32 + src/features/contribute/types.ts | 35 + 29 files changed, 3047 insertions(+), 10 deletions(-) create mode 100755 src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/experimental/morphology/page.tsx create mode 100755 src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/layout.tsx create mode 100755 src/components/explore-section/ContributeLayout/index.tsx create mode 100644 src/features/contribute/_components/atoms/index.ts create mode 100644 src/features/contribute/_components/circuit-details/circuit-details.tsx create mode 100644 src/features/contribute/_components/circuit-details/index.ts create mode 100644 src/features/contribute/_components/circuit-name/circuit-name.module.css create mode 100644 src/features/contribute/_components/circuit-preview/circuit-preview.module.css create mode 100644 src/features/contribute/_components/circuit-preview/circuit-preview.tsx create mode 100644 src/features/contribute/_components/circuit-preview/index.ts create mode 100644 src/features/contribute/_components/components.tsx create mode 100644 src/features/contribute/_components/file-viewer/index.tsx create mode 100644 src/features/contribute/_components/hooks/circuit.ts create mode 100644 src/features/contribute/_components/hooks/config-atom.ts create mode 100644 src/features/contribute/_components/hooks/schema.ts create mode 100644 src/features/contribute/_components/section.tsx create mode 100644 src/features/contribute/_components/simulation-files/index.tsx create mode 100644 src/features/contribute/_components/simulation-status/index.tsx create mode 100644 src/features/contribute/_components/tabs-selector/index.ts create mode 100644 src/features/contribute/_components/tabs-selector/tabs-selector.tsx create mode 100644 src/features/contribute/_components/tooltip/index.ts create mode 100644 src/features/contribute/_components/tooltip/tooltip.module.css create mode 100644 src/features/contribute/_components/tooltip/tooltip.tsx create mode 100644 src/features/contribute/_components/utils.ts create mode 100755 src/features/contribute/error-registry.ts create mode 100755 src/features/contribute/index.tsx create mode 100755 src/features/contribute/small-microcircuit.module.css create mode 100755 src/features/contribute/types.ts diff --git a/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/experimental/morphology/page.tsx b/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/experimental/morphology/page.tsx new file mode 100755 index 000000000..318ca8323 --- /dev/null +++ b/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/experimental/morphology/page.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { useParams } from 'next/navigation'; + +import ContributeConfig from '@/features/contribute'; +import { WorkspaceContext } from '@/types/common'; + +type Params = WorkspaceContext & { circuit_id: string }; + +export default function ContributeConfiguration() { + const { virtualLabId, projectId } = useParams(); + console.log('ContributeConfiguration function entered'); + return ( + + ); +} diff --git a/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/layout.tsx b/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/layout.tsx new file mode 100755 index 000000000..8e7fe075a --- /dev/null +++ b/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/layout.tsx @@ -0,0 +1,7 @@ +import { ReactNode } from 'react'; +import ContributeLayout from '@/components/explore-section/ContributeLayout'; + +export default function ContributeDataLayout({ children }: { children: ReactNode }) { + console.log('ContributeDataLayout function entered'); + return {children}; +} diff --git a/src/components/entities-type-stats/interactive-navigation-menu.tsx b/src/components/entities-type-stats/interactive-navigation-menu.tsx index 9346c16fc..f9c919b83 100644 --- a/src/components/entities-type-stats/interactive-navigation-menu.tsx +++ b/src/components/entities-type-stats/interactive-navigation-menu.tsx @@ -60,8 +60,10 @@ function EntityTypeStats(props: StatsPanelProps) { return match(selectedTab) .with('experimental-data', () => ( <> - {Object.entries(ExperimentalEntitiesTileTypes).map(([key, value]) => { + {Object.entries(ExperimentalEntitiesTileTypes).map(([key, value], index) => { const href = `${pathName}/${value?.explore.basePrefix}/${value.slug}`; + + const baseHref = `${pathName}/${value?.explore.basePrefix}/${value.slug}`; let records = ''; let isError = false; @@ -74,16 +76,38 @@ function EntityTypeStats(props: StatsPanelProps) { isError = !!error || typeof tmpResult === 'string'; } + // Find the last occurrence of '/interactive/' and insert '/add/' after it + const lastInteractiveIndex = pathName.lastIndexOf('/interactive'); + let addHref = baseHref; + if (lastInteractiveIndex !== -1) { + const pathBeforeInteractive = pathName.substring( + 0, + lastInteractiveIndex + '/interactive'.length + ); + const pathAfterInteractive = baseHref.substring(baseHref.indexOf('/experimental')); // Assuming /experimental is always after /interactive + addHref = `${pathBeforeInteractive}/add${pathAfterInteractive}`; + } + return ( - +
+ + {index === 0 && ( + + add + + )} +
); })} diff --git a/src/components/explore-section/ContributeLayout/index.tsx b/src/components/explore-section/ContributeLayout/index.tsx new file mode 100755 index 000000000..5a175d01d --- /dev/null +++ b/src/components/explore-section/ContributeLayout/index.tsx @@ -0,0 +1,47 @@ +// File: app/virtual-lab/explore/interactive/add/layout.tsx (or wherever your 'add' route lives) + +'use client'; + +import { ReactNode } from 'react'; +import { usePathname } from 'next/navigation'; +import { ErrorBoundary } from 'react-error-boundary'; +import SimpleErrorComponent from '@/components/GenericErrorFallback'; +import BackToInteractiveExplorationBtn from '@/components/explore-section/BackToInteractiveExplorationBtn'; + +export default function ContributeLayout({ children }: { children: ReactNode }) { + const pathname = usePathname(); + console.log('ContributeLayout function entered'); + // Determine the href for the back button. + // Goal: If current is /virtual-lab/explore/interactive/add/experimental/morphology + // It should go back to /virtual-lab/explore/interactive + const splittedPathname = pathname.split('/'); + let interactivePageHref = '/'; // Default fallback + + const addIndex = splittedPathname.indexOf('add'); + + if (addIndex > -1) { + // If 'add' is found, take all segments up to (but not including) 'add' + interactivePageHref = splittedPathname.slice(0, addIndex).join('/'); + } else { + // Fallback: If 'add' is not in the path (unexpected for this layout), + // go back one level from the current path. + interactivePageHref = splittedPathname.slice(0, splittedPathname.length - 1).join('/'); + } + + // Ensure the path starts with a '/' if it's not empty (e.g., if it's just 'virtual-lab/explore/interactive') + if (interactivePageHref === '') { + interactivePageHref = '/'; + } else if (!interactivePageHref.startsWith('/')) { + interactivePageHref = '/' + interactivePageHref; + } + + return ( +
+ + + +
{children}
+
+
+ ); +} diff --git a/src/features/contribute/_components/atoms/index.ts b/src/features/contribute/_components/atoms/index.ts new file mode 100644 index 000000000..25ceed256 --- /dev/null +++ b/src/features/contribute/_components/atoms/index.ts @@ -0,0 +1,170 @@ +import { atom } from 'jotai'; +import { atomWithRefresh } from 'jotai/utils'; +import isEqual from 'lodash/isEqual'; + +import { downloadAsset } from '@/api/entitycore/queries/assets'; +import { getCircuit } from '@/api/entitycore/queries/model/circuit'; +import { getCircuitSimulations } from '@/api/entitycore/queries/simulation/circuit-simulation'; +import { getCircuitSimulationExecutions } from '@/api/entitycore/queries/simulation/circuit-simulation-execution'; +import { getCircuitSimulationResult } from '@/api/entitycore/queries/simulation/circuit-simulation-result'; +import { EntityTypeValue } from '@/api/entitycore/types'; +import { ICircuit } from '@/api/entitycore/types/entities/circuit'; +import { ICircuitSimulation } from '@/api/entitycore/types/entities/circuit-simulation'; +import { ICircuitSimulationExecution } from '@/api/entitycore/types/entities/circuit-simulation-execution'; +import { ICircuitSimulationResult } from '@/api/entitycore/types/entities/circuit-simulation-result'; +import { getLatestSimExecStatus } from '@/features/small-microcircuit/_components/utils'; +import { SimExecStatusMap } from '@/features/small-microcircuit/types'; +import { WorkspaceContext } from '@/types/common'; +import { atomFamilyWithExpiration, readAtomFamilyWithExpiration } from '@/util/atoms'; + +const simExecBySimIdAtomFamily = readAtomFamilyWithExpiration( + ({ simulationId, context }: { simulationId: string; context: WorkspaceContext }) => + atom>(async () => { + const simulationExecutionFilters = { used__id: simulationId }; + const res = await getCircuitSimulationExecutions({ + filters: simulationExecutionFilters, + context, + }); + + return res.data[0]; + }), + { + ttl: 120000, // 2 minutes + areEqual: isEqual, + } +); + +export const simExecRemoteStatusMapAtomFamily = atomFamilyWithExpiration( + ({ simulationIds, context }: { simulationIds: string[]; context: WorkspaceContext }) => + atomWithRefresh>(async () => { + const simulationExecutionFilters = { used__id__in: simulationIds.join(',') }; + const res = await getCircuitSimulationExecutions({ + filters: simulationExecutionFilters, + context, + }); + + return res.data.reduce( + (map, simExec) => map.set(simExec.used[0].id, simExec.status), + new Map() + ); + }), + { + ttl: 120000, // 2 minutes + areEqual: isEqual, + } +); + +type SimExecStatusMapAtomFamilyArg = { simulationIds: string[]; context: WorkspaceContext }; + +export const simExecStatusMapAtomFamily = atomFamilyWithExpiration( + ({ simulationIds, context }: SimExecStatusMapAtomFamilyArg) => { + const simExecRemoteStatusMapAtom = simExecRemoteStatusMapAtomFamily({ + simulationIds, + context, + }); + + const localStatusMapAtom = atom(new Map()); + + return atom, [SimExecStatusMap], void>( + async (get) => { + const remoteStatusMap = await get(simExecRemoteStatusMapAtom); + const localStatusMap = get(localStatusMapAtom); + + const simIds = Array.from(new Set([...remoteStatusMap.keys(), ...localStatusMap.keys()])); + const statusMap = simIds.reduce((map, simId) => { + const remoteStatus = remoteStatusMap.get(simId); + const localStatus = localStatusMap.get(simId); + // If both are set we take the latest possible one, + // because the status change in a particular sequence. + // See definition of getLatestSimExecStatus + const status = + remoteStatus && localStatus + ? getLatestSimExecStatus(remoteStatus, localStatus) + : (localStatus ?? remoteStatus); + return map.set(simId, status); + }, new Map()); + + return statusMap; + }, + (get, set, newStatusMap) => set(localStatusMapAtom, new Map(newStatusMap)) + ); + }, + { + ttl: 120000, // 2 minutes + areEqual: isEqual, + } +); + +export const simResultBySimIdAtomFamily = readAtomFamilyWithExpiration( + ({ simulationId, context }: { simulationId: string; context: WorkspaceContext }) => + atom>(async (get) => { + const execution = await get(simExecBySimIdAtomFamily({ simulationId, context })); + + if (!execution?.generated?.[0]) { + throw new Error('Simulation Result not found'); + } + + return getCircuitSimulationResult({ id: execution.generated[0].id, context }); + }), + { + ttl: 120000, // 2 minutes + areEqual: isEqual, + } +); + +export const simulationsByCampaignIdAtomFamily = readAtomFamilyWithExpiration( + ({ campaignId, context }: { campaignId: string; context: WorkspaceContext }) => + atom>(async () => { + const filters = { simulation_campaign_id: campaignId }; + const res = await getCircuitSimulations({ filters, context }); + + return res.data; + }), + { + ttl: 120000, // 2 minutes + areEqual: isEqual, + } +); + +export const circuitAtomFamily = readAtomFamilyWithExpiration( + ({ circuitId, context }: { circuitId: string; context: WorkspaceContext }) => + atom>(async () => { + return getCircuit({ id: circuitId, context }); + }), + { + ttl: 120000, // 2 minutes + areEqual: isEqual, + } +); + +export const fileAtomFamily = readAtomFamilyWithExpiration( + ({ + id, + entityId, + entityType, + assetPath, + context, + }: { + id: string; + entityId: string; + entityType: EntityTypeValue; + assetPath?: string; + context: WorkspaceContext; + }) => + atom>(async () => { + const res = await downloadAsset({ + ctx: context, + entityId, + id, + entityType, + assetPath, + asRawResponse: true, + }); + + return res.json(); + }), + { + ttl: 120000, // 2 minutes + areEqual: isEqual, + } +); diff --git a/src/features/contribute/_components/circuit-details/circuit-details.tsx b/src/features/contribute/_components/circuit-details/circuit-details.tsx new file mode 100644 index 000000000..f8f856fd3 --- /dev/null +++ b/src/features/contribute/_components/circuit-details/circuit-details.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Input } from 'antd'; + +import Tooltip from '../tooltip'; + +import { ICircuit } from '@/api/entitycore/types/entities/circuit'; + +interface CircuitDetailsProps { + className?: string; + circuit: ICircuit; +} + +export default function CircuitDetails({ className, circuit }: CircuitDetailsProps) { + return ( +
+ + + + +
+ ); +} diff --git a/src/features/contribute/_components/circuit-details/index.ts b/src/features/contribute/_components/circuit-details/index.ts new file mode 100644 index 000000000..42451142f --- /dev/null +++ b/src/features/contribute/_components/circuit-details/index.ts @@ -0,0 +1 @@ +export { default } from './circuit-details'; diff --git a/src/features/contribute/_components/circuit-name/circuit-name.module.css b/src/features/contribute/_components/circuit-name/circuit-name.module.css new file mode 100644 index 000000000..61b1f9711 --- /dev/null +++ b/src/features/contribute/_components/circuit-name/circuit-name.module.css @@ -0,0 +1,50 @@ +.circuitName { + position: relative; + text-align: end; +} + +.circuitName > .name svg { + margin-left: 1em; + color: #005; +} + +.name { + font-weight: bold; + font-variant: small-caps; +} + +.description { + margin-top: 16px; + white-space: pre-wrap; + position: absolute; + background: #0050b3ee; + color: #fff; + padding: 0.5em 1em; + border-radius: 0.25em; + position: absolute; + left: 0; + right: 0; + width: 100%; + pointer-events: none; + z-index: 9; + text-align: start; + transition: all 0.3s; + opacity: 0; +} + +.circuitName:hover .description { + opacity: 1; +} + +.description::after { + content: ''; + border: 16px solid transparent; + border-bottom: 16px solid #0050b3ee; + position: absolute; + top: -16px; + left: 50%; + width: 0; + height: 0; + margin: -16px; + transform: scale(0.5, 1); +} diff --git a/src/features/contribute/_components/circuit-preview/circuit-preview.module.css b/src/features/contribute/_components/circuit-preview/circuit-preview.module.css new file mode 100644 index 000000000..2e4f7d702 --- /dev/null +++ b/src/features/contribute/_components/circuit-preview/circuit-preview.module.css @@ -0,0 +1,14 @@ +div.circuitPreview { + width: 100%; + height: 100%; + min-height: 0; + overflow: visible; +} + +.circuitPreview > .image { + background: #fff; + border-radius: 1em; + box-shadow: 0 2px 8px #0002; + width: 100%; + height: 100%; +} diff --git a/src/features/contribute/_components/circuit-preview/circuit-preview.tsx b/src/features/contribute/_components/circuit-preview/circuit-preview.tsx new file mode 100644 index 000000000..8862833f2 --- /dev/null +++ b/src/features/contribute/_components/circuit-preview/circuit-preview.tsx @@ -0,0 +1,25 @@ +/* eslint-disable @next/next/no-img-element */ +import React from 'react'; + +import { useCircuitImageURL } from '../hooks/circuit'; +import { classNames } from '@/util/utils'; +import { ICircuit } from '@/api/entitycore/types/entities/circuit'; +import ZoomableImage from '@/components/zoomable-image'; + +import styles from './circuit-preview.module.css'; + +interface CircuitPreviewProps { + className?: string; + circuit: ICircuit | undefined | null; +} + +export default function CircuitPreview({ className, circuit }: CircuitPreviewProps) { + const url = useCircuitImageURL(circuit?.id); + + return ( +
+ {/* Circuit preview */} + +
+ ); +} diff --git a/src/features/contribute/_components/circuit-preview/index.ts b/src/features/contribute/_components/circuit-preview/index.ts new file mode 100644 index 000000000..677278f17 --- /dev/null +++ b/src/features/contribute/_components/circuit-preview/index.ts @@ -0,0 +1 @@ +export { default } from './circuit-preview'; diff --git a/src/features/contribute/_components/components.tsx b/src/features/contribute/_components/components.tsx new file mode 100644 index 000000000..ee93f0735 --- /dev/null +++ b/src/features/contribute/_components/components.tsx @@ -0,0 +1,964 @@ +// components.tsx +import { useEffect, useState, useRef } from 'react'; +import { atom, useAtom } from 'jotai'; +import { InputNumber, Input, Select, Button } from 'antd'; +import { CheckCircleOutlined, CloseCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; + +import { JSONSchema } from '../types'; +import { isPlainObject } from './utils'; +import CircuitDetails from './circuit-details'; +import Tooltip from './tooltip'; + +import { ICircuit } from '@/api/entitycore/types/entities/circuit'; +import { classNames } from '@/util/utils'; + +type Primitive = null | boolean | number | string; +interface Object { + [key: string]: Primitive | Primitive[] | Object; +} + +export type ConfigValue = Primitive | Primitive[] | Object; + +export type Config = Record; + +// Updated structure for MTYPE classification +const MTYPE_CLASSES = [ + { mtype_pref_label: 'L23_NBC', mtype_id: '6605787b-ba14-43fd-a954-de9cff4b15a0' }, + { mtype_pref_label: 'L23_NGC', mtype_id: 'dd16dca0-e567-416b-b8b7-f8fbcaa05af0' }, + { mtype_pref_label: 'L23_PC', mtype_id: '0791edc9-7ad4-4a94-a4a5-feab9b690d7e' }, + { mtype_pref_label: 'L23_PTPC', mtype_id: '52ea242f-6591-425a-8eae-962fa0b4dfe0' }, + { mtype_pref_label: 'L23_SBC', mtype_id: 'fbb8b577-92f4-4c93-b355-0982ef5a3c7c' }, + { mtype_pref_label: 'L23_STPC', mtype_id: '93be8237-9861-4870-9977-ff1cf9e7462c' }, + { mtype_pref_label: 'L2_ChC', mtype_id: '91ba3deb-1139-4bc6-a12f-6a64a0ed0e92' }, + { mtype_pref_label: 'L2_IPC', mtype_id: 'e55f12e1-807c-42f6-ba98-91d6d30c57d7' }, + { mtype_pref_label: 'L2_MC', mtype_id: 'ea51f2c8-95fc-4940-a400-c37a3ff2d9eb' }, + { mtype_pref_label: 'L2_PC', mtype_id: 'dd73956b-423e-42c5-87d9-9e2cc84356b9' }, + { mtype_pref_label: 'L2_TPC', mtype_id: '7abf03d5-30b0-41ae-a02b-1f4e26c243a8' }, + { mtype_pref_label: 'L2_TPC:A', mtype_id: '9b04acb1-4737-4088-8d22-0658414bdda1' }, + { mtype_pref_label: 'L2_TPC:B', mtype_id: '4b6862b9-c438-4dfc-a2e6-1ad4d7a00eda' }, + { mtype_pref_label: 'L3_MC', mtype_id: '52578494-b41c-499b-9717-5d11f4b2f068' }, + { mtype_pref_label: 'L3_PC', mtype_id: '87fec7dd-7a2f-400a-aee0-94d1946cf1ab' }, + { mtype_pref_label: 'L3_TPC', mtype_id: '229c31f1-a6ec-4d8c-85d3-d8175ffde109' }, + { mtype_pref_label: 'L3_TPC:A', mtype_id: 'dd346e90-7bca-4976-bf9a-303b6a94b339' }, + { mtype_pref_label: 'L3_TPC:B', mtype_id: 'a71d226c-2c56-40ee-a4be-9726fc430932' }, + { mtype_pref_label: 'L3_TPC:C', mtype_id: 'd9b7bd4d-cec9-4fec-a448-79320de89f2a' }, + { mtype_pref_label: 'L4_BP', mtype_id: 'a55f6ce7-068a-4c5e-a883-de5f4304612e' }, + { mtype_pref_label: 'L4_BTC', mtype_id: '8315d249-6678-4d55-b581-3b6f9eb48e86' }, + { mtype_pref_label: 'L4_ChC', mtype_id: '0e4f3036-0d14-4fd9-b7a8-87f5e90a9fa6' }, + { mtype_pref_label: 'L4_DBC', mtype_id: '8de61c06-31e8-4483-bd98-608bc874b369' }, + { mtype_pref_label: 'L4_LBC', mtype_id: 'bb875a91-4ae5-4f6f-b050-5ad952e9cd6c' }, + { mtype_pref_label: 'L4_MC', mtype_id: '0bdf029e-a55a-444c-a7c9-9d0ff51239a5' }, + { mtype_pref_label: 'L4_NBC', mtype_id: '72673af9-2f2b-4a9a-95dc-552777ab63b9' }, + { mtype_pref_label: 'L4_NGC', mtype_id: '41f41550-5e0e-4de7-b52d-62110c338a27' }, + { mtype_pref_label: 'L4_PC', mtype_id: 'ad5769c6-7e86-4433-8f34-9efcb4f0d182' }, + { mtype_pref_label: 'L4_SBC', mtype_id: '43a7d86b-71c5-4a10-be62-ef6cf95ca694' }, + { mtype_pref_label: 'L4_SSC', mtype_id: '400a55f7-e162-4fd1-80a0-4f2facea7cec' }, + { mtype_pref_label: 'L4_TPC', mtype_id: '02e13718-5227-4c28-b838-04dd0c2c67f2' }, + { mtype_pref_label: 'L4_UPC', mtype_id: '2ef7e0b5-39e4-441b-a72a-c7186afa7f5c' }, + { mtype_pref_label: 'L56_PC', mtype_id: '629d6d6f-93f9-43d8-8a99-277740fd8f22' }, + { mtype_pref_label: 'L5_BP', mtype_id: '7b16c860-ae76-4ddf-b093-4e28620b3712' }, +]; + +// Helper function to check if a field value is considered "empty" or invalid +const isEmptyValue = (value: ConfigValue): boolean => { + if (value === null || value === undefined || value === '') return true; + if (Array.isArray(value) && value.length === 0) return true; + if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) return true; + return false; +}; + +// Helper function to validate required fields +const getRequiredFieldErrors = ( + state: Record, + schema: JSONSchema +): string[] => { + const errors: string[] = []; + const requiredFields = schema.required || []; + + requiredFields.forEach((fieldName) => { + const value = state[fieldName]; + if (isEmptyValue(value)) { + const fieldSchema = schema.properties?.[fieldName]; + const fieldTitle = fieldSchema?.title || fieldName; + errors.push(`${fieldTitle} is required`); + } + }); + + return errors; +}; + +export function JSONSchemaForm({ + disabled, + schema, + stateAtom, + config, + circuit, + onAddReferenceClick, + nodeId, + currentCategory, + onValidationChange, // New prop to communicate validation state to parent +}: { + disabled: boolean; + config: Config; + schema: JSONSchema; + circuit: ICircuit | undefined | null; + stateAtom: ReturnType>; + onAddReferenceClick: (reference: string) => void; + nodeId?: string; + currentCategory?: string; + onValidationChange?: (isValid: boolean, errors: string[]) => void; +}) { + const skip = ['type']; + + const [state, setState] = useAtom(stateAtom); + const [addingElement, setAddingElement] = useState<{ [key: string]: boolean }>({ + legacy_id: true, + }); + const [newElement, setNewElement] = useState<{ [key: string]: number | string | null }>({}); + const [validationErrors, setValidationErrors] = useState([]); + const [touchedFields, setTouchedFields] = useState>(new Set()); + + const referenceTypesToConfigKeys: Record = { + NeuronSetReference: 'neuron_sets', + TimestampsReference: 'timestamps', + }; + + const referenceTypesToTitles: Record = { + NeuronSetReference: 'Neuron Set', + TimestampsReference: 'Timestamps', + }; + + // Callback ref to avoid dependency issues + const onValidationChangeRef = useRef(onValidationChange); + useEffect(() => { + onValidationChangeRef.current = onValidationChange; + }); + + // Validate form whenever state changes + useEffect(() => { + const errors = getRequiredFieldErrors(state, schema); + + // Only update if errors actually changed + setValidationErrors((prevErrors) => { + const errorsChanged = + prevErrors.length !== errors.length || + prevErrors.some((error, index) => error !== errors[index]); + + if (errorsChanged && onValidationChangeRef.current) { + onValidationChangeRef.current(errors.length === 0, errors); + } + + return errorsChanged ? errors : prevErrors; + }); + }, [state, schema]); + + useEffect(() => { + if (!schema.properties) return; + + const initial: Record = {}; + + Object.entries(schema.properties).forEach(([key, value]) => { + if (key === 'type') initial[key] = value.const ?? null; + else if (key === 'legacy_id') initial[key] = []; + else if (key === 'strain_id') initial[key] = null; + else if (key === 'license_id') + initial[key] = 'c268a20e-b78a-4332-a5e1-38e26c4454b9'; // Default to undefined UUID + else initial[key] = value.default ?? null; + }); + + // Auto-populate brain region id if we're in morphology category + if (currentCategory === 'morphology' && nodeId) { + const brainRegionIdKey = Object.keys(schema.properties || {}).find((key) => { + const normalizedKey = key.toLowerCase().replace(/[\s_]/g, ''); + return ( + normalizedKey === 'brainregionid' || + normalizedKey === 'brain_region_id' || + normalizedKey === 'brainregion' + ); + }); + + if (brainRegionIdKey) { + initial[brainRegionIdKey] = nodeId; + } + } + + // Auto-populate species id (hardcoded mouse species ID) + const speciesIdKey = Object.keys(schema.properties || {}).find((key) => { + const normalizedKey = key.toLowerCase().replace(/[\s_]/g, ''); + return ( + normalizedKey === 'speciesid' || + normalizedKey === 'species_id' || + normalizedKey === 'species' + ); + }); + + if (speciesIdKey) { + initial[speciesIdKey] = 'b7ad4cca-4ac2-4095-9781-37fb68fe9ca1'; + } + + // Auto-populate atlas id + const atlasIdKey = Object.keys(schema.properties || {}).find((key) => { + const normalizedKey = key.toLowerCase().replace(/[\s_]/g, ''); + return ( + normalizedKey === 'atlasid' || normalizedKey === 'atlas_id' || normalizedKey === 'atlas' + ); + }); + + if (atlasIdKey) { + initial[atlasIdKey] = 'e3e70682-c209-4cac-a29f-6fbed82c07cd'; + } + + console.log('Initial state for morphology:', initial); + setState((prev) => { + const newState = { ...initial, ...prev }; + console.log('Updated state for morphology:', newState); + return newState; + }); + }, [stateAtom, setState, schema.properties, nodeId, currentCategory]); + + // Helper function to mark field as touched + const markFieldTouched = (fieldName: string) => { + setTouchedFields((prev) => new Set(prev).add(fieldName)); + }; + + // Helper function to check if a field has an error + const hasFieldError = (fieldName: string): boolean => { + const isRequired = schema.required?.includes(fieldName); + const isTouched = touchedFields.has(fieldName); + const isEmpty = isEmptyValue(state[fieldName]); + return isRequired && isTouched && isEmpty; + }; + + // Helper function to get field error message + const getFieldErrorMessage = (fieldName: string): string | null => { + if (!hasFieldError(fieldName)) return null; + const fieldSchema = schema.properties?.[fieldName]; + const fieldTitle = fieldSchema?.title || fieldName; + return `${fieldTitle} is required`; + }; + + function renderInput(k: string, v: JSONSchema) { + const obj = { + ...v, + ...v.anyOf?.find((subv) => subv.type !== 'array' && subv.type !== 'null'), + }; + const normalizedKey = k.toLowerCase().replace(/[\s_]/g, ''); + const isRequired = schema.required?.includes(k); + const fieldError = getFieldErrorMessage(k); + const hasError = hasFieldError(k); + + const isBrainRegionIdField = + currentCategory === 'morphology' && + (normalizedKey === 'brainregionid' || + normalizedKey === 'brain_region_id' || + normalizedKey === 'brainregion'); + const isSpeciesIdField = + normalizedKey === 'speciesid' || + normalizedKey === 'species_id' || + normalizedKey === 'species'; + const isAtlasIdField = + normalizedKey === 'atlasid' || normalizedKey === 'atlas_id' || normalizedKey === 'atlas'; + const isExperimentDateField = + normalizedKey === 'experimentdate' || + normalizedKey === 'experiment_date' || + normalizedKey === 'date' || + v.title?.toLowerCase().includes('date'); + const isStrainIdField = + normalizedKey === 'strainid' || normalizedKey === 'strain_id' || normalizedKey === 'strain'; + const isAgePeriodField = normalizedKey === 'ageperiod' || normalizedKey === 'age_period'; + const isLegacyIdField = normalizedKey === 'legacyid' || normalizedKey === 'legacy_id'; + const isLicenseIdField = normalizedKey === 'licenseid' || normalizedKey === 'license_id'; + const isMtypeClassIdField = normalizedKey === 'mtypeclassid'; + + // Common input props for consistency + const commonInputProps = { + disabled, + className: `w-full ${hasError ? 'border-red-500' : ''}`, + onBlur: () => markFieldTouched(k), + }; + + if (isBrainRegionIdField && nodeId) { + return ( +
+ +
+ ); + } + + if (isSpeciesIdField) { + return ( +
+ +
+ ); + } + + if (isAtlasIdField) { + return ( +
+ +
+ ); + } + + if (isExperimentDateField) { + const formatDate = (value: string) => { + const cleaned = value.replace(/[^\d\s-]/g, ''); + const parts = cleaned.split(/[\s-]+/).filter((part) => part.length > 0); + if (parts.length === 0) return ''; + if (parts.length === 1) return parts[0]; + if (parts.length === 2) return `${parts[0]} ${parts[1]}`; + return parts.slice(0, 3).join(' '); + }; + + const validateDateFormat = (value: string) => { + if (!value) return true; + const parts = value.split(' '); + if (parts.length !== 3) return false; + const [day, month, year] = parts; + const dayNum = parseInt(day); + const monthNum = parseInt(month); + const yearNum = parseInt(year); + return ( + dayNum >= 1 && + dayNum <= 31 && + monthNum >= 1 && + monthNum <= 12 && + yearNum >= 1900 && + yearNum <= new Date().getFullYear() + ); + }; + + const currentValue = typeof state[k] === 'string' ? state[k] : ''; + const isValid = validateDateFormat(currentValue); + const showDateError = !isValid && currentValue; + + return ( +
+ { + const formatted = formatDate(e.currentTarget.value); + setState({ ...state, [k]: formatted }); + }} + placeholder="DD MM YYYY (e.g., 15 03 2024)" + /> + {showDateError && ( +
+ Please use format: DD MM YYYY (day month year) +
+ )} + {fieldError &&
{fieldError}
} +
+ ); + } + + if (isStrainIdField) { + const allSpeciesStrains = [ + { + species_id: 'b7ad4cca-4ac2-4095-9781-37fb68fe9ca1', + species_name: 'Mouse', + strains: { + 'C57BL/6': '123e4567-e89b-12d3-a456-426614174000', + 'BALB/c': '456e7890-e29b-41d4-a716-446655440001', + }, + }, + { + species_id: '789e0123-e29b-41d4-a716-446655440002', + species_name: 'Rat', + strains: { + 'Sprague Dawley': '890e1234-e29b-41d4-a716-446655440003', + }, + }, + ]; + + const speciesId = 'b7ad4cca-4ac2-4095-9781-37fb68fe9ca1'; + const selectedSpecies = allSpeciesStrains.find((s) => s.species_id === speciesId); + + if (!selectedSpecies) { + return ( +
+ + {fieldError &&
{fieldError}
} +
+ ); + } + + const strainOptions = Object.entries(selectedSpecies.strains).map(([name, id]) => ({ + label: name, + value: id, + })); + + return ( +
+ { + setState({ ...state, [k]: newV }); + markFieldTouched(k); + }} + value={state[k]} + options={obj.enum.map((subv: string) => ({ + label: subv, + value: subv, + }))} + placeholder="Select age period" + /> + {fieldError &&
{fieldError}
} +
+ ); + } + console.warn('No enum found for age_period:', obj); + return ( +
+ { + setState({ ...state, [k]: e.currentTarget.value }); + }} + placeholder="Enter age period" + /> + {fieldError &&
{fieldError}
} +
+ ); + } + + if (isLegacyIdField) { + return ( +
+
+
+ {Array.isArray(state[k]) && + state[k].map((e, i) => ( +
+ {e}{' '} + {!disabled && ( + { + const newElements = [...state[k]]; + newElements.splice(i, 1); + setState({ ...state, [k]: newElements }); + markFieldTouched(k); + }} + /> + )} +
+ ))} +
+ {!addingElement[k] && !disabled && ( + setAddingElement({ ...addingElement, [k]: true })} + className="text-primary-8" + /> + )} + {addingElement[k] && !disabled && ( +
+ setNewElement({ ...newElement, [k]: e.currentTarget.value })} + placeholder="Enter legacy ID" + /> + {newElement[k] && ( + { + setState({ + ...state, + [k]: [...(Array.isArray(state[k]) ? state[k] : []), newElement[k]], + }); + setAddingElement({ ...addingElement, [k]: false }); + setNewElement({ ...newElement, [k]: null }); + markFieldTouched(k); + }} + /> + )} + { + setAddingElement({ ...addingElement, [k]: false }); + setNewElement({ ...newElement, [k]: null }); + }} + className="text-primary-8" + /> +
+ )} +
+ {fieldError &&
{fieldError}
} +
+ ); + } + + // New condition for 'license id' field + if (isLicenseIdField) { + const licenseOptions = { + undefined: 'c268a20e-b78a-4332-a5e1-38e26c4454b9', + 'CC BY-NC-SA 4.0 Deed': '9e766299-b873-4162-9207-30fcd583d933', + 'CC BY-NC 2.0 Deed': 'fafcd04d-a967-4eec-b2e1-3afb2d63a41a', + 'CC BY-NC 4.0 Deed': '1283454d-b5ad-488f-acb7-d00b9f02873d', + 'CC BY 2.0 Deed': '211ac318-504d-44b9-b250-3cae8980d2e9', + 'CC BY 4.0 Deed': 'ad8686db-3cdd-4e3f-bcbd-812380a9eba7', + 'CC0 1.0 Deed': '74b8c953-67f5-4e95-ac58-095274901328', + 'NGV_Data Licence_v1.0': '0c39107e-3b68-4f1f-904a-5c7f1b4f89c5', + }; + + const options = Object.entries(licenseOptions).map(([name, id]) => ({ + label: name, + value: id, + })); + + // Find the key corresponding to the current state value to set the label + const currentLicenseLabel = Object.keys(licenseOptions).find( + (key) => licenseOptions[key] === state[k] + ); + + return ( +
+ { + setState({ ...state, [k]: newV }); + markFieldTouched(k); + }} + value={currentMtypeLabel || state[k]} + options={options} + placeholder="Select an MTYPE CLASS" + /> + {fieldError &&
{fieldError}
} +
+ ); + } + + if (k === 'circuit' && circuit) return ; + + if (v.is_block_reference && v.properties && typeof v.properties.type.const === 'string') { + const referenceKey = referenceTypesToConfigKeys[v.properties.type.const]; + const referenceTitle = referenceTypesToTitles[v.properties.type.const]; + if (!referenceKey) return null; + const referenceConfig = config[referenceKey]; + if (!isPlainObject(referenceConfig)) return null; + + const referees = Object.entries(referenceConfig).filter(([_, val]) => { + return isPlainObject(val); + }); + + if (referees.length === 0) { + return ( +
+ + {fieldError &&
{fieldError}
} +
+ ); + } + + const defaultV = + isPlainObject(state[k]) && typeof state[k].block_name === 'string' + ? state[k].block_name + : null; + + return ( +
+ { + setState({ ...state, [k]: newV }); + markFieldTouched(k); + }} + value={state[k]} + options={obj.enum.map((subv: string) => ({ + label: subv, + value: subv, + }))} + placeholder={`Select ${v.title || k}`} + /> + {fieldError &&
{fieldError}
} +
+ ); + } + + if (obj.type === 'number' || obj.type === 'integer') { + return ( +
+ { + setState({ ...state, [k]: value }); + }} + onBlur={() => markFieldTouched(k)} + className={`w-full ${hasError ? 'border-red-500' : ''}`} + /> + {fieldError &&
{fieldError}
} +
+ ); + } + + if (obj.type === 'string') { + return ( +
+ { + setState({ ...state, [k]: e.currentTarget.value }); + }} + /> + {fieldError &&
{fieldError}
} +
+ ); + } + + console.warn(`Unhandled schema type for key: ${k}`, obj); + return ( +
+ { + setState({ ...state, [k]: e.currentTarget.value }); + }} + placeholder={`Enter value for ${v.title || k}`} + /> + {fieldError &&
{fieldError}
} +
+ ); + } + + console.log('Rendering JSONSchemaForm with config:', config); + console.log('Current state:', state); + console.log('Validation errors:', validationErrors); + + return ( +
+
{schema.title}
+
{schema.description}
+ + {/* Show overall validation errors summary if there are any */} + {validationErrors.length > 0 && ( +
+
+ Please fix the following required fields: +
+
    + {validationErrors.map((error, index) => ( +
  • {error}
  • + ))} +
+
+ )} + +
+ {schema.properties && + Object.entries(schema.properties) + .filter(([k]) => { + return !skip.includes(k); + }) + .map(([k, v]) => { + const isRequired = schema.required?.includes(k); + const normalizedKey = k.toLowerCase().replace(/[\s_]/g, ''); + const isStrainIdField = + normalizedKey === 'strainid' || + normalizedKey === 'strain_id' || + normalizedKey === 'strain'; + const isAgePeriodField = + normalizedKey === 'ageperiod' || normalizedKey === 'age_period'; + const isLicenseIdField = + normalizedKey === 'licenseid' || normalizedKey === 'license_id'; + const isMtypeClassIdField = normalizedKey === 'mtypeclassid'; + + if (currentCategory === 'subject' && normalizedKey === 'ageperiod') { + console.log('Age period field:', { + key: k, + schema: v, + title: v.title, + resolved: { + ...v, + ...v.anyOf?.find((subv) => subv.type !== 'array' && subv.type !== 'null'), + }, + }); + } + + return ( +
+
+
+ {isStrainIdField + ? 'STRAIN' + : isAgePeriodField + ? 'AGE PERIOD' + : isLicenseIdField + ? 'LICENSE' // New condition for license title + : isMtypeClassIdField + ? 'MTYPE CLASS' + : v.title || k} + {isRequired && *} +
+ {v.units &&
{v.units}
} +
+ {renderInput(k, v)} +
+ ); + })} +
+
+ ); +} + +export function Tab({ + tab, + selectedTab, + children, + onClick, + rounded = 'rounded-full', + extraClass, + disabled, +}: { + tab: string; + selectedTab: string; + onClick?: () => void; + rounded?: 'rounded-l-full' | 'rounded-r-full' | 'rounded-full'; + children?: React.ReactNode; + extraClass?: string; + disabled?: boolean; +}) { + return ( + + ); +} + +export function Chevron({ rotate }: { rotate?: number }) { + return ( + + + + ); +} diff --git a/src/features/contribute/_components/file-viewer/index.tsx b/src/features/contribute/_components/file-viewer/index.tsx new file mode 100644 index 000000000..e8d736a3a --- /dev/null +++ b/src/features/contribute/_components/file-viewer/index.tsx @@ -0,0 +1,81 @@ +import { Suspense } from 'react'; +import { match } from 'ts-pattern'; +import { useAtomValue } from 'jotai'; + +import { File } from '../simulation-files'; +import { fileAtomFamily } from '../atoms'; + +import EphysViewer from '@/features/ephys-viewer'; +import { classNames } from '@/util/utils'; +import { WorkspaceContext } from '@/types/common'; +import { ICircuitSimulationResult } from '@/api/entitycore/types/entities/circuit-simulation-result'; + +type FileViewerProps = { + file?: File; + context: WorkspaceContext; + className?: string; +}; + +export function FileViewer({ file, context, className = '' }: FileViewerProps) { + const fileName = file?.assetPath?.split('/').at(-1) ?? file?.asset.path.split('/').at(-1); + const fileExt = fileName?.split('.').at(-1)?.toLowerCase(); + + const viewerContent = match(fileExt) + .with(undefined, () =>

Select a file for preview

) + .with('json', () => ) + .with('nwb', () => ) + .otherwise(() => ); + + return ( +
+
+ Loading...
}>{viewerContent} +
+ + ); +} + +type JsonFileViewerProps = { + file: File; + context: WorkspaceContext; +}; + +function JsonFileViewer({ file, context }: JsonFileViewerProps) { + const parsedJson = useAtomValue( + fileAtomFamily({ + id: file.asset.id, + entityId: file.entity.id, + entityType: file.entity.type, + assetPath: file.assetPath, + context, + }) + ); + + return
{JSON.stringify(parsedJson, null, 2)}
; +} + +type NwbFileViewerProps = { + file: File; + context: WorkspaceContext; +}; + +function NwbFileViewer({ file, context }: NwbFileViewerProps) { + return ; +} + +type PlaceholderFileViewerProps = { + file: File; +}; + +function PlaceholderFileViewer({ file }: PlaceholderFileViewerProps) { + const fileName = file?.assetPath?.split('/').at(-1) ?? file?.asset.path.split('/').at(-1); + const fileExt = fileName?.split('.').at(-1)?.toLowerCase(); + + return ( +
+ + Preview for {fileExt} files is not supported yet + +
+ ); +} diff --git a/src/features/contribute/_components/hooks/circuit.ts b/src/features/contribute/_components/hooks/circuit.ts new file mode 100644 index 000000000..4d3ae3c9b --- /dev/null +++ b/src/features/contribute/_components/hooks/circuit.ts @@ -0,0 +1,88 @@ +import React from 'react'; + +import { getCircuit } from '@/api/entitycore/queries/model/circuit'; +import { ICircuit } from '@/api/entitycore/types/entities/circuit'; +import { useAppNotification } from '@/components/notification'; +import { AssetLabel } from '@/api/entitycore/types/shared/global'; +import { downloadAsset } from '@/api/entitycore/queries/assets'; +import { EntityTypeEnum } from '@/api/entitycore/types'; + +const pendingQueries = new Map>(); + +/** + * Retrieve a circuit from EntityCore. + * @param circuitId + * @returns `undefined` if the query is pending, `null` if an error occured and the circuit in case of success. + */ +export function useCircuit(circuitId: string | undefined): ICircuit | undefined | null { + const { error } = useAppNotification(); + const [circuit, setCircuit] = React.useState(undefined); + + React.useEffect(() => { + if (!circuitId) { + setCircuit(undefined); + return; + } + + getQuery(circuitId) + .then(setCircuit) + .catch((ex) => { + error({ + message: `Unable to retrieve circuit "${circuitId}"!\n${ex}`, + }); + setCircuit(null); + }); + }, [circuitId, error]); + + return circuit; +} + +export function useCircuitImageURL(circuitId: string | undefined) { + const { error } = useAppNotification(); + const [url, setUrl] = React.useState(undefined); + const circuit = useCircuit(circuitId); + + React.useEffect(() => { + const action = async () => { + if (!circuit) return; + + const asset = circuit.assets.find( + (item) => item.label === AssetLabel.simulation_designer_image + ); + if (!asset) { + error({ message: `No image found for circuit "${circuit.name}" (${circuitId})!` }); + return; + } + try { + const resp = await downloadAsset({ + entityType: EntityTypeEnum.Circuit, + entityId: circuit.id, + id: asset.id, + asRawResponse: false, + }); + if (!(resp instanceof ArrayBuffer)) { + throw new Error('Wrong image format: expected ArrayBuffer!'); + } + const blob = new Blob([resp], { type: asset.content_type }); + const newUrl = URL.createObjectURL(blob); + setUrl(newUrl); + } catch (ex) { + error({ + message: `Unable to download image for circuit "${circuit.name}"!\n${ex}`, + }); + } + }; + action(); + }, [circuit, circuitId, error]); + + return url; +} + +function getQuery(circuitId: string): Promise { + const query = pendingQueries.get(circuitId); + if (query) return query; + + const newQuery = getCircuit({ id: circuitId }); + pendingQueries.set(circuitId, newQuery); + return newQuery; +} diff --git a/src/features/contribute/_components/hooks/config-atom.ts b/src/features/contribute/_components/hooks/config-atom.ts new file mode 100644 index 000000000..9fbb48e61 --- /dev/null +++ b/src/features/contribute/_components/hooks/config-atom.ts @@ -0,0 +1,30 @@ +import React from 'react'; +import { atom, useAtom } from 'jotai'; + +import { JSONSchema, AtomsMap } from '../../types'; +import { Config } from '../components'; +import { isAtom } from '../utils'; + +export function useConfigAtom(schema: JSONSchema | null, atomsMap: AtomsMap) { + const configAtom = React.useMemo(() => { + return atom((get) => { + const result: Config = {}; + Object.keys(atomsMap).forEach((key) => { + if (isAtom(atomsMap[key])) result[key] = get(atomsMap[key]); + else { + result[key] = {}; + Object.entries(atomsMap[key]).forEach(([subkey, v]) => { + if (typeof result[key] === 'string') return; + result[key][subkey] = get(v); + }); + } + }); + + result.type = schema?.properties?.type.const ?? ''; + + return result; + }); + }, [atomsMap, schema]); + const [config] = useAtom(configAtom); + return config; +} diff --git a/src/features/contribute/_components/hooks/schema.ts b/src/features/contribute/_components/hooks/schema.ts new file mode 100644 index 000000000..6beb8c33f --- /dev/null +++ b/src/features/contribute/_components/hooks/schema.ts @@ -0,0 +1,157 @@ +import React from 'react'; +import { atom } from 'jotai'; +import { NotificationInstance } from 'antd/es/notification/interface'; + +import $RefParser from '@apidevtools/json-schema-ref-parser'; + +import { AtomsMap, JSONSchema } from '../../types'; +import { ConfigValue, Config } from '../components'; +import { isPlainObject, isAtom } from '../utils'; +import { assertErrorMessage } from '@/util/utils'; + +export function useObioneJsonSchema( + circuitId: string, + notification: NotificationInstance, + setSchema: React.Dispatch>, + setAtomsMap: (atomsMap: AtomsMap) => void, + initialConfig?: Config +) { + React.useEffect(() => { + async function fetchSpec() { + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_OBI_ONE_URL}/openapi.json`); + const json = await res.json(); + const dereferenced = await $RefParser.dereference(json); + // @ts-ignore + const theSchema = dereferenced.components.schemas.SimulationsForm as JSONSchema; + if (!theSchema.properties) return; + + setSchema(theSchema); + + const map: { + [key: string]: + | ReturnType>> + | Record>>>; + } = {}; + + if (initialConfig) { + Object.entries(initialConfig) + .filter(([k]) => { + return isRootCategory(theSchema, k); + }) + .forEach(([k, v]) => { + if (isPlainObject(v)) map[k] = atom>(v); + }); + + Object.entries(initialConfig) + .filter(([k]) => { + return !isRootCategory(theSchema, k); + }) + .forEach(([k, v]) => { + map[k] = {}; + + Object.entries(v).forEach(([subK, subV]) => { + if (!isPlainObject(subV) || isAtom(map[k])) return; + map[k][subK] = atom>(subV); + }); + }); + } else { + // Setting up initial values and constants. + Object.entries(theSchema.properties).forEach(([k, v]) => { + if (!v.additionalProperties) { + const initial: Record = {}; + + if (v.properties) + Object.entries(v.properties).forEach(([subkey, subValue]) => { + if (subkey === 'type') initial[subkey] = subValue.const ?? null; + else initial[subkey] = subValue.default ?? null; + }); + + if (k === 'initialize') { + initial.circuit = { + type: 'CircuitFromID', + id_str: circuitId, + }; + } + + map[k] = atom>(initial); + } else map[k] = {}; + }); + } + + setAtomsMap(map); + } catch (e) { + // eslint-disable-next-line no-console + console.error(assertErrorMessage(e)); + notification.error({ message: assertErrorMessage(e) }); + } + } + + fetchSpec(); + }, [notification, setAtomsMap, setSchema, initialConfig]); +} + +export function isRootCategory(schema: JSONSchema, key: string) { + return schema.properties?.[key] && !schema.properties[key].additionalProperties; +} + +export function resolveKey(schema: JSONSchema, tabKey: string, itemIdx: number | null) { + if (typeof itemIdx === null) throw new Error('Invalid itemIdx'); + if (!schema.properties?.[tabKey]?.singular_name) throw new Error(`Invalid schema for ${tabKey}`); + if (isRootCategory(schema, tabKey)) throw new Error("Shouldn't be a root category"); + + return `${schema.properties[tabKey].singular_name.replaceAll(' ', '')}_${itemIdx}`; +} + +export function useObioneJsonConfigurationSchema( + circuitId: string, + notification: NotificationInstance, + setSchema: React.Dispatch>, + setAtomsMap: (atomsMap: AtomsMap) => void +) { + React.useEffect(() => { + async function fetchSpec() { + try { + const res = await fetch(`http://0.0.0.0:8100/openapi.json`); + // const res = await fetch(`${process.env.NEXT_PUBLIC_OBI_ONE_URL}/openapi.json`); + console.log('Fetching schema'); + const json = await res.json(); + const dereferenced = await $RefParser.dereference(json); + // @ts-ignore + const theSchema = dereferenced.components.schemas.ContributeMorphologyForm as JSONSchema; + if (!theSchema.properties) return; + + setSchema(theSchema); + + const map: { + [key: string]: + | ReturnType>> + | Record>>>; + } = {}; + + // Setting up initial values and constants. + Object.entries(theSchema.properties).forEach(([k, v]) => { + if (!v.additionalProperties) { + const initial: Record = {}; + + if (v.properties) + Object.entries(v.properties).forEach(([subkey, subValue]) => { + if (subkey === 'type') initial[subkey] = subValue.const ?? null; + else initial[subkey] = subValue.default ?? null; + }); + + map[k] = atom>(initial); + } else map[k] = {}; + }); + + setAtomsMap(map); + } catch (e) { + // eslint-disable-next-line no-console + console.error(assertErrorMessage(e)); + notification.error({ message: assertErrorMessage(e) }); + } + } + + fetchSpec(); + }, [notification, setAtomsMap, setSchema]); +} diff --git a/src/features/contribute/_components/section.tsx b/src/features/contribute/_components/section.tsx new file mode 100644 index 000000000..b8dc739c7 --- /dev/null +++ b/src/features/contribute/_components/section.tsx @@ -0,0 +1,221 @@ +/* eslint-disable no-param-reassign */ +import React, { Fragment } from 'react'; +import { atom } from 'jotai'; +import { + CheckCircleFilled, + DeleteOutlined, + PlusCircleOutlined, + WarningFilled, +} from '@ant-design/icons'; +import { ErrorObject } from 'ajv'; + +import { AtomsMap, JSONSchema } from '../types'; +import { Chevron, Config, ConfigValue, Tab } from './components'; +import { isAtom, isPlainObject } from './utils'; +import { isRootCategory, resolveKey } from './hooks/schema'; + +import { classNames } from '@/util/utils'; + +export function Section({ + schema, + k, + sectionSchema, + atomsMap, + setAtomsMap, + configTab, + setConfigTab, + config, + campaignId, + loading, + errors, + selectedItemIdx, + setSelectedItemIdx, + setEditing, + setSelectedCategory, + readOnly, +}: { + schema: JSONSchema | null; // The global schema + k: string; // secition key + sectionSchema: JSONSchema; // The schema for this section + atomsMap: AtomsMap; + setAtomsMap: React.Dispatch>; + configTab: string; // Key for selected section + setConfigTab: (configTab: string) => void; + config: Config; + campaignId: string; + loading: boolean; + errors: ErrorObject, unknown>[] | null | undefined; + selectedItemIdx: number | null; + setSelectedItemIdx: (selectedItemIdx: number | null) => void; + setEditing: React.Dispatch>; + setSelectedCategory: React.Dispatch>; + readOnly?: boolean; +}) { + if (!schema || !schema?.properties) return; + + const handleHeaderClick = (subkey: string, subValue: unknown) => { + if (isPlainObject(subValue)) { + setSelectedCategory(typeof subValue.type === 'string' ? subValue.type : ''); + setSelectedItemIdx(parseInt(subkey.split('_')[1], 10)); + } + setEditing(true); + }; + + return ( + <> + { + if (configTab === k && !isRootCategory(schema, k)) { + setEditing(false); + setSelectedCategory(''); + setSelectedItemIdx(null); + setConfigTab(''); + return; + } + + setConfigTab(k); + setSelectedItemIdx(null); + if (!sectionSchema.additionalProperties) setEditing(true); + else setEditing(false); + }} + extraClass="w-full flex justify-between h-[50px] min-h-[50px] items-center drop-shadow" + > + {schema.properties?.[k]?.title} +
+ {errors?.find((error) => error.instancePath.startsWith('/' + k)) ? ( + + ) : ( + + )} + +
+
+ {sectionSchema.additionalProperties && configTab === k && config[k] && ( + <> + {Object.entries(config[k]).map(([subkey, subValue]) => { + const idx = parseInt(subkey.split('_')[1], 10); + + const isSelected = configTab === k && idx === selectedItemIdx; + + return ( + +
handleHeaderClick(subkey, subValue)} + onKeyDown={(evt) => { + if (evt.key === ' ' || evt.key === 'Enter') { + handleHeaderClick(subkey, subValue); + } + }} + > + {subkey} +
+ {errors?.find((error) => error.instancePath.startsWith(`/${k}/${subkey}`)) ? ( + + ) : ( + + )} + + {!campaignId && !loading && !readOnly && ( + { + e.stopPropagation(); + + setSelectedCategory(''); + setEditing(false); + + const selectedTabAtoms = atomsMap[configTab]; + if (!isAtom(selectedTabAtoms)) { + const refereeKey = resolveKey(schema, configTab, idx); + + delete selectedTabAtoms[refereeKey]; + + // Initialize case + const configInitialize = config.initialize; + if ( + isPlainObject(configInitialize) && + isPlainObject(configInitialize.node_set) && + typeof configInitialize.node_set.block_name === 'string' && + configInitialize.node_set.block_name === refereeKey + ) { + atomsMap.initialize = atom>({ + ...configInitialize, + node_set: null, + }); + } + + // Check all keys in the config + Object.entries(config) + .filter(([configK]) => configK !== 'initialize') + .forEach(([configK, configV]) => { + if (typeof configV !== 'object') return; + + // Check all keys in a section (e.g stimuli, recordings) + Object.entries(configV).forEach(([entryKey, entryV]) => { + if (!isPlainObject(entryV)) return; + + // Check all values in a particular object (a single stimuli, a single timestamp, etc) + Object.entries(entryV).forEach(([fieldK, field]) => { + if ( + !isPlainObject(entryV) || + !isPlainObject(field) || + typeof field.block_name !== 'string' || + isAtom(atomsMap[configK]) || // skip top level atoms (e.g initialize) + field.block_name !== refereeKey + ) + return; + + // Deleting the reference to current object + + delete entryV[fieldK]; //eslint-disable-line + + // The atom that has a reference to current object + atomsMap[configK][entryKey] = + atom>(entryV); + }); + }); + }); + + setAtomsMap({ + ...atomsMap, + [configTab]: { + ...selectedTabAtoms, + }, + }); + } + + setSelectedItemIdx(null); + }} + /> + )} +
+
+
+ ); + })} + {!campaignId && !loading && !readOnly && ( + + )} + + )} + + ); +} diff --git a/src/features/contribute/_components/simulation-files/index.tsx b/src/features/contribute/_components/simulation-files/index.tsx new file mode 100644 index 000000000..0d2a8ca2c --- /dev/null +++ b/src/features/contribute/_components/simulation-files/index.tsx @@ -0,0 +1,198 @@ +import { useAtomValue } from 'jotai'; +import { Suspense, useMemo } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; + +import { ICircuitSimulation } from '@/api/entitycore/types/entities/circuit-simulation'; +import { CircuitSimulationExecutionStatus } from '@/api/entitycore/types/entities/circuit-simulation-execution'; +import { IEntity } from '@/api/entitycore/types/entities/entity'; +import { IAsset } from '@/api/entitycore/types/shared/global'; +import { + circuitAtomFamily, + simResultBySimIdAtomFamily, +} from '@/features/small-microcircuit/_components/atoms'; +import { WorkspaceContext } from '@/types/common'; +import { classNames } from '@/util/utils'; + +export type File = { + asset: IAsset; + entity: IEntity; + assetPath?: string; +}; + +type SimulationFilesProps = { + simulation: ICircuitSimulation; + execStatus?: CircuitSimulationExecutionStatus | null; + selectedFile?: File; + onSelect: (file: File) => void; + context: WorkspaceContext; +}; + +export function SimulationFiles({ + simulation, + execStatus, + selectedFile, + onSelect, + context, +}: SimulationFilesProps) { + const outputAvailable = + execStatus && + [CircuitSimulationExecutionStatus.ERROR, CircuitSimulationExecutionStatus.DONE].includes( + execStatus + ); + + return ( + <> +

Input files

+ +

Output files

+ Loading...}> + {outputAvailable && ( + There was an issue loading output files + } + resetKeys={[simulation]} + > + + + )} + + + ); +} + +type SimulationInputFilesProps = { + simulation: ICircuitSimulation; + context: WorkspaceContext; + selectedFile?: File; + onSelect: (file: File) => void; + className?: string; +}; + +function SimulationInputFiles({ + simulation, + context, + selectedFile, + onSelect, + className = '', +}: SimulationInputFilesProps) { + const circuit = useAtomValue(circuitAtomFamily({ circuitId: simulation.entity_id, context })); + // TODO: fetch circuitConfig + const sonataCircuitAsset = circuit.assets.find((asset) => asset.label === 'sonata_circuit'); + const circuitConfigFile: File = useMemo( + () => ({ + entity: circuit, + asset: sonataCircuitAsset!, + assetPath: 'circuit_config.json', + }), + [circuit, sonataCircuitAsset] + ); + + const files: File[] = useMemo( + () => [circuitConfigFile, ...simulation.assets.map((asset) => ({ asset, entity: simulation }))], + [simulation, circuitConfigFile] + ); + + return ( +
+ {files.map((file) => ( + + ))} +
+ ); +} + +type SimulationOutputFilesProps = { + simulation: ICircuitSimulation; + context: WorkspaceContext; + selectedFile?: File; + onSelect: (file: File) => void; + className?: string; +}; + +function SimulationOutputFiles({ + simulation, + onSelect, + selectedFile, + context, + className = '', +}: SimulationOutputFilesProps) { + const simResult = useAtomValue( + simResultBySimIdAtomFamily({ simulationId: simulation.id, context }) + ); + + const files: File[] = useMemo( + () => simResult.assets.map((asset) => ({ asset, entity: simResult })), + [simResult] + ); + + return ( +
+ {files.map((file) => ( + + ))} +
+ ); +} + +type SimulationFileProps = { + file: File; + selected?: boolean; + onSelect: (file: File) => void; +}; + +function SimulationFile({ file, selected, onSelect }: SimulationFileProps) { + const fileName = file.assetPath?.split('/').at(-1) ?? file.asset.path.split('/').at(-1); + const fileExt = fileName?.split('.').at(-1); + + return ( + + ); +} diff --git a/src/features/contribute/_components/simulation-status/index.tsx b/src/features/contribute/_components/simulation-status/index.tsx new file mode 100644 index 000000000..499d77215 --- /dev/null +++ b/src/features/contribute/_components/simulation-status/index.tsx @@ -0,0 +1,50 @@ +import { CircuitSimulationExecutionStatus } from '@/api/entitycore/types/entities/circuit-simulation-execution'; + +export function SimulationStatusBadge({ status }: { status?: CircuitSimulationExecutionStatus }) { + const color = status ? statusColorMap[status] : '#fafafa'; + const showSpinner = status && ['pending', 'running'].includes(status); + + // TODO: move spinner outside of the module. + + return ( +
+ {showSpinner && ( + + + + + )} + + + {status ?? ''} + +
+ ); +} +const statusColorMap: Record = { + created: '#434343', + pending: '#fa8c16', + running: '#1890ff', + done: '#389e0d', + error: '#f5222d', +}; diff --git a/src/features/contribute/_components/tabs-selector/index.ts b/src/features/contribute/_components/tabs-selector/index.ts new file mode 100644 index 000000000..e516b026d --- /dev/null +++ b/src/features/contribute/_components/tabs-selector/index.ts @@ -0,0 +1 @@ +export { default } from './tabs-selector'; diff --git a/src/features/contribute/_components/tabs-selector/tabs-selector.tsx b/src/features/contribute/_components/tabs-selector/tabs-selector.tsx new file mode 100644 index 000000000..3b2b02a17 --- /dev/null +++ b/src/features/contribute/_components/tabs-selector/tabs-selector.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import { Tab } from '../components'; +import { TabType } from '../../types'; +import { classNames } from '@/util/utils'; + +interface TabsSelectorProps { + className?: string; + tab: TabType; + setTab(tab: TabType): void; + disableSimulationTab: boolean; +} + +export default function TabsSelector({ + className, + tab, + setTab, + disableSimulationTab, +}: TabsSelectorProps) { + return ( +
+
+ setTab('configuration')} + > + Configuration + + setTab('simulations')} + disabled={disableSimulationTab} + > + Simulations + +
+
+ ); +} diff --git a/src/features/contribute/_components/tooltip/index.ts b/src/features/contribute/_components/tooltip/index.ts new file mode 100644 index 000000000..bb106e207 --- /dev/null +++ b/src/features/contribute/_components/tooltip/index.ts @@ -0,0 +1 @@ +export { default } from './tooltip'; diff --git a/src/features/contribute/_components/tooltip/tooltip.module.css b/src/features/contribute/_components/tooltip/tooltip.module.css new file mode 100644 index 000000000..a4e563aa8 --- /dev/null +++ b/src/features/contribute/_components/tooltip/tooltip.module.css @@ -0,0 +1,39 @@ +.tooltipContainer { + position: relative; + overflow: visible; +} + +.tooltip { + position: relative; + background: #0050b3ee; + color: #fff; + padding: 0.5em 1em; + border-radius: 0.25em; + position: absolute; + left: 0; + right: 0; + bottom: 0; + width: 100%; + pointer-events: none; + z-index: 9; + transform: translateY(calc(100% + 16px)); + transition: all 0.2s; + opacity: 0; +} + +.tooltipContainer:hover .tooltip { + opacity: 1; +} + +.tooltip::after { + content: ''; + border: 16px solid transparent; + border-bottom: 16px solid #0050b3ee; + position: absolute; + top: -16px; + left: 50%; + width: 0; + height: 0; + margin: -16px; + transform: scale(0.5, 1); +} diff --git a/src/features/contribute/_components/tooltip/tooltip.tsx b/src/features/contribute/_components/tooltip/tooltip.tsx new file mode 100644 index 000000000..45a1501f4 --- /dev/null +++ b/src/features/contribute/_components/tooltip/tooltip.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import { classNames } from '@/util/utils'; + +import styles from './tooltip.module.css'; + +interface TooltipProps { + className?: string; + value?: React.ReactNode; + children: React.ReactNode; +} + +export default function Tooltip({ className, value, children }: TooltipProps) { + return ( +
+ {children} + {value &&
{value}
} +
+ ); +} diff --git a/src/features/contribute/_components/utils.ts b/src/features/contribute/_components/utils.ts new file mode 100644 index 000000000..f4f3c34a2 --- /dev/null +++ b/src/features/contribute/_components/utils.ts @@ -0,0 +1,73 @@ +import { Atom } from 'jotai'; +import uniq from 'lodash/uniq'; +import { CircuitSimulationExecutionStatus } from '@/api/entitycore/types/entities/circuit-simulation-execution'; + +export type Primitive = null | boolean | number | string; +export interface ConfigObject { + [key: string]: Primitive | Primitive[] | ConfigObject; +} + +export function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && !Array.isArray(value) && value !== null; +} + +export function isAtom(val: unknown): val is Atom { + return typeof val === 'object' && val !== null && 'read' in val; +} + +export const ORDERING: Record = { + assets: { + order: 0, + category: 'Assets', + }, + morphology: { + order: 1, + category: 'Setup', + }, + contribution: { + order: 2, + category: 'Contribution', + }, + subject: { + order: 4, + category: 'Subject', + }, + license: { + order: 5, + category: 'License', + }, + mtype: { + order: 6, + category: 'Mtype', + }, + // publication: { + // order: 7, + // category: 'Publication', + // }, + // scientificartifact: { + // order: 8, + // category: 'Scientific Artifact', + // }, +}; + +export const CATEGORIES: string[] = uniq(Object.values(ORDERING).map((o) => o.category)); + +const simExecStatusListordered = [ + CircuitSimulationExecutionStatus.CREATED, + CircuitSimulationExecutionStatus.PENDING, + CircuitSimulationExecutionStatus.RUNNING, + CircuitSimulationExecutionStatus.DONE, + CircuitSimulationExecutionStatus.ERROR, +]; + +export function getLatestSimExecStatus( + remoteStatus: CircuitSimulationExecutionStatus, + localStatus: CircuitSimulationExecutionStatus +) { + const remoteStatusIdx = simExecStatusListordered.indexOf(remoteStatus); + const localStatusIdx = simExecStatusListordered.indexOf(localStatus); + + const latestStatus = Math.max(remoteStatusIdx, localStatusIdx); + + return simExecStatusListordered[latestStatus]; +} diff --git a/src/features/contribute/error-registry.ts b/src/features/contribute/error-registry.ts new file mode 100755 index 000000000..afc71adf6 --- /dev/null +++ b/src/features/contribute/error-registry.ts @@ -0,0 +1,7 @@ +import { messages } from '@/i18n/en/simulation'; + +const errorCodeToTranslationKey: Record = { + ACCOUNTING_INSUFFICIENT_FUNDS_ERROR: messages.LowFundsError, +}; + +export default errorCodeToTranslationKey; diff --git a/src/features/contribute/index.tsx b/src/features/contribute/index.tsx new file mode 100755 index 000000000..66c406951 --- /dev/null +++ b/src/features/contribute/index.tsx @@ -0,0 +1,616 @@ +// index.tsx + +'use client'; + +import { + LoadingOutlined, + RightOutlined, + UploadOutlined, + CheckCircleFilled, +} from '@ant-design/icons'; +import Ajv, { AnySchema } from 'ajv'; +import { atom, useAtomValue, useSetAtom } from 'jotai'; +import { Fragment, Suspense, useEffect, useMemo, useRef, useState } from 'react'; +import { useParams } from 'next/navigation'; +import { Progress } from 'antd'; +import { match } from 'ts-pattern'; +import { + simExecRemoteStatusMapAtomFamily, + simExecStatusMapAtomFamily, + simulationsByCampaignIdAtomFamily, +} from './_components/atoms'; +import CircuitPreview from './_components/circuit-preview'; +import { Config, ConfigValue, JSONSchemaForm } from './_components/components'; +import { FileViewer } from './_components/file-viewer'; +import { useCircuit } from './_components/hooks/circuit'; +import { useConfigAtom } from './_components/hooks/config-atom'; +import { + isRootCategory, + resolveKey, + useObioneJsonConfigurationSchema, +} from './_components/hooks/schema'; +import { Section } from './_components/section'; +import TabsSelector from './_components/tabs-selector'; +import { CATEGORIES, isAtom, ORDERING } from './_components/utils'; +import { AtomsMap, JSONSchema, TabType } from './types'; +import { resolveDataKey } from '@/utils/key-builder'; +import { useBrainRegionHierarchy } from '@/features/brain-region-hierarchy/context'; +import { ICircuitSimulation } from '@/api/entitycore/types/entities/circuit-simulation'; +import { CircuitSimulationExecutionStatus } from '@/api/entitycore/types/entities/circuit-simulation-execution'; +import ApiError from '@/api/error'; +import authFetch from '@/authFetch'; +import { useAppNotification } from '@/components/notification'; +import { ButtonCopyId } from '@/features/details-view/button-copy-id'; +import { useLastTruthyValue } from '@/hooks/hooks'; +import { messages } from '@/i18n/en/simulation'; +import { runSimulation } from '@/services/small-scale-simulator/circuit'; +import { MessageType } from '@/services/small-scale-simulator/types'; +import { assertErrorMessage, classNames } from '@/util/utils'; +import { getErrorMessage } from '@/utils/error'; +import styles from './small-microcircuit.module.css'; + +export default function ContributeMorphologyConfiguration({ + circuitId, + virtualLabId, + projectId, + initialCampaignId, + initialConfig, +}: { + circuitId: string; + virtualLabId: string; + projectId: string; + initialCampaignId?: string; + initialConfig?: Config; +}) { + if (!!initialCampaignId !== !!initialConfig) + throw new Error('Both or none of initialCampaignId, initialConfigId should be passed'); + + const params = useParams(); + const { node } = useBrainRegionHierarchy({ + dataKey: resolveDataKey({ section: 'explore', projectId }), + }); + + const [configTab, setConfigTab] = useState('info'); + const [editing, setEditing] = useState(true); + const [schema, setSchema] = useState(null); + const [selectedCategory, setSelectedCategory] = useState(''); + const [selectedItemIdx, setSelectedItemIdx] = useState(null); + const [loading, setLoading] = useState(false); + const [showConfig, setShowConfig] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [fileStatus, setFileStatus] = useState<{ + message?: string; + }>({}); + const [newJsonPayload, setNewJsonPayload] = useState(null); + const notification = useAppNotification(); + const [campaignId, setCampaignId] = useState(initialCampaignId ?? ''); + const initialConfigValidated = useRef(false); + + // Add success state + const [isSuccess, setIsSuccess] = useState(false); + + // Add validation state + const [formValidation, setFormValidation] = useState<{ + isValid: boolean; + errors: string[]; + }>({ + isValid: true, + errors: [], + }); + + const selectedCatSchema = schema?.properties?.[configTab]?.additionalProperties?.anyOf?.find( + (s) => s.properties?.type.const === selectedCategory + ); + + const handleAddReferenceClick = (referenceTab: string) => { + setConfigTab(referenceTab); + setEditing(true); + setSelectedCategory(''); + }; + + // Handle validation changes from the form + const handleValidationChange = (isValid: boolean, errors: string[]) => { + setFormValidation({ isValid, errors }); + }; + + const readOnly = initialConfig !== undefined; + + const validate = useMemo(() => { + const ajv = new Ajv({ strictSchema: false, allErrors: true }); + if (!schema) return; + return ajv.compile(schema as AnySchema); + }, [schema]); + + const [atomsMap, setAtomsMap] = useState({}); + + const config = useConfigAtom(schema, atomsMap, node.id, configTab); + + if (validate && initialConfig && !initialConfigValidated.current) { + initialConfigValidated.current = true; + validate(initialConfig); + if (validate.errors) throw new Error('Invalid Simulation Campaign Configuration'); + } + + const errors = useMemo(() => { + if (validate) validate(config); + return validate?.errors; + }, [validate, config]); + + useObioneJsonConfigurationSchema(circuitId, notification, setSchema, setAtomsMap); + + const hasSuccessfulUpload = Object.values(fileStatus).some( + (status) => status?.status === 'success' + ); + + // Update the submit button condition + const canSubmit = + !errors?.length && !loading && !readOnly && selectedFile && formValidation.isValid; + + // Show success page if upload was successful + if (isSuccess) { + return ( +
+
+
+ ✓ Morphology created successfully +
+ +
+
+ ); + } + + if (!schema) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+
+
+ {/* Show overall form validation status */} + {!formValidation.isValid && formValidation.errors.length > 0 && ( +
+
Required fields missing:
+
{formValidation.errors.join(', ')}
+
+ )} + +
Assets
+
{ + setConfigTab('assets'); + setEditing(true); + setSelectedCategory(''); + setSelectedItemIdx(null); + }} + > + Assets +
+ {selectedFile ? ( + + ) : ( +
// Placeholder to maintain spacing + )} + +
+
+ {CATEGORIES.filter((c) => c !== 'Assets').map((c) => { + return ( + +
{c}
+ {schema.properties && + Object.entries(schema.properties) + .filter(([k]) => k !== 'type' && ORDERING[k]?.category === c) + .sort((a, b) => { + const order = (k: string) => ORDERING[k]?.order ?? 999; + return order(a[0]) - order(b[0]); + }) + .map(([k, v]) => { + return ( +
+ ); + })} + + ); + })} +
+ {!readOnly && ( + + )} +
+
+ {configTab === 'assets' && editing && ( +
+
+ UPLOAD MORPHOLOGY FILE +
+
+ One reference file should be loaded. +
+
+
+ + {fileStatus?.message && ( + {fileStatus.message} + )} +
+
+
+ )} + {schema.properties && + schema.properties?.[configTab]?.additionalProperties?.anyOf && + !selectedCategory && + editing && + configTab !== 'assets' && ( +
+ {schema.properties[configTab].additionalProperties.anyOf.map((o) => { + return ( + +
{ + if (isRootCategory(schema, configTab)) return; + setSelectedCategory(o.properties?.type.const ?? ''); + const initial: Record = {}; + if (o.properties) + Object.entries(o.properties).forEach(([subkey, subValue]) => { + if (subkey === 'type') initial[subkey] = subValue.const ?? null; + else initial[subkey] = subValue.default ?? null; + }); + const itemIndexes = Object.keys(atomsMap[configTab]).map((subkey) => + parseInt(subkey.split('_')[1], 10) + ); + itemIndexes.sort((a, b) => a - b); + const itemIdx = (itemIndexes.at(-1) ?? -1) + 1; + setSelectedItemIdx(itemIdx); + setAtomsMap({ + ...atomsMap, + [configTab]: { + ...atomsMap[configTab], + [resolveKey(schema, configTab, itemIdx)]: + atom>(initial), + }, + }); + }} + > +
{o.title}
+
{o.description}
+
+
+ ); + })} +
+ )} + {schema.properties && + schema.properties?.[configTab] && + editing && + (isRootCategory(schema, configTab) || selectedCatSchema) && + configTab !== 'assets' && ( + + )} +
+
+ {showConfig && ( +
+
+
+

New Record JSON

+ +
+ +
+              {JSON.stringify(newJsonPayload, null, 2)}
+            
+ +
+ +
+
+
+ )} +
+ ); +} diff --git a/src/features/contribute/small-microcircuit.module.css b/src/features/contribute/small-microcircuit.module.css new file mode 100755 index 000000000..0a4777633 --- /dev/null +++ b/src/features/contribute/small-microcircuit.module.css @@ -0,0 +1,32 @@ +div.threeColumns { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 2fr); + gap: 5px; + height: calc(100% - 100px); +} + +div.threeColumns > div { + min-height: 0; + height: 100%; +} + +div.threeColumns > div.scrollable { + overflow-y: auto; + overflow-x: visible; +} + +div.threeColumns > div:first-child { + display: grid; + grid-template-rows: 1fr auto; + gap: 1em; + overflow: unset; +} + +header.header { + display: flex; + flex-wrap: nowrap; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 1em; +} diff --git a/src/features/contribute/types.ts b/src/features/contribute/types.ts new file mode 100755 index 000000000..2217e8e2b --- /dev/null +++ b/src/features/contribute/types.ts @@ -0,0 +1,35 @@ +import { atom } from 'jotai'; + +import { ConfigValue } from './_components/components'; +import { CircuitSimulationExecutionStatus } from '@/api/entitycore/types/entities/circuit-simulation-execution'; + +export type JSONSchema = { + type?: 'string' | 'number' | 'integer' | 'object' | 'array' | 'boolean' | 'null'; + properties?: { [key: string]: JSONSchema }; + items?: JSONSchema | JSONSchema[]; + required?: string[]; + enum?: any[]; + const?: string; + additionalProperties?: JSONSchema; + oneOf?: JSONSchema[]; + anyOf?: JSONSchema[]; + allOf?: JSONSchema[]; + not?: JSONSchema; + format?: string; + title?: string; + description?: string; + default?: any; + examples?: any[]; + [key: string]: any; + singular_name?: string; +}; + +export interface AtomsMap { + [key: string]: + | ReturnType>> + | Record>>>; +} + +export type TabType = 'configuration' | 'simulations'; + +export type SimExecStatusMap = Map; From 8d79750fde788e0359011da76d13d8a96f824a53 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 14:03:59 +0200 Subject: [PATCH 02/19] fixed linting --- .../contribute/_components/components.tsx | 80 +++++++++---------- src/features/contribute/index.tsx | 42 ++-------- 2 files changed, 48 insertions(+), 74 deletions(-) diff --git a/src/features/contribute/_components/components.tsx b/src/features/contribute/_components/components.tsx index ee93f0735..e7677510f 100644 --- a/src/features/contribute/_components/components.tsx +++ b/src/features/contribute/_components/components.tsx @@ -23,40 +23,40 @@ export type Config = Record; // Updated structure for MTYPE classification const MTYPE_CLASSES = [ - { mtype_pref_label: 'L23_NBC', mtype_id: '6605787b-ba14-43fd-a954-de9cff4b15a0' }, - { mtype_pref_label: 'L23_NGC', mtype_id: 'dd16dca0-e567-416b-b8b7-f8fbcaa05af0' }, - { mtype_pref_label: 'L23_PC', mtype_id: '0791edc9-7ad4-4a94-a4a5-feab9b690d7e' }, - { mtype_pref_label: 'L23_PTPC', mtype_id: '52ea242f-6591-425a-8eae-962fa0b4dfe0' }, - { mtype_pref_label: 'L23_SBC', mtype_id: 'fbb8b577-92f4-4c93-b355-0982ef5a3c7c' }, - { mtype_pref_label: 'L23_STPC', mtype_id: '93be8237-9861-4870-9977-ff1cf9e7462c' }, - { mtype_pref_label: 'L2_ChC', mtype_id: '91ba3deb-1139-4bc6-a12f-6a64a0ed0e92' }, - { mtype_pref_label: 'L2_IPC', mtype_id: 'e55f12e1-807c-42f6-ba98-91d6d30c57d7' }, - { mtype_pref_label: 'L2_MC', mtype_id: 'ea51f2c8-95fc-4940-a400-c37a3ff2d9eb' }, - { mtype_pref_label: 'L2_PC', mtype_id: 'dd73956b-423e-42c5-87d9-9e2cc84356b9' }, - { mtype_pref_label: 'L2_TPC', mtype_id: '7abf03d5-30b0-41ae-a02b-1f4e26c243a8' }, - { mtype_pref_label: 'L2_TPC:A', mtype_id: '9b04acb1-4737-4088-8d22-0658414bdda1' }, - { mtype_pref_label: 'L2_TPC:B', mtype_id: '4b6862b9-c438-4dfc-a2e6-1ad4d7a00eda' }, - { mtype_pref_label: 'L3_MC', mtype_id: '52578494-b41c-499b-9717-5d11f4b2f068' }, - { mtype_pref_label: 'L3_PC', mtype_id: '87fec7dd-7a2f-400a-aee0-94d1946cf1ab' }, - { mtype_pref_label: 'L3_TPC', mtype_id: '229c31f1-a6ec-4d8c-85d3-d8175ffde109' }, - { mtype_pref_label: 'L3_TPC:A', mtype_id: 'dd346e90-7bca-4976-bf9a-303b6a94b339' }, - { mtype_pref_label: 'L3_TPC:B', mtype_id: 'a71d226c-2c56-40ee-a4be-9726fc430932' }, - { mtype_pref_label: 'L3_TPC:C', mtype_id: 'd9b7bd4d-cec9-4fec-a448-79320de89f2a' }, - { mtype_pref_label: 'L4_BP', mtype_id: 'a55f6ce7-068a-4c5e-a883-de5f4304612e' }, - { mtype_pref_label: 'L4_BTC', mtype_id: '8315d249-6678-4d55-b581-3b6f9eb48e86' }, - { mtype_pref_label: 'L4_ChC', mtype_id: '0e4f3036-0d14-4fd9-b7a8-87f5e90a9fa6' }, - { mtype_pref_label: 'L4_DBC', mtype_id: '8de61c06-31e8-4483-bd98-608bc874b369' }, - { mtype_pref_label: 'L4_LBC', mtype_id: 'bb875a91-4ae5-4f6f-b050-5ad952e9cd6c' }, - { mtype_pref_label: 'L4_MC', mtype_id: '0bdf029e-a55a-444c-a7c9-9d0ff51239a5' }, - { mtype_pref_label: 'L4_NBC', mtype_id: '72673af9-2f2b-4a9a-95dc-552777ab63b9' }, - { mtype_pref_label: 'L4_NGC', mtype_id: '41f41550-5e0e-4de7-b52d-62110c338a27' }, - { mtype_pref_label: 'L4_PC', mtype_id: 'ad5769c6-7e86-4433-8f34-9efcb4f0d182' }, - { mtype_pref_label: 'L4_SBC', mtype_id: '43a7d86b-71c5-4a10-be62-ef6cf95ca694' }, - { mtype_pref_label: 'L4_SSC', mtype_id: '400a55f7-e162-4fd1-80a0-4f2facea7cec' }, - { mtype_pref_label: 'L4_TPC', mtype_id: '02e13718-5227-4c28-b838-04dd0c2c67f2' }, - { mtype_pref_label: 'L4_UPC', mtype_id: '2ef7e0b5-39e4-441b-a72a-c7186afa7f5c' }, - { mtype_pref_label: 'L56_PC', mtype_id: '629d6d6f-93f9-43d8-8a99-277740fd8f22' }, - { mtype_pref_label: 'L5_BP', mtype_id: '7b16c860-ae76-4ddf-b093-4e28620b3712' }, + {"mtype_pref_label":"L23_NBC","mtype_id":"6605787b-ba14-43fd-a954-de9cff4b15a0"}, + {"mtype_pref_label":"L23_NGC","mtype_id":"dd16dca0-e567-416b-b8b7-f8fbcaa05af0"}, + {"mtype_pref_label":"L23_PC","mtype_id":"0791edc9-7ad4-4a94-a4a5-feab9b690d7e"}, + {"mtype_pref_label":"L23_PTPC","mtype_id":"52ea242f-6591-425a-8eae-962fa0b4dfe0"}, + {"mtype_pref_label":"L23_SBC","mtype_id":"fbb8b577-92f4-4c93-b355-0982ef5a3c7c"}, + {"mtype_pref_label":"L23_STPC","mtype_id":"93be8237-9861-4870-9977-ff1cf9e7462c"}, + {"mtype_pref_label":"L2_ChC","mtype_id":"91ba3deb-1139-4bc6-a12f-6a64a0ed0e92"}, + {"mtype_pref_label":"L2_IPC","mtype_id":"e55f12e1-807c-42f6-ba98-91d6d30c57d7"}, + {"mtype_pref_label":"L2_MC","mtype_id":"ea51f2c8-95fc-4940-a400-c37a3ff2d9eb"}, + {"mtype_pref_label":"L2_PC","mtype_id":"dd73956b-423e-42c5-87d9-9e2cc84356b9"}, + {"mtype_pref_label":"L2_TPC","mtype_id":"7abf03d5-30b0-41ae-a02b-1f4e26c243a8"}, + {"mtype_pref_label":"L2_TPC:A","mtype_id":"9b04acb1-4737-4088-8d22-0658414bdda1"}, + {"mtype_pref_label":"L2_TPC:B","mtype_id":"4b6862b9-c438-4dfc-a2e6-1ad4d7a00eda"}, + {"mtype_pref_label":"L3_MC","mtype_id":"52578494-b41c-499b-9717-5d11f4b2f068"}, + {"mtype_pref_label":"L3_PC","mtype_id":"87fec7dd-7a2f-400a-aee0-94d1946cf1ab"}, + {"mtype_pref_label":"L3_TPC","mtype_id":"229c31f1-a6ec-4d8c-85d3-d8175ffde109"}, + {"mtype_pref_label":"L3_TPC:A","mtype_id":"dd346e90-7bca-4976-bf9a-303b6a94b339"}, + {"mtype_pref_label":"L3_TPC:B","mtype_id":"a71d226c-2c56-40ee-a4be-9726fc430932"}, + {"mtype_pref_label":"L3_TPC:C","mtype_id":"d9b7bd4d-cec9-4fec-a448-79320de89f2a"}, + {"mtype_pref_label":"L4_BP","mtype_id":"a55f6ce7-068a-4c5e-a883-de5f4304612e"}, + {"mtype_pref_label":"L4_BTC","mtype_id":"8315d249-6678-4d55-b581-3b6f9eb48e86"}, + {"mtype_pref_label":"L4_ChC","mtype_id":"0e4f3036-0d14-4fd9-b7a8-87f5e90a9fa6"}, + {"mtype_pref_label":"L4_DBC","mtype_id":"8de61c06-31e8-4483-bd98-608bc874b369"}, + {"mtype_pref_label":"L4_LBC","mtype_id":"bb875a91-4ae5-4f6f-b050-5ad952e9cd6c"}, + {"mtype_pref_label":"L4_MC","mtype_id":"0bdf029e-a55a-444c-a7c9-9d0ff51239a5"}, + {"mtype_pref_label":"L4_NBC","mtype_id":"72673af9-2f2b-4a9a-95dc-552777ab63b9"}, + {"mtype_pref_label":"L4_NGC","mtype_id":"41f41550-5e0e-4de7-b52d-62110c338a27"}, + {"mtype_pref_label":"L4_PC","mtype_id":"ad5769c6-7e86-4433-8f34-9efcb4f0d182"}, + {"mtype_pref_label":"L4_SBC","mtype_id":"43a7d86b-71c5-4a10-be62-ef6cf95ca694"}, + {"mtype_pref_label":"L4_SSC","mtype_id":"400a55f7-e162-4fd1-80a0-4f2facea7cec"}, + {"mtype_pref_label":"L4_TPC","mtype_id":"02e13718-5227-4c28-b838-04dd0c2c67f2"}, + {"mtype_pref_label":"L4_UPC","mtype_id":"2ef7e0b5-39e4-441b-a72a-c7186afa7f5c"}, + {"mtype_pref_label":"L56_PC","mtype_id":"629d6d6f-93f9-43d8-8a99-277740fd8f22"}, + {"mtype_pref_label":"L5_BP","mtype_id":"7b16c860-ae76-4ddf-b093-4e28620b3712"} ]; // Helper function to check if a field value is considered "empty" or invalid @@ -243,7 +243,7 @@ export function JSONSchemaForm({ ...v.anyOf?.find((subv) => subv.type !== 'array' && subv.type !== 'null'), }; const normalizedKey = k.toLowerCase().replace(/[\s_]/g, ''); - const isRequired = schema.required?.includes(k); + const fieldError = getFieldErrorMessage(k); const hasError = hasFieldError(k); @@ -564,18 +564,18 @@ export function JSONSchemaForm({
); } - + // New condition for 'mtype class id' if (isMtypeClassIdField) { const options = MTYPE_CLASSES.map((mtype) => ({ label: mtype.mtype_pref_label, value: mtype.mtype_id, })); - + const currentMtypeLabel = MTYPE_CLASSES.find( (mtype) => mtype.mtype_id === state[k] )?.mtype_pref_label; - + return (
{ - console.log(`Updating strain_id to: ${newV}`); - setState((prev) => { - const newState = { ...prev, [k]: newV }; - console.log('New state after strain_id update:', newState); - return newState; - }); + setState((prev) => ({ ...prev, [k]: newV })); markFieldTouched(k); }} value={state[k]} @@ -424,7 +415,6 @@ export function JSONSchemaForm({ } if (isAgePeriodField) { - console.log('Rendering age_period field:', { key: k, schema: v, resolved: obj }); if (obj.enum) { return (
@@ -445,7 +435,6 @@ export function JSONSchemaForm({
); } - console.warn('No enum found for age_period:', obj); return (
{Array.isArray(state[k]) && state[k].map((e, i) => ( -
+
{e}{' '} {!disabled && ( { - const newElements = [...state[k]]; + const newElements = [...(Array.isArray(state[k]) ? state[k] : [])]; newElements.splice(i, 1); setState({ ...state, [k]: newElements }); markFieldTouched(k); @@ -659,7 +648,7 @@ export function JSONSchemaForm({ {isPlainObject(state[k]) && Array.isArray(state[k].elements) && state[k].elements.map((e, i) => ( -
+
{e}{' '} {!disabled && ( - {newElement[k] !== null && ( + {!isNil(newElement[k]) && ( { @@ -803,7 +793,6 @@ export function JSONSchemaForm({ ); } - console.warn(`Unhandled schema type for key: ${k}`, obj); return (
{schema.title}
@@ -836,7 +821,7 @@ export function JSONSchemaForm({
    {validationErrors.map((error, index) => ( -
  • {error}
  • +
  • {error}
  • ))}
@@ -861,18 +846,6 @@ export function JSONSchemaForm({ normalizedKey === 'licenseid' || normalizedKey === 'license_id'; const isMtypeClassIdField = normalizedKey === 'mtypeclassid'; - if (currentCategory === 'subject' && normalizedKey === 'ageperiod') { - console.log('Age period field:', { - key: k, - schema: v, - title: v.title, - resolved: { - ...v, - ...v.anyOf?.find((subv) => subv.type !== 'array' && subv.type !== 'null'), - }, - }); - } - return (
@@ -888,7 +861,7 @@ export function JSONSchemaForm({ : isAgePeriodField ? 'AGE PERIOD' : isLicenseIdField - ? 'LICENSE' // New condition for license title + ? 'LICENSE' : isMtypeClassIdField ? 'MTYPE CLASS' : v.title || k} diff --git a/src/features/contribute/_components/hooks/schema.ts b/src/features/contribute/_components/hooks/schema.ts index 6beb8c33f..41016d5eb 100644 --- a/src/features/contribute/_components/hooks/schema.ts +++ b/src/features/contribute/_components/hooks/schema.ts @@ -10,7 +10,7 @@ import { isPlainObject, isAtom } from '../utils'; import { assertErrorMessage } from '@/util/utils'; export function useObioneJsonSchema( - circuitId: string, + notification: NotificationInstance, setSchema: React.Dispatch>, setAtomsMap: (atomsMap: AtomsMap) => void, @@ -67,13 +67,6 @@ export function useObioneJsonSchema( else initial[subkey] = subValue.default ?? null; }); - if (k === 'initialize') { - initial.circuit = { - type: 'CircuitFromID', - id_str: circuitId, - }; - } - map[k] = atom>(initial); } else map[k] = {}; }); From e2169e1f8e09a8b8c7283f767f89a1c1c0414466 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 14:26:05 +0200 Subject: [PATCH 04/19] more linting --- src/features/contribute/index.tsx | 59 ++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/src/features/contribute/index.tsx b/src/features/contribute/index.tsx index 88705b9cb..69d91ed3a 100755 --- a/src/features/contribute/index.tsx +++ b/src/features/contribute/index.tsx @@ -1,4 +1,3 @@ -// index.tsx 'use client'; import { @@ -9,7 +8,7 @@ import { } from '@ant-design/icons'; import Ajv, { AnySchema } from 'ajv'; import { atom } from 'jotai'; -import { Fragment, useMemo, useRef, useState } from 'react'; +import { Fragment, useMemo, useRef, useState, KeyboardEvent } from 'react'; import { Config, ConfigValue, JSONSchemaForm } from './_components/components'; import { useConfigAtom } from './_components/hooks/config-atom'; import { @@ -121,14 +120,15 @@ export default function ContributeMorphologyConfiguration({ !errors?.length && !loading && !readOnly && selectedFile && formValidation.isValid; // Show success page if upload was successful -if (isSuccess) { + if (isSuccess) { return (
-
+
✓ Morphology created successfully
- {CATEGORIES.filter((c) => c !== 'Assets').map((c) => { - return ( - -
{c}
- {schema.properties && - Object.entries(schema.properties) - .filter(([k]) => k !== 'type' && ORDERING[k]?.category === c) - .sort((a, b) => { - const order = (k: string) => ORDERING[k]?.order ?? 999; - return order(a[0]) - order(b[0]); - }) - .map(([k, v]) => { - return ( -
- ); - })} - - ); - })} + {CATEGORIES.filter((c) => c !== 'Assets').map((c) => ( + +
{c}
+ {schema.properties && + Object.entries(schema.properties) + .filter(([k]) => k !== 'type' && ORDERING[k]?.category === c) + .sort((a, b) => { + const order = (k: string) => ORDERING[k]?.order ?? 999; + return order(a[0]) - order(b[0]); + }) + .map(([k, v]) => ( +
+ ))} + + ))}
{!readOnly && ( @@ -464,10 +482,9 @@ export default function ContributeMorphologyConfiguration({ htmlFor="file-upload" className={classNames( 'flex cursor-pointer items-center justify-center rounded-full px-4 py-2 text-sm', - loading - ? 'bg-gray-300 text-gray-500' - : 'bg-primary-8 hover:bg-primary-9 text-white', - readOnly && 'cursor-not-allowed opacity-50' + loading || readOnly + ? 'bg-gray-300 text-gray-500 cursor-not-allowed opacity-50' + : 'bg-primary-8 hover:bg-primary-9 text-white' )} > @@ -503,70 +520,67 @@ export default function ContributeMorphologyConfiguration({ editing && configTab !== 'assets' && (
- {schema.properties[configTab].additionalProperties.anyOf.map((o) => { - return ( - -
{ - if (isRootCategory(schema, configTab)) return; - setSelectedCategory(o.properties?.type.const ?? ''); - const initial: Record = {}; - if (o.properties) - Object.entries(o.properties).forEach(([subkey, subValue]) => { - if (subkey === 'type') initial[subkey] = subValue.const ?? null; - else initial[subkey] = subValue.default ?? null; - }); - const itemIndexes = Object.keys(atomsMap[configTab]).map((subkey) => - parseInt(subkey.split('_')[1], 10) - ); - itemIndexes.sort((a, b) => a - b); - const itemIdx = (itemIndexes.at(-1) ?? -1) + 1; - setSelectedItemIdx(itemIdx); - setAtomsMap({ - ...atomsMap, - [configTab]: { - ...atomsMap[configTab], - [resolveKey(schema, configTab, itemIdx)]: - atom>(initial), - }, + {schema.properties[configTab].additionalProperties.anyOf.map((o) => ( +
{ + if (isRootCategory(schema, configTab)) return; + setSelectedCategory(o.properties?.type.const ?? ''); + const initial: Record = {}; + if (o.properties) { + Object.entries(o.properties).forEach(([subkey, subValue]) => { + initial[subkey] = subkey === 'type' ? subValue.const ?? null : subValue.default ?? null; + }); + } + const itemIndexes = Object.keys(atomsMap[configTab]).map((subkey) => + parseInt(subkey.split('_')[1], 10) + ); + itemIndexes.sort((a, b) => a - b); + const itemIdx = (itemIndexes.at(-1) ?? -1) + 1; + setSelectedItemIdx(itemIdx); + setAtomsMap({ + ...atomsMap, + [configTab]: { + ...atomsMap[configTab], + [resolveKey(schema, configTab, itemIdx)]: + atom>(initial), + }, + }); + }} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + if (isRootCategory(schema, configTab)) return; + setSelectedCategory(o.properties?.type.const ?? ''); + const initial: Record = {}; + if (o.properties) { + Object.entries(o.properties).forEach(([subkey, subValue]) => { + initial[subkey] = subkey === 'type' ? subValue.const ?? null : subValue.default ?? null; }); - }} - onKeyDown={(e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - if (isRootCategory(schema, configTab)) return; - setSelectedCategory(o.properties?.type.const ?? ''); - const initial: Record = {}; - if (o.properties) - Object.entries(o.properties).forEach(([subkey, subValue]) => { - if (subkey === 'type') initial[subkey] = subValue.const ?? null; - else initial[subkey] = subValue.default ?? null; - }); - const itemIndexes = Object.keys(atomsMap[configTab]).map((subkey) => - parseInt(subkey.split('_')[1], 10) - ); - itemIndexes.sort((a, b) => a - b); - const itemIdx = (itemIndexes.at(-1) ?? -1) + 1; - setSelectedItemIdx(itemIdx); - setAtomsMap({ - ...atomsMap, - [configTab]: { - ...atomsMap[configTab], - [resolveKey(schema, configTab, itemIdx)]: - atom>(initial), - }, - }); - } - }} - > -
{o.title}
-
{o.description}
-
- - ); - })} + } + const itemIndexes = Object.keys(atomsMap[configTab]).map((subkey) => + parseInt(subkey.split('_')[1], 10) + ); + itemIndexes.sort((a, b) => a - b); + const itemIdx = (itemIndexes.at(-1) ?? -1) + 1; + setSelectedItemIdx(itemIdx); + setAtomsMap({ + ...atomsMap, + [configTab]: { + ...atomsMap[configTab], + [resolveKey(schema, configTab, itemIdx)]: + atom>(initial), + }, + }); + } + }} + > +
{o.title}
+
{o.description}
+
+ ))}
)} {schema.properties && From fe55d22ea44b5a26b2d80f586cca02421713b940 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 14:50:03 +0200 Subject: [PATCH 06/19] more linting --- .../contribute/_components/components.tsx | 116 ++++++++++-------- 1 file changed, 67 insertions(+), 49 deletions(-) diff --git a/src/features/contribute/_components/components.tsx b/src/features/contribute/_components/components.tsx index 793b4b2da..62e7f5802 100644 --- a/src/features/contribute/_components/components.tsx +++ b/src/features/contribute/_components/components.tsx @@ -1,4 +1,3 @@ -// components.tsx import { useEffect, useState, useRef } from 'react'; import { atom, useAtom } from 'jotai'; import { InputNumber, Input, Select, Button } from 'antd'; @@ -28,7 +27,7 @@ const MTYPE_CLASSES = [ {"mtype_pref_label":"L23_NGC","mtype_id":"dd16dca0-e567-416b-b8b7-f8fbcaa05af0"}, {"mtype_pref_label":"L23_PC","mtype_id":"0791edc9-7ad4-4a94-a4a5-feab9b690d7e"}, {"mtype_pref_label":"L23_PTPC","mtype_id":"52ea242f-6591-425a-8eae-962fa0b4dfe0"}, - {"mtype_pref_label":"L23_SBC","mtype_id":"fbb8b577-92f4-4c93-b355-0982ef5a3c7c"}, + {"mtype_pref_label":"L23_SBC","mtype_id":"fbb8b577-92f4-4c93-b355-0982af5a3c7c"}, {"mtype_pref_label":"L23_STPC","mtype_id":"93be8237-9861-4870-9977-ff1cf9e7462c"}, {"mtype_pref_label":"L2_ChC","mtype_id":"91ba3deb-1139-4bc6-a12f-6a64a0ed0e92"}, {"mtype_pref_label":"L2_IPC","mtype_id":"e55f12e1-807c-42f6-ba98-91d6d30c57d7"}, @@ -266,17 +265,16 @@ export function JSONSchemaForm({ const isLicenseIdField = normalizedKey === 'licenseid' || normalizedKey === 'license_id'; const isMtypeClassIdField = normalizedKey === 'mtypeclassid'; - // Common input props for consistency - const commonInputProps = { - disabled, - className: `w-full ${hasError ? 'border-red-500' : ''}`, - onBlur: () => markFieldTouched(k), - }; - if (isBrainRegionIdField && nodeId) { return (
- + markFieldTouched(k)} + value={nodeId} + readOnly + />
); } @@ -285,9 +283,10 @@ export function JSONSchemaForm({ return (
markFieldTouched(k)} value="b7ad4cca-4ac2-4095-9781-37fb68fe9ca1" - className="w-full bg-gray-100" readOnly />
@@ -298,9 +297,10 @@ export function JSONSchemaForm({ return (
markFieldTouched(k)} value="e3e70682-c209-4cac-a29f-6fbed82c07cd" - className="w-full bg-gray-100" readOnly />
@@ -342,7 +342,8 @@ export function JSONSchemaForm({ return (
markFieldTouched(k)} value={currentValue} className={`w-full ${hasError || showDateError ? 'border-red-500' : ''}`} onChange={(e) => { @@ -400,7 +401,9 @@ export function JSONSchemaForm({ return (
markFieldTouched(k)} onChange={(newV) => { setState({ ...state, [k]: newV }); markFieldTouched(k); @@ -438,7 +443,9 @@ export function JSONSchemaForm({ return (
markFieldTouched(k)} value={typeof state[k] === 'string' ? state[k] : ''} onChange={(e) => { setState({ ...state, [k]: e.currentTarget.value }); @@ -456,14 +463,14 @@ export function JSONSchemaForm({
{Array.isArray(state[k]) && - state[k].map((e, i) => ( -
+ state[k].map((e) => ( +
{e}{' '} {!disabled && ( { const newElements = [...(Array.isArray(state[k]) ? state[k] : [])]; - newElements.splice(i, 1); + newElements.splice(newElements.indexOf(e), 1); setState({ ...state, [k]: newElements }); markFieldTouched(k); }} @@ -540,7 +547,9 @@ export function JSONSchemaForm({ return (
markFieldTouched(k)} onChange={(newV) => { setState({ ...state, [k]: newV }); markFieldTouched(k); @@ -614,7 +625,9 @@ export function JSONSchemaForm({ return (
markFieldTouched(k)} onChange={(newV) => { setState({ ...state, [k]: newV }); markFieldTouched(k); @@ -782,7 +797,9 @@ export function JSONSchemaForm({ return (
markFieldTouched(k)} value={typeof state[k] === 'string' ? state[k] : ''} onChange={(e) => { setState({ ...state, [k]: e.currentTarget.value }); @@ -796,7 +813,9 @@ export function JSONSchemaForm({ return (
markFieldTouched(k)} value={typeof state[k] === 'string' ? state[k] : ''} onChange={(e) => { setState({ ...state, [k]: e.currentTarget.value }); @@ -807,6 +826,21 @@ export function JSONSchemaForm({
); } + + function getFieldTitle(k: string, v: JSONSchema, normalizedKey: string): string { + if (normalizedKey === 'strainid' || normalizedKey === 'strain_id' || normalizedKey === 'strain') { + return 'STRAIN'; + } else if (normalizedKey === 'ageperiod' || normalizedKey === 'age_period') { + return 'AGE PERIOD'; + } else if (normalizedKey === 'licenseid' || normalizedKey === 'license_id') { + return 'LICENSE'; + } else if (normalizedKey === 'mtypeclassid') { + return 'MTYPE CLASS'; + } else { + return v.title || k; + } + } + return (
@@ -820,7 +854,7 @@ export function JSONSchemaForm({ Please fix the following required fields:
    - {validationErrors.map((error, index) => ( + {validationErrors.map((error) => (
  • {error}
  • ))}
@@ -836,15 +870,7 @@ export function JSONSchemaForm({ .map(([k, v]) => { const isRequired = schema.required?.includes(k); const normalizedKey = k.toLowerCase().replace(/[\s_]/g, ''); - const isStrainIdField = - normalizedKey === 'strainid' || - normalizedKey === 'strain_id' || - normalizedKey === 'strain'; - const isAgePeriodField = - normalizedKey === 'ageperiod' || normalizedKey === 'age_period'; - const isLicenseIdField = - normalizedKey === 'licenseid' || normalizedKey === 'license_id'; - const isMtypeClassIdField = normalizedKey === 'mtypeclassid'; + const fieldTitle = getFieldTitle(k, v, normalizedKey); return (
@@ -856,15 +882,7 @@ export function JSONSchemaForm({ )} title={v.description} > - {isStrainIdField - ? 'STRAIN' - : isAgePeriodField - ? 'AGE PERIOD' - : isLicenseIdField - ? 'LICENSE' - : isMtypeClassIdField - ? 'MTYPE CLASS' - : v.title || k} + {fieldTitle} {isRequired && *}
{v.units &&
{v.units}
} From 1f511e3579d6303d0147ee8b965229c99e7ad582 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 14:54:21 +0200 Subject: [PATCH 07/19] more linting --- src/features/contribute/index.tsx | 53 ++++++++++++++++--------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/features/contribute/index.tsx b/src/features/contribute/index.tsx index 10d63382d..60c03a4dd 100755 --- a/src/features/contribute/index.tsx +++ b/src/features/contribute/index.tsx @@ -311,32 +311,35 @@ export default function ContributeMorphologyConfiguration({ const subjectId = subjectResponseData.id; // --- END: New pre-flight API call --- - const newJson = { - authorized_public: false, - license_id: null, - name: morphologyConfig.name || 'test', - description: morphologyConfig.description || 'string', - location: { - x: 0, - y: 0, - z: 0, - }, - legacy_id: Array.isArray(morphologyConfig.legacy_id) - ? morphologyConfig.legacy_id - : typeof morphologyConfig.legacy_id === 'string' - ? [morphologyConfig.legacy_id] - : [], - species_id: - morphologyConfig.species_id || - morphologyConfig.species || - 'b7ad4cca-4ac2-4095-9781-37fb68fe9ca1', - strain_id: morphologyConfig.strain_id || null, - brain_region_id: - morphologyConfig.brain_region_id || morphologyConfig.brain_region || node.id, - subject_id: subjectId, - }; - setNewJsonPayload(newJson); + const legacyId = Array.isArray(morphologyConfig.legacy_id) + ? morphologyConfig.legacy_id + : typeof morphologyConfig.legacy_id === 'string' + ? [morphologyConfig.legacy_id] + : []; + + const newJson = { + authorized_public: false, + license_id: null, + name: morphologyConfig.name || 'test', + description: morphologyConfig.description || 'string', + location: { + x: 0, + y: 0, + z: 0, + }, + legacy_id: legacyId, + species_id: + morphologyConfig.species_id || + morphologyConfig.species || + 'b7ad4cca-4ac2-4095-9781-37fb68fe9ca1', + strain_id: morphologyConfig.strain_id || null, + brain_region_id: + morphologyConfig.brain_region_id || morphologyConfig.brain_region || node.id, + subject_id: subjectId, +}; + + setNewJsonPayload(newJson); const headers = { accept: 'application/json', From 68ed8311c61165986a86d654277d894d172df2c1 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 16:20:17 +0200 Subject: [PATCH 08/19] more linting --- src/features/contribute/index.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/features/contribute/index.tsx b/src/features/contribute/index.tsx index 60c03a4dd..f29c316c3 100755 --- a/src/features/contribute/index.tsx +++ b/src/features/contribute/index.tsx @@ -312,11 +312,15 @@ export default function ContributeMorphologyConfiguration({ // --- END: New pre-flight API call --- - const legacyId = Array.isArray(morphologyConfig.legacy_id) - ? morphologyConfig.legacy_id - : typeof morphologyConfig.legacy_id === 'string' - ? [morphologyConfig.legacy_id] - : []; + let legacyId; + +if (Array.isArray(morphologyConfig.legacy_id)) { + legacyId = morphologyConfig.legacy_id; +} else if (typeof morphologyConfig.legacy_id === 'string') { + legacyId = [morphologyConfig.legacy_id]; +} else { + legacyId = []; +} const newJson = { authorized_public: false, From df59fa732be27822f86126b33f70b4b69628de69 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 16:45:30 +0200 Subject: [PATCH 09/19] more linting --- .../explore/interactive/add/layout.tsx | 1 - .../interactive-navigation-menu.tsx | 2 - .../ContributeLayout/index.tsx | 2 +- src/features/cell-composition/how-to.md | 4 - .../contribute/_components/components.tsx | 104 +++++++++--------- .../contribute/_components/hooks/schema.ts | 1 - src/features/contribute/index.tsx | 103 ++++++++--------- .../build/me-model/configure.tsx | 8 +- 8 files changed, 107 insertions(+), 118 deletions(-) diff --git a/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/layout.tsx b/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/layout.tsx index 813531a61..fdbc79a4d 100755 --- a/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/layout.tsx +++ b/src/app/app/virtual-lab/lab/[virtualLabId]/project/[projectId]/explore/interactive/add/layout.tsx @@ -2,6 +2,5 @@ import { ReactNode } from 'react'; import ContributeLayout from '@/components/explore-section/ContributeLayout'; export default function ContributeDataLayout({ children }: { children: ReactNode }) { - return {children}; } diff --git a/src/components/entities-type-stats/interactive-navigation-menu.tsx b/src/components/entities-type-stats/interactive-navigation-menu.tsx index 3e46a88cb..bcbf5438a 100644 --- a/src/components/entities-type-stats/interactive-navigation-menu.tsx +++ b/src/components/entities-type-stats/interactive-navigation-menu.tsx @@ -61,8 +61,6 @@ function EntityTypeStats(props: StatsPanelProps) { .with('experimental-data', () => ( <> {Object.entries(ExperimentalEntitiesTileTypes).map(([key, value], index) => { - - const baseHref = `${pathName}/${value?.explore.basePrefix}/${value.slug}`; let records = ''; let isError = false; diff --git a/src/components/explore-section/ContributeLayout/index.tsx b/src/components/explore-section/ContributeLayout/index.tsx index 054a58896..55bceb0f1 100755 --- a/src/components/explore-section/ContributeLayout/index.tsx +++ b/src/components/explore-section/ContributeLayout/index.tsx @@ -10,7 +10,7 @@ import BackToInteractiveExplorationBtn from '@/components/explore-section/BackTo export default function ContributeLayout({ children }: { children: ReactNode }) { const pathname = usePathname(); - + // Determine the href for the back button. // Goal: If current is /virtual-lab/explore/interactive/add/experimental/morphology // It should go back to /virtual-lab/explore/interactive diff --git a/src/features/cell-composition/how-to.md b/src/features/cell-composition/how-to.md index bcbb3c3a0..e84bf62e4 100644 --- a/src/features/cell-composition/how-to.md +++ b/src/features/cell-composition/how-to.md @@ -26,27 +26,23 @@ flowchart TD ## How to process: 1. **Initialization** - - start with a brain region id (selected by the user) - retrieve all leaf regions under that brain region (use `brainRegionHierarchyAtom`) - create volume maps for each leaf region (using `brainRegionAtlasAtom`) 2. **Node Construction** - - for each leaf region: - process mTypes and their child eTypes - build tree nodes with cell counts and densities - calculate composition data based on volumes 3. **Node Aggregation** - - merge nodes that represent the same mType/eType across different leaf regions - sum cell counts across regions - recalculate densities based on aggregated volumes - create links between parent mTypes and child eTypes 4. **Final Calculations** - - calculate total neuron and glial cell counts - determine total volume (not needed for now) - compute the whole composition densities diff --git a/src/features/contribute/_components/components.tsx b/src/features/contribute/_components/components.tsx index 62e7f5802..c731cca78 100644 --- a/src/features/contribute/_components/components.tsx +++ b/src/features/contribute/_components/components.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useRef } from 'react'; import { atom, useAtom } from 'jotai'; import { InputNumber, Input, Select, Button } from 'antd'; import { CheckCircleOutlined, CloseCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; -import { isNil } from 'lodash'; +import isNil from 'lodash/isNil'; import { JSONSchema } from '../types'; import { isPlainObject } from './utils'; @@ -23,40 +23,40 @@ export type Config = Record; // Updated structure for MTYPE classification const MTYPE_CLASSES = [ - {"mtype_pref_label":"L23_NBC","mtype_id":"6605787b-ba14-43fd-a954-de9cff4b15a0"}, - {"mtype_pref_label":"L23_NGC","mtype_id":"dd16dca0-e567-416b-b8b7-f8fbcaa05af0"}, - {"mtype_pref_label":"L23_PC","mtype_id":"0791edc9-7ad4-4a94-a4a5-feab9b690d7e"}, - {"mtype_pref_label":"L23_PTPC","mtype_id":"52ea242f-6591-425a-8eae-962fa0b4dfe0"}, - {"mtype_pref_label":"L23_SBC","mtype_id":"fbb8b577-92f4-4c93-b355-0982af5a3c7c"}, - {"mtype_pref_label":"L23_STPC","mtype_id":"93be8237-9861-4870-9977-ff1cf9e7462c"}, - {"mtype_pref_label":"L2_ChC","mtype_id":"91ba3deb-1139-4bc6-a12f-6a64a0ed0e92"}, - {"mtype_pref_label":"L2_IPC","mtype_id":"e55f12e1-807c-42f6-ba98-91d6d30c57d7"}, - {"mtype_pref_label":"L2_MC","mtype_id":"ea51f2c8-95fc-4940-a400-c37a3ff2d9eb"}, - {"mtype_pref_label":"L2_PC","mtype_id":"dd73956b-423e-42c5-87d9-9e2cc84356b9"}, - {"mtype_pref_label":"L2_TPC","mtype_id":"7abf03d5-30b0-41ae-a02b-1f4e26c243a8"}, - {"mtype_pref_label":"L2_TPC:A","mtype_id":"9b04acb1-4737-4088-8d22-0658414bdda1"}, - {"mtype_pref_label":"L2_TPC:B","mtype_id":"4b6862b9-c438-4dfc-a2e6-1ad4d7a00eda"}, - {"mtype_pref_label":"L3_MC","mtype_id":"52578494-b41c-499b-9717-5d11f4b2f068"}, - {"mtype_pref_label":"L3_PC","mtype_id":"87fec7dd-7a2f-400a-aee0-94d1946cf1ab"}, - {"mtype_pref_label":"L3_TPC","mtype_id":"229c31f1-a6ec-4d8c-85d3-d8175ffde109"}, - {"mtype_pref_label":"L3_TPC:A","mtype_id":"dd346e90-7bca-4976-bf9a-303b6a94b339"}, - {"mtype_pref_label":"L3_TPC:B","mtype_id":"a71d226c-2c56-40ee-a4be-9726fc430932"}, - {"mtype_pref_label":"L3_TPC:C","mtype_id":"d9b7bd4d-cec9-4fec-a448-79320de89f2a"}, - {"mtype_pref_label":"L4_BP","mtype_id":"a55f6ce7-068a-4c5e-a883-de5f4304612e"}, - {"mtype_pref_label":"L4_BTC","mtype_id":"8315d249-6678-4d55-b581-3b6f9eb48e86"}, - {"mtype_pref_label":"L4_ChC","mtype_id":"0e4f3036-0d14-4fd9-b7a8-87f5e90a9fa6"}, - {"mtype_pref_label":"L4_DBC","mtype_id":"8de61c06-31e8-4483-bd98-608bc874b369"}, - {"mtype_pref_label":"L4_LBC","mtype_id":"bb875a91-4ae5-4f6f-b050-5ad952e9cd6c"}, - {"mtype_pref_label":"L4_MC","mtype_id":"0bdf029e-a55a-444c-a7c9-9d0ff51239a5"}, - {"mtype_pref_label":"L4_NBC","mtype_id":"72673af9-2f2b-4a9a-95dc-552777ab63b9"}, - {"mtype_pref_label":"L4_NGC","mtype_id":"41f41550-5e0e-4de7-b52d-62110c338a27"}, - {"mtype_pref_label":"L4_PC","mtype_id":"ad5769c6-7e86-4433-8f34-9efcb4f0d182"}, - {"mtype_pref_label":"L4_SBC","mtype_id":"43a7d86b-71c5-4a10-be62-ef6cf95ca694"}, - {"mtype_pref_label":"L4_SSC","mtype_id":"400a55f7-e162-4fd1-80a0-4f2facea7cec"}, - {"mtype_pref_label":"L4_TPC","mtype_id":"02e13718-5227-4c28-b838-04dd0c2c67f2"}, - {"mtype_pref_label":"L4_UPC","mtype_id":"2ef7e0b5-39e4-441b-a72a-c7186afa7f5c"}, - {"mtype_pref_label":"L56_PC","mtype_id":"629d6d6f-93f9-43d8-8a99-277740fd8f22"}, - {"mtype_pref_label":"L5_BP","mtype_id":"7b16c860-ae76-4ddf-b093-4e28620b3712"} + { mtype_pref_label: 'L23_NBC', mtype_id: '6605787b-ba14-43fd-a954-de9cff4b15a0' }, + { mtype_pref_label: 'L23_NGC', mtype_id: 'dd16dca0-e567-416b-b8b7-f8fbcaa05af0' }, + { mtype_pref_label: 'L23_PC', mtype_id: '0791edc9-7ad4-4a94-a4a5-feab9b690d7e' }, + { mtype_pref_label: 'L23_PTPC', mtype_id: '52ea242f-6591-425a-8eae-962fa0b4dfe0' }, + { mtype_pref_label: 'L23_SBC', mtype_id: 'fbb8b577-92f4-4c93-b355-0982af5a3c7c' }, + { mtype_pref_label: 'L23_STPC', mtype_id: '93be8237-9861-4870-9977-ff1cf9e7462c' }, + { mtype_pref_label: 'L2_ChC', mtype_id: '91ba3deb-1139-4bc6-a12f-6a64a0ed0e92' }, + { mtype_pref_label: 'L2_IPC', mtype_id: 'e55f12e1-807c-42f6-ba98-91d6d30c57d7' }, + { mtype_pref_label: 'L2_MC', mtype_id: 'ea51f2c8-95fc-4940-a400-c37a3ff2d9eb' }, + { mtype_pref_label: 'L2_PC', mtype_id: 'dd73956b-423e-42c5-87d9-9e2cc84356b9' }, + { mtype_pref_label: 'L2_TPC', mtype_id: '7abf03d5-30b0-41ae-a02b-1f4e26c243a8' }, + { mtype_pref_label: 'L2_TPC:A', mtype_id: '9b04acb1-4737-4088-8d22-0658414bdda1' }, + { mtype_pref_label: 'L2_TPC:B', mtype_id: '4b6862b9-c438-4dfc-a2e6-1ad4d7a00eda' }, + { mtype_pref_label: 'L3_MC', mtype_id: '52578494-b41c-499b-9717-5d11f4b2f068' }, + { mtype_pref_label: 'L3_PC', mtype_id: '87fec7dd-7a2f-400a-aee0-94d1946cf1ab' }, + { mtype_pref_label: 'L3_TPC', mtype_id: '229c31f1-a6ec-4d8c-85d3-d8175ffde109' }, + { mtype_pref_label: 'L3_TPC:A', mtype_id: 'dd346e90-7bca-4976-bf9a-303b6a94b339' }, + { mtype_pref_label: 'L3_TPC:B', mtype_id: 'a71d226c-2c56-40ee-a4be-9726fc430932' }, + { mtype_pref_label: 'L3_TPC:C', mtype_id: 'd9b7bd4d-cec9-4fec-a448-79320de89f2a' }, + { mtype_pref_label: 'L4_BP', mtype_id: 'a55f6ce7-068a-4c5e-a883-de5f4304612e' }, + { mtype_pref_label: 'L4_BTC', mtype_id: '8315d249-6678-4d55-b581-3b6f9eb48e86' }, + { mtype_pref_label: 'L4_ChC', mtype_id: '0e4f3036-0d14-4fd9-b7a8-87f5e90a9fa6' }, + { mtype_pref_label: 'L4_DBC', mtype_id: '8de61c06-31e8-4483-bd98-608bc874b369' }, + { mtype_pref_label: 'L4_LBC', mtype_id: 'bb875a91-4ae5-4f6f-b050-5ad952e9cd6c' }, + { mtype_pref_label: 'L4_MC', mtype_id: '0bdf029e-a55a-444c-a7c9-9d0ff51239a5' }, + { mtype_pref_label: 'L4_NBC', mtype_id: '72673af9-2f2b-4a9a-95dc-552777ab63b9' }, + { mtype_pref_label: 'L4_NGC', mtype_id: '41f41550-5e0e-4de7-b52d-62110c338a27' }, + { mtype_pref_label: 'L4_PC', mtype_id: 'ad5769c6-7e86-4433-8f34-9efcb4f0d182' }, + { mtype_pref_label: 'L4_SBC', mtype_id: '43a7d86b-71c5-4a10-be62-ef6cf95ca694' }, + { mtype_pref_label: 'L4_SSC', mtype_id: '400a55f7-e162-4fd1-80a0-4f2facea7cec' }, + { mtype_pref_label: 'L4_TPC', mtype_id: '02e13718-5227-4c28-b838-04dd0c2c67f2' }, + { mtype_pref_label: 'L4_UPC', mtype_id: '2ef7e0b5-39e4-441b-a72a-c7186afa7f5c' }, + { mtype_pref_label: 'L56_PC', mtype_id: '629d6d6f-93f9-43d8-8a99-277740fd8f22' }, + { mtype_pref_label: 'L5_BP', mtype_id: '7b16c860-ae76-4ddf-b093-4e28620b3712' }, ]; // Helper function to check if a field value is considered "empty" or invalid @@ -562,18 +562,18 @@ export function JSONSchemaForm({
); } - + // New condition for 'mtype class id' if (isMtypeClassIdField) { const options = MTYPE_CLASSES.map((mtype) => ({ label: mtype.mtype_pref_label, value: mtype.mtype_id, })); - + const currentMtypeLabel = MTYPE_CLASSES.find( (mtype) => mtype.mtype_id === state[k] )?.mtype_pref_label; - + return (
{fieldError &&
{fieldError}
}
- ); + ) } const strainOptions = Object.entries(selectedSpecies.strains).map(([name, id]) => ({ label: name, value: id, - })); + })) return (
@@ -405,8 +403,8 @@ export function JSONSchemaForm({ className={`w-full ${hasError ? 'border-red-500' : ''}`} onBlur={() => markFieldTouched(k)} onChange={(newV) => { - setState((prev) => ({ ...prev, [k]: newV })); - markFieldTouched(k); + setState((prev) => ({ ...prev, [k]: newV })) + markFieldTouched(k) }} value={state[k]} options={strainOptions} @@ -414,7 +412,7 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
}
- ); + ) } if (isAgePeriodField) { @@ -426,8 +424,8 @@ export function JSONSchemaForm({ className={`w-full ${hasError ? 'border-red-500' : ''}`} onBlur={() => markFieldTouched(k)} onChange={(newV) => { - setState({ ...state, [k]: newV }); - markFieldTouched(k); + setState({ ...state, [k]: newV }) + markFieldTouched(k) }} value={state[k]} options={obj.enum.map((subv: string) => ({ @@ -438,7 +436,7 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
}
- ); + ) } return (
@@ -448,13 +446,13 @@ export function JSONSchemaForm({ onBlur={() => markFieldTouched(k)} value={typeof state[k] === 'string' ? state[k] : ''} onChange={(e) => { - setState({ ...state, [k]: e.currentTarget.value }); + setState({ ...state, [k]: e.currentTarget.value }) }} placeholder="Enter age period" /> {fieldError &&
{fieldError}
}
- ); + ) } if (isLegacyIdField) { @@ -469,10 +467,10 @@ export function JSONSchemaForm({ {!disabled && ( { - const newElements = [...(Array.isArray(state[k]) ? state[k] : [])]; - newElements.splice(newElements.indexOf(e), 1); - setState({ ...state, [k]: newElements }); - markFieldTouched(k); + const newElements = [...(Array.isArray(state[k]) ? state[k] : [])] + newElements.splice(newElements.indexOf(e), 1) + setState({ ...state, [k]: newElements }) + markFieldTouched(k) }} /> )} @@ -499,17 +497,17 @@ export function JSONSchemaForm({ setState({ ...state, [k]: [...(Array.isArray(state[k]) ? state[k] : []), newElement[k]], - }); - setAddingElement({ ...addingElement, [k]: false }); - setNewElement({ ...newElement, [k]: null }); - markFieldTouched(k); + }) + setAddingElement({ ...addingElement, [k]: false }) + setNewElement({ ...newElement, [k]: null }) + markFieldTouched(k) }} /> )} { - setAddingElement({ ...addingElement, [k]: false }); - setNewElement({ ...newElement, [k]: null }); + setAddingElement({ ...addingElement, [k]: false }) + setNewElement({ ...newElement, [k]: null }) }} className="text-primary-8" /> @@ -518,7 +516,7 @@ export function JSONSchemaForm({
{fieldError &&
{fieldError}
}
- ); + ) } // New condition for 'license id' field @@ -532,17 +530,17 @@ export function JSONSchemaForm({ 'CC BY 4.0 Deed': 'ad8686db-3cdd-4e3f-bcbd-812380a9eba7', 'CC0 1.0 Deed': '74b8c953-67f5-4e95-ac58-095274901328', 'NGV_Data Licence_v1.0': '0c39107e-3b68-4f1f-904a-5c7f1b4f89c5', - }; + } const options = Object.entries(licenseOptions).map(([name, id]) => ({ label: name, value: id, - })); + })) // Find the key corresponding to the current state value to set the label const currentLicenseLabel = Object.keys(licenseOptions).find( - (key) => licenseOptions[key] === state[k] - ); + (key) => licenseOptions[key] === state[k], + ) return (
@@ -551,8 +549,8 @@ export function JSONSchemaForm({ className={`w-full ${hasError ? 'border-red-500' : ''}`} onBlur={() => markFieldTouched(k)} onChange={(newV) => { - setState({ ...state, [k]: newV }); - markFieldTouched(k); + setState({ ...state, [k]: newV }) + markFieldTouched(k) }} value={currentLicenseLabel || state[k]} // Use the label for display, or the value if not found options={options} @@ -560,7 +558,7 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
}
- ); + ) } // New condition for 'mtype class id' @@ -568,11 +566,11 @@ export function JSONSchemaForm({ const options = MTYPE_CLASSES.map((mtype) => ({ label: mtype.mtype_pref_label, value: mtype.mtype_id, - })); + })) const currentMtypeLabel = MTYPE_CLASSES.find( - (mtype) => mtype.mtype_id === state[k] - )?.mtype_pref_label; + (mtype) => mtype.mtype_id === state[k], + )?.mtype_pref_label return (
@@ -581,8 +579,8 @@ export function JSONSchemaForm({ className={`w-full ${hasError ? 'border-red-500' : ''}`} onBlur={() => markFieldTouched(k)} onChange={(newV) => { - setState({ ...state, [k]: newV }); - markFieldTouched(k); + setState({ ...state, [k]: newV }) + markFieldTouched(k) }} value={currentMtypeLabel || state[k]} options={options} @@ -590,21 +588,21 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
}
- ); + ) } - if (k === 'circuit' && circuit) return ; + if (k === 'circuit' && circuit) return if (v.is_block_reference && v.properties && typeof v.properties.type.const === 'string') { - const referenceKey = referenceTypesToConfigKeys[v.properties.type.const]; - const referenceTitle = referenceTypesToTitles[v.properties.type.const]; - if (!referenceKey) return null; - const referenceConfig = config[referenceKey]; - if (!isPlainObject(referenceConfig)) return null; + const referenceKey = referenceTypesToConfigKeys[v.properties.type.const] + const referenceTitle = referenceTypesToTitles[v.properties.type.const] + if (!referenceKey) return null + const referenceConfig = config[referenceKey] + if (!isPlainObject(referenceConfig)) return null const referees = Object.entries(referenceConfig).filter(([, val]) => { - return isPlainObject(val); - }); + return isPlainObject(val) + }) if (referees.length === 0) { return ( @@ -614,13 +612,13 @@ export function JSONSchemaForm({ {fieldError &&
{fieldError}
}
- ); + ) } const defaultV = isPlainObject(state[k]) && typeof state[k].block_name === 'string' ? state[k].block_name - : null; + : null return (
@@ -630,7 +628,7 @@ export function JSONSchemaForm({ onBlur={() => markFieldTouched(k)} onChange={(newV: string) => { if (!v.properties?.type.const || typeof v.properties.type.const !== 'string') - throw new Error('Invalid reference definition'); + throw new Error('Invalid reference definition') setState({ ...state, @@ -639,8 +637,8 @@ export function JSONSchemaForm({ type: v.properties.type.const, block_dict_name: referenceKey, }, - }); - markFieldTouched(k); + }) + markFieldTouched(k) }} value={defaultV} options={referees.map(([subkey]) => ({ @@ -650,7 +648,7 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
}
- ); + ) } if (k === 'neuron_ids') { @@ -666,16 +664,16 @@ export function JSONSchemaForm({ {!disabled && ( { - if (!isPlainObject(state[k]) || !Array.isArray(state[k].elements)) return; + if (!isPlainObject(state[k]) || !Array.isArray(state[k].elements)) return if (state[k].elements.length === 1) { - setState({ ...state, [k]: null }); - markFieldTouched(k); - return; + setState({ ...state, [k]: null }) + markFieldTouched(k) + return } - const newElements = [...state[k].elements]; - newElements.splice(newElements.indexOf(e), 1); + const newElements = [...state[k].elements] + newElements.splice(newElements.indexOf(e), 1) setState({ ...state, [k]: { @@ -683,8 +681,8 @@ export function JSONSchemaForm({ name: 'example_id_neuron_set', elements: newElements, }, - }); - markFieldTouched(k); + }) + markFieldTouched(k) }} /> )} @@ -704,7 +702,7 @@ export function JSONSchemaForm({ step={1} min={0} onChange={(newV) => { - setNewElement({ ...newElement, [k]: newV }); + setNewElement({ ...newElement, [k]: newV }) }} /> {!isNil(newElement[k]) && ( @@ -719,7 +717,7 @@ export function JSONSchemaForm({ name: 'example_id_neuron_set', elements: [newElement[k]], }, - }); + }) } else if (isPlainObject(state[k]) && Array.isArray(state[k].elements)) { setState({ ...state, @@ -728,18 +726,18 @@ export function JSONSchemaForm({ name: 'example_id_neuron_set', elements: [...state[k].elements, newElement[k]], }, - }); + }) } - setAddingElement({ ...addingElement, [k]: false }); - setNewElement({ ...newElement, [k]: null }); - markFieldTouched(k); + setAddingElement({ ...addingElement, [k]: false }) + setNewElement({ ...newElement, [k]: null }) + markFieldTouched(k) }} /> )} { - setAddingElement({ ...addingElement, [k]: false }); - setNewElement({ ...newElement, [k]: null }); + setAddingElement({ ...addingElement, [k]: false }) + setNewElement({ ...newElement, [k]: null }) }} className="text-primary-8" /> @@ -748,7 +746,7 @@ export function JSONSchemaForm({
{fieldError &&
{fieldError}
}
- ); + ) } if (obj.enum) { @@ -759,8 +757,8 @@ export function JSONSchemaForm({ className={`w-full ${hasError ? 'border-red-500' : ''}`} onBlur={() => markFieldTouched(k)} onChange={(newV) => { - setState({ ...state, [k]: newV }); - markFieldTouched(k); + setState({ ...state, [k]: newV }) + markFieldTouched(k) }} value={state[k]} options={obj.enum.map((subv: string) => ({ @@ -771,7 +769,7 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
}
- ); + ) } if (obj.type === 'number' || obj.type === 'integer') { @@ -783,14 +781,14 @@ export function JSONSchemaForm({ disabled={disabled} value={typeof state[k] === 'number' ? state[k] : null} onChange={(value) => { - setState({ ...state, [k]: value }); + setState({ ...state, [k]: value }) }} onBlur={() => markFieldTouched(k)} className={`w-full ${hasError ? 'border-red-500' : ''}`} /> {fieldError &&
{fieldError}
}
- ); + ) } if (obj.type === 'string') { @@ -802,12 +800,12 @@ export function JSONSchemaForm({ onBlur={() => markFieldTouched(k)} value={typeof state[k] === 'string' ? state[k] : ''} onChange={(e) => { - setState({ ...state, [k]: e.currentTarget.value }); + setState({ ...state, [k]: e.currentTarget.value }) }} /> {fieldError &&
{fieldError}
}
- ); + ) } return ( @@ -818,22 +816,22 @@ export function JSONSchemaForm({ onBlur={() => markFieldTouched(k)} value={typeof state[k] === 'string' ? state[k] : ''} onChange={(e) => { - setState({ ...state, [k]: e.currentTarget.value }); + setState({ ...state, [k]: e.currentTarget.value }) }} placeholder={`Enter value for ${v.title || k}`} /> {fieldError &&
{fieldError}
}
- ); + ) } function getFieldTitle(k: string, v: JSONSchema, normalizedKey: string): string { if (normalizedKey === 'strainid' || normalizedKey === 'strain_id' || normalizedKey === 'strain') - return 'STRAIN'; - if (normalizedKey === 'ageperiod' || normalizedKey === 'age_period') return 'AGE PERIOD'; - if (normalizedKey === 'licenseid' || normalizedKey === 'license_id') return 'LICENSE'; - if (normalizedKey === 'mtypeclassid') return 'MTYPE CLASS'; - return v.title || k; + return 'STRAIN' + if (normalizedKey === 'ageperiod' || normalizedKey === 'age_period') return 'AGE PERIOD' + if (normalizedKey === 'licenseid' || normalizedKey === 'license_id') return 'LICENSE' + if (normalizedKey === 'mtypeclassid') return 'MTYPE CLASS' + return v.title || k } return ( @@ -859,12 +857,12 @@ export function JSONSchemaForm({ {schema.properties && Object.entries(schema.properties) .filter(([k]) => { - return !skip.includes(k); + return !skip.includes(k) }) .map(([k, v]) => { - const isRequired = schema.required?.includes(k); - const normalizedKey = k.toLowerCase().replace(/[\s_]/g, ''); - const fieldTitle = getFieldTitle(k, v, normalizedKey); + const isRequired = schema.required?.includes(k) + const normalizedKey = k.toLowerCase().replace(/[\s_]/g, '') + const fieldTitle = getFieldTitle(k, v, normalizedKey) return (
@@ -872,7 +870,7 @@ export function JSONSchemaForm({
@@ -883,11 +881,11 @@ export function JSONSchemaForm({
{renderInput(k, v)}
- ); + ) })}
- ); + ) } export function Tab({ @@ -899,13 +897,13 @@ export function Tab({ extraClass, disabled, }: { - tab: string; - selectedTab: string; - onClick?: () => void; - rounded?: 'rounded-l-full' | 'rounded-r-full' | 'rounded-full'; - children?: React.ReactNode; - extraClass?: string; - disabled?: boolean; + tab: string + selectedTab: string + onClick?: () => void + rounded?: 'rounded-l-full' | 'rounded-r-full' | 'rounded-full' + children?: React.ReactNode + extraClass?: string + disabled?: boolean }) { return ( - ); + ) } export function Chevron({ rotate }: { rotate?: number }) { @@ -945,5 +943,5 @@ export function Chevron({ rotate }: { rotate?: number }) { strokeLinejoin="round" /> - ); + ) } diff --git a/src/page-wrappers/build/me-model/configure.tsx b/src/page-wrappers/build/me-model/configure.tsx index 12a9631f1..7ffb859cd 100644 --- a/src/page-wrappers/build/me-model/configure.tsx +++ b/src/page-wrappers/build/me-model/configure.tsx @@ -1,56 +1,56 @@ -'use client'; +'use client' -import { App, Button } from 'antd'; -import { useAtomValue, useSetAtom } from 'jotai'; -import get from 'lodash/get'; -import omit from 'lodash/omit'; -import { useRouter } from 'next/navigation'; -import { useTransition } from 'react'; -import z from 'zod'; +import { App, Button } from 'antd' +import { useAtomValue, useSetAtom } from 'jotai' +import get from 'lodash/get' +import omit from 'lodash/omit' +import { useRouter } from 'next/navigation' +import { useTransition } from 'react' +import z from 'zod' -import EModelOverviewCard from '@/features/entities/me-model/detail-view/card-viewers/emodel-overview-card'; -import MorphologyOverviewCard from '@/features/entities/me-model/detail-view/card-viewers/morphology-overview-card'; +import EModelOverviewCard from '@/features/entities/me-model/detail-view/card-viewers/emodel-overview-card' +import MorphologyOverviewCard from '@/features/entities/me-model/detail-view/card-viewers/morphology-overview-card' // import CustomButton from '@/components/buttons/custom-btn'; -import { createMEModel } from '@/api/entitycore/queries'; -import { CreateMEModelSchema, ValidationStatus } from '@/api/entitycore/types/entities/me-model'; -import { tryCatch } from '@/api/utils'; -import { DataType } from '@/constants/explore-section/list-views'; -import { renderArray, renderEmptyOrValue } from '@/entity-configuration/definitions/renderer'; -import { MEmodel } from '@/entity-configuration/domain/model/me-model'; -import { activityAtomFamily } from '@/features/activity-view/context'; -import { useBuildMeModelSessionState } from '@/features/entities/me-model/build/create.state-session'; -import { messages } from '@/i18n/en/me-model'; -import { OneshotSession } from '@/services/accounting'; -import { useEntitiesCountAtom } from '@/services/entitycore/entities-count'; -import { runSingleNeuronAnalysis } from '@/services/small-scale-simulator'; -import { useRefreshDataAtom } from '@/state/explore-section/list-view-atoms'; -import { virtualLabProjectUsersAtomFamily } from '@/state/virtual-lab/projects'; -import { ServiceSubtype } from '@/types/accounting'; -import { WorkspaceContextSchema } from '@/types/common'; -import { classNames } from '@/util/utils'; -import { resolveDataKey } from '@/utils/key-builder'; -import { resolveExploreDetailsPageUrl } from '@/utils/url-builder'; +import { createMEModel } from '@/api/entitycore/queries' +import { CreateMEModelSchema, ValidationStatus } from '@/api/entitycore/types/entities/me-model' +import { tryCatch } from '@/api/utils' +import { DataType } from '@/constants/explore-section/list-views' +import { renderArray, renderEmptyOrValue } from '@/entity-configuration/definitions/renderer' +import { MEmodel } from '@/entity-configuration/domain/model/me-model' +import { activityAtomFamily } from '@/features/activity-view/context' +import { useBuildMeModelSessionState } from '@/features/entities/me-model/build/create.state-session' +import { messages } from '@/i18n/en/me-model' +import { OneshotSession } from '@/services/accounting' +import { useEntitiesCountAtom } from '@/services/entitycore/entities-count' +import { runSingleNeuronAnalysis } from '@/services/small-scale-simulator' +import { useRefreshDataAtom } from '@/state/explore-section/list-view-atoms' +import { virtualLabProjectUsersAtomFamily } from '@/state/virtual-lab/projects' +import { ServiceSubtype } from '@/types/accounting' +import { WorkspaceContextSchema } from '@/types/common' +import { classNames } from '@/util/utils' +import { resolveDataKey } from '@/utils/key-builder' +import { resolveExploreDetailsPageUrl } from '@/utils/url-builder' -import type { IMEModel } from '@/api/entitycore/types/entities/me-model'; -import type { WorkspaceContext } from '@/types/common'; +import type { IMEModel } from '@/api/entitycore/types/entities/me-model' +import type { WorkspaceContext } from '@/types/common' -const LOW_FUNDS_ERROR_CODE = 'INSUFFICIENT_FUNDS'; +const LOW_FUNDS_ERROR_CODE = 'INSUFFICIENT_FUNDS' -const CreateMeModelContextSchema = CreateMEModelSchema.merge(WorkspaceContextSchema); -type TCreateMeModelContext = z.infer; +const CreateMeModelContextSchema = CreateMEModelSchema.merge(WorkspaceContextSchema) +type TCreateMeModelContext = z.infer function Header({ stateId, virtualLabId, projectId }: WorkspaceContext & { stateId: string }) { const { sessionValue } = useBuildMeModelSessionState({ stateId, virtualLabId, projectId, - }); + }) const contributors = useAtomValue(virtualLabProjectUsersAtomFamily({ projectId, virtualLabId })) - ?.data?.users; - const { mmodel } = sessionValue; - const { emodel } = sessionValue; + ?.data?.users + const { mmodel } = sessionValue + const { emodel } = sessionValue const fields = [ { className: 'col-span-6', @@ -88,7 +88,7 @@ function Header({ stateId, virtualLabId, projectId }: WorkspaceContext & { state title: 'e-type', value: renderEmptyOrValue(renderArray(emodel?.etypes?.map((m) => m.pref_label) || [])), }, - ]; + ] return (
@@ -99,17 +99,17 @@ function Header({ stateId, virtualLabId, projectId }: WorkspaceContext & { state
))}
- ); + ) } type Props = { - ctx: WorkspaceContext; + ctx: WorkspaceContext searchParams: { - s: string; - m: string; - e: string; - }; -}; + s: string + m: string + e: string + } +} function CustomButton({ loading, @@ -118,11 +118,11 @@ function CustomButton({ onClick, children, }: { - loading: boolean; - disable: boolean; - className?: string; - onClick?: () => void; - children?: React.ReactNode; + loading: boolean + disable: boolean + className?: string + onClick?: () => void + children?: React.ReactNode }) { return ( - ); + ) } export default function Configure({ ctx, searchParams }: Props) { - const { push: navigate } = useRouter(); - const { notification } = App.useApp(); - const [isPending, startTransition] = useTransition(); - const emodelId = get(searchParams, 'e', undefined); - const morphologyId = get(searchParams, 'm', undefined); - const stateId = get(searchParams, 's', undefined); + const { push: navigate } = useRouter() + const { notification } = App.useApp() + const [isPending, startTransition] = useTransition() + const emodelId = get(searchParams, 'e', undefined) + const morphologyId = get(searchParams, 'm', undefined) + const stateId = get(searchParams, 's', undefined) const exploreDataKey = resolveDataKey({ projectId: ctx.projectId, section: 'explore', entity: MEmodel, - }); + }) - const refreshEntityCountsToParent = useEntitiesCountAtom(); - const refreshDataAtom = useRefreshDataAtom(exploreDataKey); + const refreshEntityCountsToParent = useEntitiesCountAtom() + const refreshDataAtom = useRefreshDataAtom(exploreDataKey) const refreshActivityAtom = useSetAtom( activityAtomFamily({ key: resolveDataKey({ @@ -171,33 +171,33 @@ export default function Configure({ ctx, searchParams }: Props) { projectId: ctx.projectId, virtualLabId: ctx.virtualLabId, type: 'memodel', - }) - ); + }), + ) const { sessionValue } = useBuildMeModelSessionState({ stateId: stateId || '', virtualLabId: ctx.virtualLabId, projectId: ctx.projectId, - }); + }) if (!stateId) { - navigate('./'); - return; + navigate('./') + return } const showErrorNotification = (error: any, type: 'validation' | 'http') => { - let message = messages.DefaultErrorMsg; + let message = messages.DefaultErrorMsg if (type === 'http') message = error?.cause?.error_code === LOW_FUNDS_ERROR_CODE ? messages.LowFundsError - : messages.DefaultErrorMsg; - else message = messages.ValidationError; + : messages.DefaultErrorMsg + else message = messages.ValidationError notification.error({ duration: 10, message, - }); - }; + }) + } const buildMeModel = async () => { const body: Partial = { @@ -211,26 +211,26 @@ export default function Configure({ ctx, searchParams }: Props) { brain_region_id: sessionValue.mmodel?.brain_region.id ?? sessionValue.brainRegion?.id, strain_id: sessionValue.mmodel?.strain?.id ?? null, validation_status: ValidationStatus.Initialized, - }; + } const { error: validationError, data: validationData } = - await CreateMeModelContextSchema.safeParseAsync(body); + await CreateMeModelContextSchema.safeParseAsync(body) if (validationError) { - showErrorNotification(validationError, 'validation'); - return { data: null, error: validationError, errorType: 'validation' as const }; + showErrorNotification(validationError, 'validation') + return { data: null, error: validationError, errorType: 'validation' as const } } const accountingSession = new OneshotSession({ subtype: ServiceSubtype.SingleCellBuild, virtualLabId: ctx.virtualLabId, projectId: ctx.projectId, count: 1, - }); + }) const { data, error } = await tryCatch( accountingSession.useWith(() => createMEModel({ body: omit(validationData, ['virtualLabId', 'projectId']), context: ctx, - }) + }), ), undefined, { @@ -240,39 +240,39 @@ export default function Configure({ ctx, searchParams }: Props) { virtualLabId: ctx.virtualLabId, projectId: ctx.projectId, }, - } - ); + }, + ) - return { data, error, errorType: 'http' as const }; - }; + return { data, error, errorType: 'http' as const } + } const onClick = async () => { startTransition(async () => { - const { data, error, errorType } = await buildMeModel(); + const { data, error, errorType } = await buildMeModel() if (error || !data) { - showErrorNotification(error, errorType); - return; + showErrorNotification(error, errorType) + return } try { - await runSingleNeuronAnalysis({ ctx, modelId: data.id }); + await runSingleNeuronAnalysis({ ctx, modelId: data.id }) } catch (runAnalysisError) { - const message = messages.RunAnalysisError; - notification.error({ message, duration: 20 }); + const message = messages.RunAnalysisError + notification.error({ message, duration: 20 }) } - refreshDataAtom(); - refreshActivityAtom(); - refreshEntityCountsToParent(data.brain_region.id); + refreshDataAtom() + refreshActivityAtom() + refreshEntityCountsToParent(data.brain_region.id) navigate( resolveExploreDetailsPageUrl({ ctx, dataType: DataType.CircuitMEModel, entityId: data.id, - }) - ); - }); - }; + }), + ) + }) + } const validateTrigger = sessionValue.emodel && sessionValue.mmodel && (
@@ -280,7 +280,7 @@ export default function Configure({ ctx, searchParams }: Props) { {isPending ? 'Creating ME-model' : 'Save'}
- ); + ) return ( <> @@ -307,5 +307,5 @@ export default function Configure({ ctx, searchParams }: Props) {
{validateTrigger} - ); + ) } From 9437077cc677ba439274714d16d1004fa16ae8bf Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 17:17:31 +0200 Subject: [PATCH 12/19] more linting --- .../contribute/_components/components.tsx | 458 +++++++++--------- .../build/me-model/configure.tsx | 194 ++++---- 2 files changed, 327 insertions(+), 325 deletions(-) diff --git a/src/features/contribute/_components/components.tsx b/src/features/contribute/_components/components.tsx index 2413d293b..cbf36e002 100644 --- a/src/features/contribute/_components/components.tsx +++ b/src/features/contribute/_components/components.tsx @@ -1,25 +1,25 @@ -import { useEffect, useState, useRef } from 'react' -import { atom, useAtom } from 'jotai' -import { InputNumber, Input, Select, Button } from 'antd' -import { CheckCircleOutlined, CloseCircleOutlined, PlusCircleOutlined } from '@ant-design/icons' -import isNil from 'lodash/isNil' +import { useEffect, useState, useRef } from 'react'; +import { atom, useAtom } from 'jotai'; +import { InputNumber, Input, Select, Button } from 'antd'; +import { CheckCircleOutlined, CloseCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import isNil from 'lodash/isNil'; -import { JSONSchema } from '../types' -import { isPlainObject } from './utils' -import CircuitDetails from './circuit-details' -import Tooltip from './tooltip' +import { JSONSchema } from '../types'; +import { isPlainObject } from './utils'; +import CircuitDetails from './circuit-details'; +import Tooltip from './tooltip'; -import { ICircuit } from '@/api/entitycore/types/entities/circuit' -import { classNames } from '@/util/utils' +import { ICircuit } from '@/api/entitycore/types/entities/circuit'; +import { classNames } from '@/util/utils'; -type Primitive = null | boolean | number | string +type Primitive = null | boolean | number | string; interface Object { - [key: string]: Primitive | Primitive[] | Object + [key: string]: Primitive | Primitive[] | Object; } -export type ConfigValue = Primitive | Primitive[] | Object +export type ConfigValue = Primitive | Primitive[] | Object; -export type Config = Record +export type Config = Record; // Updated structure for MTYPE classification const MTYPE_CLASSES = [ @@ -57,35 +57,35 @@ const MTYPE_CLASSES = [ { mtype_pref_label: 'L4_UPC', mtype_id: '2ef7e0b5-39e4-441b-a72a-c7186afa7f5c' }, { mtype_pref_label: 'L56_PC', mtype_id: '629d6d6f-93f9-43d8-8a99-277740fd8f22' }, { mtype_pref_label: 'L5_BP', mtype_id: '7b16c860-ae76-4ddf-b093-4e28620b3712' }, -] +]; // Helper function to check if a field value is considered "empty" or invalid const isEmptyValue = (value: ConfigValue): boolean => { - if (isNil(value) || value === '') return true - if (Array.isArray(value) && value.length === 0) return true - if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) return true - return false -} + if (isNil(value) || value === '') return true; + if (Array.isArray(value) && value.length === 0) return true; + if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) return true; + return false; +}; // Helper function to validate required fields const getRequiredFieldErrors = ( state: Record, - schema: JSONSchema, + schema: JSONSchema ): string[] => { - const errors: string[] = [] - const requiredFields = schema.required || [] + const errors: string[] = []; + const requiredFields = schema.required || []; requiredFields.forEach((fieldName) => { - const value = state[fieldName] + const value = state[fieldName]; if (isEmptyValue(value)) { - const fieldSchema = schema.properties?.[fieldName] - const fieldTitle = fieldSchema?.title || fieldName - errors.push(`${fieldTitle} is required`) + const fieldSchema = schema.properties?.[fieldName]; + const fieldTitle = fieldSchema?.title || fieldName; + errors.push(`${fieldTitle} is required`); } - }) + }); - return errors -} + return errors; +}; export function JSONSchemaForm({ disabled, @@ -98,170 +98,172 @@ export function JSONSchemaForm({ currentCategory, onValidationChange, }: { - disabled: boolean - config: Config - schema: JSONSchema - circuit: ICircuit | undefined | null - stateAtom: ReturnType> - onAddReferenceClick: (reference: string) => void - nodeId?: string - currentCategory?: string - onValidationChange?: (isValid: boolean, errors: string[]) => void + disabled: boolean; + config: Config; + schema: JSONSchema; + circuit: ICircuit | undefined | null; + stateAtom: ReturnType>; + onAddReferenceClick: (reference: string) => void; + nodeId?: string; + currentCategory?: string; + onValidationChange?: (isValid: boolean, errors: string[]) => void; }) { - const skip = ['type'] + const skip = ['type']; - const [state, setState] = useAtom(stateAtom) + const [state, setState] = useAtom(stateAtom); const [addingElement, setAddingElement] = useState<{ [key: string]: boolean }>({ legacy_id: true, - }) - const [newElement, setNewElement] = useState<{ [key: string]: number | string | null }>({}) - const [validationErrors, setValidationErrors] = useState([]) - const [touchedFields, setTouchedFields] = useState>(new Set()) + }); + const [newElement, setNewElement] = useState<{ [key: string]: number | string | null }>({}); + const [validationErrors, setValidationErrors] = useState([]); + const [touchedFields, setTouchedFields] = useState>(new Set()); const referenceTypesToConfigKeys: Record = { NeuronSetReference: 'neuron_sets', TimestampsReference: 'timestamps', - } + }; const referenceTypesToTitles: Record = { NeuronSetReference: 'Neuron Set', TimestampsReference: 'Timestamps', - } + }; // Callback ref to avoid dependency issues - const onValidationChangeRef = useRef(onValidationChange) + const onValidationChangeRef = useRef(onValidationChange); useEffect(() => { - onValidationChangeRef.current = onValidationChange - }) + onValidationChangeRef.current = onValidationChange; + }); // Validate form whenever state changes useEffect(() => { - const errors = getRequiredFieldErrors(state, schema) + const errors = getRequiredFieldErrors(state, schema); // Only update if errors actually changed setValidationErrors((prevErrors) => { const errorsChanged = prevErrors.length !== errors.length || - prevErrors.some((error, index) => error !== errors[index]) + prevErrors.some((error, index) => error !== errors[index]); if (errorsChanged && onValidationChangeRef.current) { - onValidationChangeRef.current(errors.length === 0, errors) + onValidationChangeRef.current(errors.length === 0, errors); } - return errorsChanged ? errors : prevErrors - }) - }, [state, schema]) + return errorsChanged ? errors : prevErrors; + }); + }, [state, schema]); useEffect(() => { - if (!schema.properties) return + if (!schema.properties) return; - const initial: Record = {} + const initial: Record = {}; Object.entries(schema.properties).forEach(([key, value]) => { - if (key === 'type') initial[key] = value.const ?? null - else if (key === 'legacy_id') initial[key] = [] - else if (key === 'strain_id') initial[key] = null + if (key === 'type') initial[key] = value.const ?? null; + else if (key === 'legacy_id') initial[key] = []; + else if (key === 'strain_id') initial[key] = null; else if (key === 'license_id') - initial[key] = 'c268a20e-b78a-4332-a5e1-38e26c4454b9' // Default to undefined UUID - else initial[key] = value.default ?? null - }) + initial[key] = 'c268a20e-b78a-4332-a5e1-38e26c4454b9'; // Default to undefined UUID + else initial[key] = value.default ?? null; + }); // Auto-populate brain region id if we're in morphology category if (currentCategory === 'morphology' && nodeId) { const brainRegionIdKey = Object.keys(schema.properties || {}).find((key) => { - const normalizedKey = key.toLowerCase().replace(/[\s_]/g, '') + const normalizedKey = key.toLowerCase().replace(/[\s_]/g, ''); return ( normalizedKey === 'brainregionid' || normalizedKey === 'brain_region_id' || normalizedKey === 'brainregion' - ) - }) + ); + }); if (brainRegionIdKey) { - initial[brainRegionIdKey] = nodeId + initial[brainRegionIdKey] = nodeId; } } // Auto-populate species id (hardcoded mouse species ID) const speciesIdKey = Object.keys(schema.properties || {}).find((key) => { - const normalizedKey = key.toLowerCase().replace(/[\s_]/g, '') + const normalizedKey = key.toLowerCase().replace(/[\s_]/g, ''); return ( normalizedKey === 'speciesid' || normalizedKey === 'species_id' || normalizedKey === 'species' - ) - }) + ); + }); if (speciesIdKey) { - initial[speciesIdKey] = 'b7ad4cca-4ac2-4095-9781-37fb68fe9ca1' + initial[speciesIdKey] = 'b7ad4cca-4ac2-4095-9781-37fb68fe9ca1'; } // Auto-populate atlas id const atlasIdKey = Object.keys(schema.properties || {}).find((key) => { - const normalizedKey = key.toLowerCase().replace(/[\s_]/g, '') + const normalizedKey = key.toLowerCase().replace(/[\s_]/g, ''); return ( normalizedKey === 'atlasid' || normalizedKey === 'atlas_id' || normalizedKey === 'atlas' - ) - }) + ); + }); if (atlasIdKey) { - initial[atlasIdKey] = 'e3e70682-c209-4cac-a29f-6fbed82c07cd' + initial[atlasIdKey] = 'e3e70682-c209-4cac-a29f-6fbed82c07cd'; } - setState((prev) => ({ ...initial, ...prev })) - }, [stateAtom, setState, schema.properties, nodeId, currentCategory]) + setState((prev) => ({ ...initial, ...prev })); + }, [stateAtom, setState, schema.properties, nodeId, currentCategory]); // Helper function to mark field as touched const markFieldTouched = (fieldName: string) => { - setTouchedFields((prev) => new Set(prev).add(fieldName)) - } + setTouchedFields((prev) => new Set(prev).add(fieldName)); + }; // Helper function to check if a field has an error const hasFieldError = (fieldName: string): boolean => { - const isRequired = schema.required?.includes(fieldName) - const isTouched = touchedFields.has(fieldName) - const isEmpty = isEmptyValue(state[fieldName]) - return isRequired && isTouched && isEmpty - } + const isRequired = schema.required?.includes(fieldName); + const isTouched = touchedFields.has(fieldName); + const isEmpty = isEmptyValue(state[fieldName]); + return isRequired && isTouched && isEmpty; + }; // Helper function to get field error message const getFieldErrorMessage = (fieldName: string): string | null => { - if (!hasFieldError(fieldName)) return null - const fieldSchema = schema.properties?.[fieldName] - const fieldTitle = fieldSchema?.title || fieldName - return `${fieldTitle} is required` - } + if (!hasFieldError(fieldName)) return null; + const fieldSchema = schema.properties?.[fieldName]; + const fieldTitle = fieldSchema?.title || fieldName; + return `${fieldTitle} is required`; + }; function renderInput(k: string, v: JSONSchema) { const obj = { ...v, ...v.anyOf?.find((subv) => subv.type !== 'array' && subv.type !== 'null'), - } - const normalizedKey = k.toLowerCase().replace(/[\s_]/g, '') + }; + const normalizedKey = k.toLowerCase().replace(/[\s_]/g, ''); - const fieldError = getFieldErrorMessage(k) - const hasError = hasFieldError(k) + const fieldError = getFieldErrorMessage(k); + const hasError = hasFieldError(k); const isBrainRegionIdField = currentCategory === 'morphology' && (normalizedKey === 'brainregionid' || normalizedKey === 'brain_region_id' || - normalizedKey === 'brainregion') + normalizedKey === 'brainregion'); const isSpeciesIdField = - normalizedKey === 'speciesid' || normalizedKey === 'species_id' || normalizedKey === 'species' + normalizedKey === 'speciesid' || + normalizedKey === 'species_id' || + normalizedKey === 'species'; const isAtlasIdField = - normalizedKey === 'atlasid' || normalizedKey === 'atlas_id' || normalizedKey === 'atlas' + normalizedKey === 'atlasid' || normalizedKey === 'atlas_id' || normalizedKey === 'atlas'; const isExperimentDateField = normalizedKey === 'experimentdate' || normalizedKey === 'experiment_date' || normalizedKey === 'date' || - v.title?.toLowerCase().includes('date') + v.title?.toLowerCase().includes('date'); const isStrainIdField = - normalizedKey === 'strainid' || normalizedKey === 'strain_id' || normalizedKey === 'strain' - const isAgePeriodField = normalizedKey === 'ageperiod' || normalizedKey === 'age_period' - const isLegacyIdField = normalizedKey === 'legacyid' || normalizedKey === 'legacy_id' - const isLicenseIdField = normalizedKey === 'licenseid' || normalizedKey === 'license_id' - const isMtypeClassIdField = normalizedKey === 'mtypeclassid' + normalizedKey === 'strainid' || normalizedKey === 'strain_id' || normalizedKey === 'strain'; + const isAgePeriodField = normalizedKey === 'ageperiod' || normalizedKey === 'age_period'; + const isLegacyIdField = normalizedKey === 'legacyid' || normalizedKey === 'legacy_id'; + const isLicenseIdField = normalizedKey === 'licenseid' || normalizedKey === 'license_id'; + const isMtypeClassIdField = normalizedKey === 'mtypeclassid'; if (isBrainRegionIdField && nodeId) { return ( @@ -274,7 +276,7 @@ export function JSONSchemaForm({ readOnly />
- ) + ); } if (isSpeciesIdField) { @@ -288,7 +290,7 @@ export function JSONSchemaForm({ readOnly />
- ) + ); } if (isAtlasIdField) { @@ -302,27 +304,27 @@ export function JSONSchemaForm({ readOnly />
- ) + ); } if (isExperimentDateField) { const formatDate = (value: string) => { - const cleaned = value.replace(/[^\d\s-]/g, '') - const parts = cleaned.split(/[\s-]+/).filter((part) => part.length > 0) - if (parts.length === 0) return '' - if (parts.length === 1) return parts[0] - if (parts.length === 2) return `${parts[0]} ${parts[1]}` - return parts.slice(0, 3).join(' ') - } + const cleaned = value.replace(/[^\d\s-]/g, ''); + const parts = cleaned.split(/[\s-]+/).filter((part) => part.length > 0); + if (parts.length === 0) return ''; + if (parts.length === 1) return parts[0]; + if (parts.length === 2) return `${parts[0]} ${parts[1]}`; + return parts.slice(0, 3).join(' '); + }; const validateDateFormat = (value: string) => { - if (!value) return true - const parts = value.split(' ') - if (parts.length !== 3) return false - const [day, month, year] = parts - const dayNum = parseInt(day, 10) - const monthNum = parseInt(month, 10) - const yearNum = parseInt(year, 10) + if (!value) return true; + const parts = value.split(' '); + if (parts.length !== 3) return false; + const [day, month, year] = parts; + const dayNum = parseInt(day, 10); + const monthNum = parseInt(month, 10); + const yearNum = parseInt(year, 10); return ( dayNum >= 1 && dayNum <= 31 && @@ -330,12 +332,12 @@ export function JSONSchemaForm({ monthNum <= 12 && yearNum >= 1900 && yearNum <= new Date().getFullYear() - ) - } + ); + }; - const currentValue = typeof state[k] === 'string' ? state[k] : '' - const isValid = validateDateFormat(currentValue) - const showDateError = !isValid && currentValue + const currentValue = typeof state[k] === 'string' ? state[k] : ''; + const isValid = validateDateFormat(currentValue); + const showDateError = !isValid && currentValue; return (
@@ -345,8 +347,8 @@ export function JSONSchemaForm({ value={currentValue} className={`w-full ${hasError || showDateError ? 'border-red-500' : ''}`} onChange={(e) => { - const formatted = formatDate(e.currentTarget.value) - setState({ ...state, [k]: formatted }) + const formatted = formatDate(e.currentTarget.value); + setState({ ...state, [k]: formatted }); }} placeholder="DD MM YYYY (e.g., 15 03 2024)" /> @@ -357,7 +359,7 @@ export function JSONSchemaForm({ )} {fieldError &&
{fieldError}
}
- ) + ); } if (isStrainIdField) { @@ -377,10 +379,10 @@ export function JSONSchemaForm({ 'Sprague Dawley': '890e1234-e29b-41d4-a716-446655440003', }, }, - ] + ]; - const speciesId = 'b7ad4cca-4ac2-4095-9781-37fb68fe9ca1' - const selectedSpecies = allSpeciesStrains.find((s) => s.species_id === speciesId) + const speciesId = 'b7ad4cca-4ac2-4095-9781-37fb68fe9ca1'; + const selectedSpecies = allSpeciesStrains.find((s) => s.species_id === speciesId); if (!selectedSpecies) { return ( @@ -388,13 +390,13 @@ export function JSONSchemaForm({ {fieldError &&
{fieldError}
} - ) + ); } const strainOptions = Object.entries(selectedSpecies.strains).map(([name, id]) => ({ label: name, value: id, - })) + })); return (
@@ -403,8 +405,8 @@ export function JSONSchemaForm({ className={`w-full ${hasError ? 'border-red-500' : ''}`} onBlur={() => markFieldTouched(k)} onChange={(newV) => { - setState((prev) => ({ ...prev, [k]: newV })) - markFieldTouched(k) + setState((prev) => ({ ...prev, [k]: newV })); + markFieldTouched(k); }} value={state[k]} options={strainOptions} @@ -412,7 +414,7 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
}
- ) + ); } if (isAgePeriodField) { @@ -424,8 +426,8 @@ export function JSONSchemaForm({ className={`w-full ${hasError ? 'border-red-500' : ''}`} onBlur={() => markFieldTouched(k)} onChange={(newV) => { - setState({ ...state, [k]: newV }) - markFieldTouched(k) + setState({ ...state, [k]: newV }); + markFieldTouched(k); }} value={state[k]} options={obj.enum.map((subv: string) => ({ @@ -436,7 +438,7 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
} - ) + ); } return (
@@ -446,13 +448,13 @@ export function JSONSchemaForm({ onBlur={() => markFieldTouched(k)} value={typeof state[k] === 'string' ? state[k] : ''} onChange={(e) => { - setState({ ...state, [k]: e.currentTarget.value }) + setState({ ...state, [k]: e.currentTarget.value }); }} placeholder="Enter age period" /> {fieldError &&
{fieldError}
}
- ) + ); } if (isLegacyIdField) { @@ -467,10 +469,10 @@ export function JSONSchemaForm({ {!disabled && ( { - const newElements = [...(Array.isArray(state[k]) ? state[k] : [])] - newElements.splice(newElements.indexOf(e), 1) - setState({ ...state, [k]: newElements }) - markFieldTouched(k) + const newElements = [...(Array.isArray(state[k]) ? state[k] : [])]; + newElements.splice(newElements.indexOf(e), 1); + setState({ ...state, [k]: newElements }); + markFieldTouched(k); }} /> )} @@ -497,17 +499,17 @@ export function JSONSchemaForm({ setState({ ...state, [k]: [...(Array.isArray(state[k]) ? state[k] : []), newElement[k]], - }) - setAddingElement({ ...addingElement, [k]: false }) - setNewElement({ ...newElement, [k]: null }) - markFieldTouched(k) + }); + setAddingElement({ ...addingElement, [k]: false }); + setNewElement({ ...newElement, [k]: null }); + markFieldTouched(k); }} /> )} { - setAddingElement({ ...addingElement, [k]: false }) - setNewElement({ ...newElement, [k]: null }) + setAddingElement({ ...addingElement, [k]: false }); + setNewElement({ ...newElement, [k]: null }); }} className="text-primary-8" /> @@ -516,7 +518,7 @@ export function JSONSchemaForm({ {fieldError &&
{fieldError}
} - ) + ); } // New condition for 'license id' field @@ -530,17 +532,17 @@ export function JSONSchemaForm({ 'CC BY 4.0 Deed': 'ad8686db-3cdd-4e3f-bcbd-812380a9eba7', 'CC0 1.0 Deed': '74b8c953-67f5-4e95-ac58-095274901328', 'NGV_Data Licence_v1.0': '0c39107e-3b68-4f1f-904a-5c7f1b4f89c5', - } + }; const options = Object.entries(licenseOptions).map(([name, id]) => ({ label: name, value: id, - })) + })); // Find the key corresponding to the current state value to set the label const currentLicenseLabel = Object.keys(licenseOptions).find( - (key) => licenseOptions[key] === state[k], - ) + (key) => licenseOptions[key] === state[k] + ); return (
@@ -549,8 +551,8 @@ export function JSONSchemaForm({ className={`w-full ${hasError ? 'border-red-500' : ''}`} onBlur={() => markFieldTouched(k)} onChange={(newV) => { - setState({ ...state, [k]: newV }) - markFieldTouched(k) + setState({ ...state, [k]: newV }); + markFieldTouched(k); }} value={currentLicenseLabel || state[k]} // Use the label for display, or the value if not found options={options} @@ -558,7 +560,7 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
}
- ) + ); } // New condition for 'mtype class id' @@ -566,11 +568,11 @@ export function JSONSchemaForm({ const options = MTYPE_CLASSES.map((mtype) => ({ label: mtype.mtype_pref_label, value: mtype.mtype_id, - })) + })); const currentMtypeLabel = MTYPE_CLASSES.find( - (mtype) => mtype.mtype_id === state[k], - )?.mtype_pref_label + (mtype) => mtype.mtype_id === state[k] + )?.mtype_pref_label; return (
@@ -579,8 +581,8 @@ export function JSONSchemaForm({ className={`w-full ${hasError ? 'border-red-500' : ''}`} onBlur={() => markFieldTouched(k)} onChange={(newV) => { - setState({ ...state, [k]: newV }) - markFieldTouched(k) + setState({ ...state, [k]: newV }); + markFieldTouched(k); }} value={currentMtypeLabel || state[k]} options={options} @@ -588,21 +590,21 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
}
- ) + ); } - if (k === 'circuit' && circuit) return + if (k === 'circuit' && circuit) return ; if (v.is_block_reference && v.properties && typeof v.properties.type.const === 'string') { - const referenceKey = referenceTypesToConfigKeys[v.properties.type.const] - const referenceTitle = referenceTypesToTitles[v.properties.type.const] - if (!referenceKey) return null - const referenceConfig = config[referenceKey] - if (!isPlainObject(referenceConfig)) return null + const referenceKey = referenceTypesToConfigKeys[v.properties.type.const]; + const referenceTitle = referenceTypesToTitles[v.properties.type.const]; + if (!referenceKey) return null; + const referenceConfig = config[referenceKey]; + if (!isPlainObject(referenceConfig)) return null; const referees = Object.entries(referenceConfig).filter(([, val]) => { - return isPlainObject(val) - }) + return isPlainObject(val); + }); if (referees.length === 0) { return ( @@ -612,13 +614,13 @@ export function JSONSchemaForm({ {fieldError &&
{fieldError}
} - ) + ); } const defaultV = isPlainObject(state[k]) && typeof state[k].block_name === 'string' ? state[k].block_name - : null + : null; return (
@@ -628,7 +630,7 @@ export function JSONSchemaForm({ onBlur={() => markFieldTouched(k)} onChange={(newV: string) => { if (!v.properties?.type.const || typeof v.properties.type.const !== 'string') - throw new Error('Invalid reference definition') + throw new Error('Invalid reference definition'); setState({ ...state, @@ -637,8 +639,8 @@ export function JSONSchemaForm({ type: v.properties.type.const, block_dict_name: referenceKey, }, - }) - markFieldTouched(k) + }); + markFieldTouched(k); }} value={defaultV} options={referees.map(([subkey]) => ({ @@ -648,7 +650,7 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
}
- ) + ); } if (k === 'neuron_ids') { @@ -664,16 +666,16 @@ export function JSONSchemaForm({ {!disabled && ( { - if (!isPlainObject(state[k]) || !Array.isArray(state[k].elements)) return + if (!isPlainObject(state[k]) || !Array.isArray(state[k].elements)) return; if (state[k].elements.length === 1) { - setState({ ...state, [k]: null }) - markFieldTouched(k) - return + setState({ ...state, [k]: null }); + markFieldTouched(k); + return; } - const newElements = [...state[k].elements] - newElements.splice(newElements.indexOf(e), 1) + const newElements = [...state[k].elements]; + newElements.splice(newElements.indexOf(e), 1); setState({ ...state, [k]: { @@ -681,8 +683,8 @@ export function JSONSchemaForm({ name: 'example_id_neuron_set', elements: newElements, }, - }) - markFieldTouched(k) + }); + markFieldTouched(k); }} /> )} @@ -702,7 +704,7 @@ export function JSONSchemaForm({ step={1} min={0} onChange={(newV) => { - setNewElement({ ...newElement, [k]: newV }) + setNewElement({ ...newElement, [k]: newV }); }} /> {!isNil(newElement[k]) && ( @@ -717,7 +719,7 @@ export function JSONSchemaForm({ name: 'example_id_neuron_set', elements: [newElement[k]], }, - }) + }); } else if (isPlainObject(state[k]) && Array.isArray(state[k].elements)) { setState({ ...state, @@ -726,18 +728,18 @@ export function JSONSchemaForm({ name: 'example_id_neuron_set', elements: [...state[k].elements, newElement[k]], }, - }) + }); } - setAddingElement({ ...addingElement, [k]: false }) - setNewElement({ ...newElement, [k]: null }) - markFieldTouched(k) + setAddingElement({ ...addingElement, [k]: false }); + setNewElement({ ...newElement, [k]: null }); + markFieldTouched(k); }} /> )} { - setAddingElement({ ...addingElement, [k]: false }) - setNewElement({ ...newElement, [k]: null }) + setAddingElement({ ...addingElement, [k]: false }); + setNewElement({ ...newElement, [k]: null }); }} className="text-primary-8" /> @@ -746,7 +748,7 @@ export function JSONSchemaForm({ {fieldError &&
{fieldError}
} - ) + ); } if (obj.enum) { @@ -757,8 +759,8 @@ export function JSONSchemaForm({ className={`w-full ${hasError ? 'border-red-500' : ''}`} onBlur={() => markFieldTouched(k)} onChange={(newV) => { - setState({ ...state, [k]: newV }) - markFieldTouched(k) + setState({ ...state, [k]: newV }); + markFieldTouched(k); }} value={state[k]} options={obj.enum.map((subv: string) => ({ @@ -769,7 +771,7 @@ export function JSONSchemaForm({ /> {fieldError &&
{fieldError}
} - ) + ); } if (obj.type === 'number' || obj.type === 'integer') { @@ -781,14 +783,14 @@ export function JSONSchemaForm({ disabled={disabled} value={typeof state[k] === 'number' ? state[k] : null} onChange={(value) => { - setState({ ...state, [k]: value }) + setState({ ...state, [k]: value }); }} onBlur={() => markFieldTouched(k)} className={`w-full ${hasError ? 'border-red-500' : ''}`} /> {fieldError &&
{fieldError}
} - ) + ); } if (obj.type === 'string') { @@ -800,12 +802,12 @@ export function JSONSchemaForm({ onBlur={() => markFieldTouched(k)} value={typeof state[k] === 'string' ? state[k] : ''} onChange={(e) => { - setState({ ...state, [k]: e.currentTarget.value }) + setState({ ...state, [k]: e.currentTarget.value }); }} /> {fieldError &&
{fieldError}
} - ) + ); } return ( @@ -816,22 +818,22 @@ export function JSONSchemaForm({ onBlur={() => markFieldTouched(k)} value={typeof state[k] === 'string' ? state[k] : ''} onChange={(e) => { - setState({ ...state, [k]: e.currentTarget.value }) + setState({ ...state, [k]: e.currentTarget.value }); }} placeholder={`Enter value for ${v.title || k}`} /> {fieldError &&
{fieldError}
} - ) + ); } function getFieldTitle(k: string, v: JSONSchema, normalizedKey: string): string { if (normalizedKey === 'strainid' || normalizedKey === 'strain_id' || normalizedKey === 'strain') - return 'STRAIN' - if (normalizedKey === 'ageperiod' || normalizedKey === 'age_period') return 'AGE PERIOD' - if (normalizedKey === 'licenseid' || normalizedKey === 'license_id') return 'LICENSE' - if (normalizedKey === 'mtypeclassid') return 'MTYPE CLASS' - return v.title || k + return 'STRAIN'; + if (normalizedKey === 'ageperiod' || normalizedKey === 'age_period') return 'AGE PERIOD'; + if (normalizedKey === 'licenseid' || normalizedKey === 'license_id') return 'LICENSE'; + if (normalizedKey === 'mtypeclassid') return 'MTYPE CLASS'; + return v.title || k; } return ( @@ -857,12 +859,12 @@ export function JSONSchemaForm({ {schema.properties && Object.entries(schema.properties) .filter(([k]) => { - return !skip.includes(k) + return !skip.includes(k); }) .map(([k, v]) => { - const isRequired = schema.required?.includes(k) - const normalizedKey = k.toLowerCase().replace(/[\s_]/g, '') - const fieldTitle = getFieldTitle(k, v, normalizedKey) + const isRequired = schema.required?.includes(k); + const normalizedKey = k.toLowerCase().replace(/[\s_]/g, ''); + const fieldTitle = getFieldTitle(k, v, normalizedKey); return (
@@ -870,7 +872,7 @@ export function JSONSchemaForm({
@@ -881,11 +883,11 @@ export function JSONSchemaForm({
{renderInput(k, v)}
- ) + ); })} - ) + ); } export function Tab({ @@ -897,13 +899,13 @@ export function Tab({ extraClass, disabled, }: { - tab: string - selectedTab: string - onClick?: () => void - rounded?: 'rounded-l-full' | 'rounded-r-full' | 'rounded-full' - children?: React.ReactNode - extraClass?: string - disabled?: boolean + tab: string; + selectedTab: string; + onClick?: () => void; + rounded?: 'rounded-l-full' | 'rounded-r-full' | 'rounded-full'; + children?: React.ReactNode; + extraClass?: string; + disabled?: boolean; }) { return ( - ) + ); } export function Chevron({ rotate }: { rotate?: number }) { @@ -943,5 +945,5 @@ export function Chevron({ rotate }: { rotate?: number }) { strokeLinejoin="round" /> - ) + ); } diff --git a/src/page-wrappers/build/me-model/configure.tsx b/src/page-wrappers/build/me-model/configure.tsx index 7ffb859cd..12a9631f1 100644 --- a/src/page-wrappers/build/me-model/configure.tsx +++ b/src/page-wrappers/build/me-model/configure.tsx @@ -1,56 +1,56 @@ -'use client' +'use client'; -import { App, Button } from 'antd' -import { useAtomValue, useSetAtom } from 'jotai' -import get from 'lodash/get' -import omit from 'lodash/omit' -import { useRouter } from 'next/navigation' -import { useTransition } from 'react' -import z from 'zod' +import { App, Button } from 'antd'; +import { useAtomValue, useSetAtom } from 'jotai'; +import get from 'lodash/get'; +import omit from 'lodash/omit'; +import { useRouter } from 'next/navigation'; +import { useTransition } from 'react'; +import z from 'zod'; -import EModelOverviewCard from '@/features/entities/me-model/detail-view/card-viewers/emodel-overview-card' -import MorphologyOverviewCard from '@/features/entities/me-model/detail-view/card-viewers/morphology-overview-card' +import EModelOverviewCard from '@/features/entities/me-model/detail-view/card-viewers/emodel-overview-card'; +import MorphologyOverviewCard from '@/features/entities/me-model/detail-view/card-viewers/morphology-overview-card'; // import CustomButton from '@/components/buttons/custom-btn'; -import { createMEModel } from '@/api/entitycore/queries' -import { CreateMEModelSchema, ValidationStatus } from '@/api/entitycore/types/entities/me-model' -import { tryCatch } from '@/api/utils' -import { DataType } from '@/constants/explore-section/list-views' -import { renderArray, renderEmptyOrValue } from '@/entity-configuration/definitions/renderer' -import { MEmodel } from '@/entity-configuration/domain/model/me-model' -import { activityAtomFamily } from '@/features/activity-view/context' -import { useBuildMeModelSessionState } from '@/features/entities/me-model/build/create.state-session' -import { messages } from '@/i18n/en/me-model' -import { OneshotSession } from '@/services/accounting' -import { useEntitiesCountAtom } from '@/services/entitycore/entities-count' -import { runSingleNeuronAnalysis } from '@/services/small-scale-simulator' -import { useRefreshDataAtom } from '@/state/explore-section/list-view-atoms' -import { virtualLabProjectUsersAtomFamily } from '@/state/virtual-lab/projects' -import { ServiceSubtype } from '@/types/accounting' -import { WorkspaceContextSchema } from '@/types/common' -import { classNames } from '@/util/utils' -import { resolveDataKey } from '@/utils/key-builder' -import { resolveExploreDetailsPageUrl } from '@/utils/url-builder' +import { createMEModel } from '@/api/entitycore/queries'; +import { CreateMEModelSchema, ValidationStatus } from '@/api/entitycore/types/entities/me-model'; +import { tryCatch } from '@/api/utils'; +import { DataType } from '@/constants/explore-section/list-views'; +import { renderArray, renderEmptyOrValue } from '@/entity-configuration/definitions/renderer'; +import { MEmodel } from '@/entity-configuration/domain/model/me-model'; +import { activityAtomFamily } from '@/features/activity-view/context'; +import { useBuildMeModelSessionState } from '@/features/entities/me-model/build/create.state-session'; +import { messages } from '@/i18n/en/me-model'; +import { OneshotSession } from '@/services/accounting'; +import { useEntitiesCountAtom } from '@/services/entitycore/entities-count'; +import { runSingleNeuronAnalysis } from '@/services/small-scale-simulator'; +import { useRefreshDataAtom } from '@/state/explore-section/list-view-atoms'; +import { virtualLabProjectUsersAtomFamily } from '@/state/virtual-lab/projects'; +import { ServiceSubtype } from '@/types/accounting'; +import { WorkspaceContextSchema } from '@/types/common'; +import { classNames } from '@/util/utils'; +import { resolveDataKey } from '@/utils/key-builder'; +import { resolveExploreDetailsPageUrl } from '@/utils/url-builder'; -import type { IMEModel } from '@/api/entitycore/types/entities/me-model' -import type { WorkspaceContext } from '@/types/common' +import type { IMEModel } from '@/api/entitycore/types/entities/me-model'; +import type { WorkspaceContext } from '@/types/common'; -const LOW_FUNDS_ERROR_CODE = 'INSUFFICIENT_FUNDS' +const LOW_FUNDS_ERROR_CODE = 'INSUFFICIENT_FUNDS'; -const CreateMeModelContextSchema = CreateMEModelSchema.merge(WorkspaceContextSchema) -type TCreateMeModelContext = z.infer +const CreateMeModelContextSchema = CreateMEModelSchema.merge(WorkspaceContextSchema); +type TCreateMeModelContext = z.infer; function Header({ stateId, virtualLabId, projectId }: WorkspaceContext & { stateId: string }) { const { sessionValue } = useBuildMeModelSessionState({ stateId, virtualLabId, projectId, - }) + }); const contributors = useAtomValue(virtualLabProjectUsersAtomFamily({ projectId, virtualLabId })) - ?.data?.users - const { mmodel } = sessionValue - const { emodel } = sessionValue + ?.data?.users; + const { mmodel } = sessionValue; + const { emodel } = sessionValue; const fields = [ { className: 'col-span-6', @@ -88,7 +88,7 @@ function Header({ stateId, virtualLabId, projectId }: WorkspaceContext & { state title: 'e-type', value: renderEmptyOrValue(renderArray(emodel?.etypes?.map((m) => m.pref_label) || [])), }, - ] + ]; return (
@@ -99,17 +99,17 @@ function Header({ stateId, virtualLabId, projectId }: WorkspaceContext & { state
))} - ) + ); } type Props = { - ctx: WorkspaceContext + ctx: WorkspaceContext; searchParams: { - s: string - m: string - e: string - } -} + s: string; + m: string; + e: string; + }; +}; function CustomButton({ loading, @@ -118,11 +118,11 @@ function CustomButton({ onClick, children, }: { - loading: boolean - disable: boolean - className?: string - onClick?: () => void - children?: React.ReactNode + loading: boolean; + disable: boolean; + className?: string; + onClick?: () => void; + children?: React.ReactNode; }) { return ( - ) + ); } export default function Configure({ ctx, searchParams }: Props) { - const { push: navigate } = useRouter() - const { notification } = App.useApp() - const [isPending, startTransition] = useTransition() - const emodelId = get(searchParams, 'e', undefined) - const morphologyId = get(searchParams, 'm', undefined) - const stateId = get(searchParams, 's', undefined) + const { push: navigate } = useRouter(); + const { notification } = App.useApp(); + const [isPending, startTransition] = useTransition(); + const emodelId = get(searchParams, 'e', undefined); + const morphologyId = get(searchParams, 'm', undefined); + const stateId = get(searchParams, 's', undefined); const exploreDataKey = resolveDataKey({ projectId: ctx.projectId, section: 'explore', entity: MEmodel, - }) + }); - const refreshEntityCountsToParent = useEntitiesCountAtom() - const refreshDataAtom = useRefreshDataAtom(exploreDataKey) + const refreshEntityCountsToParent = useEntitiesCountAtom(); + const refreshDataAtom = useRefreshDataAtom(exploreDataKey); const refreshActivityAtom = useSetAtom( activityAtomFamily({ key: resolveDataKey({ @@ -171,33 +171,33 @@ export default function Configure({ ctx, searchParams }: Props) { projectId: ctx.projectId, virtualLabId: ctx.virtualLabId, type: 'memodel', - }), - ) + }) + ); const { sessionValue } = useBuildMeModelSessionState({ stateId: stateId || '', virtualLabId: ctx.virtualLabId, projectId: ctx.projectId, - }) + }); if (!stateId) { - navigate('./') - return + navigate('./'); + return; } const showErrorNotification = (error: any, type: 'validation' | 'http') => { - let message = messages.DefaultErrorMsg + let message = messages.DefaultErrorMsg; if (type === 'http') message = error?.cause?.error_code === LOW_FUNDS_ERROR_CODE ? messages.LowFundsError - : messages.DefaultErrorMsg - else message = messages.ValidationError + : messages.DefaultErrorMsg; + else message = messages.ValidationError; notification.error({ duration: 10, message, - }) - } + }); + }; const buildMeModel = async () => { const body: Partial = { @@ -211,26 +211,26 @@ export default function Configure({ ctx, searchParams }: Props) { brain_region_id: sessionValue.mmodel?.brain_region.id ?? sessionValue.brainRegion?.id, strain_id: sessionValue.mmodel?.strain?.id ?? null, validation_status: ValidationStatus.Initialized, - } + }; const { error: validationError, data: validationData } = - await CreateMeModelContextSchema.safeParseAsync(body) + await CreateMeModelContextSchema.safeParseAsync(body); if (validationError) { - showErrorNotification(validationError, 'validation') - return { data: null, error: validationError, errorType: 'validation' as const } + showErrorNotification(validationError, 'validation'); + return { data: null, error: validationError, errorType: 'validation' as const }; } const accountingSession = new OneshotSession({ subtype: ServiceSubtype.SingleCellBuild, virtualLabId: ctx.virtualLabId, projectId: ctx.projectId, count: 1, - }) + }); const { data, error } = await tryCatch( accountingSession.useWith(() => createMEModel({ body: omit(validationData, ['virtualLabId', 'projectId']), context: ctx, - }), + }) ), undefined, { @@ -240,39 +240,39 @@ export default function Configure({ ctx, searchParams }: Props) { virtualLabId: ctx.virtualLabId, projectId: ctx.projectId, }, - }, - ) + } + ); - return { data, error, errorType: 'http' as const } - } + return { data, error, errorType: 'http' as const }; + }; const onClick = async () => { startTransition(async () => { - const { data, error, errorType } = await buildMeModel() + const { data, error, errorType } = await buildMeModel(); if (error || !data) { - showErrorNotification(error, errorType) - return + showErrorNotification(error, errorType); + return; } try { - await runSingleNeuronAnalysis({ ctx, modelId: data.id }) + await runSingleNeuronAnalysis({ ctx, modelId: data.id }); } catch (runAnalysisError) { - const message = messages.RunAnalysisError - notification.error({ message, duration: 20 }) + const message = messages.RunAnalysisError; + notification.error({ message, duration: 20 }); } - refreshDataAtom() - refreshActivityAtom() - refreshEntityCountsToParent(data.brain_region.id) + refreshDataAtom(); + refreshActivityAtom(); + refreshEntityCountsToParent(data.brain_region.id); navigate( resolveExploreDetailsPageUrl({ ctx, dataType: DataType.CircuitMEModel, entityId: data.id, - }), - ) - }) - } + }) + ); + }); + }; const validateTrigger = sessionValue.emodel && sessionValue.mmodel && (
@@ -280,7 +280,7 @@ export default function Configure({ ctx, searchParams }: Props) { {isPending ? 'Creating ME-model' : 'Save'}
- ) + ); return ( <> @@ -307,5 +307,5 @@ export default function Configure({ ctx, searchParams }: Props) { {validateTrigger} - ) + ); } From 68cc9129be7daabc32fb4a5514a185fd4f55ac20 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 17:32:54 +0200 Subject: [PATCH 13/19] more linting --- src/page-wrappers/build/me-model/configure.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/page-wrappers/build/me-model/configure.tsx b/src/page-wrappers/build/me-model/configure.tsx index 12a9631f1..4cf346531 100644 --- a/src/page-wrappers/build/me-model/configure.tsx +++ b/src/page-wrappers/build/me-model/configure.tsx @@ -184,7 +184,7 @@ export default function Configure({ ctx, searchParams }: Props) { return; } - const showErrorNotification = (error: any, type: 'validation' | 'http') => { + const showErrorNotification = (error: unknown, type: 'validation' | 'http') => { let message = messages.DefaultErrorMsg; if (type === 'http') message = @@ -256,7 +256,7 @@ export default function Configure({ ctx, searchParams }: Props) { try { await runSingleNeuronAnalysis({ ctx, modelId: data.id }); - } catch (runAnalysisError) { + } catch (_runAnalysisError) { const message = messages.RunAnalysisError; notification.error({ message, duration: 20 }); } From 8bca0b12235a25d200ac99886afc2a3f84b906f9 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 17:50:40 +0200 Subject: [PATCH 14/19] more linting --- src/features/cell-composition/how-to.md | 72 +++---------------------- 1 file changed, 7 insertions(+), 65 deletions(-) diff --git a/src/features/cell-composition/how-to.md b/src/features/cell-composition/how-to.md index e84bf62e4..6a0bed7a1 100644 --- a/src/features/cell-composition/how-to.md +++ b/src/features/cell-composition/how-to.md @@ -1,10 +1,15 @@ # HOW-TO: Construct Brain Cell Composition -This short document explains how cell composition has been constructed, which builds a hierarchical representation of brain cell types and their densities across brain regions. +This short document explains how cell composition has been constructed, +which builds a hierarchical representation of brain cell types and their +densities across brain regions. ## Overview -The cell composition feature processes hierarchical brain region data along with cell type information to generate a unified view of neuron and glial cell distributions. It handles relationships between `brain_region`, `mTypes`, `eTypes`. +The cell composition feature processes hierarchical brain region data +along with cell type information to generate a unified view of neuron +and glial cell distributions. It handles relationships between +`brain_region`, `mTypes`, `eTypes`. ## Data Flow @@ -21,66 +26,3 @@ flowchart TD H --> J[calculate total composition] I --> K[return needed data] J --> K -``` - -## How to process: - -1. **Initialization** - - start with a brain region id (selected by the user) - - retrieve all leaf regions under that brain region (use `brainRegionHierarchyAtom`) - - create volume maps for each leaf region (using `brainRegionAtlasAtom`) - -2. **Node Construction** - - for each leaf region: - - process mTypes and their child eTypes - - build tree nodes with cell counts and densities - - calculate composition data based on volumes - -3. **Node Aggregation** - - merge nodes that represent the same mType/eType across different leaf regions - - sum cell counts across regions - - recalculate densities based on aggregated volumes - - create links between parent mTypes and child eTypes - -4. **Final Calculations** - - calculate total neuron and glial cell counts - - determine total volume (not needed for now) - - compute the whole composition densities - -## Implementation Details - -### Composite IDs - -The system uses composite IDs (`mTypeId__eTypeId`) to ensure that when the same eType appears under different mTypes, they are treated as distinct nodes. This preserves the hierarchical relationship. - -```mermaid -graph TD - mType1 --> eType1_1[eType1 under mType1] - mType1 --> eType2_1[eType2 under mType1] - mType2 --> eType1_2[eType1 under mType2] - mType2 --> eType3[eType3] -``` - -### Volume and Density calculations: - -- **cell count** = density × volume -- **aggregated density** = totalCellCount / TotalVolume -- **neuron count scale** = 1e-9 - -### Node merging: - -When the same node (by composite ID) appears in multiple leaf regions: - -1. merge the lists of associated leaf regions -2. sum the cell counts from all regions -3. recalculate densities based on the total volume of all associated regions -4. combine related nodes (for mTypes pointing to eTypes) - -## Output: - -The final output contains: - -- **nodes**: Array of tree nodes representing mTypes and eTypes -- **links**: Connections between parent mTypes and child eTypes -- **totalVolume**: Combined volume of all leaf regions -- **totalComposition**: Overall neuron and glial cell densities and counts From 08b692cef9bb19af0e5d52863f5c31de6d8c6804 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 17:52:52 +0200 Subject: [PATCH 15/19] more linting --- src/page-wrappers/build/me-model/configure.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/page-wrappers/build/me-model/configure.tsx b/src/page-wrappers/build/me-model/configure.tsx index 4cf346531..92ad7fa7a 100644 --- a/src/page-wrappers/build/me-model/configure.tsx +++ b/src/page-wrappers/build/me-model/configure.tsx @@ -256,9 +256,10 @@ export default function Configure({ ctx, searchParams }: Props) { try { await runSingleNeuronAnalysis({ ctx, modelId: data.id }); - } catch (_runAnalysisError) { + } catch (runAnalysisError) { const message = messages.RunAnalysisError; notification.error({ message, duration: 20 }); + console.error(runAnalysisError); } refreshDataAtom(); From be935d71e08ad672b3bfbdbe549be3d381c294f5 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 18:25:10 +0200 Subject: [PATCH 16/19] more linting --- src/features/cell-composition/how-to.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/cell-composition/how-to.md b/src/features/cell-composition/how-to.md index 6a0bed7a1..0076f4d73 100644 --- a/src/features/cell-composition/how-to.md +++ b/src/features/cell-composition/how-to.md @@ -26,3 +26,4 @@ flowchart TD H --> J[calculate total composition] I --> K[return needed data] J --> K +``` From 21bf9a124a07276c6f6b741f8be479df0d28c7db Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Tue, 12 Aug 2025 22:06:09 +0200 Subject: [PATCH 17/19] more linting --- src/page-wrappers/build/me-model/configure.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/page-wrappers/build/me-model/configure.tsx b/src/page-wrappers/build/me-model/configure.tsx index 92ad7fa7a..0b7408feb 100644 --- a/src/page-wrappers/build/me-model/configure.tsx +++ b/src/page-wrappers/build/me-model/configure.tsx @@ -49,8 +49,7 @@ function Header({ stateId, virtualLabId, projectId }: WorkspaceContext & { state const contributors = useAtomValue(virtualLabProjectUsersAtomFamily({ projectId, virtualLabId })) ?.data?.users; - const { mmodel } = sessionValue; - const { emodel } = sessionValue; + const { mmodel, emodel } = sessionValue; const fields = [ { className: 'col-span-6', @@ -68,13 +67,7 @@ function Header({ stateId, virtualLabId, projectId }: WorkspaceContext & { state }, { title: 'created by', - value: ( -
    - {contributors?.map(({ id, name }) => ( -
  • {name}
  • - ))} -
- ), + value:
    {contributors?.map(({ id, name }) =>
  • {name}
  • )}
, }, { title: 'created date', @@ -131,8 +124,7 @@ function CustomButton({ className, 'bg-primary-9 h-14 rounded-none border border-white px-14 text-white', 'hover:border-primary-8! hover:bg-primary-8! hover:border! hover:font-bold hover:text-white! hover:shadow-xs', - 'disabled:border-gray-400 disabled:bg-white! disabled:text-gray-700! disabled:hover:text-gray-700!', - 'disabled:hover:border-gray-400! disabled:hover:bg-white! disabled:hover:text-gray-700!' + 'disabled:border-gray-400 disabled:bg-white! disabled:text-gray-700! disabled:hover:border-gray-400! disabled:hover:bg-white! disabled:hover:text-gray-700!' )} type="default" size="large" @@ -259,7 +251,7 @@ export default function Configure({ ctx, searchParams }: Props) { } catch (runAnalysisError) { const message = messages.RunAnalysisError; notification.error({ message, duration: 20 }); - console.error(runAnalysisError); + console.error(runAnalysisError); } refreshDataAtom(); From b5efac245f8e79f61a64d56893acb9ad1beed4f7 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Wed, 13 Aug 2025 13:03:09 +0200 Subject: [PATCH 18/19] made buttons consistent --- src/features/contribute/index.tsx | 49 +++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/features/contribute/index.tsx b/src/features/contribute/index.tsx index 3d190de4e..dcd8e3971 100755 --- a/src/features/contribute/index.tsx +++ b/src/features/contribute/index.tsx @@ -2,13 +2,14 @@ import { LoadingOutlined, - RightOutlined, UploadOutlined, CheckCircleFilled, + WarningFilled, + RightOutlined, } from '@ant-design/icons'; import Ajv, { AnySchema } from 'ajv'; import { atom } from 'jotai'; -import { Fragment, useMemo, useRef, useState, KeyboardEvent } from 'react'; +import { Fragment, useMemo, useRef, useState, KeyboardEvent, useEffect } from 'react'; import { Config, ConfigValue, JSONSchemaForm } from './_components/components'; import { useConfigAtom } from './_components/hooks/config-atom'; import { @@ -76,6 +77,11 @@ export default function ContributeMorphologyConfiguration({ errors: [], }); + // Debug log to verify selectedFile state + useEffect(() => { + console.log('selectedFile state:', selectedFile); + }, [selectedFile]); + const selectedCatSchema = schema?.properties?.[configTab]?.additionalProperties?.anyOf?.find( (s) => s.properties?.type.const === selectedCategory ); @@ -157,17 +163,10 @@ export default function ContributeMorphologyConfiguration({ return (
-
+
- {/* Show overall form validation status */} - {!formValidation.isValid && formValidation.errors.length > 0 && ( -
-
Required fields missing:
-
{formValidation.errors.join(', ')}
-
- )} - + {/* ... form validation */}
Assets
Assets -
+
{selectedFile ? ( - + <> + + + ) : ( -
+ <> + + + )} -
{CATEGORIES.filter((c) => c !== 'Assets').map((c) => ( @@ -491,6 +507,7 @@ export default function ContributeMorphologyConfiguration({ disabled={loading || readOnly} onChange={(e) => { const file = e.target.files?.[0]; + console.log('File selected:', file); // Debug log if (file) { setSelectedFile(file); setFileStatus({ message: `File selected: ${file.name}` }); @@ -642,4 +659,4 @@ export default function ContributeMorphologyConfiguration({ )}
); -} +} \ No newline at end of file From 2dfa4846487cc364a32ff58a4f79de46ad8ecc59 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Wed, 13 Aug 2025 13:11:46 +0200 Subject: [PATCH 19/19] more linting --- src/features/contribute/index.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/features/contribute/index.tsx b/src/features/contribute/index.tsx index dcd8e3971..78aa0b8a4 100755 --- a/src/features/contribute/index.tsx +++ b/src/features/contribute/index.tsx @@ -163,7 +163,7 @@ export default function ContributeMorphologyConfiguration({ return (
-
+
{/* ... form validation */} @@ -198,10 +198,7 @@ export default function ContributeMorphologyConfiguration({ className="text-green-600" style={{ fontSize: '14px', visibility: 'visible' }} /> - + ) : ( <> @@ -209,10 +206,7 @@ export default function ContributeMorphologyConfiguration({ className="assets-warning text-yellow-400" style={{ fontSize: '14px', visibility: 'visible' }} /> - + )}
@@ -659,4 +653,4 @@ export default function ContributeMorphologyConfiguration({ )}
); -} \ No newline at end of file +}