mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-04 14:03:15 +00:00
feat: add sticky notes
This commit is contained in:
@@ -1,9 +1,16 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import EmptyStateImage from '@/assets/empty_state.png';
|
import EmptyStateImage from '@/assets/empty_state.png';
|
||||||
import EmptyStateImageDark from '@/assets/empty_state_dark.png';
|
import EmptyStateImageDark from '@/assets/empty_state_dark.png';
|
||||||
import { Label } from '@/components/label/label';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
import {
|
||||||
|
Empty,
|
||||||
|
EmptyContent,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyHeader,
|
||||||
|
EmptyMedia,
|
||||||
|
EmptyTitle,
|
||||||
|
} from '../empty/empty';
|
||||||
|
|
||||||
export interface EmptyStateProps {
|
export interface EmptyStateProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -38,26 +45,29 @@ export const EmptyState = forwardRef<
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<img
|
<Empty>
|
||||||
src={
|
<EmptyHeader>
|
||||||
effectiveTheme === 'dark'
|
<EmptyMedia variant="icon">
|
||||||
? EmptyStateImageDark
|
{/* <Group /> */}
|
||||||
: EmptyStateImage
|
<img
|
||||||
}
|
src={
|
||||||
alt="Empty state"
|
effectiveTheme === 'dark'
|
||||||
className={cn('mb-2 w-20', imageClassName)}
|
? EmptyStateImageDark
|
||||||
/>
|
: EmptyStateImage
|
||||||
<Label className={cn('text-base', titleClassName)}>
|
}
|
||||||
{title}
|
alt="Empty state"
|
||||||
</Label>
|
className={cn('p-2', imageClassName)}
|
||||||
<Label
|
/>
|
||||||
className={cn(
|
</EmptyMedia>
|
||||||
'text-sm text-center font-normal text-muted-foreground',
|
<EmptyTitle className={titleClassName}>
|
||||||
descriptionClassName
|
{title}
|
||||||
)}
|
</EmptyTitle>
|
||||||
>
|
<EmptyDescription className={descriptionClassName}>
|
||||||
{description}
|
{description}
|
||||||
</Label>
|
</EmptyDescription>
|
||||||
|
</EmptyHeader>
|
||||||
|
<EmptyContent />
|
||||||
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
105
src/components/empty/empty.tsx
Normal file
105
src/components/empty/empty.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils/index';
|
||||||
|
|
||||||
|
function Empty({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty"
|
||||||
|
className={cn(
|
||||||
|
'flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance rounded-lg border-dashed p-6 text-center md:p-12',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-header"
|
||||||
|
className={cn(
|
||||||
|
'flex max-w-sm flex-col items-center gap-2 text-center',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyMediaVariants = cva(
|
||||||
|
'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-transparent',
|
||||||
|
icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function EmptyMedia({
|
||||||
|
className,
|
||||||
|
variant = 'default',
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-icon"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(emptyMediaVariants({ variant, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-title"
|
||||||
|
className={cn('text-lg font-medium tracking-tight', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-description"
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-content"
|
||||||
|
className={cn(
|
||||||
|
'flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-balance text-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Empty,
|
||||||
|
EmptyHeader,
|
||||||
|
EmptyTitle,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyContent,
|
||||||
|
EmptyMedia,
|
||||||
|
};
|
||||||
@@ -2,6 +2,24 @@ import { createContext } from 'react';
|
|||||||
import { emptyFn } from '@/lib/utils';
|
import { emptyFn } from '@/lib/utils';
|
||||||
import type { Graph } from '@/lib/graph';
|
import type { Graph } from '@/lib/graph';
|
||||||
import { createGraph } from '@/lib/graph';
|
import { createGraph } from '@/lib/graph';
|
||||||
|
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||||
|
|
||||||
|
export type CanvasEventType = 'pan_click';
|
||||||
|
|
||||||
|
export type CanvasEventBase<T extends CanvasEventType, D> = {
|
||||||
|
action: T;
|
||||||
|
data: D;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PanClickEvent = CanvasEventBase<
|
||||||
|
'pan_click',
|
||||||
|
{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type CanvasEvent = PanClickEvent;
|
||||||
|
|
||||||
export interface CanvasContext {
|
export interface CanvasContext {
|
||||||
reorderTables: (options?: { updateHistory?: boolean }) => void;
|
reorderTables: (options?: { updateHistory?: boolean }) => void;
|
||||||
@@ -49,6 +67,7 @@ export interface CanvasContext {
|
|||||||
y: number;
|
y: number;
|
||||||
}) => void;
|
}) => void;
|
||||||
hideCreateRelationshipNode: () => void;
|
hideCreateRelationshipNode: () => void;
|
||||||
|
events: EventEmitter<CanvasEvent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const canvasContext = createContext<CanvasContext>({
|
export const canvasContext = createContext<CanvasContext>({
|
||||||
@@ -68,4 +87,5 @@ export const canvasContext = createContext<CanvasContext>({
|
|||||||
setHoveringTableId: emptyFn,
|
setHoveringTableId: emptyFn,
|
||||||
showCreateRelationshipNode: emptyFn,
|
showCreateRelationshipNode: emptyFn,
|
||||||
hideCreateRelationshipNode: emptyFn,
|
hideCreateRelationshipNode: emptyFn,
|
||||||
|
events: new EventEmitter(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { CanvasContext } from './canvas-context';
|
import type { CanvasContext, CanvasEvent } from './canvas-context';
|
||||||
import { canvasContext } from './canvas-context';
|
import { canvasContext } from './canvas-context';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { adjustTablePositions } from '@/lib/domain/db-table';
|
import { adjustTablePositions } from '@/lib/domain/db-table';
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
CREATE_RELATIONSHIP_NODE_ID,
|
CREATE_RELATIONSHIP_NODE_ID,
|
||||||
type CreateRelationshipNodeType,
|
type CreateRelationshipNodeType,
|
||||||
} from '@/pages/editor-page/canvas/create-relationship-node/create-relationship-node';
|
} from '@/pages/editor-page/canvas/create-relationship-node/create-relationship-node';
|
||||||
|
import { useEventEmitter } from 'ahooks';
|
||||||
|
|
||||||
interface CanvasProviderProps {
|
interface CanvasProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -43,6 +44,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
|||||||
fieldId?: string;
|
fieldId?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
const events = useEventEmitter<CanvasEvent>();
|
||||||
|
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
|
|
||||||
const [tempFloatingEdge, setTempFloatingEdge] =
|
const [tempFloatingEdge, setTempFloatingEdge] =
|
||||||
@@ -212,6 +215,7 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
|||||||
setHoveringTableId,
|
setHoveringTableId,
|
||||||
showCreateRelationshipNode,
|
showCreateRelationshipNode,
|
||||||
hideCreateRelationshipNode,
|
hideCreateRelationshipNode,
|
||||||
|
events,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { DBDependency } from '@/lib/domain/db-dependency';
|
|||||||
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
|
|
||||||
export type ChartDBEventType =
|
export type ChartDBEventType =
|
||||||
| 'add_tables'
|
| 'add_tables'
|
||||||
@@ -74,6 +75,7 @@ export interface ChartDBContext {
|
|||||||
dependencies: DBDependency[];
|
dependencies: DBDependency[];
|
||||||
areas: Area[];
|
areas: Area[];
|
||||||
customTypes: DBCustomType[];
|
customTypes: DBCustomType[];
|
||||||
|
notes: Note[];
|
||||||
currentDiagram: Diagram;
|
currentDiagram: Diagram;
|
||||||
events: EventEmitter<ChartDBEvent>;
|
events: EventEmitter<ChartDBEvent>;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
@@ -255,6 +257,31 @@ export interface ChartDBContext {
|
|||||||
options?: { updateHistory: boolean }
|
options?: { updateHistory: boolean }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
|
// Note operations
|
||||||
|
createNote: (attributes?: Partial<Omit<Note, 'id'>>) => Promise<Note>;
|
||||||
|
addNote: (
|
||||||
|
note: Note,
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
|
addNotes: (
|
||||||
|
notes: Note[],
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
|
getNote: (id: string) => Note | null;
|
||||||
|
removeNote: (
|
||||||
|
id: string,
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
|
removeNotes: (
|
||||||
|
ids: string[],
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
|
updateNote: (
|
||||||
|
id: string,
|
||||||
|
note: Partial<Note>,
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
// Custom type operations
|
// Custom type operations
|
||||||
createCustomType: (
|
createCustomType: (
|
||||||
attributes?: Partial<Omit<DBCustomType, 'id'>>
|
attributes?: Partial<Omit<DBCustomType, 'id'>>
|
||||||
@@ -292,6 +319,7 @@ export const chartDBContext = createContext<ChartDBContext>({
|
|||||||
dependencies: [],
|
dependencies: [],
|
||||||
areas: [],
|
areas: [],
|
||||||
customTypes: [],
|
customTypes: [],
|
||||||
|
notes: [],
|
||||||
schemas: [],
|
schemas: [],
|
||||||
highlightCustomTypeId: emptyFn,
|
highlightCustomTypeId: emptyFn,
|
||||||
currentDiagram: {
|
currentDiagram: {
|
||||||
@@ -368,6 +396,15 @@ export const chartDBContext = createContext<ChartDBContext>({
|
|||||||
removeAreas: emptyFn,
|
removeAreas: emptyFn,
|
||||||
updateArea: emptyFn,
|
updateArea: emptyFn,
|
||||||
|
|
||||||
|
// Note operations
|
||||||
|
createNote: emptyFn,
|
||||||
|
addNote: emptyFn,
|
||||||
|
addNotes: emptyFn,
|
||||||
|
getNote: emptyFn,
|
||||||
|
removeNote: emptyFn,
|
||||||
|
removeNotes: emptyFn,
|
||||||
|
updateNote: emptyFn,
|
||||||
|
|
||||||
// Custom type operations
|
// Custom type operations
|
||||||
createCustomType: emptyFn,
|
createCustomType: emptyFn,
|
||||||
addCustomType: emptyFn,
|
addCustomType: emptyFn,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
|
|||||||
import { useEventEmitter } from 'ahooks';
|
import { useEventEmitter } from 'ahooks';
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
import { storageInitialValue } from '../storage-context/storage-context';
|
import { storageInitialValue } from '../storage-context/storage-context';
|
||||||
import { useDiff } from '../diff-context/use-diff';
|
import { useDiff } from '../diff-context/use-diff';
|
||||||
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
||||||
@@ -67,6 +68,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
|
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
|
||||||
diagram?.customTypes ?? []
|
diagram?.customTypes ?? []
|
||||||
);
|
);
|
||||||
|
const [notes, setNotes] = useState<Note[]>(diagram?.notes ?? []);
|
||||||
|
|
||||||
const { events: diffEvents } = useDiff();
|
const { events: diffEvents } = useDiff();
|
||||||
|
|
||||||
@@ -147,6 +149,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
dependencies,
|
dependencies,
|
||||||
areas,
|
areas,
|
||||||
customTypes,
|
customTypes,
|
||||||
|
notes,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
diagramId,
|
diagramId,
|
||||||
@@ -158,6 +161,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
dependencies,
|
dependencies,
|
||||||
areas,
|
areas,
|
||||||
customTypes,
|
customTypes,
|
||||||
|
notes,
|
||||||
diagramCreatedAt,
|
diagramCreatedAt,
|
||||||
diagramUpdatedAt,
|
diagramUpdatedAt,
|
||||||
]
|
]
|
||||||
@@ -171,6 +175,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
setDependencies([]);
|
setDependencies([]);
|
||||||
setAreas([]);
|
setAreas([]);
|
||||||
setCustomTypes([]);
|
setCustomTypes([]);
|
||||||
|
setNotes([]);
|
||||||
setDiagramUpdatedAt(updatedAt);
|
setDiagramUpdatedAt(updatedAt);
|
||||||
|
|
||||||
resetRedoStack();
|
resetRedoStack();
|
||||||
@@ -183,6 +188,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
db.deleteDiagramDependencies(diagramId),
|
db.deleteDiagramDependencies(diagramId),
|
||||||
db.deleteDiagramAreas(diagramId),
|
db.deleteDiagramAreas(diagramId),
|
||||||
db.deleteDiagramCustomTypes(diagramId),
|
db.deleteDiagramCustomTypes(diagramId),
|
||||||
|
db.deleteDiagramNotes(diagramId),
|
||||||
]);
|
]);
|
||||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||||
|
|
||||||
@@ -197,6 +203,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
setDependencies([]);
|
setDependencies([]);
|
||||||
setAreas([]);
|
setAreas([]);
|
||||||
setCustomTypes([]);
|
setCustomTypes([]);
|
||||||
|
setNotes([]);
|
||||||
resetRedoStack();
|
resetRedoStack();
|
||||||
resetUndoStack();
|
resetUndoStack();
|
||||||
|
|
||||||
@@ -207,6 +214,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
db.deleteDiagramDependencies(diagramId),
|
db.deleteDiagramDependencies(diagramId),
|
||||||
db.deleteDiagramAreas(diagramId),
|
db.deleteDiagramAreas(diagramId),
|
||||||
db.deleteDiagramCustomTypes(diagramId),
|
db.deleteDiagramCustomTypes(diagramId),
|
||||||
|
db.deleteDiagramNotes(diagramId),
|
||||||
]);
|
]);
|
||||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||||
|
|
||||||
@@ -1528,6 +1536,130 @@ export const ChartDBProvider: React.FC<
|
|||||||
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
|
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Note operations
|
||||||
|
const addNotes: ChartDBContext['addNotes'] = useCallback(
|
||||||
|
async (notes: Note[], options = { updateHistory: true }) => {
|
||||||
|
setNotes((currentNotes) => [...currentNotes, ...notes]);
|
||||||
|
|
||||||
|
const updatedAt = new Date();
|
||||||
|
setDiagramUpdatedAt(updatedAt);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
...notes.map((note) => db.addNote({ diagramId, note })),
|
||||||
|
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (options.updateHistory) {
|
||||||
|
addUndoAction({
|
||||||
|
action: 'addNotes',
|
||||||
|
redoData: { notes },
|
||||||
|
undoData: { noteIds: notes.map((n) => n.id) },
|
||||||
|
});
|
||||||
|
resetRedoStack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[db, diagramId, setNotes, addUndoAction, resetRedoStack]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addNote: ChartDBContext['addNote'] = useCallback(
|
||||||
|
async (note: Note, options = { updateHistory: true }) => {
|
||||||
|
return addNotes([note], options);
|
||||||
|
},
|
||||||
|
[addNotes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const createNote: ChartDBContext['createNote'] = useCallback(
|
||||||
|
async (attributes) => {
|
||||||
|
const note: Note = {
|
||||||
|
id: generateId(),
|
||||||
|
content: '',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 200,
|
||||||
|
height: 150,
|
||||||
|
color: '#ffe374', // Default warm yellow
|
||||||
|
...attributes,
|
||||||
|
};
|
||||||
|
|
||||||
|
await addNote(note);
|
||||||
|
|
||||||
|
return note;
|
||||||
|
},
|
||||||
|
[addNote]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getNote: ChartDBContext['getNote'] = useCallback(
|
||||||
|
(id: string) => notes.find((note) => note.id === id) ?? null,
|
||||||
|
[notes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeNotes: ChartDBContext['removeNotes'] = useCallback(
|
||||||
|
async (ids: string[], options = { updateHistory: true }) => {
|
||||||
|
const prevNotes = [
|
||||||
|
...notes.filter((note) => ids.includes(note.id)),
|
||||||
|
];
|
||||||
|
|
||||||
|
setNotes((notes) => notes.filter((note) => !ids.includes(note.id)));
|
||||||
|
|
||||||
|
const updatedAt = new Date();
|
||||||
|
setDiagramUpdatedAt(updatedAt);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
...ids.map((id) => db.deleteNote({ diagramId, id })),
|
||||||
|
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (prevNotes.length > 0 && options.updateHistory) {
|
||||||
|
addUndoAction({
|
||||||
|
action: 'removeNotes',
|
||||||
|
redoData: { noteIds: ids },
|
||||||
|
undoData: { notes: prevNotes },
|
||||||
|
});
|
||||||
|
resetRedoStack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[db, diagramId, setNotes, notes, addUndoAction, resetRedoStack]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeNote: ChartDBContext['removeNote'] = useCallback(
|
||||||
|
async (id: string, options = { updateHistory: true }) => {
|
||||||
|
return removeNotes([id], options);
|
||||||
|
},
|
||||||
|
[removeNotes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateNote: ChartDBContext['updateNote'] = useCallback(
|
||||||
|
async (
|
||||||
|
id: string,
|
||||||
|
note: Partial<Note>,
|
||||||
|
options = { updateHistory: true }
|
||||||
|
) => {
|
||||||
|
const prevNote = getNote(id);
|
||||||
|
|
||||||
|
setNotes((notes) =>
|
||||||
|
notes.map((n) => (n.id === id ? { ...n, ...note } : n))
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedAt = new Date();
|
||||||
|
setDiagramUpdatedAt(updatedAt);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||||
|
db.updateNote({ id, attributes: note }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!!prevNote && options.updateHistory) {
|
||||||
|
addUndoAction({
|
||||||
|
action: 'updateNote',
|
||||||
|
redoData: { noteId: id, note },
|
||||||
|
undoData: { noteId: id, note: prevNote },
|
||||||
|
});
|
||||||
|
resetRedoStack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[db, diagramId, setNotes, getNote, addUndoAction, resetRedoStack]
|
||||||
|
);
|
||||||
|
|
||||||
const highlightCustomTypeId = useCallback(
|
const highlightCustomTypeId = useCallback(
|
||||||
(id?: string) => setHighlightedCustomTypeId(id),
|
(id?: string) => setHighlightedCustomTypeId(id),
|
||||||
[setHighlightedCustomTypeId]
|
[setHighlightedCustomTypeId]
|
||||||
@@ -1554,6 +1686,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
setDiagramCreatedAt(diagram.createdAt);
|
setDiagramCreatedAt(diagram.createdAt);
|
||||||
setDiagramUpdatedAt(diagram.updatedAt);
|
setDiagramUpdatedAt(diagram.updatedAt);
|
||||||
setHighlightedCustomTypeId(undefined);
|
setHighlightedCustomTypeId(undefined);
|
||||||
|
setNotes(diagram.notes ?? []);
|
||||||
|
|
||||||
events.emit({ action: 'load_diagram', data: { diagram } });
|
events.emit({ action: 'load_diagram', data: { diagram } });
|
||||||
|
|
||||||
@@ -1574,6 +1707,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
setDiagramUpdatedAt,
|
setDiagramUpdatedAt,
|
||||||
setHighlightedCustomTypeId,
|
setHighlightedCustomTypeId,
|
||||||
events,
|
events,
|
||||||
|
setNotes,
|
||||||
resetRedoStack,
|
resetRedoStack,
|
||||||
resetUndoStack,
|
resetUndoStack,
|
||||||
]
|
]
|
||||||
@@ -1597,6 +1731,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
includeDependencies: true,
|
includeDependencies: true,
|
||||||
includeAreas: true,
|
includeAreas: true,
|
||||||
includeCustomTypes: true,
|
includeCustomTypes: true,
|
||||||
|
includeNotes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (diagram) {
|
if (diagram) {
|
||||||
@@ -1762,6 +1897,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
relationships,
|
relationships,
|
||||||
dependencies,
|
dependencies,
|
||||||
areas,
|
areas,
|
||||||
|
notes,
|
||||||
currentDiagram,
|
currentDiagram,
|
||||||
schemas,
|
schemas,
|
||||||
events,
|
events,
|
||||||
@@ -1825,6 +1961,13 @@ export const ChartDBProvider: React.FC<
|
|||||||
updateCustomType,
|
updateCustomType,
|
||||||
highlightCustomTypeId,
|
highlightCustomTypeId,
|
||||||
highlightedCustomType,
|
highlightedCustomType,
|
||||||
|
createNote,
|
||||||
|
addNote,
|
||||||
|
addNotes,
|
||||||
|
getNote,
|
||||||
|
removeNote,
|
||||||
|
removeNotes,
|
||||||
|
updateNote,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addCustomTypes,
|
addCustomTypes,
|
||||||
removeCustomTypes,
|
removeCustomTypes,
|
||||||
updateCustomType,
|
updateCustomType,
|
||||||
|
addNotes,
|
||||||
|
removeNotes,
|
||||||
|
updateNote,
|
||||||
} = useChartDB();
|
} = useChartDB();
|
||||||
|
|
||||||
const redoActionHandlers = useMemo(
|
const redoActionHandlers = useMemo(
|
||||||
@@ -135,6 +138,15 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
updateHistory: false,
|
updateHistory: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
addNotes: ({ redoData: { notes } }) => {
|
||||||
|
return addNotes(notes, { updateHistory: false });
|
||||||
|
},
|
||||||
|
removeNotes: ({ redoData: { noteIds } }) => {
|
||||||
|
return removeNotes(noteIds, { updateHistory: false });
|
||||||
|
},
|
||||||
|
updateNote: ({ redoData: { noteId, note } }) => {
|
||||||
|
return updateNote(noteId, note, { updateHistory: false });
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
addTables,
|
addTables,
|
||||||
@@ -160,6 +172,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addCustomTypes,
|
addCustomTypes,
|
||||||
removeCustomTypes,
|
removeCustomTypes,
|
||||||
updateCustomType,
|
updateCustomType,
|
||||||
|
addNotes,
|
||||||
|
removeNotes,
|
||||||
|
updateNote,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -271,6 +286,15 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
updateHistory: false,
|
updateHistory: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
addNotes: ({ undoData: { noteIds } }) => {
|
||||||
|
return removeNotes(noteIds, { updateHistory: false });
|
||||||
|
},
|
||||||
|
removeNotes: ({ undoData: { notes } }) => {
|
||||||
|
return addNotes(notes, { updateHistory: false });
|
||||||
|
},
|
||||||
|
updateNote: ({ undoData: { noteId, note } }) => {
|
||||||
|
return updateNote(noteId, note, { updateHistory: false });
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
addTables,
|
addTables,
|
||||||
@@ -296,6 +320,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addCustomTypes,
|
addCustomTypes,
|
||||||
removeCustomTypes,
|
removeCustomTypes,
|
||||||
updateCustomType,
|
updateCustomType,
|
||||||
|
addNotes,
|
||||||
|
removeNotes,
|
||||||
|
updateNote,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
|
|||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
|
|
||||||
type Action = keyof ChartDBContext;
|
type Action = keyof ChartDBContext;
|
||||||
|
|
||||||
@@ -161,6 +162,24 @@ type RedoUndoActionRemoveCustomTypes = RedoUndoActionBase<
|
|||||||
{ customTypes: DBCustomType[] }
|
{ customTypes: DBCustomType[] }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type RedoUndoActionAddNotes = RedoUndoActionBase<
|
||||||
|
'addNotes',
|
||||||
|
{ notes: Note[] },
|
||||||
|
{ noteIds: string[] }
|
||||||
|
>;
|
||||||
|
|
||||||
|
type RedoUndoActionUpdateNote = RedoUndoActionBase<
|
||||||
|
'updateNote',
|
||||||
|
{ noteId: string; note: Partial<Note> },
|
||||||
|
{ noteId: string; note: Partial<Note> }
|
||||||
|
>;
|
||||||
|
|
||||||
|
type RedoUndoActionRemoveNotes = RedoUndoActionBase<
|
||||||
|
'removeNotes',
|
||||||
|
{ noteIds: string[] },
|
||||||
|
{ notes: Note[] }
|
||||||
|
>;
|
||||||
|
|
||||||
export type RedoUndoAction =
|
export type RedoUndoAction =
|
||||||
| RedoUndoActionAddTables
|
| RedoUndoActionAddTables
|
||||||
| RedoUndoActionRemoveTables
|
| RedoUndoActionRemoveTables
|
||||||
@@ -184,7 +203,10 @@ export type RedoUndoAction =
|
|||||||
| RedoUndoActionRemoveAreas
|
| RedoUndoActionRemoveAreas
|
||||||
| RedoUndoActionAddCustomTypes
|
| RedoUndoActionAddCustomTypes
|
||||||
| RedoUndoActionUpdateCustomType
|
| RedoUndoActionUpdateCustomType
|
||||||
| RedoUndoActionRemoveCustomTypes;
|
| RedoUndoActionRemoveCustomTypes
|
||||||
|
| RedoUndoActionAddNotes
|
||||||
|
| RedoUndoActionUpdateNote
|
||||||
|
| RedoUndoActionRemoveNotes;
|
||||||
|
|
||||||
export type RedoActionData<T extends Action> = Extract<
|
export type RedoActionData<T extends Action> = Extract<
|
||||||
RedoUndoAction,
|
RedoUndoAction,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export type SidebarSection =
|
|||||||
| 'customTypes'
|
| 'customTypes'
|
||||||
| 'visuals';
|
| 'visuals';
|
||||||
|
|
||||||
export type VisualsTab = 'areas';
|
export type VisualsTab = 'areas' | 'notes';
|
||||||
|
|
||||||
export interface LayoutContext {
|
export interface LayoutContext {
|
||||||
openedTableInSidebar: string | undefined;
|
openedTableInSidebar: string | undefined;
|
||||||
@@ -29,6 +29,10 @@ export interface LayoutContext {
|
|||||||
openAreaFromSidebar: (areaId: string) => void;
|
openAreaFromSidebar: (areaId: string) => void;
|
||||||
closeAllAreasInSidebar: () => void;
|
closeAllAreasInSidebar: () => void;
|
||||||
|
|
||||||
|
openedNoteInSidebar: string | undefined;
|
||||||
|
openNoteFromSidebar: (noteId: string) => void;
|
||||||
|
closeAllNotesInSidebar: () => void;
|
||||||
|
|
||||||
openedCustomTypeInSidebar: string | undefined;
|
openedCustomTypeInSidebar: string | undefined;
|
||||||
openCustomTypeFromSidebar: (customTypeId: string) => void;
|
openCustomTypeFromSidebar: (customTypeId: string) => void;
|
||||||
closeAllCustomTypesInSidebar: () => void;
|
closeAllCustomTypesInSidebar: () => void;
|
||||||
@@ -63,6 +67,10 @@ export const layoutContext = createContext<LayoutContext>({
|
|||||||
openAreaFromSidebar: emptyFn,
|
openAreaFromSidebar: emptyFn,
|
||||||
closeAllAreasInSidebar: emptyFn,
|
closeAllAreasInSidebar: emptyFn,
|
||||||
|
|
||||||
|
openedNoteInSidebar: undefined,
|
||||||
|
openNoteFromSidebar: emptyFn,
|
||||||
|
closeAllNotesInSidebar: emptyFn,
|
||||||
|
|
||||||
openedCustomTypeInSidebar: undefined,
|
openedCustomTypeInSidebar: undefined,
|
||||||
openCustomTypeFromSidebar: emptyFn,
|
openCustomTypeFromSidebar: emptyFn,
|
||||||
closeAllCustomTypesInSidebar: emptyFn,
|
closeAllCustomTypesInSidebar: emptyFn,
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
|
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
|
const [openedNoteInSidebar, setOpenedNoteInSidebar] = React.useState<
|
||||||
|
string | undefined
|
||||||
|
>();
|
||||||
const [openedCustomTypeInSidebar, setOpenedCustomTypeInSidebar] =
|
const [openedCustomTypeInSidebar, setOpenedCustomTypeInSidebar] =
|
||||||
React.useState<string | undefined>();
|
React.useState<string | undefined>();
|
||||||
const [selectedSidebarSection, setSelectedSidebarSection] =
|
const [selectedSidebarSection, setSelectedSidebarSection] =
|
||||||
@@ -44,6 +47,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
|
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
|
||||||
() => setOpenedAreaInSidebar('');
|
() => setOpenedAreaInSidebar('');
|
||||||
|
|
||||||
|
const closeAllNotesInSidebar: LayoutContext['closeAllNotesInSidebar'] =
|
||||||
|
() => setOpenedNoteInSidebar('');
|
||||||
|
|
||||||
const closeAllCustomTypesInSidebar: LayoutContext['closeAllCustomTypesInSidebar'] =
|
const closeAllCustomTypesInSidebar: LayoutContext['closeAllCustomTypesInSidebar'] =
|
||||||
() => setOpenedCustomTypeInSidebar('');
|
() => setOpenedCustomTypeInSidebar('');
|
||||||
|
|
||||||
@@ -94,6 +100,15 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setOpenedAreaInSidebar(areaId);
|
setOpenedAreaInSidebar(areaId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openNoteFromSidebar: LayoutContext['openNoteFromSidebar'] = (
|
||||||
|
noteId
|
||||||
|
) => {
|
||||||
|
showSidePanel();
|
||||||
|
setSelectedSidebarSection('visuals');
|
||||||
|
setSelectedVisualsTab('notes');
|
||||||
|
setOpenedNoteInSidebar(noteId);
|
||||||
|
};
|
||||||
|
|
||||||
const openCustomTypeFromSidebar: LayoutContext['openCustomTypeFromSidebar'] =
|
const openCustomTypeFromSidebar: LayoutContext['openCustomTypeFromSidebar'] =
|
||||||
(customTypeId) => {
|
(customTypeId) => {
|
||||||
showSidePanel();
|
showSidePanel();
|
||||||
@@ -123,6 +138,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
openedAreaInSidebar,
|
openedAreaInSidebar,
|
||||||
openAreaFromSidebar,
|
openAreaFromSidebar,
|
||||||
closeAllAreasInSidebar,
|
closeAllAreasInSidebar,
|
||||||
|
openedNoteInSidebar,
|
||||||
|
openNoteFromSidebar,
|
||||||
|
closeAllNotesInSidebar,
|
||||||
openedCustomTypeInSidebar,
|
openedCustomTypeInSidebar,
|
||||||
openCustomTypeFromSidebar,
|
openCustomTypeFromSidebar,
|
||||||
closeAllCustomTypesInSidebar,
|
closeAllCustomTypesInSidebar,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { DBDependency } from '@/lib/domain/db-dependency';
|
|||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
|
|
||||||
export interface StorageContext {
|
export interface StorageContext {
|
||||||
// Config operations
|
// Config operations
|
||||||
@@ -30,6 +31,7 @@ export interface StorageContext {
|
|||||||
includeDependencies?: boolean;
|
includeDependencies?: boolean;
|
||||||
includeAreas?: boolean;
|
includeAreas?: boolean;
|
||||||
includeCustomTypes?: boolean;
|
includeCustomTypes?: boolean;
|
||||||
|
includeNotes?: boolean;
|
||||||
}) => Promise<Diagram[]>;
|
}) => Promise<Diagram[]>;
|
||||||
getDiagram: (
|
getDiagram: (
|
||||||
id: string,
|
id: string,
|
||||||
@@ -39,6 +41,7 @@ export interface StorageContext {
|
|||||||
includeDependencies?: boolean;
|
includeDependencies?: boolean;
|
||||||
includeAreas?: boolean;
|
includeAreas?: boolean;
|
||||||
includeCustomTypes?: boolean;
|
includeCustomTypes?: boolean;
|
||||||
|
includeNotes?: boolean;
|
||||||
}
|
}
|
||||||
) => Promise<Diagram | undefined>;
|
) => Promise<Diagram | undefined>;
|
||||||
updateDiagram: (params: {
|
updateDiagram: (params: {
|
||||||
@@ -135,6 +138,20 @@ export interface StorageContext {
|
|||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
listCustomTypes: (diagramId: string) => Promise<DBCustomType[]>;
|
listCustomTypes: (diagramId: string) => Promise<DBCustomType[]>;
|
||||||
deleteDiagramCustomTypes: (diagramId: string) => Promise<void>;
|
deleteDiagramCustomTypes: (diagramId: string) => Promise<void>;
|
||||||
|
|
||||||
|
// Note operations
|
||||||
|
addNote: (params: { diagramId: string; note: Note }) => Promise<void>;
|
||||||
|
getNote: (params: {
|
||||||
|
diagramId: string;
|
||||||
|
id: string;
|
||||||
|
}) => Promise<Note | undefined>;
|
||||||
|
updateNote: (params: {
|
||||||
|
id: string;
|
||||||
|
attributes: Partial<Note>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
deleteNote: (params: { diagramId: string; id: string }) => Promise<void>;
|
||||||
|
listNotes: (diagramId: string) => Promise<Note[]>;
|
||||||
|
deleteDiagramNotes: (diagramId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storageInitialValue: StorageContext = {
|
export const storageInitialValue: StorageContext = {
|
||||||
@@ -187,6 +204,14 @@ export const storageInitialValue: StorageContext = {
|
|||||||
deleteCustomType: emptyFn,
|
deleteCustomType: emptyFn,
|
||||||
listCustomTypes: emptyFn,
|
listCustomTypes: emptyFn,
|
||||||
deleteDiagramCustomTypes: emptyFn,
|
deleteDiagramCustomTypes: emptyFn,
|
||||||
|
|
||||||
|
// Note operations
|
||||||
|
addNote: emptyFn,
|
||||||
|
getNote: emptyFn,
|
||||||
|
updateNote: emptyFn,
|
||||||
|
deleteNote: emptyFn,
|
||||||
|
listNotes: emptyFn,
|
||||||
|
deleteDiagramNotes: emptyFn,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const storageContext =
|
export const storageContext =
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { DBDependency } from '@/lib/domain/db-dependency';
|
|||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
|
|
||||||
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -41,6 +42,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
DBCustomType & { diagramId: string },
|
DBCustomType & { diagramId: string },
|
||||||
'id' // primary key "id" (for the typings only)
|
'id' // primary key "id" (for the typings only)
|
||||||
>;
|
>;
|
||||||
|
notes: EntityTable<
|
||||||
|
Note & { diagramId: string },
|
||||||
|
'id' // primary key "id" (for the typings only)
|
||||||
|
>;
|
||||||
config: EntityTable<
|
config: EntityTable<
|
||||||
ChartDBConfig & { id: number },
|
ChartDBConfig & { id: number },
|
||||||
'id' // primary key "id" (for the typings only)
|
'id' // primary key "id" (for the typings only)
|
||||||
@@ -216,6 +221,23 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
tx.table('config').clear();
|
tx.table('config').clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dexieDB.version(13).stores({
|
||||||
|
diagrams:
|
||||||
|
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||||
|
db_tables:
|
||||||
|
'++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
|
||||||
|
db_relationships:
|
||||||
|
'++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
|
||||||
|
db_dependencies:
|
||||||
|
'++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
|
||||||
|
areas: '++id, diagramId, name, x, y, width, height, color',
|
||||||
|
db_custom_types:
|
||||||
|
'++id, diagramId, schema, type, kind, values, fields',
|
||||||
|
config: '++id, defaultDiagramId',
|
||||||
|
diagram_filters: 'diagramId, tableIds, schemasIds',
|
||||||
|
notes: '++id, diagramId, content, x, y, width, height, color',
|
||||||
|
});
|
||||||
|
|
||||||
dexieDB.on('ready', async () => {
|
dexieDB.on('ready', async () => {
|
||||||
const config = await dexieDB.config.get(1);
|
const config = await dexieDB.config.get(1);
|
||||||
|
|
||||||
@@ -550,6 +572,56 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
[db]
|
[db]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Note operations
|
||||||
|
const addNote: StorageContext['addNote'] = useCallback(
|
||||||
|
async ({ note, diagramId }) => {
|
||||||
|
await db.notes.add({
|
||||||
|
...note,
|
||||||
|
diagramId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getNote: StorageContext['getNote'] = useCallback(
|
||||||
|
async ({ diagramId, id }) => {
|
||||||
|
return await db.notes.get({ id, diagramId });
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateNote: StorageContext['updateNote'] = useCallback(
|
||||||
|
async ({ id, attributes }) => {
|
||||||
|
await db.notes.update(id, attributes);
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteNote: StorageContext['deleteNote'] = useCallback(
|
||||||
|
async ({ diagramId, id }) => {
|
||||||
|
await db.notes.where({ id, diagramId }).delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const listNotes: StorageContext['listNotes'] = useCallback(
|
||||||
|
async (diagramId) => {
|
||||||
|
return await db.notes
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.toArray();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteDiagramNotes: StorageContext['deleteDiagramNotes'] =
|
||||||
|
useCallback(
|
||||||
|
async (diagramId) => {
|
||||||
|
await db.notes.where('diagramId').equals(diagramId).delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
const addDiagram: StorageContext['addDiagram'] = useCallback(
|
const addDiagram: StorageContext['addDiagram'] = useCallback(
|
||||||
async ({ diagram }) => {
|
async ({ diagram }) => {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
@@ -597,9 +669,22 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const notes = diagram.notes ?? [];
|
||||||
|
promises.push(
|
||||||
|
...notes.map((note) => addNote({ diagramId: diagram.id, note }))
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
},
|
},
|
||||||
[db, addArea, addCustomType, addDependency, addRelationship, addTable]
|
[
|
||||||
|
db,
|
||||||
|
addArea,
|
||||||
|
addCustomType,
|
||||||
|
addDependency,
|
||||||
|
addRelationship,
|
||||||
|
addTable,
|
||||||
|
addNote,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const listDiagrams: StorageContext['listDiagrams'] = useCallback(
|
const listDiagrams: StorageContext['listDiagrams'] = useCallback(
|
||||||
@@ -610,6 +695,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
includeDependencies: false,
|
includeDependencies: false,
|
||||||
includeAreas: false,
|
includeAreas: false,
|
||||||
includeCustomTypes: false,
|
includeCustomTypes: false,
|
||||||
|
includeNotes: false,
|
||||||
}
|
}
|
||||||
): Promise<Diagram[]> => {
|
): Promise<Diagram[]> => {
|
||||||
let diagrams = await db.diagrams.toArray();
|
let diagrams = await db.diagrams.toArray();
|
||||||
@@ -663,6 +749,15 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.includeNotes) {
|
||||||
|
diagrams = await Promise.all(
|
||||||
|
diagrams.map(async (diagram) => {
|
||||||
|
diagram.notes = await listNotes(diagram.id);
|
||||||
|
return diagram;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return diagrams;
|
return diagrams;
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -672,6 +767,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
listDependencies,
|
listDependencies,
|
||||||
listRelationships,
|
listRelationships,
|
||||||
listTables,
|
listTables,
|
||||||
|
listNotes,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -684,6 +780,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
includeDependencies: false,
|
includeDependencies: false,
|
||||||
includeAreas: false,
|
includeAreas: false,
|
||||||
includeCustomTypes: false,
|
includeCustomTypes: false,
|
||||||
|
includeNotes: false,
|
||||||
}
|
}
|
||||||
): Promise<Diagram | undefined> => {
|
): Promise<Diagram | undefined> => {
|
||||||
const diagram = await db.diagrams.get(id);
|
const diagram = await db.diagrams.get(id);
|
||||||
@@ -712,6 +809,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
diagram.customTypes = await listCustomTypes(id);
|
diagram.customTypes = await listCustomTypes(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.includeNotes) {
|
||||||
|
diagram.notes = await listNotes(id);
|
||||||
|
}
|
||||||
|
|
||||||
return diagram;
|
return diagram;
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -721,6 +822,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
listDependencies,
|
listDependencies,
|
||||||
listRelationships,
|
listRelationships,
|
||||||
listTables,
|
listTables,
|
||||||
|
listNotes,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -749,6 +851,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
.where('diagramId')
|
.where('diagramId')
|
||||||
.equals(id)
|
.equals(id)
|
||||||
.modify({ diagramId: attributes.id }),
|
.modify({ diagramId: attributes.id }),
|
||||||
|
db.notes.where('diagramId').equals(id).modify({
|
||||||
|
diagramId: attributes.id,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -764,6 +869,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
db.db_dependencies.where('diagramId').equals(id).delete(),
|
db.db_dependencies.where('diagramId').equals(id).delete(),
|
||||||
db.areas.where('diagramId').equals(id).delete(),
|
db.areas.where('diagramId').equals(id).delete(),
|
||||||
db.db_custom_types.where('diagramId').equals(id).delete(),
|
db.db_custom_types.where('diagramId').equals(id).delete(),
|
||||||
|
db.notes.where('diagramId').equals(id).delete(),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
[db]
|
[db]
|
||||||
@@ -810,6 +916,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
deleteCustomType,
|
deleteCustomType,
|
||||||
listCustomTypes,
|
listCustomTypes,
|
||||||
deleteDiagramCustomTypes,
|
deleteDiagramCustomTypes,
|
||||||
|
addNote,
|
||||||
|
getNote,
|
||||||
|
updateNote,
|
||||||
|
deleteNote,
|
||||||
|
listNotes,
|
||||||
|
deleteDiagramNotes,
|
||||||
getDiagramFilter,
|
getDiagramFilter,
|
||||||
updateDiagramFilter,
|
updateDiagramFilter,
|
||||||
deleteDiagramFilter,
|
deleteDiagramFilter,
|
||||||
|
|||||||
@@ -88,6 +88,44 @@ export const useFocusOn = () => {
|
|||||||
[fitView, setNodes, hideSidePanel, isDesktop]
|
[fitView, setNodes, hideSidePanel, isDesktop]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const focusOnNote = useCallback(
|
||||||
|
(noteId: string, options: FocusOptions = {}) => {
|
||||||
|
const { select = true } = options;
|
||||||
|
|
||||||
|
if (select) {
|
||||||
|
setNodes((nodes) =>
|
||||||
|
nodes.map((node) =>
|
||||||
|
node.id === noteId
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
selected: true,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...node,
|
||||||
|
selected: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fitView({
|
||||||
|
duration: 500,
|
||||||
|
maxZoom: 1,
|
||||||
|
minZoom: 1,
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: noteId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDesktop) {
|
||||||
|
hideSidePanel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fitView, setNodes, hideSidePanel, isDesktop]
|
||||||
|
);
|
||||||
|
|
||||||
const focusOnRelationship = useCallback(
|
const focusOnRelationship = useCallback(
|
||||||
(
|
(
|
||||||
relationshipId: string,
|
relationshipId: string,
|
||||||
@@ -137,6 +175,7 @@ export const useFocusOn = () => {
|
|||||||
return {
|
return {
|
||||||
focusOnArea,
|
focusOnArea,
|
||||||
focusOnTable,
|
focusOnTable,
|
||||||
|
focusOnNote,
|
||||||
focusOnRelationship,
|
focusOnRelationship,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const ar: LanguageTranslation = {
|
|||||||
browse: 'تصفح',
|
browse: 'تصفح',
|
||||||
tables: 'الجداول',
|
tables: 'الجداول',
|
||||||
refs: 'المراجع',
|
refs: 'المراجع',
|
||||||
areas: 'المناطق',
|
|
||||||
dependencies: 'التبعيات',
|
dependencies: 'التبعيات',
|
||||||
custom_types: 'الأنواع المخصصة',
|
custom_types: 'الأنواع المخصصة',
|
||||||
visuals: 'مرئيات',
|
visuals: 'مرئيات',
|
||||||
@@ -237,6 +236,26 @@ export const ar: LanguageTranslation = {
|
|||||||
visuals: 'مرئيات',
|
visuals: 'مرئيات',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'ملاحظات',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'تصفية',
|
||||||
|
add_note: 'إضافة ملاحظة',
|
||||||
|
no_results: 'لم يتم العثور على ملاحظات',
|
||||||
|
clear: 'مسح التصفية',
|
||||||
|
empty_state: {
|
||||||
|
title: 'لا توجد ملاحظات',
|
||||||
|
description: 'أنشئ ملاحظة لإضافة تعليقات نصية على اللوحة',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'ملاحظة فارغة',
|
||||||
|
note_actions: {
|
||||||
|
title: 'إجراءات الملاحظة',
|
||||||
|
edit_content: 'تحرير المحتوى',
|
||||||
|
delete_note: 'حذف الملاحظة',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -487,6 +506,7 @@ export const ar: LanguageTranslation = {
|
|||||||
new_relationship: 'علاقة جديدة',
|
new_relationship: 'علاقة جديدة',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'ملاحظة جديدة',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const bn: LanguageTranslation = {
|
|||||||
browse: 'ব্রাউজ',
|
browse: 'ব্রাউজ',
|
||||||
tables: 'টেবিল',
|
tables: 'টেবিল',
|
||||||
refs: 'রেফস',
|
refs: 'রেফস',
|
||||||
areas: 'এলাকা',
|
|
||||||
dependencies: 'নির্ভরতা',
|
dependencies: 'নির্ভরতা',
|
||||||
custom_types: 'কাস্টম টাইপ',
|
custom_types: 'কাস্টম টাইপ',
|
||||||
visuals: 'ভিজ্যুয়াল',
|
visuals: 'ভিজ্যুয়াল',
|
||||||
@@ -239,6 +238,27 @@ export const bn: LanguageTranslation = {
|
|||||||
visuals: 'ভিজ্যুয়াল',
|
visuals: 'ভিজ্যুয়াল',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'নোট',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'ফিল্টার',
|
||||||
|
add_note: 'নোট যোগ করুন',
|
||||||
|
no_results: 'কোনো নোট পাওয়া যায়নি',
|
||||||
|
clear: 'ফিল্টার সাফ করুন',
|
||||||
|
empty_state: {
|
||||||
|
title: 'কোনো নোট নেই',
|
||||||
|
description:
|
||||||
|
'ক্যানভাসে টেক্সট টীকা যোগ করতে একটি নোট তৈরি করুন',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'খালি নোট',
|
||||||
|
note_actions: {
|
||||||
|
title: 'নোট ক্রিয়া',
|
||||||
|
edit_content: 'বিষয়বস্তু সম্পাদনা',
|
||||||
|
delete_note: 'নোট মুছুন',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -493,6 +513,7 @@ export const bn: LanguageTranslation = {
|
|||||||
new_relationship: 'নতুন সম্পর্ক',
|
new_relationship: 'নতুন সম্পর্ক',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'নতুন নোট',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const de: LanguageTranslation = {
|
|||||||
browse: 'Durchsuchen',
|
browse: 'Durchsuchen',
|
||||||
tables: 'Tabellen',
|
tables: 'Tabellen',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'Bereiche',
|
|
||||||
dependencies: 'Abhängigkeiten',
|
dependencies: 'Abhängigkeiten',
|
||||||
custom_types: 'Benutzerdefinierte Typen',
|
custom_types: 'Benutzerdefinierte Typen',
|
||||||
visuals: 'Darstellungen',
|
visuals: 'Darstellungen',
|
||||||
@@ -240,6 +239,27 @@ export const de: LanguageTranslation = {
|
|||||||
visuals: 'Darstellungen',
|
visuals: 'Darstellungen',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'Notizen',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Filter',
|
||||||
|
add_note: 'Notiz hinzufügen',
|
||||||
|
no_results: 'Keine Notizen gefunden',
|
||||||
|
clear: 'Filter löschen',
|
||||||
|
empty_state: {
|
||||||
|
title: 'Keine Notizen',
|
||||||
|
description:
|
||||||
|
'Erstellen Sie eine Notiz, um Textanmerkungen auf der Leinwand hinzuzufügen',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Leere Notiz',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Notiz-Aktionen',
|
||||||
|
edit_content: 'Inhalt bearbeiten',
|
||||||
|
delete_note: 'Notiz löschen',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -496,6 +516,7 @@ export const de: LanguageTranslation = {
|
|||||||
new_relationship: 'Neue Beziehung',
|
new_relationship: 'Neue Beziehung',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'Neue Notiz',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const en = {
|
|||||||
browse: 'Browse',
|
browse: 'Browse',
|
||||||
tables: 'Tables',
|
tables: 'Tables',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'Areas',
|
|
||||||
dependencies: 'Dependencies',
|
dependencies: 'Dependencies',
|
||||||
custom_types: 'Custom Types',
|
custom_types: 'Custom Types',
|
||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
@@ -232,6 +231,27 @@ export const en = {
|
|||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'Notes',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Filter',
|
||||||
|
add_note: 'Add Note',
|
||||||
|
no_results: 'No notes found',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No Notes',
|
||||||
|
description:
|
||||||
|
'Create a note to add text annotations on the canvas',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Empty note',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Note Actions',
|
||||||
|
edit_content: 'Edit Content',
|
||||||
|
delete_note: 'Delete Note',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -481,6 +501,7 @@ export const en = {
|
|||||||
new_view: 'New View',
|
new_view: 'New View',
|
||||||
new_relationship: 'New Relationship',
|
new_relationship: 'New Relationship',
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'New Note',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const es: LanguageTranslation = {
|
|||||||
browse: 'Examinar',
|
browse: 'Examinar',
|
||||||
tables: 'Tablas',
|
tables: 'Tablas',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'Áreas',
|
|
||||||
dependencies: 'Dependencias',
|
dependencies: 'Dependencias',
|
||||||
custom_types: 'Tipos Personalizados',
|
custom_types: 'Tipos Personalizados',
|
||||||
visuals: 'Visuales',
|
visuals: 'Visuales',
|
||||||
@@ -238,6 +237,27 @@ export const es: LanguageTranslation = {
|
|||||||
visuals: 'Visuales',
|
visuals: 'Visuales',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'Notas',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Filtrar',
|
||||||
|
add_note: 'Agregar Nota',
|
||||||
|
no_results: 'No se encontraron notas',
|
||||||
|
clear: 'Limpiar Filtro',
|
||||||
|
empty_state: {
|
||||||
|
title: 'Sin Notas',
|
||||||
|
description:
|
||||||
|
'Crea una nota para agregar anotaciones de texto en el lienzo',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Nota vacía',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Acciones de Nota',
|
||||||
|
edit_content: 'Editar Contenido',
|
||||||
|
delete_note: 'Eliminar Nota',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -495,6 +515,7 @@ export const es: LanguageTranslation = {
|
|||||||
new_relationship: 'Nueva Relación',
|
new_relationship: 'Nueva Relación',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'Nueva Nota',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const fr: LanguageTranslation = {
|
|||||||
browse: 'Parcourir',
|
browse: 'Parcourir',
|
||||||
tables: 'Tables',
|
tables: 'Tables',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'Zones',
|
|
||||||
dependencies: 'Dépendances',
|
dependencies: 'Dépendances',
|
||||||
custom_types: 'Types Personnalisés',
|
custom_types: 'Types Personnalisés',
|
||||||
visuals: 'Visuels',
|
visuals: 'Visuels',
|
||||||
@@ -236,6 +235,27 @@ export const fr: LanguageTranslation = {
|
|||||||
visuals: 'Visuels',
|
visuals: 'Visuels',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'Notes',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Filtrer',
|
||||||
|
add_note: 'Ajouter une Note',
|
||||||
|
no_results: 'Aucune note trouvée',
|
||||||
|
clear: 'Effacer le Filtre',
|
||||||
|
empty_state: {
|
||||||
|
title: 'Pas de Notes',
|
||||||
|
description:
|
||||||
|
'Créez une note pour ajouter des annotations de texte sur le canevas',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Note vide',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Actions de Note',
|
||||||
|
edit_content: 'Modifier le Contenu',
|
||||||
|
delete_note: 'Supprimer la Note',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -491,6 +511,7 @@ export const fr: LanguageTranslation = {
|
|||||||
new_relationship: 'Nouvelle Relation',
|
new_relationship: 'Nouvelle Relation',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'Nouvelle Note',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const gu: LanguageTranslation = {
|
|||||||
browse: 'બ્રાઉજ',
|
browse: 'બ્રાઉજ',
|
||||||
tables: 'ટેબલો',
|
tables: 'ટેબલો',
|
||||||
refs: 'રેફ્સ',
|
refs: 'રેફ્સ',
|
||||||
areas: 'ક્ષેત્રો',
|
|
||||||
dependencies: 'નિર્ભરતાઓ',
|
dependencies: 'નિર્ભરતાઓ',
|
||||||
custom_types: 'કસ્ટમ ટાઇપ',
|
custom_types: 'કસ્ટમ ટાઇપ',
|
||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
@@ -240,6 +239,27 @@ export const gu: LanguageTranslation = {
|
|||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'નોંધો',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'ફિલ્ટર',
|
||||||
|
add_note: 'નોંધ ઉમેરો',
|
||||||
|
no_results: 'કોઈ નોંધો મળી નથી',
|
||||||
|
clear: 'ફિલ્ટર સાફ કરો',
|
||||||
|
empty_state: {
|
||||||
|
title: 'કોઈ નોંધો નથી',
|
||||||
|
description:
|
||||||
|
'કેનવાસ પર ટેક્સ્ટ એનોટેશન ઉમેરવા માટે નોંધ બનાવો',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'ખાલી નોંધ',
|
||||||
|
note_actions: {
|
||||||
|
title: 'નોંધ ક્રિયાઓ',
|
||||||
|
edit_content: 'સામગ્રી સંપાદિત કરો',
|
||||||
|
delete_note: 'નોંધ કાઢી નાખો',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -494,6 +514,7 @@ export const gu: LanguageTranslation = {
|
|||||||
new_relationship: 'નવો સંબંધ',
|
new_relationship: 'નવો સંબંધ',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'નવી નોંધ',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const hi: LanguageTranslation = {
|
|||||||
browse: 'ब्राउज़',
|
browse: 'ब्राउज़',
|
||||||
tables: 'टेबल',
|
tables: 'टेबल',
|
||||||
refs: 'रेफ्स',
|
refs: 'रेफ्स',
|
||||||
areas: 'क्षेत्र',
|
|
||||||
dependencies: 'निर्भरताएं',
|
dependencies: 'निर्भरताएं',
|
||||||
custom_types: 'कस्टम टाइप',
|
custom_types: 'कस्टम टाइप',
|
||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
@@ -239,6 +238,27 @@ export const hi: LanguageTranslation = {
|
|||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'नोट्स',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'फ़िल्टर',
|
||||||
|
add_note: 'नोट जोड़ें',
|
||||||
|
no_results: 'कोई नोट नहीं मिला',
|
||||||
|
clear: 'फ़िल्टर साफ़ करें',
|
||||||
|
empty_state: {
|
||||||
|
title: 'कोई नोट नहीं',
|
||||||
|
description:
|
||||||
|
'कैनवास पर टेक्स्ट एनोटेशन जोड़ने के लिए एक नोट बनाएं',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'खाली नोट',
|
||||||
|
note_actions: {
|
||||||
|
title: 'नोट क्रियाएं',
|
||||||
|
edit_content: 'सामग्री संपादित करें',
|
||||||
|
delete_note: 'नोट हटाएं',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -496,6 +516,7 @@ export const hi: LanguageTranslation = {
|
|||||||
new_relationship: 'नया संबंध',
|
new_relationship: 'नया संबंध',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'नया नोट',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const hr: LanguageTranslation = {
|
|||||||
browse: 'Pregledaj',
|
browse: 'Pregledaj',
|
||||||
tables: 'Tablice',
|
tables: 'Tablice',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'Područja',
|
|
||||||
dependencies: 'Ovisnosti',
|
dependencies: 'Ovisnosti',
|
||||||
custom_types: 'Prilagođeni Tipovi',
|
custom_types: 'Prilagođeni Tipovi',
|
||||||
visuals: 'Vizuali',
|
visuals: 'Vizuali',
|
||||||
@@ -234,6 +233,27 @@ export const hr: LanguageTranslation = {
|
|||||||
visuals: 'Vizuali',
|
visuals: 'Vizuali',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Područja',
|
areas: 'Područja',
|
||||||
|
notes: 'Bilješke',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Filtriraj',
|
||||||
|
add_note: 'Dodaj Bilješku',
|
||||||
|
no_results: 'Nije pronađena nijedna bilješka',
|
||||||
|
clear: 'Očisti Filter',
|
||||||
|
empty_state: {
|
||||||
|
title: 'Nema Bilješki',
|
||||||
|
description:
|
||||||
|
'Kreirajte bilješku za dodavanje tekstualnih napomena na platnu',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Prazna bilješka',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Akcije Bilješke',
|
||||||
|
edit_content: 'Uredi Sadržaj',
|
||||||
|
delete_note: 'Obriši Bilješku',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -486,6 +506,7 @@ export const hr: LanguageTranslation = {
|
|||||||
new_view: 'Novi Pogled',
|
new_view: 'Novi Pogled',
|
||||||
new_relationship: 'Nova veza',
|
new_relationship: 'Nova veza',
|
||||||
new_area: 'Novo područje',
|
new_area: 'Novo područje',
|
||||||
|
new_note: 'Nova Bilješka',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const id_ID: LanguageTranslation = {
|
|||||||
browse: 'Jelajahi',
|
browse: 'Jelajahi',
|
||||||
tables: 'Tabel',
|
tables: 'Tabel',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'Area',
|
|
||||||
dependencies: 'Ketergantungan',
|
dependencies: 'Ketergantungan',
|
||||||
custom_types: 'Tipe Kustom',
|
custom_types: 'Tipe Kustom',
|
||||||
visuals: 'Visual',
|
visuals: 'Visual',
|
||||||
@@ -238,6 +237,27 @@ export const id_ID: LanguageTranslation = {
|
|||||||
visuals: 'Visual',
|
visuals: 'Visual',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'Catatan',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Filter',
|
||||||
|
add_note: 'Tambah Catatan',
|
||||||
|
no_results: 'Tidak ada catatan ditemukan',
|
||||||
|
clear: 'Hapus Filter',
|
||||||
|
empty_state: {
|
||||||
|
title: 'Tidak Ada Catatan',
|
||||||
|
description:
|
||||||
|
'Buat catatan untuk menambahkan anotasi teks di kanvas',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Catatan kosong',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Aksi Catatan',
|
||||||
|
edit_content: 'Edit Konten',
|
||||||
|
delete_note: 'Hapus Catatan',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -493,6 +513,7 @@ export const id_ID: LanguageTranslation = {
|
|||||||
new_relationship: 'Hubungan Baru',
|
new_relationship: 'Hubungan Baru',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'Catatan Baru',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const ja: LanguageTranslation = {
|
|||||||
browse: '参照',
|
browse: '参照',
|
||||||
tables: 'テーブル',
|
tables: 'テーブル',
|
||||||
refs: '参照',
|
refs: '参照',
|
||||||
areas: 'エリア',
|
|
||||||
dependencies: '依存関係',
|
dependencies: '依存関係',
|
||||||
custom_types: 'カスタムタイプ',
|
custom_types: 'カスタムタイプ',
|
||||||
visuals: 'ビジュアル',
|
visuals: 'ビジュアル',
|
||||||
@@ -243,6 +242,27 @@ export const ja: LanguageTranslation = {
|
|||||||
visuals: 'ビジュアル',
|
visuals: 'ビジュアル',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'ノート',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'フィルター',
|
||||||
|
add_note: 'ノートを追加',
|
||||||
|
no_results: 'ノートが見つかりません',
|
||||||
|
clear: 'フィルターをクリア',
|
||||||
|
empty_state: {
|
||||||
|
title: 'ノートがありません',
|
||||||
|
description:
|
||||||
|
'キャンバス上にテキスト注釈を追加するためのノートを作成',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: '空のノート',
|
||||||
|
note_actions: {
|
||||||
|
title: 'ノートアクション',
|
||||||
|
edit_content: 'コンテンツを編集',
|
||||||
|
delete_note: 'ノートを削除',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -498,6 +518,7 @@ export const ja: LanguageTranslation = {
|
|||||||
new_relationship: '新しいリレーションシップ',
|
new_relationship: '新しいリレーションシップ',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: '新しいメモ',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
browse: '찾아보기',
|
browse: '찾아보기',
|
||||||
tables: '테이블',
|
tables: '테이블',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: '영역',
|
|
||||||
dependencies: '종속성',
|
dependencies: '종속성',
|
||||||
custom_types: '사용자 지정 타입',
|
custom_types: '사용자 지정 타입',
|
||||||
visuals: '시각화',
|
visuals: '시각화',
|
||||||
@@ -238,6 +237,27 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
visuals: '시각화',
|
visuals: '시각화',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: '메모',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: '필터',
|
||||||
|
add_note: '메모 추가',
|
||||||
|
no_results: '메모를 찾을 수 없습니다',
|
||||||
|
clear: '필터 지우기',
|
||||||
|
empty_state: {
|
||||||
|
title: '메모 없음',
|
||||||
|
description:
|
||||||
|
'캔버스에 텍스트 주석을 추가하려면 메모를 만드세요',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: '빈 메모',
|
||||||
|
note_actions: {
|
||||||
|
title: '메모 작업',
|
||||||
|
edit_content: '내용 편집',
|
||||||
|
delete_note: '메모 삭제',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -490,6 +510,7 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
new_relationship: '새 연관관계',
|
new_relationship: '새 연관관계',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: '새 메모',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const mr: LanguageTranslation = {
|
|||||||
browse: 'ब्राउज',
|
browse: 'ब्राउज',
|
||||||
tables: 'टेबल',
|
tables: 'टेबल',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'क्षेत्रे',
|
|
||||||
dependencies: 'अवलंबने',
|
dependencies: 'अवलंबने',
|
||||||
custom_types: 'कस्टम प्रकार',
|
custom_types: 'कस्टम प्रकार',
|
||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
@@ -242,6 +241,27 @@ export const mr: LanguageTranslation = {
|
|||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'नोट्स',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'फिल्टर',
|
||||||
|
add_note: 'नोट जोडा',
|
||||||
|
no_results: 'कोणत्याही नोट्स सापडल्या नाहीत',
|
||||||
|
clear: 'फिल्टर साफ करा',
|
||||||
|
empty_state: {
|
||||||
|
title: 'नोट्स नाहीत',
|
||||||
|
description:
|
||||||
|
'कॅनव्हासवर मजकूर भाष्य जोडण्यासाठी एक नोट तयार करा',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'रिकामी नोट',
|
||||||
|
note_actions: {
|
||||||
|
title: 'नोट क्रिया',
|
||||||
|
edit_content: 'सामग्री संपादित करा',
|
||||||
|
delete_note: 'नोट हटवा',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -502,6 +522,7 @@ export const mr: LanguageTranslation = {
|
|||||||
new_relationship: 'नवीन रिलेशनशिप',
|
new_relationship: 'नवीन रिलेशनशिप',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'नवीन टीप',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const ne: LanguageTranslation = {
|
|||||||
browse: 'ब्राउज',
|
browse: 'ब्राउज',
|
||||||
tables: 'टेबलहरू',
|
tables: 'टेबलहरू',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'क्षेत्रहरू',
|
|
||||||
dependencies: 'निर्भरताहरू',
|
dependencies: 'निर्भरताहरू',
|
||||||
custom_types: 'कस्टम प्रकारहरू',
|
custom_types: 'कस्टम प्रकारहरू',
|
||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
@@ -239,6 +238,27 @@ export const ne: LanguageTranslation = {
|
|||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'टिप्पणीहरू',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'फिल्टर',
|
||||||
|
add_note: 'टिप्पणी थप्नुहोस्',
|
||||||
|
no_results: 'कुनै टिप्पणी फेला परेन',
|
||||||
|
clear: 'फिल्टर खाली गर्नुहोस्',
|
||||||
|
empty_state: {
|
||||||
|
title: 'कुनै टिप्पणी छैन',
|
||||||
|
description:
|
||||||
|
'क्यानभासमा पाठ टिप्पणी थप्न टिप्पणी सिर्जना गर्नुहोस्',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'खाली टिप्पणी',
|
||||||
|
note_actions: {
|
||||||
|
title: 'टिप्पणी कार्यहरू',
|
||||||
|
edit_content: 'सामग्री सम्पादन गर्नुहोस्',
|
||||||
|
delete_note: 'टिप्पणी मेटाउनुहोस्',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -496,6 +516,7 @@ export const ne: LanguageTranslation = {
|
|||||||
new_relationship: 'नयाँ सम्बन्ध',
|
new_relationship: 'नयाँ सम्बन्ध',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'नयाँ नोट',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
browse: 'Navegar',
|
browse: 'Navegar',
|
||||||
tables: 'Tabelas',
|
tables: 'Tabelas',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'Áreas',
|
|
||||||
dependencies: 'Dependências',
|
dependencies: 'Dependências',
|
||||||
custom_types: 'Tipos Personalizados',
|
custom_types: 'Tipos Personalizados',
|
||||||
visuals: 'Visuais',
|
visuals: 'Visuais',
|
||||||
@@ -239,6 +238,27 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
visuals: 'Visuais',
|
visuals: 'Visuais',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'Notas',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Filtrar',
|
||||||
|
add_note: 'Adicionar Nota',
|
||||||
|
no_results: 'Nenhuma nota encontrada',
|
||||||
|
clear: 'Limpar Filtro',
|
||||||
|
empty_state: {
|
||||||
|
title: 'Sem Notas',
|
||||||
|
description:
|
||||||
|
'Crie uma nota para adicionar anotações de texto na tela',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Nota vazia',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Ações de Nota',
|
||||||
|
edit_content: 'Editar Conteúdo',
|
||||||
|
delete_note: 'Excluir Nota',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -495,6 +515,7 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
new_relationship: 'Novo Relacionamento',
|
new_relationship: 'Novo Relacionamento',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'Nova Nota',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const ru: LanguageTranslation = {
|
|||||||
browse: 'Обзор',
|
browse: 'Обзор',
|
||||||
tables: 'Таблицы',
|
tables: 'Таблицы',
|
||||||
refs: 'Ссылки',
|
refs: 'Ссылки',
|
||||||
areas: 'Области',
|
|
||||||
dependencies: 'Зависимости',
|
dependencies: 'Зависимости',
|
||||||
custom_types: 'Пользовательские типы',
|
custom_types: 'Пользовательские типы',
|
||||||
visuals: 'Визуальные элементы',
|
visuals: 'Визуальные элементы',
|
||||||
@@ -236,6 +235,27 @@ export const ru: LanguageTranslation = {
|
|||||||
visuals: 'Визуальные элементы',
|
visuals: 'Визуальные элементы',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Области',
|
areas: 'Области',
|
||||||
|
notes: 'Заметки',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Фильтр',
|
||||||
|
add_note: 'Добавить Заметку',
|
||||||
|
no_results: 'Заметки не найдены',
|
||||||
|
clear: 'Очистить Фильтр',
|
||||||
|
empty_state: {
|
||||||
|
title: 'Нет Заметок',
|
||||||
|
description:
|
||||||
|
'Создайте заметку, чтобы добавить текстовые аннотации на холсте',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Пустая заметка',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Действия с Заметкой',
|
||||||
|
edit_content: 'Редактировать Содержимое',
|
||||||
|
delete_note: 'Удалить Заметку',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -490,6 +510,7 @@ export const ru: LanguageTranslation = {
|
|||||||
new_view: 'Новое представление',
|
new_view: 'Новое представление',
|
||||||
new_relationship: 'Создать отношение',
|
new_relationship: 'Создать отношение',
|
||||||
new_area: 'Новая область',
|
new_area: 'Новая область',
|
||||||
|
new_note: 'Новая Заметка',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const te: LanguageTranslation = {
|
|||||||
browse: 'బ్రాఉజ్',
|
browse: 'బ్రాఉజ్',
|
||||||
tables: 'టేబల్లు',
|
tables: 'టేబల్లు',
|
||||||
refs: 'సంబంధాలు',
|
refs: 'సంబంధాలు',
|
||||||
areas: 'ప్రదేశాలు',
|
|
||||||
dependencies: 'ఆధారతలు',
|
dependencies: 'ఆధారతలు',
|
||||||
custom_types: 'కస్టమ్ టైప్స్',
|
custom_types: 'కస్టమ్ టైప్స్',
|
||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
@@ -240,6 +239,27 @@ export const te: LanguageTranslation = {
|
|||||||
visuals: 'Visuals',
|
visuals: 'Visuals',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'గమనికలు',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'ఫిల్టర్',
|
||||||
|
add_note: 'గమనిక జోడించండి',
|
||||||
|
no_results: 'గమనికలు కనుగొనబడలేదు',
|
||||||
|
clear: 'ఫిల్టర్ను క్లియర్ చేయండి',
|
||||||
|
empty_state: {
|
||||||
|
title: 'గమనికలు లేవు',
|
||||||
|
description:
|
||||||
|
'కాన్వాస్పై టెక్స్ట్ ఉల్లేఖనలను జోడించడానికి ఒక గమనికను సృష్టించండి',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'ఖాళీ గమనిక',
|
||||||
|
note_actions: {
|
||||||
|
title: 'గమనిక చర్యలు',
|
||||||
|
edit_content: 'కంటెంట్ను సవరించండి',
|
||||||
|
delete_note: 'గమనికను తొలగించండి',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -499,6 +519,7 @@ export const te: LanguageTranslation = {
|
|||||||
new_relationship: 'కొత్త సంబంధం',
|
new_relationship: 'కొత్త సంబంధం',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'కొత్త నోట్',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const tr: LanguageTranslation = {
|
|||||||
browse: 'Gözat',
|
browse: 'Gözat',
|
||||||
tables: 'Tablolar',
|
tables: 'Tablolar',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'Alanlar',
|
|
||||||
dependencies: 'Bağımlılıklar',
|
dependencies: 'Bağımlılıklar',
|
||||||
custom_types: 'Özel Tipler',
|
custom_types: 'Özel Tipler',
|
||||||
visuals: 'Görseller',
|
visuals: 'Görseller',
|
||||||
@@ -239,6 +238,27 @@ export const tr: LanguageTranslation = {
|
|||||||
visuals: 'Görseller',
|
visuals: 'Görseller',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'Notlar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Filtrele',
|
||||||
|
add_note: 'Not Ekle',
|
||||||
|
no_results: 'Not bulunamadı',
|
||||||
|
clear: 'Filtreyi Temizle',
|
||||||
|
empty_state: {
|
||||||
|
title: 'Not Yok',
|
||||||
|
description:
|
||||||
|
'Tuval üzerinde metin açıklamaları eklemek için bir not oluşturun',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Boş not',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Not İşlemleri',
|
||||||
|
edit_content: 'İçeriği Düzenle',
|
||||||
|
delete_note: 'Notu Sil',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -484,6 +504,7 @@ export const tr: LanguageTranslation = {
|
|||||||
new_relationship: 'Yeni İlişki',
|
new_relationship: 'Yeni İlişki',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'Yeni Not',
|
||||||
},
|
},
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
edit_table: 'Tabloyu Düzenle',
|
edit_table: 'Tabloyu Düzenle',
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const uk: LanguageTranslation = {
|
|||||||
browse: 'Огляд',
|
browse: 'Огляд',
|
||||||
tables: 'Таблиці',
|
tables: 'Таблиці',
|
||||||
refs: 'Зв’язки',
|
refs: 'Зв’язки',
|
||||||
areas: 'Області',
|
|
||||||
dependencies: 'Залежності',
|
dependencies: 'Залежності',
|
||||||
custom_types: 'Користувацькі типи',
|
custom_types: 'Користувацькі типи',
|
||||||
visuals: 'Візуальні елементи',
|
visuals: 'Візуальні елементи',
|
||||||
@@ -237,6 +236,27 @@ export const uk: LanguageTranslation = {
|
|||||||
visuals: 'Візуальні елементи',
|
visuals: 'Візуальні елементи',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'Нотатки',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Фільтр',
|
||||||
|
add_note: 'Додати Нотатку',
|
||||||
|
no_results: 'Нотатки не знайдено',
|
||||||
|
clear: 'Очистити Фільтр',
|
||||||
|
empty_state: {
|
||||||
|
title: 'Немає Нотаток',
|
||||||
|
description:
|
||||||
|
'Створіть нотатку, щоб додати текстові анотації на полотні',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Порожня нотатка',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Дії з Нотаткою',
|
||||||
|
edit_content: 'Редагувати Вміст',
|
||||||
|
delete_note: 'Видалити Нотатку',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -490,6 +510,7 @@ export const uk: LanguageTranslation = {
|
|||||||
new_relationship: 'Новий звʼязок',
|
new_relationship: 'Новий звʼязок',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'Нова Нотатка',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const vi: LanguageTranslation = {
|
|||||||
browse: 'Duyệt',
|
browse: 'Duyệt',
|
||||||
tables: 'Bảng',
|
tables: 'Bảng',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: 'Khu vực',
|
|
||||||
dependencies: 'Phụ thuộc',
|
dependencies: 'Phụ thuộc',
|
||||||
custom_types: 'Kiểu tùy chỉnh',
|
custom_types: 'Kiểu tùy chỉnh',
|
||||||
visuals: 'Hình ảnh',
|
visuals: 'Hình ảnh',
|
||||||
@@ -238,6 +237,27 @@ export const vi: LanguageTranslation = {
|
|||||||
visuals: 'Hình ảnh',
|
visuals: 'Hình ảnh',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: 'Ghi chú',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: 'Lọc',
|
||||||
|
add_note: 'Thêm Ghi Chú',
|
||||||
|
no_results: 'Không tìm thấy ghi chú',
|
||||||
|
clear: 'Xóa Bộ Lọc',
|
||||||
|
empty_state: {
|
||||||
|
title: 'Không Có Ghi Chú',
|
||||||
|
description:
|
||||||
|
'Tạo ghi chú để thêm chú thích văn bản trên canvas',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: 'Ghi chú trống',
|
||||||
|
note_actions: {
|
||||||
|
title: 'Hành Động Ghi Chú',
|
||||||
|
edit_content: 'Chỉnh Sửa Nội Dung',
|
||||||
|
delete_note: 'Xóa Ghi Chú',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -491,6 +511,7 @@ export const vi: LanguageTranslation = {
|
|||||||
new_relationship: 'Tạo quan hệ mới',
|
new_relationship: 'Tạo quan hệ mới',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: 'Ghi Chú Mới',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
browse: '浏览',
|
browse: '浏览',
|
||||||
tables: '表',
|
tables: '表',
|
||||||
refs: '引用',
|
refs: '引用',
|
||||||
areas: '区域',
|
|
||||||
dependencies: '依赖关系',
|
dependencies: '依赖关系',
|
||||||
custom_types: '自定义类型',
|
custom_types: '自定义类型',
|
||||||
visuals: '视觉效果',
|
visuals: '视觉效果',
|
||||||
@@ -235,6 +234,26 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
visuals: '视觉效果',
|
visuals: '视觉效果',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: '笔记',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: '筛选',
|
||||||
|
add_note: '添加笔记',
|
||||||
|
no_results: '未找到笔记',
|
||||||
|
clear: '清除筛选',
|
||||||
|
empty_state: {
|
||||||
|
title: '没有笔记',
|
||||||
|
description: '创建笔记以在画布上添加文本注释',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: '空笔记',
|
||||||
|
note_actions: {
|
||||||
|
title: '笔记操作',
|
||||||
|
edit_content: '编辑内容',
|
||||||
|
delete_note: '删除笔记',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -486,6 +505,7 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
new_relationship: '新建关系',
|
new_relationship: '新建关系',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: '新笔记',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
browse: '瀏覽',
|
browse: '瀏覽',
|
||||||
tables: '表格',
|
tables: '表格',
|
||||||
refs: 'Refs',
|
refs: 'Refs',
|
||||||
areas: '區域',
|
|
||||||
dependencies: '相依性',
|
dependencies: '相依性',
|
||||||
custom_types: '自定義類型',
|
custom_types: '自定義類型',
|
||||||
visuals: '視覺效果',
|
visuals: '視覺效果',
|
||||||
@@ -235,6 +234,26 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
visuals: '視覺效果',
|
visuals: '視覺效果',
|
||||||
tabs: {
|
tabs: {
|
||||||
areas: 'Areas',
|
areas: 'Areas',
|
||||||
|
notes: '筆記',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notes_section: {
|
||||||
|
filter: '篩選',
|
||||||
|
add_note: '新增筆記',
|
||||||
|
no_results: '未找到筆記',
|
||||||
|
clear: '清除篩選',
|
||||||
|
empty_state: {
|
||||||
|
title: '沒有筆記',
|
||||||
|
description: '建立筆記以在畫布上新增文字註解',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
empty_note: '空白筆記',
|
||||||
|
note_actions: {
|
||||||
|
title: '筆記操作',
|
||||||
|
edit_content: '編輯內容',
|
||||||
|
delete_note: '刪除筆記',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -486,6 +505,7 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
new_relationship: '新建關聯',
|
new_relationship: '新建關聯',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
|
new_note: '新筆記',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { DBIndex } from './domain/db-index';
|
|||||||
import type { DBRelationship } from './domain/db-relationship';
|
import type { DBRelationship } from './domain/db-relationship';
|
||||||
import type { DBTable } from './domain/db-table';
|
import type { DBTable } from './domain/db-table';
|
||||||
import type { Diagram } from './domain/diagram';
|
import type { Diagram } from './domain/diagram';
|
||||||
|
import type { Note } from './domain/note';
|
||||||
import { generateId as defaultGenerateId } from './utils';
|
import { generateId as defaultGenerateId } from './utils';
|
||||||
|
|
||||||
const generateIdsMapFromTable = (
|
const generateIdsMapFromTable = (
|
||||||
@@ -49,6 +50,10 @@ const generateIdsMapFromDiagram = (
|
|||||||
idsMap.set(area.id, generateId());
|
idsMap.set(area.id, generateId());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
diagram.notes?.forEach((note) => {
|
||||||
|
idsMap.set(note.id, generateId());
|
||||||
|
});
|
||||||
|
|
||||||
diagram.customTypes?.forEach((customType) => {
|
diagram.customTypes?.forEach((customType) => {
|
||||||
idsMap.set(customType.id, generateId());
|
idsMap.set(customType.id, generateId());
|
||||||
});
|
});
|
||||||
@@ -218,6 +223,21 @@ export const cloneDiagram = (
|
|||||||
})
|
})
|
||||||
.filter((area): area is Area => area !== null) ?? [];
|
.filter((area): area is Area => area !== null) ?? [];
|
||||||
|
|
||||||
|
const notes: Note[] =
|
||||||
|
diagram.notes
|
||||||
|
?.map((note) => {
|
||||||
|
const id = getNewId(note.id);
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...note,
|
||||||
|
id,
|
||||||
|
} satisfies Note;
|
||||||
|
})
|
||||||
|
.filter((note): note is Note => note !== null) ?? [];
|
||||||
|
|
||||||
const customTypes: DBCustomType[] =
|
const customTypes: DBCustomType[] =
|
||||||
diagram.customTypes
|
diagram.customTypes
|
||||||
?.map((customType) => {
|
?.map((customType) => {
|
||||||
@@ -242,6 +262,7 @@ export const cloneDiagram = (
|
|||||||
relationships,
|
relationships,
|
||||||
tables,
|
tables,
|
||||||
areas,
|
areas,
|
||||||
|
notes,
|
||||||
customTypes,
|
customTypes,
|
||||||
createdAt: diagram.createdAt
|
createdAt: diagram.createdAt
|
||||||
? new Date(diagram.createdAt)
|
? new Date(diagram.createdAt)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { dbTableSchema } from './db-table';
|
|||||||
import { areaSchema, type Area } from './area';
|
import { areaSchema, type Area } from './area';
|
||||||
import type { DBCustomType } from './db-custom-type';
|
import type { DBCustomType } from './db-custom-type';
|
||||||
import { dbCustomTypeSchema } from './db-custom-type';
|
import { dbCustomTypeSchema } from './db-custom-type';
|
||||||
|
import type { Note } from './note';
|
||||||
|
import { noteSchema } from './note';
|
||||||
|
|
||||||
export interface Diagram {
|
export interface Diagram {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -21,6 +23,7 @@ export interface Diagram {
|
|||||||
dependencies?: DBDependency[];
|
dependencies?: DBDependency[];
|
||||||
areas?: Area[];
|
areas?: Area[];
|
||||||
customTypes?: DBCustomType[];
|
customTypes?: DBCustomType[];
|
||||||
|
notes?: Note[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -35,6 +38,7 @@ export const diagramSchema: z.ZodType<Diagram> = z.object({
|
|||||||
dependencies: z.array(dbDependencySchema).optional(),
|
dependencies: z.array(dbDependencySchema).optional(),
|
||||||
areas: z.array(areaSchema).optional(),
|
areas: z.array(areaSchema).optional(),
|
||||||
customTypes: z.array(dbCustomTypeSchema).optional(),
|
customTypes: z.array(dbCustomTypeSchema).optional(),
|
||||||
|
notes: z.array(noteSchema).optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import type { DBField } from '@/lib/domain/db-field';
|
|||||||
import type { DBIndex } from '@/lib/domain/db-index';
|
import type { DBIndex } from '@/lib/domain/db-index';
|
||||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import type { TableDiffChanged } from '../../table-diff';
|
import type { TableDiffChanged } from '../../table-diff';
|
||||||
import type { FieldDiffChanged } from '../../field-diff';
|
import type { FieldDiffChanged } from '../../field-diff';
|
||||||
import type { AreaDiffChanged } from '../../area-diff';
|
import type { AreaDiffChanged } from '../../area-diff';
|
||||||
|
import type { NoteDiffChanged } from '../../note-diff';
|
||||||
|
|
||||||
// Helper function to create a mock diagram
|
// Helper function to create a mock diagram
|
||||||
function createMockDiagram(overrides?: Partial<Diagram>): Diagram {
|
function createMockDiagram(overrides?: Partial<Diagram>): Diagram {
|
||||||
@@ -81,6 +83,20 @@ function createMockArea(overrides?: Partial<Area>): Area {
|
|||||||
} as Area;
|
} as Area;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to create a mock note
|
||||||
|
function createMockNote(overrides?: Partial<Note>): Note {
|
||||||
|
return {
|
||||||
|
id: 'note-1',
|
||||||
|
content: 'Test note content',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 200,
|
||||||
|
height: 150,
|
||||||
|
color: '#3b82f6',
|
||||||
|
...overrides,
|
||||||
|
} as Note;
|
||||||
|
}
|
||||||
|
|
||||||
describe('generateDiff', () => {
|
describe('generateDiff', () => {
|
||||||
describe('Basic Table Diffing', () => {
|
describe('Basic Table Diffing', () => {
|
||||||
it('should detect added tables', () => {
|
it('should detect added tables', () => {
|
||||||
@@ -466,6 +482,408 @@ describe('generateDiff', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Note Diffing', () => {
|
||||||
|
it('should detect added notes when includeNotes is true', () => {
|
||||||
|
const oldDiagram = createMockDiagram({ notes: [] });
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.diffMap.size).toBe(1);
|
||||||
|
const diff = result.diffMap.get('note-note-1');
|
||||||
|
expect(diff).toBeDefined();
|
||||||
|
expect(diff?.type).toBe('added');
|
||||||
|
expect(result.changedNotes.has('note-1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not detect note changes when includeNotes is false', () => {
|
||||||
|
const oldDiagram = createMockDiagram({ notes: [] });
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.diffMap.size).toBe(0);
|
||||||
|
expect(result.changedNotes.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect removed notes', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote()],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({ notes: [] });
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.diffMap.size).toBe(1);
|
||||||
|
const diff = result.diffMap.get('note-note-1');
|
||||||
|
expect(diff).toBeDefined();
|
||||||
|
expect(diff?.type).toBe('removed');
|
||||||
|
expect(result.changedNotes.has('note-1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect note content changes', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ content: 'Old content' })],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ content: 'New content' })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.diffMap.size).toBe(1);
|
||||||
|
const diff = result.diffMap.get('note-content-note-1');
|
||||||
|
expect(diff).toBeDefined();
|
||||||
|
expect(diff?.type).toBe('changed');
|
||||||
|
expect((diff as NoteDiffChanged)?.attribute).toBe('content');
|
||||||
|
expect((diff as NoteDiffChanged)?.oldValue).toBe('Old content');
|
||||||
|
expect((diff as NoteDiffChanged)?.newValue).toBe('New content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect note color changes', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ color: '#3b82f6' })],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ color: '#ef4444' })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.diffMap.size).toBe(1);
|
||||||
|
const diff = result.diffMap.get('note-color-note-1');
|
||||||
|
expect(diff).toBeDefined();
|
||||||
|
expect(diff?.type).toBe('changed');
|
||||||
|
expect((diff as NoteDiffChanged)?.attribute).toBe('color');
|
||||||
|
expect((diff as NoteDiffChanged)?.oldValue).toBe('#3b82f6');
|
||||||
|
expect((diff as NoteDiffChanged)?.newValue).toBe('#ef4444');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect note position changes', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ x: 0, y: 0 })],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ x: 100, y: 200 })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
attributes: {
|
||||||
|
notes: ['content', 'color', 'x', 'y'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.diffMap.size).toBe(2);
|
||||||
|
expect(result.diffMap.has('note-x-note-1')).toBe(true);
|
||||||
|
expect(result.diffMap.has('note-y-note-1')).toBe(true);
|
||||||
|
|
||||||
|
const xDiff = result.diffMap.get('note-x-note-1');
|
||||||
|
expect((xDiff as NoteDiffChanged)?.oldValue).toBe(0);
|
||||||
|
expect((xDiff as NoteDiffChanged)?.newValue).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect note width changes', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ width: 200 })],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ width: 300 })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
attributes: {
|
||||||
|
notes: ['width'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.diffMap.size).toBe(1);
|
||||||
|
const diff = result.diffMap.get('note-width-note-1');
|
||||||
|
expect(diff).toBeDefined();
|
||||||
|
expect(diff?.type).toBe('changed');
|
||||||
|
expect((diff as NoteDiffChanged)?.attribute).toBe('width');
|
||||||
|
expect((diff as NoteDiffChanged)?.oldValue).toBe(200);
|
||||||
|
expect((diff as NoteDiffChanged)?.newValue).toBe(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect note height changes', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ height: 150 })],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ height: 250 })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
attributes: {
|
||||||
|
notes: ['height'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.diffMap.size).toBe(1);
|
||||||
|
const diff = result.diffMap.get('note-height-note-1');
|
||||||
|
expect(diff).toBeDefined();
|
||||||
|
expect(diff?.type).toBe('changed');
|
||||||
|
expect((diff as NoteDiffChanged)?.attribute).toBe('height');
|
||||||
|
expect((diff as NoteDiffChanged)?.oldValue).toBe(150);
|
||||||
|
expect((diff as NoteDiffChanged)?.newValue).toBe(250);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect multiple note dimension changes', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [
|
||||||
|
createMockNote({ x: 0, y: 0, width: 200, height: 150 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [
|
||||||
|
createMockNote({ x: 50, y: 75, width: 300, height: 250 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
attributes: {
|
||||||
|
notes: ['x', 'y', 'width', 'height'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.diffMap.size).toBe(4);
|
||||||
|
expect(result.diffMap.has('note-x-note-1')).toBe(true);
|
||||||
|
expect(result.diffMap.has('note-y-note-1')).toBe(true);
|
||||||
|
expect(result.diffMap.has('note-width-note-1')).toBe(true);
|
||||||
|
expect(result.diffMap.has('note-height-note-1')).toBe(true);
|
||||||
|
|
||||||
|
const widthDiff = result.diffMap.get('note-width-note-1');
|
||||||
|
expect((widthDiff as NoteDiffChanged)?.oldValue).toBe(200);
|
||||||
|
expect((widthDiff as NoteDiffChanged)?.newValue).toBe(300);
|
||||||
|
|
||||||
|
const heightDiff = result.diffMap.get('note-height-note-1');
|
||||||
|
expect((heightDiff as NoteDiffChanged)?.oldValue).toBe(150);
|
||||||
|
expect((heightDiff as NoteDiffChanged)?.newValue).toBe(250);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect multiple notes with different changes', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [
|
||||||
|
createMockNote({ id: 'note-1', content: 'Note 1' }),
|
||||||
|
createMockNote({ id: 'note-2', content: 'Note 2' }),
|
||||||
|
createMockNote({ id: 'note-3', content: 'Note 3' }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [
|
||||||
|
createMockNote({
|
||||||
|
id: 'note-1',
|
||||||
|
content: 'Note 1 Updated',
|
||||||
|
}), // Changed
|
||||||
|
createMockNote({ id: 'note-2', content: 'Note 2' }), // Unchanged
|
||||||
|
// note-3 removed
|
||||||
|
createMockNote({ id: 'note-4', content: 'Note 4' }), // Added
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should detect: 1 content change, 1 removal, 1 addition
|
||||||
|
expect(result.diffMap.has('note-content-note-1')).toBe(true); // Changed
|
||||||
|
expect(result.diffMap.has('note-note-3')).toBe(true); // Removed
|
||||||
|
expect(result.diffMap.has('note-note-4')).toBe(true); // Added
|
||||||
|
|
||||||
|
expect(result.changedNotes.has('note-1')).toBe(true);
|
||||||
|
expect(result.changedNotes.has('note-3')).toBe(true);
|
||||||
|
expect(result.changedNotes.has('note-4')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use custom note matcher', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [
|
||||||
|
createMockNote({
|
||||||
|
id: 'note-1',
|
||||||
|
content: 'Unique content',
|
||||||
|
color: '#3b82f6',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [
|
||||||
|
createMockNote({
|
||||||
|
id: 'note-2',
|
||||||
|
content: 'Unique content',
|
||||||
|
color: '#ef4444',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
matchers: {
|
||||||
|
note: (note, notes) =>
|
||||||
|
notes.find((n) => n.content === note.content),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// With content-based matching, note-1 should match note-2 by content
|
||||||
|
// and detect the color change
|
||||||
|
const colorChange = result.diffMap.get('note-color-note-1');
|
||||||
|
expect(colorChange).toBeDefined();
|
||||||
|
expect(colorChange?.type).toBe('changed');
|
||||||
|
expect((colorChange as NoteDiffChanged)?.attribute).toBe('color');
|
||||||
|
expect((colorChange as NoteDiffChanged)?.oldValue).toBe('#3b82f6');
|
||||||
|
expect((colorChange as NoteDiffChanged)?.newValue).toBe('#ef4444');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only check specified note change types', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ id: 'note-1', content: 'Note 1' })],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [createMockNote({ id: 'note-2', content: 'Note 2' })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
changeTypes: {
|
||||||
|
notes: ['added'], // Only check for added notes
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should only detect added note (note-2)
|
||||||
|
const addedNotes = Array.from(result.diffMap.values()).filter(
|
||||||
|
(diff) => diff.type === 'added' && diff.object === 'note'
|
||||||
|
);
|
||||||
|
expect(addedNotes.length).toBe(1);
|
||||||
|
|
||||||
|
// Should not detect removed note (note-1)
|
||||||
|
const removedNotes = Array.from(result.diffMap.values()).filter(
|
||||||
|
(diff) => diff.type === 'removed' && diff.object === 'note'
|
||||||
|
);
|
||||||
|
expect(removedNotes.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only check specified note attributes', () => {
|
||||||
|
const oldDiagram = createMockDiagram({
|
||||||
|
notes: [
|
||||||
|
createMockNote({
|
||||||
|
id: 'note-1',
|
||||||
|
content: 'Old content',
|
||||||
|
color: '#3b82f6',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const newDiagram = createMockDiagram({
|
||||||
|
notes: [
|
||||||
|
createMockNote({
|
||||||
|
id: 'note-1',
|
||||||
|
content: 'New content',
|
||||||
|
color: '#ef4444',
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = generateDiff({
|
||||||
|
diagram: oldDiagram,
|
||||||
|
newDiagram,
|
||||||
|
options: {
|
||||||
|
includeNotes: true,
|
||||||
|
attributes: {
|
||||||
|
notes: ['content'], // Only check content changes
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should only detect content change
|
||||||
|
const contentChanges = Array.from(result.diffMap.values()).filter(
|
||||||
|
(diff) =>
|
||||||
|
diff.type === 'changed' &&
|
||||||
|
diff.attribute === 'content' &&
|
||||||
|
diff.object === 'note'
|
||||||
|
);
|
||||||
|
expect(contentChanges.length).toBe(1);
|
||||||
|
|
||||||
|
// Should not detect color or position changes
|
||||||
|
const otherChanges = Array.from(result.diffMap.values()).filter(
|
||||||
|
(diff) =>
|
||||||
|
diff.type === 'changed' &&
|
||||||
|
(diff.attribute === 'color' ||
|
||||||
|
diff.attribute === 'x' ||
|
||||||
|
diff.attribute === 'y') &&
|
||||||
|
diff.object === 'note'
|
||||||
|
);
|
||||||
|
expect(otherChanges.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Custom Matchers', () => {
|
describe('Custom Matchers', () => {
|
||||||
it('should use custom table matcher to match by name', () => {
|
it('should use custom table matcher to match by name', () => {
|
||||||
const oldDiagram = createMockDiagram({
|
const oldDiagram = createMockDiagram({
|
||||||
@@ -708,7 +1126,7 @@ describe('generateDiff', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Complex Scenarios', () => {
|
describe('Complex Scenarios', () => {
|
||||||
it('should detect all dimensional changes for tables and areas', () => {
|
it('should detect all dimensional changes for tables, areas, and notes', () => {
|
||||||
const oldDiagram = createMockDiagram({
|
const oldDiagram = createMockDiagram({
|
||||||
tables: [
|
tables: [
|
||||||
createMockTable({
|
createMockTable({
|
||||||
@@ -727,6 +1145,15 @@ describe('generateDiff', () => {
|
|||||||
height: 150,
|
height: 150,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
notes: [
|
||||||
|
createMockNote({
|
||||||
|
id: 'note-1',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 200,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const newDiagram = createMockDiagram({
|
const newDiagram = createMockDiagram({
|
||||||
@@ -747,6 +1174,15 @@ describe('generateDiff', () => {
|
|||||||
height: 175,
|
height: 175,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
notes: [
|
||||||
|
createMockNote({
|
||||||
|
id: 'note-1',
|
||||||
|
x: 40,
|
||||||
|
y: 50,
|
||||||
|
width: 350,
|
||||||
|
height: 225,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = generateDiff({
|
const result = generateDiff({
|
||||||
@@ -754,9 +1190,11 @@ describe('generateDiff', () => {
|
|||||||
newDiagram,
|
newDiagram,
|
||||||
options: {
|
options: {
|
||||||
includeAreas: true,
|
includeAreas: true,
|
||||||
|
includeNotes: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
tables: ['x', 'y', 'width'],
|
tables: ['x', 'y', 'width'],
|
||||||
areas: ['x', 'y', 'width', 'height'],
|
areas: ['x', 'y', 'width', 'height'],
|
||||||
|
notes: ['x', 'y', 'width', 'height'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -772,6 +1210,12 @@ describe('generateDiff', () => {
|
|||||||
expect(result.diffMap.has('area-width-area-1')).toBe(true);
|
expect(result.diffMap.has('area-width-area-1')).toBe(true);
|
||||||
expect(result.diffMap.has('area-height-area-1')).toBe(true);
|
expect(result.diffMap.has('area-height-area-1')).toBe(true);
|
||||||
|
|
||||||
|
// Note dimensional changes
|
||||||
|
expect(result.diffMap.has('note-x-note-1')).toBe(true);
|
||||||
|
expect(result.diffMap.has('note-y-note-1')).toBe(true);
|
||||||
|
expect(result.diffMap.has('note-width-note-1')).toBe(true);
|
||||||
|
expect(result.diffMap.has('note-height-note-1')).toBe(true);
|
||||||
|
|
||||||
// Verify the correct values
|
// Verify the correct values
|
||||||
const tableWidthDiff = result.diffMap.get('table-width-table-1');
|
const tableWidthDiff = result.diffMap.get('table-width-table-1');
|
||||||
expect((tableWidthDiff as TableDiffChanged)?.oldValue).toBe(100);
|
expect((tableWidthDiff as TableDiffChanged)?.oldValue).toBe(100);
|
||||||
@@ -784,6 +1228,14 @@ describe('generateDiff', () => {
|
|||||||
const areaHeightDiff = result.diffMap.get('area-height-area-1');
|
const areaHeightDiff = result.diffMap.get('area-height-area-1');
|
||||||
expect((areaHeightDiff as AreaDiffChanged)?.oldValue).toBe(150);
|
expect((areaHeightDiff as AreaDiffChanged)?.oldValue).toBe(150);
|
||||||
expect((areaHeightDiff as AreaDiffChanged)?.newValue).toBe(175);
|
expect((areaHeightDiff as AreaDiffChanged)?.newValue).toBe(175);
|
||||||
|
|
||||||
|
const noteWidthDiff = result.diffMap.get('note-width-note-1');
|
||||||
|
expect((noteWidthDiff as NoteDiffChanged)?.oldValue).toBe(300);
|
||||||
|
expect((noteWidthDiff as NoteDiffChanged)?.newValue).toBe(350);
|
||||||
|
|
||||||
|
const noteHeightDiff = result.diffMap.get('note-height-note-1');
|
||||||
|
expect((noteHeightDiff as NoteDiffChanged)?.oldValue).toBe(200);
|
||||||
|
expect((noteHeightDiff as NoteDiffChanged)?.newValue).toBe(225);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle multiple simultaneous changes', () => {
|
it('should handle multiple simultaneous changes', () => {
|
||||||
@@ -852,6 +1304,7 @@ describe('generateDiff', () => {
|
|||||||
expect(result.changedTables.size).toBe(0);
|
expect(result.changedTables.size).toBe(0);
|
||||||
expect(result.changedFields.size).toBe(0);
|
expect(result.changedFields.size).toBe(0);
|
||||||
expect(result.changedAreas.size).toBe(0);
|
expect(result.changedAreas.size).toBe(0);
|
||||||
|
expect(result.changedNotes.size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle diagrams with undefined collections', () => {
|
it('should handle diagrams with undefined collections', () => {
|
||||||
@@ -859,11 +1312,13 @@ describe('generateDiff', () => {
|
|||||||
tables: undefined,
|
tables: undefined,
|
||||||
relationships: undefined,
|
relationships: undefined,
|
||||||
areas: undefined,
|
areas: undefined,
|
||||||
|
notes: undefined,
|
||||||
});
|
});
|
||||||
const diagram2 = createMockDiagram({
|
const diagram2 = createMockDiagram({
|
||||||
tables: [createMockTable({ id: 'table-1' })],
|
tables: [createMockTable({ id: 'table-1' })],
|
||||||
relationships: [createMockRelationship({ id: 'rel-1' })],
|
relationships: [createMockRelationship({ id: 'rel-1' })],
|
||||||
areas: [createMockArea({ id: 'area-1' })],
|
areas: [createMockArea({ id: 'area-1' })],
|
||||||
|
notes: [createMockNote({ id: 'note-1' })],
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = generateDiff({
|
const result = generateDiff({
|
||||||
@@ -871,6 +1326,7 @@ describe('generateDiff', () => {
|
|||||||
newDiagram: diagram2,
|
newDiagram: diagram2,
|
||||||
options: {
|
options: {
|
||||||
includeAreas: true,
|
includeAreas: true,
|
||||||
|
includeNotes: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -878,6 +1334,7 @@ describe('generateDiff', () => {
|
|||||||
expect(result.diffMap.has('table-table-1')).toBe(true);
|
expect(result.diffMap.has('table-table-1')).toBe(true);
|
||||||
expect(result.diffMap.has('relationship-rel-1')).toBe(true);
|
expect(result.diffMap.has('relationship-rel-1')).toBe(true);
|
||||||
expect(result.diffMap.has('area-area-1')).toBe(true);
|
expect(result.diffMap.has('area-area-1')).toBe(true);
|
||||||
|
expect(result.diffMap.has('note-note-1')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { DBIndex } from '@/lib/domain/db-index';
|
|||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
import type { ChartDBDiff, DiffMap, DiffObject } from '@/lib/domain/diff/diff';
|
import type { ChartDBDiff, DiffMap, DiffObject } from '@/lib/domain/diff/diff';
|
||||||
import type {
|
import type {
|
||||||
FieldDiff,
|
FieldDiff,
|
||||||
@@ -11,6 +12,7 @@ import type {
|
|||||||
} from '@/lib/domain/diff/field-diff';
|
} from '@/lib/domain/diff/field-diff';
|
||||||
import type { TableDiff, TableDiffAttribute } from '../table-diff';
|
import type { TableDiff, TableDiffAttribute } from '../table-diff';
|
||||||
import type { AreaDiff, AreaDiffAttribute } from '../area-diff';
|
import type { AreaDiff, AreaDiffAttribute } from '../area-diff';
|
||||||
|
import type { NoteDiff, NoteDiffAttribute } from '../note-diff';
|
||||||
import type { IndexDiff } from '../index-diff';
|
import type { IndexDiff } from '../index-diff';
|
||||||
import type { RelationshipDiff } from '../relationship-diff';
|
import type { RelationshipDiff } from '../relationship-diff';
|
||||||
|
|
||||||
@@ -44,10 +46,12 @@ export interface GenerateDiffOptions {
|
|||||||
includeIndexes?: boolean;
|
includeIndexes?: boolean;
|
||||||
includeRelationships?: boolean;
|
includeRelationships?: boolean;
|
||||||
includeAreas?: boolean;
|
includeAreas?: boolean;
|
||||||
|
includeNotes?: boolean;
|
||||||
attributes?: {
|
attributes?: {
|
||||||
tables?: TableDiffAttribute[];
|
tables?: TableDiffAttribute[];
|
||||||
fields?: FieldDiffAttribute[];
|
fields?: FieldDiffAttribute[];
|
||||||
areas?: AreaDiffAttribute[];
|
areas?: AreaDiffAttribute[];
|
||||||
|
notes?: NoteDiffAttribute[];
|
||||||
};
|
};
|
||||||
changeTypes?: {
|
changeTypes?: {
|
||||||
tables?: TableDiff['type'][];
|
tables?: TableDiff['type'][];
|
||||||
@@ -55,6 +59,7 @@ export interface GenerateDiffOptions {
|
|||||||
indexes?: IndexDiff['type'][];
|
indexes?: IndexDiff['type'][];
|
||||||
relationships?: RelationshipDiff['type'][];
|
relationships?: RelationshipDiff['type'][];
|
||||||
areas?: AreaDiff['type'][];
|
areas?: AreaDiff['type'][];
|
||||||
|
notes?: NoteDiff['type'][];
|
||||||
};
|
};
|
||||||
matchers?: {
|
matchers?: {
|
||||||
table?: (table: DBTable, tables: DBTable[]) => DBTable | undefined;
|
table?: (table: DBTable, tables: DBTable[]) => DBTable | undefined;
|
||||||
@@ -65,6 +70,7 @@ export interface GenerateDiffOptions {
|
|||||||
relationships: DBRelationship[]
|
relationships: DBRelationship[]
|
||||||
) => DBRelationship | undefined;
|
) => DBRelationship | undefined;
|
||||||
area?: (area: Area, areas: Area[]) => Area | undefined;
|
area?: (area: Area, areas: Area[]) => Area | undefined;
|
||||||
|
note?: (note: Note, notes: Note[]) => Note | undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +87,7 @@ export function generateDiff({
|
|||||||
changedTables: Map<string, boolean>;
|
changedTables: Map<string, boolean>;
|
||||||
changedFields: Map<string, boolean>;
|
changedFields: Map<string, boolean>;
|
||||||
changedAreas: Map<string, boolean>;
|
changedAreas: Map<string, boolean>;
|
||||||
|
changedNotes: Map<string, boolean>;
|
||||||
} {
|
} {
|
||||||
// Merge with default options
|
// Merge with default options
|
||||||
const mergedOptions: GenerateDiffOptions = {
|
const mergedOptions: GenerateDiffOptions = {
|
||||||
@@ -89,6 +96,7 @@ export function generateDiff({
|
|||||||
includeIndexes: options.includeIndexes ?? true,
|
includeIndexes: options.includeIndexes ?? true,
|
||||||
includeRelationships: options.includeRelationships ?? true,
|
includeRelationships: options.includeRelationships ?? true,
|
||||||
includeAreas: options.includeAreas ?? false,
|
includeAreas: options.includeAreas ?? false,
|
||||||
|
includeNotes: options.includeNotes ?? false,
|
||||||
attributes: options.attributes ?? {},
|
attributes: options.attributes ?? {},
|
||||||
changeTypes: options.changeTypes ?? {},
|
changeTypes: options.changeTypes ?? {},
|
||||||
matchers: options.matchers ?? {},
|
matchers: options.matchers ?? {},
|
||||||
@@ -98,6 +106,7 @@ export function generateDiff({
|
|||||||
const changedTables = new Map<string, boolean>();
|
const changedTables = new Map<string, boolean>();
|
||||||
const changedFields = new Map<string, boolean>();
|
const changedFields = new Map<string, boolean>();
|
||||||
const changedAreas = new Map<string, boolean>();
|
const changedAreas = new Map<string, boolean>();
|
||||||
|
const changedNotes = new Map<string, boolean>();
|
||||||
|
|
||||||
// Use provided matchers or default ones
|
// Use provided matchers or default ones
|
||||||
const tableMatcher = mergedOptions.matchers?.table ?? defaultTableMatcher;
|
const tableMatcher = mergedOptions.matchers?.table ?? defaultTableMatcher;
|
||||||
@@ -106,6 +115,7 @@ export function generateDiff({
|
|||||||
const relationshipMatcher =
|
const relationshipMatcher =
|
||||||
mergedOptions.matchers?.relationship ?? defaultRelationshipMatcher;
|
mergedOptions.matchers?.relationship ?? defaultRelationshipMatcher;
|
||||||
const areaMatcher = mergedOptions.matchers?.area ?? defaultAreaMatcher;
|
const areaMatcher = mergedOptions.matchers?.area ?? defaultAreaMatcher;
|
||||||
|
const noteMatcher = mergedOptions.matchers?.note ?? defaultNoteMatcher;
|
||||||
|
|
||||||
// Compare tables
|
// Compare tables
|
||||||
if (mergedOptions.includeTables) {
|
if (mergedOptions.includeTables) {
|
||||||
@@ -157,7 +167,26 @@ export function generateDiff({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { diffMap: newDiffs, changedTables, changedFields, changedAreas };
|
// Compare notes if enabled
|
||||||
|
if (mergedOptions.includeNotes) {
|
||||||
|
compareNotes({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap: newDiffs,
|
||||||
|
changedNotes,
|
||||||
|
attributes: mergedOptions.attributes?.notes,
|
||||||
|
changeTypes: mergedOptions.changeTypes?.notes,
|
||||||
|
noteMatcher,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
diffMap: newDiffs,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
changedAreas,
|
||||||
|
changedNotes,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare tables between diagrams
|
// Compare tables between diagrams
|
||||||
@@ -1019,6 +1048,217 @@ function compareAreas({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare notes between diagrams
|
||||||
|
function compareNotes({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
changedNotes,
|
||||||
|
attributes,
|
||||||
|
changeTypes,
|
||||||
|
noteMatcher,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedNotes: Map<string, boolean>;
|
||||||
|
attributes?: NoteDiffAttribute[];
|
||||||
|
changeTypes?: NoteDiff['type'][];
|
||||||
|
noteMatcher: (note: Note, notes: Note[]) => Note | undefined;
|
||||||
|
}) {
|
||||||
|
const oldNotes = diagram.notes || [];
|
||||||
|
const newNotes = newDiagram.notes || [];
|
||||||
|
|
||||||
|
// If changeTypes is empty array, don't check any changes
|
||||||
|
if (changeTypes && changeTypes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If changeTypes is undefined, check all types
|
||||||
|
const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
|
||||||
|
|
||||||
|
// Check for added notes
|
||||||
|
if (typesToCheck.includes('added')) {
|
||||||
|
for (const newNote of newNotes) {
|
||||||
|
if (!noteMatcher(newNote, oldNotes)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'note',
|
||||||
|
objectId: newNote.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'note',
|
||||||
|
type: 'added',
|
||||||
|
noteAdded: newNote,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedNotes.set(newNote.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed notes
|
||||||
|
if (typesToCheck.includes('removed')) {
|
||||||
|
for (const oldNote of oldNotes) {
|
||||||
|
if (!noteMatcher(oldNote, newNotes)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'note',
|
||||||
|
objectId: oldNote.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'note',
|
||||||
|
type: 'removed',
|
||||||
|
noteId: oldNote.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedNotes.set(oldNote.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for note content and color changes
|
||||||
|
if (typesToCheck.includes('changed')) {
|
||||||
|
for (const oldNote of oldNotes) {
|
||||||
|
const newNote = noteMatcher(oldNote, newNotes);
|
||||||
|
|
||||||
|
if (!newNote) continue;
|
||||||
|
|
||||||
|
// If attributes are specified, only check those attributes
|
||||||
|
const attributesToCheck: NoteDiffAttribute[] = attributes ?? [
|
||||||
|
'content',
|
||||||
|
'color',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
attributesToCheck.includes('content') &&
|
||||||
|
oldNote.content !== newNote.content
|
||||||
|
) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'note',
|
||||||
|
objectId: oldNote.id,
|
||||||
|
attribute: 'content',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'note',
|
||||||
|
type: 'changed',
|
||||||
|
noteId: oldNote.id,
|
||||||
|
attribute: 'content',
|
||||||
|
newValue: newNote.content,
|
||||||
|
oldValue: oldNote.content,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedNotes.set(oldNote.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
attributesToCheck.includes('color') &&
|
||||||
|
oldNote.color !== newNote.color
|
||||||
|
) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'note',
|
||||||
|
objectId: oldNote.id,
|
||||||
|
attribute: 'color',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'note',
|
||||||
|
type: 'changed',
|
||||||
|
noteId: oldNote.id,
|
||||||
|
attribute: 'color',
|
||||||
|
newValue: newNote.color,
|
||||||
|
oldValue: oldNote.color,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedNotes.set(oldNote.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributesToCheck.includes('x') && oldNote.x !== newNote.x) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'note',
|
||||||
|
objectId: oldNote.id,
|
||||||
|
attribute: 'x',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'note',
|
||||||
|
type: 'changed',
|
||||||
|
noteId: oldNote.id,
|
||||||
|
attribute: 'x',
|
||||||
|
newValue: newNote.x,
|
||||||
|
oldValue: oldNote.x,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedNotes.set(oldNote.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributesToCheck.includes('y') && oldNote.y !== newNote.y) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'note',
|
||||||
|
objectId: oldNote.id,
|
||||||
|
attribute: 'y',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'note',
|
||||||
|
type: 'changed',
|
||||||
|
noteId: oldNote.id,
|
||||||
|
attribute: 'y',
|
||||||
|
newValue: newNote.y,
|
||||||
|
oldValue: oldNote.y,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedNotes.set(oldNote.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
attributesToCheck.includes('width') &&
|
||||||
|
oldNote.width !== newNote.width
|
||||||
|
) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'note',
|
||||||
|
objectId: oldNote.id,
|
||||||
|
attribute: 'width',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'note',
|
||||||
|
type: 'changed',
|
||||||
|
noteId: oldNote.id,
|
||||||
|
attribute: 'width',
|
||||||
|
newValue: newNote.width,
|
||||||
|
oldValue: oldNote.width,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedNotes.set(oldNote.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
attributesToCheck.includes('height') &&
|
||||||
|
oldNote.height !== newNote.height
|
||||||
|
) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'note',
|
||||||
|
objectId: oldNote.id,
|
||||||
|
attribute: 'height',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'note',
|
||||||
|
type: 'changed',
|
||||||
|
noteId: oldNote.id,
|
||||||
|
attribute: 'height',
|
||||||
|
newValue: newNote.height,
|
||||||
|
oldValue: oldNote.height,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedNotes.set(oldNote.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const defaultTableMatcher = (
|
const defaultTableMatcher = (
|
||||||
table: DBTable,
|
table: DBTable,
|
||||||
tables: DBTable[]
|
tables: DBTable[]
|
||||||
@@ -1050,3 +1290,7 @@ const defaultRelationshipMatcher = (
|
|||||||
const defaultAreaMatcher = (area: Area, areas: Area[]): Area | undefined => {
|
const defaultAreaMatcher = (area: Area, areas: Area[]): Area | undefined => {
|
||||||
return areas.find((a) => a.id === area.id);
|
return areas.find((a) => a.id === area.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultNoteMatcher = (note: Note, notes: Note[]): Note | undefined => {
|
||||||
|
return notes.find((n) => n.id === note.id);
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import type { TableDiff } from './table-diff';
|
|||||||
import { createTableDiffSchema } from './table-diff';
|
import { createTableDiffSchema } from './table-diff';
|
||||||
import type { AreaDiff } from './area-diff';
|
import type { AreaDiff } from './area-diff';
|
||||||
import { createAreaDiffSchema } from './area-diff';
|
import { createAreaDiffSchema } from './area-diff';
|
||||||
import type { DBField, DBIndex, DBRelationship, DBTable, Area } from '..';
|
import type { NoteDiff } from './note-diff';
|
||||||
|
import { createNoteDiffSchema } from './note-diff';
|
||||||
|
import type { DBField, DBIndex, DBRelationship, DBTable, Area, Note } from '..';
|
||||||
|
|
||||||
export type ChartDBDiff<
|
export type ChartDBDiff<
|
||||||
TTable = DBTable,
|
TTable = DBTable,
|
||||||
@@ -18,12 +20,14 @@ export type ChartDBDiff<
|
|||||||
TIndex = DBIndex,
|
TIndex = DBIndex,
|
||||||
TRelationship = DBRelationship,
|
TRelationship = DBRelationship,
|
||||||
TArea = Area,
|
TArea = Area,
|
||||||
|
TNote = Note,
|
||||||
> =
|
> =
|
||||||
| TableDiff<TTable>
|
| TableDiff<TTable>
|
||||||
| FieldDiff<TField>
|
| FieldDiff<TField>
|
||||||
| IndexDiff<TIndex>
|
| IndexDiff<TIndex>
|
||||||
| RelationshipDiff<TRelationship>
|
| RelationshipDiff<TRelationship>
|
||||||
| AreaDiff<TArea>;
|
| AreaDiff<TArea>
|
||||||
|
| NoteDiff<TNote>;
|
||||||
|
|
||||||
export const createChartDBDiffSchema = <
|
export const createChartDBDiffSchema = <
|
||||||
TTable = DBTable,
|
TTable = DBTable,
|
||||||
@@ -31,20 +35,27 @@ export const createChartDBDiffSchema = <
|
|||||||
TIndex = DBIndex,
|
TIndex = DBIndex,
|
||||||
TRelationship = DBRelationship,
|
TRelationship = DBRelationship,
|
||||||
TArea = Area,
|
TArea = Area,
|
||||||
|
TNote = Note,
|
||||||
>(
|
>(
|
||||||
tableSchema: z.ZodType<TTable>,
|
tableSchema: z.ZodType<TTable>,
|
||||||
fieldSchema: z.ZodType<TField>,
|
fieldSchema: z.ZodType<TField>,
|
||||||
indexSchema: z.ZodType<TIndex>,
|
indexSchema: z.ZodType<TIndex>,
|
||||||
relationshipSchema: z.ZodType<TRelationship>,
|
relationshipSchema: z.ZodType<TRelationship>,
|
||||||
areaSchema: z.ZodType<TArea>
|
areaSchema: z.ZodType<TArea>,
|
||||||
): z.ZodType<ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>> => {
|
noteSchema: z.ZodType<TNote>
|
||||||
|
): z.ZodType<
|
||||||
|
ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea, TNote>
|
||||||
|
> => {
|
||||||
return z.union([
|
return z.union([
|
||||||
createTableDiffSchema(tableSchema),
|
createTableDiffSchema(tableSchema),
|
||||||
createFieldDiffSchema(fieldSchema),
|
createFieldDiffSchema(fieldSchema),
|
||||||
createIndexDiffSchema(indexSchema),
|
createIndexDiffSchema(indexSchema),
|
||||||
createRelationshipDiffSchema(relationshipSchema),
|
createRelationshipDiffSchema(relationshipSchema),
|
||||||
createAreaDiffSchema(areaSchema),
|
createAreaDiffSchema(areaSchema),
|
||||||
]) as z.ZodType<ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>>;
|
createNoteDiffSchema(noteSchema),
|
||||||
|
]) as z.ZodType<
|
||||||
|
ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea, TNote>
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiffMap<
|
export type DiffMap<
|
||||||
@@ -53,7 +64,11 @@ export type DiffMap<
|
|||||||
TIndex = DBIndex,
|
TIndex = DBIndex,
|
||||||
TRelationship = DBRelationship,
|
TRelationship = DBRelationship,
|
||||||
TArea = Area,
|
TArea = Area,
|
||||||
> = Map<string, ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>>;
|
TNote = Note,
|
||||||
|
> = Map<
|
||||||
|
string,
|
||||||
|
ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea, TNote>
|
||||||
|
>;
|
||||||
|
|
||||||
export type DiffObject<
|
export type DiffObject<
|
||||||
TTable = DBTable,
|
TTable = DBTable,
|
||||||
@@ -61,12 +76,14 @@ export type DiffObject<
|
|||||||
TIndex = DBIndex,
|
TIndex = DBIndex,
|
||||||
TRelationship = DBRelationship,
|
TRelationship = DBRelationship,
|
||||||
TArea = Area,
|
TArea = Area,
|
||||||
|
TNote = Note,
|
||||||
> =
|
> =
|
||||||
| TableDiff<TTable>['object']
|
| TableDiff<TTable>['object']
|
||||||
| FieldDiff<TField>['object']
|
| FieldDiff<TField>['object']
|
||||||
| IndexDiff<TIndex>['object']
|
| IndexDiff<TIndex>['object']
|
||||||
| RelationshipDiff<TRelationship>['object']
|
| RelationshipDiff<TRelationship>['object']
|
||||||
| AreaDiff<TArea>['object'];
|
| AreaDiff<TArea>['object']
|
||||||
|
| NoteDiff<TNote>['object'];
|
||||||
|
|
||||||
type ExtractDiffKind<T> = T extends { object: infer O; type: infer Type }
|
type ExtractDiffKind<T> = T extends { object: infer O; type: infer Type }
|
||||||
? T extends { attribute: infer A }
|
? T extends { attribute: infer A }
|
||||||
@@ -80,7 +97,10 @@ export type DiffKind<
|
|||||||
TIndex = DBIndex,
|
TIndex = DBIndex,
|
||||||
TRelationship = DBRelationship,
|
TRelationship = DBRelationship,
|
||||||
TArea = Area,
|
TArea = Area,
|
||||||
> = ExtractDiffKind<ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>>;
|
TNote = Note,
|
||||||
|
> = ExtractDiffKind<
|
||||||
|
ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea, TNote>
|
||||||
|
>;
|
||||||
|
|
||||||
export const isDiffOfKind = <
|
export const isDiffOfKind = <
|
||||||
TTable = DBTable,
|
TTable = DBTable,
|
||||||
@@ -88,9 +108,10 @@ export const isDiffOfKind = <
|
|||||||
TIndex = DBIndex,
|
TIndex = DBIndex,
|
||||||
TRelationship = DBRelationship,
|
TRelationship = DBRelationship,
|
||||||
TArea = Area,
|
TArea = Area,
|
||||||
|
TNote = Note,
|
||||||
>(
|
>(
|
||||||
diff: ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>,
|
diff: ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea, TNote>,
|
||||||
kind: DiffKind<TTable, TField, TIndex, TRelationship, TArea>
|
kind: DiffKind<TTable, TField, TIndex, TRelationship, TArea, TNote>
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if ('attribute' in kind) {
|
if ('attribute' in kind) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
77
src/lib/domain/diff/note-diff.ts
Normal file
77
src/lib/domain/diff/note-diff.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import type { Note } from '../note';
|
||||||
|
|
||||||
|
export type NoteDiffAttribute = keyof Pick<
|
||||||
|
Note,
|
||||||
|
'content' | 'color' | 'x' | 'y' | 'width' | 'height'
|
||||||
|
>;
|
||||||
|
|
||||||
|
const noteDiffAttributeSchema: z.ZodType<NoteDiffAttribute> = z.union([
|
||||||
|
z.literal('content'),
|
||||||
|
z.literal('color'),
|
||||||
|
z.literal('x'),
|
||||||
|
z.literal('y'),
|
||||||
|
z.literal('width'),
|
||||||
|
z.literal('height'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export interface NoteDiffChanged {
|
||||||
|
object: 'note';
|
||||||
|
type: 'changed';
|
||||||
|
noteId: string;
|
||||||
|
attribute: NoteDiffAttribute;
|
||||||
|
oldValue?: string | number | null;
|
||||||
|
newValue?: string | number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoteDiffChangedSchema: z.ZodType<NoteDiffChanged> = z.object({
|
||||||
|
object: z.literal('note'),
|
||||||
|
type: z.literal('changed'),
|
||||||
|
noteId: z.string(),
|
||||||
|
attribute: noteDiffAttributeSchema,
|
||||||
|
oldValue: z.union([z.string(), z.number(), z.null()]).optional(),
|
||||||
|
newValue: z.union([z.string(), z.number(), z.null()]).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface NoteDiffRemoved {
|
||||||
|
object: 'note';
|
||||||
|
type: 'removed';
|
||||||
|
noteId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoteDiffRemovedSchema: z.ZodType<NoteDiffRemoved> = z.object({
|
||||||
|
object: z.literal('note'),
|
||||||
|
type: z.literal('removed'),
|
||||||
|
noteId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface NoteDiffAdded<T = Note> {
|
||||||
|
object: 'note';
|
||||||
|
type: 'added';
|
||||||
|
noteAdded: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNoteDiffAddedSchema = <T = Note>(
|
||||||
|
noteSchema: z.ZodType<T>
|
||||||
|
): z.ZodType<NoteDiffAdded<T>> => {
|
||||||
|
return z.object({
|
||||||
|
object: z.literal('note'),
|
||||||
|
type: z.literal('added'),
|
||||||
|
noteAdded: noteSchema,
|
||||||
|
}) as z.ZodType<NoteDiffAdded<T>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NoteDiff<T = Note> =
|
||||||
|
| NoteDiffChanged
|
||||||
|
| NoteDiffRemoved
|
||||||
|
| NoteDiffAdded<T>;
|
||||||
|
|
||||||
|
export const createNoteDiffSchema = <T = Note>(
|
||||||
|
noteSchema: z.ZodType<T>
|
||||||
|
): z.ZodType<NoteDiff<T>> => {
|
||||||
|
return z.union([
|
||||||
|
NoteDiffChangedSchema,
|
||||||
|
NoteDiffRemovedSchema,
|
||||||
|
createNoteDiffAddedSchema(noteSchema),
|
||||||
|
]) as z.ZodType<NoteDiff<T>>;
|
||||||
|
};
|
||||||
@@ -11,3 +11,4 @@ export * from './db-relationship';
|
|||||||
export * from './db-schema';
|
export * from './db-schema';
|
||||||
export * from './db-table';
|
export * from './db-table';
|
||||||
export * from './diagram';
|
export * from './diagram';
|
||||||
|
export * from './note';
|
||||||
|
|||||||
23
src/lib/domain/note.ts
Normal file
23
src/lib/domain/note.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export interface Note {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
color: string;
|
||||||
|
order?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const noteSchema: z.ZodType<Note> = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
content: z.string(),
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
width: z.number(),
|
||||||
|
height: z.number(),
|
||||||
|
color: z.string(),
|
||||||
|
order: z.number().optional(),
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from '@/components/context-menu/context-menu';
|
} from '@/components/context-menu/context-menu';
|
||||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||||
@@ -10,7 +11,7 @@ import { useDialog } from '@/hooks/use-dialog';
|
|||||||
import { useReactFlow } from '@xyflow/react';
|
import { useReactFlow } from '@xyflow/react';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Table, Workflow, Group, View } from 'lucide-react';
|
import { Table, Workflow, Group, View, StickyNote } from 'lucide-react';
|
||||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
import { useCanvas } from '@/hooks/use-canvas';
|
import { useCanvas } from '@/hooks/use-canvas';
|
||||||
@@ -19,7 +20,8 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
|
|||||||
export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { createTable, readonly, createArea, databaseType } = useChartDB();
|
const { createTable, readonly, createArea, databaseType, createNote } =
|
||||||
|
useChartDB();
|
||||||
const { schemasDisplayed } = useDiagramFilter();
|
const { schemasDisplayed } = useDiagramFilter();
|
||||||
const { openCreateRelationshipDialog } = useDialog();
|
const { openCreateRelationshipDialog } = useDialog();
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
@@ -121,6 +123,21 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
|||||||
[createArea, screenToFlowPosition]
|
[createArea, screenToFlowPosition]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const createNoteHandler = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
const position = screenToFlowPosition({
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
createNote({
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[createNote, screenToFlowPosition]
|
||||||
|
);
|
||||||
|
|
||||||
const createRelationshipHandler = useCallback(() => {
|
const createRelationshipHandler = useCallback(() => {
|
||||||
openCreateRelationshipDialog();
|
openCreateRelationshipDialog();
|
||||||
}, [openCreateRelationshipDialog]);
|
}, [openCreateRelationshipDialog]);
|
||||||
@@ -158,6 +175,7 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
|||||||
{t('canvas_context_menu.new_relationship')}
|
{t('canvas_context_menu.new_relationship')}
|
||||||
<Workflow className="size-3.5" />
|
<Workflow className="size-3.5" />
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={createAreaHandler}
|
onClick={createAreaHandler}
|
||||||
className="flex justify-between gap-4"
|
className="flex justify-between gap-4"
|
||||||
@@ -165,6 +183,13 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
|||||||
{t('canvas_context_menu.new_area')}
|
{t('canvas_context_menu.new_area')}
|
||||||
<Group className="size-3.5" />
|
<Group className="size-3.5" />
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={createNoteHandler}
|
||||||
|
className="flex justify-between gap-4"
|
||||||
|
>
|
||||||
|
{t('canvas_context_menu.new_note')}
|
||||||
|
<StickyNote className="size-3.5" />
|
||||||
|
</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ import { useCanvas } from '@/hooks/use-canvas';
|
|||||||
import type { AreaNodeType } from './area-node/area-node';
|
import type { AreaNodeType } from './area-node/area-node';
|
||||||
import { AreaNode } from './area-node/area-node';
|
import { AreaNode } from './area-node/area-node';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { NoteNodeType } from './note-node/note-node';
|
||||||
|
import { NoteNode } from './note-node/note-node';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
import type { TempCursorNodeType } from './temp-cursor-node/temp-cursor-node';
|
import type { TempCursorNodeType } from './temp-cursor-node/temp-cursor-node';
|
||||||
import {
|
import {
|
||||||
TEMP_CURSOR_HANDLE_ID,
|
TEMP_CURSOR_HANDLE_ID,
|
||||||
@@ -123,6 +126,7 @@ export type EdgeType =
|
|||||||
export type NodeType =
|
export type NodeType =
|
||||||
| TableNodeType
|
| TableNodeType
|
||||||
| AreaNodeType
|
| AreaNodeType
|
||||||
|
| NoteNodeType
|
||||||
| TempCursorNodeType
|
| TempCursorNodeType
|
||||||
| CreateRelationshipNodeType;
|
| CreateRelationshipNodeType;
|
||||||
|
|
||||||
@@ -137,6 +141,7 @@ const edgeTypes: EdgeTypes = {
|
|||||||
const nodeTypes: NodeTypes = {
|
const nodeTypes: NodeTypes = {
|
||||||
table: TableNode,
|
table: TableNode,
|
||||||
area: AreaNode,
|
area: AreaNode,
|
||||||
|
note: NoteNode,
|
||||||
'temp-cursor': TempCursorNode,
|
'temp-cursor': TempCursorNode,
|
||||||
'create-relationship': CreateRelationshipNode,
|
'create-relationship': CreateRelationshipNode,
|
||||||
};
|
};
|
||||||
@@ -238,6 +243,21 @@ const areaToAreaNode = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const noteToNoteNode = (note: Note): NoteNodeType => {
|
||||||
|
return {
|
||||||
|
id: note.id,
|
||||||
|
type: 'note',
|
||||||
|
position: { x: note.x, y: note.y },
|
||||||
|
data: { note },
|
||||||
|
width: note.width,
|
||||||
|
height: note.height,
|
||||||
|
zIndex: 50,
|
||||||
|
style: {
|
||||||
|
zIndex: 50,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export interface CanvasProps {
|
export interface CanvasProps {
|
||||||
initialTables: DBTable[];
|
initialTables: DBTable[];
|
||||||
}
|
}
|
||||||
@@ -254,6 +274,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
const {
|
const {
|
||||||
tables,
|
tables,
|
||||||
areas,
|
areas,
|
||||||
|
notes,
|
||||||
relationships,
|
relationships,
|
||||||
createRelationship,
|
createRelationship,
|
||||||
createDependency,
|
createDependency,
|
||||||
@@ -267,6 +288,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
readonly,
|
readonly,
|
||||||
removeArea,
|
removeArea,
|
||||||
updateArea,
|
updateArea,
|
||||||
|
removeNote,
|
||||||
|
updateNote,
|
||||||
highlightedCustomType,
|
highlightedCustomType,
|
||||||
highlightCustomTypeId,
|
highlightCustomTypeId,
|
||||||
} = useChartDB();
|
} = useChartDB();
|
||||||
@@ -287,6 +310,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
endFloatingEdgeCreation,
|
endFloatingEdgeCreation,
|
||||||
hoveringTableId,
|
hoveringTableId,
|
||||||
hideCreateRelationshipNode,
|
hideCreateRelationshipNode,
|
||||||
|
events: canvasEvents,
|
||||||
} = useCanvas();
|
} = useCanvas();
|
||||||
const { filter, loading: filterLoading } = useDiagramFilter();
|
const { filter, loading: filterLoading } = useDiagramFilter();
|
||||||
const { checkIfNewTable } = useDiff();
|
const { checkIfNewTable } = useDiff();
|
||||||
@@ -543,6 +567,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
filterLoading,
|
filterLoading,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
...notes.map((note) => noteToNoteNode(note)),
|
||||||
...prevNodes.filter(
|
...prevNodes.filter(
|
||||||
(n) =>
|
(n) =>
|
||||||
n.type === 'temp-cursor' ||
|
n.type === 'temp-cursor' ||
|
||||||
@@ -560,6 +585,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
}, [
|
}, [
|
||||||
tables,
|
tables,
|
||||||
areas,
|
areas,
|
||||||
|
notes,
|
||||||
setNodes,
|
setNodes,
|
||||||
filter,
|
filter,
|
||||||
databaseType,
|
databaseType,
|
||||||
@@ -975,6 +1001,13 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
sizeChanges: areaSizeChanges,
|
sizeChanges: areaSizeChanges,
|
||||||
} = findRelevantNodesChanges(changesToApply, 'area');
|
} = findRelevantNodesChanges(changesToApply, 'area');
|
||||||
|
|
||||||
|
// Then, detect note changes
|
||||||
|
const {
|
||||||
|
positionChanges: notePositionChanges,
|
||||||
|
removeChanges: noteRemoveChanges,
|
||||||
|
sizeChanges: noteSizeChanges,
|
||||||
|
} = findRelevantNodesChanges(changesToApply, 'note');
|
||||||
|
|
||||||
// Then, detect table changes
|
// Then, detect table changes
|
||||||
const { positionChanges, removeChanges, sizeChanges } =
|
const { positionChanges, removeChanges, sizeChanges } =
|
||||||
findRelevantNodesChanges(changesToApply, 'table');
|
findRelevantNodesChanges(changesToApply, 'table');
|
||||||
@@ -1144,6 +1177,49 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle note changes
|
||||||
|
if (
|
||||||
|
notePositionChanges.length > 0 ||
|
||||||
|
noteRemoveChanges.length > 0 ||
|
||||||
|
noteSizeChanges.length > 0
|
||||||
|
) {
|
||||||
|
const notesUpdates: Record<string, Partial<Note>> = {};
|
||||||
|
// Handle note position changes
|
||||||
|
notePositionChanges.forEach((change) => {
|
||||||
|
if (change.type === 'position' && change.position) {
|
||||||
|
notesUpdates[change.id] = {
|
||||||
|
...notesUpdates[change.id],
|
||||||
|
x: change.position.x,
|
||||||
|
y: change.position.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle note size changes
|
||||||
|
noteSizeChanges.forEach((change) => {
|
||||||
|
if (change.type === 'dimensions' && change.dimensions) {
|
||||||
|
notesUpdates[change.id] = {
|
||||||
|
...notesUpdates[change.id],
|
||||||
|
width: change.dimensions.width,
|
||||||
|
height: change.dimensions.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle note removal
|
||||||
|
noteRemoveChanges.forEach((change) => {
|
||||||
|
removeNote(change.id);
|
||||||
|
delete notesUpdates[change.id];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply note updates to storage
|
||||||
|
if (Object.keys(notesUpdates).length > 0) {
|
||||||
|
for (const [id, updates] of Object.entries(notesUpdates)) {
|
||||||
|
updateNote(id, updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return onNodesChange(changesToApply);
|
return onNodesChange(changesToApply);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -1153,6 +1229,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
findRelevantNodesChanges,
|
findRelevantNodesChanges,
|
||||||
updateArea,
|
updateArea,
|
||||||
removeArea,
|
removeArea,
|
||||||
|
updateNote,
|
||||||
|
removeNote,
|
||||||
readonly,
|
readonly,
|
||||||
tables,
|
tables,
|
||||||
areas,
|
areas,
|
||||||
@@ -1423,23 +1501,35 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
return [...edges, tempEdge];
|
return [...edges, tempEdge];
|
||||||
}, [edges, tempFloatingEdge, cursorPosition, hoveringTableId]);
|
}, [edges, tempFloatingEdge, cursorPosition, hoveringTableId]);
|
||||||
|
|
||||||
const onPaneClickHandler = useCallback(() => {
|
const onPaneClickHandler = useCallback(
|
||||||
if (tempFloatingEdge) {
|
(event: React.MouseEvent<Element, MouseEvent>) => {
|
||||||
endFloatingEdgeCreation();
|
if (tempFloatingEdge) {
|
||||||
setCursorPosition(null);
|
endFloatingEdgeCreation();
|
||||||
}
|
setCursorPosition(null);
|
||||||
|
}
|
||||||
|
|
||||||
// Close CreateRelationshipNode if it exists
|
// Close CreateRelationshipNode if it exists
|
||||||
hideCreateRelationshipNode();
|
hideCreateRelationshipNode();
|
||||||
|
|
||||||
// Exit edit table mode
|
// Exit edit table mode
|
||||||
exitEditTableMode();
|
exitEditTableMode();
|
||||||
}, [
|
|
||||||
tempFloatingEdge,
|
canvasEvents.emit({
|
||||||
exitEditTableMode,
|
action: 'pan_click',
|
||||||
endFloatingEdgeCreation,
|
data: {
|
||||||
hideCreateRelationshipNode,
|
x: event.clientX,
|
||||||
]);
|
y: event.clientY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
canvasEvents,
|
||||||
|
tempFloatingEdge,
|
||||||
|
exitEditTableMode,
|
||||||
|
endFloatingEdgeCreation,
|
||||||
|
hideCreateRelationshipNode,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CanvasContextMenu>
|
<CanvasContextMenu>
|
||||||
|
|||||||
219
src/pages/editor-page/canvas/note-node/note-node.tsx
Normal file
219
src/pages/editor-page/canvas/note-node/note-node.tsx
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import React, { useCallback, useState, useRef } from 'react';
|
||||||
|
import { NodeResizer, type NodeProps, type Node } from '@xyflow/react';
|
||||||
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||||
|
import { ColorPicker } from '@/components/color-picker/color-picker';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useCanvas } from '@/hooks/use-canvas';
|
||||||
|
import type { CanvasEvent } from '@/context/canvas-context/canvas-context';
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
|
export interface NoteNodeProps extends NodeProps {
|
||||||
|
data: {
|
||||||
|
note: Note;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NoteNodeType = Node<{ note: Note }, 'note'>;
|
||||||
|
|
||||||
|
export const NoteNode: React.FC<NoteNodeProps> = ({ data, selected }) => {
|
||||||
|
const { note } = data;
|
||||||
|
const { updateNote, removeNote, readonly } = useChartDB();
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const [content, setContent] = useState(note.content);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const { events } = useCanvas();
|
||||||
|
const { effectiveTheme } = useTheme();
|
||||||
|
|
||||||
|
const saveContent = useCallback(() => {
|
||||||
|
if (!editMode) return;
|
||||||
|
updateNote(note.id, { content: content.trim() });
|
||||||
|
setEditMode(false);
|
||||||
|
}, [editMode, content, note.id, updateNote]);
|
||||||
|
|
||||||
|
const abortEdit = useCallback(() => {
|
||||||
|
setEditMode(false);
|
||||||
|
setContent(note.content);
|
||||||
|
}, [note.content]);
|
||||||
|
|
||||||
|
const enterEditMode = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (readonly) return;
|
||||||
|
setEditMode(true);
|
||||||
|
},
|
||||||
|
[readonly]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeNote(note.id);
|
||||||
|
},
|
||||||
|
[note.id, removeNote]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleColorChange = useCallback(
|
||||||
|
(color: string) => {
|
||||||
|
updateNote(note.id, { color });
|
||||||
|
},
|
||||||
|
[note.id, updateNote]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDoubleClick = useCallback<
|
||||||
|
React.MouseEventHandler<HTMLDivElement>
|
||||||
|
>(
|
||||||
|
(e) => {
|
||||||
|
if (!readonly) {
|
||||||
|
enterEditMode(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[enterEditMode, readonly]
|
||||||
|
);
|
||||||
|
|
||||||
|
useClickAway(textareaRef, saveContent);
|
||||||
|
useKeyPressEvent('Escape', abortEdit);
|
||||||
|
|
||||||
|
const eventConsumer = useCallback(
|
||||||
|
(event: CanvasEvent) => {
|
||||||
|
if (!editMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.action === 'pan_click') {
|
||||||
|
saveContent();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[editMode, saveContent]
|
||||||
|
);
|
||||||
|
|
||||||
|
events.useSubscription(eventConsumer);
|
||||||
|
|
||||||
|
// Focus textarea when entering edit mode
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (textareaRef.current && editMode) {
|
||||||
|
textareaRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [editMode]);
|
||||||
|
|
||||||
|
const getHeaderColor = (color: string) => {
|
||||||
|
// Return the original color for header (full saturation)
|
||||||
|
return color;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBodyColor = (color: string) => {
|
||||||
|
const hex = color.replace('#', '');
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
|
|
||||||
|
const isDark = effectiveTheme === 'dark';
|
||||||
|
|
||||||
|
if (isDark) {
|
||||||
|
// Dark mode: darken the color by mixing with dark gray (30% original + 70% dark)
|
||||||
|
const darkR = Math.round(r * 0.3 + 0 * 0.7);
|
||||||
|
const darkG = Math.round(g * 0.3 + 0 * 0.7);
|
||||||
|
const darkB = Math.round(b * 0.3 + 0 * 0.7);
|
||||||
|
return `rgb(${darkR}, ${darkG}, ${darkB})`;
|
||||||
|
} else {
|
||||||
|
// Light mode: lighten the color by mixing with white (30% original + 70% white)
|
||||||
|
const lightR = Math.round(r * 0.3 + 255 * 0.7);
|
||||||
|
const lightG = Math.round(g * 0.3 + 255 * 0.7);
|
||||||
|
const lightB = Math.round(b * 0.3 + 255 * 0.7);
|
||||||
|
return `rgb(${lightR}, ${lightG}, ${lightB})`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex h-full flex-col overflow-hidden rounded border shadow-md',
|
||||||
|
selected ? 'border-pink-600 border-2' : 'border-border'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
background: getBodyColor(note.color),
|
||||||
|
}}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
|
>
|
||||||
|
{/* Notepad header with binding */}
|
||||||
|
<div
|
||||||
|
className="relative flex h-2 shrink-0 items-center justify-center"
|
||||||
|
style={{
|
||||||
|
background: getHeaderColor(note.color),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NodeResizer
|
||||||
|
minWidth={200}
|
||||||
|
minHeight={150}
|
||||||
|
isVisible={selected}
|
||||||
|
lineClassName="!border-pink-500"
|
||||||
|
handleClassName="!h-3 !w-3 !bg-pink-500 !rounded-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Note body */}
|
||||||
|
<div className="group/note relative flex-1 overflow-hidden p-2">
|
||||||
|
{/* Corner fold (bottom-right) */}
|
||||||
|
<div className="absolute bottom-0 right-0 border-b-[20px] border-l-[40px] border-r-0 border-t-0 border-b-black/10 border-l-transparent opacity-50 dark:border-b-white/10" />
|
||||||
|
|
||||||
|
{/* Content area */}
|
||||||
|
{editMode ? (
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
className="size-full resize-none overflow-auto border-none bg-transparent p-0 text-sm leading-relaxed text-gray-700 outline-none dark:text-gray-300"
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
saveContent();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
placeholder="Type your note here..."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-full overflow-auto whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300">
|
||||||
|
{note.content || (
|
||||||
|
<span className="italic text-muted-foreground">
|
||||||
|
Double-click to add text...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Quick actions on hover */}
|
||||||
|
{!editMode && !readonly && (
|
||||||
|
<div className="absolute right-2 top-2 flex gap-1 rounded bg-white/90 p-1 opacity-0 shadow-md transition-opacity group-hover/note:opacity-100 dark:bg-slate-800/90">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-7 p-0"
|
||||||
|
onClick={enterEditMode}
|
||||||
|
>
|
||||||
|
<Pencil className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
<ColorPicker
|
||||||
|
color={note.color}
|
||||||
|
onChange={handleColorChange}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-7 p-0 text-red-500 hover:text-red-700"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NoteNode.displayName = 'NoteNode';
|
||||||
@@ -59,7 +59,7 @@ export const AreasTab: React.FC<AreasTabProps> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col overflow-hidden px-2">
|
<div className="flex flex-1 flex-col overflow-hidden px-2">
|
||||||
<div className="flex items-center justify-between gap-4 py-1">
|
<div className="flex items-center justify-between gap-4 pb-1">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Input
|
<Input
|
||||||
ref={filterInputRef}
|
ref={filterInputRef}
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
GripVertical,
|
||||||
|
Trash2,
|
||||||
|
EllipsisVertical,
|
||||||
|
CircleDotDashed,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ColorPicker } from '@/components/color-picker/color-picker';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/dropdown-menu/dropdown-menu';
|
||||||
|
import { ListItemHeaderButton } from '@/pages/editor-page/side-panel/list-item-header-button/list-item-header-button';
|
||||||
|
import { useFocusOn } from '@/hooks/use-focus-on';
|
||||||
|
import { mergeRefs } from '@/lib/utils';
|
||||||
|
|
||||||
|
export interface NoteListItemProps {
|
||||||
|
note: Note;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoteListItem = React.forwardRef<HTMLDivElement, NoteListItemProps>(
|
||||||
|
({ note }, forwardedRef) => {
|
||||||
|
const { updateNote, removeNote, readonly } = useChartDB();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { focusOnNote } = useFocusOn();
|
||||||
|
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
|
useSortable({
|
||||||
|
id: note.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge the forwarded ref with the sortable ref
|
||||||
|
const combinedRef = mergeRefs<HTMLDivElement>(forwardedRef, setNodeRef);
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Translate.toString(transform),
|
||||||
|
transition,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
removeNote(note.id);
|
||||||
|
}, [note.id, removeNote]);
|
||||||
|
|
||||||
|
const handleColorChange = useCallback(
|
||||||
|
(color: string) => {
|
||||||
|
updateNote(note.id, { color });
|
||||||
|
},
|
||||||
|
[note.id, updateNote]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFocusOnNote = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
focusOnNote(note.id);
|
||||||
|
},
|
||||||
|
[focusOnNote, note.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDropDownMenu = useCallback(
|
||||||
|
() => (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<ListItemHeaderButton>
|
||||||
|
<EllipsisVertical />
|
||||||
|
</ListItemHeaderButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-fit min-w-40">
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
{t(
|
||||||
|
'side_panel.notes_section.note.note_actions.title'
|
||||||
|
)}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="flex justify-between !text-red-700"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
'side_panel.notes_section.note.note_actions.delete_note'
|
||||||
|
)}
|
||||||
|
<Trash2 className="size-3.5 text-red-700" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
[handleDelete, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full rounded-md border border-border hover:bg-accent/5"
|
||||||
|
ref={combinedRef}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
borderLeftWidth: '6px',
|
||||||
|
borderLeftColor: note.color,
|
||||||
|
}}
|
||||||
|
{...attributes}
|
||||||
|
>
|
||||||
|
<div className="group flex min-h-11 items-center justify-between gap-1 overflow-hidden p-2">
|
||||||
|
{!readonly ? (
|
||||||
|
<div
|
||||||
|
className="flex cursor-move items-center justify-center"
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<GripVertical className="size-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="flex min-w-0 flex-1">
|
||||||
|
<div className="truncate px-2 py-0.5 text-sm">
|
||||||
|
{note.content || (
|
||||||
|
<span className="italic text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
'side_panel.notes_section.note.empty_note'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="flex flex-row-reverse items-center gap-1">
|
||||||
|
{!readonly ? renderDropDownMenu() : null}
|
||||||
|
<ColorPicker
|
||||||
|
color={note.color}
|
||||||
|
onChange={handleColorChange}
|
||||||
|
disabled={readonly}
|
||||||
|
/>
|
||||||
|
<div className="hidden md:group-hover:flex">
|
||||||
|
<ListItemHeaderButton
|
||||||
|
onClick={handleFocusOnNote}
|
||||||
|
>
|
||||||
|
<CircleDotDashed />
|
||||||
|
</ListItemHeaderButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
NoteListItem.displayName = 'NoteListItem';
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { NoteListItem } from './note-list-item/note-list-item';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
|
import {
|
||||||
|
closestCenter,
|
||||||
|
DndContext,
|
||||||
|
type DragEndEvent,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb.ts';
|
||||||
|
|
||||||
|
export interface NotesListProps {
|
||||||
|
notes: Note[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotesList: React.FC<NotesListProps> = ({ notes }) => {
|
||||||
|
const { updateNote } = useChartDB();
|
||||||
|
|
||||||
|
const { openedNoteInSidebar } = useLayout();
|
||||||
|
const lastSelectedNote = React.useRef<string | null>(null);
|
||||||
|
const refs = useMemo(
|
||||||
|
() =>
|
||||||
|
notes.reduce(
|
||||||
|
(acc, note) => {
|
||||||
|
acc[note.id] = React.createRef();
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, React.RefObject<HTMLDivElement>>
|
||||||
|
),
|
||||||
|
[notes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollToNote = useCallback(
|
||||||
|
(id: string) =>
|
||||||
|
refs[id]?.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
}),
|
||||||
|
[refs]
|
||||||
|
);
|
||||||
|
|
||||||
|
const sensors = useSensors(useSensor(PointerSensor));
|
||||||
|
|
||||||
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
if (active?.id !== over?.id && !!over && !!active) {
|
||||||
|
const oldIndex = notes.findIndex((note) => note.id === active.id);
|
||||||
|
const newIndex = notes.findIndex((note) => note.id === over.id);
|
||||||
|
|
||||||
|
const newNotesOrder = arrayMove<Note>(notes, oldIndex, newIndex);
|
||||||
|
|
||||||
|
newNotesOrder.forEach((note, index) => {
|
||||||
|
updateNote(note.id, { order: index });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScrollToNote = useCallback(() => {
|
||||||
|
if (
|
||||||
|
openedNoteInSidebar &&
|
||||||
|
lastSelectedNote.current !== openedNoteInSidebar
|
||||||
|
) {
|
||||||
|
lastSelectedNote.current = openedNoteInSidebar;
|
||||||
|
scrollToNote(openedNoteInSidebar);
|
||||||
|
}
|
||||||
|
}, [scrollToNote, openedNoteInSidebar]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
handleScrollToNote();
|
||||||
|
}, [openedNoteInSidebar, handleScrollToNote]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-1">
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={notes}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{notes
|
||||||
|
.sort((note1: Note, note2: Note) => {
|
||||||
|
if (note1.order && note2.order === undefined) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note1.order === undefined && note2.order) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
note1.order !== undefined &&
|
||||||
|
note2.order !== undefined
|
||||||
|
) {
|
||||||
|
return note1.order - note2.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if both notes don't have order, sort by content
|
||||||
|
return note1.content.localeCompare(note2.content);
|
||||||
|
})
|
||||||
|
.map((note) => (
|
||||||
|
<NoteListItem
|
||||||
|
key={note.id}
|
||||||
|
note={note}
|
||||||
|
ref={refs[note.id]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import { StickyNote, X } from 'lucide-react';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
import type { Note } from '@/lib/domain/note';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
|
import { EmptyState } from '@/components/empty-state/empty-state';
|
||||||
|
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useViewport } from '@xyflow/react';
|
||||||
|
import { NotesList } from './notes-list/notes-list';
|
||||||
|
|
||||||
|
export interface NotesTabProps {}
|
||||||
|
|
||||||
|
export const NotesTab: React.FC<NotesTabProps> = () => {
|
||||||
|
const { createNote, notes, readonly } = useChartDB();
|
||||||
|
const viewport = useViewport();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { openNoteFromSidebar } = useLayout();
|
||||||
|
const [filterText, setFilterText] = React.useState('');
|
||||||
|
const filterInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const filteredNotes = useMemo(() => {
|
||||||
|
const filterNoteContent: (note: Note) => boolean = (note) =>
|
||||||
|
!filterText?.trim?.() ||
|
||||||
|
note.content.toLowerCase().includes(filterText.toLowerCase());
|
||||||
|
|
||||||
|
return notes.filter(filterNoteContent);
|
||||||
|
}, [notes, filterText]);
|
||||||
|
|
||||||
|
const createNoteWithLocation = useCallback(async () => {
|
||||||
|
const padding = 80;
|
||||||
|
const centerX = -viewport.x / viewport.zoom + padding / viewport.zoom;
|
||||||
|
const centerY = -viewport.y / viewport.zoom + padding / viewport.zoom;
|
||||||
|
const note = await createNote({
|
||||||
|
x: centerX,
|
||||||
|
y: centerY,
|
||||||
|
});
|
||||||
|
if (openNoteFromSidebar) {
|
||||||
|
openNoteFromSidebar(note.id);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
createNote,
|
||||||
|
openNoteFromSidebar,
|
||||||
|
viewport.x,
|
||||||
|
viewport.y,
|
||||||
|
viewport.zoom,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleCreateNote = useCallback(async () => {
|
||||||
|
setFilterText('');
|
||||||
|
createNoteWithLocation();
|
||||||
|
}, [createNoteWithLocation, setFilterText]);
|
||||||
|
|
||||||
|
const handleClearFilter = useCallback(() => {
|
||||||
|
setFilterText('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col overflow-hidden px-2">
|
||||||
|
<div className="flex items-center justify-between gap-4 pb-1">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
ref={filterInputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder={t('side_panel.notes_section.filter')}
|
||||||
|
className="h-8 w-full focus-visible:ring-0"
|
||||||
|
value={filterText}
|
||||||
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!readonly ? (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="h-8 p-2 text-xs"
|
||||||
|
onClick={handleCreateNote}
|
||||||
|
>
|
||||||
|
<StickyNote className="h-4" />
|
||||||
|
{t('side_panel.notes_section.add_note')}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<ScrollArea className="h-full">
|
||||||
|
{notes.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
title={t(
|
||||||
|
'side_panel.notes_section.empty_state.title'
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
'side_panel.notes_section.empty_state.description'
|
||||||
|
)}
|
||||||
|
className="mt-20"
|
||||||
|
/>
|
||||||
|
) : filterText && filteredNotes.length === 0 ? (
|
||||||
|
<div className="mt-10 flex flex-col items-center gap-2">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{t('side_panel.notes_section.no_results')}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClearFilter}
|
||||||
|
className="gap-1"
|
||||||
|
>
|
||||||
|
<X className="size-3.5" />
|
||||||
|
{t('side_panel.notes_section.clear')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<NotesList notes={filteredNotes} />
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,9 +6,12 @@ import {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from '@/components/tabs/tabs';
|
} from '@/components/tabs/tabs';
|
||||||
import { AreasTab } from './areas-tab/areas-tab';
|
import { AreasTab } from './areas-tab/areas-tab';
|
||||||
|
import { NotesTab } from './notes-tab/notes-tab';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
import type { VisualsTab } from '@/context/layout-context/layout-context';
|
import type { VisualsTab } from '@/context/layout-context/layout-context';
|
||||||
|
import { Separator } from '@/components/separator/separator';
|
||||||
|
import { Group, StickyNote } from 'lucide-react';
|
||||||
|
|
||||||
export interface VisualsSectionProps {}
|
export interface VisualsSectionProps {}
|
||||||
|
|
||||||
@@ -27,19 +30,38 @@ export const VisualsSection: React.FC<VisualsSectionProps> = () => {
|
|||||||
className="flex flex-1 flex-col overflow-hidden"
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="px-2 pt-2">
|
<div className="px-2 pt-2">
|
||||||
<TabsList className="w-full">
|
<TabsList className="grid h-auto w-full grid-cols-2 gap-1 rounded-xl border bg-background p-1">
|
||||||
<TabsTrigger value="areas" className="flex-1">
|
<TabsTrigger
|
||||||
|
value="areas"
|
||||||
|
className="gap-1.5 rounded-lg px-3 py-1 text-sm font-medium transition-all data-[state=active]:bg-sky-600 data-[state=active]:text-white data-[state=inactive]:text-muted-foreground data-[state=active]:shadow-sm data-[state=inactive]:hover:bg-muted/50 data-[state=inactive]:hover:text-foreground dark:data-[state=active]:bg-sky-500"
|
||||||
|
>
|
||||||
|
<Group className="size-3.5" />
|
||||||
{t('side_panel.visuals_section.tabs.areas')}
|
{t('side_panel.visuals_section.tabs.areas')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="notes"
|
||||||
|
className="gap-1.5 rounded-lg px-3 py-1 text-sm font-medium transition-all data-[state=active]:bg-sky-600 data-[state=active]:text-white data-[state=inactive]:text-muted-foreground data-[state=active]:shadow-sm data-[state=inactive]:hover:bg-muted/50 data-[state=inactive]:hover:text-foreground dark:data-[state=active]:bg-sky-500"
|
||||||
|
>
|
||||||
|
<StickyNote className="size-3.5" />
|
||||||
|
{t('side_panel.visuals_section.tabs.notes')}
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
<Separator orientation="horizontal" className="my-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="areas"
|
value="areas"
|
||||||
className="mt-0 flex flex-1 flex-col overflow-hidden"
|
className="mt-0 flex flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
|
||||||
>
|
>
|
||||||
<AreasTab />
|
<AreasTab />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent
|
||||||
|
value="notes"
|
||||||
|
className="mt-0 flex flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
|
||||||
|
>
|
||||||
|
<NotesTab />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user