From 14ed4571e1d1676e3b45e144af3cfbb496ea784d Mon Sep 17 00:00:00 2001 From: Norman Niati Date: Wed, 17 Jun 2026 18:26:25 +0200 Subject: [PATCH] feat(ui): host global dialogs moved here, mounted via shared store flags Add AboutView (route /about), BugReportDialog, LoginPromptDialog and AgreementDialog. The route-less dialogs are mounted persistently in the host shell via registerHeaderItem in install() and self-bind to shared store flags (app.bugDialogOpen / auth.authPromptOpen). SystemInfoView's build card and AboutView open the bug dialog through the flag. Adds the moved about.*/bugReport.*/agreement.* i18n keys (en+fr) and brand.png. Refs #121, #33. --- ui/src/__tests__/bug-report-dialog.test.js | 121 +++++++++++ ui/src/assets/brand.png | Bin 0 -> 884 bytes ui/src/components/AgreementDialog.vue | 38 ++++ ui/src/components/BugReportDialog.vue | 228 +++++++++++++++++++ ui/src/components/LoginPromptDialog.vue | 149 +++++++++++++ ui/src/i18n/en.js | 40 ++++ ui/src/i18n/fr.js | 40 ++++ ui/src/index.js | 18 +- ui/src/views/AboutView.vue | 242 +++++++++++++++++++++ ui/src/views/SystemInfoView.vue | 7 +- 10 files changed, 877 insertions(+), 6 deletions(-) create mode 100644 ui/src/__tests__/bug-report-dialog.test.js create mode 100644 ui/src/assets/brand.png create mode 100644 ui/src/components/AgreementDialog.vue create mode 100644 ui/src/components/BugReportDialog.vue create mode 100644 ui/src/components/LoginPromptDialog.vue create mode 100644 ui/src/views/AboutView.vue diff --git a/ui/src/__tests__/bug-report-dialog.test.js b/ui/src/__tests__/bug-report-dialog.test.js new file mode 100644 index 0000000..1b305ed --- /dev/null +++ b/ui/src/__tests__/bug-report-dialog.test.js @@ -0,0 +1,121 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import { nextTick } from 'vue' +import { useAuthStore, useAppStore, useI18nStore } from '@ligoj/host' +import BugReportDialog from '../components/BugReportDialog.vue' +import enMessages from '../i18n/en.js' + +// Moved from the host (#121): the dialog now self-binds to app.bugDialogOpen +// instead of a modelValue prop, and its bugReport.* labels live in plugin-ui's +// i18n bundle (merged into the shared store here so `t()` resolves to English). +// Vuetify is externalized in the plugin build, so we stub v-dialog (render its +// slot inline) and v-icon rather than installing the real plugin. +const stubs = { + 'v-dialog': { template: '
' }, + 'v-icon': { template: '' }, +} + +let wrapper + +async function openDialog() { + useAppStore().openBugDialog() + wrapper = mount(BugReportDialog, { global: { stubs } }) + await nextTick() + await nextTick() + return wrapper +} + +const template = () => wrapper.find('.bug-template').element.value + +function seedSession(applicationSettings) { + const auth = useAuthStore() + auth.session = { + userName: 'tester', + roles: ['USER'], + uiAuthorizations: ['.*'], + apiAuthorizations: [], + applicationSettings: applicationSettings ?? { + buildVersion: '4.0.2-test', + plugins: ['service:id:ldap', 'service:prov:aws'], + }, + } +} + +describe('', () => { + beforeEach(() => { + setActivePinia(createPinia()) + useI18nStore().merge(enMessages, 'en') + window.location.hash = '#/about' + }) + + it('builds a template containing the version, the URL and the plugins', async () => { + seedSession() + await openDialog() + + const text = template() + expect(text).toContain('4.0.2-test') + expect(text).toContain('#/about') + expect(text).toContain('service:id:ldap') + expect(text).toContain('service:prov:aws') + expect(text).toContain('## Description') + expect(text).toContain('## Context') + }) + + it('uses the path when there is no hash, and never leaks a domain', async () => { + seedSession() + window.location.hash = '' + await openDialog() + + const text = template() + // jsdom default pathname is "/"; no protocol/host should appear. + expect(text).not.toContain('http') + expect(text).not.toContain('localhost') + }) + + it('copies the template to the clipboard via the Clipboard API', async () => { + seedSession() + const writeText = vi.fn().mockResolvedValue() + Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }) + await openDialog() + + await wrapper.find('.bug-btn.primary').trigger('click') + + expect(writeText).toHaveBeenCalledTimes(1) + const copied = writeText.mock.calls[0][0] + expect(copied).toContain('4.0.2-test') + expect(copied).toContain('service:id:ldap') + delete navigator.clipboard + }) + + it('falls back gracefully when the Clipboard API is unavailable', async () => { + seedSession() + delete navigator.clipboard + document.execCommand = vi.fn().mockReturnValue(true) + await openDialog() + + // Should not throw, and should fall back to execCommand('copy'). + await wrapper.find('.bug-btn.primary').trigger('click') + expect(document.execCommand).toHaveBeenCalledWith('copy') + }) + + it('links to the Ligoj GitHub issue form in a new tab', async () => { + seedSession() + await openDialog() + + const link = wrapper.find('.bug-foot a.bug-btn') + expect(link.attributes('href')).toContain('https://github.com/ligoj/ligoj/issues/new') + expect(link.attributes('target')).toBe('_blank') + expect(link.attributes('rel')).toContain('noopener') + }) + + it('shows a no-plugin placeholder when none are installed', async () => { + seedSession({ buildVersion: '1.0', plugins: [] }) + await openDialog() + + const text = template() + expect(text).toContain('1.0') + expect(text).toContain('Installed plugins') + expect(text).toContain('(none)') + }) +}) diff --git a/ui/src/assets/brand.png b/ui/src/assets/brand.png new file mode 100644 index 0000000000000000000000000000000000000000..a476a956e796582d048f94614efea3008b05d9c3 GIT binary patch literal 884 zcmV-)1B?8LP)dwCN`@GLQ?+m_zW&RP%JU_7j z>>GGLB%ntRQ{H}drlm`7Z1hnMAi5#A3UEun^#Iod-BZ9l2HifuZG`mp6mBQzw!-c# zpm7+IJ=7u@MbIy<^Hy*9VdrU3woyYZSzssd9XKe`Jl8?6?ldUdS;HzG7sC6QXMqDc z2vz|U!oLKXOWbWO1Zw~S!WUP<5s~Jh>T<9GKry_Zc@8+NieNlI$nc>Nj0VsQzknd= z6dCSSS`Nkn$m6U9UX*Egs!JSxu~pn;vfKb80V3XcKWh%?5^3&ORROY_4G5k_yz@%o zJs6`Zpb-8I(4=1De|=Lqyiov2gf|8tJJr&MU`g-1UpGoipwN-aR^{$ZMPA_f5TY+OscL;XYM;@$Fo;$*p27-%kO)^rno}y_t*HP%jKV%4 zRCwzEiYmc-e_-*y&_jo}5+IMz#YR@-x6M9+R(NXx79l(dv*s7~hIpFbr4QR=iY=;* zr}%qSxztql-1d(;W4(8Y#9gL!#` + + + {{ t('agreement.title') }} + +

{{ t('agreement.text') }}

+ +
+ + + {{ t('common.cancel') }} + + {{ t('agreement.accept') }} + + +
+
+ + + diff --git a/ui/src/components/BugReportDialog.vue b/ui/src/components/BugReportDialog.vue new file mode 100644 index 0000000..e4fbecc --- /dev/null +++ b/ui/src/components/BugReportDialog.vue @@ -0,0 +1,228 @@ + +