diff --git a/package.json b/package.json index 0d0d461..d57e986 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/i18/languages/en.ts b/src/i18/languages/en.ts index fdbb057..6d84e0c 100644 --- a/src/i18/languages/en.ts +++ b/src/i18/languages/en.ts @@ -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", diff --git a/src/lib/field.ts b/src/lib/field.ts index 9a4ffbd..48010b0 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -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" } \ No newline at end of file diff --git a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx index 59f7dcf..a2a19cc 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx @@ -16,7 +16,6 @@ interface RelationshipAccordionBodyProps { const RelationshipAccordionBody: React.FC = ({ relationship }) => { const [cardinality, setCardinality] = useState(new Set([relationship.cardinality])); const { editRelationship, deleteRelationship } = useDatabaseOperations(); - const { t } = useTranslation(); const changeCardinality = (keys: SharedSelection) => { diff --git a/src/pages/database/db-controller/sql-preview.tsx b/src/pages/database/db-controller/sql-preview.tsx index b3ad2d8..c77ca25 100644 --- a/src/pages/database/db-controller/sql-preview.tsx +++ b/src/pages/database/db-controller/sql-preview.tsx @@ -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 ( diff --git a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-default-value.tsx b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-default-value.tsx new file mode 100644 index 0000000..b92fa81 --- /dev/null +++ b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-default-value.tsx @@ -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 = ({ field }) => { + const { editField } = useDatabaseOperations(); + + const { t } = useTranslation(); + const [defaultValueValidation, setDefaultValueValidation] = useState({ + isValid: true, + }); + + const defaultValueRef: Ref = useRef(null); + + const [timeSelection, setTimeSelection] = useState(() => { + + 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(() => { + 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 && + + } + { + (defaultValueType.number || defaultValueType.string) && + + + + + + + } + 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 && +
+ + + {t("db_controller.field_settings.default_value")} + + +
+ } + { + defaultValueType.time && + <> + + + { + timeSelection.at(0) == TimeDefaultValues.CUSTOM && field.type.name != "time" && + <> + + } + 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" && + + } + classNames={{ + inputWrapper: "border-divider group-hover:border-primary", + + }} + onBlur={saveDefaultDateTime} + granularity="second" + hourCycle={24} + hideTimeZone + size="sm" radius="sm" /> + } + + + } + + + ) +} + + +export default React.memo(fieldDefautlValue); \ No newline at end of file diff --git a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx index 324605e..65b9a61 100644 --- a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx +++ b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx @@ -116,7 +116,7 @@ const FieldItem: React.FC = ({ field }) => { - +