From 83176f91192e87b311319aa69e7d78909bedb8af Mon Sep 17 00:00:00 2001 From: Alban Mouton Date: Tue, 31 Mar 2026 15:02:55 +0200 Subject: [PATCH 1/6] feat: add notifications-queue component --- lib-vuetify/.gitignore | 3 + lib-vuetify/DfNotificationQueue.vue | 133 ++++++++++++++++ lib-vuetify/env.d.ts | 5 + lib-vuetify/index.ts | 1 + lib-vuetify/package.json | 27 ++++ lib-vuetify/tsconfig.json | 16 ++ package-lock.json | 188 +++++++++++++++-------- package.json | 5 +- tests/dev-notification-queue.e2e.spec.ts | 39 +++++ ui/components.d.ts | 1 - ui/eslint.config.mjs | 3 +- ui/package.json | 3 +- ui/src/main.ts | 9 +- ui/src/pages/dev.vue | 3 + 14 files changed, 358 insertions(+), 78 deletions(-) create mode 100644 lib-vuetify/.gitignore create mode 100644 lib-vuetify/DfNotificationQueue.vue create mode 100644 lib-vuetify/env.d.ts create mode 100644 lib-vuetify/index.ts create mode 100644 lib-vuetify/package.json create mode 100644 lib-vuetify/tsconfig.json create mode 100644 tests/dev-notification-queue.e2e.spec.ts diff --git a/lib-vuetify/.gitignore b/lib-vuetify/.gitignore new file mode 100644 index 0000000..d785128 --- /dev/null +++ b/lib-vuetify/.gitignore @@ -0,0 +1,3 @@ +*.js +*.d.ts +!env.d.ts diff --git a/lib-vuetify/DfNotificationQueue.vue b/lib-vuetify/DfNotificationQueue.vue new file mode 100644 index 0000000..ca69990 --- /dev/null +++ b/lib-vuetify/DfNotificationQueue.vue @@ -0,0 +1,133 @@ + + + + + + en: + loginRequired: + part1: You must + part2: log in + part3: to receive notifications. + noNotifications: You have not received any notifications yet. + openNotificationList: Open notification list + + fr: + loginRequired: + part1: Vous devez vous + part2: connecter + part3: pour recevoir des notifications. + noNotifications: Vous n'avez pas encore reçu de notification. + openNotificationList: Ouvrir la liste des notifications + diff --git a/lib-vuetify/env.d.ts b/lib-vuetify/env.d.ts new file mode 100644 index 0000000..2b97bd9 --- /dev/null +++ b/lib-vuetify/env.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/lib-vuetify/index.ts b/lib-vuetify/index.ts new file mode 100644 index 0000000..3c4bafd --- /dev/null +++ b/lib-vuetify/index.ts @@ -0,0 +1 @@ +export { default as DfNotificationQueue } from './DfNotificationQueue.vue' diff --git a/lib-vuetify/package.json b/lib-vuetify/package.json new file mode 100644 index 0000000..6936846 --- /dev/null +++ b/lib-vuetify/package.json @@ -0,0 +1,27 @@ +{ + "name": "@data-fair/lib-vuetify-events", + "version": "0.1.0", + "description": "Vuetify components for embedding events features.", + "main": "index.js", + "type": "module", + "files": [ + "**/*.js", + "**/*.d.ts", + "**/*.vue" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "tsc" + }, + "license": "MIT", + "peerDependencies": { + "vue": "^3.5.0", + "vuetify": "^4.0.0", + "@data-fair/lib-vuetify": "^2.0.0", + "@data-fair/lib-vue": "^1.26.0" + }, + "dependencies": { + "@mdi/js": "^7.4.47", + "ofetch": "^1.4.0" + } +} diff --git a/lib-vuetify/tsconfig.json b/lib-vuetify/tsconfig.json new file mode 100644 index 0000000..1dcc5e1 --- /dev/null +++ b/lib-vuetify/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": ".", + "rootDir": ".", + "lib": ["ESNext", "DOM"] + }, + "include": ["*.ts"], + "exclude": ["node_modules"] +} diff --git a/package-lock.json b/package-lock.json index 2846674..f05e25b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "MIT", "workspaces": [ "ui", - "api" + "api", + "lib-vuetify" ], "dependencies": { "@data-fair/lib-types-builder": "^1.11.6" @@ -26,7 +27,7 @@ "dotenv-cli": "^11.0.0", "eslint": "^9.39.4", "eslint-plugin-vue": "^9.33.0", - "eslint-plugin-vuetify": "github:albanm/eslint-plugin-vuetify", + "eslint-plugin-vuetify": "^2.5.1", "husky": "^9.1.7", "json-schema-to-typescript": "^11.0.5", "neostandard": "^0.12.2", @@ -90,6 +91,21 @@ "node": "^18 || >=20" } }, + "lib-vuetify": { + "name": "@data-fair/lib-vuetify-events", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@mdi/js": "^7.4.47", + "ofetch": "^1.4.0" + }, + "peerDependencies": { + "@data-fair/lib-vue": "^1.26.0", + "@data-fair/lib-vuetify": "^2.0.0", + "vue": "^3.5.0", + "vuetify": "^4.0.0" + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "11.9.3", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", @@ -708,6 +724,28 @@ } } }, + "node_modules/@data-fair/lib-vuetify": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@data-fair/lib-vuetify/-/lib-vuetify-2.0.5.tgz", + "integrity": "sha512-qRRJLH3riP5xur7mpms2yT7n8qtWv2uxQIeitYgznu/8mZ7xotD0Ka0ntaV7FGdjlEXUI/3obkNT9J4uDQioHA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@data-fair/lib-common-types": "^1.10.4", + "@mdi/js": "^7.4.47", + "@vueuse/core": "^14.0.0" + }, + "peerDependencies": { + "@data-fair/lib-vue": "^1.15.0", + "ofetch": "1", + "vue-i18n": "10 || 11", + "vuetify": "4" + } + }, + "node_modules/@data-fair/lib-vuetify-events": { + "resolved": "lib-vuetify", + "link": true + }, "node_modules/@emnapi/core": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", @@ -2421,19 +2459,6 @@ "node": ">=v12.0.0" } }, - "node_modules/@koumoul/v-iframe": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/@koumoul/v-iframe/-/v-iframe-2.4.5.tgz", - "integrity": "sha512-Y7btXvMP96vs6H5lfAZ6SOElXp+kAHE7G2vu0z0q6WrL33gaIIi0LdhUkA1K2x3XVo7RitCjwtPlaWqqijjTEw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.2" - }, - "peerDependencies": { - "iframe-resizer": "4", - "vuetify": "3" - } - }, "node_modules/@mdi/js": { "version": "7.4.47", "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", @@ -3630,6 +3655,7 @@ "integrity": "sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/utils": "^8.13.0", "eslint-visitor-keys": "^4.2.0", @@ -4815,6 +4841,7 @@ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.1", @@ -6025,8 +6052,7 @@ "version": "1.11.20", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/de-indent": { "version": "1.0.2", @@ -7139,17 +7165,89 @@ } }, "node_modules/eslint-plugin-vuetify": { - "version": "2.4.0", - "resolved": "git+ssh://git@github.com/albanm/eslint-plugin-vuetify.git#e25c604300a03f8b852593c71a5ce9f4d25c2acd", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-vuetify/-/eslint-plugin-vuetify-2.7.2.tgz", + "integrity": "sha512-c8uKnb4DFJFBaqGTKT9Yv6SDhe+cVAQpRpey7nRlpXgXfHpMEe8u6yg5DEhn+Diam0bY05nEe3+T/t/d+cEinQ==", "dev": true, "license": "MIT", "dependencies": { - "eslint-plugin-vue": "^9.6.0", + "eslint-plugin-vue": "^10.8.0", "requireindex": "^1.2.0" }, "peerDependencies": { - "eslint": "^9.0.0", - "vuetify": "^3.0.0" + "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0", + "vuetify": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/eslint-plugin-vuetify/node_modules/eslint-plugin-vue": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz", + "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^7.1.0", + "semver": "^7.6.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.0.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vuetify/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-vuetify/node_modules/vue-eslint-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-scope": { @@ -8664,21 +8762,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/iframe-resizer": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.4.5.tgz", - "integrity": "sha512-U8bCywf/Gh07O69RXo6dXAzTtODQrxaHGHRI7Nt4ipXsuq6EMxVsOP/jjaP43YtXz/ibESS0uSVDN3sOGCzSmw==", - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8.0" - }, - "funding": { - "type": "individual", - "url": "https://iframe-resizer.com//pricing" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -12727,8 +12810,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz", "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -15318,11 +15400,10 @@ } }, "node_modules/vuetify": { - "version": "3.12.3", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.12.3.tgz", - "integrity": "sha512-7QzgftMu8OYKRz/jr2yntPEJ7WFmAzMn8jyeUcW7gz539MjQbDF3UrqZXW3aWi458UVJW9WWvzQn9x5B75Aijw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.0.4.tgz", + "integrity": "sha512-sO2ux9RG0C1HKaP1HqDMro3+vbbmUJwzcKXuzaxQmUERAT/0FR0yfbwnj4PrMwWy1qc2WPJq01h4cr86FmNrFA==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/johnleider" @@ -15756,14 +15837,13 @@ "@data-fair/frame": "^0.17.7", "@data-fair/lib-vue": "^1.26.0", "@data-fair/lib-vuetify": "^2.0.0", + "@data-fair/lib-vuetify-events": "*", "@intlify/unplugin-vue-i18n": "^11.0.7", - "@koumoul/v-iframe": "^2.4.5", "@mdi/js": "^7.4.47", "@types/config": "^3.3.5", "@types/debug": "^4.1.12", "@vitejs/plugin-vue": "^6.0.5", "debug": "^4.4.3", - "iframe-resizer": "^4.4.5", "ofetch": "^1.5.1", "reconnecting-websocket": "^4.4.0", "sass-embedded": "^1.97.3", @@ -15779,23 +15859,6 @@ "vuetify": "^4.0.2" } }, - "ui/node_modules/@data-fair/lib-vuetify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@data-fair/lib-vuetify/-/lib-vuetify-2.0.0.tgz", - "integrity": "sha512-xfI15er5F/8FN+//iKHc8ZsaQmuI8XrpF1MH3FqBGOoPxSzjzNWW7HvGL4NvY5SHmTI5mrvCVcyCIteW+Ljgbw==", - "license": "MIT", - "dependencies": { - "@data-fair/lib-common-types": "^1.10.4", - "@mdi/js": "^7.4.47", - "@vueuse/core": "^14.0.0" - }, - "peerDependencies": { - "@data-fair/lib-vue": "^1.15.0", - "ofetch": "1", - "vue-i18n": "10 || 11", - "vuetify": "4" - } - }, "ui/node_modules/@vue/devtools-api": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.0.tgz", @@ -15975,7 +16038,6 @@ "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.0.2.tgz", "integrity": "sha512-klgSGmfXoLajdTuuxreilzDQjp0ojzL2U5v6Z3ZbMYtpihPPXT9rkd/FxWL3dIGevnWdgaP2Kpwoz6aS/MISDA==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/johnleider" diff --git a/package.json b/package.json index 06180f7..2c1c2d4 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "license": "MIT", "workspaces": [ "ui", - "api" + "api", + "lib-vuetify" ], "bugs": { "url": "https://github.com/data-fair/events/issues" @@ -47,7 +48,7 @@ "dotenv-cli": "^11.0.0", "eslint": "^9.39.4", "eslint-plugin-vue": "^9.33.0", - "eslint-plugin-vuetify": "github:albanm/eslint-plugin-vuetify", + "eslint-plugin-vuetify": "^2.5.1", "husky": "^9.1.7", "json-schema-to-typescript": "^11.0.5", "neostandard": "^0.12.2", diff --git a/tests/dev-notification-queue.e2e.spec.ts b/tests/dev-notification-queue.e2e.spec.ts new file mode 100644 index 0000000..f27d2de --- /dev/null +++ b/tests/dev-notification-queue.e2e.spec.ts @@ -0,0 +1,39 @@ +import { expect } from '@playwright/test' +import { test } from './fixtures/login.ts' +import { axiosAuth, clean, devBaseURL, axios } from './support/axios.ts' + +const axPush = axios({ headers: { 'x-secret-key': 'SECRET_EVENTS' }, baseURL: devBaseURL }) + +test.describe('DfNotificationQueue on dev page', () => { + test.beforeEach(clean) + + test('bell button is visible on dev page', async ({ page, goToWithAuth }) => { + await goToWithAuth('/events/dev', 'test-user1') + await expect(page.getByRole('button', { name: 'Open notification list' })).toBeVisible() + }) + + test('shows notification in dropdown after event is sent with subscription', async ({ page, goToWithAuth }) => { + // create a subscription for test-user1 + const user1 = await axiosAuth('test-user1') + await user1.post('/api/subscriptions', { + topic: { key: 'topic1' }, + sender: { type: 'user', id: 'test-user1', name: 'User 1' }, + outputs: ['devices'] + }) + + // send an event matching the subscription + await axPush.post('/api/events', [{ + date: new Date().toISOString(), + topic: { key: 'topic1' }, + title: 'Dev page notification title', + sender: { type: 'user', id: 'test-user1', name: 'User 1' } + }]) + + // wait for notification processing + await new Promise(resolve => setTimeout(resolve, 2000)) + + await goToWithAuth('/events/dev', 'test-user1') + await page.getByRole('button', { name: 'Open notification list' }).click() + await expect(page.getByText('Dev page notification title')).toBeVisible() + }) +}) diff --git a/ui/components.d.ts b/ui/components.d.ts index 22596cf..7b72686 100644 --- a/ui/components.d.ts +++ b/ui/components.d.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ // @ts-nocheck // biome-ignore lint: disable // oxlint-disable diff --git a/ui/eslint.config.mjs b/ui/eslint.config.mjs index 04edcab..0093e32 100644 --- a/ui/eslint.config.mjs +++ b/ui/eslint.config.mjs @@ -1,9 +1,8 @@ import neostandard from 'neostandard' import pluginVue from 'eslint-plugin-vue' import dfLibRecommended from '@data-fair/lib-utils/eslint/recommended.js' -// cf https://github.com/vuetifyjs/eslint-plugin-vuetify/pull/98 // @ts-ignore -import vuetify from 'eslint-plugin-vuetify/src/index.js' +import vuetify from 'eslint-plugin-vuetify' export default [ ...dfLibRecommended, diff --git a/ui/package.json b/ui/package.json index 6332c93..b6a7dd1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,16 +13,15 @@ }, "dependencies": { "@data-fair/frame": "^0.17.7", + "@data-fair/lib-vuetify-events": "*", "@data-fair/lib-vue": "^1.26.0", "@data-fair/lib-vuetify": "^2.0.0", "@intlify/unplugin-vue-i18n": "^11.0.7", - "@koumoul/v-iframe": "^2.4.5", "@mdi/js": "^7.4.47", "@types/config": "^3.3.5", "@types/debug": "^4.1.12", "@vitejs/plugin-vue": "^6.0.5", "debug": "^4.4.3", - "iframe-resizer": "^4.4.5", "ofetch": "^1.5.1", "reconnecting-websocket": "^4.4.0", "sass-embedded": "^1.97.3", diff --git a/ui/src/main.ts b/ui/src/main.ts index 32180e7..9e32b91 100644 --- a/ui/src/main.ts +++ b/ui/src/main.ts @@ -11,13 +11,8 @@ import { createSession } from '@data-fair/lib-vue/session.js' import { createUiNotif } from '@data-fair/lib-vue/ui-notif.js' import { createI18n } from 'vue-i18n' import App from './App.vue' -// TODO: remove v-iframe and iframe-resizer when d-frame is fully integrated -import '@koumoul/v-iframe/content-window' -import 'iframe-resizer/js/iframeResizer.contentWindow.js' import dFrameContent from '@data-fair/frame/lib/vue-router/d-frame-content.js' -(window as any).iFrameResizer = { heightCalculationMethod: 'taggedElement' }; - (async function () { const router = createRouter({ history: createWebHistory($sitePath + '/events/'), routes }) dFrameContent(router) @@ -29,9 +24,7 @@ import dFrameContent from '@data-fair/frame/lib/vue-router/d-frame-content.js' ...vuetifySessionOptions(session, $cspNonce), icons: { defaultSet: 'mdi', aliases, sets: { mdi, } } }) - const i18n = createI18n({ locale: session.state.lang }); - - (window as any).vIframeOptions = { router } + const i18n = createI18n({ locale: session.state.lang }) createApp(App) .use(router) diff --git a/ui/src/pages/dev.vue b/ui/src/pages/dev.vue index 0097a33..47390f5 100644 --- a/ui/src/pages/dev.vue +++ b/ui/src/pages/dev.vue @@ -2,6 +2,7 @@
+ @@ -23,6 +24,8 @@