From f97d1d9840bcaad4d71fa20f47dc3f01dd45667c Mon Sep 17 00:00:00 2001 From: Jiaan Louw Date: Mon, 20 Oct 2025 16:39:24 +0200 Subject: [PATCH 1/3] Fix GLQL rendering in Duo agentic chat Resolves GLQL markdown in Duo chat not rendering as embedded views. Changelog: fixed --- .../behaviors/markdown/render_gfm.js | 4 +-- app/assets/javascripts/glql/index.js | 7 ++++-- .../behaviors/markdown/render_gfm_spec.js | 12 ++++++--- spec/frontend/glql/index_spec.js | 25 +++++++++++++------ 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 4d3d9fd91fef4b..c02950534ff2bf 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -42,7 +42,7 @@ export function renderGFM(element) { '.js-render-mermaid', '[data-canonical-lang="json"][data-lang-params~="table"]', 'table[data-table-fields]', - '[data-canonical-lang="glql"]', + '[data-canonical-lang="glql"], .language-glql', '.gfm-project_member', '.gfm-issue, .gfm-work_item, .gfm-merge_request, .gfm-epic, .gfm-milestone', '.task-list-item-checkbox', @@ -59,6 +59,6 @@ export function renderGFM(element) { highlightCurrentUser(userEls); initPopovers(popoverEls); addAriaLabels(taskListCheckboxEls); - renderGlql(glqlEls.map((e) => e.parentNode)); + renderGlql(glqlEls); renderImageLightbox(imageEls, element); } diff --git a/app/assets/javascripts/glql/index.js b/app/assets/javascripts/glql/index.js index cecc13f0252a1b..4daffdea496ec2 100644 --- a/app/assets/javascripts/glql/index.js +++ b/app/assets/javascripts/glql/index.js @@ -4,9 +4,12 @@ import Facade from './components/common/facade.vue'; const renderGlqlNode = (el) => { const container = document.createElement('div'); - const pre = el.querySelector('pre'); + const pre = el.closest('pre'); - el.parentNode.replaceChild(container, el); + // When wrapped in a js-markdown-code block we want to hide the copy-code button + const wrapper = pre.closest('.js-markdown-code') || pre; + + wrapper.parentNode.replaceChild(container, wrapper); return new Vue({ el: container, diff --git a/spec/frontend/behaviors/markdown/render_gfm_spec.js b/spec/frontend/behaviors/markdown/render_gfm_spec.js index e9c75cff1e7892..8208e2cde45079 100644 --- a/spec/frontend/behaviors/markdown/render_gfm_spec.js +++ b/spec/frontend/behaviors/markdown/render_gfm_spec.js @@ -28,14 +28,18 @@ describe('renderGFM', () => { beforeEach(() => { element = document.createElement('div'); - element.innerHTML = - '
labels = any
'; }); - it('calls renderGlql', () => { + it.each` + description | innerHTML | selector + ${'with data-canonical-lang data attr'} | ${'
labels = any
'} | ${'[data-canonical-lang="glql"]'} + ${'with language class on code tag'} | ${'
labels = any
'} | ${'.language-glql'} + `('calls renderGlql $description', ({ innerHTML, selector }) => { + element.innerHTML = innerHTML; + renderGFM(element); - expect(renderGlql).toHaveBeenCalledWith([element.firstElementChild]); + expect(renderGlql).toHaveBeenCalledWith([element.querySelector(selector)]); }); }); diff --git a/spec/frontend/glql/index_spec.js b/spec/frontend/glql/index_spec.js index f94c47cf5e8e4c..502cf08441e8b1 100644 --- a/spec/frontend/glql/index_spec.js +++ b/spec/frontend/glql/index_spec.js @@ -7,17 +7,26 @@ jest.mock('~/glql/core/parser'); describe('renderGlqlNodes', () => { stubCrypto(); - it('loops over all glql code blocks and renders them', async () => { - const container = document.createElement('div'); + let container; + + beforeEach(async () => { + container = document.createElement('div'); container.innerHTML = ` -
assignee = currentUser()
-
label = "bug"
+
assignee = currentUser()button
+
label = "bug"button
+
labels = any
`; - await renderGlqlNodes( - [...container.querySelectorAll('[data-canonical-lang="glql"]')].map((el) => el.parentNode), - ); + await renderGlqlNodes([ + ...container.querySelectorAll('[data-canonical-lang="glql"], .language-glql'), + ]); + }); + + it('loops over all glql code blocks and renders them', () => { + expect(container.querySelectorAll('[data-testid="glql-facade"]')).toHaveLength(3); + }); - expect(container.querySelectorAll('[data-testid="glql-facade"]')).toHaveLength(2); + it('does not render the copy-code button', () => { + expect(container.querySelector('copy-code')).toBeNull(); }); }); -- GitLab From ae06800ce912a6f7719d07c2223b72fa933e95c5 Mon Sep 17 00:00:00 2001 From: Jiaan Louw Date: Wed, 22 Oct 2025 17:01:36 +0200 Subject: [PATCH 2/3] Don't render glql nodes without a parent element --- app/assets/javascripts/glql/index.js | 2 ++ spec/frontend/glql/index_spec.js | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/app/assets/javascripts/glql/index.js b/app/assets/javascripts/glql/index.js index 4daffdea496ec2..47b9a9b8d92b66 100644 --- a/app/assets/javascripts/glql/index.js +++ b/app/assets/javascripts/glql/index.js @@ -9,6 +9,8 @@ const renderGlqlNode = (el) => { // When wrapped in a js-markdown-code block we want to hide the copy-code button const wrapper = pre.closest('.js-markdown-code') || pre; + if (!wrapper.parentNode) return null; + wrapper.parentNode.replaceChild(container, wrapper); return new Vue({ diff --git a/spec/frontend/glql/index_spec.js b/spec/frontend/glql/index_spec.js index 502cf08441e8b1..b07fe304f3a162 100644 --- a/spec/frontend/glql/index_spec.js +++ b/spec/frontend/glql/index_spec.js @@ -29,4 +29,14 @@ describe('renderGlqlNodes', () => { it('does not render the copy-code button', () => { expect(container.querySelector('copy-code')).toBeNull(); }); + + it('does not render glql nodes without a parent element', async () => { + const orphanedPre = document.createElement('pre'); + orphanedPre.dataset.canonicalLang = 'glql'; + orphanedPre.innerHTML = 'assignee = currentUser()'; + + await renderGlqlNodes([orphanedPre]); + + expect(orphanedPre.querySelector('[data-testid="glql-facade"]')).toBeNull(); + }); }); -- GitLab From e2f811f63c956d8ce8a0deb07ab6ec0e233331e4 Mon Sep 17 00:00:00 2001 From: Jiaan Louw Date: Thu, 23 Oct 2025 14:56:46 +0200 Subject: [PATCH 3/3] Update dependency @gitlab/duo-ui to ^12.4.3 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b4512f75e4673e..0bacf9d3869505 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@gitlab/application-sdk-browser": "^0.3.4", "@gitlab/at.js": "1.5.7", "@gitlab/cluster-client": "^3.0.0", - "@gitlab/duo-ui": "^12.3.3", + "@gitlab/duo-ui": "^12.4.3", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.3.1", "@gitlab/query-language-rust": "0.20.9", diff --git a/yarn.lock b/yarn.lock index bed5625878d012..8d9e93fef99282 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1389,10 +1389,10 @@ core-js "^3.29.1" mitt "^3.0.1" -"@gitlab/duo-ui@^12.3.3": - version "12.4.1" - resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-12.4.1.tgz#4dfa64d86bc4fcfcd2e734001cc5b255c69c08c2" - integrity sha512-MRZDzLN4oa6FAoYvQ6zGHwk4cYjvvuZaBJqqR0U9yyLDvSxRTUowRvToHm66HuMxxKW8+7u8Ug5fDp1H75O8lQ== +"@gitlab/duo-ui@^12.4.3": + version "12.4.3" + resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-12.4.3.tgz#e44c0a4893736b049aafbe72c9356ae543280262" + integrity sha512-AVsgY7Zz0jncKtZtkVxVcflhnfPTF4T3TH2/TFO6/UGLlkfBxVpx/EwDiYVbe9n8kvQsam25j0El3PZ5wwQBWg== dependencies: "@floating-ui/dom" "1.7.4" diff "^8.0.2" -- GitLab