mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
fix: copy cell for record peak portal on forgien relationship (#44144)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES/NO ## What kind of change does this PR introduce? fixes: #37454 The context menu on the FK row is a lie, its just exposing the cell underneath. Ideally this should have its own context menu, right now we only support copy operations. --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
91cffdafd8
commit
a4d32bdd94
6 files changed
+150
-20
No files matched your search
@@ -0,0 +1,41 @@
|
||||
import { ContextMenuContent } from '@ui/components/shadcn/ui/context-menu'
|
||||
import { Copy } from 'lucide-react'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
ContextMenu_Shadcn_,
|
||||
ContextMenuItem_Shadcn_,
|
||||
ContextMenuTrigger_Shadcn_,
|
||||
copyToClipboard,
|
||||
} from 'ui'
|
||||
|
||||
import { formatClipboardValue } from '../../utils/common'
|
||||
|
||||
/**
|
||||
* [Joshen] RowRenderer should be using this so that we can deprecate `react-contextify`
|
||||
* We can probably extend this for all the other actions like edit, delete, etc
|
||||
*/
|
||||
export const CellContextMenuWrapper = ({
|
||||
value,
|
||||
children,
|
||||
}: PropsWithChildren<{ value: string | number | boolean | object | null }>) => {
|
||||
const onCopyCellContent = () => {
|
||||
const text = formatClipboardValue(value)
|
||||
copyToClipboard(text)
|
||||
toast.success('Copied cell value to clipboard')
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu_Shadcn_>
|
||||
<ContextMenuTrigger_Shadcn_ className="flex items-center w-full">
|
||||
{children}
|
||||
</ContextMenuTrigger_Shadcn_>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem_Shadcn_ className="gap-x-2" onSelect={onCopyCellContent}>
|
||||
<Copy size={12} />
|
||||
<span className="text-xs">Copy cell</span>
|
||||
</ContextMenuItem_Shadcn_>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu_Shadcn_>
|
||||
)
|
||||
}
|
||||
@@ -78,6 +78,7 @@ export const ForeignKeyFormatter = (props: Props) => {
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
className="w-6 h-6"
|
||||
aria-label="View referencing record"
|
||||
icon={<ArrowRight />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
tooltip={{ content: { side: 'bottom', text: 'View referencing record' } }}
|
||||
@@ -90,6 +91,9 @@ export const ForeignKeyFormatter = (props: Props) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<ReferenceRecordPeek
|
||||
table={targetTable}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PostgresTable } from '@supabase/postgres-meta'
|
||||
import { keepPreviousData } from '@tanstack/react-query'
|
||||
import { useParams } from 'common'
|
||||
import { COLUMN_MIN_WIDTH } from 'components/grid/constants'
|
||||
import type { SupaColumn, SupaRow } from 'components/grid/types'
|
||||
import {
|
||||
ESTIMATED_CHARACTER_PIXEL_WIDTH,
|
||||
getColumnDefaultWidth,
|
||||
@@ -11,19 +12,20 @@ import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
|
||||
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { Key } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import DataGrid, { Column } from 'react-data-grid'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import DataGrid, { CalculatedColumn, Column } from 'react-data-grid'
|
||||
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { BinaryFormatter } from './BinaryFormatter'
|
||||
import { CellContextMenuWrapper } from './CellContextMenuWrapper'
|
||||
import { DefaultFormatter } from './DefaultFormatter'
|
||||
import { JsonFormatter } from './JsonFormatter'
|
||||
|
||||
interface ReferenceRecordPeekProps {
|
||||
table: PostgresTable
|
||||
column: string
|
||||
value: any
|
||||
value: string | number | Record<string, unknown>
|
||||
}
|
||||
|
||||
export const ReferenceRecordPeek = ({ table, column, value }: ReferenceRecordPeekProps) => {
|
||||
@@ -47,6 +49,9 @@ export const ReferenceRecordPeek = ({ table, column, value }: ReferenceRecordPee
|
||||
{ placeholderData: keepPreviousData }
|
||||
)
|
||||
|
||||
const rows = useMemo(() => data?.rows ?? [], [data?.rows])
|
||||
const selectedCellRef = useRef<{ idx: number; rowIdx: number } | null>(null)
|
||||
|
||||
const primaryKeys = useMemo(() => table.primary_keys.map((x) => x.name), [table.primary_keys])
|
||||
|
||||
const columns = useMemo(() => {
|
||||
@@ -54,14 +59,14 @@ export const ReferenceRecordPeek = ({ table, column, value }: ReferenceRecordPee
|
||||
const columnDefaultWidth = getColumnDefaultWidth({
|
||||
dataType: column.data_type,
|
||||
format: column.format,
|
||||
} as any)
|
||||
} as Pick<SupaColumn, 'dataType' | 'format'> as SupaColumn)
|
||||
const columnWidthBasedOnName =
|
||||
(column.name.length + column.format.length) * ESTIMATED_CHARACTER_PIXEL_WIDTH
|
||||
const columnWidth =
|
||||
columnDefaultWidth < columnWidthBasedOnName ? columnWidthBasedOnName : columnDefaultWidth
|
||||
const isPrimaryKey = primaryKeys.includes(column.name)
|
||||
|
||||
const res: Column<any> = {
|
||||
const res: Column<SupaRow> = {
|
||||
key: column.name,
|
||||
name: column.name,
|
||||
resizable: false,
|
||||
@@ -84,11 +89,17 @@ export const ReferenceRecordPeek = ({ table, column, value }: ReferenceRecordPee
|
||||
<span className="text-xs text-foreground-light font-normal">{column.format}</span>
|
||||
</div>
|
||||
),
|
||||
renderCell: isBinaryColumn(column.data_type)
|
||||
? BinaryFormatter
|
||||
: isJsonColumn(column.data_type) && !isArrayColumn(column.data_type)
|
||||
? JsonFormatter
|
||||
: DefaultFormatter,
|
||||
renderCell: (props) => (
|
||||
<CellContextMenuWrapper value={props.row[props.column.key]}>
|
||||
{isBinaryColumn(column.data_type) ? (
|
||||
<BinaryFormatter {...props} />
|
||||
) : isJsonColumn(column.data_type) && !isArrayColumn(column.data_type) ? (
|
||||
<JsonFormatter {...props} />
|
||||
) : (
|
||||
<DefaultFormatter {...props} />
|
||||
)}
|
||||
</CellContextMenuWrapper>
|
||||
),
|
||||
}
|
||||
return res
|
||||
})
|
||||
@@ -106,7 +117,14 @@ export const ReferenceRecordPeek = ({ table, column, value }: ReferenceRecordPee
|
||||
<DataGrid
|
||||
className="h-32 rounded-b border-0"
|
||||
columns={columns}
|
||||
rows={data?.rows ?? []}
|
||||
rows={rows}
|
||||
onSelectedCellChange={(args: {
|
||||
column: CalculatedColumn<SupaRow, unknown>
|
||||
rowIdx: number
|
||||
row: SupaRow
|
||||
}) => {
|
||||
selectedCellRef.current = { idx: args.column.idx, rowIdx: args.rowIdx }
|
||||
}}
|
||||
onCellDoubleClick={(_, e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { GridProps, SupaRow } from '../../types'
|
||||
import { isPendingAddRow, isPendingDeleteRow } from '../../types'
|
||||
import { useOnRowsChange } from './Grid.utils'
|
||||
import { GridError } from './GridError'
|
||||
import RowRenderer from './RowRenderer'
|
||||
import { RowRenderer } from './RowRenderer'
|
||||
import { ResponseError } from '@/types'
|
||||
|
||||
const rowKeyGetter = (row: SupaRow) => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ROW_CONTEXT_MENU_ID } from 'components/grid/constants'
|
||||
import { SupaRow } from 'components/grid/types'
|
||||
import type { Key } from 'react'
|
||||
import { TriggerEvent, useContextMenu } from 'react-contexify'
|
||||
import { RenderRowProps, Row } from 'react-data-grid'
|
||||
|
||||
import { ROW_CONTEXT_MENU_ID } from 'components/grid/constants'
|
||||
import { SupaRow } from 'components/grid/types'
|
||||
|
||||
export default function RowRenderer(key: Key, props: RenderRowProps<SupaRow>) {
|
||||
/**
|
||||
* TODO: Refactor to use CellContextMenuWrapper instead of react-contexify
|
||||
*/
|
||||
export function RowRenderer(key: Key, props: RenderRowProps<SupaRow>) {
|
||||
const { show: showContextMenu } = useContextMenu()
|
||||
|
||||
function displayMenu(e: TriggerEvent) {
|
||||
|
||||
@@ -1355,10 +1355,10 @@ testRunner('table editor', () => {
|
||||
last_name text
|
||||
)`
|
||||
)
|
||||
await query(
|
||||
`INSERT INTO ${tableName} (first_name, last_name) VALUES ($1, $2)`,
|
||||
['Alice', 'Smith']
|
||||
)
|
||||
await query(`INSERT INTO ${tableName} (first_name, last_name) VALUES ($1, $2)`, [
|
||||
'Alice',
|
||||
'Smith',
|
||||
])
|
||||
},
|
||||
async () => {
|
||||
await dropTable(tableName)
|
||||
@@ -1521,4 +1521,69 @@ testRunner('table editor', () => {
|
||||
exact: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('copying cell content from referencing record peek copies the correct value', async ({
|
||||
page,
|
||||
ref,
|
||||
}) => {
|
||||
const targetTable = 'pw_fk_peek_target'
|
||||
const sourceTable = 'pw_fk_peek_source'
|
||||
|
||||
await using _ = await withSetupCleanup(
|
||||
async () => {
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS ${targetTable} (
|
||||
id bigint generated by default as identity primary key,
|
||||
name text
|
||||
)
|
||||
`)
|
||||
await query(`ALTER TABLE public.${targetTable} ENABLE ROW LEVEL SECURITY`)
|
||||
await query(`INSERT INTO ${targetTable} (name) VALUES ('target_value')`)
|
||||
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS ${sourceTable} (
|
||||
id bigint generated by default as identity primary key,
|
||||
label text,
|
||||
target_id bigint references ${targetTable}(id)
|
||||
)
|
||||
`)
|
||||
await query(`ALTER TABLE public.${sourceTable} ENABLE ROW LEVEL SECURITY`)
|
||||
await query(`INSERT INTO ${sourceTable} (label, target_id) VALUES ('source_label', 1)`)
|
||||
},
|
||||
async () => {
|
||||
await dropTable(sourceTable)
|
||||
await dropTable(targetTable)
|
||||
}
|
||||
)
|
||||
|
||||
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
||||
await page.getByRole('button', { name: `View ${sourceTable}`, exact: true }).click()
|
||||
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
||||
|
||||
await expect(page.getByRole('grid')).toBeVisible()
|
||||
await expect(page.getByRole('gridcell', { name: 'source_label' })).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'View referencing record' }).click()
|
||||
|
||||
const popover = page.getByText(`Referencing record from public.${targetTable}`)
|
||||
await expect(popover).toBeVisible()
|
||||
|
||||
const popoverContent = page.locator('[data-radix-popper-content-wrapper]')
|
||||
await expect(popoverContent.getByRole('gridcell', { name: 'target_value' })).toBeVisible()
|
||||
|
||||
// Right-click on the target_value cell inside the popover to open the peek context menu.
|
||||
// Before the fix, this would trigger the main grid's context menu via React portal
|
||||
// event bubbling, and "Copy cell" would copy the wrong value from the main grid.
|
||||
await popoverContent.getByRole('gridcell', { name: 'target_value' }).click({ button: 'right' })
|
||||
|
||||
// The peek grid's own context menu should appear with "Copy cell"
|
||||
await page.getByRole('menuitem', { name: 'Copy cell' }).click()
|
||||
|
||||
// Verify the correct referenced value was copied (not the main grid's FK value "1")
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: 'target_value',
|
||||
exact: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user