mirror of
https://github.com/stackrender/stackrender.git
synced 2026-10-25 13:33:44 +00:00
Implement data validation, add identifier quoting to the generated SQL & fix prefered language lost after refreching
This commit is contained in:
@@ -19,7 +19,6 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
|
||||
|
||||
|
||||
interface ComboboxProps {
|
||||
items: any[]
|
||||
@@ -28,13 +27,14 @@ interface ComboboxProps {
|
||||
selectedItem?: string;
|
||||
onSelectionChange?: (id: string) => void;
|
||||
className?: string;
|
||||
isDisabled?: boolean
|
||||
isDisabled?: boolean;
|
||||
valueLength?: number
|
||||
}
|
||||
|
||||
|
||||
export const Combobox = (props: ComboboxProps) => {
|
||||
|
||||
const { items, label = "label", placeholder, onSelectionChange, selectedItem, className, isDisabled } = props;
|
||||
const { items, label = "label", placeholder, onSelectionChange, selectedItem, className, isDisabled , valueLength } = props;
|
||||
const [query, setQuery] = React.useState<string>("");
|
||||
|
||||
const [open, setOpen] = React.useState(false)
|
||||
@@ -51,10 +51,10 @@ export const Combobox = (props: ComboboxProps) => {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open)
|
||||
setQuery("");
|
||||
setQuery("");
|
||||
}, [open])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild disabled={isDisabled}>
|
||||
@@ -64,10 +64,10 @@ export const Combobox = (props: ComboboxProps) => {
|
||||
aria-expanded={open}
|
||||
className={cn("justify-between", className)}
|
||||
>
|
||||
<label className="truncate">
|
||||
<label className="truncate ">
|
||||
{
|
||||
value
|
||||
? items.find((item) => item.id === value)?.[label]
|
||||
? (items.find((item) => item.id === value)?.[label] as string)?.substring(0 , valueLength)
|
||||
: (placeholder ? placeholder : "Select items...")}
|
||||
</label>
|
||||
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
|
||||
@@ -85,7 +85,7 @@ export function CommandMenu() {
|
||||
</CommandGroup>
|
||||
))}
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading='Theme'>
|
||||
<CommandGroup heading={t("menu.theme")}>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
|
||||
<IconSun /> <span>{t("menu.light")}</span>
|
||||
</CommandItem>
|
||||
|
||||
@@ -11,33 +11,44 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
||||
interface DatePickerProps {
|
||||
value?: Date | undefined;
|
||||
onValueChange?: (value?: Date) => void
|
||||
onValueChange?: (value?: Date) => void;
|
||||
noLabel?: boolean;
|
||||
className?: string;
|
||||
placeholder?: string ;
|
||||
}
|
||||
|
||||
const DatePicker: React.FC<DatePickerProps> = ({value, onValueChange}) => {
|
||||
const DatePicker: React.FC<DatePickerProps> = ({ value, onValueChange, noLabel = false, className , placeholder = "Pick a date"}) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [date, setDate] = React.useState<Date | undefined>(value)
|
||||
|
||||
React.useEffect(() => {
|
||||
setDate (value) ;
|
||||
} , [ value])
|
||||
|
||||
React.useEffect(() => {
|
||||
onValueChange && onValueChange(date)
|
||||
}, [date])
|
||||
}, [date])
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="date" className="px-1">
|
||||
Date of birth
|
||||
</Label>
|
||||
{
|
||||
!noLabel &&
|
||||
<Label htmlFor="date" className="px-1">
|
||||
{placeholder}
|
||||
</Label>
|
||||
}
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-full justify-between font-normal"
|
||||
className={cn("w-full justify-between font-normal" , className)}
|
||||
>
|
||||
{date ? date.toLocaleDateString() : "Select date"}
|
||||
{date ? date.toLocaleDateString() : placeholder }
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -18,8 +18,8 @@ import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
|
||||
// Custom context providers and hooks
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useTableToNode } from "@/hooks/use-table-to-node";
|
||||
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
|
||||
import { useTableToNode } from "@/features/database/hooks/use-table-to-node";
|
||||
import { useRelationshipToEdge } from "@/features/database/hooks/use-relationship-to-edge";
|
||||
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
|
||||
|
||||
// Utils and constants
|
||||
@@ -33,9 +33,9 @@ import { AlertTriangle, Menu } from "lucide-react";
|
||||
import { adjustTablesPositions } from "@/utils/tables";
|
||||
//import DatabaseControlButtons from "./database-control-buttons";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
|
||||
import useHighlightedEdges from "@/features/database/hooks/use-highlighted-edges";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useOverlappingTables from "@/hooks/use-overlapping-tables";
|
||||
import useOverlappingTables from "@/features/database/hooks/use-overlapping-tables";
|
||||
|
||||
|
||||
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
|
||||
@@ -73,7 +73,7 @@ const DatabaseDiagram: React.FC = () => {
|
||||
|
||||
// Diagram-related state (e.g. connection in progress)
|
||||
const { setIsConnectionInProgress, cardinalityStyle, showController, openController } = useDiagramOps();
|
||||
const raise = useToast() ;
|
||||
const raise = useToast();
|
||||
// Destructure tables and relationships from database
|
||||
const { tables, relationships } = database || { tables: [], relationships: [] };
|
||||
|
||||
@@ -146,10 +146,10 @@ const DatabaseDiagram: React.FC = () => {
|
||||
} else {
|
||||
|
||||
|
||||
raise(
|
||||
raise(
|
||||
t("db_controller.invalid_relationship.title"),
|
||||
t("db_controller.invalid_relationship.description"),
|
||||
"ERROR"
|
||||
"ERROR"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ const DatabaseDiagram: React.FC = () => {
|
||||
|
||||
|
||||
// Convert tables and relationships into flow elements
|
||||
useTableToNode(tables);
|
||||
useTableToNode(tables, database?.dialect);
|
||||
useRelationshipToEdge(relationships);
|
||||
useHighlightedEdges(nodes, relationships, edges);
|
||||
const { isOverlapping, puls } = useOverlappingTables(tables);
|
||||
|
||||
@@ -122,7 +122,7 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
>
|
||||
{relationships.map((relationship: RelationshipType) => (
|
||||
<>
|
||||
<RelationshipAccordionItem relationship={relationship} key={relationship.id} />
|
||||
<RelationshipAccordionItem relationship={relationship} key={relationship.id} dialect={database?.dialect}/>
|
||||
<Separator className="my-1" />
|
||||
</>
|
||||
))}
|
||||
|
||||
+3
-3
@@ -79,8 +79,8 @@ const RelationshipAccordionContent: React.FC<RelationshipAccordionContentProps>
|
||||
|
||||
return (
|
||||
<div className="w-full p-2 space-y-4">
|
||||
<div className="flex">
|
||||
<div className="w-full space-y-1">
|
||||
<div className="flex gap-2">
|
||||
<div className="space-y-1 w-full truncate">
|
||||
<Label >
|
||||
{t("db_controller.source_table")}
|
||||
</Label>
|
||||
@@ -96,7 +96,7 @@ const RelationshipAccordionContent: React.FC<RelationshipAccordionContentProps>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<div className="space-y-1 w-full truncate">
|
||||
<Label >
|
||||
{t("db_controller.referenced_table")}
|
||||
</Label>
|
||||
|
||||
+6
-3
@@ -6,17 +6,20 @@ import {
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import RelationshipAccordionTrigger from "./relationship-accordion-trigger";
|
||||
import RelationshipAccordionContent from "./relationship-accordion-content";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
|
||||
interface RelationshipAccordionItemProps {
|
||||
relationship: RelationshipType
|
||||
relationship: RelationshipType ,
|
||||
dialect? : DatabaseDialect
|
||||
|
||||
}
|
||||
|
||||
|
||||
const RelationshipAccordionItem: React.FC<RelationshipAccordionItemProps> = ({ relationship }) => {
|
||||
const RelationshipAccordionItem: React.FC<RelationshipAccordionItemProps> = ({ relationship , dialect }) => {
|
||||
|
||||
return (
|
||||
<AccordionItem value={relationship.id} className="border-none" id={relationship.id}>
|
||||
<RelationshipAccordionTrigger relationship={relationship} />
|
||||
<RelationshipAccordionTrigger relationship={relationship} dialect = { dialect} />
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<RelationshipAccordionContent
|
||||
relationship={relationship}
|
||||
|
||||
+46
-11
@@ -9,17 +9,22 @@ import hash from "object-hash";
|
||||
import { getDefaultRelationshipName } from "@/utils/relationship";
|
||||
import { AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { IconCheck, IconDotsVertical, IconFocus2, IconPencil, IconTrash } from "@tabler/icons-react";
|
||||
import { IconAlertTriangle, IconCheck, IconDotsVertical, IconFocus2, IconPencil, IconTrash } from "@tabler/icons-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { useDatabaseInvalidIdentifier } from "@/features/database/hooks/use-invalid-identifier";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
|
||||
interface RelationshipAccordionTriggerProps {
|
||||
isOpen?: boolean;
|
||||
relationship: RelationshipType
|
||||
relationship: RelationshipType ,
|
||||
dialect?: DatabaseDialect
|
||||
}
|
||||
|
||||
const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps> = ({ isOpen, relationship }) => {
|
||||
const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps> = ({ isOpen, relationship , dialect }) => {
|
||||
const defaultName: string = useMemo(() => {
|
||||
return getDefaultRelationshipName(relationship);
|
||||
}, [relationship])
|
||||
@@ -30,20 +35,27 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
|
||||
const { editRelationship, deleteRelationship } = useDatabaseOperations();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName);
|
||||
const { focusOnRelationship } = useDiagramOps();
|
||||
|
||||
|
||||
const { identifierError , showIdentifierError, identifierMaxLength } = useDatabaseInvalidIdentifier(name, dialect, "constraint");
|
||||
|
||||
const editRelationshipName = () => {
|
||||
|
||||
|
||||
if (identifierError == "empty") {
|
||||
setName(relationship.name ? relationship.name : defaultName);
|
||||
setEditMode(false);
|
||||
return;
|
||||
}
|
||||
if (relationship.name || name.trim().toLocaleLowerCase() != defaultName)
|
||||
editRelationship({
|
||||
id: relationship.id,
|
||||
name
|
||||
name : name.trim()
|
||||
} as RelationshipInsertType);
|
||||
|
||||
setEditMode(false);
|
||||
}
|
||||
|
||||
const onDeleteRelationship = () => {
|
||||
deleteRelationship(relationship.id);
|
||||
|
||||
@@ -60,7 +72,9 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
|
||||
<>
|
||||
<div className="w-full flex">
|
||||
<label
|
||||
className="py-2 !truncate text-sm cursor-pointer max-w-80"
|
||||
className={cn("py-2 truncate text-sm cursor-pointer max-w-50 ", {
|
||||
"text-destructive": identifierError != null
|
||||
})}
|
||||
>
|
||||
{relationship.name ? relationship.name : defaultName}
|
||||
</label>
|
||||
@@ -79,11 +93,28 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
|
||||
<IconFocus2 className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
identifierError &&
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant={"ghost"} size="icon" className="size-8 shrink-0 hover:bg-destructive/10 hover:border-destructive/30 dark:hover:bg-destructive/10 dark:hover:border-destructive/30" onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
showIdentifierError();
|
||||
setEditMode(true);
|
||||
}} >
|
||||
<IconAlertTriangle className="size-4 text-destructive " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-destructive fill-destructive [&_svg]:bg-destructive [&_svg]:fill-destructive">
|
||||
{t("db_controller.validation.error_message")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{
|
||||
editMode && <>
|
||||
<Input
|
||||
<Input
|
||||
placeholder={"Relationship name"}
|
||||
value={name}
|
||||
onChange={(event: any) => setName(event.target.value)}
|
||||
@@ -91,8 +122,12 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
|
||||
autoFocus
|
||||
type="text"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className="h-8"
|
||||
onKeyDown={(e: any) => {
|
||||
aria-invalid={identifierError != null}
|
||||
className={cn("h-8", {
|
||||
"text-destructive": identifierError != null
|
||||
})}
|
||||
maxLength={identifierMaxLength}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
editRelationshipName();
|
||||
|
||||
@@ -9,7 +9,7 @@ import { v4 } from "uuid";
|
||||
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import SqlPreview from "../sql-preview";
|
||||
import EmptyList from "@/components/empty-list";
|
||||
import EmptyList from "@/components/empty-list";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { IconListDetails, IconPlus } from "@tabler/icons-react";
|
||||
@@ -38,9 +38,9 @@ const TablesController: React.FC = ({ }) => {
|
||||
const { tables: allTables } = database || { tables: [] };
|
||||
const [tables, setTables] = useState<TableType[]>(allTables);
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
|
||||
const { focusedTableId , setFocusedTableId} = useDiagram();
|
||||
const { focusedTableId, setFocusedTableId } = useDiagram();
|
||||
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
@@ -100,7 +100,7 @@ const TablesController: React.FC = ({ }) => {
|
||||
}, [database, tables, getViewport, getInteger]);
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedTableId) {
|
||||
if (focusedTableId) {
|
||||
const accordionItem = document.getElementById(focusedTableId)
|
||||
if (accordionItem)
|
||||
accordionItem?.scrollIntoView({
|
||||
@@ -177,7 +177,7 @@ const TablesController: React.FC = ({ }) => {
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full"
|
||||
value={focusedTableId}
|
||||
value={focusedTableId}
|
||||
onValueChange={setFocusedTableId}
|
||||
>
|
||||
<DndContext
|
||||
@@ -193,7 +193,7 @@ const TablesController: React.FC = ({ }) => {
|
||||
|
||||
{tables.map((table: TableType) => (
|
||||
<>
|
||||
<TableAccordionItem table={table} key={table.id} />
|
||||
<TableAccordionItem table={table} key={table.id} dialect={database?.dialect} />
|
||||
<Separator className="my-1" />
|
||||
</>
|
||||
))}
|
||||
|
||||
+8
-9
@@ -277,6 +277,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
ref={defaultValueRef}
|
||||
aria-invalid={!defaultValueValidation.isValid}
|
||||
onChange={defaultValueChange}
|
||||
maxLength={255}
|
||||
defaultValue={field.defaultValue as string}
|
||||
onBlur={saveDefaultValue as any}
|
||||
placeholder={t("db_controller.field_settings.value")}
|
||||
@@ -307,14 +308,12 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
{
|
||||
|
||||
(defaultValueType.select) &&
|
||||
|
||||
<Select
|
||||
aria-label="value"
|
||||
value={selectedValues as any}
|
||||
onValueChange={enumValueChange as any}
|
||||
|
||||
>
|
||||
<SelectTrigger id="charset" className="w-full flex ">
|
||||
<SelectTrigger className="w-full flex ">
|
||||
<SelectValue placeholder={t('db_controller.field_settings.pick_value')} />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -322,13 +321,11 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
<SelectItem value={"none"} >
|
||||
{t("db_controller.field_settings.no_default")}
|
||||
</SelectItem>
|
||||
|
||||
{
|
||||
values.map((value: string) => (
|
||||
<SelectItem
|
||||
key={value}
|
||||
value={value}
|
||||
>
|
||||
value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))
|
||||
@@ -347,7 +344,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
onValueChange={enumValueChange as any}
|
||||
|
||||
>
|
||||
<SelectTrigger id="charset" className="w-full flex ">
|
||||
<SelectTrigger className="w-full flex ">
|
||||
<SelectValue placeholder={t('db_controller.field_settings.pick_value')} />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -372,7 +369,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
}
|
||||
onValueChange={enumValueChange}
|
||||
defaultValue={selectedValues as any}
|
||||
placeholder="Select fields..."
|
||||
placeholder={t("db_controller.field_settings.multi_select_placeholder")}
|
||||
variant={"secondary"}
|
||||
hideSelectAll
|
||||
/>
|
||||
@@ -408,6 +405,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
<DatePicker
|
||||
value={defaultDateTime}
|
||||
onValueChange={saveDefaultDateTime}
|
||||
placeholder={t("db_controller.field_settings.date_placeholder")}
|
||||
/>
|
||||
|
||||
}
|
||||
@@ -415,7 +413,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
timeSelection == TimeDefaultValues.CUSTOM && field.type.name == "time" &&
|
||||
<>
|
||||
<Label htmlFor="time">
|
||||
Time
|
||||
{t("db_controller.field_settings.time")}
|
||||
</Label>
|
||||
<Input
|
||||
id="time"
|
||||
@@ -424,6 +422,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
defaultValue={defaultDateTime}
|
||||
onChange={(event) => setDefaultDateTime(event.target.value)}
|
||||
onBlur={saveDefaultDateTime as any}
|
||||
maxLength={255}
|
||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</>
|
||||
|
||||
+67
-21
@@ -5,11 +5,11 @@ import { Settings2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { Key, useEffect, useState } from "react";
|
||||
import { Key, useEffect, useRef, useState } from "react";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import FieldSetting from "./field-setting";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { IconGripVertical, IconKey, IconKeyframe } from "@tabler/icons-react";
|
||||
import { IconAlertTriangle, IconGripVertical, IconKey, IconKeyframe } from "@tabler/icons-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Combobox } from "@/components/combobox";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
@@ -17,14 +17,19 @@ import { Toggle } from "@/components/ui/toggle";
|
||||
import { TooltipTrigger, Tooltip, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { useDatabaseInvalidIdentifier } from "@/features/database/hooks/use-invalid-identifier";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
field: FieldType
|
||||
field: FieldType;
|
||||
dialect?: DatabaseDialect
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
const FieldItem: React.FC<Props> = ({ field, dialect }) => {
|
||||
|
||||
const [fieldName, setFieldName] = useState<string>(field.name);
|
||||
|
||||
@@ -34,6 +39,9 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
const [selectedType, setSelectedType] = useState<string | undefined>(field.typeId as string | undefined);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { identifierError, showIdentifierError, identifierMaxLength } = useDatabaseInvalidIdentifier(fieldName, dialect, "column");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform } = useSortable({ id: field.id });
|
||||
|
||||
const style = {
|
||||
@@ -49,10 +57,17 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
}, [field.typeId])
|
||||
|
||||
|
||||
|
||||
|
||||
const saveFieldName = () => {
|
||||
|
||||
if (identifierError == "empty") {
|
||||
setFieldName(field.name);
|
||||
return;
|
||||
}
|
||||
editField({
|
||||
id: field.id,
|
||||
name: fieldName
|
||||
name: fieldName.trim()
|
||||
} as FieldType);
|
||||
}
|
||||
const updateFieldType = (key: Key | null) => {
|
||||
@@ -93,28 +108,59 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
<IconGripVertical className="size-4 text-muted-foreground hover:text-foreground cursor-move shrink-0" />
|
||||
</div>
|
||||
<div className="flex gap-2 w-full">
|
||||
<Input
|
||||
aria-label={t("db_controller.name")}
|
||||
placeholder={t("db_controller.name")}
|
||||
value={fieldName}
|
||||
onChange={(event: any) => setFieldName(event.target.value)}
|
||||
onBlur={saveFieldName}
|
||||
className=" flex flex-1 !bg-transparent"
|
||||
onKeyDown={(e : any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
saveFieldName() ;
|
||||
e.target.blur() ;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-1 relative items-center min-w-0 ">
|
||||
|
||||
<Input
|
||||
aria-label={t("db_controller.name")}
|
||||
placeholder={t("db_controller.name")}
|
||||
value={fieldName}
|
||||
onChange={(event: any) => setFieldName(event.target.value)}
|
||||
onBlur={saveFieldName}
|
||||
maxLength={identifierMaxLength}
|
||||
aria-invalid={identifierError != null}
|
||||
ref={inputRef}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
saveFieldName();
|
||||
e.target.blur();
|
||||
}
|
||||
}}
|
||||
className={cn("flex flex-1 !bg-transparent !opacity-100", {
|
||||
"text-destructive pr-10": identifierError != null
|
||||
})}
|
||||
/>
|
||||
{
|
||||
identifierError &&
|
||||
<Tooltip >
|
||||
<TooltipTrigger asChild className="absolute">
|
||||
<Button variant={"ghost"} size="icon"
|
||||
className="size-7 shrink-0 hover:bg-destructive/10 dark:hover:bg-destructive/10 right-1 bg-background rounded-sm"
|
||||
onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
showIdentifierError();
|
||||
inputRef.current?.focus();
|
||||
}}>
|
||||
<IconAlertTriangle className="size-4 text-destructive " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-destructive fill-destructive [&_svg]:bg-destructive [&_svg]:fill-destructive">
|
||||
{t("db_controller.validation.error_message")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
<Combobox
|
||||
items={data_types}
|
||||
label="name"
|
||||
placeholder={t("db_controller.type")}
|
||||
selectedItem={selectedType}
|
||||
|
||||
onSelectionChange={updateFieldType}
|
||||
className="flex flex-1 !bg-transparent !font-normal min-w-0"
|
||||
className={cn("flex flex-1 !bg-transparent !font-normal !opacity-100 min-w-0 w-full ", {
|
||||
"text-destructive border-destructive hover:text-destructive": !selectedType
|
||||
})}
|
||||
valueLength={20}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 ml-2 ">
|
||||
|
||||
+5
-4
@@ -11,20 +11,21 @@ import React, { useEffect, useState } from "react";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
|
||||
import hash from "object-hash" ;
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
|
||||
interface Props {
|
||||
tableFields: FieldType[] ;
|
||||
tableId : string ;
|
||||
tableId : string ;
|
||||
dialect?: DatabaseDialect
|
||||
}
|
||||
|
||||
|
||||
const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
|
||||
const FieldList: React.FC<Props> = ({ tableFields , tableId , dialect}) => {
|
||||
|
||||
const [fields, setFields] = useState<FieldType[]>(tableFields);
|
||||
const { orderTableFields } = useDatabaseOperations();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setFields(tableFields)
|
||||
}, [tableFields])
|
||||
|
||||
@@ -65,7 +66,7 @@ const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
|
||||
>
|
||||
{
|
||||
fields.map((field: FieldType) => (
|
||||
<FieldItem field={field} key={field.id} />
|
||||
<FieldItem field={field} key={field.id} dialect={dialect}/>
|
||||
|
||||
))
|
||||
}
|
||||
|
||||
+5
-7
@@ -273,7 +273,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-2 max-w-[260px]">
|
||||
<div className="w-full flex flex-col gap-2 max-w-[260px]">
|
||||
<h3 className="font-medium text-sm">
|
||||
{t("db_controller.field_settings.title")}
|
||||
</h3>
|
||||
@@ -366,8 +366,6 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
{
|
||||
modifiers.includes(Modifiers.SCALE) &&
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="flex items-center justify-between font-medium" htmlFor="scale">
|
||||
@@ -438,7 +436,8 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"none"} >
|
||||
No charset
|
||||
{t("db_controller.field_settings.no_charset")}
|
||||
|
||||
</SelectItem>
|
||||
|
||||
{
|
||||
@@ -464,7 +463,6 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
aria-label="collation"
|
||||
value={collation as any}
|
||||
onValueChange={changeCollation as any}
|
||||
|
||||
>
|
||||
<SelectTrigger id="collation" className="w-full">
|
||||
<SelectValue placeholder={t("db_controller.field_settings.collation")} />
|
||||
@@ -472,7 +470,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
<SelectContent>
|
||||
|
||||
<SelectItem value={"none"} >
|
||||
No collation
|
||||
{t("db_controller.field_settings.no_collation")}
|
||||
</SelectItem>
|
||||
{
|
||||
Object.values(collations).map((collation: string) => (<SelectItem
|
||||
@@ -530,7 +528,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
<InputTags
|
||||
value={jsonValues}
|
||||
onChange={updateValues as any}
|
||||
placeholder="Enter values, comma separated..."
|
||||
placeholder={t("db_controller.field_settings.input_tag_placeholder")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
+77
-19
@@ -5,14 +5,18 @@ import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useDatabaseInvalidIdentifier } from "@/features/database/hooks/use-invalid-identifier";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
|
||||
import { IndexInsertType, IndexType } from "@/lib/schemas/index-schema";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { areArraysEqual } from "@/utils/utils";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { IconAlertTriangle, IconTrash } from "@tabler/icons-react";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
@@ -21,19 +25,30 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
index: IndexType;
|
||||
fields: FieldType[]
|
||||
fields: FieldType[];
|
||||
dialect?: DatabaseDialect
|
||||
|
||||
}
|
||||
|
||||
|
||||
const IndexItem: React.FC<Props> = ({ index, fields }) => {
|
||||
const IndexItem: React.FC<Props> = ({ index, fields, dialect }) => {
|
||||
const { t } = useTranslation();
|
||||
const { editIndex, deleteIndex, editFieldIndices } = useDatabaseOperations();
|
||||
const [isUnique, setIsUnique] = useState<boolean>(index.unique as boolean);
|
||||
|
||||
const [fieldIndices, setFieldIndices] = useState<string[]>([]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [indexName, setIndexName] = useState<string>(index.name);
|
||||
const { identifierError, showIdentifierError, identifierMaxLength } = useDatabaseInvalidIdentifier(indexName, dialect, "index");
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setIndexName(index.name);
|
||||
}, [index.name]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setFieldIndices(
|
||||
@@ -52,10 +67,15 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
|
||||
unique
|
||||
} as IndexInsertType);
|
||||
}
|
||||
const editIndexName = (event: any) => {
|
||||
const editIndexName = () => {
|
||||
|
||||
if (identifierError == "empty") {
|
||||
setIndexName(index.name);
|
||||
return;
|
||||
}
|
||||
editIndex({
|
||||
id: index.id,
|
||||
name: event.target.value
|
||||
name: indexName
|
||||
} as IndexInsertType);
|
||||
}
|
||||
const removeIndex = () => {
|
||||
@@ -63,9 +83,15 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
|
||||
}
|
||||
|
||||
const updateFieldIndices = useCallback((fieldIds: string[]) => {
|
||||
|
||||
const previousFieldids: string[] = index.fieldIndices.map((fieldIndex: FieldIndexType) => fieldIndex.fieldId);
|
||||
const deletedFieldIndicesIds: string[] = index.fieldIndices.filter((fieldIndex: FieldIndexType) => !fieldIds.includes(fieldIndex.fieldId)).map((fieldIndex: FieldIndexType) => fieldIndex.id);
|
||||
const createFieldIndices: string[] = fieldIds.filter((fieldId: string) => !previousFieldids.includes(fieldId))
|
||||
|
||||
setFieldIndices(fieldIds);
|
||||
if (!areArraysEqual(fieldIds, index.fieldIndices.map((fieldIndex: FieldIndexType) => fieldIndex.fieldId)))
|
||||
editFieldIndices(index.id, fieldIds);
|
||||
editFieldIndices(index.id, createFieldIndices, deletedFieldIndicesIds);
|
||||
|
||||
}, [index.fieldIndices])
|
||||
|
||||
return (
|
||||
@@ -90,12 +116,15 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
|
||||
<Button
|
||||
size="icon"
|
||||
variant={"ghost"}
|
||||
className="dark:bg-card dark:border-none size-9 text-muted-foreground hover:text-foreground"
|
||||
className="dark:bg-card dark:border-none size-9 text-muted-foreground hover:text-foreground relative"
|
||||
>
|
||||
<Settings2 className="size-4 " />
|
||||
{
|
||||
identifierError && <span className="size-1.5 bg-destructive rounded-md absolute bottom-1.5 right-1.5"></span>
|
||||
}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" className="max-w-[210px]">
|
||||
<PopoverContent side="right" className="max-w-[224px]">
|
||||
<div className="w-full flex flex-col gap-2 ">
|
||||
<h3 className="font-medium text-sm ">
|
||||
{t("db_controller.index_setting")}
|
||||
@@ -115,15 +144,44 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
|
||||
{t("db_controller.name")}
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
id="name"
|
||||
aria-label={t("db_controller.index_name")}
|
||||
placeholder={t("db_controller.index_name")}
|
||||
onBlur={editIndexName}
|
||||
autoFocus
|
||||
defaultValue={index.name}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
|
||||
<Input
|
||||
id="name"
|
||||
aria-label={t("db_controller.index_name")}
|
||||
placeholder={t("db_controller.index_name")}
|
||||
onBlur={editIndexName}
|
||||
onChange={(event: any) => setIndexName(event.target.value)}
|
||||
ref={inputRef}
|
||||
aria-invalid={identifierError != null}
|
||||
autoFocus
|
||||
defaultValue={indexName}
|
||||
value={indexName}
|
||||
maxLength={identifierMaxLength}
|
||||
className={cn({
|
||||
"text-destructive pr-10": identifierError != null
|
||||
})}
|
||||
/>
|
||||
{
|
||||
identifierError &&
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild className="absolute">
|
||||
<Button variant={"ghost"} size="icon"
|
||||
className="size-7 shrink-0 hover:bg-destructive/10 dark:hover:bg-destructive/10 right-5.5 bg-background rounded-sm"
|
||||
onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
showIdentifierError();
|
||||
inputRef.current?.focus();
|
||||
}}>
|
||||
<IconAlertTriangle className="size-4 text-destructive " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-destructive fill-destructive [&_svg]:bg-destructive [&_svg]:fill-destructive">
|
||||
{t("db_controller.validation.error_message")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
<Separator />
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -138,7 +196,7 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+17
-8
@@ -8,38 +8,47 @@ import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { v4 } from "uuid";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface IndexesListProps {
|
||||
indices: IndexType[],
|
||||
fields: FieldType[],
|
||||
tableId: string
|
||||
tableId: string,
|
||||
table: TableType,
|
||||
dialect?: DatabaseDialect
|
||||
}
|
||||
|
||||
|
||||
|
||||
const IndexesList: React.FC<IndexesListProps> = ({ indices, fields, tableId }) => {
|
||||
|
||||
const IndexesList: React.FC<IndexesListProps> = ({ indices, fields, tableId, table, dialect }) => {
|
||||
|
||||
const { createIndex } = useDatabaseOperations();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const addIndex = (event: any) => {
|
||||
|
||||
event.stopPropagation && event.stopPropagation();
|
||||
createIndex({
|
||||
id: v4(),
|
||||
name: `index_${indices.length + 1}`,
|
||||
name: `${table.name}_index_${indices.length + 1}`,
|
||||
unique: false,
|
||||
tableId: tableId
|
||||
} as IndexInsertType);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex-col space-y-2 ">
|
||||
{
|
||||
indices.map((index: IndexType) => <IndexItem index={index} fields={fields} key={index.id} />)
|
||||
indices.map((index: IndexType) => <IndexItem index={index} fields={fields} key={index.id} dialect={dialect} />)
|
||||
}
|
||||
{
|
||||
indices.length == 0 && <p className="text-muted-foreground py-1">
|
||||
No Indices found <span className="text-foreground hover:underline cursor-pointer" onClick={addIndex}>
|
||||
create new one <IconPlus className="size-3.5 shrink-0 inline"/>
|
||||
</span>
|
||||
{t("db_controller.no_index")} { <span className="text-foreground hover:underline cursor-pointer" onClick={addIndex}>
|
||||
{t("db_controller.create_new_index")} <IconPlus className="size-3.5 shrink-0 inline" />
|
||||
</span>}
|
||||
</p>
|
||||
}
|
||||
|
||||
|
||||
+17
-17
@@ -4,25 +4,27 @@ import { useTranslation } from "react-i18next";
|
||||
import FieldList from "./field/field-list";
|
||||
import IndexesList from "./index/indexes-list";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { getNextSequence } from "@/utils/field";
|
||||
import { v4 } from "uuid";
|
||||
import { IndexInsertType } from "@/lib/schemas/index-schema";
|
||||
import { FieldInsertType } from "@/lib/schemas/field-schema";
|
||||
import { AccordionContent, Accordion, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { IconBolt , IconFolder, IconMessageCircle, } from "@tabler/icons-react";
|
||||
import { IconBolt, IconFolder, IconMessageCircle, } from "@tabler/icons-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ColorPicker from "@/components/color-picker";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
|
||||
|
||||
|
||||
export interface TableAccordionBodyProps {
|
||||
table: TableType,
|
||||
keys?: string[];
|
||||
dialect?: DatabaseDialect
|
||||
}
|
||||
|
||||
const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys }) => {
|
||||
const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys , dialect}) => {
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>(["fields"]);
|
||||
const [note, setNote] = useState<string>(table.note ? table.note : "");
|
||||
@@ -79,17 +81,16 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
|
||||
setSelectedItems([...selectedItems, "indexes"])
|
||||
}, [table.indices.length])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<AccordionContent className=" p-0 ">
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="w-full"
|
||||
value={selectedItems}
|
||||
|
||||
onValueChange={setSelectedItems as any}
|
||||
>
|
||||
|
||||
|
||||
<AccordionItem value="fields" className="border-none">
|
||||
<AccordionTrigger className="py-1 rounded-none text-muted-foreground pr-1.5" position="right" >
|
||||
<div className="w-full flex gap-1 items-center">
|
||||
@@ -98,26 +99,26 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-0 ">
|
||||
<FieldList tableFields={table.fields} tableId={table.id} />
|
||||
<FieldList tableFields={table.fields} tableId={table.id} dialect={dialect}/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
|
||||
<AccordionItem value="indexes" className="border-none">
|
||||
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground pr-1.5" position="right">
|
||||
<div className="w-full flex gap-1 items-center">
|
||||
<div className="w-full flex gap-1 items-center">
|
||||
<IconBolt className="size-4" />
|
||||
{t("db_controller.indexes")}
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-2">
|
||||
<IndexesList indices={table.indices} fields={table.fields} tableId={table.id} />
|
||||
<IndexesList indices={table.indices} fields={table.fields} table={table} tableId={table.id} dialect={dialect}/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="note" className="border-none">
|
||||
<AccordionItem value="note" className="border-none">
|
||||
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground pr-1.5" position="right">
|
||||
<div className="w-full flex gap-1 items-center">
|
||||
<IconMessageCircle className="size-4 " />
|
||||
<IconMessageCircle className="size-4 " />
|
||||
{t("db_controller.note")}
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
@@ -125,20 +126,20 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
|
||||
<Textarea
|
||||
placeholder={t("db_controller.table_note")}
|
||||
value={note}
|
||||
onChange={(event : any) => setNote(event.target.value) }
|
||||
onChange={(event: any) => setNote(event.target.value)}
|
||||
onBlur={saveNote}
|
||||
className="resize-none min-h-[86px] focus-visible:ring-0 bg-secondary dark:bg-background"
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
</AccordionItem>
|
||||
|
||||
</Accordion>
|
||||
<div className="flex pt-2 justify-between pr-1.5 items-center ">
|
||||
<div className="h-full flex items-center">
|
||||
<ColorPicker
|
||||
defaultColor={table.color as string}
|
||||
onChange={onColorChange}
|
||||
|
||||
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 ">
|
||||
@@ -168,4 +169,3 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
|
||||
|
||||
export default React.memo(TableAccordionContent);
|
||||
|
||||
|
||||
+5
-3
@@ -8,13 +8,15 @@ import TableAccordionContent from "./table-accordion-content";
|
||||
import React, { forwardRef } from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
|
||||
interface TableAccordionItemProps {
|
||||
table: TableType;
|
||||
dialect?: DatabaseDialect
|
||||
}
|
||||
|
||||
const TableAccordionItem = forwardRef<HTMLDivElement, TableAccordionItemProps>(
|
||||
({ table }, ref) => {
|
||||
({ table , dialect }, ref) => {
|
||||
const { attributes, setNodeRef, transform, transition } = useSortable({ id: table.id });
|
||||
|
||||
if (transform?.scaleY)
|
||||
@@ -35,9 +37,9 @@ const TableAccordionItem = forwardRef<HTMLDivElement, TableAccordionItemProps>(
|
||||
{...attributes}
|
||||
className="w-full "
|
||||
>
|
||||
<TableAccordionTrigger table={table} />
|
||||
<TableAccordionTrigger table={table} dialect={dialect} />
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<TableAccordionContent table={table} />
|
||||
<TableAccordionContent table={table} dialect={dialect} />
|
||||
</AccordionContent>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
|
||||
+55
-13
@@ -12,22 +12,27 @@ import { cloneTable } from "@/utils/tables";
|
||||
import { IndexInsertType } from "@/lib/schemas/index-schema";
|
||||
import { FieldInsertType } from "@/lib/schemas/field-schema";
|
||||
import { AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { IconBolt, IconCheck, IconCopy, IconDotsVertical, IconFocus2, IconFolder, IconGripVertical, IconPencil, IconTrash } from "@tabler/icons-react";
|
||||
import { IconAlertTriangle, IconBolt, IconCheck, IconCopy, IconDotsVertical, IconFocus2, IconFolder, IconGripVertical, IconPencil, IconTrash } from "@tabler/icons-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DropdownMenuTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuShortcut } from "@/components/ui/dropdown-menu";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { useDatabaseInvalidIdentifier } from "@/features/database/hooks/use-invalid-identifier";
|
||||
import { isValidIdentifier } from "@/utils/database";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
|
||||
export interface TableAccordionHeaderProps {
|
||||
table: TableType,
|
||||
isOpen?: boolean,
|
||||
dialect?: DatabaseDialect
|
||||
}
|
||||
|
||||
|
||||
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
|
||||
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen, dialect }) => {
|
||||
|
||||
const { editTable, deleteTable, createField, createTable, createIndex, getInteger } = useDatabaseOperations();
|
||||
|
||||
@@ -36,14 +41,31 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const { focusOnTable } = useDiagramOps();
|
||||
|
||||
const { identifierError, setIdentifierError, showIdentifierError, identifierMaxLength } = useDatabaseInvalidIdentifier(tableName, dialect);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setTableName(table.name);
|
||||
}, [table.name])
|
||||
}, [table.name]);
|
||||
|
||||
useEffect(() => {
|
||||
setIdentifierError(isValidIdentifier(
|
||||
tableName.trim(),
|
||||
dialect
|
||||
))
|
||||
}, [tableName]);
|
||||
|
||||
const saveTableName = useCallback(async () => {
|
||||
await editTable({ id: table.id, name: tableName } as TableInsertType);
|
||||
if (identifierError == "empty") {
|
||||
setTableName(table.name);
|
||||
setEditMode(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await editTable({ id: table.id, name: tableName.trim() } as TableInsertType);
|
||||
setEditMode(false);
|
||||
}, [tableName])
|
||||
}, [tableName, identifierError, table])
|
||||
|
||||
|
||||
const onDeleteTable = useCallback(async (event: any) => {
|
||||
event.stopPropagation();
|
||||
@@ -74,10 +96,10 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
unique: false,
|
||||
tableId: table.id
|
||||
} as IndexInsertType);
|
||||
|
||||
|
||||
}, [table])
|
||||
|
||||
const duplicate = useCallback(async (event : any) => {
|
||||
const duplicate = useCallback(async (event: any) => {
|
||||
event.stopPropagation()
|
||||
const clonedTable: TableType = cloneTable(table);
|
||||
await createTable(clonedTable)
|
||||
@@ -110,9 +132,10 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
<div className="w-full flex items-center ">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
<label
|
||||
className=" py-2 truncate text-sm cursor-pointer max-w-60"
|
||||
className={cn("py-2 truncate text-sm cursor-pointer max-w-50 ", {
|
||||
"text-destructive": identifierError != null
|
||||
})}
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
onClick={((event: any) => event.stopPropagation())}
|
||||
>
|
||||
@@ -129,7 +152,6 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
<Button variant="outline" size="icon" className="size-8 shrink-0" onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
setEditMode(true)
|
||||
|
||||
}}>
|
||||
<IconPencil className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
@@ -140,8 +162,24 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
<IconFocus2 className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
identifierError &&
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant={"ghost"} size="icon" className="size-8 shrink-0 hover:bg-destructive/10 hover:border-destructive/30 dark:hover:bg-destructive/10 dark:hover:border-destructive/30" onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
showIdentifierError();
|
||||
setEditMode(true);
|
||||
}} >
|
||||
<IconAlertTriangle className="size-4 text-destructive " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-destructive fill-destructive [&_svg]:bg-destructive [&_svg]:fill-destructive">
|
||||
{t("db_controller.validation.error_message")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
}
|
||||
</>
|
||||
|
||||
}
|
||||
{
|
||||
editMode && <>
|
||||
@@ -152,9 +190,13 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
onBlur={saveTableName}
|
||||
autoFocus
|
||||
type="text"
|
||||
aria-invalid={identifierError != null}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className="h-8"
|
||||
onKeyDown={(e: any) => {
|
||||
className={cn("h-8", {
|
||||
"text-destructive": identifierError != null
|
||||
})}
|
||||
maxLength={identifierMaxLength}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
saveTableName();
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import hash from 'object-hash';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { IconCheck, IconKey, IconKeyframe, IconMessageCircle, IconPencil, IconTrash } from "@tabler/icons-react";
|
||||
@@ -18,7 +16,8 @@ interface Props {
|
||||
showHandles?: boolean,
|
||||
highlight?: boolean,
|
||||
color?: string,
|
||||
className?: string
|
||||
className?: string,
|
||||
maxLength?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +28,7 @@ export const TARGET_PREFIX = "target_";
|
||||
|
||||
const Field: React.FC<Props> = (props) => {
|
||||
|
||||
const { field, showHandles, highlight, color, className } = props;
|
||||
const { field, showHandles, highlight, color, className, maxLength } = props;
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const { deleteField, editField } = useDatabaseOperations();
|
||||
const [fieldName, setFieldName] = useState<string>(field.name);
|
||||
@@ -46,13 +45,18 @@ const Field: React.FC<Props> = (props) => {
|
||||
|
||||
const saveFieldName = useCallback(() => {
|
||||
|
||||
if (fieldName.trim().length == 0) {
|
||||
setFieldName(field.name);
|
||||
setEditMode(false);
|
||||
return;
|
||||
}
|
||||
editField({
|
||||
id: field.id,
|
||||
name: fieldName
|
||||
name: fieldName.trim()
|
||||
} as FieldType);
|
||||
|
||||
setEditMode(false);
|
||||
}, [fieldName]);
|
||||
}, [fieldName, field]);
|
||||
|
||||
|
||||
return (
|
||||
@@ -103,6 +107,7 @@ const Field: React.FC<Props> = (props) => {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setFieldName(e.target.value)}
|
||||
className="h-6 font-bold pb-1.5 rounded-sm px-1"
|
||||
maxLength={maxLength}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -95,17 +95,23 @@ const RenameDB: React.FC = ({ }) => {
|
||||
value={dbName}
|
||||
onBlur={saveDatabaseName}
|
||||
autoFocus
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
saveDatabaseName();
|
||||
e.target.blur();
|
||||
}
|
||||
}}
|
||||
className="h-8 min-w-[164px]"
|
||||
maxLength={255}
|
||||
aria-invalid={dbName.length == 0}
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
|
||||
size="sm"
|
||||
color="primary"
|
||||
onClick={saveDatabaseName}
|
||||
|
||||
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || dbName.length == 0}
|
||||
>
|
||||
{isLoading && <Spinner />}
|
||||
<Save className="size-3" />
|
||||
|
||||
@@ -25,13 +25,14 @@ export type TableProps = Node<{
|
||||
table: TableType,
|
||||
overlapping?: boolean,
|
||||
pulsing?: boolean,
|
||||
highlightedEdges: Edge[]
|
||||
highlightedEdges: Edge[],
|
||||
identifierMaxLength: number
|
||||
}>
|
||||
|
||||
const MAX_FIELDS = 10;
|
||||
const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
|
||||
const { selected, data: { table, overlapping = false, pulsing = false, highlightedEdges = [] } } = props
|
||||
const { selected, data: { table, overlapping = false, pulsing = false, highlightedEdges = [], identifierMaxLength } } = props
|
||||
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const [tableName, setTableName] = useState<string>(table.name);
|
||||
@@ -46,9 +47,14 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
}, [table.name])
|
||||
|
||||
const saveTableName = useCallback(async () => {
|
||||
await editTable({ id: table.id, name: tableName } as TableInsertType);
|
||||
if (tableName.trim().length == 0) {
|
||||
setTableName(table.name);
|
||||
setEditMode(false);
|
||||
return;
|
||||
}
|
||||
await editTable({ id: table.id, name: tableName.trim() } as TableInsertType);
|
||||
setEditMode(false);
|
||||
}, [tableName]);
|
||||
}, [tableName, table]);
|
||||
|
||||
const focus = useCallback(() => {
|
||||
focusOnTable(table.id, false);
|
||||
@@ -56,7 +62,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
|
||||
|
||||
const fields: React.ReactNode[] = useMemo(() => {
|
||||
return table.fields.map((field: FieldType , index : number) => {
|
||||
return table.fields.map((field: FieldType, index: number) => {
|
||||
|
||||
const highlight: boolean = highlightedEdges.find((edge: any) =>
|
||||
(edge.data?.relationship as RelationshipType).sourceFieldId == field.id ||
|
||||
@@ -68,10 +74,11 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
showHandles={selected}
|
||||
highlight={highlight}
|
||||
color={table.color as string}
|
||||
className={ index == table.fields.length - 1 ? "!rounded-b-md" : undefined }
|
||||
className={index == table.fields.length - 1 ? "!rounded-b-md" : undefined}
|
||||
maxLength={identifierMaxLength}
|
||||
/>)
|
||||
})
|
||||
}, [table.fields, selected, highlightedEdges]);
|
||||
}, [table.fields, selected, highlightedEdges, identifierMaxLength]);
|
||||
|
||||
|
||||
const toggleShowMore = useCallback(() => {
|
||||
@@ -79,7 +86,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card
|
||||
<Card
|
||||
className={cn(
|
||||
"rounded-lg p-0.5 gap-0 transition-all duration-200 border-none ring-1 ring-slate-300 shadow-xs dark:ring-border ",
|
||||
overlapping
|
||||
@@ -116,17 +123,17 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
{!editMode ? <>
|
||||
<label
|
||||
className=" w-full text-editable truncate py-0.5 text-sm font-bold text-primary"
|
||||
onDoubleClick={ () => setEditMode(true) }
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
style={{ color: table.color as string }}
|
||||
>
|
||||
{tableName}
|
||||
</label>
|
||||
<div className="flex gap-1 hidden shrink-0 flex-row group-hover:flex ">
|
||||
|
||||
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={() => setEditMode(true)}>
|
||||
<IconPencil className="size-3 text-muted-foreground " />
|
||||
</Button>
|
||||
|
||||
|
||||
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={() => setEditMode(true)}>
|
||||
<IconPencil className="size-3 text-muted-foreground " />
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={focus}>
|
||||
<IconFocus2 className="size-3 text-muted-foreground " />
|
||||
</Button>
|
||||
@@ -144,6 +151,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
onChange={(event: any) => setTableName(event.target.value)}
|
||||
value={tableName}
|
||||
onBlur={saveTableName}
|
||||
maxLength={identifierMaxLength}
|
||||
style={{ color: table.color as string }}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import useToast from "@/hooks/use-toast";
|
||||
import { DatabaseDialect, DatabaseType, getDatabaseByDialect, IDENTIFIER_ERROR, IDENTIFIER_MAX_LENGTH } from "@/lib/database";
|
||||
import { isValidIdentifier } from "@/utils/database";
|
||||
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
|
||||
interface databaseInvalidIdentifierType {
|
||||
identifierError: IDENTIFIER_ERROR,
|
||||
setIdentifierError: Dispatch<SetStateAction<IDENTIFIER_ERROR>>,
|
||||
showIdentifierError: () => void,
|
||||
identifierMaxLength: number;
|
||||
identifierType? : 'table' | 'constraint' | "index" | "column"
|
||||
}
|
||||
|
||||
export const useDatabaseInvalidIdentifier = (identifier: string, dialect?: DatabaseDialect , identifierType = "table"): databaseInvalidIdentifierType => {
|
||||
|
||||
const raise = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [identifierError, setIdentifierError] = useState<IDENTIFIER_ERROR>(null);
|
||||
|
||||
const identifierMaxLength = useMemo(() => {
|
||||
return dialect ? IDENTIFIER_MAX_LENGTH[dialect] : 255
|
||||
}, [dialect])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setIdentifierError(isValidIdentifier(
|
||||
identifier.trim(),
|
||||
dialect
|
||||
))
|
||||
}, [identifier]);
|
||||
|
||||
const showIdentifierError = useCallback(() => {
|
||||
let title: string | undefined;
|
||||
let description: string | undefined;
|
||||
const identifier: string = t(`db_controller.validation.identifiers.${identifierType}`);
|
||||
|
||||
if (identifierError == "empty") {
|
||||
title = t("db_controller.validation.identifier_required").replace("${identifier}", identifier.toLocaleLowerCase())
|
||||
description = t("db_controller.validation.identifier_required_description").replace("${identifier}", identifier)
|
||||
}
|
||||
if (identifierError == "invalid") {
|
||||
title = t("db_controller.validation.invalid_identifier").replace("${identifier}", identifier.toLocaleLowerCase())
|
||||
description = t("db_controller.validation.invalid_identifier_description").replace("${identifier}", identifier.toLocaleLowerCase())
|
||||
}
|
||||
|
||||
if (identifierError === "max_length" && dialect) {
|
||||
const databaseType: DatabaseType = getDatabaseByDialect(dialect)
|
||||
title = t("db_controller.validation.identifier_length").replace("${identifier}", identifier)
|
||||
description = t("db_controller.validation.identifier_length_description")
|
||||
.replace("${identifier}", identifier.toLocaleLowerCase())
|
||||
.replace("${dialect}", databaseType.name).replace("${length}", String(identifierMaxLength))
|
||||
}
|
||||
if (title && description) {
|
||||
raise(title, description, "ERROR")
|
||||
}
|
||||
|
||||
}, [identifierError, identifierMaxLength, dialect , identifierType]);
|
||||
|
||||
|
||||
return {
|
||||
identifierError,
|
||||
setIdentifierError,
|
||||
showIdentifierError,
|
||||
identifierMaxLength
|
||||
}
|
||||
}
|
||||
+17
-11
@@ -1,17 +1,22 @@
|
||||
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { Node, useReactFlow } from "@xyflow/react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { getDefaultTableOverlapping } from "@/utils/tables";
|
||||
import hash from "object-hash";
|
||||
import { DatabaseDialect, IDENTIFIER_MAX_LENGTH } from "@/lib/database";
|
||||
|
||||
|
||||
|
||||
export const useTableToNode = (tables: TableType[]): void => {
|
||||
export const useTableToNode = (tables: TableType[], dialect?: DatabaseDialect): void => {
|
||||
const { setNodes } = useReactFlow();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
const identifierMaxLength = useMemo(() => {
|
||||
return dialect ? IDENTIFIER_MAX_LENGTH[dialect] : 255
|
||||
}, [dialect]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const tableNodes = tables.map((table: TableType) => {
|
||||
return {
|
||||
id: table.id,
|
||||
@@ -24,29 +29,30 @@ export const useTableToNode = (tables: TableType[]): void => {
|
||||
table,
|
||||
overlapping: getDefaultTableOverlapping(table, tables),
|
||||
pulsing: false,
|
||||
highlightedEdges: []
|
||||
highlightedEdges: [] ,
|
||||
identifierMaxLength
|
||||
},
|
||||
style: {
|
||||
width: 224
|
||||
}
|
||||
} as Node
|
||||
}) ;
|
||||
});
|
||||
|
||||
setNodes((nodes) => {
|
||||
return tableNodes.map((tableNode) => {
|
||||
const node: Node | undefined = nodes.find((node: Node) => node.id == tableNode.id);
|
||||
|
||||
|
||||
if (!node)
|
||||
return tableNode;
|
||||
else {
|
||||
const hashNode: string = hash(node.data.table as TableType);
|
||||
const hashTableNode: string = hash(tableNode.data.table as TableType);
|
||||
|
||||
|
||||
return hashNode == hashTableNode ? node : tableNode;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}, [tables])
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
|
||||
import Modal, { ModalProps } from "@/components/modal"
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -32,14 +32,14 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
}
|
||||
|
||||
const createNewDatabase = useCallback(async (withImport: boolean = false) => {
|
||||
const databaseId: string = v4() ;
|
||||
const databaseId: string = v4();
|
||||
|
||||
return new Promise(async (res, rej) => {
|
||||
try {
|
||||
await createDatabase({
|
||||
id: databaseId,
|
||||
name: dbName,
|
||||
dialect: selectedDbType as any
|
||||
dialect: selectedDbType as any
|
||||
});
|
||||
await switchDatabase(databaseId);
|
||||
if (withImport)
|
||||
@@ -50,11 +50,11 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
} catch (error) {
|
||||
rej(error);
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [selectedDbType, dbName])
|
||||
|
||||
useEffect(() => {
|
||||
setIsValid((selectedDbType && dbName.trim().length > 0) as boolean)
|
||||
setIsValid((selectedDbType && dbName.trim().length > 0) as boolean)
|
||||
}, [selectedDbType, dbName]);
|
||||
|
||||
return (
|
||||
@@ -64,7 +64,7 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("modals.pick_database")}
|
||||
actionName={t("modals.create")}
|
||||
|
||||
|
||||
isDisabled={!isValid}
|
||||
actionHandler={createNewDatabase}
|
||||
description={t("modals.create_database_header")}
|
||||
@@ -81,10 +81,11 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
aria-invalid={dbName.trim().length == 0}
|
||||
onChange={(event: any) => setDbName(event.target.value)}
|
||||
placeholder={t("modals.db_name")}
|
||||
maxLength={255}
|
||||
/>
|
||||
{
|
||||
dbName.trim().length == 0 &&
|
||||
<p className="text-destructive">
|
||||
<p className="text-destructive text-xs">
|
||||
{t("modals.db_name_error")}
|
||||
</p>
|
||||
}
|
||||
@@ -99,9 +100,9 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
|
||||
<Label htmlFor={db.dialect}
|
||||
className={cn("w-full p-3 rounded-md border hover:bg-secondary cursor-pointer",
|
||||
(selectedDbType == db.dialect && !db.comming )? "border-primary bg-primary/20 " : "" ,
|
||||
( db.comming )? "opacity-75 hover:bg-background cursor-default" : ""
|
||||
|
||||
(selectedDbType == db.dialect && !db.comming) ? "border-primary bg-primary/20 " : "",
|
||||
(db.comming) ? "opacity-75 hover:bg-background cursor-default" : ""
|
||||
|
||||
|
||||
)}>
|
||||
<RadioGroupItem disabled={db.comming} value={db.dialect} id={db.dialect} className="sr-only" />
|
||||
@@ -113,7 +114,7 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
{db.name}
|
||||
</span>
|
||||
{
|
||||
db.comming &&
|
||||
db.comming &&
|
||||
<Badge className="absolute -top-2 left-[50%] -translate-x-[50%] bg-chart-5 dark:bg-chart-3">
|
||||
Comming
|
||||
</Badge>
|
||||
@@ -123,7 +124,7 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
))
|
||||
}
|
||||
</RadioGroup>
|
||||
|
||||
|
||||
<div className="py-4 space-y-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
+5
-2
@@ -44,8 +44,11 @@ i18n.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
lng: 'en',
|
||||
|
||||
|
||||
detection: {
|
||||
order: ['localStorage'],
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
+25
-1
@@ -28,6 +28,9 @@ export const ar = {
|
||||
show_code: "عرض الكود",
|
||||
fields: "الحقول",
|
||||
indexes: "الفهارس",
|
||||
|
||||
no_index: "لم يتم العثور على فهارس",
|
||||
create_new_index: "إنشاء فهرس جديد",
|
||||
note: "ملاحظة",
|
||||
name: "الاسم",
|
||||
type: "النوع",
|
||||
@@ -84,6 +87,12 @@ export const ar = {
|
||||
text_setting: "إعداد النص",
|
||||
charset: "ترميز الحروف",
|
||||
collation: "التجميع",
|
||||
no_charset: "لا توجد مجموعة أحرف",
|
||||
no_collation: "لا يوجد ترتيب",
|
||||
input_tag_placeholder: "أدخل القيم، مفصولة بفواصل...",
|
||||
multi_select_placeholder: "حدد الحقول...",
|
||||
date_placeholder: "اختر تاريخًا",
|
||||
time: "الوقت",
|
||||
scale: "المقياس",
|
||||
max_length: "الطول الأقصى",
|
||||
integer_width: "عرض العدد الصحيح",
|
||||
@@ -96,7 +105,7 @@ export const ar = {
|
||||
precision_def: "إجمالي الأرقام المسموحة (قبل + بعد الفاصلة).",
|
||||
scale_def: "عدد الأرقام بعد الفاصلة.",
|
||||
no_default: "لا توجد قيمة افتراضية",
|
||||
random_uuid: "معرّف UUID عشوائي",
|
||||
random_uuid: "معرّف UUID عشوائي",
|
||||
time_default_value: {
|
||||
no_value: "لا توجد قيمة",
|
||||
custom: "وقت مخصص",
|
||||
@@ -129,6 +138,21 @@ random_uuid: "معرّف UUID عشوائي",
|
||||
description: "يحتوي المخطط الخاص بك على علاقة مفتاح خارجي دائرية بين الجداول. لإصلاحها،",
|
||||
suggestion: "قم بإزالة إحدى العلاقات أدناه التي تسبب الدورة.",
|
||||
remove_relationship: "إزالة العلاقة"
|
||||
},
|
||||
validation: {
|
||||
identifiers: {
|
||||
table: "جدول",
|
||||
column: "عمود",
|
||||
index: "فهرس",
|
||||
constraint: "قيد"
|
||||
},
|
||||
error_message: "معرّف غير صالح",
|
||||
identifier_required: "اسم ${identifier} مطلوب",
|
||||
identifier_required_description: "يرجى إدخال اسم لـ ${identifier}.",
|
||||
invalid_identifier: "اسم ${identifier} غير صالح",
|
||||
invalid_identifier_description: "استخدم الأحرف والأرقام والشرطات السفلية فقط. يجب أن يبدأ اسم ${identifier} بحرف.",
|
||||
identifier_length: "اسم ${identifier} طويل جدًا",
|
||||
identifier_length_description: "يتجاوز اسم ${identifier} هذا الحد الأقصى للطول وهو ${length} بايت الذي يسمح به ${dialect}."
|
||||
}
|
||||
},
|
||||
table: {
|
||||
|
||||
+24
-1
@@ -28,6 +28,8 @@ export const de = {
|
||||
show_code: "Code anzeigen",
|
||||
fields: "Felder",
|
||||
indexes: "Indizes",
|
||||
no_index: "Keine Indizes gefunden",
|
||||
create_new_index: "Neuen erstellen",
|
||||
note: "Notiz",
|
||||
name: "Name",
|
||||
type: "Typ",
|
||||
@@ -84,6 +86,12 @@ export const de = {
|
||||
text_setting: "Texteinstellung",
|
||||
charset: "Zeichensatz",
|
||||
collation: "Sortierung",
|
||||
no_charset: "Kein Zeichensatz",
|
||||
no_collation: "Keine Sortierung",
|
||||
input_tag_placeholder: "Werte eingeben, durch Kommas getrennt...",
|
||||
multi_select_placeholder: "Felder auswählen...",
|
||||
date_placeholder: "Datum auswählen",
|
||||
time: "Uhrzeit",
|
||||
scale: "Skalierung",
|
||||
max_length: "Maximale Länge",
|
||||
integer_width: "Ganzzahlbreite",
|
||||
@@ -96,7 +104,7 @@ export const de = {
|
||||
precision_def: "Zulässige Gesamtanzahl an Ziffern (vor + nach dem Dezimalpunkt).",
|
||||
scale_def: "Ziffern nach dem Dezimalpunkt.",
|
||||
no_default: "Kein Standardwert",
|
||||
random_uuid: "Zufällige UUID",
|
||||
random_uuid: "Zufällige UUID",
|
||||
time_default_value: {
|
||||
no_value: "Kein Wert",
|
||||
custom: "Benutzerdefinierte Zeit",
|
||||
@@ -129,6 +137,21 @@ random_uuid: "Zufällige UUID",
|
||||
description: "Dein Schema enthält eine zirkuläre Fremdschlüssel-Beziehung zwischen Tabellen. Um dies zu beheben,",
|
||||
suggestion: "entferne eine der unten aufgeführten Beziehungen, die den Kreis verursachen.",
|
||||
remove_relationship: "Beziehung entfernen"
|
||||
},
|
||||
validation: {
|
||||
identifiers: {
|
||||
table: "Tabelle",
|
||||
column: "Spalte",
|
||||
index: "Index",
|
||||
constraint: "Einschränkung"
|
||||
},
|
||||
error_message: "Ungültiger Bezeichner",
|
||||
identifier_required: "Der Name von ${identifier} ist erforderlich",
|
||||
identifier_required_description: "Bitte geben Sie einen Namen für diesen ${identifier} an.",
|
||||
invalid_identifier: "Ungültiger Name für ${identifier}",
|
||||
invalid_identifier_description: "Verwenden Sie nur Buchstaben, Zahlen und Unterstriche. Der Name von ${identifier} muss mit einem Buchstaben beginnen.",
|
||||
identifier_length: "Der Name von ${identifier} ist zu lang",
|
||||
identifier_length_description: "Dieser Name von ${identifier} überschreitet die von ${dialect} zulässige maximale Länge von ${length} Bytes."
|
||||
}
|
||||
},
|
||||
table: {
|
||||
|
||||
@@ -31,6 +31,8 @@ export const en = {
|
||||
show_code: "Show code",
|
||||
fields: "Fields",
|
||||
indexes: "Indexes",
|
||||
no_index: "No Indices found",
|
||||
create_new_index: "create new one",
|
||||
note: "Note",
|
||||
name: "Name",
|
||||
type: "Type",
|
||||
@@ -93,6 +95,12 @@ export const en = {
|
||||
text_setting: "Text Setting",
|
||||
charset: "Charset",
|
||||
collation: "Collation",
|
||||
no_charset: "No charset",
|
||||
no_collation: "No collation",
|
||||
input_tag_placeholder: "Enter values, comma separated...",
|
||||
multi_select_placeholder: "Select fields...",
|
||||
date_placeholder: "Pick date",
|
||||
time: "Time",
|
||||
scale: "Scale",
|
||||
max_length: "Max length",
|
||||
integer_width: "Integer Width",
|
||||
@@ -141,6 +149,21 @@ export const en = {
|
||||
description: "Your schema contains a circular foreign key relationship between tables. To fix it",
|
||||
suggestion: "remove one of the relationships listed below that are causing the cycle.",
|
||||
remove_relationship: "Remove relationship"
|
||||
},
|
||||
validation: {
|
||||
identifiers: {
|
||||
table: "Table",
|
||||
column: "Column",
|
||||
index: "Index",
|
||||
constraint: "Constraint"
|
||||
},
|
||||
error_message: "Invalid identifier",
|
||||
identifier_required: "${identifier} name is required",
|
||||
identifier_required_description: "Please provide a name for this ${identifier}.",
|
||||
invalid_identifier: "Invalid ${identifier} name",
|
||||
invalid_identifier_description: "Use only letters, numbers, and underscores. a ${identifier} name must start with a letter.",
|
||||
identifier_length: "${identifier} name is too long",
|
||||
identifier_length_description: "This ${identifier} name exceeds ${dialect}'s maximum length of ${length} bytes."
|
||||
}
|
||||
},
|
||||
table: {
|
||||
|
||||
+29
-6
@@ -5,8 +5,8 @@ export const es = {
|
||||
translation: {
|
||||
sidebar: {
|
||||
tables: "Tablas",
|
||||
relationships: "Relaciones",
|
||||
documentation : "Documentación"
|
||||
relationships: "Relaciones",
|
||||
documentation: "Documentación"
|
||||
},
|
||||
|
||||
color_picker: {
|
||||
@@ -27,6 +27,8 @@ export const es = {
|
||||
show_code: "Mostrar código",
|
||||
fields: "Campos",
|
||||
indexes: "Índices",
|
||||
no_index: "No se encontraron índices",
|
||||
create_new_index: "Crear uno nuevo",
|
||||
note: "Nota",
|
||||
name: "Nombre",
|
||||
type: "Tipo",
|
||||
@@ -88,6 +90,12 @@ export const es = {
|
||||
text_setting: "Configuración de texto",
|
||||
charset: "Charset",
|
||||
collation: "Intercalación",
|
||||
no_charset: "Sin juego de caracteres",
|
||||
no_collation: "Sin intercalación",
|
||||
input_tag_placeholder: "Introduzca valores separados por comas...",
|
||||
multi_select_placeholder: "Seleccione campos...",
|
||||
date_placeholder: "Seleccione una fecha",
|
||||
time: "Hora",
|
||||
scale: "Escala",
|
||||
max_length: "Longitud máxima",
|
||||
integer_width: "Ancho de entero",
|
||||
@@ -100,7 +108,7 @@ export const es = {
|
||||
precision_def: "Total de dígitos permitidos (antes + después del decimal).",
|
||||
scale_def: "Dígitos permitidos después del decimal.",
|
||||
no_default: "Sin valor predeterminado",
|
||||
random_uuid: "UUID aleatorio",
|
||||
random_uuid: "UUID aleatorio",
|
||||
time_default_value: {
|
||||
no_value: "Sin valor",
|
||||
custom: "Hora personalizada",
|
||||
@@ -136,6 +144,21 @@ random_uuid: "UUID aleatorio",
|
||||
description: "Tu esquema contiene una relación foránea circular entre tablas. Para corregirlo",
|
||||
suggestion: "elimina una de las relaciones que se indican a continuación y que están causando el ciclo.",
|
||||
remove_relationship: "Eliminar relación"
|
||||
},
|
||||
validation: {
|
||||
identifiers: {
|
||||
table: "Tabla",
|
||||
column: "Columna",
|
||||
index: "Índice",
|
||||
constraint: "Restricción"
|
||||
},
|
||||
error_message: "Identificador no válido",
|
||||
identifier_required: "El nombre de ${identifier} es obligatorio",
|
||||
identifier_required_description: "Proporcione un nombre para este ${identifier}.",
|
||||
invalid_identifier: "Nombre de ${identifier} no válido",
|
||||
invalid_identifier_description: "Use únicamente letras, números y guiones bajos. El nombre de ${identifier} debe comenzar con una letra.",
|
||||
identifier_length: "El nombre de ${identifier} es demasiado largo",
|
||||
identifier_length_description: "Este nombre de ${identifier} supera la longitud máxima de ${length} bytes permitida por ${dialect}."
|
||||
}
|
||||
},
|
||||
table: {
|
||||
@@ -202,12 +225,12 @@ random_uuid: "UUID aleatorio",
|
||||
delete_database_content: "Esta acción es irreversible y eliminará permanentemente el diagrama.",
|
||||
delete: "Eliminar",
|
||||
empty_diagram: "Diagrama vacío",
|
||||
create_and_import : "Crear e importar" ,
|
||||
create_and_import: "Crear e importar",
|
||||
|
||||
import_database: {
|
||||
title: "Importar tu base de datos",
|
||||
import: "Importar",
|
||||
view_docs : "Ver la documentación" ,
|
||||
view_docs: "Ver la documentación",
|
||||
import_options: "¿Te gustaría importar usando :",
|
||||
import_error: "Error al analizar SQL",
|
||||
import_error_description: "No pudimos importar tu SQL porque contiene sintaxis inválida.",
|
||||
@@ -275,7 +298,7 @@ random_uuid: "UUID aleatorio",
|
||||
step5: "Finalmente, copia el contenido del archivo <code>.sql</code> en el <bold>editor de código</bold> a continuación."
|
||||
|
||||
},
|
||||
|
||||
|
||||
ssms: {
|
||||
"step1": "Abra SQL Server Management Studio (SSMS).",
|
||||
"step2": "Haga clic derecho en su base de datos y luego seleccione Tareas → Generar scripts en el menú contextual.",
|
||||
|
||||
@@ -30,6 +30,8 @@ export const fr = {
|
||||
show_code: "Afficher le code",
|
||||
fields: "Champs",
|
||||
indexes: "Index",
|
||||
no_index: "Aucun index trouvé",
|
||||
create_new_index: "En créer un nouveau",
|
||||
note: "Note",
|
||||
name: "Nom",
|
||||
type: "Type",
|
||||
@@ -86,6 +88,12 @@ export const fr = {
|
||||
text_setting: "Paramètres de texte",
|
||||
charset: "Jeu de caractères",
|
||||
collation: "Collation",
|
||||
no_charset: "Aucun jeu de caractères",
|
||||
no_collation: "Aucune collation",
|
||||
input_tag_placeholder: "Saisissez des valeurs, séparées par des virgules...",
|
||||
multi_select_placeholder: "Sélectionnez des champs...",
|
||||
date_placeholder: "Sélectionnez une date",
|
||||
time: "Heure",
|
||||
scale: "Échelle",
|
||||
max_length: "Longueur max",
|
||||
integer_width: "Largeur entière",
|
||||
@@ -131,6 +139,21 @@ export const fr = {
|
||||
description: "Votre schéma contient une relation de clé étrangère circulaire entre les tables. Pour corriger cela,",
|
||||
suggestion: "supprimez l’une des relations ci-dessous qui provoque le cycle.",
|
||||
remove_relationship: "Supprimer la relation"
|
||||
},
|
||||
validation: {
|
||||
identifiers: {
|
||||
table: "Table",
|
||||
column: "Colonne",
|
||||
index: "Index",
|
||||
constraint: "Contrainte"
|
||||
},
|
||||
error_message: "Identifiant invalide",
|
||||
identifier_required: "Le nom de ${identifier} est requis",
|
||||
identifier_required_description: "Veuillez fournir un nom pour ce ${identifier}.",
|
||||
invalid_identifier: "Nom de ${identifier} invalide",
|
||||
invalid_identifier_description: "Utilisez uniquement des lettres, des chiffres et des underscores. Un nom de ${identifier} doit commencer par une lettre.",
|
||||
identifier_length: "Le nom de ${identifier} est trop long",
|
||||
identifier_length_description: "Ce nom de ${identifier} dépasse la longueur maximale de ${length} octets autorisée par ${dialect}."
|
||||
}
|
||||
},
|
||||
table: {
|
||||
|
||||
+28
-6
@@ -6,7 +6,7 @@ export const hi = {
|
||||
tables: "तालिकाएँ",
|
||||
relationships: "रिलेशनशिप्स",
|
||||
database: "डेटाबेस",
|
||||
documentation : "दस्तावेज़"
|
||||
documentation: "दस्तावेज़"
|
||||
},
|
||||
|
||||
color_picker: {
|
||||
@@ -28,6 +28,8 @@ export const hi = {
|
||||
show_code: "कोड दिखाएं",
|
||||
fields: "फ़ील्ड्स",
|
||||
indexes: "इंडेक्सेस",
|
||||
no_index: "कोई इंडेक्स नहीं मिला",
|
||||
create_new_index: "नया इंडेक्स बनाएं",
|
||||
note: "नोट",
|
||||
name: "नाम",
|
||||
type: "प्रकार",
|
||||
@@ -82,13 +84,18 @@ export const hi = {
|
||||
zeroFill: "शून्य भरें",
|
||||
autoIncrement: "स्वतः वृद्धि",
|
||||
note: "नोट",
|
||||
|
||||
delete_field: "फ़ील्ड हटाएं",
|
||||
field_note: "फ़ील्ड नोट",
|
||||
precision: "सटीकता",
|
||||
text_setting: "टेक्स्ट सेटिंग",
|
||||
charset: "अक्षरसेट",
|
||||
collation: "कोलेशन",
|
||||
no_charset: "कोई कैरेक्टर सेट नहीं",
|
||||
no_collation: "कोई कॉलेशन नहीं",
|
||||
input_tag_placeholder: "मान दर्ज करें, अल्पविराम से अलग करें...",
|
||||
multi_select_placeholder: "फ़ील्ड चुनें...",
|
||||
date_placeholder: "तारीख चुनें",
|
||||
time: "समय",
|
||||
scale: "स्केल",
|
||||
max_length: "अधिकतम लंबाई",
|
||||
integer_width: "पूर्णांक चौड़ाई",
|
||||
@@ -101,7 +108,7 @@ export const hi = {
|
||||
precision_def: "कुल अनुमत अंक (दशमलव से पहले और बाद)।",
|
||||
scale_def: "दशमलव के बाद अनुमत अंक।",
|
||||
no_default: "कोई डिफ़ॉल्ट मान नहीं",
|
||||
random_uuid: "रैंडम UUID",
|
||||
random_uuid: "रैंडम UUID",
|
||||
time_default_value: {
|
||||
no_value: "कोई मान नहीं",
|
||||
custom: "कस्टम समय",
|
||||
@@ -136,6 +143,21 @@ random_uuid: "रैंडम UUID",
|
||||
description: "आपकी स्कीमा में तालिकाओं के बीच एक परिपत्र विदेशी कुंजी संबंध है। इसे ठीक करने के लिए",
|
||||
suggestion: "नीचे दी गई चक्र उत्पन्न करने वाली रिलेशनशिप में से एक को हटाएं।",
|
||||
remove_relationship: "रिलेशनशिप हटाएं"
|
||||
},
|
||||
validation: {
|
||||
identifiers: {
|
||||
table: "तालिका",
|
||||
column: "कॉलम",
|
||||
index: "इंडेक्स",
|
||||
constraint: "बाधा"
|
||||
},
|
||||
error_message: "अमान्य पहचानकर्ता",
|
||||
identifier_required: "${identifier} का नाम आवश्यक है",
|
||||
identifier_required_description: "कृपया इस ${identifier} के लिए एक नाम प्रदान करें।",
|
||||
invalid_identifier: "${identifier} का नाम अमान्य है",
|
||||
invalid_identifier_description: "केवल अक्षरों, संख्याओं और अंडरस्कोर का उपयोग करें। ${identifier} का नाम किसी अक्षर से शुरू होना चाहिए।",
|
||||
identifier_length: "${identifier} का नाम बहुत लंबा है",
|
||||
identifier_length_description: "यह ${identifier} नाम ${dialect} द्वारा अनुमत अधिकतम ${length} बाइट की लंबाई से अधिक है।"
|
||||
}
|
||||
},
|
||||
table: {
|
||||
@@ -206,7 +228,7 @@ random_uuid: "रैंडम UUID",
|
||||
import_database: {
|
||||
title: "अपना डेटाबेस आयात करें",
|
||||
import: "आयात करें",
|
||||
view_docs : "दस्तावेज़ देखें" ,
|
||||
view_docs: "दस्तावेज़ देखें",
|
||||
import_options: "क्या आप निम्न के माध्यम से आयात करना चाहेंगे:",
|
||||
import_error: "SQL पार्सिंग त्रुटि",
|
||||
import_error_description: "हम आपका SQL आयात नहीं कर सके क्योंकि इसमें अमान्य सिंटैक्स है।",
|
||||
@@ -270,8 +292,8 @@ random_uuid: "रैंडम UUID",
|
||||
step3: "शीर्ष मेनू से <bold>File > Export > Database to SQL file</bold> पर जाएं।",
|
||||
step4: "डायलॉग में <bold>Export schema only</bold> चुनें और <bold>Save</bold> पर क्लिक करें।",
|
||||
step5: "अंत में, <code>.sql</code> फ़ाइल की सामग्री को नीचे दिए गए <bold>code editor</bold> में कॉपी करें।"
|
||||
} ,
|
||||
ssms: {
|
||||
},
|
||||
ssms: {
|
||||
"step1": "SQL Server Management Studio (SSMS) खोलें।",
|
||||
"step2": "अपना डेटाबेस पर राइट-क्लिक करें, फिर संदर्भ मेनू से Tasks → Generate Scripts चुनें।",
|
||||
"step3": "Choose Objects चरण में, Choose specific database objects चुनें, फिर सभी टेबल्स को चेक करें।",
|
||||
|
||||
+30
-7
@@ -6,7 +6,7 @@ export const pt = {
|
||||
tables: "Tabelas",
|
||||
relationships: "Relacionamentos",
|
||||
database: "Banco de dados",
|
||||
documentation : "Documentação"
|
||||
documentation: "Documentação"
|
||||
},
|
||||
|
||||
color_picker: {
|
||||
@@ -15,7 +15,7 @@ export const pt = {
|
||||
navbar: {
|
||||
rename_db: "Renomear banco de dados",
|
||||
search: "Pesquisar",
|
||||
|
||||
|
||||
command: "Digite um comando ou pesquise...",
|
||||
},
|
||||
db_controller: {
|
||||
@@ -29,10 +29,12 @@ export const pt = {
|
||||
show_code: "Mostrar código",
|
||||
fields: "Campos",
|
||||
indexes: "Índices",
|
||||
no_index: "Nenhum índice encontrado",
|
||||
create_new_index: "Criar um novo",
|
||||
note: "Nota",
|
||||
name: "Nome",
|
||||
type: "Tipo",
|
||||
|
||||
|
||||
required: "Obrigatório",
|
||||
nullable: "Aceita Nulo",
|
||||
select_fields: "Selecionar campos",
|
||||
@@ -90,6 +92,12 @@ export const pt = {
|
||||
text_setting: "Configuração de Texto",
|
||||
charset: "Charset",
|
||||
collation: "Colação",
|
||||
no_charset: "Nenhum conjunto de caracteres",
|
||||
no_collation: "Nenhuma ordenação",
|
||||
input_tag_placeholder: "Insira valores, separados por vírgulas...",
|
||||
multi_select_placeholder: "Selecione os campos...",
|
||||
date_placeholder: "Escolha uma data",
|
||||
time: "Hora",
|
||||
scale: "Escala",
|
||||
max_length: "Comprimento Máximo",
|
||||
integer_width: "Largura do Inteiro",
|
||||
@@ -102,7 +110,7 @@ export const pt = {
|
||||
precision_def: "Dígitos totais permitidos (antes + depois do decimal).",
|
||||
scale_def: "Dígitos permitidos após o decimal.",
|
||||
no_default: "Sem valor padrão",
|
||||
random_uuid: "UUID aleatório",
|
||||
random_uuid: "UUID aleatório",
|
||||
time_default_value: {
|
||||
no_value: "Sem valor",
|
||||
custom: "Hora personalizada",
|
||||
@@ -137,6 +145,21 @@ random_uuid: "UUID aleatório",
|
||||
description: "Seu esquema contém uma relação circular de chave estrangeira entre tabelas. Para corrigir,",
|
||||
suggestion: "remova um dos relacionamentos listados abaixo que está causando o ciclo.",
|
||||
remove_relationship: "Remover relacionamento"
|
||||
},
|
||||
validation: {
|
||||
identifiers: {
|
||||
table: "Tabela",
|
||||
column: "Coluna",
|
||||
index: "Índice",
|
||||
constraint: "Restrição"
|
||||
},
|
||||
error_message: "Identificador inválido",
|
||||
identifier_required: "O nome de ${identifier} é obrigatório",
|
||||
identifier_required_description: "Forneça um nome para este ${identifier}.",
|
||||
invalid_identifier: "Nome de ${identifier} inválido",
|
||||
invalid_identifier_description: "Use apenas letras, números e sublinhados. O nome de ${identifier} deve começar com uma letra.",
|
||||
identifier_length: "O nome de ${identifier} é muito longo",
|
||||
identifier_length_description: "Este nome de ${identifier} excede o comprimento máximo de ${length} bytes permitido pelo ${dialect}."
|
||||
}
|
||||
},
|
||||
table: {
|
||||
@@ -207,7 +230,7 @@ random_uuid: "UUID aleatório",
|
||||
import_database: {
|
||||
title: "Importar seu Banco de Dados",
|
||||
import: "Importar",
|
||||
view_docs : "Ver documentação" ,
|
||||
view_docs: "Ver documentação",
|
||||
import_options: "Gostaria de importar usando:",
|
||||
import_error: "Erro de Análise SQL",
|
||||
import_error_description: "Não foi possível importar seu SQL porque contém sintaxe inválida.",
|
||||
@@ -271,8 +294,8 @@ random_uuid: "UUID aleatório",
|
||||
step3: "Vá para <bold>Arquivo > Exportar > Banco de Dados para Arquivo SQL</bold>.",
|
||||
step4: "Na janela de diálogo, escolha <bold>Exportar apenas o esquema</bold> e clique em <bold>Salvar</bold>.",
|
||||
step5: "Finalmente, copie o conteúdo do arquivo <code>.sql</code> para o <bold>editor de código</bold> abaixo."
|
||||
} ,
|
||||
ssms: {
|
||||
},
|
||||
ssms: {
|
||||
"step1": "Abra o SQL Server Management Studio (SSMS).",
|
||||
"step2": "Clique com o botão direito no seu banco de dados e selecione Tarefas → Gerar Scripts no menu de contexto.",
|
||||
"step3": "Na etapa Escolher Objetos, selecione Escolher objetos específicos do banco de dados e marque todas as tabelas.",
|
||||
|
||||
+30
-5
@@ -6,7 +6,7 @@ export const ru = {
|
||||
tables: "Таблицы",
|
||||
relationships: "Связи",
|
||||
database: "База данных",
|
||||
documentation : "Документация"
|
||||
documentation: "Документация"
|
||||
},
|
||||
color_picker: {
|
||||
default_color: "Цвет по умолчанию"
|
||||
@@ -27,6 +27,9 @@ export const ru = {
|
||||
show_code: "Показать код",
|
||||
fields: "Поля",
|
||||
indexes: "Индексы",
|
||||
no_index: "Индексы не найдены",
|
||||
create_new_index: "Создать новый",
|
||||
|
||||
note: "Заметка",
|
||||
name: "Имя",
|
||||
type: "Тип",
|
||||
@@ -82,6 +85,13 @@ export const ru = {
|
||||
text_setting: "Настройки текста",
|
||||
charset: "Кодировка",
|
||||
collation: "Сравнение",
|
||||
no_charset: "Кодировка не указана",
|
||||
no_collation: "Сортировка не указана",
|
||||
input_tag_placeholder: "Введите значения через запятую...",
|
||||
multi_select_placeholder: "Выберите поля...",
|
||||
date_placeholder: "Выберите дату",
|
||||
time: "Время",
|
||||
|
||||
scale: "Масштаб",
|
||||
max_length: "Макс. длина",
|
||||
integer_width: "Ширина целого числа",
|
||||
@@ -94,7 +104,7 @@ export const ru = {
|
||||
precision_def: "Общее количество допустимых цифр (до и после запятой).",
|
||||
scale_def: "Допустимое количество цифр после запятой.",
|
||||
no_default: "Нет значения по умолчанию",
|
||||
random_uuid: "Случайный UUID",
|
||||
random_uuid: "Случайный UUID",
|
||||
time_default_value: {
|
||||
no_value: "Нет значения",
|
||||
custom: "Произвольное время",
|
||||
@@ -127,6 +137,21 @@ random_uuid: "Случайный UUID",
|
||||
description: "В вашей схеме есть циклическая связь между таблицами. Чтобы устранить её,",
|
||||
suggestion: "удалите одну из перечисленных ниже связей, вызывающих цикл.",
|
||||
remove_relationship: "Удалить связь"
|
||||
},
|
||||
validation: {
|
||||
identifiers: {
|
||||
table: "Таблица",
|
||||
column: "Столбец",
|
||||
index: "Индекс",
|
||||
constraint: "Ограничение"
|
||||
},
|
||||
error_message: "Недопустимый идентификатор",
|
||||
identifier_required: "Имя ${identifier} обязательно",
|
||||
identifier_required_description: "Пожалуйста, укажите имя для этого ${identifier}.",
|
||||
invalid_identifier: "Недопустимое имя ${identifier}",
|
||||
invalid_identifier_description: "Используйте только буквы, цифры и символы подчёркивания. Имя ${identifier} должно начинаться с буквы.",
|
||||
identifier_length: "Имя ${identifier} слишком длинное",
|
||||
identifier_length_description: "Это имя ${identifier} превышает максимальную длину в ${length} байт, разрешённую ${dialect}."
|
||||
}
|
||||
},
|
||||
table: {
|
||||
@@ -191,12 +216,12 @@ random_uuid: "Случайный UUID",
|
||||
delete_database_content: "Это действие необратимо и навсегда удалит диаграмму.",
|
||||
delete: "Удалить",
|
||||
empty_diagram: "Пустая диаграмма",
|
||||
create_and_import : "Создать и импортировать" ,
|
||||
create_and_import: "Создать и импортировать",
|
||||
|
||||
import_database: {
|
||||
title: "Импортируйте вашу базу данных",
|
||||
import: "Импорт",
|
||||
view_docs : "Просмотреть документацию" ,
|
||||
view_docs: "Просмотреть документацию",
|
||||
import_options: "Вы хотите импортировать с помощью:",
|
||||
import_error: "Ошибка разбора SQL",
|
||||
import_error_description: "Не удалось импортировать SQL, так как он содержит недопустимый синтаксис.",
|
||||
@@ -260,7 +285,7 @@ random_uuid: "Случайный UUID",
|
||||
step4: "Выберите <bold>Экспортировать только схему</bold> и нажмите <bold>Сохранить</bold>.",
|
||||
step5: "Скопируйте содержимое <code>.sql</code> файла в редактор ниже."
|
||||
},
|
||||
ssms: {
|
||||
ssms: {
|
||||
"step1": "Откройте SQL Server Management Studio (SSMS).",
|
||||
"step2": "Щёлкните правой кнопкой мыши по вашей базе данных, затем выберите Tasks → Generate Scripts в контекстном меню.",
|
||||
"step3": "В шаге Choose Objects выберите Select specific database objects и отметьте все таблицы.",
|
||||
|
||||
+26
-1
@@ -28,6 +28,9 @@ export const zh = {
|
||||
show_code: "显示代码",
|
||||
fields: "字段",
|
||||
indexes: "索引",
|
||||
|
||||
no_index: "未找到索引",
|
||||
create_new_index: "创建新的索引",
|
||||
note: "备注",
|
||||
name: "名称",
|
||||
type: "类型",
|
||||
@@ -90,6 +93,13 @@ export const zh = {
|
||||
text_setting: "文本设置",
|
||||
charset: "字符集",
|
||||
collation: "排序规则",
|
||||
no_charset: "无字符集",
|
||||
no_collation: "无排序规则",
|
||||
input_tag_placeholder: "输入值,使用逗号分隔...",
|
||||
multi_select_placeholder: "选择字段...",
|
||||
date_placeholder: "选择日期",
|
||||
time: "时间",
|
||||
|
||||
scale: "小数位数",
|
||||
max_length: "最大长度",
|
||||
integer_width: "整数宽度",
|
||||
@@ -102,7 +112,7 @@ export const zh = {
|
||||
precision_def: "允许的总位数(小数点前+后)。",
|
||||
scale_def: "小数点后的位数。",
|
||||
no_default: "无默认值",
|
||||
random_uuid: "随机 UUID",
|
||||
random_uuid: "随机 UUID",
|
||||
time_default_value: {
|
||||
no_value: "无值",
|
||||
custom: "自定义时间",
|
||||
@@ -138,6 +148,21 @@ random_uuid: "随机 UUID",
|
||||
description: "您的架构中存在表之间的循环外键关系。要修复它,",
|
||||
suggestion: "请移除以下导致循环的关系之一。",
|
||||
remove_relationship: "移除关系"
|
||||
},
|
||||
validation: {
|
||||
identifiers: {
|
||||
table: "表",
|
||||
column: "列",
|
||||
index: "索引",
|
||||
constraint: "约束"
|
||||
},
|
||||
error_message: "标识符无效",
|
||||
identifier_required: "${identifier} 名称是必需的",
|
||||
identifier_required_description: "请为此 ${identifier} 提供一个名称。",
|
||||
invalid_identifier: "${identifier} 名称无效",
|
||||
invalid_identifier_description: "只能使用字母、数字和下划线。${identifier} 名称必须以字母开头。",
|
||||
identifier_length: "${identifier} 名称过长",
|
||||
identifier_length_description: "此 ${identifier} 名称超过了 ${dialect} 允许的最大长度 ${length} 字节。"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -77,6 +77,22 @@ export enum ImportMethodType {
|
||||
|
||||
}
|
||||
|
||||
export const IDENTIFIER_REGEX = /^[\p{L}_][\p{L}\p{N}_]*$/u;
|
||||
|
||||
|
||||
export const IDENTIFIER_MAX_LENGTH: Record<DatabaseDialect, number> = {
|
||||
[DatabaseDialect.POSTGRES]: 63, // NAMEDATALEN - 1 (63 bytes)
|
||||
[DatabaseDialect.MYSQL]: 64, // 64 characters
|
||||
[DatabaseDialect.MARIADB]: 64, // 64 characters
|
||||
[DatabaseDialect.MSSQL]: 128, // 128 characters
|
||||
[DatabaseDialect.ORACLE]: 128, // 128 characters (30 in pre-12c)
|
||||
[DatabaseDialect.SQLITE]: 255 // Practically unlimited; application budget
|
||||
};
|
||||
|
||||
|
||||
export type IDENTIFIER_ERROR = null | "max_length" | "empty" | "invalid" ;
|
||||
|
||||
|
||||
export interface ImportDatabaseMethod {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -51,7 +51,7 @@ interface DatabaseOperationsContextType {
|
||||
createIndex: (index: IndexInsertType) => Promise<QueryResult>,
|
||||
editIndex: (index: IndexInsertType) => Promise<QueryResult>,
|
||||
deleteIndex: (id: string) => Promise<void>,
|
||||
editFieldIndices: (indexId: string, fieldIds: string[]) => Promise<void>,
|
||||
editFieldIndices: (indexId: string, fieldIds: string[] , deleteIds : string[]) => Promise<void>,
|
||||
// relationship operations
|
||||
createRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
|
||||
editRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
|
||||
|
||||
@@ -256,18 +256,21 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
const editIndex = useCallback(async (index: IndexInsertType): Promise<QueryResult> => {
|
||||
return await db.update(indices).set(index).where(eq(indices.id, index.id));
|
||||
}, [db]);
|
||||
|
||||
const editFieldIndices = useCallback((indexId: string, fieldIds: string[]): Promise<void> => {
|
||||
const editFieldIndices = useCallback((indexId: string, fieldIds: string[], deleteIds: string[]): Promise<void> => {
|
||||
if (!currentDatabaseId) {
|
||||
throw Error("No Database selected")
|
||||
}
|
||||
return db.transaction(async (tx) => {
|
||||
await tx.delete(field_indices).where(eq(field_indices.indexId, indexId));
|
||||
await tx.delete(field_indices).where(inArray(field_indices.id, deleteIds));
|
||||
if (fieldIds.length > 0)
|
||||
await tx.insert(field_indices).values(fieldIds.map((fieldId: string) => ({
|
||||
id: v4(),
|
||||
fieldId,
|
||||
indexId,
|
||||
databaseId: currentDatabaseId
|
||||
})))
|
||||
})
|
||||
}, [db])
|
||||
}, [db, currentDatabaseId])
|
||||
// CRUD operations for Relationships
|
||||
const createRelationship = useCallback(async (relationship: RelationshipInsertType): Promise<QueryResult> => {
|
||||
if (currentDatabaseId) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { excludeFields } from "./utils";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
|
||||
import { DatabaseDialect, IDENTIFIER_ERROR, IDENTIFIER_MAX_LENGTH, IDENTIFIER_REGEX } from "@/lib/database";
|
||||
|
||||
// Define the possible operations that can be performed when diffing databases
|
||||
export type DBDiffOperation =
|
||||
@@ -309,3 +310,20 @@ export function normalizeDatabase(db: DatabaseType): any {
|
||||
|
||||
|
||||
|
||||
export const isValidIdentifier = (value: string , dialect : DatabaseDialect | undefined): IDENTIFIER_ERROR => {
|
||||
|
||||
if (value.length == 0)
|
||||
return "empty" ;
|
||||
if ( !IDENTIFIER_REGEX.test(value) ) {
|
||||
return "invalid" ;
|
||||
}
|
||||
|
||||
const max_length = dialect ? IDENTIFIER_MAX_LENGTH[dialect] : 255 ;
|
||||
if (value.length > max_length)
|
||||
return "max_length" ;
|
||||
|
||||
return null ;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DataType } from '@/lib/schemas/data-type-schema';
|
||||
import { FieldInsertType } from '@/lib/schemas/field-schema';
|
||||
import { IndexInsertType } from '@/lib/schemas/index-schema';
|
||||
import { Cardinality, RelationshipInsertType } from '@/lib/schemas/relationship-schema';
|
||||
import { TableInsertType } from '@/lib/schemas/table-schema';
|
||||
import { TableInsertType } from '@/lib/schemas/table-schema';
|
||||
import { init, parse, ReferentialAction } from '@guanmingchiu/sqlparser-ts';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -51,7 +51,7 @@ export class BaseSqlImporter {
|
||||
fk_constraints.push(...tableFkConstraints);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
}
|
||||
|
||||
@@ -145,14 +145,27 @@ export class BaseSqlImporter {
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tables.length == 0)
|
||||
throw Error("Can't parse sql")
|
||||
throw Error("Can't parse sql")
|
||||
return {
|
||||
tables,
|
||||
relationships,
|
||||
indexes,
|
||||
tables: tables.map((table: TableInsertType) => ({
|
||||
...table,
|
||||
name: table.name.slice(0, 255),
|
||||
fields: table.fields?.map((field: FieldInsertType) => ({
|
||||
...field,
|
||||
name: field.name.slice(0, 255)
|
||||
}))
|
||||
})),
|
||||
relationships: relationships.map((relationship: RelationshipInsertType) => ({
|
||||
...relationship,
|
||||
name: relationship.name?.slice(0, 255)
|
||||
})),
|
||||
indexes: indexes.map((index: IndexInsertType) => ({
|
||||
...index,
|
||||
name: index.name.slice(0, 255)
|
||||
})),
|
||||
errors
|
||||
}
|
||||
|
||||
@@ -258,12 +271,12 @@ export class BaseSqlImporter {
|
||||
let fk_constraint: any | undefined;
|
||||
let params: any[] | undefined;
|
||||
|
||||
|
||||
|
||||
// if it's a basic type like (texts , booleans .. ect) we get the name directly
|
||||
// if it's a type that take params , the data type become an object
|
||||
if (typeof ast.data_type === "string") {
|
||||
typeName = ast.data_type.toLowerCase();
|
||||
|
||||
|
||||
|
||||
} else if (typeof ast.data_type === "object" && !ast.data_type.Custom) {
|
||||
const attributeName = Object.keys(ast.data_type)[0];
|
||||
@@ -289,10 +302,10 @@ export class BaseSqlImporter {
|
||||
}
|
||||
typeName = attributeName.toLowerCase();
|
||||
|
||||
|
||||
if (typeName == "timestamp" && this.dialect == DatabaseDialect.ORACLE && ast.data_type[attributeName].length >= 2 && ast.data_type[attributeName][1] == "WithTimeZone") {
|
||||
typeName += "withtimezone" ;
|
||||
}
|
||||
|
||||
if (typeName == "timestamp" && this.dialect == DatabaseDialect.ORACLE && ast.data_type[attributeName].length >= 2 && ast.data_type[attributeName][1] == "WithTimeZone") {
|
||||
typeName += "withtimezone";
|
||||
}
|
||||
}
|
||||
|
||||
else if (ast.data_type.Custom && Array.isArray(ast.data_type.Custom)) {
|
||||
@@ -300,9 +313,9 @@ export class BaseSqlImporter {
|
||||
extraParam = Number(ast.data_type.Custom[1]?.[0]);
|
||||
|
||||
typeName = ast.data_type.Custom[0]?.[0]?.Identifier?.value.toLowerCase();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
const dataType: DataType | undefined = this.processDataType(typeName as string);
|
||||
|
||||
if (!dataType) {
|
||||
@@ -478,9 +491,9 @@ export class BaseSqlImporter {
|
||||
return ForeignKeyActions.NO_ACTION;
|
||||
}
|
||||
|
||||
protected processDataType(typeName: string): DataType | undefined {
|
||||
|
||||
|
||||
protected processDataType(typeName: string): DataType | undefined {
|
||||
|
||||
|
||||
// get data type from the supported ones
|
||||
return this.data_types.find((dataType: DataType) => {
|
||||
let synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
|
||||
@@ -491,28 +504,28 @@ export class BaseSqlImporter {
|
||||
}
|
||||
|
||||
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||
|
||||
|
||||
if (ast.Value && ast.Value.value) {
|
||||
|
||||
|
||||
const value: any = ast.Value.value;
|
||||
if (value.Number && Array.isArray(value.Number) && value.Number.length > 0 && !isNaN(Number(value.Number[0]))) {
|
||||
return value.Number[0];
|
||||
}
|
||||
|
||||
|
||||
else if (value.SingleQuotedString || value.DoubleQuotedString) {
|
||||
const valuesArray = Object.values(value);
|
||||
if (valuesArray.length > 0)
|
||||
return valuesArray[0] as string;
|
||||
}
|
||||
|
||||
|
||||
else if (value.Boolean) {
|
||||
return "true";
|
||||
}
|
||||
|
||||
|
||||
else if (value.NationalStringLiteral) {
|
||||
return value.NationalStringLiteral
|
||||
}
|
||||
|
||||
|
||||
} else if (ast.Function) {
|
||||
if (dataType.type == DataTypes.TIME) {
|
||||
if (ast.Function.name?.[0]?.Identifier?.value && this.isCurrentTimesTampFunction(ast.Function.name?.[0]?.Identifier?.value))
|
||||
@@ -529,7 +542,7 @@ export class BaseSqlImporter {
|
||||
return valuesArray[0] as string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,11 +68,11 @@ export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
|
||||
return {
|
||||
column: {
|
||||
type: "column_ref",
|
||||
column: {
|
||||
column: (this.dialect == DatabaseDialect.MYSQL || this.dialect == DatabaseDialect.MARIADB) ? field.name : ({
|
||||
expr: {
|
||||
type: "default", value: field.name,
|
||||
type: "double_quote_string", value: field.name,
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
default_val,
|
||||
unique,
|
||||
@@ -139,12 +139,12 @@ export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
|
||||
{
|
||||
type: "column_ref",
|
||||
table: null,
|
||||
column: {
|
||||
column: (this.dialect == DatabaseDialect.MYSQL || this.dialect == DatabaseDialect.MARIADB) ? foreignKey.name : ({
|
||||
expr: {
|
||||
type: "default",
|
||||
type: "double_quote_string",
|
||||
value: foreignKey.name
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
],
|
||||
constraint_type: "FOREIGN KEY",
|
||||
@@ -155,12 +155,12 @@ export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
|
||||
{
|
||||
type: "column_ref",
|
||||
table: null,
|
||||
column: {
|
||||
column: (this.dialect == DatabaseDialect.MYSQL || this.dialect == DatabaseDialect.MARIADB) ? primaryKey.name : ({
|
||||
expr: {
|
||||
type: "default",
|
||||
type: "double_quote_string",
|
||||
value: primaryKey.name
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
],
|
||||
table: [
|
||||
@@ -292,8 +292,9 @@ export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected startTransaction(): string {
|
||||
return "BEGIN TRANSACTION;" ;
|
||||
return "START TRANSACTION;";
|
||||
}
|
||||
|
||||
protected commit(): string {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { format } from 'sql-formatter';
|
||||
import { AST } from "node-sql-parser";
|
||||
|
||||
|
||||
export default class MSSqlRenderer extends BaseDatabaseRenderer {
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ ${this.commit()}
|
||||
|
||||
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||
const ast: any = super.createTableAst(table);
|
||||
|
||||
|
||||
let indexes: string = ast.indices_ast.filter((index: string) => index != null).join("\n")
|
||||
|
||||
// get primary keys and check if we hame more than one
|
||||
@@ -43,7 +43,7 @@ ${this.commit()}
|
||||
constraints = this.getPrimaryKeyContraint(primaryKeys, table)
|
||||
}
|
||||
return `
|
||||
CREATE TABLE ${table.name} (
|
||||
CREATE TABLE "${table.name}" (
|
||||
${(ast as any).field_definitions.join(",\n")}
|
||||
${constraints ? ',\n' + constraints : ""}
|
||||
);
|
||||
@@ -80,21 +80,21 @@ ${this.commit()}
|
||||
let defaultValue: any = "";
|
||||
|
||||
if (ast.default_value !== undefined && ast.default_value !== null && !dropDefaultValue) {
|
||||
defaultValue = `CONSTRAINT DF_${table.name}_${field.name} DEFAULT ${ast.default_value}`;
|
||||
defaultValue = `CONSTRAINT "DF_${table.name}_${field.name}" DEFAULT ${ast.default_value}`;
|
||||
}
|
||||
if (!ast.primary_key) {
|
||||
unique = field.unique ? `CONSTRAINT UQ_${table.name}_${field.name} UNIQUE` : "";
|
||||
unique = field.unique ? `CONSTRAINT "UQ_${table.name}_${field.name}" UNIQUE` : "";
|
||||
}
|
||||
|
||||
const dataType = field.type.name == "uuid" ? "RAW(16)" : ast.dataType;
|
||||
|
||||
return `${field.name} ${dataType}${options} ${autoIncrement} ${nullable} ${defaultValue} ${unique} `;
|
||||
return `"${field.name}" ${dataType}${options} ${autoIncrement} ${nullable} ${defaultValue} ${unique} `;
|
||||
}
|
||||
|
||||
protected processDefaultValue(field: FieldType): AST | null {
|
||||
const ast: any = super.processDefaultValue(field);
|
||||
|
||||
if (ast && ast.default_value_type) {
|
||||
|
||||
if (ast && ast.default_value_type) {
|
||||
if (ast.default_value_type == "single_quote_string") {
|
||||
ast.default_value = `'${ast.default_value}'`
|
||||
}
|
||||
@@ -128,25 +128,25 @@ ${this.commit()}
|
||||
else
|
||||
ast.default_value = "NEWID()"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if ( ast.default_value_type == "bool") {
|
||||
if (ast.default_value) {
|
||||
ast.default_value = 1 ;
|
||||
}else {
|
||||
ast.default_value = 0 ;
|
||||
if (ast.default_value_type == "bool") {
|
||||
if (ast.default_value) {
|
||||
ast.default_value = 1;
|
||||
} else {
|
||||
ast.default_value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||
|
||||
const constraintName: string = relationship.name ? " CONSTRAINT " + relationship.name : "";
|
||||
const constraintName: string = relationship.name ? " CONSTRAINT \"" + relationship.name + "\"" : "";
|
||||
|
||||
const onDeleteFKAction: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
const onUpdateFKAction: string | null = this.foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
|
||||
@@ -157,9 +157,9 @@ ${this.commit()}
|
||||
const FKActions: string = [onDeleteAction, onUpdateAction].join(" ");
|
||||
|
||||
return `
|
||||
ALTER TABLE ${targetTable.name}
|
||||
ADD ${constraintName} FOREIGN KEY (${foreignKey.name})
|
||||
REFERENCES ${sourceTable.name}(${primaryKey.name}) ${FKActions};
|
||||
ALTER TABLE "${targetTable.name}"
|
||||
ADD ${constraintName} FOREIGN KEY ("${foreignKey.name}")
|
||||
REFERENCES "${sourceTable.name}"("${primaryKey.name}") ${FKActions};
|
||||
`
|
||||
}
|
||||
|
||||
@@ -175,29 +175,30 @@ ${this.commit()}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected createIndexAst(table: TableType, index: IndexType): ASTStatment {
|
||||
const unique: string = index.unique ? " UNIQUE" : "";
|
||||
|
||||
let columns: string[] | string = index.fields.map((field: FieldType) => field.name);
|
||||
let columns: string[] | string = index.fields.map((field: FieldType) => `"${field.name}"`);
|
||||
if (columns.length == 0)
|
||||
return null;
|
||||
|
||||
columns = `(${columns.join(",")})`
|
||||
return `CREATE${unique} INDEX ${index.name} ON ${table.name} ${columns} ;`
|
||||
|
||||
return `CREATE${unique} INDEX "${index.name}" ON "${table.name}" ${columns} ;`
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected getPrimaryKeyContraint(fields: FieldType[], table?: TableType): ASTStatment {
|
||||
const pks: string[] = fields.map((field: FieldType) => field.name);
|
||||
return `CONSTRAINT PK_${table?.name} PRIMARY KEY (${pks.join(",")})`
|
||||
}
|
||||
|
||||
const pks: string[] = fields.map((field: FieldType) => `"${field.name}"`);
|
||||
return `CONSTRAINT "PK_${table?.name}" PRIMARY KEY (${pks.join(",")})`
|
||||
}
|
||||
protected startTransaction(): string {
|
||||
return `BEGIN TRANSACTION;`
|
||||
}
|
||||
protected commit(): string {
|
||||
return "COMMIT;"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export default class OracleRenderer extends BaseDatabaseRenderer {
|
||||
pk_constraint = "";
|
||||
let indexes: string = ast.indices_ast.filter((index: string) => index != null).join("\n")
|
||||
return `
|
||||
CREATE TABLE ${table.name} (
|
||||
CREATE TABLE "${table.name}" (
|
||||
${(ast as any).field_definitions.join(",\n")}
|
||||
${pk_constraint}
|
||||
);
|
||||
@@ -88,19 +88,19 @@ export default class OracleRenderer extends BaseDatabaseRenderer {
|
||||
}
|
||||
else if (dataType === "INTERVAL DAY TO SECOND" && options) {
|
||||
dataType = `INTERVAL DAY${options} TO SECOND`
|
||||
}
|
||||
else if (dataType === "INTERVAL YEAR TO MONTH" && options) {
|
||||
dataType = `INTERVAL YEAR${options} TO MONTH`
|
||||
|
||||
}
|
||||
|
||||
else
|
||||
else if (dataType === "INTERVAL YEAR TO MONTH" && options) {
|
||||
dataType = `INTERVAL YEAR${options} TO MONTH`
|
||||
|
||||
}
|
||||
|
||||
else
|
||||
dataType = dataType + options
|
||||
|
||||
|
||||
|
||||
//const dataType = field.type.name == "uuid" ? "RAW(16)" : ast.dataType;
|
||||
|
||||
return `${field.name} ${dataType} ${defaultValue} ${nullable} ${unique} ${autoIncrement} ${primaryKey}`;
|
||||
return `"${field.name}" ${dataType} ${defaultValue} ${nullable} ${unique} ${autoIncrement} ${primaryKey}`;
|
||||
}
|
||||
protected processDefaultValue(field: FieldType): AST | null {
|
||||
|
||||
@@ -144,26 +144,26 @@ export default class OracleRenderer extends BaseDatabaseRenderer {
|
||||
protected createIndexAst(table: TableType, index: IndexType): ASTStatment {
|
||||
const unique: string = index.unique ? " UNIQUE" : "";
|
||||
|
||||
let columns: string[] | string = index.fields.map((field: FieldType) => field.name);
|
||||
let columns: string[] | string = index.fields.map((field: FieldType) => `"${field.name}"`);
|
||||
if (columns.length == 0)
|
||||
return null;
|
||||
columns = `(${columns.join(",")})`
|
||||
return `CREATE${unique} INDEX ${index.name} ON ${table.name} ${columns} ;`
|
||||
return `CREATE${unique} INDEX "${index.name}" ON "${table.name}" ${columns} ;`
|
||||
|
||||
}
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||
|
||||
const constraintName: string = relationship.name ? " CONSTRAINT " + relationship.name : "";
|
||||
const constraintName: string = relationship.name ? " CONSTRAINT \"" + relationship.name + "\"" : "";
|
||||
|
||||
const FKAction: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
const onDeleteAction: string = FKAction ? `ON DELETE ${FKAction}` : ""
|
||||
|
||||
return `
|
||||
ALTER TABLE ${targetTable.name}
|
||||
ADD ${constraintName} FOREIGN KEY (${foreignKey.name})
|
||||
REFERENCES ${sourceTable.name}(${primaryKey.name}) ${onDeleteAction};
|
||||
ALTER TABLE "${targetTable.name}"
|
||||
ADD ${constraintName} FOREIGN KEY ("${foreignKey.name}")
|
||||
REFERENCES "${sourceTable.name}"("${primaryKey.name}") ${onDeleteAction};
|
||||
`
|
||||
}
|
||||
|
||||
@@ -180,9 +180,9 @@ export default class OracleRenderer extends BaseDatabaseRenderer {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected getPrimaryKeyContraint(fields: FieldType[]): ASTStatment {
|
||||
protected getPrimaryKeyContraint(fields: FieldType[]): ASTStatment {
|
||||
//throw new Error("Method not implemented.");
|
||||
const pks: string[] = fields.map((field: FieldType) => field.name);
|
||||
const pks: string[] = fields.map((field: FieldType) => `"${field.name}"`);
|
||||
return `
|
||||
PRIMARY KEY (${pks.join(",")})
|
||||
`
|
||||
|
||||
@@ -107,4 +107,9 @@ ${this.commit()}
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
|
||||
protected startTransaction(): string {
|
||||
return "BEGIN;";
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user