Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ Visualize relationships with professional database notation styles:
2. Scroll to **Color Settings** section
3. Adjust **Line Notation Style**, **Line Stroke Style**, and **Line Thickness**
4. Enable **Color by Relationship Type** for type-specific colors
5. Toggle **Show Lookup IDs on Relationship Lines** to hide or show lookup field information

All settings are automatically saved in snapshots and shareable URLs.

Expand Down Expand Up @@ -618,5 +619,3 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
<p align="center">
Made with ❤️ for the Power Platform Community
</p>


9 changes: 9 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
decodeStateFromURL,
expandCompactState,
getFieldLabelModeFromCompact,
getShowRelationshipLookupIdsFromCompact,
getShareBaseUrl,
getStateHash,
buildMinimalShareState,
Expand Down Expand Up @@ -253,12 +254,16 @@ export default function ERDVisualizer({
const expandedState = expandCompactState(filteredState);
const currentState = state.getSerializableState();
const urlFieldLabelMode = getFieldLabelModeFromCompact(filteredState) ?? 'displayName';
const urlShowRelationshipLookupIds = getShowRelationshipLookupIdsFromCompact(filteredState);
const mergedState = {
...currentState,
...expandedState,
colorSettings: {
...currentState.colorSettings,
fieldLabelMode: urlFieldLabelMode,
...(urlShowRelationshipLookupIds !== undefined && {
showRelationshipLookupIds: urlShowRelationshipLookupIds,
}),
},
};
state.restoreState(mergedState);
Expand All @@ -273,12 +278,16 @@ export default function ERDVisualizer({
const expandedState = expandCompactState(decoded.state);
const currentState = state.getSerializableState();
const urlFieldLabelMode = getFieldLabelModeFromCompact(decoded.state) ?? 'displayName';
const urlShowRelationshipLookupIds = getShowRelationshipLookupIdsFromCompact(decoded.state);
const mergedState = {
...currentState,
...expandedState,
colorSettings: {
...currentState.colorSettings,
fieldLabelMode: urlFieldLabelMode,
...(urlShowRelationshipLookupIds !== undefined && {
showRelationshipLookupIds: urlShowRelationshipLookupIds,
}),
},
};
state.restoreState(mergedState);
Expand Down
6 changes: 4 additions & 2 deletions src/components/ReactFlowERD.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,11 +425,13 @@
},
// Show different labels based on relationship type
// N:N: Show intersection table name and cardinality badge
// 1:N/N:1: Show referencing attribute name
// 1:N/N:1: Show referencing attribute name (if showRelationshipLookupIds is enabled)
label:
rel.type === 'N:N'
? `[N:N] ${rel.intersectEntityName || rel.schemaName}`
: rel.referencingAttribute || '',
: (colorSettings.showRelationshipLookupIds ?? true)
? rel.referencingAttribute || ''
: '',
// Pass offset data for draggable edges
data: {
offset: edgeOffsets?.[rel.schemaName] ?? { x: 0, y: 0 },
Expand Down Expand Up @@ -492,7 +494,7 @@
};
})
);
}, [

Check warning on line 497 in src/components/ReactFlowERD.tsx

View workflow job for this annotation

GitHub Actions / Test & Lint

React Hook useEffect has a missing dependency: 'colorSettings'. Either include it or remove the dependency array
colorSettings.customTableColor,
colorSettings.standardTableColor,
colorSettings.fieldLabelMode,
Expand Down Expand Up @@ -521,7 +523,7 @@
};
})
);
}, [

Check warning on line 526 in src/components/ReactFlowERD.tsx

View workflow job for this annotation

GitHub Actions / Test & Lint

React Hook useEffect has a missing dependency: 'colorSettings'. Either include it or remove the dependency array
colorSettings.lookupColor,
colorSettings.edgeStyle,
colorSettings.lineNotation,
Expand Down
23 changes: 23 additions & 0 deletions src/components/SidebarSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,29 @@ export const SidebarSettings = memo(function SidebarSettings({
</label>
</div>

{/* Show Relationship Lookup IDs Checkbox */}
<div style={{ gridColumn: '1 / -1' }}>
<label
className={styles.settingsLabel}
style={{
color: textSecondary,
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}}
>
<input
type="checkbox"
checked={colorSettings.showRelationshipLookupIds ?? true}
onChange={(e) =>
onColorSettingsChange('showRelationshipLookupIds', e.target.checked.toString())
}
style={{ marginRight: '8px', cursor: 'pointer' }}
/>
Show Lookup IDs on Relationship Lines
</label>
</div>

{/* Conditional Type Color Pickers */}
{useRelationshipTypeColors && (
<>
Expand Down
1 change: 1 addition & 0 deletions src/hooks/__tests__/useERDState.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@
manyToOneColor: '#06b6d4',
manyToManyColor: '#8b5cf6',
fieldLabelMode: 'displayName',
showRelationshipLookupIds: true,
});
});

Expand Down Expand Up @@ -525,7 +526,7 @@
lookupColor: '#0000ff',
edgeStyle: 'straight' as const,
// Missing new properties
} as any,

Check warning on line 529 in src/hooks/__tests__/useERDState.test.tsx

View workflow job for this annotation

GitHub Actions / Test & Lint

Unexpected any. Specify a different type
showMinimap: false,
isSmartZoom: false,
edgeOffsets: {},
Expand Down Expand Up @@ -737,7 +738,7 @@
standardTableColor: '#64748b',
lookupColor: '#f97316',
edgeStyle: 'smoothstep' as const,
} as any,

