feat: [US-063] - Import characters/variables from another project
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
49698dd9a9
commit
a3e1d1cea2
|
|
@ -201,7 +201,7 @@ function computeMigration(initialData: FlowchartData, shouldMigrate: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inner component that uses useReactFlow hook
|
// 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
|
// Define custom node types - memoized to prevent re-renders
|
||||||
const nodeTypes: NodeTypes = useMemo(
|
const nodeTypes: NodeTypes = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -431,6 +431,7 @@ function FlowchartEditorInner({ initialData, needsMigration }: FlowchartEditorPr
|
||||||
</div>
|
</div>
|
||||||
{showSettings && (
|
{showSettings && (
|
||||||
<ProjectSettingsModal
|
<ProjectSettingsModal
|
||||||
|
projectId={projectId}
|
||||||
characters={characters}
|
characters={characters}
|
||||||
variables={variables}
|
variables={variables}
|
||||||
onCharactersChange={setCharacters}
|
onCharactersChange={setCharacters}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,386 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import { createClient } from '@/lib/supabase/client'
|
||||||
|
import type { Character, Variable } from '@/types/flowchart'
|
||||||
|
|
||||||
|
type ImportMode = 'characters' | 'variables'
|
||||||
|
|
||||||
|
type ProjectListItem = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImportFromProjectModalProps = {
|
||||||
|
mode: ImportMode
|
||||||
|
currentProjectId: string
|
||||||
|
existingCharacters: Character[]
|
||||||
|
existingVariables: Variable[]
|
||||||
|
onImportCharacters: (characters: Character[]) => void
|
||||||
|
onImportVariables: (variables: Variable[]) => void
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImportFromProjectModal({
|
||||||
|
mode,
|
||||||
|
currentProjectId,
|
||||||
|
existingCharacters,
|
||||||
|
existingVariables,
|
||||||
|
onImportCharacters,
|
||||||
|
onImportVariables,
|
||||||
|
onClose,
|
||||||
|
}: ImportFromProjectModalProps) {
|
||||||
|
const [projects, setProjects] = useState<ProjectListItem[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||||
|
const [sourceCharacters, setSourceCharacters] = useState<Character[]>([])
|
||||||
|
const [sourceVariables, setSourceVariables] = useState<Variable[]>([])
|
||||||
|
const [loadingSource, setLoadingSource] = useState(false)
|
||||||
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||||
|
const [warnings, setWarnings] = useState<string[]>([])
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/50"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div className="relative flex max-h-[80vh] w-full max-w-lg flex-col rounded-lg bg-white shadow-xl dark:bg-zinc-800">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-700">
|
||||||
|
<h3 className="text-base font-semibold text-zinc-900 dark:text-zinc-50">
|
||||||
|
Import {mode === 'characters' ? 'Characters' : 'Variables'} from Project
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="rounded p-1 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:hover:text-zinc-200"
|
||||||
|
>
|
||||||
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||||
|
{loading && (
|
||||||
|
<p className="py-8 text-center text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
|
Loading projects...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="py-4 text-center text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && !selectedProjectId && (
|
||||||
|
<>
|
||||||
|
{projects.length === 0 ? (
|
||||||
|
<p className="py-8 text-center text-sm text-zinc-400 dark:text-zinc-500">
|
||||||
|
No other projects found.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="mb-3 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
|
Select a project to import from:
|
||||||
|
</p>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<button
|
||||||
|
key={project.id}
|
||||||
|
onClick={() => handleSelectProject(project.id)}
|
||||||
|
className="flex w-full items-center rounded-lg border border-zinc-200 px-4 py-3 text-left text-sm font-medium text-zinc-900 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-50 dark:hover:bg-zinc-700/50"
|
||||||
|
>
|
||||||
|
<svg className="mr-3 h-4 w-4 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||||
|
</svg>
|
||||||
|
{project.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loadingSource && (
|
||||||
|
<p className="py-8 text-center text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
|
Loading {mode}...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedProjectId && !loadingSource && (
|
||||||
|
<>
|
||||||
|
{/* Back button */}
|
||||||
|
<button
|
||||||
|
onClick={() => { setSelectedProjectId(null); setWarnings([]) }}
|
||||||
|
className="mb-3 flex items-center gap-1 text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Back to projects
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<p className="py-8 text-center text-sm text-zinc-400 dark:text-zinc-500">
|
||||||
|
This project has no {mode} defined.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Select all/none controls */}
|
||||||
|
<div className="mb-3 flex items-center gap-3">
|
||||||
|
<span className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
|
{selectedIds.size} of {items.length} selected
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleSelectAll}
|
||||||
|
className="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
Select all
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSelectNone}
|
||||||
|
className="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
Select none
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Item list with checkboxes */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{mode === 'characters'
|
||||||
|
? sourceCharacters.map((char) => (
|
||||||
|
<label
|
||||||
|
key={char.id}
|
||||||
|
className="flex cursor-pointer items-center gap-3 rounded-lg border border-zinc-200 px-4 py-2.5 hover:bg-zinc-50 dark:border-zinc-700 dark:hover:bg-zinc-700/50"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedIds.has(char.id)}
|
||||||
|
onChange={() => handleToggleItem(char.id)}
|
||||||
|
className="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-600"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-3.5 w-3.5 rounded-full"
|
||||||
|
style={{ backgroundColor: char.color }}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-zinc-900 dark:text-zinc-50">
|
||||||
|
{char.name}
|
||||||
|
</span>
|
||||||
|
{char.description && (
|
||||||
|
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
|
- {char.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
))
|
||||||
|
: sourceVariables.map((variable) => (
|
||||||
|
<label
|
||||||
|
key={variable.id}
|
||||||
|
className="flex cursor-pointer items-center gap-3 rounded-lg border border-zinc-200 px-4 py-2.5 hover:bg-zinc-50 dark:border-zinc-700 dark:hover:bg-zinc-700/50"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedIds.has(variable.id)}
|
||||||
|
onChange={() => handleToggleItem(variable.id)}
|
||||||
|
className="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-600"
|
||||||
|
/>
|
||||||
|
<span className={`rounded px-1.5 py-0.5 text-xs font-medium ${
|
||||||
|
variable.type === 'numeric'
|
||||||
|
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||||
|
: variable.type === 'string'
|
||||||
|
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
|
||||||
|
: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
|
||||||
|
}`}>
|
||||||
|
{variable.type}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-zinc-900 dark:text-zinc-50">
|
||||||
|
{variable.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
|
(initial: {String(variable.initialValue)})
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warnings */}
|
||||||
|
{warnings.length > 0 && (
|
||||||
|
<div className="mt-3 rounded-lg border border-orange-200 bg-orange-50 p-3 dark:border-orange-800 dark:bg-orange-900/20">
|
||||||
|
<p className="mb-1 text-xs font-medium text-orange-700 dark:text-orange-300">
|
||||||
|
Import warnings:
|
||||||
|
</p>
|
||||||
|
{warnings.map((warning, i) => (
|
||||||
|
<p key={i} className="text-xs text-orange-600 dark:text-orange-400">
|
||||||
|
{warning}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer with import button */}
|
||||||
|
{selectedProjectId && !loadingSource && items.length > 0 && (
|
||||||
|
<div className="flex items-center justify-end gap-3 border-t border-zinc-200 px-6 py-3 dark:border-zinc-700">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="rounded-md border border-zinc-300 bg-white px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-600"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={selectedIds.size === 0}
|
||||||
|
className="rounded-md bg-blue-500 px-4 py-2 text-sm font-medium text-white hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
||||||
|
>
|
||||||
|
Import {selectedIds.size > 0 ? `(${selectedIds.size})` : ''}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,14 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import type { Character, Variable } from '@/types/flowchart'
|
import type { Character, Variable } from '@/types/flowchart'
|
||||||
|
import ImportFromProjectModal from './ImportFromProjectModal'
|
||||||
|
|
||||||
type Tab = 'characters' | 'variables'
|
type Tab = 'characters' | 'variables'
|
||||||
|
|
||||||
|
type ImportModalState = { open: boolean; mode: 'characters' | 'variables' }
|
||||||
|
|
||||||
type ProjectSettingsModalProps = {
|
type ProjectSettingsModalProps = {
|
||||||
|
projectId: string
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
variables: Variable[]
|
variables: Variable[]
|
||||||
onCharactersChange: (characters: Character[]) => void
|
onCharactersChange: (characters: Character[]) => void
|
||||||
|
|
@ -26,6 +30,7 @@ function randomHexColor(): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectSettingsModal({
|
export default function ProjectSettingsModal({
|
||||||
|
projectId,
|
||||||
characters,
|
characters,
|
||||||
variables,
|
variables,
|
||||||
onCharactersChange,
|
onCharactersChange,
|
||||||
|
|
@ -35,6 +40,15 @@ export default function ProjectSettingsModal({
|
||||||
getVariableUsageCount,
|
getVariableUsageCount,
|
||||||
}: ProjectSettingsModalProps) {
|
}: ProjectSettingsModalProps) {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('characters')
|
const [activeTab, setActiveTab] = useState<Tab>('characters')
|
||||||
|
const [importModal, setImportModal] = useState<ImportModalState>({ open: false, mode: 'characters' })
|
||||||
|
|
||||||
|
const handleImportCharacters = (imported: Character[]) => {
|
||||||
|
onCharactersChange([...characters, ...imported])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportVariables = (imported: Variable[]) => {
|
||||||
|
onVariablesChange([...variables, ...imported])
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
|
@ -90,6 +104,7 @@ export default function ProjectSettingsModal({
|
||||||
characters={characters}
|
characters={characters}
|
||||||
onChange={onCharactersChange}
|
onChange={onCharactersChange}
|
||||||
getUsageCount={getCharacterUsageCount}
|
getUsageCount={getCharacterUsageCount}
|
||||||
|
onImport={() => setImportModal({ open: true, mode: 'characters' })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === 'variables' && (
|
{activeTab === 'variables' && (
|
||||||
|
|
@ -97,10 +112,23 @@ export default function ProjectSettingsModal({
|
||||||
variables={variables}
|
variables={variables}
|
||||||
onChange={onVariablesChange}
|
onChange={onVariablesChange}
|
||||||
getUsageCount={getVariableUsageCount}
|
getUsageCount={getVariableUsageCount}
|
||||||
|
onImport={() => setImportModal({ open: true, mode: 'variables' })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{importModal.open && (
|
||||||
|
<ImportFromProjectModal
|
||||||
|
mode={importModal.mode}
|
||||||
|
currentProjectId={projectId}
|
||||||
|
existingCharacters={characters}
|
||||||
|
existingVariables={variables}
|
||||||
|
onImportCharacters={handleImportCharacters}
|
||||||
|
onImportVariables={handleImportVariables}
|
||||||
|
onClose={() => setImportModal({ open: false, mode: importModal.mode })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -110,6 +138,7 @@ type CharactersTabProps = {
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
onChange: (characters: Character[]) => void
|
onChange: (characters: Character[]) => void
|
||||||
getUsageCount: (characterId: string) => number
|
getUsageCount: (characterId: string) => number
|
||||||
|
onImport: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type CharacterFormData = {
|
type CharacterFormData = {
|
||||||
|
|
@ -118,7 +147,7 @@ type CharacterFormData = {
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function CharactersTab({ characters, onChange, getUsageCount }: CharactersTabProps) {
|
function CharactersTab({ characters, onChange, getUsageCount, onImport }: CharactersTabProps) {
|
||||||
const [isAdding, setIsAdding] = useState(false)
|
const [isAdding, setIsAdding] = useState(false)
|
||||||
const [editingId, setEditingId] = useState<string | null>(null)
|
const [editingId, setEditingId] = useState<string | null>(null)
|
||||||
const [formData, setFormData] = useState<CharacterFormData>({ name: '', color: randomHexColor(), description: '' })
|
const [formData, setFormData] = useState<CharacterFormData>({ name: '', color: randomHexColor(), description: '' })
|
||||||
|
|
@ -207,12 +236,20 @@ function CharactersTab({ characters, onChange, getUsageCount }: CharactersTabPro
|
||||||
Define characters that can be referenced in dialogue nodes.
|
Define characters that can be referenced in dialogue nodes.
|
||||||
</p>
|
</p>
|
||||||
{!isAdding && !editingId && (
|
{!isAdding && !editingId && (
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={() => { setIsAdding(true); resetForm(); setDeleteConfirm(null) }}
|
<button
|
||||||
className="rounded bg-blue-500 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
onClick={onImport}
|
||||||
>
|
className="rounded border border-zinc-300 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-600"
|
||||||
Add Character
|
>
|
||||||
</button>
|
Import from project
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setIsAdding(true); resetForm(); setDeleteConfirm(null) }}
|
||||||
|
className="rounded bg-blue-500 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
||||||
|
>
|
||||||
|
Add Character
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -378,6 +415,7 @@ type VariablesTabProps = {
|
||||||
variables: Variable[]
|
variables: Variable[]
|
||||||
onChange: (variables: Variable[]) => void
|
onChange: (variables: Variable[]) => void
|
||||||
getUsageCount: (variableId: string) => number
|
getUsageCount: (variableId: string) => number
|
||||||
|
onImport: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type VariableType = 'numeric' | 'string' | 'boolean'
|
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 [isAdding, setIsAdding] = useState(false)
|
||||||
const [editingId, setEditingId] = useState<string | null>(null)
|
const [editingId, setEditingId] = useState<string | null>(null)
|
||||||
const [formData, setFormData] = useState<VariableFormData>({ name: '', type: 'numeric', initialValue: '0', description: '' })
|
const [formData, setFormData] = useState<VariableFormData>({ 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.
|
Define variables that can be referenced in variable nodes and edge conditions.
|
||||||
</p>
|
</p>
|
||||||
{!isAdding && !editingId && (
|
{!isAdding && !editingId && (
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={() => { setIsAdding(true); resetForm(); setDeleteConfirm(null) }}
|
<button
|
||||||
className="rounded bg-blue-500 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
onClick={onImport}
|
||||||
>
|
className="rounded border border-zinc-300 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-600"
|
||||||
Add Variable
|
>
|
||||||
</button>
|
Import from project
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setIsAdding(true); resetForm(); setDeleteConfirm(null) }}
|
||||||
|
className="rounded bg-blue-500 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
||||||
|
>
|
||||||
|
Add Variable
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue