Created
January 15, 2026 19:45
-
-
Save parthi2929/32726fb80643850ef811d2cfc170f393 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (function () { | |
| if (window.GizaGitlabDrawioPluginLoaded) { | |
| return; | |
| } | |
| window.GizaGitlabDrawioPluginLoaded = true; | |
| var PAT_STORAGE_KEY = "giza.gitlab.pat"; | |
| var BASE_URL_STORAGE_KEY = "giza.gitlab.baseUrl"; | |
| var DEFAULT_GITLAB_BASE_URL = "https://gitlab.com"; | |
| function loadScriptOnce(id, src) { | |
| return new Promise(function (resolve, reject) { | |
| if (document.getElementById(id)) { | |
| resolve(); | |
| return; | |
| } | |
| var script = document.createElement("script"); | |
| script.id = id; | |
| script.src = src; | |
| script.async = true; | |
| script.onload = function () { | |
| resolve(); | |
| }; | |
| script.onerror = function () { | |
| reject(new Error("Failed to load script: " + src)); | |
| }; | |
| document.head.appendChild(script); | |
| }); | |
| } | |
| function loadStyleOnce(id, href) { | |
| if (document.getElementById(id)) { | |
| return; | |
| } | |
| var link = document.createElement("link"); | |
| link.id = id; | |
| link.rel = "stylesheet"; | |
| link.href = href; | |
| document.head.appendChild(link); | |
| } | |
| function ensureRenderDeps() { | |
| var tasks = [ | |
| loadScriptOnce( | |
| "giza-markdown-it", | |
| "https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js" | |
| ), | |
| loadScriptOnce( | |
| "giza-markdown-it-katex", | |
| "https://cdn.jsdelivr.net/npm/markdown-it-katex@2.0.3/dist/markdown-it-katex.min.js" | |
| ) | |
| ]; | |
| loadStyleOnce( | |
| "giza-katex-css", | |
| "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" | |
| ); | |
| return Promise.all(tasks); | |
| } | |
| function normalizeGitlabMath(markdown) { | |
| if (!markdown) { | |
| return ""; | |
| } | |
| return markdown | |
| .replace(/\\\[/g, "$$") | |
| .replace(/\\\]/g, "$$") | |
| .replace(/\\\(/g, "$") | |
| .replace(/\\\)/g, "$"); | |
| } | |
| function renderMarkdownToHtml(markdown) { | |
| return ensureRenderDeps().then(function () { | |
| if (!window.gizaMarkdownIt) { | |
| window.gizaMarkdownIt = window | |
| .markdownit({ | |
| html: true, | |
| linkify: true, | |
| breaks: true | |
| }) | |
| .use(window.markdownitKatex); | |
| } | |
| var normalized = normalizeGitlabMath(markdown); | |
| return window.gizaMarkdownIt.render(normalized); | |
| }); | |
| } | |
| function getStoredToken() { | |
| try { | |
| return localStorage.getItem(PAT_STORAGE_KEY) || ""; | |
| } catch (error) { | |
| return ""; | |
| } | |
| } | |
| function setStoredToken(token) { | |
| try { | |
| localStorage.setItem(PAT_STORAGE_KEY, token); | |
| } catch (error) { | |
| // Ignore storage errors. | |
| } | |
| } | |
| function getStoredBaseUrl() { | |
| try { | |
| return localStorage.getItem(BASE_URL_STORAGE_KEY) || ""; | |
| } catch (error) { | |
| return ""; | |
| } | |
| } | |
| function setStoredBaseUrl(baseUrl) { | |
| try { | |
| localStorage.setItem(BASE_URL_STORAGE_KEY, baseUrl); | |
| } catch (error) { | |
| // Ignore storage errors. | |
| } | |
| } | |
| function showStatus(el, message, isError) { | |
| el.textContent = message; | |
| el.style.color = isError ? "#a00" : "#0a6"; | |
| } | |
| function clearStatus(el) { | |
| el.textContent = ""; | |
| } | |
| function createDialog(ui, options) { | |
| var container = document.createElement("div"); | |
| container.style.padding = "12px"; | |
| container.style.maxWidth = "520px"; | |
| var title = document.createElement("div"); | |
| title.textContent = options.title || ""; | |
| title.style.fontWeight = "bold"; | |
| title.style.marginBottom = "10px"; | |
| var body = document.createElement("div"); | |
| body.style.marginBottom = "12px"; | |
| var footer = document.createElement("div"); | |
| footer.style.display = "flex"; | |
| footer.style.justifyContent = "flex-end"; | |
| footer.style.gap = "8px"; | |
| container.appendChild(title); | |
| container.appendChild(body); | |
| container.appendChild(footer); | |
| if (options.buildBody) { | |
| options.buildBody(body, footer); | |
| } | |
| ui.showDialog(container, options.width || 420, options.height || 260, true, true); | |
| return function closeDialog() { | |
| ui.hideDialog(); | |
| }; | |
| } | |
| function testToken(baseUrl, token) { | |
| return fetch(baseUrl + "/api/v4/user", { | |
| method: "GET", | |
| headers: { | |
| "PRIVATE-TOKEN": token | |
| } | |
| }).then(function (response) { | |
| if (!response.ok) { | |
| var error = new Error("Token validation failed"); | |
| error.status = response.status; | |
| throw error; | |
| } | |
| return response.json(); | |
| }); | |
| } | |
| function parseGitlabNoteUrl(url) { | |
| var parsed; | |
| try { | |
| parsed = new URL(url); | |
| } catch (error) { | |
| return null; | |
| } | |
| var noteMatch = parsed.hash.match(/note_(\d+)/); | |
| if (!noteMatch) { | |
| return null; | |
| } | |
| var noteId = noteMatch[1]; | |
| var pathParts = parsed.pathname.split("/").filter(Boolean); | |
| var dashIndex = pathParts.indexOf("-"); | |
| if (dashIndex === -1 || dashIndex + 2 >= pathParts.length) { | |
| return null; | |
| } | |
| var projectPath = pathParts.slice(0, dashIndex).join("/"); | |
| var type = pathParts[dashIndex + 1]; | |
| var iid = pathParts[dashIndex + 2]; | |
| if (!projectPath || !type || !iid) { | |
| return null; | |
| } | |
| if (type !== "issues" && type !== "merge_requests") { | |
| return null; | |
| } | |
| return { | |
| baseUrl: parsed.origin, | |
| projectPath: projectPath, | |
| type: type, | |
| iid: iid, | |
| noteId: noteId | |
| }; | |
| } | |
| function fetchGitlabNote(noteInfo, token) { | |
| var encodedProject = encodeURIComponent(noteInfo.projectPath); | |
| var apiUrl = | |
| noteInfo.baseUrl + | |
| "/api/v4/projects/" + | |
| encodedProject + | |
| "/" + | |
| noteInfo.type + | |
| "/" + | |
| noteInfo.iid + | |
| "/notes/" + | |
| noteInfo.noteId; | |
| return fetch(apiUrl, { | |
| method: "GET", | |
| headers: { | |
| "PRIVATE-TOKEN": token | |
| } | |
| }).then(function (response) { | |
| if (!response.ok) { | |
| var error = new Error("Failed to fetch note"); | |
| error.status = response.status; | |
| throw error; | |
| } | |
| return response.json(); | |
| }); | |
| } | |
| function getVisibleOrigin(graph) { | |
| var view = graph.view; | |
| var scale = view.scale; | |
| var tr = view.translate; | |
| return { | |
| x: Math.max(20, (-tr.x + 20) / scale), | |
| y: Math.max(20, (-tr.y + 20) / scale) | |
| }; | |
| } | |
| function getVertexBounds(graph, cell) { | |
| var bounds = graph.getCellBounds(cell); | |
| if (!bounds) { | |
| return null; | |
| } | |
| return { | |
| x: bounds.x, | |
| y: bounds.y, | |
| width: bounds.width, | |
| height: bounds.height | |
| }; | |
| } | |
| function intersects(rectA, rectB) { | |
| return !( | |
| rectB.x > rectA.x + rectA.width || | |
| rectB.x + rectB.width < rectA.x || | |
| rectB.y > rectA.y + rectA.height || | |
| rectB.y + rectB.height < rectA.y | |
| ); | |
| } | |
| function findNonOverlappingPosition(graph, width, height) { | |
| var parent = graph.getDefaultParent(); | |
| var vertices = graph.getModel().getChildVertices(parent); | |
| var origin = getVisibleOrigin(graph); | |
| var offsetX = 20; | |
| var offsetY = 20; | |
| var x = origin.x; | |
| var y = origin.y; | |
| var attempts = 0; | |
| while (attempts < 30) { | |
| var candidate = { | |
| x: x, | |
| y: y, | |
| width: width, | |
| height: height | |
| }; | |
| var overlaps = false; | |
| for (var i = 0; i < vertices.length; i += 1) { | |
| var bounds = getVertexBounds(graph, vertices[i]); | |
| if (bounds && intersects(candidate, bounds)) { | |
| overlaps = true; | |
| break; | |
| } | |
| } | |
| if (!overlaps) { | |
| return { x: x, y: y }; | |
| } | |
| x += offsetX; | |
| y = Math.max(20, y - offsetY); | |
| attempts += 1; | |
| } | |
| return { x: origin.x, y: origin.y }; | |
| } | |
| function insertNoteNode(graph, htmlContent) { | |
| var parent = graph.getDefaultParent(); | |
| var width = 360; | |
| var height = 240; | |
| var position = findNonOverlappingPosition(graph, width, height); | |
| var style = [ | |
| "shape=swimlane", | |
| "startSize=28", | |
| "horizontal=0", | |
| "html=1", | |
| "whiteSpace=wrap", | |
| "align=left", | |
| "verticalAlign=top", | |
| "spacing=8", | |
| "rounded=1" | |
| ].join(";"); | |
| var label = | |
| "<div style=\"width:100%;height:100%;overflow:hidden;font-size:12px;line-height:1.45;\">" + | |
| "<div style=\"font-weight:bold;margin-bottom:6px;\">GitLab Note</div>" + | |
| "<div>" + | |
| htmlContent + | |
| "</div>" + | |
| "</div>"; | |
| var cell; | |
| graph.getModel().beginUpdate(); | |
| try { | |
| cell = graph.insertVertex( | |
| parent, | |
| null, | |
| label, | |
| position.x, | |
| position.y, | |
| width, | |
| height, | |
| style | |
| ); | |
| } finally { | |
| graph.getModel().endUpdate(); | |
| } | |
| if (cell) { | |
| graph.setSelectionCell(cell); | |
| graph.scrollCellToVisible(cell); | |
| } | |
| } | |
| function openSettingsDialog(ui) { | |
| var close = createDialog(ui, { | |
| title: "GitLab Settings", | |
| width: 440, | |
| height: 260, | |
| buildBody: function (body, footer) { | |
| var label = document.createElement("div"); | |
| label.textContent = "Personal Access Token"; | |
| label.style.marginBottom = "6px"; | |
| var input = document.createElement("input"); | |
| input.type = "password"; | |
| input.placeholder = "glpat-..."; | |
| input.value = getStoredToken(); | |
| input.style.width = "100%"; | |
| input.style.boxSizing = "border-box"; | |
| input.style.marginBottom = "8px"; | |
| var status = document.createElement("div"); | |
| status.style.minHeight = "18px"; | |
| status.style.fontSize = "12px"; | |
| var testButton = document.createElement("button"); | |
| testButton.textContent = "Test"; | |
| var saveButton = document.createElement("button"); | |
| saveButton.textContent = "Save"; | |
| saveButton.style.fontWeight = "bold"; | |
| var cancelButton = document.createElement("button"); | |
| cancelButton.textContent = "Cancel"; | |
| testButton.onclick = function () { | |
| clearStatus(status); | |
| var token = input.value.trim(); | |
| if (!token) { | |
| showStatus(status, "Token is required.", true); | |
| return; | |
| } | |
| var baseUrl = getStoredBaseUrl() || DEFAULT_GITLAB_BASE_URL; | |
| testButton.disabled = true; | |
| showStatus(status, "Testing token...", false); | |
| testToken(baseUrl, token) | |
| .then(function () { | |
| showStatus(status, "Token is valid.", false); | |
| }) | |
| .catch(function (error) { | |
| var message = | |
| error && error.status === 401 | |
| ? "Token unauthorized." | |
| : "Token test failed."; | |
| showStatus(status, message, true); | |
| }) | |
| .finally(function () { | |
| testButton.disabled = false; | |
| }); | |
| }; | |
| saveButton.onclick = function () { | |
| clearStatus(status); | |
| var token = input.value.trim(); | |
| if (!token) { | |
| showStatus(status, "Token is required.", true); | |
| return; | |
| } | |
| setStoredToken(token); | |
| showStatus(status, "Token saved.", false); | |
| setTimeout(function () { | |
| close(); | |
| }, 400); | |
| }; | |
| cancelButton.onclick = function () { | |
| close(); | |
| }; | |
| body.appendChild(label); | |
| body.appendChild(input); | |
| body.appendChild(status); | |
| footer.appendChild(cancelButton); | |
| footer.appendChild(testButton); | |
| footer.appendChild(saveButton); | |
| } | |
| }); | |
| } | |
| function openAddNoteDialog(ui) { | |
| var close = createDialog(ui, { | |
| title: "Add a GitLab note", | |
| width: 520, | |
| height: 300, | |
| buildBody: function (body, footer) { | |
| var label = document.createElement("div"); | |
| label.textContent = "GitLab note link"; | |
| label.style.marginBottom = "6px"; | |
| var input = document.createElement("input"); | |
| input.type = "text"; | |
| input.placeholder = "https://gitlab.com/group/project/-/issues/1#note_123"; | |
| input.style.width = "100%"; | |
| input.style.boxSizing = "border-box"; | |
| input.style.marginBottom = "8px"; | |
| var status = document.createElement("div"); | |
| status.style.minHeight = "18px"; | |
| status.style.fontSize = "12px"; | |
| var addButton = document.createElement("button"); | |
| addButton.textContent = "Add"; | |
| addButton.style.fontWeight = "bold"; | |
| var cancelButton = document.createElement("button"); | |
| cancelButton.textContent = "Cancel"; | |
| addButton.onclick = function () { | |
| clearStatus(status); | |
| var token = getStoredToken(); | |
| if (!token) { | |
| showStatus(status, "Save a token in Settings first.", true); | |
| return; | |
| } | |
| var urlValue = input.value.trim(); | |
| if (!urlValue) { | |
| showStatus(status, "Note link is required.", true); | |
| return; | |
| } | |
| var noteInfo = parseGitlabNoteUrl(urlValue); | |
| if (!noteInfo) { | |
| showStatus(status, "Invalid note link.", true); | |
| return; | |
| } | |
| setStoredBaseUrl(noteInfo.baseUrl); | |
| addButton.disabled = true; | |
| showStatus(status, "Fetching note...", false); | |
| fetchGitlabNote(noteInfo, token) | |
| .then(function (note) { | |
| var bodyText = note && note.body ? note.body : ""; | |
| return renderMarkdownToHtml(bodyText); | |
| }) | |
| .then(function (html) { | |
| insertNoteNode(ui.editor.graph, html); | |
| close(); | |
| }) | |
| .catch(function (error) { | |
| var message = | |
| error && error.status === 401 | |
| ? "Unauthorized. Check your token." | |
| : "Failed to fetch note."; | |
| showStatus(status, message, true); | |
| }) | |
| .finally(function () { | |
| addButton.disabled = false; | |
| }); | |
| }; | |
| cancelButton.onclick = function () { | |
| close(); | |
| }; | |
| body.appendChild(label); | |
| body.appendChild(input); | |
| body.appendChild(status); | |
| footer.appendChild(cancelButton); | |
| footer.appendChild(addButton); | |
| } | |
| }); | |
| } | |
| function addMenu(ui) { | |
| var menu = ui.menus.addMenu("gizaTools", function (menu, parent) { | |
| var notesFlowItem = menu.addItem("Notes Flow", null, null, parent); | |
| menu.addItem("Add a GitLab note", null, function () { | |
| openAddNoteDialog(ui); | |
| }, notesFlowItem); | |
| menu.addSeparator(parent); | |
| menu.addItem("Settings", null, function () { | |
| openSettingsDialog(ui); | |
| }, parent); | |
| }); | |
| ui.menubar.addMenu(menu, "Giza Tools", "last"); | |
| } | |
| if (typeof Draw !== "undefined" && Draw.loadPlugin) { | |
| Draw.loadPlugin(function (ui) { | |
| addMenu(ui); | |
| }); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment