truncated long folder names so the buttons are still visible, migrated legacy folders to new format
This commit is contained in:
parent
db3a418d3c
commit
7c68f5d0e4
5 changed files with 348 additions and 10 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
72
backend/MIGRATION_README.md
Normal file
72
backend/MIGRATION_README.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Legacy Folder Migration Script
|
||||
|
||||
## Purpose
|
||||
This script migrates legacy folders to be compatible with the hierarchy system by adding missing fields required for drag & drop functionality.
|
||||
|
||||
## The Problem
|
||||
Legacy folders created before the hierarchy feature lack the `level` field, which causes:
|
||||
- `folder.level === 0` to evaluate as `false`
|
||||
- `canReceiveDrop` to be disabled
|
||||
- Drag & drop functionality to not work
|
||||
|
||||
## The Solution
|
||||
The script adds these required fields to legacy folders:
|
||||
- `level: 0` (makes them root folders)
|
||||
- `parent_folder_id: null`
|
||||
- `created_by: <user_id>` (if missing)
|
||||
- `created_at: <timestamp>` (if missing)
|
||||
- `updated_at: <timestamp>`
|
||||
|
||||
## Usage
|
||||
|
||||
### Dry Run (Preview Changes)
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
MONGO_PORT=27020 python migrate_legacy_folders.py --dry-run
|
||||
```
|
||||
|
||||
### Execute Migration
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
MONGO_PORT=27020 python migrate_legacy_folders.py
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
- `MONGO_PORT`: MongoDB port (default: 27017, but our setup uses 27020)
|
||||
- `MONGO_HOST`: MongoDB host (default: localhost)
|
||||
- `MONGO_USER`: MongoDB username (optional)
|
||||
- `MONGO_PASS`: MongoDB password (optional)
|
||||
|
||||
## Output Example
|
||||
```
|
||||
=== Legacy Folder Migration Script ===
|
||||
Mode: DRY RUN
|
||||
Connecting to MongoDB...
|
||||
Connected to MongoDB without authentication
|
||||
Looking up user ID for username 'user'...
|
||||
Found user ID: 68ad9c39e191a669e9b40701
|
||||
Searching for legacy folders...
|
||||
Found 1 legacy folder(s) to migrate:
|
||||
- gen prompts test (ID: 68adf75bfa4b13bb59140ce5)
|
||||
|
||||
--- DRY RUN MODE - No actual changes will be made ---
|
||||
|
||||
[1/1] Processing folder: gen prompts test
|
||||
Adding level: 0
|
||||
Adding parent_folder_id: None
|
||||
[DRY RUN] Would update folder 'gen prompts test' with: {...}
|
||||
|
||||
--- MIGRATION COMPLETE ---
|
||||
Successfully processed: 1/1 folders
|
||||
Run without --dry-run flag to execute the actual migration.
|
||||
```
|
||||
|
||||
## Safety Features
|
||||
- Dry run mode for testing
|
||||
- Detailed logging of all operations
|
||||
- Graceful error handling
|
||||
- Atomic database updates
|
||||
- Verification that all changes were successful
|
||||
|
||||
## After Migration
|
||||
Once the script runs successfully, all legacy folders will have proper drag & drop functionality in the UI.
|
||||
246
backend/migrate_legacy_folders.py
Executable file
246
backend/migrate_legacy_folders.py
Executable file
|
|
@ -0,0 +1,246 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Legacy Folder Migration Script
|
||||
|
||||
This script migrates legacy folders to be compatible with the hierarchy system
|
||||
by adding missing fields that are required for drag & drop functionality.
|
||||
|
||||
Usage:
|
||||
python migrate_legacy_folders.py --dry-run # Preview changes
|
||||
python migrate_legacy_folders.py # Execute migration
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
import os
|
||||
import logging
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def get_db_connection():
|
||||
"""Get database connection using the same logic as the app."""
|
||||
mongo_user = os.environ.get('MONGO_USER')
|
||||
mongo_pass = os.environ.get('MONGO_PASS')
|
||||
mongo_host = os.environ.get('MONGO_HOST', 'localhost')
|
||||
mongo_port = os.environ.get('MONGO_PORT', '27017')
|
||||
|
||||
# Try with standard credentials first
|
||||
standard_credentials = [
|
||||
{"user": "admin", "pass": "admin", "db": "admin"},
|
||||
{"user": "mongodb", "pass": "mongodb", "db": "admin"},
|
||||
{"user": "root", "pass": "root", "db": "admin"},
|
||||
{"user": "user", "pass": "pass", "db": "admin"}
|
||||
]
|
||||
|
||||
# Try each set of standard credentials
|
||||
for creds in standard_credentials:
|
||||
try:
|
||||
uri = f"mongodb://{creds['user']}:{creds['pass']}@{mongo_host}:{mongo_port}/semblance_db?authSource={creds['db']}"
|
||||
motor_client = AsyncIOMotorClient(uri, serverSelectionTimeoutMS=2000)
|
||||
database = motor_client.semblance_db
|
||||
# Test the connection
|
||||
await database.command('ping')
|
||||
logger.info(f"Connected to MongoDB with credentials: {creds['user']}")
|
||||
return motor_client, database
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# Try without authentication
|
||||
try:
|
||||
motor_client = AsyncIOMotorClient(f'mongodb://{mongo_host}:{mongo_port}', serverSelectionTimeoutMS=5000)
|
||||
database = motor_client.semblance_db
|
||||
await database.command('ping')
|
||||
# Test write access
|
||||
test_result = await database.test_collection.insert_one({"test": "migration_test"})
|
||||
await database.test_collection.delete_one({"_id": test_result.inserted_id})
|
||||
logger.info("Connected to MongoDB without authentication")
|
||||
return motor_client, database
|
||||
except Exception as e:
|
||||
logger.error(f"Could not connect without auth: {e}")
|
||||
|
||||
# Try with environment variables
|
||||
if mongo_user and mongo_pass:
|
||||
try:
|
||||
uri = f"mongodb://{mongo_user}:{mongo_pass}@{mongo_host}:{mongo_port}/semblance_db?authSource=admin"
|
||||
motor_client = AsyncIOMotorClient(uri, serverSelectionTimeoutMS=5000)
|
||||
database = motor_client.semblance_db
|
||||
await database.command('ping')
|
||||
logger.info(f"Connected to MongoDB with env credentials: {mongo_user}")
|
||||
return motor_client, database
|
||||
except Exception as e:
|
||||
logger.error(f"Failed with environment credentials: {e}")
|
||||
|
||||
raise Exception("Could not establish database connection")
|
||||
|
||||
async def find_user_id(database, username):
|
||||
"""Find user ID by username."""
|
||||
try:
|
||||
user = await database.users.find_one({"username": username})
|
||||
if user:
|
||||
return str(user["_id"])
|
||||
else:
|
||||
logger.warning(f"User '{username}' not found in database")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding user '{username}': {e}")
|
||||
return None
|
||||
|
||||
async def find_legacy_folders(database):
|
||||
"""Find folders that are missing the level field."""
|
||||
try:
|
||||
# Query for folders that don't have the 'level' field
|
||||
cursor = database.folders.find({"level": {"$exists": False}})
|
||||
folders = await cursor.to_list(length=None)
|
||||
return folders
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding legacy folders: {e}")
|
||||
return []
|
||||
|
||||
async def migrate_folder(database, folder, user_id, dry_run=False):
|
||||
"""Migrate a single folder to add missing hierarchy fields."""
|
||||
folder_id = folder["_id"]
|
||||
folder_name = folder.get("name", "Unknown")
|
||||
|
||||
# Prepare update fields
|
||||
update_fields = {}
|
||||
|
||||
# Add level field (make all legacy folders root level)
|
||||
if "level" not in folder:
|
||||
update_fields["level"] = 0
|
||||
logger.info(f" Adding level: 0")
|
||||
|
||||
# Add parent_folder_id field if missing
|
||||
if "parent_folder_id" not in folder:
|
||||
update_fields["parent_folder_id"] = None
|
||||
logger.info(f" Adding parent_folder_id: None")
|
||||
|
||||
# Add created_by field if missing and we have a user_id
|
||||
if "created_by" not in folder and user_id:
|
||||
update_fields["created_by"] = user_id
|
||||
logger.info(f" Adding created_by: {user_id}")
|
||||
|
||||
# Add created_at field if missing
|
||||
if "created_at" not in folder:
|
||||
update_fields["created_at"] = datetime.now(timezone.utc)
|
||||
logger.info(f" Adding created_at: {update_fields['created_at']}")
|
||||
|
||||
# Add updated_at field
|
||||
update_fields["updated_at"] = datetime.now(timezone.utc)
|
||||
|
||||
if not update_fields:
|
||||
logger.info(f" No updates needed for folder '{folder_name}'")
|
||||
return True
|
||||
|
||||
if dry_run:
|
||||
logger.info(f" [DRY RUN] Would update folder '{folder_name}' with: {update_fields}")
|
||||
return True
|
||||
|
||||
try:
|
||||
# Update the folder
|
||||
result = await database.folders.update_one(
|
||||
{"_id": folder_id},
|
||||
{"$set": update_fields}
|
||||
)
|
||||
|
||||
if result.modified_count > 0:
|
||||
logger.info(f" Successfully updated folder '{folder_name}'")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f" No changes made to folder '{folder_name}' (already up to date)")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f" Error updating folder '{folder_name}': {e}")
|
||||
return False
|
||||
|
||||
async def migrate_legacy_folders(dry_run=False):
|
||||
"""Main migration function."""
|
||||
client = None
|
||||
try:
|
||||
# Connect to database
|
||||
logger.info("Connecting to MongoDB...")
|
||||
client, database = await get_db_connection()
|
||||
|
||||
# Look up user ID for 'user'
|
||||
logger.info("Looking up user ID for username 'user'...")
|
||||
user_id = await find_user_id(database, 'user')
|
||||
if user_id:
|
||||
logger.info(f"Found user ID: {user_id}")
|
||||
else:
|
||||
logger.warning("Could not find user 'user' - will skip created_by field")
|
||||
|
||||
# Find legacy folders
|
||||
logger.info("Searching for legacy folders...")
|
||||
legacy_folders = await find_legacy_folders(database)
|
||||
|
||||
if not legacy_folders:
|
||||
logger.info("No legacy folders found. All folders are up to date!")
|
||||
return True
|
||||
|
||||
logger.info(f"Found {len(legacy_folders)} legacy folder(s) to migrate:")
|
||||
|
||||
# Display summary of folders to be migrated
|
||||
for folder in legacy_folders:
|
||||
folder_name = folder.get("name", "Unknown")
|
||||
logger.info(f" - {folder_name} (ID: {folder['_id']})")
|
||||
|
||||
if dry_run:
|
||||
logger.info("\n--- DRY RUN MODE - No actual changes will be made ---")
|
||||
else:
|
||||
logger.info("\n--- EXECUTING MIGRATION ---")
|
||||
|
||||
# Migrate each folder
|
||||
success_count = 0
|
||||
for i, folder in enumerate(legacy_folders, 1):
|
||||
folder_name = folder.get("name", "Unknown")
|
||||
logger.info(f"\n[{i}/{len(legacy_folders)}] Processing folder: {folder_name}")
|
||||
|
||||
success = await migrate_folder(database, folder, user_id, dry_run)
|
||||
if success:
|
||||
success_count += 1
|
||||
else:
|
||||
logger.error(f"Failed to migrate folder: {folder_name}")
|
||||
|
||||
# Summary
|
||||
logger.info(f"\n--- MIGRATION COMPLETE ---")
|
||||
logger.info(f"Successfully processed: {success_count}/{len(legacy_folders)} folders")
|
||||
|
||||
if dry_run:
|
||||
logger.info("Run without --dry-run flag to execute the actual migration.")
|
||||
else:
|
||||
logger.info("Migration completed successfully!")
|
||||
|
||||
return success_count == len(legacy_folders)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Migration failed: {e}")
|
||||
return False
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Migrate legacy folders for drag & drop compatibility')
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
help='Preview changes without executing them')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logger.info("=== Legacy Folder Migration Script ===")
|
||||
logger.info(f"Mode: {'DRY RUN' if args.dry_run else 'EXECUTE'}")
|
||||
|
||||
# Run the migration
|
||||
success = asyncio.run(migrate_legacy_folders(dry_run=args.dry_run))
|
||||
|
||||
if success:
|
||||
sys.exit(0)
|
||||
else:
|
||||
logger.error("Migration failed!")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
dist/index.html
vendored
2
dist/index.html
vendored
|
|
@ -7,7 +7,7 @@
|
|||
<meta name="description" content="Lovable Generated Project" />
|
||||
<meta name="author" content="Lovable" />
|
||||
<meta property="og:image" content="/og-image.png" />
|
||||
<script type="module" crossorigin src="/semblance/assets/index-CIkWTPLM.js"></script>
|
||||
<script type="module" crossorigin src="/semblance/assets/index-B0vde4xg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/semblance/assets/index-sPfhNyTb.css">
|
||||
</head>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
|
|
@ -7,6 +7,11 @@ import {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { Folder, FolderPlus, MoreHorizontal, Check, X, ChevronRight, ChevronDown } from 'lucide-react';
|
||||
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
|
||||
|
|
@ -61,6 +66,11 @@ const FolderTreeItem = ({
|
|||
currentlyDraggedFolderId = null,
|
||||
}: FolderTreeItemProps) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const folderNameRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Simple length-based check for when to show tooltip
|
||||
// Names longer than ~25 characters are likely to be truncated in this layout
|
||||
const shouldShowTooltip = folder.name.length > 25;
|
||||
|
||||
// Calculate persona count for this folder (explicit membership only)
|
||||
const personaCount = allPersonas.filter(persona =>
|
||||
|
|
@ -112,7 +122,7 @@ const FolderTreeItem = ({
|
|||
return (
|
||||
<div ref={setNodeRef} className={isDragOverlay ? 'z-50' : ''}>
|
||||
<div
|
||||
className={`flex items-center justify-between group ${
|
||||
className={`flex items-center justify-between group min-w-0 ${
|
||||
isOver && canReceiveDrop
|
||||
? 'bg-blue-50 border-2 border-blue-200 border-dashed rounded-md'
|
||||
: ''
|
||||
|
|
@ -162,7 +172,7 @@ const FolderTreeItem = ({
|
|||
// Normal display mode
|
||||
<>
|
||||
<div
|
||||
className={`flex-1 flex items-center space-x-2 px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
className={`flex-1 flex items-center space-x-2 px-3 py-2 text-sm rounded-md transition-colors min-w-0 ${
|
||||
selectedFolder === folder._id
|
||||
? 'bg-primary/10 text-primary font-medium'
|
||||
: 'hover:bg-slate-100'
|
||||
|
|
@ -189,12 +199,22 @@ const FolderTreeItem = ({
|
|||
{!hasChildren && <div className="w-4 h-4 flex-shrink-0" />} {/* Spacer for alignment */}
|
||||
|
||||
<Folder className="h-4 w-4 flex-shrink-0" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
ref={folderNameRef}
|
||||
onClick={() => onFolderSelect(folder._id)}
|
||||
className="truncate text-left flex-1 hover:underline"
|
||||
className="truncate text-left flex-1 hover:underline min-w-0"
|
||||
>
|
||||
{folder.name}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
{shouldShowTooltip && (
|
||||
<TooltipContent>
|
||||
<p>{folder.name}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
<span className="text-muted-foreground text-xs ml-auto flex-shrink-0">
|
||||
{isAllPersonasFolder ? allPersonas.length : personaCount}
|
||||
</span>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue