mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 03:05:42 +00:00
table controller integrated with the Api
This commit is contained in:
@@ -13,12 +13,14 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@heroui/react": "^2.7.6",
|
||||
"@powersync/drizzle-driver": "^0.4.0",
|
||||
"@powersync/react": "^1.5.3",
|
||||
"@powersync/web": "^1.20.0",
|
||||
"@radix-ui/react-tooltip": "^1.2.3",
|
||||
"@react-aria/visually-hidden": "3.8.21",
|
||||
"@react-types/shared": "3.28.0",
|
||||
"@xyflow/react": "^12.6.0",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"framer-motion": "11.15.0",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"lucide-react": "^0.501.0",
|
||||
|
||||
+7
-4
@@ -3,16 +3,19 @@ import "@/styles/globals.css"
|
||||
import { TooltipProvider } from "./components/tooltip/tooltip";
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import useAppRoutes from "./routes/app-route";
|
||||
import { SyncProvider } from "./providers/sync-provider";
|
||||
import { SyncProvider } from "./providers/sync-provider/sync-provider";
|
||||
import DatabaseProvider from "./providers/database-provider/database-provider";
|
||||
|
||||
function App() {
|
||||
const appRoutes = useAppRoutes();
|
||||
return (
|
||||
<SyncProvider>
|
||||
<ReactFlowProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
{appRoutes}
|
||||
</TooltipProvider>
|
||||
<DatabaseProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
{appRoutes}
|
||||
</TooltipProvider>
|
||||
</DatabaseProvider>
|
||||
</ReactFlowProvider>
|
||||
</SyncProvider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
import { Autocomplete as HeroUiAutocomplete, AutocompleteItem } from "@heroui/react";
|
||||
import { Key } from "react";
|
||||
|
||||
interface AutocompleteProps {
|
||||
|
||||
items: any[]
|
||||
label?: string,
|
||||
defaultSelection: string | undefined
|
||||
onSelectionChange?: (item: any) => void,
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder }) => {
|
||||
|
||||
|
||||
|
||||
const onItemChange = (item: Key | null) => {
|
||||
onSelectionChange && onSelectionChange(item);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeroUiAutocomplete
|
||||
className="w-full"
|
||||
defaultItems={items}
|
||||
size="sm"
|
||||
onSelectionChange={onItemChange}
|
||||
defaultSelectedKey={defaultSelection as any}
|
||||
variant="bordered"
|
||||
aria-label={placeholder}
|
||||
placeholder={placeholder}
|
||||
>
|
||||
{(item: any) => <AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>}
|
||||
</HeroUiAutocomplete>
|
||||
)
|
||||
}
|
||||
|
||||
export default Autocomplete ;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { cn } from "@heroui/react"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip"
|
||||
|
||||
interface ToggleProps {
|
||||
active?: boolean,
|
||||
children: React.ReactNode,
|
||||
onToggle?: (value: boolean) => void,
|
||||
className?: string ,
|
||||
label? : string
|
||||
}
|
||||
|
||||
|
||||
const ToggleButton: React.FC<ToggleProps> = ({ active = false, children, onToggle, className , label }) => {
|
||||
|
||||
const toggle = () => {
|
||||
onToggle && onToggle(!active)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className={
|
||||
cn("p-1 px-2 transition-all hover:bg-default rounded duration-200 ",
|
||||
className ? className : "",
|
||||
active ?
|
||||
"text-slate-700 bg-default/80 " :
|
||||
"text-icon"
|
||||
)
|
||||
}
|
||||
|
||||
onClick={toggle}
|
||||
>
|
||||
<span className="text-sm">
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default ToggleButton
|
||||
@@ -36,7 +36,11 @@ export const en = {
|
||||
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"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface DataType {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { DataType } from "../data/data-types";
|
||||
|
||||
export interface Field {
|
||||
id: string;
|
||||
name: string;
|
||||
type: DataType;
|
||||
primaryKey: boolean;
|
||||
unique: boolean;
|
||||
nullable: boolean;
|
||||
createdAt: number;
|
||||
default?: string;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Field } from "./field";
|
||||
|
||||
export interface Table {
|
||||
id: string;
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
fields: Field[];
|
||||
width?: number;
|
||||
order?: number;
|
||||
color: string;
|
||||
createdAt: number;
|
||||
comments?: string;
|
||||
}
|
||||
@@ -1,24 +1,19 @@
|
||||
import { column, Schema, Table } from '@powersync/web';
|
||||
|
||||
export const DATATYPE_TABLE = 'dataTypes';
|
||||
|
||||
|
||||
const data_types = new Table(
|
||||
{
|
||||
name: column.text,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
import { DrizzleAppSchema, wrapPowerSyncWithDrizzle } from '@powersync/drizzle-driver';
|
||||
import { data_types } from './data-type-schema';
|
||||
import { tables, tablesRelations } from './table-schema';
|
||||
import { fields, fieldsRelations } from './field-schema';
|
||||
|
||||
export const AppSchema = new Schema({
|
||||
data_types,
|
||||
|
||||
});
|
||||
export const drizzleSchema = {
|
||||
data_types ,
|
||||
tables ,
|
||||
fields ,
|
||||
|
||||
export type Database = (typeof AppSchema)['types'];
|
||||
export type DataTypesRecord = Database['data_types'];
|
||||
// OR:
|
||||
// export type Todo = RowType<typeof todos>;
|
||||
|
||||
|
||||
// relationships
|
||||
tablesRelations ,
|
||||
fieldsRelations
|
||||
};
|
||||
|
||||
// Infer the PowerSync schema from your Drizzle schema
|
||||
export const AppSchema = new DrizzleAppSchema(drizzleSchema);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { InferSelectModel } from 'drizzle-orm';
|
||||
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const data_types = sqliteTable('data_types', {
|
||||
id : text("id") ,
|
||||
name: text('name'),
|
||||
});
|
||||
|
||||
|
||||
|
||||
export interface DataType extends InferSelectModel<typeof data_types> { };
|
||||
@@ -0,0 +1,36 @@
|
||||
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';
|
||||
|
||||
|
||||
export const fields = sqliteTable("fields", {
|
||||
id: text("id").primaryKey().notNull(),
|
||||
tableId: text("tableId")
|
||||
.notNull()
|
||||
.references(() => tables.id, { onDelete: "cascade" }),
|
||||
|
||||
name: text("name").notNull(),
|
||||
isPrimary: integer("isPrimary", { mode: "boolean" }),
|
||||
unique: integer("unique", { mode: "boolean" }),
|
||||
nullable: integer("nullable", { mode: "boolean" }),
|
||||
defaultValue: text("defaultValue"),
|
||||
note: text("note"),
|
||||
typeId: text("typeId").references(() => data_types.id, { onDelete: "cascade" }),
|
||||
sequence: integer("sequence").default(0),
|
||||
});
|
||||
|
||||
export const fieldsRelations = relations(fields, ({ one }) => ({
|
||||
table: one(tables, {
|
||||
fields: [fields.tableId],
|
||||
references: [tables.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
export interface FieldType extends InferSelectModel<typeof fields> {
|
||||
sequence: number
|
||||
};
|
||||
|
||||
|
||||
export interface FieldInsertType extends InferInsertModel<typeof fields> { };
|
||||
@@ -0,0 +1,39 @@
|
||||
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { sqliteTable, text, real, integer } from 'drizzle-orm/sqlite-core';
|
||||
import { fields, FieldType } from './field-schema';
|
||||
|
||||
export const tables = sqliteTable('tables', {
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.notNull()
|
||||
.unique(),
|
||||
|
||||
databaseId: text('databaseId'),
|
||||
|
||||
name: text('name').notNull(),
|
||||
posX: real('posX').notNull().default(0),
|
||||
posY: real('posY').notNull().default(0),
|
||||
|
||||
color: text('color'),
|
||||
width: real('width'),
|
||||
note: text('note'),
|
||||
sequence: integer('sequence').default(0),
|
||||
createdAt: text('createdAt'),
|
||||
|
||||
});
|
||||
|
||||
|
||||
export const tablesRelations = relations(tables, ({ many }) => ({
|
||||
fields: many(fields),
|
||||
}));
|
||||
|
||||
|
||||
|
||||
export interface TableType extends InferSelectModel<typeof tables> {
|
||||
fields : FieldType[]
|
||||
};
|
||||
|
||||
|
||||
export interface TableInsertType extends InferInsertModel<typeof tables> {
|
||||
fields? : FieldType[]
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Table as TableType } from "@/lib/interfaces/table";
|
||||
|
||||
import { addEdge, applyEdgeChanges, applyNodeChanges, Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo } 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 { useQuery } from "@powersync/react";
|
||||
import DBController from "./db-controller/db-controller";
|
||||
import { Table as TableType} from "@/lib/schemas/table-schema";
|
||||
|
||||
|
||||
interface DatabaseProps {
|
||||
initialTables?: TableType
|
||||
@@ -173,18 +173,14 @@ const DatabasePage: React.FC<DatabaseProps> = ({ initialTables }) => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const { data , isLoading , error , isFetching } = useQuery('SELECT * FROM data_types ;' );
|
||||
|
||||
|
||||
console.log (data)
|
||||
|
||||
|
||||
const nodeTypes = useMemo(() => ({ table: Table }), []);
|
||||
const onConnect = useCallback((params: any) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen flex">
|
||||
<DBController/>
|
||||
<DBController />
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
|
||||
+153
-79
@@ -1,110 +1,184 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
|
||||
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { Autocomplete, AutocompleteItem, Button, Input } from "@heroui/react";
|
||||
import { EllipsisVertical, Grip, GripVertical, KeyRound } from "lucide-react";
|
||||
import { Button, Input, Popover, PopoverContent, PopoverTrigger, Switch, Textarea } from "@heroui/react";
|
||||
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 { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import Autocomplete from "@/components/auto-complete/auto-complete";
|
||||
import ToggleButton from "@/components/toggle/toggle";
|
||||
interface Props {
|
||||
id : string
|
||||
field: FieldType
|
||||
}
|
||||
export const animals = [
|
||||
{ label: "Cat", key: "cat", description: "The second most popular pet in the world" },
|
||||
{ label: "Dog", key: "dog", description: "The most popular pet in the world" },
|
||||
{ label: "Elephant", key: "elephant", description: "The largest land animal" },
|
||||
{ label: "Lion", key: "lion", description: "The king of the jungle" },
|
||||
{ label: "Tiger", key: "tiger", description: "The largest cat species" },
|
||||
{ label: "Giraffe", key: "giraffe", description: "The tallest land animal" },
|
||||
{
|
||||
label: "Dolphin",
|
||||
key: "dolphin",
|
||||
description: "A widely distributed and diverse group of aquatic mammals",
|
||||
},
|
||||
{ label: "Penguin", key: "penguin", description: "A group of aquatic flightless birds" },
|
||||
{ label: "Zebra", key: "zebra", description: "A several species of African equids" },
|
||||
{
|
||||
label: "Shark",
|
||||
key: "shark",
|
||||
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
|
||||
},
|
||||
{
|
||||
label: "Whale",
|
||||
key: "whale",
|
||||
description: "Diverse group of fully aquatic placental marine mammals",
|
||||
},
|
||||
{ label: "Otter", key: "otter", description: "A carnivorous mammal in the subfamily Lutrinae" },
|
||||
{ label: "Crocodile", key: "crocodile", description: "A large semiaquatic reptile" },
|
||||
];
|
||||
|
||||
|
||||
const FieldItem: React.FC<Props> = ({id }) => {
|
||||
|
||||
const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
|
||||
const [fieldName, setFieldName] = useState<string>(field.name);
|
||||
|
||||
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
|
||||
const { deleteField, editField, data_types } = useDatabase();
|
||||
|
||||
const [note, setNote] = useState<string | undefined>(field.note as string | undefined);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
|
||||
const { attributes, listeners, setNodeRef, transform } = useSortable({ id: field.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
const removeField = () => {
|
||||
setPopOverOpen(false);
|
||||
deleteField(field.id)
|
||||
}
|
||||
|
||||
const updateFieldNote = () => {
|
||||
editField({
|
||||
id: field.id,
|
||||
note: note,
|
||||
} as FieldInsertType)
|
||||
}
|
||||
const toggleUnqiue = (value: boolean) => {
|
||||
editField({
|
||||
id: field.id,
|
||||
unique: value
|
||||
} as FieldInsertType)
|
||||
}
|
||||
|
||||
const saveFieldName = () => {
|
||||
editField({
|
||||
id: field.id,
|
||||
name: fieldName
|
||||
} as FieldType);
|
||||
}
|
||||
const updateFieldType = (key: Key | null) => {
|
||||
editField({
|
||||
id: field.id,
|
||||
typeId: key
|
||||
} as FieldType);
|
||||
}
|
||||
|
||||
|
||||
const toggleNullable = (nullable: boolean) => {
|
||||
editField({
|
||||
id: field.id,
|
||||
nullable: nullable
|
||||
} as FieldType);
|
||||
}
|
||||
|
||||
const togglePrimaryKey = (primaryKey: boolean) => {
|
||||
editField({
|
||||
id: field.id,
|
||||
isPrimary: primaryKey
|
||||
} as FieldType);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full gap-1 items-center " style={style} ref={setNodeRef} {...attributes}>
|
||||
|
||||
<div {...listeners}>
|
||||
<GripVertical className="size-4 text-icon" />
|
||||
<GripVertical className="size-4 text-icon cursor-move" />
|
||||
</div>
|
||||
<Input
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
aria-label={t("db_controller.name")}
|
||||
placeholder={t("db_controller.name")}
|
||||
value={id}
|
||||
value={fieldName}
|
||||
onValueChange={setFieldName}
|
||||
onBlur={saveFieldName}
|
||||
/>
|
||||
<Autocomplete
|
||||
className="w-full"
|
||||
defaultItems={animals}
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
|
||||
items={data_types}
|
||||
onSelectionChange={updateFieldType}
|
||||
defaultSelection={field.typeId as any}
|
||||
placeholder={t("db_controller.type")}
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.key}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
/>
|
||||
|
||||
<div className="flex gap-2 ml-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className="p-1 px-3 transition-all hover:bg-default rounded duration-200" >
|
||||
<span className="text-icon text-sm">
|
||||
N
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.nullable")}?
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className="p-1 px-2 transition-all hover:bg-default rounded duration-200" >
|
||||
<span className="text-icon text-sm">
|
||||
<KeyRound className="size-4" />
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.primary_key")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
<ToggleButton
|
||||
className="px-3"
|
||||
onToggle={toggleNullable}
|
||||
active={field.nullable as boolean}
|
||||
label={`${t("db_controller.nullable")}?`}
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
N
|
||||
</ToggleButton>
|
||||
|
||||
|
||||
<ToggleButton
|
||||
onToggle={togglePrimaryKey}
|
||||
active={field.isPrimary as boolean}
|
||||
label={`${t("db_controller.primary_key")}?`}
|
||||
>
|
||||
<KeyRound className="size-4" />
|
||||
</ToggleButton>
|
||||
|
||||
<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-[210px]" >
|
||||
<div className="w-full flex flex-col gap-2 p-2">
|
||||
<h3 className="font-semibold text-sm text-gray">
|
||||
{t("db_controller.field_setting")}
|
||||
</h3>
|
||||
<hr className="text-default-200" />
|
||||
<div className="flex w-full 500 justify-between">
|
||||
<span className="text-sm text-slate-500 font-medium">
|
||||
{t("db_controller.unique")}
|
||||
</span>
|
||||
<Switch size="sm" defaultSelected={field.unique as boolean} onValueChange={toggleUnqiue}>
|
||||
</Switch>
|
||||
|
||||
</div>
|
||||
<label className="text-sm font-medium text-slate-500">
|
||||
{t("db_controller.note")}
|
||||
</label>
|
||||
<Textarea
|
||||
variant="bordered"
|
||||
className="w-full"
|
||||
label={t("db_controller.field_note")}
|
||||
value={note}
|
||||
disableAutosize
|
||||
disableAnimation
|
||||
onValueChange={setNote}
|
||||
onBlur={updateFieldNote}
|
||||
classNames={{
|
||||
inputWrapper: "bg-default/80",
|
||||
base: "max-w-xs",
|
||||
input: "resize-y min-h-[60px] max-h-[180px]",
|
||||
}} />
|
||||
<hr className="text-default-200" />
|
||||
|
||||
<Button
|
||||
className="bg-default"
|
||||
radius="sm" variant="faded"
|
||||
color="danger"
|
||||
size="sm"
|
||||
onPressEnd={removeField}>
|
||||
<span className="font-medium text-sm">
|
||||
{t("db_controller.delete_field")}
|
||||
</span>
|
||||
<Trash2 className="mr-1 size-3.5 text-danger" />
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
+41
-11
@@ -2,18 +2,31 @@ import { Button } from "@heroui/react";
|
||||
import { Plus } from "lucide-react";
|
||||
import FieldItem from "./field-item";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
|
||||
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { v4 } from "uuid";
|
||||
import { getNextSequence } from "@/utils/field";
|
||||
|
||||
|
||||
interface Props {
|
||||
|
||||
table: TableType
|
||||
}
|
||||
|
||||
|
||||
const FieldList: React.FC<Props> = ({ }) => {
|
||||
const FieldList: React.FC<Props> = ({ table }) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [fields, setFields] = useState<string[]>(["Field 1", "Field 2", "Field 3", "Field 4"])
|
||||
const [fields, setFields] = useState<FieldType[]>(table.fields);
|
||||
const { createField, orderTableFields } = useDatabase();
|
||||
|
||||
useEffect(() => {
|
||||
setFields(table.fields)
|
||||
}, [table.fields])
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor)
|
||||
@@ -23,15 +36,31 @@ const FieldList: React.FC<Props> = ({ }) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
setFields((items) => {
|
||||
const oldIndex = items.indexOf(active.id);
|
||||
const newIndex = items.indexOf(over.id);
|
||||
|
||||
return arrayMove(items, oldIndex, newIndex);
|
||||
setFields((items) => {
|
||||
|
||||
const oldIndex = items.findIndex((item: FieldType) => item.id == active.id);
|
||||
const newIndex = items.findIndex((item: FieldType) => item.id == over.id);
|
||||
|
||||
const fields = arrayMove(items, oldIndex, newIndex);
|
||||
orderTableFields(fields)
|
||||
return fields;
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const addField = () => {
|
||||
createField({
|
||||
id: v4(),
|
||||
name: `field_${table.fields.length + 1}`,
|
||||
tableId: table.id,
|
||||
sequence: getNextSequence(fields) ,
|
||||
nullable: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-2 no-select">
|
||||
<DndContext
|
||||
@@ -45,8 +74,8 @@ const FieldList: React.FC<Props> = ({ }) => {
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{
|
||||
fields.map((field: string) => (
|
||||
<FieldItem id={field}/>
|
||||
fields.map((field: FieldType) => (
|
||||
<FieldItem field={field} key={field.id} />
|
||||
|
||||
))
|
||||
}
|
||||
@@ -56,11 +85,12 @@ const FieldList: React.FC<Props> = ({ }) => {
|
||||
<Button
|
||||
variant="flat"
|
||||
radius="sm"
|
||||
|
||||
startContent={
|
||||
<Plus className="size-4 text-icon" />
|
||||
}
|
||||
className="h-8 p-2 text-xs bg-transparent hover:bg-default text-gray font-semibold"
|
||||
//onClick={handleCreateTable}
|
||||
onPressEnd={addField}
|
||||
>
|
||||
{t("db_controller.add_field")}
|
||||
</Button>
|
||||
|
||||
+56
-16
@@ -1,26 +1,51 @@
|
||||
import { Table } from "@/lib/interfaces/table"
|
||||
import { Accordion, AccordionItem, Button, cn, Divider, Textarea } from "@heroui/react";
|
||||
import { ChevronLeft, ChevronRight, FileKey, FileText, FileType, Key, MessageSquareQuote } from "lucide-react";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { Accordion, AccordionItem, Button, cn, Textarea } from "@heroui/react";
|
||||
import { ChevronLeft, FileKey, FileType, Key, MessageSquareQuote, Plus } from "lucide-react";
|
||||
|
||||
import { MouseEventHandler, useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ColorPicker from "@/components/color-picker/color-picker";
|
||||
import FieldList from "./field/field-list";
|
||||
import IndexesList from "./index/indexes-list";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { getNextSequence } from "@/utils/field";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
|
||||
export interface TableAccordionBodyProps {
|
||||
table?: Table ,
|
||||
table: TableType,
|
||||
}
|
||||
|
||||
const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState(new Set(["fields"]));
|
||||
const [note, setNote] = useState<string>(table.note ? table.note : "");
|
||||
const { t } = useTranslation();
|
||||
const { editTable, createField } = useDatabase();
|
||||
|
||||
const onColorChange = useCallback((color : string) => {
|
||||
console.log ( color)
|
||||
} ,[])
|
||||
const onColorChange = useCallback((color: string) => {
|
||||
editTable({ id: table.id, color } as TableType);
|
||||
}, [table]);
|
||||
|
||||
const addField = (event: any) => {
|
||||
|
||||
event.stopPropagation && event.stopPropagation();
|
||||
createField({
|
||||
id: v4(),
|
||||
name: `field_${table.fields.length + 1}`,
|
||||
tableId: table.id,
|
||||
sequence: getNextSequence(table.fields),
|
||||
nullable: true,
|
||||
})
|
||||
}
|
||||
|
||||
const saveNote = () => {
|
||||
editTable({
|
||||
id : table.id ,
|
||||
note
|
||||
} as TableType)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -29,25 +54,33 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
<AccordionItem key="fields" aria-label="Fields"
|
||||
indicator={({ isOpen }) => (
|
||||
<div className={cn(
|
||||
'tarnsition-all duration-200',
|
||||
'tarnsition-all duration-200 ',
|
||||
isOpen ? "rotate-[-90deg]" : ""
|
||||
)}>
|
||||
|
||||
<ChevronLeft className="size-4 text-icon" />
|
||||
|
||||
</div>
|
||||
)}
|
||||
classNames={{
|
||||
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">
|
||||
<FileType className="size-4 " />
|
||||
<div className="group flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
|
||||
<FileType className="size-4" />
|
||||
<label className="text-sm w-full cursor-pointer">
|
||||
{t("db_controller.fields")}
|
||||
</label>
|
||||
<button
|
||||
className="size-4 p-0 text-xs opacity-0 group-hover:opacity-100 transition-all duration-200 hover:text-slate-700 text-icon"
|
||||
onClick={addField}
|
||||
>
|
||||
<Plus className="size-4 " />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FieldList />
|
||||
<FieldList table={table} />
|
||||
</AccordionItem>
|
||||
<AccordionItem key="indexes" aria-label="Indexes"
|
||||
indicator={({ isOpen }) => (
|
||||
@@ -69,7 +102,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
</label>
|
||||
</div>
|
||||
}>
|
||||
<IndexesList/>
|
||||
<IndexesList />
|
||||
</AccordionItem>
|
||||
<AccordionItem key="note" aria-label="Note"
|
||||
indicator={({ isOpen }) => (
|
||||
@@ -91,16 +124,22 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
</label>
|
||||
</div>
|
||||
}>
|
||||
<Textarea variant="bordered" className="w-full " label={t("db_controller.table_note")} classNames={{
|
||||
inputWrapper: "bg-default/80",
|
||||
<Textarea variant="bordered" className="w-full " label={t("db_controller.table_note")}
|
||||
classNames={{
|
||||
inputWrapper: "bg-default/80",
|
||||
}}
|
||||
value={note}
|
||||
onValueChange={setNote}
|
||||
onBlur={saveNote}
|
||||
|
||||
}} />
|
||||
/>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<hr className="bg-default-200" />
|
||||
<div className="flex p-2 pb-0 items-start">
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
defaultColor={table.color as string}
|
||||
onChange={onColorChange}
|
||||
/>
|
||||
</div>
|
||||
@@ -117,6 +156,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
{t("db_controller.add_index")}
|
||||
</Button>
|
||||
<Button
|
||||
onPressEnd={addField}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="font-semibold p-4"
|
||||
|
||||
+103
-22
@@ -1,25 +1,57 @@
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/tooltip/tooltip";
|
||||
import { randomColor } from "@/lib/colors";
|
||||
import { Table } from "@/lib/interfaces/table"
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { Button, cn, Divider, Input } from "@heroui/react";
|
||||
import { Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, EllipsisVertical, Focus, Grip, GripVertical, Pencil } from "lucide-react";
|
||||
|
||||
import { Button, cn, Divider, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, 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 { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { v4 } from "uuid";
|
||||
import { getNextSequence } from "@/utils/field";
|
||||
|
||||
export interface TableAccordionHeaderProps {
|
||||
table?: Table,
|
||||
table: TableType,
|
||||
isOpen?: boolean,
|
||||
id: string
|
||||
}
|
||||
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen, id }) => {
|
||||
|
||||
|
||||
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
|
||||
const { editTable, deleteTable, createField } = useDatabase();
|
||||
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
|
||||
const style = {
|
||||
borderLeft: "6px solid " + randomColor(),
|
||||
borderLeft: "6px solid " + table.color,
|
||||
};
|
||||
|
||||
const [tableName, setTableName] = useState<string>(table.name);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
|
||||
const saveTableName = async () => {
|
||||
await editTable({ id: table.id, name: tableName });
|
||||
setEditMode(false);
|
||||
}
|
||||
|
||||
const onDeleteTable = async () => {
|
||||
deleteTable(table.id)
|
||||
setPopOverOpen(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const addField = () => {
|
||||
setPopOverOpen(false)
|
||||
createField({
|
||||
id: v4(),
|
||||
name: `field_${table.fields.length + 1}`,
|
||||
tableId: table.id,
|
||||
sequence: getNextSequence(table.fields),
|
||||
nullable: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="group w-full flex h-12 gap-1 border-l-4 flex p-2 items-center"
|
||||
style={style}
|
||||
@@ -31,7 +63,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
<ChevronRight className="size-4 text-icon" />
|
||||
</div>
|
||||
<div className=" w-[1px] h-full bg-default-200">
|
||||
|
||||
|
||||
</div>
|
||||
{
|
||||
!editMode &&
|
||||
@@ -42,7 +74,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
className="w-full text-editable truncate px-2 py-1 text-sm font-semibold text-black"
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
>
|
||||
{id}
|
||||
{table.name}
|
||||
</label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -54,11 +86,13 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
{
|
||||
editMode && <>
|
||||
<Input
|
||||
placeholder={"Usres"}
|
||||
placeholder={"Table name"}
|
||||
autoFocus
|
||||
size="sm"
|
||||
value={tableName}
|
||||
onChange={(event: any) => setTableName(event.target.value)}
|
||||
variant="bordered"
|
||||
onBlur={() => setEditMode(false)}
|
||||
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 "
|
||||
/>
|
||||
@@ -66,7 +100,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
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-4 text-icon" />
|
||||
@@ -94,13 +128,60 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
<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.table_actions")}
|
||||
</h3>
|
||||
</div>
|
||||
<hr className="text-default-200" />
|
||||
<Listbox aria-label="Actions" className="p-0 pb-1" >
|
||||
<ListboxItem
|
||||
key="add_field"
|
||||
onPressEnd={addField}
|
||||
endContent={
|
||||
<FileType className="size-4 text-icon" />
|
||||
|
||||
}>
|
||||
Add Field
|
||||
</ListboxItem>
|
||||
<ListboxItem
|
||||
key="add_index"
|
||||
endContent={<FileKey className="size-4 text-icon " />}
|
||||
showDivider>
|
||||
Add Index
|
||||
</ListboxItem>
|
||||
|
||||
<ListboxItem
|
||||
key="duplicate"
|
||||
showDivider
|
||||
endContent={<Copy className="size-4 text-icon" />}>
|
||||
Duplicate
|
||||
</ListboxItem>
|
||||
<ListboxItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
onPressEnd={onDeleteTable}
|
||||
endContent={<Trash className="size-4" />}
|
||||
>
|
||||
Delete Table
|
||||
</ListboxItem>
|
||||
</Listbox>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,42 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/to
|
||||
import { Accordion, AccordionItem, Button, Input } from "@heroui/react"
|
||||
import { ChevronDown, Code, EllipsisVertical, Focus, Grid, Grip, List, Pencil, Table } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import TableAccordionHeader from "./table-accordion-item/table-accordion-header";
|
||||
import TableAccordionBody from "./table-accordion-item/table-accordion-body";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { v4 } from "uuid";
|
||||
import { randomColor } from "@/lib/colors";
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface Props { }
|
||||
|
||||
|
||||
const TablesController: React.FC<Props> = ({ }) => {
|
||||
|
||||
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3", "Item 4"]);
|
||||
|
||||
const { tables, createTable } = useDatabase();
|
||||
const { t } = useTranslation();
|
||||
const [selectedTable, setSelectedTable] = useState(new Set([]));
|
||||
|
||||
|
||||
const addNewTable = useCallback(async () => {
|
||||
const newTableId: string = v4();
|
||||
|
||||
await createTable({
|
||||
id: newTableId,
|
||||
name: `table_${tables.length + 1}`,
|
||||
color: randomColor(),
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
setSelectedTable(new Set([newTableId]) as any);
|
||||
}, [tables]);
|
||||
|
||||
|
||||
|
||||
const selectedTableId = selectedTable.values().next().value;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-2">
|
||||
@@ -61,6 +85,7 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
variant="solid"
|
||||
color="primary"
|
||||
radius="sm"
|
||||
onPressEnd={addNewTable}
|
||||
startContent={
|
||||
<Table className="h-4 w-4 " />
|
||||
}
|
||||
@@ -74,20 +99,25 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
<Accordion
|
||||
hideIndicator
|
||||
variant="splitted"
|
||||
selectedKeys={selectedTable}
|
||||
onSelectionChange={setSelectedTable as any}
|
||||
>
|
||||
{items.map(item => (
|
||||
{tables.map((table: TableType) => (
|
||||
<AccordionItem
|
||||
key={item}
|
||||
aria-label={item}
|
||||
key={table.id}
|
||||
aria-label={table.name}
|
||||
classNames={{
|
||||
trigger: "w-full hover:bg-default transition-all duration-200",
|
||||
base: "rounded-md shadow p-0 overflow-hidden",
|
||||
}}
|
||||
subtitle={
|
||||
<TableAccordionHeader id={item} />
|
||||
<TableAccordionHeader
|
||||
isOpen={selectedTableId == table.id}
|
||||
table={table}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TableAccordionBody />
|
||||
<TableAccordionBody table={table} />
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
import { createContext } from "react";
|
||||
|
||||
|
||||
|
||||
|
||||
export interface DatabaseContextType {
|
||||
tables: TableType[],
|
||||
data_types: DataType[]
|
||||
|
||||
createTable: (table: TableInsertType) => Promise<QueryResult>,
|
||||
editTable: (table: TableInsertType) => Promise<QueryResult>,
|
||||
deleteTable: (id: string) => Promise<QueryResult>,
|
||||
|
||||
createField: (field: FieldInsertType) => Promise<QueryResult>,
|
||||
editField: (field: FieldInsertType) => Promise<QueryResult>,
|
||||
deleteField: (id: string) => Promise<QueryResult>,
|
||||
orderTableFields: (fields: FieldType[]) => Promise<QueryResult>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export default createContext<DatabaseContextType>({} as DatabaseContextType);
|
||||
|
||||
/*
|
||||
|
||||
return Promise.all(
|
||||
fields.map((field : FieldType , index : number) => editField({id : field.id , sequence : index} as FieldType))
|
||||
)
|
||||
|
||||
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,97 @@
|
||||
|
||||
import DatabaseContext from "./database-context";
|
||||
import { useContext } from "react";
|
||||
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 { QueryResult } from "@powersync/web";
|
||||
import { FieldInsertType, fields, FieldType } from "@/lib/schemas/field-schema";
|
||||
|
||||
interface Props { children: React.ReactNode }
|
||||
|
||||
|
||||
const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
|
||||
const { data: tablesList, isLoading, error } = useQuery(toCompilableQuery(
|
||||
db.query.tables.findMany({
|
||||
with: {
|
||||
fields: {
|
||||
orderBy: asc(fields.sequence),
|
||||
}
|
||||
},
|
||||
orderBy: desc(tables.createdAt)
|
||||
})
|
||||
));
|
||||
|
||||
const { data: data_types } = useQuery(toCompilableQuery(
|
||||
db.query.data_types.findMany()
|
||||
));
|
||||
|
||||
|
||||
const createTable = async (table: TableInsertType): Promise<QueryResult> => {
|
||||
return await db.insert(tables).values(table)
|
||||
}
|
||||
const editTable = async (table: TableInsertType): Promise<QueryResult> => {
|
||||
return await db.update(tables).set(table).where(eq(tables.id, table.id))
|
||||
}
|
||||
const deleteTable = async (id: string): Promise<QueryResult> => {
|
||||
return await db.delete(tables).where(eq(tables.id, id));
|
||||
}
|
||||
|
||||
const createField = async (field: FieldInsertType): Promise<QueryResult> => {
|
||||
return await db.insert(fields).values(field);
|
||||
}
|
||||
const editField = async (field: FieldInsertType): Promise<QueryResult> => {
|
||||
return await db.update(fields).set(field).where(eq(fields.id, field.id))
|
||||
}
|
||||
const deleteField = async (id: string): Promise<QueryResult> => {
|
||||
return await db.delete(fields).where(eq(fields.id, id));
|
||||
}
|
||||
|
||||
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}
|
||||
);`;
|
||||
|
||||
return await powerSyncDb.execute(sql)
|
||||
}
|
||||
return (
|
||||
|
||||
<DatabaseContext.Provider value={{
|
||||
createTable,
|
||||
editTable,
|
||||
deleteTable,
|
||||
|
||||
createField,
|
||||
editField,
|
||||
deleteField,
|
||||
orderTableFields,
|
||||
tables: tablesList as TableType[],
|
||||
data_types
|
||||
}}>
|
||||
{children}
|
||||
</DatabaseContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useDatabase = () => useContext(DatabaseContext);
|
||||
|
||||
export default DatabaseProvider;
|
||||
|
||||
|
||||
|
||||
@@ -2,39 +2,42 @@
|
||||
import { PowerSyncDatabase } from '@powersync/web';
|
||||
import { PowerSyncContext } from "@powersync/react";
|
||||
import { createContext, Suspense, useContext, useEffect, useState } from 'react';
|
||||
import { AppSchema } from '@/lib/schemas/app-schema';
|
||||
import { AppSchema, drizzleSchema } from '@/lib/schemas/app-schema';
|
||||
import { StackRenderConnector } from '@/utils/stackrender-connector';
|
||||
import { CircularProgress } from '@heroui/react';
|
||||
|
||||
export const db = new PowerSyncDatabase({
|
||||
import { PowerSyncSQLiteDatabase, wrapPowerSyncWithDrizzle } from '@powersync/drizzle-driver';
|
||||
import { ExtractTablesWithRelations } from 'drizzle-orm';
|
||||
|
||||
export const powerSyncDb = new PowerSyncDatabase({
|
||||
database: {
|
||||
dbFilename: 'stackrender'
|
||||
dbFilename: 'stackrender.sqlite'
|
||||
},
|
||||
schema: AppSchema,
|
||||
|
||||
|
||||
});
|
||||
|
||||
export const db: PowerSyncSQLiteDatabase<typeof drizzleSchema> = wrapPowerSyncWithDrizzle(powerSyncDb, {
|
||||
schema: drizzleSchema
|
||||
});
|
||||
|
||||
const ConnectorContext = createContext<StackRenderConnector | null>(null);
|
||||
export const useConnector = () => useContext(ConnectorContext);
|
||||
|
||||
|
||||
interface SyncProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SyncProvider: React.FC<SyncProviderProps> = ({ children }) => {
|
||||
|
||||
const [powerSync] = useState(db);
|
||||
const [powerSync] = useState(powerSyncDb);
|
||||
const [connector] = useState(new StackRenderConnector());
|
||||
useEffect(() => {
|
||||
|
||||
useEffect(() => {
|
||||
powerSync.init();
|
||||
powerSync.connect(connector);
|
||||
}, [powerSync, connector])
|
||||
|
||||
return (
|
||||
<Suspense fallback={<CircularProgress/>}>
|
||||
<Suspense fallback={<CircularProgress />}>
|
||||
<PowerSyncContext.Provider value={powerSync}>
|
||||
<ConnectorContext.Provider value={connector}>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
|
||||
|
||||
|
||||
|
||||
export const getNextSequence = (fields: FieldType[]): number => {
|
||||
|
||||
if (fields.length == 0)
|
||||
return 0;
|
||||
|
||||
const maxSequenceItem = fields.reduce((max, field: FieldType) => {
|
||||
return field.sequence > max.sequence ? field : max
|
||||
});
|
||||
return maxSequenceItem.sequence + 1;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { AbstractPowerSyncDatabase, PowerSyncBackendConnector } from '@powersync/web';
|
||||
import { AbstractPowerSyncDatabase, PowerSyncBackendConnector, UpdateType } from '@powersync/web';
|
||||
|
||||
export type DemoConfig = {
|
||||
backendUrl: string;
|
||||
@@ -52,9 +52,9 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
|
||||
}
|
||||
|
||||
async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {
|
||||
|
||||
|
||||
const transaction = await database.getNextCrudTransaction();
|
||||
|
||||
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -66,6 +66,10 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
|
||||
try {
|
||||
let batch: any[] = [];
|
||||
for (let operation of transaction.crud) {
|
||||
|
||||
if (operation.op != UpdateType.DELETE && Object.keys(operation.opData as any).length == 0)
|
||||
continue
|
||||
|
||||
let payload = {
|
||||
op: operation.op,
|
||||
table: operation.table,
|
||||
@@ -74,19 +78,20 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
|
||||
};
|
||||
batch.push(payload);
|
||||
}
|
||||
if (batch.length > 0) {
|
||||
|
||||
const response = await fetch(`${this.config.backendUrl}/api/data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ batch })
|
||||
});
|
||||
const response = await fetch(`${this.config.backendUrl}/api/data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ batch })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Received ${response.status} from /api/data: ${await response.text()}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Received ${response.status} from /api/data: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
await transaction.complete(
|
||||
import.meta.env.VITE_CHECKPOINT_MODE == CheckpointMode.CUSTOM
|
||||
? await this.getCheckpoint(this._clientId)
|
||||
@@ -103,7 +108,7 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
|
||||
* when custom Write Checkpoints are enabled during build.
|
||||
*/
|
||||
async getCheckpoint(client_id: string) {
|
||||
|
||||
|
||||
const r = await fetch(`${this.config.backendUrl}/api/data/checkpoint`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user