feat(chart max length): add support for edit char max length (#613)

* feat(chart max length): add support for edit char max length

* fix

* update datatypes for max chars

---------

Co-authored-by: johnnyfish <jonathanfishner11@gmail.com>
This commit is contained in:
Guy Ben-Aharon
2025-03-10 13:25:55 +02:00
committed by GitHub
parent 5dd7fe75d1
commit 09b1275475
32 changed files with 309 additions and 161 deletions

View File

@@ -151,6 +151,8 @@ export const ar: LanguageTranslation = {
comments: 'تعليقات',
no_comments: 'لا يوجد تعليقات',
delete_field: 'حذف الحقل',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'خصائص الفهرس',

View File

@@ -152,6 +152,8 @@ export const bn: LanguageTranslation = {
comments: 'মন্তব্য',
no_comments: 'কোনো মন্তব্য নেই',
delete_field: 'ফিল্ড মুছুন',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ইনডেক্স কর্ম',

View File

@@ -153,6 +153,8 @@ export const de: LanguageTranslation = {
comments: 'Kommentare',
no_comments: 'Keine Kommentare',
delete_field: 'Feld löschen',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Indexattribute',

View File

@@ -145,6 +145,7 @@ export const en = {
field_actions: {
title: 'Field Attributes',
unique: 'Unique',
character_length: 'Max Length',
comments: 'Comments',
no_comments: 'No comments',
delete_field: 'Delete Field',

View File

@@ -142,6 +142,8 @@ export const es: LanguageTranslation = {
comments: 'Comentarios',
no_comments: 'Sin comentarios',
delete_field: 'Eliminar Campo',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Atributos del Índice',

View File

@@ -140,6 +140,8 @@ export const fr: LanguageTranslation = {
comments: 'Commentaires',
no_comments: 'Pas de commentaires',
delete_field: 'Supprimer le Champ',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: "Attributs de l'Index",

View File

@@ -153,6 +153,8 @@ export const gu: LanguageTranslation = {
comments: 'ટિપ્પણીઓ',
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
delete_field: 'ફીલ્ડ કાઢી નાખો',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ઇન્ડેક્સ લક્ષણો',

View File

@@ -152,6 +152,8 @@ export const hi: LanguageTranslation = {
comments: 'टिप्पणियाँ',
no_comments: 'कोई टिप्पणी नहीं',
delete_field: 'फ़ील्ड हटाएँ',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'सूचकांक विशेषताएँ',

View File

@@ -151,6 +151,8 @@ export const id_ID: LanguageTranslation = {
comments: 'Komentar',
no_comments: 'Tidak ada komentar',
delete_field: 'Hapus Kolom',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Atribut Indeks',

View File

@@ -155,6 +155,8 @@ export const ja: LanguageTranslation = {
comments: 'コメント',
no_comments: 'コメントがありません',
delete_field: 'フィールドを削除',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'インデックス属性',

View File

@@ -151,6 +151,8 @@ export const ko_KR: LanguageTranslation = {
comments: '주석',
no_comments: '주석 없음',
delete_field: '필드 삭제',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '인덱스 속성',

View File

@@ -154,6 +154,8 @@ export const mr: LanguageTranslation = {
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'इंडेक्स गुणधर्म',

View File

@@ -152,6 +152,8 @@ export const ne: LanguageTranslation = {
comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्',
delete_field: 'क्षेत्र हटाउनुहोस्',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'सूचक विशेषताहरू',

View File

@@ -152,6 +152,8 @@ export const pt_BR: LanguageTranslation = {
comments: 'Comentários',
no_comments: 'Sem comentários',
delete_field: 'Excluir Campo',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Atributos do Índice',

View File

@@ -151,6 +151,8 @@ export const ru: LanguageTranslation = {
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Атрибуты индекса',

View File

@@ -152,6 +152,8 @@ export const te: LanguageTranslation = {
comments: 'వ్యాఖ్యలు',
no_comments: 'వ్యాఖ్యలు లేవు',
delete_field: 'ఫీల్డ్ తొలగించు',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ఇండెక్స్ గుణాలు',

View File

@@ -151,6 +151,8 @@ export const tr: LanguageTranslation = {
comments: 'Yorumlar',
no_comments: 'Yorum yok',
delete_field: 'Alanı Sil',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'İndeks Özellikleri',

View File

@@ -150,6 +150,8 @@ export const uk: LanguageTranslation = {
comments: 'Коментарі',
no_comments: 'Немає коментарів',
delete_field: 'Видалити поле',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Атрибути індексу',

View File

@@ -151,6 +151,8 @@ export const vi: LanguageTranslation = {
comments: 'Bình luận',
no_comments: 'Không có bình luận',
delete_field: 'Xóa trường',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Thuộc tính chỉ mục',

View File

@@ -148,6 +148,8 @@ export const zh_CN: LanguageTranslation = {
comments: '注释',
no_comments: '空',
delete_field: '删除字段',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '索引属性',

View File

@@ -148,6 +148,8 @@ export const zh_TW: LanguageTranslation = {
comments: '註解',
no_comments: '無註解',
delete_field: '刪除欄位',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '索引屬性',

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const clickhouseDataTypes: readonly DataType[] = [
export const clickhouseDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'uint8', id: 'uint8' },
{ name: 'uint16', id: 'uint16' },
@@ -48,25 +48,41 @@ export const clickhouseDataTypes: readonly DataType[] = [
{ name: 'mediumblob', id: 'mediumblob' },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'varchar', id: 'varchar' },
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'char large object', id: 'char_large_object' },
{ name: 'char varying', id: 'char_varying' },
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
{ name: 'character large object', id: 'character_large_object' },
{ name: 'character varying', id: 'character_varying' },
{
name: 'character varying',
id: 'character_varying',
hasCharMaxLength: true,
},
{ name: 'nchar large object', id: 'nchar_large_object' },
{ name: 'nchar varying', id: 'nchar_varying' },
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
{
name: 'national character large object',
id: 'national_character_large_object',
},
{ name: 'national character varying', id: 'national_character_varying' },
{ name: 'national char varying', id: 'national_char_varying' },
{ name: 'national character', id: 'national_character' },
{ name: 'national char', id: 'national_char' },
{
name: 'national character varying',
id: 'national_character_varying',
hasCharMaxLength: true,
},
{
name: 'national char varying',
id: 'national_char_varying',
hasCharMaxLength: true,
},
{
name: 'national character',
id: 'national_character',
hasCharMaxLength: true,
},
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
{ name: 'binary large object', id: 'binary_large_object' },
{ name: 'binary varying', id: 'binary_varying' },
{ name: 'fixedstring', id: 'fixedstring' },
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
{ name: 'string', id: 'string' },
// Date Types

View File

@@ -13,12 +13,16 @@ export interface DataType {
name: string;
}
export interface DataTypeData extends DataType {
hasCharMaxLength?: boolean;
}
export const dataTypeSchema: z.ZodType<DataType> = z.object({
id: z.string(),
name: z.string(),
});
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
[DatabaseType.GENERIC]: genericDataTypes,
[DatabaseType.POSTGRESQL]: postgresDataTypes,
[DatabaseType.MYSQL]: mysqlDataTypes,
@@ -64,3 +68,21 @@ export function areFieldTypesCompatible(
}
export const dataTypes = Object.values(dataTypeMap).flat();
export const dataTypeDataToDataType = (
dataTypeData: DataTypeData
): DataType => ({
id: dataTypeData.id,
name: dataTypeData.name,
});
export const findDataTypeDataById = (
id: string,
databaseType?: DatabaseType
): DataTypeData | undefined => {
const dataTypesOptions = databaseType
? dataTypeMap[databaseType]
: dataTypes;
return dataTypesOptions.find((dataType) => dataType.id === id);
};

View File

@@ -1,11 +1,11 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const genericDataTypes: readonly DataType[] = [
export const genericDataTypes: readonly DataTypeData[] = [
{ name: 'bigint', id: 'bigint' },
{ name: 'binary', id: 'binary' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'blob', id: 'blob' },
{ name: 'boolean', id: 'boolean' },
{ name: 'char', id: 'char' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
{ name: 'decimal', id: 'decimal' },
@@ -22,6 +22,6 @@ export const genericDataTypes: readonly DataType[] = [
{ name: 'time', id: 'time' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'uuid', id: 'uuid' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'varchar', id: 'varchar' },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
] as const;

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const mariadbDataTypes: readonly DataType[] = [
export const mariadbDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
@@ -23,10 +23,10 @@ export const mariadbDataTypes: readonly DataType[] = [
{ name: 'year', id: 'year' },
// String Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const mysqlDataTypes: readonly DataType[] = [
export const mysqlDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
@@ -23,10 +23,10 @@ export const mysqlDataTypes: readonly DataType[] = [
{ name: 'year', id: 'year' },
// String Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const postgresDataTypes: readonly DataType[] = [
export const postgresDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'smallint', id: 'smallint' },
{ name: 'integer', id: 'integer' },
@@ -15,9 +15,13 @@ export const postgresDataTypes: readonly DataType[] = [
{ name: 'money', id: 'money' },
// Character Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'character varying', id: 'character_varying' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{
name: 'character varying',
id: 'character_varying',
hasCharMaxLength: true,
},
{ name: 'text', id: 'text' },
// Binary Data Types

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const sqlServerDataTypes: readonly DataType[] = [
export const sqlServerDataTypes: readonly DataTypeData[] = [
// Exact Numerics
{ name: 'bigint', id: 'bigint' },
{ name: 'bit', id: 'bit' },
@@ -25,18 +25,18 @@ export const sqlServerDataTypes: readonly DataType[] = [
{ name: 'time', id: 'time' },
// Character Strings
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'text', id: 'text' },
// Unicode Character Strings
{ name: 'nchar', id: 'nchar' },
{ name: 'nvarchar', id: 'nvarchar' },
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true },
{ name: 'ntext', id: 'ntext' },
// Binary Strings
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'image', id: 'image' },
// Other Data Types

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const sqliteDataTypes: readonly DataType[] = [
export const sqliteDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'integer', id: 'integer' },
{ name: 'real', id: 'real' },
@@ -22,6 +22,6 @@ export const sqliteDataTypes: readonly DataType[] = [
{ name: 'int', id: 'int' },
{ name: 'float', id: 'float' },
{ name: 'boolean', id: 'boolean' },
{ name: 'varchar', id: 'varchar' },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'decimal', id: 'decimal' },
] as const;

View File

@@ -95,7 +95,7 @@ export const createFieldsFromMetadata = ({
nullable: col.nullable,
...(col.character_maximum_length &&
col.character_maximum_length !== 'null'
? { character_maximum_length: col.character_maximum_length }
? { characterMaximumLength: col.character_maximum_length }
: {}),
...(col.precision?.precision
? { precision: col.precision.precision }

View File

@@ -0,0 +1,156 @@
import React, { useEffect, useRef } from 'react';
import { Ellipsis, Trash2 } from 'lucide-react';
import { Input } from '@/components/input/input';
import { Button } from '@/components/button/button';
import { Separator } from '@/components/separator/separator';
import type { DBField } from '@/lib/domain/db-field';
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/popover/popover';
import { Label } from '@/components/label/label';
import { Checkbox } from '@/components/checkbox/checkbox';
import { useTranslation } from 'react-i18next';
import { Textarea } from '@/components/textarea/textarea';
import { debounce } from '@/lib/utils';
export interface TableFieldPopoverProps {
field: DBField;
updateField: (attrs: Partial<DBField>) => void;
removeField: () => void;
}
export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
field,
updateField,
removeField,
}) => {
const { t } = useTranslation();
const [localField, setLocalField] = React.useState<DBField>(field);
const debouncedUpdateFieldRef = useRef<((value?: DBField) => void) | null>(
null
);
useEffect(() => {
debouncedUpdateFieldRef.current = debounce((value?: DBField) => {
updateField({
comments: value?.comments,
characterMaximumLength: value?.characterMaximumLength,
unique: value?.unique,
});
}, 200);
return () => {
debouncedUpdateFieldRef.current = null;
};
}, [updateField]);
useEffect(() => {
if (debouncedUpdateFieldRef.current) {
debouncedUpdateFieldRef.current(localField);
}
}, [localField]);
return (
<Popover onOpenChange={(isOpen) => !isOpen && setLocalField(field)}>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="h-8 w-[32px] p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
>
<Ellipsis className="size-3.5" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-52">
<div className="flex flex-col gap-2">
<div className="text-sm font-semibold">
{t(
'side_panel.tables_section.table.field_actions.title'
)}
</div>
<Separator orientation="horizontal" />
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<Label htmlFor="width" className="text-subtitle">
{t(
'side_panel.tables_section.table.field_actions.unique'
)}
</Label>
<Checkbox
checked={localField.unique}
disabled={field.primaryKey}
onCheckedChange={(value) =>
setLocalField((current) => ({
...current,
unique: !!value,
}))
}
/>
</div>
{findDataTypeDataById(field.type.id)
?.hasCharMaxLength ? (
<div className="flex flex-col gap-2">
<Label
htmlFor="width"
className="text-subtitle"
>
{t(
'side_panel.tables_section.table.field_actions.character_length'
)}
</Label>
<Input
value={
localField.characterMaximumLength ?? ''
}
type="number"
onChange={(e) =>
setLocalField((current) => ({
...current,
characterMaximumLength:
e.target.value,
}))
}
className="w-full rounded-md bg-muted text-sm"
/>
</div>
) : null}
<div className="flex flex-col gap-2">
<Label htmlFor="width" className="text-subtitle">
{t(
'side_panel.tables_section.table.field_actions.comments'
)}
</Label>
<Textarea
value={localField.comments}
onChange={(e) =>
setLocalField((current) => ({
...current,
comments: e.target.value,
}))
}
placeholder={t(
'side_panel.tables_section.table.field_actions.no_comments'
)}
className="w-full rounded-md bg-muted text-sm"
/>
</div>
</div>
<Separator orientation="horizontal" />
<Button
variant="outline"
className="flex gap-2 !text-red-700"
onClick={removeField}
>
<Trash2 className="size-3.5 text-red-700" />
{t(
'side_panel.tables_section.table.field_actions.delete_field'
)}
</Button>
</div>
</PopoverContent>
</Popover>
);
};

View File

@@ -1,31 +1,23 @@
import React, { useEffect, useRef } from 'react';
import { Ellipsis, GripVertical, Trash2, KeyRound } from 'lucide-react';
import React from 'react';
import { GripVertical, KeyRound } from 'lucide-react';
import { Input } from '@/components/input/input';
import { Button } from '@/components/button/button';
import { Separator } from '@/components/separator/separator';
import type { DBField } from '@/lib/domain/db-field';
import { useChartDB } from '@/hooks/use-chartdb';
import { dataTypeMap } from '@/lib/data/data-types/data-types';
import {
dataTypeDataToDataType,
dataTypeMap,
} from '@/lib/data/data-types/data-types';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/popover/popover';
import { Label } from '@/components/label/label';
import { Checkbox } from '@/components/checkbox/checkbox';
import { useTranslation } from 'react-i18next';
import { Textarea } from '@/components/textarea/textarea';
import { TableFieldToggle } from './table-field-toggle';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { SelectBox } from '@/components/select-box/select-box';
import { debounce } from '@/lib/utils';
import { TableFieldPopover } from './table-field-modal/table-field-modal';
export interface TableFieldProps {
field: DBField;
@@ -40,7 +32,7 @@ export const TableField: React.FC<TableFieldProps> = ({
}) => {
const { databaseType } = useChartDB();
const { t } = useTranslation();
const [comments, setComments] = React.useState(field.comments);
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: field.id });
@@ -54,28 +46,6 @@ export const TableField: React.FC<TableFieldProps> = ({
transition,
};
const debouncedUpdateCommentRef = useRef<((value?: string) => void) | null>(
null
);
useEffect(() => {
debouncedUpdateCommentRef.current = debounce((value?: string) => {
updateField({
comments: value,
});
}, 500);
return () => {
debouncedUpdateCommentRef.current = null;
};
}, [updateField]);
useEffect(() => {
if (debouncedUpdateCommentRef.current) {
debouncedUpdateCommentRef.current(comments);
}
}, [comments]);
return (
<div
className="flex flex-1 touch-none flex-row justify-between p-1"
@@ -122,8 +92,14 @@ export const TableField: React.FC<TableFieldProps> = ({
value={field.type.id}
onChange={(value) =>
updateField({
type: dataTypeMap[databaseType].find(
(v) => v.id === value
characterMaximumLength: undefined,
type: dataTypeDataToDataType(
dataTypeMap[databaseType].find(
(v) => v.id === value
) ?? {
id: value as string,
name: value as string,
}
),
})
}
@@ -176,78 +152,11 @@ export const TableField: React.FC<TableFieldProps> = ({
{t('side_panel.tables_section.table.primary_key')}
</TooltipContent>
</Tooltip>
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="h-8 w-[32px] p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
>
<Ellipsis className="size-3.5" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-52">
<div className="flex flex-col gap-2">
<div className="text-sm font-semibold">
{t(
'side_panel.tables_section.table.field_actions.title'
)}
</div>
<Separator orientation="horizontal" />
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<Label
htmlFor="width"
className="text-subtitle"
>
{t(
'side_panel.tables_section.table.field_actions.unique'
)}
</Label>
<Checkbox
checked={field.unique}
disabled={field.primaryKey}
onCheckedChange={(value) =>
updateField({
unique: !!value,
})
}
/>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="width"
className="text-subtitle"
>
{t(
'side_panel.tables_section.table.field_actions.comments'
)}
</Label>
<Textarea
value={comments}
onChange={(e) =>
setComments(e.target.value)
}
placeholder={t(
'side_panel.tables_section.table.field_actions.no_comments'
)}
className="w-full rounded-md bg-muted text-sm"
/>
</div>
</div>
<Separator orientation="horizontal" />
<Button
variant="outline"
className="flex gap-2 !text-red-700"
onClick={removeField}
>
<Trash2 className="size-3.5 text-red-700" />
{t(
'side_panel.tables_section.table.field_actions.delete_field'
)}
</Button>
</div>
</PopoverContent>
</Popover>
<TableFieldPopover
field={field}
updateField={updateField}
removeField={removeField}
/>
</div>
</div>
);