mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 11:15:42 +00:00
Relationship controller integrated with the api
This commit is contained in:
@@ -4,18 +4,18 @@ import { Key } from "react";
|
||||
|
||||
interface AutocompleteProps {
|
||||
|
||||
items: any[]
|
||||
items?: any[]
|
||||
label?: string,
|
||||
defaultSelection: string | undefined
|
||||
defaultSelection?: string | undefined
|
||||
onSelectionChange?: (item: any) => void,
|
||||
placeholder?: string
|
||||
isDisabled?: boolean
|
||||
selectedItem?: Key
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder }) => {
|
||||
|
||||
|
||||
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder, isDisabled, selectedItem }) => {
|
||||
|
||||
const onItemChange = (item: Key | null) => {
|
||||
onSelectionChange && onSelectionChange(item);
|
||||
@@ -23,18 +23,21 @@ const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSe
|
||||
|
||||
return (
|
||||
<HeroUiAutocomplete
|
||||
radius="sm"
|
||||
isDisabled={isDisabled}
|
||||
className="w-full"
|
||||
defaultItems={items}
|
||||
defaultItems={items || []}
|
||||
size="sm"
|
||||
onSelectionChange={onItemChange}
|
||||
defaultSelectedKey={defaultSelection as any}
|
||||
variant="bordered"
|
||||
aria-label={placeholder}
|
||||
placeholder={placeholder}
|
||||
selectedKey={selectedItem as any }
|
||||
>
|
||||
{(item: any) => <AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>}
|
||||
</HeroUiAutocomplete>
|
||||
)
|
||||
}
|
||||
|
||||
export default Autocomplete ;
|
||||
export default Autocomplete;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
export const useRelationshipName = (relationship: RelationshipType) => {
|
||||
const { t } = useTranslation();
|
||||
const name: string = useMemo(() => {
|
||||
return `${relationship.sourceTable.name}_${relationship.sourceField.name} - ${relationship.targetTable.name}_${relationship.targetField.name}_fk`
|
||||
}, [relationship, t])
|
||||
return {
|
||||
name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import {
|
||||
Modal as HeroUiModal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
useDraggable,
|
||||
} from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
interface ModalProps {
|
||||
isOpen?: boolean,
|
||||
onOpenChange: (open: boolean) => void,
|
||||
className?: string,
|
||||
backdrop?: "blur" | "transparent" | "opaque",
|
||||
title: string,
|
||||
children: ReactNode,
|
||||
actionName?: string,
|
||||
actionHandler?: () => void ,
|
||||
isDisabled? : boolean
|
||||
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action" , actionHandler , isDisabled}) => {
|
||||
|
||||
|
||||
const targetRef = React.useRef(null);
|
||||
const { moveProps } = useDraggable({ targetRef, canOverflow: true, isDisabled: !isOpen });
|
||||
|
||||
const {t} = useTranslation() ;
|
||||
|
||||
const handleAction = (onClose: () => void) => {
|
||||
actionHandler && actionHandler() ;
|
||||
onClose()
|
||||
}
|
||||
return (
|
||||
<HeroUiModal
|
||||
ref={targetRef}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
className={className}
|
||||
backdrop={backdrop}
|
||||
radius="sm"
|
||||
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader {...moveProps} className="flex flex-row gap-1" >
|
||||
{title}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{children}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="flex w-full justify-between">
|
||||
<Button color="danger" variant="light" onPress={onClose} size="sm">
|
||||
{t("modal.close")}
|
||||
</Button>
|
||||
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled}>
|
||||
{actionName}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</HeroUiModal>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default Modal;
|
||||
+62
-45
@@ -1,56 +1,73 @@
|
||||
import { Language } from "../types";
|
||||
|
||||
|
||||
export const en = {
|
||||
export const en = {
|
||||
|
||||
translation : {
|
||||
sidebar : {
|
||||
tables : "Tables" ,
|
||||
relationships : "Relationships"
|
||||
} ,
|
||||
db_controller : {
|
||||
filter : "Filter" ,
|
||||
add_table : "Add Table" ,
|
||||
add_field : "Add Field" ,
|
||||
add_index : "Add Index" ,
|
||||
add_relationship : "Add Relationship" ,
|
||||
show_code : "Show code" ,
|
||||
fields : "Fields" ,
|
||||
indexes : "Indexes" ,
|
||||
note : "Note" ,
|
||||
name : "Name" ,
|
||||
type : "Type" ,
|
||||
primary_key : "Primary Key" ,
|
||||
nullable : "Nullable" ,
|
||||
select_fields : "Select fields" ,
|
||||
unique : "Unique" ,
|
||||
table_note : "Table note" ,
|
||||
collapse : "Collapse All" ,
|
||||
primary_table : "Primary Table" ,
|
||||
referenced_table : "Referenced Table" ,
|
||||
cardinality :{
|
||||
name : "Cardinality" ,
|
||||
one_to_one : "One to One" ,
|
||||
one_to_many : "One to Many" ,
|
||||
many_to_one : "Many to One" ,
|
||||
many_to_many : "Many to Many" ,
|
||||
} ,
|
||||
delete : "Delete" ,
|
||||
field_setting : "Field Setting" ,
|
||||
table_actions : "Field Actions" ,
|
||||
field_note : "Field note" ,
|
||||
delete_field : "Delete Field" ,
|
||||
|
||||
},
|
||||
table : {
|
||||
double_click : "Double click to edit"
|
||||
translation: {
|
||||
sidebar: {
|
||||
tables: "Tables",
|
||||
relationships: "Relationships"
|
||||
},
|
||||
modal: {
|
||||
close: "Close",
|
||||
create: "Create"
|
||||
},
|
||||
db_controller: {
|
||||
filter: "Filter",
|
||||
add_table: "Add Table",
|
||||
add_field: "Add Field",
|
||||
add_index: "Add Index",
|
||||
add_relationship: "Add Relationship",
|
||||
show_code: "Show code",
|
||||
fields: "Fields",
|
||||
indexes: "Indexes",
|
||||
note: "Note",
|
||||
name: "Name",
|
||||
type: "Type",
|
||||
nullable: "Nullable",
|
||||
select_fields: "Select fields",
|
||||
unique: "Unique",
|
||||
table_note: "Table note",
|
||||
collapse: "Collapse All",
|
||||
|
||||
|
||||
|
||||
primary_key: "Primary Key",
|
||||
foreign_key: "Foreign Key",
|
||||
|
||||
source_table: "Source Table",
|
||||
target_table: "Target Table",
|
||||
|
||||
select_table: "Select table",
|
||||
select_field: "Select field",
|
||||
|
||||
|
||||
cardinality: {
|
||||
name: "Cardinality",
|
||||
one_to_one: "One to One",
|
||||
one_to_many: "One to Many",
|
||||
many_to_one: "Many to One",
|
||||
many_to_many: "Many to Many",
|
||||
|
||||
},
|
||||
delete: "Delete",
|
||||
field_setting: "Field Setting",
|
||||
table_actions: "Field Actions",
|
||||
field_note: "Field note",
|
||||
delete_field: "Delete Field",
|
||||
|
||||
create_relationship: "Create Relationship",
|
||||
relationship_error: "To create a relationship, the primary key and foreign key must be of the same type.",
|
||||
},
|
||||
table: {
|
||||
double_click: "Double click to edit"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const enLanguage : Language = {
|
||||
name : "English" ,
|
||||
export const enLanguage: Language = {
|
||||
name: "English",
|
||||
nativeName: 'English',
|
||||
code: 'en',
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
export interface Relationship {
|
||||
id: string;
|
||||
name: string;
|
||||
sourceSchema?: string;
|
||||
sourceTableId: string;
|
||||
targetSchema?: string;
|
||||
targetTableId: string;
|
||||
sourceFieldId: string;
|
||||
targetFieldId: string;
|
||||
sourceCardinality: Cardinality;
|
||||
targetCardinality: Cardinality;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
|
||||
export type RelationshipType =
|
||||
| 'one_to_one'
|
||||
| 'one_to_many'
|
||||
| 'many_to_one'
|
||||
| 'many_to_many';
|
||||
export type Cardinality = 'one' | 'many';
|
||||
@@ -3,16 +3,18 @@ import { DrizzleAppSchema, wrapPowerSyncWithDrizzle } from '@powersync/drizzle-d
|
||||
import { data_types } from './data-type-schema';
|
||||
import { tables, tablesRelations } from './table-schema';
|
||||
import { fields, fieldsRelations } from './field-schema';
|
||||
import { relationshipRelations, relationships } from './relationship-schema';
|
||||
|
||||
export const drizzleSchema = {
|
||||
data_types ,
|
||||
tables ,
|
||||
fields ,
|
||||
|
||||
relationships ,
|
||||
|
||||
// relationships
|
||||
tablesRelations ,
|
||||
fieldsRelations
|
||||
fieldsRelations ,
|
||||
relationshipRelations ,
|
||||
};
|
||||
|
||||
// Infer the PowerSync schema from your Drizzle schema
|
||||
|
||||
@@ -2,6 +2,7 @@ import { sqliteTable, text, real, integer } from 'drizzle-orm/sqlite-core';
|
||||
import { tables } from './table-schema';
|
||||
import { data_types } from './data-type-schema';
|
||||
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { relationships } from './relationship-schema';
|
||||
|
||||
|
||||
export const fields = sqliteTable("fields", {
|
||||
@@ -24,7 +25,11 @@ export const fieldsRelations = relations(fields, ({ one }) => ({
|
||||
table: one(tables, {
|
||||
fields: [fields.tableId],
|
||||
references: [tables.id],
|
||||
|
||||
}),
|
||||
sourceRelations: one(relationships),
|
||||
targetRelations: one(relationships),
|
||||
|
||||
}));
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
sqliteTable,
|
||||
text,
|
||||
} from 'drizzle-orm/sqlite-core';
|
||||
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { tables, TableType } from './table-schema';
|
||||
import { fields, FieldType } from './field-schema';
|
||||
|
||||
export const relationships = sqliteTable('relationships', {
|
||||
id: text('id').primaryKey().notNull(),
|
||||
|
||||
name: text('name'),
|
||||
|
||||
sourceTableId: text('sourceTableId')
|
||||
.notNull()
|
||||
.references(() => tables.id, { onDelete: 'cascade' }),
|
||||
|
||||
targetTableId: text('targetTableId')
|
||||
.notNull()
|
||||
.references(() => tables.id, { onDelete: 'cascade' }),
|
||||
|
||||
sourceFieldId: text('sourceFieldId')
|
||||
.notNull()
|
||||
.references(() => fields.id, { onDelete: 'cascade' }),
|
||||
|
||||
targetFieldId: text('targetFieldId')
|
||||
.notNull()
|
||||
.references(() => fields.id, { onDelete: 'cascade' }),
|
||||
|
||||
cardinality: text('cardinality', {
|
||||
enum: [
|
||||
'one_to_one',
|
||||
'one_to_many',
|
||||
'many_to_one',
|
||||
'many_to_many',
|
||||
],
|
||||
})
|
||||
.notNull()
|
||||
.default('one_to_many'),
|
||||
|
||||
sourceAliasName: text('sourceAliasName'),
|
||||
targetAliasName: text('targetAliasName'),
|
||||
createdAt: text('createdAt'),
|
||||
});
|
||||
|
||||
|
||||
export const relationshipRelations = relations(relationships, ({ one }) => ({
|
||||
sourceTable: one(tables, {
|
||||
fields: [relationships.sourceTableId],
|
||||
references: [tables.id],
|
||||
}),
|
||||
targetTable: one(tables, {
|
||||
fields: [relationships.targetTableId],
|
||||
references: [tables.id],
|
||||
}),
|
||||
sourceField: one(fields, {
|
||||
fields: [relationships.sourceFieldId],
|
||||
references: [fields.id],
|
||||
}),
|
||||
targetField: one(fields, {
|
||||
fields: [relationships.targetFieldId],
|
||||
references: [fields.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
export interface RelationshipType extends InferSelectModel<typeof relationships> {
|
||||
sourceTable : TableType ,
|
||||
targetTable : TableType ,
|
||||
sourceField : FieldType ,
|
||||
targetField : FieldType ,
|
||||
};
|
||||
|
||||
export interface RelationshipInsertType extends InferInsertModel<typeof relationships> { };
|
||||
|
||||
|
||||
export enum Cardinality {
|
||||
one_to_one = "one_to_one",
|
||||
one_to_many = "one_to_many",
|
||||
many_to_one = "many_to_one",
|
||||
many_to_many = "many_to_many"
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { sqliteTable, text, real, integer } from 'drizzle-orm/sqlite-core';
|
||||
import { fields, FieldType } from './field-schema';
|
||||
import { relationships } from './relationship-schema';
|
||||
|
||||
export const tables = sqliteTable('tables', {
|
||||
id: text('id')
|
||||
@@ -25,15 +26,17 @@ export const tables = sqliteTable('tables', {
|
||||
|
||||
export const tablesRelations = relations(tables, ({ many }) => ({
|
||||
fields: many(fields),
|
||||
sourceRelations: many(relationships),
|
||||
targetRelations: many(relationships),
|
||||
}));
|
||||
|
||||
|
||||
|
||||
export interface TableType extends InferSelectModel<typeof tables> {
|
||||
fields : FieldType[]
|
||||
};
|
||||
export interface TableType extends InferSelectModel<typeof tables> {
|
||||
fields: FieldType[]
|
||||
};
|
||||
|
||||
|
||||
export interface TableInsertType extends InferInsertModel<typeof tables> {
|
||||
fields? : FieldType[]
|
||||
export interface TableInsertType extends InferInsertModel<typeof tables> {
|
||||
fields?: FieldType[]
|
||||
};
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import Autocomplete from "@/components/auto-complete/auto-complete";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { FileKey, FileMinus2, FileOutput, Key, KeyRound } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
|
||||
|
||||
interface CreateRelationshipFormProps {
|
||||
onRelationshipChanges: (relationship: RelationshipInsertType) => void,
|
||||
onValidationChanges?: (isValid: boolean) => void
|
||||
|
||||
}
|
||||
|
||||
|
||||
const CreateRelationshipForm: React.FC<CreateRelationshipFormProps> = ({ onRelationshipChanges, onValidationChanges }) => {
|
||||
|
||||
const [relationship, setRelationship] = useState<RelationshipInsertType>({
|
||||
sourceTableId: "",
|
||||
targetTableId: "",
|
||||
sourceFieldId: "",
|
||||
targetFieldId: ""
|
||||
} as RelationshipInsertType)
|
||||
|
||||
|
||||
const { tables } = useDatabase();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sourceFields: FieldType[] = useMemo(() => {
|
||||
const sourceTable: TableType | undefined = tables.find((table: TableType) => table.id == relationship.sourceTableId);
|
||||
return sourceTable?.fields ? sourceTable.fields : [];
|
||||
}, [tables, relationship.sourceTableId]);
|
||||
|
||||
|
||||
const targetFields: FieldType[] = useMemo(() => {
|
||||
const targetTable: TableType | undefined = tables.find((table: TableType) => table.id == relationship.targetTableId);
|
||||
return targetTable?.fields ? targetTable.fields : [];
|
||||
}, [tables, relationship.targetTableId]);
|
||||
|
||||
|
||||
const fieldTypesMatches: boolean = useMemo(() => {
|
||||
const sourceField: FieldType | undefined = sourceFields.find((field: FieldType) => field.id == relationship.sourceFieldId);
|
||||
const targetField: FieldType | undefined = targetFields.find((field: FieldType) => field.id == relationship.targetFieldId);
|
||||
if (sourceField && targetField)
|
||||
return sourceField.typeId == targetField.typeId;
|
||||
else
|
||||
return true;
|
||||
}, [relationship, sourceFields, targetFields])
|
||||
|
||||
|
||||
useEffect(() => setRelationship({ ...relationship, sourceFieldId: "" }), [sourceFields]);
|
||||
useEffect(() => setRelationship({ ...relationship, targetFieldId: "" }), [targetFields]);
|
||||
|
||||
useEffect(() => {
|
||||
onRelationshipChanges && onRelationshipChanges(relationship)
|
||||
}, [relationship , sourceFields , targetFields]);
|
||||
|
||||
useEffect(() => {
|
||||
onValidationChanges && onValidationChanges ((relationship.sourceFieldId && relationship.targetFieldId && fieldTypesMatches) as boolean)
|
||||
}, [relationship, fieldTypesMatches])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-2 grid-rows-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
|
||||
<FileOutput className="size-4 text-icon" />
|
||||
{t("db_controller.source_table")}
|
||||
</label>
|
||||
<Autocomplete
|
||||
items={tables}
|
||||
placeholder={t("db_controller.select_table")}
|
||||
onSelectionChange={(key) => setRelationship({ ...relationship, sourceTableId: key })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
|
||||
<FileMinus2 className="size-4 text-icon" />
|
||||
{t("db_controller.target_table")}
|
||||
</label>
|
||||
|
||||
<Autocomplete
|
||||
items={tables}
|
||||
placeholder={t("db_controller.select_table")}
|
||||
onSelectionChange={(key) => setRelationship({ ...relationship, targetTableId: key })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
|
||||
<KeyRound className="size-4 text-icon" />
|
||||
{t("db_controller.primary_key")}
|
||||
|
||||
</label>
|
||||
<Autocomplete
|
||||
placeholder={t("db_controller.select_field")}
|
||||
items={sourceFields}
|
||||
onSelectionChange={(key) => setRelationship({ ...relationship, sourceFieldId: key })}
|
||||
isDisabled={!relationship.sourceTableId}
|
||||
selectedItem={relationship.sourceFieldId}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
|
||||
<FileKey className="size-4 text-icon" />
|
||||
{t("db_controller.foreign_key")}
|
||||
</label>
|
||||
<Autocomplete
|
||||
placeholder={t("db_controller.select_field")}
|
||||
onSelectionChange={(key) => setRelationship({ ...relationship, targetFieldId: key })}
|
||||
isDisabled={!relationship.targetTableId}
|
||||
items={targetFields}
|
||||
selectedItem={relationship.targetFieldId}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
!fieldTypesMatches &&
|
||||
<div className="w-full text-danger text-sm ">
|
||||
{t("db_controller.relationship_error")}
|
||||
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default CreateRelationshipForm;
|
||||
+40
-27
@@ -1,45 +1,59 @@
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { Button, Select, SelectItem } from "@heroui/react";
|
||||
import { Cardinality, RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { Button, Select, SelectItem, SharedSelection } from "@heroui/react";
|
||||
import { ChevronsLeftRightEllipsis, FileMinus2, FileOutput, SquareArrowLeft, SquareArrowRight, Trash2 } from "lucide-react";
|
||||
import { Key, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
|
||||
interface RelationshipAccordionBodyProps {
|
||||
relationship: RelationshipType
|
||||
}
|
||||
|
||||
|
||||
export const cardinalities = [
|
||||
{ key: "one2one", label: "One to One" },
|
||||
{ key: "one2many", label: "One to Many" },
|
||||
{ key: "many2one", label: "Many to One" },
|
||||
{ key: "many2many", label: "Many 2 many" },
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
const RelationshipAccordionBody: React.FC<Props> = ({ }) => {
|
||||
const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ relationship }) => {
|
||||
const [cardinality, setCardinality] = useState(new Set([relationship.cardinality]));
|
||||
const { editRelationship, deleteRelationship } = useDatabase();
|
||||
|
||||
const {t } = useTranslation() ;
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const changeCardinality = (keys: SharedSelection) => {
|
||||
|
||||
if (keys.anchorKey != relationship.cardinality)
|
||||
editRelationship({
|
||||
id: relationship.id,
|
||||
cardinality: keys.anchorKey as Cardinality
|
||||
} as RelationshipInsertType);
|
||||
|
||||
setCardinality(keys as any);
|
||||
}
|
||||
const removeRelationship = () => {
|
||||
deleteRelationship(relationship.id);
|
||||
}
|
||||
return (
|
||||
<div className="w-full p-2 space-y-4">
|
||||
<div className="flex">
|
||||
<div className="w-full space-y-1">
|
||||
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
|
||||
<FileOutput className="size-4" />
|
||||
{t("db_controller.primary_table")}
|
||||
{t("db_controller.source_table")}
|
||||
</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="truncate text-left text-sm">
|
||||
users(id)
|
||||
{relationship.sourceTable.name}({relationship.sourceField.name})
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
users(id)
|
||||
|
||||
{relationship.sourceTable.name}({relationship.sourceField.name})
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -47,18 +61,18 @@ const RelationshipAccordionBody: React.FC<Props> = ({ }) => {
|
||||
<div className="w-full space-y-1">
|
||||
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
|
||||
<FileMinus2 className="size-4" />
|
||||
{t("db_controller.referenced_table")}
|
||||
{t("db_controller.target_table")}
|
||||
|
||||
</label>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="truncate text-left text-sm ">
|
||||
products(user_id)
|
||||
{relationship.targetTable.name}({relationship.targetField.name})
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
products(user_id)
|
||||
{relationship.targetTable.name}({relationship.targetField.name})
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -72,16 +86,14 @@ const RelationshipAccordionBody: React.FC<Props> = ({ }) => {
|
||||
className="w-full"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
selectedKeys={cardinality}
|
||||
onSelectionChange={changeCardinality}
|
||||
|
||||
>
|
||||
<SelectItem key={"one2one"}>{t("db_controller.cardinality.one_to_one")}</SelectItem>
|
||||
|
||||
<SelectItem key={"one2many"}>{t("db_controller.cardinality.one_to_many")}</SelectItem>
|
||||
|
||||
<SelectItem key={"many2one"}>{t("db_controller.cardinality.many_to_one")}</SelectItem>
|
||||
|
||||
<SelectItem key={"many2many"}>{t("db_controller.cardinality.many_to_many")}</SelectItem>
|
||||
|
||||
|
||||
<SelectItem key={Cardinality.one_to_one}>{t("db_controller.cardinality.one_to_one")}</SelectItem>
|
||||
<SelectItem key={Cardinality.one_to_many}>{t("db_controller.cardinality.one_to_many")}</SelectItem>
|
||||
<SelectItem key={Cardinality.many_to_one}>{t("db_controller.cardinality.many_to_one")}</SelectItem>
|
||||
<SelectItem key={Cardinality.many_to_many}>{t("db_controller.cardinality.many_to_many")}</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
@@ -90,6 +102,7 @@ const RelationshipAccordionBody: React.FC<Props> = ({ }) => {
|
||||
className="bg-transparent text-xs"
|
||||
radius="sm"
|
||||
size="sm"
|
||||
onPressEnd={removeRelationship}
|
||||
>
|
||||
<Trash2 className="mr-1 size-3.5 text-danger" />
|
||||
<div className="text-danger font-semibold">
|
||||
|
||||
+26
-6
@@ -1,18 +1,38 @@
|
||||
import { useRelationshipName } from "@/components/hooks/use-relationship-name";
|
||||
import { randomColor } from "@/lib/colors";
|
||||
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { Button, cn, Input } from "@heroui/react";
|
||||
import { Check, ChevronRight, EllipsisVertical, Focus, Pencil } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
|
||||
interface RelationshipAccordionHeaderProps {
|
||||
isOpen?: boolean;
|
||||
relationship: RelationshipType
|
||||
}
|
||||
|
||||
|
||||
const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> = ({ isOpen }) => {
|
||||
const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> = ({ isOpen, relationship }) => {
|
||||
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const { name: defaultName } = useRelationshipName(relationship);
|
||||
const { editRelationship } = useDatabase();
|
||||
|
||||
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName)
|
||||
|
||||
|
||||
const editRelationshipName = () => {
|
||||
if (relationship.name || name.trim().toLocaleLowerCase() != defaultName)
|
||||
editRelationship({
|
||||
id: relationship.id,
|
||||
name
|
||||
} as RelationshipInsertType);
|
||||
setEditMode(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group w-full flex h-12 gap-1 flex p-2 items-center" >
|
||||
<div className={cn(
|
||||
@@ -21,8 +41,6 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
|
||||
)}>
|
||||
<ChevronRight className="size-4 text-icon" />
|
||||
</div>
|
||||
|
||||
|
||||
{
|
||||
editMode && <>
|
||||
<Input
|
||||
@@ -30,7 +48,9 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
|
||||
autoFocus
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onBlur={() => setEditMode(false)}
|
||||
value={name}
|
||||
onValueChange={setName}
|
||||
onBlur={editRelationshipName}
|
||||
type="text"
|
||||
className="rounded-md px-2 py-0.5 w-full border-blue-400 focus-visible:ring-0 dark:bg-slate-900 text-sm "
|
||||
/>
|
||||
@@ -38,7 +58,7 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
|
||||
variant="light"
|
||||
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||
size="sm"
|
||||
onPress={() => setEditMode(false)}
|
||||
onPress={editRelationshipName}
|
||||
isIconOnly
|
||||
>
|
||||
<Check className="size-4 text-icon" />
|
||||
@@ -52,7 +72,7 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
|
||||
<label
|
||||
className="w-full truncate px-2 py-1 text-sm font-semibold text-black"
|
||||
>
|
||||
users one or many posts
|
||||
{relationship.name ? relationship.name : defaultName}
|
||||
</label>
|
||||
<div className="hidden shrink-0 flex-row group-hover:flex">
|
||||
|
||||
|
||||
+55
-13
@@ -1,10 +1,16 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"
|
||||
import { Accordion, AccordionItem, Button, Input } from "@heroui/react"
|
||||
import { Accordion, AccordionItem, Button, Input, useDisclosure } from "@heroui/react"
|
||||
import { Code, List, ListCollapse, Table, Workflow } from "lucide-react"
|
||||
import { useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import RelationshipAccordionHeader from "./relationship-accordion-item/relationship-accordion-header";
|
||||
import RelationshipAccordionBody from "./relationship-accordion-item/relationship-accordion-body";
|
||||
import Modal from "@/components/modal/modal";
|
||||
import CreateRelationshipForm from "./create-relationship-form/create-relationship-form";
|
||||
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,10 +20,26 @@ interface Props {
|
||||
|
||||
|
||||
const RelationshipController: React.FC<Props> = ({ }) => {
|
||||
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3", "Item 4"]);
|
||||
|
||||
|
||||
const [relationship, setRelationship] = useState<RelationshipInsertType | undefined>(undefined);
|
||||
const [isValid, setIsValid] = useState<boolean>(false);
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
const { createRelationship, relationships } = useDatabase();
|
||||
const { t } = useTranslation();
|
||||
const [selectedRelationship, setSelectedRelationship] = useState(new Set([]));
|
||||
|
||||
const addRelationship = useCallback(() => {
|
||||
|
||||
const newRelationshipId: string = v4();
|
||||
|
||||
createRelationship({
|
||||
id: newRelationshipId,
|
||||
...relationship,
|
||||
createdAt: new Date().toISOString()
|
||||
} as RelationshipInsertType);
|
||||
|
||||
setSelectedRelationship(new Set([newRelationshipId]) as any);
|
||||
}, [relationship]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-2">
|
||||
@@ -34,6 +56,7 @@ const RelationshipController: React.FC<Props> = ({ }) => {
|
||||
//setShowDBML((value) => !value)
|
||||
console.log("hello world ")
|
||||
}
|
||||
|
||||
>
|
||||
<ListCollapse className="size-4" />
|
||||
</Button>
|
||||
@@ -62,6 +85,7 @@ const RelationshipController: React.FC<Props> = ({ }) => {
|
||||
variant="solid"
|
||||
radius="sm"
|
||||
color="primary"
|
||||
onPressEnd={onOpen}
|
||||
startContent={
|
||||
<Workflow className="h-4 w-4 " />
|
||||
}
|
||||
@@ -75,26 +99,44 @@ const RelationshipController: React.FC<Props> = ({ }) => {
|
||||
<Accordion
|
||||
hideIndicator
|
||||
dividerProps={{
|
||||
className : "bg-default-200"
|
||||
className: "bg-default-200"
|
||||
}}
|
||||
isCompact
|
||||
selectedKeys={selectedRelationship}
|
||||
onSelectionChange={setSelectedRelationship as any}
|
||||
>
|
||||
{items.map(item => (
|
||||
{relationships.map((relationship: RelationshipType) => (
|
||||
<AccordionItem
|
||||
key={item}
|
||||
aria-label={item}
|
||||
key={relationship.id}
|
||||
aria-label={relationship.id}
|
||||
classNames={{
|
||||
trigger: "w-full hover:bg-default transition-all duration-200",
|
||||
base: "rounded-md p-0 overflow-hidden",
|
||||
trigger: "w-full hover:bg-default transition-all duration-200 h-12",
|
||||
base: "rounded-md p-0 overflow-hidden",
|
||||
}}
|
||||
subtitle={
|
||||
<RelationshipAccordionHeader/>
|
||||
<RelationshipAccordionHeader relationship={relationship} />
|
||||
}
|
||||
>
|
||||
<RelationshipAccordionBody/>
|
||||
<RelationshipAccordionBody relationship={relationship} />
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("db_controller.create_relationship")}
|
||||
actionName={t("modal.create")}
|
||||
className="min-w-[520px]"
|
||||
isDisabled={!isValid}
|
||||
actionHandler={addRelationship}
|
||||
|
||||
>
|
||||
<CreateRelationshipForm
|
||||
onRelationshipChanges={setRelationship}
|
||||
onValidationChanges={setIsValid}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
-1
@@ -63,7 +63,6 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
} as FieldType);
|
||||
}
|
||||
|
||||
|
||||
const toggleNullable = (nullable: boolean) => {
|
||||
editField({
|
||||
id: field.id,
|
||||
|
||||
+3
-2
@@ -50,6 +50,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Accordion
|
||||
isCompact
|
||||
variant={"light"} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys as any}>
|
||||
<AccordionItem key="fields" aria-label="Fields"
|
||||
indicator={({ isOpen }) => (
|
||||
@@ -92,7 +93,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
</div>
|
||||
)}
|
||||
classNames={{
|
||||
trigger: "hover:bg-default"
|
||||
trigger: "hover:bg-default h-6"
|
||||
}}
|
||||
subtitle={
|
||||
<div className="flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
|
||||
@@ -114,7 +115,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
</div>
|
||||
)}
|
||||
classNames={{
|
||||
trigger: "hover:bg-default"
|
||||
trigger: "hover:bg-default h-6"
|
||||
}}
|
||||
subtitle={
|
||||
<div className="flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
|
||||
|
||||
@@ -101,13 +101,15 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
variant="splitted"
|
||||
selectedKeys={selectedTable}
|
||||
onSelectionChange={setSelectedTable as any}
|
||||
isCompact
|
||||
|
||||
>
|
||||
{tables.map((table: TableType) => (
|
||||
<AccordionItem
|
||||
key={table.id}
|
||||
aria-label={table.name}
|
||||
classNames={{
|
||||
trigger: "w-full hover:bg-default transition-all duration-200",
|
||||
trigger: "w-full h-12 hover:bg-default transition-all duration-200",
|
||||
base: "rounded-md shadow p-0 overflow-hidden",
|
||||
}}
|
||||
subtitle={
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
import { createContext } from "react";
|
||||
@@ -10,17 +11,21 @@ import { createContext } from "react";
|
||||
|
||||
export interface DatabaseContextType {
|
||||
tables: TableType[],
|
||||
data_types: DataType[]
|
||||
|
||||
data_types: DataType[] ,
|
||||
relationships : RelationshipType[] ,
|
||||
// table operations
|
||||
createTable: (table: TableInsertType) => Promise<QueryResult>,
|
||||
editTable: (table: TableInsertType) => Promise<QueryResult>,
|
||||
deleteTable: (id: string) => Promise<QueryResult>,
|
||||
|
||||
// field operations
|
||||
createField: (field: FieldInsertType) => Promise<QueryResult>,
|
||||
editField: (field: FieldInsertType) => Promise<QueryResult>,
|
||||
deleteField: (id: string) => Promise<QueryResult>,
|
||||
orderTableFields: (fields: FieldType[]) => Promise<QueryResult>
|
||||
|
||||
orderTableFields: (fields: FieldType[]) => Promise<QueryResult> ,
|
||||
// relationship operations
|
||||
createRelationship : ( relationship : RelationshipInsertType) => Promise<QueryResult> ,
|
||||
editRelationship : ( relationship : RelationshipInsertType) => Promise<QueryResult> ,
|
||||
deleteRelationship : ( id : string) => Promise<QueryResult> ,
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +33,3 @@ export interface DatabaseContextType {
|
||||
|
||||
export default createContext<DatabaseContextType>({} as DatabaseContextType);
|
||||
|
||||
/*
|
||||
|
||||
return Promise.all(
|
||||
fields.map((field : FieldType , index : number) => editField({id : field.id , sequence : index} as FieldType))
|
||||
)
|
||||
|
||||
|
||||
|
||||
*/
|
||||
@@ -8,6 +8,7 @@ import { toCompilableQuery } from "@powersync/drizzle-driver";
|
||||
import { asc, desc, eq, sql } from "drizzle-orm";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
import { FieldInsertType, fields, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { RelationshipInsertType, relationships, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
|
||||
interface Props { children: React.ReactNode }
|
||||
|
||||
@@ -26,6 +27,18 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
})
|
||||
));
|
||||
|
||||
|
||||
const { data: relationshipsList } = useQuery(toCompilableQuery(
|
||||
db.query.relationships.findMany({
|
||||
with: {
|
||||
sourceTable: true,
|
||||
targetTable: true,
|
||||
sourceField: true,
|
||||
targetField: true,
|
||||
},
|
||||
orderBy: desc(tables.createdAt)
|
||||
})
|
||||
));
|
||||
const { data: data_types } = useQuery(toCompilableQuery(
|
||||
db.query.data_types.findMany()
|
||||
));
|
||||
@@ -52,24 +65,32 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}
|
||||
|
||||
const orderTableFields = async (fields: FieldType[]): Promise<QueryResult> => {
|
||||
|
||||
const caseStatements = fields
|
||||
.map((field: FieldType, index: number) => `WHEN '${field.id}' THEN ${index}`)
|
||||
.join('\n ');
|
||||
|
||||
const ids = fields.map(u => `'${u.id}'`).join(',\n ');
|
||||
|
||||
const sql = `
|
||||
UPDATE fields
|
||||
SET sequence = CASE id
|
||||
${caseStatements}
|
||||
END
|
||||
WHERE id IN (
|
||||
${ids}
|
||||
);`;
|
||||
|
||||
UPDATE fields
|
||||
SET sequence = CASE id
|
||||
${caseStatements}
|
||||
END
|
||||
WHERE id IN (
|
||||
${ids}
|
||||
);`;
|
||||
return await powerSyncDb.execute(sql)
|
||||
}
|
||||
|
||||
const createRelationship = async (relationship: RelationshipInsertType): Promise<QueryResult> => {
|
||||
return await db.insert(relationships).values(relationship);
|
||||
}
|
||||
const editRelationship = async (relationship: RelationshipInsertType): Promise<QueryResult> => {
|
||||
return await db.update(relationships).set(relationship).where(eq(relationships.id, relationship.id))
|
||||
}
|
||||
const deleteRelationship = async (id: string): Promise<QueryResult> => {
|
||||
return await db.delete(relationships).where(eq(relationships.id, id));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<DatabaseContext.Provider value={{
|
||||
@@ -81,7 +102,13 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
editField,
|
||||
deleteField,
|
||||
orderTableFields,
|
||||
|
||||
createRelationship,
|
||||
editRelationship,
|
||||
deleteRelationship,
|
||||
|
||||
tables: tablesList as TableType[],
|
||||
relationships : relationshipsList as RelationshipType[] ,
|
||||
data_types
|
||||
}}>
|
||||
{children}
|
||||
|
||||
@@ -48,4 +48,9 @@ div[data-slot="mainWrapper"] button[data-slot="trigger"] {
|
||||
|
||||
div[data-slot="content"] hr[role="separator"] {
|
||||
display: none ;
|
||||
}
|
||||
|
||||
|
||||
button[data-slot="trogger"] {
|
||||
background-color: red !important;
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ export default {
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form).js",
|
||||
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form|modal).js",
|
||||
|
||||
],
|
||||
darkMode: "class",
|
||||
|
||||
Reference in New Issue
Block a user