Chore/minor qol improvements to queue operations (#44169)

## Context

Resolves FE-2877

Some minor UI nudges for the queue operations feature

- Update all "cancel" copy to "discard" for clarity
<img width="502" height="194" alt="image"
src="https://github.com/user-attachments/assets/719772ad-aa15-4f30-ae56-9c2aad4f6dd2"
/>
- Shift "review" and "cancel" actions in action bar into a dropdown
<img width="368" height="180" alt="image"
src="https://github.com/user-attachments/assets/8762625d-fe2e-4b63-84ab-1f078311d97e"
/>
This commit is contained in:
Joshen Lim
2026-03-25 15:46:14 +08:00
committed by GitHub
parent 8cd43e3584
commit 6e0fbbd2f4
7 changed files with 72 additions and 46 deletions

View File

@@ -2,10 +2,18 @@ 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 { useEffect, useState } from 'react'
import { Eye, MoreVertical, Trash } from 'lucide-react'
import { createPortal } from 'react-dom'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Button } from 'ui'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
KeyboardShortcut,
} from 'ui'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
@@ -48,24 +56,10 @@ export const SaveQueueActionBar = () => {
}}
>
<div className="flex items-center gap-x-12 pl-4 pr-2 py-2 bg-surface-100 border rounded-lg shadow-lg">
<p className="text-xs text-foreground-light max-w-40 truncate">
{operationCount} pending change{operationCount !== 1 ? 's' : ''}
</p>
<div className="flex items-center gap-x-2">
<span className="text-xs text-foreground-light max-w-40 truncate">
{operationCount} pending change{operationCount !== 1 ? 's' : ''}
</span>
<Button
type="default"
size="tiny"
disabled={isSaving}
onClick={() => snap.toggleViewOperationQueue()}
>
Review{' '}
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}.`}</span>
</Button>
</div>
<div className="flex items-center gap-x-2">
<Button type="default" onClick={confirmOnClose} disabled={isSaving}>
Cancel
</Button>
<Button
size="tiny"
type="primary"
@@ -76,6 +70,35 @@ export const SaveQueueActionBar = () => {
Save
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}S`}</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="outline"
className="w-7"
icon={<MoreVertical />}
aria-label="More options"
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40">
<DropdownMenuItem
className="justify-between"
onClick={() => snap.toggleViewOperationQueue()}
>
<div className="flex items-center gap-x-2">
<Eye size={14} />
<span>Review</span>
</div>
<KeyboardShortcut keys={['Meta', '.']} />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={confirmOnClose}>
<div className="flex items-center gap-x-2">
<Trash size={14} />
<span>Discard</span>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</motion.div>

View File

