-
-
-
{fullTableName}
-
-
New row
+
+
+ {fullTableName}
+
+
+ New row
-
}
+ aria-label="Revert change"
+ className="px-1.5"
+ icon={
}
onClick={handleDelete}
- className="shrink-0"
- aria-label="Remove operation"
+ tooltip={{
+ content: {
+ side: 'bottom',
+ align: 'end',
+ text: 'Revert change',
+ },
+ }}
/>
-
+
-
+
{previewColumns.map(([key, value]) => (
-
-
{key}:
-
+
+ +
+ {key}:
+
{formatOperationItemValue(value)}
))}
{remainingCount > 0 && (
- +{remainingCount} more column(s)
+
+ +
+ +{remainingCount} more column(s)
+
)}
-
-
+
+
)
}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx
index 8e641460788..56069c3b2c6 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx
@@ -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 (
-
-
+
+
-
-
-
{fullTableName}
-
-
Delete row
-
·
-
where {whereClause}
+
+
+ {fullTableName}
+
+
+ Delete row
+ ·
+ where {whereClause}
-
}
+ aria-label="Revert change"
+ className="px-1.5"
+ icon={
}
onClick={handleDelete}
- className="shrink-0"
- aria-label="Remove operation"
+ tooltip={{
+ content: {
+ side: 'bottom',
+ align: 'end',
+ text: 'Revert change',
+ },
+ }}
/>
-
+
-
-
+
+ Row will be deleted
+
+
)
}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx
index c9476493de6..4773099a270 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx
@@ -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 (
-
-
+
+
-
{fullTableName}
-
-
{columnName}
-
•
-
where {whereClause}
+
+ {fullTableName}
+
+
+ {columnName}
+ ·
+ where {whereClause}
}
+ aria-label="Revert change"
+ className="px-1.5"
+ icon={
}
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',
+ },
+ }}
/>
-
+
-
-
-
-
-
+
+
+ -
+
{formattedOldValue}
-
-
+
-
+
+ +
+
{formattedNewValue}
-
-
+
+
)
}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx
index 0468be02806..1c3dc3b7559 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx
@@ -15,7 +15,11 @@ interface OperationListProps {
export const OperationList = ({ operations }: OperationListProps) => {
if (operations.length === 0) {
- return
No pending changes
+ return (
+
+ )
}
const addOperations = operations.filter(isAddRowOperation)
@@ -23,12 +27,13 @@ export const OperationList = ({ operations }: OperationListProps) => {
const editOperations = operations.filter(isEditCellContentOperation)
return (
-
+
{deleteOperations.length > 0 && (
-
- Rows to Delete ({deleteOperations.length})
+
+ {deleteOperations.length} row deletion{deleteOperations.length !== 1 ? 's' : ''}
+
{deleteOperations.map((op) => (
{
{addOperations.length > 0 && (
-
- Rows to Add ({addOperations.length})
+
+ {addOperations.length} row addition{addOperations.length !== 1 ? 's' : ''}
{addOperations.map((op) => (
@@ -62,9 +67,10 @@ export const OperationList = ({ operations }: OperationListProps) => {
{editOperations.length > 0 && (
-
- Cell Edits ({editOperations.length})
+
+ {editOperations.length} cell edit{editOperations.length !== 1 ? 's' : ''}
+
{editOperations.map((op) => (
event.preventDefault()} // Prevent focus on first focussable element since it is the revert changes ButtonTooltip
header={
Pending changes
-
+
{operations.length} operation{operations.length !== 1 ? 's' : ''}
@@ -41,25 +42,29 @@ export const OperationQueueSidePanel = ({ visible, closePanel }: OperationQueueS
Close
- {modKey}.
+ {modKey}.
-
- Cancel All
+
+ Revert{operations.length > 1 && ' all'}
- Save All
- {modKey}S
+ Save{operations.length > 1 && ' all'}
+ {modKey}S
}
>
-
+
diff --git a/apps/studio/styles/grid.scss b/apps/studio/styles/grid.scss
index 8664d99a400..faed1baaf7c 100644
--- a/apps/studio/styles/grid.scss
+++ b/apps/studio/styles/grid.scss
@@ -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;
}
diff --git a/e2e/studio/features/queue-table-operations.spec.ts b/e2e/studio/features/queue-table-operations.spec.ts
index 89d637f4e8a..c3eacb6ba71 100644
--- a/e2e/studio/features/queue-table-operations.spec.ts
+++ b/e2e/studio/features/queue-table-operations.spec.ts
@@ -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()