Check warning on line 741 in src/hooks/__tests__/useERDState.test.tsx

View workflow job for this annotation

GitHub Actions / Test & Lint

Unexpected any. Specify a different type
showMinimap: false,
isSmartZoom: false,
edgeOffsets: {},
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/useDataverseData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ export function useDataverseData(options?: UseDataverseDataOptions): UseDatavers
);

useEffect(() => {
// Fetch initial data on mount. This is intentionally done here to
// initialize hook state once and avoid duplicating the fetching logic.
// eslint-disable-next-line react-hooks/set-state-in-effect
fetchData();
}, [fetchData]);

Expand Down
1 change: 1 addition & 0 deletions src/hooks/useERDState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export function useERDState({ entities, relationships }: UseERDStateProps) {
manyToOneColor: '#06b6d4',
manyToManyColor: '#8b5cf6',
fieldLabelMode: 'displayName',
showRelationshipLookupIds: true,
});

// Features
Expand Down
5 changes: 4 additions & 1 deletion src/types/erdTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export interface ColorSettings {

// Field label display mode
fieldLabelMode: FieldLabelMode;

// Relationship label display
showRelationshipLookupIds?: boolean;
}

/** Valid range for lineThickness */
Expand All @@ -82,7 +85,7 @@ export function parseColorSettingValue(
if (Number.isNaN(parsed)) return LINE_THICKNESS_DEFAULT;
return Math.max(LINE_THICKNESS_MIN, Math.min(LINE_THICKNESS_MAX, parsed));
}
if (key === 'useRelationshipTypeColors') {
if (key === 'useRelationshipTypeColors' || key === 'showRelationshipLookupIds') {
return value === 'true';
}
return value;
Expand Down
15 changes: 15 additions & 0 deletions src/utils/__tests__/drawioExport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,21 @@
expect(text).toContain('N:1'); // Cardinality
});

it('should omit lookup IDs in relationship labels when disabled', async () => {
const blob = await exportToDrawio({
...baseOptions,
colorSettings: {
...mockColorSettings,
showRelationshipLookupIds: false,
},
});
const text = await blobToText(blob);

expect(text).toContain('contact_account');
expect(text).not.toContain('parentcustomerid');
expect(text).not.toContain('→');
});

it('should call progress callback during export', async () => {
const progressCalls: Array<{ progress: number; message: string }> = [];

Expand Down Expand Up @@ -371,7 +386,7 @@
click: vi.fn(),
};

const createElementSpy = vi.spyOn(document, 'createElement').mockReturnValue(mockLink as any);

Check warning on line 389 in src/utils/__tests__/drawioExport.test.ts

View workflow job for this annotation

GitHub Actions / Test & Lint

Unexpected any. Specify a different type
const createObjectURLSpy = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url');
const revokeObjectURLSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});

Expand All @@ -398,7 +413,7 @@
click: vi.fn(),
};

const createElementSpy = vi.spyOn(document, 'createElement').mockReturnValue(mockLink as any);

