Skip to content

Surface dirty-draft indicators in Properties/Individuals trees and on the Entity tabs #201

@JohnRDOrazio

Description

@JohnRDOrazio

Problem

Editing a class in the Details panel surfaces an amber-dot dirty indicator next to that class in the Classes tree. Editing a property or individual is silent — neither the Properties tree nor the Individuals list shows a per-row dirty marker, and the Entity tab bar never indicates which tab(s) currently hold unsaved drafts.

This makes it easy to lose track of in-progress edits the moment the user switches tabs.

Current state (verified)

  • The draft store (lib/stores/draftStore.ts) already keys drafts as ${projectId}:${branch}:${iri} and each AnyDraftEntry carries an entityType ("class" | "property" | "individual").
  • getDraftIris(projectId, branch) (lines 93-98) returns a flat list of all dirty IRIs, type-blind.
  • StandardEditorLayout.tsx:135-141 and DeveloperEditorLayout.tsx:175-180 build a single draftIris: Set<string> from that flat list.
  • That set is passed only to <ClassTree> (StandardEditorLayout.tsx:370, DeveloperEditorLayout.tsx:500).
  • <PropertyTree> (components/editor/standard/PropertyTree.tsx:9-14) and <IndividualList> (components/editor/standard/IndividualList.tsx:9-15) don't accept draftIris at all.
  • The shared <EntityTree> component already accepts draftIris?: Set<string> (components/editor/shared/EntityTree.tsx:18) and <EntityTreeNode> already renders the amber dot from it (components/editor/shared/EntityTreeNode.tsx:25). So the rendering plumbing is in place — only the data plumbing is missing.
  • <EntityTabBar> (components/editor/standard/EntityTabBar.tsx:7-11) has no per-tab dirty awareness.

Proposed plan

1. Make getDraftIris type-aware

Extend the store API to filter by entity type, since each draft already carries one:

// lib/stores/draftStore.ts
getDraftIris: (projectId: string, branch: string, entityType?: SelectableEntityType) => string[];

Implementation filters by both prefix and entry.entityType when entityType is provided. Keep the existing two-arg signature working for any callers we missed (back-compat for the clearAllDrafts flow).

2. Compute three sets in the layouts

In StandardEditorLayout and DeveloperEditorLayout, replace the single draftIris memo with three:

const classDraftIris      = useMemo(() => new Set(getDraftIris(projectId, branch, "class")),      [...]);
const propertyDraftIris   = useMemo(() => new Set(getDraftIris(projectId, branch, "property")),   [...]);
const individualDraftIris = useMemo(() => new Set(getDraftIris(projectId, branch, "individual")), [...]);

3. Plumb the right set into each tree

  • Add draftIris?: Set<string> to PropertyTreeProps and IndividualListProps.
  • Pass it through to the underlying <EntityTree> (already supports the prop — no rendering change needed in EntityTreeNode).
  • Wire classDraftIris / propertyDraftIris / individualDraftIris into <ClassTree>, <PropertyTree>, <IndividualList> respectively in both layouts.

4. Tab-level indicator in EntityTabBar

Add a small dirty-marker (matching the amber dot on tree rows) next to a tab label when its corresponding set is non-empty:

interface EntityTabBarProps {
  activeTab: EntityTab;
  onTabChange: (tab: EntityTab) => void;
  dirtyTabs?: { classes?: boolean; properties?: boolean; individuals?: boolean };
}

Both layouts pass dirtyTabs={{ classes: classDraftIris.size > 0, properties: propertyDraftIris.size > 0, individuals: individualDraftIris.size > 0 }}. Render the dot adjacent to the tab label, with an aria-label like "unsaved changes" for screen readers.

5. Tests

  • __tests__/lib/stores/draftStore.test.ts (existing) — extend with cases for getDraftIris filtered by each entityType.
  • __tests__/components/editor/standard/PropertyTree.test.tsx and IndividualList.test.tsx — add a case asserting the amber-dot renders when an IRI is in draftIris.
  • A new test (or extension of an existing layout test) that confirms EntityTabBar shows the dot on the right tab(s) given mixed-type drafts.

Done criteria

  • Editing a property shows the amber dot next to that property in the Properties tree, and a dot on the Properties tab when Properties tab isn't active.
  • Same for individuals via the Individuals list and tab.
  • Class behavior unchanged (regression coverage).
  • When the user discards / saves a draft, the corresponding dot disappears within the next render.
  • Tab-level dot is aria-label-ed for screen readers.

Files touched

  • lib/stores/draftStore.ts — add type filter to getDraftIris
  • components/editor/standard/PropertyTree.tsx — accept and forward draftIris
  • components/editor/standard/IndividualList.tsx — accept and forward draftIris
  • components/editor/standard/EntityTabBar.tsx — accept dirtyTabs, render per-tab dot
  • components/editor/standard/StandardEditorLayout.tsx — split into three sets, pass each
  • components/editor/developer/DeveloperEditorLayout.tsx — same
  • Tests as listed above

Out of scope

  • Reactive cross-page indicators (e.g., showing the dot in the Viewer/Editor switcher). The stores are session-local, and that decision deserves its own design pass.
  • Bulk-discard UI per tab. Only the visual indicator is in scope here.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions