Remove ProjectDialoguesPanel component and related files
This commit is contained in:
@@ -1,253 +0,0 @@
|
||||
import { useEffect, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
|
||||
|
||||
import {
|
||||
getProjectDialogues,
|
||||
type ProjectDialogue,
|
||||
type ProjectDialogueLibrary
|
||||
} from "../dialogues/project-dialogues";
|
||||
|
||||
interface ProjectDialoguesPanelProps {
|
||||
dialogues: ProjectDialogueLibrary;
|
||||
selectedDialogueId: string | null;
|
||||
onSelectDialogue(dialogueId: string | null): void;
|
||||
onAddDialogue(): void;
|
||||
onDeleteDialogue(dialogueId: string): void;
|
||||
onSetDialogueTitle(dialogueId: string, title: string): void;
|
||||
onAddDialogueLine(dialogueId: string): void;
|
||||
onDeleteDialogueLine(dialogueId: string, lineId: string): void;
|
||||
onSetDialogueLineSpeaker(
|
||||
dialogueId: string,
|
||||
lineId: string,
|
||||
speakerName: string | null
|
||||
): void;
|
||||
onSetDialogueLineText(dialogueId: string, lineId: string, text: string): void;
|
||||
}
|
||||
|
||||
function commitOnEnter(
|
||||
event: ReactKeyboardEvent<HTMLInputElement>,
|
||||
commit: () => void
|
||||
) {
|
||||
if (event.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.currentTarget.blur();
|
||||
commit();
|
||||
}
|
||||
|
||||
export function ProjectDialoguesPanel({
|
||||
dialogues,
|
||||
selectedDialogueId,
|
||||
onSelectDialogue,
|
||||
onAddDialogue,
|
||||
onDeleteDialogue,
|
||||
onSetDialogueTitle,
|
||||
onAddDialogueLine,
|
||||
onDeleteDialogueLine,
|
||||
onSetDialogueLineSpeaker,
|
||||
onSetDialogueLineText
|
||||
}: ProjectDialoguesPanelProps) {
|
||||
const dialogueList = getProjectDialogues(dialogues);
|
||||
const selectedDialogue =
|
||||
selectedDialogueId === null ? null : dialogues.dialogues[selectedDialogueId] ?? null;
|
||||
const [titleDraft, setTitleDraft] = useState(selectedDialogue?.title ?? "");
|
||||
const [lineDrafts, setLineDrafts] = useState<
|
||||
Record<string, { speakerName: string; text: string }>
|
||||
>({});
|
||||
|
||||
useEffect(() => {
|
||||
setTitleDraft(selectedDialogue?.title ?? "");
|
||||
setLineDrafts(
|
||||
selectedDialogue === null
|
||||
? {}
|
||||
: Object.fromEntries(
|
||||
selectedDialogue.lines.map((line) => [
|
||||
line.id,
|
||||
{
|
||||
speakerName: line.speakerName ?? "",
|
||||
text: line.text
|
||||
}
|
||||
])
|
||||
)
|
||||
);
|
||||
}, [selectedDialogueId, selectedDialogue]);
|
||||
|
||||
const commitTitle = () => {
|
||||
if (selectedDialogue === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSetDialogueTitle(selectedDialogue.id, titleDraft);
|
||||
};
|
||||
|
||||
const getLineDraft = (dialogue: ProjectDialogue, lineId: string) =>
|
||||
lineDrafts[lineId] ??
|
||||
(() => {
|
||||
const line = dialogue.lines.find((candidate) => candidate.id === lineId);
|
||||
return {
|
||||
speakerName: line?.speakerName ?? "",
|
||||
text: line?.text ?? ""
|
||||
};
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="form-section">
|
||||
<div className="label">Dialogues</div>
|
||||
{dialogueList.length === 0 ? (
|
||||
<div className="outliner-empty">
|
||||
No project dialogues authored yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="outliner-list">
|
||||
{dialogueList.map((dialogue) => (
|
||||
<div
|
||||
key={dialogue.id}
|
||||
className={`outliner-item outliner-item--compact ${
|
||||
selectedDialogue?.id === dialogue.id
|
||||
? "outliner-item--selected"
|
||||
: ""
|
||||
}`.trim()}
|
||||
>
|
||||
<div className="outliner-item__row">
|
||||
<button
|
||||
className="outliner-item__select"
|
||||
type="button"
|
||||
onClick={() => onSelectDialogue(dialogue.id)}
|
||||
>
|
||||
<span className="outliner-item__title">{dialogue.title}</span>
|
||||
<span className="outliner-item__meta">
|
||||
{dialogue.lines.length} line
|
||||
{dialogue.lines.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="outliner-item__delete"
|
||||
type="button"
|
||||
aria-label={`Delete ${dialogue.title}`}
|
||||
onClick={() => onDeleteDialogue(dialogue.id)}
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="inline-actions">
|
||||
<button className="toolbar__button" type="button" onClick={onAddDialogue}>
|
||||
Add Dialogue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{selectedDialogue === null ? (
|
||||
<div className="outliner-empty">
|
||||
Select a dialogue to edit its title and lines.
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-section">
|
||||
<label className="form-field">
|
||||
<span className="label">Title</span>
|
||||
<input
|
||||
className="text-input"
|
||||
type="text"
|
||||
value={titleDraft}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.currentTarget.value;
|
||||
setTitleDraft(nextValue);
|
||||
}}
|
||||
onBlur={commitTitle}
|
||||
onKeyDown={(event) => commitOnEnter(event, commitTitle)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="label">Lines</div>
|
||||
<div className="outliner-list">
|
||||
{selectedDialogue.lines.map((line, index) => {
|
||||
const draft = getLineDraft(selectedDialogue, line.id);
|
||||
|
||||
return (
|
||||
<div key={line.id} className="outliner-item">
|
||||
<div className="outliner-item__row">
|
||||
<div className="outliner-item__meta">{`Line ${index + 1}`}</div>
|
||||
<button
|
||||
className="outliner-item__delete"
|
||||
type="button"
|
||||
aria-label={`Delete line ${index + 1}`}
|
||||
onClick={() => onDeleteDialogueLine(selectedDialogue.id, line.id)}
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
<label className="form-field">
|
||||
<span className="label">Speaker</span>
|
||||
<input
|
||||
className="text-input"
|
||||
type="text"
|
||||
placeholder="Optional"
|
||||
value={draft.speakerName}
|
||||
onChange={(event) => {
|
||||
const nextSpeakerName = event.currentTarget.value;
|
||||
|
||||
setLineDrafts((current) => ({
|
||||
...current,
|
||||
[line.id]: {
|
||||
...draft,
|
||||
speakerName: nextSpeakerName
|
||||
}
|
||||
}));
|
||||
}}
|
||||
onBlur={() =>
|
||||
onSetDialogueLineSpeaker(
|
||||
selectedDialogue.id,
|
||||
line.id,
|
||||
draft.speakerName
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field">
|
||||
<span className="label">Text</span>
|
||||
<textarea
|
||||
className="text-input"
|
||||
rows={3}
|
||||
value={draft.text}
|
||||
onChange={(event) => {
|
||||
const nextText = event.currentTarget.value;
|
||||
|
||||
setLineDrafts((current) => ({
|
||||
...current,
|
||||
[line.id]: {
|
||||
...draft,
|
||||
text: nextText
|
||||
}
|
||||
}));
|
||||
}}
|
||||
onBlur={() =>
|
||||
onSetDialogueLineText(
|
||||
selectedDialogue.id,
|
||||
line.id,
|
||||
draft.text
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="inline-actions">
|
||||
<button
|
||||
className="toolbar__button"
|
||||
type="button"
|
||||
onClick={() => onAddDialogueLine(selectedDialogue.id)}
|
||||
>
|
||||
Add Line
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import {
|
||||
cloneProjectDialogueLibrary,
|
||||
type ProjectDialogueLibrary
|
||||
} from "../dialogues/project-dialogues";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
interface SetProjectDialoguesCommandOptions {
|
||||
label: string;
|
||||
dialogues: ProjectDialogueLibrary;
|
||||
}
|
||||
|
||||
export function createSetProjectDialoguesCommand(
|
||||
options: SetProjectDialoguesCommandOptions
|
||||
): EditorCommand {
|
||||
const nextDialogues = cloneProjectDialogueLibrary(options.dialogues);
|
||||
let previousDialogues: ProjectDialogueLibrary | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: options.label,
|
||||
execute(context) {
|
||||
const currentProjectDocument = context.getProjectDocument();
|
||||
|
||||
if (previousDialogues === null) {
|
||||
previousDialogues = cloneProjectDialogueLibrary(
|
||||
currentProjectDocument.dialogues
|
||||
);
|
||||
}
|
||||
|
||||
context.setProjectDocument({
|
||||
...currentProjectDocument,
|
||||
dialogues: cloneProjectDialogueLibrary(nextDialogues)
|
||||
});
|
||||
},
|
||||
undo(context) {
|
||||
if (previousDialogues === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProjectDocument = context.getProjectDocument();
|
||||
context.setProjectDocument({
|
||||
...currentProjectDocument,
|
||||
dialogues: cloneProjectDialogueLibrary(previousDialogues)
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ProjectDialoguesPanel } from "../../src/app/ProjectDialoguesPanel";
|
||||
import { createProjectDialogue } from "../../src/dialogues/project-dialogues";
|
||||
|
||||
describe("ProjectDialoguesPanel", () => {
|
||||
it("lets the user type into title, speaker, and text fields without crashing", () => {
|
||||
const dialogue = createProjectDialogue({
|
||||
id: "dialogue-market",
|
||||
title: "Market Greeting",
|
||||
lines: [
|
||||
{
|
||||
id: "dialogue-line-1",
|
||||
speakerName: "Merchant",
|
||||
text: "Fresh fruit."
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
render(
|
||||
<ProjectDialoguesPanel
|
||||
dialogues={{
|
||||
dialogues: {
|
||||
[dialogue.id]: dialogue
|
||||
}
|
||||
}}
|
||||
selectedDialogueId={dialogue.id}
|
||||
onSelectDialogue={() => {}}
|
||||
onAddDialogue={() => {}}
|
||||
onDeleteDialogue={() => {}}
|
||||
onSetDialogueTitle={vi.fn()}
|
||||
onAddDialogueLine={() => {}}
|
||||
onDeleteDialogueLine={() => {}}
|
||||
onSetDialogueLineSpeaker={vi.fn()}
|
||||
onSetDialogueLineText={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const titleInput = screen.getByDisplayValue("Market Greeting");
|
||||
const speakerInput = screen.getByDisplayValue("Merchant");
|
||||
const textInput = screen.getByDisplayValue("Fresh fruit.");
|
||||
|
||||
fireEvent.change(titleInput, {
|
||||
target: {
|
||||
value: "Morning Market"
|
||||
}
|
||||
});
|
||||
fireEvent.change(speakerInput, {
|
||||
target: {
|
||||
value: "Vendor"
|
||||
}
|
||||
});
|
||||
fireEvent.change(textInput, {
|
||||
target: {
|
||||
value: "Fresh fruit and bread."
|
||||
}
|
||||
});
|
||||
|
||||
expect(titleInput).toHaveValue("Morning Market");
|
||||
expect(speakerInput).toHaveValue("Vendor");
|
||||
expect(textInput).toHaveValue("Fresh fruit and bread.");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user