Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c959b35
feat: integrate Tabs and Footer components into ExcalidrawWrapper
atyrode Apr 28, 2025
e45e911
chore: update @atyrode/excalidraw dependency to version 0.18.0-5
atyrode Apr 28, 2025
5b5c193
chore: update yarn lockfile
atyrode Apr 28, 2025
8d36ba1
chore: update @atyrode/excalidraw dependency to version 0.18.0-6
atyrode Apr 28, 2025
0bb4cdf
refactor: restructure pad folder
atyrode Apr 28, 2025
a7be4df
chore: update @atyrode/excalidraw dependency to version 0.18.0-7
atyrode Apr 28, 2025
47e27fd
feat: add Tabs component styling and functionality
atyrode Apr 28, 2025
a113138
chore: update @atyrode/excalidraw dependency to version 0.18.0-8
atyrode Apr 29, 2025
b37b7d4
chore: update @atyrode/excalidraw dependency to version 0.18.0-8
atyrode Apr 29, 2025
b0aa09d
fix: (temp) work around cytoscape issue by forcing version
atyrode Apr 29, 2025
f006ebd
Merge branch 'main' into tabs
atyrode May 4, 2025
89b3f50
Merge branch 'main' into tabs
atyrode May 4, 2025
76bf5fb
refactor: replace console.log with console.debug for build info and l…
atyrode May 4, 2025
50d0623
refactor: remove main execution block from CoderAPI
atyrode May 4, 2025
672a66b
feat: add tabs
atyrode May 4, 2025
3e6f7de
feat: add pad backups retrieval functionality
atyrode May 4, 2025
db88d42
chore: update @atyrode/excalidraw dependency to version 0.18.0-9
atyrode May 4, 2025
d21232a
chore: add clsx dependency and update excalidraw version in package.j…
atyrode May 4, 2025
2322fea
feat: enhance context menu and tab functionality
atyrode May 5, 2025
975b83d
feat: enhance tab navigation and styling
atyrode May 5, 2025
fbda6b8
feat: improve tab visibility and tooltip display
atyrode May 5, 2025
0ab5d9a
style: update tab button borders for improved aesthetics
atyrode May 5, 2025
0e70063
feat: add PadsDialog for managing pads
atyrode May 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions src/backend/coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,3 @@ def set_workspace_dormancy(self, workspace_id, dormant: bool):
response = requests.put(endpoint, headers=headers, json=data)
response.raise_for_status()
return response.json()


if __name__ == "__main__":
coder = CoderAPI()
workspace_id = coder.get_workspace_status_for_user("alex")["id"]
coder.set_workspace_dormancy(workspace_id, True)
state = coder.get_workspace_status_for_user("alex")
print(state)
4 changes: 2 additions & 2 deletions src/backend/database/repository/pad_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ async def get_by_id(self, pad_id: UUID) -> Optional[PadModel]:
return result.scalars().first()

async def get_by_owner(self, owner_id: UUID) -> List[PadModel]:
"""Get all pads for a specific owner"""
stmt = select(PadModel).where(PadModel.owner_id == owner_id)
"""Get all pads for a specific owner, sorted by created_at timestamp"""
stmt = select(PadModel).where(PadModel.owner_id == owner_id).order_by(PadModel.created_at)
result = await self.session.execute(stmt)
return result.scalars().all()

Expand Down
15 changes: 2 additions & 13 deletions src/backend/database/service/pad_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from ..repository import PadRepository, UserRepository
from .user_service import UserService

