mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 03:05:42 +00:00
Relationship and Tables in diagram integrated with the API
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
export interface CardinalityMarkerProps {
|
||||
|
||||
selected?: boolean,
|
||||
direction?: "start" | "end",
|
||||
type: "one" | "many"
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false, type, direction = "start" }) => {
|
||||
|
||||
const id = `${type}_${direction}${selected ? "_selected" : ""}`;
|
||||
const renderMarker = () => {
|
||||
if (type == "many") {
|
||||
if (direction == "start")
|
||||
return (<path d="M 0 50 L 100 50 M 100 50 L 0 0 M 100 50 L 0 100 " />)
|
||||
else if (direction == "end")
|
||||
return (<path d="M 100 50 L 0 50 M 0 50 L 100 0 M 0 50 L 100 100" />)
|
||||
|
||||
}
|
||||
|
||||
if (type == "one") {
|
||||
if (direction == "start") {
|
||||
return (<path d="M 0 50 L 100 50 M 50 50 M 50 50 M 75 25 L 75 75" />)
|
||||
}
|
||||
if (direction == "end") {
|
||||
return (<path d="M 100 50 L 0 50 M 50 50 M 50 50 M 25 25 L 25 75" />)
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<marker
|
||||
id={id}
|
||||
markerWidth="24"
|
||||
markerHeight="24"
|
||||
refX="12"
|
||||
refY="12"
|
||||
orient="auto"
|
||||
markerUnits="userSpaceOnUse"
|
||||
>
|
||||
<svg
|
||||
fill="transparent"
|
||||
className={selected ? "stroke-primary" : "stroke-slate-300"}
|
||||
strokeWidth="8"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 100 100">
|
||||
{
|
||||
renderMarker()
|
||||
}
|
||||
</svg>
|
||||
</marker>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default CardinalityMarker;
|
||||
@@ -1,22 +1,26 @@
|
||||
import { colorOptions } from "@/lib/colors";
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
|
||||
import { Slash } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ColorPickerProps {
|
||||
defaultColor?: string;
|
||||
onChange?: (color: string) => void
|
||||
onChange?: (color: string | undefined) => void
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) => {
|
||||
const [color , setColor] = useState<string>( defaultColor as string ) ;
|
||||
const [color, setColor] = useState<string | undefined>(defaultColor as string);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onSelect = useCallback((color: string) => {
|
||||
const onSelect = useCallback((color: string | undefined) => {
|
||||
setIsOpen(false);
|
||||
onChange && onChange(color) ;
|
||||
setColor ( color)
|
||||
onChange && onChange(color);
|
||||
setColor(color)
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
@@ -26,7 +30,7 @@ const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) =>
|
||||
className="size-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md"
|
||||
isIconOnly
|
||||
style={{
|
||||
backgroundColor : color
|
||||
backgroundColor: color ? color : undefined
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -46,6 +50,26 @@ const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) =>
|
||||
|
||||
</div>
|
||||
))}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
key={undefined}
|
||||
className="size-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md"
|
||||
style={{
|
||||
backgroundColor: "white"
|
||||
}}
|
||||
onClick={() => onSelect(undefined)}
|
||||
>
|
||||
<Slash className="size-full text-danger-500" />
|
||||
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("color_picker.default_color")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
+3
-1
@@ -6,7 +6,9 @@ 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`
|
||||
if ( !relationship.sourceTable || !relationship.targetTable || !relationship.sourceField || !relationship.targetField)
|
||||
return "" ;
|
||||
return `${relationship.sourceTable?.name}_${relationship.sourceField?.name} - ${relationship.targetTable?.name}_${relationship.targetField?.name}_fk`
|
||||
}, [relationship, t])
|
||||
return {
|
||||
name
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { LEFT_PREFIX, TARGET_PREFIX } from "@/pages/database/table/field";
|
||||
|
||||
import { Edge, useReactFlow } from "@xyflow/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const useRelationshipToEdge = (relationships: RelationshipType[]): void => {
|
||||
const { setEdges } = useReactFlow();
|
||||
useEffect(() => {
|
||||
const edges = relationships.map((relationship: RelationshipType) => {
|
||||
return {
|
||||
id: relationship.id,
|
||||
source: relationship.sourceTableId,
|
||||
sourceHandle: LEFT_PREFIX + relationship.sourceFieldId,
|
||||
target: relationship.targetTableId,
|
||||
targetHandle: TARGET_PREFIX + relationship.targetFieldId,
|
||||
data : {
|
||||
relationship
|
||||
}
|
||||
} as Edge
|
||||
})
|
||||
setEdges(edges)
|
||||
}, [relationships])
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { Node, useReactFlow } from "@xyflow/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const useTableToNode = (tables: TableType[]): void => {
|
||||
const { setNodes } = useReactFlow();
|
||||
useEffect(() => {
|
||||
const nodes = tables.map((table: TableType) => {
|
||||
return {
|
||||
id: table.id,
|
||||
type: "table",
|
||||
position: {
|
||||
x: table.posX,
|
||||
y: table.posY
|
||||
},
|
||||
data: {
|
||||
table
|
||||
},
|
||||
style: {
|
||||
width: 224
|
||||
}
|
||||
} as Node
|
||||
})
|
||||
setNodes(nodes)
|
||||
}, [tables])
|
||||
|
||||
}
|
||||
@@ -12,6 +12,9 @@ export const en = {
|
||||
close: "Close",
|
||||
create: "Create"
|
||||
},
|
||||
color_picker : {
|
||||
default_color : "Default color"
|
||||
} ,
|
||||
db_controller: {
|
||||
filter: "Filter",
|
||||
add_table: "Add Table",
|
||||
@@ -53,6 +56,8 @@ export const en = {
|
||||
delete: "Delete",
|
||||
field_setting: "Field Setting",
|
||||
table_actions: "Field Actions",
|
||||
actions : "Actions" ,
|
||||
|
||||
field_note: "Field note",
|
||||
delete_field: "Delete Field",
|
||||
|
||||
|
||||
+19
-2
@@ -1,9 +1,8 @@
|
||||
|
||||
export const colorOptions = [
|
||||
"#fd7f6f", // Warm coral red
|
||||
"#7eb0d5", // Soft sky blue
|
||||
"#b2e061", // Fresh lime green
|
||||
"#bd7ebe", // Soft lavender pink
|
||||
|
||||
"#ffb55a", // Vibrant orange
|
||||
"#ffee65", // Warm yellow
|
||||
"#beb9db", // Light purple grey
|
||||
@@ -16,6 +15,24 @@ export const colorOptions = [
|
||||
|
||||
];
|
||||
|
||||
/*
|
||||
|
||||
export const colorOptions = [
|
||||
'#ff6363', // A brighter red.
|
||||
'#ff6b8a', // A vibrant pink.
|
||||
'#c05dcf', // A rich purple.
|
||||
'#b067e9', // A lighter purple.
|
||||
'#8a61f5', // A bold indigo.
|
||||
'#7175fa', // A lighter indigo.
|
||||
'#8eb7ff', // A sky blue.
|
||||
'#42e0c0', // A fresh aqua.
|
||||
'#4dee8a', // A mint green.
|
||||
'#9ef07a', // A lime green.
|
||||
'#ffe374', // A warm yellow.
|
||||
'#ff9f74', // A peachy orange.
|
||||
];
|
||||
*/
|
||||
|
||||
export const randomColor = () => {
|
||||
return colorOptions[Math.floor(Math.random() * colorOptions.length)];
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { DrizzleAppSchema, wrapPowerSyncWithDrizzle } from '@powersync/drizzle-driver';
|
||||
import { data_types } from './data-type-schema';
|
||||
import { data_types, dataTypeRelations } from './data-type-schema';
|
||||
import { tables, tablesRelations } from './table-schema';
|
||||
import { fields, fieldsRelations } from './field-schema';
|
||||
import { relationshipRelations, relationships } from './relationship-schema';
|
||||
@@ -15,6 +15,7 @@ export const drizzleSchema = {
|
||||
tablesRelations ,
|
||||
fieldsRelations ,
|
||||
relationshipRelations ,
|
||||
dataTypeRelations
|
||||
};
|
||||
|
||||
// Infer the PowerSync schema from your Drizzle schema
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { InferSelectModel } from 'drizzle-orm';
|
||||
import { InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { fields } from './field-schema';
|
||||
|
||||
export const data_types = sqliteTable('data_types', {
|
||||
id : text("id") ,
|
||||
@@ -8,4 +9,9 @@ export const data_types = sqliteTable('data_types', {
|
||||
|
||||
|
||||
|
||||
export const dataTypeRelations = relations(fields, ({ many }) => ({
|
||||
fields : many(fields) ,
|
||||
}));
|
||||
|
||||
|
||||
export interface DataType extends InferSelectModel<typeof data_types> { };
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sqliteTable, text, real, integer } from 'drizzle-orm/sqlite-core';
|
||||
import { tables } from './table-schema';
|
||||
import { data_types } from './data-type-schema';
|
||||
import { data_types, DataType } from './data-type-schema';
|
||||
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { relationships } from './relationship-schema';
|
||||
|
||||
@@ -25,8 +25,11 @@ export const fieldsRelations = relations(fields, ({ one }) => ({
|
||||
table: one(tables, {
|
||||
fields: [fields.tableId],
|
||||
references: [tables.id],
|
||||
|
||||
}),
|
||||
type : one(data_types , {
|
||||
fields : [ fields.typeId] ,
|
||||
references : [data_types.id]
|
||||
}) ,
|
||||
sourceRelations: one(relationships),
|
||||
targetRelations: one(relationships),
|
||||
|
||||
@@ -34,7 +37,8 @@ export const fieldsRelations = relations(fields, ({ one }) => ({
|
||||
|
||||
|
||||
export interface FieldType extends InferSelectModel<typeof fields> {
|
||||
sequence: number
|
||||
sequence: number ,
|
||||
type : DataType
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,209 +1,153 @@
|
||||
|
||||
import { addEdge, applyEdgeChanges, applyNodeChanges, Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
|
||||
import { addEdge, applyEdgeChanges, applyNodeChanges, Background, Connection, Controls, Edge, EdgeChange, EdgeRemoveChange, MiniMap, Node, NodeChange, NodePositionChange, NodeRemoveChange, NodeSelectionChange, OnEdgesChange, OnNodesChange, ReactFlow, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react";
|
||||
import { SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Table from "./table/table";
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { Relationship } from "./table/relationship";
|
||||
import DBController from "./db-controller/db-controller";
|
||||
import { Table as TableType} from "@/lib/schemas/table-schema";
|
||||
|
||||
import Relationship from "./table/relationship";
|
||||
import DBController from "./db-controller/db-controller";
|
||||
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { useTableToNode } from "@/hooks/use-table-to-node";
|
||||
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
|
||||
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { v4 } from "uuid";
|
||||
import { LEFT_PREFIX, TARGET_PREFIX } from "./table/field";
|
||||
import CardinalityMarker from "@/components/cardinality-marker/cardinality-marker";
|
||||
|
||||
|
||||
interface DatabaseProps {
|
||||
initialTables?: TableType
|
||||
}
|
||||
|
||||
|
||||
const initialNodes = [
|
||||
{
|
||||
id: '1', type: "table", position: { x: 500, y: 500 }, data: {
|
||||
|
||||
table: {
|
||||
id: "1",
|
||||
name: "users",
|
||||
fields: [
|
||||
{
|
||||
id: 'e5n46eojbyxpg65pb1sesysi2',
|
||||
name: 'id',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: true,
|
||||
unique: true,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: '7nbmg2bt6dzltr8rfouxctdlc',
|
||||
name: 'reservation_id',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'f9kcpflp010xe5ad3rmexla63',
|
||||
name: 'rating',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: '3znekob0pqusyvdoq14nhlvgo',
|
||||
name: 'comment',
|
||||
type: {
|
||||
id: 'character_varying',
|
||||
name: 'character varying',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
style: { width: 224 }
|
||||
},
|
||||
|
||||
{
|
||||
id: '2', type: "table", position: { x: 200, y: 500 }, data: {
|
||||
|
||||
table: {
|
||||
id: "1",
|
||||
name: "products",
|
||||
fields: [
|
||||
{
|
||||
id: 'e5n46eojbyxpg65pb1sesysi2',
|
||||
name: 'id',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: true,
|
||||
unique: true,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: '7nbmg2bt6dzltr8rfouxctdlc',
|
||||
name: 'user_id',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'f9kcpflp010xe5ad3rmexla63',
|
||||
name: 'rating',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'f9kcpflp010xe5ad3rmexla63',
|
||||
name: 'rating',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'f9kcpflp010xe5ad3rmexla63',
|
||||
name: 'rating',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: '3znekob0pqusyvdoq14nhlvgo',
|
||||
name: 'comment',
|
||||
type: {
|
||||
id: 'character_varying',
|
||||
name: 'character varying',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
style: { width: 224 }
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: any[] = [];
|
||||
|
||||
|
||||
const edgeTypes = {
|
||||
'relationship-edge': Relationship,
|
||||
};
|
||||
|
||||
const DatabasePage: React.FC<DatabaseProps> = ({ initialTables }) => {
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const { tables, relationships, updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabase();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
|
||||
useTableToNode(tables);
|
||||
useRelationshipToEdge(relationships);
|
||||
|
||||
|
||||
const nodeTypes = useMemo(() => ({ table: Table }), []);
|
||||
const onConnect = useCallback((params: any) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
|
||||
createRelationship({
|
||||
id: v4(),
|
||||
sourceTableId: connection.source,
|
||||
targetTableId: connection.target,
|
||||
sourceFieldId: (connection.sourceHandle as string).split("_").pop(),
|
||||
targetFieldId: (connection.targetHandle as string).replace(TARGET_PREFIX, "")
|
||||
} as RelationshipInsertType)
|
||||
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
|
||||
}, [setEdges]);
|
||||
|
||||
const handleNodesChanges: OnNodesChange<never> = useCallback((changes: NodeChange<never>[]) => {
|
||||
|
||||
const nodePositionChanges: NodePositionChange[] = changes.filter((change: NodeChange) => change.type == "position" && !change.dragging) as NodePositionChange[];
|
||||
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
|
||||
|
||||
if (nodePositionChanges.length > 0)
|
||||
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
|
||||
id: change.id,
|
||||
posX: change.position?.x,
|
||||
posY: change.position?.y
|
||||
} as TableInsertType)));
|
||||
|
||||
|
||||
if (nodeRemoveChanges.length > 0)
|
||||
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
|
||||
|
||||
onNodesChange(changes);
|
||||
|
||||
}, [onNodesChange]);
|
||||
|
||||
|
||||
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
|
||||
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
|
||||
if (edgeRemoveChanges.length > 0) {
|
||||
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id))
|
||||
}
|
||||
onEdgesChange(changes as EdgeChange<never>[]);
|
||||
}, [onEdgesChange]);
|
||||
|
||||
|
||||
const selectedTableIds: string[] = useMemo(() => {
|
||||
return nodes.filter((node: Node) => node.selected).map((node: Node) => node.id)
|
||||
}, [nodes]);
|
||||
|
||||
const selectedRelationshipIds: string[] = useMemo(() => {
|
||||
return relationships.filter((relationship: RelationshipType) =>
|
||||
selectedTableIds.includes(relationship.sourceTableId) ||
|
||||
selectedTableIds.includes(relationship.targetTableId)
|
||||
).map((relationship: RelationshipType) => relationship.id);
|
||||
}, [selectedTableIds, relationships]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setEdges((edges: any) => {
|
||||
return edges.map((edge: any) => {
|
||||
const selected: boolean = selectedRelationshipIds.includes(edge.id);
|
||||
return {
|
||||
...edge ,
|
||||
animated : selected ,
|
||||
|
||||
} as Edge
|
||||
})
|
||||
|
||||
})
|
||||
}, [setEdges, selectedRelationshipIds])
|
||||
return (
|
||||
<div className="w-full h-screen flex">
|
||||
|
||||
<DBController />
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
fitView
|
||||
className="w-full h-full"
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
className="w-full h-full nodes-animated"
|
||||
onNodesChange={handleNodesChanges}
|
||||
onEdgesChange={handleEdgeChanges}
|
||||
onConnect={onConnect}
|
||||
defaultEdgeOptions={{
|
||||
animated: false,
|
||||
|
||||
type: 'relationship-edge',
|
||||
}}
|
||||
|
||||
onlyRenderVisibleElements
|
||||
panOnDrag={true}
|
||||
zoomOnScroll={true}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
snapGrid={[20, 20]}
|
||||
|
||||
>
|
||||
|
||||
<Controls />
|
||||
<Background className="bg-default/10" />
|
||||
<Background className="bg-default/30" />
|
||||
</ReactFlow>
|
||||
|
||||
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
|
||||
<defs>
|
||||
<CardinalityMarker type="one" direction="start" />
|
||||
<CardinalityMarker type="one" direction="start" selected />
|
||||
|
||||
<CardinalityMarker type="one" direction="end" />
|
||||
<CardinalityMarker type="one" direction="end" selected />
|
||||
|
||||
<CardinalityMarker type="many" direction="start" />
|
||||
<CardinalityMarker type="many" direction="start" selected />
|
||||
|
||||
<CardinalityMarker type="many" direction="end" />
|
||||
<CardinalityMarker type="many" direction="end" selected />
|
||||
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+7
-5
@@ -37,6 +37,9 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
|
||||
const removeRelationship = () => {
|
||||
deleteRelationship(relationship.id);
|
||||
}
|
||||
|
||||
if (!relationship.sourceTable || !relationship.targetTable)
|
||||
return;
|
||||
return (
|
||||
<div className="w-full p-2 space-y-4">
|
||||
<div className="flex">
|
||||
@@ -48,12 +51,11 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="truncate text-left text-sm">
|
||||
{relationship.sourceTable.name}({relationship.sourceField.name})
|
||||
{relationship.sourceTable?.name}({relationship.sourceField?.name})
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
|
||||
{relationship.sourceTable.name}({relationship.sourceField.name})
|
||||
{relationship.sourceTable?.name}({relationship.sourceField?.name})
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -68,11 +70,11 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="truncate text-left text-sm ">
|
||||
{relationship.targetTable.name}({relationship.targetField.name})
|
||||
{relationship.targetTable?.name}({relationship.targetField?.name})
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{relationship.targetTable.name}({relationship.targetField.name})
|
||||
{relationship.targetTable?.name}({relationship.targetField?.name})
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
+45
-13
@@ -1,9 +1,8 @@
|
||||
import { useRelationshipName } from "@/components/hooks/use-relationship-name";
|
||||
import { randomColor } from "@/lib/colors";
|
||||
import { useRelationshipName } from "@/hooks/use-relationship-name";
|
||||
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 { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
|
||||
import { Check, ChevronRight, EllipsisVertical, Focus, Pencil, Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -19,8 +18,9 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
|
||||
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const { name: defaultName } = useRelationshipName(relationship);
|
||||
const { editRelationship } = useDatabase();
|
||||
|
||||
const { editRelationship, deleteRelationship } = useDatabase();
|
||||
const { t } = useTranslation();
|
||||
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
|
||||
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName)
|
||||
|
||||
|
||||
@@ -33,6 +33,11 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
|
||||
setEditMode(false);
|
||||
}
|
||||
|
||||
const onDeleteRelationship = () => {
|
||||
|
||||
deleteRelationship(relationship.id);
|
||||
setPopOverOpen(false);
|
||||
}
|
||||
return (
|
||||
<div className="group w-full flex h-12 gap-1 flex p-2 items-center" >
|
||||
<div className={cn(
|
||||
@@ -92,13 +97,40 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
|
||||
<Pencil className="size-4 text-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-slate-500" />
|
||||
</Button>
|
||||
|
||||
|
||||
|
||||
<Popover placement="bottom" radius="sm" shadow="sm" showArrow isOpen={popOverOpen} onOpenChange={setPopOverOpen}>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-slate-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[160px]" >
|
||||
<div className="w-full flex flex-col gap-2 ">
|
||||
<h3 className="font-semibold text-sm text-gray p-2">
|
||||
{t("db_controller.actions")}
|
||||
</h3>
|
||||
</div>
|
||||
<hr className="text-default-200" />
|
||||
<Listbox aria-label="Actions" className="p-0 pb-1" >
|
||||
|
||||
<ListboxItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
onPressEnd={onDeleteRelationship}
|
||||
endContent={<Trash className="size-4" />}
|
||||
>
|
||||
{t("db_controller.delete")}
|
||||
</ListboxItem>
|
||||
</Listbox>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
+5
-1
@@ -6,7 +6,7 @@ import { EllipsisVertical, GripVertical, KeyRound, Trash2 } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { Key , useState } from "react";
|
||||
import { Key , useEffect, useState } from "react";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import Autocomplete from "@/components/auto-complete/auto-complete";
|
||||
import ToggleButton from "@/components/toggle/toggle";
|
||||
@@ -32,6 +32,10 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFieldName(field.name) ;
|
||||
} , [field.name])
|
||||
|
||||
const removeField = () => {
|
||||
setPopOverOpen(false);
|
||||
deleteField(field.id)
|
||||
|
||||
+5
-4
@@ -24,8 +24,9 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
const { t } = useTranslation();
|
||||
const { editTable, createField } = useDatabase();
|
||||
|
||||
const onColorChange = useCallback((color: string) => {
|
||||
editTable({ id: table.id, color } as TableType);
|
||||
const onColorChange = useCallback((color: string | undefined) => {
|
||||
console.log(color)
|
||||
editTable({ id: table.id, color: color ? color : null } as TableType);
|
||||
}, [table]);
|
||||
|
||||
const addField = (event: any) => {
|
||||
@@ -42,8 +43,8 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
|
||||
const saveNote = () => {
|
||||
editTable({
|
||||
id : table.id ,
|
||||
note
|
||||
id: table.id,
|
||||
note
|
||||
} as TableType)
|
||||
}
|
||||
|
||||
|
||||
+13
-11
@@ -1,8 +1,8 @@
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/tooltip/tooltip";
|
||||
|
||||
import { Button, cn, Divider, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react";
|
||||
import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react";
|
||||
import { Check, ChevronRight, Copy, EllipsisVertical, FileKey, FileType, Focus, Pencil, Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
@@ -19,15 +19,16 @@ export interface TableAccordionHeaderProps {
|
||||
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
|
||||
const { editTable, deleteTable, createField } = useDatabase();
|
||||
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
|
||||
const style = {
|
||||
borderLeft: "6px solid " + table.color,
|
||||
};
|
||||
|
||||
const [tableName, setTableName] = useState<string>(table.name);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
setTableName(table.name) ;
|
||||
} , [table.name])
|
||||
|
||||
const saveTableName = async () => {
|
||||
await editTable({ id: table.id, name: tableName });
|
||||
setEditMode(false);
|
||||
@@ -50,11 +51,12 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
nullable: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="group w-full flex h-12 gap-1 border-l-4 flex p-2 items-center"
|
||||
style={style}
|
||||
<div className="group w-full flex h-12 gap-1 border-l-4 flex p-2 items-center border-l-[6px] "
|
||||
style={{
|
||||
borderColor: table.color as string | undefined
|
||||
}}
|
||||
>
|
||||
<div className={cn(
|
||||
'tarnsition-all duration-200',
|
||||
@@ -94,7 +96,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
variant="bordered"
|
||||
onBlur={saveTableName}
|
||||
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 "
|
||||
className="rounded-md px-2 py-0.5 w-full border-primary-700 focus-visible:ring-0 dark:bg-slate-900 text-sm "
|
||||
/>
|
||||
<Button
|
||||
variant="light"
|
||||
|
||||
@@ -28,7 +28,6 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
await createTable({
|
||||
id: newTableId,
|
||||
name: `table_${tables.length + 1}`,
|
||||
color: randomColor(),
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
||||
@@ -1,54 +1,105 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { Field as FieldType } from "@/lib/interfaces/field";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { Button, cn, select } from "@heroui/react";
|
||||
import { Handle, Position, useConnection } from "@xyflow/react";
|
||||
import { Check } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { Check, KeyRound, Trash, Trash2 } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
field: FieldType,
|
||||
showHandles?: boolean
|
||||
showHandles?: boolean,
|
||||
highlight?: boolean
|
||||
}
|
||||
|
||||
|
||||
export const LEFT_PREFIX = "left_";
|
||||
export const RIGHT_PREFIX = "right_";
|
||||
export const TARGET_PREFIX = "target_";
|
||||
|
||||
const Field: React.FC<Props> = ({ field, showHandles }) => {
|
||||
|
||||
const Field: React.FC<Props> = ({ field, showHandles, highlight }) => {
|
||||
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const { deleteField, editField } = useDatabase();
|
||||
const [fieldName, setFieldName] = useState<string>(field.name);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setFieldName(field.name);
|
||||
}, [field.name]);
|
||||
|
||||
|
||||
|
||||
const removeField = () => {
|
||||
deleteField(field.id)
|
||||
}
|
||||
|
||||
|
||||
const saveFieldName = () => {
|
||||
editField({
|
||||
id: field.id,
|
||||
name: fieldName
|
||||
} as FieldType);
|
||||
setEditMode(false);
|
||||
}
|
||||
|
||||
const connection = useConnection();
|
||||
|
||||
console.log(highlight)
|
||||
return (
|
||||
<div className="group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 transition-all duration-200 ease-in-out ">
|
||||
<div className={cn(
|
||||
"group relative flex h-8 items-center justify-between gap-1 border-t border-default px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 transition-all duration-200 ease-in-out" ,
|
||||
highlight ? "bg-primary/5" : ""
|
||||
)}>
|
||||
{
|
||||
!editMode &&
|
||||
<>
|
||||
<label
|
||||
className=" truncate text-sm "
|
||||
className={cn(
|
||||
"truncate text-xs text-slate-900",
|
||||
field.isPrimary ? "font-semibold" : ""
|
||||
)}
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
>
|
||||
{field.name}
|
||||
</label>
|
||||
<span className="content-center truncate text-right text-xs text-muted-foreground text-default-600" >
|
||||
{field.type.name.split(' ')[0]}
|
||||
<span className={cn("content-center truncate flex items-center h-full gap-1 text-right text-xs text-default-600 group-hover:hidden font-semibold text-icon",
|
||||
field.isPrimary ? "font-semibold text-slate-700" : ""
|
||||
)}>
|
||||
{field.isPrimary && <KeyRound className="size-3" />} {field.type?.name?.split(' ')[0]}{field.nullable ? "?" : ""}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
radius="none"
|
||||
isIconOnly
|
||||
variant="faded"
|
||||
size="sm"
|
||||
color="danger"
|
||||
className=" bg-transparent border-none hidden group-hover:flex "
|
||||
onPressEnd={removeField}
|
||||
|
||||
>
|
||||
<Trash2 className="size-3" />
|
||||
</Button>
|
||||
|
||||
</>
|
||||
}
|
||||
{
|
||||
editMode &&
|
||||
<>
|
||||
<input
|
||||
// ref={inputRef}
|
||||
onBlur={() => setEditMode(false)}
|
||||
|
||||
onBlur={saveFieldName}
|
||||
placeholder={field.name}
|
||||
autoFocus
|
||||
type="text"
|
||||
// value={fieldName}
|
||||
value={fieldName}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
// onChange={(e) => setFieldName(e.target.value)}
|
||||
className="rounded-md px-2 py-0.5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm "
|
||||
onChange={(e) => setFieldName(e.target.value)}
|
||||
|
||||
className="rounded-md outline-none px-2 py-0.5 w-full border-[0.5px] border-primary-700 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm "
|
||||
|
||||
/>
|
||||
<Button
|
||||
@@ -56,12 +107,13 @@ const Field: React.FC<Props> = ({ field, showHandles }) => {
|
||||
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"
|
||||
isIconOnly
|
||||
onPress={() => setEditMode(false)}
|
||||
onPress={saveFieldName}
|
||||
>
|
||||
<Check className="size-4" />
|
||||
<Check className="size-3" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
<div className={
|
||||
cn(
|
||||
"absolute w-full left-0 ",
|
||||
@@ -70,16 +122,14 @@ const Field: React.FC<Props> = ({ field, showHandles }) => {
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
id={"left-"+ field.id}
|
||||
className="w-4 h-4 border-3 bg-primary"
|
||||
id={LEFT_PREFIX + field.id}
|
||||
className="w-4 h-4 border-4 bg-primary"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-4 h-4 border-3 bg-primary"
|
||||
|
||||
id={"right-"+ field.id}
|
||||
|
||||
className="w-4 h-4 border-4 bg-primary"
|
||||
id={RIGHT_PREFIX + field.id}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
@@ -89,7 +139,7 @@ const Field: React.FC<Props> = ({ field, showHandles }) => {
|
||||
!connection.inProgress ? "invisible" : "visible"
|
||||
)} >
|
||||
<Handle
|
||||
id={`${"target"}_${field.id}`}
|
||||
id={`${TARGET_PREFIX}${field.id}`}
|
||||
className={
|
||||
true
|
||||
? '!absolute !left-0 !top-0 !h-full !w-full !transform-none !rounded-none !border-none !opacity-0'
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Relationship as RelationshipType } from "@/lib/interfaces/relationship";
|
||||
import { cn } from "@heroui/react";
|
||||
import { Edge, EdgeProps, getSmoothStepPath, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { card, cn } from "@heroui/react";
|
||||
import { Edge, EdgeProps, getBezierPath, getSmoothStepPath, InternalNode, Node, Position, useReactFlow } from "@xyflow/react";
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
|
||||
|
||||
@@ -11,40 +13,31 @@ export type RelationshipProps = Edge<{
|
||||
selected?: boolean
|
||||
}, 'relationship-edge'>
|
||||
|
||||
export const Relationship: React.FC<EdgeProps<RelationshipProps>> = ({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
source,
|
||||
target,
|
||||
selected,
|
||||
data,
|
||||
}) => {
|
||||
const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
|
||||
|
||||
let { id, sourceX, sourceY, targetX, targetY, source, target, selected, data , animated } = props;
|
||||
const { getInternalNode, getEdge } = useReactFlow();
|
||||
//const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
|
||||
//const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
|
||||
|
||||
|
||||
const sourceNode = getInternalNode(source);
|
||||
const targetNode = getInternalNode(target);
|
||||
const edge = getEdge(id);
|
||||
|
||||
const sourceHandle: 'left' | 'right' = edge?.sourceHandle?.startsWith?.(
|
||||
"right-"
|
||||
"right_"
|
||||
)
|
||||
? 'right'
|
||||
: 'left';
|
||||
|
||||
const sourceWidth = sourceNode?.measured.width ?? 0;
|
||||
const sourceLeftX =
|
||||
sourceHandle === 'left' ? sourceX + 3 : sourceX - sourceWidth - 10;
|
||||
const sourceRightX =
|
||||
sourceHandle === 'left' ? sourceX + sourceWidth + 9 : sourceX;
|
||||
const sourceLeftX = sourceHandle === 'left' ? sourceX + 6 : sourceX - sourceWidth - 10;
|
||||
const sourceRightX = sourceHandle === 'left' ? sourceX + sourceWidth + 10 : sourceX;
|
||||
|
||||
const targetWidth = targetNode?.measured.width ?? 0;
|
||||
const targetLeftX = targetX - 1;
|
||||
const targetRightX = targetX + targetWidth + 10;
|
||||
const targetLeftX = targetX - 2;
|
||||
const targetRightX = targetX + targetWidth + 3;
|
||||
|
||||
|
||||
|
||||
const { sourceSide, targetSide } = useMemo(() => {
|
||||
const distances = {
|
||||
@@ -84,12 +77,12 @@ export const Relationship: React.FC<EdgeProps<RelationshipProps>> = ({
|
||||
sourceY,
|
||||
targetX: targetSide === 'left' ? targetLeftX : targetRightX,
|
||||
targetY,
|
||||
borderRadius: 14,
|
||||
borderRadius: 6,
|
||||
sourcePosition:
|
||||
sourceSide === 'left' ? Position.Left : Position.Right,
|
||||
targetPosition:
|
||||
targetSide === 'left' ? Position.Left : Position.Right,
|
||||
// offset: (edgeNumber + 1) * 14,
|
||||
|
||||
}),
|
||||
[
|
||||
sourceSide,
|
||||
@@ -100,20 +93,37 @@ export const Relationship: React.FC<EdgeProps<RelationshipProps>> = ({
|
||||
targetRightX,
|
||||
sourceY,
|
||||
targetY,
|
||||
// edgeNumber,
|
||||
|
||||
]
|
||||
);
|
||||
const { startMarker, endMarker } = useMemo(() => {
|
||||
const cardinality: Cardinality = data?.relationship.cardinality as Cardinality;
|
||||
if (cardinality) {
|
||||
const cardinalities: string[] = cardinality.split("_to_");
|
||||
return {
|
||||
startMarker: `${cardinalities[0]}_${"start"}${selected ? "_selected" : ""}`,
|
||||
endMarker: `${cardinalities[1]}_${"end"}${selected ? "_selected" : ""}`
|
||||
}
|
||||
}
|
||||
return {
|
||||
startMarker: `${"one"}_${"start"}${selected ? "_selected" : ""}`,
|
||||
endMarker: `${"many"}_${"end"}${selected ? "_selected" : ""}`
|
||||
}
|
||||
}, [data?.relationship.cardinality, selected]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<path
|
||||
id={id}
|
||||
d={edgePath}
|
||||
//markerStart={`url(#${sourceMarker})`}
|
||||
//markerEnd={`url(#${targetMarker})`}
|
||||
markerStart={`url(#${startMarker})`}
|
||||
markerEnd={`url(#${endMarker})`}
|
||||
fill="none"
|
||||
className={cn([
|
||||
'react-flow__edge-path',
|
||||
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
|
||||
|
||||
`!stroke-2 ${selected ? '!stroke-primary' : 'stroke-slate-300'}`,
|
||||
|
||||
])}
|
||||
onClick={(e) => {
|
||||
@@ -122,13 +132,18 @@ export const Relationship: React.FC<EdgeProps<RelationshipProps>> = ({
|
||||
// openRelationshipInEditor();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
strokeDasharray: (selected || animated) ? '5, 5' : '0',
|
||||
animation: (selected || animated) ? 'dash 0.5s linear infinite' : 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<path
|
||||
d={edgePath}
|
||||
fill="none"
|
||||
strokeOpacity={0}
|
||||
strokeWidth={20}
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
strokeWidth={16}
|
||||
|
||||
className="react-flow__edge-interaction"
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
@@ -136,5 +151,20 @@ export const Relationship: React.FC<EdgeProps<RelationshipProps>> = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
</>)
|
||||
}
|
||||
}
|
||||
|
||||
export default Relationship;
|
||||
|
||||
/*
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Table as TableType } from "@/lib/interfaces/table";
|
||||
|
||||
import { Handle, Node, NodeProps, NodeResizer, Position } from "@xyflow/react";
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Edge, Handle, Node, NodeProps, NodeResizer, Position, useReactFlow, useStore } from "@xyflow/react";
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Card, CardBody, CardHeader, cn, Divider, Input } from "@heroui/react";
|
||||
import {
|
||||
ChevronsLeftRight,
|
||||
@@ -16,10 +16,16 @@ import {
|
||||
SquarePlus,
|
||||
SquareMinus,
|
||||
Divide,
|
||||
Focus,
|
||||
} from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { Field } from "@/lib/interfaces/field";
|
||||
|
||||
import FieldComponent from "./field";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
|
||||
|
||||
export const MAX_TABLE_SIZE = 450;
|
||||
@@ -38,40 +44,64 @@ const Table: React.FC<NodeProps<TableProps>> = React.memo(({
|
||||
id,
|
||||
data: { table, },
|
||||
}) => {
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
|
||||
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const [tableName, setTableName] = useState<string>(table.name);
|
||||
const { editTable } = useDatabase();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setTableName(table.name);
|
||||
}, [table.name])
|
||||
|
||||
const saveTableName = async () => {
|
||||
await editTable({ id: table.id, name: tableName });
|
||||
setEditMode(false);
|
||||
}
|
||||
const edges = useStore((store) => Array.from(store.edges.values())) as Edge[];
|
||||
|
||||
const highlightedEdges: Edge[] = useMemo(() => {
|
||||
return edges.filter((edge: Edge) => edge.animated || edge.selected);
|
||||
}, [edges]);
|
||||
|
||||
|
||||
|
||||
console.log(highlightedEdges)
|
||||
return (
|
||||
|
||||
<div className={cn(
|
||||
"w-full h-full bg-background rounded-lg entity-card border-2 noselect ",
|
||||
<Card className={cn(
|
||||
"w-full h-full bg-background rounded-lg entity-card noselect overflow-visible ",
|
||||
selected
|
||||
? ' border-2 border-primary-500'
|
||||
? 'ring-1 ring-primary'
|
||||
: '',
|
||||
|
||||
)}>
|
||||
<div
|
||||
className="h-2 rounded-t-[6px] "
|
||||
style={{ backgroundColor: "rgb(255, 107, 138)" }}
|
||||
></div>
|
||||
|
||||
|
||||
<div className="group gap-2 flex h-9 items-center justify-between bg-default px-2 dark:bg-default">
|
||||
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
|
||||
)}
|
||||
shadow="sm"
|
||||
>
|
||||
<div className="px-[2px] ">
|
||||
<div
|
||||
className=" border-t-[4px] rounded-t-[6px] border-primary"
|
||||
style={{ borderColor: table.color as string }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="group gap-2 flex h-9 items-center justify-between bg-default/50 px-2 dark:bg-default ">
|
||||
<Table2 className="size-4 shrink-0 text-icon dark:text-primary" />
|
||||
{
|
||||
editMode && <>
|
||||
<input
|
||||
placeholder={table.name}
|
||||
autoFocus
|
||||
onBlur={() => setEditMode(false)}
|
||||
onChange={(event: any) => setTableName(event.target.value)}
|
||||
value={tableName}
|
||||
onBlur={saveTableName}
|
||||
type="text"
|
||||
className="rounded-md px-2 py-0.5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm "
|
||||
className="rounded-md outline-none px-2 py-0.5 w-full border-[0.5px] border-primary-700 font-bold bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm "
|
||||
/>
|
||||
<Button
|
||||
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"
|
||||
onClick={() => setEditMode(false)}
|
||||
onPressEnd={saveTableName}
|
||||
isIconOnly
|
||||
>
|
||||
<Check className="size-3" />
|
||||
@@ -91,7 +121,7 @@ const Table: React.FC<NodeProps<TableProps>> = React.memo(({
|
||||
</label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Double click to edit
|
||||
{t("table.double_click")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -103,7 +133,7 @@ const Table: React.FC<NodeProps<TableProps>> = React.memo(({
|
||||
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"
|
||||
isIconOnly
|
||||
>
|
||||
<CircleDotDashed className="size-4" />
|
||||
<Focus className="size-4 text-icon" />
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -123,26 +153,33 @@ const Table: React.FC<NodeProps<TableProps>> = React.memo(({
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
className="transition-[max-height] duration-200 ease-in-out"
|
||||
>
|
||||
{table.fields.map((field: Field) => (
|
||||
<FieldComponent
|
||||
key={field.id}
|
||||
field={field}
|
||||
showHandles={selected}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="transition-[max-height] duration-200 ease-in-out"
|
||||
>
|
||||
{table.fields.map((field: FieldType) => {
|
||||
|
||||
const highlight: boolean = highlightedEdges.find((edge: any) =>
|
||||
(edge.data?.relationship as RelationshipType).sourceFieldId == field.id ||
|
||||
(edge.data?.relationship as RelationshipType).targetFieldId == field.id) != null;
|
||||
|
||||
|
||||
return (<FieldComponent
|
||||
key={field.id}
|
||||
field={field}
|
||||
showHandles={selected}
|
||||
highlight={highlight}
|
||||
|
||||
/>)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Card>
|
||||
|
||||
)
|
||||
});
|
||||
|
||||
Table.displayName = 'table';
|
||||
|
||||
|
||||
export default Table;
|
||||
|
||||
|
||||
@@ -11,21 +11,25 @@ import { createContext } from "react";
|
||||
|
||||
export interface DatabaseContextType {
|
||||
tables: TableType[],
|
||||
data_types: DataType[] ,
|
||||
relationships : RelationshipType[] ,
|
||||
data_types: DataType[],
|
||||
relationships: RelationshipType[],
|
||||
// table operations
|
||||
createTable: (table: TableInsertType) => Promise<QueryResult>,
|
||||
editTable: (table: TableInsertType) => Promise<QueryResult>,
|
||||
deleteTable: (id: string) => Promise<QueryResult>,
|
||||
updateTablePositions: (tables: TableInsertType[]) => Promise<QueryResult> ,
|
||||
deleteMultiTables: (ids: 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> ,
|
||||
createRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
|
||||
editRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
|
||||
deleteRelationship: (id: string) => Promise<QueryResult>,
|
||||
deleteMultiRelationships: (ids: string[]) => Promise<QueryResult>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { db, powerSyncDb } from "../sync-provider/sync-provider";
|
||||
import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema";
|
||||
import { useQuery } from "@powersync/react";
|
||||
import { toCompilableQuery } from "@powersync/drizzle-driver";
|
||||
import { asc, desc, eq, sql } from "drizzle-orm";
|
||||
import { asc, desc, eq, inArray, 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";
|
||||
@@ -16,11 +16,14 @@ interface Props { children: React.ReactNode }
|
||||
const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
|
||||
const { data: tablesList, isLoading, error } = useQuery(toCompilableQuery(
|
||||
const { data: tablesList } = useQuery(toCompilableQuery(
|
||||
db.query.tables.findMany({
|
||||
with: {
|
||||
fields: {
|
||||
orderBy: asc(fields.sequence),
|
||||
with: {
|
||||
type: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: desc(tables.createdAt)
|
||||
@@ -53,6 +56,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
const deleteTable = async (id: string): Promise<QueryResult> => {
|
||||
return await db.delete(tables).where(eq(tables.id, id));
|
||||
}
|
||||
const deleteMultiTables = async (ids: string[]): Promise<QueryResult> => {
|
||||
return await db.delete(tables).where(inArray(tables.id, ids ))
|
||||
}
|
||||
|
||||
const createField = async (field: FieldInsertType): Promise<QueryResult> => {
|
||||
return await db.insert(fields).values(field);
|
||||
@@ -90,13 +96,44 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
return await db.delete(relationships).where(eq(relationships.id, id));
|
||||
}
|
||||
|
||||
const deleteMultiRelationships = async (ids: string[]): Promise<QueryResult> => {
|
||||
return await db.delete(relationships).where(inArray(relationships.id, ids ))
|
||||
}
|
||||
|
||||
const updateTablePositions = async (tables: TableInsertType[]): Promise<QueryResult> => {
|
||||
|
||||
const posXCases = tables
|
||||
.map((table: TableInsertType) => `WHEN '${table.id}' THEN ${table.posX}`)
|
||||
.join('\n ');
|
||||
|
||||
const posYCases = tables
|
||||
.map((table: TableInsertType) => `WHEN '${table.id}' THEN ${table.posY}`)
|
||||
.join('\n ');
|
||||
|
||||
const ids = tables.map((table: TableInsertType) => `'${table.id}'`).join(',\n ');
|
||||
|
||||
const sql = `
|
||||
UPDATE tables
|
||||
SET
|
||||
posX = CASE id
|
||||
${posXCases}
|
||||
END,
|
||||
posY = CASE id
|
||||
${posYCases}
|
||||
END
|
||||
WHERE id IN (
|
||||
${ids}
|
||||
);`;
|
||||
return await powerSyncDb.execute(sql);
|
||||
}
|
||||
return (
|
||||
|
||||
<DatabaseContext.Provider value={{
|
||||
createTable,
|
||||
editTable,
|
||||
deleteTable,
|
||||
updateTablePositions,
|
||||
deleteMultiTables ,
|
||||
|
||||
createField,
|
||||
editField,
|
||||
@@ -106,9 +143,10 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
createRelationship,
|
||||
editRelationship,
|
||||
deleteRelationship,
|
||||
deleteMultiRelationships ,
|
||||
|
||||
tables: tablesList as TableType[],
|
||||
relationships : relationshipsList as RelationshipType[] ,
|
||||
relationships: relationshipsList as RelationshipType[],
|
||||
data_types
|
||||
}}>
|
||||
{children}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
|
||||
.text-editable {
|
||||
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
|
||||
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] group-hover:ring-primary-700 rounded-md cursor-pointer;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ div[data-slot="content"] hr[role="separator"] {
|
||||
}
|
||||
|
||||
|
||||
button[data-slot="trogger"] {
|
||||
background-color: red !important;
|
||||
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: -10;
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -1,5 +1,5 @@
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,4 +12,8 @@ export const getNextSequence = (fields: FieldType[]): number => {
|
||||
return field.sequence > max.sequence ? field : max
|
||||
});
|
||||
return maxSequenceItem.sequence + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+20
-20
@@ -19,17 +19,17 @@ export default {
|
||||
foreground: 'hsl(240 5.3% 26.1%)',
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: "#961FFE",
|
||||
50: '#961FFE0D', // 5% opacity
|
||||
100: '#961FFE1A', // 10% opacity
|
||||
200: '#961FFE33', // 20% opacity
|
||||
300: '#961FFE4D', // 30% opacity
|
||||
400: '#961FFE66', // 40% opacity
|
||||
500: '#961FFE80', // 50% opacity
|
||||
600: '#961FFE99', // 60% opacity
|
||||
700: '#961FFEB3', // 70% opacity
|
||||
DEFAULT: "#9822ff",
|
||||
50: '#9822ff0D', // 5% opacity
|
||||
100: '#9822ff1A', // 10% opacity
|
||||
200: '#9822ff33', // 20% opacity
|
||||
300: '#9822ff4D', // 30% opacity
|
||||
400: '#9822ff66', // 40% opacity
|
||||
500: '#9822ff80', // 50% opacity
|
||||
600: '#9822ff99', // 60% opacity
|
||||
700: '#9822ffB3', // 70% opacity
|
||||
800: '#961FFFCC', // 80% opacity
|
||||
900: '#961FFEE6', // 90% opacity
|
||||
900: '#9822ffE6', // 90% opacity
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -50,17 +50,17 @@ export default {
|
||||
} ,
|
||||
|
||||
primary: {
|
||||
DEFAULT: "#961FFE",
|
||||
50: '#961FFE0D', // 5% opacity
|
||||
100: '#961FFE1A', // 10% opacity
|
||||
200: '#961FFE33', // 20% opacity
|
||||
300: '#961FFE4D', // 30% opacity
|
||||
400: '#961FFE66', // 40% opacity
|
||||
500: '#961FFE80', // 50% opacity
|
||||
600: '#961FFE99', // 60% opacity
|
||||
700: '#961FFEB3', // 70% opacity
|
||||
DEFAULT: "#9822ff",
|
||||
50: '#9822ff0D', // 5% opacity
|
||||
100: '#9822ff1A', // 10% opacity
|
||||
200: '#9822ff33', // 20% opacity
|
||||
300: '#9822ff4D', // 30% opacity
|
||||
400: '#9822ff66', // 40% opacity
|
||||
500: '#9822ff80', // 50% opacity
|
||||
600: '#9822ff99', // 60% opacity
|
||||
700: '#9822ffB3', // 70% opacity
|
||||
800: '#961FFFCC', // 80% opacity
|
||||
900: '#961FFEE6', // 90% opacity
|
||||
900: '#9822ffE6', // 90% opacity
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// vite.config.ts
|
||||
import { defineConfig } from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite/dist/node/index.js";
|
||||
import react from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/@vitejs/plugin-react/dist/index.mjs";
|
||||
import tsconfigPaths from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite-tsconfig-paths/dist/index.mjs";
|
||||
import topLevelAwait from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite-plugin-top-level-await/exports/import.mjs";
|
||||
var vite_config_default = defineConfig({
|
||||
plugins: [react(), tsconfigPaths(), topLevelAwait()],
|
||||
server: {
|
||||
port: 3e3
|
||||
},
|
||||
optimizeDeps: {
|
||||
// Don't optimize these packages as they contain web workers and WASM files.
|
||||
// https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673
|
||||
exclude: ["@journeyapps/wa-sqlite", "@powersync/web"],
|
||||
include: ["@powersync/web > js-logger"]
|
||||
},
|
||||
worker: {
|
||||
format: "es",
|
||||
plugins: () => [topLevelAwait()]
|
||||
}
|
||||
});
|
||||
export {
|
||||
vite_config_default as default
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0YW1hblxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXHN0YWNrcmVuZGVyXFxcXHN0YWNrcmVuZGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0YW1hblxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXHN0YWNrcmVuZGVyXFxcXHN0YWNrcmVuZGVyXFxcXHZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9DOi9Vc2Vycy90YW1hbi9PbmVEcml2ZS9EZXNrdG9wL3N0YWNrcmVuZGVyL3N0YWNrcmVuZGVyL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSdcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCdcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnXG5pbXBvcnQgdG9wTGV2ZWxBd2FpdCBmcm9tICd2aXRlLXBsdWdpbi10b3AtbGV2ZWwtYXdhaXQnO1xuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHBsdWdpbnM6IFtyZWFjdCgpLCB0c2NvbmZpZ1BhdGhzKCkgLCB0b3BMZXZlbEF3YWl0KCldLFxuICBzZXJ2ZXIgOiB7IFxuICAgIHBvcnQgOiAzMDAwXG4gIH0gLCBcbiAgb3B0aW1pemVEZXBzOiB7XG4gICAgLy8gRG9uJ3Qgb3B0aW1pemUgdGhlc2UgcGFja2FnZXMgYXMgdGhleSBjb250YWluIHdlYiB3b3JrZXJzIGFuZCBXQVNNIGZpbGVzLlxuICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS92aXRlanMvdml0ZS9pc3N1ZXMvMTE2NzIjaXNzdWVjb21tZW50LTE0MTU4MjA2NzNcbiAgICBleGNsdWRlOiBbJ0Bqb3VybmV5YXBwcy93YS1zcWxpdGUnLCAnQHBvd2Vyc3luYy93ZWInXSxcbiAgICBpbmNsdWRlOiBbJ0Bwb3dlcnN5bmMvd2ViID4ganMtbG9nZ2VyJ11cbiAgfSxcbiAgd29ya2VyOiB7XG4gICAgZm9ybWF0OiAnZXMnLFxuICAgIHBsdWdpbnM6ICgpID0+IFsgdG9wTGV2ZWxBd2FpdCgpXVxuICB9XG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFxVyxTQUFTLG9CQUFvQjtBQUNsWSxPQUFPLFdBQVc7QUFDbEIsT0FBTyxtQkFBbUI7QUFDMUIsT0FBTyxtQkFBbUI7QUFFMUIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUyxDQUFDLE1BQU0sR0FBRyxjQUFjLEdBQUksY0FBYyxDQUFDO0FBQUEsRUFDcEQsUUFBUztBQUFBLElBQ1AsTUFBTztBQUFBLEVBQ1Q7QUFBQSxFQUNBLGNBQWM7QUFBQTtBQUFBO0FBQUEsSUFHWixTQUFTLENBQUMsMEJBQTBCLGdCQUFnQjtBQUFBLElBQ3BELFNBQVMsQ0FBQyw0QkFBNEI7QUFBQSxFQUN4QztBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sUUFBUTtBQUFBLElBQ1IsU0FBUyxNQUFNLENBQUUsY0FBYyxDQUFDO0FBQUEsRUFDbEM7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=
|
||||
Reference in New Issue
Block a user