diff --git a/apps/studio/components/interfaces/Storage/__tests__/DeleteBucketModal.test.tsx b/apps/studio/components/interfaces/Storage/__tests__/DeleteBucketModal.test.tsx index aa407b64a63..8ec31935b78 100644 --- a/apps/studio/components/interfaces/Storage/__tests__/DeleteBucketModal.test.tsx +++ b/apps/studio/components/interfaces/Storage/__tests__/DeleteBucketModal.test.tsx @@ -110,7 +110,7 @@ describe(`DeleteBucketModal`, () => { await userEvent.click(openButton) await screen.findByRole(`dialog`) - const input = screen.getByLabelText(/Type/) + const input = screen.getByPlaceholderText(/Type/) await userEvent.type(input, `test`) const confirmButton = screen.getByRole(`button`, { name: `Delete bucket` }) diff --git a/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx b/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx index fd7ea831601..6dcb98600a1 100644 --- a/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx +++ b/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx @@ -655,10 +655,13 @@ describe('SupportFormPage', () => { renderSupportFormPage() - await waitFor(() => { - expect(getOrganizationSelector(screen)).toHaveTextContent('Organization 1') - expect(getProjectSelector(screen)).toHaveTextContent('Project 1') - }) + await waitFor( + () => { + expect(getOrganizationSelector(screen)).toHaveTextContent('Organization 1') + expect(getProjectSelector(screen)).toHaveTextContent('Project 1') + }, + { timeout: 5_000 } + ) await selectCategoryOption(screen, 'Dashboard bug') await waitFor(() => { @@ -748,10 +751,13 @@ describe('SupportFormPage', () => { renderSupportFormPage() - await waitFor(() => { - expect(getOrganizationSelector(screen)).toHaveTextContent('Organization 1') - expect(getProjectSelector(screen)).toHaveTextContent('Project 1') - }) + await waitFor( + () => { + expect(getOrganizationSelector(screen)).toHaveTextContent('Organization 1') + expect(getProjectSelector(screen)).toHaveTextContent('Project 1') + }, + { timeout: 5_000 } + ) await selectCategoryOption(screen, 'APIs and client libraries') await waitFor(() => { diff --git a/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx b/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx index ad47117abb0..b68e8230141 100644 --- a/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx @@ -261,7 +261,7 @@ const DeleteConfirmationDialogs = ({ }} onConfirm={onConfirmDeleteTable} > -
+

Are you sure you want to delete the selected table? This action cannot be undone.

diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx index c84c8b5b32e..935915f6e7e 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx @@ -157,6 +157,7 @@ const RowEditor = ({ return ( -
    +
    • Project members will no longer be able to view this query. diff --git a/e2e/studio/env.config.ts b/e2e/studio/env.config.ts index e7d451460d6..bb743583f85 100644 --- a/e2e/studio/env.config.ts +++ b/e2e/studio/env.config.ts @@ -18,4 +18,4 @@ export const env = { process.env.VERCEL_AUTOMATION_BYPASS_SELFHOSTED_STUDIO || 'false', } -export const STORAGE_STATE_PATH = path.join(__dirname, './playwright/.auth/user.json') +export const STORAGE_STATE_PATH = path.join(import.meta.dirname, './playwright/.auth/user.json') diff --git a/e2e/studio/features/_global.setup.ts b/e2e/studio/features/_global.setup.ts index 187e72fb8fb..b0e0cc82880 100644 --- a/e2e/studio/features/_global.setup.ts +++ b/e2e/studio/features/_global.setup.ts @@ -1,7 +1,7 @@ import { expect, test as setup } from '@playwright/test' import dotenv from 'dotenv' import path from 'path' -import { env, STORAGE_STATE_PATH } from '../env.config' +import { env, STORAGE_STATE_PATH } from '../env.config.js' /** * Run any setup tasks for the tests. @@ -9,7 +9,7 @@ import { env, STORAGE_STATE_PATH } from '../env.config' */ dotenv.config({ - path: path.resolve(__dirname, '..', '.env.local'), + path: path.resolve(import.meta.dirname, '..', '.env.local'), override: true, }) diff --git a/e2e/studio/features/database.spec.ts b/e2e/studio/features/database.spec.ts index d90b1100dae..4e69ef126e7 100644 --- a/e2e/studio/features/database.spec.ts +++ b/e2e/studio/features/database.spec.ts @@ -1,12 +1,12 @@ import { expect, Page } from '@playwright/test' -import { env } from '../env.config' -import { test } from '../utils/test' -import { toUrl } from '../utils/to-url' +import { env } from '../env.config.js' +import { test } from '../utils/test.js' +import { toUrl } from '../utils/to-url.js' import { + createApiResponseWaiter, waitForApiResponse, waitForDatabaseToLoad, - createApiResponseWaiter, -} from '../utils/wait-for-response' +} from '../utils/wait-for-response.js' const databaseTableName = 'pw_database_table' const databaseTableNameNew = 'pw_database_table_new' @@ -386,9 +386,7 @@ test.describe.serial('Database', () => { const triggerRow = await page.getByRole('row', { name: databaseTriggerName }) await triggerRow.getByRole('button', { name: 'More options' }).click() await page.getByRole('menuitem', { name: 'Delete trigger' }).click() - await page - .getByRole('textbox', { name: `Type ${databaseTriggerName} to confirm.` }) - .fill(databaseTriggerName) + await page.getByPlaceholder('Type in name of trigger').fill(databaseTriggerName) await page.getByRole('button', { name: `Delete trigger ${databaseTriggerName}` }).click() await expect( page.getByText(`Successfully removed ${databaseTriggerName}`), @@ -441,9 +439,7 @@ test.describe.serial('Database', () => { // delete trigger await updatedTriggerRow.getByRole('button', { name: 'More options' }).click() await page.getByRole('menuitem', { name: 'Delete trigger' }).click() - await page - .getByRole('textbox', { name: `Type ${databaseTriggerNameUpdated} to confirm.` }) - .fill(databaseTriggerNameUpdated) + await page.getByPlaceholder('Type in name of trigger').fill(databaseTriggerNameUpdated) await page .getByRole('button', { name: `Delete trigger ${databaseTriggerNameUpdated}` }) .click() @@ -791,9 +787,7 @@ END;`) // delete function await updatedFunctionRow.getByRole('button', { name: 'More options' }).click() await page.getByRole('menuitem', { name: 'Delete function' }).click() - await page - .getByRole('textbox', { name: `Type ${databaseFunctionNameUpdated} to confirm.` }) - .fill(databaseFunctionNameUpdated) + await page.getByPlaceholder('Type in name of function').fill(databaseFunctionNameUpdated) await page .getByRole('button', { name: `Delete function ${databaseFunctionNameUpdated}` }) .click() diff --git a/e2e/studio/features/sql-editor.spec.ts b/e2e/studio/features/sql-editor.spec.ts index 46b56cd8e83..2312e5f5e8f 100644 --- a/e2e/studio/features/sql-editor.spec.ts +++ b/e2e/studio/features/sql-editor.spec.ts @@ -1,11 +1,11 @@ import { expect, Page } from '@playwright/test' import fs from 'fs' -import { isCLI } from '../utils/is-cli' -import { test } from '../utils/test' -import { toUrl } from '../utils/to-url' -import { waitForApiResponse } from '../utils/wait-for-response' -import { waitForApiResponseWithTimeout } from '../utils/wait-for-response-with-timeout' -import { resetLocalStorage } from '../utils/reset-local-storage' +import { isCLI } from '../utils/is-cli.js' +import { resetLocalStorage } from '../utils/reset-local-storage.js' +import { test } from '../utils/test.js' +import { toUrl } from '../utils/to-url.js' +import { waitForApiResponseWithTimeout } from '../utils/wait-for-response-with-timeout.js' +import { waitForApiResponse } from '../utils/wait-for-response.js' const sqlSnippetName = 'pw_sql_snippet' const sqlSnippetNameDuplicate = 'pw_sql_snippet (Duplicate)' @@ -122,10 +122,14 @@ test.describe('SQL Editor', () => { await page.locator('.view-lines').click() await page.keyboard.press('ControlOrMeta+KeyA') await page.keyboard.type(`select 'hello world';`) + + const sqlMutationPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { + method: 'POST', + }) await page.getByTestId('sql-run-button').click() + await sqlMutationPromise // verify the result - await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) await expect(page.getByRole('gridcell', { name: 'hello world' })).toBeVisible() // SQL written in the editor should not be the previous query. @@ -353,7 +357,11 @@ hello world`) await sharedSnippet.getByText(sqlSnippetNameShare).click({ button: 'right' }) await page.getByRole('menuitem', { name: 'Unshare query with team' }).click() await expect(page.getByRole('heading', { name: 'Confirm to unshare query:' })).toBeVisible() + + const unsharePromise = waitForApiResponse(page, 'projects', ref, 'content', { method: 'PUT' }) await page.getByRole('button', { name: 'Unshare query', exact: true }).click() + await unsharePromise + await expect(page.getByTestId('confirm-unshare-snippet-modal')).not.toBeVisible() await expect(sharedSnippet.getByText(sqlSnippetNameShare, { exact: true })).not.toBeVisible() // clear SQL snippet diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts index dfb5116bfb3..20151da746b 100644 --- a/e2e/studio/features/table-editor.spec.ts +++ b/e2e/studio/features/table-editor.spec.ts @@ -1,16 +1,17 @@ import { expect, Locator, Page } from '@playwright/test' import fs from 'fs' import path from 'path' -import { isCLI } from '../utils/is-cli' -import { resetLocalStorage } from '../utils/reset-local-storage' -import { test } from '../utils/test' -import { toUrl } from '../utils/to-url' +import { isCLI } from '../utils/is-cli.js' +import { releaseFileOnceCleanup, withFileOnceSetup } from '../utils/once-per-file.js' +import { resetLocalStorage } from '../utils/reset-local-storage.js' +import { test } from '../utils/test.js' +import { toUrl } from '../utils/to-url.js' +import { waitForApiResponseWithTimeout } from '../utils/wait-for-response-with-timeout.js' import { waitForApiResponse, waitForGridDataToLoad, waitForTableToLoad, -} from '../utils/wait-for-response' -import { waitForApiResponseWithTimeout } from '../utils/wait-for-response-with-timeout' +} from '../utils/wait-for-response.js' const tableNamePrefix = 'pw_table' const columnName = 'pw_column' @@ -66,6 +67,7 @@ const createTable = async (page: Page, ref: string, tableName: string) => { ) await page.getByRole('button', { name: 'Save' }).click() await Promise.all([createTablePromise, tablesPromise, entitiesPromise]) + await page.waitForSelector('[data-testid="table-editor-side-panel"]', { state: 'detached' }) await expect( page.getByRole('button', { name: `View ${tableName}`, exact: true }), 'Table should be visible after creation' @@ -85,6 +87,7 @@ const deleteTable = async (page: Page, ref: string, tableName: string) => { const revalidatePromise = waitForApiResponse(page, 'pg-meta', ref, `query?key=entity-types-`) await page.getByRole('button', { name: 'Delete' }).click() await Promise.all([apiPromise, revalidatePromise]) + await expect(page.getByTestId('confirm-delete-table-modal')).not.toBeVisible() } const deleteEnumIfExist = async (page: Page, ref: string, enumName: string) => { @@ -104,36 +107,47 @@ const deleteEnumIfExist = async (page: Page, ref: string, enumName: string) => { await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) } -test.describe.serial('table editor', () => { +test.describe('table editor', () => { test.beforeAll(async ({ browser, ref }) => { - const ctx = await browser.newContext() - const page = await ctx.newPage() + 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 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)) + 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) - } + 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) + } + }) }) test.beforeEach(async ({ page, ref }) => { const loadPromise = waitForTableToLoad(page, ref) - await page.goto(toUrl(`/project/${ref}/editor?schema=public`)) + page.goto(toUrl(`/project/${ref}/editor?schema=public`)) await loadPromise }) + test.afterAll(async () => { + await releaseFileOnceCleanup(import.meta.url) + }) + test('sidebar actions works as expected', async ({ page, ref }) => { const tableNameActions = 'pw_table_actions' const tableNameActionsDuplicate = 'pw_table_actions_duplicate' @@ -184,9 +198,6 @@ test.describe.serial('table editor', () => { await expect( page.getByLabel(`View ${tableNameActionsDuplicate}`, { exact: true }) ).toBeVisible() - - await deleteTable(page, ref, tableNameActionsDuplicate) - await deleteTable(page, ref, tableNameActions) }) test('switching schemas work as expected', async ({ page, ref }) => { @@ -286,12 +297,12 @@ test.describe.serial('table editor', () => { ).toBeVisible({ timeout: 50000, }) + await expect(page.getByTestId('table-editor-side-panel')).not.toBeVisible() // Wait for the grid to be visible and data to be loaded - await expect( - page.getByRole('grid'), - 'Grid should be visible after inserting data' - ).toBeVisible() + await expect(page.getByRole('grid'), 'Grid should be visible after inserting data').toBeVisible( + { timeout: 10_000 } + ) await expect(page.getByRole('columnheader', { name: enum_name })).toBeVisible() // insert row with enum value @@ -299,6 +310,7 @@ test.describe.serial('table editor', () => { await page.getByText('Insert a new row into').click() await page.getByRole('combobox').selectOption('value1') await page.getByTestId('action-bar-save-row').click() + await expect(page.getByTestId('side-panel-row-editor')).not.toBeVisible() await expect(page.getByRole('gridcell', { name: 'value1' })).toBeVisible() // insert row with another enum value @@ -306,7 +318,7 @@ test.describe.serial('table editor', () => { await page.getByText('Insert a new row into').click() await page.getByRole('combobox').selectOption('value2') await page.getByTestId('action-bar-save-row').click() - await expect(page.getByRole('gridcell', { name: 'value2' })).toBeVisible() + await expect(page.getByRole('gridcell', { name: 'value2' })).toBeVisible({ timeout: 10_000 }) // delete enum and enum table await deleteTable(page, ref, tableNameEnum) @@ -560,7 +572,7 @@ test.describe.serial('table editor', () => { await page.waitForURL(/\/editor\/\d+\?schema=public$/) // importing 50 data via csv file - const csvFilePath = path.join(__dirname, 'files', 'table-editor-import-file.csv') + const csvFilePath = path.join(import.meta.dirname, 'files', 'table-editor-import-file.csv') await page.getByRole('button', { name: 'Import data from CSV' }).click() await page.getByRole('tab', { name: 'Upload CSV' }).click() await page.setInputFiles('input[type="file"]', csvFilePath) @@ -571,7 +583,7 @@ test.describe.serial('table editor', () => { await expect(page.getByText('50 records')).toBeVisible() // importing 51 data via paste text - const filePath = path.join(__dirname, 'files', 'table-editor-import-paste.txt') + const filePath = path.join(import.meta.dirname, 'files', 'table-editor-import-paste.txt') const fileContent = fs.readFileSync(filePath, 'utf-8') await page.getByTestId('table-editor-insert-new-row').click() await page.getByRole('menuitem', { name: 'Import data from CSV' }).click() diff --git a/e2e/studio/package.json b/e2e/studio/package.json index e88c363639a..e788802b57b 100644 --- a/e2e/studio/package.json +++ b/e2e/studio/package.json @@ -1,6 +1,7 @@ { "name": "e2e-studio", "version": "1.0.0", + "type": "module", "main": "index.js", "scripts": { "e2e": "playwright test", diff --git a/e2e/studio/playwright.config.ts b/e2e/studio/playwright.config.ts index d64ba62916a..b99cb159e61 100644 --- a/e2e/studio/playwright.config.ts +++ b/e2e/studio/playwright.config.ts @@ -1,11 +1,9 @@ import { defineConfig } from '@playwright/test' -import { env, STORAGE_STATE_PATH } from './env.config' import dotenv from 'dotenv' import path from 'path' +import { env, STORAGE_STATE_PATH } from './env.config.js' -dotenv.config({ - path: path.resolve(__dirname, '.env.local'), -}) +dotenv.config({ path: path.resolve(import.meta.dirname, '.env.local') }) const IS_CI = !!process.env.CI diff --git a/e2e/studio/tsconfig.json b/e2e/studio/tsconfig.json index fa766b8295d..332d2ddf7bd 100644 --- a/e2e/studio/tsconfig.json +++ b/e2e/studio/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { + "target": "ES2022", + "module": "nodenext", "jsx": "react", "skipLibCheck": true, "esModuleInterop": true diff --git a/e2e/studio/utils/once-per-file.ts b/e2e/studio/utils/once-per-file.ts new file mode 100644 index 00000000000..298b7a08bdc --- /dev/null +++ b/e2e/studio/utils/once-per-file.ts @@ -0,0 +1,175 @@ +import crypto from 'node:crypto' +import fs, { type FileHandle } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = path.join(os.tmpdir(), 'playwright-locks') + +const sha1 = (str: string) => crypto.createHash('sha1').update(str).digest('hex') +const uniqueWorkerId = () => { + const workerIndex = process.env.TEST_WORKER_INDEX + const hostname = os.hostname() + const pid = process.pid + const uniquePart = crypto.randomBytes(4).toString('hex') + + return `${hostname}-${pid}-${workerIndex}-${uniquePart}` +} + +const keyFromModuleUrl = (moduleUrl: string) => { + const absPath = fileURLToPath(moduleUrl) + const key = sha1(absPath) + const dir = path.join(ROOT, key.slice(0, 2), key.slice(2)) + return { absPath, key, dir } +} + +const exists = async (path: string) => { + try { + await fs.access(path) + return true + } catch { + return false + } +} + +const getCodeFromUnknownError = (err: unknown): string | undefined => { + if (err && typeof err === 'object' && 'code' in err) { + return String(err.code) + } +} + +export const withFileOnceSetup = async ( + currentModuleUrl: string, + fn: () => Promise, + { timeoutMs = 120_000 } = {} +): Promise => { + const { absPath, dir } = keyFromModuleUrl(currentModuleUrl) + const lockfile = path.join(dir, `setup.lock`) + const doneFile = path.join(dir, `setup.done.json`) + const leasesDir = path.join(dir, `leases`) + const leaseId = uniqueWorkerId() + const myLease = path.join(leasesDir, leaseId) + + await fs.mkdir(leasesDir, { recursive: true }) + + // Acquire a lease for this worker on this test file + await fs.writeFile(myLease, '') + + // Fast path: another worker already finished setup + if (await exists(doneFile)) { + return + } + + // Try to acquire the setup lock + let fd: FileHandle | null = null + try { + fd = await fs.open(lockfile, 'wx') + } catch (err: unknown) { + if (getCodeFromUnknownError(err) !== 'EEXIST') { + // Drop lease and bail + try { + await fs.unlink(myLease) + } catch {} + throw err + } + + // Another worker is doing setup, wait for done + const start = Date.now() + const baseSleepMs = 500 + const incrementMs = 100 + let attempt = 0 + + while (!(await exists(doneFile))) { + if (Date.now() - start > timeoutMs) { + // Drop lease and bail + try { + await fs.unlink(myLease) + } catch {} + throw new Error(`Timed out waiting for once-per-file setup in ${absPath}`) + } + + const jitter = (Math.random() - 0.5) * 100 + const sleep = baseSleepMs + incrementMs * Math.pow(2, attempt++) + jitter + await new Promise((resolve) => setTimeout(resolve, sleep)) + } + } + + // We have the lock, perform setup and mark done + try { + if (!(await exists(doneFile))) { + await fn() + await fs.writeFile( + doneFile, + JSON.stringify({ at: new Date().toISOString(), file: absPath, pid: process.pid }, null, 2) + ) + } + } finally { + try { + await fd?.close() + } catch {} + try { + await fs.unlink(lockfile) + } catch {} + } +} + +export const releaseFileOnceCleanup = async (currentModuleUrl: string): Promise => { + const { dir } = keyFromModuleUrl(currentModuleUrl) + const leasesDir = path.join(dir, `leases`) + const doneFile = path.join(dir, `setup.done.json`) + const cleanupLock = path.join(dir, `cleanup.lock`) + + // Remove all leases associated with this worker + try { + const entries = await fs.readdir(leasesDir) + await Promise.all( + entries.map(async (name) => { + if (name.includes(`-${process.pid}-`)) { + try { + await fs.unlink(path.join(leasesDir, name)) + } catch {} + } + }) + ) + } catch {} + + // Try to acquire cleanup lock + let fd: FileHandle | null = null + try { + await fs.mkdir(dir, { recursive: true }) + fd = await fs.open(cleanupLock, 'wx') + } catch { + // Another worker is cleaning up; we're done + return + } + + try { + // Only delete if no leases remain + let remaining = 0 + try { + const entries = await fs.readdir(leasesDir) + remaining = entries.length + } catch {} + + if (remaining === 0) { + // Remove done marker and empty directories + try { + await fs.unlink(doneFile) + } catch {} + try { + await fs.rmdir(leasesDir) + } catch {} + // Attempt to remove the key directory if empty + try { + await fs.rmdir(dir) + } catch {} + } + } finally { + try { + await fd?.close() + } catch {} + try { + await fs.unlink(cleanupLock) + } catch {} + } +} diff --git a/e2e/studio/utils/test.ts b/e2e/studio/utils/test.ts index 0aa9754ed7e..267f4644cf9 100644 --- a/e2e/studio/utils/test.ts +++ b/e2e/studio/utils/test.ts @@ -1,10 +1,10 @@ import { test as base } from '@playwright/test' import dotenv from 'dotenv' import path from 'path' -import { env } from '../env.config' +import { env } from '../env.config.js' dotenv.config({ - path: path.resolve(__dirname, '../.env.local'), + path: path.resolve(import.meta.dirname, '../.env.local'), override: true, }) diff --git a/packages/ui/src/components/SidePanel/SidePanel.tsx b/packages/ui/src/components/SidePanel/SidePanel.tsx index b1d9608229f..8c0f9d53c9e 100644 --- a/packages/ui/src/components/SidePanel/SidePanel.tsx +++ b/packages/ui/src/components/SidePanel/SidePanel.tsx @@ -104,6 +104,14 @@ const SidePanel = ({ open = open || visible + const { + onOpenAutoFocus, + onCloseAutoFocus, + onEscapeKeyDown, + onPointerDownOutside, + onInteractOutside, + } = props + return ( {triggerElement && {triggerElement}} @@ -117,15 +125,16 @@ const SidePanel = ({ __styles.align[align], className && className, ].join(' ')} - onOpenAutoFocus={props.onOpenAutoFocus} - onCloseAutoFocus={props.onCloseAutoFocus} - onEscapeKeyDown={props.onEscapeKeyDown} - onPointerDownOutside={props.onPointerDownOutside} + onOpenAutoFocus={onOpenAutoFocus} + onCloseAutoFocus={onCloseAutoFocus} + onEscapeKeyDown={onEscapeKeyDown} + onPointerDownOutside={onPointerDownOutside} onInteractOutside={(event) => { const isToast = (event.target as Element)?.closest('#toast') if (isToast) event.preventDefault() - if (props.onInteractOutside) props.onInteractOutside(event) + if (onInteractOutside) onInteractOutside(event) }} + {...props} > {header &&
      {header}
      }
      {children}