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
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
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)
lib/stores/draftStore.ts) already keys drafts as${projectId}:${branch}:${iri}and eachAnyDraftEntrycarries anentityType("class" | "property" | "individual").getDraftIris(projectId, branch)(lines 93-98) returns a flat list of all dirty IRIs, type-blind.StandardEditorLayout.tsx:135-141andDeveloperEditorLayout.tsx:175-180build a singledraftIris: Set<string>from that flat list.<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 acceptdraftIrisat all.<EntityTree>component already acceptsdraftIris?: 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
getDraftIristype-awareExtend the store API to filter by entity type, since each draft already carries one:
Implementation filters by both prefix and
entry.entityTypewhenentityTypeis provided. Keep the existing two-arg signature working for any callers we missed (back-compat for theclearAllDraftsflow).2. Compute three sets in the layouts
In
StandardEditorLayoutandDeveloperEditorLayout, replace the singledraftIrismemo with three:3. Plumb the right set into each tree
draftIris?: Set<string>toPropertyTreePropsandIndividualListProps.<EntityTree>(already supports the prop — no rendering change needed inEntityTreeNode).classDraftIris/propertyDraftIris/individualDraftIrisinto<ClassTree>,<PropertyTree>,<IndividualList>respectively in both layouts.4. Tab-level indicator in
EntityTabBarAdd a small dirty-marker (matching the amber dot on tree rows) next to a tab label when its corresponding set is non-empty:
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 anaria-labellike "unsaved changes" for screen readers.5. Tests
__tests__/lib/stores/draftStore.test.ts(existing) — extend with cases forgetDraftIrisfiltered by eachentityType.__tests__/components/editor/standard/PropertyTree.test.tsxandIndividualList.test.tsx— add a case asserting the amber-dot renders when an IRI is indraftIris.EntityTabBarshows the dot on the right tab(s) given mixed-type drafts.Done criteria
aria-label-ed for screen readers.Files touched
lib/stores/draftStore.ts— add type filter togetDraftIriscomponents/editor/standard/PropertyTree.tsx— accept and forwarddraftIriscomponents/editor/standard/IndividualList.tsx— accept and forwarddraftIriscomponents/editor/standard/EntityTabBar.tsx— acceptdirtyTabs, render per-tab dotcomponents/editor/standard/StandardEditorLayout.tsx— split into three sets, pass eachcomponents/editor/developer/DeveloperEditorLayout.tsx— sameOut of scope
Related