mirror of
https://github.com/supabase/supabase.git
synced 2026-07-01 00:34:18 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const OperationQueueSidePanel = () => {
|
||||
onClick={confirmOnClose}
|
||||
disabled={isSaving || operations.length === 0}
|
||||
>
|
||||
Cancel
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user