chore(studio): queue operations UI improvements (#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
https://github.com/supabase/supabase/pull/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>
This commit is contained in:
Danny White
2026-02-09 10:25:06 +11:00
committed by GitHub
parent 32817d2e74
commit e8bb51ec84
8 changed files with 231 additions and 155 deletions

View File

@@ -2,11 +2,10 @@ import { useOperationQueueActions } from 'components/grid/hooks/useOperationQueu
import { useOperationQueueShortcuts } from 'components/grid/hooks/useOperationQueueShortcuts'
import { useIsQueueOperationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { AnimatePresence, motion } from 'framer-motion'
import { Eye } from 'lucide-react'
import { createPortal } from 'react-dom'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Button } from 'ui'
import { useEffect, useRef, useState } from 'react'
import { getModKeyLabel } from '@/lib/helpers'
export const SaveQueueActionBar = () => {
@@ -14,6 +13,7 @@ export const SaveQueueActionBar = () => {
const snap = useTableEditorStateSnapshot()
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
const { handleSave } = useOperationQueueActions()
const [leftPosition, setLeftPosition] = useState<string>('50%')
useOperationQueueShortcuts()
@@ -24,42 +24,80 @@ export const SaveQueueActionBar = () => {
const isVisible =
isQueueOperationsEnabled && snap.hasPendingOperations && !isOperationQueuePanelOpen
// Center position relative to grid container (viewport alignment)
useEffect(() => {
if (!isVisible) return
const gridContainer = document.querySelector('.sb-grid')
const updatePosition = () => {
if (!gridContainer) {
setLeftPosition('50%')
return
}
const gridRect = gridContainer.getBoundingClientRect()
const gridCenter = gridRect.left + gridRect.width / 2
setLeftPosition(`${gridCenter}px`)
}
updatePosition()
if (!gridContainer) return
const resizeObserver = new ResizeObserver(updatePosition)
resizeObserver.observe(gridContainer)
window.addEventListener('resize', updatePosition)
window.addEventListener('scroll', updatePosition, true)
return () => {
resizeObserver.disconnect()
window.removeEventListener('resize', updatePosition)
window.removeEventListener('scroll', updatePosition, true)
}
}, [isVisible])
const content = (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.2 }}
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50"
<div
className="fixed bottom-12 z-50 transform-gpu will-change-transform"
style={{ left: leftPosition, transform: 'translateX(-50%)' }}
>
<div className="flex items-center gap-8 px-4 py-3 bg-surface-100 border rounded-lg shadow-lg">
<span className="text-sm text-foreground">
{operationCount} pending change{operationCount !== 1 ? 's' : ''}
</span>
<div className="flex items-center gap-3">
<button
onClick={() => snap.toggleViewOperationQueue()}
className="text-foreground-light hover:text-foreground transition-colors flex items-center"
aria-label="View Details"
>
<Eye size={14} />
<span className="text-foreground-lighter text-[10px] ml-1">{`${modKey}.`}</span>
</button>
<Button
size="tiny"
type="primary"
onClick={handleSave}
disabled={isSaving}
loading={isSaving}
>
Save
<span className="text-foreground-lighter text-[10px] ml-1">{`${modKey}S`}</span>
</Button>
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 16 }}
transition={{
type: 'spring',
stiffness: 420,
damping: 30,
mass: 0.4,
}}
>
<div className="flex items-center gap-8 pl-4 pr-2 py-2 bg-surface-100 border rounded-lg shadow-lg">
<span className="text-xs text-foreground-light max-w-40 truncate">
{operationCount} pending change{operationCount !== 1 ? 's' : ''}
</span>
<div className="flex items-center gap-2">
<Button type="default" size="tiny" onClick={() => snap.toggleViewOperationQueue()}>
Review{' '}
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}.`}</span>
</Button>
<Button
size="tiny"
type="primary"
onClick={handleSave}
disabled={isSaving}
loading={isSaving}
>
Save{operationCount > 1 && ' all'}
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}S`}</span>
</Button>
</div>
</div>
</div>
</motion.div>
</motion.div>
</div>
)}
</AnimatePresence>
)

