Skip to content

No UI for viewing or editing nested (object/array) schema properties #2009

Closed

Description

@malibio

Overview

GenericSchemaForm's field renderer handles five field types (enum, date, number, boolean, string/text) and falls through to a literal "Unknown field type: {field.type}" placeholder for anything else — including object and array, both of which are real, already-defined field shapes in SchemaField (fields: Option<Vec<SchemaField>> for nested objects, item_type/item_fields for arrays). A property with nested structure is currently unviewable and uneditable in the UI; the only way to inspect or change it is direct DB/RPC access.

Problem Statement

packages/desktop-app/src/lib/components/schema/generic-schema-form.svelte:163-236 handles field types via an if/else chain:

{#if field.type === 'enum'} ...
{:else if field.type === 'date'} ...
{:else if field.type === 'number'} ...
{:else if field.type === 'boolean'} ...
{:else if field.type === 'string' || field.type === 'text'} ...
{:else}
  <div class="text-sm text-muted-foreground">Unknown field type: {field.type}</div>
{/if}

An object-typed field (or an array with item_type: 'object') renders this fallback text with no editor at all. task-schema-form.svelte and schema-property-form.svelte were not checked for the identical gap but are built on the same field-type-dispatch pattern and likely share it — confirm during implementation.

This blocks any schema field intentionally modeled with nesting (e.g. an address object with street/city sub-fields, or a list of structured line items) from having any UI at all, and blocks deeper-nested values in general from being viewed, expanded/collapsed, or deleted.

Proposed Solution

Decision: evaluate svelte-jsoneditor first, before committing to a hand-built recursive renderer.

A research pass surveyed the available options for editable nested-JSON UI components:

  • svelte-jsoneditor (josdejong) — the one realistic off-the-shelf candidate. Native Svelte 5 component, actively maintained, full CRUD (tree/text/table modes, add/edit/delete keys and values, arbitrary nesting depth), and — unlike most alternatives — exposes a genuine per-value/per-path custom renderer hook (onRenderValue), with built-in examples for enum-dropdown and timestamp-picker style leaf renderers. Caveat: this hook is explicitly marked experimental in the library's own docs, and the package carries real dependency weight (lodash-es, ajv, jmespath — potential 150-300KB+ added). It was not built around a schema system like NodeSpace's SchemaField; making it render NodeSpace's own date-picker/enum-dropdown components (rather than generic inputs) means wiring NodeSpace components into its renderer-injection API, not just using them as-is.
  • Ruled out: svelte-json-tree and svelte-json-view-lite/@humanspeak/svelte-json-view-lite are Svelte-native but read-only (view only, no edit/delete). react-json-view (maintained fork) and json-edit-react are strong, well-designed, but React-only. jsoneditor (vanilla JS) is superseded by vanilla-jsoneditor, which svelte-jsoneditor already wraps — no reason to use the legacy package directly.

Spike first: before implementation, time-box an evaluation of whether svelte-jsoneditor's onRenderValue hook can cleanly render NodeSpace's existing field components (date picker, enum dropdown, etc.) at each leaf, keyed off the corresponding SchemaField/EdgeField type at that path. If it integrates cleanly, prefer it over hand-building — it gets expand/collapse, delete, and tree/table view modes for free, matching the "modal view, expand/collapse, view, delete" ask directly. If the integration proves awkward (fighting the experimental hook's API, dependency weight not justified for what ends up being a thin wrapper), fall back to a hand-built recursive component: extend the existing five-case renderer in generic-schema-form.svelte to recurse into field.fields/item_fields, reusing NodeSpace's own field-type renderers at each leaf rather than introducing a new dependency. Record the spike's outcome and rationale in this issue before starting the full implementation, so the decision isn't silently made mid-PR.

Either path should support:

  • Viewing a nested property's full structure (arbitrary depth)
  • Expand/collapse per nesting level
  • Editing leaf values using the same typed controls already used for top-level fields of that type (date picker for a date leaf, dropdown for an enum leaf, checkbox for boolean, etc.) — not generic text inputs for every value
  • Deleting a key/value at any nesting level
  • A modal presentation, per the original ask, rather than inline in the already-cramped property panel

Acceptance Criteria

  • Spike outcome recorded: svelte-jsoneditor evaluated against NodeSpace's field-type renderers; decision (adopt it, or hand-build) documented with rationale in this issue before full implementation begins
  • Opening a node with an object-typed or array-of-object-typed property shows a working editor/viewer instead of "Unknown field type"
  • Nested values render using NodeSpace's existing typed controls per leaf (date picker, enum dropdown, boolean checkbox, etc.), not generic text/JSON-raw inputs
  • Expand/collapse works at any nesting depth
  • Deleting a key/value at any nesting level works and persists correctly
  • Presented as a modal (matching the pattern already used for RelationshipViewerModal, #1918) rather than crammed into the inline properties panel
  • task-schema-form.svelte/schema-property-form.svelte audited for the same "Unknown field type" fallback gap; fixed in the same PR if found, or filed separately if genuinely distinct scope
  • If svelte-jsoneditor is adopted: bundle-size impact measured and considered acceptable; note any other apps/pages affected by the added dependency weight
  • No new test failures (bun run test:all)
  • Code passes bun run quality:fix

Technical Specifications

Reference Files

  • packages/desktop-app/src/lib/components/schema/generic-schema-form.svelte:163-236 — the field-type dispatch chain; the fallback case (line 234-236) is the concrete symptom
  • packages/nodespace-types/src/schema.rs (or packages/core/src/models/schema_node.rs, per Store SchemaRelationship declarations as relationship-table edges instead of schema-node JSON #2008's finding on which SchemaNode/SchemaField is actually live) — SchemaField.fields: Option<Vec<SchemaField>> (nested object shape), item_type/item_fields (array shape) — the existing data model this feature needs to render, already defined but never consumed by any frontend renderer
  • packages/desktop-app/src/lib/components/relationships/relationship-viewer-modal.svelte — existing modal pattern to follow for presentation, per Relationship viewer: view and manage typed relationships between nodes #1918
  • packages/desktop-app/src/lib/components/property-forms/task-schema-form.svelte, schema-property-form.svelte — likely share the same fallback gap; audit as part of this work

Non-Goals

  • Does not change the SchemaField/EdgeField data model — nested object/array shapes already exist and are already stored correctly; this is purely a rendering/editing gap on the frontend.
  • Does not address isCustomSchemaType conflates "is core" with "has a dedicated frontend integration" across 4 call sites #1998's scope (the properties-form rendering gate itself, i.e. whether a form renders at all for a given node type) — this issue assumes the form renders and is about what happens once it does, for a field whose type is object/array.

Activity

  1. mstomar125 commented on Aug 10, 2026

    @mstomar125
    Contributor

    Spike outcome — hand-build a recursive schema-driven renderer (do NOT adopt svelte-jsoneditor)

    Evaluated svelte-jsoneditor (the one realistic off-the-shelf candidate) against NodeSpace's actual data model. Decision: hand-build, for concrete reasons rather than NIH:

    1. NodeSpace's nested data is schema-typed, not arbitrary JSON. SchemaField already models the nesting: fields: Option<Vec<SchemaField>> (nested object) and item_type/item_fields (array + array-of-object). A recursive component that walks field.fields / item_fields and reuses the existing five leaf renderers (enum → dropdown, date → picker, number, boolean → checkbox, string/text) maps 1:1 onto this model. That's exactly the AC's "render nested values using NodeSpace's existing typed controls per leaf."
    2. svelte-jsoneditor is JSON-generic. Getting it to render NodeSpace's typed leaf components means driving its experimental onRenderValue hook, keyed off the SchemaField type at each path — i.e. re-deriving the schema→component mapping we already have, through an injection API not built for it. That's fighting the tool to end up with a thin wrapper.
    3. Dependency weight. It pulls lodash-es + ajv + jmespath (~150–300KB) into a Tauri desktop bundle for what becomes wrapper code. Not justified.
    4. Presentation control. The AC wants a modal matching RelationshipViewerModal (Relationship viewer: view and manage typed relationships between nodes #1918) with expand/collapse + delete — trivial to own in a hand-built component, versus theming/constraining a third-party editor.

    Implementation plan (hand-build)

    • Type gap first: the frontend SchemaField (packages/desktop-app/src/lib/types/schema-node.ts) currently exposes only itemType — it's missing fields?: SchemaField[] (nested object) and itemFields?: SchemaField[] (array item schema), which already arrive on the wire. Add them.
    • Recursive renderer: extend the field dispatch in generic-schema-form.svelte — replace the object/array fall-through to "Unknown field type" with a recursive NestedFieldEditor that renders field.fields (object) or iterates item_fields per array element (array-of-object), and delegates each leaf back to the existing five-case renderer. Expand/collapse per level; add/delete key/element.
    • Modal: present via a modal mirroring RelationshipViewerModal, per the original ask, rather than inline in the cramped property panel.
    • Audit: task-schema-form.svelte / schema-property-form.svelte share the same five-case dispatch — fix the identical object/array fall-through there or file separately.
    • Persistence: nested edits write back through the same property-update path top-level fields already use.

    This is a sizable feature (recursive component + modal + type extension + audit); logging the spike decision now per the issue's requirement so implementation can proceed against a settled direction.

  2. self-assigned this
    on Aug 11, 2026
  3. mstomar125 commented on Aug 11, 2026

    @mstomar125
    Contributor

    Resolved in #2028 (squash-merged to main).

    Hand-built a recursive, schema-driven editor for nested object/array properties (per the spike recorded above — no svelte-jsoneditor):

    • Shared schema-field-leaf.svelte (five leaf renderers extracted; one place for leaf rendering) + pure recursive nested-field-editor.svelte (value in, onChange out, never touches the store) + pure nested-property-ops.ts + nested-property-modal.svelte (owns persistence via the same sharedNodeStore.updateNode path, mirroring RelationshipViewerModal).
    • GenericSchemaForm object/array fields now open the modal ("N items"/"N fields" + Edit) instead of showing "Unknown field type".
    • Arbitrary depth, per-level expand/collapse, add/delete at any level, typed leaf controls (date picker/enum dropdown/checkbox/etc.).
    • Nested schema already arrives on the wire (Rust serializes fields/itemFields/itemType camelCase, adapter passes through) — only the TS type needed extending; no data-model/Rust change.

    Audit → #2027: task-schema-form.svelte and schema-property-form.svelte share the fall-through but persist under different namespaces (properties.task.* / properties[nodeType].*), so the flat modal can't be dropped in as-is — filed #2027 for a namespace-aware read/persist strategy (plus schema-property-form's stubbed boolean).

    Two-pass adversarial review (no blockers/majors); addressed two minor Collapsible open-state findings (index-shift on delete + per-field remount), unit-tested. Full suite green (198 files / 4543 tests).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions