Add cancel action in save queue action bar (#44065)

## Context

Related to the queue table operations feature preview

Adding a "cancel" action in the save queue operation bar for convenience
to clear all changes (instead of having to go into the review panel)

Also aligning the positioning of the CTAs to match the review panel
- "Review" imo is a secondary action, while "Save" or "Cancel" are the
primary ones
- Hence am shifting the "review" CTA to the left, contextually beside
the number of pending changes text
<img width="449" height="100" alt="image"
src="https://github.com/user-attachments/assets/c3faa6c1-e244-40ee-b251-44ab1e785c6e"
/>
- This also aligns with the CTA placements in the review panel
<img width="502" height="71" alt="image"
src="https://github.com/user-attachments/assets/35b7de0a-dbf4-4e8a-acef-53508c9b13b9"
/>
- Also removed plural grammar for the button CTAs - thinking thats not
necessary, wanna keep button CTA texts short and sweet + The "x pending
change(s)" also captures the plurality
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-03-24 16:33:20 +08:00
1 parent 98b1b79909
commit 19aea11aa9
3 files changed
+65 -44

No files matched your search

@@ -7,13 +7,15 @@ import { createPortal } from 'react-dom'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Button } from 'ui'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { getModKeyLabel } from '@/lib/helpers'
export const SaveQueueActionBar = () => {
const modKey = getModKeyLabel()
const snap = useTableEditorStateSnapshot()
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
const { handleSave } = useOperationQueueActions()
const { handleSave, handleCancel } = useOperationQueueActions()
useOperationQueueShortcuts()
@@ -24,46 +26,65 @@ export const SaveQueueActionBar = () => {
const isVisible =
isQueueOperationsEnabled && snap.hasPendingOperations && !isOperationQueuePanelOpen
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
checkIsDirty: () => true,
onClose: () => handleCancel(),
})
const content = (
<AnimatePresence>
{isVisible && (
<div className="fixed bottom-12 z-50 left-1/2 -translate-x-1/2">
<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>
<>
<AnimatePresence>
{isVisible && (
<div className="fixed bottom-12 z-50 left-1/2 -translate-x-1/2">
<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-x-12 pl-4 pr-2 py-2 bg-surface-100 border rounded-lg shadow-lg">
<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"
onClick={handleSave}
disabled={isSaving}
loading={isSaving}
>
Save
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}S`}</span>
</Button>
</div>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</motion.div>
</div>
)}
</AnimatePresence>
<DiscardChangesConfirmationDialog {...closeConfirmationModalProps} />
</>
)
if (typeof document === 'undefined' || !document.body) return null
@@ -63,14 +63,14 @@ export const OperationQueueSidePanel = () => {
onClick={confirmOnClose}
disabled={isSaving || operations.length === 0}
>
Revert{operations.length > 1 && ' all'}
Cancel
</Button>
<Button
onClick={handleSave}
disabled={isSaving || operations.length === 0}
loading={isSaving}
>
Save{operations.length > 1 && ' all'}
Save
<span className="text-foreground/40 text-[10px] ml-1.5">{modKey}S</span>
</Button>
</div>
@@ -103,7 +103,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: 'Revert', exact: true }).click()
await page.getByRole('button', { name: 'Cancel', exact: true }).click()
const confirmDialog = page.getByRole('alertdialog')
await expect(confirmDialog.getByRole('heading', { name: 'Unsaved changes' })).toBeVisible()
@@ -145,7 +145,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: 'Revert', exact: true }).click()
await page.getByRole('button', { name: 'Cancel', exact: true }).click()
const confirmDialog = page.getByRole('alertdialog')
await expect(confirmDialog.getByRole('heading', { name: 'Unsaved changes' })).toBeVisible()
@@ -561,7 +561,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: 'Revert', exact: true }).click()
await page.getByRole('button', { name: 'Cancel', exact: true }).click()
const confirmDialog = page.getByRole('alertdialog')
await expect(confirmDialog.getByRole('heading', { name: 'Unsaved changes' })).toBeVisible()