View File

@@ -1,12 +1,13 @@
import { useQueryClient } from '@tanstack/react-query'
import { Plus, Undo2 } from 'lucide-react'
import { tableRowKeys } from 'data/table-rows/keys'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Plus, X } from 'lucide-react'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Button } from 'ui'
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { AddRowPayload } from '@/state/table-editor-operation-queue.types'
import { Card, CardContent, CardHeader } from 'ui'
interface AddRowOperationItemProps {
operationId: string
@@ -47,40 +48,52 @@ export const AddRowOperationItem = ({
}
return (
<div className="border rounded-md overflow-hidden bg-surface-100 border-l-4 border-l-brand-500">
<div className="px-3 py-2 border-b border-default bg-surface-200 flex items-start justify-between gap-2">
<Card className="overflow-hidden">
<CardHeader className="pt-2.5 flex flex-row gap-2">
<div className="min-w-0 flex-1 flex items-start gap-2">
<Plus size={14} className="text-brand-500 mt-0.5 shrink-0" />
<div>
<div className="text-xs text-foreground font-mono">{fullTableName}</div>
<div className="text-sm text-foreground-muted mt-0.5">
<span className="font-medium text-foreground">New row</span>
<div className="min-w-0 flex-1">
<code className="text-code-inline dark:bg-surface-300 dark:border-foreground-muted/50">
{fullTableName}
</code>
<div className="text-xs text-foreground mt-1 ml-0.5">
<span>New row</span>
</div>
</div>
</div>
<Button
<ButtonTooltip
type="text"
size="tiny"
icon={<X size={14} />}
aria-label="Revert change"
className="px-1.5"
icon={<Undo2 />}
onClick={handleDelete}
className="shrink-0"
aria-label="Remove operation"
tooltip={{
content: {
side: 'bottom',
align: 'end',
text: 'Revert change',
},
}}
/>
</div>
</CardHeader>
<div className="px-3 py-2 text-xs font-mono space-y-1 bg-brand-100/30">
<CardContent className="font-mono text-xs space-y-1 text-brand-link">
{previewColumns.map(([key, value]) => (
<div key={key} className="flex items-start gap-2 text-foreground">
<span className="text-foreground-light">{key}:</span>
<span className="truncate" title={formatOperationItemValue(value)}>
<div key={key} className="flex gap-2 py-0.5">
<span className="text-brand-link select-none font-medium">+</span>
<span className="shrink-0">{key}:</span>
<span className="truncate min-w-0" title={formatOperationItemValue(value)}>
{formatOperationItemValue(value)}
</span>
</div>
))}
{remainingCount > 0 && (
<div className="text-foreground-light">+{remainingCount} more column(s)</div>
<div className="flex gap-2 py-0.5">
<span className="text-brand-link select-none font-medium">+</span>
<span>+{remainingCount} more column(s)</span>
</div>
)}
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,12 +1,13 @@
import { useQueryClient } from '@tanstack/react-query'
import { Trash2, X } from 'lucide-react'
import { Button } from 'ui'
import { Undo2 } from 'lucide-react'
import { tableRowKeys } from 'data/table-rows/keys'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { DeleteRowPayload } from '@/state/table-editor-operation-queue.types'
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { DeleteRowPayload } from '@/state/table-editor-operation-queue.types'
import { Card, CardContent, CardHeader } from 'ui'
interface DeleteRowOperationItemProps {
operationId: string
@@ -45,32 +46,40 @@ export const DeleteRowOperationItem = ({
}
return (
<div className="border rounded-md overflow-hidden bg-surface-100 border-l-4 border-l-destructive-500">
<div className="px-3 py-2 border-b border-default bg-surface-200 flex items-start justify-between gap-2">
<Card className="overflow-hidden border-destructive-500 bg-destructive-500/5">
<CardHeader className="pt-2.5 flex flex-row gap-2 border-b border-destructive-500">
<div className="min-w-0 flex-1 flex items-start gap-2">
<Trash2 size={14} className="text-destructive-500 mt-0.5 shrink-0" />
<div>
<div className="text-xs text-foreground font-mono">{fullTableName}</div>
<div className="text-sm text-foreground-muted mt-0.5">
<span className="font-medium text-foreground">Delete row</span>
<span className="text-foreground-muted mx-2">·</span>
<span className="text-foreground text-xs">where {whereClause}</span>
<div className="min-w-0 flex-1">
<code className="text-code-inline dark:bg-surface-300 dark:border-foreground-muted/50">
{fullTableName}
</code>
<div className="text-xs text-foreground mt-1 ml-0.5">
<span>Delete row</span>
<span className="text-foreground-muted mx-1.5">·</span>
<span>where {whereClause}</span>
</div>
</div>
</div>
<Button
<ButtonTooltip
type="text"
size="tiny"
icon={<X size={14} />}
aria-label="Revert change"
className="px-1.5"
icon={<Undo2 />}
onClick={handleDelete}
className="shrink-0"
aria-label="Remove operation"
tooltip={{
content: {
side: 'bottom',
align: 'end',
text: 'Revert change',
},
}}
/>
</div>
</CardHeader>
<div className="px-3 py-2 text-xs font-mono bg-destructive-100/30">
<div className="text-destructive-500 line-through opacity-70">Row will be deleted</div>
</div>
</div>
<CardContent className="font-mono text-xs bg-destructive-100/30">
<div className="text-destructive py-0.5">Row will be deleted</div>
</CardContent>
</Card>
)
}

View File

@@ -1,12 +1,13 @@
import { useQueryClient } from '@tanstack/react-query'
import { Undo2 } from 'lucide-react'
import { tableRowKeys } from 'data/table-rows/keys'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { X } from 'lucide-react'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { EditCellContentPayload } from '@/state/table-editor-operation-queue.types'
import { Card, CardContent, CardHeader } from 'ui'
interface OperationItemProps {
operationId: string
@@ -44,42 +45,50 @@ export const OperationItem = ({ operationId, tableId, content }: OperationItemPr
}
return (
<div className="border rounded-md overflow-hidden bg-surface-100">
<div className="px-3 py-2 border-b border-default bg-surface-200 flex items-start justify-between gap-2">
<Card className="overflow-hidden">
<CardHeader className="pt-2.5 flex flex-row gap-2">
<div className="min-w-0 flex-1">
<div className="text-xs text-foreground font-mono">{fullTableName}</div>
<div className="text-sm text-foreground-muted mt-0.5">
<span className="font-medium text-foreground">{columnName}</span>
<span className="text-foreground-muted mx-2"></span>
<span className="text-foreground text-xs">where {whereClause}</span>
<code className="text-code-inline dark:bg-surface-300 dark:border-foreground-muted/50">
{fullTableName}
</code>
<div className="text-xs text-foreground mt-1 ml-0.5">
<span>{columnName}</span>
<span className="text-foreground-muted mx-1.5">·</span>
<span>where {whereClause}</span>
</div>
</div>
<ButtonTooltip
type="text"
size="tiny"
icon={<X size={14} />}
aria-label="Revert change"
className="px-1.5"
icon={<Undo2 />}
onClick={handleDelete}
className="shrink-0 w-7"
aria-label="Remove operation"
tooltip={{ content: { side: 'bottom', text: 'Remove operation' } }}
tooltip={{
content: {
side: 'bottom',
align: 'end',
text: 'Revert change',
},
}}
/>
</div>
</CardHeader>
<div className="font-mono text-xs">
<div className="flex items-start gap-2 px-3 py-0.5 bg-red-400/20">
<span className="text-red-900 select-none font-bold">-</span>
<span className="text-red-900 truncate max-w-full" title={formattedOldValue}>
<CardContent className="font-mono text-xs">
<div className="flex gap-2 py-0.5">
<span className="text-destructive select-none font-medium">-</span>
<span className="text-destructive truncate max-w-full" title={formattedOldValue}>
{formattedOldValue}
</span>
</div>
<div className="flex items-start gap-2 px-3 py-0.5 bg-green-400/20">
<span className="text-green-900 select-none font-bold">+</span>
<span className="text-green-900 truncate max-w-full" title={formattedNewValue}>
<div className="flex gap-2 py-0.5">
<span className="text-brand-link select-none font-medium">+</span>
<span className="text-brand-link truncate max-w-full" title={formattedNewValue}>
{formattedNewValue}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -15,7 +15,11 @@ interface OperationListProps {
export const OperationList = ({ operations }: OperationListProps) => {
if (operations.length === 0) {
return <p className="text-sm text-foreground-light">No pending changes</p>
return (
<div className="flex flex-col items-center justify-center h-full">
<p className="text-base text-foreground-muted">No pending changes</p>
</div>
)
}
const addOperations = operations.filter(isAddRowOperation)
@@ -23,12 +27,13 @@ export const OperationList = ({ operations }: OperationListProps) => {
const editOperations = operations.filter(isEditCellContentOperation)
return (
<div className="space-y-6">
<div className="space-y-8">
{deleteOperations.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-foreground-light">
Rows to Delete ({deleteOperations.length})
<h3 className="text-xs text-foreground-lighter">
{deleteOperations.length} row deletion{deleteOperations.length !== 1 ? 's' : ''}
</h3>
<div className="space-y-3">
{deleteOperations.map((op) => (
<DeleteRowOperationItem
@@ -44,8 +49,8 @@ export const OperationList = ({ operations }: OperationListProps) => {
{addOperations.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-foreground-light">
Rows to Add ({addOperations.length})
<h3 className="text-xs text-foreground-lighter">
{addOperations.length} row addition{addOperations.length !== 1 ? 's' : ''}
</h3>
<div className="space-y-3">
{addOperations.map((op) => (
@@ -62,9 +67,10 @@ export const OperationList = ({ operations }: OperationListProps) => {
{editOperations.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-foreground-light">
Cell Edits ({editOperations.length})
<h3 className="text-xs text-foreground-lighter">
{editOperations.length} cell edit{editOperations.length !== 1 ? 's' : ''}
</h3>
<div className="space-y-3">
{editOperations.map((op) => (
<OperationItem

View File

@@ -27,11 +27,12 @@ export const OperationQueueSidePanel = ({ visible, closePanel }: OperationQueueS
size="large"
visible={visible}
onCancel={closePanel}
onOpenAutoFocus={(event) => event.preventDefault()} // Prevent focus on first focussable element since it is the revert changes ButtonTooltip
header={
<div className="flex items-center justify-between w-full">
<div className="flex flex-col gap-1">
<span>Pending changes</span>
<span className="text-xs text-foreground-light">
<span className="text-xs text-foreground-lighter">
{operations.length} operation{operations.length !== 1 ? 's' : ''}
</span>
</div>
@@ -41,25 +42,29 @@ export const OperationQueueSidePanel = ({ visible, closePanel }: OperationQueueS
<div className="flex w-full justify-between border-t border-default px-3 py-4">
<Button type="default" onClick={closePanel}>
Close
<span className="text-foreground-lighter text-xs ml-1.5">{modKey}.</span>
<span className="text-foreground/40 text-[10px] ml-1.5">{modKey}.</span>
</Button>
<div className="flex space-x-3">
<Button type="default" onClick={handleCancel} disabled={isSaving}>
Cancel All
<Button
type="default"
onClick={handleCancel}
disabled={isSaving || operations.length === 0}
>
Revert{operations.length > 1 && ' all'}
</Button>
<Button
onClick={handleSave}
disabled={isSaving || operations.length === 0}
loading={isSaving}
>
Save All
<span className="text-foreground-lighter text-xs ml-1.5">{modKey}S</span>
Save{operations.length > 1 && ' all'}
<span className="text-foreground/40 text-[10px] ml-1.5">{modKey}S</span>
</Button>
</div>
</div>
}
>
<SidePanel.Content className="py-4">
<SidePanel.Content className="py-4 h-full">
<OperationList operations={operations} />
</SidePanel.Content>
</SidePanel>

View File

@@ -36,17 +36,13 @@
box-shadow: inset 0 0 0 1px #24b47e;
}
// Cell with unsaved changes - yellow/amber border
// Cell with unsaved changes - warning (amber) background/text color
.rdg-cell.rdg-cell--dirty {
box-shadow: inset 0 0 0 2px hsl(var(--warning-default));
background-color: hsl(var(--warning-300) / 0.75);
color: hsl(var(--warning-default));
}
// When a dirty cell is also selected, keep the amber border
.rdg-cell.rdg-cell--dirty[aria-selected='true'] {
box-shadow: inset 0 0 0 2px hsl(var(--warning-default));
}
// Row pending addition - green background
// Row pending addition - green background, green text
.rdg-row.rdg-row--added {
background-color: hsl(var(--brand-200) / 0.3);
@@ -56,32 +52,33 @@
.rdg-cell {
border-left-color: hsl(var(--brand-default));
color: hsl(var(--brand-link));
}
// First cell gets a stronger left border to indicate new row
.rdg-cell:first-child {
box-shadow: inset 3px 0 0 0 hsl(var(--brand-default));
box-shadow: inset 2px 0 0 0 hsl(var(--brand-default));
}
}
// Row pending deletion - red background with strikethrough effect
// Row pending deletion - red background with strikethrough effect, red text
.rdg-row.rdg-row--deleted {
background-color: hsl(var(--destructive-200) / 0.3);
opacity: 0.6;
background-color: hsl(var(--destructive-300) / 0.3);
&:hover {
background-color: hsl(var(--destructive-200) / 0.5);
background-color: hsl(var(--destructive-300) / 0.5);
}
.rdg-cell {
text-decoration: line-through;
text-decoration-color: hsl(var(--destructive-default));
border-left-color: hsl(var(--destructive-default));
color: hsl(var(--destructive-default));
}
// First cell gets a stronger left border to indicate deleted row
.rdg-cell:first-child {
box-shadow: inset 3px 0 0 0 hsl(var(--destructive-default));
box-shadow: inset 2px 0 0 0 hsl(var(--destructive-default));
}
}
@@ -89,7 +86,6 @@
@apply box-border select-none overflow-x-auto overflow-y-scroll bg-dash-canvas;
@apply border-t border-r-0 border-l-0;
contain: strict;
-webkit-user-select: none;
font-size: 14px;
}

View File

@@ -86,15 +86,15 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: 'View Details' }).click()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
await expect(sidePanel.getByText('Cell Edits (1)')).toBeVisible()
await expect(sidePanel.getByText('1 cell edit')).toBeVisible()
await expect(sidePanel.getByTitle('original value')).toBeVisible()
await expect(sidePanel.getByTitle('edited value')).toBeVisible()
await sidePanel.getByRole('button', { name: /Save All/ }).click()
await sidePanel.getByRole('button', { name: /^Save/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'edited value' })).toBeVisible()
@@ -126,9 +126,9 @@ test.describe('Queue Table Operations', () => {
await page.keyboard.press('Enter')
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: 'View Details' }).click()
await page.getByRole('button', { name: /Review/ }).click()
await page.getByRole('button', { name: 'Cancel All' }).click()
await page.getByRole('button', { name: 'Revert', exact: true }).click()
await expect(page.getByRole('gridcell', { name: 'keep this value' })).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'should be cancelled' })).not.toBeVisible()
@@ -161,14 +161,14 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByRole('gridcell', { name: 'new row value' })).toBeVisible()
await page.getByRole('button', { name: 'View Details' }).click()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
await expect(sidePanel.getByText('Rows to Add (1)')).toBeVisible()
await expect(sidePanel.getByText('1 row addition')).toBeVisible()
await expect(sidePanel.getByText('New row', { exact: true })).toBeVisible()
await sidePanel.getByRole('button', { name: /Save All/ }).click()
await sidePanel.getByRole('button', { name: /^Save/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'new row value' })).toBeVisible()
@@ -202,13 +202,13 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('2 pending changes')).toBeVisible()
await page.getByRole('button', { name: 'View Details' }).click()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('2 operations')).toBeVisible()
await expect(sidePanel.getByText('Rows to Add (2)')).toBeVisible()
await expect(sidePanel.getByText('2 row additions')).toBeVisible()
await sidePanel.getByRole('button', { name: /Save All/ }).click()
await sidePanel.getByRole('button', { name: /^Save/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'row one' })).toBeVisible()
@@ -243,15 +243,15 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('2 pending changes')).toBeVisible()
await page.getByRole('button', { name: 'View Details' }).click()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
const removeButtons = sidePanel.getByRole('button', { name: 'Remove operation' })
const removeButtons = sidePanel.getByRole('button', { name: 'Revert change' })
await removeButtons.last().click()
await expect(sidePanel.getByText('1 operation')).toBeVisible()
await sidePanel.getByRole('button', { name: /Save All/ }).click()
await sidePanel.getByRole('button', { name: /^Save/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'keep this row' })).toBeVisible()
@@ -285,7 +285,7 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByRole('dialog').getByText('Pending changes')).toBeVisible()
await page.keyboard.press('ControlOrMeta+.')
await expect(page.getByRole('button', { name: 'View Details' })).toBeVisible()
await expect(page.getByRole('button', { name: /Review/ })).toBeVisible()
await page.keyboard.press('ControlOrMeta+s')
await expect(page.getByText('Changes saved successfully')).toBeVisible()
@@ -318,14 +318,14 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: 'View Details' }).click()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
await expect(sidePanel.getByText('Rows to Delete (1)')).toBeVisible()
await expect(sidePanel.getByText('1 row deletion')).toBeVisible()
await expect(sidePanel.getByText('Delete row', { exact: true })).toBeVisible()
await sidePanel.getByRole('button', { name: /Save All/ }).click()
await sidePanel.getByRole('button', { name: /^Save/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'row to delete' })).not.toBeVisible()
@@ -354,8 +354,8 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: 'View Details' }).click()
await page.getByRole('button', { name: 'Cancel All' }).click()
await page.getByRole('button', { name: /Review/ }).click()
await page.getByRole('button', { name: 'Revert', exact: true }).click()
await expect(page.getByRole('gridcell', { name: 'should not be deleted' })).toBeVisible()
await expect(page.getByText('pending change')).not.toBeVisible()
@@ -397,14 +397,14 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('3 pending changes')).toBeVisible()
await page.getByRole('button', { name: 'View Details' }).click()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('3 operations')).toBeVisible()
await expect(sidePanel.getByText('Rows to Delete (1)')).toBeVisible()
await expect(sidePanel.getByText('Rows to Add (1)')).toBeVisible()
await expect(sidePanel.getByText('Cell Edits (1)')).toBeVisible()
await expect(sidePanel.getByText('1 row deletion')).toBeVisible()
await expect(sidePanel.getByText('1 row addition')).toBeVisible()
await expect(sidePanel.getByText('1 cell edit')).toBeVisible()
await sidePanel.getByRole('button', { name: /Save All/ }).click()
await sidePanel.getByRole('button', { name: /^Save/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'edited row' })).toBeVisible()
@@ -447,11 +447,11 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('2 pending changes')).toBeVisible()
await page.getByRole('button', { name: 'View Details' }).click()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('2 operations')).toBeVisible()
await sidePanel.getByRole('button', { name: /Save All/ }).click()
await sidePanel.getByRole('button', { name: /^Save/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await page.getByRole('button', { name: `View ${tableName1}`, exact: true }).click()