Skip to content

Latest commit

 

History

History
249 lines (169 loc) · 10.7 KB

File metadata and controls

249 lines (169 loc) · 10.7 KB

Architecture & Project Guide

What this project is

An open-source editor for 1EdTech CASE frameworks. You can create and edit frameworks (CFDocument), items (CFItem), and associations (CFAssociation) through a visual canvas, with a draft → publish workflow. It's deliberately focused on core authoring — not trying to replace commercial CASE tools.

Under the hood, the CASE model is treated as a graph (items + associations). The canvas you see in the UI is just one way to visualise that graph — the domain model is what matters.

Tech stack

  • React 19 + TypeScript (Vite)
  • Tailwind CSS v4
  • shadcn/ui components
  • React Flow (@xyflow/react)
  • Vitest (unit testing)
  • oidc-client-ts (authentication)

Architectural goals (non-negotiables)

These are the principles we don't bend on:

  1. Domain model is the source of truth — The domain representation of a framework (items + associations) is authoritative.

  2. React Flow is a projection of the domain graph — React Flow nodes/edges are derived from the domain model. They're not the canonical data.

  3. All user interactions become application commands — UI events (edit node, add child, connect, delete) must translate to commands handled by the application layer.

  4. Clean architecture dependency direction — Dependencies always point inward:

    • domain has no dependencies on React, React Flow, APIs, or storage
    • application depends on domain
    • infrastructure implements interfaces defined by application
    • ui depends on application (and uses infrastructure through composition)

Folder structure

Pure logic (reducer, layout algorithms, geometry) is isolated from React so it can be tested and reasoned about independently.

src
├── app
│   ├── App.tsx                     # App shell — topology detection + pre-render layout
│   ├── main.tsx
│   └── providers/

├── domain
│   └── framework/
│       └── model/                  # Framework, Item, Association entities

├── application
│   └── framework/
│       ├── mappers/                # Domain ↔ CASE JSON DTO mapping
│       └── ports/                  # Repository / ID generator interfaces

├── infrastructure
│   └── caseApi/
│       ├── CaseApiClient.ts        # HTTP client for CASE API
│       └── http.ts                 # Fetch-based HTTP client

├── ui
│   ├── editor
│   │   ├── components/             # Canvas, header, dialogs
│   │   ├── reactflow/
│   │   │   ├── nodeTypes/          # CaseItemNode, CaseFrameworkNode
│   │   │   ├── edgeTypes/          # LabeledEdge
│   │   │   ├── mapping/
│   │   │   │   ├── toReactFlow.ts  # Domain → React Flow nodes/edges
│   │   │   │   └── fromEditorGraph.ts  # React Flow → Domain + layout
│   │   │   └── types.ts           # CaseEditorNodeType, CaseEditorEdge, CaseEdgeData
│   │   ├── state/
│   │   │   ├── EditorContext.tsx   # React provider (orchestration only)
│   │   │   ├── editorReducer.ts   # Pure state reducer (no React dependency)
│   │   │   ├── editorFactories.ts # Node/edge factory helpers
│   │   │   └── helpers/
│   │   │       └── nodeGeometry.ts # Pure geometry, type guards, adjacency
│   │   ├── layout/
│   │   │   ├── detectTopology.ts       # Classify graph as hierarchy/star/tree
│   │   │   ├── applyInitialLayout.ts   # Pre-render layout orchestrator
│   │   │   ├── hierarchyLayout.ts      # Flat column layout (smoothstep)
│   │   │   ├── starLayout.ts           # Elliptical radial layout (bezier)
│   │   │   └── treeLayout.ts           # Recursive tree-spread layout (bezier)
│   │   └── terminology/
│   │       ├── lens.ts
│   │       └── strings.ts
│   ├── home/                       # Home screen, framework cards, import dialog
│   └── shared/
│       ├── components/ui/          # shadcn/ui primitives
│       └── hooks/

├── __tests__
│   └── fixtures.ts                 # Reusable mock factories for tests

├── styles/
│   └── index.css

└── lib/
    └── utils.ts

Domain model

We treat CASE as a graph:

  • Item = node (CFItem)
  • Association = edge (CFAssociation)
  • Framework = aggregate root owning items + associations (CFDocument + supporting structures)

The domain model has no knowledge of React Flow, the UI, or any external system.


Domain ↔ React Flow mapping

This is the most important boundary in the codebase. The mapping code is intentionally centralised in two files:

  • src/ui/editor/reactflow/mapping/toReactFlow.ts — Domain → React Flow
  • src/ui/editor/reactflow/mapping/fromEditorGraph.ts — React Flow → Domain

How it works:

  1. Domain → React Flow is derived — React Flow nodes/edges are built from the domain Framework (Item → Node, Association → Edge).
  2. UI changes become commands — When a user drags, edits, or connects something, the UI translates that into an application command. The command updates the domain model, and the UI re-renders by re-projecting the domain.
  3. Layout is separate — Positions (x, y) are a UI concern. They're stored in UI state, not in domain entities.

