Files
supabase/e2e/studio/features/queue-table-operations.spec.ts
Danny WhiteandAli Waseem e8bb51ec84 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>
2026-02-09 10:25:06 +11:00

467 lines
19 KiB
TypeScript

import { expect, Page } from '@playwright/test'
import { createTable, dropTable } from '../utils/db/index.js'
import { releaseFileOnceCleanup, withFileOnceSetup } from '../utils/once-per-file.js'
import { deleteTable } from '../utils/table-helpers.js'
import { test } from '../utils/test.js'
import { toUrl } from '../utils/to-url.js'
import { waitForTableToLoad } from '../utils/wait-for-response.js'
const QUEUE_OPERATIONS_KEY = 'supabase-ui-queue-operations'
const tableNamePrefix = 'pw_queue_table'
const enableQueueOperations = async (page: Page) => {
await page.evaluate((key) => {
localStorage.setItem(key, 'true')
}, QUEUE_OPERATIONS_KEY)
}
test.describe('Queue Table Operations', () => {
test.beforeAll(async ({ browser, ref }) => {
await withFileOnceSetup(import.meta.url, async () => {
const ctx = await browser.newContext()
const page = await ctx.newPage()
const loadPromise = waitForTableToLoad(page, ref)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await loadPromise
const viewButtons = page.getByRole('button', { name: /^View / })
const names = await Promise.all(
(await viewButtons.all()).map(async (btn) => {
const ariaLabel = await btn.getAttribute('aria-label')
const name = ariaLabel ? ariaLabel.replace(/^View\s+/, '').trim() : ''
return name
})
)
const tablesToDelete = names.filter((tableName) => tableName.startsWith(tableNamePrefix))
for (const tableName of tablesToDelete) {
await deleteTable(page, ref, tableName)
await expect
.poll(async () => {
return await page.getByLabel(`View ${tableName}`, { exact: true }).count()
})
.toBe(0)
}
await ctx.close()
})
})
test.beforeEach(async ({ page, ref }) => {
const loadPromise = waitForTableToLoad(page, ref)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await loadPromise
await enableQueueOperations(page)
await page.reload({ waitUntil: 'networkidle' })
})
test.afterAll(async () => {
await releaseFileOnceCleanup(import.meta.url)
})
test('cell edits are queued and can be saved', async ({ page, ref }) => {
const tableName = `${tableNamePrefix}_cell_edit`
const columnName = 'name'
await createTable(tableName, columnName, [{ name: 'original value' }])
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await enableQueueOperations(page)
await page.reload()
await waitForTableToLoad(page, ref)
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await expect(page.getByRole('gridcell', { name: 'original value' })).toBeVisible()
const cell = page.getByRole('gridcell', { name: 'original value' })
await cell.dblclick()
const editor = page.getByRole('textbox', { name: /Editor content/ })
await expect(editor).toBeVisible()
await editor.fill('edited value')
await page.keyboard.press('Enter')
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('Pending changes')).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/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'edited value' })).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'original value' })).not.toBeVisible()
await dropTable(tableName)
})
test('cell edits can be cancelled', async ({ page, ref }) => {
const tableName = `${tableNamePrefix}_cell_cancel`
const columnName = 'name'
await createTable(tableName, columnName, [{ name: 'keep this value' }])
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await enableQueueOperations(page)
await page.reload()
await waitForTableToLoad(page, ref)
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await expect(page.getByRole('gridcell', { name: 'keep this value' })).toBeVisible()
const cell = page.getByRole('gridcell', { name: 'keep this value' })
await cell.dblclick()
const editor = page.getByRole('textbox', { name: /Editor content/ })
await editor.fill('should be cancelled')
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: '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()
await expect(page.getByText('pending change')).not.toBeVisible()
await dropTable(tableName)
})
test('row inserts are queued and can be saved', async ({ page, ref }) => {
const tableName = `${tableNamePrefix}_row_insert`
const columnName = 'name'
await createTable(tableName, columnName)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await enableQueueOperations(page)
await page.reload()
await waitForTableToLoad(page, ref)
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${columnName}-input`).fill('new row value')
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByText('1 pending change')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'new row value' })).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
await expect(sidePanel.getByText('1 row addition')).toBeVisible()
await expect(sidePanel.getByText('New row', { exact: true })).toBeVisible()
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()
await dropTable(tableName)
})
test('multiple operations can be batched and saved together', async ({ page, ref }) => {
const tableName = `${tableNamePrefix}_batch_ops`
const columnName = 'name'
await createTable(tableName, columnName)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await enableQueueOperations(page)
await page.reload()
await waitForTableToLoad(page, ref)
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${columnName}-input`).fill('row one')
await page.getByTestId('action-bar-save-row').click()
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${columnName}-input`).fill('row two')
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByText('2 pending changes')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('2 operations')).toBeVisible()
await expect(sidePanel.getByText('2 row additions')).toBeVisible()
await sidePanel.getByRole('button', { name: /^Save/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'row one' })).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'row two' })).toBeVisible()
await dropTable(tableName)
})
test('individual operations can be removed from the queue', async ({ page, ref }) => {
const tableName = `${tableNamePrefix}_remove_op`
const columnName = 'name'
await createTable(tableName, columnName)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await enableQueueOperations(page)
await page.reload()
await waitForTableToLoad(page, ref)
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${columnName}-input`).fill('keep this row')
await page.getByTestId('action-bar-save-row').click()
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${columnName}-input`).fill('remove this row')
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByText('2 pending changes')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
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/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'keep this row' })).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'remove this row' })).not.toBeVisible()
await dropTable(tableName)
})
test('keyboard shortcuts work for queue operations', async ({ page, ref }) => {
const tableName = `${tableNamePrefix}_shortcuts`
const columnName = 'name'
await createTable(tableName, columnName)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await enableQueueOperations(page)
await page.reload()
await waitForTableToLoad(page, ref)
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${columnName}-input`).fill('shortcut test')
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByText('1 pending change')).toBeVisible()
await page.keyboard.press('ControlOrMeta+.')
await expect(page.getByRole('dialog').getByText('Pending changes')).toBeVisible()
await page.keyboard.press('ControlOrMeta+.')
await expect(page.getByRole('button', { name: /Review/ })).toBeVisible()
await page.keyboard.press('ControlOrMeta+s')
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'shortcut test' })).toBeVisible()
await dropTable(tableName)
})
test('row deletes via context menu are queued', async ({ page, ref }) => {
const tableName = `${tableNamePrefix}_row_delete`
const columnName = 'name'
await createTable(tableName, columnName, [{ name: 'row to delete' }])
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await enableQueueOperations(page)
await page.reload()
await waitForTableToLoad(page, ref)
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await expect(page.getByRole('gridcell', { name: 'row to delete' })).toBeVisible()
const cell = page.getByRole('gridcell', { name: 'row to delete' })
await cell.click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Delete row' }).click()
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
await expect(sidePanel.getByText('1 row deletion')).toBeVisible()
await expect(sidePanel.getByText('Delete row', { exact: true })).toBeVisible()
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()
await expect(page.getByText('0 records')).toBeVisible()
await deleteTable(page, ref, tableName)
})
test('row deletes can be cancelled', async ({ page, ref }) => {
const tableName = `${tableNamePrefix}_delete_cancel`
const columnName = 'name'
await createTable(tableName, columnName, [{ name: 'should not be deleted' }])
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await enableQueueOperations(page)
await page.reload()
await waitForTableToLoad(page, ref)
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
const cell = page.getByRole('gridcell', { name: 'should not be deleted' })
await cell.click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Delete row' }).click()
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 expect(page.getByRole('gridcell', { name: 'should not be deleted' })).toBeVisible()
await expect(page.getByText('pending change')).not.toBeVisible()
await dropTable(tableName)
})
test('mixed operations (add, edit, delete) can be batched', async ({ page, ref }) => {
const tableName = `${tableNamePrefix}_mixed_ops`
const columnName = 'name'
await createTable(tableName, columnName, [
{ name: 'row to edit' },
{ name: 'row to delete' },
])
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await enableQueueOperations(page)
await page.reload()
await waitForTableToLoad(page, ref)
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
const cellToEdit = page.getByRole('gridcell', { name: 'row to edit' })
await cellToEdit.dblclick()
const editor = page.getByRole('textbox', { name: /Editor content/ })
await editor.fill('edited row')
await page.keyboard.press('Enter')
const cellToDelete = page.getByRole('gridcell', { name: 'row to delete' })
await cellToDelete.click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Delete row' }).click()
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${columnName}-input`).fill('new row')
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByText('3 pending changes')).toBeVisible()
await page.getByRole('button', { name: /Review/ }).click()
const sidePanel = page.getByRole('dialog')
await expect(sidePanel.getByText('3 operations')).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/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'edited row' })).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'new row' })).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'row to delete' })).not.toBeVisible()
await expect(page.getByRole('gridcell', { name: 'row to edit' })).not.toBeVisible()
await dropTable(tableName)
})
test('pending changes persist when switching between tables', async ({ page, ref }) => {
const tableName1 = `${tableNamePrefix}_persist1`
const tableName2 = `${tableNamePrefix}_persist2`
const columnName = 'name'
await createTable(tableName1, columnName)
await createTable(tableName2, columnName)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await enableQueueOperations(page)
await page.reload()
await waitForTableToLoad(page, ref)
await page.getByRole('button', { name: `View ${tableName1}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${columnName}-input`).fill('pending in table 1')
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByText('1 pending change')).toBeVisible()
await page.getByRole('button', { name: `View ${tableName2}`, exact: true }).click()
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${columnName}-input`).fill('pending in table 2')
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByText('2 pending changes')).toBeVisible()
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/ }).click()
await expect(page.getByText('Changes saved successfully')).toBeVisible()
await page.getByRole('button', { name: `View ${tableName1}`, exact: true }).click()
await expect(page.getByRole('gridcell', { name: 'pending in table 1' })).toBeVisible()
await page.getByRole('button', { name: `View ${tableName2}`, exact: true }).click()
await expect(page.getByRole('gridcell', { name: 'pending in table 2' })).toBeVisible()
await dropTable(tableName1)
await dropTable(tableName2)
})
})