gestion des chapitres modification et réoganisation
This commit is contained in:
BIN
.next/dev/cache/turbopack/23c46498/CURRENT
vendored
BIN
.next/dev/cache/turbopack/23c46498/CURRENT
vendored
Binary file not shown.
6
.next/dev/cache/turbopack/23c46498/LOG
vendored
6
.next/dev/cache/turbopack/23c46498/LOG
vendored
@@ -6116,3 +6116,9 @@ FAM | META SEQ | SST SEQ | RANGE
|
|||||||
0 | 00013985 | 00013984 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
|
0 | 00013985 | 00013984 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
|
||||||
1 | 00013986 | 00013982 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
|
1 | 00013986 | 00013982 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
|
||||||
2 | 00013987 | 00013983 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
|
2 | 00013987 | 00013983 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
|
||||||
|
Time 2026-03-05T17:21:32.7902721Z
|
||||||
|
Commit 00014935 4 keys in 16ms 322µs 500ns
|
||||||
|
FAM | META SEQ | SST SEQ | RANGE
|
||||||
|
0 | 00014933 | 00014932 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
|
||||||
|
1 | 00014934 | 00014930 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
|
||||||
|
2 | 00014935 | 00014931 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
49
src/app/api/projects/[id]/chapters/reorder/route.ts
Normal file
49
src/app/api/projects/[id]/chapters/reorder/route.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { auth } from '@/lib/auth';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function PUT(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: projectId } = await params;
|
||||||
|
const body = await request.json();
|
||||||
|
const { chapters } = body as { chapters: { id: string, orderIndex: number }[] };
|
||||||
|
|
||||||
|
if (!Array.isArray(chapters)) {
|
||||||
|
return NextResponse.json({ error: 'Format invalide' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ownership
|
||||||
|
const existing = await prisma.project.findFirst({
|
||||||
|
where: { id: projectId, userId: session.user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return NextResponse.json({ error: 'Projet non trouvé' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Bulk update chapters' orderIndex within a transaction
|
||||||
|
await prisma.$transaction(
|
||||||
|
chapters.map((chapter) =>
|
||||||
|
prisma.chapter.update({
|
||||||
|
where: { id: chapter.id, projectId },
|
||||||
|
data: { orderIndex: chapter.orderIndex },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la réorganisation des chapitres:', error);
|
||||||
|
return NextResponse.json({ error: 'Erreur interne' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,8 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
|
|||||||
updateProject, updateChapter, addChapter,
|
updateProject, updateChapter, addChapter,
|
||||||
createEntity, updateEntity, deleteEntity,
|
createEntity, updateEntity, deleteEntity,
|
||||||
createIdea, updateIdea, deleteIdea,
|
createIdea, updateIdea, deleteIdea,
|
||||||
deleteProject
|
deleteProject,
|
||||||
|
reorderChapters
|
||||||
} = useProjects(user);
|
} = useProjects(user);
|
||||||
const { chatHistory, isGenerating, sendMessage } = useChat();
|
const { chatHistory, isGenerating, sendMessage } = useChat();
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle early returns for non-existent projects
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full flex flex-col items-center justify-center bg-slate-900 text-white">
|
<div className="h-screen w-full flex flex-col items-center justify-center bg-slate-900 text-white">
|
||||||
@@ -129,6 +131,8 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
|
|||||||
onViewModeChange={handleViewModeChange}
|
onViewModeChange={handleViewModeChange}
|
||||||
onChapterSelect={(id) => { setCurrentChapterId(id); router.push(`/project/${projectId}`); }}
|
onChapterSelect={(id) => { setCurrentChapterId(id); router.push(`/project/${projectId}`); }}
|
||||||
onUpdateProject={(updates) => updateProject(projectId, updates)}
|
onUpdateProject={(updates) => updateProject(projectId, updates)}
|
||||||
|
onUpdateChapter={(chapterId, data) => updateChapter(projectId, chapterId, data)}
|
||||||
|
onReorderChapters={(chapters) => reorderChapters(projectId, chapters)}
|
||||||
onAddChapter={async () => {
|
onAddChapter={async () => {
|
||||||
const id = await addChapter(projectId, {});
|
const id = await addChapter(projectId, {});
|
||||||
if (id) {
|
if (id) {
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ interface AppRouterProps {
|
|||||||
onUpdateProfile: (updates: Partial<UserProfile>) => void;
|
onUpdateProfile: (updates: Partial<UserProfile>) => void;
|
||||||
onUpgradePlan: (plan: any) => void;
|
onUpgradePlan: (plan: any) => void;
|
||||||
onSendMessage: (msg: string) => void;
|
onSendMessage: (msg: string) => void;
|
||||||
onIncrementUsage: () => void;
|
|
||||||
onDeleteProject: (projectId: string) => void;
|
onDeleteProject: (projectId: string) => void;
|
||||||
|
onReorderChapters: (projectId: string, chapters: { id: string, orderIndex: number }[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppRouter: React.FC<AppRouterProps> = (props) => {
|
const AppRouter: React.FC<AppRouterProps> = (props) => {
|
||||||
@@ -139,6 +139,7 @@ const AppRouter: React.FC<AppRouterProps> = (props) => {
|
|||||||
onViewModeChange={props.onViewModeChange}
|
onViewModeChange={props.onViewModeChange}
|
||||||
onChapterSelect={(id) => { setCurrentChapterId(id); props.onViewModeChange('write'); }}
|
onChapterSelect={(id) => { setCurrentChapterId(id); props.onViewModeChange('write'); }}
|
||||||
onUpdateProject={props.onUpdateProject}
|
onUpdateProject={props.onUpdateProject}
|
||||||
|
onReorderChapters={(chapters) => props.onReorderChapters(project.id, chapters)}
|
||||||
onAddChapter={async () => {
|
onAddChapter={async () => {
|
||||||
console.log("[AppRouter] onAddChapter triggered");
|
console.log("[AppRouter] onAddChapter triggered");
|
||||||
const id = await props.onAddChapter();
|
const id = await props.onAddChapter();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { BookProject, UserProfile, ViewMode, ChatMessage } from '@/lib/types';
|
import { BookProject, UserProfile, ViewMode, ChatMessage, Chapter } from '@/lib/types';
|
||||||
import AIPanel from '@/components/AIPanel';
|
import AIPanel from '@/components/AIPanel';
|
||||||
import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2, Wand2 } from 'lucide-react';
|
import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2, Wand2 } from 'lucide-react';
|
||||||
import { useLanguage } from '@/providers/LanguageProvider';
|
import { useLanguage } from '@/providers/LanguageProvider';
|
||||||
@@ -18,6 +18,8 @@ interface EditorShellProps {
|
|||||||
onViewModeChange: (mode: ViewMode) => void;
|
onViewModeChange: (mode: ViewMode) => void;
|
||||||
onChapterSelect: (id: string) => void;
|
onChapterSelect: (id: string) => void;
|
||||||
onUpdateProject: (updates: Partial<BookProject>) => void;
|
onUpdateProject: (updates: Partial<BookProject>) => void;
|
||||||
|
onUpdateChapter?: (chapterId: string, updates: Partial<Chapter>) => void;
|
||||||
|
onReorderChapters?: (chapters: { id: string, orderIndex: number }[]) => void;
|
||||||
onAddChapter: () => Promise<void>;
|
onAddChapter: () => Promise<void>;
|
||||||
onDeleteChapter: (id: string) => void;
|
onDeleteChapter: (id: string) => void;
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
@@ -32,10 +34,28 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
|
|||||||
const { project, user, viewMode, currentChapterId, children } = props;
|
const { project, user, viewMode, currentChapterId, children } = props;
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||||
const [isAiPanelOpen, setIsAiPanelOpen] = useState(true);
|
const [isAiPanelOpen, setIsAiPanelOpen] = useState(true);
|
||||||
|
const [draggedChapterIdx, setDraggedChapterIdx] = useState<number | null>(null);
|
||||||
|
const [dragOverChapterIdx, setDragOverChapterIdx] = useState<number | null>(null);
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
const currentChapter = project.chapters.find(c => c.id === currentChapterId);
|
const currentChapter = project.chapters.find(c => c.id === currentChapterId);
|
||||||
|
|
||||||
|
// Local state for debounced title input
|
||||||
|
const [localTitle, setLocalTitle] = useState(currentChapter?.title || "");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only update local title from props if it differs from what's currently in state,
|
||||||
|
// to prevent overwriting while the user is typing, but still catch external updates (e.g. from DB load)
|
||||||
|
if (currentChapter?.title && currentChapter.title !== localTitle) {
|
||||||
|
setLocalTitle(currentChapter.title);
|
||||||
|
} else if (!currentChapter) {
|
||||||
|
setLocalTitle("");
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentChapter?.title, currentChapterId]);
|
||||||
|
|
||||||
|
const titleDebounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Auto-close sidebars on mobile when navigating
|
// Auto-close sidebars on mobile when navigating
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined' && window.innerWidth < 1024) {
|
if (typeof window !== 'undefined' && window.innerWidth < 1024) {
|
||||||
@@ -81,14 +101,63 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
|
|||||||
{t('nav.chapters')} <button onClick={props.onAddChapter} className="hover:text-blue-400"><Plus size={14} /></button>
|
{t('nav.chapters')} <button onClick={props.onAddChapter} className="hover:text-blue-400"><Plus size={14} /></button>
|
||||||
</div>
|
</div>
|
||||||
{project.chapters.map((chap, idx) => (
|
{project.chapters.map((chap, idx) => (
|
||||||
<div key={chap.id} className="group relative">
|
<div
|
||||||
|
key={chap.id}
|
||||||
|
className={`group relative transition-all duration-200 border-y-2 ${draggedChapterIdx === idx ? 'opacity-20 scale-95 border-transparent' :
|
||||||
|
dragOverChapterIdx === idx ? (
|
||||||
|
draggedChapterIdx !== null && draggedChapterIdx > idx
|
||||||
|
? 'border-t-blue-500 border-b-transparent bg-blue-900/20'
|
||||||
|
: 'border-b-blue-500 border-t-transparent bg-blue-900/20'
|
||||||
|
) : 'border-transparent'
|
||||||
|
}`}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => {
|
||||||
|
setDraggedChapterIdx(idx);
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
}}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
}}
|
||||||
|
onDragEnter={() => {
|
||||||
|
if (draggedChapterIdx !== null && draggedChapterIdx !== idx) {
|
||||||
|
setDragOverChapterIdx(idx);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDragLeave={() => {
|
||||||
|
// optional edge smoothing here
|
||||||
|
}}
|
||||||
|
onDragEnd={() => {
|
||||||
|
setDraggedChapterIdx(null);
|
||||||
|
setDragOverChapterIdx(null);
|
||||||
|
}}
|
||||||
|
onDrop={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (draggedChapterIdx === null || draggedChapterIdx === idx) return;
|
||||||
|
|
||||||
|
const newChapters = [...project.chapters];
|
||||||
|
const [removed] = newChapters.splice(draggedChapterIdx, 1);
|
||||||
|
newChapters.splice(idx, 0, removed);
|
||||||
|
|
||||||
|
setDraggedChapterIdx(null);
|
||||||
|
setDragOverChapterIdx(null);
|
||||||
|
|
||||||
|
// Re-index chapters to match new visual order
|
||||||
|
const updatedChapters = newChapters.map((c, i) => ({ id: c.id, orderIndex: i }));
|
||||||
|
if (props.onReorderChapters) {
|
||||||
|
props.onReorderChapters(updatedChapters);
|
||||||
|
} else {
|
||||||
|
props.onUpdateProject({ chapters: newChapters.map((c, i) => ({ ...c, orderIndex: i })) });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => props.onChapterSelect(chap.id)}
|
onClick={() => props.onChapterSelect(chap.id)}
|
||||||
className={`w-full text-left px-4 py-2 text-sm truncate transition-colors ${currentChapterId === chap.id && viewMode === 'write' ? 'bg-blue-900 text-white border-r-2 border-blue-400' : 'hover:bg-slate-800'}`}
|
className={`w-full text-left px-4 py-2 text-sm truncate transition-colors cursor-grab active:cursor-grabbing ${currentChapterId === chap.id && viewMode === 'write' ? 'bg-blue-900 text-white border-r-2 border-blue-400' : 'hover:bg-slate-800'} ${draggedChapterIdx !== null ? 'pointer-events-none' : ''}`}
|
||||||
>
|
>
|
||||||
{idx + 1}. {chap.title}
|
{idx + 1}. {chap.title}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.onDeleteChapter(chap.id)} className="absolute right-2 top-2 text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100"><Trash2 size={14} /></button>
|
<button onClick={() => props.onDeleteChapter(chap.id)} className="absolute right-2 top-2 text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 bg-slate-900/50 backdrop-blur rounded p-1"><Trash2 size={12} /></button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -123,8 +192,28 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
|
|||||||
{viewMode === 'write' ? (
|
{viewMode === 'write' ? (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={currentChapter?.title || ""}
|
value={localTitle}
|
||||||
onChange={(e) => props.onUpdateProject({ chapters: project.chapters.map(c => c.id === currentChapterId ? { ...c, title: e.target.value } : c) })}
|
onChange={(e) => {
|
||||||
|
const newTitle = e.target.value;
|
||||||
|
setLocalTitle(newTitle);
|
||||||
|
|
||||||
|
if (currentChapterId.startsWith('placeholder-')) return;
|
||||||
|
|
||||||
|
// Local optimistic update via project for UI consistency
|
||||||
|
props.onUpdateProject({
|
||||||
|
chapters: project.chapters.map(c =>
|
||||||
|
c.id === currentChapterId ? { ...c, title: newTitle } : c
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Debounce the actual backend save
|
||||||
|
if (titleDebounceRef.current) clearTimeout(titleDebounceRef.current);
|
||||||
|
titleDebounceRef.current = setTimeout(() => {
|
||||||
|
if (props.onUpdateChapter) {
|
||||||
|
props.onUpdateChapter(currentChapterId, { title: newTitle });
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}}
|
||||||
className="font-serif font-bold text-lg bg-transparent border-b border-transparent focus:border-blue-500 focus:outline-none"
|
className="font-serif font-bold text-lg bg-transparent border-b border-transparent focus:border-blue-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ export const useProjects = (user: UserProfile | null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateChapter = async (projectId: string, chapterId: string, data: Partial<Chapter>) => {
|
const updateChapter = async (projectId: string, chapterId: string, data: Partial<Chapter>) => {
|
||||||
|
if (chapterId.startsWith('placeholder-')) return;
|
||||||
|
|
||||||
setProjects(prev => prev.map(p => {
|
setProjects(prev => prev.map(p => {
|
||||||
if (p.id !== projectId) return p;
|
if (p.id !== projectId) return p;
|
||||||
return {
|
return {
|
||||||
@@ -228,6 +230,34 @@ export const useProjects = (user: UserProfile | null) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reorderChapters = async (projectId: string, chaptersToReorder: { id: string, orderIndex: number }[]) => {
|
||||||
|
// Optimistic UI update
|
||||||
|
setProjects(prev => prev.map(p => {
|
||||||
|
if (p.id !== projectId) return p;
|
||||||
|
|
||||||
|
// Map the new order indexes to the existing chapters
|
||||||
|
const orderMap = new Map(chaptersToReorder.map(c => [c.id, c.orderIndex]));
|
||||||
|
const updatedChapters = p.chapters.map(c => {
|
||||||
|
if (orderMap.has(c.id)) {
|
||||||
|
return { ...c, orderIndex: orderMap.get(c.id)! };
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort them immediately so the UI doesn't jump on next render
|
||||||
|
updatedChapters.sort((a, b) => (a.orderIndex || 0) - (b.orderIndex || 0));
|
||||||
|
|
||||||
|
return { ...p, chapters: updatedChapters };
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.chapters.reorder(projectId, chaptersToReorder);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to reorder chapters", err);
|
||||||
|
// In a real app we might revert the state here on failure
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const createEntity = async (projectId: string, type: EntityType, initialData?: Partial<Entity>) => {
|
const createEntity = async (projectId: string, type: EntityType, initialData?: Partial<Entity>) => {
|
||||||
try {
|
try {
|
||||||
const newEntity = await api.entities.create({
|
const newEntity = await api.entities.create({
|
||||||
@@ -363,6 +393,7 @@ export const useProjects = (user: UserProfile | null) => {
|
|||||||
updateProject,
|
updateProject,
|
||||||
addChapter,
|
addChapter,
|
||||||
updateChapter,
|
updateChapter,
|
||||||
|
reorderChapters,
|
||||||
createEntity,
|
createEntity,
|
||||||
updateEntity,
|
updateEntity,
|
||||||
deleteEntity,
|
deleteEntity,
|
||||||
|
|||||||
@@ -113,6 +113,13 @@ const api = {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async reorder(projectId: string, chapters: { id: string, orderIndex: number }[]) {
|
||||||
|
return api.request(`/projects/${projectId}/chapters/reorder`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ chapters }),
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- ENTITIES ---
|
// --- ENTITIES ---
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export interface Chapter {
|
|||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
|
orderIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlotNodeType = 'story' | 'dialogue' | 'action';
|
export type PlotNodeType = 'story' | 'dialogue' | 'action';
|
||||||
|
|||||||
Reference in New Issue
Block a user