mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Refactor table editor logic for handling null and undefined values (#44331)
## Context Resolves https://github.com/supabase/supabase/issues/43548 There's currently an issue with the Table Editor where if you have, for example, a nullable `text` column with a default value, inserting a new row and selecting "Set to NULL" doesn't do anything, and saving will insert the row with the default value <img width="700" height="258" alt="image" src="https://github.com/user-attachments/assets/6a284ebb-c346-40a6-9a30-793118844084" /> This stems from a legacy logic in the Table Editor whereby we treat `null` values as "no input" - which is incorrect as `null` values are also valid values. So the PR here changes a few things to resolve this properly: ## Changes involved Main fix: - `undefined` will be the "no input" value instead, and it'll be the default value when generating the row object for inserting a new row - `NULL` or even empty string like `''` will be treated as they are (valid inputs) Secondary adjustments: - (Queue operations) Queueing an insert with no value but default value is NULL, will show the placeholder as `DEFAULT` instead of `NULL` for better accuracy in representation <img width="892" height="96" alt="image" src="https://github.com/user-attachments/assets/02cf86bf-c17b-4e25-9a8f-17960b1d2575" /> - Added a `Set to Default` CTA here, but will only show up if adding a new row or updating a queued insert row operation, which will set the value of the input field back to `undefined` for PG to handle it as the default value <img width="734" height="208" alt="image" src="https://github.com/user-attachments/assets/23887c0c-533e-4494-acbe-61309ff5d7c5" /> ## To test Verify within the Table Editor (along with queue operation feature preview) - For inserting a new row, setting value to NULL and setting value to Default works - For updating a row, setting value to NULL works
This commit is contained in:
1 parent
4295e41e81
commit
a1abc2d00f
9 files changed
+187
-84
No files matched your search
@@ -200,15 +200,12 @@ function withPendingAddPlaceholders(
|
||||
const value = props.row[props.column.key]
|
||||
|
||||
if (isPendingAddRow(props.row) && (value === undefined || value === null || value === '')) {
|
||||
if (columnDef.defaultValue !== undefined && columnDef.defaultValue !== null) {
|
||||
return <DefaultValue />
|
||||
}
|
||||
if (columnDef.isIdentity || columnDef.isGeneratable) {
|
||||
return <DefaultValue />
|
||||
}
|
||||
if (columnDef.isNullable) {
|
||||
if (value === null) {
|
||||
return <NullValue />
|
||||
}
|
||||
if (columnDef.defaultValue !== undefined || columnDef.isIdentity || columnDef.isGeneratable) {
|
||||
return <DefaultValue />
|
||||
}
|
||||
}
|
||||
|
||||
return <Formatter {...props} />
|
||||
|
||||
+29
-7
@@ -1,11 +1,11 @@
|
||||
import { includes, noop } from 'lodash'
|
||||
import { Edit, Eye } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Input,
|
||||
Select,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
SelectValue_Shadcn_,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
import { DATETIME_TYPES, JSON_TYPES, TEXT_TYPES } from '../SidePanelEditor.constants'
|
||||
import { DateTimeInput } from './DateTimeInput'
|
||||
import type { EditValue, RowField } from './RowEditor.types'
|
||||
@@ -28,6 +29,7 @@ const TRUNCATE_DESCRIPTION =
|
||||
export interface InputFieldProps {
|
||||
field: RowField
|
||||
errors: any
|
||||
isNewRow?: boolean
|
||||
isEditable?: boolean
|
||||
onUpdateField?: (changes: object) => void
|
||||
onEditJson?: (data: any) => void
|
||||
@@ -38,6 +40,7 @@ export interface InputFieldProps {
|
||||
export const InputField = ({
|
||||
field,
|
||||
errors,
|
||||
isNewRow = false,
|
||||
isEditable = true,
|
||||
onUpdateField = noop,
|
||||
onEditJson = noop,
|
||||
@@ -146,6 +149,11 @@ export const InputField = ({
|
||||
if (includes(TEXT_TYPES, field.format)) {
|
||||
const isTruncated = isValueTruncated(field.value)
|
||||
|
||||
/**
|
||||
* Handle `undefined` as the default value of the input field
|
||||
* Otherwise, NULL should be treated as NULL, empty strings should be treated as empty strings
|
||||
*/
|
||||
|
||||
return (
|
||||
<div className="text-area-text-sm">
|
||||
<Input.TextArea
|
||||
@@ -166,24 +174,38 @@ export const InputField = ({
|
||||
rows={5}
|
||||
value={field.value ?? ''}
|
||||
placeholder={
|
||||
field.value === null && field.defaultValue === null
|
||||
field.value === null
|
||||
? 'NULL'
|
||||
: field.value === '' ||
|
||||
(typeof field.defaultValue === 'string' && field.defaultValue.length === 0)
|
||||
? 'EMPTY'
|
||||
: `Default: ${field.defaultValue}`
|
||||
: `Default: ${field.defaultValue === null ? 'NULL' : field.defaultValue}`
|
||||
}
|
||||
actions={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" icon={<Edit />} className="px-1.5" />
|
||||
<Button
|
||||
data-testid={`${field.name}-field-actions`}
|
||||
type="default"
|
||||
icon={<Edit />}
|
||||
className="px-1.5"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-28">
|
||||
{isEditable && (
|
||||
<DropdownMenuItem onClick={() => onUpdateField({ [field.name]: null })}>
|
||||
Set to NULL
|
||||
</DropdownMenuItem>
|
||||
<>
|
||||
<DropdownMenuItem onClick={() => onUpdateField({ [field.name]: null })}>
|
||||
Set to NULL
|
||||
</DropdownMenuItem>
|
||||
{isNewRow && (
|
||||
<DropdownMenuItem onClick={() => onUpdateField({ [field.name]: undefined })}>
|
||||
Set to Default
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => onEditText({ column: field.name, value: field.value || '' })}
|
||||
>
|
||||
|
||||
+3
-1
@@ -133,7 +133,7 @@ export const RowEditor = ({
|
||||
updateEditorDirty()
|
||||
|
||||
const payload = isNewRecord
|
||||
? generateRowObjectFromFields(rowFields)
|
||||
? generateRowObjectFromFields({ fields: rowFields })
|
||||
: generateUpdateRowPayload(row, rowFields)
|
||||
|
||||
const configuration = { identifiers: {}, rowIdx: -1 }
|
||||
@@ -226,6 +226,7 @@ export const RowEditor = ({
|
||||
onEditText={setSelectedValueForTextEdit}
|
||||
onSelectForeignKey={() => onOpenForeignRowSelector(field)}
|
||||
isEditable={editable}
|
||||
isNewRow={isNewRecord || '__tempId' in row}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -254,6 +255,7 @@ export const RowEditor = ({
|
||||
onEditJson={setSelectedValueForJsonEdit}
|
||||
onSelectForeignKey={() => onOpenForeignRowSelector(field)}
|
||||
isEditable={editable}
|
||||
isNewRow={isNewRecord || '__tempId' in row}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export interface RowField {
|
||||
comment: string
|
||||
format: string
|
||||
enums: string[]
|
||||
value: string | null
|
||||
value: string | null | undefined
|
||||
defaultValue: string | null
|
||||
foreignKey?: PostgresRelationship
|
||||
isNullable: boolean
|
||||
|
||||
+65
-37
@@ -108,7 +108,7 @@ describe('generateRowObjectFromFields', () => {
|
||||
{
|
||||
id: '1',
|
||||
name: 'id',
|
||||
value: '',
|
||||
value: undefined,
|
||||
comment: '',
|
||||
defaultValue: null,
|
||||
format: 'int8',
|
||||
@@ -120,7 +120,7 @@ describe('generateRowObjectFromFields', () => {
|
||||
{
|
||||
id: '2',
|
||||
name: 'time_not_null',
|
||||
value: '',
|
||||
value: undefined,
|
||||
comment: '',
|
||||
defaultValue: 'now()',
|
||||
format: 'timestamptz',
|
||||
@@ -132,7 +132,7 @@ describe('generateRowObjectFromFields', () => {
|
||||
{
|
||||
id: '3',
|
||||
name: 'time_nullable',
|
||||
value: '',
|
||||
value: undefined,
|
||||
comment: '',
|
||||
defaultValue: 'now()',
|
||||
format: 'timestamptz',
|
||||
@@ -142,7 +142,7 @@ describe('generateRowObjectFromFields', () => {
|
||||
isPrimaryKey: false,
|
||||
},
|
||||
]
|
||||
const result = generateRowObjectFromFields(sampleRowFields)
|
||||
const result = generateRowObjectFromFields({ fields: sampleRowFields })
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
it('should discern EMPTY values for text', () => {
|
||||
@@ -150,7 +150,7 @@ describe('generateRowObjectFromFields', () => {
|
||||
{
|
||||
id: '1',
|
||||
name: 'id',
|
||||
value: '',
|
||||
value: undefined,
|
||||
comment: '',
|
||||
defaultValue: null,
|
||||
format: 'int8',
|
||||
@@ -172,7 +172,7 @@ describe('generateRowObjectFromFields', () => {
|
||||
isPrimaryKey: false,
|
||||
},
|
||||
]
|
||||
const result = generateRowObjectFromFields(sampleRowFields)
|
||||
const result = generateRowObjectFromFields({ fields: sampleRowFields })
|
||||
expect(result).toEqual({ name: '' })
|
||||
})
|
||||
it('should discern NULL values for text', () => {
|
||||
@@ -180,7 +180,7 @@ describe('generateRowObjectFromFields', () => {
|
||||
{
|
||||
id: '1',
|
||||
name: 'id',
|
||||
value: '',
|
||||
value: undefined,
|
||||
comment: '',
|
||||
defaultValue: null,
|
||||
format: 'int8',
|
||||
@@ -202,15 +202,15 @@ describe('generateRowObjectFromFields', () => {
|
||||
isPrimaryKey: false,
|
||||
},
|
||||
]
|
||||
const result = generateRowObjectFromFields(sampleRowFields)
|
||||
expect(result).toEqual({})
|
||||
const result = generateRowObjectFromFields({ fields: sampleRowFields })
|
||||
expect(result).toEqual({ name: null })
|
||||
})
|
||||
it('should discern NULL values for booleans', () => {
|
||||
const sampleRowFields: RowField[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'id',
|
||||
value: '',
|
||||
value: undefined,
|
||||
comment: '',
|
||||
defaultValue: null,
|
||||
format: 'int8',
|
||||
@@ -232,8 +232,8 @@ describe('generateRowObjectFromFields', () => {
|
||||
isPrimaryKey: false,
|
||||
},
|
||||
]
|
||||
const result = generateRowObjectFromFields(sampleRowFields)
|
||||
expect(result).toEqual({})
|
||||
const result = generateRowObjectFromFields({ fields: sampleRowFields })
|
||||
expect(result).toEqual({ 'bool-test': null })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -527,7 +527,7 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: '["tag1", "tag2"]',
|
||||
}),
|
||||
]
|
||||
expect(generateRowObjectFromFields(fields)).toEqual({ tags: ['tag1', 'tag2'] })
|
||||
expect(generateRowObjectFromFields({ fields })).toEqual({ tags: ['tag1', 'tag2'] })
|
||||
})
|
||||
|
||||
it('should handle null array fields', () => {
|
||||
@@ -538,7 +538,18 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: null,
|
||||
}),
|
||||
]
|
||||
expect(generateRowObjectFromFields(fields)).toEqual({})
|
||||
expect(generateRowObjectFromFields({ fields })).toEqual({ tags: null })
|
||||
})
|
||||
|
||||
it('should handle undefined array fields', () => {
|
||||
const fields: RowField[] = [
|
||||
createField({
|
||||
name: 'tags',
|
||||
format: '_text',
|
||||
value: undefined,
|
||||
}),
|
||||
]
|
||||
expect(generateRowObjectFromFields({ fields })).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle JSON fields', () => {
|
||||
@@ -549,7 +560,7 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: '{"key": "value"}',
|
||||
}),
|
||||
]
|
||||
expect(generateRowObjectFromFields(fields)).toEqual({ metadata: { key: 'value' } })
|
||||
expect(generateRowObjectFromFields({ fields })).toEqual({ metadata: { key: 'value' } })
|
||||
})
|
||||
|
||||
it('should handle JSON fields with object values', () => {
|
||||
@@ -560,7 +571,7 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: { key: 'value' } as any,
|
||||
}),
|
||||
]
|
||||
expect(generateRowObjectFromFields(fields)).toEqual({ metadata: { key: 'value' } })
|
||||
expect(generateRowObjectFromFields({ fields })).toEqual({ metadata: { key: 'value' } })
|
||||
})
|
||||
|
||||
it('should handle boolean true/false/null', () => {
|
||||
@@ -581,9 +592,9 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: 'null',
|
||||
}),
|
||||
]
|
||||
// By default, null values are omitted unless includeNullProperties is true
|
||||
const result = generateRowObjectFromFields(fields)
|
||||
expect(result).toEqual({ active: true, deleted: false })
|
||||
// Null values are kept (explicit NULL), only undefined values are omitted
|
||||
const result = generateRowObjectFromFields({ fields })
|
||||
expect(result).toEqual({ active: true, deleted: false, optional: null })
|
||||
})
|
||||
|
||||
it('should handle boolean true/false/null with includeNullProperties', () => {
|
||||
@@ -604,19 +615,19 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: 'null',
|
||||
}),
|
||||
]
|
||||
const result = generateRowObjectFromFields(fields, true)
|
||||
const result = generateRowObjectFromFields({ fields, includeUndefinedValues: true })
|
||||
expect(result).toEqual({ active: true, deleted: false, optional: null })
|
||||
})
|
||||
|
||||
it('should handle boolean with empty value', () => {
|
||||
it('should handle boolean with undefined value', () => {
|
||||
const fields: RowField[] = [
|
||||
createField({
|
||||
name: 'active',
|
||||
format: 'bool',
|
||||
value: '',
|
||||
value: undefined,
|
||||
}),
|
||||
]
|
||||
const result = generateRowObjectFromFields(fields)
|
||||
const result = generateRowObjectFromFields({ fields })
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
@@ -628,7 +639,7 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: '2023-12-01T10:30:45',
|
||||
}),
|
||||
]
|
||||
const result: any = generateRowObjectFromFields(fields)
|
||||
const result: any = generateRowObjectFromFields({ fields })
|
||||
expect(result.created_at).toBeDefined()
|
||||
expect(typeof result.created_at).toBe('string')
|
||||
})
|
||||
@@ -641,17 +652,17 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: '2023-12-01T10:30',
|
||||
}),
|
||||
]
|
||||
const result: any = generateRowObjectFromFields(fields)
|
||||
const result: any = generateRowObjectFromFields({ fields })
|
||||
expect(result.created_at).toBeDefined()
|
||||
expect(typeof result.created_at).toBe('string')
|
||||
})
|
||||
|
||||
it('should include null properties when includeNullProperties is true', () => {
|
||||
it('should include undefined properties when includeUndefinedValues is true', () => {
|
||||
const fields: RowField[] = [
|
||||
createField({
|
||||
name: 'optional_field',
|
||||
format: 'text',
|
||||
value: null,
|
||||
value: undefined,
|
||||
}),
|
||||
createField({
|
||||
name: 'active_field',
|
||||
@@ -659,15 +670,32 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: 'value',
|
||||
}),
|
||||
]
|
||||
const result = generateRowObjectFromFields(fields, true)
|
||||
expect(result).toEqual({ optional_field: null, active_field: 'value' })
|
||||
const result = generateRowObjectFromFields({ fields, includeUndefinedValues: true })
|
||||
expect(result).toEqual({ optional_field: undefined, active_field: 'value' })
|
||||
})
|
||||
|
||||
it('should omit null properties when includeNullProperties is false', () => {
|
||||
it('should omit undefined properties by default', () => {
|
||||
const fields: RowField[] = [
|
||||
createField({
|
||||
name: 'optional_field',
|
||||
format: 'text',
|
||||
value: undefined,
|
||||
}),
|
||||
createField({
|
||||
name: 'active_field',
|
||||
format: 'text',
|
||||
value: 'value',
|
||||
}),
|
||||
]
|
||||
const result = generateRowObjectFromFields({ fields })
|
||||
expect(result).toEqual({ active_field: 'value' })
|
||||
})
|
||||
|
||||
it('should keep null properties (explicit NULL) even when includeUndefinedValues is false', () => {
|
||||
const fields: RowField[] = [
|
||||
createField({
|
||||
name: 'nullable_field',
|
||||
format: 'text',
|
||||
value: null,
|
||||
}),
|
||||
createField({
|
||||
@@ -676,8 +704,8 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: 'value',
|
||||
}),
|
||||
]
|
||||
const result = generateRowObjectFromFields(fields, false)
|
||||
expect(result).toEqual({ active_field: 'value' })
|
||||
const result = generateRowObjectFromFields({ fields })
|
||||
expect(result).toEqual({ nullable_field: null, active_field: 'value' })
|
||||
})
|
||||
|
||||
it('should preserve empty strings for text types', () => {
|
||||
@@ -693,24 +721,24 @@ describe('generateRowObjectFromFields - additional cases', () => {
|
||||
value: '',
|
||||
}),
|
||||
]
|
||||
const result = generateRowObjectFromFields(fields)
|
||||
const result = generateRowObjectFromFields({ fields })
|
||||
expect(result).toEqual({ description: '', title: '' })
|
||||
})
|
||||
|
||||
it('should convert empty values to null for non-text types', () => {
|
||||
it('should omit undefined values for non-text types', () => {
|
||||
const fields: RowField[] = [
|
||||
createField({
|
||||
name: 'count',
|
||||
format: 'int4',
|
||||
value: '',
|
||||
value: undefined,
|
||||
}),
|
||||
createField({
|
||||
name: 'price',
|
||||
format: 'numeric',
|
||||
value: '',
|
||||
value: undefined,
|
||||
}),
|
||||
]
|
||||
const result = generateRowObjectFromFields(fields)
|
||||
const result = generateRowObjectFromFields({ fields })
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
})
|
||||
+25
-27
@@ -2,7 +2,7 @@ import { MAX_ARRAY_SIZE, MAX_CHARACTERS } from '@supabase/pg-meta/src/query/tabl
|
||||
import type { PostgresColumn, PostgresRelationship, PostgresTable } from '@supabase/postgres-meta'
|
||||
import dayjs from 'dayjs'
|
||||
import { minifyJSON, tryParseJson } from 'lib/helpers'
|
||||
import { compact, isEqual, isNull, isString, omitBy } from 'lodash'
|
||||
import { compact, isEqual, isNull, isString, isUndefined, omitBy } from 'lodash'
|
||||
import type { Dictionary } from 'types'
|
||||
|
||||
import { ForeignKey } from '../ForeignKeySelector/ForeignKeySelector.types'
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
DATETIME_TYPES,
|
||||
JSON_TYPES,
|
||||
TEXT_TYPES,
|
||||
TIMESTAMP_TYPES,
|
||||
TIME_TYPES,
|
||||
TIMESTAMP_TYPES,
|
||||
} from '../SidePanelEditor.constants'
|
||||
import type { RowField } from './RowEditor.types'
|
||||
|
||||
@@ -20,7 +20,7 @@ const getRowValue = ({ column, row }: { column: PostgresColumn; row?: Dictionary
|
||||
|
||||
if (isNewRow) {
|
||||
if (TEXT_TYPES.includes(column.format)) {
|
||||
return null
|
||||
return undefined
|
||||
} else if (column.format === 'bool') {
|
||||
if (column.default_value) {
|
||||
return column.default_value
|
||||
@@ -28,7 +28,7 @@ const getRowValue = ({ column, row }: { column: PostgresColumn; row?: Dictionary
|
||||
return 'null'
|
||||
} else return null
|
||||
} else {
|
||||
return ''
|
||||
return undefined
|
||||
}
|
||||
} else {
|
||||
if (column.format === 'bool' && row[column.name] === null) {
|
||||
@@ -165,8 +165,11 @@ const convertPostgresDatetimeToInputDatetime = (format: string, value: string) =
|
||||
}
|
||||
}
|
||||
|
||||
const convertInputDatetimeToPostgresDatetime = (format: string, value: string | null) => {
|
||||
if (!value || value.length == 0) return null
|
||||
const convertInputDatetimeToPostgresDatetime = (
|
||||
format: string,
|
||||
value: string | null | undefined
|
||||
) => {
|
||||
if (!value || value.length === 0) return value
|
||||
|
||||
switch (format) {
|
||||
case 'timestamptz':
|
||||
@@ -182,26 +185,20 @@ const convertInputDatetimeToPostgresDatetime = (format: string, value: string |
|
||||
}
|
||||
}
|
||||
|
||||
// [Joshen] JFYI this presents a small problem in particular when creating a new row
|
||||
// given that we don't include null properties. Because of that if the column has a default
|
||||
// value, the column value will then always be the default value, instead of null
|
||||
// which may be considered a bug if e.g for a boolean column the user specifically selects "NULL" option
|
||||
// This would probably also apply to other column types like numbers (e.g user specifically wants a null value)
|
||||
export const generateRowObjectFromFields = (
|
||||
fields: RowField[],
|
||||
includeNullProperties = false
|
||||
): object => {
|
||||
/**
|
||||
* Treat `undefined` as "no input" - all else values as valid inputs (e.g NULL or empty string)
|
||||
*/
|
||||
export const generateRowObjectFromFields = ({
|
||||
fields,
|
||||
includeUndefinedValues = false,
|
||||
}: {
|
||||
fields: RowField[]
|
||||
includeUndefinedValues?: boolean
|
||||
}): object => {
|
||||
const rowObject = {} as any
|
||||
fields.forEach((field) => {
|
||||
const isArray = field.format.startsWith('_')
|
||||
|
||||
// Do not convert empty field inputs to NULL for text types
|
||||
// so that we discern NULL and EMPTY
|
||||
const value = TEXT_TYPES.includes(field.format)
|
||||
? field.value
|
||||
: (field?.value ?? '').length === 0
|
||||
? null
|
||||
: field.value
|
||||
const value = field.value
|
||||
|
||||
if (isArray && value !== null) {
|
||||
rowObject[field.name] = tryParseJson(value)
|
||||
@@ -224,12 +221,13 @@ export const generateRowObjectFromFields = (
|
||||
rowObject[field.name] = value
|
||||
}
|
||||
})
|
||||
return includeNullProperties ? rowObject : omitBy(rowObject, isNull)
|
||||
|
||||
const results = includeUndefinedValues ? rowObject : omitBy(rowObject, isUndefined)
|
||||
return results
|
||||
}
|
||||
|
||||
export const generateUpdateRowPayload = (originalRow: any, fields: RowField[]) => {
|
||||
const includeNullProperties = true
|
||||
const rowObject = generateRowObjectFromFields(fields, includeNullProperties) as any
|
||||
const rowObject = generateRowObjectFromFields({ fields, includeUndefinedValues: true }) as any
|
||||
|
||||
const payload = {} as any
|
||||
const properties = Object.keys(rowObject)
|
||||
@@ -251,7 +249,7 @@ export const generateUpdateRowPayload = (originalRow: any, fields: RowField[]) =
|
||||
payload[property] = rowObject[property]
|
||||
}
|
||||
} else {
|
||||
const originalValue = originalRow[property] === undefined ? null : originalRow[property]
|
||||
const originalValue = originalRow[property]
|
||||
const newValue = rowObject[property]
|
||||
if (!isEqual(originalValue, newValue)) {
|
||||
payload[property] = newValue
|
||||
|
||||
+1
-1
@@ -3,8 +3,8 @@ import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useParams } from 'common'
|
||||
import { useTableRowOperations } from 'components/grid/hooks/useTableRowOperations'
|
||||
import { type GeneratedPolicy } from 'components/interfaces/Auth/Policies/Policies.utils'
|
||||
import { useIsQueueOperationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { type GeneratedPolicy } from 'components/interfaces/Auth/Policies/Policies.utils'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { databasePoliciesKeys } from 'data/database-policies/keys'
|
||||
import { useDatabasePublicationCreateMutation } from 'data/database-publications/database-publications-create-mutation'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Query } from '@supabase/pg-meta/src/query'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { RoleImpersonationState, wrapWithRoleImpersonation } from 'lib/role-impersonation'
|
||||
import { toast } from 'sonner'
|
||||
import { isRoleImpersonationEnabled } from 'state/role-impersonation-state'
|
||||
import type { ResponseError, UseCustomMutationOptions } from 'types'
|
||||
|
||||
import { tableRowKeys } from './keys'
|
||||
|
||||
export type TableRowCreateVariables = {
|
||||
|
||||
@@ -1522,6 +1522,62 @@ testRunner('table editor', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('inserting a row with NULL value for nullable text column with default', async ({
|
||||
page,
|
||||
ref,
|
||||
}) => {
|
||||
const tableName = 'pw_table_null_text_insert'
|
||||
const columnName = 'description'
|
||||
|
||||
await using _ = await withSetupCleanup(
|
||||
async () => {
|
||||
await query(
|
||||
`CREATE TABLE IF NOT EXISTS ${tableName} (
|
||||
id bigint generated by default as identity primary key,
|
||||
created_at timestamp with time zone null default now(),
|
||||
${columnName} text null default 'hello world'
|
||||
)`
|
||||
)
|
||||
},
|
||||
async () => {
|
||||
await dropTable(tableName)
|
||||
}
|
||||
)
|
||||
|
||||
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
||||
await waitForTableToLoad(page, ref)
|
||||
|
||||
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
||||
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
||||
|
||||
// Open side panel to insert a new row
|
||||
await page.getByTestId('table-editor-insert-new-row').click()
|
||||
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
|
||||
|
||||
// Open the field actions dropdown and set the text column to NULL
|
||||
await page.getByTestId(`${columnName}-field-actions`).click()
|
||||
await page.getByRole('menuitem', { name: 'Set to NULL' }).click()
|
||||
|
||||
// Save the row
|
||||
const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
|
||||
method: 'POST',
|
||||
})
|
||||
await page.getByTestId('action-bar-save-row').click()
|
||||
await insertPromise
|
||||
|
||||
// Should show success toast
|
||||
await expect(
|
||||
page.getByText('Successfully created row'),
|
||||
'Success toast should appear after row creation'
|
||||
).toBeVisible({ timeout: 10000 })
|
||||
|
||||
// The new row's text column should show NULL in the grid
|
||||
await expect(
|
||||
page.getByRole('gridcell', { name: 'NULL' }).first(),
|
||||
'Text column should display NULL after setting to NULL via row editor'
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('copying cell content from referencing record peek copies the correct value', async ({
|
||||
page,
|
||||
ref,
|
||||
|
||||
Reference in new issue
Block a user