From 7d77ebfae77725ae7b3e5c2bc199edf73201c448 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Tue, 28 Jan 2025 15:22:38 +0100
Subject: [PATCH 01/17] Editable EntrySection.

---
 .../ui_demonstration/entry-data.archive.yaml  |  11 ++
 .../schema_packages/values_test_schema.py     |   6 +
 src/components/archive/Section.tsx            |  85 ++++++++++++
 src/components/archive/SectionProvider.tsx    |  14 ++
 src/components/archive/useArchive.test.tsx    |   5 +-
 src/components/archive/useArchive.tsx         | 129 +++++++++++++++---
 .../{editor => archive}/useSubSectionPath.tsx |   0
 src/components/editor/ImagePreviewItem.tsx    |   2 +-
 src/components/editor/PlotEditor.tsx          |   2 +-
 src/components/editor/QuantityEditor.tsx      |   2 +-
 src/components/editor/RichTextEditor.tsx      |   2 +-
 src/components/editor/SubSectionEditor.tsx    |  16 +--
 .../editor/SubSectionTableEditor.tsx          |   2 +-
 src/components/page/Page.tsx                  |   2 +-
 src/components/routing/Routes.tsx             |   9 +-
 src/components/routing/loader.ts              |   4 +-
 src/components/routing/types.ts               |  12 ++
 src/components/routing/useDataForRoute.tsx    |  12 ++
 src/hooks/useData.tsx                         |  11 +-
 src/hooks/useImmediateEffect.test.tsx         |  81 +++++++++++
 src/hooks/useImmediateEffect.tsx              |  39 ++++++
 src/pages/entry/EntryOverview.tsx             |  75 +++-------
 src/pages/entry/EntryPageTitle.tsx            |  65 +++++++++
 src/pages/entry/EntrySection.tsx              |  63 ++++-----
 src/pages/entry/entryRoute.tsx                |   5 +-
 25 files changed, 520 insertions(+), 134 deletions(-)
 create mode 100644 src/components/archive/Section.tsx
 create mode 100644 src/components/archive/SectionProvider.tsx
 rename src/components/{editor => archive}/useSubSectionPath.tsx (100%)
 create mode 100644 src/hooks/useImmediateEffect.test.tsx
 create mode 100644 src/hooks/useImmediateEffect.tsx
 create mode 100644 src/pages/entry/EntryPageTitle.tsx

diff --git a/infra/src/nomad_plugin_gui/example_uploads/ui_demonstration/entry-data.archive.yaml b/infra/src/nomad_plugin_gui/example_uploads/ui_demonstration/entry-data.archive.yaml
index b78a6314..2ae880cd 100644
--- a/infra/src/nomad_plugin_gui/example_uploads/ui_demonstration/entry-data.archive.yaml
+++ b/infra/src/nomad_plugin_gui/example_uploads/ui_demonstration/entry-data.archive.yaml
@@ -19,3 +19,14 @@ data:
     unit_quantity: 'm'
     section_reference: '#/data/referenced'
   referenced: {}
+  sub_section: {
+    string_quantity: 'Hello World'
+  }
+  repeated_sub_section: [
+    {
+      string_quantity: 'Hello World'
+    },
+    {
+      string_quantity: 'Hello World'
+    }
+  ]
diff --git a/infra/src/nomad_plugin_gui/schema_packages/values_test_schema.py b/infra/src/nomad_plugin_gui/schema_packages/values_test_schema.py
index 8897d637..8bcd161f 100644
--- a/infra/src/nomad_plugin_gui/schema_packages/values_test_schema.py
+++ b/infra/src/nomad_plugin_gui/schema_packages/values_test_schema.py
@@ -53,9 +53,15 @@ class QuantityTypes(MSection):
 
     section_reference = Quantity(type=Reference(Referenced))
 
+class ASection(MSection):
+    string_quantity = Quantity(type=str)
 
 class Main(Schema):
     quantity_types = SubSection(sub_section=QuantityTypes)
+    sub_section = SubSection(sub_section=ASection)
+    repeated_sub_section = SubSection(sub_section=ASection, repeats=True)
+    empty_sub_section = SubSection(sub_section=ASection)
+    empty_repeated_sub_section = SubSection(sub_section=ASection, repeats=True)
     referenced = SubSection(sub_section=Referenced)
 
 
diff --git a/src/components/archive/Section.tsx b/src/components/archive/Section.tsx
new file mode 100644
index 00000000..a433e0fa
--- /dev/null
+++ b/src/components/archive/Section.tsx
@@ -0,0 +1,85 @@
+import {Card, CardContent, CardHeader} from '@mui/material'
+import {useCallback} from 'react'
+
+import {MSectionResponse} from '../../models/graphResponseModels'
+import {
+  Quantity as QuantityDefinition,
+  Section as SectionDefinition,
+} from '../../utils/metainfo'
+import {JSONValue} from '../../utils/types'
+import {assert} from '../../utils/utils'
+import ErrorBoundary from '../ErrorBoundary'
+import {getPropertyLabel} from '../editor/PropertyEditor'
+import DynamicValue from '../values/utils/DynamicValue'
+import {createDynamicComponentSpec} from '../values/utils/dynamicComponents'
+import SectionProvider from './SectionProvider'
+import {useArchiveProperty} from './useArchive'
+import useSubSectionPath from './useSubSectionPath'
+
+type QantityProps = {
+  definition: QuantityDefinition
+  editable?: boolean
+}
+
+function Quantity({definition, editable = false}: QantityProps) {
+  const path = useSubSectionPath(definition.name)
+
+  const {value, loading, change} = useArchiveProperty<
+    JSONValue | undefined,
+    QuantityDefinition
+  >(path)
+
+  assert(!loading, 'Loading should already be handled')
+
+  const handleChange = useCallback(
+    (newValue: JSONValue | undefined) => {
+      change('upsert', newValue)
+    },
+    [change],
+  )
+
+  const component = createDynamicComponentSpec(definition)
+
+  return (
+    <DynamicValue
+      component={component}
+      label={getPropertyLabel(definition.name)}
+      placeholder='no value'
+      fullWidth
+      editable={editable}
+      value={value}
+      onChange={handleChange}
+    />
+  )
+}
+
+export type SectionProps = {
+  editable?: boolean
+  path: string
+}
+
+export default function Section({path, editable = false}: SectionProps) {
+  const {value} = useArchiveProperty<MSectionResponse>(path)
+  const definition = value?.m_def as SectionDefinition
+
+  assert(value !== undefined, 'The section should have been loaded by now')
+  assert(
+    definition !== undefined,
+    'The section definition should have been loaded by now',
+  )
+
+  return (
+    <Card>
+      <CardHeader title='Quantities' />
+      <CardContent sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
+        <SectionProvider path={path}>
+          {Object.values(definition.all_quantities).map((quantity) => (
+            <ErrorBoundary key={quantity.name}>
+              <Quantity definition={quantity} editable={editable} />
+            </ErrorBoundary>
+          ))}
+        </SectionProvider>
+      </CardContent>
+    </Card>
+  )
+}
diff --git a/src/components/archive/SectionProvider.tsx b/src/components/archive/SectionProvider.tsx
new file mode 100644
index 00000000..851e9390
--- /dev/null
+++ b/src/components/archive/SectionProvider.tsx
@@ -0,0 +1,14 @@
+import {PropsWithChildren} from 'react'
+
+import {subSectionContext} from './useSubSectionPath'
+
+export default function SectionProvider({
+  path: path,
+  children,
+}: {path: string} & PropsWithChildren) {
+  return (
+    <subSectionContext.Provider value={{path}}>
+      {children}
+    </subSectionContext.Provider>
+  )
+}
diff --git a/src/components/archive/useArchive.test.tsx b/src/components/archive/useArchive.test.tsx
index 286c36c2..3badc1de 100644
--- a/src/components/archive/useArchive.test.tsx
+++ b/src/components/archive/useArchive.test.tsx
@@ -167,14 +167,14 @@ describe('useArchive', () => {
         addDefinitions(response, {q: 1, s: {}, r: []})
 
         archive.archive = initialArchive
-        const updates = archive.updateArchive(request, response, {})
+        const {changes} = archive.updateArchive(request, response, {})
         expect(archive.archive.m_def).not.toBeUndefined()
 
         // remove definitions to make the comparison simpler
         removeDefinitions(archive.archive)
 
         expect(archive.archive).toEqual(expectedArchive)
-        expect(updates).toEqual(expectedUpdates)
+        expect(changes).toEqual(expectedUpdates)
       },
     )
   })
@@ -292,6 +292,7 @@ describe('useArchiveProperty', () => {
     'quantity',
     'sub_section/quantity',
     'repeating_sub_section/0/quantity',
+    'repeating_sub_section[0]/quantity',
     'sub_section/nested_sub_section/quantity',
     'sub_section',
     'sub_section/nested_sub_section',
diff --git a/src/components/archive/useArchive.tsx b/src/components/archive/useArchive.tsx
index 1848762f..e0ec92f0 100644
--- a/src/components/archive/useArchive.tsx
+++ b/src/components/archive/useArchive.tsx
@@ -17,6 +17,11 @@ import {assert} from '../../utils/utils'
 import {getIndexedKey} from '../routing/loader'
 import useRouteData from '../routing/useRouteData'
 
+export type ArchiveChangeConflict = {
+  type: 'parent' | 'child' | 'exact'
+  change: ArchiveChange
+}
+
 class MultiMap<K, V> {
   map = new Map<K, V[]>()
 
@@ -72,10 +77,7 @@ export class Archive {
     this.archive = {}
     this.changeStack = []
     this.responsesWithReferencedArchives = new MultiMap()
-
-    // TODO loading needs to be integrated with routing and the use of
-    // useDataForRoute
-    this.loading = true
+    this.loading = false
   }
 
   registerElement(path: string, dispatch: ArchivePropertyDispatch) {
@@ -118,7 +120,7 @@ export class Archive {
   ) {
     // apply the value to the archive
     let current = this.archive
-    const parts = path.split('/')
+    const parts = normalizeArchivePath(path)
     const subSections = parts.slice(0, -1)
     for (const key of subSections) {
       current = current[key] as MSectionResponse
@@ -173,7 +175,7 @@ export class Archive {
     if (path === '') {
       return current as T
     }
-    for (const key of path.split('/')) {
+    for (const key of normalizeArchivePath(path)) {
       if (current[key] === undefined) {
         return undefined
       }
@@ -215,10 +217,48 @@ export class Archive {
     return resolved
   }
 
+  /**
+   * Detects conflicts between changing the given path and the changes in the
+   * current chanage stack. The function returns 'parent' if the path is a
+   * prefix to a path in the change stack, 'child' if a path in the change stack
+   * is a prefix to the given path, 'none' if there is no conflict, and 'exact'
+   * of the path is exactly the same as a path in the change stack.
+   * If multiple conflicting changes exist, the function returns the change
+   * with the shortest path.
+   */
+  conflictsWithChangeStack(path: string): ArchiveChangeConflict | undefined {
+    // TODO if this ever causes performance issues (On), we need to use some more
+    // efficient data structure here, e.g. represent the change stack as a
+    // tree (Ologn).
+    const conflicts: ArchiveChangeConflict[] = []
+    for (const change of this.changeStack) {
+      if (change.path === path) {
+        conflicts.push({type: 'exact', change})
+      }
+      if (change.path.startsWith(path)) {
+        conflicts.push({type: 'parent', change})
+      }
+      if (path.startsWith(change.path)) {
+        conflicts.push({type: 'child', change})
+      }
+    }
+
+    if (conflicts.length === 0) {
+      return undefined
+    }
+
+    return conflicts.sort(
+      (a, b) => a.change.path.length - b.change.path.length,
+    )[0]
+  }
+
   /**
    * Merges new archive data represented as a request/response pair into the
    * archive. It detects a collection of changes and dispatches them to the
    * registered elements.
+   *
+   * TODO what happens when the update effects something that we already
+   * have local user changes for?
    */
   updateArchive(
     request: MSectionRequest,
@@ -227,6 +267,7 @@ export class Archive {
     path?: string,
   ) {
     const changes: [string, unknown][] = []
+    const conflicts: ArchiveChangeConflict[] = []
 
     // collect referenced archives (this might include the current one for
     // intra-entry references)
@@ -251,6 +292,16 @@ export class Archive {
       })
     })
 
+    const checkForConflicts = (path: string) => {
+      const conflict = this.conflictsWithChangeStack(path)
+      if (conflict) {
+        conflicts.push(conflict)
+        return true
+      } else {
+        return false
+      }
+    }
+
     // traverse
     const visit = (
       path: string,
@@ -311,6 +362,10 @@ export class Archive {
 
         if (response[key] === undefined) {
           // delete
+          if (checkForConflicts(pathForCurrentKey)) {
+            continue
+          }
+
           if (!archive[key]) {
             continue
           }
@@ -331,6 +386,10 @@ export class Archive {
           changes.push([pathForCurrentKey, response[key]])
         } else if (archive[key] === undefined) {
           // add
+          if (checkForConflicts(pathForCurrentKey)) {
+            continue
+          }
+
           if (indexed) {
             const archiveList =
               (archive[key] as unknown as MSectionResponse[]) || []
@@ -345,9 +404,17 @@ export class Archive {
         } else {
           // update
           if (!childIsSubSection) {
+            if (checkForConflicts(pathForCurrentKey)) {
+              continue
+            }
+
             archive[key] = response[key]
             changes.push([pathForCurrentKey, response[key]])
           } else if (childIsRepeatingSubSection) {
+            if (checkForConflicts(pathForCurrentKey)) {
+              continue
+            }
+
             // The semantics is that the sections with the same index are
             // the same. Even if a section is removed from the middle of the
             // list.
@@ -419,29 +486,40 @@ export class Archive {
       visit('', this.archive, request, response)
     }
 
-    // TODO the loading state needs to be properly handled.
-    if (this.loading) {
-      this.loading = false
-      for (const [path, elements] of this.elements.map.entries()) {
-        for (const dispatch of elements) {
+    if (!this.loading) {
+      for (const [path, value] of changes) {
+        for (const dispatch of this.elements.get(path)) {
           dispatch?.({
-            value: this.getValue(path),
+            value,
             loading: false,
           })
         }
       }
-    } else {
-      for (const [path, value] of changes) {
-        for (const dispatch of this.elements.get(path)) {
+    }
+
+    return {
+      changes,
+      conflicts,
+    }
+  }
+
+  setLoading(loading: boolean) {
+    if (this.loading === loading) {
+      return
+    }
+
+    this.loading = loading
+
+    if (!this.loading) {
+      for (const [path, elements] of this.elements.map.entries()) {
+        for (const dispatch of elements) {
           dispatch?.({
-            value,
+            value: this.getValue(path),
             loading: false,
           })
         }
       }
     }
-
-    return changes
   }
 
   /**
@@ -458,11 +536,13 @@ export class Archive {
     assert(pathWithoutIndex, `Invalid property path ${path}`)
 
     let section = this.archive
+    assert(section, 'No archive loaded')
     const parts = path.split('/')
     const subSections = parts.slice(0, -1)
     const property = parts[parts.length - 1]
     for (const key of subSections) {
       section = section[key] as MSectionResponse
+      assert(section, `No section for path ${path}, ${key} is missing`)
     }
     const sectionDef = section.m_def as Section
     const propertyDef = sectionDef?.all_properties?.[property]
@@ -570,6 +650,19 @@ export type ArchiveProperty<Type, DefinitionType extends Property> = {
   resolveRef: (ref: string) => MSectionResponse | undefined
 }
 
+function normalizeArchivePath(path: string) {
+  const result: string[] = []
+  path.split('/').forEach((segment) => {
+    const match = segment.match(/([^[]+)\[(\d+)\]$/)
+    if (match) {
+      result.push(match[1], match[2])
+    } else {
+      result.push(segment)
+    }
+  })
+  return result
+}
+
 /**
  * A hook that interacts with the archive of the current entry. It allows
  * to get, set, and react to changes of individual properties in the archive.
diff --git a/src/components/editor/useSubSectionPath.tsx b/src/components/archive/useSubSectionPath.tsx
similarity index 100%
rename from src/components/editor/useSubSectionPath.tsx
rename to src/components/archive/useSubSectionPath.tsx
diff --git a/src/components/editor/ImagePreviewItem.tsx b/src/components/editor/ImagePreviewItem.tsx
index 96d89a3d..7ca4b4f6 100644
--- a/src/components/editor/ImagePreviewItem.tsx
+++ b/src/components/editor/ImagePreviewItem.tsx
@@ -1,9 +1,9 @@
 import {Skeleton} from '@mui/material'
 
 import {useArchiveProperty} from '../archive/useArchive'
+import useSubSectionPath from '../archive/useSubSectionPath'
 import {LayoutItem} from '../layout/Layout'
 import {AbstractProperty} from './PropertyEditor'
-import useSubSectionPath from './useSubSectionPath'
 
 export type ImagePreview = AbstractProperty & {
   type: 'imagePreview'
diff --git a/src/components/editor/PlotEditor.tsx b/src/components/editor/PlotEditor.tsx
index 0f007647..4e55e97e 100644
--- a/src/components/editor/PlotEditor.tsx
+++ b/src/components/editor/PlotEditor.tsx
@@ -3,9 +3,9 @@ import {useLayoutEffect, useRef} from 'react'
 import {useAsync} from 'react-use'
 
 import {useArchiveProperty} from '../archive/useArchive'
+import useSubSectionPath from '../archive/useSubSectionPath'
 import {LayoutItem} from '../layout/Layout'
 import {AbstractProperty} from './PropertyEditor'
-import useSubSectionPath from './useSubSectionPath'
 
 export type Plot = AbstractProperty & {
   type: 'plot'
diff --git a/src/components/editor/QuantityEditor.tsx b/src/components/editor/QuantityEditor.tsx
index 987bf2de..6eec46fa 100644
--- a/src/components/editor/QuantityEditor.tsx
+++ b/src/components/editor/QuantityEditor.tsx
@@ -3,6 +3,7 @@ import {useCallback} from 'react'
 
 import {Quantity as QuantityDefinition} from '../../utils/metainfo'
 import {useArchiveProperty} from '../archive/useArchive'
+import useSubSectionPath from '../archive/useSubSectionPath'
 import {LayoutItem} from '../layout/Layout'
 import ContainerProps, {
   DynamicContainerProps,
@@ -13,7 +14,6 @@ import {
   createDynamicComponentSpec,
 } from '../values/utils/dynamicComponents'
 import {AbstractProperty, getLabel} from './PropertyEditor'
-import useSubSectionPath from './useSubSectionPath'
 
 export type Quantity = AbstractProperty & {
   type: 'quantity'
diff --git a/src/components/editor/RichTextEditor.tsx b/src/components/editor/RichTextEditor.tsx
index 2c443b36..7f48ed0f 100644
--- a/src/components/editor/RichTextEditor.tsx
+++ b/src/components/editor/RichTextEditor.tsx
@@ -2,10 +2,10 @@ import {Skeleton} from '@mui/material'
 import {useCallback, useRef} from 'react'
 
 import {useArchiveProperty} from '../archive/useArchive'
+import useSubSectionPath from '../archive/useSubSectionPath'
 import {LayoutItem} from '../layout/Layout'
 import Editor, {RichTextEditorProps} from '../richText/RichTextEditor'
 import {AbstractProperty, getLabel} from './PropertyEditor'
-import useSubSectionPath from './useSubSectionPath'
 
 export type RichText = AbstractProperty & {
   type: 'richText'
diff --git a/src/components/editor/SubSectionEditor.tsx b/src/components/editor/SubSectionEditor.tsx
index 8b67c02b..d4951fd6 100644
--- a/src/components/editor/SubSectionEditor.tsx
+++ b/src/components/editor/SubSectionEditor.tsx
@@ -1,27 +1,17 @@
 import {Box, Button, IconButton} from '@mui/material'
 import Grid from '@mui/material/Grid2'
-import {PropsWithChildren, useCallback} from 'react'
+import {useCallback} from 'react'
 
 import {MSectionResponse} from '../../models/graphResponseModels'
 import {SubSection as SubSectionDefinition} from '../../utils/metainfo'
 import {assert, splice} from '../../utils/utils'
+import SectionProvider from '../archive/SectionProvider'
 import {ArchiveProperty, useArchiveProperty} from '../archive/useArchive'
+import useSubSectionPath from '../archive/useSubSectionPath'
 import {Remove} from '../icons'
 import {Item, Layout, LayoutItem} from '../layout/Layout'
 import {layoutPropsContext} from '../layout/useLayoutProps'
 import {AbstractProperty, getLabel} from './PropertyEditor'
-import useSubSectionPath, {subSectionContext} from './useSubSectionPath'
-
-function SectionProvider({
-  path: path,
-  children,
-}: {path: string} & PropsWithChildren) {
-  return (
-    <subSectionContext.Provider value={{path}}>
-      {children}
-    </subSectionContext.Provider>
-  )
-}
 
 function SubSectionComponent({
   label,
diff --git a/src/components/editor/SubSectionTableEditor.tsx b/src/components/editor/SubSectionTableEditor.tsx
index 28daf9b2..3258426d 100644
--- a/src/components/editor/SubSectionTableEditor.tsx
+++ b/src/components/editor/SubSectionTableEditor.tsx
@@ -8,6 +8,7 @@ import {
 } from '../../utils/metainfo'
 import {assert, splice} from '../../utils/utils'
 import useArchive, {useArchiveProperty} from '../archive/useArchive'
+import useSubSectionPath from '../archive/useSubSectionPath'
 import {LayoutItem} from '../layout/Layout'
 import RichTableEditor, {
   RichTableEditorColumn,
@@ -23,7 +24,6 @@ import {
 } from '../values/utils/dynamicComponents'
 import {AbstractProperty, getLabel} from './PropertyEditor'
 import {Quantity} from './QuantityEditor'
-import useSubSectionPath from './useSubSectionPath'
 
 export type SubSectionTable = AbstractProperty & {
   type: 'table'
diff --git a/src/components/page/Page.tsx b/src/components/page/Page.tsx
index 32c6f985..9c13f570 100644
--- a/src/components/page/Page.tsx
+++ b/src/components/page/Page.tsx
@@ -233,7 +233,7 @@ const PageRoot = styled('div', {
   height: '100%',
 }))
 
-interface PageTitleProps {
+export interface PageTitleProps {
   /** The subtitle of the page or part of the page, shown below the title. */
   subtitle?: string | ReactNode
   /** The title of the page or part of the page. */
diff --git a/src/components/routing/Routes.tsx b/src/components/routing/Routes.tsx
index 5f85b543..7b984133 100644
--- a/src/components/routing/Routes.tsx
+++ b/src/components/routing/Routes.tsx
@@ -258,9 +258,16 @@ export default function Routes({
     value: RouteContextValue,
     component: ElementType,
   ) => {
+    let key: string | undefined = undefined
+    if (value.fullMatch[value.index].route.renderWithPathAsKey) {
+      key = value.fullMatch
+        .slice(0, value.index + 1)
+        .map((match) => match.path)
+        .join('/')
+    }
     return React.createElement(
       routeContext.Provider,
-      {value},
+      {value, key},
       React.createElement(component),
     )
   }
diff --git a/src/components/routing/loader.ts b/src/components/routing/loader.ts
index 1a7fd583..0eeb1847 100644
--- a/src/components/routing/loader.ts
+++ b/src/components/routing/loader.ts
@@ -125,6 +125,9 @@ export function createRequestForRoute(
     if (request) {
       assert(path !== undefined, 'Request for empty paths are not supported.')
       requests[path] = request
+      if (routeRequests) {
+        routeRequests[index] = request
+      }
     }
     parentPath = path
   })
@@ -142,7 +145,6 @@ export function createRequestForRoute(
     Object.assign(currentRequest, request)
   })
 
-  routeRequests?.push(rootRequest)
   return rootRequest
 }
 
diff --git a/src/components/routing/types.ts b/src/components/routing/types.ts
index db027b48..a5cf8845 100644
--- a/src/components/routing/types.ts
+++ b/src/components/routing/types.ts
@@ -204,6 +204,18 @@ export type Route<
    */
   lazyComponent?: () => Promise<{default: ElementType}>
 
+  /**
+   * React determines component identities and subsequent updattes, mounts, and
+   * unmounts based on a components position in the tree. If the path of
+   * variable routes changes, the component will still be treated as the same
+   * and it will only be updated. This is not always desired, e.g. if entryId
+   * changes, you usually want to start from scratch. If `renderWithPathAsKey`
+   * is set to true, the component will be rendered with a key that is set
+   * to the path. So if the path changes, it is guaranteed that the old component
+   * gets unmounted and a new component is created.
+   */
+  renderWithPathAsKey?: boolean
+
   /**
    * If defined, a given component will only be rendered if the current location
    * matches on of the given routes. Routes can be given as `Route` objects
diff --git a/src/components/routing/useDataForRoute.tsx b/src/components/routing/useDataForRoute.tsx
index bd541669..c1e22963 100644
--- a/src/components/routing/useDataForRoute.tsx
+++ b/src/components/routing/useDataForRoute.tsx
@@ -2,6 +2,7 @@ import {useMemo} from 'react'
 import {useLatest} from 'react-use'
 
 import useData, {UseDataParams, UseDataResult} from '../../hooks/useData'
+import {GraphRequest} from '../../models/graphRequestModels'
 import {GraphResponse} from '../../models/graphResponseModels'
 import {DefaultToObject, JSONObject} from '../../utils/types'
 import {createRequestForRoute, getResponsesForRoute} from './loader'
@@ -29,6 +30,7 @@ export default function useDataForRoute<
 >({
   request,
   route,
+  onBeforeFetch: onBeforeFetchForRoute,
   onFetch: onFetchForRoute,
   onError,
   reloadCount,
@@ -51,6 +53,15 @@ export default function useDataForRoute<
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [request])
 
+  const onBeforeFetch = useMemo(() => {
+    if (!onBeforeFetchForRoute) {
+      return undefined
+    }
+    return (_: JSONObject, fullRequest: GraphRequest) => {
+      onBeforeFetchForRoute(request as Request, fullRequest)
+    }
+  }, [onBeforeFetchForRoute, request])
+
   const onFetch = useMemo(() => {
     if (!onFetchForRoute) {
       return undefined
@@ -65,6 +76,7 @@ export default function useDataForRoute<
 
   const {data, ...otherUseDataResults} = useData({
     request: fullRequest,
+    onBeforeFetch,
     onFetch,
     onError,
     ...useDataParams,
diff --git a/src/hooks/useData.tsx b/src/hooks/useData.tsx
index 31e9cd82..86470bed 100644
--- a/src/hooks/useData.tsx
+++ b/src/hooks/useData.tsx
@@ -10,6 +10,7 @@ import {graphApi} from '../utils/api'
 import {resolveAllMDefs} from '../utils/metainfo'
 import {DefaultToObject, JSONObject, JSONValue} from '../utils/types'
 import useAuth from './useAuth'
+import useImmediateEffect from './useImmediateEffect'
 import useRender from './useRender'
 
 /**
@@ -63,6 +64,11 @@ export type UseDataParams<Request, Response> = {
    */
   noImplicitFetch?: boolean
 
+  /**
+   * An optional callback that is called before the fetch is started.
+   */
+  onBeforeFetch?: (request: Request, fullRequest: GraphRequest) => void
+
   /**
    * An optional callback that is called when the data was successfully
    * received. This allows to trigger state changes now, instead of needing
@@ -100,6 +106,7 @@ export default function useData<
 >({
   request,
   noImplicitFetch = false,
+  onBeforeFetch,
   onFetch,
   onError,
 }: UseDataParams<Request, Response>): UseDataResult<Response> {
@@ -120,6 +127,7 @@ export default function useData<
     requestRef.current = request
     const fetch = async () => {
       normalizedRequestRef.current = normalizeGraph(request as JSONObject)
+      onBeforeFetch?.(request, normalizedRequestRef.current)
       try {
         const result = await graphApi(
           normalizedRequestRef.current,
@@ -151,12 +159,13 @@ export default function useData<
     setData,
     normalizedRequestRef,
     request,
+    onBeforeFetch,
     onFetch,
     onError,
     user,
   ])
 
-  useEffect(() => {
+  useImmediateEffect(() => {
     if (!noImplicitFetch) {
       return fetch()
     }
diff --git a/src/hooks/useImmediateEffect.test.tsx b/src/hooks/useImmediateEffect.test.tsx
new file mode 100644
index 00000000..60bc7e1f
--- /dev/null
+++ b/src/hooks/useImmediateEffect.test.tsx
@@ -0,0 +1,81 @@
+import {renderHook} from '@testing-library/react'
+import {vi} from 'vitest'
+
+import {JSONObject} from '../utils/types'
+import useImmediateEffect from './useImmediateEffect'
+
+it('calls effect on render without deps', () => {
+  const effect = vi.fn()
+  renderHook(() => {
+    return useImmediateEffect(effect, [])
+  })
+  expect(effect).toHaveBeenCalledTimes(1)
+})
+
+it('calls effect on render with deps', () => {
+  const effect = vi.fn()
+  renderHook(() => {
+    return useImmediateEffect(effect, [true])
+  })
+  expect(effect).toHaveBeenCalledTimes(1)
+})
+
+it('does not call effect on re-render with unchanged deps', () => {
+  const effect = vi.fn()
+  const {rerender} = renderHook(
+    ({dep}: {dep: boolean}) => {
+      useImmediateEffect(effect, [dep])
+    },
+    {initialProps: {dep: false}},
+  )
+  expect(effect).toHaveBeenCalledTimes(1)
+
+  rerender({dep: false})
+  expect(effect).toHaveBeenCalledTimes(1)
+})
+
+it('calls effect on re-render with changed deps', () => {
+  const effect = vi.fn()
+  const {rerender} = renderHook(
+    ({dep}: {dep: boolean}) => {
+      useImmediateEffect(effect, [dep])
+    },
+    {initialProps: {dep: false}},
+  )
+  expect(effect).toHaveBeenCalledTimes(1)
+
+  rerender({dep: true})
+  expect(effect).toHaveBeenCalledTimes(2)
+})
+
+it('calls effect on re-render with changed object deps', () => {
+  const effect = vi.fn()
+  const obj = {a: 1}
+  const {rerender} = renderHook(
+    ({dep}: {dep: JSONObject}) => {
+      useImmediateEffect(effect, [dep])
+    },
+    {initialProps: {dep: obj}},
+  )
+  expect(effect).toHaveBeenCalledTimes(1)
+
+  rerender({dep: {...obj}})
+  expect(effect).toHaveBeenCalledTimes(2)
+})
+
+it('calls destructuor', () => {
+  const destructor = vi.fn()
+  const {rerender} = renderHook(
+    ({dep}: {dep: boolean}) => {
+      useImmediateEffect(() => destructor, [dep])
+    },
+    {initialProps: {dep: false}},
+  )
+  expect(destructor).toHaveBeenCalledTimes(0)
+
+  rerender({dep: false})
+  expect(destructor).toHaveBeenCalledTimes(0)
+
+  rerender({dep: true})
+  expect(destructor).toHaveBeenCalledTimes(1)
+})
diff --git a/src/hooks/useImmediateEffect.tsx b/src/hooks/useImmediateEffect.tsx
new file mode 100644
index 00000000..311f4bfe
--- /dev/null
+++ b/src/hooks/useImmediateEffect.tsx
@@ -0,0 +1,39 @@
+import {DependencyList, useCallback, useEffect, useRef} from 'react'
+import {usePrevious} from 'react-use'
+
+/**
+ * A hook that has the same interface and behavior as `useEffect` but runs the
+ * effect "immediately" upon beeing called and within the current render.
+ */
+export default function useImmediateEffect(
+  effect: React.EffectCallback,
+  deps: DependencyList,
+) {
+  let hasChanged = true !== usePrevious(true)
+  for (let i = 0; i < deps.length; i++) {
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    if (deps[i] !== usePrevious(deps[i])) {
+      hasChanged = true
+    }
+  }
+
+  const effectDestructor = useRef<ReturnType<React.EffectCallback>>()
+
+  if (hasChanged) {
+    if (effectDestructor.current) {
+      effectDestructor.current()
+    }
+    effectDestructor.current = effect()
+  }
+
+  const unmount = useCallback(() => {
+    if (effectDestructor.current) {
+      effectDestructor.current()
+      effectDestructor.current = undefined
+    }
+  }, [])
+
+  useEffect(() => {
+    return unmount
+  }, [unmount])
+}
diff --git a/src/pages/entry/EntryOverview.tsx b/src/pages/entry/EntryOverview.tsx
index 71b42405..0ccde221 100644
--- a/src/pages/entry/EntryOverview.tsx
+++ b/src/pages/entry/EntryOverview.tsx
@@ -1,18 +1,12 @@
-import GoToIcon from '@mui/icons-material/ChevronRight'
-import {Box, Button} from '@mui/material'
+import {Box} from '@mui/material'
 import {useCallback, useMemo, useState} from 'react'
 
 import ErrorMessage from '../../components/app/ErrorMessage'
-import EditStatus from '../../components/archive/EditStatus'
 import useArchive from '../../components/archive/useArchive'
 import {calculateRequestFromLayout} from '../../components/editor/utils'
 import {LayoutItem} from '../../components/layout/Layout'
 import useSelect from '../../components/navigation/useSelect'
-import {PageTitle} from '../../components/page/Page'
-import usePage from '../../components/page/usePage'
-import Link from '../../components/routing/Link'
 import useDataForRoute from '../../components/routing/useDataForRoute'
-import useRoute from '../../components/routing/useRoute'
 import useRouteData from '../../components/routing/useRouteData'
 import {EntryRequest, MSectionRequest} from '../../models/graphRequestModels'
 import {
@@ -25,16 +19,17 @@ import {assert} from '../../utils/utils'
 import UploadMetadataEditor from '../upload/UploadMetadataEditor'
 import EntryDataEditor from './EntryDataEditor'
 import EntryMetadataEditor from './EntryMetadataEditor'
+import EntryPageTitle from './EntryPageTitle'
 import EntrySubSectionTable from './EntrySubSectionTable'
 import entryRoute, {archiveRequest} from './entryRoute'
 
 function EntryOverviewEditor() {
   const {archive: archiveData} = useRouteData(entryRoute)
   const [error, setError] = useState<Error | undefined>()
-  const schema = (archiveData?.data as MSectionResponse)?.m_def as Section
   const {isPage} = useSelect()
 
   const layout = useMemo(() => {
+    const schema = (archiveData?.data as MSectionResponse)?.m_def as Section
     let layout: LayoutItem
     const layouts = schema?.m_annotations?.layout as unknown as
       | LayoutItem
@@ -53,7 +48,7 @@ function EntryOverviewEditor() {
       property: 'data',
       layout,
     } satisfies LayoutItem
-  }, [schema])
+  }, [archiveData])
 
   const request = useMemo(() => {
     const request = {
@@ -73,6 +68,10 @@ function EntryOverviewEditor() {
 
   const archive = useArchive()
 
+  const onBeforeFetch = useCallback(() => {
+    archive.setLoading(true)
+  }, [archive])
+
   const onFetch = useCallback(
     (data: EntryResponse, fullResponse: GraphResponse) => {
       archive.updateArchive(
@@ -80,14 +79,24 @@ function EntryOverviewEditor() {
         data?.archive as MSectionResponse,
         fullResponse,
       )
+      archive.setLoading(false)
     },
     [archive, request],
   )
 
+  const onError = useCallback(
+    (error: Error) => {
+      archive.setLoading(false)
+      setError(error)
+    },
+    [archive],
+  )
+
   useDataForRoute<EntryRequest, EntryResponse>({
     request,
+    onBeforeFetch,
     onFetch,
-    onError: setError,
+    onError,
   })
 
   let content: React.ReactNode = ''
@@ -119,35 +128,13 @@ function EntryOverviewEditor() {
 }
 
 export default function EntryOverview() {
-  const {url} = useRoute()
-  const {
-    upload_id,
-    mainfile_path,
-    archive: rootSectionData,
-  } = useRouteData(entryRoute)
+  const {archive: rootSectionData} = useRouteData(entryRoute)
   assert(
     rootSectionData !== undefined,
     'An entry should always have a root section',
   )
 
   const {isPage, isSelect} = useSelect()
-  const {isScrolled} = usePage()
-
-  const actions = (
-    <Box display='flex' alignItems='center' flexDirection='row' gap={1}>
-      <EditStatus />
-      <Button
-        variant='contained'
-        component={Link}
-        to={url({
-          path: `/uploads/${upload_id}/files/${mainfile_path}`,
-        })}
-        endIcon={<GoToIcon />}
-      >
-        Go to File
-      </Button>
-    </Box>
-  )
 
   // This memo is an optimization to avoid re-rendering the entire
   // EntryOverviewEditor after usePage causes a srcoll event triggered
@@ -170,27 +157,7 @@ export default function EntryOverview() {
 
   return (
     <>
-      {isPage && (
-        <PageTitle
-          sx={{
-            position: 'sticky',
-            top: 0,
-            marginTop: -1,
-            marginX: -2,
-            paddingX: 2,
-            paddingY: 1,
-            background: (theme) => theme.palette.background.default,
-            zIndex: (theme) => theme.zIndex.appBar,
-            ...(isScrolled
-              ? {
-                  borderBottom: '1px solid',
-                  borderColor: (theme) => theme.palette.divider,
-                }
-              : {}),
-          }}
-          actions={actions}
-        />
-      )}
+      {isPage && <EntryPageTitle />}
       {pageContent}
     </>
   )
diff --git a/src/pages/entry/EntryPageTitle.tsx b/src/pages/entry/EntryPageTitle.tsx
new file mode 100644
index 00000000..9057baab
--- /dev/null
+++ b/src/pages/entry/EntryPageTitle.tsx
@@ -0,0 +1,65 @@
+import GoToIcon from '@mui/icons-material/ChevronRight'
+import {Box, Button} from '@mui/material'
+
+import EditStatus from '../../components/archive/EditStatus'
+import {PageTitle, PageTitleProps} from '../../components/page/Page'
+import usePage from '../../components/page/usePage'
+import Link from '../../components/routing/Link'
+import useRoute from '../../components/routing/useRoute'
+import useRouteData from '../../components/routing/useRouteData'
+import {assert} from '../../utils/utils'
+import entryRoute from './entryRoute'
+
+export default function EntryPageTitle(props: PageTitleProps) {
+  const {url} = useRoute()
+  const {
+    upload_id,
+    mainfile_path,
+    archive: rootSectionData,
+  } = useRouteData(entryRoute)
+  assert(
+    rootSectionData !== undefined,
+    'An entry should always have a root section',
+  )
+
+  const {isScrolled} = usePage()
+
+  const actions = (
+    <Box display='flex' alignItems='center' flexDirection='row' gap={1}>
+      <EditStatus />
+      <Button
+        variant='contained'
+        component={Link}
+        to={url({
+          path: `/uploads/${upload_id}/files/${mainfile_path}`,
+        })}
+        endIcon={<GoToIcon />}
+      >
+        Go to File
+      </Button>
+    </Box>
+  )
+
+  return (
+    <PageTitle
+      sx={{
+        position: 'sticky',
+        top: 0,
+        marginTop: -1,
+        marginX: -2,
+        paddingX: 2,
+        paddingY: 1,
+        background: (theme) => theme.palette.background.default,
+        zIndex: (theme) => theme.zIndex.appBar,
+        ...(isScrolled
+          ? {
+              borderBottom: '1px solid',
+              borderColor: (theme) => theme.palette.divider,
+            }
+          : {}),
+      }}
+      actions={actions}
+      {...props}
+    />
+  )
+}
diff --git a/src/pages/entry/EntrySection.tsx b/src/pages/entry/EntrySection.tsx
index 0332a176..5c7818fb 100644
--- a/src/pages/entry/EntrySection.tsx
+++ b/src/pages/entry/EntrySection.tsx
@@ -2,16 +2,15 @@ import {Box, Card, CardContent, CardHeader} from '@mui/material'
 import {useMemo} from 'react'
 
 import ErrorBoundary from '../../components/ErrorBoundary'
-import QuantityValue from '../../components/archive/QuantityValue'
+import Section from '../../components/archive/Section'
 import JsonViewer from '../../components/fileviewer/JsonViewer'
-import {PageTitle} from '../../components/page/Page'
 import Outlet from '../../components/routing/Outlet'
 import useRoute from '../../components/routing/useRoute'
 import useRouteData from '../../components/routing/useRouteData'
 import {MSectionRequest} from '../../models/graphRequestModels'
 import {MSectionResponse} from '../../models/graphResponseModels'
-import {Section as SectionDefinition} from '../../utils/metainfo'
-import {JSONObject, JSONValue} from '../../utils/types'
+import {JSONObject} from '../../utils/types'
+import EntryPageTitle from './EntryPageTitle'
 import EntrySubSectionTable from './EntrySubSectionTable'
 
 const sortRawDataKeys = (a: string, b: string) => {
@@ -24,48 +23,38 @@ const sortRawDataKeys = (a: string, b: string) => {
   return 1
 }
 
-function Section() {
-  const data = useRouteData<MSectionRequest, MSectionResponse>()
-
-  const definition = data.m_def as SectionDefinition
-  const quantities = useMemo(() => {
-    return Object.keys(definition.all_quantities)
-      .filter((key) => data[key] !== undefined)
-      .map((key) => definition.all_quantities[key])
-    //.toSorted((a, b) => a.name.localeCompare(b.name))
-  }, [data, definition])
-
-  return (
-    <>
-      <EntrySubSectionTable data={data} />
-      <Card>
-        <CardHeader title='Quantities' />
-        <CardContent sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
-          {quantities.map((quantity) => (
-            <ErrorBoundary key={quantity.name}>
-              <QuantityValue
-                quantityDef={quantity}
-                value={data[quantity.name] as JSONValue | undefined}
-              />
-            </ErrorBoundary>
-          ))}
-        </CardContent>
-      </Card>
-    </>
-  )
-}
-
 export default function EntrySection() {
   const {isLeaf, fullMatch} = useRoute()
   const data = useRouteData<MSectionRequest, MSectionResponse>()
+  const sectionPath = useMemo(() => {
+    if (!isLeaf) {
+      return ''
+    }
+    const startIndex = fullMatch.findIndex(
+      (match) => match.route.path === 'archive',
+    )
+    return fullMatch
+      .slice(startIndex + 1)
+      .map((match) => match.path)
+      .join('/')
+  }, [fullMatch, isLeaf])
+
   if (!isLeaf) {
     return <Outlet />
   }
+
   return (
     <>
-      <PageTitle title={fullMatch[fullMatch.length - 1].path} />
+      <EntryPageTitle title={fullMatch[fullMatch.length - 1].path} />
       <Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
-        {data.m_def && <Section />}
+        <ErrorBoundary>
+          <EntrySubSectionTable data={data} />
+        </ErrorBoundary>
+        <ErrorBoundary>
+          {data.m_def && data.m_def.name !== 'EntryArchive' && (
+            <Section path={sectionPath} editable />
+          )}
+        </ErrorBoundary>
         <Card>
           <CardHeader title='Raw data' />
           <CardContent>
diff --git a/src/pages/entry/entryRoute.tsx b/src/pages/entry/entryRoute.tsx
index 424efebd..725e9b3b 100644
--- a/src/pages/entry/entryRoute.tsx
+++ b/src/pages/entry/entryRoute.tsx
@@ -31,6 +31,7 @@ export const archiveRoute: Route<MSectionRequest, MSectionResponse> = {
   path: ':name',
   request: {...archiveRequest},
   lazyComponent: async () => import('./EntrySection'),
+  renderWithPathAsKey: true,
 }
 archiveRoute.children = [archiveRoute]
 
@@ -52,6 +53,7 @@ const entryRoute: Route<EntryRequest, EntryResponse> = {
     archive: {...archiveRequest},
   },
   lazyComponent: async () => import('./EntryPage'),
+  renderWithPathAsKey: true,
   breadcrumb: ({response}) => path.basename(response?.mainfile_path as string),
   children: [
     {
@@ -71,12 +73,13 @@ const entryRoute: Route<EntryRequest, EntryResponse> = {
     }
     const entryId = match.path
     const archive = getArchive(entryId)
-
+    archive.setLoading(true)
     archive.updateArchive(
       request.archive,
       response.archive,
       fullResponse as GraphResponse,
     )
+    archive.setLoading(false)
   },
 }
 
-- 
GitLab


From 32d22275971d71a0f5e1750f66c9157fed488221 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Wed, 29 Jan 2025 17:54:41 +0100
Subject: [PATCH 02/17] Reconciling archive updates with current changes.

---
 docs/Archive.mdx                           |  12 +-
 src/components/archive/archive.helper.tsx  |  10 +-
 src/components/archive/useArchive.test.tsx |  91 ++++++++++++---
 src/components/archive/useArchive.tsx      | 123 +++++++++++++++------
 src/components/routing/Routes.tsx          |  52 ++++-----
 src/components/routing/useDataForRoute.tsx |   2 +-
 src/hooks/useData.tsx                      |  25 ++++-
 src/hooks/useImmediateEffect.test.tsx      |  81 --------------
 src/hooks/useImmediateEffect.tsx           |  39 -------
 src/index.tsx                              |  35 +++---
 src/pages/entry/EntryArchiveNav.tsx        |  11 +-
 src/pages/entry/EntryOverview.tsx          |  35 ++++--
 src/pages/entry/entryRoute.tsx             |  15 +--
 13 files changed, 277 insertions(+), 254 deletions(-)
 delete mode 100644 src/hooks/useImmediateEffect.test.tsx
 delete mode 100644 src/hooks/useImmediateEffect.tsx

diff --git a/docs/Archive.mdx b/docs/Archive.mdx
index bd32a9b9..e0a08939 100644
--- a/docs/Archive.mdx
+++ b/docs/Archive.mdx
@@ -4,7 +4,6 @@ To show and edit processed data and data from `archive.json` raw-files (e.g. "ar
 we provide some functionality to manage archive data in the UI. This
 functionality can be use via the `useArchive` hook.
 
-
 ## Goals
 
 - load and maintain partial archives, successively update them with subsequent
@@ -13,14 +12,12 @@ functionality can be use via the `useArchive` hook.
 - tracking changes to the archive data and allowing to save changes back to the
   API
 
-
 ## Basic usage
 
 There are several hooks to interact with the archive. In any case, the archive
 is always tight to the current entry. Where the current entry is determined
 based on the current route.
 
-
 ### useArchive
 
 This hook gives you access to the archive object. See also the doc strings
@@ -32,10 +29,8 @@ const archive = useArchive()
 
 const onFetch = useCallback(
   (data: EntryResponse) => {
-    archive.updateArchive(
-      request.archive as MSectionRequest,
-      data?.archive as MSectionResponse,
-    )
+    archive.startUpdate(request.archive as MSectionRequest)
+    archive.commitUpdate(data?.archive as MSectionResponse)
   },
   [archive, request],
 )
@@ -66,9 +61,8 @@ propery within the archive. The path segments are names of sub-sections and
 quantities; numbers are used for indexing repeating sub-sections. As such
 paths should be mostly compatible with the graph API and Metainfo references.
 
-
 ### useArchiveChanges
 
 This hooks provides access to the changes made to the archive. When a
 property is changed via `useArchiveProperty(path).change(...)`, the change is
-tracked and stored in a stack of changes.
\ No newline at end of file
+tracked and stored in a stack of changes.
diff --git a/src/components/archive/archive.helper.tsx b/src/components/archive/archive.helper.tsx
index 2caa1f89..98c4b3ca 100644
--- a/src/components/archive/archive.helper.tsx
+++ b/src/components/archive/archive.helper.tsx
@@ -31,6 +31,8 @@ export function mockArchive({
   if (data) {
     archive.archive = data
     archive.loading = false
+  } else {
+    archive.loading = true
   }
   allArchives.set(entryId, archive)
   archive.changeStack.push(...changes)
@@ -116,9 +118,9 @@ export function addDefinitions(
       if (Array.isArray(value)) {
         if (value.length === 0 || typeof value[0] === 'object') {
           // repeating sub section
-          const sectionDefs = value.map((item, index) =>
-            visit(item, `${childPath}/${index}`),
-          )
+          const sectionDefs = value
+            .filter((item) => !!item)
+            .map((item, index) => visit(item, `${childPath}/${index}`))
           all_sub_sections[key] = {
             m_def: 'SubSection',
             name: key,
@@ -196,7 +198,7 @@ export function removeDefinitions(data: MSectionResponse) {
   delete data.m_def
   for (const value of Object.values(data)) {
     if (Array.isArray(value)) {
-      value.forEach((item) => removeDefinitions(item))
+      value.filter((item) => !!item).forEach((item) => removeDefinitions(item))
     } else if (typeof value === 'object') {
       removeDefinitions(value as MSectionResponse)
     }
diff --git a/src/components/archive/useArchive.test.tsx b/src/components/archive/useArchive.test.tsx
index 3badc1de..34a1a99b 100644
--- a/src/components/archive/useArchive.test.tsx
+++ b/src/components/archive/useArchive.test.tsx
@@ -74,11 +74,19 @@ describe('useArchive', () => {
       ],
       [
         'updating an existing repeating SubSection',
-        {r: [{}]},
+        {r: [{q: 1}]},
         {r: '*'},
-        {r: [{}]},
-        {r: [{}]},
-        [],
+        {r: [{q: 2}]},
+        {r: [{q: 2}]},
+        [['r', [{q: 2}]]],
+      ],
+      [
+        'updating an existing repeating SubSection recursively',
+        {r: [{q: 1}]},
+        {r: {q: '*'}},
+        {r: [{q: 2}]},
+        {r: [{q: 2}]},
+        [['r/0/q', 2]],
       ],
       ['ignoring a repeating SubSection', {r: [{}]}, {}, {}, {r: [{}]}, []],
       // a sub section in a repeating sub section
@@ -90,6 +98,22 @@ describe('useArchive', () => {
         {r: [{}]},
         [['r', [{}]]],
       ],
+      [
+        'index adding to a repeating SubSection with a whole',
+        {},
+        {'r[1]': '*'},
+        {r: [null, {}]},
+        {r: [undefined, {}]},
+        [['r', [undefined, {}]]],
+      ],
+      [
+        'index adding to an existing repeating SubSection',
+        {r: [{q: 1}]},
+        {'r[1]': '*'},
+        {r: [null, {q: 2}]},
+        {r: [{q: 1}, {q: 2}]},
+        [['r/1', {q: 2}]],
+      ],
       [
         'index removing from a repeating SubSection',
         {r: [{}]},
@@ -116,11 +140,38 @@ describe('useArchive', () => {
       ],
       [
         'index updating in a repeating SubSection',
-        {r: [{}]},
+        {r: [{q: 1}]},
         {'r[0]': '*'},
-        {r: [{}]},
-        {r: [{}]},
-        [],
+        {r: [{q: 2}]},
+        {r: [{q: 2}]},
+        [['r/0', {q: 2}]],
+      ],
+      [
+        'index updating a missing section in a repeating SubSection',
+        {r: [undefined, undefined, {q: 2}]},
+        {'r[1]': '*'},
+        {r: [null, {q: 1}]},
+        {r: [undefined, {q: 1}, {q: 2}]},
+        [['r/1', {q: 1}]],
+      ],
+      [
+        'index updating a missing section in a repeating SubSection recursively',
+        {r: [undefined, undefined, {q: 2}]},
+        {'r[1]': {q: '*'}},
+        {r: [null, {q: 1}]},
+        {r: [undefined, {q: 1}, {q: 2}]},
+        [
+          ['r/1', {q: 1}],
+          ['r/1/q', 1],
+        ],
+      ],
+      [
+        'index updating in a repeating SubSection recursively',
+        {r: [{q: 1}]},
+        {'r[0]': {q: '*'}},
+        {r: [{q: 2}]},
+        {r: [{q: 2}]},
+        [['r/0/q', 2]],
       ],
       [
         'ignoring a SubSection in a repeating SubSection',
@@ -167,7 +218,8 @@ describe('useArchive', () => {
         addDefinitions(response, {q: 1, s: {}, r: []})
 
         archive.archive = initialArchive
-        const {changes} = archive.updateArchive(request, response, {})
+        archive.startUpdate(request)
+        const {changes} = archive.commitUpdate(response, {})
         expect(archive.archive.m_def).not.toBeUndefined()
 
         // remove definitions to make the comparison simpler
@@ -211,7 +263,10 @@ describe('useArchive', () => {
   it('re-renders on update', () => {
     const {result} = renderHook(() => useArchiveWithRenderCounter('q'))
     expect(result.current.value).toEqual(undefined)
-    act(() => archive.updateArchive({q: '*'}, addDefinitions({q: 1}), {}))
+    act(() => {
+      archive.startUpdate({q: '*'})
+      archive.commitUpdate(addDefinitions({q: 1}), {})
+    })
     expect(result.current.value).toEqual(1)
     expect(renderCounter).toHaveBeenCalledTimes(2)
   })
@@ -219,7 +274,10 @@ describe('useArchive', () => {
   it('does not re-render if something else updates', () => {
     const {result} = renderHook(() => useArchiveWithRenderCounter('q'))
     expect(result.current.value).toEqual(undefined)
-    act(() => archive.updateArchive({s: '*'}, addDefinitions({s: {}}), {}))
+    act(() => {
+      archive.startUpdate({s: '*'})
+      archive.commitUpdate(addDefinitions({s: {}}), {})
+    })
     expect(result.current.value).toEqual(undefined)
     expect(renderCounter).toHaveBeenCalledTimes(1)
   })
@@ -256,8 +314,9 @@ describe('useArchive', () => {
       entry_id: 'otherEntryId',
     })
     const {result, rerender} = renderHook(() => useArchive())
-    result.current.updateArchive(
-      {data: {ref: '*'}},
+    const archive = result.current
+    archive.startUpdate({data: {ref: '*'}})
+    archive.commitUpdate(
       addDefinitions({data: {ref: '/uploads/1/entries/1/archive/data'}}),
       {
         uploads: {
@@ -387,8 +446,10 @@ describe('useArchiveProperty', () => {
       entry_id: 'otherEntryId',
     })
     const {result} = renderHook(() => useArchive())
-    result.current.updateArchive(
-      {data: {ref: '*'}},
+    const archive = result.current
+
+    archive.startUpdate({data: {ref: '*'}})
+    archive.commitUpdate(
       addDefinitions({data: {ref: '/uploads/1/entries/1/archive/data'}}),
       {
         uploads: {
diff --git a/src/components/archive/useArchive.tsx b/src/components/archive/useArchive.tsx
index e0ec92f0..14700a6e 100644
--- a/src/components/archive/useArchive.tsx
+++ b/src/components/archive/useArchive.tsx
@@ -70,6 +70,7 @@ export class Archive {
   changeStack: ArchiveChange[]
   loading: boolean
   responsesWithReferencedArchives: MultiMap<string, GraphResponse>
+  currentUpdateRequests: MSectionRequest | undefined
 
   constructor() {
     this.elements = new MultiMap<string, ArchivePropertyDispatch>()
@@ -78,6 +79,7 @@ export class Archive {
     this.changeStack = []
     this.responsesWithReferencedArchives = new MultiMap()
     this.loading = false
+    this.currentUpdateRequests = undefined
   }
 
   registerElement(path: string, dispatch: ArchivePropertyDispatch) {
@@ -260,7 +262,7 @@ export class Archive {
    * TODO what happens when the update effects something that we already
    * have local user changes for?
    */
-  updateArchive(
+  private updateArchive(
     request: MSectionRequest,
     response: MSectionResponse,
     apiResponse: GraphResponse,
@@ -396,11 +398,14 @@ export class Archive {
             archive[key] = archiveList
             const responseList = response[key] as MSectionResponse[]
             archiveList[index] = responseList[index]
+            changes.push([
+              pathForCurrentKey,
+              responseList.map((item) => item || undefined),
+            ])
           } else {
             archive[key] = response[key]
+            changes.push([pathForCurrentKey, response[key]])
           }
-
-          changes.push([pathForCurrentKey, response[key]])
         } else {
           // update
           if (!childIsSubSection) {
@@ -411,37 +416,62 @@ export class Archive {
             archive[key] = response[key]
             changes.push([pathForCurrentKey, response[key]])
           } else if (childIsRepeatingSubSection) {
-            if (checkForConflicts(pathForCurrentKey)) {
-              continue
-            }
+            if (indexed) {
+              const indexedPathForCurrentKey = `${pathForCurrentKey}/${index}`
+              if (checkForConflicts(indexedPathForCurrentKey)) {
+                continue
+              }
 
-            // The semantics is that the sections with the same index are
-            // the same. Even if a section is removed from the middle of the
-            // list.
-            // The recursion will update the sections themsleves.
-            // Here, we only remove/add at the end of the list.
-            const archiveList = archive[key] as MSectionResponse[]
-            archive[key] = archiveList
-            const responseList = response[key] as MSectionResponse[]
-            if (responseList.length !== archiveList.length) {
-              if (responseList.length > archiveList.length) {
-                for (
-                  let index = archiveList.length;
-                  index < responseList.length;
-                  index++
-                ) {
-                  archiveList.push(responseList[index])
-                }
+              const archiveList = archive[key] as MSectionResponse[]
+              const responseList = response[key] as MSectionResponse[]
+              if (archiveList[index]) {
+                // update
+                if (requestValue === '*') {
+                  archiveList[index] = responseList[index]
+                  changes.push([indexedPathForCurrentKey, responseList[index]])
+                } // else { nothing to do, the recursion will update the section }
+              } else {
+                // add
+                archiveList[index] = responseList[index]
+                changes.push([indexedPathForCurrentKey, responseList[index]])
               }
-              if (responseList.length < archiveList.length) {
-                archiveList.splice(
-                  responseList.length,
-                  archiveList.length - responseList.length,
-                )
+            } else {
+              if (checkForConflicts(pathForCurrentKey)) {
+                continue
               }
-              changes.push([pathForCurrentKey, response[key]])
+
+              // The semantics is that the sections with the same index are
+              // the same. Even if a section is removed from the middle of the
+              // list.
+              // We remove/add at the end of the list, if the response is shorter/longer.
+              // We replace the section, if the request was about the whole sections.
+              // We leave it to recursion if the request goes deeper into the sections.
+              const archiveList = archive[key] as MSectionResponse[]
+              archive[key] = archiveList
+              const responseList = response[key] as MSectionResponse[]
+              if (responseList.length !== archiveList.length) {
+                if (responseList.length > archiveList.length) {
+                  for (
+                    let index = archiveList.length;
+                    index < responseList.length;
+                    index++
+                  ) {
+                    archiveList.push(responseList[index])
+                  }
+                }
+                if (responseList.length < archiveList.length) {
+                  archiveList.splice(
+                    responseList.length,
+                    archiveList.length - responseList.length,
+                  )
+                }
+                changes.push([pathForCurrentKey, response[key]])
+              } else if (requestValue === '*') {
+                archive[key] = response[key]
+                changes.push([pathForCurrentKey, response[key]])
+              } // else { nothing to do, the recursion will update the section }
             }
-          } // else { nothing to do, the recursion will update the section }
+          }
         }
 
         // recurse
@@ -503,7 +533,38 @@ export class Archive {
     }
   }
 
-  setLoading(loading: boolean) {
+  startUpdate(request: MSectionRequest, setLoading: boolean = false) {
+    assert(
+      this.currentUpdateRequests === undefined,
+      'Update already in progress',
+    )
+    this.currentUpdateRequests = request
+    if (setLoading) {
+      this.setLoading(true)
+    }
+  }
+
+  commitUpdate(
+    response: MSectionResponse,
+    apiResponse: GraphResponse,
+  ): {changes: [string, unknown][]; conflicts: ArchiveChangeConflict[]} {
+    assert(this.currentUpdateRequests, 'No update in progress')
+    const result = this.updateArchive(
+      this.currentUpdateRequests,
+      response,
+      apiResponse,
+    )
+    this.currentUpdateRequests = undefined
+    this.setLoading(false)
+    return result
+  }
+
+  abortUpdate() {
+    this.currentUpdateRequests = undefined
+    this.setLoading(false)
+  }
+
+  private setLoading(loading: boolean) {
     if (this.loading === loading) {
       return
     }
diff --git a/src/components/routing/Routes.tsx b/src/components/routing/Routes.tsx
index 7b984133..ae737786 100644
--- a/src/components/routing/Routes.tsx
+++ b/src/components/routing/Routes.tsx
@@ -131,32 +131,32 @@ function useLoadRouteData(
   reloadCount?: number,
 ): AsyncState<LoaderResult | undefined> {
   const {user} = useAuth()
-  const fetch = useMemo(
-    () => loader(fullMatch, user || undefined),
-    [fullMatch, loader, user],
-  )
-  const state = useAsyncConditional(fetch, [fetch, reloadCount])
-  if (state.value) {
-    state.value.routeResponses.forEach((response, index) => {
-      const match = fullMatch[index]
-      if (response && state.value?.response) {
-        match.response = response
-        try {
-          match.route.onFetch?.({
-            fullMatch,
-            match,
-            response,
-            request: state.value.routeRequests[index],
-            fullRequest: state.value?.request,
-            fullResponse: state.value?.response,
-          })
-        } catch (e) {
-          state.error = e as Error
-        }
-      }
-    })
-  }
-  return state
+  const fetch = useMemo(() => {
+    const result = loader(fullMatch, user || undefined)
+    if (!result) {
+      return
+    }
+    return () => {
+      return result().then((result) => {
+        result.routeResponses.forEach((response, index) => {
+          const match = fullMatch[index]
+          if (response && result?.response) {
+            match.response = response
+            match.route.onFetch?.({
+              fullMatch,
+              match,
+              response,
+              request: result.routeRequests[index],
+              fullRequest: result?.request,
+              fullResponse: result?.response,
+            })
+          }
+        })
+        return result
+      })
+    }
+  }, [fullMatch, loader, user])
+  return useAsyncConditional(fetch, [fetch, reloadCount])
 }
 
 function useRouteMatch(route: Route, {path, rawSearch}: Location) {
diff --git a/src/components/routing/useDataForRoute.tsx b/src/components/routing/useDataForRoute.tsx
index c1e22963..28f1e5ca 100644
--- a/src/components/routing/useDataForRoute.tsx
+++ b/src/components/routing/useDataForRoute.tsx
@@ -58,7 +58,7 @@ export default function useDataForRoute<
       return undefined
     }
     return (_: JSONObject, fullRequest: GraphRequest) => {
-      onBeforeFetchForRoute(request as Request, fullRequest)
+      return onBeforeFetchForRoute(request as Request, fullRequest)
     }
   }, [onBeforeFetchForRoute, request])
 
diff --git a/src/hooks/useData.tsx b/src/hooks/useData.tsx
index 86470bed..21f27cee 100644
--- a/src/hooks/useData.tsx
+++ b/src/hooks/useData.tsx
@@ -9,8 +9,8 @@ import {GraphResponse} from '../models/graphResponseModels'
 import {graphApi} from '../utils/api'
 import {resolveAllMDefs} from '../utils/metainfo'
 import {DefaultToObject, JSONObject, JSONValue} from '../utils/types'
+import {assert} from '../utils/utils'
 import useAuth from './useAuth'
-import useImmediateEffect from './useImmediateEffect'
 import useRender from './useRender'
 
 /**
@@ -65,9 +65,14 @@ export type UseDataParams<Request, Response> = {
   noImplicitFetch?: boolean
 
   /**
-   * An optional callback that is called before the fetch is started.
+   * An optional callback that is called before the fetch is started. If this
+   * returns a function, this function will be called when the request effect
+   * is canceled.
    */
-  onBeforeFetch?: (request: Request, fullRequest: GraphRequest) => void
+  onBeforeFetch?: (
+    request: Request,
+    fullRequest: GraphRequest,
+  ) => void | (() => void)
 
   /**
    * An optional callback that is called when the data was successfully
@@ -125,10 +130,17 @@ export default function useData<
       return
     }
     requestRef.current = request
+    normalizedRequestRef.current = normalizeGraph(request as JSONObject)
+    const effectDestructor = onBeforeFetch?.(
+      request,
+      normalizedRequestRef.current,
+    )
     const fetch = async () => {
-      normalizedRequestRef.current = normalizeGraph(request as JSONObject)
-      onBeforeFetch?.(request, normalizedRequestRef.current)
       try {
+        assert(
+          normalizedRequestRef.current !== undefined,
+          'request is undefined',
+        )
         const result = await graphApi(
           normalizedRequestRef.current,
           user || undefined,
@@ -153,6 +165,7 @@ export default function useData<
     fetch()
     return () => {
       requestRef.current = undefined
+      effectDestructor?.()
     }
   }, [
     requestRef,
@@ -165,7 +178,7 @@ export default function useData<
     user,
   ])
 
-  useImmediateEffect(() => {
+  useEffect(() => {
     if (!noImplicitFetch) {
       return fetch()
     }
diff --git a/src/hooks/useImmediateEffect.test.tsx b/src/hooks/useImmediateEffect.test.tsx
deleted file mode 100644
index 60bc7e1f..00000000
--- a/src/hooks/useImmediateEffect.test.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import {renderHook} from '@testing-library/react'
-import {vi} from 'vitest'
-
-import {JSONObject} from '../utils/types'
-import useImmediateEffect from './useImmediateEffect'
-
-it('calls effect on render without deps', () => {
-  const effect = vi.fn()
-  renderHook(() => {
-    return useImmediateEffect(effect, [])
-  })
-  expect(effect).toHaveBeenCalledTimes(1)
-})
-
-it('calls effect on render with deps', () => {
-  const effect = vi.fn()
-  renderHook(() => {
-    return useImmediateEffect(effect, [true])
-  })
-  expect(effect).toHaveBeenCalledTimes(1)
-})
-
-it('does not call effect on re-render with unchanged deps', () => {
-  const effect = vi.fn()
-  const {rerender} = renderHook(
-    ({dep}: {dep: boolean}) => {
-      useImmediateEffect(effect, [dep])
-    },
-    {initialProps: {dep: false}},
-  )
-  expect(effect).toHaveBeenCalledTimes(1)
-
-  rerender({dep: false})
-  expect(effect).toHaveBeenCalledTimes(1)
-})
-
-it('calls effect on re-render with changed deps', () => {
-  const effect = vi.fn()
-  const {rerender} = renderHook(
-    ({dep}: {dep: boolean}) => {
-      useImmediateEffect(effect, [dep])
-    },
-    {initialProps: {dep: false}},
-  )
-  expect(effect).toHaveBeenCalledTimes(1)
-
-  rerender({dep: true})
-  expect(effect).toHaveBeenCalledTimes(2)
-})
-
-it('calls effect on re-render with changed object deps', () => {
-  const effect = vi.fn()
-  const obj = {a: 1}
-  const {rerender} = renderHook(
-    ({dep}: {dep: JSONObject}) => {
-      useImmediateEffect(effect, [dep])
-    },
-    {initialProps: {dep: obj}},
-  )
-  expect(effect).toHaveBeenCalledTimes(1)
-
-  rerender({dep: {...obj}})
-  expect(effect).toHaveBeenCalledTimes(2)
-})
-
-it('calls destructuor', () => {
-  const destructor = vi.fn()
-  const {rerender} = renderHook(
-    ({dep}: {dep: boolean}) => {
-      useImmediateEffect(() => destructor, [dep])
-    },
-    {initialProps: {dep: false}},
-  )
-  expect(destructor).toHaveBeenCalledTimes(0)
-
-  rerender({dep: false})
-  expect(destructor).toHaveBeenCalledTimes(0)
-
-  rerender({dep: true})
-  expect(destructor).toHaveBeenCalledTimes(1)
-})
diff --git a/src/hooks/useImmediateEffect.tsx b/src/hooks/useImmediateEffect.tsx
deleted file mode 100644
index 311f4bfe..00000000
--- a/src/hooks/useImmediateEffect.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import {DependencyList, useCallback, useEffect, useRef} from 'react'
-import {usePrevious} from 'react-use'
-
-/**
- * A hook that has the same interface and behavior as `useEffect` but runs the
- * effect "immediately" upon beeing called and within the current render.
- */
-export default function useImmediateEffect(
-  effect: React.EffectCallback,
-  deps: DependencyList,
-) {
-  let hasChanged = true !== usePrevious(true)
-  for (let i = 0; i < deps.length; i++) {
-    // eslint-disable-next-line react-hooks/rules-of-hooks
-    if (deps[i] !== usePrevious(deps[i])) {
-      hasChanged = true
-    }
-  }
-
-  const effectDestructor = useRef<ReturnType<React.EffectCallback>>()
-
-  if (hasChanged) {
-    if (effectDestructor.current) {
-      effectDestructor.current()
-    }
-    effectDestructor.current = effect()
-  }
-
-  const unmount = useCallback(() => {
-    if (effectDestructor.current) {
-      effectDestructor.current()
-      effectDestructor.current = undefined
-    }
-  }, [])
-
-  useEffect(() => {
-    return unmount
-  }, [unmount])
-}
diff --git a/src/index.tsx b/src/index.tsx
index 37115408..58bde89a 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,7 +2,6 @@ import '@fontsource/titillium-web/400.css'
 import '@fontsource/titillium-web/700.css'
 import {LocalizationProvider} from '@mui/x-date-pickers'
 import {AdapterDateFns} from '@mui/x-date-pickers/AdapterDateFnsV3'
-import React from 'react'
 import ReactDOM from 'react-dom/client'
 import {RecoilRoot} from 'recoil'
 
@@ -45,23 +44,23 @@ enableMocking().then(() => {
     document.getElementById('root') as HTMLElement,
   )
   root.render(
-    <React.StrictMode>
-      <LocalizationProvider dateAdapter={AdapterDateFns}>
-        <RecoilRoot>
-          <Theme>
-            <AuthProvider>
-              <ApiProvider>
-                <UnitProvider>
-                  <DevTools enabled>
-                    <Routes />
-                  </DevTools>
-                </UnitProvider>
-              </ApiProvider>
-            </AuthProvider>
-          </Theme>
-        </RecoilRoot>
-      </LocalizationProvider>
-    </React.StrictMode>,
+    // <React.StrictMode>
+    <LocalizationProvider dateAdapter={AdapterDateFns}>
+      <RecoilRoot>
+        <Theme>
+          <AuthProvider>
+            <ApiProvider>
+              <UnitProvider>
+                <DevTools enabled>
+                  <Routes />
+                </DevTools>
+              </UnitProvider>
+            </ApiProvider>
+          </AuthProvider>
+        </Theme>
+      </RecoilRoot>
+    </LocalizationProvider>,
+    // </React.StrictMode>,
   )
 })
 
diff --git a/src/pages/entry/EntryArchiveNav.tsx b/src/pages/entry/EntryArchiveNav.tsx
index 612f5c93..5967b9cb 100644
--- a/src/pages/entry/EntryArchiveNav.tsx
+++ b/src/pages/entry/EntryArchiveNav.tsx
@@ -113,10 +113,11 @@ export default function EntryArchiveNav({
     fullMatch.length === index + 2 && fullMatch[index + 1]?.path === 'archive'
 
   const request = useMemo(
-    () =>
-      ({
-        ...archiveRequest,
-      } as JSONObject),
+    () => ({
+      ...archiveRequest,
+      // TODO this does not seem to have the desired effect
+      '*': '*',
+    }),
     [],
   )
 
@@ -126,7 +127,7 @@ export default function EntryArchiveNav({
       path='archive'
       variant='menuItem'
       icon={undefined}
-      request={request}
+      request={request as unknown as JSONObject}
       expandable={expandable}
       getChildProps={getChildProps}
       filterChildKeys={filterChildKeys}
diff --git a/src/pages/entry/EntryOverview.tsx b/src/pages/entry/EntryOverview.tsx
index 0ccde221..7c5f6101 100644
--- a/src/pages/entry/EntryOverview.tsx
+++ b/src/pages/entry/EntryOverview.tsx
@@ -1,5 +1,6 @@
 import {Box} from '@mui/material'
 import {useCallback, useMemo, useState} from 'react'
+import {usePrevious} from 'react-use'
 
 import ErrorMessage from '../../components/app/ErrorMessage'
 import useArchive from '../../components/archive/useArchive'
@@ -7,6 +8,7 @@ import {calculateRequestFromLayout} from '../../components/editor/utils'
 import {LayoutItem} from '../../components/layout/Layout'
 import useSelect from '../../components/navigation/useSelect'
 import useDataForRoute from '../../components/routing/useDataForRoute'
+import useRoute from '../../components/routing/useRoute'
 import useRouteData from '../../components/routing/useRouteData'
 import {EntryRequest, MSectionRequest} from '../../models/graphRequestModels'
 import {
@@ -25,6 +27,7 @@ import entryRoute, {archiveRequest} from './entryRoute'
 
 function EntryOverviewEditor() {
   const {archive: archiveData} = useRouteData(entryRoute)
+  const {reloadCount} = useRoute()
   const [error, setError] = useState<Error | undefined>()
   const {isPage} = useSelect()
 
@@ -64,29 +67,41 @@ function EntryOverviewEditor() {
       calculateRequestFromLayout(layout, request.archive)
     }
     return request as EntryRequest
-  }, [layout])
+    // Depending on the reloadCount is important to for a new request and
+    // therefore a new fetch.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [layout, reloadCount])
 
   const archive = useArchive()
 
+  // Ideally we would call this in the onBeforeFetch callback, but
+  // this will be too late because the effect is called after the render.
+  // When rendering the EntryOverviewEditor and the
+  // archive was loaded before, the render will think the archive
+  // is already loaded and updated, even though the update has not even started.
+  // While this works for now, it will be a problem if the startUpdate function
+  // will cause state updates in the future as those are not allowed during
+  // render.
+  if (request !== usePrevious(request)) {
+    archive.startUpdate(request.archive as MSectionRequest, true)
+  }
+
   const onBeforeFetch = useCallback(() => {
-    archive.setLoading(true)
+    return () => {
+      archive.abortUpdate()
+    }
   }, [archive])
 
   const onFetch = useCallback(
     (data: EntryResponse, fullResponse: GraphResponse) => {
-      archive.updateArchive(
-        request.archive as MSectionRequest,
-        data?.archive as MSectionResponse,
-        fullResponse,
-      )
-      archive.setLoading(false)
+      archive.commitUpdate(data?.archive as MSectionResponse, fullResponse)
     },
-    [archive, request],
+    [archive],
   )
 
   const onError = useCallback(
     (error: Error) => {
-      archive.setLoading(false)
+      archive.abortUpdate()
       setError(error)
     },
     [archive],
diff --git a/src/pages/entry/entryRoute.tsx b/src/pages/entry/entryRoute.tsx
index 725e9b3b..0d30e586 100644
--- a/src/pages/entry/entryRoute.tsx
+++ b/src/pages/entry/entryRoute.tsx
@@ -37,7 +37,9 @@ archiveRoute.children = [archiveRoute]
 
 const entryRoute: Route<EntryRequest, EntryResponse> = {
   path: ':entryId',
-  request: {
+  // TODO The function should not be necessary, but some buggy thing is modifying the
+  // request. With the function we force to recreate the request object every time.
+  request: () => ({
     mainfile_path: '*',
     entry_id: '*',
     upload_id: '*',
@@ -51,7 +53,7 @@ const entryRoute: Route<EntryRequest, EntryResponse> = {
       datasets: '*',
     } as EntryMetadataRequest,
     archive: {...archiveRequest},
-  },
+  }),
   lazyComponent: async () => import('./EntryPage'),
   renderWithPathAsKey: true,
   breadcrumb: ({response}) => path.basename(response?.mainfile_path as string),
@@ -73,13 +75,8 @@ const entryRoute: Route<EntryRequest, EntryResponse> = {
     }
     const entryId = match.path
     const archive = getArchive(entryId)
-    archive.setLoading(true)
-    archive.updateArchive(
-      request.archive,
-      response.archive,
-      fullResponse as GraphResponse,
-    )
-    archive.setLoading(false)
+    archive.startUpdate(request.archive, true)
+    archive.commitUpdate(response.archive, fullResponse as GraphResponse)
   },
 }
 
-- 
GitLab


From a09176d18f78afb6164c216b3938a2a2771f5908 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Mon, 10 Feb 2025 13:47:26 +0100
Subject: [PATCH 03/17] Undo/Redo

---
 src/components/archive/EditStatus.tsx      |  45 +++++--
 src/components/archive/Section.tsx         |   4 +-
 src/components/archive/archive.helper.tsx  |   5 +-
 src/components/archive/useArchive.test.tsx | 103 ++++++++++++++++
 src/components/archive/useArchive.tsx      | 135 +++++++++++++++++----
 src/components/icons.tsx                   |   4 +
 src/pages/entry/EntryOverview.tsx          |   8 +-
 7 files changed, 263 insertions(+), 41 deletions(-)

diff --git a/src/components/archive/EditStatus.tsx b/src/components/archive/EditStatus.tsx
index 3cac3796..73eaaf5a 100644
--- a/src/components/archive/EditStatus.tsx
+++ b/src/components/archive/EditStatus.tsx
@@ -1,6 +1,7 @@
-import {Button} from '@mui/material'
+import {Button, IconButton} from '@mui/material'
 import {useAsyncFn} from 'react-use'
 
+import {Redo, Undo} from '../../components/icons'
 import useAuth from '../../hooks/useAuth'
 import {MSectionResponse} from '../../models/graphResponseModels'
 import entryRoute from '../../pages/entry/entryRoute'
@@ -8,13 +9,14 @@ import {archiveEditApi} from '../../utils/api'
 import {assert} from '../../utils/utils'
 import useRoute from '../routing/useRoute'
 import useRouteData from '../routing/useRouteData'
-import {useArchiveChanges} from './useArchive'
+import useArchive, {useArchiveChanges} from './useArchive'
 
 export default function EditStatus() {
   const {user} = useAuth()
   const {reload} = useRoute()
   const {entry_id} = useRouteData(entryRoute)
   const {changeStack, clearChangeStack} = useArchiveChanges()
+  const archive = useArchive()
 
   const [saving, save] = useAsyncFn(async () => {
     const apiChanges = changeStack.map((change) => {
@@ -29,6 +31,9 @@ export default function EditStatus() {
         }
         delete (apiChange.new_value as MSectionResponse)['m_def']
       }
+      if (apiChange.oldValue) {
+        delete apiChange['oldValue']
+      }
 
       return apiChange
     })
@@ -40,16 +45,30 @@ export default function EditStatus() {
   }, [changeStack, clearChangeStack, reload])
 
   return (
-    <Button
-      onClick={save}
-      variant='contained'
-      disabled={changeStack.length === 0 || saving.loading}
-    >
-      {saving.loading
-        ? 'saving ...'
-        : changeStack.length === 0
-        ? 'no changes to save'
-        : `save changes`}
-    </Button>
+    <>
+      <IconButton
+        disabled={!archive.hasChanges()}
+        onClick={() => archive.undo()}
+      >
+        <Undo />
+      </IconButton>
+      <IconButton
+        disabled={!archive.hasUnDoneChanges()}
+        onClick={() => archive.redo()}
+      >
+        <Redo />
+      </IconButton>
+      <Button
+        onClick={save}
+        variant='contained'
+        disabled={changeStack.length === 0 || saving.loading}
+      >
+        {saving.loading
+          ? 'saving ...'
+          : changeStack.length === 0
+          ? 'no changes to save'
+          : `save changes`}
+      </Button>
+    </>
   )
 }
diff --git a/src/components/archive/Section.tsx b/src/components/archive/Section.tsx
index a433e0fa..a1028313 100644
--- a/src/components/archive/Section.tsx
+++ b/src/components/archive/Section.tsx
@@ -62,10 +62,10 @@ export default function Section({path, editable = false}: SectionProps) {
   const {value} = useArchiveProperty<MSectionResponse>(path)
   const definition = value?.m_def as SectionDefinition
 
-  assert(value !== undefined, 'The section should have been loaded by now')
+  assert(value !== undefined, 'The section should have been loaded by now.')
   assert(
     definition !== undefined,
-    'The section definition should have been loaded by now',
+    'The section definition should have been loaded by now.',
   )
 
   return (
diff --git a/src/components/archive/archive.helper.tsx b/src/components/archive/archive.helper.tsx
index 98c4b3ca..39014f30 100644
--- a/src/components/archive/archive.helper.tsx
+++ b/src/components/archive/archive.helper.tsx
@@ -2,18 +2,17 @@ import {useEffect, useState} from 'react'
 import {vi} from 'vitest'
 
 import {UseDataResult} from '../../hooks/useData'
-import {ArchiveChange} from '../../models/entryEditRequestModels'
 import {MDefResponse, MSectionResponse} from '../../models/graphResponseModels'
 import {Quantity, Section, SubSection} from '../../utils/metainfo'
 import {DefaultToObject} from '../../utils/types'
 import * as useDataForRoute from '../routing/useDataForRoute'
 import * as useRouteData from '../routing/useRouteData'
-import {Archive, allArchives} from './useArchive'
+import {Archive, ArchiveChangeWithOldValue, allArchives} from './useArchive'
 
 export type MockArchiveArgs = {
   entryId?: string
   data?: MSectionResponse
-  changes?: ArchiveChange[]
+  changes?: ArchiveChangeWithOldValue[]
 }
 
 /**
diff --git a/src/components/archive/useArchive.test.tsx b/src/components/archive/useArchive.test.tsx
index 34a1a99b..3683a5e9 100644
--- a/src/components/archive/useArchive.test.tsx
+++ b/src/components/archive/useArchive.test.tsx
@@ -331,6 +331,109 @@ describe('useArchive', () => {
       q: 'test',
     })
   })
+
+  it.each([
+    ['quantity set', {q: '*'}, {q: 1}, 'q', 'upsert', 2],
+    ['nested quantity set', {s: {q: '*'}}, {s: {q: 1}}, 's/q', 'upsert', 2],
+    [
+      'repeated nested quantity set',
+      {s: [{q: '*'}]},
+      {s: [{q: 1}]},
+      's/0/q',
+      'upsert',
+      2,
+    ],
+    [
+      'repeated nested quantity set unnormalized',
+      {s: [{q: '*'}]},
+      {s: [{q: 1}]},
+      's[0]/q',
+      'upsert',
+      2,
+    ],
+    ['sub-section replace', {s: '*'}, {s: {q: 1}}, 's', 'upsert', {q: 2}],
+    [
+      'repeated sub-section replace implicit',
+      {s: '*'},
+      {s: [{q: 1}]},
+      's',
+      'upsert',
+      [{q: 2}],
+    ],
+    [
+      'repeated sub-section implicit add',
+      {s: '*'},
+      {s: [{q: 1}]},
+      's',
+      'upsert',
+      [{q: 1}, {q: 2}],
+    ],
+    [
+      'repeated sub-section implicit remove',
+      {s: '*'},
+      {s: [{q: 1}, {q: 2}]},
+      's',
+      'upsert',
+      [{q: 1}],
+    ],
+    [
+      'repeated sub-section explicit replace',
+      {s: '*'},
+      {s: [{q: 1}]},
+      's/0',
+      'upsert',
+      {q: 2},
+    ],
+    [
+      'repeated sub-section explicit add',
+      {s: '*'},
+      {s: [{q: 1}]},
+      's/1',
+      'upsert',
+      {q: 2},
+    ],
+    [
+      'repeated sub-section explicit remove',
+      {s: '*'},
+      {s: [{q: 1}, {q: 2}]},
+      's/1',
+      'remove',
+      undefined,
+    ],
+  ])(
+    'does undo/redo %s',
+    (description, request, response, path, action, value) => {
+      const dispatch = vi.fn()
+      const changeListener = vi.fn()
+      const {result} = renderHook(() => useArchive())
+      act(() => {
+        archive.startUpdate(request)
+        archive.commitUpdate(addDefinitions(response), {})
+      })
+      const oldValue = result.current.getValue(path)
+      act(() => {
+        archive.registerElement(path, dispatch)
+        archive.registerChangeListener(changeListener)
+        archive.submitElementChange(path, action as 'upsert' | 'remove', value)
+      })
+      expect(result.current.getValue(path)).toBe(value)
+      expect(dispatch).toHaveBeenCalledTimes(1)
+      expect(changeListener).toHaveBeenCalledTimes(1)
+      expect(result.current.changeStack).toHaveLength(1)
+
+      act(() => result.current.undo())
+      expect(result.current.changeStack).toHaveLength(0)
+      expect(result.current.getValue(path)).toBe(oldValue)
+      expect(dispatch).toHaveBeenCalledTimes(2)
+      expect(changeListener).toHaveBeenCalledTimes(2)
+
+      act(() => result.current.redo())
+      expect(result.current.changeStack).toHaveLength(1)
+      expect(result.current.getValue(path)).toBe(value)
+      expect(dispatch).toHaveBeenCalledTimes(3)
+      expect(changeListener).toHaveBeenCalledTimes(3)
+    },
+  )
 })
 
 describe('useArchiveProperty', () => {
diff --git a/src/components/archive/useArchive.tsx b/src/components/archive/useArchive.tsx
index 14700a6e..82038f5d 100644
--- a/src/components/archive/useArchive.tsx
+++ b/src/components/archive/useArchive.tsx
@@ -21,6 +21,7 @@ export type ArchiveChangeConflict = {
   type: 'parent' | 'child' | 'exact'
   change: ArchiveChange
 }
+export type ArchiveChangeWithOldValue = ArchiveChange & {oldValue?: unknown}
 
 class MultiMap<K, V> {
   map = new Map<K, V[]>()
@@ -67,7 +68,8 @@ export class Archive {
   elements: MultiMap<string, ArchivePropertyDispatch>
   changeListener: (() => void)[]
   archive: MSectionResponse
-  changeStack: ArchiveChange[]
+  changeStack: ArchiveChangeWithOldValue[]
+  redoStack: ArchiveChangeWithOldValue[]
   loading: boolean
   responsesWithReferencedArchives: MultiMap<string, GraphResponse>
   currentUpdateRequests: MSectionRequest | undefined
@@ -77,6 +79,7 @@ export class Archive {
     this.changeListener = []
     this.archive = {}
     this.changeStack = []
+    this.redoStack = []
     this.responsesWithReferencedArchives = new MultiMap()
     this.loading = false
     this.currentUpdateRequests = undefined
@@ -103,6 +106,96 @@ export class Archive {
     this.changeListener.forEach((listener) => listener())
   }
 
+  hasUnDoneChanges() {
+    return this.redoStack.length > 0
+  }
+
+  hasChanges() {
+    return this.changeStack.length > 0
+  }
+
+  /**
+   * Removes the last change from the change stack and
+   * - pushes it to the redo stack
+   * - applies the change to the archive
+   * - notifies all components that have registered to the particular path
+   *   via `useArchiveProperty` (and `registerElement`).
+   * - notifies all components that have registered to via `registerChangeListener`.
+   */
+  undo() {
+    const change = this.changeStack.pop()
+    assert(change, 'No changes to undo.')
+    this.applyAndDispatchValue(change.path, change.oldValue)
+    this.redoStack.push(change)
+    if (change) {
+      this.changeListener.forEach((listener) => listener())
+    }
+  }
+
+  /**
+   * Removes the last change from the redo stack and
+   * - pushes it back to the change stack
+   * - applies the change (old value) to the archive
+   * - notifies all components that have registered to the particular path
+   *   via `useArchiveProperty` (and `registerElement`).
+   * - notifies all components that have registered to via `registerChangeListener`.
+   */
+  redo() {
+    const change = this.redoStack.pop()
+    assert(change, 'No changes to redo.')
+    this.applyAndDispatchValue(change.path, change.new_value)
+    this.changeStack.push(change)
+    if (change) {
+      this.changeListener.forEach((listener) => listener())
+    }
+  }
+
+  /**
+   * Perform a change on the archive data and notify all components that have
+   * registered to the particular path via `useArchiveProperty` (and
+   * `registerElement`).
+   *
+   * @param path The path to the property.
+   * @param value The new value to set.
+   * @returns The old value of the property.
+   */
+  private applyAndDispatchValue(path: string, value: unknown) {
+    // apply
+    let current = this.archive
+    const parts = normalizeArchivePath(path)
+    const subSections = parts.slice(0, -1)
+    for (const key of subSections) {
+      current = current[key] as MSectionResponse
+    }
+    const quantityOrIndex = parts[parts.length - 1]
+    const oldValue = current[quantityOrIndex]
+    current[quantityOrIndex] = value
+
+    // additionally remove deleted sub sections at the end of repeated
+    // sub sections
+    if (
+      value === undefined &&
+      Array.isArray(current) &&
+      current.length === parseInt(quantityOrIndex) + 1
+    ) {
+      current.pop()
+    }
+
+    // additionally dispatch the repeated sub section if a section was
+    // added or removed
+    if (Array.isArray(current)) {
+      for (const dispatch of this.elements.get(subSections.join('/'))) {
+        dispatch({value: current, loading: false})
+      }
+    }
+    // dispatch change
+    for (const dispatch of this.elements.get(path)) {
+      dispatch({value, loading: false})
+    }
+
+    return oldValue
+  }
+
   /**
    * Submits a change to the archive. This will update the archive,
    * dispatch the change to all registered elements, and keep track
@@ -120,37 +213,28 @@ export class Archive {
     value: unknown,
     index?: number,
   ) {
-    // apply the value to the archive
-    let current = this.archive
-    const parts = normalizeArchivePath(path)
-    const subSections = parts.slice(0, -1)
-    for (const key of subSections) {
-      current = current[key] as MSectionResponse
-    }
-    const valueIndex = parts[parts.length - 1]
-    current[valueIndex] = value
-
-    // dispatch change
-    for (const dispatch of this.elements.get(path)) {
-      dispatch({value, loading: false})
-    }
+    assert(!this.loading)
+    const oldValue = this.applyAndDispatchValue(path, value)
 
     // translate the change into an ArchiveChange and keep the change in
     // this.changes and this.changeStack
     let change
     if (index !== undefined) {
       assert(Array.isArray(value), 'Value must be an array for indexed changes')
+      assert(Array.isArray(oldValue) || oldValue === undefined)
       change = {
         path: `${path}/${index}`,
         new_value: action === 'remove' ? undefined : {...value[index]},
+        oldValue: action === 'remove' ? oldValue?.[index] : undefined,
         action,
-      } satisfies ArchiveChange
+      } satisfies ArchiveChangeWithOldValue
     } else {
       change = {
         path,
         new_value: action === 'remove' ? undefined : value,
+        oldValue: oldValue,
         action,
-      } satisfies ArchiveChange
+      } satisfies ArchiveChangeWithOldValue
 
       if (this.changeStack.length > 0) {
         const lastChange = this.changeStack[this.changeStack.length - 1]
@@ -221,7 +305,7 @@ export class Archive {
 
   /**
    * Detects conflicts between changing the given path and the changes in the
-   * current chanage stack. The function returns 'parent' if the path is a
+   * current change stack. The function returns 'parent' if the path is a
    * prefix to a path in the change stack, 'child' if a path in the change stack
    * is a prefix to the given path, 'none' if there is no conflict, and 'exact'
    * of the path is exactly the same as a path in the change stack.
@@ -640,7 +724,7 @@ export default function useArchive() {
 }
 
 export type ArchiveChanges = {
-  changeStack: ArchiveChange[]
+  changeStack: ArchiveChangeWithOldValue[]
   clearChangeStack: () => void
   // undo: () => void,
   // redo: () => void,
@@ -652,7 +736,7 @@ export type ArchiveChanges = {
  */
 export function useArchiveChanges() {
   const archive = useArchive()
-  const [changeStack, setChangeStack] = useState<ArchiveChange[]>([
+  const [changeStack, setChangeStack] = useState<ArchiveChangeWithOldValue[]>([
     ...archive.changeStack,
   ])
 
@@ -711,6 +795,14 @@ export type ArchiveProperty<Type, DefinitionType extends Property> = {
   resolveRef: (ref: string) => MSectionResponse | undefined
 }
 
+/**
+ * Normalizes the use of repeated sub section syntax in paths. The
+ * normalized syntax uses index segments (e.g. `section/0`) instead
+ * of having the index in the sub section segment (e.g. `section[0]`).
+ *
+ * @param path The path to normalize.
+ * @returns The normalized path.
+ */
 function normalizeArchivePath(path: string) {
   const result: string[] = []
   path.split('/').forEach((segment) => {
@@ -733,7 +825,7 @@ function normalizeArchivePath(path: string) {
  * root to the property. The paths use `/` as separator and align with the syntax
  * of the graph API or archive references.
  *
- * What it means that an propery changes depends on the
+ * What it means that an property changes depends on the
  * kind of property. A quantity value changes if the value changes, a (repeating)
  * sub-section changes if a/the section is added, removed, or replaced.
  *
@@ -746,6 +838,7 @@ function normalizeArchivePath(path: string) {
 export function useArchiveProperty<T = unknown, D extends Property = Property>(
   path: string,
 ): ArchiveProperty<T, D> {
+  path = normalizeArchivePath(path).join('/')
   const archive = useArchive()
 
   const previousPath = usePrevious(path)
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index 97bc8b78..c74127c2 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -28,7 +28,9 @@ import PieChartIcon from '@mui/icons-material/PieChart'
 import PublicIcon from '@mui/icons-material/Public'
 import PushPinIcon from '@mui/icons-material/PushPin'
 import QuestionMarkIcon from '@mui/icons-material/QuestionMark'
+import RedoIcon from '@mui/icons-material/Redo'
 import SearchIcon from '@mui/icons-material/Search'
+import UndoIcon from '@mui/icons-material/Undo'
 import UploadIcon from '@mui/icons-material/Upload'
 import VisibilityIcon from '@mui/icons-material/Visibility'
 import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'
@@ -68,6 +70,8 @@ export const Help = HelpIcon
 export const Dataset = DatasetIcon
 export const Pin = PushPinIcon
 export const Group = GroupIcon
+export const Undo = UndoIcon
+export const Redo = RedoIcon
 
 export function ToggleColorMode() {
   const theme = useTheme()
diff --git a/src/pages/entry/EntryOverview.tsx b/src/pages/entry/EntryOverview.tsx
index 7c5f6101..e48971a1 100644
--- a/src/pages/entry/EntryOverview.tsx
+++ b/src/pages/entry/EntryOverview.tsx
@@ -82,15 +82,19 @@ function EntryOverviewEditor() {
   // While this works for now, it will be a problem if the startUpdate function
   // will cause state updates in the future as those are not allowed during
   // render.
-  if (request !== usePrevious(request)) {
+  const requestHasChanged = request !== usePrevious(request)
+  if (requestHasChanged) {
     archive.startUpdate(request.archive as MSectionRequest, true)
   }
 
   const onBeforeFetch = useCallback(() => {
+    if (!archive.loading) {
+      archive.startUpdate(request.archive as MSectionRequest, true)
+    }
     return () => {
       archive.abortUpdate()
     }
-  }, [archive])
+  }, [archive, request])
 
   const onFetch = useCallback(
     (data: EntryResponse, fullResponse: GraphResponse) => {
-- 
GitLab


From a21e2f77c02972f3e9a2e821353aab564266ea10 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Tue, 18 Feb 2025 12:58:33 +0100
Subject: [PATCH 04/17] Fixed archive requests depth to minimum necessary.

---
 README.md                             |  7 ++--
 src/components/archive/useArchive.tsx |  4 ++
 src/components/routing/loader.ts      |  3 +-
 src/pages/entry/entryRoute.tsx        |  1 +
 src/pages/upload/uploadRoute.tsx      | 33 +++++++++-------
 src/utils/metainfo.test.ts            | 49 +++++++++++++++++++++++-
 src/utils/metainfo.ts                 | 54 +++++++++++++++++++++++++++
 7 files changed, 131 insertions(+), 20 deletions(-)

diff --git a/README.md b/README.md
index fe94dccc..b45dc92d 100644
--- a/README.md
+++ b/README.md
@@ -60,8 +60,8 @@ in development.
    From the gui project root folder:
 
    ```sh
-   python3.11 -m venv .pyenv
-   source .pyenv/bin/activate
+   python3.11 -m venv .venv
+   source .venv/bin/activate
    pip install uv
    uv pip install --upgrade pip
    uv pip install -e infra \
@@ -79,13 +79,12 @@ in development.
 
 3. Run the required infrastructure services. Typically you will run them through a `docker-compose.yaml` file that is part of your development setup.
 
-
 4. Run NOMAD. Run from the `infra` directory it has the necessary `nomad.yaml` file:
 
    ```sh
    cd infra
    nomad admin run appworker
-   ````
+   ```
 
 5. Upload some data. Again, mind the directory. Run from `infra`.
 
diff --git a/src/components/archive/useArchive.tsx b/src/components/archive/useArchive.tsx
index 82038f5d..5762fe13 100644
--- a/src/components/archive/useArchive.tsx
+++ b/src/components/archive/useArchive.tsx
@@ -531,6 +531,10 @@ export class Archive {
               // We replace the section, if the request was about the whole sections.
               // We leave it to recursion if the request goes deeper into the sections.
               const archiveList = archive[key] as MSectionResponse[]
+              assert(
+                Array.isArray(archiveList),
+                `Expected array for key "${key}", but got "${archiveList}"`,
+              )
               archive[key] = archiveList
               const responseList = response[key] as MSectionResponse[]
               if (responseList.length !== archiveList.length) {
diff --git a/src/components/routing/loader.ts b/src/components/routing/loader.ts
index 0eeb1847..788c3a0d 100644
--- a/src/components/routing/loader.ts
+++ b/src/components/routing/loader.ts
@@ -1,7 +1,7 @@
 import {User} from 'oidc-client-ts'
 
 import {graphApi} from '../../utils/api'
-import {resolveAllMDefs} from '../../utils/metainfo'
+import {removeInternalRefs, resolveAllMDefs} from '../../utils/metainfo'
 import {JSONObject} from '../../utils/types'
 import {assert} from '../../utils/utils'
 import {DefaultSearch, LoaderResult, RouteMatch} from './types'
@@ -181,6 +181,7 @@ export default function loader(
     // entries, files, etc. Currently we apply this to all and the complete
     // responses no matter what they are.
     resolveAllMDefs(response)
+    removeInternalRefs(response)
     return {
       routeResponses: getResponsesForRoute(match, response as JSONObject),
       routeRequests: routeRequests,
diff --git a/src/pages/entry/entryRoute.tsx b/src/pages/entry/entryRoute.tsx
index 0d30e586..e54922f8 100644
--- a/src/pages/entry/entryRoute.tsx
+++ b/src/pages/entry/entryRoute.tsx
@@ -23,6 +23,7 @@ export const archiveRequest = {
   m_request: {
     directive: 'plain',
     include_definition: 'both',
+    depth: 2,
   },
   m_def: mDefRquest,
 } as MSectionRequest
diff --git a/src/pages/upload/uploadRoute.tsx b/src/pages/upload/uploadRoute.tsx
index ed18029c..f1624cd0 100644
--- a/src/pages/upload/uploadRoute.tsx
+++ b/src/pages/upload/uploadRoute.tsx
@@ -58,21 +58,26 @@ const entriesRoute: Route<
 > = {
   path: 'entries',
   breadcrumb: <b>entries</b>,
-  request: ({search}) => ({
-    m_request: {
-      pagination: createPaginationRequest(search),
-    },
-    '*': {
-      entry_id: '*',
-      mainfile_path: '*',
-      entry_create_time: '*',
-      complete_time: '*',
-      process_status: '*',
-      metadata: {
-        entry_type: '*',
+  request: ({search, isLeaf}) => {
+    if (!isLeaf) {
+      return {} as EntriesRequest
+    }
+    return {
+      m_request: {
+        pagination: createPaginationRequest(search),
       },
-    },
-  }),
+      '*': {
+        entry_id: '*',
+        mainfile_path: '*',
+        entry_create_time: '*',
+        complete_time: '*',
+        process_status: '*',
+        metadata: {
+          entry_type: '*',
+        },
+      },
+    }
+  },
   lazyComponent: async () => import('./Entries'),
   validateSearch: validatePaginationSearch,
   onlyRender: '',
diff --git a/src/utils/metainfo.test.ts b/src/utils/metainfo.test.ts
index 4d5d146a..66dbe5fc 100644
--- a/src/utils/metainfo.test.ts
+++ b/src/utils/metainfo.test.ts
@@ -1,6 +1,6 @@
 import {vi} from 'vitest'
 
-import {Metainfo, resolveAllMDefs} from './metainfo'
+import {Metainfo, removeInternalRefs, resolveAllMDefs} from './metainfo'
 import {JSONObject} from './types'
 
 describe('resolveSectionDef', () => {
@@ -173,6 +173,53 @@ describe('resolveSectionDef', () => {
   })
 })
 
+describe('removeInternalRefs', () => {
+  it('removes internal refs in a JSON object', () => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const data: any = {
+      m_def: {
+        all_sub_sections: {
+          child_with_m_def: {
+            sub_section: 'm_def',
+          },
+          child_repeats_with_m_def: {
+            sub_section: 'm_def',
+            repeats: true,
+          },
+          child_list_with_m_def: {
+            sub_section: 'm_def',
+            repeats: true,
+          },
+        },
+      },
+      value: '__INTERNAL__:',
+      child: {
+        value: '__INTERNAL__:',
+      },
+      child_list: ['__INTERNAL__:', 'not internal'],
+      recursive: {
+        child: {
+          child: '__INTERNAL__:',
+        },
+      },
+      child_full_list: ['__INTERNAL__:', '__INTERNAL__:'],
+      child_with_m_def: '__INTERNAL__:',
+      child_repeats_with_m_def: '__INTERNAL__:',
+      child_list_with_m_def: ['__INTERNAL__:'],
+    }
+    removeInternalRefs(data)
+
+    expect(data.value).toBeUndefined()
+    expect(data.child.value).toBeUndefined()
+    expect(data.child_list).toEqual([null, 'not internal'])
+    expect(data.recursive.child.child).toBeUndefined()
+    expect(data.child_full_list).toEqual([])
+    expect(data.child_with_m_def).toEqual({m_def: 'm_def'})
+    expect(data.child_repeats_with_m_def).toEqual([])
+    expect(data.child_list_with_m_def).toEqual([{m_def: 'm_def'}])
+  })
+})
+
 describe('resolveAllMDefs', () => {
   it('resolves m_defs in a JSON object', () => {
     function createMDef(m_def: string) {
diff --git a/src/utils/metainfo.ts b/src/utils/metainfo.ts
index ddcbef72..3bcf45e8 100644
--- a/src/utils/metainfo.ts
+++ b/src/utils/metainfo.ts
@@ -252,6 +252,60 @@ export class Metainfo {
   }
 }
 
+/**
+ * Recursively traverses through the given data object and
+ * removes all internal references that where added by the API.
+ * If there is not m_def available the internal reference are simply removed.
+ * If there are m_defs and we can figure out the type of the internal
+ * reference, they are replaces with empty sections or arrays.
+ */
+export function removeInternalRefs(data: JSONObject) {
+  const isInternalRef = (value: JSONValue) =>
+    typeof value === 'string' && value.startsWith('__INTERNAL__:')
+
+  const m_def = data?.m_def as Section | undefined
+
+  for (const key in data) {
+    if (key.startsWith('m_')) {
+      continue
+    }
+    const value = data[key]
+    const subSection = m_def?.all_sub_sections?.[key]
+    if (isInternalRef(value)) {
+      if (subSection) {
+        if (subSection.repeats) {
+          data[key] = []
+        } else {
+          data[key] = {
+            m_def: subSection.sub_section,
+          } as JSONObject
+        }
+      } else {
+        delete data[key]
+      }
+    } else if (Array.isArray(value)) {
+      value.forEach((item, index) => {
+        if (isInternalRef(item)) {
+          if (subSection) {
+            value[index] = {
+              m_def: subSection.sub_section,
+            } as JSONObject
+          } else {
+            value[index] = null
+          }
+        } else if (typeof item === 'object') {
+          removeInternalRefs(item as JSONObject)
+        }
+      })
+      if (value.every((item) => item === null)) {
+        data[key] = []
+      }
+    } else if (typeof value === 'object') {
+      removeInternalRefs(value as JSONObject)
+    }
+  }
+}
+
 /**
  * Recursively traverses through the given data object and
  * resolves all m_def references it can find.
-- 
GitLab


From be092f54a4a5eaf88feb7aa43f07115d1e0d1ba5 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Wed, 19 Feb 2025 13:26:53 +0100
Subject: [PATCH 05/17] Fixed indexed repeated sub section request related
 loader and archive issues.

---
 src/components/archive/archive.helper.tsx    | 10 ++++---
 src/components/archive/useArchive.test.tsx   | 24 ++++++++++++++-
 src/components/archive/useArchive.tsx        | 29 +++++++++++++-----
 src/components/routing/loader.test.ts        |  2 +-
 src/components/routing/loader.ts             | 14 +++++++--
 src/components/routing/types.ts              | 10 +++++++
 src/components/routing/useRouteData.test.tsx | 31 ++++++++++++++++++++
 src/pages/entry/entryRoute.tsx               | 22 +++++++++++++-
 8 files changed, 125 insertions(+), 17 deletions(-)

diff --git a/src/components/archive/archive.helper.tsx b/src/components/archive/archive.helper.tsx
index 39014f30..97a56e08 100644
--- a/src/components/archive/archive.helper.tsx
+++ b/src/components/archive/archive.helper.tsx
@@ -4,7 +4,7 @@ import {vi} from 'vitest'
 import {UseDataResult} from '../../hooks/useData'
 import {MDefResponse, MSectionResponse} from '../../models/graphResponseModels'
 import {Quantity, Section, SubSection} from '../../utils/metainfo'
-import {DefaultToObject} from '../../utils/types'
+import {DefaultToObject, JSONObject} from '../../utils/types'
 import * as useDataForRoute from '../routing/useDataForRoute'
 import * as useRouteData from '../routing/useRouteData'
 import {Archive, ArchiveChangeWithOldValue, allArchives} from './useArchive'
@@ -193,13 +193,15 @@ export function addDefinitions(
  * Recursively removes `m_def`s from the given response. This is useful
  * if you want to compare responses with manually written expected responses.
  */
-export function removeDefinitions(data: MSectionResponse) {
+export function removeDefinitions(data: JSONObject) {
   delete data.m_def
   for (const value of Object.values(data)) {
     if (Array.isArray(value)) {
-      value.filter((item) => !!item).forEach((item) => removeDefinitions(item))
+      value
+        .filter((item) => !!item)
+        .forEach((item) => removeDefinitions(item as JSONObject))
     } else if (typeof value === 'object') {
-      removeDefinitions(value as MSectionResponse)
+      removeDefinitions(value as JSONObject)
     }
   }
 
diff --git a/src/components/archive/useArchive.test.tsx b/src/components/archive/useArchive.test.tsx
index 3683a5e9..5284ea9b 100644
--- a/src/components/archive/useArchive.test.tsx
+++ b/src/components/archive/useArchive.test.tsx
@@ -5,6 +5,7 @@ import {ArchiveChangeAction} from '../../models/entryEditRequestModels'
 import {MSectionRequest} from '../../models/graphRequestModels'
 import {MSectionResponse} from '../../models/graphResponseModels'
 import {archiveRequest} from '../../pages/entry/entryRoute'
+import {JSONObject} from '../../utils/types'
 import {assert} from '../../utils/utils'
 import * as useRouteData from '../routing/useRouteData'
 import {addDefinitions, mockArchive, removeDefinitions} from './archive.helper'
@@ -80,6 +81,26 @@ describe('useArchive', () => {
         {r: [{q: 2}]},
         [['r', [{q: 2}]]],
       ],
+      [
+        'updating and closing hole in a repeating SubSection',
+        {r: [undefined, {q: 2}]},
+        {r: '*'},
+        {r: [{q: 1}, {q: 2}]},
+        {r: [{q: 1}, {q: 2}]},
+        [['r', [{q: 1}, {q: 2}]]],
+      ],
+      [
+        'indirectly updating and closing hole in a repeating SubSection',
+        {r: [undefined, {q: 2}]},
+        {r: {q: '*'}},
+        {r: [{q: 1}, {q: 2}]},
+        {r: [{q: 1}, {q: 2}]},
+        [
+          ['r', [{q: 1}, {q: 2}]],
+          ['r/0/q', 1],
+          ['r/1/q', 2],
+        ],
+      ],
       [
         'updating an existing repeating SubSection recursively',
         {r: [{q: 1}]},
@@ -223,7 +244,8 @@ describe('useArchive', () => {
         expect(archive.archive.m_def).not.toBeUndefined()
 
         // remove definitions to make the comparison simpler
-        removeDefinitions(archive.archive)
+        removeDefinitions(archive.archive as JSONObject)
+        removeDefinitions(changes as unknown as JSONObject)
 
         expect(archive.archive).toEqual(expectedArchive)
         expect(changes).toEqual(expectedUpdates)
diff --git a/src/components/archive/useArchive.tsx b/src/components/archive/useArchive.tsx
index 5762fe13..63404670 100644
--- a/src/components/archive/useArchive.tsx
+++ b/src/components/archive/useArchive.tsx
@@ -395,6 +395,7 @@ export class Archive {
       originalRequest: MSectionRequest,
       response: MSectionResponse,
     ) => {
+      assert(archive && response)
       const m_def = response.m_def as Section
       assert(m_def, `MSectionResponse for ${path} must come with a definition`)
       // TODO Is this good? Do we need to merge something here? What if
@@ -477,11 +478,10 @@ export class Archive {
           }
 
           if (indexed) {
-            const archiveList =
-              (archive[key] as unknown as MSectionResponse[]) || []
+            const archiveList: MSectionResponse[] = []
             archive[key] = archiveList
             const responseList = response[key] as MSectionResponse[]
-            archiveList[index] = responseList[index]
+            archiveList[index] = responseList[index] || undefined
             changes.push([
               pathForCurrentKey,
               responseList.map((item) => item || undefined),
@@ -511,12 +511,12 @@ export class Archive {
               if (archiveList[index]) {
                 // update
                 if (requestValue === '*') {
-                  archiveList[index] = responseList[index]
+                  archiveList[index] = responseList[index] || undefined
                   changes.push([indexedPathForCurrentKey, responseList[index]])
                 } // else { nothing to do, the recursion will update the section }
               } else {
                 // add
-                archiveList[index] = responseList[index]
+                archiveList[index] = responseList[index] || undefined
                 changes.push([indexedPathForCurrentKey, responseList[index]])
               }
             } else {
@@ -535,7 +535,6 @@ export class Archive {
                 Array.isArray(archiveList),
                 `Expected array for key "${key}", but got "${archiveList}"`,
               )
-              archive[key] = archiveList
               const responseList = response[key] as MSectionResponse[]
               if (responseList.length !== archiveList.length) {
                 if (responseList.length > archiveList.length) {
@@ -544,7 +543,7 @@ export class Archive {
                     index < responseList.length;
                     index++
                   ) {
-                    archiveList.push(responseList[index])
+                    archiveList.push(responseList[index] || undefined)
                   }
                 }
                 if (responseList.length < archiveList.length) {
@@ -577,8 +576,22 @@ export class Archive {
             const archiveList = archive[key] as MSectionResponse[]
             const responseList = response[key] as MSectionResponse[]
             for (let index = 0; index < responseList.length; index++) {
+              const archiveListItem = archiveList[index] || undefined
+              archiveList[index] = archiveListItem
+              const indexedPathForCurrentKey = `${pathForCurrentKey}/${index}`
+              if (archiveListItem === undefined) {
+                // patching a hole in the list
+                if (checkForConflicts(indexedPathForCurrentKey)) {
+                  continue
+                }
+                archiveList[index] = responseList[index] || undefined
+                changes.push([
+                  pathForCurrentKey,
+                  responseList.map((item) => item || undefined),
+                ])
+              }
               visit(
-                `${pathForCurrentKey}/${index}`,
+                indexedPathForCurrentKey,
                 archiveList[index],
                 requestValue as MSectionRequest,
                 responseList[index],
diff --git a/src/components/routing/loader.test.ts b/src/components/routing/loader.test.ts
index 82e55c5a..72d56774 100644
--- a/src/components/routing/loader.test.ts
+++ b/src/components/routing/loader.test.ts
@@ -40,7 +40,7 @@ describe('getResponseForPath', () => {
   })
 })
 
-describe('getResonseForRoute', () => {
+describe('getResponseForRoute', () => {
   it('works', () => {
     const result = getResponsesForRoute(
       [{route: {requestKey: 'key', request: '*'}} as unknown as RouteMatch],
diff --git a/src/components/routing/loader.ts b/src/components/routing/loader.ts
index 788c3a0d..054b9b33 100644
--- a/src/components/routing/loader.ts
+++ b/src/components/routing/loader.ts
@@ -15,7 +15,7 @@ export class DoesNotExistError extends Error {
 
 /**
  * Parses the given key and returns the base key and the index if the key is
- * if the key is indexed (e.g. 'key[0]'). Otherwise, returns the key itself ans
+ * if the key is indexed (e.g. 'key[0]'). Otherwise, returns the key itself and
  * `undefined`.
  */
 export function getIndexedKey(key: string): [string, number | undefined] {
@@ -70,7 +70,17 @@ export function getResponsesForRoute(
       return undefined
     }
     const hasRequest = match[index].route.request !== undefined
-    return getResponseForPath(path, response, hasRequest)
+    const responseForPath = getResponseForPath(path, response, hasRequest)
+    if (responseForPath === undefined) {
+      return undefined
+    }
+
+    const transformResponse =
+      match[index].route.response || ((response: unknown) => response)
+    return transformResponse(responseForPath, {
+      ...match[index],
+      search: match[index].search as DefaultSearch,
+    }) as JSONObject
   })
 }
 
diff --git a/src/components/routing/types.ts b/src/components/routing/types.ts
index a5cf8845..5034f404 100644
--- a/src/components/routing/types.ts
+++ b/src/components/routing/types.ts
@@ -301,6 +301,16 @@ export type Route<
         locationData: RouteMatch<Request, Response, Search>,
       ) => DefaultToObject<Request>)
 
+  /**
+   * An optional function that transforms the response data. The transformed
+   * data will be available in the `response` property of the respective
+   * `RouteData` object, e.g. obtained via `useRoute`.
+   */
+  response?: (
+    response: DefaultToObject<unknown>,
+    locationData: RouteMatch<Request, Response, Search>,
+  ) => DefaultToObject<Response>
+
   /**
    * An optional function that is called after the data for the route has
    * been fetched. The function receives the route segment data and the full response.
diff --git a/src/components/routing/useRouteData.test.tsx b/src/components/routing/useRouteData.test.tsx
index 37ff942b..c3d0b780 100644
--- a/src/components/routing/useRouteData.test.tsx
+++ b/src/components/routing/useRouteData.test.tsx
@@ -209,6 +209,37 @@ describe('useRouteData', () => {
     expect(mockedApi).toHaveBeenCalledWith({parent: {'*': '*'}}, undefined)
   })
 
+  it('calls response', async () => {
+    const response = vi
+      .fn()
+      .mockImplementation(({dataKey: data}) => ({dataKey: data + 'changed'}))
+    const route: Route = {
+      path: '',
+      component: Outlet,
+      children: [
+        {
+          path: 'data',
+          request: {dataKey: '*'},
+          response,
+          component: function Component() {
+            const {dataKey: data} = useRouteData<unknown, {dataKey: string}>()
+            return <div>{data}</div>
+          },
+        },
+      ],
+    }
+    mockedApi.mockResolvedValue({
+      data: {
+        dataKey: 'value',
+      },
+    } as GraphResponse)
+    window.history.replaceState(null, '', '/data')
+    await act(() => render(<Router route={route} loader={loader} />))
+    expect(mockedApi).toBeCalledTimes(1)
+    expect(response).toBeCalledTimes(1)
+    expect(screen.getByText('valuechanged')).toBeInTheDocument()
+  })
+
   it('calls onFetch', async () => {
     const onFetch = vi.fn()
     const route: Route = {
diff --git a/src/pages/entry/entryRoute.tsx b/src/pages/entry/entryRoute.tsx
index e54922f8..efaf736d 100644
--- a/src/pages/entry/entryRoute.tsx
+++ b/src/pages/entry/entryRoute.tsx
@@ -1,6 +1,7 @@
 import path from 'path-browserify'
 
 import {getArchive} from '../../components/archive/useArchive'
+import {getIndexedKey} from '../../components/routing/loader'
 import {Route} from '../../components/routing/types'
 import {
   EntryMetadataRequest,
@@ -30,7 +31,26 @@ export const archiveRequest = {
 
 export const archiveRoute: Route<MSectionRequest, MSectionResponse> = {
   path: ':name',
-  request: {...archiveRequest},
+  requestKey: ({path}) => getIndexedKey(path)[0],
+  request: ({path}) => {
+    const index = getIndexedKey(path)[1]
+    if (index !== undefined) {
+      return {
+        ...archiveRequest,
+        [index]: {...archiveRequest},
+      }
+    } else {
+      return {...archiveRequest}
+    }
+  },
+  response: (response, {path}) => {
+    const index = getIndexedKey(path)[1]
+    if (index !== undefined) {
+      return response[index] as MSectionResponse
+    } else {
+      return response
+    }
+  },
   lazyComponent: async () => import('./EntrySection'),
   renderWithPathAsKey: true,
 }
-- 
GitLab


From aa51856b613681594e960f2134e7d144065c9e66 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Wed, 19 Feb 2025 22:40:28 +0100
Subject: [PATCH 06/17] The entry nav syncs with the archive now.

---
 src/components/archive/useArchive.tsx |   2 -
 src/components/editor/utils.ts        |   4 +-
 src/components/navigation/TreeNav.tsx |  51 ++++++--
 src/pages/entry/EntryArchiveNav.tsx   | 181 +++++++++++++++-----------
 src/pages/entry/EntryOverview.tsx     |  45 +------
 src/pages/entry/entryRoute.tsx        |  69 +++++++++-
 6 files changed, 220 insertions(+), 132 deletions(-)

diff --git a/src/components/archive/useArchive.tsx b/src/components/archive/useArchive.tsx
index 63404670..d0216d06 100644
--- a/src/components/archive/useArchive.tsx
+++ b/src/components/archive/useArchive.tsx
@@ -864,8 +864,6 @@ export function useArchiveProperty<T = unknown, D extends Property = Property>(
     'Archive path must not change',
   )
 
-  // TODO the value in the archive might have changed for the same path,
-  // but we keep relying on the old and wrong localState.
   const propertyState = archive.getState(path) as ArchivePropertyState<T>
   const render = useRender()
 
diff --git a/src/components/editor/utils.ts b/src/components/editor/utils.ts
index 40844b1e..0d99d1cd 100644
--- a/src/components/editor/utils.ts
+++ b/src/components/editor/utils.ts
@@ -1,7 +1,7 @@
 import {has, isEqual, isObject, merge} from 'lodash'
 
 import {MSectionRequest} from '../../models/graphRequestModels'
-import {mDefRquest} from '../../pages/entry/entryRoute'
+import {mDefRequest} from '../../pages/entry/entryRoute'
 import {assert} from '../../utils/utils'
 import {LayoutItem} from '../layout/Layout'
 
@@ -11,7 +11,7 @@ export const defaultSubSectionRequest = {
     include_definition: 'both',
     exclude: ['*'],
   },
-  m_def: mDefRquest,
+  m_def: mDefRequest,
 } as MSectionRequest
 
 /**
diff --git a/src/components/navigation/TreeNav.tsx b/src/components/navigation/TreeNav.tsx
index e8f15134..6885abbf 100644
--- a/src/components/navigation/TreeNav.tsx
+++ b/src/components/navigation/TreeNav.tsx
@@ -4,16 +4,30 @@ import {useFirstMountState, usePrevious} from 'react-use'
 
 import {JSONObject} from '../../utils/types'
 import {getResponseForPath} from '../routing/loader'
-import useDataForRoute from '../routing/useDataForRoute'
+import useDataForRoute, {
+  UseDataForRouteParams,
+} from '../routing/useDataForRoute'
 import useRoute from '../routing/useRoute'
 import {useAvailableRouteData} from '../routing/useRouteData'
 import NavItem, {NavItemProps} from './NavItem'
 
 export type TreeNavProps = {
   /**
-   * The request object that is used to fetch children.
+   * The request object that is used to fetch children. Either the object
+   * itself or a function that creates the object. The function is called with
+   * the path of the child.
    */
-  request: JSONObject
+  request: JSONObject | ((path: string) => JSONObject)
+  /**
+   * An optional function that transforms the response from fetching children
+   * before it is used.
+   */
+  response?: (response: JSONObject, path: string) => JSONObject
+  /**
+   * Optional parameters passed to the useDataForRoute hook, when fetching
+   * more data with the given `request`.
+   */
+  useDataForRouteParams?: Partial<UseDataForRouteParams>
   /**
    * Determines if available route data is sufficient to render the tree item
    * or if the data has to be fetched. By default route data will always be
@@ -76,6 +90,8 @@ export type TreeNavProps = {
 export default function TreeNav({
   path,
   request,
+  response = (response) => response,
+  useDataForRouteParams: dataForRouteParams = {},
   dataIsSufficient = () => true,
   filterChildKeys = () => true,
   getChildProps = () => ({}),
@@ -101,14 +117,21 @@ export default function TreeNav({
   const splitPath = useMemo(() => path.split('/'), [path])
   const key = useMemo(() => splitPath.at(-1) || 'no path', [splitPath])
 
-  const useDataForRouteParams = useMemo(
-    () => ({request: {[path]: request}, noImplicitFetch: true}),
-    [request, path],
-  )
-  const dataForRouteResult = useDataForRoute(useDataForRouteParams)
-  const {fetch, loading} = dataForRouteResult
-  const fetchedData =
-    dataForRouteResult.data && getResponseForPath(path, dataForRouteResult.data)
+  const useDataForRouteParams = useMemo(() => {
+    return {
+      request:
+        typeof request === 'function' ? request(path) : {[path]: {...request}},
+      noImplicitFetch: true,
+      ...dataForRouteParams,
+    }
+  }, [request, path, dataForRouteParams])
+
+  const {
+    fetch,
+    loading,
+    data: dataForRoute,
+  } = useDataForRoute(useDataForRouteParams)
+  const fetchedData = dataForRoute && getResponseForPath(path, dataForRoute)
   const hasFetchedData = !!fetchedData
 
   const {index, fullMatch, navigate} = useRoute()
@@ -224,7 +247,7 @@ export default function TreeNav({
     isFirstMountState,
   ])
 
-  const renderData = currentData.current
+  const renderData = currentData.current && response(currentData.current, path)
   const renderExpanded = expanded || (justGotInRoute && expandable)
   const renderSelected = selected || (inRoute && (!renderExpanded || loading))
 
@@ -279,6 +302,8 @@ export default function TreeNav({
                     path={`${path}/${key}`}
                     expandable={expandable}
                     request={request}
+                    response={response}
+                    useDataForRouteParams={dataForRouteParams}
                     dataIsSufficient={dataIsSufficient}
                     getChildProps={getChildProps}
                     orderCompareFn={orderCompareFn}
@@ -301,6 +326,8 @@ export default function TreeNav({
                     path={`${path}/${leastChildKey}`}
                     expandable={expandable}
                     request={request}
+                    response={response}
+                    useDataForRouteParams={dataForRouteParams}
                     dataIsSufficient={dataIsSufficient}
                     getChildProps={getChildProps}
                     orderCompareFn={orderCompareFn}
diff --git a/src/pages/entry/EntryArchiveNav.tsx b/src/pages/entry/EntryArchiveNav.tsx
index 5967b9cb..db99a9de 100644
--- a/src/pages/entry/EntryArchiveNav.tsx
+++ b/src/pages/entry/EntryArchiveNav.tsx
@@ -2,14 +2,21 @@ import RepeatedSubSectionIcon from '@mui/icons-material/DataArray'
 import SectionIcon from '@mui/icons-material/DataObjectOutlined'
 import QuantityIcon from '@mui/icons-material/LooksOneOutlined'
 import UnknownIcon from '@mui/icons-material/QuestionMark'
-import {useMemo} from 'react'
+import {useCallback, useEffect} from 'react'
 
+import useArchive from '../../components/archive/useArchive'
 import TreeNav, {TreeNavProps} from '../../components/navigation/TreeNav'
+import {UseDataForRouteParams} from '../../components/routing/useDataForRoute'
 import useRoute from '../../components/routing/useRoute'
+import useRender from '../../hooks/useRender'
 import {Section} from '../../utils/metainfo'
 import {JSONObject} from '../../utils/types'
 import {assert} from '../../utils/utils'
-import {archiveRequest} from './entryRoute'
+import {
+  archiveRequest,
+  mDefRequest,
+  useEntryDataForRouteParams,
+} from './entryRoute'
 
 export type EntryArchiveNavProps = Pick<
   TreeNavProps,
@@ -38,86 +45,112 @@ function expandable(data: JSONObject) {
   )
 }
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-function getChildProps(data: JSONObject, key: string, path: string) {
-  const sectionDef = data.m_def as Section
-  const property = sectionDef.all_properties[key] || {}
-
-  const isQuantity = property.m_def === 'Quantity'
-  const isSubSection = property.m_def === 'SubSection' && !property.repeats
-  const isUnknown = property.m_def === undefined
-  const isRepeatingSubSection =
-    property.m_def === 'SubSection' && property.repeats
-
-  let icon
-  if (isQuantity) {
-    icon = <QuantityIcon />
-  } else if (isSubSection) {
-    icon = <SectionIcon />
-  } else if (isRepeatingSubSection) {
-    icon = <RepeatedSubSectionIcon />
-  } else if (isUnknown) {
-    icon = <UnknownIcon />
-  } else {
-    throw new Error('Impossible state.')
-  }
+export default function EntryArchiveNav({
+  ...treeNavProps
+}: EntryArchiveNavProps) {
+  const {fullMatch, index} = useRoute()
+  const highlighted =
+    fullMatch.length === index + 2 && fullMatch[index + 1]?.path === 'archive'
 
-  const sharedProps = {
-    variant: 'treeNode',
-    icon,
-    alignWithExpandIcons: true,
-  }
+  const archive = useArchive()
+  const rerender = useRender()
 
-  if (isQuantity || isUnknown) {
-    return {
-      ...sharedProps,
-      expandable: false,
-      disabled: isUnknown,
-    } as TreeNavProps
-  }
+  useEffect(() => {
+    return archive.registerChangeListener(rerender)
+  }, [archive, rerender])
 
-  if (isSubSection) {
-    return {
-      ...sharedProps,
-      getChildProps,
-      filterChildKeys,
-      expandable,
-    } as TreeNavProps
-  }
+  const request = useCallback((path: string) => {
+    const request: JSONObject = {m_def: mDefRequest}
+    let current = request
+    for (const key of path.split('/').slice(1)) {
+      current[key] = {m_def: mDefRequest}
+      current = current[key]
+    }
+    Object.assign(current, archiveRequest)
+    const result = {archive: request}
+    return result
+  }, [])
+
+  const response = useCallback(
+    (response: JSONObject, path: string) => {
+      path = path.replace(/^archive\/?/, '')
+      return archive.getValue(path) as JSONObject
+    },
+    [archive],
+  )
+
+  const useDataForRouteParams = useEntryDataForRouteParams((error) => {
+    // eslint-disable-next-line no-console
+    console.error('Error while fetching nav data :', error)
+  }) as Partial<UseDataForRouteParams>
+
+  const getChildProps = useCallback(
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    (data: JSONObject, key: string, path: string) => {
+      const sectionDef = data.m_def as Section
+      const property = sectionDef.all_properties[key] || {}
 
-  if (isRepeatingSubSection) {
-    return {
-      ...sharedProps,
-      expandable: true,
-      getChildProps: (data: JSONObject, key: string, path: string) =>
-        ({
+      const isQuantity = property.m_def === 'Quantity'
+      const isSubSection = property.m_def === 'SubSection' && !property.repeats
+      const isUnknown = property.m_def === undefined
+      const isRepeatingSubSection =
+        property.m_def === 'SubSection' && property.repeats
+
+      let icon
+      if (isQuantity) {
+        icon = <QuantityIcon />
+      } else if (isSubSection) {
+        icon = <SectionIcon />
+      } else if (isRepeatingSubSection) {
+        icon = <RepeatedSubSectionIcon />
+      } else if (isUnknown) {
+        icon = <UnknownIcon />
+      } else {
+        throw new Error('Impossible state.')
+      }
+
+      const sharedProps = {
+        variant: 'treeNode',
+        icon,
+        alignWithExpandIcons: true,
+      }
+
+      if (isQuantity || isUnknown) {
+        return {
+          ...sharedProps,
+          expandable: false,
+          disabled: isUnknown,
+        } as TreeNavProps
+      }
+
+      if (isSubSection) {
+        return {
           ...sharedProps,
           getChildProps,
           filterChildKeys,
           expandable,
-          icon: <SectionIcon />,
-          label: key,
-          path: `${path}[${key}]`,
-        } as TreeNavProps),
-    } as TreeNavProps
-  }
-
-  throw new Error('Impossible state.')
-}
+        } as TreeNavProps
+      }
 
-export default function EntryArchiveNav({
-  ...treeNavProps
-}: EntryArchiveNavProps) {
-  const {fullMatch, index} = useRoute()
-  const highlighted =
-    fullMatch.length === index + 2 && fullMatch[index + 1]?.path === 'archive'
+      if (isRepeatingSubSection) {
+        return {
+          ...sharedProps,
+          expandable: true,
+          getChildProps: (data: JSONObject, key: string, path: string) =>
+            ({
+              ...sharedProps,
+              getChildProps,
+              filterChildKeys,
+              expandable,
+              icon: <SectionIcon />,
+              label: key,
+              path: `${path}[${key}]`,
+            } as TreeNavProps),
+        } as TreeNavProps
+      }
 
-  const request = useMemo(
-    () => ({
-      ...archiveRequest,
-      // TODO this does not seem to have the desired effect
-      '*': '*',
-    }),
+      throw new Error('Impossible state.')
+    },
     [],
   )
 
@@ -127,7 +160,9 @@ export default function EntryArchiveNav({
       path='archive'
       variant='menuItem'
       icon={undefined}
-      request={request as unknown as JSONObject}
+      request={request}
+      response={response}
+      useDataForRouteParams={useDataForRouteParams}
       expandable={expandable}
       getChildProps={getChildProps}
       filterChildKeys={filterChildKeys}
diff --git a/src/pages/entry/EntryOverview.tsx b/src/pages/entry/EntryOverview.tsx
index e48971a1..12161ea5 100644
--- a/src/pages/entry/EntryOverview.tsx
+++ b/src/pages/entry/EntryOverview.tsx
@@ -1,5 +1,5 @@
 import {Box} from '@mui/material'
-import {useCallback, useMemo, useState} from 'react'
+import {useMemo, useState} from 'react'
 import {usePrevious} from 'react-use'
 
 import ErrorMessage from '../../components/app/ErrorMessage'
@@ -7,15 +7,10 @@ import useArchive from '../../components/archive/useArchive'
 import {calculateRequestFromLayout} from '../../components/editor/utils'
 import {LayoutItem} from '../../components/layout/Layout'
 import useSelect from '../../components/navigation/useSelect'
-import useDataForRoute from '../../components/routing/useDataForRoute'
 import useRoute from '../../components/routing/useRoute'
 import useRouteData from '../../components/routing/useRouteData'
 import {EntryRequest, MSectionRequest} from '../../models/graphRequestModels'
-import {
-  EntryResponse,
-  GraphResponse,
-  MSectionResponse,
-} from '../../models/graphResponseModels'
+import {MSectionResponse} from '../../models/graphResponseModels'
 import {Section} from '../../utils/metainfo'
 import {assert} from '../../utils/utils'
 import UploadMetadataEditor from '../upload/UploadMetadataEditor'
@@ -23,7 +18,7 @@ import EntryDataEditor from './EntryDataEditor'
 import EntryMetadataEditor from './EntryMetadataEditor'
 import EntryPageTitle from './EntryPageTitle'
 import EntrySubSectionTable from './EntrySubSectionTable'
-import entryRoute, {archiveRequest} from './entryRoute'
+import entryRoute, {archiveRequest, useEntryDataForRoute} from './entryRoute'
 
 function EntryOverviewEditor() {
   const {archive: archiveData} = useRouteData(entryRoute)
@@ -72,8 +67,6 @@ function EntryOverviewEditor() {
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [layout, reloadCount])
 
-  const archive = useArchive()
-
   // Ideally we would call this in the onBeforeFetch callback, but
   // this will be too late because the effect is called after the render.
   // When rendering the EntryOverviewEditor and the
@@ -83,40 +76,12 @@ function EntryOverviewEditor() {
   // will cause state updates in the future as those are not allowed during
   // render.
   const requestHasChanged = request !== usePrevious(request)
+  const archive = useArchive()
   if (requestHasChanged) {
     archive.startUpdate(request.archive as MSectionRequest, true)
   }
 
-  const onBeforeFetch = useCallback(() => {
-    if (!archive.loading) {
-      archive.startUpdate(request.archive as MSectionRequest, true)
-    }
-    return () => {
-      archive.abortUpdate()
-    }
-  }, [archive, request])
-
-  const onFetch = useCallback(
-    (data: EntryResponse, fullResponse: GraphResponse) => {
-      archive.commitUpdate(data?.archive as MSectionResponse, fullResponse)
-    },
-    [archive],
-  )
-
-  const onError = useCallback(
-    (error: Error) => {
-      archive.abortUpdate()
-      setError(error)
-    },
-    [archive],
-  )
-
-  useDataForRoute<EntryRequest, EntryResponse>({
-    request,
-    onBeforeFetch,
-    onFetch,
-    onError,
-  })
+  useEntryDataForRoute(request, setError)
 
   let content: React.ReactNode = ''
   if (error) {
diff --git a/src/pages/entry/entryRoute.tsx b/src/pages/entry/entryRoute.tsx
index efaf736d..b910f90e 100644
--- a/src/pages/entry/entryRoute.tsx
+++ b/src/pages/entry/entryRoute.tsx
@@ -1,8 +1,12 @@
 import path from 'path-browserify'
+import {useCallback, useMemo} from 'react'
 
-import {getArchive} from '../../components/archive/useArchive'
+import useArchive, {getArchive} from '../../components/archive/useArchive'
 import {getIndexedKey} from '../../components/routing/loader'
 import {Route} from '../../components/routing/types'
+import useDataForRoute, {
+  UseDataForRouteParams,
+} from '../../components/routing/useDataForRoute'
 import {
   EntryMetadataRequest,
   EntryRequest,
@@ -14,7 +18,7 @@ import {
   MSectionResponse,
 } from '../../models/graphResponseModels'
 
-export const mDefRquest = {
+export const mDefRequest = {
   m_request: {
     directive: 'plain',
   },
@@ -26,7 +30,7 @@ export const archiveRequest = {
     include_definition: 'both',
     depth: 2,
   },
-  m_def: mDefRquest,
+  m_def: mDefRequest,
 } as MSectionRequest
 
 export const archiveRoute: Route<MSectionRequest, MSectionResponse> = {
@@ -56,6 +60,65 @@ export const archiveRoute: Route<MSectionRequest, MSectionResponse> = {
 }
 archiveRoute.children = [archiveRoute]
 
+/**
+ * Provides parameters for the `useDataForRoute` hook that will make the
+ * hook also update the archive data.
+ */
+export function useEntryDataForRouteParams(
+  setError?: (error: Error) => void,
+): Pick<
+  UseDataForRouteParams<EntryRequest, EntryResponse>,
+  'onBeforeFetch' | 'onFetch' | 'onError'
+> {
+  const archive = useArchive()
+
+  const onBeforeFetch = useCallback(
+    (request: EntryRequest) => {
+      if (!archive.loading) {
+        archive.startUpdate(request.archive as MSectionRequest, true)
+      }
+      return () => {
+        archive.abortUpdate()
+      }
+    },
+    [archive],
+  )
+
+  const onFetch = useCallback(
+    (data: EntryResponse, fullResponse: GraphResponse) => {
+      archive.commitUpdate(data?.archive as MSectionResponse, fullResponse)
+    },
+    [archive],
+  )
+
+  const onError = useCallback(
+    (error: Error) => {
+      archive.abortUpdate()
+      setError?.(error)
+    },
+    [archive, setError],
+  )
+
+  return useMemo(
+    () => ({onBeforeFetch, onFetch, onError}),
+    [onBeforeFetch, onFetch, onError],
+  )
+}
+
+/**
+ * A specific variant of `useEntryDataForRoute` that updates the
+ * archive data for the entry.
+ */
+export function useEntryDataForRoute(
+  request: EntryRequest,
+  setError?: (error: Error) => void,
+) {
+  return useDataForRoute<EntryRequest, EntryResponse>({
+    request,
+    ...useEntryDataForRouteParams(setError),
+  })
+}
+
 const entryRoute: Route<EntryRequest, EntryResponse> = {
   path: ':entryId',
   // TODO The function should not be necessary, but some buggy thing is modifying the
-- 
GitLab


From 562e6adfba9dbadaf3ea342171e954eeb85d9880 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Mon, 3 Mar 2025 12:54:41 +0100
Subject: [PATCH 07/17] Added unsaved changes badges.

---
 src/components/app/MainMenu.tsx            | 57 +++++++------
 src/components/archive/archive.helper.tsx  | 11 ++-
 src/components/archive/useArchive.test.tsx | 78 +++++++++++++++++
 src/components/archive/useArchive.tsx      | 98 ++++++++++++++++++++--
 src/components/navigation/NavItem.tsx      | 17 +++-
 src/index.tsx                              |  2 +-
 src/pages/entry/UnsavedChangesNav.tsx      | 62 ++++++++++++++
 src/pages/upload/UploadPage.tsx            |  3 +
 src/pages/uploads/UploadsPage.tsx          | 31 ++++++-
 9 files changed, 314 insertions(+), 45 deletions(-)
 create mode 100644 src/pages/entry/UnsavedChangesNav.tsx

diff --git a/src/components/app/MainMenu.tsx b/src/components/app/MainMenu.tsx
index 17f66bdf..d1e3f0a7 100644
--- a/src/components/app/MainMenu.tsx
+++ b/src/components/app/MainMenu.tsx
@@ -1,5 +1,5 @@
 import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'
-import {List} from '@mui/material'
+import {Badge, BadgeProps, List} from '@mui/material'
 import Box from '@mui/material/Box'
 import Divider from '@mui/material/Divider'
 import Drawer from '@mui/material/Drawer'
@@ -10,17 +10,10 @@ import ListItemText from '@mui/material/ListItemText'
 import {CSSObject, Theme, styled} from '@mui/material/styles'
 import * as React from 'react'
 
+import {UploadsMainMenuItem} from '../../pages/uploads/UploadsPage'
 import {SxProps} from '../../utils/types'
 import useDevTools from '../devTools/useDevTools'
-import {
-  Analyze,
-  Dev,
-  Explore,
-  Group,
-  Manage,
-  Menu,
-  ToggleColorMode,
-} from '../icons'
+import {Analyze, Dev, Explore, Group, Menu, ToggleColorMode} from '../icons'
 import Link from '../routing/Link'
 import useRoute from '../routing/useRoute'
 import {Logo} from '../theme/Theme'
@@ -81,6 +74,10 @@ export type MainMenuItemProps = React.PropsWithChildren<{
    * Whether the item is active and should be highlighted
    */
   active?: boolean
+  /**
+   * Props for a MUI badge to show on icon of the item
+   */
+  badge?: BadgeProps
 }> &
   ListItemButtonProps &
   SxProps
@@ -93,11 +90,30 @@ export const MainMenuItem = React.forwardRef<HTMLElement, MainMenuItemProps>(
       icon,
       active = false,
       children,
+      badge,
       sx = {},
       ...listItemButtonProps
     },
     ref,
   ) {
+    const iconElement = icon && (
+      <Badge
+        {...badge}
+        sx={{
+          '& .MuiBadge-badge': {left: 0, right: 'initial'},
+        }}
+      >
+        <ListItemIcon
+          sx={{
+            minWidth: 0,
+            mr: open ? 3 : 'auto',
+            justifyContent: 'center',
+          }}
+        >
+          {icon}
+        </ListItemIcon>
+      </Badge>
+    )
     return (
       <ListItem
         ref={ref as React.Ref<HTMLLIElement>}
@@ -140,17 +156,7 @@ export const MainMenuItem = React.forwardRef<HTMLElement, MainMenuItemProps>(
               {children}
             </Box>
           )}
-          {icon && (
-            <ListItemIcon
-              sx={{
-                minWidth: 0,
-                mr: open ? 3 : 'auto',
-                justifyContent: 'center',
-              }}
-            >
-              {icon}
-            </ListItemIcon>
-          )}
+          {iconElement}
           <ListItemText primary={label} sx={{opacity: open ? 1 : 0}} />
         </ListItemButton>
       </ListItem>
@@ -193,14 +199,7 @@ export default function MainMenu() {
       </List>
       <Divider />
       <List sx={{flexGrow: 1}}>
-        <MainMenuItem
-          label='Manage'
-          open={open}
-          icon={<Manage />}
-          component={Link}
-          to='/uploads'
-          active={matches('/uploads')}
-        />
+        <UploadsMainMenuItem open={open} component={Link} />
         <MainMenuItem
           label='Explore'
           open={open}
diff --git a/src/components/archive/archive.helper.tsx b/src/components/archive/archive.helper.tsx
index 97a56e08..e6df20be 100644
--- a/src/components/archive/archive.helper.tsx
+++ b/src/components/archive/archive.helper.tsx
@@ -7,12 +7,13 @@ import {Quantity, Section, SubSection} from '../../utils/metainfo'
 import {DefaultToObject, JSONObject} from '../../utils/types'
 import * as useDataForRoute from '../routing/useDataForRoute'
 import * as useRouteData from '../routing/useRouteData'
-import {Archive, ArchiveChangeWithOldValue, allArchives} from './useArchive'
+import {ArchiveChangeWithOldValue, allArchives, getArchive} from './useArchive'
 
 export type MockArchiveArgs = {
   entryId?: string
   data?: MSectionResponse
   changes?: ArchiveChangeWithOldValue[]
+  clearArchives?: boolean
 }
 
 /**
@@ -24,16 +25,18 @@ export function mockArchive({
   entryId = 'entryId',
   data,
   changes = [],
+  clearArchives = true,
 }: MockArchiveArgs = {}) {
-  allArchives.clear()
-  const archive = new Archive()
+  if (clearArchives) {
+    allArchives.clear()
+  }
+  const archive = getArchive(entryId)
   if (data) {
     archive.archive = data
     archive.loading = false
   } else {
     archive.loading = true
   }
-  allArchives.set(entryId, archive)
   archive.changeStack.push(...changes)
   vi.spyOn(useRouteData, 'default').mockReturnValue({
     entry_id: entryId,
diff --git a/src/components/archive/useArchive.test.tsx b/src/components/archive/useArchive.test.tsx
index 5284ea9b..ed3890d6 100644
--- a/src/components/archive/useArchive.test.tsx
+++ b/src/components/archive/useArchive.test.tsx
@@ -14,8 +14,13 @@ import useArchive, {
   allArchives,
   useArchiveChanges,
   useArchiveProperty,
+  useChangedArchives,
 } from './useArchive'
 
+afterEach(() => {
+  allArchives.clear()
+})
+
 describe('useArchive', () => {
   describe('Archive', () => {
     let archive: Archive
@@ -617,3 +622,76 @@ describe('useArchiveChanges', () => {
     expect(result.current.changeStack.length).toBe(0)
   })
 })
+
+describe('useChangedArchives', () => {
+  it('returns empty list with no archives', () => {
+    const {result} = renderHook(() => useChangedArchives())
+    expect(result.current).toHaveLength(0)
+  })
+
+  it('returns empty list with no changes', () => {
+    vi.spyOn(useRouteData, 'default').mockReturnValue({
+      entry_id: 'entryId',
+    })
+    renderHook(() => useArchive())
+    const {result} = renderHook(() => useChangedArchives())
+    expect(result.current).toHaveLength(0)
+  })
+
+  it('returns the list of changed archives', () => {
+    mockArchive({
+      entryId: 'entryId0',
+      data: addDefinitions({
+        quantity: 'test_value',
+      }),
+      changes: [{path: 'quantity', new_value: 'new_value', action: 'upsert'}],
+    })
+    mockArchive({
+      clearArchives: false,
+      entryId: 'entryId1',
+      data: addDefinitions({
+        quantity: 'test_value',
+      }),
+      changes: [
+        {path: 'quantity', new_value: 'new_value', action: 'upsert'},
+        {path: 'quantity', new_value: 'new_value', action: 'upsert'},
+      ],
+    })
+    mockArchive({
+      clearArchives: false,
+      entryId: 'entryId2',
+      data: addDefinitions({
+        quantity: 'test_value',
+      }),
+      changes: [],
+    })
+    const {result} = renderHook(() => useChangedArchives())
+    expect(result.current).toHaveLength(2)
+  })
+
+  it('renders on change', () => {
+    const archive = mockArchive({
+      data: addDefinitions({
+        quantity: 'test_value',
+      }),
+    })
+    const {result} = renderHook(() => useChangedArchives())
+    expect(result.current).toHaveLength(0)
+    act(() => archive.submitElementChange('quantity', 'upsert', 'new_value'))
+    expect(result.current).toHaveLength(1)
+  })
+
+  it('renders on new changed archive', () => {
+    const {result} = renderHook(() => useChangedArchives())
+    expect(result.current).toHaveLength(0)
+    act(() =>
+      mockArchive({
+        data: addDefinitions({
+          quantity: 'test_value',
+        }),
+        changes: [{path: 'quantity', new_value: 'new_value', action: 'upsert'}],
+      }),
+    )
+    expect(result.current).toHaveLength(1)
+  })
+})
diff --git a/src/components/archive/useArchive.tsx b/src/components/archive/useArchive.tsx
index d0216d06..9cb63302 100644
--- a/src/components/archive/useArchive.tsx
+++ b/src/components/archive/useArchive.tsx
@@ -60,11 +60,17 @@ type ArchivePropertyState<T> = {
 
 type ArchivePropertyDispatch = React.Dispatch<ArchivePropertyState<unknown>>
 
+type ArchiveEntry = Pick<
+  EntryResponse,
+  'entry_id' | 'upload_id' | 'mainfile_path'
+>
+
 /**
  * A class to manage the client-side state of an entry's archive. See
  * `useArchive` for the hook that provides access it.
  */
 export class Archive {
+  entry: ArchiveEntry
   elements: MultiMap<string, ArchivePropertyDispatch>
   changeListener: (() => void)[]
   archive: MSectionResponse
@@ -74,7 +80,8 @@ export class Archive {
   responsesWithReferencedArchives: MultiMap<string, GraphResponse>
   currentUpdateRequests: MSectionRequest | undefined
 
-  constructor() {
+  constructor(entry?: ArchiveEntry) {
+    this.entry = entry || {}
     this.elements = new MultiMap<string, ArchivePropertyDispatch>()
     this.changeListener = []
     this.archive = {}
@@ -92,6 +99,12 @@ export class Archive {
     }
   }
 
+  /**
+   * Registers a callback that is called when the archive changes.
+   *
+   * @param listener The callback that is called on each change.
+   * @returns A function to unregister the listener.
+   */
   registerChangeListener(listener: () => void) {
     this.changeListener.push(listener)
     return () => {
@@ -719,11 +732,30 @@ export class Archive {
 
 export const allArchives = new Map<string, Archive>()
 
-export function getArchive(entry_id: string) {
+export function getArchive(entry_id: string, entry?: ArchiveEntry) {
   if (!allArchives.has(entry_id)) {
-    allArchives.set(entry_id, new Archive())
+    const archive = new Archive(entry)
+    allArchives.set(entry_id, archive)
+    allArchivesChangeListeners.forEach((listener) => listener())
+  }
+  const archive = allArchives.get(entry_id) as Archive
+  archive.entry = {...archive.entry, ...entry}
+  return archive
+}
+
+const allArchivesChangeListeners = new Set<() => void>()
+
+/**
+ * Registers a callback that is called when a new archive is created.
+ *
+ * @param listener The callback that is called when a new archive is created.
+ * @returns A function to unregister the listener.
+ */
+function registerArchivesChangeListener(listener: () => void) {
+  allArchivesChangeListeners.add(listener)
+  return () => {
+    allArchivesChangeListeners.delete(listener)
   }
-  return allArchives.get(entry_id) as Archive
 }
 
 /**
@@ -732,12 +764,64 @@ export function getArchive(entry_id: string) {
  * the client-side state of the archive. This includes the archive data
  * updated from the latest API requests and user changes, the stack of
  * unsaved changes, and a registry for components listening to properties
- * of the archive via `useArhiveProperty`.
+ * of the archive via `useArchiveProperty`.
+ *
+ * The archive is constant for the current entry and this hook will not
+ * re-render on archive changes. Use `useArchiveChanges` or `useArchiveProperty`
+ * or `useChangedArchives` to react to changes.
  */
 export default function useArchive() {
-  const {entry_id} = useRouteData(entryRoute)
+  const {entry_id, mainfile_path, upload_id} = useRouteData(entryRoute)
   assert(entry_id, 'UseArchive only works within an entry route.')
-  return getArchive(entry_id)
+  return getArchive(entry_id, {entry_id, upload_id, mainfile_path})
+}
+
+/**
+ * A hook that returns a list of all archives that have been loaded and
+ * re-renders when a new archive is loaded.
+ */
+function useArchives() {
+  const [archives, setArchives] = useState<Archive[]>(
+    Array.from(allArchives.values()),
+  )
+
+  useEffect(() => {
+    return registerArchivesChangeListener(() => {
+      setArchives(Array.from(allArchives.values()))
+    })
+  }, [])
+
+  return archives
+}
+
+/**
+ * A hook that returns a list of archives that have unsaved changes.
+ * The hook causes a re-render, if there are new changes.
+ */
+export function useChangedArchives() {
+  const archives = useArchives()
+
+  const [changedArchives, setChangedArchives] = useState<Archive[]>(
+    archives.filter((archive) => archive.hasChanges()),
+  )
+
+  useEffect(() => {
+    const handleChange = () => {
+      setChangedArchives(archives.filter((archive) => archive.hasChanges()))
+    }
+
+    const listeners = Array.from(allArchives.values()).map((archive) =>
+      archive.registerChangeListener(handleChange),
+    )
+
+    handleChange()
+
+    return () => {
+      listeners.forEach((unregister) => unregister())
+    }
+  }, [archives])
+
+  return changedArchives
 }
 
 export type ArchiveChanges = {
diff --git a/src/components/navigation/NavItem.tsx b/src/components/navigation/NavItem.tsx
index 8b88d315..35a9b045 100644
--- a/src/components/navigation/NavItem.tsx
+++ b/src/components/navigation/NavItem.tsx
@@ -1,5 +1,7 @@
 import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
 import {
+  Badge,
+  BadgeProps,
   ButtonBase,
   Collapse,
   IconButton,
@@ -194,6 +196,10 @@ export type NavItemProps = {
    * disabled.
    */
   disabled?: boolean
+  /**
+   * Optional props to create a MUI badge on the item label.
+   */
+  badge?: BadgeProps
 }
 
 /**
@@ -214,6 +220,7 @@ export default function NavItem(props: NavItemProps & SxProps) {
     label,
     alignWithExpandIcons = false,
     children,
+    badge,
     sx = [],
     ...buttonBaseProps
   } = props
@@ -227,6 +234,12 @@ export default function NavItem(props: NavItemProps & SxProps) {
     [onToggle],
   )
 
+  const labelElement = (
+    <NavItemLabel className={classes.label} ownerState={props}>
+      {label}
+    </NavItemLabel>
+  )
+
   return (
     <NavItemRoot className={classes.root} sx={sx}>
       <NavItemButton
@@ -250,9 +263,7 @@ export default function NavItem(props: NavItemProps & SxProps) {
           </NavItemToggleButton>
         )}
         {icon && <NavItemIcon className={classes.icon}>{icon}</NavItemIcon>}
-        <NavItemLabel className={classes.label} ownerState={props}>
-          {label}
-        </NavItemLabel>
+        <Badge {...badge}>{labelElement}</Badge>
       </NavItemButton>
       {expandable && children && (
         <NavItemContent
diff --git a/src/index.tsx b/src/index.tsx
index 58bde89a..39fe09c6 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -60,7 +60,7 @@ enableMocking().then(() => {
         </Theme>
       </RecoilRoot>
     </LocalizationProvider>,
-    // </React.StrictMode>,
+    // </React.StrictMode>
   )
 })
 
diff --git a/src/pages/entry/UnsavedChangesNav.tsx b/src/pages/entry/UnsavedChangesNav.tsx
new file mode 100644
index 00000000..4af527cf
--- /dev/null
+++ b/src/pages/entry/UnsavedChangesNav.tsx
@@ -0,0 +1,62 @@
+import {useMemo} from 'react'
+
+import {useChangedArchives} from '../../components/archive/useArchive'
+import NavItem from '../../components/navigation/NavItem'
+import useSelect from '../../components/navigation/useSelect'
+import Link from '../../components/routing/Link'
+import useRoute from '../../components/routing/useRoute'
+
+type UnsavedChangesNavProps = {
+  uploadId?: string
+}
+
+export default function UnsavedChangesNav({uploadId}: UnsavedChangesNavProps) {
+  const {isPage} = useSelect()
+  const changedArchives = useChangedArchives()
+  const {url} = useRoute()
+  const relevantChangedArchives = useMemo(() => {
+    if (!uploadId) {
+      return changedArchives
+    }
+    return changedArchives.filter((archive) => {
+      return archive.entry.upload_id === uploadId
+    })
+  }, [uploadId, changedArchives])
+
+  if (!isPage) {
+    return ''
+  }
+
+  if (relevantChangedArchives.length === 0) {
+    return ''
+  }
+
+  return (
+    <NavItem
+      expandable
+      expanded
+      label={'Entries with unsaved changes'}
+      badge={{badgeContent: relevantChangedArchives.length, color: 'error'}}
+    >
+      {relevantChangedArchives
+        .filter((archive) => {
+          return archive.entry.entry_id && archive.entry.upload_id
+        })
+        .map((archive, index) => (
+          <NavItem
+            key={archive.entry.entry_id || index}
+            variant='treeNode'
+            label={
+              archive.entry.mainfile_path ||
+              archive.entry.entry_id ||
+              'Unknown name'
+            }
+            component={Link}
+            to={url({
+              path: `/uploads/${archive.entry.upload_id}/entries/${archive.entry.entry_id}`,
+            })}
+          />
+        ))}
+    </NavItem>
+  )
+}
diff --git a/src/pages/upload/UploadPage.tsx b/src/pages/upload/UploadPage.tsx
index 862ebdda..a5625c65 100644
--- a/src/pages/upload/UploadPage.tsx
+++ b/src/pages/upload/UploadPage.tsx
@@ -12,6 +12,7 @@ import useRoute from '../../components/routing/useRoute'
 import useRouteData from '../../components/routing/useRouteData'
 import useRecent from '../../hooks/useRecent'
 import {JSONObject} from '../../utils/types'
+import UnsavedChangesNav from '../entry/UnsavedChangesNav'
 import UploadFilesNav from './UploadFilesNav'
 import UploadNavAbout from './UploadNavAbout'
 import uploadRoute from './uploadRoute'
@@ -99,6 +100,8 @@ export default function UploadPage() {
               />
             </>
           )}
+          <Divider />
+          <UnsavedChangesNav uploadId={upload_id} />
         </Nav>
       }
     >
diff --git a/src/pages/uploads/UploadsPage.tsx b/src/pages/uploads/UploadsPage.tsx
index 69d79424..01545cc7 100644
--- a/src/pages/uploads/UploadsPage.tsx
+++ b/src/pages/uploads/UploadsPage.tsx
@@ -1,14 +1,41 @@
-import {Divider, TextField} from '@mui/material'
+import {BadgeProps, Divider, TextField} from '@mui/material'
 
+import {MainMenuItem, MainMenuItemProps} from '../../components/app/MainMenu'
+import {useChangedArchives} from '../../components/archive/useArchive'
+import {Manage} from '../../components/icons'
 import Nav from '../../components/navigation/Nav'
 import NavItem from '../../components/navigation/NavItem'
 import RecentNav from '../../components/navigation/RecentNav'
 import useSelect from '../../components/navigation/useSelect'
 import Page, {PageActions, PageTitle} from '../../components/page/Page'
+import useRoute from '../../components/routing/useRoute'
+import UnsavedChangesNav from '../entry/UnsavedChangesNav'
 import UploadsActions from './UploadsActions'
 import UploadsTable from './UploadsTable'
 import UploadsTableFilterMenu from './UploadsTableFilterMenu'
 
+export function UploadsMainMenuItem(
+  mainMenuProps: Omit<MainMenuItemProps, 'label'>,
+) {
+  const {matches} = useRoute()
+  const changedArchives = useChangedArchives()
+  const badge = {
+    badgeContent: changedArchives.length,
+    color: 'error',
+  } satisfies BadgeProps
+
+  return (
+    <MainMenuItem
+      {...mainMenuProps}
+      label='Manage'
+      to='/uploads'
+      icon={<Manage />}
+      active={matches('/uploads')}
+      badge={badge}
+    />
+  )
+}
+
 export default function UploadsPage() {
   const {isPage, pageVariant} = useSelect()
 
@@ -24,6 +51,8 @@ export default function UploadsPage() {
             scope='global'
             storageKey='uploads'
           />
+          <Divider />
+          <UnsavedChangesNav />
         </Nav>
       }
     >
-- 
GitLab


From 3419010b80c23817a73dcac102248708c13c9ed5 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Mon, 3 Mar 2025 13:31:59 +0100
Subject: [PATCH 08/17] Fixed archive updates in react strict mode.

---
 src/index.tsx                     | 35 ++++++++++++++++---------------
 src/pages/entry/EntryOverview.tsx |  2 +-
 2 files changed, 19 insertions(+), 18 deletions(-)

diff --git a/src/index.tsx b/src/index.tsx
index 39fe09c6..37115408 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,6 +2,7 @@ import '@fontsource/titillium-web/400.css'
 import '@fontsource/titillium-web/700.css'
 import {LocalizationProvider} from '@mui/x-date-pickers'
 import {AdapterDateFns} from '@mui/x-date-pickers/AdapterDateFnsV3'
+import React from 'react'
 import ReactDOM from 'react-dom/client'
 import {RecoilRoot} from 'recoil'
 
@@ -44,23 +45,23 @@ enableMocking().then(() => {
     document.getElementById('root') as HTMLElement,
   )
   root.render(
-    // <React.StrictMode>
-    <LocalizationProvider dateAdapter={AdapterDateFns}>
-      <RecoilRoot>
-        <Theme>
-          <AuthProvider>
-            <ApiProvider>
-              <UnitProvider>
-                <DevTools enabled>
-                  <Routes />
-                </DevTools>
-              </UnitProvider>
-            </ApiProvider>
-          </AuthProvider>
-        </Theme>
-      </RecoilRoot>
-    </LocalizationProvider>,
-    // </React.StrictMode>
+    <React.StrictMode>
+      <LocalizationProvider dateAdapter={AdapterDateFns}>
+        <RecoilRoot>
+          <Theme>
+            <AuthProvider>
+              <ApiProvider>
+                <UnitProvider>
+                  <DevTools enabled>
+                    <Routes />
+                  </DevTools>
+                </UnitProvider>
+              </ApiProvider>
+            </AuthProvider>
+          </Theme>
+        </RecoilRoot>
+      </LocalizationProvider>
+    </React.StrictMode>,
   )
 })
 
diff --git a/src/pages/entry/EntryOverview.tsx b/src/pages/entry/EntryOverview.tsx
index 12161ea5..be4f5dae 100644
--- a/src/pages/entry/EntryOverview.tsx
+++ b/src/pages/entry/EntryOverview.tsx
@@ -77,7 +77,7 @@ function EntryOverviewEditor() {
   // render.
   const requestHasChanged = request !== usePrevious(request)
   const archive = useArchive()
-  if (requestHasChanged) {
+  if (requestHasChanged && archive.currentUpdateRequests === undefined) {
     archive.startUpdate(request.archive as MSectionRequest, true)
   }
 
-- 
GitLab


From 69655eacfb70bc395d7f1766c8637f4e8e217b17 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Wed, 5 Mar 2025 10:16:40 +0100
Subject: [PATCH 09/17] Fixed NavItem issues.

---
 src/components/navigation/NavItem.tsx | 29 ++++++++++++++++++---------
 src/pages/entry/EntryArchiveNav.tsx   |  2 +-
 src/pages/entry/EntryPage.tsx         |  2 +-
 3 files changed, 22 insertions(+), 11 deletions(-)

diff --git a/src/components/navigation/NavItem.tsx b/src/components/navigation/NavItem.tsx
index 35a9b045..bcc0d44a 100644
--- a/src/components/navigation/NavItem.tsx
+++ b/src/components/navigation/NavItem.tsx
@@ -2,6 +2,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
 import {
   Badge,
   BadgeProps,
+  Box,
   ButtonBase,
   Collapse,
   IconButton,
@@ -22,6 +23,7 @@ const classes = generateUtilityClasses('NavItem', [
   'button',
   'icon',
   'label',
+  'badge',
   'toggleButton',
   'content',
 ])
@@ -106,14 +108,17 @@ const NavItemIcon = styled('span', {name: 'NavItem', slot: 'icon'})(
   }),
 )
 
+const NavItemBadge = styled(Badge, {name: 'NavItem', slot: 'badge'})({
+  maxWidth: '100%',
+})
+
 const NavItemLabel = styled(Typography, {name: 'NavItem', slot: 'label'})<{
   ownerState: NavItemOwnerState
 }>(({ownerState: {variant = 'menuItem'}}) => ({
-  flexGrow: 1,
+  width: '100%',
   textAlign: 'left',
   overflow: 'clip',
   textOverflow: 'ellipsis',
-  width: '100%',
   whiteSpace: 'nowrap',
   ...(variant === 'menuItem' && {
     textTransform: 'uppercase',
@@ -234,12 +239,6 @@ export default function NavItem(props: NavItemProps & SxProps) {
     [onToggle],
   )
 
-  const labelElement = (
-    <NavItemLabel className={classes.label} ownerState={props}>
-      {label}
-    </NavItemLabel>
-  )
-
   return (
     <NavItemRoot className={classes.root} sx={sx}>
       <NavItemButton
@@ -263,7 +262,19 @@ export default function NavItem(props: NavItemProps & SxProps) {
           </NavItemToggleButton>
         )}
         {icon && <NavItemIcon className={classes.icon}>{icon}</NavItemIcon>}
-        <Badge {...badge}>{labelElement}</Badge>
+        <Box
+          sx={{
+            flexGrow: 1,
+            textAlign: 'left',
+            width: '100%',
+          }}
+        >
+          <NavItemBadge className={classes.badge} {...badge}>
+            <NavItemLabel className={classes.label} ownerState={props}>
+              {label}
+            </NavItemLabel>
+          </NavItemBadge>
+        </Box>
       </NavItemButton>
       {expandable && children && (
         <NavItemContent
diff --git a/src/pages/entry/EntryArchiveNav.tsx b/src/pages/entry/EntryArchiveNav.tsx
index db99a9de..9daa3e3f 100644
--- a/src/pages/entry/EntryArchiveNav.tsx
+++ b/src/pages/entry/EntryArchiveNav.tsx
@@ -20,7 +20,7 @@ import {
 
 export type EntryArchiveNavProps = Pick<
   TreeNavProps,
-  'getChildProps' | 'label' | 'expanded' | 'selected'
+  'getChildProps' | 'label' | 'expanded' | 'selected' | 'defaultExpanded'
 >
 
 function filterChildKeys(data: JSONObject, key: string) {
diff --git a/src/pages/entry/EntryPage.tsx b/src/pages/entry/EntryPage.tsx
index 56ea8ee9..f1597de2 100644
--- a/src/pages/entry/EntryPage.tsx
+++ b/src/pages/entry/EntryPage.tsx
@@ -80,7 +80,7 @@ export default function EntryPage() {
           )}
           <EntryArchiveNav
             label='data'
-            expanded={tab !== 'archive' ? false : undefined}
+            defaultExpanded={tab !== 'archive' ? false : undefined}
             selected={tab === 'archive'}
           />
         </Nav>
-- 
GitLab


From ba93e5aa0436c57bc1f958ca0ce8b7b8a8836aba Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Wed, 5 Mar 2025 14:42:28 +0100
Subject: [PATCH 10/17] Added SubSectionNavEditor and used it for EntrySection.

---
 src/components/archive/Section.tsx            |  88 +++++-
 src/components/editor/PropertyEditor.tsx      |   1 +
 .../editor/SubSectionNavEditor.test.tsx       | 226 +++++++++++++++
 src/components/editor/SubSectionNavEditor.tsx | 269 ++++++++++++++++++
 src/components/layout/Layout.tsx              |   3 +
 src/components/navigation/NavItem.tsx         |  89 ++++--
 src/components/navigation/TreeNav.tsx         |   9 +-
 src/components/routing/normalizeRoute.ts      |   5 +
 src/pages/entry/EntryArchiveNav.tsx           |   1 +
 src/pages/entry/EntryOverview.tsx             |   8 +-
 src/pages/entry/EntrySection.tsx              |  29 +-
 src/pages/entry/EntrySubSectionTable.tsx      |  52 ----
 12 files changed, 665 insertions(+), 115 deletions(-)
 create mode 100644 src/components/editor/SubSectionNavEditor.test.tsx
 create mode 100644 src/components/editor/SubSectionNavEditor.tsx
 delete mode 100644 src/pages/entry/EntrySubSectionTable.tsx

diff --git a/src/components/archive/Section.tsx b/src/components/archive/Section.tsx
index a1028313..d0da427b 100644
--- a/src/components/archive/Section.tsx
+++ b/src/components/archive/Section.tsx
@@ -1,27 +1,47 @@
 import {Card, CardContent, CardHeader} from '@mui/material'
-import {useCallback} from 'react'
+import {useCallback, useMemo} from 'react'
 
 import {MSectionResponse} from '../../models/graphResponseModels'
 import {
   Quantity as QuantityDefinition,
   Section as SectionDefinition,
+  SubSection as SubSectionDefinition,
 } from '../../utils/metainfo'
 import {JSONValue} from '../../utils/types'
 import {assert} from '../../utils/utils'
 import ErrorBoundary from '../ErrorBoundary'
 import {getPropertyLabel} from '../editor/PropertyEditor'
+import SubSectionNavEditor from '../editor/SubSectionNavEditor'
 import DynamicValue from '../values/utils/DynamicValue'
 import {createDynamicComponentSpec} from '../values/utils/dynamicComponents'
 import SectionProvider from './SectionProvider'
 import {useArchiveProperty} from './useArchive'
 import useSubSectionPath from './useSubSectionPath'
 
-type QantityProps = {
+type SubSectionProps = {
+  definition: SubSectionDefinition
+  editable?: boolean
+}
+
+function SubSection({definition, editable = false}: SubSectionProps) {
+  return (
+    <SubSectionNavEditor
+      layout={{
+        label: getPropertyLabel(definition.name),
+        type: 'subSectionNav',
+        property: definition.name,
+        editable,
+      }}
+    />
+  )
+}
+
+type QuantityProps = {
   definition: QuantityDefinition
   editable?: boolean
 }
 
-function Quantity({definition, editable = false}: QantityProps) {
+function Quantity({definition, editable = false}: QuantityProps) {
   const path = useSubSectionPath(definition.name)
 
   const {value, loading, change} = useArchiveProperty<
@@ -68,18 +88,56 @@ export default function Section({path, editable = false}: SectionProps) {
     'The section definition should have been loaded by now.',
   )
 
+  const subSections = useMemo(() => {
+    return Object.keys(definition.all_sub_sections)
+      .map((key) => definition.all_sub_sections[key])
+      .toSorted((a, b) => a.name.localeCompare(b.name))
+  }, [definition])
+
+  const quantities = useMemo(() => {
+    return Object.keys(definition.all_quantities).map(
+      (key) => definition.all_quantities[key],
+    )
+  }, [definition.all_quantities])
+
   return (
-    <Card>
-      <CardHeader title='Quantities' />
-      <CardContent sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
-        <SectionProvider path={path}>
-          {Object.values(definition.all_quantities).map((quantity) => (
-            <ErrorBoundary key={quantity.name}>
-              <Quantity definition={quantity} editable={editable} />
-            </ErrorBoundary>
-          ))}
-        </SectionProvider>
-      </CardContent>
-    </Card>
+    <>
+      {subSections.length > 0 && (
+        <ErrorBoundary>
+          <Card>
+            <CardHeader title='Sub Sections' />
+            <CardContent
+              sx={{display: 'flex', flexDirection: 'column', gap: 2}}
+            >
+              <SectionProvider path={path}>
+                {subSections.map((subSection) => (
+                  <ErrorBoundary key={subSection.name}>
+                    <SubSection definition={subSection} editable={editable} />
+                  </ErrorBoundary>
+                ))}
+              </SectionProvider>
+            </CardContent>
+          </Card>
+        </ErrorBoundary>
+      )}
+      {definition.name !== 'EntryArchive' && quantities.length > 0 && (
+        <ErrorBoundary>
+          <Card>
+            <CardHeader title='Quantities' />
+            <CardContent
+              sx={{display: 'flex', flexDirection: 'column', gap: 2}}
+            >
+              <SectionProvider path={path}>
+                {quantities.map((quantity) => (
+                  <ErrorBoundary key={quantity.name}>
+                    <Quantity definition={quantity} editable={editable} />
+                  </ErrorBoundary>
+                ))}
+              </SectionProvider>
+            </CardContent>
+          </Card>
+        </ErrorBoundary>
+      )}
+    </>
   )
 }
diff --git a/src/components/editor/PropertyEditor.tsx b/src/components/editor/PropertyEditor.tsx
index f049bcec..c1823e96 100644
--- a/src/components/editor/PropertyEditor.tsx
+++ b/src/components/editor/PropertyEditor.tsx
@@ -3,6 +3,7 @@ import {AbstractItem} from '../layout/Layout'
 export type AbstractProperty = AbstractItem & {
   property: string
   label?: string
+  editable?: boolean
 }
 
 export function getLabel<T extends AbstractProperty>(layout: T) {
diff --git a/src/components/editor/SubSectionNavEditor.test.tsx b/src/components/editor/SubSectionNavEditor.test.tsx
new file mode 100644
index 00000000..f64dd1d8
--- /dev/null
+++ b/src/components/editor/SubSectionNavEditor.test.tsx
@@ -0,0 +1,226 @@
+import {Link} from '@mui/material'
+import {act, render, screen} from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import {PropsWithChildren} from 'react'
+import {vi} from 'vitest'
+
+import entryRoute from '../../pages/entry/entryRoute'
+import {addDefinitions, mockArchive} from '../archive/archive.helper'
+import {Layout} from '../layout/Layout'
+import {RouteData} from '../routing/types'
+import * as useRoute from '../routing/useRoute'
+import * as useRouter from '../routing/useRouter'
+import {SubSectionNav} from './SubSectionNavEditor'
+
+describe('SubSectionNavEditor', async () => {
+  const layout = {
+    type: 'subSectionNav',
+    property: 'test_subsection',
+    editable: true,
+  } as SubSectionNav
+
+  function mockData(args?: {
+    repeats?: boolean
+    withLoading?: boolean
+    empty?: boolean
+    definitionsExtra?: Record<string, unknown>
+  }) {
+    const {
+      repeats = false,
+      withLoading = false,
+      empty = false,
+      definitionsExtra,
+    } = args || {}
+    const defaultSubSectionData = {
+      test_quantity: 'test_value',
+    }
+    let subSectionData
+    if (repeats) {
+      if (empty) {
+        subSectionData = []
+      } else {
+        subSectionData = [defaultSubSectionData]
+      }
+    } else {
+      if (empty) {
+        subSectionData = undefined
+      } else {
+        subSectionData = defaultSubSectionData
+      }
+    }
+    const data = addDefinitions(
+      {
+        ...(subSectionData
+          ? {
+              test_subsection: subSectionData,
+            }
+          : {}),
+      },
+      definitionsExtra,
+    )
+    return mockArchive({
+      data: withLoading ? undefined : data,
+    })
+  }
+
+  const url = vi.fn()
+  vi.spyOn(useRoute, 'default').mockReturnValue({
+    url,
+    fullMatch: [
+      {
+        path: '/test',
+        route: {...entryRoute, path: '/test'},
+      },
+    ],
+  } as unknown as RouteData)
+
+  const createLink = vi
+    .fn()
+    .mockImplementation(({to, ...props}: {to: string} & PropsWithChildren) => (
+      <Link href={to} {...props} />
+    ))
+  vi.spyOn(useRouter, 'default').mockReturnValue({
+    createLink,
+  } as unknown as useRouter.RouterValue)
+
+  beforeEach(() => {
+    url.mockReset()
+  })
+
+  it('initially renders with loading indicator', async () => {
+    mockData({withLoading: true})
+    render(<Layout layout={layout} />)
+    expect(screen.queryByText('test subsection')).not.toBeInTheDocument()
+  })
+
+  it('renders sub section', async () => {
+    mockData()
+    render(<Layout layout={layout} />)
+    expect(screen.getByText('test subsection')).toBeInTheDocument()
+    expect(url).toHaveBeenCalledWith({path: '/test/archive/test_subsection'})
+  })
+
+  it('allows to remove non repeating sub section', async () => {
+    const archive = mockData()
+    render(<Layout layout={layout} />)
+    expect(screen.getByText('test subsection')).toBeInTheDocument()
+    await userEvent.click(screen.getByRole('button', {name: 'remove'}))
+
+    expect(archive.changeStack.length).toBe(1)
+    expect(archive.changeStack[0]).toMatchObject({
+      action: 'remove',
+      new_value: undefined,
+      path: 'test_subsection',
+    })
+  })
+
+  it('hides remove button for non repeating sub section if not editable', async () => {
+    mockData()
+    render(<Layout layout={{...layout, editable: false}} />)
+    expect(screen.getByText('test subsection')).toBeInTheDocument()
+    expect(
+      screen.queryByRole('button', {name: 'remove'}),
+    ).not.toBeInTheDocument()
+  })
+
+  it('allows to add non repeating sub section', async () => {
+    const archive = mockData({
+      empty: true,
+      definitionsExtra: {test_subsection: {test_quantity: 'test_value'}},
+    })
+    render(<Layout layout={layout} />)
+    const addButton = screen.getByRole('button', {name: 'add'})
+    expect(addButton).toBeInTheDocument()
+    await userEvent.click(addButton)
+
+    expect(archive.changeStack.length).toBe(1)
+    expect(archive.changeStack[0]).toMatchObject({
+      action: 'upsert',
+      new_value: expect.objectContaining({m_def: expect.objectContaining({})}),
+      path: 'test_subsection',
+    })
+  })
+
+  it('hides add button for non repeating sub section if not editable', async () => {
+    mockData({
+      empty: true,
+      definitionsExtra: {test_subsection: {test_quantity: 'test_value'}},
+    })
+    render(<Layout layout={{...layout, editable: false}} />)
+    expect(screen.getByText('test subsection')).toBeInTheDocument()
+    expect(screen.queryByRole('button', {name: 'add'})).not.toBeInTheDocument()
+  })
+
+  it('renders repeating sub section', async () => {
+    mockData({repeats: true})
+    render(<Layout layout={layout} />)
+    expect(screen.getByText('test subsection')).toBeInTheDocument()
+    expect(url).not.toHaveBeenCalled()
+  })
+
+  it('expands repeating sub section', async () => {
+    mockData({repeats: true})
+    render(<Layout layout={layout} />)
+    expect(screen.getByText('test subsection')).toBeInTheDocument()
+    await act(async () => {
+      await userEvent.click(screen.getByText('test subsection'))
+    })
+    expect(screen.getByText('0')).toBeInTheDocument()
+    expect(url).toHaveBeenCalledWith({path: '/test/archive/test_subsection[0]'})
+  })
+
+  it('allows to remove repeating sub section', async () => {
+    url.mockReturnValue('/test/archive/test_subsection[0]')
+    const archive = mockData({repeats: true})
+    render(<Layout layout={layout} />)
+    await act(async () => {
+      await userEvent.click(screen.getByText('test subsection'))
+    })
+    const removeButton = screen.getByRole('button', {name: 'remove'})
+    await userEvent.click(removeButton)
+
+    expect(archive.changeStack.length).toBe(1)
+    expect(archive.changeStack[0]).toMatchObject({
+      action: 'remove',
+      new_value: {},
+      path: 'test_subsection/0',
+    })
+  })
+
+  it('hides remove button for repeating sub section if not editable', async () => {
+    mockData({repeats: true})
+    render(<Layout layout={{...layout, editable: false}} />)
+    await act(async () => {
+      await userEvent.click(screen.getByText('test subsection'))
+    })
+    expect(screen.getByText('0')).toBeInTheDocument()
+    expect(
+      screen.queryByRole('button', {name: 'remove'}),
+    ).not.toBeInTheDocument()
+  })
+
+  it('allows to add repeating sub section', async () => {
+    const archive = mockData({
+      empty: true,
+      repeats: true,
+      definitionsExtra: {test_subsection: [{test_quantity: 'test_value'}]},
+    })
+    render(<Layout layout={layout} />)
+    const addButton = screen.getByRole('button', {name: 'add'})
+    await userEvent.click(addButton)
+
+    expect(archive.changeStack.length).toBe(1)
+    expect(archive.changeStack[0]).toMatchObject({
+      action: 'upsert',
+      new_value: expect.objectContaining({}),
+      path: 'test_subsection/0',
+    })
+  })
+
+  it('hides add button for repeating sub section if not editable', async () => {
+    mockData({empty: true, repeats: true})
+    render(<Layout layout={{...layout, editable: false}} />)
+    expect(screen.getByText('test subsection')).toBeInTheDocument()
+    expect(screen.queryByRole('button', {name: 'add'})).not.toBeInTheDocument()
+  })
+})
diff --git a/src/components/editor/SubSectionNavEditor.tsx b/src/components/editor/SubSectionNavEditor.tsx
new file mode 100644
index 00000000..cd2a02a8
--- /dev/null
+++ b/src/components/editor/SubSectionNavEditor.tsx
@@ -0,0 +1,269 @@
+import {Box, IconButton, Skeleton, Typography} from '@mui/material'
+import {MouseEventHandler, useCallback, useMemo, useState} from 'react'
+
+import Link from '../../components/routing/Link'
+import {MSectionResponse} from '../../models/graphResponseModels'
+import entryRoute from '../../pages/entry/entryRoute'
+import {SubSection as SubSectionDefinition} from '../../utils/metainfo'
+import {assert, splice} from '../../utils/utils'
+import {ArchiveProperty, useArchiveProperty} from '../archive/useArchive'
+import useSubSectionPath from '../archive/useSubSectionPath'
+import {Add, Remove} from '../icons'
+import {LayoutItem} from '../layout/Layout'
+import NavItem from '../navigation/NavItem'
+import useRoute from '../routing/useRoute'
+import {AbstractProperty, getLabel} from './PropertyEditor'
+
+function SubSectionNavComponent({
+  label,
+  property,
+  editable,
+}: {
+  label: string
+  property: ArchiveProperty<SubSectionData, SubSectionDefinition>
+  editable?: boolean
+}) {
+  const {url, fullMatch} = useRoute()
+  const {value, getDefinition, change, path} = property
+  const definition = useMemo(() => getDefinition(), [getDefinition])
+  const repeats = definition?.repeats
+  const entryRouteIndex = fullMatch.findIndex(
+    (part) => part.route.routeId === entryRoute.routeId,
+  )
+  const archivePath =
+    fullMatch
+      .filter((part, index) => index <= entryRouteIndex)
+      .map((part) => part.path)
+      .join('/') + '/archive'
+
+  // TODO implement remove and add
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const handleRemove = useCallback(
+    (event: React.MouseEvent<HTMLSpanElement>, index?: number) => {
+      event.stopPropagation()
+      event.nativeEvent.stopImmediatePropagation()
+      event.preventDefault()
+      if (repeats) {
+        assert(
+          index !== undefined,
+          'Update operation on repeating sub section without index',
+        )
+        change(
+          'remove',
+          splice((value || []) as MSectionResponse[], index, 1),
+          index,
+        )
+      } else {
+        change('remove', undefined)
+      }
+    },
+    [repeats, change, value],
+  )
+
+  // TODO implement remove and add
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const handleAdd = useCallback<MouseEventHandler<HTMLSpanElement>>(
+    (event) => {
+      // TODO the definition might not be available. With an improved
+      // useArchive implementation, it might still be just a references and
+      // we might still need to explicitly fetch to actual definition.
+      // I.e., getDefinition() might need to be async.
+      event.stopPropagation()
+      event.nativeEvent.stopImmediatePropagation()
+      event.preventDefault()
+      const newSubSectionData = {
+        m_def: getDefinition()?.sub_section,
+      } as MSectionResponse
+      if (repeats) {
+        assert(value === undefined || Array.isArray(value))
+        change(
+          'upsert',
+          [...((value || []) as MSectionResponse[]), newSubSectionData],
+          (value || []).length,
+        )
+      } else {
+        change('upsert', newSubSectionData)
+      }
+    },
+    [repeats, change, value, getDefinition],
+  )
+
+  const [expanded, setExpanded] = useState(false)
+  const handleExpand = useCallback(() => {
+    setExpanded((expanded) => !expanded)
+  }, [])
+
+  if (repeats) {
+    assert(value === undefined || Array.isArray(value))
+    const subSections = value || []
+    return (
+      <NavItem
+        label={label}
+        variant='wide'
+        expandable
+        expanded={expanded}
+        onToggle={handleExpand}
+        onSelect={handleExpand}
+        actions={
+          editable && (
+            <IconButton
+              component='span'
+              size='small'
+              onClick={handleAdd}
+              aria-label='add'
+              title='add'
+            >
+              <Add />
+            </IconButton>
+          )
+        }
+        sx={{
+          margin: -1,
+          '& .NavItem-button': {
+            paddingX: 1.5,
+          },
+        }}
+      >
+        {expanded && (
+          <Box
+            sx={{
+              display: 'flex',
+              flexDirection: 'column',
+              gap: 2,
+              marginBottom: 1,
+            }}
+          >
+            {subSections.length === 0 ? (
+              <Typography
+                sx={(theme) => ({
+                  marginLeft: 6,
+                  fontStyle: 'italic',
+                  color: theme.palette.action.disabled,
+                })}
+              >
+                empty
+              </Typography>
+            ) : (
+              subSections.map((subSection, index) => (
+                <NavItem
+                  key={index}
+                  variant='wide'
+                  label={`${index}`}
+                  expandable={false}
+                  expanded={false}
+                  component={Link}
+                  to={url({path: `${archivePath}/${path}[${index}]`})}
+                  actions={
+                    editable && (
+                      <IconButton
+                        component='span'
+                        size='small'
+                        onClick={(event) => handleRemove(event, index)}
+                        aria-label='remove'
+                        title='remove'
+                      >
+                        <Remove />
+                      </IconButton>
+                    )
+                  }
+                  sx={{
+                    margin: -1,
+                    '& .NavItem-button': {
+                      paddingLeft: 6,
+                    },
+                  }}
+                />
+              ))
+            )}
+          </Box>
+        )}
+      </NavItem>
+    )
+  }
+
+  return (
+    <NavItem
+      disabled={value === undefined}
+      label={label}
+      expandable={false}
+      expanded={false}
+      component={Link}
+      to={url({path: `${archivePath}/${path}`})}
+      variant='wide'
+      sx={{
+        margin: -1,
+        '& .NavItem-button': {
+          paddingX: 1.5,
+        },
+      }}
+      actions={
+        editable &&
+        (value !== undefined ? (
+          <IconButton
+            component='span'
+            size='small'
+            onClick={handleRemove}
+            aria-label='remove'
+            title='remove'
+          >
+            <Remove />
+          </IconButton>
+        ) : (
+          <IconButton
+            component='span'
+            size='small'
+            onClick={handleAdd}
+            aria-label='add'
+            title='add'
+            sx={{pointerEvents: 'auto'}}
+          >
+            <Add />
+          </IconButton>
+        ))
+      }
+    />
+  )
+}
+export type SubSectionNav = AbstractProperty & {
+  type: 'subSectionNav'
+}
+
+type SubSectionData = MSectionResponse | MSectionResponse[] | undefined
+
+/**
+ * Renders all sections in a sub section including actions to modify
+ * the sub section. Uses SectionEditor to render each sub section.
+ */
+export default function SubSectionNavEditor({layout}: {layout: LayoutItem}) {
+  const subSection = layout as SubSectionNav
+  const {property: propertyName, editable} = subSection
+  const label = getLabel(subSection)
+  const path = useSubSectionPath(propertyName)
+
+  const property = useArchiveProperty<SubSectionData, SubSectionDefinition>(
+    path,
+  )
+  const {loading} = property
+
+  if (loading) {
+    return (
+      <Skeleton>
+        <NavItem
+          label='loading'
+          variant='treeNode'
+          sx={{
+            margin: -1,
+          }}
+        />
+      </Skeleton>
+    )
+  }
+
+  return (
+    <SubSectionNavComponent
+      label={label}
+      property={property}
+      editable={editable}
+    />
+  )
+}
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx
index 5e652928..a6c26317 100644
--- a/src/components/layout/Layout.tsx
+++ b/src/components/layout/Layout.tsx
@@ -8,6 +8,7 @@ import PlotEditor, {Plot} from '../editor/PlotEditor'
 import QuantityEditor, {Quantity} from '../editor/QuantityEditor'
 import RichTextEditor, {RichText} from '../editor/RichTextEditor'
 import SubSectionEditor, {SubSection} from '../editor/SubSectionEditor'
+import SubSectionNavEditor, {SubSectionNav} from '../editor/SubSectionNavEditor'
 import SubSectionTableEditor, {
   SubSectionTable,
 } from '../editor/SubSectionTableEditor'
@@ -39,6 +40,7 @@ const components = {
   element: ElementItem,
   richText: RichTextEditor,
   subSection: SubSectionEditor,
+  subSectionNav: SubSectionNavEditor,
   table: SubSectionTableEditor,
   imagePreview: ImagePreviewItem,
   plot: PlotEditor,
@@ -56,6 +58,7 @@ export type LayoutItem =
   | Element
   | RichText
   | SubSection
+  | SubSectionNav
   | SubSectionTable
   | ImagePreview
   | Plot
diff --git a/src/components/navigation/NavItem.tsx b/src/components/navigation/NavItem.tsx
index bcc0d44a..9fa70e81 100644
--- a/src/components/navigation/NavItem.tsx
+++ b/src/components/navigation/NavItem.tsx
@@ -26,19 +26,19 @@ const classes = generateUtilityClasses('NavItem', [
   'badge',
   'toggleButton',
   'content',
+  'actions',
 ])
 
 const NavItemToggleButton = styled(IconButton, {
   name: 'NavItem',
   slot: 'toggleButton',
 })<{ownerState: NavItemOwnerState}>(({theme, ownerState}) => {
-  const {expandable, expanded, variant = 'menuItem'} = ownerState
+  const {expandable, expanded} = ownerState
   return {
-    marginY: -theme.spacing(1),
-    marginLeft: theme.spacing(2),
-    marginRight: theme.spacing(variant === 'treeNode' ? 1 : 0),
-    order: variant === 'menuItem' ? 1 : 0,
-    padding: 0,
+    // marginY: -theme.spacing(1),
+    // marginRight: theme.spacing(1),
+    // marginLeft: theme.spacing(0.5),
+    // padding: 0,
     color: theme.palette.action.active,
     '&.Mui-disabled': {
       color: expandable ? theme.palette.action.active : 'rgba(0, 0, 0, 0)',
@@ -56,7 +56,11 @@ const NavItemToggleButton = styled(IconButton, {
   }
 }) as OverridableComponent<IconButtonTypeMap<{ownerState: NavItemOwnerState}>>
 
-const NavItemRoot = styled('div', {name: 'NavItem', slot: 'root'})(() => ({}))
+const NavItemRoot = styled('div', {name: 'NavItem', slot: 'root'})(() => ({
+  '&:not(:hover) .NavItem-actions': {
+    display: 'none',
+  },
+}))
 
 const NavItemButton = styled(ButtonBase, {
   name: 'NavItem',
@@ -103,6 +107,7 @@ const NavItemButton = styled(ButtonBase, {
 const NavItemIcon = styled('span', {name: 'NavItem', slot: 'icon'})(
   ({theme}) => ({
     marginRight: theme.spacing(1),
+    marginLeft: theme.spacing(2),
     paddingTop: 2,
     color: theme.palette.action.active,
   }),
@@ -115,11 +120,10 @@ const NavItemBadge = styled(Badge, {name: 'NavItem', slot: 'badge'})({
 const NavItemLabel = styled(Typography, {name: 'NavItem', slot: 'label'})<{
   ownerState: NavItemOwnerState
 }>(({ownerState: {variant = 'menuItem'}}) => ({
-  width: '100%',
   textAlign: 'left',
-  overflow: 'clip',
-  textOverflow: 'ellipsis',
   whiteSpace: 'nowrap',
+  overflow: 'hidden',
+  textOverflow: 'ellipsis',
   ...(variant === 'menuItem' && {
     textTransform: 'uppercase',
     fontSize: 14,
@@ -127,6 +131,12 @@ const NavItemLabel = styled(Typography, {name: 'NavItem', slot: 'label'})<{
   }),
 }))
 
+const NavItemActions = styled('div', {name: 'NavItem', slot: 'actions'})(
+  () => ({
+    whiteSpace: 'nowrap',
+  }),
+)
+
 const NavItemContent = styled(Collapse, {name: 'NavItem', slot: 'content'})(
   ({theme}) => ({
     '& .MuiCollapse-wrapperInner': {
@@ -192,10 +202,10 @@ export type NavItemProps = {
   /**
    * The variant of the nav item. Default is `menuItem` and is used to produce
    * a top-level nav item menu. The `treeNode` variant is used to create trees.
-   * The `treeNode` variant has the expand icon button on the left, the `menuItem`
-   * on the right.
+   * The `wide` variant is used to create a navigation items outside the
+   * navigation panel.
    */
-  variant?: 'menuItem' | 'treeNode'
+  variant?: 'menuItem' | 'treeNode' | 'wide'
   /**
    * The item is disabled. The appreance is different and all actions are
    * disabled.
@@ -205,6 +215,10 @@ export type NavItemProps = {
    * Optional props to create a MUI badge on the item label.
    */
   badge?: BadgeProps
+  /**
+   * Optional actions to display on the right side of the nav item.
+   */
+  actions?: ReactNode
 }
 
 /**
@@ -227,6 +241,8 @@ export default function NavItem(props: NavItemProps & SxProps) {
     children,
     badge,
     sx = [],
+    variant = 'menuItem',
+    actions,
     ...buttonBaseProps
   } = props
   const handleItemClick = useCallback(() => onSelect?.(), [onSelect])
@@ -248,25 +264,12 @@ export default function NavItem(props: NavItemProps & SxProps) {
         disabled={!onSelect && !buttonBaseProps.to}
         {...buttonBaseProps}
       >
-        {((expandable && onToggle) || alignWithExpandIcons) && (
-          <NavItemToggleButton
-            data-testid='toggle'
-            className={classes.toggleButton}
-            ownerState={props}
-            size='small'
-            component='span'
-            disabled={!expandable}
-            onClick={handleExpandClick}
-          >
-            <ExpandMoreIcon />
-          </NavItemToggleButton>
-        )}
         {icon && <NavItemIcon className={classes.icon}>{icon}</NavItemIcon>}
         <Box
           sx={{
-            flexGrow: 1,
+            flexGrow: variant === 'wide' ? 0 : 1,
             textAlign: 'left',
-            width: '100%',
+            minWidth: 0,
           }}
         >
           <NavItemBadge className={classes.badge} {...badge}>
@@ -275,6 +278,36 @@ export default function NavItem(props: NavItemProps & SxProps) {
             </NavItemLabel>
           </NavItemBadge>
         </Box>
+        {((expandable && onToggle) || alignWithExpandIcons || actions) && (
+          <Box
+            sx={(theme) => ({
+              marginY: -theme.spacing(1),
+              marginRight: theme.spacing(1),
+              marginLeft: theme.spacing(0.5),
+              padding: 0,
+              display: 'flex',
+              flexDirection: 'row',
+              whiteSpace: 'nowrap',
+            })}
+          >
+            {((expandable && onToggle) || alignWithExpandIcons) && (
+              <NavItemToggleButton
+                data-testid='toggle'
+                className={classes.toggleButton}
+                ownerState={props}
+                size='small'
+                component='span'
+                disabled={!expandable}
+                onClick={handleExpandClick}
+              >
+                <ExpandMoreIcon />
+              </NavItemToggleButton>
+            )}
+            <NavItemActions className={classes.actions}>
+              {actions}
+            </NavItemActions>
+          </Box>
+        )}
       </NavItemButton>
       {expandable && children && (
         <NavItemContent
diff --git a/src/components/navigation/TreeNav.tsx b/src/components/navigation/TreeNav.tsx
index 6885abbf..3423b5cc 100644
--- a/src/components/navigation/TreeNav.tsx
+++ b/src/components/navigation/TreeNav.tsx
@@ -76,6 +76,12 @@ export type TreeNavProps = {
   depth?: number
 
   expandable?: boolean | ((data: JSONObject) => boolean)
+  /**
+   * Selectable tree items will cause a navigation to the path of the item.
+   * None selectable tree items will only expand and collapse.
+   * Default is true.
+   */
+  selectable?: boolean
 } & Partial<Omit<NavItemProps, 'expandable'>>
 
 /**
@@ -102,6 +108,7 @@ export default function TreeNav({
   onToggle,
   defaultExpanded,
   expandable: isExpandable = false,
+  selectable = true,
   ...navItemProps
 }: TreeNavProps) {
   const expandedIsControlled = controlledExpanded !== undefined
@@ -257,7 +264,7 @@ export default function TreeNav({
       selected={renderSelected}
       highlighted={selected}
       onToggle={handleToggle}
-      onSelect={handleSelect}
+      onSelect={selectable ? handleSelect : handleToggle}
       expandable={expandable}
       label={key}
       sx={{
diff --git a/src/components/routing/normalizeRoute.ts b/src/components/routing/normalizeRoute.ts
index ac9e7d7c..4fe9d550 100644
--- a/src/components/routing/normalizeRoute.ts
+++ b/src/components/routing/normalizeRoute.ts
@@ -1,6 +1,7 @@
 import {DefaultSearch, Route} from './types'
 
 const normalizedRoutes = new Map<Route, Route>()
+const routeIds = {currentId: 1}
 
 export function routesEqual<Request, Response, Search extends DefaultSearch>(
   a: Route<Request, Response, Search>,
@@ -56,6 +57,10 @@ export function routesEqual<Request, Response, Search extends DefaultSearch>(
 }
 
 export default function normalizeRoute(route: Route): Route {
+  if (route.routeId === undefined) {
+    route.routeId = routeIds.currentId++
+  }
+
   if (normalizedRoutes.has(route)) {
     return normalizedRoutes.get(route) as Route
   }
diff --git a/src/pages/entry/EntryArchiveNav.tsx b/src/pages/entry/EntryArchiveNav.tsx
index 9daa3e3f..35e4c635 100644
--- a/src/pages/entry/EntryArchiveNav.tsx
+++ b/src/pages/entry/EntryArchiveNav.tsx
@@ -136,6 +136,7 @@ export default function EntryArchiveNav({
         return {
           ...sharedProps,
           expandable: true,
+          selectable: false,
           getChildProps: (data: JSONObject, key: string, path: string) =>
             ({
               ...sharedProps,
diff --git a/src/pages/entry/EntryOverview.tsx b/src/pages/entry/EntryOverview.tsx
index be4f5dae..9def0a76 100644
--- a/src/pages/entry/EntryOverview.tsx
+++ b/src/pages/entry/EntryOverview.tsx
@@ -17,7 +17,7 @@ import UploadMetadataEditor from '../upload/UploadMetadataEditor'
 import EntryDataEditor from './EntryDataEditor'
 import EntryMetadataEditor from './EntryMetadataEditor'
 import EntryPageTitle from './EntryPageTitle'
-import EntrySubSectionTable from './EntrySubSectionTable'
+import EntrySection from './EntrySection'
 import entryRoute, {archiveRequest, useEntryDataForRoute} from './entryRoute'
 
 function EntryOverviewEditor() {
@@ -121,7 +121,7 @@ export default function EntryOverview() {
   const {isPage, isSelect} = useSelect()
 
   // This memo is an optimization to avoid re-rendering the entire
-  // EntryOverviewEditor after usePage causes a srcoll event triggered
+  // EntryOverviewEditor after usePage causes a scroll event triggered
   // rerender that only effects the page title.
   const pageContent = useMemo(
     () => (
@@ -133,10 +133,10 @@ export default function EntryOverview() {
         >
           <EntryOverviewEditor />
         </Box>
-        {isSelect && <EntrySubSectionTable data={rootSectionData} />}
+        {isSelect && <EntrySection />}
       </>
     ),
-    [isSelect, rootSectionData],
+    [isSelect],
   )
 
   return (
diff --git a/src/pages/entry/EntrySection.tsx b/src/pages/entry/EntrySection.tsx
index 5c7818fb..e551076b 100644
--- a/src/pages/entry/EntrySection.tsx
+++ b/src/pages/entry/EntrySection.tsx
@@ -1,7 +1,6 @@
 import {Box, Card, CardContent, CardHeader} from '@mui/material'
 import {useMemo} from 'react'
 
-import ErrorBoundary from '../../components/ErrorBoundary'
 import Section from '../../components/archive/Section'
 import JsonViewer from '../../components/fileviewer/JsonViewer'
 import Outlet from '../../components/routing/Outlet'
@@ -11,7 +10,6 @@ import {MSectionRequest} from '../../models/graphRequestModels'
 import {MSectionResponse} from '../../models/graphResponseModels'
 import {JSONObject} from '../../utils/types'
 import EntryPageTitle from './EntryPageTitle'
-import EntrySubSectionTable from './EntrySubSectionTable'
 
 const sortRawDataKeys = (a: string, b: string) => {
   if (a.startsWith('m_') === b.startsWith('m_')) {
@@ -26,35 +24,36 @@ const sortRawDataKeys = (a: string, b: string) => {
 export default function EntrySection() {
   const {isLeaf, fullMatch} = useRoute()
   const data = useRouteData<MSectionRequest, MSectionResponse>()
+  const startIndex = useMemo(
+    () => fullMatch.findIndex((match) => match.route.path === 'archive'),
+    [fullMatch],
+  )
   const sectionPath = useMemo(() => {
     if (!isLeaf) {
       return ''
     }
-    const startIndex = fullMatch.findIndex(
-      (match) => match.route.path === 'archive',
-    )
     return fullMatch
       .slice(startIndex + 1)
       .map((match) => match.path)
       .join('/')
-  }, [fullMatch, isLeaf])
+  }, [fullMatch, isLeaf, startIndex])
 
   if (!isLeaf) {
     return <Outlet />
   }
 
+  const editable = true // TODO This needs to be set based on the user's permissions
+
+  const title =
+    fullMatch.length === startIndex + 1
+      ? 'Entry Data'
+      : `Section ${fullMatch[fullMatch.length - 1].path}`
+
   return (
     <>
-      <EntryPageTitle title={fullMatch[fullMatch.length - 1].path} />
+      <EntryPageTitle title={title} />
       <Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
-        <ErrorBoundary>
-          <EntrySubSectionTable data={data} />
-        </ErrorBoundary>
-        <ErrorBoundary>
-          {data.m_def && data.m_def.name !== 'EntryArchive' && (
-            <Section path={sectionPath} editable />
-          )}
-        </ErrorBoundary>
+        <Section path={sectionPath} editable={editable} />
         <Card>
           <CardHeader title='Raw data' />
           <CardContent>
diff --git a/src/pages/entry/EntrySubSectionTable.tsx b/src/pages/entry/EntrySubSectionTable.tsx
deleted file mode 100644
index 3f183afd..00000000
--- a/src/pages/entry/EntrySubSectionTable.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import {useMemo} from 'react'
-
-import useRoute from '../../components/routing/useRoute'
-import PlainDataTable from '../../components/table/PlainDataTable'
-import {MSectionResponse} from '../../models/graphResponseModels'
-import {
-  Section as SectionDefinition,
-  SubSection as SubSectionDefinition,
-} from '../../utils/metainfo'
-
-const subSectionColumns = [
-  {
-    enableSorting: true,
-    accessorKey: 'name',
-    header: 'Name',
-    grow: 1,
-  },
-]
-
-export type EntrySubSectionTableProps = {
-  data: MSectionResponse
-}
-
-export default function EntrySubSectionTable({
-  data,
-}: EntrySubSectionTableProps) {
-  const {navigate} = useRoute()
-
-  const definition = data.m_def as SectionDefinition
-  const subSections = useMemo(() => {
-    return Object.keys(definition.all_sub_sections)
-      .filter((key) => data[key] !== undefined)
-      .map((key) => definition.all_sub_sections[key])
-      .toSorted((a, b) => a.name.localeCompare(b.name))
-  }, [data, definition])
-
-  if (subSections.length === 0) {
-    return null
-  }
-
-  return (
-    <PlainDataTable
-      onRowClick={(data: SubSectionDefinition) => {
-        navigate({
-          path: data.name,
-        })
-      }}
-      columns={subSectionColumns}
-      data={subSections}
-    />
-  )
-}
-- 
GitLab


From 1d818a1fd9ba2bb411430510ae4ecc7bc3d74096 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Wed, 12 Mar 2025 12:22:10 +0100
Subject: [PATCH 11/17] Fixed that EntrySection reacts to editability. Fixed
 issues related to appEntryRoute.

---
 infra/README.md                               |  4 +-
 infra/pyproject.toml                          |  6 +-
 src/components/archive/EditStatus.tsx         |  5 +-
 src/components/archive/useArchive.tsx         |  5 +-
 src/components/editor/SubSectionNavEditor.tsx | 15 ++++-
 src/components/routing/useRoute.tsx           | 14 ++---
 src/components/routing/useRouteData.tsx       | 18 ++++--
 .../values/primitives/FileReference.tsx       |  5 +-
 .../values/primitives/SectionReference.tsx    |  5 +-
 src/hooks/useViewWriteAbilities.tsx           | 61 +++++++++++++++++++
 src/index.tsx                                 | 35 ++++++-----
 src/pages/apps/appsRoute.tsx                  |  7 +++
 src/pages/entry/EntryMetadataEditor.tsx       |  5 +-
 src/pages/entry/EntryNavAbout.tsx             |  6 +-
 src/pages/entry/EntryOverview.tsx             |  8 +--
 src/pages/entry/EntryPage.test.tsx            | 43 +++++++------
 src/pages/entry/EntryPage.tsx                 |  4 +-
 src/pages/entry/EntryPageTitle.tsx            |  5 +-
 src/pages/entry/EntrySection.tsx              | 16 ++++-
 src/pages/entry/entryRoute.tsx                |  1 +
 src/pages/entry/useEntryRouteData.tsx         |  7 +++
 src/pages/groups/GroupsTable.test.tsx         |  2 +
 .../upload/UploadMetadataEditor.test.tsx      | 24 +++++---
 src/pages/upload/UploadMetadataEditor.tsx     |  9 ++-
 src/pages/upload/UploadOverview.tsx           | 10 ++-
 src/pages/upload/uploadRoute.tsx              | 25 +++++---
 src/pages/uploads/UploadsTable.test.tsx       |  2 +
 27 files changed, 240 insertions(+), 107 deletions(-)
 create mode 100644 src/hooks/useViewWriteAbilities.tsx
 create mode 100644 src/pages/entry/useEntryRouteData.tsx

diff --git a/infra/README.md b/infra/README.md
index 547c72d1..ad24c8e2 100644
--- a/infra/README.md
+++ b/infra/README.md
@@ -10,8 +10,8 @@ This is how you run NOMAD, upload some data, and start using the GUI with it:
    From the gui project root folder:
 
 ```sh
-python3.11 -m venv .pyenv
-source .pyenv/bin/activate
+python3.11 -m venv .venv
+source .venv/bin/activate
 pip install uv
 uv pip install --upgrade pip
 uv pip install -e infra \
diff --git a/infra/pyproject.toml b/infra/pyproject.toml
index 15b2168d..3508134f 100644
--- a/infra/pyproject.toml
+++ b/infra/pyproject.toml
@@ -6,11 +6,11 @@ build-backend = "setuptools.build_meta"
 name = "nomad-plugin-gui"
 description = "The NOMAD plugin for the new NOMAD GUI."
 authors = [
-    { name = "NOMAD Laboratory", email = 'markus.scheidgen@physik.hu-berlin.de' },
+  { name = "NOMAD Laboratory", email = 'markus.scheidgen@physik.hu-berlin.de' },
 ]
 dynamic = ["version"]
 requires-python = ">=3.9"
-dependencies = ["nomad-lab[parsing,infrastructure]>=1.3.4"]
+dependencies = ["nomad-lab[parsing,infrastructure]==1.3.13"]
 [project.optional-dependencies]
 dev = ["pytest"]
 
@@ -69,4 +69,4 @@ gui_api = "nomad_plugin_gui.apis:gui_api"
 demo_schema = "nomad_plugin_gui.schema_packages:demo_schema"
 values_test_schema = "nomad_plugin_gui.schema_packages:values_test_schema"
 excercise_schema = "nomad_plugin_gui.schema_packages:exercise_schema"
-ui_demonstration_example_upload = "nomad_plugin_gui.example_uploads:ui_demonstration_example_upload"
\ No newline at end of file
+ui_demonstration_example_upload = "nomad_plugin_gui.example_uploads:ui_demonstration_example_upload"
diff --git a/src/components/archive/EditStatus.tsx b/src/components/archive/EditStatus.tsx
index 73eaaf5a..fee1de21 100644
--- a/src/components/archive/EditStatus.tsx
+++ b/src/components/archive/EditStatus.tsx
@@ -4,17 +4,16 @@ import {useAsyncFn} from 'react-use'
 import {Redo, Undo} from '../../components/icons'
 import useAuth from '../../hooks/useAuth'
 import {MSectionResponse} from '../../models/graphResponseModels'
-import entryRoute from '../../pages/entry/entryRoute'
+import useEntryRouteData from '../../pages/entry/useEntryRouteData'
 import {archiveEditApi} from '../../utils/api'
 import {assert} from '../../utils/utils'
 import useRoute from '../routing/useRoute'
-import useRouteData from '../routing/useRouteData'
 import useArchive, {useArchiveChanges} from './useArchive'
 
 export default function EditStatus() {
   const {user} = useAuth()
   const {reload} = useRoute()
-  const {entry_id} = useRouteData(entryRoute)
+  const {entry_id} = useEntryRouteData()
   const {changeStack, clearChangeStack} = useArchiveChanges()
   const archive = useArchive()
 
diff --git a/src/components/archive/useArchive.tsx b/src/components/archive/useArchive.tsx
index 9cb63302..09bab42a 100644
--- a/src/components/archive/useArchive.tsx
+++ b/src/components/archive/useArchive.tsx
@@ -11,11 +11,10 @@ import {
   MSectionResponse,
   UploadResponse,
 } from '../../models/graphResponseModels'
-import entryRoute from '../../pages/entry/entryRoute'
+import useEntryRouteData from '../../pages/entry/useEntryRouteData'
 import {Property, Section, resolveRef} from '../../utils/metainfo'
 import {assert} from '../../utils/utils'
 import {getIndexedKey} from '../routing/loader'
-import useRouteData from '../routing/useRouteData'
 
 export type ArchiveChangeConflict = {
   type: 'parent' | 'child' | 'exact'
@@ -771,7 +770,7 @@ function registerArchivesChangeListener(listener: () => void) {
  * or `useChangedArchives` to react to changes.
  */
 export default function useArchive() {
-  const {entry_id, mainfile_path, upload_id} = useRouteData(entryRoute)
+  const {entry_id, mainfile_path, upload_id} = useEntryRouteData()
   assert(entry_id, 'UseArchive only works within an entry route.')
   return getArchive(entry_id, {entry_id, upload_id, mainfile_path})
 }
diff --git a/src/components/editor/SubSectionNavEditor.tsx b/src/components/editor/SubSectionNavEditor.tsx
index cd2a02a8..40709f82 100644
--- a/src/components/editor/SubSectionNavEditor.tsx
+++ b/src/components/editor/SubSectionNavEditor.tsx
@@ -3,6 +3,7 @@ import {MouseEventHandler, useCallback, useMemo, useState} from 'react'
 
 import Link from '../../components/routing/Link'
 import {MSectionResponse} from '../../models/graphResponseModels'
+import {appEntryRoute} from '../../pages/apps/appsRoute'
 import entryRoute from '../../pages/entry/entryRoute'
 import {SubSection as SubSectionDefinition} from '../../utils/metainfo'
 import {assert, splice} from '../../utils/utils'
@@ -27,9 +28,17 @@ function SubSectionNavComponent({
   const {value, getDefinition, change, path} = property
   const definition = useMemo(() => getDefinition(), [getDefinition])
   const repeats = definition?.repeats
-  const entryRouteIndex = fullMatch.findIndex(
-    (part) => part.route.routeId === entryRoute.routeId,
-  )
+
+  const entryRouteIndex = useMemo(() => {
+    const entryRouteIndex = fullMatch.findIndex(
+      (part) => part.route.routeId === entryRoute.routeId,
+    )
+    const appEntryRouteIndex = fullMatch.findIndex(
+      (part) => part.route.routeId === appEntryRoute.routeId,
+    )
+    return entryRouteIndex === -1 ? appEntryRouteIndex : entryRouteIndex
+  }, [fullMatch])
+
   const archivePath =
     fullMatch
       .filter((part, index) => index <= entryRouteIndex)
diff --git a/src/components/routing/useRoute.tsx b/src/components/routing/useRoute.tsx
index 1e8c92ad..d9f29782 100644
--- a/src/components/routing/useRoute.tsx
+++ b/src/components/routing/useRoute.tsx
@@ -1,6 +1,5 @@
 import {useMemo} from 'react'
 
-import {routesEqual} from './normalizeRoute'
 import {DefaultSearch, Route, RouteData} from './types'
 import useRouteContext from './useRouteContext'
 
@@ -9,7 +8,7 @@ export default function useRoute<
   Response = unknown,
   Search extends DefaultSearch = DefaultSearch,
 >(
-  route?: Route<Request, Response, Search>,
+  ...route: Route<Request, Response, Search>[]
 ): RouteData<Request, Response, Search> {
   const routeContext = useRouteContext()
   return useMemo(() => {
@@ -17,12 +16,8 @@ export default function useRoute<
     if (route) {
       const {fullMatch} = globalRouteData
       for (let i = index; i >= 0; i--) {
-        if (
-          routesEqual(
-            route,
-            fullMatch[i].route as unknown as Route<Request, Response, Search>,
-          )
-        ) {
+        const matchRouteId = fullMatch[i].route.routeId
+        if (route.find((r) => r.routeId === matchRouteId)) {
           return createRouteData(globalRouteData, i) as unknown as RouteData<
             Request,
             Response,
@@ -36,5 +31,6 @@ export default function useRoute<
       Response,
       Search
     >
-  }, [route, routeContext])
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [...route, routeContext])
 }
diff --git a/src/components/routing/useRouteData.tsx b/src/components/routing/useRouteData.tsx
index f2059cf3..a4f9300a 100644
--- a/src/components/routing/useRouteData.tsx
+++ b/src/components/routing/useRouteData.tsx
@@ -6,9 +6,17 @@ export function useAvailableRouteData<
   Response = unknown,
   Search extends DefaultSearch = DefaultSearch,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
->(route?: Route<Request, Response, Search>) {
-  const {response} = useRoute<Request, Response, Search>()
-  return response as Response | undefined
+>(...route: Route<Request, Response, Search>[]) {
+  const {route: usedRoute, response} = useRoute<Request, Response, Search>(
+    ...route,
+  )
+  if (
+    (route.length > 0 && !route.find((r) => r.routeId === usedRoute.routeId)) ||
+    !response
+  ) {
+    return undefined
+  }
+  return response as Response
 }
 
 export default function useRouteData<
@@ -16,8 +24,8 @@ export default function useRouteData<
   Response = unknown,
   Search extends DefaultSearch = DefaultSearch,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
->(route?: Route<Request, Response, Search>) {
-  const {response} = useRoute<Request, Response, Search>(route)
+>(...route: Route<Request, Response, Search>[]) {
+  const response = useAvailableRouteData(...route)
   if (!response) {
     throw new Error(
       'Route data should be available. This indicates a bug in <Routes/>.',
diff --git a/src/components/values/primitives/FileReference.tsx b/src/components/values/primitives/FileReference.tsx
index d17b859a..efae8bdd 100644
--- a/src/components/values/primitives/FileReference.tsx
+++ b/src/components/values/primitives/FileReference.tsx
@@ -1,6 +1,5 @@
-import entryRoute from '../../../pages/entry/entryRoute'
+import useEntryRouteData from '../../../pages/entry/useEntryRouteData'
 import {FileSelectOptions} from '../../navigation/useSelect'
-import useRouteData from '../../routing/useRouteData'
 import Reference, {DynamicReferenceProps, ReferenceProps} from './Reference'
 
 export type FileReferenceProps = ReferenceProps &
@@ -16,7 +15,7 @@ export default function FileReference({
   filePattern,
   ...referenceProps
 }: FileReferenceProps) {
-  const {upload_id, mainfile_path} = useRouteData(entryRoute)
+  const {upload_id, mainfile_path} = useEntryRouteData()
 
   const selectOptions = {
     entity: 'file',
diff --git a/src/components/values/primitives/SectionReference.tsx b/src/components/values/primitives/SectionReference.tsx
index 2a49b8f7..b552a7d6 100644
--- a/src/components/values/primitives/SectionReference.tsx
+++ b/src/components/values/primitives/SectionReference.tsx
@@ -1,6 +1,5 @@
-import entryRoute from '../../../pages/entry/entryRoute'
+import useEntryRouteData from '../../../pages/entry/useEntryRouteData'
 import {SectionSelectOptions} from '../../navigation/useSelect'
-import useRouteData from '../../routing/useRouteData'
 import Reference, {DynamicReferenceProps, ReferenceProps} from './Reference'
 
 export type SectionReferenceProps = ReferenceProps &
@@ -16,7 +15,7 @@ export default function SectionReference({
   sectionDefinition,
   ...referenceProps
 }: SectionReferenceProps) {
-  const {upload_id, entry_id} = useRouteData(entryRoute)
+  const {upload_id, entry_id} = useEntryRouteData()
 
   const selectOptions = {
     entity: 'section',
diff --git a/src/hooks/useViewWriteAbilities.tsx b/src/hooks/useViewWriteAbilities.tsx
new file mode 100644
index 00000000..6af7cfde
--- /dev/null
+++ b/src/hooks/useViewWriteAbilities.tsx
@@ -0,0 +1,61 @@
+import {useAvailableRouteData} from '../components/routing/useRouteData'
+import {appEntryRoute} from '../pages/apps/appsRoute'
+import entryRoute from '../pages/entry/entryRoute'
+import uploadRoute from '../pages/upload/uploadRoute'
+import {assert} from '../utils/utils'
+import useAuth from './useAuth'
+
+/**
+ * A hook that provides information about the current user's abilities
+ * to read and write data.
+ *
+ * Considers upload authors, reviewers, and respective groups, publish, and
+ * processing status. Works in the context of upload page, entry page (
+ * within upload and search routes).
+ */
+export default function useViewWriteAbilities() {
+  const {user} = useAuth()
+
+  const uploadResponse = useAvailableRouteData(uploadRoute) || {}
+  const entryResponse = useAvailableRouteData(entryRoute) || {}
+  const appEntryResponse = useAvailableRouteData(appEntryRoute) || {
+    metadata: {},
+  }
+  const data = {
+    ...uploadResponse,
+    ...entryResponse,
+    ...appEntryResponse.metadata,
+  }
+  const {
+    viewers,
+    viewer_groups,
+    writers,
+    writer_groups,
+    process_running,
+    published,
+    with_embargo,
+  } = data
+
+  if (!(writers && writer_groups && viewers && viewer_groups)) {
+    return {
+      canRead: false,
+      canWrite: false,
+    }
+  }
+  assert(
+    published !== undefined && with_embargo !== undefined,
+    'published data missing in route data',
+  )
+  const userId = user?.profile?.sub
+  return {
+    canRead:
+      (published && !with_embargo) ||
+      viewers.map((u) => u.user_id).includes(userId),
+    // TODO viewer groups
+    canWrite:
+      !process_running &&
+      !published &&
+      writers.map((u) => u.user_id).includes(userId),
+    // TODO writer groups
+  }
+}
diff --git a/src/index.tsx b/src/index.tsx
index 37115408..58bde89a 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,7 +2,6 @@ import '@fontsource/titillium-web/400.css'
 import '@fontsource/titillium-web/700.css'
 import {LocalizationProvider} from '@mui/x-date-pickers'
 import {AdapterDateFns} from '@mui/x-date-pickers/AdapterDateFnsV3'
-import React from 'react'
 import ReactDOM from 'react-dom/client'
 import {RecoilRoot} from 'recoil'
 
@@ -45,23 +44,23 @@ enableMocking().then(() => {
     document.getElementById('root') as HTMLElement,
   )
   root.render(
-    <React.StrictMode>
-      <LocalizationProvider dateAdapter={AdapterDateFns}>
-        <RecoilRoot>
-          <Theme>
-            <AuthProvider>
-              <ApiProvider>
-                <UnitProvider>
-                  <DevTools enabled>
-                    <Routes />
-                  </DevTools>
-                </UnitProvider>
-              </ApiProvider>
-            </AuthProvider>
-          </Theme>
-        </RecoilRoot>
-      </LocalizationProvider>
-    </React.StrictMode>,
+    // <React.StrictMode>
+    <LocalizationProvider dateAdapter={AdapterDateFns}>
+      <RecoilRoot>
+        <Theme>
+          <AuthProvider>
+            <ApiProvider>
+              <UnitProvider>
+                <DevTools enabled>
+                  <Routes />
+                </DevTools>
+              </UnitProvider>
+            </ApiProvider>
+          </AuthProvider>
+        </Theme>
+      </RecoilRoot>
+    </LocalizationProvider>,
+    // </React.StrictMode>,
   )
 })
 
diff --git a/src/pages/apps/appsRoute.tsx b/src/pages/apps/appsRoute.tsx
index a7e9243e..9bacaac2 100644
--- a/src/pages/apps/appsRoute.tsx
+++ b/src/pages/apps/appsRoute.tsx
@@ -17,11 +17,18 @@ export const appEntryRoute: Route<EntryRequest, EntryResponse> = {
     process_status: '*',
     entry_create_time: '*',
     complete_time: '*',
+    parser_name: '*',
     metadata: {
       entry_name: '*',
       references: '*',
       authors: '*',
       datasets: '*',
+      writers: '*',
+      writer_groups: '*',
+      viewers: '*',
+      viewer_groups: '*',
+      published: '*',
+      with_embargo: '*',
     } as EntryMetadataRequest,
     archive: {...archiveRequest},
   },
diff --git a/src/pages/entry/EntryMetadataEditor.tsx b/src/pages/entry/EntryMetadataEditor.tsx
index 376d5d53..676b41e1 100644
--- a/src/pages/entry/EntryMetadataEditor.tsx
+++ b/src/pages/entry/EntryMetadataEditor.tsx
@@ -2,12 +2,11 @@ import {useMemo} from 'react'
 
 import ProcessStatus from '../../components/ProcessStatus'
 import {Layout, LayoutItem} from '../../components/layout/Layout'
-import useRouteData from '../../components/routing/useRouteData'
 import List from '../../components/values/containers/List'
 import Text from '../../components/values/primitives/Text'
 import {EntryResponse} from '../../models/graphResponseModels'
 import {assert} from '../../utils/utils'
-import entryRoute from './entryRoute'
+import useEntryRouteData from './useEntryRouteData'
 
 function DatasetList({data}: {data: EntryResponse}) {
   // TODO We should remove the "|| []" at some point. This is only there,
@@ -39,7 +38,7 @@ export default function EntryMetadataEditor({
   editable = false,
   expandedByDefault = false,
 }: EntryMetadataEditorProps) {
-  const data = useRouteData(entryRoute) as EntryResponse
+  const data = useEntryRouteData()
   const layout = useMemo(
     () =>
       ({
diff --git a/src/pages/entry/EntryNavAbout.tsx b/src/pages/entry/EntryNavAbout.tsx
index 9eb9bb31..f807be2f 100644
--- a/src/pages/entry/EntryNavAbout.tsx
+++ b/src/pages/entry/EntryNavAbout.tsx
@@ -1,14 +1,12 @@
 import ProcessStatus from '../../components/ProcessStatus'
 import NavAbout from '../../components/navigation/NavAbout'
-import useRouteData from '../../components/routing/useRouteData'
 import CopyToClipboard from '../../components/values/actions/CopyToClipboard'
 import Value from '../../components/values/containers/Value'
 import Id from '../../components/values/primitives/Id'
-import {EntryRequest} from '../../models/graphRequestModels'
-import {EntryResponse} from '../../models/graphResponseModels'
+import useEntryRouteData from './useEntryRouteData'
 
 export default function EntryNavAbout() {
-  const entry = useRouteData<EntryRequest, EntryResponse>()
+  const entry = useEntryRouteData()
   const {mainfile_path, entry_id} = entry
   return (
     <NavAbout label='entry' title={mainfile_path as string}>
diff --git a/src/pages/entry/EntryOverview.tsx b/src/pages/entry/EntryOverview.tsx
index 9def0a76..ef80ae65 100644
--- a/src/pages/entry/EntryOverview.tsx
+++ b/src/pages/entry/EntryOverview.tsx
@@ -8,7 +8,6 @@ import {calculateRequestFromLayout} from '../../components/editor/utils'
 import {LayoutItem} from '../../components/layout/Layout'
 import useSelect from '../../components/navigation/useSelect'
 import useRoute from '../../components/routing/useRoute'
-import useRouteData from '../../components/routing/useRouteData'
 import {EntryRequest, MSectionRequest} from '../../models/graphRequestModels'
 import {MSectionResponse} from '../../models/graphResponseModels'
 import {Section} from '../../utils/metainfo'
@@ -18,10 +17,11 @@ import EntryDataEditor from './EntryDataEditor'
 import EntryMetadataEditor from './EntryMetadataEditor'
 import EntryPageTitle from './EntryPageTitle'
 import EntrySection from './EntrySection'
-import entryRoute, {archiveRequest, useEntryDataForRoute} from './entryRoute'
+import {archiveRequest, useEntryDataForRoute} from './entryRoute'
+import useEntryRouteData from './useEntryRouteData'
 
 function EntryOverviewEditor() {
-  const {archive: archiveData} = useRouteData(entryRoute)
+  const {archive: archiveData} = useEntryRouteData()
   const {reloadCount} = useRoute()
   const [error, setError] = useState<Error | undefined>()
   const {isPage} = useSelect()
@@ -112,7 +112,7 @@ function EntryOverviewEditor() {
 }
 
 export default function EntryOverview() {
-  const {archive: rootSectionData} = useRouteData(entryRoute)
+  const {archive: rootSectionData} = useEntryRouteData()
   assert(
     rootSectionData !== undefined,
     'An entry should always have a root section',
diff --git a/src/pages/entry/EntryPage.test.tsx b/src/pages/entry/EntryPage.test.tsx
index 9201f47b..4143a490 100644
--- a/src/pages/entry/EntryPage.test.tsx
+++ b/src/pages/entry/EntryPage.test.tsx
@@ -3,20 +3,20 @@ import {expect, it, vi} from 'vitest'
 
 import {addDefinitions} from '../../components/archive/archive.helper'
 import {getArchive} from '../../components/archive/useArchive'
-import {GraphResponse} from '../../models/graphResponseModels'
+import {Route} from '../../components/routing/types'
 import * as api from '../../utils/api'
 import {
   importLazyComponents,
   renderWithRouteData,
 } from '../../utils/test.helper'
-import entryRoute from './entryRoute'
+import uploadsRoute from '../uploads/uploadsRoute'
 
-await importLazyComponents(entryRoute)
+await importLazyComponents(uploadsRoute)
 
 describe('EntryPage', () => {
   it('loads and initially renders', async () => {
     const mockedApi = vi.spyOn(api, 'graphApi')
-    window.history.replaceState(null, '', '/entryId')
+    window.history.replaceState(null, '', 'uploads/uploadId/entries/entryId')
 
     const metadata = {
       entry_name: 'entry-name',
@@ -24,22 +24,29 @@ describe('EntryPage', () => {
     }
 
     mockedApi.mockResolvedValue({
-      entryId: {
-        entry_id: 'entryId',
-        mainfile_path: 'entry.archive.json',
-        metadata: {
-          ...metadata,
-          datasets: [],
-        },
-        archive: addDefinitions({
-          metadata,
-          material: {
-            name: 'gold',
+      uploads: {
+        uploadId: {
+          upload_id: 'uploadId',
+          entries: {
+            entryId: {
+              entry_id: 'entryId',
+              mainfile_path: 'entry.archive.json',
+              metadata: {
+                ...metadata,
+                datasets: [],
+              },
+              archive: addDefinitions({
+                metadata,
+                material: {
+                  name: 'gold',
+                },
+              }),
+            },
           },
-        }),
+        },
       },
-    } as GraphResponse)
-    await renderWithRouteData(entryRoute)
+    })
+    await renderWithRouteData(uploadsRoute as unknown as Route)
     await waitFor(() =>
       expect(screen.getAllByText(/entry.archive.json/)).toHaveLength(3),
     )
diff --git a/src/pages/entry/EntryPage.tsx b/src/pages/entry/EntryPage.tsx
index f1597de2..cb25ed95 100644
--- a/src/pages/entry/EntryPage.tsx
+++ b/src/pages/entry/EntryPage.tsx
@@ -18,13 +18,13 @@ import UploadNavAbout from '../upload/UploadNavAbout'
 import uploadRoute from '../upload/uploadRoute'
 import EntryArchiveNav from './EntryArchiveNav'
 import EntryNavItem from './EntryNavAbout'
-import entryRoute from './entryRoute'
+import useEntryRouteData from './useEntryRouteData'
 
 export default function EntryPage() {
   const {url, fullMatch, index} = useRoute()
   const nextPath = fullMatch[index + 1]?.path
   const tab = nextPath === '' ? 'overview' : nextPath
-  const {entry_id, mainfile_path} = useRouteData(entryRoute)
+  const {entry_id, mainfile_path} = useEntryRouteData()
   const {upload_id} = useRouteData(uploadRoute)
   const {isPage, pageVariant} = useSelect()
 
diff --git a/src/pages/entry/EntryPageTitle.tsx b/src/pages/entry/EntryPageTitle.tsx
index 9057baab..f6da367f 100644
--- a/src/pages/entry/EntryPageTitle.tsx
+++ b/src/pages/entry/EntryPageTitle.tsx
@@ -6,9 +6,8 @@ import {PageTitle, PageTitleProps} from '../../components/page/Page'
 import usePage from '../../components/page/usePage'
 import Link from '../../components/routing/Link'
 import useRoute from '../../components/routing/useRoute'
-import useRouteData from '../../components/routing/useRouteData'
 import {assert} from '../../utils/utils'
-import entryRoute from './entryRoute'
+import useEntryRouteData from './useEntryRouteData'
 
 export default function EntryPageTitle(props: PageTitleProps) {
   const {url} = useRoute()
@@ -16,7 +15,7 @@ export default function EntryPageTitle(props: PageTitleProps) {
     upload_id,
     mainfile_path,
     archive: rootSectionData,
-  } = useRouteData(entryRoute)
+  } = useEntryRouteData()
   assert(
     rootSectionData !== undefined,
     'An entry should always have a root section',
diff --git a/src/pages/entry/EntrySection.tsx b/src/pages/entry/EntrySection.tsx
index e551076b..1d4ebd61 100644
--- a/src/pages/entry/EntrySection.tsx
+++ b/src/pages/entry/EntrySection.tsx
@@ -3,13 +3,16 @@ import {useMemo} from 'react'
 
 import Section from '../../components/archive/Section'
 import JsonViewer from '../../components/fileviewer/JsonViewer'
+import useSelect from '../../components/navigation/useSelect'
 import Outlet from '../../components/routing/Outlet'
 import useRoute from '../../components/routing/useRoute'
 import useRouteData from '../../components/routing/useRouteData'
+import useViewWriteAbilities from '../../hooks/useViewWriteAbilities'
 import {MSectionRequest} from '../../models/graphRequestModels'
 import {MSectionResponse} from '../../models/graphResponseModels'
 import {JSONObject} from '../../utils/types'
 import EntryPageTitle from './EntryPageTitle'
+import useEntryRouteData from './useEntryRouteData'
 
 const sortRawDataKeys = (a: string, b: string) => {
   if (a.startsWith('m_') === b.startsWith('m_')) {
@@ -21,9 +24,15 @@ const sortRawDataKeys = (a: string, b: string) => {
   return 1
 }
 
+const nonEditableSections = ['results', 'metadata']
+
 export default function EntrySection() {
   const {isLeaf, fullMatch} = useRoute()
   const data = useRouteData<MSectionRequest, MSectionResponse>()
+  const {parser_name} = useEntryRouteData()
+  const {canWrite} = useViewWriteAbilities()
+  const {isPage} = useSelect()
+
   const startIndex = useMemo(
     () => fullMatch.findIndex((match) => match.route.path === 'archive'),
     [fullMatch],
@@ -42,7 +51,12 @@ export default function EntrySection() {
     return <Outlet />
   }
 
-  const editable = true // TODO This needs to be set based on the user's permissions
+  const editable =
+    canWrite &&
+    isPage &&
+    sectionPath !== '' &&
+    !nonEditableSections.find((s) => sectionPath.startsWith(s)) &&
+    parser_name === 'parsers/archive'
 
   const title =
     fullMatch.length === startIndex + 1
diff --git a/src/pages/entry/entryRoute.tsx b/src/pages/entry/entryRoute.tsx
index b910f90e..2a09ceca 100644
--- a/src/pages/entry/entryRoute.tsx
+++ b/src/pages/entry/entryRoute.tsx
@@ -130,6 +130,7 @@ const entryRoute: Route<EntryRequest, EntryResponse> = {
     process_status: '*',
     entry_create_time: '*',
     complete_time: '*',
+    parser_name: '*',
     metadata: {
       entry_name: '*',
       references: '*',
diff --git a/src/pages/entry/useEntryRouteData.tsx b/src/pages/entry/useEntryRouteData.tsx
new file mode 100644
index 00000000..73e76423
--- /dev/null
+++ b/src/pages/entry/useEntryRouteData.tsx
@@ -0,0 +1,7 @@
+import useRouteData from '../../components/routing/useRouteData'
+import {appEntryRoute} from '../apps/appsRoute'
+import entryRoute from './entryRoute'
+
+export default function useEntryRouteData() {
+  return useRouteData(entryRoute, appEntryRoute)
+}
diff --git a/src/pages/groups/GroupsTable.test.tsx b/src/pages/groups/GroupsTable.test.tsx
index e1432ef6..799dbbd6 100644
--- a/src/pages/groups/GroupsTable.test.tsx
+++ b/src/pages/groups/GroupsTable.test.tsx
@@ -2,6 +2,7 @@ import {render, screen, within} from '@testing-library/react'
 import {describe, expect, it, vi} from 'vitest'
 
 import GroupsTable from './GroupsTable'
+import groupsRoute from './groupsRoute'
 
 const setPaginate = vi.fn()
 const navigate = vi.fn()
@@ -22,6 +23,7 @@ vi.mock('../../components/routing/usePagination', () => ({
 vi.mock('../../components/routing/useRoute', () => ({
   default: () => ({
     navigate,
+    route: groupsRoute,
     search: {},
     url: () => '/',
     response: Array.from({length: 12}, (_, index) => ({
diff --git a/src/pages/upload/UploadMetadataEditor.test.tsx b/src/pages/upload/UploadMetadataEditor.test.tsx
index 50d99e15..8ba93cd2 100644
--- a/src/pages/upload/UploadMetadataEditor.test.tsx
+++ b/src/pages/upload/UploadMetadataEditor.test.tsx
@@ -2,19 +2,20 @@ import {render, screen} from '@testing-library/react'
 import userEvent from '@testing-library/user-event'
 import {vi} from 'vitest'
 
+import * as useRouteData from '../../components/routing/useRouteData'
 import {UploadResponse} from '../../models/graphResponseModels'
 import UploadMetadataEditor from './UploadMetadataEditor'
 
 describe('UploadMetadataEditor', () => {
+  const useAvailableRouteData = vi.spyOn(useRouteData, 'useAvailableRouteData')
+
   it('renders with name and id', async () => {
-    vi.mock('../../components/routing/useRouteData', () => ({
-      default: vi.fn().mockReturnValue({
-        upload_id: 'test-id',
-        upload_name: 'test-name',
-        main_author: {name: 'test-main-author'},
-        coauthors: [{name: 'test-coauthor'}],
-      } as UploadResponse),
-    }))
+    useAvailableRouteData.mockReturnValue({
+      upload_id: 'test-id',
+      upload_name: 'test-name',
+      main_author: {name: 'test-main-author'},
+      coauthors: [{name: 'test-coauthor'}],
+    } as UploadResponse)
 
     render(<UploadMetadataEditor />)
 
@@ -24,4 +25,11 @@ describe('UploadMetadataEditor', () => {
     expect(screen.getByText(/test-main-author/)).toBeInTheDocument()
     expect(screen.getByText(/test-coauthor/)).toBeInTheDocument()
   })
+
+  it('renders nothing if no upload data is available', () => {
+    useAvailableRouteData.mockReturnValue(undefined)
+    render(<UploadMetadataEditor />)
+
+    expect(screen.queryByText('test-name')).not.toBeInTheDocument()
+  })
 })
diff --git a/src/pages/upload/UploadMetadataEditor.tsx b/src/pages/upload/UploadMetadataEditor.tsx
index 67e72700..b260fd36 100644
--- a/src/pages/upload/UploadMetadataEditor.tsx
+++ b/src/pages/upload/UploadMetadataEditor.tsx
@@ -3,7 +3,7 @@ import {useMemo} from 'react'
 import ProcessStatus from '../../components/ProcessStatus'
 import Visibility from '../../components/Visibility'
 import {Layout, LayoutItem} from '../../components/layout/Layout'
-import useRouteData from '../../components/routing/useRouteData'
+import {useAvailableRouteData} from '../../components/routing/useRouteData'
 import List from '../../components/values/containers/List'
 import Text from '../../components/values/primitives/Text'
 import {UploadResponse} from '../../models/graphResponseModels'
@@ -44,7 +44,8 @@ export default function UploadMetadataEditor({
   main = false,
 }: UploadMetadataEditorProps) {
   expandedByDefault = main ? true : expandedByDefault
-  const data = useRouteData(uploadRoute) as UploadResponse
+  const availableData = useAvailableRouteData(uploadRoute)
+  const data = useMemo(() => availableData || {}, [availableData])
 
   const layout = useMemo(
     () =>
@@ -132,5 +133,9 @@ export default function UploadMetadataEditor({
     [editable, expandedByDefault, main, data],
   )
 
+  if (availableData === undefined) {
+    return ''
+  }
+
   return <Layout layout={layout} />
 }
diff --git a/src/pages/upload/UploadOverview.tsx b/src/pages/upload/UploadOverview.tsx
index d579c453..aafbeced 100644
--- a/src/pages/upload/UploadOverview.tsx
+++ b/src/pages/upload/UploadOverview.tsx
@@ -2,15 +2,16 @@ import {Box} from '@mui/material'
 
 import {PageTitle} from '../../components/page/Page'
 import useRouteData from '../../components/routing/useRouteData'
+import {DirectoryResponse} from '../../models/graphResponseModels'
 import UploadActions from './UploadActions'
 import {UploadFilePreview} from './UploadFiles'
 import UploadFilesDownload from './UploadFilesDownload'
 import UploadFilesTable from './UploadFilesTable'
 import UploadMetadataEditor from './UploadMetadataEditor'
-import {filesRoute} from './uploadRoute'
+import {uploadOverviewRoute} from './uploadRoute'
 
 export default function UploadOverview() {
-  const data = useRouteData(filesRoute)
+  const data = useRouteData(uploadOverviewRoute)
   const hasReadme = Object.keys(data).includes('README.md')
 
   return (
@@ -20,7 +21,10 @@ export default function UploadOverview() {
         <UploadMetadataEditor main editable />
       </Box>
       <UploadActions />
-      <UploadFilesTable data={data} navigatePath={(path) => ['files', path]} />
+      <UploadFilesTable
+        data={data as DirectoryResponse}
+        navigatePath={(path) => ['files', path]}
+      />
       {hasReadme && <UploadFilePreview file='README.md' sx={{mt: 4}} />}
     </>
   )
diff --git a/src/pages/upload/uploadRoute.tsx b/src/pages/upload/uploadRoute.tsx
index f1624cd0..fb15bbda 100644
--- a/src/pages/upload/uploadRoute.tsx
+++ b/src/pages/upload/uploadRoute.tsx
@@ -84,6 +84,18 @@ const entriesRoute: Route<
   children: [entryRoute],
 }
 
+export const uploadOverviewRoute: Route<
+  DirectoryRequest,
+  DirectoryResponse,
+  Required<PageBasedPagination>
+> = {
+  ...pathRoute,
+  path: '',
+  breadcrumb: <b>overview</b>,
+  lazyComponent: async () => import('./UploadOverview'),
+  requestKey: 'files',
+}
+
 const uploadRoute: Route<UploadRequest, UploadResponse> = {
   path: ':uploadId',
   request: {
@@ -96,6 +108,11 @@ const uploadRoute: Route<UploadRequest, UploadResponse> = {
     complete_time: '*',
     main_author: {name: '*'},
     coauthors: '*',
+    writers: '*',
+    writer_groups: '*',
+    viewers: '*',
+    viewer_groups: '*',
+    process_running: '*',
   },
   lazyComponent: async () => import('./UploadPage'),
   breadcrumb: ({response}) =>
@@ -108,13 +125,7 @@ const uploadRoute: Route<UploadRequest, UploadResponse> = {
     ),
   onlyRender: ['', 'files/*', 'entries', 'settings'],
   children: [
-    {
-      ...pathRoute,
-      path: '',
-      breadcrumb: <b>overview</b>,
-      lazyComponent: async () => import('./UploadOverview'),
-      requestKey: 'files',
-    },
+    uploadOverviewRoute,
     filesRoute,
     entriesRoute,
     {
diff --git a/src/pages/uploads/UploadsTable.test.tsx b/src/pages/uploads/UploadsTable.test.tsx
index 97f1c651..bdb71e35 100644
--- a/src/pages/uploads/UploadsTable.test.tsx
+++ b/src/pages/uploads/UploadsTable.test.tsx
@@ -2,6 +2,7 @@ import {act, fireEvent, render, screen} from '@testing-library/react'
 import {describe, expect, it, vi} from 'vitest'
 
 import UploadsTable from './UploadsTable'
+import uploadsRoute from './uploadsRoute'
 
 const setPaginate = vi.fn()
 const navigate = vi.fn()
@@ -24,6 +25,7 @@ vi.mock('../../components/routing/useRoute', () => ({
     navigate,
     search: {},
     url: () => '/',
+    route: uploadsRoute,
     response: Array.from({length: 12}, (_, index) => ({
       upload_id: `${index + 1}`,
       upload_name: `Upload ${index + 1}`,
-- 
GitLab


From 1a207c995229e3929b66f43e447b9d21405e9ff5 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Tue, 18 Mar 2025 12:22:03 +0100
Subject: [PATCH 12/17] Added ELN preview and expanded editability control.

---
 src/components/editor/PropertyEditor.tsx      |  1 -
 src/components/editor/SubSectionEditor.tsx    | 10 ++++--
 .../editor/SubSectionTableEditor.test.tsx     |  3 +-
 .../editor/SubSectionTableEditor.tsx          |  3 +-
 src/components/layout/CardItem.tsx            |  7 ++--
 src/components/layout/ContainerItem.tsx       |  8 ++---
 src/components/layout/Layout.tsx              |  1 +
 src/components/layout/useLayoutProps.tsx      | 16 ++++++++++
 src/components/richTable/RichTableEditor.tsx  | 18 ++++++++---
 src/components/richText/RichTextEditor.tsx    | 11 ++++---
 src/components/routing/useSearch.tsx          |  8 +++--
 src/hooks/useViewWriteAbilities.tsx           |  1 +
 src/pages/apps/AppEntryPage.tsx               |  4 +--
 src/pages/apps/appsRoute.tsx                  |  5 +--
 src/pages/entry/EntryDataEditor.test.tsx      |  3 +-
 src/pages/entry/EntryMetadataEditor.tsx       | 11 ++++---
 src/pages/entry/EntryOverview.tsx             | 12 +++++--
 src/pages/entry/EntryPageTitle.tsx            | 32 ++++++++++++++++---
 src/pages/entry/EntrySection.tsx              |  6 +++-
 src/pages/entry/entryRoute.tsx                |  7 +++-
 src/pages/upload/UploadMetadataEditor.tsx     |  9 ++++--
 src/pages/upload/UploadOverview.tsx           |  5 ++-
 src/pages/upload/UploadPage.test.tsx          |  5 +++
 23 files changed, 139 insertions(+), 47 deletions(-)

diff --git a/src/components/editor/PropertyEditor.tsx b/src/components/editor/PropertyEditor.tsx
index c1823e96..f049bcec 100644
--- a/src/components/editor/PropertyEditor.tsx
+++ b/src/components/editor/PropertyEditor.tsx
@@ -3,7 +3,6 @@ import {AbstractItem} from '../layout/Layout'
 export type AbstractProperty = AbstractItem & {
   property: string
   label?: string
-  editable?: boolean
 }
 
 export function getLabel<T extends AbstractProperty>(layout: T) {
diff --git a/src/components/editor/SubSectionEditor.tsx b/src/components/editor/SubSectionEditor.tsx
index d4951fd6..53cf63f2 100644
--- a/src/components/editor/SubSectionEditor.tsx
+++ b/src/components/editor/SubSectionEditor.tsx
@@ -10,7 +10,7 @@ import {ArchiveProperty, useArchiveProperty} from '../archive/useArchive'
 import useSubSectionPath from '../archive/useSubSectionPath'
 import {Remove} from '../icons'
 import {Item, Layout, LayoutItem} from '../layout/Layout'
-import {layoutPropsContext} from '../layout/useLayoutProps'
+import {editableLayout, layoutPropsContext} from '../layout/useLayoutProps'
 import {AbstractProperty, getLabel} from './PropertyEditor'
 
 function SubSectionComponent({
@@ -153,7 +153,11 @@ type SubSectionData = MSectionResponse | MSectionResponse[] | undefined
  */
 export default function SubSectionEditor({layout}: {layout: LayoutItem}) {
   const subSection = layout as SubSection
-  const {property: propertyName, layout: subSectionLayout} = subSection
+  const {
+    property: propertyName,
+    layout: subSectionLayout,
+    editable,
+  } = subSection
   const label = getLabel(subSection)
   const path = useSubSectionPath(propertyName)
 
@@ -173,7 +177,7 @@ export default function SubSectionEditor({layout}: {layout: LayoutItem}) {
   return (
     <SubSectionComponent
       label={label}
-      layout={subSectionLayout}
+      layout={editableLayout(subSectionLayout, editable)}
       property={property}
     />
   )
diff --git a/src/components/editor/SubSectionTableEditor.test.tsx b/src/components/editor/SubSectionTableEditor.test.tsx
index de242bbc..6003c896 100644
--- a/src/components/editor/SubSectionTableEditor.test.tsx
+++ b/src/components/editor/SubSectionTableEditor.test.tsx
@@ -8,15 +8,16 @@ import {SubSectionTable} from './SubSectionTableEditor'
 
 describe('SubSectionTableEditor', async () => {
   const layout = {
+    editable: true,
     type: 'table',
     property: 'test_subsection',
     columns: [
       {
         property: 'quantity',
+        editable: false,
       },
       {
         property: 'editable_quantity',
-        editable: true,
       },
     ],
   } as SubSectionTable
diff --git a/src/components/editor/SubSectionTableEditor.tsx b/src/components/editor/SubSectionTableEditor.tsx
index 3258426d..ceea79ed 100644
--- a/src/components/editor/SubSectionTableEditor.tsx
+++ b/src/components/editor/SubSectionTableEditor.tsx
@@ -64,7 +64,7 @@ function createCellEditor(
 
 export default function SubSectionTableEditor({layout}: {layout: LayoutItem}) {
   const table = layout as SubSectionTable
-  const {property, title, columns} = table
+  const {property, title, columns, editable} = table
   const label = getLabel(table)
 
   const path = useSubSectionPath(property)
@@ -185,6 +185,7 @@ export default function SubSectionTableEditor({layout}: {layout: LayoutItem}) {
         onAdd={handleAdd}
         onDelete={handleDelete}
         itemLabel={label}
+        editable={editable}
       />
     </>
   )
diff --git a/src/components/layout/CardItem.tsx b/src/components/layout/CardItem.tsx
index 8018ecee..ce037dc2 100644
--- a/src/components/layout/CardItem.tsx
+++ b/src/components/layout/CardItem.tsx
@@ -10,7 +10,7 @@ import {
 import React, {useCallback, useMemo, useState} from 'react'
 
 import ExpandMore from '../ExpandMore'
-import {layoutPropsContext} from '../layout/useLayoutProps'
+import {editableLayout, layoutPropsContext} from '../layout/useLayoutProps'
 import ContainerItem, {Container} from './ContainerItem'
 import {AbstractContainer, LayoutItem} from './Layout'
 import useLayoutProps from './useLayoutProps'
@@ -70,6 +70,7 @@ export default function CardItem({layout}: CardItemProps) {
     expandedByDefault,
     expandTransitionTimeout,
     variant = 'standard',
+    editable,
     ...cardProps
   } = card
   const [expanded, setExpanded] = useState(
@@ -120,7 +121,9 @@ export default function CardItem({layout}: CardItemProps) {
             }}
           >
             <Box sx={{flexGrow: 1, width: '100%'}}>
-              <ContainerItem layout={collapsedLayout} />
+              <ContainerItem
+                layout={editableLayout(collapsedLayout, editable)}
+              />
             </Box>
             {!title && actions.length > 0 && (
               <Box sx={{marginLeft: 1, marginRight: -1, marginTop: -1}}>
diff --git a/src/components/layout/ContainerItem.tsx b/src/components/layout/ContainerItem.tsx
index d74d2de6..8683803e 100644
--- a/src/components/layout/ContainerItem.tsx
+++ b/src/components/layout/ContainerItem.tsx
@@ -2,7 +2,7 @@ import {Box, BoxProps} from '@mui/material'
 import Grid from '@mui/material/Grid2'
 
 import {AbstractContainer, Item, Layout, LayoutItem} from './Layout'
-import useLayoutProps from './useLayoutProps'
+import useLayoutProps, {editableLayout} from './useLayoutProps'
 
 export type Container = AbstractContainer & {
   type: 'container'
@@ -11,7 +11,7 @@ export type Container = AbstractContainer & {
 
 export default function ContainerItem({layout}: {layout: LayoutItem}) {
   const container = layout as Container
-  const {type, children, variant = 'grid', ...boxProps} = container
+  const {type, children, variant = 'grid', editable, ...boxProps} = container
   const {action} = useLayoutProps()
 
   let contents
@@ -29,7 +29,7 @@ export default function ContainerItem({layout}: {layout: LayoutItem}) {
       >
         {children.map((child, index) => (
           <Box key={index} sx={{...(child.grow && {flexGrow: 1})}}>
-            <Layout layout={child} />
+            <Layout layout={editableLayout(child, editable)} />
           </Box>
         ))}
       </Box>
@@ -39,7 +39,7 @@ export default function ContainerItem({layout}: {layout: LayoutItem}) {
       <Grid container rowSpacing={2} columnSpacing={2} sx={{flexGrow: 1}}>
         {children.map((child, index) => (
           <Item layout={child} key={index}>
-            <Layout layout={child} />
+            <Layout layout={editableLayout(child, editable)} />
           </Item>
         ))}
       </Grid>
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx
index a6c26317..c74a244b 100644
--- a/src/components/layout/Layout.tsx
+++ b/src/components/layout/Layout.tsx
@@ -66,6 +66,7 @@ export type LayoutItem =
 export type AbstractItem = {
   type: string
   grow?: boolean
+  editable?: boolean
 } & RegularBreakpoints
 
 export type AbstractContainer = AbstractItem & {
diff --git a/src/components/layout/useLayoutProps.tsx b/src/components/layout/useLayoutProps.tsx
index fd8b7ea2..b2f1cb7a 100644
--- a/src/components/layout/useLayoutProps.tsx
+++ b/src/components/layout/useLayoutProps.tsx
@@ -1,5 +1,21 @@
 import React from 'react'
 
+import {LayoutItem} from './Layout'
+
+export function editableLayout<Layout extends LayoutItem = LayoutItem>(
+  layout: Layout,
+  editable?: boolean,
+): Layout {
+  if (layout.editable === undefined) {
+    return {...layout, editable}
+  } else {
+    return {
+      ...layout,
+      editable: editable && layout.editable,
+    }
+  }
+}
+
 type LayoutPropsContextValue = {
   action?: React.ReactNode
 }
diff --git a/src/components/richTable/RichTableEditor.tsx b/src/components/richTable/RichTableEditor.tsx
index fbf46b59..e521fb03 100644
--- a/src/components/richTable/RichTableEditor.tsx
+++ b/src/components/richTable/RichTableEditor.tsx
@@ -54,6 +54,7 @@ export type RichtTableEditorProps = {
   // pressed escape to "abort" the edit. Parent components need to manage
   // the state and restore the old value.
   onAbort?: (columnKey: string, rowIndex: number) => void
+  editable?: boolean
 }
 
 const TableCellWithPrimitive = React.forwardRef(function TabelCellInput(
@@ -152,7 +153,7 @@ const TableCellWithPrimitive = React.forwardRef(function TabelCellInput(
           {error}
         </Typography>
       </Popover>
-      <Box ref={anchorRef}>
+      <Box ref={anchorRef} sx={!props.editable ? {padding: 2} : {}}>
         <Component
           valueIndex={valueIndex}
           {...componentProps}
@@ -210,6 +211,7 @@ export default function RichTableEditor({
   onAdd,
   onDelete,
   itemLabel,
+  editable = true,
 }: RichtTableEditorProps) {
   const cellRefs = useRef<HTMLElement[]>([])
   const editorRefs = useRef<HTMLInputElement[]>([])
@@ -372,7 +374,7 @@ export default function RichTableEditor({
                 </TableCell>
               )
             })}
-            {onDelete && <TableCell />}
+            {onDelete && editable && <TableCell />}
           </TableRow>
         </TableHead>
         <TableBody sx={{backgroundColor: 'background.default'}}>
@@ -393,7 +395,13 @@ export default function RichTableEditor({
                       element as HTMLElement
                   }}
                   component={column.component}
-                  componentProps={column.props}
+                  componentProps={{
+                    ...column.props,
+                    editable:
+                      column.props?.editable === undefined
+                        ? editable
+                        : column.props.editable && editable,
+                  }}
                   value={row[column.key]}
                   onChange={(value) => onChange?.(column.key, rowIndex, value)}
                   inputRef={(element: HTMLInputElement | null) => {
@@ -402,7 +410,7 @@ export default function RichTableEditor({
                   }}
                 />
               ))}
-              {onDelete && (
+              {onDelete && editable && (
                 <TableCell sx={{width: '64px', paddingY: 0}}>
                   <IconButton onClick={() => onDelete(rowIndex)} size='small'>
                     <Delete />
@@ -413,7 +421,7 @@ export default function RichTableEditor({
           ))}
         </TableBody>
       </Table>
-      {onAdd && (
+      {onAdd && editable && (
         <>
           <Divider />
           <Box sx={{margin: 1, display: 'flex', flexDirection: 'row-reverse'}}>
diff --git a/src/components/richText/RichTextEditor.tsx b/src/components/richText/RichTextEditor.tsx
index 383bd0b7..0d566930 100644
--- a/src/components/richText/RichTextEditor.tsx
+++ b/src/components/richText/RichTextEditor.tsx
@@ -98,8 +98,9 @@ function Editor({
   onChange,
   placeholder,
   initialValue,
-  readonly,
+  editable,
 }: RichTextEditorProps) {
+  const readonly = !!editable
   const [editor] = useLexicalComposerContext()
 
   const [showDev, setShowDev] = useState(false)
@@ -288,15 +289,15 @@ export type RichTextEditorProps = {
   onChange?: (html: string) => void
   placeholder?: string
   initialValue?: string
-  readonly?: boolean
+  editable?: boolean
 }
 
 export default function RichTextEditor(props: RichTextEditorProps) {
-  const {readonly} = props
+  const {editable = true} = props
   return (
     <LexicalComposer
-      initialConfig={{...editorConfig, editable: !readonly}}
-      key={`${!readonly}`}
+      initialConfig={{...editorConfig, editable: !!editable}}
+      key={`${!!editable}`}
     >
       <Editor {...props} />
     </LexicalComposer>
diff --git a/src/components/routing/useSearch.tsx b/src/components/routing/useSearch.tsx
index 66f1561e..c7d3be3f 100644
--- a/src/components/routing/useSearch.tsx
+++ b/src/components/routing/useSearch.tsx
@@ -1,9 +1,13 @@
 import {DefaultSearch, Route} from './types'
 import useRoute from './useRoute'
 
-export default function useSearch<Search extends DefaultSearch = DefaultSearch>(
+export default function useSearch<
+  Request = unknown,
+  Response = unknown,
+  Search extends DefaultSearch = DefaultSearch,
+>(
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  route?: Route<unknown, unknown, Search>,
+  route?: Route<Request, Response, Search>,
 ) {
   const {search} = useRoute()
   return search
diff --git a/src/hooks/useViewWriteAbilities.tsx b/src/hooks/useViewWriteAbilities.tsx
index 6af7cfde..4e08cd65 100644
--- a/src/hooks/useViewWriteAbilities.tsx
+++ b/src/hooks/useViewWriteAbilities.tsx
@@ -47,6 +47,7 @@ export default function useViewWriteAbilities() {
     'published data missing in route data',
   )
   const userId = user?.profile?.sub
+
   return {
     canRead:
       (published && !with_embargo) ||
diff --git a/src/pages/apps/AppEntryPage.tsx b/src/pages/apps/AppEntryPage.tsx
index a350cf14..36129615 100644
--- a/src/pages/apps/AppEntryPage.tsx
+++ b/src/pages/apps/AppEntryPage.tsx
@@ -23,9 +23,9 @@ export default function EntryPage() {
       navigation={
         <Nav>
           <NavItem
-            label='projects'
+            label='search apps'
             component={Link}
-            to={url({path: '../../../../uploads'})}
+            to={url({path: '../../../../explore'})}
           />
           <Divider />
           <EntryNavItem />
diff --git a/src/pages/apps/appsRoute.tsx b/src/pages/apps/appsRoute.tsx
index 9bacaac2..260631fc 100644
--- a/src/pages/apps/appsRoute.tsx
+++ b/src/pages/apps/appsRoute.tsx
@@ -4,10 +4,10 @@ import {
   EntryRequest,
 } from '../../models/graphRequestModels'
 import {EntryResponse} from '../../models/graphResponseModels'
-import {archiveRequest, archiveRoute} from '../entry/entryRoute'
+import {EntrySearch, archiveRequest, archiveRoute} from '../entry/entryRoute'
 import AppsPage from './AppsPage'
 
-export const appEntryRoute: Route<EntryRequest, EntryResponse> = {
+export const appEntryRoute: Route<EntryRequest, EntryResponse, EntrySearch> = {
   path: 'explore/:appName/:entryId',
   requestPath: ({path}) => `entries/${path}`,
   request: {
@@ -34,6 +34,7 @@ export const appEntryRoute: Route<EntryRequest, EntryResponse> = {
   },
   lazyComponent: async () => import('./AppEntryPage'),
   breadcrumb: ({path}) => path,
+  validateSearch: ({rawSearch}) => ({preview: rawSearch.preview === 'true'}),
   children: [
     {
       path: '',
diff --git a/src/pages/entry/EntryDataEditor.test.tsx b/src/pages/entry/EntryDataEditor.test.tsx
index e54c05be..3d13e51f 100644
--- a/src/pages/entry/EntryDataEditor.test.tsx
+++ b/src/pages/entry/EntryDataEditor.test.tsx
@@ -11,11 +11,11 @@ import EntryDataEditor from './EntryDataEditor'
 describe('EntryDataEditor', async () => {
   const layout: LayoutItem = {
     type: 'container',
+    editable: true,
     children: [
       {
         type: 'quantity',
         property: 'test_quantity1',
-        editable: true,
       },
       {
         type: 'subSection',
@@ -26,7 +26,6 @@ describe('EntryDataEditor', async () => {
             {
               type: 'quantity',
               property: 'test_quantity2',
-              editable: true,
             },
           ],
         },
diff --git a/src/pages/entry/EntryMetadataEditor.tsx b/src/pages/entry/EntryMetadataEditor.tsx
index 676b41e1..a1d43144 100644
--- a/src/pages/entry/EntryMetadataEditor.tsx
+++ b/src/pages/entry/EntryMetadataEditor.tsx
@@ -2,6 +2,7 @@ import {useMemo} from 'react'
 
 import ProcessStatus from '../../components/ProcessStatus'
 import {Layout, LayoutItem} from '../../components/layout/Layout'
+import {editableLayout} from '../../components/layout/useLayoutProps'
 import List from '../../components/values/containers/List'
 import Text from '../../components/values/primitives/Text'
 import {EntryResponse} from '../../models/graphResponseModels'
@@ -57,6 +58,7 @@ export default function EntryMetadataEditor({
               {
                 type: 'value',
                 label: 'mainfile',
+                editable: false,
                 grow: true,
                 value: data.mainfile_path,
                 component: {
@@ -67,6 +69,7 @@ export default function EntryMetadataEditor({
               },
               {
                 type: 'value',
+                editable: false,
                 component: {
                   Id: {
                     abbreviated: true,
@@ -90,7 +93,6 @@ export default function EntryMetadataEditor({
             children: [
               {
                 type: 'quantity',
-                editable,
                 grow: true,
                 label: 'entry name',
                 property: 'metadata/entry_name',
@@ -105,6 +107,7 @@ export default function EntryMetadataEditor({
                 type: 'value',
                 label: 'created at',
                 value: data.entry_create_time,
+                editable: false,
                 component: {
                   Datetime: {
                     variant: 'datetime',
@@ -115,6 +118,7 @@ export default function EntryMetadataEditor({
                 type: 'value',
                 label: 'last change',
                 value: data.complete_time,
+                editable: false,
                 component: {
                   Datetime: {
                     variant: 'datetime',
@@ -125,7 +129,6 @@ export default function EntryMetadataEditor({
           },
           {
             md: 6,
-            editable: true,
             type: 'quantity',
             label: 'references',
             placeholder: 'no references',
@@ -148,8 +151,8 @@ export default function EntryMetadataEditor({
           },
         ],
       } as LayoutItem),
-    [editable, expandedByDefault, data],
+    [expandedByDefault, data],
   )
 
-  return <Layout layout={layout} />
+  return <Layout layout={editableLayout(layout, editable)} />
 }
diff --git a/src/pages/entry/EntryOverview.tsx b/src/pages/entry/EntryOverview.tsx
index ef80ae65..aefef973 100644
--- a/src/pages/entry/EntryOverview.tsx
+++ b/src/pages/entry/EntryOverview.tsx
@@ -6,8 +6,11 @@ import ErrorMessage from '../../components/app/ErrorMessage'
 import useArchive from '../../components/archive/useArchive'
 import {calculateRequestFromLayout} from '../../components/editor/utils'
 import {LayoutItem} from '../../components/layout/Layout'
+import {editableLayout} from '../../components/layout/useLayoutProps'
 import useSelect from '../../components/navigation/useSelect'
 import useRoute from '../../components/routing/useRoute'
+import useSearch from '../../components/routing/useSearch'
+import useViewWriteAbilities from '../../hooks/useViewWriteAbilities'
 import {EntryRequest, MSectionRequest} from '../../models/graphRequestModels'
 import {MSectionResponse} from '../../models/graphResponseModels'
 import {Section} from '../../utils/metainfo'
@@ -17,7 +20,7 @@ import EntryDataEditor from './EntryDataEditor'
 import EntryMetadataEditor from './EntryMetadataEditor'
 import EntryPageTitle from './EntryPageTitle'
 import EntrySection from './EntrySection'
-import {archiveRequest, useEntryDataForRoute} from './entryRoute'
+import entryRoute, {archiveRequest, useEntryDataForRoute} from './entryRoute'
 import useEntryRouteData from './useEntryRouteData'
 
 function EntryOverviewEditor() {
@@ -25,6 +28,8 @@ function EntryOverviewEditor() {
   const {reloadCount} = useRoute()
   const [error, setError] = useState<Error | undefined>()
   const {isPage} = useSelect()
+  const {canWrite} = useViewWriteAbilities()
+  const {preview} = useSearch(entryRoute)
 
   const layout = useMemo(() => {
     const schema = (archiveData?.data as MSectionResponse)?.m_def as Section
@@ -82,6 +87,7 @@ function EntryOverviewEditor() {
   }
 
   useEntryDataForRoute(request, setError)
+  const editable = canWrite && isPage && !preview
 
   let content: React.ReactNode = ''
   if (error) {
@@ -91,7 +97,7 @@ function EntryOverviewEditor() {
   } else {
     if (isPage) {
       if (layout) {
-        content = <EntryDataEditor layout={layout} />
+        content = <EntryDataEditor layout={editableLayout(layout, editable)} />
       } else {
         content = <ErrorMessage error={new Error('No layout defined')} />
       }
@@ -104,7 +110,7 @@ function EntryOverviewEditor() {
         <UploadMetadataEditor />
       </Box>
       <Box sx={{marginBottom: 4}}>
-        <EntryMetadataEditor editable={isPage} expandedByDefault={isPage} />
+        <EntryMetadataEditor editable={editable} expandedByDefault={isPage} />
       </Box>
       {content}
     </>
diff --git a/src/pages/entry/EntryPageTitle.tsx b/src/pages/entry/EntryPageTitle.tsx
index f6da367f..5dd2512f 100644
--- a/src/pages/entry/EntryPageTitle.tsx
+++ b/src/pages/entry/EntryPageTitle.tsx
@@ -1,31 +1,55 @@
 import GoToIcon from '@mui/icons-material/ChevronRight'
-import {Box, Button} from '@mui/material'
+import {Box, Button, Checkbox, FormControlLabel, FormGroup} from '@mui/material'
+import {useCallback} from 'react'
 
 import EditStatus from '../../components/archive/EditStatus'
+import useSelect from '../../components/navigation/useSelect'
 import {PageTitle, PageTitleProps} from '../../components/page/Page'
 import usePage from '../../components/page/usePage'
 import Link from '../../components/routing/Link'
 import useRoute from '../../components/routing/useRoute'
+import useSearch from '../../components/routing/useSearch'
+import useViewWriteAbilities from '../../hooks/useViewWriteAbilities'
 import {assert} from '../../utils/utils'
+import entryRoute from './entryRoute'
 import useEntryRouteData from './useEntryRouteData'
 
 export default function EntryPageTitle(props: PageTitleProps) {
-  const {url} = useRoute()
+  const {isScrolled} = usePage()
+  const {url, navigate} = useRoute()
+  const {isPage} = useSelect()
+  const {canWrite} = useViewWriteAbilities()
+
   const {
     upload_id,
     mainfile_path,
     archive: rootSectionData,
   } = useEntryRouteData()
+  const {preview} = useSearch(entryRoute)
   assert(
     rootSectionData !== undefined,
     'An entry should always have a root section',
   )
 
-  const {isScrolled} = usePage()
+  const handlePreviewChange = useCallback(() => {
+    navigate({searchUpdates: {preview: preview ? undefined : true}})
+  }, [navigate, preview])
+
+  const editable = canWrite && isPage
 
   const actions = (
     <Box display='flex' alignItems='center' flexDirection='row' gap={1}>
-      <EditStatus />
+      {editable && (
+        <FormGroup>
+          <FormControlLabel
+            control={
+              <Checkbox checked={!!preview} onChange={handlePreviewChange} />
+            }
+            label='Preview'
+          />
+        </FormGroup>
+      )}
+      {editable && <EditStatus />}
       <Button
         variant='contained'
         component={Link}
diff --git a/src/pages/entry/EntrySection.tsx b/src/pages/entry/EntrySection.tsx
index 1d4ebd61..bde06daf 100644
--- a/src/pages/entry/EntrySection.tsx
+++ b/src/pages/entry/EntrySection.tsx
@@ -7,11 +7,13 @@ import useSelect from '../../components/navigation/useSelect'
 import Outlet from '../../components/routing/Outlet'
 import useRoute from '../../components/routing/useRoute'
 import useRouteData from '../../components/routing/useRouteData'
+import useSearch from '../../components/routing/useSearch'
 import useViewWriteAbilities from '../../hooks/useViewWriteAbilities'
 import {MSectionRequest} from '../../models/graphRequestModels'
 import {MSectionResponse} from '../../models/graphResponseModels'
 import {JSONObject} from '../../utils/types'
 import EntryPageTitle from './EntryPageTitle'
+import entryRoute from './entryRoute'
 import useEntryRouteData from './useEntryRouteData'
 
 const sortRawDataKeys = (a: string, b: string) => {
@@ -32,6 +34,7 @@ export default function EntrySection() {
   const {parser_name} = useEntryRouteData()
   const {canWrite} = useViewWriteAbilities()
   const {isPage} = useSelect()
+  const {preview} = useSearch(entryRoute)
 
   const startIndex = useMemo(
     () => fullMatch.findIndex((match) => match.route.path === 'archive'),
@@ -56,7 +59,8 @@ export default function EntrySection() {
     isPage &&
     sectionPath !== '' &&
     !nonEditableSections.find((s) => sectionPath.startsWith(s)) &&
-    parser_name === 'parsers/archive'
+    parser_name === 'parsers/archive' &&
+    !preview
 
   const title =
     fullMatch.length === startIndex + 1
diff --git a/src/pages/entry/entryRoute.tsx b/src/pages/entry/entryRoute.tsx
index 2a09ceca..9bc78f78 100644
--- a/src/pages/entry/entryRoute.tsx
+++ b/src/pages/entry/entryRoute.tsx
@@ -119,7 +119,11 @@ export function useEntryDataForRoute(
   })
 }
 
-const entryRoute: Route<EntryRequest, EntryResponse> = {
+export type EntrySearch = {
+  preview?: boolean
+}
+
+const entryRoute: Route<EntryRequest, EntryResponse, EntrySearch> = {
   path: ':entryId',
   // TODO The function should not be necessary, but some buggy thing is modifying the
   // request. With the function we force to recreate the request object every time.
@@ -142,6 +146,7 @@ const entryRoute: Route<EntryRequest, EntryResponse> = {
   lazyComponent: async () => import('./EntryPage'),
   renderWithPathAsKey: true,
   breadcrumb: ({response}) => path.basename(response?.mainfile_path as string),
+  validateSearch: ({rawSearch}) => ({preview: rawSearch.preview === 'true'}),
   children: [
     {
       path: '',
diff --git a/src/pages/upload/UploadMetadataEditor.tsx b/src/pages/upload/UploadMetadataEditor.tsx
index b260fd36..7aa80cc2 100644
--- a/src/pages/upload/UploadMetadataEditor.tsx
+++ b/src/pages/upload/UploadMetadataEditor.tsx
@@ -3,6 +3,7 @@ import {useMemo} from 'react'
 import ProcessStatus from '../../components/ProcessStatus'
 import Visibility from '../../components/Visibility'
 import {Layout, LayoutItem} from '../../components/layout/Layout'
+import {editableLayout} from '../../components/layout/useLayoutProps'
 import {useAvailableRouteData} from '../../components/routing/useRouteData'
 import List from '../../components/values/containers/List'
 import Text from '../../components/values/primitives/Text'
@@ -66,7 +67,6 @@ export default function UploadMetadataEditor({
                 grow: true,
                 type: 'value',
                 label: 'project name',
-                editable: editable,
                 value: data.upload_name,
                 component: {
                   Text: {
@@ -83,6 +83,7 @@ export default function UploadMetadataEditor({
               {
                 xs: 5,
                 type: 'value',
+                editable: false,
                 component: {
                   Id: {
                     abbreviated: true,
@@ -109,6 +110,7 @@ export default function UploadMetadataEditor({
               {
                 type: 'value',
                 label: 'created at',
+                editable: false,
                 component: {
                   Datetime: {
                     variant: 'datetime',
@@ -119,6 +121,7 @@ export default function UploadMetadataEditor({
               {
                 type: 'value',
                 label: 'last change',
+                editable: false,
                 component: {
                   Datetime: {
                     variant: 'datetime',
@@ -130,12 +133,12 @@ export default function UploadMetadataEditor({
           },
         ],
       } as LayoutItem),
-    [editable, expandedByDefault, main, data],
+    [expandedByDefault, main, data],
   )
 
   if (availableData === undefined) {
     return ''
   }
 
-  return <Layout layout={layout} />
+  return <Layout layout={editableLayout(layout, editable)} />
 }
diff --git a/src/pages/upload/UploadOverview.tsx b/src/pages/upload/UploadOverview.tsx
index aafbeced..8b35baec 100644
--- a/src/pages/upload/UploadOverview.tsx
+++ b/src/pages/upload/UploadOverview.tsx
@@ -2,6 +2,7 @@ import {Box} from '@mui/material'
 
 import {PageTitle} from '../../components/page/Page'
 import useRouteData from '../../components/routing/useRouteData'
+import useViewWriteAbilities from '../../hooks/useViewWriteAbilities'
 import {DirectoryResponse} from '../../models/graphResponseModels'
 import UploadActions from './UploadActions'
 import {UploadFilePreview} from './UploadFiles'
@@ -13,12 +14,14 @@ import {uploadOverviewRoute} from './uploadRoute'
 export default function UploadOverview() {
   const data = useRouteData(uploadOverviewRoute)
   const hasReadme = Object.keys(data).includes('README.md')
+  const {canWrite} = useViewWriteAbilities()
+  const editable = canWrite
 
   return (
     <>
       <PageTitle actions={<UploadFilesDownload />} />
       <Box sx={{mb: 4}}>
-        <UploadMetadataEditor main editable />
+        <UploadMetadataEditor main editable={editable} />
       </Box>
       <UploadActions />
       <UploadFilesTable
diff --git a/src/pages/upload/UploadPage.test.tsx b/src/pages/upload/UploadPage.test.tsx
index 4c3af358..0249e4e5 100644
--- a/src/pages/upload/UploadPage.test.tsx
+++ b/src/pages/upload/UploadPage.test.tsx
@@ -1,6 +1,7 @@
 import {screen, waitFor} from '@testing-library/react'
 import {expect, it, vi} from 'vitest'
 
+import * as useViewWriteAbilities from '../../hooks/useViewWriteAbilities'
 import {GraphResponse} from '../../models/graphResponseModels'
 import * as api from '../../utils/api'
 import {
@@ -14,6 +15,10 @@ await importLazyComponents(uploadRoute)
 describe('UploadPage', () => {
   it('loads and initially renders the overview', async () => {
     const mockedApi = vi.spyOn(api, 'graphApi')
+    vi.spyOn(useViewWriteAbilities, 'default').mockReturnValue({
+      canWrite: true,
+      canRead: true,
+    })
     window.history.replaceState(null, '', '/project')
     mockedApi.mockResolvedValue({
       project: {
-- 
GitLab


From 029d3561bece43f567abadb9bb988e7240818d2a Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Wed, 19 Mar 2025 13:04:23 +0100
Subject: [PATCH 13/17] Added default layout and layout tabs.

---
 src/components/archive/Section.tsx           |  37 +++++-
 src/components/richTable/RichTableEditor.tsx |   5 +-
 src/components/routing/Routes.tsx            |   4 +-
 src/components/routing/loader.ts             |  16 ++-
 src/components/routing/types.ts              |   4 +
 src/pages/apps/appsRoute.tsx                 |   5 +-
 src/pages/entry/EntryOverview.tsx            | 123 +++++++++++--------
 src/pages/entry/EntryPage.test.tsx           |   4 +-
 src/pages/entry/EntrySection.tsx             |  41 ++-----
 src/pages/entry/entryRoute.tsx               |   5 +-
 10 files changed, 151 insertions(+), 93 deletions(-)

diff --git a/src/components/archive/Section.tsx b/src/components/archive/Section.tsx
index d0da427b..a852d515 100644
--- a/src/components/archive/Section.tsx
+++ b/src/components/archive/Section.tsx
@@ -1,17 +1,20 @@
-import {Card, CardContent, CardHeader} from '@mui/material'
+import {Box, Card, CardContent, CardHeader} from '@mui/material'
 import {useCallback, useMemo} from 'react'
 
+import {MSectionRequest} from '../../models/graphRequestModels'
 import {MSectionResponse} from '../../models/graphResponseModels'
 import {
   Quantity as QuantityDefinition,
   Section as SectionDefinition,
   SubSection as SubSectionDefinition,
 } from '../../utils/metainfo'
-import {JSONValue} from '../../utils/types'
+import {JSONObject, JSONValue} from '../../utils/types'
 import {assert} from '../../utils/utils'
 import ErrorBoundary from '../ErrorBoundary'
 import {getPropertyLabel} from '../editor/PropertyEditor'
 import SubSectionNavEditor from '../editor/SubSectionNavEditor'
+import JsonViewer from '../fileviewer/JsonViewer'
+import useRouteData from '../routing/useRouteData'
 import DynamicValue from '../values/utils/DynamicValue'
 import {createDynamicComponentSpec} from '../values/utils/dynamicComponents'
 import SectionProvider from './SectionProvider'
@@ -81,8 +84,12 @@ export type SectionProps = {
 export default function Section({path, editable = false}: SectionProps) {
   const {value} = useArchiveProperty<MSectionResponse>(path)
   const definition = value?.m_def as SectionDefinition
+  const data = useRouteData<MSectionRequest, MSectionResponse>()
 
-  assert(value !== undefined, 'The section should have been loaded by now.')
+  assert(
+    value !== undefined,
+    `The section ${path} should have been loaded by now.`,
+  )
   assert(
     definition !== undefined,
     'The section definition should have been loaded by now.',
@@ -100,8 +107,18 @@ export default function Section({path, editable = false}: SectionProps) {
     )
   }, [definition.all_quantities])
 
+  const sortRawDataKeys = (a: string, b: string) => {
+    if (a.startsWith('m_') === b.startsWith('m_')) {
+      return a.localeCompare(b)
+    }
+    if (a.startsWith('m_')) {
+      return -1
+    }
+    return 1
+  }
+
   return (
-    <>
+    <Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
       {subSections.length > 0 && (
         <ErrorBoundary>
           <Card>
@@ -138,6 +155,16 @@ export default function Section({path, editable = false}: SectionProps) {
           </Card>
         </ErrorBoundary>
       )}
-    </>
+      <Card>
+        <CardHeader title='Raw data' />
+        <CardContent>
+          <JsonViewer
+            value={data as JSONObject}
+            defaultInspectDepth={1}
+            objectSortKeys={sortRawDataKeys}
+          />
+        </CardContent>
+      </Card>
+    </Box>
   )
 }
diff --git a/src/components/richTable/RichTableEditor.tsx b/src/components/richTable/RichTableEditor.tsx
index e521fb03..b72ed983 100644
--- a/src/components/richTable/RichTableEditor.tsx
+++ b/src/components/richTable/RichTableEditor.tsx
@@ -153,7 +153,10 @@ const TableCellWithPrimitive = React.forwardRef(function TabelCellInput(
           {error}
         </Typography>
       </Popover>
-      <Box ref={anchorRef} sx={!props.editable ? {padding: 2} : {}}>
+      <Box
+        ref={anchorRef}
+        sx={!props.editable ? {padding: componentProps?.editable ? 0 : 2} : {}}
+      >
         <Component
           valueIndex={valueIndex}
           {...componentProps}
diff --git a/src/components/routing/Routes.tsx b/src/components/routing/Routes.tsx
index ae737786..84409f91 100644
--- a/src/components/routing/Routes.tsx
+++ b/src/components/routing/Routes.tsx
@@ -12,6 +12,7 @@ import {AsyncState} from 'react-use/lib/useAsyncFn'
 
 import useAsyncConditional from '../../hooks/useAsyncConditional'
 import useAuth from '../../hooks/useAuth'
+import {JSONObject} from '../../utils/types'
 import {assert} from '../../utils/utils'
 import DevToolsJsonViewer from '../devTools/DevToolsJsonViewer'
 import useDevTools from '../devTools/useDevTools'
@@ -131,8 +132,9 @@ function useLoadRouteData(
   reloadCount?: number,
 ): AsyncState<LoaderResult | undefined> {
   const {user} = useAuth()
+  const cache = useRef<{request?: JSONObject; response?: JSONObject}>({})
   const fetch = useMemo(() => {
-    const result = loader(fullMatch, user || undefined)
+    const result = loader(fullMatch, user || undefined, cache.current)
     if (!result) {
       return
     }
diff --git a/src/components/routing/loader.ts b/src/components/routing/loader.ts
index 054b9b33..bf335ae5 100644
--- a/src/components/routing/loader.ts
+++ b/src/components/routing/loader.ts
@@ -1,3 +1,4 @@
+import _ from 'lodash'
 import {User} from 'oidc-client-ts'
 
 import {graphApi} from '../../utils/api'
@@ -161,6 +162,10 @@ export function createRequestForRoute(
 export default function loader(
   match: RouteMatch[],
   user?: User,
+  cache: {
+    request?: JSONObject
+    response?: JSONObject
+  } = {},
 ): (() => Promise<LoaderResult>) | undefined {
   let isEmpty = true
   function getRequest(routeMatch: RouteMatch) {
@@ -186,7 +191,16 @@ export default function loader(
   }
 
   return async () => {
-    const response = (await graphApi(request, user)) as JSONObject
+    // Some route changes do not change the request, so we can reuse the last
+    // response. TODO How to implement reload buttons? Pass some reload flag.
+    let response: JSONObject
+    if (_.isEqual(request, cache.request) && cache.response) {
+      response = cache.response
+    } else {
+      response = (await graphApi(request, user)) as JSONObject
+    }
+    cache.request = request
+    cache.response = response
     // TODO We should only resolve m_defs in archive data and not in uploads,
     // entries, files, etc. Currently we apply this to all and the complete
     // responses no matter what they are.
diff --git a/src/components/routing/types.ts b/src/components/routing/types.ts
index 5034f404..66566d16 100644
--- a/src/components/routing/types.ts
+++ b/src/components/routing/types.ts
@@ -21,6 +21,10 @@ export type LoaderResult = {
 export type Loader = (
   fullMatch: RouteMatch[],
   user?: User,
+  cache?: {
+    request?: JSONObject
+    response?: JSONObject
+  },
 ) => (() => Promise<LoaderResult>) | undefined
 
 export type RouteMatch<
diff --git a/src/pages/apps/appsRoute.tsx b/src/pages/apps/appsRoute.tsx
index 260631fc..3aee3808 100644
--- a/src/pages/apps/appsRoute.tsx
+++ b/src/pages/apps/appsRoute.tsx
@@ -34,7 +34,10 @@ export const appEntryRoute: Route<EntryRequest, EntryResponse, EntrySearch> = {
   },
   lazyComponent: async () => import('./AppEntryPage'),
   breadcrumb: ({path}) => path,
-  validateSearch: ({rawSearch}) => ({preview: rawSearch.preview === 'true'}),
+  validateSearch: ({rawSearch}) => ({
+    preview: rawSearch.preview === 'true',
+    layout: rawSearch.layout,
+  }),
   children: [
     {
       path: '',
diff --git a/src/pages/entry/EntryOverview.tsx b/src/pages/entry/EntryOverview.tsx
index aefef973..10c0474f 100644
--- a/src/pages/entry/EntryOverview.tsx
+++ b/src/pages/entry/EntryOverview.tsx
@@ -1,8 +1,10 @@
-import {Box} from '@mui/material'
-import {useMemo, useState} from 'react'
+import {Box, Tab, Tabs, Typography} from '@mui/material'
+import {useCallback, useMemo, useState} from 'react'
 import {usePrevious} from 'react-use'
 
+import ErrorBoundary from '../../components/ErrorBoundary'
 import ErrorMessage from '../../components/app/ErrorMessage'
+import Section from '../../components/archive/Section'
 import useArchive from '../../components/archive/useArchive'
 import {calculateRequestFromLayout} from '../../components/editor/utils'
 import {LayoutItem} from '../../components/layout/Layout'
@@ -13,7 +15,7 @@ import useSearch from '../../components/routing/useSearch'
 import useViewWriteAbilities from '../../hooks/useViewWriteAbilities'
 import {EntryRequest, MSectionRequest} from '../../models/graphRequestModels'
 import {MSectionResponse} from '../../models/graphResponseModels'
-import {Section} from '../../utils/metainfo'
+import {Section as SectionDefinition} from '../../utils/metainfo'
 import {assert} from '../../utils/utils'
 import UploadMetadataEditor from '../upload/UploadMetadataEditor'
 import EntryDataEditor from './EntryDataEditor'
@@ -23,35 +25,16 @@ import EntrySection from './EntrySection'
 import entryRoute, {archiveRequest, useEntryDataForRoute} from './entryRoute'
 import useEntryRouteData from './useEntryRouteData'
 
-function EntryOverviewEditor() {
-  const {archive: archiveData} = useEntryRouteData()
+function EntryOverviewEditor({
+  layout,
+  editable,
+}: {
+  layout: LayoutItem
+  editable?: boolean
+}) {
   const {reloadCount} = useRoute()
   const [error, setError] = useState<Error | undefined>()
   const {isPage} = useSelect()
-  const {canWrite} = useViewWriteAbilities()
-  const {preview} = useSearch(entryRoute)
-
-  const layout = useMemo(() => {
-    const schema = (archiveData?.data as MSectionResponse)?.m_def as Section
-    let layout: LayoutItem
-    const layouts = schema?.m_annotations?.layout as unknown as
-      | LayoutItem
-      | LayoutItem[]
-      | undefined
-    if (Array.isArray(layouts)) {
-      layout = layouts[0]
-    } else if (layouts !== undefined) {
-      layout = layouts
-    } else {
-      return undefined
-    }
-
-    return {
-      type: 'subSection',
-      property: 'data',
-      layout,
-    } satisfies LayoutItem
-  }, [archiveData])
 
   const request = useMemo(() => {
     const request = {
@@ -87,7 +70,6 @@ function EntryOverviewEditor() {
   }
 
   useEntryDataForRoute(request, setError)
-  const editable = canWrite && isPage && !preview
 
   let content: React.ReactNode = ''
   if (error) {
@@ -96,35 +78,57 @@ function EntryOverviewEditor() {
     content = <ErrorMessage error={error} />
   } else {
     if (isPage) {
-      if (layout) {
-        content = <EntryDataEditor layout={editableLayout(layout, editable)} />
-      } else {
-        content = <ErrorMessage error={new Error('No layout defined')} />
-      }
+      content = <EntryDataEditor layout={editableLayout(layout, editable)} />
     }
   }
 
-  return (
-    <>
-      <Box sx={{marginBottom: 2}}>
-        <UploadMetadataEditor />
-      </Box>
-      <Box sx={{marginBottom: 4}}>
-        <EntryMetadataEditor editable={editable} expandedByDefault={isPage} />
-      </Box>
-      {content}
-    </>
-  )
+  return <ErrorBoundary>{content}</ErrorBoundary>
 }
 
 export default function EntryOverview() {
   const {archive: rootSectionData} = useEntryRouteData()
+  const {canWrite} = useViewWriteAbilities()
+  const {preview} = useSearch(entryRoute)
+  const {navigate} = useRoute()
+
   assert(
     rootSectionData !== undefined,
     'An entry should always have a root section',
   )
 
   const {isPage, isSelect} = useSelect()
+  const layout = useMemo(() => {
+    const schema = (rootSectionData?.data as MSectionResponse)
+      ?.m_def as SectionDefinition
+    let layout: LayoutItem
+    const layouts = schema?.m_annotations?.layout as unknown as
+      | LayoutItem
+      | LayoutItem[]
+      | undefined
+    if (Array.isArray(layouts)) {
+      layout = layouts[0]
+    } else if (layouts !== undefined) {
+      layout = layouts
+    } else {
+      return undefined
+    }
+
+    return {
+      type: 'subSection',
+      property: 'data',
+      layout,
+    } satisfies LayoutItem
+  }, [rootSectionData])
+
+  const {layout: layoutTab = layout ? 'layout' : 'data'} = useSearch(entryRoute)
+  const handleLayoutTabChange = useCallback(
+    (_: React.SyntheticEvent, newValue: 'layout' | 'data') => {
+      navigate({searchUpdates: {layout: newValue}})
+    },
+    [navigate],
+  )
+
+  const editable = canWrite && isPage && !preview
 
   // This memo is an optimization to avoid re-rendering the entire
   // EntryOverviewEditor after usePage causes a scroll event triggered
@@ -137,12 +141,35 @@ export default function EntryOverview() {
             marginTop: -1,
           }}
         >
-          <EntryOverviewEditor />
+          <Box sx={{marginBottom: 2}}>
+            <UploadMetadataEditor />
+          </Box>
+          <Box sx={{marginBottom: 4}}>
+            <EntryMetadataEditor
+              editable={editable}
+              expandedByDefault={isPage}
+            />
+          </Box>
+          <Tabs value={layoutTab} onChange={handleLayoutTabChange} sx={{mb: 2}}>
+            <Tab label='Layout' value='layout' />
+            <Tab label='Data' value='data' />
+          </Tabs>
+          {layoutTab === 'layout' &&
+            (layout ? (
+              <EntryOverviewEditor layout={layout} editable={editable} />
+            ) : (
+              <Typography>
+                This entry has no custom layout defined in it&apos;s schema.
+              </Typography>
+            ))}
+          {layoutTab === 'data' && (
+            <Section path={'data'} editable={editable} />
+          )}
         </Box>
         {isSelect && <EntrySection />}
       </>
     ),
-    [isSelect],
+    [editable, handleLayoutTabChange, isPage, isSelect, layout, layoutTab],
   )
 
   return (
diff --git a/src/pages/entry/EntryPage.test.tsx b/src/pages/entry/EntryPage.test.tsx
index 4143a490..094e494f 100644
--- a/src/pages/entry/EntryPage.test.tsx
+++ b/src/pages/entry/EntryPage.test.tsx
@@ -6,6 +6,7 @@ import {getArchive} from '../../components/archive/useArchive'
 import {Route} from '../../components/routing/types'
 import * as api from '../../utils/api'
 import {
+  createMatchMedia,
   importLazyComponents,
   renderWithRouteData,
 } from '../../utils/test.helper'
@@ -16,6 +17,7 @@ await importLazyComponents(uploadsRoute)
 describe('EntryPage', () => {
   it('loads and initially renders', async () => {
     const mockedApi = vi.spyOn(api, 'graphApi')
+    vi.spyOn(window, 'matchMedia').mockImplementation(createMatchMedia(500))
     window.history.replaceState(null, '', 'uploads/uploadId/entries/entryId')
 
     const metadata = {
@@ -37,7 +39,7 @@ describe('EntryPage', () => {
               },
               archive: addDefinitions({
                 metadata,
-                material: {
+                data: {
                   name: 'gold',
                 },
               }),
diff --git a/src/pages/entry/EntrySection.tsx b/src/pages/entry/EntrySection.tsx
index bde06daf..af54b782 100644
--- a/src/pages/entry/EntrySection.tsx
+++ b/src/pages/entry/EntrySection.tsx
@@ -1,45 +1,30 @@
-import {Box, Card, CardContent, CardHeader} from '@mui/material'
 import {useMemo} from 'react'
 
 import Section from '../../components/archive/Section'
-import JsonViewer from '../../components/fileviewer/JsonViewer'
 import useSelect from '../../components/navigation/useSelect'
 import Outlet from '../../components/routing/Outlet'
 import useRoute from '../../components/routing/useRoute'
-import useRouteData from '../../components/routing/useRouteData'
 import useSearch from '../../components/routing/useSearch'
 import useViewWriteAbilities from '../../hooks/useViewWriteAbilities'
-import {MSectionRequest} from '../../models/graphRequestModels'
-import {MSectionResponse} from '../../models/graphResponseModels'
-import {JSONObject} from '../../utils/types'
+import {assert} from '../../utils/utils'
 import EntryPageTitle from './EntryPageTitle'
 import entryRoute from './entryRoute'
 import useEntryRouteData from './useEntryRouteData'
 
-const sortRawDataKeys = (a: string, b: string) => {
-  if (a.startsWith('m_') === b.startsWith('m_')) {
-    return a.localeCompare(b)
-  }
-  if (a.startsWith('m_')) {
-    return -1
-  }
-  return 1
-}
-
 const nonEditableSections = ['results', 'metadata']
 
 export default function EntrySection() {
   const {isLeaf, fullMatch} = useRoute()
-  const data = useRouteData<MSectionRequest, MSectionResponse>()
   const {parser_name} = useEntryRouteData()
   const {canWrite} = useViewWriteAbilities()
   const {isPage} = useSelect()
   const {preview} = useSearch(entryRoute)
 
-  const startIndex = useMemo(
-    () => fullMatch.findIndex((match) => match.route.path === 'archive'),
-    [fullMatch],
-  )
+  const startIndex = useMemo(() => {
+    const index = fullMatch.findIndex((match) => match.route.path === 'archive')
+    assert(index !== -1, 'not an archive route')
+    return index
+  }, [fullMatch])
   const sectionPath = useMemo(() => {
     if (!isLeaf) {
       return ''
@@ -70,19 +55,7 @@ export default function EntrySection() {
   return (
     <>
       <EntryPageTitle title={title} />
-      <Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
-        <Section path={sectionPath} editable={editable} />
-        <Card>
-          <CardHeader title='Raw data' />
-          <CardContent>
-            <JsonViewer
-              value={data as JSONObject}
-              defaultInspectDepth={1}
-              objectSortKeys={sortRawDataKeys}
-            />
-          </CardContent>
-        </Card>
-      </Box>
+      <Section path={sectionPath} editable={editable} />
     </>
   )
 }
diff --git a/src/pages/entry/entryRoute.tsx b/src/pages/entry/entryRoute.tsx
index 9bc78f78..35db6f60 100644
--- a/src/pages/entry/entryRoute.tsx
+++ b/src/pages/entry/entryRoute.tsx
@@ -146,7 +146,10 @@ const entryRoute: Route<EntryRequest, EntryResponse, EntrySearch> = {
   lazyComponent: async () => import('./EntryPage'),
   renderWithPathAsKey: true,
   breadcrumb: ({response}) => path.basename(response?.mainfile_path as string),
-  validateSearch: ({rawSearch}) => ({preview: rawSearch.preview === 'true'}),
+  validateSearch: ({rawSearch}) => ({
+    preview: rawSearch.preview === 'true',
+    layout: rawSearch.layout,
+  }),
   children: [
     {
       path: '',
-- 
GitLab


From b78a12677241a2af857e800fbb25d54cc7ec692d Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Mon, 24 Mar 2025 12:36:42 +0100
Subject: [PATCH 14/17] Fixed Reference and Select issues.

---
 README.md                                          | 3 +--
 src/components/navigation/Select.tsx               | 7 ++++---
 src/components/values/primitives/FileReference.tsx | 9 ++++++---
 src/components/values/primitives/Reference.tsx     | 7 ++++---
 4 files changed, 15 insertions(+), 11 deletions(-)

diff --git a/README.md b/README.md
index b45dc92d..edbcea40 100644
--- a/README.md
+++ b/README.md
@@ -90,8 +90,7 @@ in development.
 
    ```sh
    cd infra
-   nomad client upload --upload-name demo-schema --ignore-path-prefix data/demo-schema
-   nomad client upload --upload-name entry-data --ignore-path-prefix  data/entry-data
+   nomad client upload --upload-name demo-schema --ignore-path-prefix src/nomad_plugin_gui/example_uploads/ui_demonstration
    nomad client upload --upload-name upload-navigation data/upload-navigation.zip
    ```
 
diff --git a/src/components/navigation/Select.tsx b/src/components/navigation/Select.tsx
index f8e09d5d..61b5aac3 100644
--- a/src/components/navigation/Select.tsx
+++ b/src/components/navigation/Select.tsx
@@ -11,7 +11,7 @@ import {
 import {PropsWithChildren, useCallback, useState} from 'react'
 
 import {archiveRoute} from '../../pages/entry/entryRoute'
-import {filesRoute} from '../../pages/upload/uploadRoute'
+import {filesRoute, pathRoute} from '../../pages/upload/uploadRoute'
 import uploadsRoute from '../../pages/uploads/uploadsRoute'
 import {Section} from '../../utils/metainfo'
 import {assert} from '../../utils/utils'
@@ -237,7 +237,7 @@ export default function Select<S extends SelectOptions = SelectOptions>({
       setPath(location)
       let matchesEntity = false
       if (entity === 'file') {
-        matchesEntity = leafMatch.route.routeId === filesRoute.routeId
+        matchesEntity = leafMatch.route.routeId === pathRoute.routeId
         matchesEntity &&= leafMatch.response?.m_is === 'File'
         const {filePattern} = selectOptions as unknown as FileSelectOptions
         if (filePattern) {
@@ -260,7 +260,8 @@ export default function Select<S extends SelectOptions = SelectOptions>({
         }
       }
 
-      onChange?.(matchesEntity ? location : undefined)
+      const decodedLocation = decodeURIComponent(location)
+      onChange?.(matchesEntity ? decodedLocation : undefined)
     },
     [setPath, onChange, entity, selectOptions],
   )
diff --git a/src/components/values/primitives/FileReference.tsx b/src/components/values/primitives/FileReference.tsx
index efae8bdd..94305f4d 100644
--- a/src/components/values/primitives/FileReference.tsx
+++ b/src/components/values/primitives/FileReference.tsx
@@ -1,4 +1,5 @@
 import useEntryRouteData from '../../../pages/entry/useEntryRouteData'
+import {assert} from '../../../utils/utils'
 import {FileSelectOptions} from '../../navigation/useSelect'
 import Reference, {DynamicReferenceProps, ReferenceProps} from './Reference'
 
@@ -16,16 +17,18 @@ export default function FileReference({
   ...referenceProps
 }: FileReferenceProps) {
   const {upload_id, mainfile_path} = useEntryRouteData()
+  assert(mainfile_path, 'mainfile_path is required')
 
   const selectOptions = {
     entity: 'file',
     filePattern,
   } satisfies FileSelectOptions
 
-  const initialPath = `/uploads/${upload_id}/files${mainfile_path?.replace(
-    /\/[^/]+$/,
+  const directoryPath = mainfile_path.replace(/[^/]+$/, '').replace(/^\/?/, '')
+  const initialPath = `/uploads/${upload_id}/files/${directoryPath}`.replace(
+    /\/$/,
     '',
-  )}`
+  )
 
   return (
     <Reference
diff --git a/src/components/values/primitives/Reference.tsx b/src/components/values/primitives/Reference.tsx
index 544a03c1..4f9866a3 100644
--- a/src/components/values/primitives/Reference.tsx
+++ b/src/components/values/primitives/Reference.tsx
@@ -78,10 +78,11 @@ export default function Reference<S extends SelectOptions = SelectOptions>({
 
   const handleOpen = useCallback(() => {
     assert(value !== undefined, 'Open should only be possible with value.')
-    if (value.startsWith('/')) {
-      navigate({path: value})
+    const normalizedValue = value.replace(/^((\.|#)\/)?/, '')
+    if (normalizedValue.startsWith('/')) {
+      navigate({path: normalizedValue})
     } else {
-      navigate({path: `${initialPath}/${value}`})
+      navigate({path: `${initialPath}/${normalizedValue}`})
     }
   }, [value, navigate, initialPath])
 
-- 
GitLab


From 5ee42fbb749d08d362b8d356f4c2e551aa17cde3 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Mon, 24 Mar 2025 12:51:02 +0100
Subject: [PATCH 15/17] Revert using local keycloak to central keycloak.

---
 infra/nomad.yaml | 3 +--
 public/config.js | 2 +-
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/infra/nomad.yaml b/infra/nomad.yaml
index 48639927..d333579c 100644
--- a/infra/nomad.yaml
+++ b/infra/nomad.yaml
@@ -1,7 +1,6 @@
 keycloak:
-  server_url: 'http://keycloak:8080/'
+  server_url: 'https://nomad-lab.eu/fairdi/keycloak/auth/'
   realm_name: 'fairdi_nomad_test'
-  public_server_url: 'http://localhost:8080/'
 services:
   admin_user_id: 'c97facc2-92ec-4fa6-80cf-a08ed957255b'
 client:
diff --git a/public/config.js b/public/config.js
index 9d8981b0..6ed1474f 100644
--- a/public/config.js
+++ b/public/config.js
@@ -1 +1 @@
-window.nomadConfig = {"keycloak": {"realm_name": "fairdi_nomad_test", "client_id": "nomad_public", "public_server_url": "http://localhost:8080"}, "services": {"console_log_level": 30, "api_host": "localhost", "api_port": 8000, "api_base_path": "/fairdi/nomad/latest", "api_secret": "defaultApiSecret", "api_timeout": 600, "https": false, "https_upload": false, "admin_user_id": "c97facc2-92ec-4fa6-80cf-a08ed957255b", "encyclopedia_base": "https://nomad-lab.eu/prod/rae/encyclopedia/#", "optimade_enabled": true, "dcat_enabled": true, "h5grove_enabled": true, "upload_limit": 10, "force_raw_file_decoding": false, "max_entry_download": 50000, "unavailable_value": "unavailable", "app_token_max_expires_in": 2592000, "html_resource_http_max_age": 60, "image_resource_http_max_age": 2592000, "upload_members_group_search_enabled": false, "log_api_queries": true}, "ui": {"unit_systems": {"items": [{"label": "Custom", "units": {"angle": {"definition": "\u00b0", "locked": false}, "energy": {"definition": "eV", "locked": false}, "length": {"definition": "\u00c5", "locked": false}, "pressure": {"definition": "GPa", "locked": false}, "time": {"definition": "fs", "locked": false}, "dimensionless": {"definition": "dimensionless", "locked": false}, "mass": {"definition": "kg", "locked": false}, "current": {"definition": "A", "locked": false}, "temperature": {"definition": "K", "locked": false}, "luminosity": {"definition": "cd", "locked": false}, "luminous_flux": {"definition": "lm", "locked": false}, "substance": {"definition": "mol", "locked": false}, "information": {"definition": "bit", "locked": false}, "force": {"definition": "N", "locked": false}, "power": {"definition": "W", "locked": false}, "charge": {"definition": "C", "locked": false}, "resistance": {"definition": "\u03a9", "locked": false}, "conductance": {"definition": "S", "locked": false}, "inductance": {"definition": "H", "locked": false}, "magnetic_flux": {"definition": "Wb", "locked": false}, "magnetic_field": {"definition": "T", "locked": false}, "frequency": {"definition": "Hz", "locked": false}, "luminance": {"definition": "nit", "locked": false}, "illuminance": {"definition": "lx", "locked": false}, "electric_potential": {"definition": "V", "locked": false}, "capacitance": {"definition": "F", "locked": false}, "activity": {"definition": "kat", "locked": false}}, "id": "Custom"}, {"label": "International System of Units (SI)", "units": {"activity": {"definition": "kat", "locked": true}, "angle": {"definition": "rad", "locked": true}, "capacitance": {"definition": "F", "locked": true}, "charge": {"definition": "C", "locked": true}, "conductance": {"definition": "S", "locked": true}, "current": {"definition": "A", "locked": true}, "dimensionless": {"definition": "dimensionless", "locked": true}, "electric_potential": {"definition": "V", "locked": true}, "energy": {"definition": "J", "locked": true}, "force": {"definition": "N", "locked": true}, "frequency": {"definition": "Hz", "locked": true}, "illuminance": {"definition": "lx", "locked": true}, "inductance": {"definition": "H", "locked": true}, "information": {"definition": "bit", "locked": true}, "length": {"definition": "m", "locked": true}, "luminance": {"definition": "nit", "locked": true}, "luminosity": {"definition": "cd", "locked": true}, "luminous_flux": {"definition": "lm", "locked": true}, "magnetic_field": {"definition": "T", "locked": true}, "magnetic_flux": {"definition": "Wb", "locked": true}, "mass": {"definition": "kg", "locked": true}, "power": {"definition": "W", "locked": true}, "pressure": {"definition": "Pa", "locked": true}, "resistance": {"definition": "\u03a9", "locked": true}, "substance": {"definition": "mol", "locked": true}, "temperature": {"definition": "K", "locked": true}, "time": {"definition": "s", "locked": true}}, "id": "SI"}, {"label": "Hartree atomic units (AU)", "units": {"activity": {"definition": "kat", "locked": false}, "angle": {"definition": "rad", "locked": false}, "capacitance": {"definition": "F", "locked": false}, "charge": {"definition": "C", "locked": false}, "conductance": {"definition": "S", "locked": false}, "current": {"definition": "atomic_unit_of_current", "locked": true}, "dimensionless": {"definition": "dimensionless", "locked": true}, "electric_potential": {"definition": "V", "locked": false}, "energy": {"definition": "Ha", "locked": true}, "force": {"definition": "atomic_unit_of_force", "locked": true}, "frequency": {"definition": "Hz", "locked": false}, "illuminance": {"definition": "lx", "locked": false}, "inductance": {"definition": "H", "locked": false}, "information": {"definition": "bit", "locked": false}, "length": {"definition": "bohr", "locked": true}, "luminance": {"definition": "nit", "locked": false}, "luminosity": {"definition": "cd", "locked": false}, "luminous_flux": {"definition": "lm", "locked": false}, "magnetic_field": {"definition": "T", "locked": false}, "magnetic_flux": {"definition": "Wb", "locked": false}, "mass": {"definition": "m_e", "locked": true}, "power": {"definition": "W", "locked": false}, "pressure": {"definition": "atomic_unit_of_pressure", "locked": true}, "resistance": {"definition": "\u03a9", "locked": false}, "substance": {"definition": "mol", "locked": false}, "temperature": {"definition": "atomic_unit_of_temperature", "locked": true}, "time": {"definition": "atomic_unit_of_time", "locked": true}}, "id": "AU"}], "selected": "Custom"}}, "plugins": {"entry_points": {"items": [{"id": "apps/1_all/1_entries", "entry_point_type": "app", "app": {"label": "Entries", "path": "entries", "resource": "entries", "category": "All", "description": "Search entries across all domains", "readme": "This page allows you to search **entries** within NOMAD. Entries represent any individual data items that have been uploaded to NOMAD, no matter whether they come from theoretical calculations, experiments, lab notebooks or any other source of data. This allows you to perform cross-domain queries, but if you are interested in a specific subfield, you should see if a specific application exists for it in the explore menu to get more details.", "pagination": {"order_by": "upload_create_time", "order": "desc", "page_size": 20}, "columns": [{"search_quantity": "entry_name", "selected": true, "title": "Name", "align": "left"}, {"search_quantity": "results.material.chemical_formula_hill", "selected": true, "title": "Formula", "align": "left"}, {"search_quantity": "entry_type", "selected": true, "align": "left"}, {"search_quantity": "upload_create_time", "selected": true, "title": "Upload time", "align": "left"}, {"search_quantity": "authors", "selected": true, "align": "left"}, {"search_quantity": "upload_name", "selected": false, "align": "left"}, {"search_quantity": "upload_id", "selected": false, "align": "left"}, {"search_quantity": "results.method.method_name", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.program_name", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.dft.xc_functional_type", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.apw_cutoff", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.basis_set", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.k_line_density", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.native_tier", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.planewave_cutoff", "selected": false, "align": "left"}, {"search_quantity": "results.material.structural_type", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.crystal_system", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.space_group_symbol", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.space_group_number", "selected": false, "align": "left"}, {"search_quantity": "results.eln.lab_ids", "selected": false, "align": "left"}, {"search_quantity": "results.eln.sections", "selected": false, "align": "left"}, {"search_quantity": "results.eln.methods", "selected": false, "align": "left"}, {"search_quantity": "results.eln.tags", "selected": false, "align": "left"}, {"search_quantity": "results.eln.instruments", "selected": false, "align": "left"}, {"search_quantity": "mainfile", "selected": false, "align": "left"}, {"search_quantity": "comment", "selected": false, "align": "left"}, {"search_quantity": "references", "selected": false, "align": "left"}, {"search_quantity": "datasets", "selected": false, "align": "left"}, {"search_quantity": "published", "selected": false, "title": "Access", "align": "left"}], "rows": {"actions": {"enabled": true}, "details": {"enabled": true}, "selection": {"enabled": true}}, "menu": {"width": 12, "show_header": true, "title": "Filters", "type": "menu", "size": "sm", "indentation": 0, "items": [{"width": 12, "show_header": true, "title": "Material", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Elements / Formula", "type": "menu", "size": "xxl", "indentation": 1, "items": [{"type": "periodic_table", "search_quantity": "results.material.elements", "scale": "linear", "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_hill", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_iupac", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_reduced", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_anonymous", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.material.n_elements", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Structure / Symmetry", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.material.structural_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.bravais_lattice", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.crystal_system", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.space_group_symbol", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.structure_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.strukturbericht_designation", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.point_group", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.hall_symbol", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.prototype_aflow_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Method", "type": "menu", "size": "md", "items": [{"search_quantity": "results.method.simulation.program_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.program_version", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Precision", "type": "menu", "size": "md", "indentation": 1, "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.k_line_density", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.precision.native_tier", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.precision.basis_set", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.planewave_cutoff", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.apw_cutoff", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "DFT", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"DFT": {"label": "Search DFT entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.dft.xc_functional_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.xc_functional_names", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dft.exact_exchange_mixing_factor", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dft.hubbard_kanamori_model.u_effective", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.core_electron_treatment", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.relativity_method", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "TB", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"TB": {"label": "Search TB entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.tb.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.tb.localization_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "GW", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"GW": {"label": "Search GW entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.gw.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.gw.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.gw.basis_set_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "BSE", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"BSE": {"label": "Search BSE entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.bse.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.solver", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.basis_set_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.gw_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "DMFT", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"DMFT": {"label": "Search DMFT entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.dmft.impurity_solver_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.inverse_temperature", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dmft.magnetic_state", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.u", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.jh", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dmft.analytical_continuation", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "EELS", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"EELS": {"label": "Search EELS entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.spectroscopic.spectra.provenance.eels", "items": [{"search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.detector_type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.resolution", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.min_energy", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.max_energy", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Workflow", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Molecular dynamics", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.workflow_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"MolecularDynamics": {"label": "Search molecular dynamics entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.thermodynamic.trajectory", "items": [{"search_quantity": "results.properties.thermodynamic.trajectory.available_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 4, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.ensemble_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 2, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.time_step", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Geometry Optimization", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.properties.available_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"geometry_optimization": {"label": "Search geometry optimization entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.geometry_optimization", "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_energy_difference", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_force_maximum", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_displacement_maximum", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Properties", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Electronic", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "electronic_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.band_structure_electronic.band_gap", "items": [{"search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 2, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.band_structure_electronic", "items": [{"search_quantity": "results.properties.electronic.band_structure_electronic.spin_polarized", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.dos_electronic", "items": [{"search_quantity": "results.properties.electronic.dos_electronic.spin_polarized", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Vibrational", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "vibrational_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Mechanical", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "mechanical_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.bulk_modulus", "items": [{"search_quantity": "results.properties.mechanical.bulk_modulus.type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.mechanical.bulk_modulus.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.shear_modulus", "items": [{"search_quantity": "results.properties.mechanical.shear_modulus.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.mechanical.shear_modulus.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.energy_volume_curve", "items": [{"search_quantity": "results.properties.mechanical.energy_volume_curve.type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Use Cases", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Solar Cells", "type": "menu", "size": "md", "indentation": 1, "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.efficiency", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.fill_factor", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.open_circuit_voltage", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.short_circuit_current_density", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.illumination_intensity", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.device_area", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.device_architecture", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.device_stack", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.absorber", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.absorber_fabrication", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.electron_transport_layer", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.hole_transport_layer", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.substrate", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.back_contact", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Heterogeneous Catalysis", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.properties.catalytic.reaction.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.catalytic.reaction.reactants", "items": [{"search_quantity": "results.properties.catalytic.reaction.reactants.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.reactants.conversion", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.reactants.mole_fraction_in", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.reactants.mole_fraction_out", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.catalytic.reaction.products", "items": [{"search_quantity": "results.properties.catalytic.reaction.products.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.products.selectivity", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.products.mole_fraction_out", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.reaction_conditions.temperature", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.catalytic.catalyst", "items": [{"search_quantity": "results.properties.catalytic.catalyst.catalyst_type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.catalytic.catalyst.support", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.catalytic.catalyst.preparation_method", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.catalytic.catalyst.catalyst_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.catalytic.catalyst.characterization_methods", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.catalyst.surface_area", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Author / Origin / Dataset", "type": "menu", "size": "lg", "items": [{"search_quantity": "authors.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "upload_create_time", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "external_db", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.dataset_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.doi", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Visibility / IDs / Schema", "type": "menu", "size": "md", "items": [{"width": 12, "show_header": true, "type": "visibility"}, {"search_quantity": "entry_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "upload_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "upload_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.material_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.dataset_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "definitions"}]}, {"width": 12, "show_header": true, "title": "Optimade", "type": "menu", "size": "lg", "items": [{"width": 12, "show_header": true, "type": "optimade"}]}]}, "search_quantities": {"exclude": ["mainfile", "entry_name", "combine"]}, "search_syntaxes": {"exclude": ["free_text"]}}}, {"id": "apps/2_theory/1_calculations", "entry_point_type": "app", "app": {"label": "Calculations", "path": "calculations", "resource": "entries", "category": "Theory", "description": "Search calculations", "readme": "This page allows you to search **calculations** within NOMAD. Calculations typically come from a specific simulation software that uses an approximate model to investigate and report different physical properties.", "pagination": {"order_by": "upload_create_time", "order": "desc", "page_size": 20}, "columns": [{"search_quantity": "results.material.chemical_formula_hill", "selected": true, "title": "Formula", "align": "left"}, {"search_quantity": "results.method.simulation.program_name", "selected": true, "align": "left"}, {"search_quantity": "results.method.method_name", "selected": true, "align": "left"}, {"search_quantity": "results.method.simulation.dft.xc_functional_type", "selected": true, "align": "left"}, {"search_quantity": "upload_create_time", "selected": true, "title": "Upload time", "align": "left"}, {"search_quantity": "authors", "selected": true, "align": "left"}, {"search_quantity": "results.method.simulation.precision.apw_cutoff", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.basis_set", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.k_line_density", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.native_tier", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.planewave_cutoff", "selected": false, "align": "left"}, {"search_quantity": "results.material.structural_type", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.crystal_system", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.space_group_symbol", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.space_group_number", "selected": false, "align": "left"}, {"search_quantity": "entry_name", "selected": false, "title": "Name", "align": "left"}, {"search_quantity": "mainfile", "selected": false, "align": "left"}, {"search_quantity": "comment", "selected": false, "align": "left"}, {"search_quantity": "references", "selected": false, "align": "left"}, {"search_quantity": "datasets", "selected": false, "align": "left"}, {"search_quantity": "published", "selected": false, "title": "Access", "align": "left"}], "rows": {"actions": {"enabled": true}, "details": {"enabled": true}, "selection": {"enabled": true}}, "menu": {"width": 12, "show_header": true, "title": "Filters", "type": "menu", "size": "sm", "indentation": 0, "items": [{"width": 12, "show_header": true, "title": "Material", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Elements / Formula", "type": "menu", "size": "xxl", "indentation": 1, "items": [{"type": "periodic_table", "search_quantity": "results.material.elements", "scale": "linear", "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_hill", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_iupac", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_reduced", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_anonymous", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.material.n_elements", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Structure / Symmetry", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.material.structural_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.bravais_lattice", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.crystal_system", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.space_group_symbol", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.structure_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.strukturbericht_designation", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.point_group", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.hall_symbol", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.prototype_aflow_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Method", "type": "menu", "size": "md", "items": [{"search_quantity": "results.method.simulation.program_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.program_version", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Precision", "type": "menu", "size": "md", "indentation": 1, "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.k_line_density", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.precision.native_tier", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.precision.basis_set", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.planewave_cutoff", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.apw_cutoff", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "DFT", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"DFT": {"label": "Search DFT entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.dft.xc_functional_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.xc_functional_names", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dft.exact_exchange_mixing_factor", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dft.hubbard_kanamori_model.u_effective", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.core_electron_treatment", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.relativity_method", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "TB", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"TB": {"label": "Search TB entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.tb.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.tb.localization_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "GW", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"GW": {"label": "Search GW entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.gw.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.gw.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.gw.basis_set_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "BSE", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"BSE": {"label": "Search BSE entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.bse.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.solver", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.basis_set_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.gw_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "DMFT", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"DMFT": {"label": "Search DMFT entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.dmft.impurity_solver_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.inverse_temperature", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dmft.magnetic_state", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.u", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.jh", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dmft.analytical_continuation", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Workflow", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Molecular dynamics", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.workflow_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"MolecularDynamics": {"label": "Search molecular dynamics entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.thermodynamic.trajectory", "items": [{"search_quantity": "results.properties.thermodynamic.trajectory.available_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 4, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.ensemble_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 2, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.time_step", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Geometry Optimization", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.properties.available_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"geometry_optimization": {"label": "Search geometry optimization entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.geometry_optimization", "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_energy_difference", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_force_maximum", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_displacement_maximum", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Properties", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Electronic", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "electronic_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.band_structure_electronic.band_gap", "items": [{"search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 2, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.band_structure_electronic", "items": [{"search_quantity": "results.properties.electronic.band_structure_electronic.spin_polarized", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.dos_electronic", "items": [{"search_quantity": "results.properties.electronic.dos_electronic.spin_polarized", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Vibrational", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "vibrational_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Mechanical", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "mechanical_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.bulk_modulus", "items": [{"search_quantity": "results.properties.mechanical.bulk_modulus.type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.mechanical.bulk_modulus.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.shear_modulus", "items": [{"search_quantity": "results.properties.mechanical.shear_modulus.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.mechanical.shear_modulus.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.energy_volume_curve", "items": [{"search_quantity": "results.properties.mechanical.energy_volume_curve.type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Author / Origin / Dataset", "type": "menu", "size": "lg", "items": [{"search_quantity": "authors.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "upload_create_time", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "external_db", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.dataset_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.doi", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Visibility / IDs / Schema", "type": "menu", "size": "md", "items": [{"width": 12, "show_header": true, "type": "visibility"}, {"search_quantity": "entry_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "upload_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "upload_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.material_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.dataset_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "definitions"}]}, {"width": 12, "show_header": true, "title": "Optimade", "type": "menu", "size": "lg", "items": [{"width": 12, "show_header": true, "type": "optimade"}]}]}, "search_quantities": {"exclude": ["mainfile", "entry_name", "combine"]}, "dashboard": {"widgets": [{"type": "periodictable", "search_quantity": "results.material.elements", "scale": "linear", "layout": {"lg": {"h": 11, "w": 14, "x": 0, "y": 0, "minH": 3, "minW": 3}, "md": {"h": 8, "w": 12, "x": 0, "y": 0, "minH": 3, "minW": 3}, "sm": {"h": 8, "w": 12, "x": 0, "y": 0, "minH": 3, "minW": 3}, "xl": {"h": 11, "w": 14, "x": 0, "y": 0, "minH": 3, "minW": 3}, "xxl": {"h": 9, "w": 13, "x": 0, "y": 0, "minH": 3, "minW": 3}}}, {"search_quantity": "results.material.symmetry.space_group_symbol", "type": "terms", "scale": "linear", "show_input": true, "layout": {"lg": {"h": 5, "w": 5, "x": 19, "y": 6, "minH": 3, "minW": 3}, "md": {"h": 6, "w": 6, "x": 12, "y": 8, "minH": 3, "minW": 3}, "sm": {"h": 5, "w": 6, "x": 6, "y": 13, "minH": 3, "minW": 3}, "xl": {"h": 6, "w": 6, "x": 24, "y": 5, "minH": 3, "minW": 3}, "xxl": {"h": 9, "w": 6, "x": 30, "y": 0, "minH": 3, "minW": 3}}}, {"search_quantity": "results.material.structural_type", "type": "terms", "scale": "log", "show_input": false, "layout": {"lg": {"h": 6, "w": 5, "x": 19, "y": 0, "minH": 3, "minW": 3}, "md": {"h": 6, "w": 6, "x": 0, "y": 8, "minH": 3, "minW": 3}, "sm": {"h": 5, "w": 6, "x": 6, "y": 8, "minH": 3, "minW": 3}, "xl": {"h": 11, "w": 5, "x": 19, "y": 0, "minH": 3, "minW": 3}, "xxl": {"h": 9, "w": 6, "x": 19, "y": 0, "minH": 3, "minW": 3}}}, {"search_quantity": "results.method.simulation.program_name", "type": "terms", "scale": "log", "show_input": true, "layout": {"lg": {"h": 6, "w": 5, "x": 14, "y": 0, "minH": 3, "minW": 3}, "md": {"h": 8, "w": 6, "x": 12, "y": 0, "minH": 3, "minW": 3}, "sm": {"h": 5, "w": 6, "x": 0, "y": 8, "minH": 3, "minW": 3}, "xl": {"h": 11, "w": 5, "x": 14, "y": 0, "minH": 3, "minW": 3}, "xxl": {"h": 9, "w": 6, "x": 13, "y": 0, "minH": 3, "minW": 3}}}, {"search_quantity": "results.material.symmetry.crystal_system", "type": "terms", "scale": "linear", "show_input": false, "layout": {"lg": {"h": 5, "w": 5, "x": 14, "y": 6, "minH": 3, "minW": 3}, "md": {"h": 6, "w": 6, "x": 6, "y": 8, "minH": 3, "minW": 3}, "sm": {"h": 5, "w": 6, "x": 0, "y": 13, "minH": 3, "minW": 3}, "xl": {"h": 5, "w": 6, "x": 24, "y": 0, "minH": 3, "minW": 3}, "xxl": {"h": 9, "w": 5, "x": 25, "y": 0, "minH": 3, "minW": 3}}}]}, "filters_locked": {"quantities": "results.method.simulation.program_name"}, "search_syntaxes": {"exclude": ["free_text"]}}}, {"id": "apps/2_theory/2_materials", "entry_point_type": "app", "app": {"label": "Materials", "path": "materials", "resource": "materials", "category": "Theory", "description": "Search materials that are identified from calculations", "readme": "This page allows you to search **materials** within NOMAD. NOMAD can often automatically detect the material from individual calculations that contain the full atomistic structure and can then group the data by using these detected materials. This allows you to search individual materials which have properties that are aggregated from several entries. Following the link for a specific material will take you to the corresponding [NOMAD Encyclopedia](https://nomad-lab.eu/prod/rae/encyclopedia/#/search) page for that material. NOMAD Encyclopedia is a service that is specifically oriented towards materials property exploration.\nNotice that by default the properties that you search can be combined from several different entries. If instead you wish to search for a material with an individual entry fullfilling your search criteria, uncheck the **combine results from several entries**-checkbox.", "pagination": {"order_by": "chemical_formula_hill", "order": "asc", "page_size": 20}, "columns": [{"search_quantity": "chemical_formula_hill", "selected": true, "title": "Formula", "align": "left"}, {"search_quantity": "structural_type", "selected": true, "align": "left"}, {"search_quantity": "symmetry.structure_name", "selected": true, "align": "left"}, {"search_quantity": "symmetry.space_group_number", "selected": true, "align": "left"}, {"search_quantity": "symmetry.crystal_system", "selected": true, "align": "left"}, {"search_quantity": "symmetry.space_group_symbol", "selected": false, "align": "left"}, {"search_quantity": "material_id", "selected": false, "align": "left"}], "rows": {"actions": {"enabled": true}, "details": {"enabled": false}, "selection": {"enabled": false}}, "menu": {"width": 12, "show_header": true, "title": "Filters", "type": "menu", "size": "sm", "indentation": 0, "items": [{"width": 12, "show_header": true, "title": "Material", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Elements / Formula", "type": "menu", "size": "xxl", "indentation": 1, "items": [{"type": "periodic_table", "search_quantity": "results.material.elements", "scale": "linear", "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_hill", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_iupac", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_reduced", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_anonymous", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.material.n_elements", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Structure / Symmetry", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.material.structural_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.bravais_lattice", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.crystal_system", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.space_group_symbol", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.structure_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.strukturbericht_designation", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.point_group", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.hall_symbol", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.prototype_aflow_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Method", "type": "menu", "size": "md", "items": [{"search_quantity": "results.method.simulation.program_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.program_version", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "DFT", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"DFT": {"label": "Search DFT entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.dft.xc_functional_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.xc_functional_names", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dft.exact_exchange_mixing_factor", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dft.hubbard_kanamori_model.u_effective", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.core_electron_treatment", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.relativity_method", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "TB", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"TB": {"label": "Search TB entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.tb.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.tb.localization_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "GW", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"GW": {"label": "Search GW entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.gw.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.gw.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.gw.basis_set_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "BSE", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"BSE": {"label": "Search BSE entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.bse.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.solver", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.basis_set_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.gw_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "DMFT", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"DMFT": {"label": "Search DMFT entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.dmft.impurity_solver_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.inverse_temperature", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dmft.magnetic_state", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.u", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.jh", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dmft.analytical_continuation", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Workflow", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Molecular dynamics", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.workflow_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"MolecularDynamics": {"label": "Search molecular dynamics entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.thermodynamic.trajectory", "items": [{"search_quantity": "results.properties.thermodynamic.trajectory.available_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 4, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.ensemble_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 2, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.time_step", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Geometry Optimization", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.properties.available_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"geometry_optimization": {"label": "Search geometry optimization entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.geometry_optimization", "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_energy_difference", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_force_maximum", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_displacement_maximum", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Properties", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Electronic", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "electronic_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.band_structure_electronic.band_gap", "items": [{"search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 2, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.band_structure_electronic", "items": [{"search_quantity": "results.properties.electronic.band_structure_electronic.spin_polarized", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.dos_electronic", "items": [{"search_quantity": "results.properties.electronic.dos_electronic.spin_polarized", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Vibrational", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "vibrational_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Mechanical", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "mechanical_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.bulk_modulus", "items": [{"search_quantity": "results.properties.mechanical.bulk_modulus.type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.mechanical.bulk_modulus.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.shear_modulus", "items": [{"search_quantity": "results.properties.mechanical.shear_modulus.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.mechanical.shear_modulus.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.energy_volume_curve", "items": [{"search_quantity": "results.properties.mechanical.energy_volume_curve.type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Author / Origin / Dataset", "type": "menu", "size": "lg", "items": [{"search_quantity": "authors.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "upload_create_time", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "external_db", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.dataset_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.doi", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Visibility / IDs / Schema", "type": "menu", "size": "md", "items": [{"width": 12, "show_header": true, "type": "visibility"}, {"search_quantity": "entry_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "upload_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "upload_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.material_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.dataset_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "definitions"}]}, {"width": 12, "show_header": true, "title": "Optimade", "type": "menu", "size": "lg", "items": [{"width": 12, "show_header": true, "type": "optimade"}]}, {"search_quantity": "combine", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"True": {"label": "Combine results from several entries", "description": "If selected, your filters may be matched from several entries that contain the same material. When unchecked, the material has to have a single entry that matches all your filters."}}, "n_columns": 1, "sort_static": true, "show_statistics": false}]}, "search_quantities": {"exclude": ["mainfile", "entry_name"]}, "search_syntaxes": {"exclude": ["free_text"]}}}]}}};
+window.nomadConfig = {"keycloak": {"realm_name": "fairdi_nomad_test", "client_id": "nomad_public", "public_server_url": "https://nomad-lab.eu/fairdi/keycloak/auth/"}, "services": {"api_host": "localhost", "api_port": 8000, "api_base_path": "/fairdi/nomad/latest", "api_secret": "defaultApiSecret", "api_timeout": 600, "https": false, "https_upload": false, "admin_user_id": "c97facc2-92ec-4fa6-80cf-a08ed957255b", "encyclopedia_base": "https://nomad-lab.eu/prod/rae/encyclopedia/#", "optimade_enabled": true, "dcat_enabled": true, "h5grove_enabled": true, "console_log_level": 30, "upload_limit": 10, "force_raw_file_decoding": false, "max_entry_download": 50000, "unavailable_value": "unavailable", "app_token_max_expires_in": 2592000, "html_resource_http_max_age": 60, "image_resource_http_max_age": 2592000, "upload_members_group_search_enabled": false, "log_api_queries": true}, "ui": {"unit_systems": {"items": [{"label": "Custom", "units": {"angle": {"definition": "\u00b0", "locked": false}, "energy": {"definition": "eV", "locked": false}, "length": {"definition": "\u00c5", "locked": false}, "pressure": {"definition": "GPa", "locked": false}, "time": {"definition": "fs", "locked": false}, "dimensionless": {"definition": "dimensionless", "locked": false}, "mass": {"definition": "kg", "locked": false}, "current": {"definition": "A", "locked": false}, "temperature": {"definition": "K", "locked": false}, "luminosity": {"definition": "cd", "locked": false}, "luminous_flux": {"definition": "lm", "locked": false}, "substance": {"definition": "mol", "locked": false}, "information": {"definition": "bit", "locked": false}, "force": {"definition": "N", "locked": false}, "power": {"definition": "W", "locked": false}, "charge": {"definition": "C", "locked": false}, "resistance": {"definition": "\u03a9", "locked": false}, "conductance": {"definition": "S", "locked": false}, "inductance": {"definition": "H", "locked": false}, "magnetic_flux": {"definition": "Wb", "locked": false}, "magnetic_field": {"definition": "T", "locked": false}, "frequency": {"definition": "Hz", "locked": false}, "luminance": {"definition": "nit", "locked": false}, "illuminance": {"definition": "lx", "locked": false}, "electric_potential": {"definition": "V", "locked": false}, "capacitance": {"definition": "F", "locked": false}, "activity": {"definition": "kat", "locked": false}}, "id": "Custom"}, {"label": "International System of Units (SI)", "units": {"activity": {"definition": "kat", "locked": true}, "angle": {"definition": "rad", "locked": true}, "capacitance": {"definition": "F", "locked": true}, "charge": {"definition": "C", "locked": true}, "conductance": {"definition": "S", "locked": true}, "current": {"definition": "A", "locked": true}, "dimensionless": {"definition": "dimensionless", "locked": true}, "electric_potential": {"definition": "V", "locked": true}, "energy": {"definition": "J", "locked": true}, "force": {"definition": "N", "locked": true}, "frequency": {"definition": "Hz", "locked": true}, "illuminance": {"definition": "lx", "locked": true}, "inductance": {"definition": "H", "locked": true}, "information": {"definition": "bit", "locked": true}, "length": {"definition": "m", "locked": true}, "luminance": {"definition": "nit", "locked": true}, "luminosity": {"definition": "cd", "locked": true}, "luminous_flux": {"definition": "lm", "locked": true}, "magnetic_field": {"definition": "T", "locked": true}, "magnetic_flux": {"definition": "Wb", "locked": true}, "mass": {"definition": "kg", "locked": true}, "power": {"definition": "W", "locked": true}, "pressure": {"definition": "Pa", "locked": true}, "resistance": {"definition": "\u03a9", "locked": true}, "substance": {"definition": "mol", "locked": true}, "temperature": {"definition": "K", "locked": true}, "time": {"definition": "s", "locked": true}}, "id": "SI"}, {"label": "Hartree atomic units (AU)", "units": {"activity": {"definition": "kat", "locked": false}, "angle": {"definition": "rad", "locked": false}, "capacitance": {"definition": "F", "locked": false}, "charge": {"definition": "C", "locked": false}, "conductance": {"definition": "S", "locked": false}, "current": {"definition": "atomic_unit_of_current", "locked": true}, "dimensionless": {"definition": "dimensionless", "locked": true}, "electric_potential": {"definition": "V", "locked": false}, "energy": {"definition": "Ha", "locked": true}, "force": {"definition": "atomic_unit_of_force", "locked": true}, "frequency": {"definition": "Hz", "locked": false}, "illuminance": {"definition": "lx", "locked": false}, "inductance": {"definition": "H", "locked": false}, "information": {"definition": "bit", "locked": false}, "length": {"definition": "bohr", "locked": true}, "luminance": {"definition": "nit", "locked": false}, "luminosity": {"definition": "cd", "locked": false}, "luminous_flux": {"definition": "lm", "locked": false}, "magnetic_field": {"definition": "T", "locked": false}, "magnetic_flux": {"definition": "Wb", "locked": false}, "mass": {"definition": "m_e", "locked": true}, "power": {"definition": "W", "locked": false}, "pressure": {"definition": "atomic_unit_of_pressure", "locked": true}, "resistance": {"definition": "\u03a9", "locked": false}, "substance": {"definition": "mol", "locked": false}, "temperature": {"definition": "atomic_unit_of_temperature", "locked": true}, "time": {"definition": "atomic_unit_of_time", "locked": true}}, "id": "AU"}], "selected": "Custom"}}, "plugins": {"entry_points": {"items": [{"id": "apps/1_all/1_entries", "entry_point_type": "app", "app": {"label": "Entries", "path": "entries", "resource": "entries", "category": "All", "description": "Search entries across all domains", "readme": "This page allows you to search **entries** within NOMAD. Entries represent any individual data items that have been uploaded to NOMAD, no matter whether they come from theoretical calculations, experiments, lab notebooks or any other source of data. This allows you to perform cross-domain queries, but if you are interested in a specific subfield, you should see if a specific application exists for it in the explore menu to get more details.", "pagination": {"order_by": "upload_create_time", "order": "desc", "page_size": 20}, "columns": [{"search_quantity": "entry_name", "selected": true, "title": "Name", "align": "left"}, {"search_quantity": "results.material.chemical_formula_hill", "selected": true, "title": "Formula", "align": "left"}, {"search_quantity": "entry_type", "selected": true, "align": "left"}, {"search_quantity": "upload_create_time", "selected": true, "title": "Upload time", "align": "left"}, {"search_quantity": "authors", "selected": true, "align": "left"}, {"search_quantity": "upload_name", "selected": false, "align": "left"}, {"search_quantity": "upload_id", "selected": false, "align": "left"}, {"search_quantity": "results.method.method_name", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.program_name", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.dft.xc_functional_type", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.apw_cutoff", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.basis_set", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.k_line_density", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.native_tier", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.planewave_cutoff", "selected": false, "align": "left"}, {"search_quantity": "results.material.structural_type", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.crystal_system", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.space_group_symbol", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.space_group_number", "selected": false, "align": "left"}, {"search_quantity": "results.eln.lab_ids", "selected": false, "align": "left"}, {"search_quantity": "results.eln.sections", "selected": false, "align": "left"}, {"search_quantity": "results.eln.methods", "selected": false, "align": "left"}, {"search_quantity": "results.eln.tags", "selected": false, "align": "left"}, {"search_quantity": "results.eln.instruments", "selected": false, "align": "left"}, {"search_quantity": "mainfile", "selected": false, "align": "left"}, {"search_quantity": "comment", "selected": false, "align": "left"}, {"search_quantity": "references", "selected": false, "align": "left"}, {"search_quantity": "datasets", "selected": false, "align": "left"}, {"search_quantity": "published", "selected": false, "title": "Access", "align": "left"}], "rows": {"actions": {"enabled": true, "items": []}, "details": {"enabled": true}, "selection": {"enabled": true}}, "menu": {"width": 12, "show_header": true, "title": "Filters", "type": "menu", "size": "sm", "indentation": 0, "items": [{"width": 12, "show_header": true, "title": "Material", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Elements / Formula", "type": "menu", "size": "xxl", "indentation": 1, "items": [{"type": "periodic_table", "search_quantity": "results.material.elements", "scale": "linear", "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_hill", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_iupac", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_reduced", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_anonymous", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.material.n_elements", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Structure / Symmetry", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.material.structural_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.bravais_lattice", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.crystal_system", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.space_group_symbol", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.structure_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.strukturbericht_designation", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.point_group", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.hall_symbol", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.prototype_aflow_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Method", "type": "menu", "size": "md", "items": [{"search_quantity": "results.method.simulation.program_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.program_version", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Precision", "type": "menu", "size": "md", "indentation": 1, "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.k_line_density", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.precision.native_tier", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.precision.basis_set", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.planewave_cutoff", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.apw_cutoff", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "DFT", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"DFT": {"label": "Search DFT entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.dft.xc_functional_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.xc_functional_names", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dft.exact_exchange_mixing_factor", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dft.hubbard_kanamori_model.u_effective", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.core_electron_treatment", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.relativity_method", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "TB", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"TB": {"label": "Search TB entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.tb.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.tb.localization_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "GW", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"GW": {"label": "Search GW entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.gw.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.gw.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.gw.basis_set_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "BSE", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"BSE": {"label": "Search BSE entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.bse.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.solver", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.basis_set_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.gw_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "DMFT", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"DMFT": {"label": "Search DMFT entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.dmft.impurity_solver_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.inverse_temperature", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dmft.magnetic_state", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.u", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.jh", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dmft.analytical_continuation", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "EELS", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"EELS": {"label": "Search EELS entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.spectroscopic.spectra.provenance.eels", "items": [{"search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.detector_type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.resolution", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.min_energy", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.spectroscopic.spectra.provenance.eels.max_energy", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Workflow", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Molecular dynamics", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.workflow_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"MolecularDynamics": {"label": "Search molecular dynamics entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.thermodynamic.trajectory", "items": [{"search_quantity": "results.properties.thermodynamic.trajectory.available_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 4, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.ensemble_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 2, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.time_step", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Geometry Optimization", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.properties.available_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"geometry_optimization": {"label": "Search geometry optimization entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.geometry_optimization", "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_energy_difference", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_force_maximum", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_displacement_maximum", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Properties", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Electronic", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "electronic_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.band_structure_electronic.band_gap", "items": [{"search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 2, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.band_structure_electronic", "items": [{"search_quantity": "results.properties.electronic.band_structure_electronic.spin_polarized", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.dos_electronic", "items": [{"search_quantity": "results.properties.electronic.dos_electronic.spin_polarized", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Vibrational", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "vibrational_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Mechanical", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "mechanical_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.bulk_modulus", "items": [{"search_quantity": "results.properties.mechanical.bulk_modulus.type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.mechanical.bulk_modulus.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.shear_modulus", "items": [{"search_quantity": "results.properties.mechanical.shear_modulus.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.mechanical.shear_modulus.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.energy_volume_curve", "items": [{"search_quantity": "results.properties.mechanical.energy_volume_curve.type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Use Cases", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Solar Cells", "type": "menu", "size": "md", "indentation": 1, "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.efficiency", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.fill_factor", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.open_circuit_voltage", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.short_circuit_current_density", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.illumination_intensity", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.optoelectronic.solar_cell.device_area", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.device_architecture", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.device_stack", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.absorber", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.absorber_fabrication", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.electron_transport_layer", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.hole_transport_layer", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.substrate", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.optoelectronic.solar_cell.back_contact", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Heterogeneous Catalysis", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.properties.catalytic.reaction.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.catalytic.reaction.reactants", "items": [{"search_quantity": "results.properties.catalytic.reaction.reactants.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.reactants.conversion", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.reactants.mole_fraction_in", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.reactants.mole_fraction_out", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.catalytic.reaction.products", "items": [{"search_quantity": "results.properties.catalytic.reaction.products.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.products.selectivity", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.products.mole_fraction_out", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.reaction.reaction_conditions.temperature", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.catalytic.catalyst", "items": [{"search_quantity": "results.properties.catalytic.catalyst.catalyst_type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.catalytic.catalyst.support", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.catalytic.catalyst.preparation_method", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.catalytic.catalyst.catalyst_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.catalytic.catalyst.characterization_methods", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.catalytic.catalyst.surface_area", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Author / Origin / Dataset", "type": "menu", "size": "lg", "items": [{"search_quantity": "authors.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "upload_create_time", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "external_db", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.dataset_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.doi", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Visibility / IDs / Schema", "type": "menu", "size": "md", "items": [{"width": 12, "show_header": true, "type": "visibility"}, {"search_quantity": "entry_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "upload_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "upload_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.material_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.dataset_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "definitions"}]}, {"width": 12, "show_header": true, "title": "Optimade", "type": "menu", "size": "lg", "items": [{"width": 12, "show_header": true, "type": "optimade"}]}]}, "search_quantities": {"exclude": ["mainfile", "entry_name", "combine"]}, "search_syntaxes": {"exclude": ["free_text"]}}}, {"id": "apps/2_theory/1_calculations", "entry_point_type": "app", "app": {"label": "Calculations", "path": "calculations", "resource": "entries", "category": "Theory", "description": "Search calculations", "readme": "This page allows you to search **calculations** within NOMAD. Calculations typically come from a specific simulation software that uses an approximate model to investigate and report different physical properties.", "pagination": {"order_by": "upload_create_time", "order": "desc", "page_size": 20}, "columns": [{"search_quantity": "results.material.chemical_formula_hill", "selected": true, "title": "Formula", "align": "left"}, {"search_quantity": "results.method.simulation.program_name", "selected": true, "align": "left"}, {"search_quantity": "results.method.method_name", "selected": true, "align": "left"}, {"search_quantity": "results.method.simulation.dft.xc_functional_type", "selected": true, "align": "left"}, {"search_quantity": "upload_create_time", "selected": true, "title": "Upload time", "align": "left"}, {"search_quantity": "authors", "selected": true, "align": "left"}, {"search_quantity": "results.method.simulation.precision.apw_cutoff", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.basis_set", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.k_line_density", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.native_tier", "selected": false, "align": "left"}, {"search_quantity": "results.method.simulation.precision.planewave_cutoff", "selected": false, "align": "left"}, {"search_quantity": "results.material.structural_type", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.crystal_system", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.space_group_symbol", "selected": false, "align": "left"}, {"search_quantity": "results.material.symmetry.space_group_number", "selected": false, "align": "left"}, {"search_quantity": "entry_name", "selected": false, "title": "Name", "align": "left"}, {"search_quantity": "mainfile", "selected": false, "align": "left"}, {"search_quantity": "comment", "selected": false, "align": "left"}, {"search_quantity": "references", "selected": false, "align": "left"}, {"search_quantity": "datasets", "selected": false, "align": "left"}, {"search_quantity": "published", "selected": false, "title": "Access", "align": "left"}], "rows": {"actions": {"enabled": true, "items": []}, "details": {"enabled": true}, "selection": {"enabled": true}}, "menu": {"width": 12, "show_header": true, "title": "Filters", "type": "menu", "size": "sm", "indentation": 0, "items": [{"width": 12, "show_header": true, "title": "Material", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Elements / Formula", "type": "menu", "size": "xxl", "indentation": 1, "items": [{"type": "periodic_table", "search_quantity": "results.material.elements", "scale": "linear", "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_hill", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_iupac", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_reduced", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.chemical_formula_anonymous", "type": "terms", "scale": "linear", "show_input": true, "width": 6, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.material.n_elements", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Structure / Symmetry", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.material.structural_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.bravais_lattice", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.crystal_system", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.space_group_symbol", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.structure_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.strukturbericht_designation", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.point_group", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.hall_symbol", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.symmetry.prototype_aflow_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Method", "type": "menu", "size": "md", "items": [{"search_quantity": "results.method.simulation.program_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.program_version", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Precision", "type": "menu", "size": "md", "indentation": 1, "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.k_line_density", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.precision.native_tier", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.precision.basis_set", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.planewave_cutoff", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.precision.apw_cutoff", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "DFT", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"DFT": {"label": "Search DFT entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.dft.xc_functional_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.xc_functional_names", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dft.exact_exchange_mixing_factor", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dft.hubbard_kanamori_model.u_effective", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.core_electron_treatment", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dft.relativity_method", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "TB", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"TB": {"label": "Search TB entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.tb.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.tb.localization_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "GW", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"GW": {"label": "Search GW entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.gw.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.gw.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.gw.basis_set_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "BSE", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"BSE": {"label": "Search BSE entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.bse.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.solver", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.starting_point_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.basis_set_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.bse.gw_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "DMFT", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.method_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"DMFT": {"label": "Search DMFT entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"search_quantity": "results.method.simulation.dmft.impurity_solver_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 2, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.inverse_temperature", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dmft.magnetic_state", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.u", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.method.simulation.dmft.jh", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "results.method.simulation.dmft.analytical_continuation", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Workflow", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Molecular dynamics", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.method.workflow_name", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"MolecularDynamics": {"label": "Search molecular dynamics entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.thermodynamic.trajectory", "items": [{"search_quantity": "results.properties.thermodynamic.trajectory.available_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 4, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.ensemble_type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 2, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.thermodynamic.trajectory.provenance.molecular_dynamics.time_step", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Geometry Optimization", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "results.properties.available_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": false, "options": {"geometry_optimization": {"label": "Search geometry optimization entries"}}, "n_columns": 1, "sort_static": true, "show_statistics": false}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.geometry_optimization", "items": [{"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_energy_difference", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_force_maximum", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.geometry_optimization.final_displacement_maximum", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Properties", "type": "menu", "size": "md"}, {"width": 12, "show_header": true, "title": "Electronic", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "electronic_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.band_structure_electronic.band_gap", "items": [{"search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 2, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.electronic.band_structure_electronic.band_gap.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.band_structure_electronic", "items": [{"search_quantity": "results.properties.electronic.band_structure_electronic.spin_polarized", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.electronic.dos_electronic", "items": [{"search_quantity": "results.properties.electronic.dos_electronic.spin_polarized", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Vibrational", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "vibrational_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Mechanical", "type": "menu", "size": "md", "indentation": 1, "items": [{"search_quantity": "mechanical_properties", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.bulk_modulus", "items": [{"search_quantity": "results.properties.mechanical.bulk_modulus.type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.mechanical.bulk_modulus.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.shear_modulus", "items": [{"search_quantity": "results.properties.mechanical.shear_modulus.type", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "results.properties.mechanical.shear_modulus.value", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "type": "nested_object", "path": "results.properties.mechanical.energy_volume_curve", "items": [{"search_quantity": "results.properties.mechanical.energy_volume_curve.type", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}]}]}, {"width": 12, "show_header": true, "title": "Author / Origin / Dataset", "type": "menu", "size": "lg", "items": [{"search_quantity": "authors.name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"type": "histogram", "show_input": true, "x": {"search_quantity": "upload_create_time", "scale": "linear"}, "y": {"scale": "linear"}, "autorange": false, "width": 12, "show_header": true, "show_statistics": true}, {"search_quantity": "external_db", "type": "terms", "scale": "linear", "show_input": false, "width": 12, "show_header": true, "options": 5, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.dataset_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.doi", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}]}, {"width": 12, "show_header": true, "title": "Visibility / IDs / Schema", "type": "menu", "size": "md", "items": [{"width": 12, "show_header": true, "type": "visibility"}, {"search_quantity": "entry_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "upload_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "upload_name", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "results.material.material_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"search_quantity": "datasets.dataset_id", "type": "terms", "scale": "linear", "show_input": true, "width": 12, "show_header": true, "options": 0, "n_columns": 1, "sort_static": true, "show_statistics": true}, {"width": 12, "show_header": true, "type": "definitions"}]}, {"width": 12, "show_header": true, "title": "Optimade", "type": "menu", "size": "lg", "items": [{"width": 12, "show_header": true, "type": "optimade"}]}]}, "search_quantities": {"exclude": ["mainfile", "entry_name", "combine"]}, "dashboard": {"widgets": [{"type": "periodictable", "search_quantity": "results.material.elements", "scale": "linear", "layout": {"lg": {"h": 11, "w": 14, "x": 0, "y": 0, "minH": 3, "minW": 3}, "md": {"h": 8, "w": 12, "x": 0, "y": 0, "minH": 3, "minW": 3}, "sm": {"h": 8, "w": 12, "x": 0, "y": 0, "minH": 3, "minW": 3}, "xl": {"h": 11, "w": 14, "x": 0, "y": 0, "minH": 3, "minW": 3}, "xxl": {"h": 9, "w": 13, "x": 0, "y": 0, "minH": 3, "minW": 3}}}, {"search_quantity": "results.material.symmetry.space_group_symbol", "type": "terms", "scale": "linear", "show_input": true, "layout": {"lg": {"h": 5, "w": 5, "x": 19, "y": 6, "minH": 3, "minW": 3}, "md": {"h": 6, "w": 6, "x": 12, "y": 8, "minH": 3, "minW": 3}, "sm": {"h": 5, "w": 6, "x": 6, "y": 13, "minH": 3, "minW": 3}, "xl": {"h": 6, "w": 6, "x": 24, "y": 5, "minH": 3, "minW": 3}, "xxl": {"h": 9, "w": 6, "x": 30, "y": 0, "minH": 3, "minW": 3}}}, {"search_quantity": "results.material.structural_type", "type": "terms", "scale": "log", "show_input": false, "layout": {"lg": {"h": 6, "w": 5, "x": 19, "y": 0, "minH": 3, "minW": 3}, "md": {"h": 6, "w": 6, "x": 0, "y": 8, "minH": 3, "minW": 3}, "sm": {"h": 5, "w": 6, "x": 6, "y": 8, "minH": 3, "minW": 3}, "xl": {"h": 11, "w": 5, "x": 19, "y": 0, "minH": 3, "minW": 3}, "xxl": {"h": 9, "w": 6, "x": 19, "y": 0, "minH": 3, "minW": 3}}}, {"search_quantity": "results.method.simulation.program_name", "type": "terms", "scale": "log", "show_input": true, "layout": {"lg": {"h": 6, "w": 5, "x": 14, "y": 0, "minH": 3, "minW": 3}, "md": {"h": 8, "w": 6, "x": 12, "y": 0, "minH": 3, "minW": 3}, "sm": {"h": 5, "w": 6, "x": 0, "y": 8, "minH": 3, "minW": 3}, "xl": {"h": 11, "w": 5, "x": 14, "y": 0, "minH": 3, "minW": 3}, "xxl": {"h": 9, "w": 6, "x": 13, "y": 0, "minH": 3, "minW": 3}}}, {"search_quantity": "results.material.symmetry.crystal_system", "type": "terms", "scale": "linear", "show_input": false, "layout": {"lg": {"h": 5, "w": 5, "x": 14, "y": 6, "minH": 3, "minW": 3}, "md": {"h": 6, "w": 6, "x": 6, "y": 8, "minH": 3, "minW": 3}, "sm": {"h": 5, "w": 6, "x": 0, "y": 13, "minH": 3, "minW": 3}, "xl": {"h": 5, "w": 6, "x": 24, "y": 0, "minH": 3, "minW": 3}, "xxl": {"h": 9, "w": 5, "x": 25, "y": 0, "minH": 3, "minW": 3}}}]}, "filters_locked": {"quantities": "results.method.simulation.program_name"}, "search_syntaxes": {"exclude": ["free_text"]}}}]}}};
-- 
GitLab


From ef17c2e1c7cfd9dec11078c6d07845b771cdb52b Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Mon, 24 Mar 2025 12:59:14 +0100
Subject: [PATCH 16/17] Fixed input label padding on none filled inputs.

---
 src/components/values/containers/ContainerControl.tsx | 8 +++++---
 src/components/values/containers/Matrix.tsx           | 6 +++++-
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/components/values/containers/ContainerControl.tsx b/src/components/values/containers/ContainerControl.tsx
index 2d690df9..fd177bc2 100644
--- a/src/components/values/containers/ContainerControl.tsx
+++ b/src/components/values/containers/ContainerControl.tsx
@@ -6,7 +6,6 @@ import {
   InputLabel,
   Stack,
   styled,
-  useTheme,
 } from '@mui/material'
 import {FocusEvent, PropsWithChildren, useCallback} from 'react'
 
@@ -66,7 +65,6 @@ export function ValueFormControl({...props}: ValueFormControlProps) {
     ...formControlProps
   } = props
   const {inputId, editable} = useProps<ContainerProps>()
-  const theme = useTheme()
 
   return (
     <FormControl
@@ -83,7 +81,11 @@ export function ValueFormControl({...props}: ValueFormControlProps) {
             shrink
             id={editable ? undefined : inputId && `${inputId}-label`}
             htmlFor={editable ? inputId : undefined}
-            style={{marginLeft: editable ? theme.spacing(1.5) : 0}}
+            sx={{
+              '&.MuiInputLabel-filled': {
+                marginLeft: 1.5,
+              },
+            }}
           >
             {label}
           </StaticInputLabel>
diff --git a/src/components/values/containers/Matrix.tsx b/src/components/values/containers/Matrix.tsx
index 267525b1..b68f4802 100644
--- a/src/components/values/containers/Matrix.tsx
+++ b/src/components/values/containers/Matrix.tsx
@@ -173,7 +173,11 @@ export default function Matrix<Value = unknown>({
       <div
         style={{...style, paddingLeft: 4, paddingRight: 4, textAlign: 'center'}}
       >
-        <Value value={matrixValue?.[rowIndex][columnIndex]} hiddenUnit>
+        <Value
+          value={matrixValue?.[rowIndex][columnIndex]}
+          hiddenUnit
+          editable={false}
+        >
           {renderValue?.({})}
         </Value>
       </div>
-- 
GitLab


From 841c7ed0f085d04c391cf4357f03a30160a98107 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Mon, 24 Mar 2025 13:15:13 +0100
Subject: [PATCH 17/17] No matrix full width out dynamic component specs.

---
 src/components/archive/Section.tsx                    | 2 +-
 src/components/values/containers/ContainerControl.tsx | 2 +-
 src/components/values/containers/Matrix.tsx           | 5 ++++-
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/components/archive/Section.tsx b/src/components/archive/Section.tsx
index a852d515..83dd6dc6 100644
--- a/src/components/archive/Section.tsx
+++ b/src/components/archive/Section.tsx
@@ -68,9 +68,9 @@ function Quantity({definition, editable = false}: QuantityProps) {
       component={component}
       label={getPropertyLabel(definition.name)}
       placeholder='no value'
-      fullWidth
       editable={editable}
       value={value}
+      fullWidth={component.Matrix ? false : true}
       onChange={handleChange}
     />
   )
diff --git a/src/components/values/containers/ContainerControl.tsx b/src/components/values/containers/ContainerControl.tsx
index fd177bc2..8c3c09bc 100644
--- a/src/components/values/containers/ContainerControl.tsx
+++ b/src/components/values/containers/ContainerControl.tsx
@@ -92,7 +92,7 @@ export function ValueFormControl({...props}: ValueFormControlProps) {
           <LabelActionsContainer>{labelActions}</LabelActionsContainer>
         </LabelContainer>
       )}
-      <Stack direction='row' width='100%'>
+      <Stack direction='row' sx={{width: fullWidth ? '100%' : 'inherit'}}>
         {children}
       </Stack>
       {(edit || error) && helperText && (
diff --git a/src/components/values/containers/Matrix.tsx b/src/components/values/containers/Matrix.tsx
index b68f4802..f651d82f 100644
--- a/src/components/values/containers/Matrix.tsx
+++ b/src/components/values/containers/Matrix.tsx
@@ -254,7 +254,10 @@ export default function Matrix<Value = unknown>({
 
   return (
     <ContainerControl showLabel {...containerProps}>
-      <Stack width='100%' marginTop={label ? 3 : undefined}>
+      <Stack
+        sx={{width: containerProps.fullWidth ? '100%' : 'inherit'}}
+        marginTop={label ? 3 : undefined}
+      >
         {matrixElement}
       </Stack>
     </ContainerControl>
-- 
GitLab