diff --git a/package.json b/package.json
index 2ad006b79..221f35632 100644
--- a/package.json
+++ b/package.json
@@ -44,10 +44,10 @@
"@portabletext/react": "^3.2.0",
"@radix-ui/react-accordion": "^1.1.1",
"@radix-ui/react-checkbox": "^1.0.3",
- "@radix-ui/react-dialog": "^1.0.4",
+ "@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-menubar": "^1.1.6",
- "@radix-ui/react-popover": "^1.0.5",
+ "@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.2.3",
@@ -67,6 +67,7 @@
"@t3-oss/env-nextjs": "^0.13.0",
"@tanstack/react-query": "^5.84.1",
"@tanstack/react-query-devtools": "^5.84.1",
+ "@tanstack/react-virtual": "^3.13.12",
"@testing-library/user-event": "^14.5.1",
"@tolokoban/tgd": "^2.0.33",
"@types/d3": "^7.4.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e77716126..709c406ab 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -63,7 +63,7 @@ importers:
specifier: ^1.0.3
version: 1.1.5(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-dialog':
- specifier: ^1.0.4
+ specifier: ^1.1.7
version: 1.1.7(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.5
@@ -72,7 +72,7 @@ importers:
specifier: ^1.1.6
version: 1.1.7(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-popover':
- specifier: ^1.0.5
+ specifier: ^1.1.7
version: 1.1.7(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-select':
specifier: ^2.2.6
@@ -131,6 +131,9 @@ importers:
'@tanstack/react-query-devtools':
specifier: ^5.84.1
version: 5.84.2(@tanstack/react-query@5.84.2(react@19.1.0))(react@19.1.0)
+ '@tanstack/react-virtual':
+ specifier: ^3.13.12
+ version: 3.13.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@testing-library/user-event':
specifier: ^14.5.1
version: 14.6.1(@testing-library/dom@10.4.0)
@@ -4537,8 +4540,8 @@ packages:
react: '>=16.8'
react-dom: '>=16.8'
- '@tanstack/react-virtual@3.13.11':
- resolution: {integrity: sha512-u5EaOSJOq08T9NXFuDopMdxZBNDFuEMohIFFU45fBYDXXh9SjYdbpNq1OLFSOpQnDRPjqgmY96ipZTkzom9t9Q==}
+ '@tanstack/react-virtual@3.13.12':
+ resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -4547,8 +4550,8 @@ packages:
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
- '@tanstack/virtual-core@3.13.11':
- resolution: {integrity: sha512-ORL6UyuZJ0D9X33LDR4TcgcM+K2YiS2j4xbvH1vnhhObwR1Z4dKwPTL/c0kj2Yeb4Yp2lBv1wpyVaqlohk8zpg==}
+ '@tanstack/virtual-core@3.13.12':
+ resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
@@ -16939,15 +16942,15 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
- '@tanstack/react-virtual@3.13.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ '@tanstack/react-virtual@3.13.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
- '@tanstack/virtual-core': 3.13.11
+ '@tanstack/virtual-core': 3.13.12
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@tanstack/table-core@8.21.3': {}
- '@tanstack/virtual-core@3.13.11': {}
+ '@tanstack/virtual-core@3.13.12': {}
'@testing-library/dom@10.4.0':
dependencies:
@@ -24532,7 +24535,7 @@ snapshots:
'@sanity/uuid': 3.0.2
'@sentry/react': 8.55.0(react@19.1.0)
'@tanstack/react-table': 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
- '@tanstack/react-virtual': 3.13.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@tanstack/react-virtual': 3.13.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@types/react-is': 19.0.0
'@types/shallow-equals': 1.0.3
'@types/speakingurl': 13.0.6
diff --git a/src/api/entitycore/types/entities/me-model.ts b/src/api/entitycore/types/entities/me-model.ts
index 76f95973b..17195d063 100644
--- a/src/api/entitycore/types/entities/me-model.ts
+++ b/src/api/entitycore/types/entities/me-model.ts
@@ -84,7 +84,7 @@ export interface IMEModelFilter
}
export const CreateMEModelSchema = z.object({
- name: z.string(),
+ name: z.string().nonempty(),
description: z.string(),
validation_status: z.nativeEnum(ValidationStatus),
brain_region_id: z.string().uuid(),
diff --git a/src/api/entitycore/types/entities/single-neuron-synaptome.ts b/src/api/entitycore/types/entities/single-neuron-synaptome.ts
index 317fe9137..1d4df30b6 100644
--- a/src/api/entitycore/types/entities/single-neuron-synaptome.ts
+++ b/src/api/entitycore/types/entities/single-neuron-synaptome.ts
@@ -71,8 +71,8 @@ const SingleNeuronSynaptomeExclusionRuleSchema = z
})
.refine(
(data) => {
- if (!isNil(data.distance_soma_gte) || !isNil(data.distance_soma_lte)) return true;
- return false;
+ if (isNil(data.distance_soma_gte) && isNil(data.distance_soma_lte)) return false;
+ return true;
},
{
message: 'At least one of distance_soma_gte or distance_soma_lte must be provided',
@@ -80,48 +80,48 @@ const SingleNeuronSynaptomeExclusionRuleSchema = z
}
);
-export const SingleNeuronSynaptomeConfigurationSchema = z
- .object({
- id: z.string().uuid(),
- name: z.string(),
- target: z.string().optional(),
- seed: z.number(),
- color: z.string(),
- formula: z.string().optional(),
- soma_synapse_count: z.number().optional(),
- type: z.union([z.literal(110), z.literal(10)]),
- exclusion_rules: z.array(SingleNeuronSynaptomeExclusionRuleSchema).nullable(),
- })
- .superRefine((synapse, ctx) => {
+export const SingleNeuronSynaptomeBaseSchema = z.object({
+ id: z.string().uuid(),
+ name: z.string().nonempty(),
+ target: z.string().optional(),
+ seed: z.number(),
+ color: z.string(),
+ formula: z.string().optional(),
+ soma_synapse_count: z.number().optional(),
+ type: z.union([z.literal(110), z.literal(10)]),
+ exclusion_rules: z.array(SingleNeuronSynaptomeExclusionRuleSchema).nullable(),
+});
+
+export const SingleNeuronSynaptomeConfigurationSchema = SingleNeuronSynaptomeBaseSchema.superRefine(
+ (synapse, ctx) => {
if (synapse.target !== 'soma' && isNil(synapse.formula)) {
- return ctx.addIssue({
+ ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'formula should be provided when target is different then "soma"',
path: ['formula'],
});
}
+
if (synapse.target === 'soma' && isNil(synapse.soma_synapse_count)) {
- return ctx.addIssue({
+ ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'soma_synapse_count must be a valid number when target is "soma"',
path: ['soma_synapse_count'],
});
}
- })
- .superRefine(async (synapse, ctx) => {
- if (synapse.target !== 'soma') {
- return validateSingleNeuronSynapseGenerationFormula(synapse.formula!).then((v) => {
- if (!v) {
- return ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: 'formula is not valid',
- path: ['formula'],
- });
- }
+ }
+).superRefine(async (synapse, ctx) => {
+ if (synapse.target !== 'soma') {
+ const v = await validateSingleNeuronSynapseGenerationFormula(synapse.formula!);
+ if (!v) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'formula is not valid',
+ path: ['formula'],
});
}
- return true;
- });
+ }
+});
export type TSingleNeuronSynaptomeConfiguration = z.infer<
typeof SingleNeuronSynaptomeConfigurationSchema
diff --git a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/_template.tsx b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/_template.tsx
deleted file mode 100644
index 262b996e9..000000000
--- a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/_template.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { ReactNode } from 'react';
-import { PageTransition } from '@/ui/segments/workflows/page-transition';
-
-export default function Template({ children }: { children: ReactNode }) {
- return {children};
-}
diff --git a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/browse/[type]/page.tsx b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/browse/[type]/page.tsx
index 874837611..32daefe2f 100644
--- a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/browse/[type]/page.tsx
+++ b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/browse/[type]/page.tsx
@@ -1,6 +1,6 @@
import snakeCase from 'lodash/snakeCase';
-import { BrowseAction } from '@/ui/segments/workflows/elements/browse-action';
+import { BrowseAction } from '@/ui/segments/workflows/elements/browse-build-action';
import { BrowseEntityScope } from '@/features/views/listing/browse-entity';
import { WorkspaceScope, WorkspaceSection } from '@/constants';
@@ -31,6 +31,7 @@ export default async function Page({
classNames={{ container: 'max-h-full' }}
dataType={dataType}
scope={scope ?? WorkspaceScope.Public}
+ miniViewProps={{ section: WorkspaceSection.BuildWorkflow }}
/>
diff --git a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/configure/memodel/page.tsx b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/configure/memodel/page.tsx
new file mode 100644
index 000000000..559f4dec4
--- /dev/null
+++ b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/configure/memodel/page.tsx
@@ -0,0 +1,35 @@
+import { Header } from '@/ui/segments/workflows/build/memodel/header';
+import { Menu } from '@/ui/segments/workflows/build/memodel/menu';
+import { Content } from '@/ui/segments/workflows/build/memodel';
+
+import type { BuildStepKeys } from '@/ui/segments/workflows/build/memodel/helpers';
+import type { ServerSideComponentProp, WorkspaceContext } from '@/types/common';
+
+export default async function Page({
+ searchParams,
+}: ServerSideComponentProp<
+ WorkspaceContext & { id: string },
+ { step: BuildStepKeys; sessionId: string }
+>) {
+ let { sessionId } = await searchParams;
+ if (!sessionId) sessionId = crypto.randomUUID();
+
+ return (
+
+ );
+}
diff --git a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/configure/single-neuron-synaptome/page.tsx b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/configure/single-neuron-synaptome/page.tsx
new file mode 100644
index 000000000..7c6bf988f
--- /dev/null
+++ b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/configure/single-neuron-synaptome/page.tsx
@@ -0,0 +1,35 @@
+import { Header } from '@/ui/segments/workflows/build/single-neuron-synaptome/header';
+import { Menu } from '@/ui/segments/workflows/build/single-neuron-synaptome/menu';
+import { Content } from '@/ui/segments/workflows/build/single-neuron-synaptome';
+
+import type { BuildStepKeys } from '@/ui/segments/workflows/build/single-neuron-synaptome/helpers';
+import type { ServerSideComponentProp, WorkspaceContext } from '@/types/common';
+
+export default async function Page({
+ searchParams,
+}: ServerSideComponentProp<
+ WorkspaceContext & { id: string },
+ { step: BuildStepKeys; sessionId: string }
+>) {
+ let { sessionId } = await searchParams;
+ if (!sessionId) sessionId = crypto.randomUUID();
+
+ return (
+
+ );
+}
diff --git a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/new/[type]/layout.tsx b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/new/[type]/layout.tsx
new file mode 100644
index 000000000..4af3bfb8d
--- /dev/null
+++ b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/new/[type]/layout.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { type ReactNode, useState } from 'react';
+import { motion } from 'motion/react';
+
+import { BuildWorkflowsBreadcrumb } from '@/ui/segments/workflows/elements/build-breadcrumb';
+import { useDisableElementOverflow } from '@/ui/hooks/use-disable-element-overflow';
+import { useSelectEntityClickEvent } from '@/ui/segments/mini-detail-view/event';
+import { cn } from '@/utils/css-class';
+
+export default function Layout({ children }: { children: ReactNode }) {
+ const [miniViewPresent, setMiniViewPresent] = useState(false);
+ useDisableElementOverflow({ id: 'workspace-body' });
+ useSelectEntityClickEvent((ev) => {
+ setMiniViewPresent(ev.detail.display);
+ });
+
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/new/[type]/page.tsx b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/new/[type]/page.tsx
new file mode 100644
index 000000000..00293ca8d
--- /dev/null
+++ b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/new/[type]/page.tsx
@@ -0,0 +1,39 @@
+import snakeCase from 'lodash/snakeCase';
+
+import { BrowseEntityScope } from '@/features/views/listing/browse-entity';
+import { WorkspaceScope, WorkspaceSection } from '@/constants';
+
+import type { TExtendedEntitiesTypeDict } from '@/api/entitycore/types/extended-entity-type';
+import type { ServerSideComponentProp, WorkspaceContext } from '@/types/common';
+import type { TWorkspaceScope } from '@/constants';
+import type { KebabCase } from '@/utils/type';
+
+export default async function Page({
+ params,
+ searchParams,
+}: ServerSideComponentProp<
+ WorkspaceContext & { type: KebabCase
},
+ { scope: TWorkspaceScope | null }
+>) {
+ const { scope } = await searchParams;
+ const { type } = await params;
+
+ const dataType = snakeCase(type) as TExtendedEntitiesTypeDict;
+
+ return (
+
+ );
+}
diff --git a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/page.tsx b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/page.tsx
deleted file mode 100644
index db6413644..000000000
--- a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/build/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Page() {
- return Build main page
;
-}
diff --git a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/page.tsx b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/page.tsx
index 997fb60cb..82c315aee 100644
--- a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/page.tsx
+++ b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/page.tsx
@@ -12,7 +12,7 @@ import { V2_MIGRATION_TEMPORARY_BASE_PATH } from '@/config';
import type { TExtendedEntitiesTypeDict } from '@/api/entitycore/types/extended-entity-type';
import type { ServerSideComponentProp, WorkspaceContext } from '@/types/common';
-import type { TCategoryValue } from '@/ui/segments/workflows/elements/helpers';
+import { CategoryValues, type TCategoryValue } from '@/ui/segments/workflows/elements/helpers';
export default function Page({ params }: ServerSideComponentProp) {
useDisableElementOverflow({ id: 'workspace-body' });
@@ -32,6 +32,13 @@ export default function Page({ params }: ServerSideComponentProp {
updateWorkflowState((prev) => ({ ...prev, entityType: value }));
+ if (category === CategoryValues.Build) {
+ const sessionId = crypto.randomUUID();
+ push(
+ `${V2_MIGRATION_TEMPORARY_BASE_PATH}/${virtualLabId}/${projectId}/workflows/${category}/configure/${kebabCase(value)}?sessionId=${sessionId}`
+ );
+ return;
+ }
push(
`${V2_MIGRATION_TEMPORARY_BASE_PATH}/${virtualLabId}/${projectId}/workflows/${category}/browse/${kebabCase(value)}`
);
diff --git a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/simulate/browse/[type]/page.tsx b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/simulate/browse/[type]/page.tsx
index 99083c9e2..907efda95 100644
--- a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/simulate/browse/[type]/page.tsx
+++ b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/simulate/browse/[type]/page.tsx
@@ -1,6 +1,6 @@
import snakeCase from 'lodash/snakeCase';
-import { BrowseAction } from '@/ui/segments/workflows/elements/browse-action';
+import { BrowseAction } from '@/ui/segments/workflows/elements/browse-simulate-action';
import { BrowseEntityScope } from '@/features/views/listing/browse-entity';
import { WorkspaceScope, WorkspaceSection } from '@/constants';
@@ -31,6 +31,7 @@ export default async function Page({
classNames={{ container: 'max-h-full' }}
dataType={dataType}
scope={scope ?? WorkspaceScope.Public}
+ miniViewProps={{ section: WorkspaceSection.SimulateWorkflow }}
/>
diff --git a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/simulate/new/[type]/layout.tsx b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/simulate/new/[type]/layout.tsx
index 09cc79708..430979979 100644
--- a/src/app/app/v2/[virtualLabId]/[projectId]/workflows/simulate/new/[type]/layout.tsx
+++ b/src/app/app/v2/[virtualLabId]/[projectId]/workflows/simulate/new/[type]/layout.tsx
@@ -3,7 +3,7 @@
import { type ReactNode, useState } from 'react';
import { motion } from 'motion/react';
-import { SimulationBreadcrumb } from '@/ui/segments/workflows/elements/simulation-breadcrumb';
+import { SimulateWorkflowsBreadcrumb } from '@/ui/segments/workflows/elements/simulate-breadcrumb';
import { useDisableElementOverflow } from '@/ui/hooks/use-disable-element-overflow';
import { useSelectEntityClickEvent } from '@/ui/segments/mini-detail-view/event';
import { cn } from '@/utils/css-class';
@@ -17,7 +17,7 @@ export default function Layout({ children }: { children: ReactNode }) {
return (
-
+
{children}
diff --git a/src/components/FilterControls/FilterControls.tsx b/src/components/FilterControls/FilterControls.tsx
index 4a2e3a6c8..dc1e04f21 100644
--- a/src/components/FilterControls/FilterControls.tsx
+++ b/src/components/FilterControls/FilterControls.tsx
@@ -1,6 +1,6 @@
import { HTMLProps, PropsWithChildren, useState } from 'react';
import ControlPanel from './ControlPanel';
-import SettingsIcon from '@/components/icons/Settings';
+import { SettingsIcon } from '@/components/icons/Settings';
import { classNames } from '@/util/utils';
export default function FilterControls({
diff --git a/src/components/explore-section/ExploreSectionListingView/FilterControls.tsx b/src/components/explore-section/ExploreSectionListingView/FilterControls.tsx
index 602b2568b..8330aab59 100644
--- a/src/components/explore-section/ExploreSectionListingView/FilterControls.tsx
+++ b/src/components/explore-section/ExploreSectionListingView/FilterControls.tsx
@@ -14,11 +14,11 @@ import { unwrap } from 'jotai/utils';
import { Spin } from 'antd';
import ExploreSectionNameSearch from '@/components/explore-section/ExploreSectionListingView/ExploreSectionNameSearch';
-import SettingsIcon from '@/components/icons/Settings';
import { activeColumnsAtom } from '@/state/explore-section/list-view-atoms';
import { ExploreDataScope } from '@/types/explore-section/application';
import { filterHasValue } from '@/features/listing-filter-panel/util';
+import { SettingsIcon } from '@/components/icons/Settings';
import { classNames } from '@/util/utils';
import type { TExtendedEntitiesTypeDict } from '@/api/entitycore/types/extended-entity-type';
diff --git a/src/components/icons/Settings.tsx b/src/components/icons/Settings.tsx
index d4efa7220..9ca252af3 100644
--- a/src/components/icons/Settings.tsx
+++ b/src/components/icons/Settings.tsx
@@ -6,7 +6,7 @@ type SettingsIconProps = {
fill?: string;
};
-export default function SettingsIcon({ className, style, fill }: SettingsIconProps) {
+export function SettingsIcon({ className, style, fill }: SettingsIconProps) {
return (
);
}
+
+export function ArrowSyncFilled(props: React.SVGProps) {
+ return (
+
+ );
+}
diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts
index dc86ee744..bc114a57d 100644
--- a/src/components/icons/index.ts
+++ b/src/components/icons/index.ts
@@ -24,10 +24,10 @@ import IconPlus from './Plus';
import RangeIcon from './Range';
import ReadMeIcon from './ReadMeIcon';
import ResetIcon from './ResetIcon';
-import SettingsIcon from './Settings';
import UndoIcon from './UndoIcon';
import UserIcon from './UserIcon';
import VirtualLabIcon from './VirtualLab';
+import { SettingsIcon } from './Settings';
import { ZoomInIcon, ZoomOutIcon } from './Zoom';
export {
diff --git a/src/components/neuron-viewer/index.tsx b/src/components/neuron-viewer/index.tsx
index 229ffcc24..6cab1b36c 100644
--- a/src/components/neuron-viewer/index.tsx
+++ b/src/components/neuron-viewer/index.tsx
@@ -135,14 +135,14 @@ export default function NeuronViewer({
}
return (
-
+
{loading && (
)}
{children?.({
useZoomer,
diff --git a/src/components/neuron-viewer/NeuronViewerWithActions.tsx b/src/components/neuron-viewer/neuron-viewer-with-actions.tsx
similarity index 97%
rename from src/components/neuron-viewer/NeuronViewerWithActions.tsx
rename to src/components/neuron-viewer/neuron-viewer-with-actions.tsx
index 7f068aeb3..1fc2f855c 100644
--- a/src/components/neuron-viewer/NeuronViewerWithActions.tsx
+++ b/src/components/neuron-viewer/neuron-viewer-with-actions.tsx
@@ -24,7 +24,7 @@ type Props = {
virtualLabId: string;
projectId: string;
};
-export default function NeuronViewerContainer({
+export function NeuronViewerContainer({
meModelId,
zoomPlacement = 'right',
useZoomer = false,
@@ -108,3 +108,5 @@ export default function NeuronViewerContainer({
);
}
+
+export default NeuronViewerContainer;
diff --git a/src/constants.tsx b/src/constants.tsx
index 76a9040e7..4a06055b5 100644
--- a/src/constants.tsx
+++ b/src/constants.tsx
@@ -13,7 +13,9 @@ export const WorkspaceScope = {
Project: 'project',
Bookmarks: 'bookmarks',
Custom: 'custom',
- BuildMeModel: 'build-me-model',
+ BuildMeModelM: 'build-me-model/m-model',
+ BuildMeModelE: 'build-me-model/e-model',
+ BuildSynaptomeModel: 'build-single-neuron-synaptome-model/memodel',
} as const;
export type TWorkspaceScope = (typeof WorkspaceScope)[keyof typeof WorkspaceScope];
diff --git a/src/entity-configuration/definitions/renderer.tsx b/src/entity-configuration/definitions/renderer.tsx
index eb47c9856..44e708e7e 100644
--- a/src/entity-configuration/definitions/renderer.tsx
+++ b/src/entity-configuration/definitions/renderer.tsx
@@ -77,7 +77,7 @@ export const renderDictionaryKeys = (
);
};
-export const renderDate = (isoDateString: string) => {
+export const renderDate = (isoDateString?: string | null) => {
if (!isoDateString) return EmptyValue;
return format(parseISO(isoDateString), 'dd.MM.yyyy');
};
diff --git a/src/entity-configuration/domain/simulation/paired-neurons-simulation.ts b/src/entity-configuration/domain/simulation/paired-neurons-simulation.ts
index 8c35d1d44..2770e41be 100644
--- a/src/entity-configuration/domain/simulation/paired-neurons-simulation.ts
+++ b/src/entity-configuration/domain/simulation/paired-neurons-simulation.ts
@@ -32,12 +32,12 @@ async function resolveSimulationCampaigns({
withFacets,
context,
filters,
- circuitFilter,
+ circuitScaleFilter,
}: {
withFacets?: boolean;
context: WorkspaceContext | undefined;
filters?: Partial
;
- circuitFilter?: Partial;
+ circuitScaleFilter?: Partial;
}) {
// eslint-disable-next-line no-param-reassign
filters = discardBrainRegionQueryParams(filters);
@@ -80,7 +80,7 @@ async function resolveSimulationCampaigns({
const circuits = await getCircuits({
context,
- filters: { id__in: source.data.map((l) => l.entity_id), ...circuitFilter },
+ filters: { id__in: source.data.map((l) => l.entity_id), ...circuitScaleFilter },
});
const circuitMap = keyBy(circuits.data, 'id');
const result = enrichedData.map((entity) => ({
@@ -143,7 +143,7 @@ export const PairedNeuronCircuitSimulation: EntityCoreTypeConfig[0]) =>
resolveSimulationCampaigns({
...params,
- circuitFilter: {
+ circuitScaleFilter: {
scale: 'pair',
},
}),
diff --git a/src/entity-configuration/domain/simulation/small-microcircuit-simulation.ts b/src/entity-configuration/domain/simulation/small-microcircuit-simulation.ts
index 20b8b4320..2e31db369 100644
--- a/src/entity-configuration/domain/simulation/small-microcircuit-simulation.ts
+++ b/src/entity-configuration/domain/simulation/small-microcircuit-simulation.ts
@@ -33,12 +33,12 @@ async function resolveSimulationCampaigns({
withFacets,
context,
filters,
- circuitFilter,
+ circuitScaleFilter,
}: {
withFacets?: boolean;
context: WorkspaceContext | undefined;
filters?: Partial;
- circuitFilter?: Partial;
+ circuitScaleFilter?: Partial;
}) {
// eslint-disable-next-line no-param-reassign
filters = discardBrainRegionQueryParams(filters);
@@ -80,7 +80,7 @@ async function resolveSimulationCampaigns({
const circuits = await getCircuits({
context,
- filters: { id__in: source.data.map((l) => l.entity_id), ...circuitFilter },
+ filters: { id__in: source.data.map((l) => l.entity_id), ...circuitScaleFilter },
});
const circuitMap = keyBy(circuits.data, 'id');
const result = enrichedData.map((entity) => ({
@@ -143,7 +143,7 @@ export const SmallMicrocircuitSimulation: EntityCoreTypeConfig[0]) =>
resolveSimulationCampaigns({
...params,
- circuitFilter: {
+ circuitScaleFilter: {
scale: 'small',
},
}),
diff --git a/src/features/brain-region-dropdown/index.tsx b/src/features/brain-region-dropdown/index.tsx
new file mode 100644
index 000000000..854776a09
--- /dev/null
+++ b/src/features/brain-region-dropdown/index.tsx
@@ -0,0 +1,204 @@
+import { CheckOutlined, DownOutlined, LoadingOutlined, SearchOutlined } from '@ant-design/icons';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useVirtualizer } from '@tanstack/react-virtual';
+import { loadable, unwrap } from 'jotai/utils';
+import { useAtomValue } from 'jotai';
+import omit from 'lodash/omit';
+
+import { Popover, PopoverContent, PopoverTrigger } from '@/ui/molecules/popover';
+import { useDefaultBreakpoint } from '@/ui/hooks/create-break-point';
+import {
+ brainRegionBasicCellGroupsRegionsHierarchyAtom,
+ useBrainRegionHierarchy,
+ useSetSelectedBrainRegion,
+ type BrainRegionHierarchyOption,
+} from '@/features/brain-region-hierarchy/context';
+import { Button } from '@/ui/molecules/button';
+import { BrainIcon } from '@/components/icons';
+import { cn } from '@/utils/css-class';
+
+export function BrainRegionDropdown({ dataKey }: { dataKey: string }) {
+ const breakpoint = useDefaultBreakpoint();
+ const [open, setOpen] = useState(false);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [parent, setParent] = useState(null);
+
+ const brainRegionHierarchy = useAtomValue(
+ useMemo(() => unwrap(brainRegionBasicCellGroupsRegionsHierarchyAtom), [])
+ );
+ const isLoading =
+ useAtomValue(loadable(brainRegionBasicCellGroupsRegionsHierarchyAtom)).state === 'loading';
+
+ const { updateSelectedBrainRegion } = useSetSelectedBrainRegion();
+
+ const { node, updateHierarchyConfig } = useBrainRegionHierarchy({
+ dataKey,
+ });
+
+ const parentSetter = useCallback(
+ (el: HTMLDivElement) => {
+ setParent(el);
+ },
+ [setParent]
+ );
+
+ const filteredOptions = useMemo>(() => {
+ const options = (brainRegionHierarchy?.options ?? []) as Array;
+ if (!searchTerm.trim()) return options;
+
+ return options.filter(({ label }) => label.toLowerCase().includes(searchTerm.toLowerCase()));
+ }, [brainRegionHierarchy, searchTerm]);
+
+ const rowVirtualizer = useVirtualizer({
+ count: filteredOptions.length,
+ enabled: open && filteredOptions.length > 0 && !!parent,
+ useAnimationFrameWithResizeObserver: true,
+ getScrollElement: () => parent,
+ estimateSize: (index) => {
+ const option = filteredOptions[index];
+ if (!option) return 40;
+ const charsPerLine = 25;
+ const lineHeight = 24;
+ const lines = Math.ceil(option.label.length / charsPerLine);
+ return lines * lineHeight + 16;
+ },
+ overscan: 5,
+ paddingEnd: 10,
+ });
+
+ useEffect(() => {
+ let id: number;
+ if (open && parent) {
+ id = requestAnimationFrame(() => {
+ rowVirtualizer.measure();
+ });
+ }
+ return () => {
+ if (id) {
+ cancelAnimationFrame(id);
+ }
+ };
+ }, [open, rowVirtualizer, parent]);
+
+ const onSelect = (option: BrainRegionHierarchyOption) => {
+ updateHierarchyConfig(option.data);
+ updateSelectedBrainRegion(omit(option.data, 'children'));
+ setOpen(false);
+ };
+
+ const currentValue = (v: string) =>
+ brainRegionHierarchy?.options.find(({ value: _value }) => v === _value);
+
+ const handleOpenChange = (newOpen: boolean) => {
+ setOpen(newOpen);
+ if (!newOpen) {
+ setSearchTerm('');
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className={cn(
+ 'border-none',
+ 'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
+ { 'h-9 text-base': breakpoint === 'l' },
+ { 'h-10 text-lg': breakpoint === 'xl' }
+ )}
+ />
+
+
+
+
+ {filteredOptions.length === 0 ? (
+
+ No brain regions found
+
+ ) : (
+
+ {rowVirtualizer.getVirtualItems().map((virtualItem) => {
+ const option = filteredOptions[virtualItem.index];
+ if (!option) return null;
+
+ const { value: v, label, data } = option;
+
+ return (
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/features/brain-region-hierarchy/context.tsx b/src/features/brain-region-hierarchy/context.tsx
index 18013ace7..f9a33b19d 100644
--- a/src/features/brain-region-hierarchy/context.tsx
+++ b/src/features/brain-region-hierarchy/context.tsx
@@ -50,7 +50,7 @@ export const DEFAULT_BRAIN_REGION_ANNOTATION_FIELD = 'annotation_value';
export const DEFAULT_BRAIN_REGION_QUERY_ID = 'br_id';
export const DEFAULT_BRAIN_REGION_QUERY_ANNOTATION_VALUE = 'br_av';
-type BrainRegionHierarchyOption = {
+export type BrainRegionHierarchyOption = {
value: string;
label: string;
data: IBrainRegionHierarchy;
diff --git a/src/features/entities/me-model/build/create.state-session.tsx b/src/features/entities/me-model/build/create.state-session.tsx
index faa5bf1cb..6c66d5711 100644
--- a/src/features/entities/me-model/build/create.state-session.tsx
+++ b/src/features/entities/me-model/build/create.state-session.tsx
@@ -1,4 +1,4 @@
-import { useSessionStorage } from '@/hooks/useSessionStorage';
+import { useSessionStorage } from '@/hooks/use-session-storage';
import type { IReconstructionMorphology } from '@/api/entitycore/types/entities/reconstruction-morphology';
import type { BrainRegionHierarchyBase } from '@/api/entitycore/types/entities/brain-region';
diff --git a/src/features/entities/neuron-simulation/experiment/setup/advanced-simulation-config.tsx b/src/features/entities/neuron-simulation/experiment/setup/advanced-simulation-config.tsx
index 4c5cc9fa5..44cb27d08 100644
--- a/src/features/entities/neuron-simulation/experiment/setup/advanced-simulation-config.tsx
+++ b/src/features/entities/neuron-simulation/experiment/setup/advanced-simulation-config.tsx
@@ -2,9 +2,9 @@
import { useParams } from 'next/navigation';
-import NeuronViewerContainer from '@/components/neuron-viewer/NeuronViewerWithActions';
import Wrapper from '@/features/entities/neuron-simulation/experiment/elements/wrapper';
import ParameterView from '@/features/entities/neuron-simulation/experiment/steps-wizard';
+import { NeuronViewerContainer } from '@/components/neuron-viewer/neuron-viewer-with-actions';
import type { SingleNeuronSynaptomePayload } from '@/features/entities/neuron-simulation/experiment/containers/synaptome';
import type { WorkspaceContext } from '@/types/common';
diff --git a/src/features/entities/single-neuron-synaptome/build/create.state-session.tsx b/src/features/entities/single-neuron-synaptome/build/create.state-session.tsx
index a68d32c57..a08fc38c6 100644
--- a/src/features/entities/single-neuron-synaptome/build/create.state-session.tsx
+++ b/src/features/entities/single-neuron-synaptome/build/create.state-session.tsx
@@ -1,7 +1,7 @@
import { parseAsString, useQueryStates } from 'nuqs';
import type { Parser } from 'nuqs';
-import { useSessionStorage } from '@/hooks/useSessionStorage';
+import { useSessionStorage } from '@/hooks/use-session-storage';
import type { WorkspaceContext } from '@/types/common';
import type { IMEModel } from '@/api/entitycore/types';
diff --git a/src/features/entities/single-neuron-synaptome/build/phases/placement-config.tsx b/src/features/entities/single-neuron-synaptome/build/phases/placement-config.tsx
index c7c81c651..5861ae837 100644
--- a/src/features/entities/single-neuron-synaptome/build/phases/placement-config.tsx
+++ b/src/features/entities/single-neuron-synaptome/build/phases/placement-config.tsx
@@ -8,7 +8,7 @@ import DefaultLoadingSuspense from '@/components/DefaultLoadingSuspense';
import type { WorkspaceContext } from '@/types/common';
const NeuronViewerContainer = dynamic(
- () => import('@/components/neuron-viewer/NeuronViewerWithActions'),
+ () => import('@/components/neuron-viewer/neuron-viewer-with-actions'),
{
ssr: false,
}
diff --git a/src/features/views/listing/browse-entity.tsx b/src/features/views/listing/browse-entity.tsx
index d658f9558..1ae968da0 100644
--- a/src/features/views/listing/browse-entity.tsx
+++ b/src/features/views/listing/browse-entity.tsx
@@ -3,7 +3,7 @@
'use client';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
-import { useState, type ComponentProps } from 'react';
+import { ReactElement, useState, type ComponentProps } from 'react';
import { WarningOutlined } from '@ant-design/icons';
import compact from 'lodash/compact';
import dynamic from 'next/dynamic';
@@ -32,10 +32,14 @@ import type { TExtendedEntitiesTypeDict } from '@/api/entitycore/types/extended-
import type { EntityCoreIdentifiableNamed } from '@/api/entitycore/types/shared/global';
import type { EntityCoreResponse } from '@/api/entitycore/types/shared/response';
import type { TWorkspaceScope, TWorkspaceSection } from '@/constants';
+import type { Props as MainTableProps } from '@/ui/segments/data-table';
-const MainTable = dynamic(() => import('@/ui/segments/data-table'), { ssr: false });
+const MainTable = dynamic(() => import('@/ui/segments/data-table'), { ssr: false }) as (
+ props: MainTableProps
+) => ReactElement | null;
type Props = {
+ id?: string;
section?: TWorkspaceSection;
requireBrainRegion?: boolean;
requireMiniDetailView?: boolean;
@@ -44,16 +48,19 @@ type Props = {
miniView?: ComponentProps<'div'>['className'];
};
scope: TWorkspaceScope;
+ defaultBrainRegion?: string;
dataType: TExtendedEntitiesTypeDict;
mainTableProps?: Partial>;
miniViewProps?: Partial>;
};
export function BrowseEntityScope({
+ id,
classNames,
section = WorkspaceSection.Explore,
requireBrainRegion = true,
requireMiniDetailView = true,
+ defaultBrainRegion,
dataType,
scope,
mainTableProps,
@@ -61,7 +68,7 @@ export function BrowseEntityScope({
}: Props) {
const { virtualLabId, projectId } = useWorkspace();
- const dataKey = compact([virtualLabId, projectId, section, dataType, scope]).join('/');
+ const dataKey = compact([virtualLabId, projectId, section, dataType, scope, id]).join('/');
const entity = getEntityByExtendedType({ type: dataType });
const setPageNumber = useSetAtom(corePageNumberAtom(dataKey));
@@ -98,6 +105,7 @@ export function BrowseEntityScope({
});
},
requireBrainRegion,
+ defaultBrainRegion,
enabled: ({ queryKey }) => {
const [{ queryParameters }] = queryKey;
if (requireBrainRegion && !get(queryParameters, 'within_brain_region_brain_region_id', null))
@@ -138,8 +146,8 @@ export function BrowseEntityScope({
return (
<>
({
workspace,
queryFn,
requireBrainRegion,
+ defaultBrainRegion,
...rest
}: {
context: QueryContext;
@@ -109,6 +124,7 @@ export function useQueryExtendedEntityType({
| undefined;
useKeepPreviousData?: boolean;
requireBrainRegion?: boolean;
+ defaultBrainRegion?: string;
} & Omit<
UseQueryOptions<
TData,
@@ -127,7 +143,10 @@ export function useQueryExtendedEntityType({
>,
'queryKey' | 'queryFn' | 'placeholderData'
>) {
- const queryParameters = useQueryParameters({ context }, requireBrainRegion);
+ const queryParameters = useQueryParameters(
+ { context, workspace },
+ { requireBrainRegion, defaultBrainRegion }
+ );
return useQuery({
queryKey: buildQueryKey({ workspace, context, queryParameters, requireBrainRegion }),
queryFn,
diff --git a/src/ui/molecules/popover/index.tsx b/src/ui/molecules/popover/index.tsx
new file mode 100644
index 000000000..6f8dbd114
--- /dev/null
+++ b/src/ui/molecules/popover/index.tsx
@@ -0,0 +1,43 @@
+/* eslint-disable react/jsx-props-no-spreading */
+
+'use client';
+
+import * as React from 'react';
+import * as PopoverPrimitive from '@radix-ui/react-popover';
+import { cn } from '@/utils/css-class';
+
+function Popover({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function PopoverTrigger({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function PopoverContent({
+ className,
+ align = 'center',
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function PopoverAnchor({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
diff --git a/src/ui/segments/data-table/elements/filter-controls.tsx b/src/ui/segments/data-table/elements/filter-controls.tsx
index b6e8d4f50..88eb6f010 100644
--- a/src/ui/segments/data-table/elements/filter-controls.tsx
+++ b/src/ui/segments/data-table/elements/filter-controls.tsx
@@ -1,18 +1,12 @@
'use client';
-import { HTMLProps, ReactNode, useEffect, useMemo, useState } from 'react';
-import { useAtomValue } from 'jotai';
-import { unwrap } from 'jotai/utils';
-import { Spin } from 'antd';
-
-import SettingsIcon from '@/components/icons/Settings';
+import { HTMLProps, ReactNode } from 'react';
import { filterHasValue } from '@/ui/segments/data-table/elements/listing-filter-panel/util';
-import { coreActiveColumnsAtom } from '@/ui/segments/data-table/elements/context';
+import { SettingsIcon } from '@/components/icons/Settings';
import { classNames } from '@/util/utils';
import { cn } from '@/utils/css-class';
-import type { TExtendedEntitiesTypeDict } from '@/api/entitycore/types/extended-entity-type';
import type { CoreFilter } from '@/entity-configuration/definitions/types';
function FilterBtn({ disabled, className, children, onClick }: HTMLProps) {
@@ -39,8 +33,6 @@ export function FilterControls({
children,
displayControlPanel,
setDisplayControlPanel,
- dataType,
- dataKey,
filters,
disabled,
className,
@@ -48,40 +40,27 @@ export function FilterControls({
children?: ReactNode;
displayControlPanel: boolean;
setDisplayControlPanel: (v: boolean) => void;
- dataType: TExtendedEntitiesTypeDict;
- dataKey: string;
filters?: CoreFilter[];
disabled?: boolean;
className?: HTMLProps['className'];
}) {
- const [activeColumnsLength, setActiveColumnsLength] = useState(undefined);
-
- const activeColumns = useAtomValue(
- useMemo(() => unwrap(coreActiveColumnsAtom({ dataType, key: dataKey })), [dataType, dataKey])
- );
-
const selectedFiltersCount = filters
? filters.filter((filter) => filterHasValue(filter)).length
: 0;
- useEffect(() => {
- if (activeColumns && activeColumns.length) {
- setActiveColumnsLength(activeColumns.length - 1);
- }
- }, [activeColumns]);
-
const onFilterClick = () => setDisplayControlPanel(!displayControlPanel);
+
return (