Files
PubScript/apps/web/src/features/entities/EntityEditor.tsx
T
Schwitzz 4efa71515e feat(entity): Chunk #4 data layer + entity UI (characters & world-building)
Data layer:
- Fuzzy scorer (packages/core/src/fuzzy.ts) with index recovery + unit tests
- Backlink computation (packages/core/src/backlinks.ts) incl. entity-level backlinks
- Consistency checker (packages/core/src/consistency.ts) incl. reciprocity/scope + unit tests
- StorageAdapter contract extended with listEntities/searchEntities/saveEntity/deleteEntity;
  implemented in Local (Dexie) and Server adapters; contract tests cover entity CRUD + ?q= search
- WorldItem.fields now supports list values (keyPeople, rivals); expanded RelationshipKind
- @mention autocomplete extension + picker in the editor package

Entity UI (apps/web/src/features/entities/):
- Generic field renderer (EntityFields) driven by ENTITY_FIELDS/FieldDef
- Tabbed directory with fuzzy search + entity cards (EntitiesView)
- Create/edit/delete drawer (EntityEditor) reusing shared fields
- Relationship editor + force-directed SVG relationship map
- Entity engine stylesheet; wired into nav

Store: project-store entity methods (load/create/update/delete) over the StorageAdapter contract.

CI-green: typecheck, lint, 65 tests, and web build all pass.
2026-10-25 22:42:55 -05:00

110 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* EntityEditor — the right-rail drawer for creating and editing an entity.
*
* Composes the generic `EntityFields` renderer plus, for characters, the
* `RelationshipEditor`. Emits the full entity on Save and deletes on demand.
*/
import React from "react";
import type { Character, Entity } from "@pubscript/core";
import { Button } from "@pubscript/ui";
import { EntityFields } from "./EntityFields.js";
import { RelationshipEditor } from "./RelationshipEditor.js";
import { RelationshipMap } from "./RelationshipMap.js";
interface EntityEditorProps {
entity: Entity | null;
entities: Entity[];
open: boolean;
onClose: () => void;
onSave: (entity: Entity) => void;
onDelete: (id: string) => void;
}
function isCharacter(e: Entity): e is Character {
return "relationships" in e;
}
export function EntityEditor({
entity,
entities,
open,
onClose,
onSave,
onDelete,
}: EntityEditorProps) {
const [draft, setDraft] = React.useState<Entity | null>(entity);
React.useEffect(() => {
setDraft(entity);
}, [entity]);
if (!open || !draft) return null;
const isExisting = entities.some((e) => e.id === draft.id);
const isCharacterDraft = isCharacter(draft);
const canDelete = isExisting && isCharacterDraft;
const title = isCharacterDraft
? (draft.name || "Edit character")
: "fields" in draft
? `Edit ${draft.kind}`
: "Edit note";
const patch = (p: Partial<Entity>) => {
setDraft({ ...draft, ...p });
};
const save = () => {
onSave(draft);
onClose();
};
return (
<aside className="ps-entity-editor">
<header className="ps-entity-editor-header">
<h2>{isExisting ? title : "New entity"}</h2>
<button type="button" className="ps-icon-btn" aria-label="Close" onClick={onClose}>
×
</button>
</header>
<div className="ps-entity-editor-body">
{isCharacterDraft ? (
<>
<EntityFields entity={draft} onChange={patch} />
<div className="ps-relationship-section">
<h3>Relationships</h3>
<RelationshipEditor
character={draft}
entities={entities}
onChange={(relationships) => patch({ relationships })}
/>
<div className="ps-relationship-map-wrap">
<RelationshipMap entities={entities} focusId={draft.id} />
</div>
</div>
</>
) : (
<EntityFields entity={draft} onChange={patch} />
)}
</div>
<footer className="ps-entity-editor-actions">
<div className="ps-spacer" />
{canDelete ? (
<Button
variant="danger"
onClick={() => onDelete(draft.id)}
>
Delete
</Button>
) : null}
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button onClick={save}>Save</Button>
</footer>
</aside>
);
}