Check warning on line 416 in src/utils/__tests__/drawioExport.test.ts

View workflow job for this annotation

GitHub Actions / Test & Lint

Unexpected any. Specify a different type
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url');
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});

Expand Down
41 changes: 41 additions & 0 deletions src/utils/__tests__/urlStateCodec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ describe('urlStateCodec', () => {
expect(decoded.state?.d).toBe(true);
});

it('should preserve showRelationshipLookupIds when false', () => {
const stateWithLookupIdsHidden = {
...mockState,
showRelationshipLookupIds: false,
};

const encoded = encodeStateToURL(stateWithLookupIdsHidden);
const decoded = decodeStateFromURL(encoded);

expect(decoded.success).toBe(true);
expect(decoded.state?.sl).toBe(false);
});

it('should return error for invalid/corrupted URLs', () => {
expect(decodeStateFromURL('invalid-base64').success).toBe(false);
expect(decodeStateFromURL('').success).toBe(false);
Expand Down Expand Up @@ -191,6 +204,16 @@ describe('urlStateCodec', () => {
expect(expanded.isDarkMode).toBe(true);
});

it('should restore showRelationshipLookupIds from compact state', () => {
const compactWithLookupHidden: CompactState = {
...compactState,
sl: false,
};

const expanded = expandCompactState(compactWithLookupHidden);
expect(expanded.colorSettings).toEqual({ showRelationshipLookupIds: false });
});

it('should handle multiple entities', () => {
const multiEntityState: CompactState = {
e: ['account', 'contact', 'opportunity'],
Expand Down Expand Up @@ -903,6 +926,24 @@ describe('urlStateCodec', () => {
expect(minimal).not.toHaveProperty('fieldLabelMode');
});

it('should omit showRelationshipLookupIds when true (default)', () => {
const minimal = buildMinimalShareState(fullState);
expect(minimal).not.toHaveProperty('showRelationshipLookupIds');
});

it('should include showRelationshipLookupIds when false', () => {
const stateWithHiddenIds = {
...fullState,
colorSettings: {
...fullState.colorSettings,
showRelationshipLookupIds: false,
},
};
const minimal = buildMinimalShareState(stateWithHiddenIds);

expect(minimal.showRelationshipLookupIds).toBe(false);
});

it('should produce a valid state for encodeStateToURL', () => {
const minimal = buildMinimalShareState(fullState);

Expand Down
22 changes: 16 additions & 6 deletions src/utils/drawioExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,10 @@ function calculateEntityHeight(
* Format multi-line relationship label for connector
* Escapes each component to prevent XML injection
*/
function formatRelationshipLabel(relationship: EntityRelationship): string {
function formatRelationshipLabel(
relationship: EntityRelationship,
showLookupIds: boolean = true
): string {
const lines: string[] = [];

// Line 1: Cardinality (escaped)
Expand All @@ -250,8 +253,8 @@ function formatRelationshipLabel(relationship: EntityRelationship): string {
// Line 2: Schema name (escaped)
lines.push(escapeXml(relationship.schemaName));

// Line 3: Field mapping (if available, escape each part)
if (relationship.referencingAttribute && relationship.referencedAttribute) {
// Line 3: Field mapping (if available and enabled, escape each part)
if (showLookupIds && relationship.referencingAttribute && relationship.referencedAttribute) {
lines.push(
`${escapeXml(relationship.referencingAttribute)} → ${escapeXml(relationship.referencedAttribute)}`
);
Expand Down Expand Up @@ -476,9 +479,10 @@ function generateConnectorCell(
id: string,
sourceId: string,
targetId: string,
relationship: EntityRelationship
relationship: EntityRelationship,
showLookupIds: boolean = true
): string {
const label = formatRelationshipLabel(relationship);
const label = formatRelationshipLabel(relationship, showLookupIds);

return ` <mxCell id="${id}" value="${label}" style="${CONNECTOR_STYLE}" edge="1" parent="1" source="${sourceId}" target="${targetId}">
<mxGeometry relative="1" as="geometry" />
Expand Down Expand Up @@ -600,7 +604,13 @@ function generateDrawioXml(options: DrawioExportOptions): string {

if (sourceId && targetId) {
const id = generateId('connector', index);
cells[cellIndex++] = generateConnectorCell(id, sourceId, targetId, rel);
cells[cellIndex++] = generateConnectorCell(
id,
sourceId,
targetId,
rel,
colorSettings.showRelationshipLookupIds ?? true
);
}

// Report progress for relationships (80-95% range)
Expand Down
33 changes: 30 additions & 3 deletions src/utils/urlStateCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as LZString from 'lz-string';
import type { EntityPosition } from '@/types';
import type { LayoutMode, FieldLabelMode } from '@/types/erdTypes';
import type { LayoutMode, FieldLabelMode, ColorSettings } from '@/types/erdTypes';
import type { SerializableState } from '@/types/snapshotTypes';

const CODEC_VERSION = '1.0.0';
Expand Down Expand Up @@ -35,6 +35,7 @@ export interface CompactState {
gf?: string; // groupFilter (optional, only present when not 'all')
sf?: Record<string, string[]>; // selectedFields (optional, only present when non-empty)
fo?: Record<string, string[]>; // fieldOrder (optional, only present when non-empty)
sl?: boolean; // showRelationshipLookupIds (optional, only present when false)
}

/**
Expand Down Expand Up @@ -94,6 +95,9 @@ export function buildMinimalShareState(state: SerializableState): MinimalShareSt
state.colorSettings.fieldLabelMode !== 'displayName' && {
fieldLabelMode: state.colorSettings.fieldLabelMode,
}),
...(state.colorSettings?.showRelationshipLookupIds === false && {
showRelationshipLookupIds: state.colorSettings.showRelationshipLookupIds,
}),
...(filteredFields &&
Object.keys(filteredFields).length > 0 && {
selectedFields: filteredFields,
Expand Down Expand Up @@ -149,6 +153,7 @@ export function encodeStateToURL(state: {
entityColorOverrides?: Record<string, string>;
groupNames?: Record<string, string>;
fieldLabelMode?: FieldLabelMode;
showRelationshipLookupIds?: boolean;
groupFilter?: string;
selectedFields?: Record<string, string[]>;
fieldOrder?: Record<string, string[]>;
Expand Down Expand Up @@ -203,6 +208,11 @@ export function encodeStateToURL(state: {
compactState.fo = state.fieldOrder;
}

// Only include showRelationshipLookupIds if false (default is true)
if (state.showRelationshipLookupIds === false) {
compactState.sl = false;
}

// Serialize to JSON
const json = JSON.stringify(compactState);

Expand Down Expand Up @@ -271,7 +281,11 @@ export function decodeStateFromURL(hash: string): DecodeResult {
* @param compact Compact state from URL
* @returns Partial serializable state for restoreState()
*/
export function expandCompactState(compact: CompactState): Partial<SerializableState> {
type ExpandedURLState = Omit<Partial<SerializableState>, 'colorSettings'> & {
colorSettings?: Partial<ColorSettings>;
};

export function expandCompactState(compact: CompactState): ExpandedURLState {
return {
selectedEntities: compact.e,
entityPositions: expandPositions(compact.p),
Expand All @@ -292,9 +306,13 @@ export function expandCompactState(compact: CompactState): Partial<SerializableS
...(compact.sf ? { selectedFields: compact.sf } : {}),
// Restore fieldOrder if present
...(compact.fo ? { fieldOrder: compact.fo } : {}),
// Restore showRelationshipLookupIds if present
...(compact.sl !== undefined
? { colorSettings: { showRelationshipLookupIds: compact.sl } }
: {}),
// Fields NOT restored from URL (use existing state or defaults):
// - collapsedEntities
// - colorSettings (except fieldLabelMode, handled below)
// - colorSettings (except fieldLabelMode and showRelationshipLookupIds, handled below)
// - showMinimap
// - isSmartZoom
// - edgeOffsets
Expand All @@ -309,6 +327,15 @@ export function getFieldLabelModeFromCompact(compact: CompactState): FieldLabelM
return compact.flm;
}

/**
* Extract showRelationshipLookupIds from CompactState (if present)
*/
export function getShowRelationshipLookupIdsFromCompact(
compact: CompactState
): boolean | undefined {
return compact.sl;
}

/**
* Get the proper base URL for sharing, preserving Dynamics 365 navigation context.
*
Expand Down
Loading