mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 03:05:42 +00:00
Empty List implemented
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { Image } from "@heroui/react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
|
||||
|
||||
|
||||
interface EmptyListProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EmptyList: React.FC<EmptyListProps> = ({ title, description }) => {
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2 h-full pt-[86px]">
|
||||
{
|
||||
<Image
|
||||
width={82}
|
||||
src={"/public/stackrender.png"}
|
||||
|
||||
/>
|
||||
}
|
||||
{
|
||||
title &&
|
||||
<h1 className="text-font/90 font-bold text-lg">
|
||||
{title}
|
||||
</h1>
|
||||
}
|
||||
{
|
||||
|
||||
description &&
|
||||
<p className="text-sm text-font/70">
|
||||
{description}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default EmptyList;
|
||||
@@ -21,7 +21,7 @@ const Menu: React.FC = ({ }) => {
|
||||
const { canRedo, canUndo, undo, redo } = useDatabaseHistory();
|
||||
const { deleteMultiTables } = useDatabaseOperations();
|
||||
const { database } = useDatabase();
|
||||
const { setShowController, showController, cardinalityStyle, setCardinalityStyle } = useDiagramOps();
|
||||
const { openController, showController, cardinalityStyle, changeCardinalityStyle } = useDiagramOps();
|
||||
|
||||
const menu: MenuDropdownProps[] = useMemo(() => [
|
||||
{
|
||||
@@ -94,21 +94,21 @@ const Menu: React.FC = ({ }) => {
|
||||
id: "menu.symbolic",
|
||||
selected: cardinalityStyle == CardinalityStyle.SYMBOLIC,
|
||||
title: t("menu.symbolic"), clickHandler: () => {
|
||||
setCardinalityStyle(CardinalityStyle.SYMBOLIC)
|
||||
changeCardinalityStyle(CardinalityStyle.SYMBOLIC)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "menu.numeric",
|
||||
selected: cardinalityStyle == CardinalityStyle.NUMERIC,
|
||||
title: t("menu.numeric"), clickHandler: () => {
|
||||
setCardinalityStyle(CardinalityStyle.NUMERIC)
|
||||
changeCardinalityStyle(CardinalityStyle.NUMERIC)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "menu.hidden",
|
||||
selected: cardinalityStyle == CardinalityStyle.HIDDEN,
|
||||
title: t("menu.hidden"), clickHandler: () => {
|
||||
setCardinalityStyle(CardinalityStyle.HIDDEN)
|
||||
changeCardinalityStyle(CardinalityStyle.HIDDEN)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -117,7 +117,7 @@ const Menu: React.FC = ({ }) => {
|
||||
{
|
||||
id: "menu.controller_visibility",
|
||||
title: showController ? t("menu.hide_controller") : t("menu.show_controller"), shortcut: "Ctnl + B", divide: true, clickHandler: () => {
|
||||
setShowController(!showController)
|
||||
openController(!showController)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -151,7 +151,7 @@ const Menu: React.FC = ({ }) => {
|
||||
{ id: "menu.join_discord", title: t("menu.join_discord") },
|
||||
],
|
||||
},
|
||||
], [t, canRedo, canUndo, undo, redo, deleteMultiTables, database, showController, setShowController, cardinalityStyle, setCardinalityStyle, resolvedTheme]);
|
||||
], [t, canRedo, canUndo, undo, redo, deleteMultiTables, database, showController, openController ,changeCardinalityStyle , cardinalityStyle, resolvedTheme]);
|
||||
|
||||
return <div className="gap-1 flex">
|
||||
{
|
||||
|
||||
@@ -16,8 +16,6 @@ const ConnectionStatus: React.FC = () => {
|
||||
const clearProgressHandler: any = useRef(undefined);
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
|
||||
const [lastUploadAt, setLastUploadAt] = useState<Date | undefined>(() => {
|
||||
const last_upload_at = localStorage.getItem("last_upload_at");
|
||||
if (last_upload_at)
|
||||
@@ -47,6 +45,7 @@ const ConnectionStatus: React.FC = () => {
|
||||
return () => l?.();
|
||||
}, [powerSync]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (syncStatus.downloadProgress?.downloadedOperations && syncStatus.downloadProgress?.totalOperations) {
|
||||
|
||||
@@ -93,7 +92,7 @@ const ConnectionStatus: React.FC = () => {
|
||||
<div className="flex items-center ">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="border-1 flex gap-2 p-2 px-3 rounded-lg text-xs relative overflow-hidden border-divider dark:border-font/10">
|
||||
<div className="border-1 flex gap-2 p-2 px-3 rounded-sm text-xs relative overflow-hidden border-divider dark:border-font/10">
|
||||
<span className={cn("flex gap-2 font-semibold", syncStatus.connected ? "text-success" : "text-danger")}>
|
||||
{
|
||||
syncStatus.connected ?
|
||||
@@ -117,9 +116,9 @@ const ConnectionStatus: React.FC = () => {
|
||||
|
||||
<Progress
|
||||
classNames={{
|
||||
base: "max-w-md absolute bottom-[-2px] left-0 w-full ",
|
||||
base: "max-w-md absolute bottom-[-2px] left-0 w-full rounded-sm",
|
||||
track: " bg-transparent",
|
||||
indicator: "bg-gradient-to-r from-secondary-200 to-primary ",
|
||||
indicator: "bg-gradient-to-r from-secondary-100 to-primary ",
|
||||
}}
|
||||
radius="sm"
|
||||
aria-label="upload progress"
|
||||
@@ -130,7 +129,7 @@ const ConnectionStatus: React.FC = () => {
|
||||
|
||||
</>
|
||||
:
|
||||
<span className="text-font/70 truncate font-semibold" >
|
||||
<span className="text-font/90 truncate font-semibold" >
|
||||
{t("connection_status.saved")} {deltaUploadTime}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -43,6 +43,13 @@ export const en = {
|
||||
select_table: "Select table",
|
||||
select_field: "Select field",
|
||||
|
||||
empty_list : {
|
||||
no_tables : "No Tables" ,
|
||||
no_tables_description : "Create a Table to get started." ,
|
||||
|
||||
no_relationships : "No Relationships" ,
|
||||
no_relationships_description : "Create relationships to connect two tables"
|
||||
} ,
|
||||
|
||||
cardinality: {
|
||||
name: "Cardinality",
|
||||
@@ -99,12 +106,8 @@ export const en = {
|
||||
index_setting: "Index Setting",
|
||||
table_actions: "Table Actions",
|
||||
actions: "Actions",
|
||||
|
||||
|
||||
delete_index: "Delete Index",
|
||||
index_name: "Index name",
|
||||
|
||||
|
||||
create_relationship: "Create Relationship",
|
||||
relationship_error: "To create a relationship, the primary key and foreign key must be of the same type.",
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ const DatabasePage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { resolvedTheme } = useTheme();
|
||||
// Extract database state and operations
|
||||
const { database, getField, databases, isSwitchingDatabase, isLoading, isFetching } = useDatabase();
|
||||
const { database, getField, databases, isSwitchingDatabase, isLoading, isFetching, isSyncing } = useDatabase();
|
||||
|
||||
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
|
||||
|
||||
@@ -64,7 +64,7 @@ const DatabasePage: React.FC = () => {
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
// Diagram-related state (e.g. connection in progress)
|
||||
const { setIsConnectionInProgress, cardinalityStyle, setCardinalityStyle } = useDiagramOps();
|
||||
const { setIsConnectionInProgress, cardinalityStyle } = useDiagramOps();
|
||||
|
||||
// Destructure tables and relationships from database
|
||||
const { tables, relationships } = database || { tables: [], relationships: [] };
|
||||
@@ -78,7 +78,7 @@ const DatabasePage: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (isLoading || isFetching || !(syncStatus.currentStatus as any).options.hasSynced)
|
||||
if (isSyncing)
|
||||
return;
|
||||
|
||||
// no database selected , open (open database) Modal
|
||||
@@ -94,7 +94,7 @@ const DatabasePage: React.FC = () => {
|
||||
closable: false
|
||||
});
|
||||
}
|
||||
}, [database?.id, isLoading, isFetching, (syncStatus.currentStatus as any).options.hasSynced])
|
||||
}, [database?.id, isSyncing])
|
||||
|
||||
useEffect(() => {
|
||||
if (isSwitchingDatabase) {
|
||||
@@ -254,11 +254,15 @@ const DatabasePage: React.FC = () => {
|
||||
</Controls >
|
||||
<MiniMap
|
||||
nodeStrokeWidth={4}
|
||||
className="dark:bg-content1 bg-background border-1 border-default-200 dark:border-divider rounded-md"
|
||||
className="dark:bg-content1 bg-background border-1 border-default-200 dark:border-divider rounded-md "
|
||||
maskStrokeColor={resolvedTheme == "dark" ? "#272c35" : "#e9edf1"}
|
||||
maskColor={resolvedTheme == "dark" ? "#20252c99" : "#f2f4f733"}
|
||||
maskStrokeWidth={1}
|
||||
nodeClassName={"fill-default-300 dark:fill-font/30"}
|
||||
style={{
|
||||
width: 164,
|
||||
height: 128
|
||||
}}
|
||||
|
||||
/>
|
||||
<Background className="dark:bg-background-100 " />
|
||||
|
||||
+46
-33
@@ -6,14 +6,16 @@ import { useTranslation } from "react-i18next";
|
||||
import RelationshipAccordionHeader from "./relationship-accordion-item/relationship-accordion-header";
|
||||
import RelationshipAccordionBody from "./relationship-accordion-item/relationship-accordion-body";
|
||||
|
||||
import CreateRelationshipForm from "../../modals/create-relationship-modal";
|
||||
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
|
||||
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
import { getDefaultRelationshipName } from "@/utils/relationship";
|
||||
import EmptyList from "@/components/empty-list/empty-list";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +27,7 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
|
||||
|
||||
const { open } = useModal();
|
||||
const { database } = useDatabase();
|
||||
const { database, isSyncing } = useDatabase();
|
||||
const { relationships: allRelationships } = database || { relationships: [] };
|
||||
const [relationships, setRelationships] = useState<RelationshipType[]>(allRelationships);
|
||||
|
||||
@@ -49,7 +51,7 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
open(Modals.CREATE_RELATIONSHIP, {
|
||||
onRlationshipCreated: (id: string) => setSelectedRelationship(new Set([id]) as any)
|
||||
onRlationshipCreated: (id: string) => setSelectedRelationship(new Set([id]) as any)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -130,37 +132,48 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
>{t("db_controller.add_relationship")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className=" flex-1 overflow-auto">
|
||||
{
|
||||
allRelationships.length > 0 &&
|
||||
<div className=" flex-1 overflow-auto">
|
||||
<Accordion
|
||||
hideIndicator
|
||||
|
||||
<Accordion
|
||||
hideIndicator
|
||||
|
||||
isCompact
|
||||
selectedKeys={selectedRelationship}
|
||||
onSelectionChange={setSelectedRelationship as any}
|
||||
>
|
||||
{relationships.map((relationship: RelationshipType) => (
|
||||
<AccordionItem
|
||||
key={relationship.id}
|
||||
id={relationship.id}
|
||||
aria-label={relationship.id}
|
||||
classNames={{
|
||||
trigger: "w-full hover:bg-default transition-all duration-200 h-12 dark:hover:bg-background",
|
||||
isCompact
|
||||
selectedKeys={selectedRelationship}
|
||||
onSelectionChange={setSelectedRelationship as any}
|
||||
>
|
||||
{relationships.map((relationship: RelationshipType) => (
|
||||
<AccordionItem
|
||||
key={relationship.id}
|
||||
id={relationship.id}
|
||||
aria-label={relationship.id}
|
||||
classNames={{
|
||||
trigger: "w-full hover:bg-default transition-all duration-200 h-12 dark:hover:bg-background",
|
||||
|
||||
|
||||
base: "rounded-md p-0 overflow-hidden",
|
||||
}}
|
||||
subtitle={
|
||||
<RelationshipAccordionHeader
|
||||
isOpen={selectedRelationshipId == relationship.id}
|
||||
relationship={relationship} />
|
||||
}
|
||||
>
|
||||
<RelationshipAccordionBody relationship={relationship} />
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
base: "rounded-md p-0 overflow-hidden",
|
||||
}}
|
||||
subtitle={
|
||||
<RelationshipAccordionHeader
|
||||
isOpen={selectedRelationshipId == relationship.id}
|
||||
relationship={relationship} />
|
||||
}
|
||||
>
|
||||
<RelationshipAccordionBody relationship={relationship} />
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
!isSyncing && allRelationships.length == 0 &&
|
||||
<EmptyList
|
||||
title={t("db_controller.empty_list.no_relationships")}
|
||||
description={t("db_controller.empty_list.no_relationships_description")}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import { v4 } from "uuid";
|
||||
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import SqlPreview from "../sql-preview";
|
||||
import EmptyList from "@/components/empty-list/empty-list";
|
||||
|
||||
|
||||
const PADDING_X = 40;
|
||||
@@ -19,19 +20,19 @@ const PADDING_Y = 80;
|
||||
|
||||
const TablesController: React.FC = ({ }) => {
|
||||
|
||||
const { database } = useDatabase();
|
||||
const { database , isSyncing } = useDatabase();
|
||||
const { createTable, getInteger } = useDatabaseOperations();
|
||||
const { getViewport } = useReactFlow();
|
||||
const { tables: allTables } = database || { tables: [] };
|
||||
const [tables, setTables] = useState<TableType[]>(allTables);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [selectedTable, setSelectedTable] = useState(new Set([]));
|
||||
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
|
||||
const { focusedTableId } = useDiagram();
|
||||
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
const accordionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
|
||||
useEffect(() => searchTables(), [allTables]);
|
||||
|
||||
const addNewTable = useCallback(async () => {
|
||||
@@ -65,7 +66,7 @@ const TablesController: React.FC = ({ }) => {
|
||||
useEffect(() => {
|
||||
if (focusedTableId) {
|
||||
setSelectedTable(new Set([focusedTableId]) as any);
|
||||
setShowSqlPreview(false) ;
|
||||
setShowSqlPreview(false);
|
||||
const accordionItem = document.getElementById(focusedTableId)
|
||||
if (accordionItem)
|
||||
accordionItem?.scrollIntoView({
|
||||
@@ -145,7 +146,7 @@ const TablesController: React.FC = ({ }) => {
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
!showSqlPreview &&
|
||||
allTables.length > 0 && !showSqlPreview &&
|
||||
<div className=" flex-1 overflow-auto">
|
||||
<Accordion
|
||||
hideIndicator
|
||||
@@ -177,14 +178,20 @@ const TablesController: React.FC = ({ }) => {
|
||||
</div>
|
||||
}
|
||||
{
|
||||
showSqlPreview &&
|
||||
allTables.length > 0 && showSqlPreview &&
|
||||
<div className=" flex-1 overflow-auto ">
|
||||
<SqlPreview
|
||||
tableFilterIds={tableFilterIds}
|
||||
/>
|
||||
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!isSyncing && allTables.length == 0 &&
|
||||
<EmptyList
|
||||
title={t("db_controller.empty_list.no_tables")}
|
||||
description={t("db_controller.empty_list.no_tables_description")}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ interface Props { children: React.ReactNode };
|
||||
const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
const udpateDbFlag = useRef(false);
|
||||
const { database , isLoading , isFetching } = useDatabase() ;
|
||||
const { executeDbDiffOps } = useDatabaseOperations();
|
||||
const { database, isLoading, isFetching } = useDatabase();
|
||||
const { executeDbDiffOps } = useDatabaseOperations();
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
|
||||
const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo, reset }] = useUndo<DatabaseType>(database as DatabaseType);
|
||||
@@ -26,12 +26,12 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
useEffect(() => {
|
||||
if (database && datatbaseState.present && !isLoading && !isFetching) {
|
||||
udpateDbFlag.current = false;
|
||||
|
||||
|
||||
const presentHash: string | undefined = hash(datatbaseState.present, { algorithm: 'sha1' });
|
||||
const databaseHash: string = hash(database, { algorithm: 'sha1' });
|
||||
|
||||
if (presentHash != databaseHash) {
|
||||
|
||||
if (presentHash != databaseHash) {
|
||||
|
||||
set(database);
|
||||
}
|
||||
}
|
||||
@@ -39,18 +39,18 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
|
||||
const undo = useCallback(() => {
|
||||
if (!isProcessing) {
|
||||
if (!isProcessing && !isLoading && !isFetching) {
|
||||
udpateDbFlag.current = true;
|
||||
undoChanges();
|
||||
}
|
||||
}, [undoChanges, udpateDbFlag, isProcessing]);
|
||||
}, [undoChanges, udpateDbFlag, isProcessing , isLoading , isFetching]);
|
||||
|
||||
const redo = useCallback(() => {
|
||||
if (!isProcessing) {
|
||||
if (!isProcessing && !isLoading && !isFetching) {
|
||||
udpateDbFlag.current = true;
|
||||
redoChanges();
|
||||
}
|
||||
}, [redoChanges, udpateDbFlag, isProcessing]);
|
||||
}, [redoChanges, udpateDbFlag, isProcessing, isLoading , isFetching]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -66,8 +66,8 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
const normalizedPresent = normalizeDatabase(datatbaseState.present);
|
||||
const differences = compare(normalizedDatabase, normalizedPresent);
|
||||
|
||||
//console.log ( differences )
|
||||
|
||||
|
||||
if (differences && differences.length > 0) {
|
||||
setIsProcessing(true);
|
||||
|
||||
@@ -75,13 +75,13 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// console.log ( operations) ;
|
||||
console.log(operations);
|
||||
await executeDbDiffOps(operations)
|
||||
setIsProcessing(false);
|
||||
}
|
||||
catch (error) {
|
||||
console.log ( error ) ;
|
||||
/// set(database);
|
||||
console.log(error);
|
||||
|
||||
setIsProcessing(false);
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -20,6 +20,7 @@ interface DatabaseDataContextType {
|
||||
databases: DatabaseType[],
|
||||
isLoading: boolean,
|
||||
isFetching : boolean ,
|
||||
isSyncing : boolean ,
|
||||
isSwitchingDatabase: boolean,
|
||||
getField: (tableId: string, id: string) => FieldType | undefined,
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DatabaseDataContext, DatabaseOperationsContext } from "./database-conte
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { db } from "../sync-provider/sync-provider";
|
||||
import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema";
|
||||
import { useQuery } from "@powersync/react";
|
||||
import { usePowerSync, useQuery } from "@powersync/react";
|
||||
import { toCompilableQuery } from "@powersync/drizzle-driver";
|
||||
import { asc, count, desc, eq, inArray, or } from "drizzle-orm";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
@@ -23,7 +23,7 @@ import { deleteFieldsWithCascade, deleteTablesWithCascade } from "@/utils/cascad
|
||||
interface Props { children: React.ReactNode }
|
||||
|
||||
const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
const syncStatus = usePowerSync();
|
||||
const [currentDatabaseId, setCurrentDatabaseId] = useState<string | undefined>(localStorage.getItem("database_id") as string | undefined);
|
||||
// Fetch all databases
|
||||
const { data: databases, isLoading: loadingDatabases, isFetching: fetchingDatabases } = useQuery(toCompilableQuery(
|
||||
@@ -101,7 +101,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase;
|
||||
const isFetching: boolean = fetchingDatabase || fetchingDatatypes || fetchingDatabases;
|
||||
|
||||
|
||||
const isSyncing : boolean = isLoading ||isFetching || !(syncStatus.currentStatus as any).options.hasSynced ;
|
||||
|
||||
const isSwitchingDatabase: boolean = useMemo(() => {
|
||||
return fetchingDatabase && currentDatabaseId != (database as any)?.id
|
||||
@@ -136,7 +136,6 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}).where(eq(databaseModel.id, databaseId))
|
||||
}, []);
|
||||
|
||||
|
||||
// CRUD operations for Tables
|
||||
const createTable = useCallback(async (table: TableInsertType): Promise<void> => {
|
||||
if (currentDatabaseId) {
|
||||
@@ -153,8 +152,6 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
throw Error("No Database selected");
|
||||
}
|
||||
@@ -302,8 +299,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
name: operation.chnages.name
|
||||
}).where(eq(databaseModel.id, currentDatabaseId)));
|
||||
}
|
||||
else if (operation.type == "UPDATE_NUM_TABLES") {
|
||||
console.log("update number of tables with ", operation.value);
|
||||
else if (operation.type == "UPDATE_NUM_TABLES") {
|
||||
if (currentDatabaseId)
|
||||
operations.push(
|
||||
tx.update(databaseModel).set({
|
||||
@@ -394,12 +390,10 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(operations);
|
||||
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
throw error
|
||||
}
|
||||
}, [db, currentDatabaseId]);
|
||||
@@ -520,6 +514,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
isSwitchingDatabase,
|
||||
isFetching,
|
||||
getField,
|
||||
isSyncing
|
||||
}}>
|
||||
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
|
||||
<DatabaseHistoryProvider>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CardinalityStyle } from "@/lib/database";
|
||||
import { createContext, Dispatch } from "react";
|
||||
import { createContext, Dispatch } from "react";
|
||||
|
||||
|
||||
interface DiagramDataContextType {
|
||||
@@ -9,14 +9,13 @@ interface DiagramDataContextType {
|
||||
|
||||
interface DiagramOpsContextType {
|
||||
focusOnTable: (id: string, transition?: boolean) => void,
|
||||
focusOnRelationship: (id: string, transition?: boolean , withNavigate? : boolean) => void,
|
||||
focusOnRelationship: (id: string, transition?: boolean, withNavigate?: boolean) => void,
|
||||
setIsConnectionInProgress: Dispatch<boolean>,
|
||||
isConnectionInProgress: boolean ,
|
||||
showController : boolean ,
|
||||
setShowController : ( value : boolean ) => void ,
|
||||
cardinalityStyle : CardinalityStyle ,
|
||||
setCardinalityStyle : (style : CardinalityStyle) => void
|
||||
|
||||
isConnectionInProgress: boolean,
|
||||
showController: boolean,
|
||||
openController: (value: boolean) => void,
|
||||
cardinalityStyle: CardinalityStyle,
|
||||
changeCardinalityStyle: (style: CardinalityStyle) => void
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import { DiagramDataContext, DiagramOpsContext } from "./diagram-context";
|
||||
import { CardinalityStyle } from "@/lib/database";
|
||||
|
||||
|
||||
const CONTROLLER_VISIBILITY_KEY = "is_controller_open";
|
||||
const CARDINALITY_STYLE_KEY = "cardinality_style";
|
||||
|
||||
interface Props { children: React.ReactNode }
|
||||
|
||||
@@ -17,8 +19,20 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
const [focusedTableId, setFocusedTableId] = useState<string | undefined>(undefined)
|
||||
const [focusedRelationshipId, setFocusedRelationshipId] = useState<string | undefined>(undefined)
|
||||
const [isConnectionInProgress, setIsConnectionInProgress] = useState<boolean>(false);
|
||||
const [showController, setShowController] = useState<boolean>(true);
|
||||
const [cardinalityStyle, setCardinalityStyle] = useState<CardinalityStyle>(CardinalityStyle.SYMBOLIC);
|
||||
|
||||
const [showController, setShowController] = useState<boolean>(() => {
|
||||
const value: string | null = localStorage.getItem(CONTROLLER_VISIBILITY_KEY) ;
|
||||
if (value)
|
||||
return JSON.parse(value);
|
||||
return true;
|
||||
});
|
||||
const [cardinalityStyle, setCardinalityStyle] = useState<CardinalityStyle>(() => {
|
||||
const value: string | null = localStorage.getItem(CARDINALITY_STYLE_KEY);
|
||||
|
||||
if (value)
|
||||
return value as CardinalityStyle;
|
||||
return CardinalityStyle.SYMBOLIC;
|
||||
});
|
||||
|
||||
const focusOnTable = useCallback((id: string, transition: boolean = false) => {
|
||||
navigate("/database/tables");
|
||||
@@ -26,8 +40,8 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
const selected: boolean = node.id === id;
|
||||
|
||||
|
||||
|
||||
|
||||
if (selected && transition) {
|
||||
fitView({
|
||||
duration: 500,
|
||||
@@ -60,7 +74,7 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
setEdges((edges) =>
|
||||
edges.map((edge) => {
|
||||
|
||||
|
||||
const selected: boolean = edge.id === id;
|
||||
|
||||
if (selected && transition) {
|
||||
@@ -87,6 +101,16 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
}, [setFocusedRelationshipId]);
|
||||
|
||||
|
||||
const changeCardinalityStyle = useCallback((style : CardinalityStyle) => {
|
||||
localStorage.setItem(CARDINALITY_STYLE_KEY , style) ;
|
||||
setCardinalityStyle( style) ;
|
||||
} , []) ;
|
||||
|
||||
const openController = useCallback((isOpen : boolean) => {
|
||||
localStorage.setItem( CONTROLLER_VISIBILITY_KEY , JSON.stringify(isOpen) ) ;
|
||||
setShowController(isOpen) ;
|
||||
} , [])
|
||||
|
||||
const contextDatatValue = useMemo(() => ({
|
||||
focusedTableId,
|
||||
focusedRelationshipId,
|
||||
@@ -99,9 +123,9 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
setIsConnectionInProgress,
|
||||
isConnectionInProgress,
|
||||
showController,
|
||||
setShowController,
|
||||
cardinalityStyle,
|
||||
setCardinalityStyle
|
||||
changeCardinalityStyle ,
|
||||
openController
|
||||
|
||||
}), [focusOnTable, focusOnRelationship, setIsConnectionInProgress, isConnectionInProgress, showController, setShowController, cardinalityStyle,
|
||||
setCardinalityStyle])
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
|
||||
import { PowerSyncDatabase } from '@powersync/web';
|
||||
import { PowerSyncContext } from "@powersync/react";
|
||||
import { PowerSyncContext, usePowerSync } from "@powersync/react";
|
||||
import { createContext, Suspense, useContext, useEffect, useState } from 'react';
|
||||
import { AppSchema, drizzleSchema } from '@/lib/schemas/app-schema';
|
||||
import { StackRenderConnector } from '@/utils/stackrender-connector';
|
||||
import { CircularProgress } from '@heroui/react';
|
||||
import { PowerSyncSQLiteDatabase, wrapPowerSyncWithDrizzle } from '@powersync/drizzle-driver';
|
||||
|
||||
|
||||
export const powerSyncDb = new PowerSyncDatabase({
|
||||
database: {
|
||||
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);
|
||||
|
||||
@@ -39,7 +39,11 @@ export const SyncProvider: React.FC<SyncProviderProps> = ({ children }) => {
|
||||
powerSync.connect(connector);
|
||||
};
|
||||
setup();
|
||||
}, [powerSync, connector])
|
||||
}, [powerSync, connector]);
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Suspense fallback={<CircularProgress />}>
|
||||
<PowerSyncContext.Provider value={powerSync}>
|
||||
|
||||
+76
-17
@@ -1,3 +1,15 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// ❗ PowerSync Limitation: Manual Cascade Deletes Required
|
||||
//
|
||||
// PowerSync uses SQLite *views* instead of real tables, which disables support
|
||||
// for standard SQLite features like FOREIGN KEY constraints and ON DELETE CASCADE.
|
||||
//
|
||||
// Because of this, any time we delete a parent record (like a table, field, or index),
|
||||
// we must manually delete all dependent child records to maintain data integrity.
|
||||
//
|
||||
// These functions perform **manual cascade deletes** to avoid orphaned records.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
import { fields } from "@/lib/schemas/field-schema";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
import { ExtractTablesWithRelations, inArray, or } from "drizzle-orm";
|
||||
@@ -7,45 +19,92 @@ import { relationships } from "@/lib/schemas/relationship-schema";
|
||||
import { field_indices } from "@/lib/schemas/field_index-schema";
|
||||
import { indices } from "@/lib/schemas/index-schema";
|
||||
|
||||
|
||||
|
||||
export const deleteFieldsWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>): Promise<QueryResult[]> => {
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// deleteFieldsWithCascade
|
||||
//
|
||||
// Manually deletes all related data when a field is removed.
|
||||
// Removes associated relationships (source/target), field-index mappings, then the field itself.
|
||||
// Used when deleting one or more fields by ID.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
export const deleteFieldsWithCascade = async (
|
||||
ids: string[],
|
||||
tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>
|
||||
): Promise<QueryResult[]> => {
|
||||
return Promise.all([
|
||||
tx.delete(relationships).where(or(inArray(relationships.sourceFieldId, ids), inArray(relationships.targetFieldId, ids))),
|
||||
tx.delete(relationships).where(or(
|
||||
inArray(relationships.sourceFieldId, ids),
|
||||
inArray(relationships.targetFieldId, ids)
|
||||
)),
|
||||
tx.delete(field_indices).where(inArray(field_indices.fieldId, ids)),
|
||||
tx.delete(fields).where(inArray(fields.id, ids)),
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTablesWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>): Promise<QueryResult[]> => {
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// deleteTablesWithCascade
|
||||
//
|
||||
// Handles deletion of entire tables, and manually cascades to:
|
||||
// - all fields belonging to the table
|
||||
// - all indices attached to the table
|
||||
// - all relationships and field-index mappings tied to those fields/indices
|
||||
//
|
||||
// This ensures full cleanup when a table is removed in PowerSync context.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
export const deleteTablesWithCascade = async (
|
||||
ids: string[],
|
||||
tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>
|
||||
): Promise<QueryResult[]> => {
|
||||
|
||||
// Fetch all field IDs belonging to the tables
|
||||
let fieldIds: { id: string }[] | string[] = await tx.select({
|
||||
id: fields.id
|
||||
}).from(fields).where(inArray(fields.tableId, ids));
|
||||
|
||||
// Fetch all index IDs belonging to the tables
|
||||
let indicesIds: { id: string }[] | string[] = await tx.select({
|
||||
id: indices.id
|
||||
}).from(indices).where(inArray(indices.tableId, ids));
|
||||
|
||||
fieldIds = fieldIds.map((fieldId: { id: string }) => fieldId.id);
|
||||
indicesIds = indicesIds.map((indexId: { id: string }) => indexId.id);
|
||||
// Normalize into string arrays
|
||||
fieldIds = fieldIds.map((fieldId) => fieldId.id);
|
||||
indicesIds = indicesIds.map((indexId) => indexId.id);
|
||||
|
||||
return Promise.all([
|
||||
tx.delete(relationships).where(or(inArray(relationships.sourceFieldId, fieldIds), inArray(relationships.targetFieldId, fieldIds))),
|
||||
// Delete relationships where fields are involved
|
||||
tx.delete(relationships).where(or(
|
||||
inArray(relationships.sourceFieldId, fieldIds),
|
||||
inArray(relationships.targetFieldId, fieldIds)
|
||||
)),
|
||||
|
||||
// Delete field-index links for those fields
|
||||
tx.delete(field_indices).where(inArray(field_indices.fieldId, fieldIds)),
|
||||
|
||||
// Delete all fields in the table
|
||||
tx.delete(fields).where(inArray(fields.id, fieldIds)),
|
||||
|
||||
// Delete field-index links for indices
|
||||
tx.delete(field_indices).where(inArray(field_indices.indexId, indicesIds)),
|
||||
|
||||
// Delete all indices belonging to the table
|
||||
tx.delete(indices).where(inArray(indices.id, indicesIds)),
|
||||
|
||||
// Finally, delete the table(s) themselves
|
||||
tx.delete(tables).where(inArray(tables.id, ids)),
|
||||
]);
|
||||
};
|
||||
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
export const deleteIndicesWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>): Promise<QueryResult[]> => {
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// deleteIndicesWithCascade
|
||||
//
|
||||
// When removing an index, this ensures that any field-index links are removed
|
||||
// before the index itself is deleted. Prevents dangling associations.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
export const deleteIndicesWithCascade = async (
|
||||
ids: string[],
|
||||
tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>
|
||||
): Promise<QueryResult[]> => {
|
||||
return Promise.all([
|
||||
tx.delete(field_indices).where(inArray(field_indices.indexId, ids)),
|
||||
tx.delete(indices).where(inArray(indices.id, ids))
|
||||
])
|
||||
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -279,7 +279,10 @@ export function normalizeDatabase(db: DatabaseType): any {
|
||||
{
|
||||
...table,
|
||||
fields: Object.fromEntries(
|
||||
table.fields.map(field => [field.id, field])
|
||||
table.fields.map(field => [field.id, {
|
||||
...field ,
|
||||
type : undefined
|
||||
}])
|
||||
),
|
||||
indices: Object.fromEntries(
|
||||
table.indices.map(index => [index.id, {
|
||||
|
||||
Reference in New Issue
Block a user