mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-03 13:33:25 +00:00
add diff mode (#614)
* initial diff viewer * test * test * name change * new table show * diff viewer
This commit is contained in:
@@ -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<ChartDBProviderProps>
|
||||
> = ({ children, diagram, readonly }) => {
|
||||
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||
const { hasDiff } = useDiff();
|
||||
let db = useStorage();
|
||||
const events = useEventEmitter<ChartDBEvent>();
|
||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||
@@ -53,9 +56,33 @@ export const ChartDBProvider: React.FC<
|
||||
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
||||
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;
|
||||
}
|
||||
|
||||
433
src/context/diff-context/diff-check/diff-check.ts
Normal file
433
src/context/diff-context/diff-check/diff-check.ts
Normal file
@@ -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<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
} {
|
||||
const newDiffs = new Map<string, ChartDBDiff>();
|
||||
const changedTables = new Map<string, boolean>();
|
||||
const changedFields = new Map<string, boolean>();
|
||||
|
||||
// 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<string, boolean>;
|
||||
}) {
|
||||
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<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
}) {
|
||||
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<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
}) {
|
||||
// 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<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
}) {
|
||||
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<string, boolean>;
|
||||
}) {
|
||||
// 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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/context/diff-context/diff-context.tsx
Normal file
75
src/context/diff-context/diff-context.tsx
Normal file
@@ -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<T extends DiffEventType, D> = {
|
||||
action: T;
|
||||
data: D;
|
||||
};
|
||||
|
||||
export type DiffCalculatedEvent = DiffEventBase<
|
||||
'diff_calculated',
|
||||
{
|
||||
tablesAdded: DBTable[];
|
||||
fieldsAdded: Map<string, DBField[]>;
|
||||
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<DiffEvent>;
|
||||
}
|
||||
|
||||
export const diffContext = createContext<DiffContext | undefined>(undefined);
|
||||
327
src/context/diff-context/diff-provider.tsx
Normal file
327
src/context/diff-context/diff-provider.tsx
Normal file
@@ -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<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
|
||||
const [diffMap, setDiffMap] = React.useState<DiffMap>(
|
||||
new Map<string, ChartDBDiff>()
|
||||
);
|
||||
const [tablesChanged, setTablesChanged] = React.useState<
|
||||
Map<string, boolean>
|
||||
>(new Map<string, boolean>());
|
||||
const [fieldsChanged, setFieldsChanged] = React.useState<
|
||||
Map<string, boolean>
|
||||
>(new Map<string, boolean>());
|
||||
|
||||
const events = useEventEmitter<DiffEvent>();
|
||||
|
||||
const generateNewFieldsMap = useCallback(
|
||||
({
|
||||
diffMap,
|
||||
newDiagram,
|
||||
}: {
|
||||
diffMap: DiffMap;
|
||||
newDiagram: Diagram;
|
||||
}) => {
|
||||
const newFieldsMap = new Map<string, DBField[]>();
|
||||
|
||||
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<DiffContext['getTableNewName']>(
|
||||
({ 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<DiffContext['checkIfNewTable']>(
|
||||
({ tableId }) => {
|
||||
const tableKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: tableId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(tableKey) && diffMap.get(tableKey)?.type === 'added'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfTableRemoved = useCallback<DiffContext['checkIfTableRemoved']>(
|
||||
({ 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<DiffContext['checkIfFieldRemoved']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(fieldKey) &&
|
||||
diffMap.get(fieldKey)?.type === 'removed'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfNewField = useCallback<DiffContext['checkIfNewField']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(fieldKey) && diffMap.get(fieldKey)?.type === 'added'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewName = useCallback<DiffContext['getFieldNewName']>(
|
||||
({ 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<DiffContext['getFieldNewType']>(
|
||||
({ 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 (
|
||||
<diffContext.Provider
|
||||
value={{
|
||||
newDiagram,
|
||||
diffMap,
|
||||
hasDiff: diffMap.size > 0,
|
||||
|
||||
calculateDiff,
|
||||
|
||||
// table diff
|
||||
getTableNewName,
|
||||
checkIfNewTable,
|
||||
checkIfTableRemoved,
|
||||
checkIfTableHasChange,
|
||||
|
||||
// field diff
|
||||
checkIfFieldHasChange,
|
||||
checkIfFieldRemoved,
|
||||
checkIfNewField,
|
||||
getFieldNewName,
|
||||
getFieldNewType,
|
||||
|
||||
// relationship diff
|
||||
checkIfNewRelationship,
|
||||
checkIfRelationshipRemoved,
|
||||
|
||||
events,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</diffContext.Provider>
|
||||
);
|
||||
};
|
||||
53
src/context/diff-context/types.ts
Normal file
53
src/context/diff-context/types.ts
Normal file
@@ -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<string, ChartDBDiff>;
|
||||
|
||||
export type DiffObject =
|
||||
| TableDiff['object']
|
||||
| FieldDiff['object']
|
||||
| IndexDiff['object']
|
||||
| RelationshipDiff['object'];
|
||||
10
src/context/diff-context/use-diff.ts
Normal file
10
src/context/diff-context/use-diff.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -103,10 +103,9 @@ const tableToTableNode = (
|
||||
|
||||
export interface CanvasProps {
|
||||
initialTables: DBTable[];
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
|
||||
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
|
||||
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
|
||||
@@ -127,6 +126,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
||||
filteredSchemas,
|
||||
events,
|
||||
dependencies,
|
||||
readonly,
|
||||
} = useChartDB();
|
||||
const { showSidePanel } = useLayout();
|
||||
const { effectiveTheme } = useTheme();
|
||||
|
||||
@@ -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<EdgeProps<RelationshipEdgeType>> = ({
|
||||
}) => {
|
||||
const { getInternalNode, getEdge } = useReactFlow();
|
||||
const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
|
||||
const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
|
||||
|
||||
const { relationships } = useChartDB();
|
||||
|
||||
@@ -149,6 +151,25 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
||||
}),
|
||||
[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 (
|
||||
<>
|
||||
<path
|
||||
@@ -160,6 +181,10 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
||||
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) {
|
||||
|
||||
@@ -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<TableNodeFieldProps> = 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<TableNodeFieldProps> = React.memo(
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 ${
|
||||
highlighted ? 'bg-pink-100 dark:bg-pink-900' : ''
|
||||
} transition-all duration-200 ease-in-out ${
|
||||
visible
|
||||
? 'max-h-8 opacity-100'
|
||||
: 'z-0 max-h-0 overflow-hidden opacity-0'
|
||||
}`}
|
||||
className={cn(
|
||||
'group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
{
|
||||
'bg-pink-100 dark:bg-pink-900': highlighted,
|
||||
'max-h-8 opacity-100': visible,
|
||||
'z-0 max-h-0 overflow-hidden opacity-0': !visible,
|
||||
'bg-sky-200 dark:bg-sky-800 hover:bg-sky-100 dark:hover:bg-sky-900 border-sky-300 dark:border-sky-700':
|
||||
isDiffFieldChanged &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField,
|
||||
'bg-red-200 dark:bg-red-800 hover:bg-red-100 dark:hover:bg-red-900 border-red-300 dark:border-red-700':
|
||||
isDiffFieldRemoved,
|
||||
'bg-green-200 dark:bg-green-800 hover:bg-green-100 dark:hover:bg-green-900 border-green-300 dark:border-green-700':
|
||||
isDiffNewField,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{isConnectable ? (
|
||||
<>
|
||||
@@ -161,7 +217,14 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
}
|
||||
)}
|
||||
>
|
||||
{editMode ? (
|
||||
{isDiffFieldRemoved ? (
|
||||
<SquareMinus className="size-3.5 text-red-800 dark:text-red-200" />
|
||||
) : isDiffNewField ? (
|
||||
<SquarePlus className="size-3.5 text-green-800 dark:text-green-200" />
|
||||
) : isDiffFieldChanged ? (
|
||||
<SquareDot className="size-3.5 shrink-0 text-sky-800 dark:text-sky-200" />
|
||||
) : null}
|
||||
{editMode && !readonly ? (
|
||||
<>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
@@ -190,10 +253,27 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
// {field.name}
|
||||
// </span>
|
||||
<span
|
||||
className="truncate"
|
||||
className={cn('truncate', {
|
||||
'text-red-800 font-normal dark:text-red-200':
|
||||
isDiffFieldRemoved,
|
||||
'text-green-800 font-normal dark:text-green-200':
|
||||
isDiffNewField,
|
||||
'text-sky-800 font-normal dark:text-sky-200':
|
||||
isDiffFieldChanged &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField,
|
||||
})}
|
||||
onDoubleClick={enterEditMode}
|
||||
>
|
||||
{field.name}
|
||||
{fieldDiffChangedName ? (
|
||||
<>
|
||||
{field.name}{' '}
|
||||
<span className="font-medium">→</span>{' '}
|
||||
{fieldDiffChangedName}
|
||||
</>
|
||||
) : (
|
||||
field.name
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{/* <span className="truncate">{field.name}</span> */}
|
||||
@@ -214,7 +294,18 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground',
|
||||
!readonly ? 'group-hover:hidden' : ''
|
||||
!readonly ? 'group-hover:hidden' : '',
|
||||
isDiffFieldRemoved
|
||||
? 'text-red-800 dark:text-red-200'
|
||||
: '',
|
||||
isDiffNewField
|
||||
? 'text-green-800 dark:text-green-200'
|
||||
: '',
|
||||
isDiffFieldChanged &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField
|
||||
? 'text-sky-800 dark:text-sky-200'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
<KeyRound size={14} />
|
||||
@@ -223,11 +314,31 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'content-center truncate text-right text-xs text-muted-foreground shrink-0',
|
||||
!readonly ? 'group-hover:hidden' : ''
|
||||
'content-center truncate text-right text-xs text-muted-foreground',
|
||||
!readonly ? 'group-hover:hidden' : '',
|
||||
isDiffFieldRemoved
|
||||
? 'text-red-800 dark:text-red-200'
|
||||
: '',
|
||||
isDiffNewField
|
||||
? 'text-green-800 dark:text-green-200'
|
||||
: '',
|
||||
isDiffFieldChanged &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField
|
||||
? 'text-sky-800 dark:text-sky-200'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
{field.type.name.split(' ')[0]}
|
||||
{fieldDiffChangedType ? (
|
||||
<>
|
||||
<span className="line-through">
|
||||
{field.type.name.split(' ')[0]}
|
||||
</span>{' '}
|
||||
{fieldDiffChangedType.name.split(' ')[0]}
|
||||
</>
|
||||
) : (
|
||||
field.type.name.split(' ')[0]
|
||||
)}
|
||||
{field.nullable ? '?' : ''}
|
||||
</div>
|
||||
{readonly ? null : (
|
||||
|
||||
@@ -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<TableNodeStatusProps> = ({ status }) => {
|
||||
if (status === 'none') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="absolute left-1/2 top-0 z-10 -translate-x-1/2 -translate-y-1/2">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium text-white',
|
||||
{
|
||||
'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100':
|
||||
status === 'new',
|
||||
'bg-sky-100 text-sky-800 dark:bg-sky-800 dark:text-sky-100':
|
||||
status === 'changed',
|
||||
'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100':
|
||||
status === 'removed',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{status === 'new'
|
||||
? 'New'
|
||||
: status === 'changed'
|
||||
? 'Modified'
|
||||
: 'Deleted'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<NodeProps<TableNodeType>> = React.memo(
|
||||
const [tableName, setTableName] = useState(table.name);
|
||||
const inputRef = React.useRef<HTMLInputElement>(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<NodeProps<TableNodeType>> = 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<NodeProps<TableNodeType>> = 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<NodeProps<TableNodeType>> = 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<NodeProps<TableNodeType>> = React.memo(
|
||||
table={table}
|
||||
focused={focused}
|
||||
/>
|
||||
{/* Badge added here */}
|
||||
<TableNodeStatus
|
||||
status={
|
||||
isDiffNewTable
|
||||
? 'new'
|
||||
: isDiffTableRemoved
|
||||
? 'removed'
|
||||
: isDiffTableChanged
|
||||
? 'changed'
|
||||
: 'none'
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="h-2 rounded-t-[6px]"
|
||||
style={{ backgroundColor: table.color }}
|
||||
></div>
|
||||
<div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
|
||||
{editMode ? (
|
||||
{isDiffNewTable ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SquarePlus
|
||||
className="size-3.5 shrink-0 text-green-600"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>New Table</TooltipContent>
|
||||
</Tooltip>
|
||||
) : isDiffTableRemoved ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SquareMinus
|
||||
className="size-3.5 shrink-0 text-red-600"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Table Removed
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : isDiffTableChanged ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SquareDot
|
||||
className="size-3.5 shrink-0 text-sky-600"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Table Changed
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
|
||||
)}
|
||||
|
||||
{tableChangedName ? (
|
||||
<Label className="flex h-5 items-center justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
|
||||
<span className="truncate">
|
||||
{table.name}
|
||||
</span>
|
||||
<span className="mx-1 font-semibold">
|
||||
→
|
||||
</span>
|
||||
<span className="truncate">
|
||||
{tableChangedName}
|
||||
</span>
|
||||
</Label>
|
||||
) : isDiffNewTable ? (
|
||||
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-green-200 px-2 py-0.5 text-sm font-normal text-green-900 dark:bg-green-800 dark:text-green-200">
|
||||
{table.name}
|
||||
</Label>
|
||||
) : isDiffTableRemoved ? (
|
||||
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-red-200 px-2 py-0.5 text-sm font-normal text-red-900 dark:bg-red-800 dark:text-red-200">
|
||||
{table.name}
|
||||
</Label>
|
||||
) : isDiffTableChanged ? (
|
||||
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
|
||||
{table.name}
|
||||
</Label>
|
||||
) : editMode && !readonly ? (
|
||||
<>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
@@ -273,11 +391,11 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = 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) => (
|
||||
<TableNodeField
|
||||
key={field.id}
|
||||
focused={focused}
|
||||
@@ -295,7 +413,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{table.fields.length > TABLE_MINIMIZED_FIELDS && (
|
||||
{fields.length > TABLE_MINIMIZED_FIELDS && (
|
||||
<div
|
||||
className="z-10 flex h-8 cursor-pointer items-center justify-center rounded-b-md border-t text-xs text-muted-foreground transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
onClick={toggleExpand}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { AlertProvider } from '@/context/alert-context/alert-provider';
|
||||
import { CanvasProvider } from '@/context/canvas-context/canvas-provider';
|
||||
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
|
||||
import { useDiagramLoader } from './use-diagram-loader';
|
||||
import { DiffProvider } from '@/context/diff-context/diff-provider';
|
||||
|
||||
const OPEN_STAR_US_AFTER_SECONDS = 30;
|
||||
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
|
||||
@@ -235,23 +236,25 @@ export const EditorPage: React.FC = () => (
|
||||
<StorageProvider>
|
||||
<ConfigProvider>
|
||||
<RedoUndoStackProvider>
|
||||
<ChartDBProvider>
|
||||
<HistoryProvider>
|
||||
<ReactFlowProvider>
|
||||
<CanvasProvider>
|
||||
<ExportImageProvider>
|
||||
<AlertProvider>
|
||||
<DialogProvider>
|
||||
<KeyboardShortcutsProvider>
|
||||
<EditorPageComponent />
|
||||
</KeyboardShortcutsProvider>
|
||||
</DialogProvider>
|
||||
</AlertProvider>
|
||||
</ExportImageProvider>
|
||||
</CanvasProvider>
|
||||
</ReactFlowProvider>
|
||||
</HistoryProvider>
|
||||
</ChartDBProvider>
|
||||
<DiffProvider>
|
||||
<ChartDBProvider>
|
||||
<HistoryProvider>
|
||||
<ReactFlowProvider>
|
||||
<CanvasProvider>
|
||||
<ExportImageProvider>
|
||||
<AlertProvider>
|
||||
<DialogProvider>
|
||||
<KeyboardShortcutsProvider>
|
||||
<EditorPageComponent />
|
||||
</KeyboardShortcutsProvider>
|
||||
</DialogProvider>
|
||||
</AlertProvider>
|
||||
</ExportImageProvider>
|
||||
</CanvasProvider>
|
||||
</ReactFlowProvider>
|
||||
</HistoryProvider>
|
||||
</ChartDBProvider>
|
||||
</DiffProvider>
|
||||
</RedoUndoStackProvider>
|
||||
</ConfigProvider>
|
||||
</StorageProvider>
|
||||
|
||||
@@ -289,7 +289,6 @@ const TemplatePageComponent: React.FC = () => {
|
||||
readonly
|
||||
>
|
||||
<Canvas
|
||||
readonly
|
||||
initialTables={
|
||||
template.diagram.tables ?? []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user