Skip to content

Commit 3e6f7de

Browse files
committed
feat: add pad backups retrieval functionality
- Implemented a new API endpoint to retrieve backups for a specific pad, including ownership verification and backup data formatting. - Updated frontend API hooks to support fetching pad backups and integrated this functionality into the BackupsDialog component. - Enhanced the context menu and tab components to improve user interaction with pad actions and backup management. - Refactored styles for the tab context menu to improve UI consistency and responsiveness.
1 parent 672a66b commit 3e6f7de

File tree

6 files changed

+171
-50
lines changed

6 files changed

+171
-50
lines changed

src/backend/routers/pad_router.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,52 @@ async def create_pad_from_template(
237237
raise HTTPException(status_code=500, detail=f"Failed to create pad from template: {str(e)}")
238238

239239

240+
@pad_router.get("/{pad_id}/backups")
241+
async def get_pad_backups(
242+
pad_id: UUID,
243+
limit: int = MAX_BACKUPS_PER_USER,
244+
user: UserSession = Depends(require_auth),
245+
pad_service: PadService = Depends(get_pad_service),
246+
backup_service: BackupService = Depends(get_backup_service)
247+
):
248+
"""Get backups for a specific pad"""
249+
# Limit the number of backups to the maximum configured value
250+
if limit > MAX_BACKUPS_PER_USER:
251+
limit = MAX_BACKUPS_PER_USER
252+
253+
try:
254+
# Get the pad to verify ownership
255+
pad = await pad_service.get_pad(pad_id)
256+
257+
if not pad:
258+
raise HTTPException(status_code=404, detail="Pad not found")
259+
260+
# Verify the user owns this pad
261+
if str(pad["owner_id"]) != str(user.id):
262+
raise HTTPException(status_code=403, detail="You don't have permission to access this pad's backups")
263+
264+
# Get backups for this specific pad
265+
backups_data = await backup_service.get_backups_by_source(pad_id)
266+
267+
# Limit the number of backups if needed
268+
if len(backups_data) > limit:
269+
backups_data = backups_data[:limit]
270+
271+
# Format backups to match the expected response format
272+
backups = []
273+
for backup in backups_data:
274+
backups.append({
275+
"id": backup["id"],
276+
"timestamp": backup["created_at"],
277+
"data": backup["data"]
278+
})
279+
280+
return {"backups": backups, "pad_name": pad["display_name"]}
281+
except Exception as e:
282+
print(f"Error getting pad backups: {str(e)}")
283+
raise HTTPException(status_code=500, detail=f"Failed to get pad backups: {str(e)}")
284+
285+
240286
@pad_router.get("/recent")
241287
async def get_recent_canvas_backups(
242288
limit: int = MAX_BACKUPS_PER_USER,

src/frontend/src/api/hooks.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface CanvasBackup {
4646

4747
export interface CanvasBackupsResponse {
4848
backups: CanvasBackup[];
49+
pad_name?: string;
4950
}
5051

5152
export interface BuildInfo {
@@ -186,6 +187,15 @@ export const api = {
186187
}
187188
},
188189

190+
getPadBackups: async (padId: string, limit: number = 10): Promise<CanvasBackupsResponse> => {
191+
try {
192+
const result = await fetchApi(`/api/pad/${padId}/backups?limit=${limit}`);
193+
return result;
194+
} catch (error) {
195+
throw error;
196+
}
197+
},
198+
189199
// Build Info
190200
getBuildInfo: async (): Promise<BuildInfo> => {
191201
try {
@@ -245,6 +255,15 @@ export function useCanvasBackups(limit: number = 10, options?: UseQueryOptions<C
245255
});
246256
}
247257

258+
export function usePadBackups(padId: string | null, limit: number = 10, options?: UseQueryOptions<CanvasBackupsResponse>) {
259+
return useQuery({
260+
queryKey: ['padBackups', padId, limit],
261+
queryFn: () => padId ? api.getPadBackups(padId, limit) : Promise.reject('No pad ID provided'),
262+
enabled: !!padId, // Only run the query if padId is provided
263+
...options,
264+
});
265+
}
266+
248267
export function useBuildInfo(options?: UseQueryOptions<BuildInfo>) {
249268
return useQuery({
250269
queryKey: ['buildInfo'],
@@ -281,8 +300,14 @@ export function useSaveCanvas(options?: UseMutationOptions<any, Error, CanvasDat
281300
return useMutation({
282301
mutationFn: api.saveCanvas,
283302
onSuccess: () => {
284-
// Invalidate canvas backups query to trigger refetch
303+
// Get the active pad ID from the global variable
304+
const activePadId = (window as any).activePadId;
305+
306+
// Invalidate canvas backups queries to trigger refetch
285307
queryClient.invalidateQueries({ queryKey: ['canvasBackups'] });
308+
if (activePadId) {
309+
queryClient.invalidateQueries({ queryKey: ['padBackups', activePadId] });
310+
}
286311
},
287312
...options,
288313
});

src/frontend/src/ui/BackupsDialog.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useState, useCallback } from "react";
22
import { Dialog } from "@atyrode/excalidraw";
3-
import { useCanvasBackups, CanvasBackup } from "../api/hooks";
4-
import { normalizeCanvasData } from "../utils/canvasUtils";
3+
import { usePadBackups, CanvasBackup } from "../api/hooks";
4+
import { normalizeCanvasData, getActivePad } from "../utils/canvasUtils";
55
import "./BackupsDialog.scss";
66

77
interface BackupsModalProps {
@@ -14,7 +14,8 @@ const BackupsModal: React.FC<BackupsModalProps> = ({
1414
onClose,
1515
}) => {
1616
const [modalIsShown, setModalIsShown] = useState(true);
17-
const { data, isLoading, error } = useCanvasBackups();
17+
const activePadId = getActivePad();
18+
const { data, isLoading, error } = usePadBackups(activePadId);
1819
const [selectedBackup, setSelectedBackup] = useState<CanvasBackup | null>(null);
1920

2021
// Functions from CanvasBackups.tsx
@@ -113,7 +114,9 @@ const BackupsModal: React.FC<BackupsModalProps> = ({
113114
onCloseRequest={handleClose}
114115
title={
115116
<div className="backups-modal__title-container">
116-
<h2 className="backups-modal__title">Canvas Backups</h2>
117+
<h2 className="backups-modal__title">
118+
{data?.pad_name ? `${data.pad_name} (this pad) - Backups` : 'Canvas Backups'}
119+
</h2>
117120
</div>
118121
}
119122
closeOnClickOutside={true}
Lines changed: 89 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,102 @@
11
.tab-context-menu {
22
position: fixed;
3-
background-color: #fff;
4-
border: 1px solid #ccc;
53
border-radius: 4px;
6-
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
7-
min-width: 120px;
4+
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
5+
padding: 0.5rem 0;
6+
list-style: none;
7+
user-select: none;
8+
margin: -0.25rem 0 0 0.125rem;
9+
min-width: 9.5rem;
810
z-index: 1000;
11+
background-color: var(--popup-secondary-bg-color, #fff);
12+
border: 1px solid var(--button-gray-3, #ccc);
13+
cursor: default;
14+
}
15+
16+
.tab-context-menu button {
17+
color: var(--popup-text-color, #333);
18+
}
19+
20+
.tab-context-menu .menu-item {
21+
position: relative;
22+
width: 100%;
23+
min-width: 9.5rem;
24+
margin: 0;
25+
padding: 0.25rem 1rem 0.25rem 1.25rem;
26+
text-align: start;
27+
border-radius: 0;
28+
background-color: transparent;
29+
border: none;
30+
white-space: nowrap;
31+
font-family: inherit;
32+
cursor: pointer;
33+
34+
display: grid;
35+
grid-template-columns: 1fr 0.2fr;
36+
align-items: center;
37+
38+
.menu-item__label {
39+
justify-self: start;
40+
margin-inline-end: 20px;
41+
}
942

10-
.menu-item {
11-
padding: 8px 12px;
12-
cursor: pointer;
13-
14-
&:hover {
15-
background-color: #f5f5f5;
16-
}
17-
18-
&.delete {
43+
&.delete {
44+
.menu-item__label {
1945
color: #e53935;
20-
21-
&:hover {
22-
background-color: #ffebee;
23-
}
2446
}
2547
}
26-
27-
form {
28-
padding: 8px;
29-
display: flex;
30-
31-
input {
32-
flex: 1;
33-
padding: 4px 8px;
34-
border: 1px solid #ccc;
35-
border-radius: 4px;
36-
margin-right: 4px;
48+
}
49+
50+
.tab-context-menu .menu-item:hover {
51+
color: var(--popup-bg-color, #fff);
52+
background-color: var(--select-highlight-color, #f5f5f5);
53+
54+
&.delete {
55+
.menu-item__label {
56+
color: var(--popup-bg-color, #fff);
3757
}
58+
background-color: #e53935;
59+
}
60+
}
61+
62+
.tab-context-menu .menu-item:focus {
63+
z-index: 1;
64+
}
65+
66+
@media (max-width: 640px) {
67+
.tab-context-menu .menu-item {
68+
display: block;
69+
70+
.menu-item__label {
71+
margin-inline-end: 0;
72+
}
73+
}
74+
}
75+
76+
.tab-context-menu form {
77+
padding: 0.5rem 1rem;
78+
display: flex;
79+
80+
input {
81+
flex: 1;
82+
padding: 0.25rem 0.5rem;
83+
border: 1px solid var(--button-gray-3, #ccc);
84+
border-radius: 4px;
85+
margin-right: 0.25rem;
86+
background-color: var(--input-bg-color, #fff);
87+
color: var(--text-color-primary, #333);
88+
}
89+
90+
button {
91+
background-color: var(--color-surface-primary-container, #4285f4);
92+
color: var(--color-on-primary-container, white);
93+
border: none;
94+
border-radius: 4px;
95+
padding: 0.25rem 0.5rem;
96+
cursor: pointer;
3897

39-
button {
40-
background-color: #4285f4;
41-
color: white;
42-
border: none;
43-
border-radius: 4px;
44-
padding: 4px 8px;
45-
cursor: pointer;
46-
47-
&:hover {
48-
background-color: #3367d6;
49-
}
98+
&:hover {
99+
background-color: var(--color-primary-light, #3367d6);
50100
}
51101
}
52102
}

src/frontend/src/ui/TabContextMenu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ const TabContextMenu: React.FC<TabContextMenuProps> = ({
9595
) : (
9696
<>
9797
<div className="menu-item" onClick={handleRenameClick}>
98-
Rename
98+
<span className="menu-item__label">Rename</span>
9999
</div>
100100
<div className="menu-item delete" onClick={handleDeleteClick}>
101-
Delete
101+
<span className="menu-item__label">Delete</span>
102102
</div>
103103
</>
104104
)}

src/frontend/src/ui/Tabs.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@ import { Stack, Button, Section, Tooltip } from "@atyrode/excalidraw";
55
import { FilePlus2 } from "lucide-react";
66
import { useAllPads, useSaveCanvas, useRenamePad, useDeletePad, PadData } from "../api/hooks";
77
import { queryClient } from "../api/queryClient";
8-
import { fetchApi } from "../api/apiUtils";
98
import {
10-
normalizeCanvasData,
119
getPadData,
1210
storePadData,
1311
setActivePad,
14-
getActivePad,
1512
getStoredActivePad,
1613
loadPadData,
1714
saveCurrentPadBeforeSwitching,
@@ -29,7 +26,7 @@ const Tabs: React.FC<TabsProps> = ({
2926
}: {
3027
excalidrawAPI: ExcalidrawImperativeAPI;
3128
}) => {
32-
const { data: pads, isLoading, refetch: refetchPads } = useAllPads();
29+
const { data: pads, isLoading } = useAllPads();
3330
const appState = excalidrawAPI.getAppState();
3431
const [isCreatingPad, setIsCreatingPad] = useState(false);
3532
const [activePadId, setActivePadId] = useState<string | null>(null);

0 commit comments

Comments
 (0)