mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Merge pull request #9223 from supabase/chore/improve-grid-cell-editors
Chore/improve grid cell editors
This commit is contained in:
17 files changed
+250
-121
No files matched your search
@@ -1,9 +1,9 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
import { EditorProps } from '@supabase/react-data-grid'
|
||||
import { useTrackedState } from '../../store'
|
||||
import { BlockKeys, MonacoEditor, NullValue } from '../common'
|
||||
import { Popover } from 'ui'
|
||||
import { useState, useCallback } from 'react'
|
||||
import { EditorProps } from '@supabase/react-data-grid'
|
||||
|
||||
import { useTrackedState } from 'components/grid/store'
|
||||
import { BlockKeys, MonacoEditor, NullValue } from 'components/grid/components/common'
|
||||
|
||||
export function JsonEditor<TRow, TSummaryRow = unknown>({
|
||||
row,
|
||||
@@ -11,12 +11,13 @@ export function JsonEditor<TRow, TSummaryRow = unknown>({
|
||||
onRowChange,
|
||||
}: EditorProps<TRow, TSummaryRow>) {
|
||||
const state = useTrackedState()
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(true)
|
||||
|
||||
const gridColumn = state.gridColumns.find((x) => x.name == column.key)
|
||||
const initialValue = row[column.key as keyof TRow] as unknown
|
||||
const jsonString = initialValue ? JSON.stringify(initialValue) : ''
|
||||
const prettyJsonValue = prettifyJSON(jsonString)
|
||||
const [value, setValue] = useState<string | null>(prettyJsonValue)
|
||||
const jsonString = prettifyJSON(initialValue ? JSON.stringify(initialValue) : '')
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(true)
|
||||
const [value, setValue] = useState<string | null>(jsonString)
|
||||
|
||||
const cancelChanges = useCallback(() => {
|
||||
onRowChange(row, true)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import { Listbox } from '@supabase/ui'
|
||||
import { EditorProps } from '@supabase/react-data-grid'
|
||||
|
||||
import { useTrackedState } from 'components/grid/store'
|
||||
|
||||
interface SelectEditorProps<TRow, TSummaryRow = unknown> extends EditorProps<TRow, TSummaryRow> {
|
||||
options: { label: string; value: string }[]
|
||||
}
|
||||
@@ -12,10 +14,12 @@ export function SelectEditor<TRow, TSummaryRow = unknown>({
|
||||
onClose,
|
||||
options,
|
||||
}: SelectEditorProps<TRow, TSummaryRow>) {
|
||||
const state = useTrackedState()
|
||||
const gridColumn = state.gridColumns.find((x) => x.name == column.key)
|
||||
|
||||
const value = row[column.key as keyof TRow] as unknown as string
|
||||
|
||||
function onChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const value = event.target.value
|
||||
function onChange(value: string) {
|
||||
if (!value || value == '') {
|
||||
onRowChange({ ...row, [column.key]: null }, true)
|
||||
} else {
|
||||
@@ -28,19 +32,25 @@ export function SelectEditor<TRow, TSummaryRow = unknown>({
|
||||
}
|
||||
|
||||
return (
|
||||
<select
|
||||
className="sb-grid-select-editor bg-scale-400 text-grid p-0 px-3"
|
||||
value={value ?? ''}
|
||||
<Listbox
|
||||
autoFocus
|
||||
id="select-editor"
|
||||
name="select-editor"
|
||||
size="small"
|
||||
defaultValue={value ?? ''}
|
||||
className="sb-grid-select-editor !gap-2"
|
||||
style={{ width: `${gridColumn?.width || column.width}px` }}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
autoFocus
|
||||
>
|
||||
<option value={''}>[null]</option>
|
||||
<Listbox.Option id="NULL" label="NULL" value="">
|
||||
NULL
|
||||
</Listbox.Option>
|
||||
{options.map(({ label, value }) => (
|
||||
<option key={value} value={value}>
|
||||
<Listbox.Option key={value} label={label} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</select>
|
||||
</Listbox>
|
||||
)
|
||||
}
|
||||
@@ -39,6 +39,7 @@ export const Grid = memo(
|
||||
const changedColumn = Object.keys(rowData).find(
|
||||
(name) => rowData[name] !== originRowData![name]
|
||||
)
|
||||
|
||||
if (changedColumn) {
|
||||
const { error } = state.rowService!.update(rowData, changedColumn, (payload) => {
|
||||
dispatch({
|
||||
|
||||
@@ -35,18 +35,22 @@ export function deleteQuery(
|
||||
filters?: Filter[],
|
||||
options?: {
|
||||
returning?: boolean
|
||||
enumArrayColumns?: string[]
|
||||
}
|
||||
) {
|
||||
if (!filters || filters.length === 0) {
|
||||
throw { message: 'no filters for this delete query' }
|
||||
}
|
||||
let query = `delete from ${queryTable(table)}`
|
||||
const { returning } = options ?? {}
|
||||
const { returning, enumArrayColumns } = options ?? {}
|
||||
if (filters) {
|
||||
query = applyFilters(query, filters)
|
||||
}
|
||||
if (returning) {
|
||||
query += ' returning *'
|
||||
query +=
|
||||
enumArrayColumns === undefined || enumArrayColumns.length === 0
|
||||
? ` returning *`
|
||||
: ` returning *, ${enumArrayColumns.map((x) => `"${x}"::text[]`).join(',')}`
|
||||
}
|
||||
return query + ';'
|
||||
}
|
||||
@@ -56,12 +60,13 @@ export function insertQuery(
|
||||
values: Dictionary<any>[],
|
||||
options?: {
|
||||
returning?: boolean
|
||||
enumArrayColumns?: string[]
|
||||
}
|
||||
) {
|
||||
if (!values || values.length === 0) {
|
||||
throw { message: 'no value to insert' }
|
||||
}
|
||||
const { returning } = options ?? {}
|
||||
const { returning, enumArrayColumns } = options ?? {}
|
||||
const queryColumns = Object.keys(values[0])
|
||||
.map((x) => ident(x))
|
||||
.join(',')
|
||||
@@ -81,14 +86,17 @@ export function insertQuery(
|
||||
)
|
||||
}
|
||||
if (returning) {
|
||||
query += ' returning *'
|
||||
query +=
|
||||
enumArrayColumns === undefined || enumArrayColumns.length === 0
|
||||
? ` returning *`
|
||||
: ` returning *, ${enumArrayColumns.map((x) => `"${x}"::text[]`).join(',')}`
|
||||
}
|
||||
return query + ';'
|
||||
}
|
||||
|
||||
export function selectQuery(
|
||||
table: QueryTable,
|
||||
columns?: string[],
|
||||
columns?: string,
|
||||
options?: {
|
||||
filters?: Filter[]
|
||||
pagination?: QueryPagination
|
||||
@@ -96,7 +104,7 @@ export function selectQuery(
|
||||
}
|
||||
) {
|
||||
let query = ''
|
||||
const queryColumn = columns?.map((x) => ident(x)).join(', ') ?? '*'
|
||||
const queryColumn = columns ?? '*'
|
||||
query += `select ${queryColumn} from ${queryTable(table)}`
|
||||
|
||||
const { filters, pagination, sorts } = options ?? {}
|
||||
@@ -119,9 +127,10 @@ export function updateQuery(
|
||||
options?: {
|
||||
filters?: Filter[]
|
||||
returning?: boolean
|
||||
enumArrayColumns?: string[]
|
||||
}
|
||||
) {
|
||||
const { filters, returning } = options ?? {}
|
||||
const { filters, returning, enumArrayColumns } = options ?? {}
|
||||
if (!filters || filters.length === 0) {
|
||||
throw { message: 'no filters for this update query' }
|
||||
}
|
||||
@@ -138,8 +147,12 @@ export function updateQuery(
|
||||
query = applyFilters(query, filters)
|
||||
}
|
||||
if (returning) {
|
||||
query += ' returning *'
|
||||
query +=
|
||||
enumArrayColumns === undefined || enumArrayColumns.length === 0
|
||||
? ` returning *`
|
||||
: ` returning *, ${enumArrayColumns.map((x) => `"${x}"::text[]`).join(',')}`
|
||||
}
|
||||
|
||||
return query + ';'
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface IQueryAction {
|
||||
count: () => IQueryFilter
|
||||
delete: (options?: { returning: boolean }) => IQueryFilter
|
||||
insert: (values: Dictionary<any>[], options?: { returning: boolean }) => IQueryFilter
|
||||
select: (columns?: string[]) => IQueryFilter
|
||||
select: (columns?: string) => IQueryFilter
|
||||
update: (value: Dictionary<any>, options?: { returning: boolean }) => IQueryFilter
|
||||
truncate: (options?: { returning: boolean }) => IQueryFilter
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export class QueryAction implements IQueryAction {
|
||||
*
|
||||
* @param options.returning If `true`, return the deleted row(s) in the response.
|
||||
*/
|
||||
delete(options?: { returning: boolean }) {
|
||||
delete(options?: { returning: boolean; enumArrayColumns?: string[] }) {
|
||||
return new QueryFilter(this.table, 'delete', undefined, options)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export class QueryAction implements IQueryAction {
|
||||
* @param values The values to insert.
|
||||
* @param options.returning If `true`, return the inserted row(s) in the response.
|
||||
*/
|
||||
insert(values: Dictionary<any>[], options?: { returning: boolean }) {
|
||||
insert(values: Dictionary<any>[], options?: { returning: boolean; enumArrayColumns?: string[] }) {
|
||||
return new QueryFilter(this.table, 'insert', values, options)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class QueryAction implements IQueryAction {
|
||||
*
|
||||
* @param columns the query columns, by default set to '*'.
|
||||
*/
|
||||
select(columns?: string[]) {
|
||||
select(columns?: string) {
|
||||
return new QueryFilter(this.table, 'select', columns)
|
||||
}
|
||||
|
||||
@@ -54,14 +54,14 @@ export class QueryAction implements IQueryAction {
|
||||
* @param value The value to update.
|
||||
* @param options.returning If `true`, return the updated row(s) in the response.
|
||||
*/
|
||||
update(value: Dictionary<any>, options?: { returning: boolean }) {
|
||||
update(value: Dictionary<any>, options?: { returning: boolean; enumArrayColumns?: string[] }) {
|
||||
return new QueryFilter(this.table, 'update', value, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a TRUNCATE on the table
|
||||
*/
|
||||
truncate(options?: { returning: boolean }) {
|
||||
truncate(options?: { returning: boolean; enumArrayColumns?: string[] }) {
|
||||
return new QueryFilter(this.table, 'truncate', undefined, options)
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ export class QueryFilter implements IQueryFilter, IQueryModifier {
|
||||
constructor(
|
||||
protected table: QueryTable,
|
||||
protected action: 'count' | 'delete' | 'insert' | 'select' | 'update' | 'truncate',
|
||||
protected actionValue?: string[] | Dictionary<any> | Dictionary<any>[],
|
||||
protected actionOptions?: { returning: boolean }
|
||||
protected actionValue?: string | string[] | Dictionary<any> | Dictionary<any>[],
|
||||
protected actionOptions?: { returning: boolean; enumArrayColumns?: string[] }
|
||||
) {}
|
||||
|
||||
filter(column: string, operator: FilterOperator, value: any) {
|
||||
|
||||
@@ -20,8 +20,8 @@ export class QueryModifier implements IQueryModifier {
|
||||
protected table: QueryTable,
|
||||
protected action: 'count' | 'delete' | 'insert' | 'select' | 'update' | 'truncate',
|
||||
protected options?: {
|
||||
actionValue?: string[] | Dictionary<any> | Dictionary<any>[]
|
||||
actionOptions?: { returning?: boolean; cascade?: boolean }
|
||||
actionValue?: string | string[] | Dictionary<any> | Dictionary<any>[]
|
||||
actionOptions?: { returning?: boolean; cascade?: boolean; enumArrayColumns?: string[] }
|
||||
filters?: Filter[]
|
||||
sorts?: Sort[]
|
||||
}
|
||||
@@ -51,15 +51,17 @@ export class QueryModifier implements IQueryModifier {
|
||||
case 'delete': {
|
||||
return deleteQuery(this.table, filters, {
|
||||
returning: actionOptions?.returning,
|
||||
enumArrayColumns: actionOptions?.enumArrayColumns,
|
||||
})
|
||||
}
|
||||
case 'insert': {
|
||||
return insertQuery(this.table, actionValue as Dictionary<any>[], {
|
||||
returning: actionOptions?.returning,
|
||||
enumArrayColumns: actionOptions?.enumArrayColumns,
|
||||
})
|
||||
}
|
||||
case 'select': {
|
||||
return selectQuery(this.table, actionValue as string[] | undefined, {
|
||||
return selectQuery(this.table, actionValue as string | undefined, {
|
||||
filters,
|
||||
pagination: this.pagination,
|
||||
sorts,
|
||||
@@ -69,6 +71,7 @@ export class QueryModifier implements IQueryModifier {
|
||||
return updateQuery(this.table, actionValue as Dictionary<any>, {
|
||||
filters,
|
||||
returning: actionOptions?.returning,
|
||||
enumArrayColumns: actionOptions?.enumArrayColumns,
|
||||
})
|
||||
}
|
||||
case 'truncate': {
|
||||
|
||||
@@ -89,7 +89,20 @@ export class SqlRowService implements IRowService {
|
||||
const pageFromZero = page > 0 ? page - 1 : page
|
||||
const from = pageFromZero * rowsPerPage
|
||||
const to = (pageFromZero + 1) * rowsPerPage - 1
|
||||
let queryChains = this.query.from(this.table.name, this.table.schema ?? undefined).select()
|
||||
|
||||
const enumArrayColumns = this.table.columns
|
||||
.filter((column) => {
|
||||
return (column?.enum ?? []).length > 0 && column.dataType.toLowerCase() === 'array'
|
||||
})
|
||||
.map((column) => column.name)
|
||||
|
||||
let queryChains =
|
||||
enumArrayColumns.length > 0
|
||||
? this.query
|
||||
.from(this.table.name, this.table.schema ?? undefined)
|
||||
.select(`*,${enumArrayColumns.map((x) => `"${x}"::text[]`).join(',')}`)
|
||||
: this.query.from(this.table.name, this.table.schema ?? undefined).select()
|
||||
|
||||
filters
|
||||
.filter((x) => x.value && x.value != '')
|
||||
.forEach((x) => {
|
||||
@@ -174,6 +187,11 @@ export class SqlRowService implements IRowService {
|
||||
// remove primary key from updated value object
|
||||
delete value[key]
|
||||
})
|
||||
const enumArrayColumns = this.table.columns
|
||||
.filter((column) => {
|
||||
return (column?.enum ?? []).length > 0 && column.dataType.toLowerCase() === 'array'
|
||||
})
|
||||
.map((column) => column.name)
|
||||
const query = this.query
|
||||
.from(this.table.name, this.table.schema ?? undefined)
|
||||
.update(
|
||||
@@ -182,7 +200,7 @@ export class SqlRowService implements IRowService {
|
||||
[changedColumn]: value[changedColumn],
|
||||
}
|
||||
: value,
|
||||
{ returning: true }
|
||||
{ returning: true, enumArrayColumns }
|
||||
)
|
||||
.match(matchValues)
|
||||
.toSql()
|
||||
|
||||
@@ -25,10 +25,14 @@ import {
|
||||
TextEditor,
|
||||
TimeEditor,
|
||||
TimeWithTimezoneEditor,
|
||||
} from '../components/editor'
|
||||
import { AddColumn, ColumnHeader, SelectColumn } from '../components/grid'
|
||||
import { COLUMN_MIN_WIDTH } from '../constants'
|
||||
import { BooleanFormatter, DefaultFormatter, ForeignKeyFormatter } from '../components/formatter'
|
||||
} from 'components/grid/components/editor'
|
||||
import { AddColumn, ColumnHeader, SelectColumn } from 'components/grid/components/grid'
|
||||
import { COLUMN_MIN_WIDTH } from 'components/grid/constants'
|
||||
import {
|
||||
BooleanFormatter,
|
||||
DefaultFormatter,
|
||||
ForeignKeyFormatter,
|
||||
} from 'components/grid/components/formatter'
|
||||
|
||||
const ESTIMATED_CHARACTER_PIXEL_WIDTH = 9
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const LogTable = ({
|
||||
const hasId = columnNames.includes('id')
|
||||
const hasTimestamp = columnNames.includes('timestamp')
|
||||
|
||||
const DEFAULT_COLUMNS = columnNames.map((v) => {
|
||||
const DEFAULT_COLUMNS = columnNames.map((v, idx: number) => {
|
||||
let formatter = undefined
|
||||
|
||||
formatter = (received: FormatterArg) => {
|
||||
@@ -83,7 +83,14 @@ const LogTable = ({
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
return { key: v, name: v, resizable: true, formatter, header: v, minWidth: 128 }
|
||||
return {
|
||||
key: `logs-column-${idx}`,
|
||||
name: v,
|
||||
resizable: true,
|
||||
formatter,
|
||||
header: v,
|
||||
minWidth: 128,
|
||||
}
|
||||
})
|
||||
|
||||
let columns
|
||||
|
||||
+47
-20
@@ -24,26 +24,53 @@ const InputField: FC<Props> = ({
|
||||
onViewForeignKey = () => {},
|
||||
}) => {
|
||||
if (field.enums.length > 0) {
|
||||
return (
|
||||
<Select
|
||||
size="medium"
|
||||
layout="horizontal"
|
||||
value={field.value ?? ''}
|
||||
label={field.name}
|
||||
labelOptional={field.format}
|
||||
descriptionText={field.comment}
|
||||
disabled={!isEditable}
|
||||
error={errors[field.name]}
|
||||
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
|
||||
>
|
||||
<Select.Option value="">---</Select.Option>
|
||||
{field.enums.map((value: string) => (
|
||||
<Select.Option key={value} value={value}>
|
||||
{value}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
const isArray = field.format[0] === '_'
|
||||
if (isArray) {
|
||||
return (
|
||||
<div className="text-area-text-sm">
|
||||
<Input.TextArea
|
||||
layout="horizontal"
|
||||
label={field.name}
|
||||
className="text-sm"
|
||||
descriptionText={field.comment}
|
||||
labelOptional={field.format}
|
||||
disabled={!isEditable}
|
||||
error={errors[field.name]}
|
||||
rows={5}
|
||||
value={field.value ?? ''}
|
||||
placeholder={
|
||||
field.defaultValue === null
|
||||
? ''
|
||||
: typeof field.defaultValue === 'string' && field.defaultValue.length === 0
|
||||
? 'Default: Empty string'
|
||||
: `Default: ${field.defaultValue}`
|
||||
}
|
||||
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Select
|
||||
size="medium"
|
||||
layout="horizontal"
|
||||
value={field.value ?? ''}
|
||||
label={field.name}
|
||||
labelOptional={field.format}
|
||||
descriptionText={field.comment}
|
||||
disabled={!isEditable}
|
||||
error={errors[field.name]}
|
||||
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
|
||||
>
|
||||
<Select.Option value="">---</Select.Option>
|
||||
{field.enums.map((value: string) => (
|
||||
<Select.Option key={value} value={value}>
|
||||
{value}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUndefined(field.foreignKey)) {
|
||||
|
||||
+1
-1
@@ -147,6 +147,7 @@ const RowEditor: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
hideFooter
|
||||
size="large"
|
||||
key="RowEditor"
|
||||
visible={visible}
|
||||
@@ -155,7 +156,6 @@ const RowEditor: FC<Props> = ({
|
||||
isEditingJson || isViewingReferenceRow ? ' mr-32' : ''
|
||||
}`}
|
||||
onCancel={closePanel}
|
||||
hideFooter
|
||||
onInteractOutside={(event) => {
|
||||
const isToast = (event.target as Element)?.closest('#toast')
|
||||
if (isToast) {
|
||||
|
||||
+16
-11
@@ -1,7 +1,7 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { find, isUndefined, compact, includes, isEqual, omitBy, isNull, isString } from 'lodash'
|
||||
import { Dictionary } from 'components/grid'
|
||||
import { PostgresTable } from '@supabase/postgres-meta'
|
||||
import { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
import { uuidv4, minifyJSON, tryParseJson } from 'lib/helpers'
|
||||
import { RowField } from './RowEditor.types'
|
||||
@@ -53,18 +53,20 @@ export const generateRowFields = (
|
||||
export const validateFields = (fields: RowField[]) => {
|
||||
const errors = {} as any
|
||||
fields.forEach((field) => {
|
||||
if (field.format.startsWith('_') && (field.value?.length ?? 0) > 0) {
|
||||
const isArray = field.format.startsWith('_')
|
||||
|
||||
if (isArray && field.value) {
|
||||
try {
|
||||
minifyJSON(field.value ?? '')
|
||||
JSON.parse(field.value)
|
||||
} catch {
|
||||
errors[field.name] = 'Invalid array'
|
||||
errors[field.name] = 'Value is an invalid array'
|
||||
}
|
||||
}
|
||||
if (field.format.includes('json') && (field.value?.length ?? 0) > 0) {
|
||||
try {
|
||||
minifyJSON(field.value ?? '')
|
||||
} catch {
|
||||
errors[field.name] = 'Invalid JSON'
|
||||
errors[field.name] = 'Value is an invalid JSON'
|
||||
}
|
||||
}
|
||||
if (field.isIdentity || field.defaultValue) return
|
||||
@@ -102,8 +104,9 @@ const parseValue = (originalValue: string, format: string) => {
|
||||
return JSON.stringify(originalValue)
|
||||
} else if (typeof originalValue === 'boolean') {
|
||||
return (originalValue as any).toString()
|
||||
} else {
|
||||
return originalValue
|
||||
}
|
||||
return originalValue
|
||||
} catch (error) {
|
||||
return originalValue
|
||||
}
|
||||
@@ -181,14 +184,16 @@ export const generateRowObjectFromFields = (
|
||||
): object => {
|
||||
const rowObject = {} as any
|
||||
fields.forEach((field) => {
|
||||
const isArray = field.format.startsWith('_')
|
||||
const value = (field?.value ?? '').length === 0 ? null : field.value
|
||||
if (field.format.includes('json') || (field.format.startsWith('_') && value)) {
|
||||
|
||||
if (isArray && value !== null) {
|
||||
rowObject[field.name] = tryParseJson(value)
|
||||
} else if (field.format.includes('json')) {
|
||||
if (typeof field.value === 'object') {
|
||||
rowObject[field.name] = value
|
||||
} else {
|
||||
if (isString(value)) {
|
||||
rowObject[field.name] = tryParseJson(value)
|
||||
}
|
||||
} else if (isString(value)) {
|
||||
rowObject[field.name] = tryParseJson(value)
|
||||
}
|
||||
} else if (field.format === 'bool' && value) {
|
||||
rowObject[field.name] = value === 'true'
|
||||
|
||||
@@ -63,11 +63,20 @@ const SidePanelEditor: FC<Props> = ({
|
||||
configuration: { identifiers: any; rowIdx: number },
|
||||
onComplete: Function
|
||||
) => {
|
||||
if (selectedTable === undefined) return
|
||||
|
||||
let saveRowError = false
|
||||
// @ts-ignore
|
||||
const enumArrayColumns = selectedTable.columns
|
||||
.filter((column) => {
|
||||
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
|
||||
})
|
||||
.map((column) => column.name)
|
||||
|
||||
if (isNewRecord) {
|
||||
const insertQuery = new Query()
|
||||
.from(selectedTable!.name, selectedTable!.schema)
|
||||
.insert([payload], { returning: true })
|
||||
.from(selectedTable.name, selectedTable.schema)
|
||||
.insert([payload], { returning: true, enumArrayColumns })
|
||||
.toSql()
|
||||
|
||||
const res: any = await meta.query(insertQuery)
|
||||
@@ -80,10 +89,10 @@ const SidePanelEditor: FC<Props> = ({
|
||||
} else {
|
||||
const hasChanges = !isEmpty(payload)
|
||||
if (hasChanges) {
|
||||
if (selectedTable!.primary_keys.length > 0) {
|
||||
if (selectedTable.primary_keys.length > 0) {
|
||||
const updateQuery = new Query()
|
||||
.from(selectedTable!.name, selectedTable!.schema)
|
||||
.update(payload, { returning: true })
|
||||
.from(selectedTable.name, selectedTable.schema)
|
||||
.update(payload, { returning: true, enumArrayColumns })
|
||||
.match(configuration.identifiers)
|
||||
.toSql()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRef, useEffect, useState, FormEvent, KeyboardEvent, ReactNode } from 'react'
|
||||
import { orderBy, filter, without } from 'lodash'
|
||||
import { Popover, IconCheck, IconAlertCircle, IconSearch } from 'ui'
|
||||
import { Popover, IconCheck, IconAlertCircle, IconSearch, IconPlus } from 'ui'
|
||||
|
||||
import { BadgeDisabled, BadgeSelected } from './Badges'
|
||||
|
||||
@@ -12,14 +12,16 @@ export interface MultiSelectOption {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
options: MultiSelectOption[]
|
||||
value: string[]
|
||||
options: MultiSelectOption[]
|
||||
label?: string
|
||||
error?: string
|
||||
placeholder?: string | ReactNode
|
||||
searchPlaceholder?: string
|
||||
descriptionText?: string | ReactNode
|
||||
emptyMessage?: string | ReactNode
|
||||
disabled?: boolean
|
||||
allowDuplicateSelection?: boolean
|
||||
onChange?(x: string[]): void
|
||||
}
|
||||
|
||||
@@ -32,11 +34,13 @@ export default function MultiSelect({
|
||||
options,
|
||||
value,
|
||||
label,
|
||||
error,
|
||||
descriptionText,
|
||||
placeholder,
|
||||
searchPlaceholder = 'Search for option',
|
||||
emptyMessage,
|
||||
disabled,
|
||||
allowDuplicateSelection = false,
|
||||
onChange = () => {},
|
||||
}: Props) {
|
||||
const ref = useRef(null)
|
||||
@@ -51,7 +55,7 @@ export default function MultiSelect({
|
||||
// Calculate width of the Popover
|
||||
useEffect(() => {
|
||||
setInputWidth(ref.current ? (ref.current as any).offsetWidth : inputWidth)
|
||||
}, [])
|
||||
}, [ref.current])
|
||||
|
||||
const width = `${inputWidth}px`
|
||||
|
||||
@@ -69,11 +73,19 @@ export default function MultiSelect({
|
||||
return isOptionSelected !== undefined
|
||||
}
|
||||
|
||||
const handleRemove = (idx: number) => {
|
||||
const updatedSelected = selected.filter((x, index) => index !== idx)
|
||||
setSelected(updatedSelected)
|
||||
onChange(updatedSelected)
|
||||
}
|
||||
|
||||
const handleChange = (option: MultiSelectOption) => {
|
||||
const _selected = selectedOptions
|
||||
const isActive = checkIfActive(option)
|
||||
|
||||
const updatedPayload = isActive
|
||||
const updatedPayload = allowDuplicateSelection
|
||||
? [..._selected.concat([option.value])]
|
||||
: isActive
|
||||
? [...without(_selected, option.value)]
|
||||
: [..._selected.concat([option.value])]
|
||||
|
||||
@@ -81,7 +93,10 @@ export default function MultiSelect({
|
||||
const compulsoryOptions = options
|
||||
.filter((option) => option.disabled)
|
||||
.map((option) => option.name)
|
||||
const formattedPayload = [...new Set(updatedPayload.concat(compulsoryOptions))]
|
||||
|
||||
const formattedPayload = allowDuplicateSelection
|
||||
? updatedPayload.concat(compulsoryOptions)
|
||||
: [...new Set(updatedPayload.concat(compulsoryOptions))]
|
||||
|
||||
setSelected(formattedPayload)
|
||||
onChange(formattedPayload)
|
||||
@@ -103,6 +118,7 @@ export default function MultiSelect({
|
||||
'form-control form-control--multi-select',
|
||||
'border border-scale-700 bg-scaleA-200',
|
||||
'multi-select relative block w-full w-full space-x-1 overflow-auto rounded',
|
||||
`${error !== undefined ? 'border-red-800 bg-red-100' : ''}`,
|
||||
].join(' ')}
|
||||
ref={ref}
|
||||
>
|
||||
@@ -131,6 +147,7 @@ export default function MultiSelect({
|
||||
{filteredOptions.length >= 1 ? (
|
||||
filteredOptions.map((option) => {
|
||||
const active =
|
||||
!allowDuplicateSelection &&
|
||||
selectedOptions &&
|
||||
selectedOptions.find((selected) => {
|
||||
return selected === option.value
|
||||
@@ -140,11 +157,11 @@ export default function MultiSelect({
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`multiselect-option-${option.value}`}
|
||||
key={`multiselect-option-${option.id}`}
|
||||
onClick={() => handleChange(option)}
|
||||
className={[
|
||||
'text-typography-body-light dark:text-typography-body-dark',
|
||||
'flex cursor-pointer items-center justify-between transition',
|
||||
'group flex cursor-pointer items-center justify-between transition',
|
||||
'space-x-1 rounded bg-transparent p-2 px-4 text-sm hover:bg-gray-600',
|
||||
`${active ? ' dark:bg-green-600 dark:bg-opacity-25' : ''}`,
|
||||
].join(' ')}
|
||||
@@ -159,6 +176,12 @@ export default function MultiSelect({
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{allowDuplicateSelection && (
|
||||
<div className="flex items-center opacity-0 group-hover:opacity-100 transition space-x-1">
|
||||
<IconPlus size={14} />
|
||||
<p className="text-sm">Add value</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -207,21 +230,22 @@ export default function MultiSelect({
|
||||
{selectedOptions.length === 0 && placeholder && (
|
||||
<div className="px-2 text-sm text-scale-1000">{placeholder}</div>
|
||||
)}
|
||||
{formattedOptions.map((option) => {
|
||||
const active =
|
||||
selectedOptions &&
|
||||
selectedOptions.find((selected) => {
|
||||
return selected === option.value
|
||||
})
|
||||
|
||||
if (option.disabled) {
|
||||
return <BadgeDisabled key={option.id} name={option.name} />
|
||||
} else if (active) {
|
||||
{selectedOptions.map((value, idx) => {
|
||||
const id = `${value}-${idx}`
|
||||
const option = formattedOptions.find((x) => x.value === value)
|
||||
const isDisabled = option?.disabled ?? false
|
||||
if (!option) {
|
||||
return <></>
|
||||
} else if (isDisabled) {
|
||||
return <BadgeDisabled key={id} name={value} />
|
||||
} else {
|
||||
return (
|
||||
<BadgeSelected
|
||||
key={option.id}
|
||||
name={option.name}
|
||||
handleRemove={() => handleChange(option)}
|
||||
key={id}
|
||||
name={value}
|
||||
handleRemove={() =>
|
||||
allowDuplicateSelection ? handleRemove(idx) : handleChange(option)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -233,6 +257,7 @@ export default function MultiSelect({
|
||||
{descriptionText && (
|
||||
<span className="form-text text-muted mt-2 text-sm">{descriptionText}</span>
|
||||
)}
|
||||
{error && <span className="text-red-900 text-sm mt-2">{error}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+19
-16
@@ -1,4 +1,5 @@
|
||||
import { v4 as _uuidV4 } from 'uuid'
|
||||
import { PostgresColumn } from '@supabase/postgres-meta'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { PASSWORD_STRENGTH, DEFAULT_MINIMUM_PASSWORD_STRENGTH, API_URL } from 'lib/constants'
|
||||
|
||||
@@ -168,31 +169,33 @@ export async function passwordStrength(value: string) {
|
||||
let strength = 0
|
||||
|
||||
if (value && value !== '') {
|
||||
if(value.length > 99){
|
||||
if (value.length > 99) {
|
||||
message = `${PASSWORD_STRENGTH[0]} Maximum length of password exceeded`
|
||||
warning = `Password should be less than 100 characters`
|
||||
} else {
|
||||
const response = await post(`${API_URL}/profile/password-check`, { password: value })
|
||||
if (!response.error) {
|
||||
const { result } = response
|
||||
const score = (PASSWORD_STRENGTH as any)[result.score]
|
||||
const suggestions = result.feedback?.suggestions ? result.feedback.suggestions.join(' ') : ''
|
||||
const response = await post(`${API_URL}/profile/password-check`, { password: value })
|
||||
if (!response.error) {
|
||||
const { result } = response
|
||||
const score = (PASSWORD_STRENGTH as any)[result.score]
|
||||
const suggestions = result.feedback?.suggestions
|
||||
? result.feedback.suggestions.join(' ')
|
||||
: ''
|
||||
|
||||
// set message :string
|
||||
message = `${score} ${suggestions}`
|
||||
// set message :string
|
||||
message = `${score} ${suggestions}`
|
||||
|
||||
// set strength :number
|
||||
strength = result.score
|
||||
// set strength :number
|
||||
strength = result.score
|
||||
|
||||
// warning message for anything below 4 strength :string
|
||||
if (result.score < DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
|
||||
warning = `${
|
||||
result?.feedback?.warning ? result?.feedback?.warning + '.' : ''
|
||||
} You need a stronger password.`
|
||||
// warning message for anything below 4 strength :string
|
||||
if (result.score < DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
|
||||
warning = `${
|
||||
result?.feedback?.warning ? result?.feedback?.warning + '.' : ''
|
||||
} You need a stronger password.`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
|
||||
@@ -386,7 +386,10 @@
|
||||
*/
|
||||
|
||||
.sb-grid-select-editor {
|
||||
@apply h-full w-full;
|
||||
button {
|
||||
@apply border-none rounded-none shadow-none;
|
||||
box-shadow: none !important
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in new issue
Block a user