diff --git a/src/context/chartdb-context/chartdb-provider.tsx b/src/context/chartdb-context/chartdb-provider.tsx index 26691ea3..bde83fdc 100644 --- a/src/context/chartdb-context/chartdb-provider.tsx +++ b/src/context/chartdb-context/chartdb-provider.tsx @@ -22,6 +22,8 @@ import { defaultSchemas } from '@/lib/data/default-schemas'; import { useEventEmitter } from 'ahooks'; import type { DBDependency } from '@/lib/domain/db-dependency'; import { storageInitialValue } from '../storage-context/storage-context'; +import { useDiff } from '../diff-context/use-diff'; +import type { DiffCalculatedEvent } from '../diff-context/diff-context'; export interface ChartDBProviderProps { diagram?: Diagram; @@ -30,7 +32,8 @@ export interface ChartDBProviderProps { export const ChartDBProvider: React.FC< React.PropsWithChildren -> = ({ children, diagram, readonly }) => { +> = ({ children, diagram, readonly: readonlyProp }) => { + const { hasDiff } = useDiff(); let db = useStorage(); const events = useEventEmitter(); const { setSchemasFilter, schemasFilter } = useLocalConfig(); @@ -53,9 +56,33 @@ export const ChartDBProvider: React.FC< const [dependencies, setDependencies] = useState( diagram?.dependencies ?? [] ); + const { events: diffEvents } = useDiff(); + + const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => { + const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data; + setTables((tables) => + [...tables, ...(tablesAdded ?? [])].map((table) => { + const fields = fieldsAdded.get(table.id); + return fields + ? { ...table, fields: [...table.fields, ...fields] } + : table; + }) + ); + setRelationships((relationships) => [ + ...relationships, + ...(relationshipsAdded ?? []), + ]); + }, []); + + diffEvents.useSubscription(diffCalculatedHandler); const defaultSchemaName = defaultSchemas[databaseType]; + const readonly = useMemo( + () => readonlyProp ?? hasDiff ?? false, + [readonlyProp, hasDiff] + ); + if (readonly) { db = storageInitialValue; } diff --git a/src/context/diff-context/diff-check/diff-check.ts b/src/context/diff-context/diff-check/diff-check.ts new file mode 100644 index 00000000..31376efd --- /dev/null +++ b/src/context/diff-context/diff-check/diff-check.ts @@ -0,0 +1,433 @@ +import type { Diagram } from '@/lib/domain/diagram'; +import type { + ChartDBDiff, + DiffMap, + DiffObject, + FieldDiffAttribute, +} from '../types'; +import type { DBField } from '@/lib/domain/db-field'; +import type { DBIndex } from '@/lib/domain/db-index'; + +export function getDiffMapKey({ + diffObject, + objectId, + attribute, +}: { + diffObject: DiffObject; + objectId: string; + attribute?: string; +}): string { + return attribute + ? `${diffObject}-${attribute}-${objectId}` + : `${diffObject}-${objectId}`; +} + +export function generateDiff({ + diagram, + newDiagram, +}: { + diagram: Diagram; + newDiagram: Diagram; +}): { + diffMap: DiffMap; + changedTables: Map; + changedFields: Map; +} { + const newDiffs = new Map(); + const changedTables = new Map(); + const changedFields = new Map(); + + // Compare tables + compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables }); + + // Compare fields and indexes for matching tables + compareTableContents({ + diagram, + newDiagram, + diffMap: newDiffs, + changedTables, + changedFields, + }); + + // Compare relationships + compareRelationships({ diagram, newDiagram, diffMap: newDiffs }); + + return { diffMap: newDiffs, changedTables, changedFields }; +} + +// Compare tables between diagrams +function compareTables({ + diagram, + newDiagram, + diffMap, + changedTables, +}: { + diagram: Diagram; + newDiagram: Diagram; + diffMap: DiffMap; + changedTables: Map; +}) { + const oldTables = diagram.tables || []; + const newTables = newDiagram.tables || []; + + // Check for added tables + for (const newTable of newTables) { + if (!oldTables.find((t) => t.id === newTable.id)) { + diffMap.set( + getDiffMapKey({ diffObject: 'table', objectId: newTable.id }), + { + object: 'table', + type: 'added', + tableId: newTable.id, + } + ); + changedTables.set(newTable.id, true); + } + } + + // Check for removed tables + for (const oldTable of oldTables) { + if (!newTables.find((t) => t.id === oldTable.id)) { + diffMap.set( + getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }), + { + object: 'table', + type: 'removed', + tableId: oldTable.id, + } + ); + changedTables.set(oldTable.id, true); + } + } + + // Check for table name and comments changes + for (const oldTable of oldTables) { + const newTable = newTables.find((t) => t.id === oldTable.id); + + if (!newTable) continue; + + if (oldTable.name !== newTable.name) { + diffMap.set( + getDiffMapKey({ + diffObject: 'table', + objectId: oldTable.id, + attribute: 'name', + }), + { + object: 'table', + type: 'changed', + tableId: oldTable.id, + attributes: 'name', + newValue: newTable.name, + oldValue: oldTable.name, + } + ); + + changedTables.set(oldTable.id, true); + } + + if (oldTable.comments !== newTable.comments) { + diffMap.set( + getDiffMapKey({ + diffObject: 'table', + objectId: oldTable.id, + attribute: 'comments', + }), + { + object: 'table', + type: 'changed', + tableId: oldTable.id, + attributes: 'comments', + newValue: newTable.comments, + oldValue: oldTable.comments, + } + ); + + changedTables.set(oldTable.id, true); + } + } +} + +// Compare fields and indexes for matching tables +function compareTableContents({ + diagram, + newDiagram, + diffMap, + changedTables, + changedFields, +}: { + diagram: Diagram; + newDiagram: Diagram; + diffMap: DiffMap; + changedTables: Map; + changedFields: Map; +}) { + const oldTables = diagram.tables || []; + const newTables = newDiagram.tables || []; + + // For each table that exists in both diagrams + for (const oldTable of oldTables) { + const newTable = newTables.find((t) => t.id === oldTable.id); + if (!newTable) continue; + + // Compare fields + compareFields({ + tableId: oldTable.id, + oldFields: oldTable.fields, + newFields: newTable.fields, + diffMap, + changedTables, + changedFields, + }); + + // Compare indexes + compareIndexes({ + tableId: oldTable.id, + oldIndexes: oldTable.indexes, + newIndexes: newTable.indexes, + diffMap, + changedTables, + }); + } +} + +// Compare fields between tables +function compareFields({ + tableId, + oldFields, + newFields, + diffMap, + changedTables, + changedFields, +}: { + tableId: string; + oldFields: DBField[]; + newFields: DBField[]; + diffMap: DiffMap; + changedTables: Map; + changedFields: Map; +}) { + // Check for added fields + for (const newField of newFields) { + if (!oldFields.find((f) => f.id === newField.id)) { + diffMap.set( + getDiffMapKey({ + diffObject: 'field', + objectId: newField.id, + }), + { + object: 'field', + type: 'added', + fieldId: newField.id, + tableId, + } + ); + changedTables.set(tableId, true); + changedFields.set(newField.id, true); + } + } + + // Check for removed fields + for (const oldField of oldFields) { + if (!newFields.find((f) => f.id === oldField.id)) { + diffMap.set( + getDiffMapKey({ + diffObject: 'field', + objectId: oldField.id, + }), + { + object: 'field', + type: 'removed', + fieldId: oldField.id, + tableId, + } + ); + + changedTables.set(tableId, true); + changedFields.set(oldField.id, true); + } + } + + // Check for field changes + for (const oldField of oldFields) { + const newField = newFields.find((f) => f.id === oldField.id); + if (!newField) continue; + + // Compare basic field properties + compareFieldProperties({ + tableId, + oldField, + newField, + diffMap, + changedTables, + changedFields, + }); + } +} + +// Compare field properties +function compareFieldProperties({ + tableId, + oldField, + newField, + diffMap, + changedTables, + changedFields, +}: { + tableId: string; + oldField: DBField; + newField: DBField; + diffMap: DiffMap; + changedTables: Map; + changedFields: Map; +}) { + const changedAttributes: FieldDiffAttribute[] = []; + + if (oldField.name !== newField.name) { + changedAttributes.push('name'); + } + + if (oldField.type.id !== newField.type.id) { + changedAttributes.push('type'); + } + + if (oldField.primaryKey !== newField.primaryKey) { + changedAttributes.push('primaryKey'); + } + + if (oldField.unique !== newField.unique) { + changedAttributes.push('unique'); + } + + if (oldField.nullable !== newField.nullable) { + changedAttributes.push('nullable'); + } + + if (oldField.comments !== newField.comments) { + changedAttributes.push('comments'); + } + + if (changedAttributes.length > 0) { + for (const attribute of changedAttributes) { + diffMap.set( + getDiffMapKey({ + diffObject: 'field', + objectId: oldField.id, + attribute: attribute, + }), + { + object: 'field', + type: 'changed', + fieldId: oldField.id, + tableId, + attributes: attribute, + oldValue: oldField[attribute], + newValue: newField[attribute], + } + ); + } + changedTables.set(tableId, true); + changedFields.set(oldField.id, true); + } +} + +// Compare indexes between tables +function compareIndexes({ + tableId, + oldIndexes, + newIndexes, + diffMap, + changedTables, +}: { + tableId: string; + oldIndexes: DBIndex[]; + newIndexes: DBIndex[]; + diffMap: DiffMap; + changedTables: Map; +}) { + // Check for added indexes + for (const newIndex of newIndexes) { + if (!oldIndexes.find((i) => i.id === newIndex.id)) { + diffMap.set( + getDiffMapKey({ + diffObject: 'index', + objectId: newIndex.id, + }), + { + object: 'index', + type: 'added', + indexId: newIndex.id, + tableId, + } + ); + changedTables.set(tableId, true); + } + } + + // Check for removed indexes + for (const oldIndex of oldIndexes) { + if (!newIndexes.find((i) => i.id === oldIndex.id)) { + diffMap.set( + getDiffMapKey({ + diffObject: 'index', + objectId: oldIndex.id, + }), + { + object: 'index', + type: 'removed', + indexId: oldIndex.id, + tableId, + } + ); + changedTables.set(tableId, true); + } + } +} + +// Compare relationships between diagrams +function compareRelationships({ + diagram, + newDiagram, + diffMap, +}: { + diagram: Diagram; + newDiagram: Diagram; + diffMap: DiffMap; +}) { + const oldRelationships = diagram.relationships || []; + const newRelationships = newDiagram.relationships || []; + + // Check for added relationships + for (const newRelationship of newRelationships) { + if (!oldRelationships.find((r) => r.id === newRelationship.id)) { + diffMap.set( + getDiffMapKey({ + diffObject: 'relationship', + objectId: newRelationship.id, + }), + { + object: 'relationship', + type: 'added', + relationshipId: newRelationship.id, + } + ); + } + } + + // Check for removed relationships + for (const oldRelationship of oldRelationships) { + if (!newRelationships.find((r) => r.id === oldRelationship.id)) { + diffMap.set( + getDiffMapKey({ + diffObject: 'relationship', + objectId: oldRelationship.id, + }), + { + object: 'relationship', + type: 'removed', + relationshipId: oldRelationship.id, + } + ); + } + } +} diff --git a/src/context/diff-context/diff-context.tsx b/src/context/diff-context/diff-context.tsx new file mode 100644 index 00000000..155f37c0 --- /dev/null +++ b/src/context/diff-context/diff-context.tsx @@ -0,0 +1,75 @@ +import { createContext } from 'react'; +import type { DiffMap } from './types'; +import type { Diagram } from '@/lib/domain/diagram'; +import type { DBTable } from '@/lib/domain/db-table'; +import type { EventEmitter } from 'ahooks/lib/useEventEmitter'; +import type { DBField } from '@/lib/domain/db-field'; +import type { DataType } from '@/lib/data/data-types/data-types'; +import type { DBRelationship } from '@/lib/domain/db-relationship'; + +export type DiffEventType = 'diff_calculated'; + +export type DiffEventBase = { + action: T; + data: D; +}; + +export type DiffCalculatedEvent = DiffEventBase< + 'diff_calculated', + { + tablesAdded: DBTable[]; + fieldsAdded: Map; + relationshipsAdded: DBRelationship[]; + } +>; + +export type DiffEvent = DiffCalculatedEvent; + +export interface DiffContext { + newDiagram: Diagram | null; + diffMap: DiffMap; + hasDiff: boolean; + + calculateDiff: ({ + diagram, + newDiagram, + }: { + diagram: Diagram; + newDiagram: Diagram; + }) => void; + + // table diff + checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean; + checkIfNewTable: ({ tableId }: { tableId: string }) => boolean; + checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean; + getTableNewName: ({ tableId }: { tableId: string }) => string | null; + + // field diff + checkIfFieldHasChange: ({ + tableId, + fieldId, + }: { + tableId: string; + fieldId: string; + }) => boolean; + checkIfFieldRemoved: ({ fieldId }: { fieldId: string }) => boolean; + checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean; + getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null; + getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null; + + // relationship diff + checkIfNewRelationship: ({ + relationshipId, + }: { + relationshipId: string; + }) => boolean; + checkIfRelationshipRemoved: ({ + relationshipId, + }: { + relationshipId: string; + }) => boolean; + + events: EventEmitter; +} + +export const diffContext = createContext(undefined); diff --git a/src/context/diff-context/diff-provider.tsx b/src/context/diff-context/diff-provider.tsx new file mode 100644 index 00000000..ee9acaea --- /dev/null +++ b/src/context/diff-context/diff-provider.tsx @@ -0,0 +1,327 @@ +import React, { useCallback } from 'react'; +import type { DiffContext, DiffEvent } from './diff-context'; +import { diffContext } from './diff-context'; +import type { ChartDBDiff, DiffMap } from './types'; +import { generateDiff, getDiffMapKey } from './diff-check/diff-check'; +import type { Diagram } from '@/lib/domain/diagram'; +import { useEventEmitter } from 'ahooks'; +import type { DBField } from '@/lib/domain/db-field'; +import type { DataType } from '@/lib/data/data-types/data-types'; +import type { DBRelationship } from '@/lib/domain/db-relationship'; + +export const DiffProvider: React.FC = ({ + children, +}) => { + const [newDiagram, setNewDiagram] = React.useState(null); + const [diffMap, setDiffMap] = React.useState( + new Map() + ); + const [tablesChanged, setTablesChanged] = React.useState< + Map + >(new Map()); + const [fieldsChanged, setFieldsChanged] = React.useState< + Map + >(new Map()); + + const events = useEventEmitter(); + + const generateNewFieldsMap = useCallback( + ({ + diffMap, + newDiagram, + }: { + diffMap: DiffMap; + newDiagram: Diagram; + }) => { + const newFieldsMap = new Map(); + + diffMap.forEach((diff) => { + if (diff.object === 'field' && diff.type === 'added') { + const field = newDiagram?.tables + ?.find((table) => table.id === diff.tableId) + ?.fields.find((f) => f.id === diff.fieldId); + + if (field) { + newFieldsMap.set(diff.tableId, [ + ...(newFieldsMap.get(diff.tableId) ?? []), + field, + ]); + } + } + }); + + return newFieldsMap; + }, + [] + ); + + const findNewRelationships = useCallback( + ({ + diffMap, + newDiagram, + }: { + diffMap: DiffMap; + newDiagram: Diagram; + }) => { + const relationships: DBRelationship[] = []; + diffMap.forEach((diff) => { + if (diff.object === 'relationship' && diff.type === 'added') { + const relationship = newDiagram?.relationships?.find( + (rel) => rel.id === diff.relationshipId + ); + + if (relationship) { + relationships.push(relationship); + } + } + }); + + return relationships; + }, + [] + ); + + const calculateDiff: DiffContext['calculateDiff'] = useCallback( + ({ diagram, newDiagram: newDiagramArg }) => { + const { + diffMap: newDiffs, + changedTables: newChangedTables, + changedFields: newChangedFields, + } = generateDiff({ diagram, newDiagram: newDiagramArg }); + + setDiffMap(newDiffs); + setTablesChanged(newChangedTables); + setFieldsChanged(newChangedFields); + setNewDiagram(newDiagramArg); + + events.emit({ + action: 'diff_calculated', + data: { + tablesAdded: + newDiagramArg?.tables?.filter((table) => { + const tableKey = getDiffMapKey({ + diffObject: 'table', + objectId: table.id, + }); + + return ( + newDiffs.has(tableKey) && + newDiffs.get(tableKey)?.type === 'added' + ); + }) ?? [], + + fieldsAdded: generateNewFieldsMap({ + diffMap: newDiffs, + newDiagram: newDiagramArg, + }), + relationshipsAdded: findNewRelationships({ + diffMap: newDiffs, + newDiagram: newDiagramArg, + }), + }, + }); + }, + [setDiffMap, events, generateNewFieldsMap, findNewRelationships] + ); + + const getTableNewName = useCallback( + ({ tableId }) => { + const tableNameKey = getDiffMapKey({ + diffObject: 'table', + objectId: tableId, + attribute: 'name', + }); + + if (diffMap.has(tableNameKey)) { + const diff = diffMap.get(tableNameKey); + + if (diff?.type === 'changed') { + return diff.newValue as string; + } + } + + return null; + }, + [diffMap] + ); + + const checkIfTableHasChange = useCallback< + DiffContext['checkIfTableHasChange'] + >(({ tableId }) => tablesChanged.get(tableId) ?? false, [tablesChanged]); + + const checkIfNewTable = useCallback( + ({ tableId }) => { + const tableKey = getDiffMapKey({ + diffObject: 'table', + objectId: tableId, + }); + + return ( + diffMap.has(tableKey) && diffMap.get(tableKey)?.type === 'added' + ); + }, + [diffMap] + ); + + const checkIfTableRemoved = useCallback( + ({ tableId }) => { + const tableKey = getDiffMapKey({ + diffObject: 'table', + objectId: tableId, + }); + + return ( + diffMap.has(tableKey) && + diffMap.get(tableKey)?.type === 'removed' + ); + }, + [diffMap] + ); + + const checkIfFieldHasChange = useCallback< + DiffContext['checkIfFieldHasChange'] + >( + ({ fieldId }) => { + return fieldsChanged.get(fieldId) ?? false; + }, + [fieldsChanged] + ); + + const checkIfFieldRemoved = useCallback( + ({ fieldId }) => { + const fieldKey = getDiffMapKey({ + diffObject: 'field', + objectId: fieldId, + }); + + return ( + diffMap.has(fieldKey) && + diffMap.get(fieldKey)?.type === 'removed' + ); + }, + [diffMap] + ); + + const checkIfNewField = useCallback( + ({ fieldId }) => { + const fieldKey = getDiffMapKey({ + diffObject: 'field', + objectId: fieldId, + }); + + return ( + diffMap.has(fieldKey) && diffMap.get(fieldKey)?.type === 'added' + ); + }, + [diffMap] + ); + + const getFieldNewName = useCallback( + ({ fieldId }) => { + const fieldKey = getDiffMapKey({ + diffObject: 'field', + objectId: fieldId, + attribute: 'name', + }); + + if (diffMap.has(fieldKey)) { + const diff = diffMap.get(fieldKey); + + if (diff?.type === 'changed') { + return diff.newValue as string; + } + } + + return null; + }, + [diffMap] + ); + + const getFieldNewType = useCallback( + ({ fieldId }) => { + const fieldKey = getDiffMapKey({ + diffObject: 'field', + objectId: fieldId, + attribute: 'type', + }); + + if (diffMap.has(fieldKey)) { + const diff = diffMap.get(fieldKey); + + if (diff?.type === 'changed') { + return diff.newValue as DataType; + } + } + + return null; + }, + [diffMap] + ); + + const checkIfNewRelationship = useCallback< + DiffContext['checkIfNewRelationship'] + >( + ({ relationshipId }) => { + const relationshipKey = getDiffMapKey({ + diffObject: 'relationship', + objectId: relationshipId, + }); + + return ( + diffMap.has(relationshipKey) && + diffMap.get(relationshipKey)?.type === 'added' + ); + }, + [diffMap] + ); + + const checkIfRelationshipRemoved = useCallback< + DiffContext['checkIfRelationshipRemoved'] + >( + ({ relationshipId }) => { + const relationshipKey = getDiffMapKey({ + diffObject: 'relationship', + objectId: relationshipId, + }); + + return ( + diffMap.has(relationshipKey) && + diffMap.get(relationshipKey)?.type === 'removed' + ); + }, + [diffMap] + ); + + return ( + 0, + + calculateDiff, + + // table diff + getTableNewName, + checkIfNewTable, + checkIfTableRemoved, + checkIfTableHasChange, + + // field diff + checkIfFieldHasChange, + checkIfFieldRemoved, + checkIfNewField, + getFieldNewName, + getFieldNewType, + + // relationship diff + checkIfNewRelationship, + checkIfRelationshipRemoved, + + events, + }} + > + {children} + + ); +}; diff --git a/src/context/diff-context/types.ts b/src/context/diff-context/types.ts new file mode 100644 index 00000000..1b80c76e --- /dev/null +++ b/src/context/diff-context/types.ts @@ -0,0 +1,53 @@ +import type { DataType } from '@/lib/data/data-types/data-types'; + +export type TableDiffAttribute = 'name' | 'comments'; + +export interface TableDiff { + object: 'table'; + type: 'added' | 'removed' | 'changed'; + tableId: string; + attributes?: TableDiffAttribute; + oldValue?: string; + newValue?: string; +} + +export interface RelationshipDiff { + object: 'relationship'; + type: 'added' | 'removed'; + relationshipId: string; +} + +export type FieldDiffAttribute = + | 'name' + | 'type' + | 'primaryKey' + | 'unique' + | 'nullable' + | 'comments'; + +export interface FieldDiff { + object: 'field'; + type: 'added' | 'removed' | 'changed'; + fieldId: string; + tableId: string; + attributes?: FieldDiffAttribute; + oldValue?: string | boolean | DataType; + newValue?: string | boolean | DataType; +} + +export interface IndexDiff { + object: 'index'; + type: 'added' | 'removed'; + indexId: string; + tableId: string; +} + +export type ChartDBDiff = TableDiff | FieldDiff | IndexDiff | RelationshipDiff; + +export type DiffMap = Map; + +export type DiffObject = + | TableDiff['object'] + | FieldDiff['object'] + | IndexDiff['object'] + | RelationshipDiff['object']; diff --git a/src/context/diff-context/use-diff.ts b/src/context/diff-context/use-diff.ts new file mode 100644 index 00000000..9951044e --- /dev/null +++ b/src/context/diff-context/use-diff.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { diffContext } from './diff-context'; + +export const useDiff = () => { + const context = useContext(diffContext); + if (context === undefined) { + throw new Error('useDiff must be used within an DiffProvider'); + } + return context; +}; diff --git a/src/lib/data/export-metadata/export-sql-script.ts b/src/lib/data/export-metadata/export-sql-script.ts index 23bc1b7b..428803e2 100644 --- a/src/lib/data/export-metadata/export-sql-script.ts +++ b/src/lib/data/export-metadata/export-sql-script.ts @@ -115,8 +115,22 @@ export const exportBaseSQL = (diagram: Diagram): string => { // Remove the type cast part after :: if it exists if (fieldDefault.includes('::')) { + const endedWithParentheses = fieldDefault.endsWith(')'); fieldDefault = fieldDefault.split('::')[0]; + + if ( + (fieldDefault.startsWith('(') && + !fieldDefault.endsWith(')')) || + endedWithParentheses + ) { + fieldDefault += ')'; + } } + + if (fieldDefault === `('now')`) { + fieldDefault = `now()`; + } + sqlScript += ` DEFAULT ${fieldDefault}`; } diff --git a/src/pages/editor-page/canvas/canvas.tsx b/src/pages/editor-page/canvas/canvas.tsx index 90a86895..1c92a46a 100644 --- a/src/pages/editor-page/canvas/canvas.tsx +++ b/src/pages/editor-page/canvas/canvas.tsx @@ -103,10 +103,9 @@ const tableToTableNode = ( export interface CanvasProps { initialTables: DBTable[]; - readonly?: boolean; } -export const Canvas: React.FC = ({ initialTables, readonly }) => { +export const Canvas: React.FC = ({ initialTables }) => { const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow(); const [selectedTableIds, setSelectedTableIds] = useState([]); const [selectedRelationshipIds, setSelectedRelationshipIds] = useState< @@ -127,6 +126,7 @@ export const Canvas: React.FC = ({ initialTables, readonly }) => { filteredSchemas, events, dependencies, + readonly, } = useChartDB(); const { showSidePanel } = useLayout(); const { effectiveTheme } = useTheme(); diff --git a/src/pages/editor-page/canvas/relationship-edge.tsx b/src/pages/editor-page/canvas/relationship-edge.tsx index 84e6776e..c98bca0a 100644 --- a/src/pages/editor-page/canvas/relationship-edge.tsx +++ b/src/pages/editor-page/canvas/relationship-edge.tsx @@ -7,6 +7,7 @@ import { useChartDB } from '@/hooks/use-chartdb'; import { useLayout } from '@/hooks/use-layout'; import { cn } from '@/lib/utils'; import { getCardinalityMarkerId } from './canvas-utils'; +import { useDiff } from '@/context/diff-context/use-diff'; export type RelationshipEdgeType = Edge< { @@ -29,6 +30,7 @@ export const RelationshipEdge: React.FC> = ({ }) => { const { getInternalNode, getEdge } = useReactFlow(); const { openRelationshipFromSidebar, selectSidebarSection } = useLayout(); + const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff(); const { relationships } = useChartDB(); @@ -149,6 +151,25 @@ export const RelationshipEdge: React.FC> = ({ }), [relationship?.targetCardinality, selected, targetSide] ); + + const isDiffNewRelationship = useMemo( + () => + relationship?.id + ? checkIfNewRelationship({ relationshipId: relationship.id }) + : false, + [checkIfNewRelationship, relationship?.id] + ); + + const isDiffRelationshipRemoved = useMemo( + () => + relationship?.id + ? checkIfRelationshipRemoved({ + relationshipId: relationship.id, + }) + : false, + [checkIfRelationshipRemoved, relationship?.id] + ); + return ( <> > = ({ className={cn([ 'react-flow__edge-path', `!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`, + { + '!stroke-green-500': isDiffNewRelationship, + '!stroke-red-500': isDiffRelationshipRemoved, + }, ])} onClick={(e) => { if (e.detail === 2) { diff --git a/src/pages/editor-page/canvas/table-node/table-node-field.tsx b/src/pages/editor-page/canvas/table-node/table-node-field.tsx index b79f90fc..f73cbab2 100644 --- a/src/pages/editor-page/canvas/table-node/table-node-field.tsx +++ b/src/pages/editor-page/canvas/table-node/table-node-field.tsx @@ -12,7 +12,15 @@ import { useUpdateNodeInternals, } from '@xyflow/react'; import { Button } from '@/components/button/button'; -import { Check, KeyRound, MessageCircleMore, Trash2 } from 'lucide-react'; +import { + Check, + KeyRound, + MessageCircleMore, + SquareDot, + SquareMinus, + SquarePlus, + Trash2, +} from 'lucide-react'; import type { DBField } from '@/lib/domain/db-field'; import { useChartDB } from '@/hooks/use-chartdb'; import { cn } from '@/lib/utils'; @@ -23,6 +31,7 @@ import { } from '@/components/tooltip/tooltip'; import { useClickAway, useKeyPressEvent } from 'react-use'; import { Input } from '@/components/input/input'; +import { useDiff } from '@/context/diff-context/use-diff'; export const LEFT_HANDLE_ID_PREFIX = 'left_rel_'; export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_'; @@ -95,6 +104,43 @@ export const TableNodeField: React.FC = React.memo( useKeyPressEvent('Enter', editFieldName); useKeyPressEvent('Escape', abortEdit); + const { + checkIfFieldRemoved, + checkIfNewField, + getFieldNewName, + getFieldNewType, + checkIfFieldHasChange, + } = useDiff(); + + const isDiffFieldRemoved = useMemo( + () => checkIfFieldRemoved({ fieldId: field.id }), + [checkIfFieldRemoved, field.id] + ); + + const isDiffNewField = useMemo( + () => checkIfNewField({ fieldId: field.id }), + [checkIfNewField, field.id] + ); + + const fieldDiffChangedName = useMemo( + () => getFieldNewName({ fieldId: field.id }), + [getFieldNewName, field.id] + ); + + const fieldDiffChangedType = useMemo( + () => getFieldNewType({ fieldId: field.id }), + [getFieldNewType, field.id] + ); + + const isDiffFieldChanged = useMemo( + () => + checkIfFieldHasChange({ + fieldId: field.id, + tableId: tableNodeId, + }), + [checkIfFieldHasChange, field.id, tableNodeId] + ); + const enterEditMode = (e: React.MouseEvent) => { e.stopPropagation(); setEditMode(true); @@ -102,13 +148,23 @@ export const TableNodeField: React.FC = React.memo( return (
{isConnectable ? ( <> @@ -161,7 +217,14 @@ export const TableNodeField: React.FC = React.memo( } )} > - {editMode ? ( + {isDiffFieldRemoved ? ( + + ) : isDiffNewField ? ( + + ) : isDiffFieldChanged ? ( + + ) : null} + {editMode && !readonly ? ( <> = React.memo( // {field.name} // - {field.name} + {fieldDiffChangedName ? ( + <> + {field.name}{' '} + {' '} + {fieldDiffChangedName} + + ) : ( + field.name + )} )} {/* {field.name} */} @@ -214,7 +294,18 @@ export const TableNodeField: React.FC = React.memo(
@@ -223,11 +314,31 @@ export const TableNodeField: React.FC = React.memo(
- {field.type.name.split(' ')[0]} + {fieldDiffChangedType ? ( + <> + + {field.type.name.split(' ')[0]} + {' '} + {fieldDiffChangedType.name.split(' ')[0]} + + ) : ( + field.type.name.split(' ')[0] + )} {field.nullable ? '?' : ''}
{readonly ? null : ( diff --git a/src/pages/editor-page/canvas/table-node/table-node-status/table-node-status.tsx b/src/pages/editor-page/canvas/table-node/table-node-status/table-node-status.tsx new file mode 100644 index 00000000..44678a39 --- /dev/null +++ b/src/pages/editor-page/canvas/table-node/table-node-status/table-node-status.tsx @@ -0,0 +1,35 @@ +import { cn } from '@/lib/utils'; +import React from 'react'; + +export interface TableNodeStatusProps { + status: 'new' | 'changed' | 'removed' | 'none'; +} + +export const TableNodeStatus: React.FC = ({ status }) => { + if (status === 'none') { + return null; + } + return ( +
+ + {status === 'new' + ? 'New' + : status === 'changed' + ? 'Modified' + : 'Deleted'} + +
+ ); +}; diff --git a/src/pages/editor-page/canvas/table-node/table-node.tsx b/src/pages/editor-page/canvas/table-node/table-node.tsx index 4146641d..7e8d48eb 100644 --- a/src/pages/editor-page/canvas/table-node/table-node.tsx +++ b/src/pages/editor-page/canvas/table-node/table-node.tsx @@ -10,6 +10,9 @@ import { ChevronUp, Check, CircleDotDashed, + SquareDot, + SquarePlus, + SquareMinus, } from 'lucide-react'; import { Label } from '@/components/label/label'; import type { DBTable } from '@/lib/domain/db-table'; @@ -30,6 +33,8 @@ import { TooltipContent, TooltipTrigger, } from '@/components/tooltip/tooltip'; +import { useDiff } from '@/context/diff-context/use-diff'; +import { TableNodeStatus } from './table-node-status/table-node-status'; export type TableNodeType = Node< { @@ -61,6 +66,35 @@ export const TableNode: React.FC> = React.memo( const [tableName, setTableName] = useState(table.name); const inputRef = React.useRef(null); + const { + getTableNewName, + checkIfTableHasChange, + checkIfNewTable, + checkIfTableRemoved, + } = useDiff(); + + const fields = useMemo(() => table.fields, [table.fields]); + + const tableChangedName = useMemo( + () => getTableNewName({ tableId: table.id }), + [getTableNewName, table.id] + ); + + const isDiffTableChanged = useMemo( + () => checkIfTableHasChange({ tableId: table.id }), + [checkIfTableHasChange, table.id] + ); + + const isDiffNewTable = useMemo( + () => checkIfNewTable({ tableId: table.id }), + [checkIfNewTable, table.id] + ); + + const isDiffTableRemoved = useMemo( + () => checkIfTableRemoved({ tableId: table.id }), + [checkIfTableRemoved, table.id] + ); + const selectedRelEdges = edges.filter( (edge) => (edge.source === id || edge.target === id) && @@ -109,13 +143,13 @@ export const TableNode: React.FC> = React.memo( const visibleFields = useMemo(() => { if (expanded) { - return table.fields; + return fields; } - const mustDisplayedFields = table.fields.filter((field: DBField) => + const mustDisplayedFields = fields.filter((field: DBField) => isMustDisplayedField(field) ); - const nonMustDisplayedFields = table.fields.filter( + const nonMustDisplayedFields = fields.filter( (field: DBField) => !isMustDisplayedField(field) ); @@ -133,8 +167,8 @@ export const TableNode: React.FC> = React.memo( return [ ...visibleMustDisplayedFields, ...visibleNonMustDisplayedFields, - ].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b)); - }, [expanded, table.fields, isMustDisplayedField]); + ].sort((a, b) => fields.indexOf(a) - fields.indexOf(b)); + }, [expanded, fields, isMustDisplayedField]); const editTableName = useCallback(() => { if (!editMode) return; @@ -174,6 +208,17 @@ export const TableNode: React.FC> = React.memo( : '', highlightOverlappingTables && isOverlapping ? 'animate-scale-2' + : '', + isDiffTableChanged && + !isDiffNewTable && + !isDiffTableRemoved + ? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]' + : '', + isDiffNewTable + ? 'outline outline-[3px] outline-green-500 dark:outline-green-900 outline-offset-[5px]' + : '', + isDiffTableRemoved + ? 'outline outline-[3px] outline-red-500 dark:outline-red-900 outline-offset-[5px]' : '' )} onClick={(e) => { @@ -194,14 +239,87 @@ export const TableNode: React.FC> = React.memo( table={table} focused={focused} /> + {/* Badge added here */} +
- - {editMode ? ( + {isDiffNewTable ? ( + + + + + New Table + + ) : isDiffTableRemoved ? ( + + + + + + Table Removed + + + ) : isDiffTableChanged ? ( + + + + + + Table Changed + + + ) : ( + + )} + + {tableChangedName ? ( + + ) : isDiffNewTable ? ( + + ) : isDiffTableRemoved ? ( + + ) : isDiffTableChanged ? ( + + ) : editMode && !readonly ? ( <> > = React.memo( className="transition-[max-height] duration-200 ease-in-out" style={{ maxHeight: expanded - ? `${table.fields.length * 2}rem` // h-8 per field + ? `${fields.length * 2}rem` // h-8 per field : `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field }} > - {table.fields.map((field: DBField) => ( + {fields.map((field: DBField) => ( > = React.memo( /> ))}
- {table.fields.length > TABLE_MINIMIZED_FIELDS && ( + {fields.length > TABLE_MINIMIZED_FIELDS && (
( - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/template-page/template-page.tsx b/src/pages/template-page/template-page.tsx index 596b1a5f..aefa67cf 100644 --- a/src/pages/template-page/template-page.tsx +++ b/src/pages/template-page/template-page.tsx @@ -289,7 +289,6 @@ const TemplatePageComponent: React.FC = () => { readonly >