Fix build errors
This commit is contained in:
@@ -132,135 +132,135 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
||||
}, [handleLogout]);
|
||||
|
||||
// Sidebar content (reusable for both desktop and mobile)
|
||||
const SidebarContent = () => (
|
||||
const SidebarContent = () => (
|
||||
<>
|
||||
{/* Logo */}
|
||||
<div className="h-16 flex items-center gap-2 px-6 border-b border-neutral-200">
|
||||
<Image src="/assets/logo.png" alt="Plunk" width={28} height={28} className="rounded" />
|
||||
<h1 className="text-xl font-bold text-neutral-900">Plunk</h1>
|
||||
</div>
|
||||
{/* Logo */}
|
||||
<div className="h-16 flex items-center gap-2 px-6 border-b border-neutral-200">
|
||||
<Image src="/assets/logo.png" alt="Plunk" width={28} height={28} className="rounded" />
|
||||
<h1 className="text-xl font-bold text-neutral-900">Plunk</h1>
|
||||
</div>
|
||||
|
||||
{/* Project Switcher */}
|
||||
<div className="p-4 border-b border-neutral-200">
|
||||
<div className="relative" ref={projectMenuRef}>
|
||||
<button
|
||||
onClick={handleToggleProjectMenu}
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg hover:bg-neutral-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div className="h-8 w-8 rounded-lg bg-neutral-900 text-white flex items-center justify-center text-xs font-medium flex-shrink-0">
|
||||
{activeProject?.name.charAt(0).toUpperCase() || 'P'}
|
||||
{/* Project Switcher */}
|
||||
<div className="p-4 border-b border-neutral-200">
|
||||
<div className="relative" ref={projectMenuRef}>
|
||||
<button
|
||||
onClick={handleToggleProjectMenu}
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg hover:bg-neutral-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div className="h-8 w-8 rounded-lg bg-neutral-900 text-white flex items-center justify-center text-xs font-medium flex-shrink-0">
|
||||
{activeProject?.name.charAt(0).toUpperCase() || 'P'}
|
||||
</div>
|
||||
<span className="font-medium text-neutral-900 truncate">{activeProject?.name || 'Select project'}</span>
|
||||
</div>
|
||||
<span className="font-medium text-neutral-900 truncate">{activeProject?.name || 'Select project'}</span>
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 text-neutral-500 flex-shrink-0" />
|
||||
</button>
|
||||
<ChevronDown className="h-4 w-4 text-neutral-500 flex-shrink-0" />
|
||||
</button>
|
||||
|
||||
{/* Project Dropdown */}
|
||||
{showProjectMenu && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1">
|
||||
{availableProjects.map(project => (
|
||||
<button
|
||||
key={project.id}
|
||||
onClick={() => {
|
||||
setActiveProject(project);
|
||||
setShowProjectMenu(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors"
|
||||
>
|
||||
<div className="h-6 w-6 rounded bg-neutral-900 text-white flex items-center justify-center text-xs font-medium">
|
||||
{project.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="text-neutral-900">{project.name}</span>
|
||||
{activeProject?.id === project.id && (
|
||||
<div className="ml-auto h-1.5 w-1.5 rounded-full bg-neutral-900" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<div className="border-t border-neutral-200 my-1" />
|
||||
<Link
|
||||
href="/projects/create"
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-neutral-700"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Create project</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-4 overflow-y-auto">
|
||||
{navigation.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex} className={sectionIndex > 0 ? 'mt-6' : ''}>
|
||||
{section.title && (
|
||||
<p className="px-3 mb-2 text-xs font-semibold text-neutral-500 uppercase tracking-wider">
|
||||
{section.title}
|
||||
</p>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
{section.items.map(item => {
|
||||
const isActive = router.pathname === item.href;
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setShowMobileMenu(false)}
|
||||
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 ${
|
||||
isActive ? 'bg-neutral-100' : 'hover:bg-neutral-50 hover:text-neutral-900'
|
||||
}`}
|
||||
{/* Project Dropdown */}
|
||||
{showProjectMenu && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1">
|
||||
{availableProjects.map(project => (
|
||||
<button
|
||||
key={project.id}
|
||||
onClick={() => {
|
||||
setActiveProject(project);
|
||||
setShowProjectMenu(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors"
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="h-6 w-6 rounded bg-neutral-900 text-white flex items-center justify-center text-xs font-medium">
|
||||
{project.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="text-neutral-900">{project.name}</span>
|
||||
{activeProject?.id === project.id && (
|
||||
<div className="ml-auto h-1.5 w-1.5 rounded-full bg-neutral-900" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<div className="border-t border-neutral-200 my-1" />
|
||||
<Link
|
||||
href="/projects/create"
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-neutral-700"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Create project</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Settings & User Menu */}
|
||||
<div className="border-t border-neutral-200 p-3 space-y-1">
|
||||
<Link
|
||||
href="/settings"
|
||||
onClick={() => setShowMobileMenu(false)}
|
||||
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 ${
|
||||
router.pathname.startsWith('/settings') ? 'bg-neutral-100' : 'hover:bg-neutral-50 hover:text-neutral-900'
|
||||
}`}
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
Settings
|
||||
</Link>
|
||||
|
||||
<div className="relative" ref={userMenuRef}>
|
||||
<button
|
||||
onClick={handleToggleUserMenu}
|
||||
className="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg text-neutral-700 hover:bg-neutral-50 hover:text-neutral-900 transition-colors"
|
||||
>
|
||||
<User className="h-5 w-5" />
|
||||
<span className="flex-1 text-left truncate">{user?.email}</span>
|
||||
<ChevronDown className="h-4 w-4 text-neutral-500" />
|
||||
</button>
|
||||
|
||||
{/* User Dropdown */}
|
||||
{showUserMenu && (
|
||||
<div className="absolute bottom-full left-0 right-0 mb-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1">
|
||||
<button
|
||||
onClick={handleLogoutClick}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-red-600"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span>Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-4 overflow-y-auto">
|
||||
{navigation.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex} className={sectionIndex > 0 ? 'mt-6' : ''}>
|
||||
{section.title && (
|
||||
<p className="px-3 mb-2 text-xs font-semibold text-neutral-500 uppercase tracking-wider">
|
||||
{section.title}
|
||||
</p>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
{section.items.map(item => {
|
||||
const isActive = router.pathname === item.href;
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setShowMobileMenu(false)}
|
||||
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 ${
|
||||
isActive ? 'bg-neutral-100' : 'hover:bg-neutral-50 hover:text-neutral-900'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Settings & User Menu */}
|
||||
<div className="border-t border-neutral-200 p-3 space-y-1">
|
||||
<Link
|
||||
href="/settings"
|
||||
onClick={() => setShowMobileMenu(false)}
|
||||
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 ${
|
||||
router.pathname.startsWith('/settings') ? 'bg-neutral-100' : 'hover:bg-neutral-50 hover:text-neutral-900'
|
||||
}`}
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
Settings
|
||||
</Link>
|
||||
|
||||
<div className="relative" ref={userMenuRef}>
|
||||
<button
|
||||
onClick={handleToggleUserMenu}
|
||||
className="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg text-neutral-700 hover:bg-neutral-50 hover:text-neutral-900 transition-colors"
|
||||
>
|
||||
<User className="h-5 w-5" />
|
||||
<span className="flex-1 text-left truncate">{user?.email}</span>
|
||||
<ChevronDown className="h-4 w-4 text-neutral-500" />
|
||||
</button>
|
||||
|
||||
{/* User Dropdown */}
|
||||
{showUserMenu && (
|
||||
<div className="absolute bottom-full left-0 right-0 mb-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1">
|
||||
<button
|
||||
onClick={handleLogoutClick}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-red-600"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span>Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-neutral-50">
|
||||
|
||||
@@ -35,7 +35,6 @@ interface EmailEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
canUploadImages?: boolean;
|
||||
// Optional props for preview header
|
||||
subject?: string;
|
||||
from?: string;
|
||||
|
||||
@@ -119,6 +119,44 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
// Helper to get default value based on field type
|
||||
const getDefaultValueForType = useCallback((type: string) => {
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
return true;
|
||||
case 'number':
|
||||
return 0;
|
||||
case 'date':
|
||||
return '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Helper to get valid operators for a field type
|
||||
const getOperatorsForType = useCallback((type: string, isEvent: boolean) => {
|
||||
if (isEvent) {
|
||||
return EVENT_OPERATORS;
|
||||
}
|
||||
|
||||
if (type === 'boolean') {
|
||||
return STANDARD_OPERATORS.filter(op =>
|
||||
['equals', 'notEquals', 'exists', 'notExists'].includes(op.value)
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'number' || type === 'date') {
|
||||
return STANDARD_OPERATORS.filter(op =>
|
||||
['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterThanOrEqual', 'lessThanOrEqual', 'exists', 'notExists', 'within'].includes(op.value)
|
||||
);
|
||||
}
|
||||
|
||||
// String type - no within operator, no comparison operators
|
||||
return STANDARD_OPERATORS.filter(op =>
|
||||
['equals', 'notEquals', 'contains', 'notContains', 'exists', 'notExists'].includes(op.value)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const needsValue = !['exists', 'notExists', 'triggered', 'notTriggered'].includes(filter.operator);
|
||||
const needsUnit = ['within', 'triggeredWithin'].includes(filter.operator);
|
||||
|
||||
@@ -208,45 +246,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
|
||||
setOpen(false);
|
||||
setSearch('');
|
||||
}, [availableFields, filter.operator, fieldType, onChange]);
|
||||
|
||||
// Helper to get default value based on field type
|
||||
const getDefaultValueForType = useCallback((type: string) => {
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
return true;
|
||||
case 'number':
|
||||
return 0;
|
||||
case 'date':
|
||||
return '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Helper to get valid operators for a field type
|
||||
const getOperatorsForType = useCallback((type: string, isEvent: boolean) => {
|
||||
if (isEvent) {
|
||||
return EVENT_OPERATORS;
|
||||
}
|
||||
|
||||
if (type === 'boolean') {
|
||||
return STANDARD_OPERATORS.filter(op =>
|
||||
['equals', 'notEquals', 'exists', 'notExists'].includes(op.value)
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'number' || type === 'date') {
|
||||
return STANDARD_OPERATORS.filter(op =>
|
||||
['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterThanOrEqual', 'lessThanOrEqual', 'exists', 'notExists', 'within'].includes(op.value)
|
||||
);
|
||||
}
|
||||
|
||||
// String type - no within operator, no comparison operators
|
||||
return STANDARD_OPERATORS.filter(op =>
|
||||
['equals', 'notEquals', 'contains', 'notContains', 'exists', 'notExists'].includes(op.value)
|
||||
);
|
||||
}, []);
|
||||
}, [availableFields, filter.operator, fieldType, onChange, getDefaultValueForType, getOperatorsForType]);
|
||||
|
||||
// Get label for selected field
|
||||
const getFieldLabel = useCallback(() => {
|
||||
|
||||
@@ -334,6 +334,18 @@ export function WorkflowBuilder({workflowId, steps, onUpdate}: WorkflowBuilderPr
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [stepToDelete, setStepToDelete] = useState<string | null>(null);
|
||||
|
||||
// Define handlers before they are used in useMemo
|
||||
const handleEditStep = useCallback((stepId: string) => {
|
||||
// This will be handled by the parent component
|
||||
const event = new CustomEvent('workflow-edit-step', {detail: {stepId}});
|
||||
window.dispatchEvent(event);
|
||||
}, []);
|
||||
|
||||
const handleDeleteStepClick = useCallback((stepId: string) => {
|
||||
setStepToDelete(stepId);
|
||||
setShowDeleteDialog(true);
|
||||
}, []);
|
||||
|
||||
// Convert workflow steps to React Flow nodes
|
||||
|
||||
const rawNodes: Node[] = useMemo(() => {
|
||||
@@ -622,17 +634,6 @@ export function WorkflowBuilder({workflowId, steps, onUpdate}: WorkflowBuilderPr
|
||||
[addStepContext, workflowId, onUpdate],
|
||||
);
|
||||
|
||||
const handleEditStep = useCallback((stepId: string) => {
|
||||
// This will be handled by the parent component
|
||||
const event = new CustomEvent('workflow-edit-step', {detail: {stepId}});
|
||||
window.dispatchEvent(event);
|
||||
}, []);
|
||||
|
||||
const handleDeleteStepClick = useCallback((stepId: string) => {
|
||||
setStepToDelete(stepId);
|
||||
setShowDeleteDialog(true);
|
||||
}, []);
|
||||
|
||||
// Get all steps that will be affected by deleting a step (the step itself + all downstream steps)
|
||||
const getAffectedSteps = useCallback(
|
||||
(stepId: string): typeof steps => {
|
||||
|
||||
Reference in New Issue
Block a user