diff --git a/src/app/editor/[projectId]/FlowchartEditor.tsx b/src/app/editor/[projectId]/FlowchartEditor.tsx index 284a6ed..f468ed6 100644 --- a/src/app/editor/[projectId]/FlowchartEditor.tsx +++ b/src/app/editor/[projectId]/FlowchartEditor.tsx @@ -201,7 +201,7 @@ function computeMigration(initialData: FlowchartData, shouldMigrate: boolean) { } // Inner component that uses useReactFlow hook -function FlowchartEditorInner({ initialData, needsMigration }: FlowchartEditorProps) { +function FlowchartEditorInner({ projectId, initialData, needsMigration }: FlowchartEditorProps) { // Define custom node types - memoized to prevent re-renders const nodeTypes: NodeTypes = useMemo( () => ({ @@ -431,6 +431,7 @@ function FlowchartEditorInner({ initialData, needsMigration }: FlowchartEditorPr {showSettings && ( void + onImportVariables: (variables: Variable[]) => void + onClose: () => void +} + +export default function ImportFromProjectModal({ + mode, + currentProjectId, + existingCharacters, + existingVariables, + onImportCharacters, + onImportVariables, + onClose, +}: ImportFromProjectModalProps) { + const [projects, setProjects] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [selectedProjectId, setSelectedProjectId] = useState(null) + const [sourceCharacters, setSourceCharacters] = useState([]) + const [sourceVariables, setSourceVariables] = useState([]) + const [loadingSource, setLoadingSource] = useState(false) + const [selectedIds, setSelectedIds] = useState>(new Set()) + const [warnings, setWarnings] = useState([]) + + // Load user's projects on mount + useEffect(() => { + async function fetchProjects() { + const supabase = createClient() + const { data, error: fetchError } = await supabase + .from('projects') + .select('id, name') + .neq('id', currentProjectId) + .order('name') + + if (fetchError) { + setError('Failed to load projects') + setLoading(false) + return + } + + setProjects(data || []) + setLoading(false) + } + + fetchProjects() + }, [currentProjectId]) + + // Load source project's characters/variables when a project is selected + const handleSelectProject = async (projectId: string) => { + setSelectedProjectId(projectId) + setLoadingSource(true) + setWarnings([]) + setSelectedIds(new Set()) + + const supabase = createClient() + const { data, error: fetchError } = await supabase + .from('projects') + .select('flowchart_data') + .eq('id', projectId) + .single() + + if (fetchError || !data) { + setError('Failed to load project data') + setLoadingSource(false) + return + } + + const flowchartData = data.flowchart_data || {} + const chars: Character[] = flowchartData.characters || [] + const vars: Variable[] = flowchartData.variables || [] + + setSourceCharacters(chars) + setSourceVariables(vars) + setLoadingSource(false) + + // Select all by default + const items = mode === 'characters' ? chars : vars + setSelectedIds(new Set(items.map((item) => item.id))) + } + + const handleToggleItem = (id: string) => { + setSelectedIds((prev) => { + const next = new Set(prev) + if (next.has(id)) { + next.delete(id) + } else { + next.add(id) + } + return next + }) + } + + const handleSelectAll = () => { + const items = mode === 'characters' ? sourceCharacters : sourceVariables + setSelectedIds(new Set(items.map((item) => item.id))) + } + + const handleSelectNone = () => { + setSelectedIds(new Set()) + } + + const handleImport = () => { + const importWarnings: string[] = [] + + if (mode === 'characters') { + const selectedCharacters = sourceCharacters.filter((c) => selectedIds.has(c.id)) + const existingNames = new Set(existingCharacters.map((c) => c.name.toLowerCase())) + const toImport: Character[] = [] + + for (const char of selectedCharacters) { + if (existingNames.has(char.name.toLowerCase())) { + importWarnings.push(`Skipped "${char.name}" (already exists)`) + } else { + // Create new ID to avoid conflicts + toImport.push({ ...char, id: nanoid() }) + existingNames.add(char.name.toLowerCase()) + } + } + + if (importWarnings.length > 0) { + setWarnings(importWarnings) + } + + if (toImport.length > 0) { + onImportCharacters(toImport) + } + + if (importWarnings.length === 0) { + onClose() + } + } else { + const selectedVariables = sourceVariables.filter((v) => selectedIds.has(v.id)) + const existingNames = new Set(existingVariables.map((v) => v.name.toLowerCase())) + const toImport: Variable[] = [] + + for (const variable of selectedVariables) { + if (existingNames.has(variable.name.toLowerCase())) { + importWarnings.push(`Skipped "${variable.name}" (already exists)`) + } else { + // Create new ID to avoid conflicts + toImport.push({ ...variable, id: nanoid() }) + existingNames.add(variable.name.toLowerCase()) + } + } + + if (importWarnings.length > 0) { + setWarnings(importWarnings) + } + + if (toImport.length > 0) { + onImportVariables(toImport) + } + + if (importWarnings.length === 0) { + onClose() + } + } + } + + const items = mode === 'characters' ? sourceCharacters : sourceVariables + + return ( +
+ + + {importModal.open && ( + setImportModal({ open: false, mode: importModal.mode })} + /> + )}
) } @@ -110,6 +138,7 @@ type CharactersTabProps = { characters: Character[] onChange: (characters: Character[]) => void getUsageCount: (characterId: string) => number + onImport: () => void } type CharacterFormData = { @@ -118,7 +147,7 @@ type CharacterFormData = { description: string } -function CharactersTab({ characters, onChange, getUsageCount }: CharactersTabProps) { +function CharactersTab({ characters, onChange, getUsageCount, onImport }: CharactersTabProps) { const [isAdding, setIsAdding] = useState(false) const [editingId, setEditingId] = useState(null) const [formData, setFormData] = useState({ name: '', color: randomHexColor(), description: '' }) @@ -207,12 +236,20 @@ function CharactersTab({ characters, onChange, getUsageCount }: CharactersTabPro Define characters that can be referenced in dialogue nodes.

{!isAdding && !editingId && ( - +
+ + +
)} @@ -378,6 +415,7 @@ type VariablesTabProps = { variables: Variable[] onChange: (variables: Variable[]) => void getUsageCount: (variableId: string) => number + onImport: () => void } type VariableType = 'numeric' | 'string' | 'boolean' @@ -406,7 +444,7 @@ function parseInitialValue(type: VariableType, raw: string): number | string | b } } -function VariablesTab({ variables, onChange, getUsageCount }: VariablesTabProps) { +function VariablesTab({ variables, onChange, getUsageCount, onImport }: VariablesTabProps) { const [isAdding, setIsAdding] = useState(false) const [editingId, setEditingId] = useState(null) const [formData, setFormData] = useState({ name: '', type: 'numeric', initialValue: '0', description: '' }) @@ -503,12 +541,20 @@ function VariablesTab({ variables, onChange, getUsageCount }: VariablesTabProps) Define variables that can be referenced in variable nodes and edge conditions.

{!isAdding && !editingId && ( - +
+ + +
)}