from ..models import PadModel
# Use TYPE_CHECKING to avoid circular imports
if TYPE_CHECKING:
from dependencies import UserSession
Expand Down Expand Up @@ -63,11 +63,6 @@ async def create_pad(self, owner_id: UUID, display_name: str, data: Dict[str, An
print(f"Error creating user as failsafe: {str(e)}")
raise ValueError(f"Failed to create user with ID '{owner_id}': {str(e)}")

# Check if pad with same name already exists for this owner
existing_pad = await self.repository.get_by_name(owner_id, display_name)
if existing_pad:
raise ValueError(f"Pad with name '{display_name}' already exists for this user")

# Create pad
pad = await self.repository.create(owner_id, display_name, data)
return pad.to_dict()
Expand All @@ -86,7 +81,7 @@ async def get_pads_by_owner(self, owner_id: UUID) -> List[Dict[str, Any]]:
# This allows the pad_router to handle the case where a user doesn't exist
return []

pads = await self.repository.get_by_owner(owner_id)
pads: list[PadModel] = await self.repository.get_by_owner(owner_id)
return [pad.to_dict() for pad in pads]

async def get_pad_by_name(self, owner_id: UUID, display_name: str) -> Optional[Dict[str, Any]]:
Expand All @@ -105,12 +100,6 @@ async def update_pad(self, pad_id: UUID, data: Dict[str, Any]) -> Optional[Dict[
if 'display_name' in data and not data['display_name']:
raise ValueError("Display name cannot be empty")

# Check if new display_name already exists for this owner (if being updated)
if 'display_name' in data and data['display_name'] != pad.display_name:
existing_pad = await self.repository.get_by_name(pad.owner_id, data['display_name'])
if existing_pad:
raise ValueError(f"Pad with name '{data['display_name']}' already exists for this user")

# Update pad
updated_pad = await self.repository.update(pad_id, data)
return updated_pad.to_dict() if updated_pad else None
Expand Down
220 changes: 191 additions & 29 deletions src/backend/routers/pad_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,71 +9,175 @@
from config import MAX_BACKUPS_PER_USER, MIN_INTERVAL_MINUTES, DEFAULT_PAD_NAME, DEFAULT_TEMPLATE_NAME
pad_router = APIRouter()

def ensure_pad_metadata(data: Dict[str, Any], pad_id: str, display_name: str) -> Dict[str, Any]:
"""
Ensure the pad metadata (uniqueId and displayName) is set in the data.

Args:
data: The pad data to modify
pad_id: The pad ID to set as uniqueId
display_name: The display name to set

Returns:
The modified data
"""
# Ensure the appState and pad objects exist
if "appState" not in data:
data["appState"] = {}
if "pad" not in data["appState"]:
data["appState"]["pad"] = {}

# Set the uniqueId to match the database ID
data["appState"]["pad"]["uniqueId"] = str(pad_id)
data["appState"]["pad"]["displayName"] = display_name

return data


@pad_router.post("")
async def save_pad(
@pad_router.post("/{pad_id}")
async def update_specific_pad(
pad_id: UUID,
data: Dict[str, Any],
user: UserSession = Depends(require_auth),
pad_service: PadService = Depends(get_pad_service),
backup_service: BackupService = Depends(get_backup_service),
):
"""Save pad data for the authenticated user"""
"""Update a specific pad's data for the authenticated user"""
try:
# Check if user already has a pad
user_pads = await pad_service.get_pads_by_owner(user.id)
# Get the pad to verify ownership
pad = await pad_service.get_pad(pad_id)

if not user_pads:
# Create a new pad if user doesn't have one
pad = await pad_service.create_pad(
owner_id=user.id,
display_name=DEFAULT_PAD_NAME,
data=data,
user_session=user
)
else:
# Update existing pad
pad = user_pads[0] # Use the first pad (assuming one pad per user for now)
await pad_service.update_pad_data(pad["id"], data)
if not pad:
raise HTTPException(status_code=404, detail="Pad not found")

# Create a backup only if needed (if none exist or latest is > 5 min old)
# Verify the user owns this pad
if str(pad["owner_id"]) != str(user.id):
raise HTTPException(status_code=403, detail="You don't have permission to update this pad")

# Ensure the uniqueId and displayName are set in the data
data = ensure_pad_metadata(data, str(pad_id), pad["display_name"])

# Update the pad
await pad_service.update_pad_data(pad_id, data)

# Create a backup if needed
await backup_service.create_backup_if_needed(
source_id=pad["id"],
source_id=pad_id,
data=data,
min_interval_minutes=MIN_INTERVAL_MINUTES,
max_backups=MAX_BACKUPS_PER_USER
)

return {"status": "success"}
except Exception as e:
print(f"Error saving pad data: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to save canvas data: {str(e)}")
print(f"Error updating pad: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to update pad: {str(e)}")


@pad_router.patch("/{pad_id}")
async def rename_pad(
pad_id: UUID,
data: Dict[str, str],
user: UserSession = Depends(require_auth),
pad_service: PadService = Depends(get_pad_service),
):
"""Rename a pad for the authenticated user"""
try:
# Get the pad to verify ownership
pad = await pad_service.get_pad(pad_id)

if not pad:
raise HTTPException(status_code=404, detail="Pad not found")

# Verify the user owns this pad
if str(pad["owner_id"]) != str(user.id):
raise HTTPException(status_code=403, detail="You don't have permission to rename this pad")

# Check if display_name is provided
if "display_name" not in data:
raise HTTPException(status_code=400, detail="display_name is required")

# Update the pad's display name
update_data = {"display_name": data["display_name"]}
updated_pad = await pad_service.update_pad(pad_id, update_data)

return {"status": "success", "pad": updated_pad}
except ValueError as e:
print(f"Error renaming pad: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
print(f"Error renaming pad: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to rename pad: {str(e)}")


@pad_router.delete("/{pad_id}")
async def delete_pad(
pad_id: UUID,
user: UserSession = Depends(require_auth),
pad_service: PadService = Depends(get_pad_service),
):
"""Delete a pad for the authenticated user"""
try:
# Get the pad to verify ownership
pad = await pad_service.get_pad(pad_id)

if not pad:
raise HTTPException(status_code=404, detail="Pad not found")

# Verify the user owns this pad
if str(pad["owner_id"]) != str(user.id):
raise HTTPException(status_code=403, detail="You don't have permission to delete this pad")

# Delete the pad
success = await pad_service.delete_pad(pad_id)

if not success:
raise HTTPException(status_code=500, detail="Failed to delete pad")

return {"status": "success"}
except ValueError as e:
print(f"Error deleting pad: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
print(f"Error deleting pad: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to delete pad: {str(e)}")


@pad_router.get("")
async def get_pad(
async def get_all_pads(
user: UserSession = Depends(require_auth),
pad_service: PadService = Depends(get_pad_service),
template_pad_service: TemplatePadService = Depends(get_template_pad_service),
backup_service: BackupService = Depends(get_backup_service)
):
"""Get pad data for the authenticated user"""
"""Get all pads for the authenticated user"""
try:
# Get user's pads
user_pads = await pad_service.get_pads_by_owner(user.id)

if not user_pads:
# Return default canvas if user doesn't have a pad
return await create_pad_from_template(
# Create a default pad if user doesn't have any
new_pad = await create_pad_from_template(
name=DEFAULT_TEMPLATE_NAME,
display_name=DEFAULT_PAD_NAME,
user=user,
pad_service=pad_service,
template_pad_service=template_pad_service,
backup_service=backup_service
)

# Return the new pad in a list
return [new_pad]

# Ensure each pad's data has the uniqueId and displayName set
for pad in user_pads:
pad_data = pad["data"]

# Ensure the uniqueId and displayName are set in the data
pad_data = ensure_pad_metadata(pad_data, str(pad["id"]), pad["display_name"])

# Return the first pad's data (assuming one pad per user for now)
return user_pads[0]["data"]
# Return all pads
return user_pads
except Exception as e:
print(f"Error getting pad data: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get pad data: {str(e)}")
Expand All @@ -96,18 +200,30 @@ async def create_pad_from_template(
if not template:
raise HTTPException(status_code=404, detail="Template not found")

# Get the template data
template_data = template["data"]

# Before creating, ensure the pad object exists in the data
template_data = ensure_pad_metadata(template_data, "", "")

# Create a new pad using the template data
pad = await pad_service.create_pad(
owner_id=user.id,
display_name=display_name,
data=template["data"],
data=template_data,
user_session=user
)

# Set the uniqueId and displayName to match the database ID and display name
template_data = ensure_pad_metadata(template_data, str(pad["id"]), display_name)

# Update the pad with the modified data
await pad_service.update_pad_data(pad["id"], template_data)

# Create an initial backup for the new pad
await backup_service.create_backup_if_needed(
source_id=pad["id"],
data=template["data"],
data=template_data,
min_interval_minutes=0, # Always create initial backup
max_backups=MAX_BACKUPS_PER_USER
)
Expand All @@ -121,6 +237,52 @@ async def create_pad_from_template(
raise HTTPException(status_code=500, detail=f"Failed to create pad from template: {str(e)}")


@pad_router.get("/{pad_id}/backups")
async def get_pad_backups(
pad_id: UUID,
limit: int = MAX_BACKUPS_PER_USER,
user: UserSession = Depends(require_auth),
pad_service: PadService = Depends(get_pad_service),
backup_service: BackupService = Depends(get_backup_service)
):
"""Get backups for a specific pad"""
# Limit the number of backups to the maximum configured value
if limit > MAX_BACKUPS_PER_USER:
limit = MAX_BACKUPS_PER_USER

try:
# Get the pad to verify ownership
pad = await pad_service.get_pad(pad_id)

if not pad:
raise HTTPException(status_code=404, detail="Pad not found")

# Verify the user owns this pad
if str(pad["owner_id"]) != str(user.id):
raise HTTPException(status_code=403, detail="You don't have permission to access this pad's backups")

# Get backups for this specific pad
backups_data = await backup_service.get_backups_by_source(pad_id)

# Limit the number of backups if needed
if len(backups_data) > limit:
backups_data = backups_data[:limit]

# Format backups to match the expected response format
backups = []
for backup in backups_data:
backups.append({
"id": backup["id"],
"timestamp": backup["created_at"],
"data": backup["data"]
})

return {"backups": backups, "pad_name": pad["display_name"]}
except Exception as e:
print(f"Error getting pad backups: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get pad backups: {str(e)}")


@pad_router.get("/recent")
async def get_recent_canvas_backups(
limit: int = MAX_BACKUPS_PER_USER,
Expand Down
7 changes: 6 additions & 1 deletion src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
"version": "1.0.0",
"private": true,
"dependencies": {
"@atyrode/excalidraw": "^0.18.0-4",
"@atyrode/excalidraw": "^0.18.0-9",
"@monaco-editor/react": "^4.7.0",
"@tanstack/react-query": "^5.74.3",
"@tanstack/react-query-devtools": "^5.74.3",
"@types/crypto-js": "^4.2.2",
"browser-fs-access": "0.29.1",
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"lucide-react": "^0.488.0",
"posthog-js": "^1.236.0",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"resolutions": {
"cytoscape": "3.31.2",
"**/cytoscape": "3.31.2"
},
"devDependencies": {
"@types/node": "^22.14.0",
"typescript": "^5",
Expand Down
Loading