From ab841da32ced5c0f9b3469e5086fdff10ca91e6f Mon Sep 17 00:00:00 2001 From: David Bond Date: Sun, 4 Oct 2026 13:40:40 +0100 Subject: [PATCH] Reduce Codacy issues: format JS with Prettier, replace Write-Host, add ESLint config, tidy C# Co-Authored-By: Claude Sonnet 5.5 --- .eslintrc.json | 15 + Publish.ps1 | 22 +- SchemaMagic.Core/CoreSchemaAnalysisService.cs | 3 +- SchemaMagic.Web/Services/GitHubService.cs | 9 +- SchemaMagic.Web/wwwroot/js/app.js | 58 +- SchemaMagic.Web/wwwroot/service-worker.js | 81 +- SchemaMagic/GitHubService.cs | 12 +- SchemaMagic/Program.cs | 4 +- SchemaMagic/SchemaGenerator.cs | 8 +- SimpleTestDbContext.cs | 2 +- Templates/controls.js | 296 ++++---- Templates/event-listeners.js | 72 +- Templates/force-directed-layout.js | 309 +++++--- Templates/pan-zoom.js | 140 ++-- Templates/property-utilities.js | 588 +++++++++------ Templates/relationships.js | 698 +++++++++++------- Templates/schema-generation.js | 53 +- Templates/settings.js | 64 +- Templates/table-generation.js | 585 +++++++++------ Templates/table-grouping.js | 284 +++---- Templates/table-interaction.js | 197 ++--- Templates/variables.js | 495 ++++++++++--- original-template.cs | 192 ----- publish-manual.ps1 | 166 ++--- temp-original.txt | 192 ----- test-styles.css | 148 ---- 26 files changed, 2574 insertions(+), 2119 deletions(-) create mode 100644 .eslintrc.json delete mode 100644 original-template.cs delete mode 100644 temp-original.txt delete mode 100644 test-styles.css diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..227a0bc --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "root": true, + "env": { "browser": true, "es2021": true, "serviceworker": true }, + "parserOptions": { "ecmaVersion": 2021 }, + "extends": ["eslint:recommended"], + "overrides": [ + { + "files": ["Templates/*.js"], + "rules": { + "no-undef": "off", + "no-unused-vars": "off" + } + } + ] +} diff --git a/Publish.ps1 b/Publish.ps1 index 055245c..7506e9b 100644 --- a/Publish.ps1 +++ b/Publish.ps1 @@ -59,7 +59,7 @@ if ($LASTEXITCODE -ne 0) { exit 1 } $version = ($buildOutput | Select-Object -Last 1).ToString().Trim() -Write-Host "Version: $version" +Write-Information "Version: $version" -InformationAction Continue # Check if tag already exists $existingTag = git tag -l $version @@ -71,7 +71,7 @@ if ($existingTag) { # Create and push tag git tag $version git push origin $version -Write-Host "Tag $version pushed." +Write-Information "Tag $version pushed." -InformationAction Continue if ($SkipPublishVerification) { Write-Warning "Not waiting for the release run (-SkipPublishVerification). Nothing has confirmed that a package reached nuget.org." @@ -82,7 +82,7 @@ if ($SkipPublishVerification) { $originUrl = git remote get-url origin $repoFullName = ($originUrl -replace '^.*github\.com[:/]', '') -replace '\.git$', '' -Write-Host "Waiting for the release run for $version..." +Write-Information "Waiting for the release run for $version..." -InformationAction Continue # The run takes a few seconds to appear after the tag push. $runId = $null @@ -100,13 +100,13 @@ if (-not $runId) { exit 1 } -Write-Host "Run: https://github.com/$repoFullName/actions/runs/$runId" +Write-Information "Run: https://github.com/$repoFullName/actions/runs/$runId" -InformationAction Continue gh run watch $runId --repo $repoFullName --exit-status --interval 20 $runExitCode = $LASTEXITCODE if ($runExitCode -ne 0) { - Write-Host "" - Write-Host "The release run did not succeed: https://github.com/$repoFullName/actions/runs/$runId" -ForegroundColor Red + Write-Information "" -InformationAction Continue + Write-Information "The release run did not succeed: https://github.com/$repoFullName/actions/runs/$runId" -InformationAction Continue # A refused job β€” an exhausted Actions budget, for instance β€” fails before any step runs, so it # has no failed step to report. The check-run annotation is the only place the reason appears. @@ -114,14 +114,14 @@ if ($runExitCode -ne 0) { if ($LASTEXITCODE -eq 0 -and $jobId) { $annotation = gh api "repos/$repoFullName/check-runs/$jobId/annotations" --jq '.[0].message' 2>$null if ($LASTEXITCODE -eq 0 -and $annotation) { - Write-Host "Reason: $annotation" -ForegroundColor Red + Write-Information "Reason: $annotation" -InformationAction Continue } } - Write-Host "" - Write-Host "Tag $version is pushed but no package was published. Once the cause is fixed:" -ForegroundColor Yellow - Write-Host " gh run rerun $runId --repo $repoFullName --failed" -ForegroundColor Cyan + Write-Information "" -InformationAction Continue + Write-Information "Tag $version is pushed but no package was published. Once the cause is fixed:" -InformationAction Continue + Write-Information " gh run rerun $runId --repo $repoFullName --failed" -InformationAction Continue exit 1 } -Write-Host "Package $version published." -ForegroundColor Green \ No newline at end of file +Write-Information "Package $version published." -InformationAction Continue \ No newline at end of file diff --git a/SchemaMagic.Core/CoreSchemaAnalysisService.cs b/SchemaMagic.Core/CoreSchemaAnalysisService.cs index f7ca69a..cd2a202 100644 --- a/SchemaMagic.Core/CoreSchemaAnalysisService.cs +++ b/SchemaMagic.Core/CoreSchemaAnalysisService.cs @@ -552,7 +552,6 @@ private static int SearchProjectForEntities(string projectPath, IEnumerable> FindDbContextFilesAsync(string repoUrl, s { throw new InvalidOperationException("GitHub API rate limit exceeded. Please provide a Personal Access Token for higher limits, or try again later."); } - catch (InvalidOperationException) - { - // Re-throw our own exceptions - throw; - } - catch (Exception ex) + catch (Exception ex) when (ex is not InvalidOperationException) { throw new InvalidOperationException($"Error accessing GitHub repository: {ex.Message}"); } @@ -229,7 +224,9 @@ private static (string Owner, string Name)? ParseGitHubUrl(string url) { var uri = new Uri(url); if (uri.Host != "github.com") + { return null; + } var segments = uri.AbsolutePath.Trim('/').Split('/'); if (segments.Length >= 2) diff --git a/SchemaMagic.Web/wwwroot/js/app.js b/SchemaMagic.Web/wwwroot/js/app.js index 14c31cb..e8b3a62 100644 --- a/SchemaMagic.Web/wwwroot/js/app.js +++ b/SchemaMagic.Web/wwwroot/js/app.js @@ -1,43 +1,43 @@ window.openSchemaInNewTab = (htmlContent) => { - const blob = new Blob([htmlContent], { type: 'text/html' }); - const url = URL.createObjectURL(blob); - const newWindow = window.open(url, '_blank'); - - // Clean up the URL after a delay - setTimeout(() => URL.revokeObjectURL(url), 1000); + const blob = new Blob([htmlContent], { type: "text/html" }); + const url = URL.createObjectURL(blob); + const newWindow = window.open(url, "_blank"); + + // Clean up the URL after a delay + setTimeout(() => URL.revokeObjectURL(url), 1000); }; window.downloadFile = (fileName, content, contentType) => { - const blob = new Blob([content], { type: contentType }); - const url = URL.createObjectURL(blob); - - const link = document.createElement('a'); - link.href = url; - link.download = fileName; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - URL.revokeObjectURL(url); + const blob = new Blob([content], { type: contentType }); + const url = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + URL.revokeObjectURL(url); }; // Set focus on repository input when page loads window.setRepositoryInputFocus = () => { - const repoInput = document.getElementById('repoUrl'); - if (repoInput) { - repoInput.focus(); - } + const repoInput = document.getElementById("repoUrl"); + if (repoInput) { + repoInput.focus(); + } }; // Enhanced error handling -window.addEventListener('unhandledrejection', (event) => { - console.error('Unhandled promise rejection:', event.reason); +window.addEventListener("unhandledrejection", (event) => { + console.error("Unhandled promise rejection:", event.reason); }); // Performance monitoring -window.addEventListener('load', () => { - if ('performance' in window) { - const loadTime = performance.now(); - console.log(`SchemaMagic Web loaded in ${loadTime.toFixed(2)}ms`); - } -}); \ No newline at end of file +window.addEventListener("load", () => { + if ("performance" in window) { + const loadTime = performance.now(); + console.log(`SchemaMagic Web loaded in ${loadTime.toFixed(2)}ms`); + } +}); diff --git a/SchemaMagic.Web/wwwroot/service-worker.js b/SchemaMagic.Web/wwwroot/service-worker.js index 0ee9a70..6e6159f 100644 --- a/SchemaMagic.Web/wwwroot/service-worker.js +++ b/SchemaMagic.Web/wwwroot/service-worker.js @@ -1,39 +1,68 @@ // Caution! Be sure you understand the caveats before publishing an application with // offline support. See https://aka.ms/blazor-offline-considerations -self.importScripts('./service-worker-assets.js'); -self.addEventListener('install', event => event.waitUntil(onInstall(event))); -self.addEventListener('fetch', event => event.respondWith(onFetch(event))); +self.importScripts("./service-worker-assets.js"); +self.addEventListener("install", (event) => event.waitUntil(onInstall(event))); +self.addEventListener("fetch", (event) => event.respondWith(onFetch(event))); -const cacheNamePrefix = 'schemamagic-web-cache-'; +const cacheNamePrefix = "schemamagic-web-cache-"; const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; -const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; -const offlineAssetsExclude = [ /^service-worker\.js$/ ]; +const offlineAssetsInclude = [ + /\.dll$/, + /\.pdb$/, + /\.wasm/, + /\.html/, + /\.js$/, + /\.json$/, + /\.css$/, + /\.woff$/, + /\.png$/, + /\.jpe?g$/, + /\.gif$/, + /\.ico$/, + /\.blat$/, + /\.dat$/, +]; +const offlineAssetsExclude = [/^service-worker\.js$/]; async function onInstall(event) { - console.info('Service worker: Install'); + console.info("Service worker: Install"); - // Fetch and cache all matching items from the assets manifest - const assetsRequests = self.assetsManifest.assets - .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) - .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) - .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); - await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); + // Fetch and cache all matching items from the assets manifest + const assetsRequests = self.assetsManifest.assets + .filter((asset) => + offlineAssetsInclude.some((pattern) => pattern.test(asset.url)), + ) + .filter( + (asset) => + !offlineAssetsExclude.some((pattern) => + pattern.test(asset.url), + ), + ) + .map( + (asset) => + new Request(asset.url, { + integrity: asset.hash, + cache: "no-cache", + }), + ); + await caches.open(cacheName).then((cache) => cache.addAll(assetsRequests)); } async function onFetch(event) { - let cachedResponse = null; - if (event.request.method === 'GET') { - // For all navigation requests, try to serve index.html from cache - // If you need some URLs to be server-rendered, edit the following check to exclude those URLs - const shouldServeIndexHtml = event.request.mode === 'navigate' - && !event.request.url.includes('/api/') - && !event.request.url.includes('/_framework/'); + let cachedResponse = null; + if (event.request.method === "GET") { + // For all navigation requests, try to serve index.html from cache + // If you need some URLs to be server-rendered, edit the following check to exclude those URLs + const shouldServeIndexHtml = + event.request.mode === "navigate" && + !event.request.url.includes("/api/") && + !event.request.url.includes("/_framework/"); - const request = shouldServeIndexHtml ? 'index.html' : event.request; - const cache = await caches.open(cacheName); - cachedResponse = await cache.match(request); - } + const request = shouldServeIndexHtml ? "index.html" : event.request; + const cache = await caches.open(cacheName); + cachedResponse = await cache.match(request); + } - return cachedResponse || fetch(event.request); -} \ No newline at end of file + return cachedResponse || fetch(event.request); +} diff --git a/SchemaMagic/GitHubService.cs b/SchemaMagic/GitHubService.cs index ea162f3..b8e6750 100644 --- a/SchemaMagic/GitHubService.cs +++ b/SchemaMagic/GitHubService.cs @@ -201,11 +201,9 @@ private async Task> FindEntityFilesAsync( { // Prioritize files in common entity directories var path = item.Path.ToLower(); - if (path.Contains("/models/")) return 0; - if (path.Contains("/entities/")) return 1; - if (path.Contains("/domain/")) return 2; - if (path.Contains("/data/")) return 3; - return 4; + var priorityDirectories = new[] { "/models/", "/entities/", "/domain/", "/data/" }; + var priority = Array.FindIndex(priorityDirectories, directory => path.Contains(directory)); + return priority < 0 ? priorityDirectories.Length : priority; }) .ThenBy(item => item.Path) .Take(500) // Limit to prevent too many API calls @@ -218,7 +216,9 @@ private async Task> FindEntityFilesAsync( { // Stop if we found all entities if (remainingEntities.Count == 0) + { break; + } try { @@ -350,7 +350,9 @@ private static (string Owner, string Name)? ParseGitHubUrl(string url) { var uri = new Uri(url); if (uri.Host != "github.com") + { return null; + } var segments = uri.AbsolutePath.Trim('/').Split('/'); if (segments.Length >= 2) diff --git a/SchemaMagic/Program.cs b/SchemaMagic/Program.cs index fb8b421..18e2ea9 100644 --- a/SchemaMagic/Program.cs +++ b/SchemaMagic/Program.cs @@ -6,7 +6,7 @@ namespace SchemaMagic; -internal class Program +internal static class Program { static async Task Main(string[] args) { @@ -109,7 +109,7 @@ GitHub repository (private with PAT): πŸ“¦ NuGet: https://www.nuget.org/packages/SchemaMagic "; - rootCommand.SetAction(async (parseResult, cancellationToken) => + rootCommand.SetAction(async (parseResult, _) => { var dbContextFile = parseResult.GetValue(dbContextFileArgument); var githubRepo = parseResult.GetValue(githubRepoOption); diff --git a/SchemaMagic/SchemaGenerator.cs b/SchemaMagic/SchemaGenerator.cs index 38c9ee3..36825ef 100644 --- a/SchemaMagic/SchemaGenerator.cs +++ b/SchemaMagic/SchemaGenerator.cs @@ -80,10 +80,6 @@ public string GenerateSchemaVisualization() Console.WriteLine("============================================================"); Console.WriteLine($"πŸ” Processing DbContext: {Path.GetFileName(_dbContextPath)}"); - // Read DbContext content - var sourceCode = File.ReadAllText(_dbContextPath); - var fileName = Path.GetFileName(_dbContextPath); - // Use the Core library for analysis with the actual file path for better entity discovery var result = CoreSchemaAnalysisService.AnalyzeDbContextFile(_dbContextPath); @@ -120,8 +116,8 @@ public string GenerateSchemaVisualization() { var customCss = File.ReadAllText(_customCssPath); var entitiesJson = JsonSerializer.Serialize(result.Entities, _jsonSerializerOptions); - var documentGuid = _documentGuid ?? Guid.NewGuid().ToString(); - htmlContent = ModularHtmlTemplate.GenerateWithCustomCss(entitiesJson, documentGuid, customCss); + var customCssGuid = _documentGuid ?? Guid.NewGuid().ToString(); + htmlContent = ModularHtmlTemplate.GenerateWithCustomCss(entitiesJson, customCssGuid, customCss); } else { diff --git a/SimpleTestDbContext.cs b/SimpleTestDbContext.cs index c2006aa..ed630aa 100644 --- a/SimpleTestDbContext.cs +++ b/SimpleTestDbContext.cs @@ -155,7 +155,7 @@ public class User public bool IsActive { get; set; } = true; [Comment("Email verification status - true when user confirms email")] - public bool EmailVerified { get; set; } = false; + public bool EmailVerified { get; set; } /// /// Timestamp of the user's last successful login diff --git a/Templates/controls.js b/Templates/controls.js index 97ccd7a..e3acdef 100644 --- a/Templates/controls.js +++ b/Templates/controls.js @@ -6,21 +6,21 @@ let legendVisible = true; function updateButtonStates() { // Update all button states to reflect current settings - const relationshipsBtn = document.getElementById('relationships-btn'); - const navPropsBtn = document.getElementById('nav-props-btn'); - const inheritedPropsBtn = document.getElementById('inherited-props-btn'); - const fullHeightBtn = document.getElementById('full-height-btn'); - const snapGridBtn = document.getElementById('snap-grid-btn'); - const legendToggleBtn = document.getElementById('legend-toggle-btn'); + const relationshipsBtn = document.getElementById("relationships-btn"); + const navPropsBtn = document.getElementById("nav-props-btn"); + const inheritedPropsBtn = document.getElementById("inherited-props-btn"); + const fullHeightBtn = document.getElementById("full-height-btn"); + const snapGridBtn = document.getElementById("snap-grid-btn"); + const legendToggleBtn = document.getElementById("legend-toggle-btn"); // Helper function to update toggle indicator const updateToggleIndicator = (button, isActive) => { if (button) { - const indicator = button.querySelector('.toggle-indicator'); + const indicator = button.querySelector(".toggle-indicator"); if (indicator) { - indicator.style.opacity = isActive ? '1' : '0'; + indicator.style.opacity = isActive ? "1" : "0"; } - button.classList.toggle('active', isActive); + button.classList.toggle("active", isActive); } }; @@ -39,9 +39,9 @@ function updateButtonStates() { // Snap to grid button updateToggleIndicator(snapGridBtn, snapToGrid); // Also update grid background visibility - const gridBg = document.getElementById('grid-background'); + const gridBg = document.getElementById("grid-background"); if (gridBg) { - gridBg.style.display = snapToGrid ? 'block' : 'none'; + gridBg.style.display = snapToGrid ? "block" : "none"; } // Legend toggle button @@ -81,9 +81,9 @@ function toggleSnapToGrid() { function toggleLegend() { legendVisible = !legendVisible; - const legend = document.getElementById('legend'); + const legend = document.getElementById("legend"); if (legend) { - legend.style.display = legendVisible ? 'block' : 'none'; + legend.style.display = legendVisible ? "block" : "none"; } updateButtonStates(); saveSettings(); @@ -91,9 +91,9 @@ function toggleLegend() { function toggleToolbar() { toolbarVisible = !toolbarVisible; - const toolbar = document.getElementById('toolbar'); + const toolbar = document.getElementById("toolbar"); if (toolbar) { - toolbar.style.display = toolbarVisible ? 'flex' : 'none'; + toolbar.style.display = toolbarVisible ? "flex" : "none"; } // Don't save toolbar state - it should always start visible } @@ -107,7 +107,7 @@ function autoLayoutTables() { // Persist the new positions so they survive reloads saveSettings(); - console.log('πŸͺ„ Auto layout applied and saved'); + console.log("πŸͺ„ Auto layout applied and saved"); } function downloadSchema() { @@ -130,42 +130,46 @@ function downloadSchema() { showInheritedProperties, fullHeightMode, snapToGrid, - legendVisible + legendVisible, }, // Capture selected table if any selectedTable, - showOnlySelectedRelations + showOnlySelectedRelations, }; // Extract current table positions - document.querySelectorAll('.table-group').forEach(table => { - const entityName = table.getAttribute('data-entity'); - const rect = table.querySelector('.table-box'); + document.querySelectorAll(".table-group").forEach((table) => { + const entityName = table.getAttribute("data-entity"); + const rect = table.querySelector(".table-box"); currentState.tablePositions[entityName] = { - x: parseFloat(rect.getAttribute('x')), - y: parseFloat(rect.getAttribute('y')) + x: parseFloat(rect.getAttribute("x")), + y: parseFloat(rect.getAttribute("y")), }; }); // Get the complete HTML document including DOCTYPE - const doctype = document.doctype ? - `` : - ''; + const doctype = document.doctype + ? `` + : ""; - const currentHtml = doctype + '\n' + document.documentElement.outerHTML; + const currentHtml = doctype + "\n" + document.documentElement.outerHTML; // Create a modified version that embeds the current state with new GUID - const modifiedHtml = embedCurrentStateIntoHtml(currentHtml, currentState, newDocumentGuid); + const modifiedHtml = embedCurrentStateIntoHtml( + currentHtml, + currentState, + newDocumentGuid, + ); // Create a blob and download link - const blob = new Blob([modifiedHtml], { type: 'text/html' }); + const blob = new Blob([modifiedHtml], { type: "text/html" }); const url = URL.createObjectURL(blob); // Create download link with descriptive filename - const link = document.createElement('a'); + const link = document.createElement("a"); link.href = url; - const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); + const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, "-"); const zoomLevel = Math.round(currentZoom * 100); link.download = `schema-customized-${timestamp}-zoom${zoomLevel}%.html`; @@ -177,15 +181,20 @@ function downloadSchema() { // Clean up URL.revokeObjectURL(url); - console.log(`πŸ“₯ Downloaded schema with new GUID: ${newDocumentGuid} (includes DOCTYPE for Standards Mode)`); + console.log( + `πŸ“₯ Downloaded schema with new GUID: ${newDocumentGuid} (includes DOCTYPE for Standards Mode)`, + ); } function generateGuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( + /[xy]/g, + function (c) { + const r = (Math.random() * 16) | 0; + const v = c == "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }, + ); } function embedCurrentStateIntoHtml(html, state, newDocumentGuid) { @@ -195,11 +204,14 @@ function embedCurrentStateIntoHtml(html, state, newDocumentGuid) { html = html.replace(guidRegex, newGuidDeclaration); // Find the entities JSON in the HTML - improve the regex to be more robust - const entitiesRegex = /const entities = (\{[\s\S]*?\});(?=\s*\/\/|\s*const|\s*let|\s*var|\s*function|\s*document|\s*$)/; + const entitiesRegex = + /const entities = (\{[\s\S]*?\});(?=\s*\/\/|\s*const|\s*let|\s*var|\s*function|\s*document|\s*$)/; const match = html.match(entitiesRegex); if (!match) { - console.warn('Could not find entities JSON in HTML, downloading without entities - document may not be fully interactive'); + console.warn( + "Could not find entities JSON in HTML, downloading without entities - document may not be fully interactive", + ); // Continue anyway - the state injection might still make it partially functional } @@ -209,101 +221,105 @@ function embedCurrentStateIntoHtml(html, state, newDocumentGuid) { // Create a localStorage implementation that starts with embedded data but allows full editing const stateInjectionScript = - '// Fully editable downloaded document with embedded initial state\n' + - '(function() {\n' + - ' const embeddedStateBase64 = \'' + stateBase64 + '\';\n' + - ' const newDocumentGuid = \'' + newDocumentGuid + '\';\n' + - ' \n' + - ' try {\n' + - ' // Decode the embedded state\n' + - ' const stateJson = decodeURIComponent(escape(atob(embeddedStateBase64)));\n' + - ' const embeddedState = JSON.parse(stateJson);\n' + - ' \n' + - ' // Create the new document-specific localStorage keys\n' + - ' const newStorageKeys = {\n' + - ' tablePositions: `schemaMagic_${newDocumentGuid}_tablePositions`,\n' + - ' viewSettings: `schemaMagic_${newDocumentGuid}_viewSettings`,\n' + - ' viewBox: `schemaMagic_${newDocumentGuid}_viewBox`\n' + - ' };\n' + - ' \n' + - ' // Initialize localStorage with embedded state for new document GUID\n' + - ' // This creates a fully functional localStorage setup\n' + - ' if (embeddedState.tablePositions) {\n' + - ' localStorage.setItem(newStorageKeys.tablePositions, JSON.stringify(embeddedState.tablePositions));\n' + - ' }\n' + - ' if (embeddedState.settings) {\n' + - ' localStorage.setItem(newStorageKeys.viewSettings, JSON.stringify(embeddedState.settings));\n' + - ' }\n' + - ' if (embeddedState.viewBox) {\n' + - ' localStorage.setItem(newStorageKeys.viewBox, JSON.stringify(embeddedState.viewBox));\n' + - ' }\n' + - ' \n' + - ' // No need to override localStorage - the new GUID will work with normal localStorage\n' + - ' console.log(\'πŸ“¦ Downloaded document initialized with embedded state as localStorage\');\n' + - ' console.log(\'πŸ†” New document GUID:\', newDocumentGuid);\n' + - ' console.log(\'✏️ Document is fully editable - changes will be saved to localStorage\');\n' + - ' console.log(\'πŸ“Š Embedded state:\', {\n' + - ' tableCount: Object.keys(embeddedState.tablePositions || {}).length,\n' + - ' zoom: embeddedState.zoom,\n' + - ' settings: embeddedState.settings\n' + - ' });\n' + - ' \n' + - ' } catch (error) {\n' + - ' console.error(\'❌ Failed to initialize embedded state:\', error);\n' + - ' console.log(\'πŸ”„ Document will start with default layout and be fully editable\');\n' + - ' }\n' + - '})();\n' + - '\n' + - '// Restore state and ensure normal functionality after DOM is loaded\n' + - 'document.addEventListener(\'DOMContentLoaded\', function() {\n' + - ' setTimeout(function() {\n' + - ' try {\n' + - ' // Load settings normally - localStorage now contains the embedded data\n' + - ' loadSettings();\n' + - ' \n' + - ' // Generate schema - this will use saved positions or auto-optimize if none exist\n' + - ' generateSchema();\n' + - ' \n' + - ' // Update button states to reflect loaded settings\n' + - ' updateButtonStates();\n' + - ' \n' + - ' // Restore view state if available\n' + - ' const savedViewBox = localStorage.getItem(STORAGE_KEYS.viewBox);\n' + - ' if (savedViewBox) {\n' + - ' try {\n' + - ' const viewBoxData = JSON.parse(savedViewBox);\n' + - ' if (viewBoxData.zoom) {\n' + - ' currentZoom = viewBoxData.zoom;\n' + - ' }\n' + - ' if (viewBoxData.viewBox) {\n' + - ' svgViewBox = { ...viewBoxData.viewBox };\n' + - ' updateViewBox();\n' + - ' }\n' + - ' } catch (e) {\n' + - ' console.warn(\'Could not restore view state:\', e);\n' + - ' }\n' + - ' }\n' + - ' \n' + - ' // Ensure event handlers are properly set up\n' + - ' setupEventListeners();\n' + - ' \n' + - ' console.log(\'βœ… Downloaded document fully restored and ready for editing\');\n' + - ' console.log(\'πŸ’Ύ All changes will be saved to localStorage with GUID:\', DOCUMENT_GUID);\n' + - ' \n' + - ' } catch (error) {\n' + - ' console.error(\'❌ Failed to restore downloaded document state:\', error);\n' + - ' console.log(\'πŸ”„ Falling back to default state - document is still fully functional\');\n' + - ' // Try to set up basic functionality even if restore fails\n' + - ' try {\n' + - ' setupEventListeners();\n' + - ' generateSchema();\n' + - ' updateButtonStates();\n' + - ' } catch (fallbackError) {\n' + - ' console.error(\'❌ Fallback initialization also failed:\', fallbackError);\n' + - ' }\n' + - ' }\n' + - ' }, 100);\n' + - '});'; + "// Fully editable downloaded document with embedded initial state\n" + + "(function() {\n" + + " const embeddedStateBase64 = '" + + stateBase64 + + "';\n" + + " const newDocumentGuid = '" + + newDocumentGuid + + "';\n" + + " \n" + + " try {\n" + + " // Decode the embedded state\n" + + " const stateJson = decodeURIComponent(escape(atob(embeddedStateBase64)));\n" + + " const embeddedState = JSON.parse(stateJson);\n" + + " \n" + + " // Create the new document-specific localStorage keys\n" + + " const newStorageKeys = {\n" + + " tablePositions: `schemaMagic_${newDocumentGuid}_tablePositions`,\n" + + " viewSettings: `schemaMagic_${newDocumentGuid}_viewSettings`,\n" + + " viewBox: `schemaMagic_${newDocumentGuid}_viewBox`\n" + + " };\n" + + " \n" + + " // Initialize localStorage with embedded state for new document GUID\n" + + " // This creates a fully functional localStorage setup\n" + + " if (embeddedState.tablePositions) {\n" + + " localStorage.setItem(newStorageKeys.tablePositions, JSON.stringify(embeddedState.tablePositions));\n" + + " }\n" + + " if (embeddedState.settings) {\n" + + " localStorage.setItem(newStorageKeys.viewSettings, JSON.stringify(embeddedState.settings));\n" + + " }\n" + + " if (embeddedState.viewBox) {\n" + + " localStorage.setItem(newStorageKeys.viewBox, JSON.stringify(embeddedState.viewBox));\n" + + " }\n" + + " \n" + + " // No need to override localStorage - the new GUID will work with normal localStorage\n" + + " console.log('πŸ“¦ Downloaded document initialized with embedded state as localStorage');\n" + + " console.log('πŸ†” New document GUID:', newDocumentGuid);\n" + + " console.log('✏️ Document is fully editable - changes will be saved to localStorage');\n" + + " console.log('πŸ“Š Embedded state:', {\n" + + " tableCount: Object.keys(embeddedState.tablePositions || {}).length,\n" + + " zoom: embeddedState.zoom,\n" + + " settings: embeddedState.settings\n" + + " });\n" + + " \n" + + " } catch (error) {\n" + + " console.error('❌ Failed to initialize embedded state:', error);\n" + + " console.log('πŸ”„ Document will start with default layout and be fully editable');\n" + + " }\n" + + "})();\n" + + "\n" + + "// Restore state and ensure normal functionality after DOM is loaded\n" + + "document.addEventListener('DOMContentLoaded', function() {\n" + + " setTimeout(function() {\n" + + " try {\n" + + " // Load settings normally - localStorage now contains the embedded data\n" + + " loadSettings();\n" + + " \n" + + " // Generate schema - this will use saved positions or auto-optimize if none exist\n" + + " generateSchema();\n" + + " \n" + + " // Update button states to reflect loaded settings\n" + + " updateButtonStates();\n" + + " \n" + + " // Restore view state if available\n" + + " const savedViewBox = localStorage.getItem(STORAGE_KEYS.viewBox);\n" + + " if (savedViewBox) {\n" + + " try {\n" + + " const viewBoxData = JSON.parse(savedViewBox);\n" + + " if (viewBoxData.zoom) {\n" + + " currentZoom = viewBoxData.zoom;\n" + + " }\n" + + " if (viewBoxData.viewBox) {\n" + + " svgViewBox = { ...viewBoxData.viewBox };\n" + + " updateViewBox();\n" + + " }\n" + + " } catch (e) {\n" + + " console.warn('Could not restore view state:', e);\n" + + " }\n" + + " }\n" + + " \n" + + " // Ensure event handlers are properly set up\n" + + " setupEventListeners();\n" + + " \n" + + " console.log('βœ… Downloaded document fully restored and ready for editing');\n" + + " console.log('πŸ’Ύ All changes will be saved to localStorage with GUID:', DOCUMENT_GUID);\n" + + " \n" + + " } catch (error) {\n" + + " console.error('❌ Failed to restore downloaded document state:', error);\n" + + " console.log('πŸ”„ Falling back to default state - document is still fully functional');\n" + + " // Try to set up basic functionality even if restore fails\n" + + " try {\n" + + " setupEventListeners();\n" + + " generateSchema();\n" + + " updateButtonStates();\n" + + " } catch (fallbackError) {\n" + + " console.error('❌ Fallback initialization also failed:', fallbackError);\n" + + " }\n" + + " }\n" + + " }, 100);\n" + + "});"; // ⚠️ CRITICAL: DO NOT CHANGE THE LINE BELOW! ⚠️ // The scriptEndTag MUST be '/script>' NOT the full closing tag with < bracket @@ -313,19 +329,23 @@ function embedCurrentStateIntoHtml(html, state, newDocumentGuid) { // tag terminates the script block even when it appears inside quotes or comments. // Using '/script>' (without the < bracket) avoids this issue entirely. // 🚨 NEVER "fix" this to include the opening < bracket! 🚨 - const scriptEndTag = '/script>'; + const scriptEndTag = "/script>"; const scriptEndIndex = html.lastIndexOf(scriptEndTag); if (scriptEndIndex === -1) { - console.warn('Could not find script tag to inject state, downloading without state'); + console.warn( + "Could not find script tag to inject state, downloading without state", + ); return html; } // Insert the state initialization script before the closing script tag. const modifiedHtml = html.substring(0, scriptEndIndex - 1) + - '\n\t\t' + stateInjectionScript + '\n\t' + + "\n\t\t" + + stateInjectionScript + + "\n\t" + html.substring(scriptEndIndex - 1); return modifiedHtml; -} \ No newline at end of file +} diff --git a/Templates/event-listeners.js b/Templates/event-listeners.js index 55185cd..3f8949a 100644 --- a/Templates/event-listeners.js +++ b/Templates/event-listeners.js @@ -1,65 +1,73 @@ // Event listeners and initialization -document.addEventListener('DOMContentLoaded', function () { - console.log('?? SchemaMagic: DOM loaded, initializing...'); - console.log('?? Entities found:', typeof entities !== 'undefined' ? Object.keys(entities).length : 'UNDEFINED'); - console.log('?? Document GUID:', typeof DOCUMENT_GUID !== 'undefined' ? DOCUMENT_GUID : 'UNDEFINED'); +document.addEventListener("DOMContentLoaded", function () { + console.log("?? SchemaMagic: DOM loaded, initializing..."); + console.log( + "?? Entities found:", + typeof entities !== "undefined" + ? Object.keys(entities).length + : "UNDEFINED", + ); + console.log( + "?? Document GUID:", + typeof DOCUMENT_GUID !== "undefined" ? DOCUMENT_GUID : "UNDEFINED", + ); try { loadSettings(); - console.log('?? Settings loaded successfully'); + console.log("?? Settings loaded successfully"); } catch (e) { - console.error('? Failed to load settings:', e); + console.error("? Failed to load settings:", e); } try { loadTableGroupingRules(); - console.log('?? Table grouping rules loaded successfully'); + console.log("?? Table grouping rules loaded successfully"); } catch (e) { - console.error('? Failed to load table grouping rules:', e); + console.error("? Failed to load table grouping rules:", e); } try { generateSchema(); - console.log('?? Schema generated successfully'); + console.log("?? Schema generated successfully"); } catch (e) { - console.error('? Failed to generate schema:', e); + console.error("? Failed to generate schema:", e); } // Load view state AFTER schema has been generated (so svgViewBox is initialized) try { const loaded = loadViewState(); if (loaded) { - console.log('?? Zoom/pan state restored from previous session'); + console.log("?? Zoom/pan state restored from previous session"); } else { - console.log('?? No saved zoom/pan state found - using defaults'); + console.log("?? No saved zoom/pan state found - using defaults"); } } catch (e) { - console.error('? Failed to load view state:', e); + console.error("? Failed to load view state:", e); } try { setupEventListeners(); - console.log('?? Event listeners set up successfully'); + console.log("?? Event listeners set up successfully"); } catch (e) { - console.error('? Failed to set up event listeners:', e); + console.error("? Failed to set up event listeners:", e); } }); function setupEventListeners() { - const container = document.getElementById('schema-container'); - const svg = document.getElementById('schema-svg'); - const backgroundArea = document.getElementById('background-pan-area'); + const container = document.getElementById("schema-container"); + const svg = document.getElementById("schema-svg"); + const backgroundArea = document.getElementById("background-pan-area"); - backgroundArea.addEventListener('mousedown', startBackgroundPan); - container.addEventListener('mousemove', handlePan); - container.addEventListener('mouseup', endPan); - container.addEventListener('mouseleave', endPan); + backgroundArea.addEventListener("mousedown", startBackgroundPan); + container.addEventListener("mousemove", handlePan); + container.addEventListener("mouseup", endPan); + container.addEventListener("mouseleave", endPan); - svg.addEventListener('wheel', handleWheel); - svg.addEventListener('contextmenu', e => e.preventDefault()); + svg.addEventListener("wheel", handleWheel); + svg.addEventListener("contextmenu", (e) => e.preventDefault()); // Add click anywhere to show toolbar - container.addEventListener('click', function (e) { + container.addEventListener("click", function (e) { // If toolbar is hidden and user clicks anywhere, show it if (!toolbarVisible) { toggleToolbar(); @@ -67,13 +75,13 @@ function setupEventListeners() { }); // Add keyboard shortcuts - document.addEventListener('keydown', function (e) { - if (e.key === 'Escape' && selectedTable) { + document.addEventListener("keydown", function (e) { + if (e.key === "Escape" && selectedTable) { clearSelection(); e.preventDefault(); } // Add Ctrl+H to toggle toolbar - if (e.ctrlKey && e.key === 'h') { + if (e.ctrlKey && e.key === "h") { toggleToolbar(); e.preventDefault(); } @@ -88,7 +96,7 @@ function clearSelection() { // Enhanced background pan handling with selection clearing function startBackgroundPan(e) { // Only pan if clicking on background, not on table elements - if (e.target.closest('.table-group')) return; + if (e.target.closest(".table-group")) return; // Clear selection when clicking on background if (selectedTable) { @@ -98,8 +106,8 @@ function startBackgroundPan(e) { isPanning = true; panStart = { x: e.clientX - svgViewBox.x, - y: e.clientY - svgViewBox.y + y: e.clientY - svgViewBox.y, }; - document.getElementById('schema-container').style.cursor = 'grabbing'; + document.getElementById("schema-container").style.cursor = "grabbing"; e.preventDefault(); -} \ No newline at end of file +} diff --git a/Templates/force-directed-layout.js b/Templates/force-directed-layout.js index 21aaee6..b7cd423 100644 --- a/Templates/force-directed-layout.js +++ b/Templates/force-directed-layout.js @@ -12,15 +12,15 @@ // ======================================== const LAYOUT_CONFIG = { - columnGap: 550, // Horizontal corridor between columns (room for orthogonal edge routing + crow's feet) - rowGap: 130, // Vertical space between stacked tables in a column - componentGap: 700, // Space between separate connected components - singletonGap: 150, // Grid spacing between unconnected tables - orderingSweeps: 8, // Barycenter crossing-reduction passes - alignmentPasses: 12, // Vertical neighbor-alignment relaxation passes - minColumnHeight: 3000, // Never split a column shorter than this - maxColumnHeight: 13000, // Hard cap: split taller columns into side-by-side banks - gridSize: 50, // Snap-to-grid size + columnGap: 550, // Horizontal corridor between columns (room for orthogonal edge routing + crow's feet) + rowGap: 130, // Vertical space between stacked tables in a column + componentGap: 700, // Space between separate connected components + singletonGap: 150, // Grid spacing between unconnected tables + orderingSweeps: 8, // Barycenter crossing-reduction passes + alignmentPasses: 12, // Vertical neighbor-alignment relaxation passes + minColumnHeight: 3000, // Never split a column shorter than this + maxColumnHeight: 13000, // Hard cap: split taller columns into side-by-side banks + gridSize: 50, // Snap-to-grid size }; /** @@ -32,11 +32,18 @@ function calculateSmartTablePositions() { const entityNames = Object.keys(entities); if (hasSavedDocumentState()) { - console.log(`πŸ“„ Document ${DOCUMENT_GUID}: Using saved positions (no auto-layout)`); - return convertSavedToStandardFormat(loadSavedDocumentPositions(), entityNames); + console.log( + `πŸ“„ Document ${DOCUMENT_GUID}: Using saved positions (no auto-layout)`, + ); + return convertSavedToStandardFormat( + loadSavedDocumentPositions(), + entityNames, + ); } - console.log(`🎯 Document ${DOCUMENT_GUID}: No saved state found, computing smart layout...`); + console.log( + `🎯 Document ${DOCUMENT_GUID}: No saved state found, computing smart layout...`, + ); return computeSmartLayout(entityNames); } @@ -55,7 +62,7 @@ function loadSavedDocumentPositions() { return JSON.parse(saved); } } catch (e) { - console.warn('Failed to load saved document positions:', e); + console.warn("Failed to load saved document positions:", e); } return null; } @@ -66,7 +73,7 @@ function loadSavedDocumentPositions() { function convertSavedToStandardFormat(savedPositions, entityNames) { const positions = {}; - entityNames.forEach(entityName => { + entityNames.forEach((entityName) => { let x, y; if (savedPositions && savedPositions[entityName]) { x = savedPositions[entityName].x; @@ -98,7 +105,9 @@ function computeSmartLayout(entityNames) { const relationships = extractRelationships(); const graph = buildLayoutGraph(entityNames, relationships); const dims = {}; - entityNames.forEach(name => { dims[name] = getTableDimensions(name); }); + entityNames.forEach((name) => { + dims[name] = getTableDimensions(name); + }); // Split into connected components (deterministic order) const components = findConnectedComponents(entityNames, graph); @@ -106,7 +115,7 @@ function computeSmartLayout(entityNames) { const blocks = []; const singletons = []; - components.forEach(component => { + components.forEach((component) => { if (component.length === 1) { singletons.push(component[0]); } else { @@ -120,7 +129,9 @@ function computeSmartLayout(entityNames) { blocks.push(layoutSingletonGrid(singletons, dims)); } - console.log(`🧩 Smart layout: ${components.length} components (${singletons.length} unconnected tables)`); + console.log( + `🧩 Smart layout: ${components.length} components (${singletons.length} unconnected tables)`, + ); // Pack component blocks into a compact overall arrangement const packed = packBlocks(blocks); @@ -135,13 +146,21 @@ function computeSmartLayout(entityNames) { */ function buildLayoutGraph(entityNames, relationships) { const adjacency = {}; - entityNames.forEach(name => { adjacency[name] = new Map(); }); + entityNames.forEach((name) => { + adjacency[name] = new Map(); + }); - relationships.forEach(rel => { + relationships.forEach((rel) => { if (rel.from === rel.to) return; // self-loops don't influence placement if (!adjacency[rel.from] || !adjacency[rel.to]) return; - adjacency[rel.from].set(rel.to, (adjacency[rel.from].get(rel.to) || 0) + 1); - adjacency[rel.to].set(rel.from, (adjacency[rel.to].get(rel.from) || 0) + 1); + adjacency[rel.from].set( + rel.to, + (adjacency[rel.from].get(rel.to) || 0) + 1, + ); + adjacency[rel.to].set( + rel.from, + (adjacency[rel.to].get(rel.from) || 0) + 1, + ); }); return adjacency; @@ -155,7 +174,7 @@ function findConnectedComponents(entityNames, graph) { const components = []; const sortedNames = [...entityNames].sort(); - sortedNames.forEach(start => { + sortedNames.forEach((start) => { if (visited.has(start)) return; const component = []; @@ -166,7 +185,7 @@ function findConnectedComponents(entityNames, graph) { const node = queue.shift(); component.push(node); const neighbors = [...graph[node].keys()].sort(); - neighbors.forEach(neighbor => { + neighbors.forEach((neighbor) => { if (!visited.has(neighbor)) { visited.add(neighbor); queue.push(neighbor); @@ -190,10 +209,12 @@ function layoutComponent(nodes, graph, dims) { // --- 1. Layer assignment (columns) --- const layerOf = assignLayers(nodes, graph); let layerCount = 0; - nodes.forEach(n => { layerCount = Math.max(layerCount, layerOf[n] + 1); }); + nodes.forEach((n) => { + layerCount = Math.max(layerCount, layerOf[n] + 1); + }); const layers = Array.from({ length: layerCount }, () => []); - [...nodes].sort().forEach(n => layers[layerOf[n]].push(n)); + [...nodes].sort().forEach((n) => layers[layerOf[n]].push(n)); // --- 2. Crossing reduction (barycenter ordering sweeps) --- orderLayers(layers, graph); @@ -206,7 +227,7 @@ function layoutComponent(nodes, graph, dims) { const columnX = []; let cumX = 0; columns.forEach((column, index) => { - const colWidth = Math.max(...column.map(n => dims[n].width)); + const colWidth = Math.max(...column.map((n) => dims[n].width)); columnX[index] = { x: cumX, width: colWidth }; cumX += colWidth + LAYOUT_CONFIG.columnGap; }); @@ -217,11 +238,13 @@ function layoutComponent(nodes, graph, dims) { columns.forEach((column, colIndex) => { const stackHeight = columnStackHeight(column, dims); let y = -stackHeight / 2; - column.forEach(name => { + column.forEach((name) => { const d = dims[name]; pos[name] = { - x: columnX[colIndex].x + (columnX[colIndex].width - d.width) / 2, - y: y + x: + columnX[colIndex].x + + (columnX[colIndex].width - d.width) / 2, + y: y, }; columnOf[name] = colIndex; y += d.height + LAYOUT_CONFIG.rowGap; @@ -235,18 +258,21 @@ function layoutComponent(nodes, graph, dims) { for (let pass = 0; pass < LAYOUT_CONFIG.alignmentPasses; pass++) { const forward = pass % 2 === 0; const allowReorder = pass < LAYOUT_CONFIG.alignmentPasses / 2; - const indices = forward ? - columns.map((_, i) => i) : - columns.map((_, i) => columns.length - 1 - i); + const indices = forward + ? columns.map((_, i) => i) + : columns.map((_, i) => columns.length - 1 - i); - indices.forEach(colIndex => { + indices.forEach((colIndex) => { relaxColumn(columns[colIndex], graph, dims, pos, allowReorder); }); } // --- 5. Normalize to (0, 0) and measure --- - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - nodes.forEach(name => { + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity; + nodes.forEach((name) => { const d = dims[name]; minX = Math.min(minX, pos[name].x); minY = Math.min(minY, pos[name].y); @@ -255,10 +281,10 @@ function layoutComponent(nodes, graph, dims) { }); const positions = {}; - nodes.forEach(name => { + nodes.forEach((name) => { positions[name] = { x: pos[name].x - minX, - y: pos[name].y - minY + y: pos[name].y - minY, }; }); @@ -275,21 +301,24 @@ function assignLayers(nodes, graph) { const sorted = [...nodes].sort(); // Deterministic starting point: highest weighted degree - const weightedDegree = name => { + const weightedDegree = (name) => { let sum = 0; - graph[name].forEach(weight => { sum += weight; }); + graph[name].forEach((weight) => { + sum += weight; + }); return sum; }; let start = sorted[0]; - sorted.forEach(n => { + sorted.forEach((n) => { if (weightedDegree(n) > weightedDegree(start)) start = n; }); // Double BFS: farthest node from the hub is a pseudo-peripheral root const firstPass = bfsDistances(start, graph); let root = start; - sorted.forEach(n => { - if (firstPass[n] !== undefined && firstPass[n] > (firstPass[root] || 0)) root = n; + sorted.forEach((n) => { + if (firstPass[n] !== undefined && firstPass[n] > (firstPass[root] || 0)) + root = n; }); return bfsDistances(root, graph); @@ -305,7 +334,7 @@ function bfsDistances(root, graph) { while (queue.length > 0) { const node = queue.shift(); const neighbors = [...graph[node].keys()].sort(); - neighbors.forEach(neighbor => { + neighbors.forEach((neighbor) => { if (dist[neighbor] === undefined) { dist[neighbor] = dist[node] + 1; queue.push(neighbor); @@ -324,12 +353,15 @@ function bfsDistances(root, graph) { function orderLayers(layers, graph) { // Current index of each node within its layer const indexOf = {}; - const refreshIndices = layer => layer.forEach((n, i) => { indexOf[n] = i; }); + const refreshIndices = (layer) => + layer.forEach((n, i) => { + indexOf[n] = i; + }); layers.forEach(refreshIndices); const sortByBarycenter = (layer, referenceLayer) => { const referenceSet = new Set(referenceLayer); - const keyed = layer.map(name => { + const keyed = layer.map((name) => { let weightSum = 0; let weightedIndexSum = 0; graph[name].forEach((weight, neighbor) => { @@ -339,12 +371,16 @@ function orderLayers(layers, graph) { } }); // Tables with no partners in the reference layer keep their position - const barycenter = weightSum > 0 ? weightedIndexSum / weightSum : indexOf[name]; + const barycenter = + weightSum > 0 ? weightedIndexSum / weightSum : indexOf[name]; return { name, barycenter }; }); - keyed.sort((a, b) => a.barycenter - b.barycenter || a.name.localeCompare(b.name)); - const reordered = keyed.map(k => k.name); + keyed.sort( + (a, b) => + a.barycenter - b.barycenter || a.name.localeCompare(b.name), + ); + const reordered = keyed.map((k) => k.name); layer.length = 0; layer.push(...reordered); refreshIndices(layer); @@ -384,17 +420,21 @@ function columnStackHeight(column, dims) { function splitTallLayers(layers, dims) { // Aim for a roughly 4:3 component; cap column height accordingly let totalArea = 0; - layers.forEach(layer => layer.forEach(name => { - totalArea += (dims[name].width + LAYOUT_CONFIG.columnGap) * (dims[name].height + LAYOUT_CONFIG.rowGap); - })); + layers.forEach((layer) => + layer.forEach((name) => { + totalArea += + (dims[name].width + LAYOUT_CONFIG.columnGap) * + (dims[name].height + LAYOUT_CONFIG.rowGap); + }), + ); const idealHeight = Math.sqrt(totalArea * 0.75); // height of a 4:3 rectangle with this area const maxColumnHeight = Math.min( Math.max(LAYOUT_CONFIG.minColumnHeight, idealHeight), - LAYOUT_CONFIG.maxColumnHeight + LAYOUT_CONFIG.maxColumnHeight, ); const columns = []; - layers.forEach(layer => { + layers.forEach((layer) => { const stackHeight = columnStackHeight(layer, dims); if (stackHeight <= maxColumnHeight || layer.length <= 1) { columns.push([...layer]); @@ -409,7 +449,7 @@ function splitTallLayers(layers, dims) { } }); - return columns.filter(c => c.length > 0); + return columns.filter((c) => c.length > 0); } /** @@ -420,7 +460,7 @@ function splitTallLayers(layers, dims) { * least-squares block placement. */ function relaxColumn(column, graph, dims, pos, allowReorder) { - const entries = column.map(name => { + const entries = column.map((name) => { const d = dims[name]; let weightSum = 0; let weightedCenterSum = 0; @@ -433,24 +473,30 @@ function relaxColumn(column, graph, dims, pos, allowReorder) { }); const currentCenter = pos[name].y + d.height / 2; - const desiredCenter = weightSum > 0 ? weightedCenterSum / weightSum : currentCenter; + const desiredCenter = + weightSum > 0 ? weightedCenterSum / weightSum : currentCenter; return { name, desiredTop: desiredCenter - d.height / 2, size: d.height + LAYOUT_CONFIG.rowGap, - weight: Math.max(weightSum, 0.1) + weight: Math.max(weightSum, 0.1), }; }); if (allowReorder) { - entries.sort((a, b) => a.desiredTop - b.desiredTop || a.name.localeCompare(b.name)); + entries.sort( + (a, b) => + a.desiredTop - b.desiredTop || a.name.localeCompare(b.name), + ); column.length = 0; - column.push(...entries.map(e => e.name)); + column.push(...entries.map((e) => e.name)); } const tops = placeSequence1D(entries); - column.forEach((name, i) => { pos[name].y = tops[i]; }); + column.forEach((name, i) => { + pos[name].y = tops[i]; + }); } /** @@ -462,12 +508,12 @@ function relaxColumn(column, graph, dims, pos, allowReorder) { function placeSequence1D(entries) { const blocks = []; - entries.forEach(entry => { + entries.forEach((entry) => { let block = { desiredTop: entry.desiredTop, weight: entry.weight, size: entry.size, - entries: [entry] + entries: [entry], }; // Merge with previous blocks while they would overlap @@ -478,12 +524,14 @@ function placeSequence1D(entries) { blocks.pop(); const combinedWeight = last.weight + block.weight; const combinedDesired = - (last.desiredTop * last.weight + (block.desiredTop - last.size) * block.weight) / combinedWeight; + (last.desiredTop * last.weight + + (block.desiredTop - last.size) * block.weight) / + combinedWeight; block = { desiredTop: combinedDesired, weight: combinedWeight, size: last.size + block.size, - entries: [...last.entries, ...block.entries] + entries: [...last.entries, ...block.entries], }; } @@ -491,9 +539,9 @@ function placeSequence1D(entries) { }); const tops = []; - blocks.forEach(block => { + blocks.forEach((block) => { let y = block.desiredTop; - block.entries.forEach(entry => { + block.entries.forEach((entry) => { tops.push(y); y += entry.size; }); @@ -517,7 +565,7 @@ function layoutSingletonGrid(singletons, dims) { const row = singletons.slice(rowStart, rowStart + columnsCount); let x = 0; let rowHeight = 0; - row.forEach(name => { + row.forEach((name) => { positions[name] = { x, y }; x += dims[name].width + LAYOUT_CONFIG.singletonGap; rowHeight = Math.max(rowHeight, dims[name].height); @@ -536,13 +584,23 @@ function layoutSingletonGrid(singletons, dims) { function packBlocks(blocks) { if (blocks.length === 0) return { positions: {}, width: 0, height: 0 }; - const sorted = [...blocks].sort((a, b) => b.height - a.height || b.width - a.width); + const sorted = [...blocks].sort( + (a, b) => b.height - a.height || b.width - a.width, + ); const totalArea = sorted.reduce( - (sum, b) => sum + (b.width + LAYOUT_CONFIG.componentGap) * (b.height + LAYOUT_CONFIG.componentGap), 0); + (sum, b) => + sum + + (b.width + LAYOUT_CONFIG.componentGap) * + (b.height + LAYOUT_CONFIG.componentGap), + 0, + ); const canvasAspect = CANVAS_WIDTH / CANVAS_HEIGHT; - const maxBlockWidth = Math.max(...sorted.map(b => b.width)); - const targetWidth = Math.max(maxBlockWidth, Math.sqrt(totalArea * canvasAspect)); + const maxBlockWidth = Math.max(...sorted.map((b) => b.width)); + const targetWidth = Math.max( + maxBlockWidth, + Math.sqrt(totalArea * canvasAspect), + ); const positions = {}; let shelfX = 0; @@ -551,7 +609,7 @@ function packBlocks(blocks) { let totalWidth = 0; let totalHeight = 0; - sorted.forEach(block => { + sorted.forEach((block) => { if (shelfX > 0 && shelfX + block.width > targetWidth) { // Wrap to next shelf shelfY += shelfHeight + LAYOUT_CONFIG.componentGap; @@ -559,10 +617,10 @@ function packBlocks(blocks) { shelfHeight = 0; } - Object.keys(block.positions).forEach(name => { + Object.keys(block.positions).forEach((name) => { positions[name] = { x: block.positions[name].x + shelfX, - y: block.positions[name].y + shelfY + y: block.positions[name].y + shelfY, }; }); @@ -584,16 +642,30 @@ function finalizePositions(packed) { const neededWidth = packed.width + BOUNDARY_MARGIN * 2; const neededHeight = packed.height + BOUNDARY_MARGIN * 2; if (neededWidth > CANVAS_WIDTH || neededHeight > CANVAS_HEIGHT) { - CANVAS_WIDTH = Math.max(CANVAS_WIDTH, Math.ceil(neededWidth / 1000) * 1000); - CANVAS_HEIGHT = Math.max(CANVAS_HEIGHT, Math.ceil(neededHeight / 1000) * 1000); - console.log(`πŸ–ΌοΈ Expanded canvas to ${CANVAS_WIDTH}Γ—${CANVAS_HEIGHT} to fit the layout`); + CANVAS_WIDTH = Math.max( + CANVAS_WIDTH, + Math.ceil(neededWidth / 1000) * 1000, + ); + CANVAS_HEIGHT = Math.max( + CANVAS_HEIGHT, + Math.ceil(neededHeight / 1000) * 1000, + ); + console.log( + `πŸ–ΌοΈ Expanded canvas to ${CANVAS_WIDTH}Γ—${CANVAS_HEIGHT} to fit the layout`, + ); } - const offsetX = Math.max(BOUNDARY_MARGIN, (CANVAS_WIDTH - packed.width) / 2); - const offsetY = Math.max(BOUNDARY_MARGIN, (CANVAS_HEIGHT - packed.height) / 2); + const offsetX = Math.max( + BOUNDARY_MARGIN, + (CANVAS_WIDTH - packed.width) / 2, + ); + const offsetY = Math.max( + BOUNDARY_MARGIN, + (CANVAS_HEIGHT - packed.height) / 2, + ); const result = {}; - Object.keys(packed.positions).forEach(name => { + Object.keys(packed.positions).forEach((name) => { let x = packed.positions[name].x + offsetX; let y = packed.positions[name].y + offsetY; @@ -605,7 +677,9 @@ function finalizePositions(packed) { result[name] = { x: Math.round(x), y: Math.round(y) }; }); - console.log(`πŸ“ Smart layout complete: ${Object.keys(result).length} tables in ${Math.round(packed.width)}Γ—${Math.round(packed.height)}px`); + console.log( + `πŸ“ Smart layout complete: ${Object.keys(result).length} tables in ${Math.round(packed.width)}Γ—${Math.round(packed.height)}px`, + ); return result; } @@ -615,28 +689,34 @@ function finalizePositions(packed) { function extractRelationships() { const relationships = []; - Object.values(entities).forEach(entity => { + Object.values(entities).forEach((entity) => { const fromEntityName = entity.type; // Include inherited FK properties so relationships declared on base classes // influence the layout (keeps base-linked tables near their target). const seenPropNames = new Set(); - const allProps = [...(entity.properties || []), ...(entity.inheritedProperties || [])] - .filter(p => !seenPropNames.has(p.name) && seenPropNames.add(p.name)); - allProps.forEach(prop => { + const allProps = [ + ...(entity.properties || []), + ...(entity.inheritedProperties || []), + ].filter( + (p) => !seenPropNames.has(p.name) && seenPropNames.add(p.name), + ); + allProps.forEach((prop) => { if (prop.isForeignKey) { const targetEntityName = findTargetEntity(prop.name); if (targetEntityName && entities[targetEntityName]) { relationships.push({ from: fromEntityName, to: targetEntityName, - property: prop.name + property: prop.name, }); } } }); }); - console.log(`πŸ”— Extracted ${relationships.length} relationships from ${Object.keys(entities).length} entities`); + console.log( + `πŸ”— Extracted ${relationships.length} relationships from ${Object.keys(entities).length} entities`, + ); return relationships; } @@ -650,35 +730,40 @@ function getTableDimensions(entityName) { if (!entity) return { width: 1000, height: 500 }; const properties = entity.properties || []; - const inheritedProps = showInheritedProperties ? (entity.inheritedProperties || []) : []; + const inheritedProps = showInheritedProperties + ? entity.inheritedProperties || [] + : []; const allProperties = [...inheritedProps, ...properties]; - const visibleProperties = showNavigationProperties ? - allProperties : - allProperties.filter(p => !isNavigationProperty(p)); + const visibleProperties = showNavigationProperties + ? allProperties + : allProperties.filter((p) => !isNavigationProperty(p)); const rowHeight = 55; const headerHeight = 88; - const inheritanceHeight = (entity.baseType && showInheritedProperties) ? 50 : 0; + const inheritanceHeight = + entity.baseType && showInheritedProperties ? 50 : 0; const padding = 30; const minWidth = 1000; const iconWidth = 60; const maxNameLength = Math.max( entity.type.length, - ...visibleProperties.map(p => p.name.length), - 0 + ...visibleProperties.map((p) => p.name.length), + 0, ); const maxTypeLength = Math.max( 8, - ...visibleProperties.map(p => { + ...visibleProperties.map((p) => { let typeText = p.type; if (isNavigationProperty(p)) { - typeText = typeText.replace(/^ICollection<(.+)>$/, '$1').replace(/^List<(.+)>$/, '$1'); + typeText = typeText + .replace(/^ICollection<(.+)>$/, "$1") + .replace(/^List<(.+)>$/, "$1"); } return typeText.length; - }) + }), ); const nameColumnWidth = Math.max(200, maxNameLength * 18); @@ -688,11 +773,19 @@ function getTableDimensions(entityName) { const width = Math.max( minWidth, - iconWidth + nameColumnWidth + typeColumnWidth + typeIconSpace + spacingBuffer + (padding * 2) + iconWidth + + nameColumnWidth + + typeColumnWidth + + typeIconSpace + + spacingBuffer + + padding * 2, ); - const visibleRows = fullHeightMode ? visibleProperties.length : Math.min(visibleProperties.length, 15); - const height = headerHeight + inheritanceHeight + (visibleRows * rowHeight) + padding; + const visibleRows = fullHeightMode + ? visibleProperties.length + : Math.min(visibleProperties.length, 15); + const height = + headerHeight + inheritanceHeight + visibleRows * rowHeight + padding; return { width, height: Math.max(height, 200) }; } @@ -703,14 +796,14 @@ function getTableDimensions(entityName) { function getCurrentTablePositions() { const positions = {}; - document.querySelectorAll('.table-group').forEach(tableGroup => { - const entityName = tableGroup.getAttribute('data-entity'); - const rect = tableGroup.querySelector('.table-box'); + document.querySelectorAll(".table-group").forEach((tableGroup) => { + const entityName = tableGroup.getAttribute("data-entity"); + const rect = tableGroup.querySelector(".table-box"); if (rect && entityName) { positions[entityName] = { - x: parseFloat(rect.getAttribute('x')), - y: parseFloat(rect.getAttribute('y')) + x: parseFloat(rect.getAttribute("x")), + y: parseFloat(rect.getAttribute("y")), }; } }); @@ -722,8 +815,10 @@ function getCurrentTablePositions() { * Apply computed positions to existing tables and refresh relationship lines */ function applyPositionsToTables(positions) { - Object.keys(positions).forEach(entityName => { - const tableGroup = document.querySelector(`[data-entity="${entityName}"]`); + Object.keys(positions).forEach((entityName) => { + const tableGroup = document.querySelector( + `[data-entity="${entityName}"]`, + ); if (tableGroup && positions[entityName]) { const newPos = positions[entityName]; moveTable(tableGroup, newPos.x, newPos.y); diff --git a/Templates/pan-zoom.js b/Templates/pan-zoom.js index 586ca99..1ff23d6 100644 --- a/Templates/pan-zoom.js +++ b/Templates/pan-zoom.js @@ -3,134 +3,144 @@ let isDragging = false; let dragStart = { x: 0, y: 0 }; // Zoom limits - using constants from variables.js -const MIN_ZOOM = 0.05; // 5% - very zoomed out +const MIN_ZOOM = 0.05; // 5% - very zoomed out // MAX_ZOOM is defined in variables.js - don't redeclare it here function startBackgroundPan(e) { - if (e.target.closest('.table-group')) return; + if (e.target.closest(".table-group")) return; // Clear selection when clicking background if (selectedTable) { clearSelection(); } - const svg = document.getElementById('schema-svg'); + const svg = document.getElementById("schema-svg"); const pt = svg.createSVGPoint(); pt.x = e.clientX; pt.y = e.clientY; - + // Convert screen coordinates to SVG coordinates const svgP = pt.matrixTransform(svg.getScreenCTM().inverse()); - + isDragging = true; dragStart = { x: svgP.x, y: svgP.y }; - - document.getElementById('schema-container').style.cursor = 'grabbing'; + + document.getElementById("schema-container").style.cursor = "grabbing"; e.preventDefault(); } function handlePan(e) { if (!isDragging) return; - const svg = document.getElementById('schema-svg'); + const svg = document.getElementById("schema-svg"); const pt = svg.createSVGPoint(); pt.x = e.clientX; pt.y = e.clientY; - + // Convert screen coordinates to SVG coordinates const svgP = pt.matrixTransform(svg.getScreenCTM().inverse()); - + // Calculate how much we've moved in SVG space const dx = svgP.x - dragStart.x; const dy = svgP.y - dragStart.y; - + // Simply move the viewBox by the delta svgViewBox.x -= dx; svgViewBox.y -= dy; - + updateViewBox(); e.preventDefault(); - + // Debug logging - console.log(`Pan: screen(${e.clientX},${e.clientY}) -> svg(${svgP.x.toFixed(1)},${svgP.y.toFixed(1)}) delta(${dx.toFixed(1)},${dy.toFixed(1)})`); + console.log( + `Pan: screen(${e.clientX},${e.clientY}) -> svg(${svgP.x.toFixed(1)},${svgP.y.toFixed(1)}) delta(${dx.toFixed(1)},${dy.toFixed(1)})`, + ); } function endPan(e) { if (!isDragging) return; isDragging = false; - document.getElementById('schema-container').style.cursor = 'grab'; - + document.getElementById("schema-container").style.cursor = "grab"; + // Save view state when panning ends saveViewState(); } function handleWheel(e) { e.preventDefault(); - - const svg = document.getElementById('schema-svg'); + + const svg = document.getElementById("schema-svg"); const pt = svg.createSVGPoint(); pt.x = e.clientX; pt.y = e.clientY; - + // Get the mouse position in SVG coordinates BEFORE zoom const svgP = pt.matrixTransform(svg.getScreenCTM().inverse()); - + // Determine zoom direction and factor const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; - const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, currentZoom * zoomFactor)); - + const newZoom = Math.max( + MIN_ZOOM, + Math.min(MAX_ZOOM, currentZoom * zoomFactor), + ); + if (newZoom === currentZoom) return; // Hit zoom limits - + // Calculate new viewBox size const newWidth = CANVAS_WIDTH / newZoom; const newHeight = CANVAS_HEIGHT / newZoom; - + // Keep the mouse position fixed during zoom const mouseXRatio = (svgP.x - svgViewBox.x) / svgViewBox.width; const mouseYRatio = (svgP.y - svgViewBox.y) / svgViewBox.height; - + // Calculate new viewBox position to keep mouse position fixed - svgViewBox.x = svgP.x - (mouseXRatio * newWidth); - svgViewBox.y = svgP.y - (mouseYRatio * newHeight); + svgViewBox.x = svgP.x - mouseXRatio * newWidth; + svgViewBox.y = svgP.y - mouseYRatio * newHeight; svgViewBox.width = newWidth; svgViewBox.height = newHeight; - + currentZoom = newZoom; updateViewBox(); - + // Save view state when zooming saveViewState(); - - console.log(`Zoom: ${(currentZoom * 100).toFixed(0)}% at (${svgP.x.toFixed(1)},${svgP.y.toFixed(1)})`); + + console.log( + `Zoom: ${(currentZoom * 100).toFixed(0)}% at (${svgP.x.toFixed(1)},${svgP.y.toFixed(1)})`, + ); } function updateViewBox() { - const svg = document.getElementById('schema-svg'); - svg.setAttribute('viewBox', `${svgViewBox.x} ${svgViewBox.y} ${svgViewBox.width} ${svgViewBox.height}`); + const svg = document.getElementById("schema-svg"); + svg.setAttribute( + "viewBox", + `${svgViewBox.x} ${svgViewBox.y} ${svgViewBox.width} ${svgViewBox.height}`, + ); } function zoomIn() { // Zoom into the center of the current view const centerX = svgViewBox.x + svgViewBox.width / 2; const centerY = svgViewBox.y + svgViewBox.height / 2; - + const newZoom = Math.min(MAX_ZOOM, currentZoom * 1.2); if (newZoom === currentZoom) return; - + const newWidth = CANVAS_WIDTH / newZoom; const newHeight = CANVAS_HEIGHT / newZoom; - + svgViewBox.x = centerX - newWidth / 2; svgViewBox.y = centerY - newHeight / 2; svgViewBox.width = newWidth; svgViewBox.height = newHeight; - + currentZoom = newZoom; updateViewBox(); - + // Save view state when zooming via button saveViewState(); - + console.log(`Button Zoom In: ${(currentZoom * 100).toFixed(0)}%`); } @@ -138,36 +148,41 @@ function zoomOut() { // Zoom out from the center of the current view const centerX = svgViewBox.x + svgViewBox.width / 2; const centerY = svgViewBox.y + svgViewBox.height / 2; - + const newZoom = Math.max(MIN_ZOOM, currentZoom * 0.8); if (newZoom === currentZoom) return; - + const newWidth = CANVAS_WIDTH / newZoom; const newHeight = CANVAS_HEIGHT / newZoom; - + svgViewBox.x = centerX - newWidth / 2; svgViewBox.y = centerY - newHeight / 2; svgViewBox.width = newWidth; svgViewBox.height = newHeight; - + currentZoom = newZoom; updateViewBox(); - + // Save view state when zooming via button saveViewState(); - + console.log(`Button Zoom Out: ${(currentZoom * 100).toFixed(0)}%`); } function resetZoom() { currentZoom = 0.35; // Changed from 1 to 0.35 to match initial zoom - svgViewBox = { x: 0, y: 0, width: CANVAS_WIDTH / currentZoom, height: CANVAS_HEIGHT / currentZoom }; + svgViewBox = { + x: 0, + y: 0, + width: CANVAS_WIDTH / currentZoom, + height: CANVAS_HEIGHT / currentZoom, + }; updateViewBox(); - + // Save view state when resetting zoom saveViewState(); - - console.log('Zoom Reset: 35%'); // Updated log message + + console.log("Zoom Reset: 35%"); // Updated log message } // Save the current view state (position and zoom level) to local storage using document-specific key @@ -175,13 +190,16 @@ function saveViewState() { const viewState = { centerX: svgViewBox.x + svgViewBox.width / 2, centerY: svgViewBox.y + svgViewBox.height / 2, - zoom: currentZoom + zoom: currentZoom, }; try { - localStorage.setItem(getDocumentStorageKey('viewState'), JSON.stringify(viewState)); - console.log('?? View state saved:', viewState); + localStorage.setItem( + getDocumentStorageKey("viewState"), + JSON.stringify(viewState), + ); + console.log("?? View state saved:", viewState); } catch (e) { - console.warn('?? Failed to save view state:', e); + console.warn("?? Failed to save view state:", e); } } @@ -189,27 +207,29 @@ function saveViewState() { // This should be called AFTER generateSchema() has initialized svgViewBox properly function loadViewState() { try { - const savedState = localStorage.getItem(getDocumentStorageKey('viewState')); + const savedState = localStorage.getItem( + getDocumentStorageKey("viewState"), + ); if (savedState) { const viewState = JSON.parse(savedState); - + // Apply the saved zoom first to get correct viewBox dimensions currentZoom = viewState.zoom; const newWidth = CANVAS_WIDTH / currentZoom; const newHeight = CANVAS_HEIGHT / currentZoom; - + // Then apply the saved center position svgViewBox.x = viewState.centerX - newWidth / 2; svgViewBox.y = viewState.centerY - newHeight / 2; svgViewBox.width = newWidth; svgViewBox.height = newHeight; - + updateViewBox(); - console.log('?? View state loaded:', viewState); + console.log("?? View state loaded:", viewState); return true; } } catch (e) { - console.warn('?? Failed to load view state:', e); + console.warn("?? Failed to load view state:", e); } return false; -} \ No newline at end of file +} diff --git a/Templates/property-utilities.js b/Templates/property-utilities.js index b1f3f39..93074f4 100644 --- a/Templates/property-utilities.js +++ b/Templates/property-utilities.js @@ -6,106 +6,158 @@ function getPropertyIcon(property) { function getPropertyLevelIcon(property) { // Primary keys ALWAYS show PK, even if inherited - if (property.isKey) return 'PK'; + if (property.isKey) return "PK"; // Then check for inherited (non-PK properties) - if (property.isInherited) return 'INH'; + if (property.isInherited) return "INH"; // Then foreign keys - if (property.isForeignKey) return 'FK'; + if (property.isForeignKey) return "FK"; // Then navigation properties - if (isNavigationProperty(property)) return 'N'; + if (isNavigationProperty(property)) return "N"; // No icon for regular properties return null; } function getTypeIcon(type) { // Normalize the type (handle nullables) - const cleanType = type.replace(/\?$/, ''); // Remove trailing ? - const isNullable = type.endsWith('?'); - + const cleanType = type.replace(/\?$/, ""); // Remove trailing ? + const isNullable = type.endsWith("?"); + // Return type information for visual icon creation - + // Basic C# types - if (cleanType === 'string') return { iconType: 'string', isNullable }; - if (['int', 'long', 'short', 'byte'].includes(cleanType)) return { iconType: 'int', isNullable }; - if (['double', 'float', 'decimal'].includes(cleanType)) return { iconType: 'double', isNullable }; - if (cleanType === 'bool') return { iconType: 'bool', isNullable }; - + if (cleanType === "string") return { iconType: "string", isNullable }; + if (["int", "long", "short", "byte"].includes(cleanType)) + return { iconType: "int", isNullable }; + if (["double", "float", "decimal"].includes(cleanType)) + return { iconType: "double", isNullable }; + if (cleanType === "bool") return { iconType: "bool", isNullable }; + // .NET Framework types - if (cleanType === 'Guid') return { iconType: 'Guid', isNullable }; - if (['DateTime', 'DateTimeOffset', 'TimeSpan'].includes(cleanType)) return { iconType: 'DateTime', isNullable }; - + if (cleanType === "Guid") return { iconType: "Guid", isNullable }; + if (["DateTime", "DateTimeOffset", "TimeSpan"].includes(cleanType)) + return { iconType: "DateTime", isNullable }; + // Collection types - if (cleanType.startsWith('ICollection') || cleanType.startsWith('List') || cleanType.startsWith('IList')) { - return { iconType: 'Collection', isNullable }; + if ( + cleanType.startsWith("ICollection") || + cleanType.startsWith("List") || + cleanType.startsWith("IList") + ) { + return { iconType: "Collection", isNullable }; } - + // JSON types - if (cleanType === 'JsonDocument') { - return { iconType: 'JsonDocument', isNullable }; + if (cleanType === "JsonDocument") { + return { iconType: "JsonDocument", isNullable }; } - + // Navigation properties (Model/Entity types) - show arrow icon for these - if (cleanType.includes('Model') || cleanType.includes('Entity')) { - return { iconType: 'Navigation', isNullable }; + if (cleanType.includes("Model") || cleanType.includes("Entity")) { + return { iconType: "Navigation", isNullable }; } - + // Enum types (usually end with certain patterns or are single words without Model suffix) - if (!cleanType.includes('<') && !cleanType.includes('Model') && !cleanType.includes('Entity') && - !['string', 'int', 'long', 'bool', 'double', 'float', 'decimal', 'byte', 'short', 'Guid', 'DateTime', 'DateTimeOffset', 'TimeSpan', 'JsonDocument'].includes(cleanType)) { - return { iconType: 'Enum', isNullable }; + if ( + !cleanType.includes("<") && + !cleanType.includes("Model") && + !cleanType.includes("Entity") && + ![ + "string", + "int", + "long", + "bool", + "double", + "float", + "decimal", + "byte", + "short", + "Guid", + "DateTime", + "DateTimeOffset", + "TimeSpan", + "JsonDocument", + ].includes(cleanType) + ) { + return { iconType: "Enum", isNullable }; } - + // Default for unknown types - return { iconType: 'unknown', isNullable }; + return { iconType: "unknown", isNullable }; } function getTypeColorClass(type) { // Get the color class for property type text - const cleanType = type.replace(/\?$/, ''); // Remove trailing ? - + const cleanType = type.replace(/\?$/, ""); // Remove trailing ? + // Basic C# types - if (cleanType === 'string') return 'property-type-string'; - if (['int', 'long', 'short', 'byte'].includes(cleanType)) return 'property-type-int'; - if (['double', 'float', 'decimal'].includes(cleanType)) return 'property-type-double'; - if (cleanType === 'bool') return 'property-type-bool'; - + if (cleanType === "string") return "property-type-string"; + if (["int", "long", "short", "byte"].includes(cleanType)) + return "property-type-int"; + if (["double", "float", "decimal"].includes(cleanType)) + return "property-type-double"; + if (cleanType === "bool") return "property-type-bool"; + // .NET Framework types - if (cleanType === 'Guid') return 'property-type-guid'; - if (['DateTime', 'DateTimeOffset', 'TimeSpan'].includes(cleanType)) return 'property-type-datetime'; - + if (cleanType === "Guid") return "property-type-guid"; + if (["DateTime", "DateTimeOffset", "TimeSpan"].includes(cleanType)) + return "property-type-datetime"; + // Collection types - if (cleanType.startsWith('ICollection') || cleanType.startsWith('List') || cleanType.startsWith('IList')) { - return 'property-type-collection'; + if ( + cleanType.startsWith("ICollection") || + cleanType.startsWith("List") || + cleanType.startsWith("IList") + ) { + return "property-type-collection"; } - + // JSON types - if (cleanType === 'JsonDocument') { - return 'property-type-jsondocument'; + if (cleanType === "JsonDocument") { + return "property-type-jsondocument"; } - + // Navigation properties (Model/Entity types) - if (cleanType.includes('Model') || cleanType.includes('Entity')) { - return 'property-type-navigation'; + if (cleanType.includes("Model") || cleanType.includes("Entity")) { + return "property-type-navigation"; } - + // Enum types - if (!cleanType.includes('<') && !cleanType.includes('Model') && !cleanType.includes('Entity') && - !['string', 'int', 'long', 'bool', 'double', 'float', 'decimal', 'byte', 'short', 'Guid', 'DateTime', 'DateTimeOffset', 'TimeSpan', 'JsonDocument'].includes(cleanType)) { - return 'property-type-enum'; + if ( + !cleanType.includes("<") && + !cleanType.includes("Model") && + !cleanType.includes("Entity") && + ![ + "string", + "int", + "long", + "bool", + "double", + "float", + "decimal", + "byte", + "short", + "Guid", + "DateTime", + "DateTimeOffset", + "TimeSpan", + "JsonDocument", + ].includes(cleanType) + ) { + return "property-type-enum"; } - + // Default - return 'property-type-unknown'; + return "property-type-unknown"; } function sortProperties(properties) { return properties.sort((a, b) => { // FIRST: Id (case-insensitive) always comes first - const aIsId = a.name.toLowerCase() === 'id'; - const bIsId = b.name.toLowerCase() === 'id'; + const aIsId = a.name.toLowerCase() === "id"; + const bIsId = b.name.toLowerCase() === "id"; if (aIsId && !bIsId) return -1; if (!aIsId && bIsId) return 1; - + // SECOND: Inherited properties come before regular properties if (a.isInherited && !b.isInherited) return -1; if (!a.isInherited && b.isInherited) return 1; @@ -119,40 +171,41 @@ function groupPropertiesWithNavigation(visibleProperties, allProperties) { const grouped = []; const processed = new Set(); - visibleProperties.forEach(prop => { + visibleProperties.forEach((prop) => { if (processed.has(prop.name)) return; // Only group when both FK and NAV are visible and we're showing navigation properties if (prop.isForeignKey && showNavigationProperties) { // Find corresponding navigation property - be more specific about matching - const navPropName = prop.name.replace(/Id$/, ''); // Remove 'Id' suffix - const navProp = visibleProperties.find(p => - p.name === navPropName && - isNavigationProperty(p) && - !processed.has(p.name) // Make sure it hasn't been processed yet + const navPropName = prop.name.replace(/Id$/, ""); // Remove 'Id' suffix + const navProp = visibleProperties.find( + (p) => + p.name === navPropName && + isNavigationProperty(p) && + !processed.has(p.name), // Make sure it hasn't been processed yet ); if (navProp) { // Group FK and NAV together grouped.push({ - type: 'group', - properties: [prop, navProp] + type: "group", + properties: [prop, navProp], }); processed.add(prop.name); processed.add(navProp.name); } else { // Just add the FK property alone grouped.push({ - type: 'single', - property: prop + type: "single", + property: prop, }); processed.add(prop.name); } } else { // Single property (not part of FK-NAV group) grouped.push({ - type: 'single', - property: prop + type: "single", + property: prop, }); processed.add(prop.name); } @@ -169,16 +222,16 @@ function getDisplayType(property) { if (isNavigationProperty(property)) { displayType = displayType - .replace(/^ICollection<(.+)>$/, '$1') - .replace(/^List<(.+)>$/, '$1'); + .replace(/^ICollection<(.+)>$/, "$1") + .replace(/^List<(.+)>$/, "$1"); return displayType; } if (property.maxLength && property.maxLength > 0) { // Insert the length before any nullable marker: "string?" -> "string(255)?" - const isNullable = displayType.endsWith('?'); + const isNullable = displayType.endsWith("?"); const base = isNullable ? displayType.slice(0, -1) : displayType; - displayType = `${base}(${property.maxLength})${isNullable ? '?' : ''}`; + displayType = `${base}(${property.maxLength})${isNullable ? "?" : ""}`; } return displayType; @@ -192,21 +245,23 @@ function isNavigationProperty(property) { /^HashSet pattern.test(property.type))) { + if (navPatterns.some((pattern) => pattern.test(property.type))) { return true; } // ENHANCED: Second check - detect entity references by checking if the type exists in our entities object // This catches many-to-one relationships like "Tag", "Task", "User", etc. - const cleanType = property.type.replace(/\?$/, ''); // Remove nullable marker - + const cleanType = property.type.replace(/\?$/, ""); // Remove nullable marker + // Check if this type name matches an existing entity - if (typeof entities !== 'undefined' && entities[cleanType]) { - console.log(`πŸ” Detected navigation property: ${property.name} (${property.type}) - matches entity ${cleanType}`); + if (typeof entities !== "undefined" && entities[cleanType]) { + console.log( + `πŸ” Detected navigation property: ${property.name} (${property.type}) - matches entity ${cleanType}`, + ); return true; } @@ -215,18 +270,23 @@ function isNavigationProperty(property) { function navigateToEntity(typeName) { // Clean up type name (remove generic brackets, nullability, etc.) - const cleanTypeName = typeName.replace(/\?$/, '').replace(/ICollection<(.+)>/, '$1').replace(/List<(.+)>/, '$1'); + const cleanTypeName = typeName + .replace(/\?$/, "") + .replace(/ICollection<(.+)>/, "$1") + .replace(/List<(.+)>/, "$1"); - const targetTable = document.querySelector('[data-entity="' + cleanTypeName + '"]'); + const targetTable = document.querySelector( + '[data-entity="' + cleanTypeName + '"]', + ); if (targetTable) { selectTable(cleanTypeName); // Center the target table in view - const rect = targetTable.querySelector('.table-box'); - const targetX = parseFloat(rect.getAttribute('x')); - const targetY = parseFloat(rect.getAttribute('y')); - const targetWidth = parseFloat(rect.getAttribute('width')); - const targetHeight = parseFloat(rect.getAttribute('height')); + const rect = targetTable.querySelector(".table-box"); + const targetX = parseFloat(rect.getAttribute("x")); + const targetY = parseFloat(rect.getAttribute("y")); + const targetWidth = parseFloat(rect.getAttribute("width")); + const targetHeight = parseFloat(rect.getAttribute("height")); svgViewBox.x = targetX - svgViewBox.width / 2 + targetWidth / 2; svgViewBox.y = targetY - svgViewBox.height / 2 + targetHeight / 2; @@ -237,205 +297,253 @@ function navigateToEntity(typeName) { function createPropertyIcon(iconType, x, y) { const iconSize = 30; // Increased from 12 to 30 (2.5x larger) const elements = []; - + // Handle property-level icons (PK, FK, N, INH) - these get circles - if (['PK', 'FK', 'N', 'INH'].includes(iconType)) { + if (["PK", "FK", "N", "INH"].includes(iconType)) { // Create background circle - aligned with text middle - const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - bgCircle.setAttribute('cx', x + iconSize/2 + 10); // Increased from 4 to 10 (2.5x larger) - bgCircle.setAttribute('cy', y - 12); // Increased from -5 to -12 (2.5x larger) - bgCircle.setAttribute('r', iconSize/2); - bgCircle.classList.add('property-icon-circle'); - + const bgCircle = document.createElementNS( + "http://www.w3.org/2000/svg", + "circle", + ); + bgCircle.setAttribute("cx", x + iconSize / 2 + 10); // Increased from 4 to 10 (2.5x larger) + bgCircle.setAttribute("cy", y - 12); // Increased from -5 to -12 (2.5x larger) + bgCircle.setAttribute("r", iconSize / 2); + bgCircle.classList.add("property-icon-circle"); + // Create text - aligned with text middle - const iconText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - iconText.setAttribute('x', x + iconSize/2 + 10); // Increased from 4 to 10 (2.5x larger) - iconText.setAttribute('y', y - 12); // Increased from -5 to -12 (2.5x larger) - iconText.setAttribute('text-anchor', 'middle'); - iconText.setAttribute('dominant-baseline', 'central'); - iconText.classList.add('property-icon-text'); + const iconText = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + iconText.setAttribute("x", x + iconSize / 2 + 10); // Increased from 4 to 10 (2.5x larger) + iconText.setAttribute("y", y - 12); // Increased from -5 to -12 (2.5x larger) + iconText.setAttribute("text-anchor", "middle"); + iconText.setAttribute("dominant-baseline", "central"); + iconText.classList.add("property-icon-text"); iconText.textContent = iconType; - + // Style based on icon type - switch(iconType) { - case 'PK': - bgCircle.classList.add('icon-pk'); + switch (iconType) { + case "PK": + bgCircle.classList.add("icon-pk"); break; - case 'FK': - bgCircle.classList.add('icon-fk'); + case "FK": + bgCircle.classList.add("icon-fk"); break; - case 'N': - bgCircle.classList.add('icon-nav'); + case "N": + bgCircle.classList.add("icon-nav"); break; - case 'INH': - bgCircle.classList.add('icon-inh'); + case "INH": + bgCircle.classList.add("icon-inh"); break; } - + elements.push(bgCircle); elements.push(iconText); } // Handle type icons - NO circles, larger size like capital M - else if (typeof iconType === 'object' && iconType.iconType) { + else if (typeof iconType === "object" && iconType.iconType) { const { iconType: type, isNullable } = iconType; - + let iconContent = null; const textY = y - 12; // Increased from -5 to -12 (2.5x larger) - const centerX = x + iconSize/2 + 10; // Increased from 4 to 10 (2.5x larger) - const strokeColor = isNullable ? '#9ca3af' : '#4b5563'; - const fillColor = isNullable ? '#9ca3af' : '#4b5563'; - - switch(type) { - case 'string': + const centerX = x + iconSize / 2 + 10; // Increased from 4 to 10 (2.5x larger) + const strokeColor = isNullable ? "#9ca3af" : "#4b5563"; + const fillColor = isNullable ? "#9ca3af" : "#4b5563"; + + switch (type) { + case "string": // Letter "a" for string - larger size - iconContent = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - iconContent.setAttribute('x', centerX); - iconContent.setAttribute('y', textY); - iconContent.setAttribute('text-anchor', 'middle'); - iconContent.setAttribute('dominant-baseline', 'central'); - iconContent.setAttribute('fill', fillColor); - iconContent.classList.add('type-icon-symbol-large'); - iconContent.textContent = 'a'; + iconContent = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + iconContent.setAttribute("x", centerX); + iconContent.setAttribute("y", textY); + iconContent.setAttribute("text-anchor", "middle"); + iconContent.setAttribute("dominant-baseline", "central"); + iconContent.setAttribute("fill", fillColor); + iconContent.classList.add("type-icon-symbol-large"); + iconContent.textContent = "a"; break; - - case 'int': - case 'long': - case 'short': - case 'byte': + + case "int": + case "long": + case "short": + case "byte": // Hash symbol for integers - larger size - iconContent = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - iconContent.setAttribute('x', centerX); - iconContent.setAttribute('y', textY); - iconContent.setAttribute('text-anchor', 'middle'); - iconContent.setAttribute('dominant-baseline', 'central'); - iconContent.setAttribute('fill', fillColor); - iconContent.classList.add('type-icon-symbol-large'); - iconContent.textContent = '#'; + iconContent = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + iconContent.setAttribute("x", centerX); + iconContent.setAttribute("y", textY); + iconContent.setAttribute("text-anchor", "middle"); + iconContent.setAttribute("dominant-baseline", "central"); + iconContent.setAttribute("fill", fillColor); + iconContent.classList.add("type-icon-symbol-large"); + iconContent.textContent = "#"; break; - - case 'double': - case 'float': - case 'decimal': + + case "double": + case "float": + case "decimal": // Decimal point for floating point numbers - larger size - iconContent = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - iconContent.setAttribute('x', centerX); - iconContent.setAttribute('y', textY); - iconContent.setAttribute('text-anchor', 'middle'); - iconContent.setAttribute('dominant-baseline', 'central'); - iconContent.setAttribute('fill', fillColor); - iconContent.classList.add('type-icon-symbol-large'); - iconContent.textContent = '.'; + iconContent = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + iconContent.setAttribute("x", centerX); + iconContent.setAttribute("y", textY); + iconContent.setAttribute("text-anchor", "middle"); + iconContent.setAttribute("dominant-baseline", "central"); + iconContent.setAttribute("fill", fillColor); + iconContent.classList.add("type-icon-symbol-large"); + iconContent.textContent = "."; break; - - case 'bool': + + case "bool": // Checkmark for boolean - larger size - iconContent = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - iconContent.setAttribute('x', centerX); - iconContent.setAttribute('y', textY); - iconContent.setAttribute('text-anchor', 'middle'); - iconContent.setAttribute('dominant-baseline', 'central'); - iconContent.setAttribute('fill', fillColor); - iconContent.classList.add('type-icon-symbol-large'); - iconContent.textContent = 'βœ“'; + iconContent = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + iconContent.setAttribute("x", centerX); + iconContent.setAttribute("y", textY); + iconContent.setAttribute("text-anchor", "middle"); + iconContent.setAttribute("dominant-baseline", "central"); + iconContent.setAttribute("fill", fillColor); + iconContent.classList.add("type-icon-symbol-large"); + iconContent.textContent = "βœ“"; break; - - case 'Guid': + + case "Guid": // Simple square for Guid (unique identifier) - larger size - iconContent = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - iconContent.setAttribute('x', centerX - 8); // Increased from 3 to 8 (2.5x larger) - iconContent.setAttribute('y', textY - 8); // Increased from 3 to 8 (2.5x larger) - iconContent.setAttribute('width', '16'); // Increased from 6 to 16 (2.5x larger) - iconContent.setAttribute('height', '16'); // Increased from 6 to 16 (2.5x larger) - iconContent.setAttribute('fill', fillColor); - iconContent.setAttribute('stroke', strokeColor); - iconContent.classList.add('type-icon-shape-large'); + iconContent = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect", + ); + iconContent.setAttribute("x", centerX - 8); // Increased from 3 to 8 (2.5x larger) + iconContent.setAttribute("y", textY - 8); // Increased from 3 to 8 (2.5x larger) + iconContent.setAttribute("width", "16"); // Increased from 6 to 16 (2.5x larger) + iconContent.setAttribute("height", "16"); // Increased from 6 to 16 (2.5x larger) + iconContent.setAttribute("fill", fillColor); + iconContent.setAttribute("stroke", strokeColor); + iconContent.classList.add("type-icon-shape-large"); break; - - case 'DateTime': - case 'DateTimeOffset': - case 'TimeSpan': + + case "DateTime": + case "DateTimeOffset": + case "TimeSpan": // Simple calendar icon (rectangle with line) - larger size - create as individual elements instead of group - const dateRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - dateRect.setAttribute('x', centerX - 10); // Increased from 4 to 10 (2.5x larger) - dateRect.setAttribute('y', textY - 8); // Increased from 3 to 8 (2.5x larger) - dateRect.setAttribute('width', '20'); // Increased from 8 to 20 (2.5x larger) - dateRect.setAttribute('height', '16'); // Increased from 6 to 16 (2.5x larger) - dateRect.setAttribute('fill', 'none'); - dateRect.setAttribute('stroke', strokeColor); - dateRect.setAttribute('stroke-width', '2'); // Increased from 0.8 to 2 (2.5x larger) - dateRect.classList.add('type-icon-shape-large'); + const dateRect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect", + ); + dateRect.setAttribute("x", centerX - 10); // Increased from 4 to 10 (2.5x larger) + dateRect.setAttribute("y", textY - 8); // Increased from 3 to 8 (2.5x larger) + dateRect.setAttribute("width", "20"); // Increased from 8 to 20 (2.5x larger) + dateRect.setAttribute("height", "16"); // Increased from 6 to 16 (2.5x larger) + dateRect.setAttribute("fill", "none"); + dateRect.setAttribute("stroke", strokeColor); + dateRect.setAttribute("stroke-width", "2"); // Increased from 0.8 to 2 (2.5x larger) + dateRect.classList.add("type-icon-shape-large"); elements.push(dateRect); - - const dateLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); - dateLine.setAttribute('x1', centerX - 8); // Increased from 3 to 8 (2.5x larger) - dateLine.setAttribute('y1', textY - 2); // Increased from 1 to 2 (2.5x larger) - dateLine.setAttribute('x2', centerX + 8); // Increased from 3 to 8 (2.5x larger) - dateLine.setAttribute('y2', textY - 2); // Increased from 1 to 2 (2.5x larger) - dateLine.setAttribute('stroke', strokeColor); - dateLine.setAttribute('stroke-width', '2'); // Increased from 0.8 to 2 (2.5x larger) - dateLine.classList.add('type-icon-shape-large'); + + const dateLine = document.createElementNS( + "http://www.w3.org/2000/svg", + "line", + ); + dateLine.setAttribute("x1", centerX - 8); // Increased from 3 to 8 (2.5x larger) + dateLine.setAttribute("y1", textY - 2); // Increased from 1 to 2 (2.5x larger) + dateLine.setAttribute("x2", centerX + 8); // Increased from 3 to 8 (2.5x larger) + dateLine.setAttribute("y2", textY - 2); // Increased from 1 to 2 (2.5x larger) + dateLine.setAttribute("stroke", strokeColor); + dateLine.setAttribute("stroke-width", "2"); // Increased from 0.8 to 2 (2.5x larger) + dateLine.classList.add("type-icon-shape-large"); elements.push(dateLine); - + // Return early for DateTime since we already added both elements return elements; - - case 'JsonDocument': + + case "JsonDocument": // Curly braces for JSON - larger size - iconContent = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - iconContent.setAttribute('x', centerX); - iconContent.setAttribute('y', textY); - iconContent.setAttribute('text-anchor', 'middle'); - iconContent.setAttribute('dominant-baseline', 'central'); - iconContent.setAttribute('fill', fillColor); - iconContent.classList.add('type-icon-symbol-large'); - iconContent.textContent = '{}'; + iconContent = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + iconContent.setAttribute("x", centerX); + iconContent.setAttribute("y", textY); + iconContent.setAttribute("text-anchor", "middle"); + iconContent.setAttribute("dominant-baseline", "central"); + iconContent.setAttribute("fill", fillColor); + iconContent.classList.add("type-icon-symbol-large"); + iconContent.textContent = "{}"; break; - - case 'Collection': + + case "Collection": // Square brackets for collections - larger size - iconContent = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - iconContent.setAttribute('x', centerX); - iconContent.setAttribute('y', textY); - iconContent.setAttribute('text-anchor', 'middle'); - iconContent.setAttribute('dominant-baseline', 'central'); - iconContent.setAttribute('fill', fillColor); - iconContent.classList.add('type-icon-symbol-large'); - iconContent.textContent = '[]'; + iconContent = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + iconContent.setAttribute("x", centerX); + iconContent.setAttribute("y", textY); + iconContent.setAttribute("text-anchor", "middle"); + iconContent.setAttribute("dominant-baseline", "central"); + iconContent.setAttribute("fill", fillColor); + iconContent.classList.add("type-icon-symbol-large"); + iconContent.textContent = "[]"; break; - - case 'Enum': + + case "Enum": // Simple diamond for enums - larger size - iconContent = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); - iconContent.setAttribute('points', `${centerX},${textY - 8} ${centerX + 8},${textY} ${centerX},${textY + 8} ${centerX - 8},${textY}`); // Increased from 3 to 8 (2.5x larger) - iconContent.setAttribute('fill', fillColor); - iconContent.setAttribute('stroke', strokeColor); - iconContent.classList.add('type-icon-shape-large'); + iconContent = document.createElementNS( + "http://www.w3.org/2000/svg", + "polygon", + ); + iconContent.setAttribute( + "points", + `${centerX},${textY - 8} ${centerX + 8},${textY} ${centerX},${textY + 8} ${centerX - 8},${textY}`, + ); // Increased from 3 to 8 (2.5x larger) + iconContent.setAttribute("fill", fillColor); + iconContent.setAttribute("stroke", strokeColor); + iconContent.classList.add("type-icon-shape-large"); break; - - case 'Navigation': + + case "Navigation": // Arrow for navigation properties - larger size - iconContent = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); - iconContent.setAttribute('points', `${centerX - 8},${textY - 6} ${centerX + 6},${textY} ${centerX - 8},${textY + 6} ${centerX - 5},${textY + 3} ${centerX + 3},${textY} ${centerX - 5},${textY - 3}`); - iconContent.classList.add('type-icon-shape-large'); - iconContent.setAttribute('fill', '#059669'); // Green color for navigation + iconContent = document.createElementNS( + "http://www.w3.org/2000/svg", + "polygon", + ); + iconContent.setAttribute( + "points", + `${centerX - 8},${textY - 6} ${centerX + 6},${textY} ${centerX - 8},${textY + 6} ${centerX - 5},${textY + 3} ${centerX + 3},${textY} ${centerX - 5},${textY - 3}`, + ); + iconContent.classList.add("type-icon-shape-large"); + iconContent.setAttribute("fill", "#059669"); // Green color for navigation break; - + default: // Question mark for unknown types - larger size - iconContent = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - iconContent.setAttribute('x', centerX); - iconContent.setAttribute('y', textY); - iconContent.setAttribute('text-anchor', 'middle'); - iconContent.setAttribute('dominant-baseline', 'central'); - iconContent.setAttribute('fill', fillColor); - iconContent.classList.add('type-icon-symbol-large'); - iconContent.textContent = '?'; + iconContent = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + iconContent.setAttribute("x", centerX); + iconContent.setAttribute("y", textY); + iconContent.setAttribute("text-anchor", "middle"); + iconContent.setAttribute("dominant-baseline", "central"); + iconContent.setAttribute("fill", fillColor); + iconContent.classList.add("type-icon-symbol-large"); + iconContent.textContent = "?"; } - + if (iconContent) { elements.push(iconContent); } } - + return elements; -} \ No newline at end of file +} diff --git a/Templates/relationships.js b/Templates/relationships.js index d115b1c..283a710 100644 --- a/Templates/relationships.js +++ b/Templates/relationships.js @@ -1,29 +1,37 @@ ο»Ώ// Relationship lines generation and management with Crow's Foot notation function generateRelationships(svg) { if (!showRelationships) { - console.log('πŸ”— Relationships disabled - skipping generation'); + console.log("πŸ”— Relationships disabled - skipping generation"); return; } - console.log('πŸ”— Starting relationship generation with Crow\'s Foot notation...'); + console.log( + "πŸ”— Starting relationship generation with Crow's Foot notation...", + ); let relationshipCount = 0; let attemptedCount = 0; - Object.values(entities).forEach(entity => { + Object.values(entities).forEach((entity) => { const fromEntityName = entity.type; // Include inherited FK/navigation properties (e.g. an AccountId declared on an // abstract base class) so relationships defined on base types are drawn too. const seenPropNames = new Set(); - const allProps = [...(entity.properties || []), ...(entity.inheritedProperties || [])] - .filter(p => !seenPropNames.has(p.name) && seenPropNames.add(p.name)); - const foreignKeys = allProps.filter(p => p.isForeignKey); + const allProps = [ + ...(entity.properties || []), + ...(entity.inheritedProperties || []), + ].filter( + (p) => !seenPropNames.has(p.name) && seenPropNames.add(p.name), + ); + const foreignKeys = allProps.filter((p) => p.isForeignKey); if (foreignKeys.length > 0) { - console.log(`πŸ”— Entity ${fromEntityName} has ${foreignKeys.length} foreign keys:`, - foreignKeys.map(fk => fk.name)); + console.log( + `πŸ”— Entity ${fromEntityName} has ${foreignKeys.length} foreign keys:`, + foreignKeys.map((fk) => fk.name), + ); } - allProps.forEach(prop => { + allProps.forEach((prop) => { if (prop.isForeignKey) { attemptedCount++; console.log(`πŸ”— Processing FK: ${fromEntityName}.${prop.name}`); @@ -32,29 +40,45 @@ function generateRelationships(svg) { const targetEntityName = findTargetEntity(prop.name); if (targetEntityName && entities[targetEntityName]) { - console.log(`βœ… Found target: ${prop.name} -> ${targetEntityName}`); - createCrowsFootRelationshipLine(svg, fromEntityName, targetEntityName, prop.name); + console.log( + `βœ… Found target: ${prop.name} -> ${targetEntityName}`, + ); + createCrowsFootRelationshipLine( + svg, + fromEntityName, + targetEntityName, + prop.name, + ); relationshipCount++; } else { - console.log(`❌ Could not find target entity for FK ${fromEntityName}.${prop.name}`); - console.log(` Available entities:`, Object.keys(entities).slice(0, 5).join(', ') + '...'); + console.log( + `❌ Could not find target entity for FK ${fromEntityName}.${prop.name}`, + ); + console.log( + ` Available entities:`, + Object.keys(entities).slice(0, 5).join(", ") + "...", + ); } } }); }); - console.log(`πŸ”— Relationship generation complete: ${relationshipCount}/${attemptedCount} relationships created`); + console.log( + `πŸ”— Relationship generation complete: ${relationshipCount}/${attemptedCount} relationships created`, + ); // Debug: check if any lines were actually added to the SVG - const relationshipLines = svg.querySelectorAll('.relationship-line'); - console.log(`πŸ”— Total relationship lines in SVG: ${relationshipLines.length}`); + const relationshipLines = svg.querySelectorAll(".relationship-line"); + console.log( + `πŸ”— Total relationship lines in SVG: ${relationshipLines.length}`, + ); } function findTargetEntity(foreignKeyName) { console.log(`πŸ” Finding target for FK: ${foreignKeyName}`); // Strategy 1: Remove 'Id' suffix (e.g., DeviceId -> Device) - let candidate = foreignKeyName.replace(/Id$/, ''); + let candidate = foreignKeyName.replace(/Id$/, ""); console.log(` Strategy 1 - Remove Id: ${candidate}`); if (entities[candidate]) { console.log(` βœ… Found: ${candidate}`); @@ -63,9 +87,9 @@ function findTargetEntity(foreignKeyName) { // Strategy 2: Try common patterns const patterns = [ - foreignKeyName.replace(/Id$/, 'Model'), // DeviceId -> DeviceModel + foreignKeyName.replace(/Id$/, "Model"), // DeviceId -> DeviceModel foreignKeyName, // Sometimes the FK name matches exactly - foreignKeyName.replace(/Model$/, ''), // DeviceModelId -> Device + foreignKeyName.replace(/Model$/, ""), // DeviceModelId -> Device ]; console.log(` Strategy 2 - Common patterns:`, patterns); @@ -78,11 +102,15 @@ function findTargetEntity(foreignKeyName) { // Strategy 3: Look for entities with similar names const entityNames = Object.keys(entities); - console.log(` Strategy 3 - Fuzzy matching against ${entityNames.length} entities`); + console.log( + ` Strategy 3 - Fuzzy matching against ${entityNames.length} entities`, + ); for (const entityName of entityNames) { // Check if the entity name is contained in the FK name - if (foreignKeyName.toLowerCase().includes(entityName.toLowerCase()) || - entityName.toLowerCase().includes(candidate.toLowerCase())) { + if ( + foreignKeyName.toLowerCase().includes(entityName.toLowerCase()) || + entityName.toLowerCase().includes(candidate.toLowerCase()) + ) { console.log(` βœ… Fuzzy match found: ${entityName}`); return entityName; } @@ -92,19 +120,36 @@ function findTargetEntity(foreignKeyName) { return null; } -function createCrowsFootRelationshipLine(svg, fromEntityName, toEntityName, propertyName) { - console.log(`🐾 Creating Crow's Foot relationship: ${fromEntityName}.${propertyName} -> ${toEntityName}`); +function createCrowsFootRelationshipLine( + svg, + fromEntityName, + toEntityName, + propertyName, +) { + console.log( + `🐾 Creating Crow's Foot relationship: ${fromEntityName}.${propertyName} -> ${toEntityName}`, + ); // Analyze the relationship to determine cardinality - const relationship = analyzeRelationshipCardinality(fromEntityName, toEntityName, propertyName); + const relationship = analyzeRelationshipCardinality( + fromEntityName, + toEntityName, + propertyName, + ); console.log(`πŸ“Š Relationship analysis:`, relationship); // Get connection points for the specific property rows - const fromInfo = getTableConnectionPoint(fromEntityName, propertyName, null); - const toInfo = getTableConnectionPoint(toEntityName, 'Id', null); + const fromInfo = getTableConnectionPoint( + fromEntityName, + propertyName, + null, + ); + const toInfo = getTableConnectionPoint(toEntityName, "Id", null); if (!fromInfo || !toInfo) { - console.log(`❌ Could not get connection points for ${fromEntityName} -> ${toEntityName}`); + console.log( + `❌ Could not get connection points for ${fromEntityName} -> ${toEntityName}`, + ); return; } @@ -122,107 +167,118 @@ function createCrowsFootRelationshipLine(svg, fromEntityName, toEntityName, prop // Check if this is a self-referential relationship if (fromEntityName === toEntityName) { - console.log(`πŸ”„ Self-referential relationship detected: ${fromEntityName} -> ${fromEntityName}`); - + console.log( + `πŸ”„ Self-referential relationship detected: ${fromEntityName} -> ${fromEntityName}`, + ); + fromX = fromInfo.leftEdge; toX = toInfo.leftEdge; - fromDirection = 'left'; - toDirection = 'left'; - + fromDirection = "left"; + toDirection = "left"; + const loopLeft = fromInfo.leftEdge - SELF_REF_LOOP_WIDTH; - + pathSegments = [ `M ${fromX} ${fromY}`, `L ${loopLeft} ${fromY}`, `L ${loopLeft} ${toY}`, - `L ${toX} ${toY}` + `L ${toX} ${toY}`, ]; - + console.log(`πŸ”„ Self-referential loop`); } else if (fromInfo.centerX < toInfo.centerX) { // LEFT->RIGHT fromX = fromInfo.rightEdge; toX = toInfo.leftEdge; - fromDirection = 'right'; - toDirection = 'left'; - + fromDirection = "right"; + toDirection = "left"; + const verticalSeparation = Math.abs(fromY - toY); const horizontalSeparation = toX - fromX; - + if (verticalSeparation > 250) { - const minHorizontalFirst = Math.max(MARKER_OFFSET, horizontalSeparation * 0.4); + const minHorizontalFirst = Math.max( + MARKER_OFFSET, + horizontalSeparation * 0.4, + ); const midX = fromX + minHorizontalFirst; const entryX = toX - MARKER_OFFSET; - + pathSegments = [ `M ${fromX} ${fromY}`, `L ${midX} ${fromY}`, `L ${midX} ${toY}`, `L ${entryX} ${toY}`, - `L ${toX} ${toY}` + `L ${toX} ${toY}`, ]; } else { const exitX = fromX + MARKER_OFFSET; const entryX = toX - MARKER_OFFSET; - + pathSegments = [ `M ${fromX} ${fromY}`, `L ${exitX} ${fromY}`, `L ${exitX} ${toY}`, `L ${entryX} ${toY}`, - `L ${toX} ${toY}` + `L ${toX} ${toY}`, ]; } } else { // RIGHT->LEFT fromX = fromInfo.leftEdge; toX = toInfo.rightEdge; - fromDirection = 'left'; - toDirection = 'right'; - + fromDirection = "left"; + toDirection = "right"; + const verticalSeparation = Math.abs(fromY - toY); const horizontalSeparation = fromX - toX; - + if (verticalSeparation > 250) { - const minHorizontalFirst = Math.max(MARKER_OFFSET, horizontalSeparation * 0.4); + const minHorizontalFirst = Math.max( + MARKER_OFFSET, + horizontalSeparation * 0.4, + ); const midX = fromX - minHorizontalFirst; const entryX = toX + MARKER_OFFSET; - + pathSegments = [ `M ${fromX} ${fromY}`, `L ${midX} ${fromY}`, `L ${midX} ${toY}`, `L ${entryX} ${toY}`, - `L ${toX} ${toY}` + `L ${toX} ${toY}`, ]; } else { const exitX = fromX - MARKER_OFFSET; const entryX = toX + MARKER_OFFSET; - + pathSegments = [ `M ${fromX} ${fromY}`, `L ${exitX} ${fromY}`, `L ${exitX} ${toY}`, `L ${entryX} ${toY}`, - `L ${toX} ${toY}` + `L ${toX} ${toY}`, ]; } } - const pathData = pathSegments.join(' '); + const pathData = pathSegments.join(" "); // Create the main line WITHOUT markers - const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - line.setAttribute('d', pathData); - line.classList.add('relationship-line'); - line.setAttribute('data-from', fromEntityName); - line.setAttribute('data-to', toEntityName); - line.setAttribute('data-property', propertyName); - line.setAttribute('data-relationship-type', relationship.type); - line.setAttribute('data-self-referential', fromEntityName === toEntityName ? 'true' : 'false'); + const line = document.createElementNS("http://www.w3.org/2000/svg", "path"); + line.setAttribute("d", pathData); + line.classList.add("relationship-line"); + line.setAttribute("data-from", fromEntityName); + line.setAttribute("data-to", toEntityName); + line.setAttribute("data-property", propertyName); + line.setAttribute("data-relationship-type", relationship.type); + line.setAttribute( + "data-self-referential", + fromEntityName === toEntityName ? "true" : "false", + ); // Insert line BEFORE any table groups to ensure it appears behind tables - const firstTableGroup = svg.querySelector('.table-group'); + const firstTableGroup = svg.querySelector(".table-group"); if (firstTableGroup) { svg.insertBefore(line, firstTableGroup); } else { @@ -230,20 +286,36 @@ function createCrowsFootRelationshipLine(svg, fromEntityName, toEntityName, prop } // Draw crow's foot notation as explicit SVG elements and track them - const fromCrowsFoot = drawCrowsFootNotation(svg, fromX, fromY, fromDirection, relationship, true); // Start point (many side) - const toCrowsFoot = drawCrowsFootNotation(svg, toX, toY, toDirection, relationship, false); // End point (one side) - + const fromCrowsFoot = drawCrowsFootNotation( + svg, + fromX, + fromY, + fromDirection, + relationship, + true, + ); // Start point (many side) + const toCrowsFoot = drawCrowsFootNotation( + svg, + toX, + toY, + toDirection, + relationship, + false, + ); // End point (one side) + // Add data attributes to crow's feet to track which relationship they belong to - fromCrowsFoot.setAttribute('data-from', fromEntityName); - fromCrowsFoot.setAttribute('data-to', toEntityName); - fromCrowsFoot.setAttribute('data-property', propertyName); - - toCrowsFoot.setAttribute('data-from', fromEntityName); - toCrowsFoot.setAttribute('data-to', toEntityName); - toCrowsFoot.setAttribute('data-property', propertyName); + fromCrowsFoot.setAttribute("data-from", fromEntityName); + fromCrowsFoot.setAttribute("data-to", toEntityName); + fromCrowsFoot.setAttribute("data-property", propertyName); + + toCrowsFoot.setAttribute("data-from", fromEntityName); + toCrowsFoot.setAttribute("data-to", toEntityName); + toCrowsFoot.setAttribute("data-property", propertyName); console.log(`🐾 Crow's Foot line created with explicit notation`); - console.log(` Type: ${relationship.type}, From: ${fromDirection}, To: ${toDirection}`); + console.log( + ` Type: ${relationship.type}, From: ${fromDirection}, To: ${toDirection}`, + ); } function drawCrowsFootNotation(svg, x, y, direction, relationship, isFromSide) { @@ -251,146 +323,179 @@ function drawCrowsFootNotation(svg, x, y, direction, relationship, isFromSide) { const CIRCLE_RADIUS = 20; // Radius for optional indicator const GAP = 5; // Small gap between circle and notation const CIRCLE_OFFSET = CROW_SIZE + CIRCLE_RADIUS + GAP; // Position circle with gap from notation - + // Use exact same blue color as relationship lines from CSS (.relationship-line) - const color = '#2563eb'; // Must match CSS .relationship-line stroke color + const color = "#2563eb"; // Must match CSS .relationship-line stroke color const strokeWidth = 8; // Must match CSS .relationship-line stroke-width - + // Determine what notation to draw based on relationship type and side let notationType; if (isFromSide) { // From side (FK table) = many side - notationType = relationship.fromOptional ? 'many-optional' : 'many'; + notationType = relationship.fromOptional ? "many-optional" : "many"; } else { // To side (PK table) = one side - notationType = relationship.toOptional ? 'one-optional' : 'one'; + notationType = relationship.toOptional ? "one-optional" : "one"; } - - console.log(`🐾 Drawing crow's foot: ${notationType} at (${x}, ${y}) facing ${direction}`); - + + console.log( + `🐾 Drawing crow's foot: ${notationType} at (${x}, ${y}) facing ${direction}`, + ); + // Calculate rotation angle based on direction let angle = 0; switch (direction) { - case 'right': + case "right": angle = 0; break; - case 'left': + case "left": angle = 180; break; - case 'up': + case "up": angle = -90; break; - case 'down': + case "down": angle = 90; break; } - + // Create a group for the notation - const group = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - group.setAttribute('transform', `translate(${x}, ${y}) rotate(${angle})`); - group.classList.add('crows-foot-notation'); - + const group = document.createElementNS("http://www.w3.org/2000/svg", "g"); + group.setAttribute("transform", `translate(${x}, ${y}) rotate(${angle})`); + group.classList.add("crows-foot-notation"); + // Draw notation based on type - if (notationType.includes('many')) { + if (notationType.includes("many")) { // Draw circle for optional relationships FIRST (positioned with gap from crow's foot) - if (notationType.includes('optional')) { - const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - circle.setAttribute('cx', CIRCLE_OFFSET); - circle.setAttribute('cy', '0'); - circle.setAttribute('r', CIRCLE_RADIUS); - circle.setAttribute('stroke', color); - circle.setAttribute('stroke-width', strokeWidth); - circle.setAttribute('fill', 'white'); // White fill so circle is visible against any background + if (notationType.includes("optional")) { + const circle = document.createElementNS( + "http://www.w3.org/2000/svg", + "circle", + ); + circle.setAttribute("cx", CIRCLE_OFFSET); + circle.setAttribute("cy", "0"); + circle.setAttribute("r", CIRCLE_RADIUS); + circle.setAttribute("stroke", color); + circle.setAttribute("stroke-width", strokeWidth); + circle.setAttribute("fill", "white"); // White fill so circle is visible against any background group.appendChild(circle); - console.log(`β­• Drew optional circle for many side at cx=${CIRCLE_OFFSET}, r=${CIRCLE_RADIUS}`); + console.log( + `β­• Drew optional circle for many side at cx=${CIRCLE_OFFSET}, r=${CIRCLE_RADIUS}`, + ); } - + // Draw crow's foot as a path with two segments forming a > shape // The path should point AWAY from the table (to the right in unrotated state) // Tip at x=CROW_SIZE (same distance as "one" side vertical line) - const crowPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + const crowPath = document.createElementNS( + "http://www.w3.org/2000/svg", + "path", + ); const pathData = `M 0,${-CROW_SIZE} L ${CROW_SIZE},0 L 0,${CROW_SIZE}`; - crowPath.setAttribute('d', pathData); - crowPath.setAttribute('stroke', color); - crowPath.setAttribute('stroke-width', strokeWidth); - crowPath.setAttribute('fill', 'none'); - crowPath.setAttribute('stroke-linejoin', 'miter'); + crowPath.setAttribute("d", pathData); + crowPath.setAttribute("stroke", color); + crowPath.setAttribute("stroke-width", strokeWidth); + crowPath.setAttribute("fill", "none"); + crowPath.setAttribute("stroke-linejoin", "miter"); group.appendChild(crowPath); } else { // Draw circle for optional relationships FIRST (positioned with gap from vertical line) - if (notationType.includes('optional')) { - const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - circle.setAttribute('cx', CIRCLE_OFFSET); - circle.setAttribute('cy', '0'); - circle.setAttribute('r', CIRCLE_RADIUS); - circle.setAttribute('stroke', color); - circle.setAttribute('stroke-width', strokeWidth); - circle.setAttribute('fill', 'white'); // White fill so circle is visible against any background + if (notationType.includes("optional")) { + const circle = document.createElementNS( + "http://www.w3.org/2000/svg", + "circle", + ); + circle.setAttribute("cx", CIRCLE_OFFSET); + circle.setAttribute("cy", "0"); + circle.setAttribute("r", CIRCLE_RADIUS); + circle.setAttribute("stroke", color); + circle.setAttribute("stroke-width", strokeWidth); + circle.setAttribute("fill", "white"); // White fill so circle is visible against any background group.appendChild(circle); - console.log(`β­• Drew optional circle for one side at cx=${CIRCLE_OFFSET}, r=${CIRCLE_RADIUS}`); + console.log( + `β­• Drew optional circle for one side at cx=${CIRCLE_OFFSET}, r=${CIRCLE_RADIUS}`, + ); } - + // One side - vertical line at CROW_SIZE distance (same as crow's foot tip) - const verticalLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); - verticalLine.setAttribute('x1', CROW_SIZE); // Same distance as crow's foot tip - verticalLine.setAttribute('y1', -CROW_SIZE); - verticalLine.setAttribute('x2', CROW_SIZE); - verticalLine.setAttribute('y2', CROW_SIZE); - verticalLine.setAttribute('stroke', color); - verticalLine.setAttribute('stroke-width', strokeWidth); + const verticalLine = document.createElementNS( + "http://www.w3.org/2000/svg", + "line", + ); + verticalLine.setAttribute("x1", CROW_SIZE); // Same distance as crow's foot tip + verticalLine.setAttribute("y1", -CROW_SIZE); + verticalLine.setAttribute("x2", CROW_SIZE); + verticalLine.setAttribute("y2", CROW_SIZE); + verticalLine.setAttribute("stroke", color); + verticalLine.setAttribute("stroke-width", strokeWidth); group.appendChild(verticalLine); } - + // Insert crow's foot BEFORE tables (at the beginning of SVG) to ensure proper z-indexing - const firstTableGroup = svg.querySelector('.table-group'); + const firstTableGroup = svg.querySelector(".table-group"); if (firstTableGroup) { svg.insertBefore(group, firstTableGroup); } else { svg.appendChild(group); } - + return group; // Return the group so we can track it with the relationship line } -function analyzeRelationshipCardinality(fromEntityName, toEntityName, propertyName) { +function analyzeRelationshipCardinality( + fromEntityName, + toEntityName, + propertyName, +) { const fromEntity = entities[fromEntityName]; const toEntity = entities[toEntityName]; if (!fromEntity || !toEntity) { - return { type: '1:N', fromOptional: false, toOptional: false }; + return { type: "1:N", fromOptional: false, toOptional: false }; } // Find the foreign key property details - const fkProperty = fromEntity.properties.find(p => p.name === propertyName); - const allFromProps = [...(fromEntity.properties || []), ...(fromEntity.inheritedProperties || [])]; - const fkProp = allFromProps.find(p => p.name === propertyName); + const fkProperty = fromEntity.properties.find( + (p) => p.name === propertyName, + ); + const allFromProps = [ + ...(fromEntity.properties || []), + ...(fromEntity.inheritedProperties || []), + ]; + const fkProp = allFromProps.find((p) => p.name === propertyName); // Check if FK is nullable (optional relationship on the "many" side) // FIXED: Only check type string for '?' - PropertyInfo doesn't have a nullable property - const isNullable = fkProp && fkProp.type.includes('?'); + const isNullable = fkProp && fkProp.type.includes("?"); // Look for navigation properties to determine relationship type - const allToProps = [...(toEntity.properties || []), ...(toEntity.inheritedProperties || [])]; + const allToProps = [ + ...(toEntity.properties || []), + ...(toEntity.inheritedProperties || []), + ]; // Check if target entity has a collection navigation property back to source - const navPropName = fromEntityName.replace(/Model$/, ''); // Remove 'Model' suffix for collection name - const collectionNavProp = allToProps.find(p => - p.type.includes(`ICollection<${fromEntityName}>`) || - p.type.includes(`List<${fromEntityName}>`) || - p.name.toLowerCase().includes(navPropName.toLowerCase() + 's') // Plural form + const navPropName = fromEntityName.replace(/Model$/, ""); // Remove 'Model' suffix for collection name + const collectionNavProp = allToProps.find( + (p) => + p.type.includes(`ICollection<${fromEntityName}>`) || + p.type.includes(`List<${fromEntityName}>`) || + p.name.toLowerCase().includes(navPropName.toLowerCase() + "s"), // Plural form ); // Check if source entity has a navigation property to target - const sourceNavPropName = toEntityName.replace(/Model$/, ''); - const sourceNavProp = allFromProps.find(p => - p.type === toEntityName || - p.type === `${toEntityName}?` || // Nullable navigation property - p.name === sourceNavPropName + const sourceNavPropName = toEntityName.replace(/Model$/, ""); + const sourceNavProp = allFromProps.find( + (p) => + p.type === toEntityName || + p.type === `${toEntityName}?` || // Nullable navigation property + p.name === sourceNavPropName, ); // Check if the navigation property on the source is nullable (indicates optional on "one" side) // FIXED: Only check type string for '?' - PropertyInfo doesn't have a nullable property - const sourceNavPropIsNullable = sourceNavProp && sourceNavProp.type.includes('?'); + const sourceNavPropIsNullable = + sourceNavProp && sourceNavProp.type.includes("?"); // Determine relationship type based on analysis let relationshipType; @@ -398,27 +503,34 @@ function analyzeRelationshipCardinality(fromEntityName, toEntityName, propertyNa if (collectionNavProp && sourceNavProp) { // Both sides have navigation properties - this is 1:N (most common) - relationshipType = '1:N'; + relationshipType = "1:N"; // The "one" side is optional if EITHER the FK is nullable OR the navigation property is nullable toOptional = isNullable || sourceNavPropIsNullable; } else if (sourceNavProp && !collectionNavProp) { // Only source has nav prop, no collection on target - could be 1:1 or 1:0..1 - relationshipType = (isNullable || sourceNavPropIsNullable) ? '1:0..1' : '1:1'; + relationshipType = + isNullable || sourceNavPropIsNullable ? "1:0..1" : "1:1"; toOptional = isNullable || sourceNavPropIsNullable; // The "one" side is optional if either is nullable } else if (collectionNavProp) { // Target has collection - definitely 1:N - relationshipType = '1:N'; + relationshipType = "1:N"; toOptional = isNullable; // The "one" side is optional if FK is nullable } else { // No navigation properties found - assume 1:N (most common for FK relationships) - relationshipType = '1:N'; + relationshipType = "1:N"; toOptional = isNullable; // The "one" side is optional if FK is nullable } - console.log(`πŸ“Š Relationship analysis for ${fromEntityName}.${propertyName} -> ${toEntityName}:`); + console.log( + `πŸ“Š Relationship analysis for ${fromEntityName}.${propertyName} -> ${toEntityName}:`, + ); console.log(` FK nullable: ${isNullable}`); - console.log(` Collection nav prop: ${collectionNavProp ? collectionNavProp.name : 'none'}`); - console.log(` Source nav prop: ${sourceNavProp ? sourceNavProp.name : 'none'}`); + console.log( + ` Collection nav prop: ${collectionNavProp ? collectionNavProp.name : "none"}`, + ); + console.log( + ` Source nav prop: ${sourceNavProp ? sourceNavProp.name : "none"}`, + ); console.log(` Source nav prop nullable: ${sourceNavPropIsNullable}`); console.log(` Determined type: ${relationshipType}`); console.log(` From optional (many side): ${isNullable}`); @@ -427,7 +539,7 @@ function analyzeRelationshipCardinality(fromEntityName, toEntityName, propertyNa return { type: relationshipType, fromOptional: isNullable, // FK side optionality (many side) - toOptional: toOptional // PK side optionality (one side) + toOptional: toOptional, // PK side optionality (one side) }; } @@ -436,25 +548,29 @@ function getCrowsFootMarkers(relationship) { let fromSide, toSide; switch (relationship.type) { - case '1:1': + case "1:1": // One-to-one: both sides have single line - fromSide = 'url(#one-side)'; - toSide = 'url(#one-side)'; + fromSide = "url(#one-side)"; + toSide = "url(#one-side)"; break; - case '1:0..1': + case "1:0..1": // One-to-zero-or-one: FK side has circle+line (optional), PK side has line - fromSide = relationship.fromOptional ? 'url(#one-optional-side)' : 'url(#one-side)'; - toSide = 'url(#one-side)'; + fromSide = relationship.fromOptional + ? "url(#one-optional-side)" + : "url(#one-side)"; + toSide = "url(#one-side)"; break; - case '1:N': + case "1:N": default: // For One-to-Many relationships: // - FROM side (FK table) = "many" side = needs crow's foot // - TO side (PK table) = "one" side = needs single line (optionally with circle if nullable) - fromSide = relationship.fromOptional ? 'url(#many-optional-side)' : 'url(#many-side)'; - toSide = 'url(#one-side)'; + fromSide = relationship.fromOptional + ? "url(#many-optional-side)" + : "url(#many-side)"; + toSide = "url(#one-side)"; break; } @@ -466,20 +582,24 @@ function getCrowsFootMarkersHighlighted(relationship) { let fromSide, toSide; switch (relationship.type) { - case '1:1': - fromSide = 'url(#one-side-highlighted)'; - toSide = 'url(#one-side-highlighted)'; + case "1:1": + fromSide = "url(#one-side-highlighted)"; + toSide = "url(#one-side-highlighted)"; break; - case '1:0..1': - fromSide = relationship.fromOptional ? 'url(#one-optional-side-highlighted)' : 'url(#one-side-highlighted)'; - toSide = 'url(#one-side-highlighted)'; + case "1:0..1": + fromSide = relationship.fromOptional + ? "url(#one-optional-side-highlighted)" + : "url(#one-side-highlighted)"; + toSide = "url(#one-side-highlighted)"; break; - case '1:N': + case "1:N": default: - fromSide = relationship.fromOptional ? 'url(#many-optional-side-highlighted)' : 'url(#many-side-highlighted)'; - toSide = 'url(#one-side-highlighted)'; + fromSide = relationship.fromOptional + ? "url(#many-optional-side-highlighted)" + : "url(#many-side-highlighted)"; + toSide = "url(#one-side-highlighted)"; break; } @@ -487,29 +607,35 @@ function getCrowsFootMarkersHighlighted(relationship) { } function getTableConnectionPoint(entityName, propertyName, direction) { - const tableGroup = document.querySelector('[data-entity="' + entityName + '"]'); + const tableGroup = document.querySelector( + '[data-entity="' + entityName + '"]', + ); if (!tableGroup) { console.log(`❌ Could not find table group for entity: ${entityName}`); return null; } - const rect = tableGroup.querySelector('.table-box'); + const rect = tableGroup.querySelector(".table-box"); if (!rect) { console.log(`❌ Could not find table-box for entity: ${entityName}`); return null; } - const x = parseFloat(rect.getAttribute('x')); - const y = parseFloat(rect.getAttribute('y')); - const width = parseFloat(rect.getAttribute('width')); - const height = parseFloat(rect.getAttribute('height')); + const x = parseFloat(rect.getAttribute("x")); + const y = parseFloat(rect.getAttribute("y")); + const width = parseFloat(rect.getAttribute("width")); + const height = parseFloat(rect.getAttribute("height")); // Get the center point of the table for direction calculation const centerX = x + width / 2; const centerY = y + height / 2; // Find the specific property connection point - const propertyY = findPropertyConnectionPoint(tableGroup, propertyName, centerY); + const propertyY = findPropertyConnectionPoint( + tableGroup, + propertyName, + centerY, + ); // Return connection point info without determining side yet return { @@ -519,73 +645,89 @@ function getTableConnectionPoint(entityName, propertyName, direction) { centerX: centerX, centerY: centerY, leftEdge: x, - rightEdge: x + width + rightEdge: x + width, }; } function findPropertyConnectionPoint(tableGroup, propertyName, fallbackY) { // Try to find the specific property text element first - const propertyTexts = tableGroup.querySelectorAll('.property-text'); + const propertyTexts = tableGroup.querySelectorAll(".property-text"); for (const propText of propertyTexts) { if (propText.textContent === propertyName) { - const y = parseFloat(propText.getAttribute('y')); + const y = parseFloat(propText.getAttribute("y")); // FIXED: Adjust Y to be at the CENTER of the property text/icon, not the baseline // Property icons are rendered at y-12 (which is the center), so we need to match that const centerY = y - 12; - console.log(`🎯 Found property ${propertyName} at y=${y}, centerY=${centerY}`); + console.log( + `🎯 Found property ${propertyName} at y=${y}, centerY=${centerY}`, + ); return centerY; } } // If not found in visible properties, check if it's 'Id' and likely inherited - if (propertyName === 'Id') { - console.log(`🎯 Property 'Id' not found in visible properties - assuming inherited, using header center`); + if (propertyName === "Id") { + console.log( + `🎯 Property 'Id' not found in visible properties - assuming inherited, using header center`, + ); - const headerRect = tableGroup.querySelector('.table-header'); + const headerRect = tableGroup.querySelector(".table-header"); if (headerRect) { - const headerY = parseFloat(headerRect.getAttribute('y')); - const headerHeight = parseFloat(headerRect.getAttribute('height')); + const headerY = parseFloat(headerRect.getAttribute("y")); + const headerHeight = parseFloat(headerRect.getAttribute("height")); const headerCenterY = headerY + headerHeight / 2; - console.log(`🎯 Using header center: headerY=${headerY}, height=${headerHeight}, center=${headerCenterY}`); + console.log( + `🎯 Using header center: headerY=${headerY}, height=${headerHeight}, center=${headerCenterY}`, + ); return headerCenterY; } } // Last resort fallback - console.log(`🎯 Using fallback center Y=${fallbackY} for property ${propertyName}`); + console.log( + `🎯 Using fallback center Y=${fallbackY} for property ${propertyName}`, + ); return fallbackY; } function updateRelationships() { - const lines = document.querySelectorAll('.relationship-line'); - const crowsFeet = document.querySelectorAll('.crows-foot-notation'); - - console.log(`πŸ”„ Updating ${lines.length} relationship lines and ${crowsFeet.length} crow's feet`); + const lines = document.querySelectorAll(".relationship-line"); + const crowsFeet = document.querySelectorAll(".crows-foot-notation"); + + console.log( + `πŸ”„ Updating ${lines.length} relationship lines and ${crowsFeet.length} crow's feet`, + ); // Remove all existing crow's feet - crowsFeet.forEach(cf => cf.remove()); + crowsFeet.forEach((cf) => cf.remove()); - lines.forEach(line => { - const from = line.getAttribute('data-from'); - const to = line.getAttribute('data-to'); - const propertyName = line.getAttribute('data-property'); - const relationshipType = line.getAttribute('data-relationship-type') || '1:N'; - const isSelfReferential = line.getAttribute('data-self-referential') === 'true'; + lines.forEach((line) => { + const from = line.getAttribute("data-from"); + const to = line.getAttribute("data-to"); + const propertyName = line.getAttribute("data-property"); + const relationshipType = + line.getAttribute("data-relationship-type") || "1:N"; + const isSelfReferential = + line.getAttribute("data-self-referential") === "true"; // Re-analyze the relationship to get proper optional indicators - const relationship = analyzeRelationshipCardinality(from, to, propertyName); + const relationship = analyzeRelationshipCardinality( + from, + to, + propertyName, + ); // Get updated connection points const fromInfo = getTableConnectionPoint(from, propertyName, null); - const toInfo = getTableConnectionPoint(to, 'Id', null); + const toInfo = getTableConnectionPoint(to, "Id", null); if (fromInfo && toInfo) { // Recreate the routing with same logic as creation const MARKER_OFFSET = 50; const SELF_REF_LOOP_WIDTH = 200; - + let fromX, fromY, toX, toY; let fromDirection, toDirection; let pathSegments = []; @@ -597,154 +739,174 @@ function updateRelationships() { // Self-referential fromX = fromInfo.leftEdge; toX = toInfo.leftEdge; - fromDirection = 'left'; - toDirection = 'left'; - + fromDirection = "left"; + toDirection = "left"; + const loopLeft = fromInfo.leftEdge - SELF_REF_LOOP_WIDTH; - + pathSegments = [ `M ${fromX} ${fromY}`, `L ${loopLeft} ${fromY}`, `L ${loopLeft} ${toY}`, - `L ${toX} ${toY}` + `L ${toX} ${toY}`, ]; } else if (fromInfo.centerX < toInfo.centerX) { // LEFT->RIGHT fromX = fromInfo.rightEdge; toX = toInfo.leftEdge; - fromDirection = 'right'; - toDirection = 'left'; - + fromDirection = "right"; + toDirection = "left"; + const verticalSeparation = Math.abs(fromY - toY); const horizontalSeparation = toX - fromX; - + if (verticalSeparation > 250) { - const minHorizontalFirst = Math.max(MARKER_OFFSET, horizontalSeparation * 0.4); + const minHorizontalFirst = Math.max( + MARKER_OFFSET, + horizontalSeparation * 0.4, + ); const midX = fromX + minHorizontalFirst; const entryX = toX - MARKER_OFFSET; - + pathSegments = [ `M ${fromX} ${fromY}`, `L ${midX} ${fromY}`, `L ${midX} ${toY}`, `L ${entryX} ${toY}`, - `L ${toX} ${toY}` + `L ${toX} ${toY}`, ]; } else { const exitX = fromX + MARKER_OFFSET; const entryX = toX - MARKER_OFFSET; - + pathSegments = [ `M ${fromX} ${fromY}`, `L ${exitX} ${fromY}`, `L ${exitX} ${toY}`, `L ${entryX} ${toY}`, - `L ${toX} ${toY}` + `L ${toX} ${toY}`, ]; } } else { // RIGHT->LEFT fromX = fromInfo.leftEdge; toX = toInfo.rightEdge; - fromDirection = 'left'; - toDirection = 'right'; - + fromDirection = "left"; + toDirection = "right"; + const verticalSeparation = Math.abs(fromY - toY); const horizontalSeparation = fromX - toX; - + if (verticalSeparation > 250) { - const minHorizontalFirst = Math.max(MARKER_OFFSET, horizontalSeparation * 0.4); + const minHorizontalFirst = Math.max( + MARKER_OFFSET, + horizontalSeparation * 0.4, + ); const midX = fromX - minHorizontalFirst; const entryX = toX + MARKER_OFFSET; - + pathSegments = [ `M ${fromX} ${fromY}`, `L ${midX} ${fromY}`, `L ${midX} ${toY}`, `L ${entryX} ${toY}`, - `L ${toX} ${toY}` + `L ${toX} ${toY}`, ]; } else { const exitX = fromX - MARKER_OFFSET; const entryX = toX + MARKER_OFFSET; - + pathSegments = [ `M ${fromX} ${fromY}`, `L ${exitX} ${fromY}`, `L ${exitX} ${toY}`, `L ${entryX} ${toY}`, - `L ${toX} ${toY}` + `L ${toX} ${toY}`, ]; } } - const pathData = pathSegments.join(' '); - line.setAttribute('d', pathData); - + const pathData = pathSegments.join(" "); + line.setAttribute("d", pathData); + // Recreate crow's feet at new positions with proper relationship analysis - const svg = document.getElementById('schema-svg'); - const fromCrowsFoot = drawCrowsFootNotation(svg, fromX, fromY, fromDirection, relationship, true); - const toCrowsFoot = drawCrowsFootNotation(svg, toX, toY, toDirection, relationship, false); - + const svg = document.getElementById("schema-svg"); + const fromCrowsFoot = drawCrowsFootNotation( + svg, + fromX, + fromY, + fromDirection, + relationship, + true, + ); + const toCrowsFoot = drawCrowsFootNotation( + svg, + toX, + toY, + toDirection, + relationship, + false, + ); + // Add data attributes to track crow's feet - fromCrowsFoot.setAttribute('data-from', from); - fromCrowsFoot.setAttribute('data-to', to); - fromCrowsFoot.setAttribute('data-property', propertyName); - - toCrowsFoot.setAttribute('data-from', from); - toCrowsFoot.setAttribute('data-to', to); - toCrowsFoot.setAttribute('data-property', propertyName); - + fromCrowsFoot.setAttribute("data-from", from); + fromCrowsFoot.setAttribute("data-to", to); + fromCrowsFoot.setAttribute("data-property", propertyName); + + toCrowsFoot.setAttribute("data-from", from); + toCrowsFoot.setAttribute("data-to", to); + toCrowsFoot.setAttribute("data-property", propertyName); + // Apply same styling to crow's feet as the relationship line const lineClasses = Array.from(line.classList); - if (lineClasses.includes('highlighted')) { - fromCrowsFoot.classList.add('highlighted'); - toCrowsFoot.classList.add('highlighted'); - } else if (lineClasses.includes('dimmed')) { - fromCrowsFoot.classList.add('dimmed'); - toCrowsFoot.classList.add('dimmed'); + if (lineClasses.includes("highlighted")) { + fromCrowsFoot.classList.add("highlighted"); + toCrowsFoot.classList.add("highlighted"); + } else if (lineClasses.includes("dimmed")) { + fromCrowsFoot.classList.add("dimmed"); + toCrowsFoot.classList.add("dimmed"); } } - line.classList.remove('highlighted', 'dimmed', 'hidden'); + line.classList.remove("highlighted", "dimmed", "hidden"); if (!showRelationships) { - line.style.display = 'none'; + line.style.display = "none"; } else { - line.style.display = 'block'; + line.style.display = "block"; if (showOnlySelectedRelations && selectedTable) { if (from === selectedTable || to === selectedTable) { - line.classList.add('highlighted'); + line.classList.add("highlighted"); } else { - line.classList.add('dimmed'); + line.classList.add("dimmed"); } } } }); - + // Update crow's feet visibility to match relationship lines - document.querySelectorAll('.crows-foot-notation').forEach(cf => { - const from = cf.getAttribute('data-from'); - const to = cf.getAttribute('data-to'); - + document.querySelectorAll(".crows-foot-notation").forEach((cf) => { + const from = cf.getAttribute("data-from"); + const to = cf.getAttribute("data-to"); + if (!showRelationships) { - cf.style.display = 'none'; + cf.style.display = "none"; } else { - cf.style.display = 'block'; - + cf.style.display = "block"; + // Apply highlighting/dimming based on selected table if (showOnlySelectedRelations && selectedTable) { if (from === selectedTable || to === selectedTable) { - cf.classList.add('highlighted'); - cf.classList.remove('dimmed'); + cf.classList.add("highlighted"); + cf.classList.remove("dimmed"); } else { - cf.classList.add('dimmed'); - cf.classList.remove('highlighted'); + cf.classList.add("dimmed"); + cf.classList.remove("highlighted"); } } else { - cf.classList.remove('highlighted', 'dimmed'); + cf.classList.remove("highlighted", "dimmed"); } } }); -} \ No newline at end of file +} diff --git a/Templates/schema-generation.js b/Templates/schema-generation.js index 9a7fe71..9331d07 100644 --- a/Templates/schema-generation.js +++ b/Templates/schema-generation.js @@ -1,10 +1,10 @@ // Schema generation and table positioning function generateSchema() { - console.log('??? Starting schema generation...'); - const svg = document.getElementById('schema-svg'); - + console.log("??? Starting schema generation..."); + const svg = document.getElementById("schema-svg"); + if (!svg) { - console.error('? SVG element not found!'); + console.error("? SVG element not found!"); return; } @@ -12,35 +12,42 @@ function generateSchema() { const currentSelection = selectedTable; // Remove existing content but keep selection overlays (they're part of table groups) - const existingContent = svg.querySelectorAll('.table-group, .relationship-line'); + const existingContent = svg.querySelectorAll( + ".table-group, .relationship-line", + ); console.log(`?? Removing ${existingContent.length} existing elements`); - existingContent.forEach(el => el.remove()); + existingContent.forEach((el) => el.remove()); - if (typeof entities === 'undefined') { - console.error('? Entities not defined!'); + if (typeof entities === "undefined") { + console.error("? Entities not defined!"); return; } const entityCount = Object.keys(entities).length; - console.log(`?? Processing ${entityCount} entities:`, Object.keys(entities)); + console.log( + `?? Processing ${entityCount} entities:`, + Object.keys(entities), + ); const tablePositions = calculateTablePositions(); - console.log('?? Table positions calculated:', tablePositions); + console.log("?? Table positions calculated:", tablePositions); let tablesGenerated = 0; Object.keys(entities).forEach((entityName) => { try { const entity = entities[entityName]; const position = tablePositions[entityName]; - + if (!position) { console.warn(`?? No position found for entity: ${entityName}`); return; } - + generateTable(svg, entity, position.x, position.y); tablesGenerated++; - console.log(`? Generated table for ${entityName} at (${position.x}, ${position.y})`); + console.log( + `? Generated table for ${entityName} at (${position.x}, ${position.y})`, + ); } catch (e) { console.error(`? Failed to generate table for ${entityName}:`, e); } @@ -51,9 +58,9 @@ function generateSchema() { if (showRelationships) { try { generateRelationships(svg); - console.log('?? Relationships generated'); + console.log("?? Relationships generated"); } catch (e) { - console.error('? Failed to generate relationships:', e); + console.error("? Failed to generate relationships:", e); } } @@ -63,15 +70,15 @@ function generateSchema() { } updateButtonStates(); - console.log('? Schema generation completed'); + console.log("? Schema generation completed"); } function calculateTablePositions() { // Always use the smart layered layout by default (auto-layout unless saved positions exist) - if (typeof calculateSmartTablePositions === 'function') { + if (typeof calculateSmartTablePositions === "function") { return calculateSmartTablePositions(); } - + // Fallback to basic grid layout if force-directed not available return calculateBasicGridLayout(); } @@ -85,7 +92,7 @@ function calculateBasicGridLayout() { savedPositions = JSON.parse(saved); } } catch (e) { - console.warn('Failed to load saved positions:', e); + console.warn("Failed to load saved positions:", e); } const positions = {}; @@ -93,17 +100,17 @@ function calculateBasicGridLayout() { if (savedPositions) { // Use saved positions if available, but snap to grid if enabled - entityNames.forEach(entityName => { + entityNames.forEach((entityName) => { if (savedPositions[entityName]) { let x = savedPositions[entityName].x; let y = savedPositions[entityName].y; - + // Snap to grid if enabled if (snapToGrid) { x = Math.round(x / 20) * 20; y = Math.round(y / 20) * 20; } - + positions[entityName] = { x, y }; } else { // Default position for new entities, snapped to grid @@ -145,4 +152,4 @@ function calculateBasicGridLayout() { } return positions; -} \ No newline at end of file +} diff --git a/Templates/settings.js b/Templates/settings.js index c063700..7ec1ead 100644 --- a/Templates/settings.js +++ b/Templates/settings.js @@ -5,15 +5,18 @@ function saveSettings() { // Save table positions const positions = {}; - document.querySelectorAll('.table-group').forEach(table => { - const entityName = table.getAttribute('data-entity'); - const rect = table.querySelector('.table-box'); + document.querySelectorAll(".table-group").forEach((table) => { + const entityName = table.getAttribute("data-entity"); + const rect = table.querySelector(".table-box"); positions[entityName] = { - x: parseFloat(rect.getAttribute('x')), - y: parseFloat(rect.getAttribute('y')) + x: parseFloat(rect.getAttribute("x")), + y: parseFloat(rect.getAttribute("y")), }; }); - localStorage.setItem(STORAGE_KEYS.tablePositions, JSON.stringify(positions)); + localStorage.setItem( + STORAGE_KEYS.tablePositions, + JSON.stringify(positions), + ); // Save view settings const settings = { @@ -22,7 +25,7 @@ function saveSettings() { showInheritedProperties, fullHeightMode, snapToGrid, - legendVisible + legendVisible, }; localStorage.setItem(STORAGE_KEYS.viewSettings, JSON.stringify(settings)); } @@ -31,7 +34,7 @@ function saveViewState() { // Save current zoom and view box state const viewState = { zoom: currentZoom, - viewBox: { ...svgViewBox } + viewBox: { ...svgViewBox }, }; localStorage.setItem(STORAGE_KEYS.viewBox, JSON.stringify(viewState)); } @@ -43,20 +46,21 @@ function loadSettings() { if (savedSettings) { const settings = JSON.parse(savedSettings); showRelationships = settings.showRelationships ?? true; - showNavigationProperties = settings.showNavigationProperties ?? false; + showNavigationProperties = + settings.showNavigationProperties ?? false; showInheritedProperties = settings.showInheritedProperties ?? true; fullHeightMode = settings.fullHeightMode ?? false; snapToGrid = settings.snapToGrid ?? true; legendVisible = settings.legendVisible ?? true; // Apply legend visibility - const legend = document.getElementById('legend'); + const legend = document.getElementById("legend"); if (legend) { - legend.style.display = legendVisible ? 'block' : 'none'; + legend.style.display = legendVisible ? "block" : "none"; } } } catch (e) { - console.warn('Failed to load settings:', e); + console.warn("Failed to load settings:", e); } // Load view state @@ -72,27 +76,33 @@ function loadSettings() { } } } catch (e) { - console.warn('Failed to load view state:', e); + console.warn("Failed to load view state:", e); } } function updateButtonStates() { // Update button active states based on current settings - const relationshipsBtn = document.getElementById('relationships-btn'); - relationshipsBtn.classList.toggle('active', showRelationships); // Fixed: was inverted - relationshipsBtn.textContent = showRelationships ? 'Hide Relations' : 'Show Relations'; + const relationshipsBtn = document.getElementById("relationships-btn"); + relationshipsBtn.classList.toggle("active", showRelationships); // Fixed: was inverted + relationshipsBtn.textContent = showRelationships + ? "Hide Relations" + : "Show Relations"; - const navPropsBtn = document.getElementById('nav-props-btn'); - navPropsBtn.classList.toggle('active', showNavigationProperties); + const navPropsBtn = document.getElementById("nav-props-btn"); + navPropsBtn.classList.toggle("active", showNavigationProperties); - const inheritedBtn = document.getElementById('inherited-props-btn'); - inheritedBtn.classList.toggle('active', showInheritedProperties); - inheritedBtn.textContent = showInheritedProperties ? 'Hide Inherited' : 'Show Inherited'; + const inheritedBtn = document.getElementById("inherited-props-btn"); + inheritedBtn.classList.toggle("active", showInheritedProperties); + inheritedBtn.textContent = showInheritedProperties + ? "Hide Inherited" + : "Show Inherited"; - const fullHeightBtn = document.getElementById('full-height-btn'); - fullHeightBtn.classList.toggle('active', fullHeightMode); + const fullHeightBtn = document.getElementById("full-height-btn"); + fullHeightBtn.classList.toggle("active", fullHeightMode); - const snapGridBtn = document.getElementById('snap-grid-btn'); - snapGridBtn.classList.toggle('active', snapToGrid); - document.getElementById('grid-background').style.display = snapToGrid ? 'block' : 'none'; -} \ No newline at end of file + const snapGridBtn = document.getElementById("snap-grid-btn"); + snapGridBtn.classList.toggle("active", snapToGrid); + document.getElementById("grid-background").style.display = snapToGrid + ? "block" + : "none"; +} diff --git a/Templates/table-generation.js b/Templates/table-generation.js index 1b39e42..ede03de 100644 --- a/Templates/table-generation.js +++ b/Templates/table-generation.js @@ -1,40 +1,48 @@ ο»Ώ// Table generation and rendering function generateTable(svg, entity, x, y) { const properties = entity.properties || []; - const inheritedProps = showInheritedProperties ? (entity.inheritedProperties || []) : []; + const inheritedProps = showInheritedProperties + ? entity.inheritedProperties || [] + : []; // Get the matching grouping rule for this table const matchingRule = getMatchingRule(entity.type); - console.log(`πŸ“Š Table "${entity.type}" matched rule: ${matchingRule ? matchingRule.name : 'none'}`); + console.log( + `πŸ“Š Table "${entity.type}" matched rule: ${matchingRule ? matchingRule.name : "none"}`, + ); // Combine and mark inherited properties const allProperties = [ - ...inheritedProps.map(p => ({ ...p, isInherited: true })), - ...properties.map(p => ({ ...p, isInherited: false })) + ...inheritedProps.map((p) => ({ ...p, isInherited: true })), + ...properties.map((p) => ({ ...p, isInherited: false })), ]; // Sort properties: inherited first (alphabetically), then regular (alphabetically) const sortedProperties = sortProperties(allProperties); // Filter visible properties based on navigation toggle - const visibleProperties = showNavigationProperties ? - sortedProperties : - sortedProperties.filter(p => !isNavigationProperty(p)); + const visibleProperties = showNavigationProperties + ? sortedProperties + : sortedProperties.filter((p) => !isNavigationProperty(p)); // Group properties with their related navigation properties - const groupedProperties = groupPropertiesWithNavigation(visibleProperties, sortedProperties); + const groupedProperties = groupPropertiesWithNavigation( + visibleProperties, + sortedProperties, + ); const rowHeight = 55; // Increased from 22 to 55 (2.5x larger) const headerHeight = 88; // Increased from 35 to 88 (2.5x larger) - const inheritanceHeight = (entity.baseType && showInheritedProperties) ? 50 : 0; // Increased from 20 to 50 (2.5x larger) + const inheritanceHeight = + entity.baseType && showInheritedProperties ? 50 : 0; // Increased from 20 to 50 (2.5x larger) const padding = 30; // Increased from 12 to 30 (2.5x larger) const minWidth = 1000; // Increased from 400 to 1000 (2.5x larger) const iconWidth = 60; // Increased from 24 to 60 (2.5x larger) // Calculate actual displayed rows for proper height calculation let totalDisplayRows = 0; - groupedProperties.forEach(item => { - if (item.type === 'group') { + groupedProperties.forEach((item) => { + if (item.type === "group") { totalDisplayRows += item.properties.length; } else { totalDisplayRows += 1; @@ -43,8 +51,8 @@ function generateTable(svg, entity, x, y) { // Calculate width based on ACTUAL content that will be displayed const allDisplayedProperties = []; - groupedProperties.forEach(item => { - if (item.type === 'group') { + groupedProperties.forEach((item) => { + if (item.type === "group") { allDisplayedProperties.push(...item.properties); } else { allDisplayedProperties.push(item.property); @@ -54,110 +62,145 @@ function generateTable(svg, entity, x, y) { // Calculate the maximum lengths for proper width allocation const maxNameLength = Math.max( entity.type.length, - ...allDisplayedProperties.map(p => p.name.length) + ...allDisplayedProperties.map((p) => p.name.length), ); const maxTypeLength = Math.max( 8, // minimum for "string" - ...allDisplayedProperties.map(p => { + ...allDisplayedProperties.map((p) => { // Clean up type names for accurate length calculation (includes any "(maxLength)" suffix) return getDisplayType(p).length; - }) + }), ); // Calculate table width with optimized spacing and space for type icons - const nameColumnWidth = Math.max(200, maxNameLength * 18); // Increased from 80 and 7 to 200 and 18 (2.5x larger) - const typeColumnWidth = Math.max(150, maxTypeLength * 15); // Increased from 60 and 6 to 150 and 15 (2.5x larger) + const nameColumnWidth = Math.max(200, maxNameLength * 18); // Increased from 80 and 7 to 200 and 18 (2.5x larger) + const typeColumnWidth = Math.max(150, maxTypeLength * 15); // Increased from 60 and 6 to 150 and 15 (2.5x larger) const typeIconSpace = 50; // Increased from 20 to 50 (2.5x larger) - const spacingBuffer = 100; // Increased from 40 to 100 (2.5x larger) + const spacingBuffer = 100; // Increased from 40 to 100 (2.5x larger) // Add space for table icon in header const tableIconSpace = 40; const tableWidth = Math.max( minWidth, - iconWidth + nameColumnWidth + typeColumnWidth + typeIconSpace + spacingBuffer + (padding * 2) + iconWidth + + nameColumnWidth + + typeColumnWidth + + typeIconSpace + + spacingBuffer + + padding * 2, ); // For full height mode, use actual total rows, otherwise limit to 15 - const maxVisibleRows = fullHeightMode ? totalDisplayRows : Math.min(totalDisplayRows, 15); - const calculatedHeight = headerHeight + inheritanceHeight + (maxVisibleRows * rowHeight) + padding; + const maxVisibleRows = fullHeightMode + ? totalDisplayRows + : Math.min(totalDisplayRows, 15); + const calculatedHeight = + headerHeight + inheritanceHeight + maxVisibleRows * rowHeight + padding; const tableHeight = calculatedHeight; - const tableGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - tableGroup.classList.add('table-group'); - tableGroup.setAttribute('data-entity', entity.type); + const tableGroup = document.createElementNS( + "http://www.w3.org/2000/svg", + "g", + ); + tableGroup.classList.add("table-group"); + tableGroup.setAttribute("data-entity", entity.type); // Apply custom background color if rule matched - const tableRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - tableRect.setAttribute('x', x); - tableRect.setAttribute('y', y); - tableRect.setAttribute('width', tableWidth); - tableRect.setAttribute('height', tableHeight); - tableRect.classList.add('table-box'); + const tableRect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect", + ); + tableRect.setAttribute("x", x); + tableRect.setAttribute("y", y); + tableRect.setAttribute("width", tableWidth); + tableRect.setAttribute("height", tableHeight); + tableRect.classList.add("table-box"); if (matchingRule) { - tableRect.style.fill = matchingRule.color + '10'; // 10% opacity for background + tableRect.style.fill = matchingRule.color + "10"; // 10% opacity for background tableRect.style.stroke = matchingRule.color; } tableGroup.appendChild(tableRect); // SECOND: Render property group backgrounds and section dividers - const propertiesToShow = getPropertiesToShow(groupedProperties, maxVisibleRows); + const propertiesToShow = getPropertiesToShow( + groupedProperties, + maxVisibleRows, + ); let currentY = y + headerHeight + inheritanceHeight; let lastPropertyWasInherited = false; propertiesToShow.forEach((item, index) => { - if (item.type === 'group') { + if (item.type === "group") { // Check if we need a section divider const firstProp = item.properties[0]; if (lastPropertyWasInherited && !firstProp.isInherited) { // Add section divider line - adjusted Y position to be closer to properties - const divider = document.createElementNS('http://www.w3.org/2000/svg', 'line'); - divider.setAttribute('x1', x + 10); - divider.setAttribute('y1', currentY - 5); // Changed from -10 to -5 to move it down - divider.setAttribute('x2', x + tableWidth - 10); - divider.setAttribute('y2', currentY - 5); // Changed from -10 to -5 to move it down - divider.classList.add('section-divider'); + const divider = document.createElementNS( + "http://www.w3.org/2000/svg", + "line", + ); + divider.setAttribute("x1", x + 10); + divider.setAttribute("y1", currentY - 5); // Changed from -10 to -5 to move it down + divider.setAttribute("x2", x + tableWidth - 10); + divider.setAttribute("y2", currentY - 5); // Changed from -10 to -5 to move it down + divider.classList.add("section-divider"); tableGroup.appendChild(divider); } // Draw group background with proper positioning - const groupRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - groupRect.setAttribute('x', x + 5); - groupRect.setAttribute('y', currentY - 10); - groupRect.setAttribute('width', tableWidth - 10); - groupRect.setAttribute('height', item.properties.length * rowHeight + 10); - groupRect.classList.add('property-group'); + const groupRect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect", + ); + groupRect.setAttribute("x", x + 5); + groupRect.setAttribute("y", currentY - 10); + groupRect.setAttribute("width", tableWidth - 10); + groupRect.setAttribute( + "height", + item.properties.length * rowHeight + 10, + ); + groupRect.classList.add("property-group"); if (firstProp.isInherited) { - groupRect.classList.add('inherited-section'); + groupRect.classList.add("inherited-section"); } tableGroup.appendChild(groupRect); // Add connecting line for FK-NAV groups - if (item.properties.length === 2 && + if ( + item.properties.length === 2 && item.properties[0].isForeignKey && - isNavigationProperty(item.properties[1])) { - const connectLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); - connectLine.setAttribute('x1', x + 25); - connectLine.setAttribute('y1', currentY + 10); - connectLine.setAttribute('x2', x + 25); - connectLine.setAttribute('y2', currentY + rowHeight + 10); - connectLine.classList.add('fk-nav-connector'); + isNavigationProperty(item.properties[1]) + ) { + const connectLine = document.createElementNS( + "http://www.w3.org/2000/svg", + "line", + ); + connectLine.setAttribute("x1", x + 25); + connectLine.setAttribute("y1", currentY + 10); + connectLine.setAttribute("x2", x + 25); + connectLine.setAttribute("y2", currentY + rowHeight + 10); + connectLine.classList.add("fk-nav-connector"); tableGroup.appendChild(connectLine); } currentY += item.properties.length * rowHeight; - lastPropertyWasInherited = item.properties[item.properties.length - 1].isInherited; + lastPropertyWasInherited = + item.properties[item.properties.length - 1].isInherited; } else { // Check if we need a section divider if (lastPropertyWasInherited && !item.property.isInherited) { // Add section divider line - adjusted Y position to be closer to properties - const divider = document.createElementNS('http://www.w3.org/2000/svg', 'line'); - divider.setAttribute('x1', x + 10); - divider.setAttribute('y1', currentY - 5); // Changed from -10 to -5 to move it down - divider.setAttribute('x2', x + tableWidth - 10); - divider.setAttribute('y2', currentY - 5); // Changed from -10 to -5 to move it down - divider.classList.add('section-divider'); + const divider = document.createElementNS( + "http://www.w3.org/2000/svg", + "line", + ); + divider.setAttribute("x1", x + 10); + divider.setAttribute("y1", currentY - 5); // Changed from -10 to -5 to move it down + divider.setAttribute("x2", x + tableWidth - 10); + divider.setAttribute("y2", currentY - 5); // Changed from -10 to -5 to move it down + divider.classList.add("section-divider"); tableGroup.appendChild(divider); } @@ -167,12 +210,15 @@ function generateTable(svg, entity, x, y) { }); // Apply custom header color if rule matched - const headerRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - headerRect.setAttribute('x', x); - headerRect.setAttribute('y', y); - headerRect.setAttribute('width', tableWidth); - headerRect.setAttribute('height', headerHeight); - headerRect.classList.add('table-header'); + const headerRect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect", + ); + headerRect.setAttribute("x", x); + headerRect.setAttribute("y", y); + headerRect.setAttribute("width", tableWidth); + headerRect.setAttribute("height", headerHeight); + headerRect.classList.add("table-header"); if (matchingRule) { headerRect.style.fill = matchingRule.color; } @@ -181,19 +227,25 @@ function generateTable(svg, entity, x, y) { // Table icon removed - it was broken and not needed // THIRD: Draw the header and title (these will appear ON TOP of the property groups) - all tables use same style now - const titleText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + const titleText = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); // Center title normally (no icon offset needed) const titleX = x + tableWidth / 2; - titleText.setAttribute('x', titleX); - titleText.setAttribute('y', y + headerHeight / 2 + 5); // Changed from +18 to +5 for better vertical centering - titleText.setAttribute('text-anchor', 'middle'); - titleText.setAttribute('dominant-baseline', 'middle'); // Add this for proper vertical centering - titleText.classList.add('table-title'); + titleText.setAttribute("x", titleX); + titleText.setAttribute("y", y + headerHeight / 2 + 5); // Changed from +18 to +5 for better vertical centering + titleText.setAttribute("text-anchor", "middle"); + titleText.setAttribute("dominant-baseline", "middle"); // Add this for proper vertical centering + titleText.classList.add("table-title"); titleText.textContent = entity.type; - + // Add tooltip if table comment exists if (entity.comment) { - const title = document.createElementNS('http://www.w3.org/2000/svg', 'title'); + const title = document.createElementNS( + "http://www.w3.org/2000/svg", + "title", + ); title.textContent = entity.comment; titleText.appendChild(title); } @@ -202,7 +254,10 @@ function generateTable(svg, entity, x, y) { const baseFontSize = 40; // Current base font size const maxFontSize = 80; // Double the base for better readability when zoomed out const minFontSize = 32; // Minimum readable size - const availableWidth = tableWidth - (padding * 2) - (matchingRule && matchingRule.icon ? tableIconSpace : 0); // Leave some padding on sides + const availableWidth = + tableWidth - + padding * 2 - + (matchingRule && matchingRule.icon ? tableIconSpace : 0); // Leave some padding on sides // Estimate text width: approximately 0.6 * fontSize * characterCount for Segoe UI Bold let optimalFontSize = maxFontSize; @@ -219,16 +274,21 @@ function generateTable(svg, entity, x, y) { titleText.style.fontSize = `${optimalFontSize}px`; tableGroup.appendChild(titleText); - console.log(`πŸ“ Title "${entity.type}": ${characterCount} chars, optimal font: ${optimalFontSize}px, available width: ${availableWidth}px`); + console.log( + `πŸ“ Title "${entity.type}": ${characterCount} chars, optimal font: ${optimalFontSize}px, available width: ${availableWidth}px`, + ); // Add inheritance indicator (only if showing inherited properties) if (entity.baseType && showInheritedProperties) { - const inheritanceText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - inheritanceText.setAttribute('x', x + tableWidth / 2); - inheritanceText.setAttribute('y', y + headerHeight + 35); // Increased from 14 to 35 (2.5x larger) - inheritanceText.setAttribute('text-anchor', 'middle'); - inheritanceText.classList.add('inheritance-text'); - inheritanceText.textContent = 'inherits from ' + entity.baseType; + const inheritanceText = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + inheritanceText.setAttribute("x", x + tableWidth / 2); + inheritanceText.setAttribute("y", y + headerHeight + 35); // Increased from 14 to 35 (2.5x larger) + inheritanceText.setAttribute("text-anchor", "middle"); + inheritanceText.classList.add("inheritance-text"); + inheritanceText.textContent = "inherits from " + entity.baseType; tableGroup.appendChild(inheritanceText); } @@ -237,18 +297,36 @@ function generateTable(svg, entity, x, y) { let rowCount = 0; propertiesToShow.forEach((item, index) => { - if (item.type === 'group') { + if (item.type === "group") { // Render each property in the group item.properties.forEach((property, propIndex) => { - const propY = currentY + (propIndex * rowHeight) + 40; // Increased from 16 to 40 (2.5x larger) - renderProperty(tableGroup, property, x, propY, tableWidth, padding, iconWidth, entity.type); + const propY = currentY + propIndex * rowHeight + 40; // Increased from 16 to 40 (2.5x larger) + renderProperty( + tableGroup, + property, + x, + propY, + tableWidth, + padding, + iconWidth, + entity.type, + ); }); currentY += item.properties.length * rowHeight; rowCount += item.properties.length; } else { const propY = currentY + 40; // Increased from 16 to 40 (2.5x larger) - renderProperty(tableGroup, item.property, x, propY, tableWidth, padding, iconWidth, entity.type); + renderProperty( + tableGroup, + item.property, + x, + propY, + tableWidth, + padding, + iconWidth, + entity.type, + ); currentY += rowHeight; rowCount += 1; } @@ -257,26 +335,33 @@ function generateTable(svg, entity, x, y) { // Add ellipsis if there are more properties if (!fullHeightMode && totalDisplayRows > maxVisibleRows) { const ellipsisY = currentY + 15; // Increased from 6 to 15 (2.5x larger) - const ellipsisText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - ellipsisText.setAttribute('x', x + tableWidth / 2); - ellipsisText.setAttribute('y', ellipsisY); - ellipsisText.setAttribute('text-anchor', 'middle'); - ellipsisText.classList.add('property-text'); - ellipsisText.textContent = '... (+' + (totalDisplayRows - maxVisibleRows) + ' more)'; - ellipsisText.style.fontSize = '28px'; // Increased from 11px to 28px (2.5x larger) - ellipsisText.style.fill = '#9ca3af'; + const ellipsisText = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + ellipsisText.setAttribute("x", x + tableWidth / 2); + ellipsisText.setAttribute("y", ellipsisY); + ellipsisText.setAttribute("text-anchor", "middle"); + ellipsisText.classList.add("property-text"); + ellipsisText.textContent = + "... (+" + (totalDisplayRows - maxVisibleRows) + " more)"; + ellipsisText.style.fontSize = "28px"; // Increased from 11px to 28px (2.5x larger) + ellipsisText.style.fill = "#9ca3af"; tableGroup.appendChild(ellipsisText); } // FIFTH: Create selection overlay LAST (always present but hidden) - this ensures it renders on top - const selectionOverlay = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - selectionOverlay.setAttribute('x', x); - selectionOverlay.setAttribute('y', y); - selectionOverlay.setAttribute('width', tableWidth); - selectionOverlay.setAttribute('height', tableHeight); - selectionOverlay.classList.add('selection-overlay'); - selectionOverlay.setAttribute('data-entity', entity.type); - selectionOverlay.style.display = 'none'; // Hidden by default + const selectionOverlay = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect", + ); + selectionOverlay.setAttribute("x", x); + selectionOverlay.setAttribute("y", y); + selectionOverlay.setAttribute("width", tableWidth); + selectionOverlay.setAttribute("height", tableHeight); + selectionOverlay.classList.add("selection-overlay"); + selectionOverlay.setAttribute("data-entity", entity.type); + selectionOverlay.style.display = "none"; // Hidden by default tableGroup.appendChild(selectionOverlay); setupTableInteraction(tableGroup, entity.type); @@ -288,7 +373,7 @@ function getPropertiesToShow(groupedProperties, maxVisibleRows) { let currentRowCount = 0; for (const item of groupedProperties) { - if (item.type === 'group') { + if (item.type === "group") { const remainingRows = maxVisibleRows - currentRowCount; if (remainingRows <= 0) break; @@ -299,8 +384,8 @@ function getPropertiesToShow(groupedProperties, maxVisibleRows) { } else { // Partially include group result.push({ - type: 'group', - properties: item.properties.slice(0, remainingRows) + type: "group", + properties: item.properties.slice(0, remainingRows), }); currentRowCount = maxVisibleRows; break; @@ -315,7 +400,16 @@ function getPropertiesToShow(groupedProperties, maxVisibleRows) { return result; } -function renderProperty(tableGroup, property, x, propY, tableWidth, padding, iconWidth, entityName) { +function renderProperty( + tableGroup, + property, + x, + propY, + tableWidth, + padding, + iconWidth, + entityName, +) { // Fixed left position for all property names (consistent alignment) const propertyNameX = x + iconWidth + 10; // Increased from 4 to 10 (2.5x larger) @@ -324,14 +418,17 @@ function renderProperty(tableGroup, property, x, propY, tableWidth, padding, ico if (propertyIcon) { const iconElements = createPropertyIcon(propertyIcon, x + 10, propY); // Increased from 4 to 10 (2.5x larger) - iconElements.forEach(element => { + iconElements.forEach((element) => { tableGroup.appendChild(element); // Add click handler for FK icons - navigate to related entity - if (propertyIcon === 'FK' && property.isForeignKey) { - element.style.cursor = 'pointer'; - element.addEventListener('click', () => { - const target = getNavigationTargetType(property.name, entityName); + if (propertyIcon === "FK" && property.isForeignKey) { + element.style.cursor = "pointer"; + element.addEventListener("click", () => { + const target = getNavigationTargetType( + property.name, + entityName, + ); if (target) { navigateToEntity(target); } @@ -339,81 +436,101 @@ function renderProperty(tableGroup, property, x, propY, tableWidth, padding, ico } // Add click handler for Navigation icons - navigate to related entity - if (propertyIcon === 'N' && isNavigationProperty(property)) { - element.style.cursor = 'pointer'; - element.addEventListener('click', () => navigateToEntity(property.type)); + if (propertyIcon === "N" && isNavigationProperty(property)) { + element.style.cursor = "pointer"; + element.addEventListener("click", () => + navigateToEntity(property.type), + ); } }); } // Property name positioned at consistent left position - const propText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - propText.setAttribute('x', propertyNameX); - propText.setAttribute('y', propY); - propText.classList.add('property-text'); + const propText = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); + propText.setAttribute("x", propertyNameX); + propText.setAttribute("y", propY); + propText.classList.add("property-text"); if (property.isInherited) { - propText.classList.add('property-inherited'); + propText.classList.add("property-inherited"); } else if (property.isKey) { - propText.classList.add('property-key'); + propText.classList.add("property-key"); } else if (property.isForeignKey) { - propText.classList.add('property-foreign-key'); + propText.classList.add("property-foreign-key"); // Add click handler for FK property names too - propText.style.cursor = 'pointer'; - propText.addEventListener('click', () => { + propText.style.cursor = "pointer"; + propText.addEventListener("click", () => { const target = getNavigationTargetType(property.name, entityName); if (target) { navigateToEntity(target); } }); } else if (isNavigationProperty(property)) { - propText.classList.add('property-navigation'); - propText.style.cursor = 'pointer'; - propText.addEventListener('click', () => navigateToEntity(property.type)); + propText.classList.add("property-navigation"); + propText.style.cursor = "pointer"; + propText.addEventListener("click", () => + navigateToEntity(property.type), + ); } propText.textContent = property.name; - + // Add tooltip if comment exists if (property.comment) { - const title = document.createElementNS('http://www.w3.org/2000/svg', 'title'); + const title = document.createElementNS( + "http://www.w3.org/2000/svg", + "title", + ); title.textContent = property.comment; propText.appendChild(title); } - + tableGroup.appendChild(propText); // Add underline for navigation properties since SVG doesn't support text-decoration properly if (isNavigationProperty(property)) { // Estimate text width based on character count and font size const estimatedTextWidth = property.name.length * 19; // Increased from 7.5 to 19 (2.5x larger) - const underline = document.createElementNS('http://www.w3.org/2000/svg', 'line'); - underline.setAttribute('x1', propertyNameX); - underline.setAttribute('y1', propY + 5); // Increased from 2 to 5 (2.5x larger) - underline.setAttribute('x2', propertyNameX + estimatedTextWidth); - underline.setAttribute('y2', propY + 5); // Increased from 2 to 5 (2.5x larger) - underline.setAttribute('stroke', '#059669'); - underline.setAttribute('stroke-width', '2.5'); // Increased from 1 to 2.5 (2.5x larger) - underline.classList.add('navigation-underline'); - underline.style.cursor = 'pointer'; - underline.addEventListener('click', () => navigateToEntity(property.type)); + const underline = document.createElementNS( + "http://www.w3.org/2000/svg", + "line", + ); + underline.setAttribute("x1", propertyNameX); + underline.setAttribute("y1", propY + 5); // Increased from 2 to 5 (2.5x larger) + underline.setAttribute("x2", propertyNameX + estimatedTextWidth); + underline.setAttribute("y2", propY + 5); // Increased from 2 to 5 (2.5x larger) + underline.setAttribute("stroke", "#059669"); + underline.setAttribute("stroke-width", "2.5"); // Increased from 1 to 2.5 (2.5x larger) + underline.classList.add("navigation-underline"); + underline.style.cursor = "pointer"; + underline.addEventListener("click", () => + navigateToEntity(property.type), + ); tableGroup.appendChild(underline); } // Property type (right-aligned with proper spacing and color coding) - const typeText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + const typeText = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); // For navigation properties, always show the navigation arrow icon - const typeIcon = isNavigationProperty(property) ? - { iconType: 'Navigation', isNullable: property.type.endsWith('?') } : - getTypeIcon(property.type); - - const typeTextX = typeIcon ? (x + tableWidth - padding - 50) : (x + tableWidth - padding); // Increased from 20 to 50 (2.5x larger) + const typeIcon = isNavigationProperty(property) + ? { iconType: "Navigation", isNullable: property.type.endsWith("?") } + : getTypeIcon(property.type); + + const typeTextX = typeIcon + ? x + tableWidth - padding - 50 + : x + tableWidth - padding; // Increased from 20 to 50 (2.5x larger) - typeText.setAttribute('x', typeTextX); - typeText.setAttribute('y', propY); - typeText.setAttribute('text-anchor', 'end'); // Right-align the type text - typeText.classList.add('property-text', 'property-type'); + typeText.setAttribute("x", typeTextX); + typeText.setAttribute("y", propY); + typeText.setAttribute("text-anchor", "end"); // Right-align the type text + typeText.classList.add("property-text", "property-type"); // Add type-specific color class const typeColorClass = getTypeColorClass(property.type); @@ -425,8 +542,15 @@ function renderProperty(tableGroup, property, x, propY, tableWidth, padding, ico typeText.textContent = displayType; // Surface the max length in the type tooltip too, so it is discoverable on hover - if (property.maxLength && property.maxLength > 0 && !isNavigationProperty(property)) { - const typeTitle = document.createElementNS('http://www.w3.org/2000/svg', 'title'); + if ( + property.maxLength && + property.maxLength > 0 && + !isNavigationProperty(property) + ) { + const typeTitle = document.createElementNS( + "http://www.w3.org/2000/svg", + "title", + ); typeTitle.textContent = `Maximum length: ${property.maxLength}`; typeText.appendChild(typeTitle); } @@ -435,14 +559,20 @@ function renderProperty(tableGroup, property, x, propY, tableWidth, padding, ico // Add type icon to the RIGHT of the type text - with corrected Y positioning if (typeIcon) { - const typeIconElements = createPropertyIcon(typeIcon, x + tableWidth - padding - 40, propY); // Increased from 16 to 40 (2.5x larger) - typeIconElements.forEach(element => { + const typeIconElements = createPropertyIcon( + typeIcon, + x + tableWidth - padding - 40, + propY, + ); // Increased from 16 to 40 (2.5x larger) + typeIconElements.forEach((element) => { tableGroup.appendChild(element); // Add click handler for navigation property type icons if (isNavigationProperty(property)) { - element.style.cursor = 'pointer'; - element.addEventListener('click', () => navigateToEntity(property.type)); + element.style.cursor = "pointer"; + element.addEventListener("click", () => + navigateToEntity(property.type), + ); } }); } @@ -451,16 +581,18 @@ function renderProperty(tableGroup, property, x, propY, tableWidth, padding, ico // Helper function to get navigation target type from FK property name function getNavigationTargetType(fkPropertyName, entityName) { // Remove 'Id' suffix to get the expected navigation property name - const navPropName = fkPropertyName.replace(/Id$/, ''); + const navPropName = fkPropertyName.replace(/Id$/, ""); // Look up the entity data from our global entities array - const entityData = entities.find(e => e.type === entityName); + const entityData = entities.find((e) => e.type === entityName); if (entityData) { // Look for a navigation property with the expected name - const allProps = [...(entityData.properties || []), ...(entityData.inheritedProperties || [])]; - const navProp = allProps.find(p => - p.name === navPropName && - isNavigationProperty(p) + const allProps = [ + ...(entityData.properties || []), + ...(entityData.inheritedProperties || []), + ]; + const navProp = allProps.find( + (p) => p.name === navPropName && isNavigationProperty(p), ); if (navProp) { @@ -469,35 +601,37 @@ function getNavigationTargetType(fkPropertyName, entityName) { } // Fallback: assume it follows naming convention - return navPropName + 'Model'; + return navPropName + "Model"; } // Table interaction setup - handles selection and deselection function setupTableInteraction(tableGroup, entityType) { // Add click handler to the entire table group - tableGroup.addEventListener('click', function(e) { + tableGroup.addEventListener("click", function (e) { e.stopPropagation(); // Prevent event bubbling to background - + // Check if user clicked on an interactive element (navigation links, icons, etc.) const clickedElement = e.target; - + // More specific check for interactive elements that should not trigger table selection - const isInteractiveElement = - clickedElement.style.cursor === 'pointer' && ( - // FK or Navigation property links - clickedElement.classList.contains('property-foreign-key') || - clickedElement.classList.contains('property-navigation') || - clickedElement.classList.contains('navigation-underline') || + const isInteractiveElement = + clickedElement.style.cursor === "pointer" && + // FK or Navigation property links + (clickedElement.classList.contains("property-foreign-key") || + clickedElement.classList.contains("property-navigation") || + clickedElement.classList.contains("navigation-underline") || // Property icons that have navigation functionality - (clickedElement.tagName === 'circle' || clickedElement.tagName === 'rect' || clickedElement.tagName === 'text') && - clickedElement.parentElement && clickedElement.parentElement.style.cursor === 'pointer' - ); - + ((clickedElement.tagName === "circle" || + clickedElement.tagName === "rect" || + clickedElement.tagName === "text") && + clickedElement.parentElement && + clickedElement.parentElement.style.cursor === "pointer")); + // If clicked on interactive element, let it handle its own action if (isInteractiveElement) { return; } - + // Handle table selection/deselection if (selectedTable === entityType) { // If this table is already selected, deselect it @@ -509,14 +643,14 @@ function setupTableInteraction(tableGroup, entityType) { }); // Add hover effect for better user experience - tableGroup.addEventListener('mouseenter', function() { + tableGroup.addEventListener("mouseenter", function () { if (selectedTable !== entityType) { - tableGroup.style.cursor = 'pointer'; + tableGroup.style.cursor = "pointer"; } }); - tableGroup.addEventListener('mouseleave', function() { - tableGroup.style.cursor = 'default'; + tableGroup.addEventListener("mouseleave", function () { + tableGroup.style.cursor = "default"; }); } @@ -524,32 +658,32 @@ function setupTableInteraction(tableGroup, entityType) { function selectTable(entityType) { // Clear any previously selected table clearTableSelection(); - + // Set new selected table selectedTable = entityType; showOnlySelectedRelations = true; - + // Find and highlight the selected table const tableGroup = document.querySelector(`[data-entity="${entityType}"]`); if (tableGroup) { // Add selected class to table group for CSS styling - tableGroup.classList.add('selected'); - - const selectionOverlay = tableGroup.querySelector('.selection-overlay'); + tableGroup.classList.add("selected"); + + const selectionOverlay = tableGroup.querySelector(".selection-overlay"); if (selectionOverlay) { - selectionOverlay.style.display = 'block'; + selectionOverlay.style.display = "block"; } - + // Add selected class to table box for additional styling if needed - const tableBox = tableGroup.querySelector('.table-box'); + const tableBox = tableGroup.querySelector(".table-box"); if (tableBox) { - tableBox.classList.add('selected'); + tableBox.classList.add("selected"); } } - + // Update relationships to show only selected table's relationships updateRelationships(); - + console.log(`πŸ“‹ Selected table: ${entityType}`); } @@ -557,32 +691,35 @@ function selectTable(entityType) { function deselectTable() { if (selectedTable) { const entityType = selectedTable; - + // Clear selection state selectedTable = null; showOnlySelectedRelations = false; - + // Remove visual selection indicators - const tableGroup = document.querySelector(`[data-entity="${entityType}"]`); + const tableGroup = document.querySelector( + `[data-entity="${entityType}"]`, + ); if (tableGroup) { // Remove selected class from table group - tableGroup.classList.remove('selected'); - - const selectionOverlay = tableGroup.querySelector('.selection-overlay'); + tableGroup.classList.remove("selected"); + + const selectionOverlay = + tableGroup.querySelector(".selection-overlay"); if (selectionOverlay) { - selectionOverlay.style.display = 'none'; + selectionOverlay.style.display = "none"; } - + // Remove selected class from table box - const tableBox = tableGroup.querySelector('.table-box'); + const tableBox = tableGroup.querySelector(".table-box"); if (tableBox) { - tableBox.classList.remove('selected'); + tableBox.classList.remove("selected"); } } - + // Update relationships to show all relationships again updateRelationships(); - + console.log(`πŸ“‹ Deselected table: ${entityType}`); } } @@ -595,31 +732,31 @@ function clearTableSelection() { } function moveTable(tableGroup, x, y) { - const oldRect = tableGroup.querySelector('.table-box'); - const oldX = parseFloat(oldRect.getAttribute('x')); - const oldY = parseFloat(oldRect.getAttribute('y')); + const oldRect = tableGroup.querySelector(".table-box"); + const oldX = parseFloat(oldRect.getAttribute("x")); + const oldY = parseFloat(oldRect.getAttribute("y")); const deltaX = x - oldX; const deltaY = y - oldY; // Update all rect, text, and line elements - const elements = tableGroup.querySelectorAll('rect, text, line'); - elements.forEach(element => { - if (element.tagName === 'line') { + const elements = tableGroup.querySelectorAll("rect, text, line"); + elements.forEach((element) => { + if (element.tagName === "line") { // Handle line elements - const x1 = parseFloat(element.getAttribute('x1')); - const y1 = parseFloat(element.getAttribute('y1')); - const x2 = parseFloat(element.getAttribute('x2')); - const y2 = parseFloat(element.getAttribute('y2')); - element.setAttribute('x1', x1 + deltaX); - element.setAttribute('y1', y1 + deltaY); - element.setAttribute('x2', x2 + deltaX); - element.setAttribute('y2', y2 + deltaY); + const x1 = parseFloat(element.getAttribute("x1")); + const y1 = parseFloat(element.getAttribute("y1")); + const x2 = parseFloat(element.getAttribute("x2")); + const y2 = parseFloat(element.getAttribute("y2")); + element.setAttribute("x1", x1 + deltaX); + element.setAttribute("y1", y1 + deltaY); + element.setAttribute("x2", x2 + deltaX); + element.setAttribute("y2", y2 + deltaY); } else { // Handle rect and text elements - const currentX = parseFloat(element.getAttribute('x')); - const currentY = parseFloat(element.getAttribute('y')); - element.setAttribute('x', currentX + deltaX); - element.setAttribute('y', currentY + deltaY); + const currentX = parseFloat(element.getAttribute("x")); + const currentY = parseFloat(element.getAttribute("y")); + element.setAttribute("x", currentX + deltaX); + element.setAttribute("y", currentY + deltaY); } }); -} \ No newline at end of file +} diff --git a/Templates/table-grouping.js b/Templates/table-grouping.js index bd497b0..70c2a8b 100644 --- a/Templates/table-grouping.js +++ b/Templates/table-grouping.js @@ -4,17 +4,17 @@ function openTableGroupingDialog() { // Automatically remove unused rules before populating UI autoRemoveUnusedRules(); - + populateRulesUI(); analyzeUnusedRules(); // Analyze again to show any remaining issues - document.getElementById('grouping-modal-overlay').style.display = 'block'; - document.getElementById('grouping-modal').style.display = 'block'; + document.getElementById("grouping-modal-overlay").style.display = "block"; + document.getElementById("grouping-modal").style.display = "block"; } // Close the table grouping dialog function closeTableGroupingDialog() { - document.getElementById('grouping-modal-overlay').style.display = 'none'; - document.getElementById('grouping-modal').style.display = 'none'; + document.getElementById("grouping-modal-overlay").style.display = "none"; + document.getElementById("grouping-modal").style.display = "none"; } // Automatically remove unused rules without confirmation @@ -22,22 +22,24 @@ function autoRemoveUnusedRules() { const entityNames = Object.keys(entities); const rulesToKeep = []; let removedCount = 0; - + tableGroupingRules.forEach((rule, index) => { // Always keep the default catch-all rule (usually last) - if (rule.pattern === '.*') { + if (rule.pattern === ".*") { rulesToKeep.push(rule); return; } - + try { - const regex = new RegExp(rule.pattern, 'i'); - const matches = entityNames.filter(name => regex.test(name)); - + const regex = new RegExp(rule.pattern, "i"); + const matches = entityNames.filter((name) => regex.test(name)); + if (matches.length === 0) { // Rule doesn't match anything - skip it (don't add to rulesToKeep) removedCount++; - console.log(`??? Auto-removed unused rule: "${rule.name}" (pattern: ${rule.pattern})`); + console.log( + `??? Auto-removed unused rule: "${rule.name}" (pattern: ${rule.pattern})`, + ); } else { // Rule matches at least one table - keep it rulesToKeep.push(rule); @@ -45,16 +47,22 @@ function autoRemoveUnusedRules() { } catch (e) { // Invalid regex - remove it removedCount++; - console.warn(`??? Auto-removed invalid rule "${rule.name}": ${e.message}`); + console.warn( + `??? Auto-removed invalid rule "${rule.name}": ${e.message}`, + ); } }); - + if (removedCount > 0) { tableGroupingRules = rulesToKeep; saveTableGroupingRulesToStorage(); - console.log(`? Auto-removed ${removedCount} unused rule(s). ${tableGroupingRules.length} rules remain.`); + console.log( + `? Auto-removed ${removedCount} unused rule(s). ${tableGroupingRules.length} rules remain.`, + ); } else { - console.log(`? All ${tableGroupingRules.length} rules are in use - no cleanup needed`); + console.log( + `? All ${tableGroupingRules.length} rules are in use - no cleanup needed`, + ); } } @@ -62,72 +70,80 @@ function autoRemoveUnusedRules() { function analyzeUnusedRules() { const entityNames = Object.keys(entities); const unusedRules = []; - + tableGroupingRules.forEach((rule, index) => { // Skip the default catch-all rule (usually last) - if (rule.pattern === '.*') { + if (rule.pattern === ".*") { return; } - + try { - const regex = new RegExp(rule.pattern, 'i'); - const matches = entityNames.filter(name => regex.test(name)); - + const regex = new RegExp(rule.pattern, "i"); + const matches = entityNames.filter((name) => regex.test(name)); + if (matches.length === 0) { unusedRules.push({ index: index, rule: rule, name: rule.name, - pattern: rule.pattern + pattern: rule.pattern, }); } } catch (e) { - console.warn(`Invalid regex pattern in rule "${rule.name}": ${rule.pattern}`, e); + console.warn( + `Invalid regex pattern in rule "${rule.name}": ${rule.pattern}`, + e, + ); } }); - + // Only show notification if there are STILL unused rules after auto-cleanup // (This should rarely happen, but good to have for edge cases) if (unusedRules.length > 0) { - console.log(`?? Found ${unusedRules.length} unused rules after auto-cleanup (edge case):`); - unusedRules.forEach(item => { + console.log( + `?? Found ${unusedRules.length} unused rules after auto-cleanup (edge case):`, + ); + unusedRules.forEach((item) => { console.log(` - "${item.name}" (pattern: ${item.pattern})`); }); showUnusedRulesNotification(unusedRules); } else { - console.log('? All rules match at least one table'); + console.log("? All rules match at least one table"); closeUnusedRulesNotification(); } - + return unusedRules; } // Show notification about unused rules (only for edge cases now) function showUnusedRulesNotification(unusedRules) { // Check if notification element already exists - let notification = document.getElementById('unused-rules-notification'); + let notification = document.getElementById("unused-rules-notification"); if (!notification) { - notification = document.createElement('div'); - notification.id = 'unused-rules-notification'; - notification.className = 'unused-rules-notification'; - + notification = document.createElement("div"); + notification.id = "unused-rules-notification"; + notification.className = "unused-rules-notification"; + // Insert after modal description - const modalBody = document.querySelector('#grouping-modal .modal-body'); - const description = modalBody.querySelector('.modal-description'); - description.insertAdjacentElement('afterend', notification); + const modalBody = document.querySelector("#grouping-modal .modal-body"); + const description = modalBody.querySelector(".modal-description"); + description.insertAdjacentElement("afterend", notification); } - + // Build notification content - const rulesList = unusedRules.map(item => - `
  • + const rulesList = unusedRules + .map( + (item) => + `
  • ${item.name} ${item.pattern} -
  • ` - ).join(''); - + `, + ) + .join(""); + notification.innerHTML = `
    @@ -146,64 +162,66 @@ function showUnusedRulesNotification(unusedRules) {
    `; - - notification.style.display = 'block'; + + notification.style.display = "block"; } // Close the unused rules notification function closeUnusedRulesNotification() { - const notification = document.getElementById('unused-rules-notification'); + const notification = document.getElementById("unused-rules-notification"); if (notification) { - notification.style.display = 'none'; + notification.style.display = "none"; } } // Remove all unused rules at once (still available for manual use) function removeAllUnusedRules() { const unusedRules = analyzeUnusedRules(); - + if (unusedRules.length === 0) { - alert('No unused rules to remove!'); + alert("No unused rules to remove!"); return; } - + // Sort indices in descending order to avoid index shifting issues - const indicesToRemove = unusedRules.map(item => item.index).sort((a, b) => b - a); - - indicesToRemove.forEach(index => { + const indicesToRemove = unusedRules + .map((item) => item.index) + .sort((a, b) => b - a); + + indicesToRemove.forEach((index) => { tableGroupingRules.splice(index, 1); }); - + console.log(`? Manually removed ${indicesToRemove.length} unused rules`); - + // Refresh the UI populateRulesUI(); closeUnusedRulesNotification(); - + alert(`Successfully removed ${indicesToRemove.length} unused rule(s)!`); } // Populate the rules UI function populateRulesUI() { - const container = document.getElementById('rules-container'); - container.innerHTML = ''; - + const container = document.getElementById("rules-container"); + container.innerHTML = ""; + tableGroupingRules.forEach((rule, index) => { const ruleElement = createRuleElement(rule, index); container.appendChild(ruleElement); }); - + // Initialize sortable for drag-and-drop reordering initializeSortable(); } // Create a single rule element function createRuleElement(rule, index) { - const div = document.createElement('div'); - div.className = 'rule-item'; + const div = document.createElement("div"); + div.className = "rule-item"; div.dataset.index = index; - div.setAttribute('draggable', 'true'); // Make the element draggable - + div.setAttribute("draggable", "true"); // Make the element draggable + div.innerHTML = `
    @@ -212,7 +230,7 @@ function createRuleElement(rule, index) {
    `; - + return div; } // Initialize drag-and-drop sorting using HTML5 Drag and Drop API function initializeSortable() { - const container = document.getElementById('rules-container'); + const container = document.getElementById("rules-container"); let draggedElement = null; - - container.querySelectorAll('.rule-item').forEach(item => { + + container.querySelectorAll(".rule-item").forEach((item) => { // Drag start - store the dragged element - item.addEventListener('dragstart', (e) => { + item.addEventListener("dragstart", (e) => { draggedElement = item; - item.classList.add('dragging'); - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/html', item.innerHTML); + item.classList.add("dragging"); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/html", item.innerHTML); }); - + // Drag end - clean up - item.addEventListener('dragend', (e) => { - item.classList.remove('dragging'); + item.addEventListener("dragend", (e) => { + item.classList.remove("dragging"); }); - + // Drag over - allow dropping - item.addEventListener('dragover', (e) => { + item.addEventListener("dragover", (e) => { e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - + e.dataTransfer.dropEffect = "move"; + if (draggedElement && draggedElement !== item) { const bounding = item.getBoundingClientRect(); const offset = e.clientY - bounding.top; - + // Insert before or after based on mouse position if (offset > bounding.height / 2) { - item.parentNode.insertBefore(draggedElement, item.nextSibling); + item.parentNode.insertBefore( + draggedElement, + item.nextSibling, + ); } else { item.parentNode.insertBefore(draggedElement, item); } } }); - + // Drop - finalize the drop - item.addEventListener('drop', (e) => { + item.addEventListener("drop", (e) => { e.preventDefault(); e.stopPropagation(); }); }); - + // Also handle drag over the container itself - container.addEventListener('dragover', (e) => { + container.addEventListener("dragover", (e) => { e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; + e.dataTransfer.dropEffect = "move"; }); } // Rule CRUD operations function addNewRule() { const newRule = { - name: 'New Rule', - pattern: '.*', - icon: 'fa-database', - color: '#3b82f6', - enabled: true + name: "New Rule", + pattern: ".*", + icon: "fa-database", + color: "#3b82f6", + enabled: true, }; - + tableGroupingRules.push(newRule); populateRulesUI(); analyzeUnusedRules(); // Re-analyze after adding @@ -342,18 +363,21 @@ function updateRuleColor(index, value) { function testPattern(index) { const rule = tableGroupingRules[index]; const matchesContainer = document.getElementById(`rule-matches-${index}`); - + try { - const regex = new RegExp(rule.pattern, 'i'); - const matches = Object.keys(entities).filter(name => regex.test(name)); - + const regex = new RegExp(rule.pattern, "i"); + const matches = Object.keys(entities).filter((name) => + regex.test(name), + ); + if (matches.length === 0) { - matchesContainer.innerHTML = '
    ?? No tables match this pattern
    '; + matchesContainer.innerHTML = + '
    ?? No tables match this pattern
    '; } else { matchesContainer.innerHTML = `
    ? Matches ${matches.length} table(s): - ${matches.slice(0, 10).join(', ')}${matches.length > 10 ? ` ... and ${matches.length - 10} more` : ''} + ${matches.slice(0, 10).join(", ")}${matches.length > 10 ? ` ... and ${matches.length - 10} more` : ""}
    `; } @@ -363,13 +387,17 @@ function testPattern(index) { } function resetToDefaultRules() { - if (confirm('Reset all rules to default settings? This will overwrite your current rules.')) { + if ( + confirm( + "Reset all rules to default settings? This will overwrite your current rules.", + ) + ) { tableGroupingRules = [...DEFAULT_TABLE_GROUPING_RULES]; saveTableGroupingRulesToStorage(); - + // Auto-remove unused defaults immediately autoRemoveUnusedRules(); - + populateRulesUI(); analyzeUnusedRules(); // Re-analyze after reset } @@ -377,46 +405,46 @@ function resetToDefaultRules() { function saveTableGroupingRules() { // Reorder rules based on current DOM order - const container = document.getElementById('rules-container'); + const container = document.getElementById("rules-container"); const orderedRules = []; - - container.querySelectorAll('.rule-item').forEach(item => { - const index = parseInt(item.dataset.index); + + container.querySelectorAll(".rule-item").forEach((item) => { + const index = parseInt(item.dataset.index, 10); orderedRules.push(tableGroupingRules[index]); }); - + tableGroupingRules = orderedRules; saveTableGroupingRulesToStorage(); - + // Regenerate schema with new rules generateSchema(); - + closeTableGroupingDialog(); - - alert('Table grouping rules saved successfully!'); + + alert("Table grouping rules saved successfully!"); } // Icon Picker Functions function openIconPicker(ruleIndex) { currentEditingRuleIndex = ruleIndex; populateIconPicker(); - document.getElementById('icon-picker-overlay').style.display = 'block'; - document.getElementById('icon-picker-modal').style.display = 'block'; + document.getElementById("icon-picker-overlay").style.display = "block"; + document.getElementById("icon-picker-modal").style.display = "block"; } function closeIconPicker() { currentEditingRuleIndex = null; - document.getElementById('icon-picker-overlay').style.display = 'none'; - document.getElementById('icon-picker-modal').style.display = 'none'; + document.getElementById("icon-picker-overlay").style.display = "none"; + document.getElementById("icon-picker-modal").style.display = "none"; } function populateIconPicker() { - const grid = document.getElementById('icon-grid'); - grid.innerHTML = ''; - - AVAILABLE_ICONS.forEach(icon => { - const iconElement = document.createElement('div'); - iconElement.className = 'icon-item'; + const grid = document.getElementById("icon-grid"); + grid.innerHTML = ""; + + AVAILABLE_ICONS.forEach((icon) => { + const iconElement = document.createElement("div"); + iconElement.className = "icon-item"; iconElement.innerHTML = ``; iconElement.onclick = () => selectIcon(icon); grid.appendChild(iconElement); @@ -424,16 +452,16 @@ function populateIconPicker() { } function filterIcons(searchTerm) { - const grid = document.getElementById('icon-grid'); - const items = grid.querySelectorAll('.icon-item'); + const grid = document.getElementById("icon-grid"); + const items = grid.querySelectorAll(".icon-item"); const term = searchTerm.toLowerCase(); - - items.forEach(item => { - const iconClass = item.querySelector('i').className; + + items.forEach((item) => { + const iconClass = item.querySelector("i").className; if (iconClass.toLowerCase().includes(term)) { - item.style.display = 'block'; + item.style.display = "block"; } else { - item.style.display = 'none'; + item.style.display = "none"; } }); } diff --git a/Templates/table-interaction.js b/Templates/table-interaction.js index feee957..5ec856c 100644 --- a/Templates/table-interaction.js +++ b/Templates/table-interaction.js @@ -2,40 +2,48 @@ function setupTableInteraction(tableGroup, entityName) { let dragData = null; - tableGroup.addEventListener('mousedown', function(e) { + tableGroup.addEventListener("mousedown", function (e) { e.stopPropagation(); selectTable(entityName); - + // Bring table to front when starting to drag bringTableToFront(tableGroup); - const rect = tableGroup.querySelector('.table-box'); + const rect = tableGroup.querySelector(".table-box"); - const svg = document.getElementById('schema-svg'); + const svg = document.getElementById("schema-svg"); const svgRect = svg.getBoundingClientRect(); - const svgX = (e.clientX - svgRect.left) * svgViewBox.width / svgRect.width + svgViewBox.x; - const svgY = (e.clientY - svgRect.top) * svgViewBox.height / svgRect.height + svgViewBox.y; + const svgX = + ((e.clientX - svgRect.left) * svgViewBox.width) / svgRect.width + + svgViewBox.x; + const svgY = + ((e.clientY - svgRect.top) * svgViewBox.height) / svgRect.height + + svgViewBox.y; dragData = { startX: svgX, startY: svgY, - elementX: parseFloat(rect.getAttribute('x')), - elementY: parseFloat(rect.getAttribute('y')) + elementX: parseFloat(rect.getAttribute("x")), + elementY: parseFloat(rect.getAttribute("y")), }; - tableGroup.classList.add('dragging'); + tableGroup.classList.add("dragging"); - document.addEventListener('mousemove', handleTableDrag); - document.addEventListener('mouseup', handleTableDragEnd); + document.addEventListener("mousemove", handleTableDrag); + document.addEventListener("mouseup", handleTableDragEnd); }); function handleTableDrag(e) { if (!dragData) return; - const svg = document.getElementById('schema-svg'); + const svg = document.getElementById("schema-svg"); const svgRect = svg.getBoundingClientRect(); - const svgX = (e.clientX - svgRect.left) * svgViewBox.width / svgRect.width + svgViewBox.x; - const svgY = (e.clientY - svgRect.top) * svgViewBox.height / svgRect.height + svgViewBox.y; + const svgX = + ((e.clientX - svgRect.left) * svgViewBox.width) / svgRect.width + + svgViewBox.x; + const svgY = + ((e.clientY - svgRect.top) * svgViewBox.height) / svgRect.height + + svgViewBox.y; const deltaX = svgX - dragData.startX; const deltaY = svgY - dragData.startY; @@ -44,17 +52,20 @@ function setupTableInteraction(tableGroup, entityName) { let newY = dragData.elementY + deltaY; // Check boundaries and show indicator - const rect = tableGroup.querySelector('.table-box'); - const tableWidth = parseFloat(rect.getAttribute('width')); - const tableHeight = parseFloat(rect.getAttribute('height')); - - const boundaryRect = document.getElementById('boundary-rect'); - if (newX < BOUNDARY_MARGIN || newY < BOUNDARY_MARGIN || + const rect = tableGroup.querySelector(".table-box"); + const tableWidth = parseFloat(rect.getAttribute("width")); + const tableHeight = parseFloat(rect.getAttribute("height")); + + const boundaryRect = document.getElementById("boundary-rect"); + if ( + newX < BOUNDARY_MARGIN || + newY < BOUNDARY_MARGIN || newX + tableWidth > CANVAS_WIDTH - BOUNDARY_MARGIN || - newY + tableHeight > CANVAS_HEIGHT - BOUNDARY_MARGIN) { - boundaryRect.style.display = 'block'; + newY + tableHeight > CANVAS_HEIGHT - BOUNDARY_MARGIN + ) { + boundaryRect.style.display = "block"; } else { - boundaryRect.style.display = 'none'; + boundaryRect.style.display = "none"; } if (snapToGrid) { @@ -68,138 +79,150 @@ function setupTableInteraction(tableGroup, entityName) { function handleTableDragEnd() { if (dragData) { - tableGroup.classList.remove('dragging'); - document.getElementById('boundary-rect').style.display = 'none'; + tableGroup.classList.remove("dragging"); + document.getElementById("boundary-rect").style.display = "none"; // Save positions after drag saveSettings(); } dragData = null; - document.removeEventListener('mousemove', handleTableDrag); - document.removeEventListener('mouseup', handleTableDragEnd); + document.removeEventListener("mousemove", handleTableDrag); + document.removeEventListener("mouseup", handleTableDragEnd); } } function bringTableToFront(tableGroup) { - const svg = document.getElementById('schema-svg'); - + const svg = document.getElementById("schema-svg"); + // Remove the table group from its current position tableGroup.remove(); - + // Re-append it to the end (this puts it on top in z-order) svg.appendChild(tableGroup); } function moveTable(tableGroup, x, y) { // FIXED: Include polygon and g elements (for type icons like diamonds and navigation arrows) - const elements = tableGroup.querySelectorAll('rect, text, line, circle, polygon, g'); - const oldRect = tableGroup.querySelector('.table-box'); - const oldX = parseFloat(oldRect.getAttribute('x')); - const oldY = parseFloat(oldRect.getAttribute('y')); + const elements = tableGroup.querySelectorAll( + "rect, text, line, circle, polygon, g", + ); + const oldRect = tableGroup.querySelector(".table-box"); + const oldX = parseFloat(oldRect.getAttribute("x")); + const oldY = parseFloat(oldRect.getAttribute("y")); const deltaX = x - oldX; const deltaY = y - oldY; - elements.forEach(element => { - if (element.tagName === 'line') { + elements.forEach((element) => { + if (element.tagName === "line") { // Handle line elements - const x1 = parseFloat(element.getAttribute('x1')); - const y1 = parseFloat(element.getAttribute('y1')); - const x2 = parseFloat(element.getAttribute('x2')); - const y2 = parseFloat(element.getAttribute('y2')); - element.setAttribute('x1', x1 + deltaX); - element.setAttribute('y1', y1 + deltaY); - element.setAttribute('x2', x2 + deltaX); - element.setAttribute('y2', y2 + deltaY); - } else if (element.tagName === 'circle') { + const x1 = parseFloat(element.getAttribute("x1")); + const y1 = parseFloat(element.getAttribute("y1")); + const x2 = parseFloat(element.getAttribute("x2")); + const y2 = parseFloat(element.getAttribute("y2")); + element.setAttribute("x1", x1 + deltaX); + element.setAttribute("y1", y1 + deltaY); + element.setAttribute("x2", x2 + deltaX); + element.setAttribute("y2", y2 + deltaY); + } else if (element.tagName === "circle") { // Handle circle elements (icon backgrounds) - const cx = parseFloat(element.getAttribute('cx')); - const cy = parseFloat(element.getAttribute('cy')); - element.setAttribute('cx', cx + deltaX); - element.setAttribute('cy', cy + deltaY); - } else if (element.tagName === 'polygon') { + const cx = parseFloat(element.getAttribute("cx")); + const cy = parseFloat(element.getAttribute("cy")); + element.setAttribute("cx", cx + deltaX); + element.setAttribute("cy", cy + deltaY); + } else if (element.tagName === "polygon") { // FIXED: Handle polygon elements (enum diamonds, navigation arrows) - const pointsAttr = element.getAttribute('points'); + const pointsAttr = element.getAttribute("points"); if (pointsAttr) { // Parse points string: "x1,y1 x2,y2 x3,y3 ..." - const points = pointsAttr.trim().split(/\s+/).map(pair => { - const [x, y] = pair.split(',').map(parseFloat); - return `${x + deltaX},${y + deltaY}`; - }); - element.setAttribute('points', points.join(' ')); + const points = pointsAttr + .trim() + .split(/\s+/) + .map((pair) => { + const [x, y] = pair.split(",").map(parseFloat); + return `${x + deltaX},${y + deltaY}`; + }); + element.setAttribute("points", points.join(" ")); } - } else if (element.tagName === 'g') { + } else if (element.tagName === "g") { // FIXED: Handle g (group) elements by transforming them - const currentTransform = element.getAttribute('transform') || ''; - const translateMatch = currentTransform.match(/translate\(([^,]+),([^)]+)\)/); - + const currentTransform = element.getAttribute("transform") || ""; + const translateMatch = currentTransform.match( + /translate\(([^,]+),([^)]+)\)/, + ); + if (translateMatch) { // Update existing translate const currentX = parseFloat(translateMatch[1]); const currentY = parseFloat(translateMatch[2]); const newTransform = currentTransform.replace( /translate\([^)]+\)/, - `translate(${currentX + deltaX},${currentY + deltaY})` + `translate(${currentX + deltaX},${currentY + deltaY})`, ); - element.setAttribute('transform', newTransform); + element.setAttribute("transform", newTransform); } else { // Add new translate - const newTransform = currentTransform + ` translate(${deltaX},${deltaY})`; - element.setAttribute('transform', newTransform.trim()); + const newTransform = + currentTransform + ` translate(${deltaX},${deltaY})`; + element.setAttribute("transform", newTransform.trim()); } - } else if (element.tagName === 'rect' || element.tagName === 'text') { + } else if (element.tagName === "rect" || element.tagName === "text") { // Handle rect and text elements - const currentX = parseFloat(element.getAttribute('x')); - const currentY = parseFloat(element.getAttribute('y')); - element.setAttribute('x', currentX + deltaX); - element.setAttribute('y', currentY + deltaY); + const currentX = parseFloat(element.getAttribute("x")); + const currentY = parseFloat(element.getAttribute("y")); + element.setAttribute("x", currentX + deltaX); + element.setAttribute("y", currentY + deltaY); } }); } function selectTable(entityName) { // Hide all selection overlays - document.querySelectorAll('.selection-overlay').forEach(overlay => { - overlay.style.display = 'none'; + document.querySelectorAll(".selection-overlay").forEach((overlay) => { + overlay.style.display = "none"; }); // Clear selected class from all table boxes - document.querySelectorAll('.table-box.selected').forEach(box => { - box.classList.remove('selected'); + document.querySelectorAll(".table-box.selected").forEach((box) => { + box.classList.remove("selected"); }); - const tableGroup = document.querySelector('[data-entity="' + entityName + '"]'); + const tableGroup = document.querySelector( + '[data-entity="' + entityName + '"]', + ); if (tableGroup) { - const tableBox = tableGroup.querySelector('.table-box'); - tableBox.classList.add('selected'); + const tableBox = tableGroup.querySelector(".table-box"); + tableBox.classList.add("selected"); selectedTable = entityName; // Show the selection overlay for this table - const selectionOverlay = tableGroup.querySelector('.selection-overlay'); + const selectionOverlay = tableGroup.querySelector(".selection-overlay"); if (selectionOverlay) { - selectionOverlay.style.display = 'block'; + selectionOverlay.style.display = "block"; } // Enable "show only selected relations" mode when a table is selected showOnlySelectedRelations = true; updateRelationships(); - - console.log(`?? Selected table: ${entityName} - highlighting related connections`); + + console.log( + `?? Selected table: ${entityName} - highlighting related connections`, + ); } } function clearSelection() { // Hide all selection overlays - document.querySelectorAll('.selection-overlay').forEach(overlay => { - overlay.style.display = 'none'; + document.querySelectorAll(".selection-overlay").forEach((overlay) => { + overlay.style.display = "none"; }); // Clear selected class from all table boxes - document.querySelectorAll('.table-box.selected').forEach(box => { - box.classList.remove('selected'); + document.querySelectorAll(".table-box.selected").forEach((box) => { + box.classList.remove("selected"); }); selectedTable = null; showOnlySelectedRelations = false; updateRelationships(); - - console.log('?? Selection cleared - showing all relationships normally'); -} \ No newline at end of file + + console.log("?? Selection cleared - showing all relationships normally"); +} diff --git a/Templates/variables.js b/Templates/variables.js index 6b0601e..8e4267f 100644 --- a/Templates/variables.js +++ b/Templates/variables.js @@ -2,7 +2,7 @@ const entities = ENTITIES_JSON_PLACEHOLDER; // Document identifier for localStorage - injected at generation time -const DOCUMENT_GUID = 'DOCUMENT_GUID_PLACEHOLDER'; +const DOCUMENT_GUID = "DOCUMENT_GUID_PLACEHOLDER"; let selectedTable = null; let showOnlySelectedRelations = false; @@ -24,16 +24,18 @@ const MAX_ZOOM = 15; const STORAGE_KEYS = { tablePositions: `schemaMagic_${DOCUMENT_GUID}_tablePositions`, viewSettings: `schemaMagic_${DOCUMENT_GUID}_viewSettings`, - viewBox: `schemaMagic_${DOCUMENT_GUID}_viewBox` + viewBox: `schemaMagic_${DOCUMENT_GUID}_viewBox`, }; // Helper function to check if this document has saved state function hasSavedDocumentState() { try { - const savedPositions = localStorage.getItem(STORAGE_KEYS.tablePositions); - return savedPositions !== null && savedPositions !== ''; + const savedPositions = localStorage.getItem( + STORAGE_KEYS.tablePositions, + ); + return savedPositions !== null && savedPositions !== ""; } catch (e) { - console.warn('Failed to check saved document state:', e); + console.warn("Failed to check saved document state:", e); return false; } } @@ -46,98 +48,418 @@ function getDocumentStorageKey(keyType) { // Table Grouping Rules - Comprehensive Default Set const DEFAULT_TABLE_GROUPING_RULES = [ // Core Business Entities - { name: "Organizations", pattern: "Company|Organization|Business|Enterprise|Corporation|Firm", icon: "fa-building", color: "#3b82f6", enabled: true }, - { name: "Departments", pattern: "Department|Division|Unit|Team|Group", icon: "fa-sitemap", color: "#06b6d4", enabled: true }, - { name: "Locations", pattern: "Location|Address|Site|Office|Branch|Facility", icon: "fa-location-dot", color: "#8b5cf6", enabled: true }, - + { + name: "Organizations", + pattern: "Company|Organization|Business|Enterprise|Corporation|Firm", + icon: "fa-building", + color: "#3b82f6", + enabled: true, + }, + { + name: "Departments", + pattern: "Department|Division|Unit|Team|Group", + icon: "fa-sitemap", + color: "#06b6d4", + enabled: true, + }, + { + name: "Locations", + pattern: "Location|Address|Site|Office|Branch|Facility", + icon: "fa-location-dot", + color: "#8b5cf6", + enabled: true, + }, + // People & Users - { name: "Users", pattern: "User|Account|Profile|Identity", icon: "fa-user", color: "#ec4899", enabled: true }, - { name: "Employees", pattern: "Employee|Staff|Personnel|Worker", icon: "fa-id-badge", color: "#f59e0b", enabled: true }, - { name: "Customers", pattern: "Customer|Client|Contact|Lead|Prospect", icon: "fa-user-tie", color: "#10b981", enabled: true }, - { name: "Members", pattern: "Member|Subscriber|Participant", icon: "fa-users", color: "#6366f1", enabled: true }, - + { + name: "Users", + pattern: "User|Account|Profile|Identity", + icon: "fa-user", + color: "#ec4899", + enabled: true, + }, + { + name: "Employees", + pattern: "Employee|Staff|Personnel|Worker", + icon: "fa-id-badge", + color: "#f59e0b", + enabled: true, + }, + { + name: "Customers", + pattern: "Customer|Client|Contact|Lead|Prospect", + icon: "fa-user-tie", + color: "#10b981", + enabled: true, + }, + { + name: "Members", + pattern: "Member|Subscriber|Participant", + icon: "fa-users", + color: "#6366f1", + enabled: true, + }, + // Relationships & Associations - { name: "Junctions", pattern: ".*To.*|.*Member.*|.*Assignment.*|.*Mapping.*", icon: "fa-link", color: "#64748b", enabled: true }, - { name: "Roles & Permissions", pattern: "Role|Permission|Access|Authorization|Privilege", icon: "fa-shield-halved", color: "#dc2626", enabled: true }, - + { + name: "Junctions", + pattern: ".*To.*|.*Member.*|.*Assignment.*|.*Mapping.*", + icon: "fa-link", + color: "#64748b", + enabled: true, + }, + { + name: "Roles & Permissions", + pattern: "Role|Permission|Access|Authorization|Privilege", + icon: "fa-shield-halved", + color: "#dc2626", + enabled: true, + }, + // Financial - { name: "Financial", pattern: "Invoice|Payment|Transaction|Order|Purchase|Sale|Price|Cost|Revenue|Billing", icon: "fa-money-bill-wave", color: "#16a34a", enabled: true }, - { name: "Products", pattern: "Product|Item|SKU|Inventory|Stock|Catalog", icon: "fa-box", color: "#ea580c", enabled: true }, - + { + name: "Financial", + pattern: + "Invoice|Payment|Transaction|Order|Purchase|Sale|Price|Cost|Revenue|Billing", + icon: "fa-money-bill-wave", + color: "#16a34a", + enabled: true, + }, + { + name: "Products", + pattern: "Product|Item|SKU|Inventory|Stock|Catalog", + icon: "fa-box", + color: "#ea580c", + enabled: true, + }, + // Content & Media - { name: "Documents", pattern: "Document|File|Attachment|Upload|Asset|Media", icon: "fa-file-lines", color: "#0ea5e9", enabled: true }, - { name: "Posts & Articles", pattern: "Post|Article|Blog|Content|Page|News", icon: "fa-newspaper", color: "#8b5cf6", enabled: true }, - { name: "Comments", pattern: "Comment|Reply|Feedback|Review|Rating", icon: "fa-comment", color: "#a855f7", enabled: true }, - { name: "Tags", pattern: "Tag|Category|Label|Classification", icon: "fa-tags", color: "#14b8a6", enabled: true }, - + { + name: "Documents", + pattern: "Document|File|Attachment|Upload|Asset|Media", + icon: "fa-file-lines", + color: "#0ea5e9", + enabled: true, + }, + { + name: "Posts & Articles", + pattern: "Post|Article|Blog|Content|Page|News", + icon: "fa-newspaper", + color: "#8b5cf6", + enabled: true, + }, + { + name: "Comments", + pattern: "Comment|Reply|Feedback|Review|Rating", + icon: "fa-comment", + color: "#a855f7", + enabled: true, + }, + { + name: "Tags", + pattern: "Tag|Category|Label|Classification", + icon: "fa-tags", + color: "#14b8a6", + enabled: true, + }, + // Communication - { name: "Messages", pattern: "Message|Email|Notification|Alert|SMS|Chat", icon: "fa-envelope", color: "#6366f1", enabled: true }, - { name: "Conversations", pattern: "Conversation|Thread|Discussion|Ticket", icon: "fa-comments", color: "#8b5cf6", enabled: true }, - + { + name: "Messages", + pattern: "Message|Email|Notification|Alert|SMS|Chat", + icon: "fa-envelope", + color: "#6366f1", + enabled: true, + }, + { + name: "Conversations", + pattern: "Conversation|Thread|Discussion|Ticket", + icon: "fa-comments", + color: "#8b5cf6", + enabled: true, + }, + // Projects & Tasks - { name: "Projects", pattern: "Project|Initiative|Program|Campaign", icon: "fa-diagram-project", color: "#0891b2", enabled: true }, - { name: "Tasks", pattern: "Task|Todo|Activity|Action|Assignment", icon: "fa-list-check", color: "#f97316", enabled: true }, - { name: "Milestones", pattern: "Milestone|Phase|Sprint|Iteration|Release", icon: "fa-flag-checkered", color: "#7c3aed", enabled: true }, - + { + name: "Projects", + pattern: "Project|Initiative|Program|Campaign", + icon: "fa-diagram-project", + color: "#0891b2", + enabled: true, + }, + { + name: "Tasks", + pattern: "Task|Todo|Activity|Action|Assignment", + icon: "fa-list-check", + color: "#f97316", + enabled: true, + }, + { + name: "Milestones", + pattern: "Milestone|Phase|Sprint|Iteration|Release", + icon: "fa-flag-checkered", + color: "#7c3aed", + enabled: true, + }, + // Scheduling & Time - { name: "Events", pattern: "Event|Meeting|Appointment|Booking|Reservation", icon: "fa-calendar-days", color: "#db2777", enabled: true }, - { name: "Schedule", pattern: "Schedule|Shift|Roster|Timetable|Availability", icon: "fa-calendar-check", color: "#059669", enabled: true }, - + { + name: "Events", + pattern: "Event|Meeting|Appointment|Booking|Reservation", + icon: "fa-calendar-days", + color: "#db2777", + enabled: true, + }, + { + name: "Schedule", + pattern: "Schedule|Shift|Roster|Timetable|Availability", + icon: "fa-calendar-check", + color: "#059669", + enabled: true, + }, + // Technical & System - { name: "Logs & Audits", pattern: "Log|Audit|History|Track|Change|Event", icon: "fa-clipboard-list", color: "#475569", enabled: true }, - { name: "Configuration", pattern: "Config|Setting|Preference|Option|Parameter", icon: "fa-gear", color: "#71717a", enabled: true }, - { name: "Integration", pattern: "Integration|API|Webhook|Sync|Import|Export", icon: "fa-plug", color: "#6b7280", enabled: true }, - + { + name: "Logs & Audits", + pattern: "Log|Audit|History|Track|Change|Event", + icon: "fa-clipboard-list", + color: "#475569", + enabled: true, + }, + { + name: "Configuration", + pattern: "Config|Setting|Preference|Option|Parameter", + icon: "fa-gear", + color: "#71717a", + enabled: true, + }, + { + name: "Integration", + pattern: "Integration|API|Webhook|Sync|Import|Export", + icon: "fa-plug", + color: "#6b7280", + enabled: true, + }, + // Monitoring & Analytics - { name: "Metrics", pattern: "Metric|Statistic|Analytics|Report|Dashboard|KPI", icon: "fa-chart-line", color: "#2563eb", enabled: true }, - { name: "Alerts", pattern: "Alert|Warning|Error|Exception|Issue", icon: "fa-triangle-exclamation", color: "#ef4444", enabled: true }, - + { + name: "Metrics", + pattern: "Metric|Statistic|Analytics|Report|Dashboard|KPI", + icon: "fa-chart-line", + color: "#2563eb", + enabled: true, + }, + { + name: "Alerts", + pattern: "Alert|Warning|Error|Exception|Issue", + icon: "fa-triangle-exclamation", + color: "#ef4444", + enabled: true, + }, + // Devices & Network - { name: "Devices", pattern: "Device|Equipment|Asset|Hardware|Machine|Sensor", icon: "fa-microchip", color: "#0284c7", enabled: true }, - { name: "Network", pattern: "Network|Connection|Link|Node|Interface", icon: "fa-network-wired", color: "#0d9488", enabled: true }, - + { + name: "Devices", + pattern: "Device|Equipment|Asset|Hardware|Machine|Sensor", + icon: "fa-microchip", + color: "#0284c7", + enabled: true, + }, + { + name: "Network", + pattern: "Network|Connection|Link|Node|Interface", + icon: "fa-network-wired", + color: "#0d9488", + enabled: true, + }, + // Workflow & Status - { name: "Workflow", pattern: "Workflow|Process|Pipeline|Stage|Status", icon: "fa-route", color: "#7c2d12", enabled: true }, - { name: "Queue", pattern: "Queue|Job|Batch|Background|Worker", icon: "fa-list-ol", color: "#92400e", enabled: true }, - + { + name: "Workflow", + pattern: "Workflow|Process|Pipeline|Stage|Status", + icon: "fa-route", + color: "#7c2d12", + enabled: true, + }, + { + name: "Queue", + pattern: "Queue|Job|Batch|Background|Worker", + icon: "fa-list-ol", + color: "#92400e", + enabled: true, + }, + // Security - { name: "Security", pattern: "Security|Token|Key|Certificate|Credential|Secret", icon: "fa-lock", color: "#991b1b", enabled: true }, - { name: "Sessions", pattern: "Session|Login|Authentication|OAuth|SSO", icon: "fa-right-to-bracket", color: "#be123c", enabled: true }, - + { + name: "Security", + pattern: "Security|Token|Key|Certificate|Credential|Secret", + icon: "fa-lock", + color: "#991b1b", + enabled: true, + }, + { + name: "Sessions", + pattern: "Session|Login|Authentication|OAuth|SSO", + icon: "fa-right-to-bracket", + color: "#be123c", + enabled: true, + }, + // Default fallback (matches everything) - should be last - { name: "Default", pattern: ".*", icon: "fa-database", color: "#0ea5e9", enabled: true } + { + name: "Default", + pattern: ".*", + icon: "fa-database", + color: "#0ea5e9", + enabled: true, + }, ]; // Available FontAwesome 6 Free Icons (Solid) const AVAILABLE_ICONS = [ - "fa-database", "fa-table", "fa-building", "fa-sitemap", "fa-location-dot", - "fa-user", "fa-users", "fa-id-badge", "fa-user-tie", "fa-link", "fa-shield-halved", - "fa-money-bill-wave", "fa-box", "fa-file-lines", "fa-newspaper", "fa-comment", - "fa-tags", "fa-envelope", "fa-comments", "fa-diagram-project", "fa-list-check", - "fa-flag-checkered", "fa-calendar-days", "fa-calendar-check", "fa-clipboard-list", - "fa-gear", "fa-plug", "fa-chart-line", "fa-triangle-exclamation", "fa-microchip", - "fa-network-wired", "fa-route", "fa-list-ol", "fa-lock", "fa-right-to-bracket", - "fa-circle", "fa-square", "fa-heart", "fa-star", "fa-bookmark", "fa-bell", - "fa-flag", "fa-cloud", "fa-truck", "fa-shopping-cart", "fa-credit-card", - "fa-certificate", "fa-trophy", "fa-gift", "fa-phone", "fa-mobile", - "fa-laptop", "fa-desktop", "fa-globe", "fa-map", "fa-compass", - "fa-wrench", "fa-cog", "fa-code", "fa-bug", "fa-terminal", - "fa-folder", "fa-folder-open", "fa-file", "fa-file-pdf", "fa-file-word", - "fa-image", "fa-video", "fa-music", "fa-camera", "fa-print", - "fa-search", "fa-filter", "fa-sort", "fa-bars", "fa-th", - "fa-home", "fa-briefcase", "fa-graduation-cap", "fa-hospital", "fa-medkit", - "fa-chart-bar", "fa-chart-pie", "fa-chart-area", "fa-bullseye", "fa-crosshairs", - "fa-server", "fa-hdd", "fa-wifi", "fa-signal", "fa-battery-full" + "fa-database", + "fa-table", + "fa-building", + "fa-sitemap", + "fa-location-dot", + "fa-user", + "fa-users", + "fa-id-badge", + "fa-user-tie", + "fa-link", + "fa-shield-halved", + "fa-money-bill-wave", + "fa-box", + "fa-file-lines", + "fa-newspaper", + "fa-comment", + "fa-tags", + "fa-envelope", + "fa-comments", + "fa-diagram-project", + "fa-list-check", + "fa-flag-checkered", + "fa-calendar-days", + "fa-calendar-check", + "fa-clipboard-list", + "fa-gear", + "fa-plug", + "fa-chart-line", + "fa-triangle-exclamation", + "fa-microchip", + "fa-network-wired", + "fa-route", + "fa-list-ol", + "fa-lock", + "fa-right-to-bracket", + "fa-circle", + "fa-square", + "fa-heart", + "fa-star", + "fa-bookmark", + "fa-bell", + "fa-flag", + "fa-cloud", + "fa-truck", + "fa-shopping-cart", + "fa-credit-card", + "fa-certificate", + "fa-trophy", + "fa-gift", + "fa-phone", + "fa-mobile", + "fa-laptop", + "fa-desktop", + "fa-globe", + "fa-map", + "fa-compass", + "fa-wrench", + "fa-cog", + "fa-code", + "fa-bug", + "fa-terminal", + "fa-folder", + "fa-folder-open", + "fa-file", + "fa-file-pdf", + "fa-file-word", + "fa-image", + "fa-video", + "fa-music", + "fa-camera", + "fa-print", + "fa-search", + "fa-filter", + "fa-sort", + "fa-bars", + "fa-th", + "fa-home", + "fa-briefcase", + "fa-graduation-cap", + "fa-hospital", + "fa-medkit", + "fa-chart-bar", + "fa-chart-pie", + "fa-chart-area", + "fa-bullseye", + "fa-crosshairs", + "fa-server", + "fa-hdd", + "fa-wifi", + "fa-signal", + "fa-battery-full", ]; // Color palette options for rules const COLOR_PALETTE = [ - "#ef4444", "#f97316", "#f59e0b", "#eab308", "#84cc16", "#22c55e", "#10b981", "#14b8a6", - "#06b6d4", "#0ea5e9", "#3b82f6", "#6366f1", "#8b5cf6", "#a855f7", "#d946ef", "#ec4899", - "#f43f5e", "#dc2626", "#ea580c", "#d97706", "#ca8a04", "#65a30d", "#16a34a", "#059669", - "#0d9488", "#0891b2", "#0284c7", "#2563eb", "#4f46e5", "#7c3aed", "#9333ea", "#c026d3", - "#db2777", "#be123c", "#991b1b", "#9a3412", "#92400e", "#713f12", "#365314", "#14532d", - "#064e3b", "#134e4a", "#164e63", "#1e3a8a", "#312e81", "#4c1d95", "#581c87", "#701a75" + "#ef4444", + "#f97316", + "#f59e0b", + "#eab308", + "#84cc16", + "#22c55e", + "#10b981", + "#14b8a6", + "#06b6d4", + "#0ea5e9", + "#3b82f6", + "#6366f1", + "#8b5cf6", + "#a855f7", + "#d946ef", + "#ec4899", + "#f43f5e", + "#dc2626", + "#ea580c", + "#d97706", + "#ca8a04", + "#65a30d", + "#16a34a", + "#059669", + "#0d9488", + "#0891b2", + "#0284c7", + "#2563eb", + "#4f46e5", + "#7c3aed", + "#9333ea", + "#c026d3", + "#db2777", + "#be123c", + "#991b1b", + "#9a3412", + "#92400e", + "#713f12", + "#365314", + "#14532d", + "#064e3b", + "#134e4a", + "#164e63", + "#1e3a8a", + "#312e81", + "#4c1d95", + "#581c87", + "#701a75", ]; // Current active rule for icon picker @@ -151,19 +473,26 @@ function loadTableGroupingRules() { if (storedRules) { try { tableGroupingRules = JSON.parse(storedRules); - console.log(`?? Loaded ${tableGroupingRules.length} table grouping rules`); + console.log( + `?? Loaded ${tableGroupingRules.length} table grouping rules`, + ); } catch (e) { - console.warn('Failed to parse stored rules, using defaults', e); + console.warn("Failed to parse stored rules, using defaults", e); tableGroupingRules = [...DEFAULT_TABLE_GROUPING_RULES]; } } else { tableGroupingRules = [...DEFAULT_TABLE_GROUPING_RULES]; - console.log(`?? Initialized with ${tableGroupingRules.length} default rules`); + console.log( + `?? Initialized with ${tableGroupingRules.length} default rules`, + ); } } function saveTableGroupingRulesToStorage() { - localStorage.setItem(STORAGE_KEYS.tableGroupingRules, JSON.stringify(tableGroupingRules)); + localStorage.setItem( + STORAGE_KEYS.tableGroupingRules, + JSON.stringify(tableGroupingRules), + ); console.log(`?? Saved ${tableGroupingRules.length} table grouping rules`); } @@ -171,9 +500,9 @@ function saveTableGroupingRulesToStorage() { function getMatchingRule(tableName) { for (const rule of tableGroupingRules) { if (!rule.enabled) continue; - + try { - const regex = new RegExp(rule.pattern, 'i'); // Case-insensitive + const regex = new RegExp(rule.pattern, "i"); // Case-insensitive if (regex.test(tableName)) { return rule; } @@ -182,4 +511,4 @@ function getMatchingRule(tableName) { } } return null; // No match found -} \ No newline at end of file +} diff --git a/original-template.cs b/original-template.cs deleted file mode 100644 index 2d7e55c..0000000 --- a/original-template.cs +++ /dev/null @@ -1,192 +0,0 @@ -ο»Ώusing System.Reflection; -using System.Text; - -namespace SchemaMagic.Tool; - -public static class ModularHtmlTemplate -{ - private static readonly Dictionary _templateCache = []; - - public static string Generate(string entitiesJson, string? documentGuid = null, string? customCssPath = null) - { - // Generate a new GUID if none provided - var guid = documentGuid ?? Guid.NewGuid().ToString(); - - var html = LoadTemplate("template.html"); - var css = GetMergedCss(customCssPath); - var javascript = CombineJavaScriptFiles(entitiesJson, guid); - - // Replace placeholders - html = html.Replace("", $""); - html = html.Replace("", javascript); - - return html; - } - - public static string GenerateForDownload(string entitiesJson, Dictionary currentState, string? customCssPath = null) - { - // Generate a new GUID for the downloaded document - var newGuid = Guid.NewGuid().ToString(); - - var html = LoadTemplate("template.html"); - var css = GetMergedCss(customCssPath); - var javascript = CombineJavaScriptFiles(entitiesJson, newGuid, currentState); - - // Replace placeholders - html = html.Replace("", $""); - html = html.Replace("", javascript); - - return html; - } - - public static string GetDefaultCss() - { - return LoadTemplate("styles.css"); - } - - private static string GetMergedCss(string? customCssPath) - { - var defaultCss = LoadTemplate("styles.css"); - - // If no custom CSS provided, return default - if (string.IsNullOrEmpty(customCssPath) || !File.Exists(customCssPath)) - { - return defaultCss; - } - - try - { - var customCss = File.ReadAllText(customCssPath); - - // Create merged CSS with proper comments and organization - var mergedCss = new StringBuilder(); - - // Add header comment - mergedCss.AppendLine("/* SchemaMagic Generated Styles */"); - mergedCss.AppendLine("/* Default styles with custom overrides applied */"); - mergedCss.AppendLine(); - - // Add default styles first - mergedCss.AppendLine("/* ========== DEFAULT STYLES ========== */"); - mergedCss.AppendLine(defaultCss); - mergedCss.AppendLine(); - - // Add custom overrides - mergedCss.AppendLine("/* ========== CUSTOM OVERRIDES ========== */"); - mergedCss.AppendLine($"/* Loaded from: {Path.GetFileName(customCssPath)} */"); - mergedCss.AppendLine(customCss); - - Console.WriteLine($"🎨 CSS merged successfully:"); - Console.WriteLine($" Default styles: {defaultCss.Length:N0} characters"); - Console.WriteLine($" Custom styles: {customCss.Length:N0} characters"); - Console.WriteLine($" Total merged: {mergedCss.Length:N0} characters"); - - return mergedCss.ToString(); - } - catch (Exception ex) - { - Console.WriteLine($"⚠️ Warning: Could not load custom CSS file '{customCssPath}': {ex.Message}"); - Console.WriteLine("πŸ”„ Falling back to default styles"); - return defaultCss; - } - } - - private static string CombineJavaScriptFiles(string entitiesJson, string documentGuid, Dictionary? embeddedState = null) - { - var jsFiles = new[] - { - "variables.js", - "event-listeners.js", - "pan-zoom.js", - "settings.js", - "force-directed-layout.js", // Add force-directed layout before schema-generation - "schema-generation.js", - "table-generation.js", - "property-utilities.js", - "table-interaction.js", - "relationships.js", - "controls.js" - }; - - var combinedJs = string.Join("\n\n", jsFiles.Select(LoadTemplate)); - - // Replace the entities JSON placeholder - combinedJs = combinedJs.Replace("ENTITIES_JSON_PLACEHOLDER", entitiesJson); - - // Replace the document GUID placeholder - combinedJs = combinedJs.Replace("DOCUMENT_GUID_PLACEHOLDER", documentGuid); - - // If we have embedded state (for download), inject it - if (embeddedState != null) - { - var stateScript = GenerateEmbeddedStateScript(embeddedState, documentGuid); - combinedJs += "\n\n" + stateScript; - } - - return combinedJs; - } - - private static string GenerateEmbeddedStateScript(Dictionary state, string documentGuid) - { - var script = @" -// Embedded state for downloaded document -// This acts as a proxy for localStorage to preserve user customizations -(function() { - const embeddedState = " + System.Text.Json.JsonSerializer.Serialize(state) + @"; - const originalGuid = '" + documentGuid + @"'; - - // Override localStorage for this document's keys - const originalGetItem = localStorage.getItem.bind(localStorage); - const originalSetItem = localStorage.setItem.bind(localStorage); - - localStorage.getItem = function(key) { - // Check if this is one of our document-specific keys - if (key.startsWith(`schemaMagic_${originalGuid}_`)) { - const keyType = key.replace(`schemaMagic_${originalGuid}_`, ''); - return embeddedState[keyType] || null; - } - return originalGetItem(key); - }; - - localStorage.setItem = function(key, value) { - // Allow normal localStorage operation for the new document GUID - originalSetItem(key, value); - }; - - console.log('πŸ“¦ Downloaded document with embedded state from original document'); -})();"; - - return script; - } - - private static string LoadTemplate(string fileName) - { - if (_templateCache.TryGetValue(fileName, out var cached)) - { - return cached; - } - - var assembly = Assembly.GetExecutingAssembly(); - var resourceName = $"SchemaMagic.Tool.Templates.{fileName}"; - - using var stream = assembly.GetManifestResourceStream(resourceName); - if (stream == null) - { - // Fallback to file system if embedded resource not found - var filePath = Path.Combine("Templates", fileName); - if (File.Exists(filePath)) - { - var content = File.ReadAllText(filePath); - _templateCache[fileName] = content; - return content; - } - - throw new FileNotFoundException($"Template file not found: {fileName}"); - } - - using var reader = new StreamReader(stream); - var template = reader.ReadToEnd(); - _templateCache[fileName] = template; - return template; - } -} \ No newline at end of file diff --git a/publish-manual.ps1 b/publish-manual.ps1 index fe44f99..fac4a40 100644 --- a/publish-manual.ps1 +++ b/publish-manual.ps1 @@ -41,102 +41,102 @@ $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 -Write-Host "" -Write-Host "?? SchemaMagic Manual Publish Script" -ForegroundColor Cyan -Write-Host "=====================================" -ForegroundColor Cyan -Write-Host "" +Write-Information "" -InformationAction Continue +Write-Information "?? SchemaMagic Manual Publish Script" -InformationAction Continue +Write-Information "=====================================" -InformationAction Continue +Write-Information "" -InformationAction Continue # Check if nbgv is installed try { $null = nbgv --version } catch { - Write-Host "? Nerdbank.GitVersioning (nbgv) not installed" -ForegroundColor Red - Write-Host "?? Installing nbgv..." -ForegroundColor Yellow + Write-Information "? Nerdbank.GitVersioning (nbgv) not installed" -InformationAction Continue + Write-Information "?? Installing nbgv..." -InformationAction Continue dotnet tool install -g nbgv } # Check API key file exists if (-not (Test-Path $ApiKeyFile)) { - Write-Host "? API key file not found: $ApiKeyFile" -ForegroundColor Red - Write-Host "" - Write-Host "?? Create the file and add your NuGet API key:" -ForegroundColor Yellow - Write-Host " 1. Get key from: https://www.nuget.org/account/apikeys" -ForegroundColor White - Write-Host " 2. Save it to: $ApiKeyFile" -ForegroundColor White - Write-Host "" + Write-Information "? API key file not found: $ApiKeyFile" -InformationAction Continue + Write-Information "" -InformationAction Continue + Write-Information "?? Create the file and add your NuGet API key:" -InformationAction Continue + Write-Information " 1. Get key from: https://www.nuget.org/account/apikeys" -InformationAction Continue + Write-Information " 2. Save it to: $ApiKeyFile" -InformationAction Continue + Write-Information "" -InformationAction Continue exit 1 } # Load API key $API_KEY = (Get-Content $ApiKeyFile -Raw).Trim() if ([string]::IsNullOrWhiteSpace($API_KEY)) { - Write-Host "? API key file is empty: $ApiKeyFile" -ForegroundColor Red + Write-Information "? API key file is empty: $ApiKeyFile" -InformationAction Continue exit 1 } -Write-Host "? API key loaded from $ApiKeyFile" -ForegroundColor Green +Write-Information "? API key loaded from $ApiKeyFile" -InformationAction Continue # Get version -Write-Host "" -Write-Host "?? Getting version..." -ForegroundColor Yellow +Write-Information "" -InformationAction Continue +Write-Information "?? Getting version..." -InformationAction Continue $VERSION = nbgv get-version -v SemVer2 $SIMPLE_VERSION = nbgv get-version -v SimpleVersion -Write-Host "? Version: $VERSION" -ForegroundColor Green -Write-Host " Simple: $SIMPLE_VERSION" -ForegroundColor Cyan +Write-Information "? Version: $VERSION" -InformationAction Continue +Write-Information " Simple: $SIMPLE_VERSION" -InformationAction Continue # Check git status $gitStatus = git status --porcelain if ($gitStatus) { - Write-Host "" - Write-Host "?? Warning: You have uncommitted changes" -ForegroundColor Yellow - Write-Host "$gitStatus" -ForegroundColor Gray + Write-Information "" -InformationAction Continue + Write-Information "?? Warning: You have uncommitted changes" -InformationAction Continue + Write-Information "$gitStatus" -InformationAction Continue $continue = Read-Host "Continue anyway? (y/N)" if ($continue -ne "y") { - Write-Host "? Cancelled" -ForegroundColor Red + Write-Information "? Cancelled" -InformationAction Continue exit 1 } } # Clean -Write-Host "" -Write-Host "?? Cleaning previous builds..." -ForegroundColor Yellow +Write-Information "" -InformationAction Continue +Write-Information "?? Cleaning previous builds..." -InformationAction Continue dotnet clean --verbosity quiet Remove-Item -Recurse -Force ./nupkg -ErrorAction SilentlyContinue -Write-Host "? Clean complete" -ForegroundColor Green +Write-Information "? Clean complete" -InformationAction Continue # Restore -Write-Host "" -Write-Host "?? Restoring dependencies..." -ForegroundColor Yellow +Write-Information "" -InformationAction Continue +Write-Information "?? Restoring dependencies..." -InformationAction Continue dotnet restore --verbosity quiet -Write-Host "? Dependencies restored" -ForegroundColor Green +Write-Information "? Dependencies restored" -InformationAction Continue # Run tests if (-not $SkipTests) { - Write-Host "" - Write-Host "?? Running tests..." -ForegroundColor Yellow + Write-Information "" -InformationAction Continue + Write-Information "?? Running tests..." -InformationAction Continue dotnet test --configuration Release --verbosity quiet --nologo if ($LASTEXITCODE -ne 0) { - Write-Host "? Tests failed" -ForegroundColor Red + Write-Information "? Tests failed" -InformationAction Continue exit 1 } - Write-Host "? All tests passed" -ForegroundColor Green + Write-Information "? All tests passed" -InformationAction Continue } else { - Write-Host "" - Write-Host "?? Skipping tests" -ForegroundColor Yellow + Write-Information "" -InformationAction Continue + Write-Information "?? Skipping tests" -InformationAction Continue } # Build -Write-Host "" -Write-Host "?? Building Release configuration..." -ForegroundColor Yellow +Write-Information "" -InformationAction Continue +Write-Information "?? Building Release configuration..." -InformationAction Continue dotnet build --configuration Release --no-restore --verbosity quiet --nologo if ($LASTEXITCODE -ne 0) { - Write-Host "? Build failed" -ForegroundColor Red + Write-Information "? Build failed" -InformationAction Continue exit 1 } -Write-Host "? Build successful" -ForegroundColor Green +Write-Information "? Build successful" -InformationAction Continue # Pack -Write-Host "" -Write-Host "?? Creating NuGet package..." -ForegroundColor Yellow +Write-Information "" -InformationAction Continue +Write-Information "?? Creating NuGet package..." -InformationAction Continue dotnet pack SchemaMagic/SchemaMagic.csproj ` --configuration Release ` --output ./nupkg ` @@ -145,84 +145,84 @@ dotnet pack SchemaMagic/SchemaMagic.csproj ` --nologo if ($LASTEXITCODE -ne 0) { - Write-Host "? Pack failed" -ForegroundColor Red + Write-Information "? Pack failed" -InformationAction Continue exit 1 } # List generated packages -Write-Host "? Package created" -ForegroundColor Green -Write-Host "" -Write-Host "?? Generated packages:" -ForegroundColor Cyan +Write-Information "? Package created" -InformationAction Continue +Write-Information "" -InformationAction Continue +Write-Information "?? Generated packages:" -InformationAction Continue Get-ChildItem ./nupkg/*.nupkg | ForEach-Object { $size = [math]::Round($_.Length / 1KB, 2) - Write-Host " $($_.Name) ($size KB)" -ForegroundColor White + Write-Information " $($_.Name) ($size KB)" -InformationAction Continue } if ($DryRun) { - Write-Host "" - Write-Host "?? DRY RUN - Skipping publish" -ForegroundColor Yellow - Write-Host "" - Write-Host "? Build successful! Package ready at ./nupkg/" -ForegroundColor Green - Write-Host "" - Write-Host "?? To publish for real, run without -DryRun:" -ForegroundColor Yellow - Write-Host " .\publish-manual.ps1" -ForegroundColor White - Write-Host "" + Write-Information "" -InformationAction Continue + Write-Information "?? DRY RUN - Skipping publish" -InformationAction Continue + Write-Information "" -InformationAction Continue + Write-Information "? Build successful! Package ready at ./nupkg/" -InformationAction Continue + Write-Information "" -InformationAction Continue + Write-Information "?? To publish for real, run without -DryRun:" -InformationAction Continue + Write-Information " .\publish-manual.ps1" -InformationAction Continue + Write-Information "" -InformationAction Continue exit 0 } # Confirm publish -Write-Host "" -Write-Host "?? About to publish version $VERSION to NuGet.org" -ForegroundColor Yellow +Write-Information "" -InformationAction Continue +Write-Information "?? About to publish version $VERSION to NuGet.org" -InformationAction Continue $confirm = Read-Host "Continue? (y/N)" if ($confirm -ne "y") { - Write-Host "? Cancelled" -ForegroundColor Red + Write-Information "? Cancelled" -InformationAction Continue exit 1 } # Publish -Write-Host "" -Write-Host "πŸš€ Publishing to NuGet.org..." -ForegroundColor Yellow +Write-Information "" -InformationAction Continue +Write-Information "Publishing to NuGet.org..." -InformationAction Continue # Get the actual nupkg file path $nupkgFiles = Get-ChildItem ./nupkg/*.nupkg -ErrorAction Stop if ($nupkgFiles.Count -eq 0) { - Write-Host "❌ No .nupkg files found in ./nupkg/" -ForegroundColor Red + Write-Information "❌ No .nupkg files found in ./nupkg/" -InformationAction Continue exit 1 } -Write-Host "πŸ“¦ Found $($nupkgFiles.Count) package(s) to publish" -ForegroundColor Cyan +Write-Information "Found $($nupkgFiles.Count) package(s) to publish" -InformationAction Continue foreach ($pkg in $nupkgFiles) { - Write-Host " Publishing: $($pkg.Name)" -ForegroundColor White + Write-Information " Publishing: $($pkg.Name)" -InformationAction Continue dotnet nuget push $pkg.FullName ` --api-key $API_KEY ` --source https://api.nuget.org/v3/index.json ` --skip-duplicate if ($LASTEXITCODE -ne 0) { - Write-Host "" - Write-Host "❌ Publish failed for $($pkg.Name) with exit code $LASTEXITCODE" -ForegroundColor Red - Write-Host "" - Write-Host "Common issues:" -ForegroundColor Yellow - Write-Host " β€’ API key expired or invalid" -ForegroundColor White - Write-Host " β€’ Version already published (NuGet doesn't allow overwrites)" -ForegroundColor White - Write-Host " β€’ Network connectivity issues" -ForegroundColor White - Write-Host "" + Write-Information "" -InformationAction Continue + Write-Information "❌ Publish failed for $($pkg.Name) with exit code $LASTEXITCODE" -InformationAction Continue + Write-Information "" -InformationAction Continue + Write-Information "Common issues:" -InformationAction Continue + Write-Information " β€’ API key expired or invalid" -InformationAction Continue + Write-Information " β€’ Version already published (NuGet doesn't allow overwrites)" -InformationAction Continue + Write-Information " β€’ Network connectivity issues" -InformationAction Continue + Write-Information "" -InformationAction Continue exit 1 } } -Write-Host "" -Write-Host "? Published successfully!" -ForegroundColor Green -Write-Host "" -Write-Host "?? Package URL: https://www.nuget.org/packages/SchemaMagic/$SIMPLE_VERSION" -ForegroundColor Cyan -Write-Host "" -Write-Host "?? Next steps:" -ForegroundColor Yellow -Write-Host " 1. Wait 5-10 minutes for NuGet indexing" -ForegroundColor White -Write-Host " 2. Create git tag:" -ForegroundColor White -Write-Host " git tag -a v$VERSION -m 'Release v$VERSION'" -ForegroundColor Cyan -Write-Host " 3. Push tag to GitHub:" -ForegroundColor White -Write-Host " git push origin v$VERSION" -ForegroundColor Cyan -Write-Host " 4. Test installation:" -ForegroundColor White -Write-Host " dotnet tool update -g SchemaMagic" -ForegroundColor Cyan -Write-Host "" +Write-Information "" -InformationAction Continue +Write-Information "? Published successfully!" -InformationAction Continue +Write-Information "" -InformationAction Continue +Write-Information "?? Package URL: https://www.nuget.org/packages/SchemaMagic/$SIMPLE_VERSION" -InformationAction Continue +Write-Information "" -InformationAction Continue +Write-Information "?? Next steps:" -InformationAction Continue +Write-Information " 1. Wait 5-10 minutes for NuGet indexing" -InformationAction Continue +Write-Information " 2. Create git tag:" -InformationAction Continue +Write-Information " git tag -a v$VERSION -m 'Release v$VERSION'" -InformationAction Continue +Write-Information " 3. Push tag to GitHub:" -InformationAction Continue +Write-Information " git push origin v$VERSION" -InformationAction Continue +Write-Information " 4. Test installation:" -InformationAction Continue +Write-Information " dotnet tool update -g SchemaMagic" -InformationAction Continue +Write-Information "" -InformationAction Continue diff --git a/temp-original.txt b/temp-original.txt deleted file mode 100644 index 2d7e55c..0000000 --- a/temp-original.txt +++ /dev/null @@ -1,192 +0,0 @@ -ο»Ώusing System.Reflection; -using System.Text; - -namespace SchemaMagic.Tool; - -public static class ModularHtmlTemplate -{ - private static readonly Dictionary _templateCache = []; - - public static string Generate(string entitiesJson, string? documentGuid = null, string? customCssPath = null) - { - // Generate a new GUID if none provided - var guid = documentGuid ?? Guid.NewGuid().ToString(); - - var html = LoadTemplate("template.html"); - var css = GetMergedCss(customCssPath); - var javascript = CombineJavaScriptFiles(entitiesJson, guid); - - // Replace placeholders - html = html.Replace("", $""); - html = html.Replace("", javascript); - - return html; - } - - public static string GenerateForDownload(string entitiesJson, Dictionary currentState, string? customCssPath = null) - { - // Generate a new GUID for the downloaded document - var newGuid = Guid.NewGuid().ToString(); - - var html = LoadTemplate("template.html"); - var css = GetMergedCss(customCssPath); - var javascript = CombineJavaScriptFiles(entitiesJson, newGuid, currentState); - - // Replace placeholders - html = html.Replace("", $""); - html = html.Replace("", javascript); - - return html; - } - - public static string GetDefaultCss() - { - return LoadTemplate("styles.css"); - } - - private static string GetMergedCss(string? customCssPath) - { - var defaultCss = LoadTemplate("styles.css"); - - // If no custom CSS provided, return default - if (string.IsNullOrEmpty(customCssPath) || !File.Exists(customCssPath)) - { - return defaultCss; - } - - try - { - var customCss = File.ReadAllText(customCssPath); - - // Create merged CSS with proper comments and organization - var mergedCss = new StringBuilder(); - - // Add header comment - mergedCss.AppendLine("/* SchemaMagic Generated Styles */"); - mergedCss.AppendLine("/* Default styles with custom overrides applied */"); - mergedCss.AppendLine(); - - // Add default styles first - mergedCss.AppendLine("/* ========== DEFAULT STYLES ========== */"); - mergedCss.AppendLine(defaultCss); - mergedCss.AppendLine(); - - // Add custom overrides - mergedCss.AppendLine("/* ========== CUSTOM OVERRIDES ========== */"); - mergedCss.AppendLine($"/* Loaded from: {Path.GetFileName(customCssPath)} */"); - mergedCss.AppendLine(customCss); - - Console.WriteLine($"🎨 CSS merged successfully:"); - Console.WriteLine($" Default styles: {defaultCss.Length:N0} characters"); - Console.WriteLine($" Custom styles: {customCss.Length:N0} characters"); - Console.WriteLine($" Total merged: {mergedCss.Length:N0} characters"); - - return mergedCss.ToString(); - } - catch (Exception ex) - { - Console.WriteLine($"⚠️ Warning: Could not load custom CSS file '{customCssPath}': {ex.Message}"); - Console.WriteLine("πŸ”„ Falling back to default styles"); - return defaultCss; - } - } - - private static string CombineJavaScriptFiles(string entitiesJson, string documentGuid, Dictionary? embeddedState = null) - { - var jsFiles = new[] - { - "variables.js", - "event-listeners.js", - "pan-zoom.js", - "settings.js", - "force-directed-layout.js", // Add force-directed layout before schema-generation - "schema-generation.js", - "table-generation.js", - "property-utilities.js", - "table-interaction.js", - "relationships.js", - "controls.js" - }; - - var combinedJs = string.Join("\n\n", jsFiles.Select(LoadTemplate)); - - // Replace the entities JSON placeholder - combinedJs = combinedJs.Replace("ENTITIES_JSON_PLACEHOLDER", entitiesJson); - - // Replace the document GUID placeholder - combinedJs = combinedJs.Replace("DOCUMENT_GUID_PLACEHOLDER", documentGuid); - - // If we have embedded state (for download), inject it - if (embeddedState != null) - { - var stateScript = GenerateEmbeddedStateScript(embeddedState, documentGuid); - combinedJs += "\n\n" + stateScript; - } - - return combinedJs; - } - - private static string GenerateEmbeddedStateScript(Dictionary state, string documentGuid) - { - var script = @" -// Embedded state for downloaded document -// This acts as a proxy for localStorage to preserve user customizations -(function() { - const embeddedState = " + System.Text.Json.JsonSerializer.Serialize(state) + @"; - const originalGuid = '" + documentGuid + @"'; - - // Override localStorage for this document's keys - const originalGetItem = localStorage.getItem.bind(localStorage); - const originalSetItem = localStorage.setItem.bind(localStorage); - - localStorage.getItem = function(key) { - // Check if this is one of our document-specific keys - if (key.startsWith(`schemaMagic_${originalGuid}_`)) { - const keyType = key.replace(`schemaMagic_${originalGuid}_`, ''); - return embeddedState[keyType] || null; - } - return originalGetItem(key); - }; - - localStorage.setItem = function(key, value) { - // Allow normal localStorage operation for the new document GUID - originalSetItem(key, value); - }; - - console.log('πŸ“¦ Downloaded document with embedded state from original document'); -})();"; - - return script; - } - - private static string LoadTemplate(string fileName) - { - if (_templateCache.TryGetValue(fileName, out var cached)) - { - return cached; - } - - var assembly = Assembly.GetExecutingAssembly(); - var resourceName = $"SchemaMagic.Tool.Templates.{fileName}"; - - using var stream = assembly.GetManifestResourceStream(resourceName); - if (stream == null) - { - // Fallback to file system if embedded resource not found - var filePath = Path.Combine("Templates", fileName); - if (File.Exists(filePath)) - { - var content = File.ReadAllText(filePath); - _templateCache[fileName] = content; - return content; - } - - throw new FileNotFoundException($"Template file not found: {fileName}"); - } - - using var reader = new StreamReader(stream); - var template = reader.ReadToEnd(); - _templateCache[fileName] = template; - return template; - } -} \ No newline at end of file diff --git a/test-styles.css b/test-styles.css deleted file mode 100644 index d1bfd18..0000000 --- a/test-styles.css +++ /dev/null @@ -1,148 +0,0 @@ - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); - height: 100vh; - overflow: hidden; -} - -#schema-container { - width: 100%; - height: 100vh; - display: flex; - flex-direction: column; -} - -#toolbar { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(10px); - border-bottom: 1px solid #e0e0e0; - padding: 12px 20px; - display: flex; - justify-content: space-between; - align-items: center; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); -} - -#toolbar h1 { - font-size: 1.5em; - color: #333; - margin: 0; -} - -.controls { - display: flex; - gap: 10px; -} - -.controls button { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border: none; - padding: 8px 16px; - border-radius: 6px; - cursor: pointer; - font-size: 0.9em; - transition: all 0.3s ease; -} - -.controls button:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); -} - -#schema-svg { - flex: 1; - background: linear-gradient(45deg, #f8f9fa 25%, transparent 25%), - linear-gradient(-45deg, #f8f9fa 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, #f8f9fa 75%), - linear-gradient(-45deg, transparent 75%, #f8f9fa 75%); - background-size: 20px 20px; - background-position: 0 0, 0 10px, 10px -10px, -10px 0px; -} - -.table { - transition: all 0.3s ease; -} - -.table:hover { - filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.15)); -} - -.table.selected { - filter: drop-shadow(0 8px 20px rgba(102, 126, 234, 0.4)); -} - -.table-bg { - fill: white; - stroke: #ddd; - stroke-width: 1; - filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1)); -} - -.table-header { - fill: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - stroke: none; -} - -.table-title { - fill: white; - font-size: 14px; - font-weight: 600; -} - -.property { - fill: #444; - font-size: 12px; - font-family: 'Monaco', 'Consolas', monospace; -} - -.property-key { - fill: #f39c12; - font-weight: 600; -} - -.property-fk { - fill: #9b59b6; - font-weight: 500; -} - -.relationship-line { - stroke: #666; - stroke-width: 2; - fill: none; - marker-end: url(#arrowhead); -} - -.relationship-line.inheritance { - stroke: #3498db; - stroke-dasharray: 5,5; -} - -.relationship-line.navigation { - stroke: #27ae60; -} - -@media (max-width: 768px) { - #toolbar { - flex-direction: column; - gap: 10px; - padding: 10px; - } - - .controls { - flex-wrap: wrap; - justify-content: center; - } - - .controls button { - font-size: 0.8em; - padding: 6px 12px; - } -} \ No newline at end of file