mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 03:05:42 +00:00
Mysql Datetime data types modifiers implemented
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@heroui/react": "^2.7.6",
|
||||
"@internationalized/date": "^3.8.2",
|
||||
"@nextui-org/react": "^2.6.11",
|
||||
"@powersync/drizzle-driver": "^0.4.0",
|
||||
"@powersync/react": "^1.5.3",
|
||||
@@ -24,6 +25,7 @@
|
||||
"@uiw/react-codemirror": "^4.23.12",
|
||||
"@xyflow/react": "^12.6.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"elkjs": "^0.10.0",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
|
||||
+18
-1
@@ -70,11 +70,28 @@ export const en = {
|
||||
collation : "Collation" ,
|
||||
scale : "Scale" ,
|
||||
max_length : "Max length" ,
|
||||
integer_width : "Integer Width" ,
|
||||
width : "width" ,
|
||||
default_value : "Default value" ,
|
||||
value : "Value" ,
|
||||
length : "Length" ,
|
||||
values : "Values" ,
|
||||
type_enter : "Type and press enter"
|
||||
type_enter : "Type and press enter" ,
|
||||
precision_def : "Total digits allowed (before + after the decimal)." ,
|
||||
scale_def : "Digits allowed after the decimal." ,
|
||||
time_default_value : {
|
||||
no_value : "No value" ,
|
||||
custom : "Custom time" ,
|
||||
now : "Now"
|
||||
} ,
|
||||
errors : {
|
||||
max_length : "must be positive number, no decimals." ,
|
||||
integer_default_value : "Invalid default value for Integer" ,
|
||||
precision : "Precision must be positive number, no decimals." ,
|
||||
scale : "Scale must be positive number, no decimals." ,
|
||||
scale_max_value : "Scale must be ≤ precision."
|
||||
|
||||
}
|
||||
},
|
||||
delete: "Delete",
|
||||
|
||||
|
||||
+29
-9
@@ -1,14 +1,14 @@
|
||||
|
||||
export enum Modifiers {
|
||||
LENGTH = "length",
|
||||
UNSIGNED = "unsigned",
|
||||
ZEROFILL = "zerofill",
|
||||
AUTO_INCREMENT = "auto_increment",
|
||||
PRECISION = "precision",
|
||||
SCALE = "scale",
|
||||
CHARSET = "charset",
|
||||
COLLATE = "collate",
|
||||
VALUES = "values" ,
|
||||
LENGTH = "length",
|
||||
UNSIGNED = "unsigned",
|
||||
ZEROFILL = "zerofill",
|
||||
AUTO_INCREMENT = "auto_increment",
|
||||
PRECISION = "precision",
|
||||
SCALE = "scale",
|
||||
CHARSET = "charset",
|
||||
COLLATE = "collate",
|
||||
VALUES = "values",
|
||||
}
|
||||
|
||||
export enum MySQLCharset {
|
||||
@@ -73,4 +73,24 @@ export enum SQLiteCollation {
|
||||
Binary = 'BINARY',
|
||||
NoCase = 'NOCASE',
|
||||
RTrim = 'RTRIM',
|
||||
}
|
||||
|
||||
|
||||
|
||||
export enum DataTypes {
|
||||
INTEGER = "integer",
|
||||
NUMERIC = "numeric",
|
||||
BOOLEAN = "boolean",
|
||||
TIME = "time",
|
||||
TEXT = "text",
|
||||
BINARY = "binary",
|
||||
JSON = "JSON",
|
||||
GEOMETRY = "geometry"
|
||||
}
|
||||
|
||||
|
||||
export enum TimeDefaultValues {
|
||||
NO_VALUE = "no_value",
|
||||
CUSTOM = "custom" ,
|
||||
NOW = "now"
|
||||
}
|
||||
-1
@@ -16,7 +16,6 @@ interface RelationshipAccordionBodyProps {
|
||||
const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ relationship }) => {
|
||||
const [cardinality, setCardinality] = useState(new Set([relationship.cardinality]));
|
||||
const { editRelationship, deleteRelationship } = useDatabaseOperations();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const changeCardinality = (keys: SharedSelection) => {
|
||||
|
||||
@@ -11,11 +11,15 @@ import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
const parser = new Parser();
|
||||
const code = `
|
||||
CREATE TABLE \`users\` (
|
||||
username VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT "test" ,
|
||||
username VARCHAR(100) NOT NULL DEFAULT "test" CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
|
||||
|
||||
)
|
||||
CREATE TABLE events (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
|
||||
logged_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
|
||||
|
||||
@@ -30,8 +34,9 @@ const SqlPreview: React.FC = ({ }) => {
|
||||
useEffect(() => {
|
||||
const ast = parser.astify(code, {
|
||||
database: "Mysql"
|
||||
})
|
||||
// console.log(ast)
|
||||
}) ;
|
||||
|
||||
console.log(ast)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema"
|
||||
import { Checkbox, DatePicker, Input, Select, SelectItem, Switch, TimeInput, Tooltip } from "@heroui/react";
|
||||
import React, { Ref, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModifierValidation } from "./field-setting";
|
||||
import { DataTypes, TimeDefaultValues } from "@/lib/field";
|
||||
import { Calendar, Clock, TriangleAlert } from "lucide-react";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import dayjs from 'dayjs';
|
||||
import { now, parseAbsoluteToLocal, Time } from "@internationalized/date";
|
||||
|
||||
interface DefaultValueType {
|
||||
number?: boolean;
|
||||
string?: boolean;
|
||||
boolean?: boolean;
|
||||
time?: boolean;
|
||||
}
|
||||
|
||||
interface FieldDefaultValueProps {
|
||||
field: FieldType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
const { editField } = useDatabaseOperations();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [defaultValueValidation, setDefaultValueValidation] = useState<ModifierValidation>({
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
const defaultValueRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [timeSelection, setTimeSelection] = useState<string[]>(() => {
|
||||
|
||||
if (field.defaultValue == TimeDefaultValues.NOW)
|
||||
return [TimeDefaultValues.NOW];
|
||||
if (!field.defaultValue)
|
||||
return [TimeDefaultValues.NO_VALUE]
|
||||
|
||||
if (field.type.name == "time" && field.defaultValue)
|
||||
return [TimeDefaultValues.CUSTOM]
|
||||
|
||||
const date = new Date(field.defaultValue);
|
||||
|
||||
if (!isNaN(date.getTime())) {
|
||||
return [TimeDefaultValues.CUSTOM];
|
||||
}
|
||||
return [TimeDefaultValues.NO_VALUE];
|
||||
});
|
||||
|
||||
const [defaultDateTime, setDefaultDateTime] = useState<any>(() => {
|
||||
try {
|
||||
if (field.type.name == "time" && field.defaultValue) {
|
||||
|
||||
const [hours, minuts, seconds] = field.defaultValue.split(":");
|
||||
return new Time(parseInt(hours), parseInt(minuts), parseInt(seconds))
|
||||
}
|
||||
|
||||
if (field.defaultValue) {
|
||||
const date = new Date(field.defaultValue)
|
||||
return parseAbsoluteToLocal(date.toISOString());
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const defaultValueChange = useCallback((value: any) => {
|
||||
if (value && value.trim().length > 0) {
|
||||
if (field.type?.type == DataTypes.INTEGER) {
|
||||
const isValid: boolean = Number.isInteger(Number(value));
|
||||
setDefaultValueValidation({
|
||||
isValid,
|
||||
errorMessage: t("db_controller.field_settings.errors.integer_default_value")
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setDefaultValueValidation({
|
||||
isValid: true,
|
||||
errorMessage: undefined
|
||||
});
|
||||
}
|
||||
}, [field, defaultValueRef]);
|
||||
|
||||
|
||||
const saveDefaultValue = useCallback(() => {
|
||||
|
||||
|
||||
if (defaultValueValidation.isValid) {
|
||||
let value: string | undefined;
|
||||
if (field.type.type != DataTypes.BOOLEAN)
|
||||
value = defaultValueRef.current?.value;
|
||||
else {
|
||||
value = String(defaultValueRef.current?.checked);
|
||||
}
|
||||
|
||||
editField({
|
||||
id: field.id,
|
||||
defaultValue: value ? String(value) : null,
|
||||
} as FieldInsertType);
|
||||
}
|
||||
}, [defaultValueRef, field, defaultValueValidation]);
|
||||
|
||||
|
||||
const defaultValueType: DefaultValueType = useMemo(() => {
|
||||
return {
|
||||
number: field.type?.type == DataTypes.INTEGER || field.type?.type == DataTypes.NUMERIC,
|
||||
string: field.type.type == DataTypes.TEXT || field.type.name == "year",
|
||||
boolean: field.type.type == DataTypes.BOOLEAN,
|
||||
time: field.type.type == DataTypes.TIME && field.type.name != "year"
|
||||
}
|
||||
}, [field])
|
||||
|
||||
const changeTimeDefaultValue = (selection: any) => {
|
||||
let value: string | undefined;
|
||||
|
||||
if (!selection.currentKey)
|
||||
return;
|
||||
|
||||
if (selection.currentKey == TimeDefaultValues.NOW)
|
||||
value = TimeDefaultValues.NOW;
|
||||
|
||||
if (selection.currentKey == TimeDefaultValues.CUSTOM) {
|
||||
|
||||
const currentDateTime = now(Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||
const date: Date = currentDateTime?.toDate();
|
||||
|
||||
setDefaultDateTime(currentDateTime);
|
||||
if (field.type.name == "date")
|
||||
value = dayjs(date).format("YYYY-MM-DD")
|
||||
else if (field.type.name == "datetime" || field.type.name == "timestamp")
|
||||
value = dayjs(date).format("YYYY-MM-DD HH:mm:ss")
|
||||
else if (field.type.name == "time")
|
||||
value = dayjs(date).format("HH:mm:ss")
|
||||
|
||||
}
|
||||
|
||||
editField(({
|
||||
id: field.id,
|
||||
defaultValue: value ? String(value) : null
|
||||
}) as FieldInsertType)
|
||||
|
||||
setTimeSelection([selection?.currentKey]);
|
||||
}
|
||||
|
||||
const saveDefaultDateTime = useCallback(() => {
|
||||
let value: string | undefined;
|
||||
if (field.type.name != "time") {
|
||||
const date = new Date(defaultDateTime?.toDate());
|
||||
if (field.type.name == "date")
|
||||
value = dayjs(date).format("YYYY-MM-DD")
|
||||
else if (field.type.name == "datetime" || field.type.name == "timestamp")
|
||||
value = dayjs(date).format("YYYY-MM-DD HH:mm:ss")
|
||||
}
|
||||
else {
|
||||
if (defaultDateTime) {
|
||||
value = `${defaultDateTime.hour}:${defaultDateTime.minute}:${defaultDateTime.second}`
|
||||
}
|
||||
}
|
||||
|
||||
editField(({
|
||||
id: field.id,
|
||||
defaultValue: value ? String(value) : null
|
||||
}) as FieldInsertType);
|
||||
}, [field, defaultDateTime])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!defaultValueType.boolean &&
|
||||
<label className="text-xs font-medium text-font/70 dark:text-font/90">
|
||||
{t("db_controller.field_settings.default_value")}
|
||||
</label>
|
||||
}
|
||||
{
|
||||
(defaultValueType.number || defaultValueType.string) &&
|
||||
|
||||
<Input
|
||||
type={defaultValueType.number ? "number" : "text"}
|
||||
size="sm"
|
||||
ref={defaultValueRef}
|
||||
isInvalid={!defaultValueValidation.isValid}
|
||||
endContent={
|
||||
!defaultValueValidation.isValid && <>
|
||||
<Tooltip showArrow={true} content={defaultValueValidation.errorMessage} radius="sm" color="danger">
|
||||
<TriangleAlert className="size-4 text-danger cursor-default" />
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
||||
onValueChange={defaultValueChange}
|
||||
defaultValue={field.defaultValue as string}
|
||||
onBlur={saveDefaultValue}
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
placeholder={t("db_controller.field_settings.value")}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
{
|
||||
defaultValueType.boolean &&
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
{t("db_controller.field_settings.default_value")}
|
||||
</span>
|
||||
<Checkbox
|
||||
defaultSelected={field.defaultValue == "true"}
|
||||
ref={defaultValueRef}
|
||||
size="md"
|
||||
onValueChange={saveDefaultValue}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
defaultValueType.time &&
|
||||
<>
|
||||
<Select
|
||||
className="w-full"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
aria-label="cardinality"
|
||||
selectedKeys={timeSelection}
|
||||
onSelectionChange={changeTimeDefaultValue}
|
||||
classNames={{
|
||||
trigger: "border-divider group-hover:border-primary",
|
||||
}}
|
||||
>
|
||||
<SelectItem key={TimeDefaultValues.NO_VALUE}>{t("db_controller.field_settings.time_default_value.no_value")}</SelectItem>
|
||||
<SelectItem key={TimeDefaultValues.CUSTOM}>{t("db_controller.field_settings.time_default_value.custom")}</SelectItem>
|
||||
{
|
||||
(field.type.name == "datetime" || field.type.name == "timestamp") ?
|
||||
<SelectItem key={TimeDefaultValues.NOW}>{t("db_controller.field_settings.time_default_value.now")}</SelectItem> : null
|
||||
}
|
||||
</Select>
|
||||
|
||||
{
|
||||
timeSelection.at(0) == TimeDefaultValues.CUSTOM && field.type.name != "time" &&
|
||||
<>
|
||||
<DatePicker
|
||||
aria-label="Custom datetime"
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
showMonthAndYearPickers
|
||||
radius="sm"
|
||||
value={defaultDateTime}
|
||||
|
||||
endContent={
|
||||
<Calendar className="text-icon size-4" />
|
||||
}
|
||||
onBlur={saveDefaultDateTime}
|
||||
onChange={setDefaultDateTime}
|
||||
granularity={field.type.name == "date" ? "day" : "second"}
|
||||
hideTimeZone
|
||||
classNames={{
|
||||
inputWrapper: "border-divider group-hover:border-primary",
|
||||
|
||||
}}
|
||||
calendarProps={{
|
||||
classNames: {
|
||||
title: " text-font/90"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
</>
|
||||
}
|
||||
{
|
||||
timeSelection.at(0) == TimeDefaultValues.CUSTOM && field.type.name == "time" &&
|
||||
<TimeInput aria-label="Current Time"
|
||||
ref={defaultValueRef}
|
||||
value={defaultDateTime}
|
||||
onChange={setDefaultDateTime}
|
||||
variant="bordered"
|
||||
endContent={
|
||||
<Clock className="text-icon size-4" />
|
||||
}
|
||||
classNames={{
|
||||
inputWrapper: "border-divider group-hover:border-primary",
|
||||
|
||||
}}
|
||||
onBlur={saveDefaultDateTime}
|
||||
granularity="second"
|
||||
hourCycle={24}
|
||||
hideTimeZone
|
||||
size="sm" radius="sm" />
|
||||
}
|
||||
</>
|
||||
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(fieldDefautlValue);
|
||||
+1
-1
@@ -116,7 +116,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
<KeyRound className="size-4" />
|
||||
</ToggleButton>
|
||||
|
||||
<Popover placement="bottom" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
|
||||
<Popover placement="right" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
+206
-89
@@ -1,12 +1,14 @@
|
||||
import TagInput from "@/components/tag-input/tag-input";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { Modifiers, MySQLCharset, MySQLCollation, PostgreSQLCharset, PostgreSQLCollation, SQLiteCharset, SQLiteCollation } from "@/lib/field";
|
||||
import { DataTypes, Modifiers, MySQLCharset, MySQLCollation, PostgreSQLCharset, PostgreSQLCollation, SQLiteCharset, SQLiteCollation } from "@/lib/field";
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { Button, Checkbox, Input, Select, SelectItem, SharedSelection, Switch, Textarea } from "@heroui/react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import React, { Ref, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { Button, Checkbox, Input, Select, SelectItem, SharedSelection, Switch, Textarea, Tooltip as HeroUITooltip } from "@heroui/react";
|
||||
import { CircleHelp, Trash2, TriangleAlert } from "lucide-react";
|
||||
import React, { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FieldDefaultValue from "./field-default-value";
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +17,10 @@ interface FieldSettingProps {
|
||||
field: FieldType
|
||||
}
|
||||
|
||||
|
||||
export interface ModifierValidation {
|
||||
isValid: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
|
||||
@@ -27,11 +32,27 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
const [collation, setCollation] = useState(new Set([field.collate]));
|
||||
|
||||
const [note, setNote] = useState<string | undefined>(field.note as string | undefined);
|
||||
const defaultNameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
|
||||
const maxLengthRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
const scaleRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
const precisionRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [maxLengthValidation, setMaxLengthValidation] = useState<ModifierValidation>({
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
|
||||
|
||||
const [precisionValidation, setPrecisionValidation] = useState<ModifierValidation>({
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
|
||||
const [scaleValidation, setScaleValidation] = useState<ModifierValidation>({
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
|
||||
const collations = useMemo(() => {
|
||||
if (!field.type)
|
||||
return undefined;
|
||||
@@ -59,7 +80,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
const removeField = () => {
|
||||
deleteField(field.id)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const updateFieldNote = useCallback(() => {
|
||||
editField({
|
||||
@@ -98,76 +119,64 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
|
||||
|
||||
const changeCharset = useCallback((keys: SharedSelection) => {
|
||||
|
||||
|
||||
if (keys.anchorKey != field.charset) {
|
||||
|
||||
|
||||
editField({
|
||||
id: field.id,
|
||||
charset: keys.anchorKey,
|
||||
|
||||
} as FieldInsertType);
|
||||
}
|
||||
}
|
||||
setCharset(keys as any);
|
||||
}, [field]);
|
||||
|
||||
|
||||
const changeCollation = useCallback((keys: SharedSelection) => {
|
||||
|
||||
|
||||
if (keys.anchorKey != field.collate) {
|
||||
|
||||
|
||||
editField({
|
||||
id: field.id,
|
||||
collate: keys.anchorKey,
|
||||
|
||||
} as FieldInsertType);
|
||||
}
|
||||
}
|
||||
setCollation(keys as any);
|
||||
}, [field])
|
||||
|
||||
const saveDefaultValue = useCallback(() => {
|
||||
const value: string | undefined = defaultNameRef.current?.value;
|
||||
editField({
|
||||
id: field.id,
|
||||
defaultValue: value ? String(value) : null,
|
||||
|
||||
} as FieldInsertType);
|
||||
}, [defaultNameRef, field]);
|
||||
|
||||
|
||||
const saveMaxLength = useCallback(() => {
|
||||
const value: string | undefined = maxLengthRef.current?.value;
|
||||
if (maxLengthValidation.isValid) {
|
||||
|
||||
const value: string | undefined = maxLengthRef.current?.value;
|
||||
editField({
|
||||
id: field.id,
|
||||
maxLength: value ? value : null,
|
||||
|
||||
} as FieldInsertType);
|
||||
}
|
||||
}, [maxLengthRef, maxLengthValidation, field]);
|
||||
|
||||
|
||||
const saveScaleAndPrecision = useCallback(() => {
|
||||
|
||||
const precision: number | null = Number(precisionRef.current?.value)
|
||||
const scale: number | null = Number(scaleRef.current?.value);
|
||||
|
||||
editField({
|
||||
id: field.id,
|
||||
maxLength: value ? value : null,
|
||||
|
||||
} as FieldInsertType);
|
||||
}, [maxLengthRef, field]);
|
||||
|
||||
|
||||
const savePrecision = useCallback(() => {
|
||||
const value: string | undefined = precisionRef.current?.value;
|
||||
editField({
|
||||
id: field.id,
|
||||
precision: value ? value : null,
|
||||
|
||||
} as FieldInsertType);
|
||||
}, [precisionRef, field]);
|
||||
|
||||
|
||||
const saveScale = useCallback(() => {
|
||||
const value: string | undefined = scaleRef.current?.value;
|
||||
editField({
|
||||
id: field.id,
|
||||
scale: value ? value : null,
|
||||
|
||||
} as FieldInsertType);
|
||||
}, [scaleRef, field]);
|
||||
precision: precisionValidation.isValid ? (precision > 0 ? precision : null) : undefined,
|
||||
scale: (scaleValidation.isValid && precision > 0) ? (scale > 0 ? scale : null) : null
|
||||
} as FieldInsertType)
|
||||
|
||||
}, [field, scaleRef, precisionRef, scaleValidation, precisionValidation])
|
||||
|
||||
|
||||
const updateValues = useCallback((values: string[]) => {
|
||||
const jsonValues = JSON.stringify(values);
|
||||
|
||||
|
||||
if (jsonValues != field.values)
|
||||
editField({
|
||||
id: field.id,
|
||||
@@ -182,6 +191,72 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
|
||||
|
||||
|
||||
const maxLengthChange = useCallback((value: any) => {
|
||||
if (value && value.trim().length > 0) {
|
||||
const isValid = Number.isInteger(Number(value)) && value > 0;
|
||||
|
||||
setMaxLengthValidation({
|
||||
isValid,
|
||||
errorMessage: (field.type?.type == DataTypes.INTEGER ?
|
||||
t("db_controller.field_settings.width")
|
||||
:
|
||||
t("db_controller.field_settings.max_length")) + " " + t("db_controller.field_settings.errors.max_length")
|
||||
})
|
||||
} else
|
||||
setMaxLengthValidation({
|
||||
isValid: true,
|
||||
errorMessage: undefined
|
||||
});
|
||||
|
||||
}, [maxLengthRef, field]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const validateScaleAndPrecision = useCallback(() => {
|
||||
const scale: number | null = scaleRef.current && Number(scaleRef.current?.value);
|
||||
const precision: number | null = precisionRef.current && Number(precisionRef.current?.value);
|
||||
if (precision) {
|
||||
const isValid = Number.isInteger(precision) && precision > 0;
|
||||
setPrecisionValidation({
|
||||
isValid,
|
||||
errorMessage: t("db_controller.field_settings.errors.precision")
|
||||
});
|
||||
}
|
||||
else {
|
||||
setPrecisionValidation({
|
||||
isValid: true,
|
||||
errorMessage: undefined
|
||||
});
|
||||
}
|
||||
|
||||
if (scale) {
|
||||
const isPositive = Number.isInteger(scale) && scale >= 0;
|
||||
let isLessThanPrecision: boolean = true;
|
||||
|
||||
if (precision && precision >= 0) {
|
||||
isLessThanPrecision = precision >= scale;
|
||||
}
|
||||
const errorMessage: string | undefined = !isPositive ? t("db_controller.field_settings.errors.scale") :
|
||||
((!isLessThanPrecision) ? t("db_controller.field_settings.errors.scale_max_value") : undefined)
|
||||
|
||||
setScaleValidation({
|
||||
isValid: (isPositive && isLessThanPrecision) as boolean,
|
||||
errorMessage: errorMessage
|
||||
});
|
||||
|
||||
} else {
|
||||
setScaleValidation({
|
||||
isValid: true,
|
||||
errorMessage: undefined
|
||||
});
|
||||
}
|
||||
|
||||
}, [precisionRef, scaleRef])
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-2 p-2 min-w-[260px] max-w-[260px]">
|
||||
<h3 className="font-semibold text-sm text-font/90">
|
||||
@@ -190,7 +265,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
<hr className="border-divider" />
|
||||
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="text-xs text-icon font-medium dark:text-font/90">
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
{t("db_controller.field_settings.unique")}
|
||||
</span>
|
||||
<Checkbox defaultSelected={field.unique as boolean} size="md" onValueChange={toggleUnqiue} />
|
||||
@@ -204,7 +279,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
{
|
||||
modifiers.includes(Modifiers.AUTO_INCREMENT) &&
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="text-xs text-icon font-medium dark:text-font/90">
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
{t("db_controller.field_settings.autoIncrement")}
|
||||
</span>
|
||||
<Checkbox defaultSelected={field.autoIncrement as boolean} size="md" onValueChange={toggleAutoIncrement} />
|
||||
@@ -213,7 +288,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
{
|
||||
modifiers.includes(Modifiers.UNSIGNED) &&
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="text-xs text-icon font-medium dark:text-font/90">
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
{t("db_controller.field_settings.unsigned")}
|
||||
</span>
|
||||
<Checkbox defaultSelected={field.unsigned as boolean} size="md" onValueChange={toggleUnsigned} />
|
||||
@@ -222,7 +297,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
{
|
||||
modifiers.includes(Modifiers.ZEROFILL) &&
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="text-xs text-icon font-medium dark:text-font/90">
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
{t("db_controller.field_settings.zeroFill")}
|
||||
</span>
|
||||
<Checkbox defaultSelected={field.zeroFill as boolean} size="md" onValueChange={toggleZeroFill} />
|
||||
@@ -241,16 +316,36 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
{
|
||||
modifiers.includes(Modifiers.PRECISION) &&
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<label className="text-xs font-medium text-icon dark:text-font/90">
|
||||
{t("db_controller.field_settings.precision")}
|
||||
</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<label className="flex items-center text-font/70 justify-between text-xs font-medium dark:text-font/90">
|
||||
<span>
|
||||
{t("db_controller.field_settings.precision")}
|
||||
</span>
|
||||
<CircleHelp className="size-3.5" />
|
||||
</label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.field_settings.precision_def")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Input
|
||||
|
||||
type="number"
|
||||
size="sm"
|
||||
ref={precisionRef}
|
||||
defaultValue={String(field.precision)}
|
||||
onBlur={savePrecision}
|
||||
onBlur={saveScaleAndPrecision}
|
||||
onValueChange={validateScaleAndPrecision}
|
||||
isInvalid={!precisionValidation.isValid}
|
||||
endContent={
|
||||
!precisionValidation.isValid && <>
|
||||
<HeroUITooltip showArrow={true} content={precisionValidation.errorMessage} radius="sm" color="danger">
|
||||
|
||||
<TriangleAlert className="size-4 text-danger cursor-default" />
|
||||
|
||||
</HeroUITooltip>
|
||||
</>
|
||||
}
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
placeholder={t("db_controller.field_settings.precision")}
|
||||
@@ -265,17 +360,36 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
{
|
||||
modifiers.includes(Modifiers.SCALE) &&
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<label className="text-xs font-medium text-icon dark:text-font/90">
|
||||
{t("db_controller.field_settings.scale")}
|
||||
</label>
|
||||
<Input
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<label className="flex items-center justify-between text-xs font-medium text-icon dark:text-font/90">
|
||||
<span>
|
||||
{t("db_controller.field_settings.scale")}
|
||||
</span>
|
||||
<CircleHelp className="size-3.5" />
|
||||
</label>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
{t("db_controller.field_settings.scale_def")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Input
|
||||
type="number"
|
||||
size="sm"
|
||||
|
||||
ref={scaleRef}
|
||||
defaultValue={String(field.scale)}
|
||||
onBlur={saveScale}
|
||||
onBlur={saveScaleAndPrecision}
|
||||
isDisabled={!precisionRef.current?.value || !precisionValidation.isValid}
|
||||
onValueChange={validateScaleAndPrecision}
|
||||
isInvalid={!scaleValidation.isValid}
|
||||
endContent={
|
||||
!scaleValidation.isValid && <>
|
||||
<HeroUITooltip showArrow={true} content={scaleValidation.errorMessage} radius="sm" color="danger">
|
||||
<TriangleAlert className="size-4 text-danger cursor-default" />
|
||||
</HeroUITooltip>
|
||||
</>
|
||||
}
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
placeholder={t("db_controller.field_settings.scale")}
|
||||
@@ -348,30 +462,49 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
}
|
||||
{
|
||||
modifiers.includes(Modifiers.LENGTH) && <>
|
||||
<label className="text-xs font-medium text-icon dark:text-font/90">
|
||||
{t("db_controller.field_settings.max_length")}
|
||||
<label className="text-xs font-medium text-font/70 dark:text-font/90">
|
||||
{
|
||||
field.type?.type == DataTypes.INTEGER ?
|
||||
t("db_controller.field_settings.integer_width")
|
||||
:
|
||||
t("db_controller.field_settings.max_length")
|
||||
}
|
||||
</label>
|
||||
<Input
|
||||
|
||||
type="number"
|
||||
size="sm"
|
||||
ref={maxLengthRef}
|
||||
defaultValue={String(field.maxLength)}
|
||||
onBlur={saveMaxLength}
|
||||
radius="sm"
|
||||
isInvalid={!maxLengthValidation.isValid}
|
||||
onValueChange={maxLengthChange}
|
||||
endContent={
|
||||
!maxLengthValidation.isValid && <>
|
||||
<HeroUITooltip showArrow={true} content={maxLengthValidation.errorMessage} radius="sm" color="danger">
|
||||
|
||||
<TriangleAlert className="size-4 text-danger cursor-default" />
|
||||
|
||||
</HeroUITooltip>
|
||||
</>
|
||||
}
|
||||
variant="faded"
|
||||
placeholder={t("db_controller.field_settings.length")}
|
||||
placeholder={
|
||||
field.type?.type == DataTypes.INTEGER ?
|
||||
t("db_controller.field_settings.width")
|
||||
:
|
||||
t("db_controller.field_settings.max_length")
|
||||
|
||||
}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
|
||||
}}
|
||||
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
{
|
||||
|
||||
|
||||
modifiers.includes(Modifiers.VALUES) && <>
|
||||
<h3 className="font-semibold text-sm text-font/90">
|
||||
{field.type.name != null && (field.type.name?.[0].toUpperCase() + field.type.name?.slice(1))} {t("db_controller.field_settings.values")}
|
||||
@@ -384,26 +517,10 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
<label className="text-xs font-medium text-icon dark:text-font/90">
|
||||
{t("db_controller.field_settings.default_value")}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
size="sm"
|
||||
ref={defaultNameRef}
|
||||
defaultValue={field.defaultValue as string}
|
||||
onBlur={saveDefaultValue}
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
placeholder={t("db_controller.field_settings.value")}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
|
||||
}}
|
||||
/>
|
||||
|
||||
<label className="text-xs font-medium text-icon dark:text-font/90">
|
||||
|
||||
<FieldDefaultValue field={field} />
|
||||
|
||||
<label className="text-xs font-medium text-font/70 dark:text-font/90">
|
||||
{t("db_controller.field_settings.note")}
|
||||
</label>
|
||||
<Textarea
|
||||
|
||||
@@ -206,4 +206,10 @@ button[data-testid="remove"] svg {
|
||||
|
||||
display: none ;
|
||||
|
||||
}
|
||||
|
||||
[data-invalid = "true"] div[data-slot="input-wrapper"] {
|
||||
border : 1px solid red !important ;
|
||||
background-color: transparent ;
|
||||
outline: none ;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { Modifiers } from "@/lib/field";
|
||||
import { DataTypes, Modifiers, TimeDefaultValues } from "@/lib/field";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
@@ -26,8 +26,6 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) =>
|
||||
const sortedTables: TableType[] = sortedTablesIds.map((id: string) =>
|
||||
renderableTables.find((table: TableType) => table.id == id) as TableType
|
||||
);
|
||||
|
||||
|
||||
for (const table of sortedTables) {
|
||||
dbAst.push(TableToAst(table, data_types));
|
||||
|
||||
@@ -90,7 +88,7 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
|
||||
|
||||
|
||||
let length: number | null = null;
|
||||
let scale: number | null = null;
|
||||
let scale: number | string | null = null;
|
||||
|
||||
let character_set: any | null = null;
|
||||
let collate: any | null = null;
|
||||
@@ -100,8 +98,11 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
|
||||
|
||||
let default_val: any | null = null;
|
||||
|
||||
if (modifiers.includes(Modifiers.PRECISION) && field.precision)
|
||||
if (modifiers.includes(Modifiers.PRECISION) && field.precision) {
|
||||
length = field.precision;
|
||||
if (modifiers.includes(Modifiers.SCALE) && !field.scale)
|
||||
scale = "0";
|
||||
}
|
||||
|
||||
if (modifiers.includes(Modifiers.SCALE) && field.scale)
|
||||
scale = field.scale;
|
||||
@@ -153,13 +154,31 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
|
||||
value: field.defaultValue
|
||||
}
|
||||
}
|
||||
if (field.defaultValue == "true" || field.defaultValue == "false") {
|
||||
if (field.type.type == DataTypes.TIME && field.defaultValue == TimeDefaultValues.NOW)
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: "function",
|
||||
name: {
|
||||
name: [
|
||||
{
|
||||
type: "origin",
|
||||
value: "CURRENT_TIMESTAMP"
|
||||
}
|
||||
]
|
||||
},
|
||||
over: null
|
||||
}
|
||||
}
|
||||
|
||||
else if (field.defaultValue == "true" || field.defaultValue == "false") {
|
||||
|
||||
default_val.value.type = "bool"
|
||||
default_val.value.value = Boolean(field.defaultValue);
|
||||
default_val.value.value = field.defaultValue == "true" ? true : false;
|
||||
}
|
||||
|
||||
// Check if it's a number (but not empty string or just whitespace)
|
||||
if (!isNaN(Number(field.defaultValue))) {
|
||||
else if (!isNaN(Number(field.defaultValue))) {
|
||||
default_val.value.type = "number"
|
||||
default_val.value.value = Number(field.defaultValue);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ export function fixCharsetPlacement(sql: string): string {
|
||||
const lines = sql.split('\n');
|
||||
let currentColumn = '';
|
||||
const result: string[] = [];
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
|
||||
// Handle table start/end and other non-column lines
|
||||
if (isTableStructureLine(trimmed)) {
|
||||
if (currentColumn) {
|
||||
@@ -21,7 +21,7 @@ export function fixCharsetPlacement(sql: string): string {
|
||||
result.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Handle column definitions
|
||||
if (trimmed.endsWith(',')) {
|
||||
currentColumn += ' ' + trimmed.slice(0, -1);
|
||||
@@ -31,12 +31,10 @@ export function fixCharsetPlacement(sql: string): string {
|
||||
currentColumn += ' ' + trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining column
|
||||
if (currentColumn) {
|
||||
result.push(processColumn(currentColumn));
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
@@ -44,11 +42,11 @@ export function fixCharsetPlacement(sql: string): string {
|
||||
* Checks if a line is part of table structure (not a column definition)
|
||||
*/
|
||||
function isTableStructureLine(line: string): boolean {
|
||||
return line.startsWith('CREATE TABLE') ||
|
||||
line === '(' ||
|
||||
line === ')' ||
|
||||
line.endsWith('(') ||
|
||||
line.endsWith(')');
|
||||
return line.startsWith('CREATE TABLE') ||
|
||||
line === '(' ||
|
||||
line === ')' ||
|
||||
line.endsWith('(') ||
|
||||
line.endsWith(')');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,18 +60,18 @@ function processColumn(columnDef: string): string {
|
||||
|
||||
const [_, colName, dataType] = typeMatch;
|
||||
const rest = columnDef.slice(typeMatch[0].length);
|
||||
|
||||
|
||||
// Extract charset and collate
|
||||
const charsetMatch = rest.match(/CHARACTER\s+SET\s+\S+/i);
|
||||
const collateMatch = rest.match(/COLLATE\s+\S+/i);
|
||||
|
||||
|
||||
// Clean the remaining attributes
|
||||
const cleanRest = rest
|
||||
.replace(/CHARACTER\s+SET\s+\S+/gi, '')
|
||||
.replace(/COLLATE\s+\S+/gi, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
|
||||
// Reconstruct in correct order
|
||||
let reconstructed = `${colName} ${dataType}`;
|
||||
if (charsetMatch) reconstructed += ` ${charsetMatch[0]}`;
|
||||
|
||||
+1
-2
@@ -64,8 +64,7 @@ function groupBy(array: any[], key: string) {
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export {
|
||||
areArraysEqual,
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ export default {
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form|modal|toast).js",
|
||||
|
||||
|
||||
|
||||
],
|
||||
darkMode: "class",
|
||||
|
||||
|
||||
Reference in New Issue
Block a user