Skip to content

Commit e8bb51e

Browse files
dnywhawaseem
andauthored
chore(studio): queue operations UI improvements (supabase#42272)
## What kind of change does this PR introduce? UI improvements ## What is the current behavior? @awaseem added a schmick new “Queue table operations” feature in supabase#42120 ## What is the new behavior? This adds some UI polish to that feature. ## Additional context https://github.com/user-attachments/assets/0b823bc9-44bd-42d1-8042-162084d058c7 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **UI/UX Improvements** * Repositioned and animated floating save/review bar for smoother entrance and centered alignment * Card-based redesign of operation items with clearer old/new value badges and improved action controls * Updated side panel text, button labels ("Review", "Save/Revert" with "all" when applicable), and focus behavior * Refined empty-state layout and amber background highlighting for modified cells * Improved spacing, padding, and visual polish across the operation queue UI <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ali Waseem <waseema393@gmail.com>
1 parent 32817d2 commit e8bb51e

File tree

8 files changed

+231
-155
lines changed

8 files changed

+231
-155
lines changed

apps/studio/components/grid/components/footer/operations/SaveQueueActionBar.tsx

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ import { useOperationQueueActions } from 'components/grid/hooks/useOperationQueu
22
import { useOperationQueueShortcuts } from 'components/grid/hooks/useOperationQueueShortcuts'
33
import { useIsQueueOperationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
44
import { AnimatePresence, motion } from 'framer-motion'
5-
import { Eye } from 'lucide-react'
65
import { createPortal } from 'react-dom'
76
import { useTableEditorStateSnapshot } from 'state/table-editor'
87
import { Button } from 'ui'
9-
8+
import { useEffect, useRef, useState } from 'react'
109
import { getModKeyLabel } from '@/lib/helpers'
1110

1211
export const SaveQueueActionBar = () => {
1312
const modKey = getModKeyLabel()
1413
const snap = useTableEditorStateSnapshot()
1514
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
1615
const { handleSave } = useOperationQueueActions()
16+
const [leftPosition, setLeftPosition] = useState<string>('50%')
1717

1818
useOperationQueueShortcuts()
1919

@@ -24,42 +24,80 @@ export const SaveQueueActionBar = () => {
2424
const isVisible =
2525
isQueueOperationsEnabled && snap.hasPendingOperations && !isOperationQueuePanelOpen
2626

27+
// Center position relative to grid container (viewport alignment)
28+
useEffect(() => {
29+
if (!isVisible) return
30+
31+
const gridContainer = document.querySelector('.sb-grid')
32+
const updatePosition = () => {
33+
if (!gridContainer) {
34+
setLeftPosition('50%')
35+
return
36+
}
37+
38+
const gridRect = gridContainer.getBoundingClientRect()
39+
const gridCenter = gridRect.left + gridRect.width / 2
40+
setLeftPosition(`${gridCenter}px`)
41+
}
42+
43+
updatePosition()
44+
45+
if (!gridContainer) return
46+
47+
const resizeObserver = new ResizeObserver(updatePosition)
48+
resizeObserver.observe(gridContainer)
49+
50+
window.addEventListener('resize', updatePosition)
51+
window.addEventListener('scroll', updatePosition, true)
52+
53+
return () => {
54+
resizeObserver.disconnect()
55+
window.removeEventListener('resize', updatePosition)
56+
window.removeEventListener('scroll', updatePosition, true)
57+
}
58+
}, [isVisible])
59+
2760
const content = (
2861
<AnimatePresence>
2962
{isVisible && (
30-
<motion.div
31-
initial={{ opacity: 0, y: 20 }}
32-
animate={{ opacity: 1, y: 0 }}
33-
exit={{ opacity: 0, y: 20 }}
34-
transition={{ duration: 0.2 }}
35-
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50"
63+
<div
64+
className="fixed bottom-12 z-50 transform-gpu will-change-transform"
65+
style={{ left: leftPosition, transform: 'translateX(-50%)' }}
3666
>
37-
<div className="flex items-center gap-8 px-4 py-3 bg-surface-100 border rounded-lg shadow-lg">
38-
<span className="text-sm text-foreground">
39-
{operationCount} pending change{operationCount !== 1 ? 's' : ''}
40-
</span>
41-
<div className="flex items-center gap-3">
42-
<button
43-
onClick={() => snap.toggleViewOperationQueue()}
44-
className="text-foreground-light hover:text-foreground transition-colors flex items-center"
45-
aria-label="View Details"
46-
>
47-
<Eye size={14} />
48-
<span className="text-foreground-lighter text-[10px] ml-1">{`${modKey}.`}</span>
49-
</button>
50-
<Button
51-
size="tiny"
52-
type="primary"
53-
onClick={handleSave}
54-
disabled={isSaving}
55-
loading={isSaving}
56-
>
57-
Save
58-
<span className="text-foreground-lighter text-[10px] ml-1">{`${modKey}S`}</span>
59-
</Button>
67+
<motion.div
68+
initial={{ opacity: 0, y: 16 }}
69+
animate={{ opacity: 1, y: 0 }}
70+
exit={{ opacity: 0, y: 16 }}
71+
transition={{
72+
type: 'spring',
73+
stiffness: 420,
74+
damping: 30,
75+
mass: 0.4,
76+
}}
77+
>
78+
<div className="flex items-center gap-8 pl-4 pr-2 py-2 bg-surface-100 border rounded-lg shadow-lg">
79+
<span className="text-xs text-foreground-light max-w-40 truncate">
80+
{operationCount} pending change{operationCount !== 1 ? 's' : ''}
81+
</span>
82+
<div className="flex items-center gap-2">
83+
<Button type="default" size="tiny" onClick={() => snap.toggleViewOperationQueue()}>
84+
Review{' '}
85+
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}.`}</span>
86+
</Button>
87+
<Button
88+
size="tiny"
89+
type="primary"
90+
onClick={handleSave}
91+
disabled={isSaving}
92+
loading={isSaving}
93+
>
94+
Save{operationCount > 1 && ' all'}
95+
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}S`}</span>
96+
</Button>
97+
</div>
6098
</div>
61-
</div>
62-
</motion.div>
99+
</motion.div>
100+
</div>
63101
)}
64102
</AnimatePresence>
65103
)

apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/AddRowOperationItem.tsx

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useQueryClient } from '@tanstack/react-query'
2+
import { Plus, Undo2 } from 'lucide-react'
23
import { tableRowKeys } from 'data/table-rows/keys'
34
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
4-
import { Plus, X } from 'lucide-react'
55
import { useTableEditorStateSnapshot } from 'state/table-editor'
6-
import { Button } from 'ui'
76

87
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
8+
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
99
import { AddRowPayload } from '@/state/table-editor-operation-queue.types'
10+
import { Card, CardContent, CardHeader } from 'ui'
1011

1112
interface AddRowOperationItemProps {
1213
operationId: string
@@ -47,40 +48,52 @@ export const AddRowOperationItem = ({
4748
}
4849

4950
return (
50-
<div className="border rounded-md overflow-hidden bg-surface-100 border-l-4 border-l-brand-500">
51-
<div className="px-3 py-2 border-b border-default bg-surface-200 flex items-start justify-between gap-2">
51+
<Card className="overflow-hidden">
52+
<CardHeader className="pt-2.5 flex flex-row gap-2">
5253
<div className="min-w-0 flex-1 flex items-start gap-2">
53-
<Plus size={14} className="text-brand-500 mt-0.5 shrink-0" />
54-
<div>
55-
<div className="text-xs text-foreground font-mono">{fullTableName}</div>
56-
<div className="text-sm text-foreground-muted mt-0.5">
57-
<span className="font-medium text-foreground">New row</span>
54+
<div className="min-w-0 flex-1">
55+
<code className="text-code-inline dark:bg-surface-300 dark:border-foreground-muted/50">
56+
{fullTableName}
57+
</code>
58+
<div className="text-xs text-foreground mt-1 ml-0.5">
59+
<span>New row</span>
5860
</div>
5961
</div>
6062
</div>
61-
<Button
63+
<ButtonTooltip
6264
type="text"
6365
size="tiny"
64-
icon={<X size={14} />}
66+
aria-label="Revert change"
67+
className="px-1.5"
68+
icon={<Undo2 />}
6569
onClick={handleDelete}
66-
className="shrink-0"
67-
aria-label="Remove operation"
70+
tooltip={{
71+
content: {
72+
side: 'bottom',
73+
align: 'end',
74+
text: 'Revert change',
75+
},
76+
}}
6877
/>
69-
</div>
78+
</CardHeader>
7079

71-
<div className="px-3 py-2 text-xs font-mono space-y-1 bg-brand-100/30">
80+
<CardContent className="font-mono text-xs space-y-1 text-brand-link">
7281
{previewColumns.map(([key, value]) => (
73-
<div key={key} className="flex items-start gap-2 text-foreground">
74-
<span className="text-foreground-light">{key}:</span>
75-
<span className="truncate" title={formatOperationItemValue(value)}>
82+
<div key={key} className="flex gap-2 py-0.5">
83+
<span className="text-brand-link select-none font-medium">+</span>
84+
<span className="shrink-0">{key}:</span>
85+
<span className="truncate min-w-0" title={formatOperationItemValue(value)}>
7686
{formatOperationItemValue(value)}
7787
</span>
7888
</div>
7989
))}
8090
{remainingCount > 0 && (
81-
<div className="text-foreground-light">+{remainingCount} more column(s)</div>
91+
<div className="flex gap-2 py-0.5">
92+
<span className="text-brand-link select-none font-medium">+</span>
93+
<span>+{remainingCount} more column(s)</span>
94+
</div>
8295
)}
83-
</div>
84-
</div>
96+
</CardContent>
97+
</Card>
8598
)
8699
}

apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useQueryClient } from '@tanstack/react-query'
2-
import { Trash2, X } from 'lucide-react'
3-
import { Button } from 'ui'
4-
2+
import { Undo2 } from 'lucide-react'
53
import { tableRowKeys } from 'data/table-rows/keys'
64
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
75
import { useTableEditorStateSnapshot } from 'state/table-editor'
8-
import { DeleteRowPayload } from '@/state/table-editor-operation-queue.types'
6+
97
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
8+
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
9+
import { DeleteRowPayload } from '@/state/table-editor-operation-queue.types'
10+
import { Card, CardContent, CardHeader } from 'ui'
1011

1112
interface DeleteRowOperationItemProps {
1213
operationId: string
@@ -45,32 +46,40 @@ export const DeleteRowOperationItem = ({
4546
}
4647

4748
return (
48-
<div className="border rounded-md overflow-hidden bg-surface-100 border-l-4 border-l-destructive-500">
49-
<div className="px-3 py-2 border-b border-default bg-surface-200 flex items-start justify-between gap-2">
49+
<Card className="overflow-hidden border-destructive-500 bg-destructive-500/5">
50+
<CardHeader className="pt-2.5 flex flex-row gap-2 border-b border-destructive-500">
5051
<div className="min-w-0 flex-1 flex items-start gap-2">
51-
<Trash2 size={14} className="text-destructive-500 mt-0.5 shrink-0" />
52-
<div>
53-
<div className="text-xs text-foreground font-mono">{fullTableName}</div>
54-
<div className="text-sm text-foreground-muted mt-0.5">
55-
<span className="font-medium text-foreground">Delete row</span>
56-
<span className="text-foreground-muted mx-2">·</span>
57-
<span className="text-foreground text-xs">where {whereClause}</span>
52+
<div className="min-w-0 flex-1">
53+
<code className="text-code-inline dark:bg-surface-300 dark:border-foreground-muted/50">
54+
{fullTableName}
55+
</code>
56+
<div className="text-xs text-foreground mt-1 ml-0.5">
57+
<span>Delete row</span>
58+
<span className="text-foreground-muted mx-1.5">·</span>
59+
<span>where {whereClause}</span>
5860
</div>
5961
</div>
6062
</div>
61-
<Button
63+
<ButtonTooltip
6264
type="text"
6365
size="tiny"
64-
icon={<X size={14} />}
66+
aria-label="Revert change"
67+
className="px-1.5"
68+
icon={<Undo2 />}
6569
onClick={handleDelete}
66-
className="shrink-0"
67-
aria-label="Remove operation"
70+
tooltip={{
71+
content: {
72+
side: 'bottom',
73+
align: 'end',
74+
text: 'Revert change',
75+
},
76+
}}
6877
/>
69-
</div>
78+
</CardHeader>
7079

71-
<div className="px-3 py-2 text-xs font-mono bg-destructive-100/30">
72-
<div className="text-destructive-500 line-through opacity-70">Row will be deleted</div>
73-
</div>
74-
</div>
80+
<CardContent className="font-mono text-xs bg-destructive-100/30">
81+
<div className="text-destructive py-0.5">Row will be deleted</div>
82+
</CardContent>
83+
</Card>
7584
)
7685
}

apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useQueryClient } from '@tanstack/react-query'
2+
import { Undo2 } from 'lucide-react'
23
import { tableRowKeys } from 'data/table-rows/keys'
34
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
4-
import { X } from 'lucide-react'
55
import { useTableEditorStateSnapshot } from 'state/table-editor'
66

77
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
88
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
99
import { EditCellContentPayload } from '@/state/table-editor-operation-queue.types'
10+
import { Card, CardContent, CardHeader } from 'ui'
1011

1112
interface OperationItemProps {
1213
operationId: string
@@ -44,42 +45,50 @@ export const OperationItem = ({ operationId, tableId, content }: OperationItemPr
4445
}
4546

4647
return (
47-
<div className="border rounded-md overflow-hidden bg-surface-100">
48-
<div className="px-3 py-2 border-b border-default bg-surface-200 flex items-start justify-between gap-2">
48+
<Card className="overflow-hidden">
49+
<CardHeader className="pt-2.5 flex flex-row gap-2">
4950
<div className="min-w-0 flex-1">
50-
<div className="text-xs text-foreground font-mono">{fullTableName}</div>
51-
<div className="text-sm text-foreground-muted mt-0.5">
52-
<span className="font-medium text-foreground">{columnName}</span>
53-
<span className="text-foreground-muted mx-2"></span>
54-
<span className="text-foreground text-xs">where {whereClause}</span>
51+
<code className="text-code-inline dark:bg-surface-300 dark:border-foreground-muted/50">
52+
{fullTableName}
53+
</code>
54+
<div className="text-xs text-foreground mt-1 ml-0.5">
55+
<span>{columnName}</span>
56+
<span className="text-foreground-muted mx-1.5">·</span>
57+
<span>where {whereClause}</span>
5558
</div>
5659
</div>
5760
<ButtonTooltip
5861
type="text"
5962
size="tiny"
60-
icon={<X size={14} />}
63+
aria-label="Revert change"
64+
className="px-1.5"
65+
icon={<Undo2 />}
6166
onClick={handleDelete}
62-
className="shrink-0 w-7"
63-
aria-label="Remove operation"
64-
tooltip={{ content: { side: 'bottom', text: 'Remove operation' } }}
67+
tooltip={{
68+
content: {
69+
side: 'bottom',
70+
align: 'end',
71+
text: 'Revert change',
72+
},
73+
}}
6574
/>
66-
</div>
75+
</CardHeader>
6776

68-
<div className="font-mono text-xs">
69-
<div className="flex items-start gap-2 px-3 py-0.5 bg-red-400/20">
70-
<span className="text-red-900 select-none font-bold">-</span>
71-
<span className="text-red-900 truncate max-w-full" title={formattedOldValue}>
77+
<CardContent className="font-mono text-xs">
78+
<div className="flex gap-2 py-0.5">
79+
<span className="text-destructive select-none font-medium">-</span>
80+
<span className="text-destructive truncate max-w-full" title={formattedOldValue}>
7281
{formattedOldValue}
7382
</span>
7483
</div>
7584

76-
<div className="flex items-start gap-2 px-3 py-0.5 bg-green-400/20">
77-
<span className="text-green-900 select-none font-bold">+</span>
78-
<span className="text-green-900 truncate max-w-full" title={formattedNewValue}>
85+
<div className="flex gap-2 py-0.5">
86+
<span className="text-brand-link select-none font-medium">+</span>
87+
<span className="text-brand-link truncate max-w-full" title={formattedNewValue}>
7988
{formattedNewValue}
8089
</span>
8190
</div>
82-
</div>
83-
</div>
91+
</CardContent>
92+
</Card>
8493
)
8594
}

0 commit comments

Comments
 (0)