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 (
+
+
+
+ {/* Header */}
+
+
+ Import {mode === 'characters' ? 'Characters' : 'Variables'} from Project
+
+
+
+
+ {/* Content */}
+
+ {loading && (
+
+ Loading projects...
+
+ )}
+
+ {error && (
+
{error}
+ )}
+
+ {!loading && !error && !selectedProjectId && (
+ <>
+ {projects.length === 0 ? (
+
+ No other projects found.
+
+ ) : (
+
+
+ Select a project to import from:
+
+ {projects.map((project) => (
+
+ ))}
+
+ )}
+ >
+ )}
+
+ {loadingSource && (
+
+ Loading {mode}...
+
+ )}
+
+ {selectedProjectId && !loadingSource && (
+ <>
+ {/* Back button */}
+
+
+ {items.length === 0 ? (
+
+ This project has no {mode} defined.
+
+ ) : (
+ <>
+ {/* Select all/none controls */}
+
+
+ {selectedIds.size} of {items.length} selected
+
+
+
+
+
+ {/* Item list with checkboxes */}
+
+
+ {/* Warnings */}
+ {warnings.length > 0 && (
+
+
+ Import warnings:
+
+ {warnings.map((warning, i) => (
+
+ {warning}
+
+ ))}
+
+ )}
+ >
+ )}
+ >
+ )}
+
+
+ {/* Footer with import button */}
+ {selectedProjectId && !loadingSource && items.length > 0 && (
+
+
+
+
+ )}
+
+
+ )
+}
diff --git a/src/components/editor/ProjectSettingsModal.tsx b/src/components/editor/ProjectSettingsModal.tsx
index 4add2f7..b0e64fb 100644
--- a/src/components/editor/ProjectSettingsModal.tsx
+++ b/src/components/editor/ProjectSettingsModal.tsx
@@ -3,10 +3,14 @@
import { useState } from 'react'
import { nanoid } from 'nanoid'
import type { Character, Variable } from '@/types/flowchart'
+import ImportFromProjectModal from './ImportFromProjectModal'
type Tab = 'characters' | 'variables'
+type ImportModalState = { open: boolean; mode: 'characters' | 'variables' }
+
type ProjectSettingsModalProps = {
+ projectId: string
characters: Character[]
variables: Variable[]
onCharactersChange: (characters: Character[]) => void
@@ -26,6 +30,7 @@ function randomHexColor(): string {
}
export default function ProjectSettingsModal({
+ projectId,
characters,
variables,
onCharactersChange,
@@ -35,6 +40,15 @@ export default function ProjectSettingsModal({
getVariableUsageCount,
}: ProjectSettingsModalProps) {
const [activeTab, setActiveTab] = useState('characters')
+ const [importModal, setImportModal] = useState({ open: false, mode: 'characters' })
+
+ const handleImportCharacters = (imported: Character[]) => {
+ onCharactersChange([...characters, ...imported])
+ }
+
+ const handleImportVariables = (imported: Variable[]) => {
+ onVariablesChange([...variables, ...imported])
+ }
return (
@@ -90,6 +104,7 @@ export default function ProjectSettingsModal({
characters={characters}
onChange={onCharactersChange}
getUsageCount={getCharacterUsageCount}
+ onImport={() => setImportModal({ open: true, mode: 'characters' })}
/>
)}
{activeTab === 'variables' && (
@@ -97,10 +112,23 @@ export default function ProjectSettingsModal({
variables={variables}
onChange={onVariablesChange}
getUsageCount={getVariableUsageCount}
+ onImport={() => setImportModal({ open: true, mode: 'variables' })}
/>
)}
+
+ {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 && (
-
+
+
+
+
)}