State management

Editor state uses a pure reducer pattern, cleanly separated from React:

Pure modules (no React dependency)

  • state/editorReducer.ts — A single pure function that handles all state transitions: selection, CRUD, layout, connections, graph loading, etc.
  • state/helpers/nodeGeometry.ts — Geometry utilities (sizing, handles, overlap detection), type guards, and graph adjacency builders.
  • state/editorFactories.ts — Factory functions for items, documents, edges, and graph structures.

Layout algorithms (also pure)

  • layout/detectTopology.ts — Looks at a graph's shape and classifies it as hierarchy, star, or tree.
  • layout/hierarchyLayout.ts — Vertical column: framework at top, items stacked below. Smoothstep edges.
  • layout/starLayout.ts — Radial: framework at centre, branches fan out in angular sectors. Bezier edges.
  • layout/treeLayout.ts — Recursive horizontal tree-spread. Bezier edges.
  • layout/applyInitialLayout.ts — Pre-render orchestrator. Detects topology and applies layout before the first React Flow render, so there's no visible jump.

React orchestration

  • state/EditorContext.tsx — A thin React provider. It imports the reducer and layout functions, wires them up with hooks, and exposes the EditorContext API. Business logic stays out.

Application layer

Commands define what a user can do:

  • AddItem, UpdateItem, RemoveItem
  • AddAssociation, RemoveAssociation
  • PublishFramework

Command handlers validate inputs, enforce domain invariants, update the model, and call repository ports as needed.


Infrastructure

Implements the interfaces defined in application/ports:

  • FrameworkRepository — load/save/publish
  • IdGenerator — GUID/URI generation
  • HTTP client for the CASE API
  • Serialisation between domain objects and CASE JSON DTOs

UI layer

  • Renders the canvas editor (React Flow)
  • Provides panels and dialogs for editing properties
  • Translates UI events → commands
  • Shows draft status and the explicit publishing flow
  • Applies a terminology lens (K-12 / HE / Workforce) — UI labels adapt to context without touching the domain

Framework versioning (CFDocument.version)

The version field on a framework's metadata (CFDocument.version, e.g. "1.0", "2024.1") is author-controlled free text — the editor never generates or mutates it.

Before: frameworkToCfPackage auto-incremented a major.minor.build version on every save (incrementVersion: true), and defaulted unset versions to 1.0.0. The incrementVersion flag threaded through frameworkToCfDocument, frameworkToCfPackage, and FrameworkExportParams, and the version field in NodePropertiesPanel was a read-only display (hidden entirely when unset).

Now: version is a straight passthrough of framework.metadata.version — unset stays unset, and nothing increments it across saves. NodePropertiesPanel exposes it as a free-text input, same pattern as title/creator/publisher.

Why: auto-incrementing build numbers on every save was arbitrary versioning the author never asked for and couldn't override — it didn't track anything meaningful (e.g. semantic changes, publish milestones), and a hidden default (1.0.0) masked the fact that no version had actually been assigned. Making it a plain authored field puts version semantics under the framework author's control, consistent with other metadata fields like title and publisher.

Note this is distinct from the storage versioning the backend does on every save ({docId}_v0001.json, _v0002.json, ...) — see apps/opencase/docs/DEVELOPER.md. That's an internal, immutable revision history; CFDocument.version is the framework's own public-facing version label.

See apps/editor/src/application/framework/mappers/case/toCasePackage.ts and its test file for the passthrough behavior.


Draft → Publish workflow

  • All edits happen in a draft state
  • Switching frameworks discards the draft (with a warning)
  • Publishing is explicit: review changes, optionally add notes, then publish

Testing

All pure modules have unit tests (Vitest, jsdom). Tests live alongside the code they cover (*.test.ts), with shared fixtures in src/__tests__/fixtures.ts.

npm run test         # Single run
npm run test:watch   # Watch mode

If you're adding a new reducer action or layout algorithm, please add tests to go with it.


Contributing

Where to put things:

  • Business meaning → domain/
  • Use-case or command → application/
  • Presentation → ui/
  • External integrations → infrastructure/

If you're working on nodes/edges:

  • Change components in ui/editor/reactflow/*
  • Keep mapping logic in mapping/*

If you're adding state logic:

  • Add new actions to the Action union in editorReducer.ts
  • Handle them in the reducer (keep it pure)
  • Keep EditorContext.tsx as a thin orchestrator
  • Write tests

Why the mapping matters

CASE frameworks are inherently items + associations — a graph. Even though the current UI mostly shows a tree, future features (crosswalks, alignment links, references) will need edges that aren't purely hierarchical. Getting the mapping right now avoids a painful re-architecture later.