@@ -62,7 +62,7 @@ export const AddRowOperationItem = ({
</div>
<ButtonTooltip
type="text"
aria-label="Revert change"
aria-label="Discard change"
className="w-7"
icon={<Undo2 />}
onClick={handleDelete}
@@ -70,7 +70,7 @@ export const AddRowOperationItem = ({
content: {
side: 'left',
align: 'end',
text: 'Revert change',
text: 'Discard change',
},
}}
/>

View File

@@ -62,7 +62,7 @@ export const DeleteRowOperationItem = ({
</div>
<ButtonTooltip
type="text"
aria-label="Revert change"
aria-label="Discard change"
className="w-7"
icon={<Undo2 />}
onClick={handleDelete}
@@ -70,7 +70,7 @@ export const DeleteRowOperationItem = ({
content: {
side: 'left',
align: 'end',
text: 'Revert change',
text: 'Discard change',
},
}}
/>

View File

@@ -59,7 +59,7 @@ export const OperationItem = ({ operationId, tableId, content }: OperationItemPr
</div>
<ButtonTooltip
type="text"
aria-label="Revert change"
aria-label="Discard change"
className="px-1.5"
icon={<Undo2 />}
onClick={handleDelete}
@@ -67,7 +67,7 @@ export const OperationItem = ({ operationId, tableId, content }: OperationItemPr
content: {
side: 'left',
align: 'end',
text: 'Revert change',
text: 'Discard change',
},
}}
/>

View File

@@ -63,7 +63,7 @@ export const OperationQueueSidePanel = () => {
onClick={confirmOnClose}
disabled={isSaving || operations.length === 0}
>
Cancel
Discard
</Button>
<Button
onClick={handleSave}

View File

@@ -14,6 +14,14 @@ const enableQueueOperations = async (page: Page) => {
}, QUEUE_OPERATIONS_KEY)
}
const openQueueDropdownAndClick = async (page: Page, itemName: string) => {
await page.getByRole('button', { name: 'More options' }).click()
await page.getByRole('menuitem', { name: itemName }).click()
}
const clickReview = async (page: Page) => openQueueDropdownAndClick(page, 'Review')
const clickDiscard = async (page: Page) => openQueueDropdownAndClick(page, 'Discard')
test.describe('Queue Table Operations', () => {
test.beforeEach(async ({ page, ref }) => {
const loadPromise = waitForTableToLoad(page, ref)
@@ -56,7 +64,7 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
await clickReview(page)
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
@@ -101,9 +109,8 @@ test.describe('Queue Table Operations', () => {
await page.keyboard.press('Enter')
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
await page.getByRole('button', { name: 'Cancel', exact: true }).click()
await clickDiscard(page)
const confirmDialog = page.getByRole('alertdialog')
await expect(confirmDialog.getByRole('heading', { name: 'Unsaved changes' })).toBeVisible()
@@ -143,17 +150,14 @@ test.describe('Queue Table Operations', () => {
await page.keyboard.press('Enter')
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
await page.getByRole('button', { name: 'Cancel', exact: true }).click()
await clickDiscard(page)
const confirmDialog = page.getByRole('alertdialog')
await expect(confirmDialog.getByRole('heading', { name: 'Unsaved changes' })).toBeVisible()
await confirmDialog.getByRole('button', { name: 'Keep editing' }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
await expect(sidePanel.getByText('1 operation')).toBeVisible()
await expect(page.getByText('1 pending change')).toBeVisible()
})
test('row inserts are queued and can be saved', async ({ page, ref }) => {
@@ -186,7 +190,7 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByRole('gridcell', { name: 'new row value' })).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
await clickReview(page)
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
@@ -231,7 +235,7 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('2 pending changes')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
await clickReview(page)
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('2 operations')).toBeVisible()
@@ -276,10 +280,10 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('2 pending changes')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
await clickReview(page)
const sidePanel = page.getByRole('dialog')
const removeButtons = sidePanel.getByRole('button', { name: 'Revert change' })
const removeButtons = sidePanel.getByRole('button', { name: 'Discard change' })
await removeButtons.last().click()
await expect(sidePanel.getByText('1 operation')).toBeVisible()
@@ -323,7 +327,7 @@ test.describe('Queue Table Operations', () => {
await page.keyboard.press('ControlOrMeta+.')
await expect(page.getByRole('dialog')).not.toBeVisible()
await expect(page.getByRole('button', { name: /Review/ })).toBeVisible()
await expect(page.getByText('pending change')).toBeVisible()
await page.keyboard.press('ControlOrMeta+s')
await expect(page.getByText('Changes saved successfully')).toBeVisible()
@@ -519,7 +523,7 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
await clickReview(page)
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
@@ -560,8 +564,7 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
await page.getByRole('button', { name: 'Cancel', exact: true }).click()
await clickDiscard(page)
const confirmDialog = page.getByRole('alertdialog')
await expect(confirmDialog.getByRole('heading', { name: 'Unsaved changes' })).toBeVisible()
@@ -611,7 +614,7 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('3 pending changes')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
await clickReview(page)
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('3 operations')).toBeVisible()
await expect(sidePanel.getByText('1 row deletion')).toBeVisible()
@@ -667,7 +670,7 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByText('2 pending changes')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
await clickReview(page)
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('2 operations')).toBeVisible()
@@ -740,7 +743,7 @@ test.describe('Queue Table Operations', () => {
await expect(page.getByRole('gridcell', { name: 'Jones' })).toBeVisible()
// Review the queued operations
await page.getByRole('button', { name: /Review/ }).click()
await clickReview(page)
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('2 cell edits')).toBeVisible()

View File

@@ -24,7 +24,7 @@ export const KeyboardShortcut = ({ keys }: { keys: string[] }) => {
<span
className={cn(
['Shift', 'Ctrl'].includes(key) ? 'px-1.5 py-0.5' : 'w-[23px] h-[23px]',
'border border-foreground-lightest',
'border border-control',
'rounded flex items-center justify-center cursor-default'
)}
key={key}