diff --git a/.github/workflows/extralit-frontend.yml b/.github/workflows/extralit-frontend.yml index 63c41ba51..e327f6ebb 100644 --- a/.github/workflows/extralit-frontend.yml +++ b/.github/workflows/extralit-frontend.yml @@ -86,17 +86,20 @@ jobs: - name: Build package 📦 env: - # BASE_URL is used in the server to support parameterizable base root path - BASE_URL: "@@baseUrl@@" - DIST_FOLDER: ./dist + # Nuxt 4 SPA (ssr:false) ships as static files served at the site root, so build at + # base "/". (The Nuxt 2 "@@baseUrl@@" placeholder breaks Nuxt 4's prerender crawler; + # parameterizable sub-path hosting is a separate follow-up.) + BASE_URL: "/" run: | - npm run build + # `nuxi generate` prerenders the SPA shell to .output/public (index.html + _nuxt + # assets); `nuxi build` only emits a Nitro server with no static index.html. + npm run generate - name: Upload frontend statics as artifact uses: actions/upload-artifact@v4 with: name: extralit-frontend - path: extralit-frontend/dist + path: extralit-frontend/.output/public # build_dev_docker_image: # name: Build development extralit-frontend docker image diff --git a/.github/workflows/extralit-pr-preview.yml b/.github/workflows/extralit-pr-preview.yml index 13c4e3194..10cbeda83 100644 --- a/.github/workflows/extralit-pr-preview.yml +++ b/.github/workflows/extralit-pr-preview.yml @@ -75,15 +75,24 @@ jobs: - name: Install frontend dependencies working-directory: extralit-frontend env: - BASE_URL: "@@baseUrl@@" - DIST_FOLDER: ./dist + # Nuxt 4 SPA (ssr:false) is served as static files from the server's static dir at + # the site root, so build at base "/". (The Nuxt 2 "@@baseUrl@@" placeholder, which + # the server rewrote at runtime, breaks Nuxt 4's prerender crawler and yields "//_nuxt" + # asset paths; parameterizable sub-path hosting is a separate follow-up.) + BASE_URL: "/" run: | npm install - npm run build + # `nuxi generate` prerenders the SPA shell to .output/public (index.html + _nuxt + # assets); `nuxi build` only emits a Nitro server with no static index.html. + npm run generate - name: Build package run: | - cp -r ../extralit-frontend/dist src/extralit_server/static + set -euo pipefail + # Bake the compiled SPA into the wheel's static dir. Fail loudly if it's missing or + # empty, otherwise the server ships with no statics and 404s every route. + cp -r ../extralit-frontend/.output/public src/extralit_server/static + test -f src/extralit_server/static/index.html uv build - name: Upload artifact diff --git a/.github/workflows/extralit-server.yml b/.github/workflows/extralit-server.yml index 1c1552807..68fb4a816 100644 --- a/.github/workflows/extralit-server.yml +++ b/.github/workflows/extralit-server.yml @@ -141,15 +141,24 @@ jobs: - name: Install frontend dependencies working-directory: extralit-frontend env: - BASE_URL: "@@baseUrl@@" - DIST_FOLDER: ./dist + # Nuxt 4 SPA (ssr:false) is served as static files from the server's static dir at + # the site root, so build at base "/". (The Nuxt 2 "@@baseUrl@@" placeholder, which + # the server rewrote at runtime, breaks Nuxt 4's prerender crawler and yields "//_nuxt" + # asset paths; parameterizable sub-path hosting is a separate follow-up.) + BASE_URL: "/" run: | npm install - npm run build + # `nuxi generate` prerenders the SPA shell to .output/public (index.html + _nuxt + # assets); `nuxi build` only emits a Nitro server with no static index.html. + npm run generate # End of frontend build section - name: Build package run: | - cp -r ../extralit-frontend/dist src/extralit_server/static + set -euo pipefail + # Bake the compiled SPA into the wheel's static dir. Fail loudly if it's missing or + # empty, otherwise the server ships with no statics and 404s every route. + cp -r ../extralit-frontend/.output/public src/extralit_server/static + test -f src/extralit_server/static/index.html uv build - name: Upload artifact diff --git a/.gitignore b/.gitignore index e54a6f72e..4bd584bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -172,4 +172,4 @@ output/ **/.playwright-mcp/ **/.nuxt-stale-root/ -.worktree/ \ No newline at end of file +.worktree/ diff --git a/CLAUDE.md b/CLAUDE.md index 1b5d0acea..336513d57 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Architecture Notes - **extralit-server/**: FastAPI + PostgreSQL + Redis Queue -- **extralit-frontend/**: Vue.js/Nuxt.js (Vuex → Pinia migration) +- **extralit-frontend/**: Vue 3 / Nuxt 4 (Vite); Pinia state management - **extralit/**: Python SDK client - **extralit-hf-space/**: Self-contained HF Spaces deployment bundle (Docker; bundles Elasticsearch + Redis + OCR) — git submodule - **Vector DB**: Elasticsearch/OpenSearch (separate service) diff --git a/extralit-frontend/.eslintrc.js b/extralit-frontend/.eslintrc.js index ac5402b60..b526a5c77 100644 --- a/extralit-frontend/.eslintrc.js +++ b/extralit-frontend/.eslintrc.js @@ -10,14 +10,17 @@ module.exports = { "plugin:@intlify/vue-i18n/recommended", "plugin:prettier/recommended", "plugin:nuxt/recommended", - "prettier/vue", ], + plugins: ["vue"], settings: { "vue-i18n": { - localeDir: "./translation/*.json", + localeDir: "./translation/*.js", }, }, rules: { + // Formatting is advisory here (parity with the *.ts override and the separate + // `npm run format` step); keeps `lint --quiet` focused on real correctness rules. + "prettier/prettier": "warn", "no-console": process.env.NODE_ENV === "production" ? "error" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", "prefer-const": "warn", @@ -48,14 +51,38 @@ module.exports = { }, globals: { $nuxt: true, + vi: true, + // Nuxt 4 auto-imports, hand-declared to satisfy no-undef. This list can drift; + // the real fix is adopting `@nuxt/eslint` (flat config), which auto-generates + // these globals from the build manifest. Tracked as follow-up, not done here. + defineNuxtPlugin: "readonly", + defineNuxtRouteMiddleware: "readonly", + definePageMeta: "readonly", + navigateTo: "readonly", + abortNavigation: "readonly", + useNuxtApp: "readonly", + useRuntimeConfig: "readonly", + useRoute: "readonly", + useRouter: "readonly", + useState: "readonly", + useCookie: "readonly", + useHead: "readonly", + useSeoMeta: "readonly", + useError: "readonly", + createError: "readonly", + clearError: "readonly", + showError: "readonly", }, + parser: "vue-eslint-parser", parserOptions: { - parser: "@babel/eslint-parser", + parser: "@typescript-eslint/parser", + ecmaVersion: 2022, + sourceType: "module", }, overrides: [ { files: ["**/*.ts"], - extends: ["@nuxtjs/eslint-config-typescript", "prettier"], + extends: ["plugin:@typescript-eslint/recommended", "prettier"], parser: "@typescript-eslint/parser", plugins: ["@typescript-eslint", "prettier"], parserOptions: { project: ["./tsconfig.json"] }, diff --git a/extralit-frontend/.gitignore b/extralit-frontend/.gitignore index 75e854d8d..3c1dfe310 100644 --- a/extralit-frontend/.gitignore +++ b/extralit-frontend/.gitignore @@ -2,3 +2,9 @@ node_modules/ /test-results/ /playwright-report/ /playwright/.cache/ +.nuxtrc +.nuxt/ +.output/ +.nitro/ +.cache/ +dist/ diff --git a/extralit-frontend/CLAUDE.md b/extralit-frontend/CLAUDE.md index 81ba50731..555eb0f6e 100644 --- a/extralit-frontend/CLAUDE.md +++ b/extralit-frontend/CLAUDE.md @@ -25,7 +25,7 @@ API_BASE_URL=https://extralit-public-demo.hf.space/ npm run dev ## Testing ```bash -npm run test # Jest unit tests +npm run test # Vitest unit tests (run once) npm run test:watch # Watch mode npm run test:coverage # With coverage @@ -34,35 +34,63 @@ npm run e2e:silent # Playwright headless npm run e2e:report # View test report ``` +> Unit tests run on **Vitest** (`vitest.config.ts` + `test/setup.ts`), using +> `@vue/test-utils` v2 and `@nuxt/test-utils`. Specs needing Nuxt runtime context use +> `// @vitest-environment nuxt` or `mockNuxtImport`. +> +> The Playwright e2e suite is inherited from upstream Argilla. The shared login helper +> (`e2e/common/login-and-wait-for.ts`) has been reconciled to Extralit's real sign-in UI: +> it fills `getByLabel("Username"/"Password")`, submits the `"Sign in"` button, mocks +> `/api/v1/token` + `/api/v1/me` offline, and waits for the home/datasets landing at `/` +> (there is no `/datasets` route). This flow is runtime-verified via the CDP browser. The +> per-page specs still need fresh Extralit screenshot baselines (`--update-snapshots`); the +> inherited ones are Argilla's. The local Playwright browser can't launch on the Orin dev +> host (missing OS libs, no sudo) — run the headless gate in CI. + ## Code Quality ```bash -npm run lint # ESLint check +npm run lint # ESLint check (eslint 8 + vue-eslint-parser) npm run lint:fix # Fix ESLint issues npm run format # Format with Prettier npm run format:check # Check formatting npm run generate-icons # Generate icon components from SVG + +npx nuxi typecheck # vue-tsc type check +npm run build # Production build (vite/nitro) ``` ## Requirements -- Node.js 18+ +- Node.js 18+ (developed on Node 24) - Backend server running for full functionality ## Architecture -**Migration in progress**: Vuex → Pinia - -- **v1/** directory: New Pinia architecture with domain-driven design -- Domain-driven design with entities, use cases, dependency injection +- **v1/** directory: Pinia + domain-driven design (entities, use cases, dependency injection + via `ts-injecty`). The domain/use-case layer is framework-agnostic; only the Vue/Nuxt + adapters (HTTP, Auth, Icons) were swapped during the Vue 3 / Nuxt 4 migration. - Component hierarchy: base (stateless) → features (page-specific) → global (reusable) +- HTTP: plain `axios` in `plugins/2.axios.ts` (replaced `@nuxtjs/axios`), re-injected into DI. +- Auth: `AuthService` (`v1/infrastructure/services/AuthService.ts`) implementing `IAuthService`, + provided as `$auth` by `plugins/1.auth.ts` (replaced `@nuxtjs/auth-next`). +- Icons: custom `` (`components/base/BaseSvgIcon.vue`) reading `static/icons/*.svg` + (replaced `vue-svgicon`). +- Plugins load in order via numeric prefixes (`1.auth` → `2.axios` → `3.di`); middleware are + Nuxt-4 globals (`middleware/*.global.ts`). + +> **TS posture:** `tsconfig.json` keeps `strict:false` (matching the pre-Vue3 config) and +> disables Nuxt-4's new `verbatimModuleSyntax`/`noImplicitOverride`. Tightening to strict is a +> separate hardening effort. Note: Vite/esbuild (`isolatedModules`) requires type-only imports +> to use the inline `import { type X }` modifier or they throw at runtime in dev. ## Key Technologies -- Vue.js + Nuxt.js -- Pinia (state management, replacing Vuex) -- Jest (unit tests) + Playwright (e2e) -- ESLint + Prettier +- Vue 3.5 + Nuxt 4 (Vite + Nitro) +- Pinia (state management; Vuex fully removed) +- Vitest + @vue/test-utils v2 (unit) + Playwright (e2e) +- @nuxtjs/i18n v10 (vue-i18n v11), @vueuse/core, mitt +- ESLint 8 + Prettier ## Structure diff --git a/extralit-frontend/__mocks__/empty.css b/extralit-frontend/__mocks__/empty.css new file mode 100644 index 000000000..874c6aaa5 --- /dev/null +++ b/extralit-frontend/__mocks__/empty.css @@ -0,0 +1 @@ +/* test stub */ diff --git a/extralit-frontend/assets/icon-template.js.tmp b/extralit-frontend/assets/icon-template.js.tmp deleted file mode 100644 index dc575f361..000000000 --- a/extralit-frontend/assets/icon-template.js.tmp +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - '${name}': { - width: ${width}, - height: ${height}, - viewBox: ${viewBox}, - data: '${data}' - } -}) \ No newline at end of file diff --git a/extralit-frontend/assets/icons/arrow-down.js b/extralit-frontend/assets/icons/arrow-down.js deleted file mode 100644 index 94ddb6ff3..000000000 --- a/extralit-frontend/assets/icons/arrow-down.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'arrow-down': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/arrow-up.js b/extralit-frontend/assets/icons/arrow-up.js deleted file mode 100644 index e70d7960e..000000000 --- a/extralit-frontend/assets/icons/arrow-up.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'arrow-up': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/assign.js b/extralit-frontend/assets/icons/assign.js deleted file mode 100644 index a61f876be..000000000 --- a/extralit-frontend/assets/icons/assign.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'assign': { - width: 10, - height: 10, - viewBox: '0 0 10 10', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/bulk-mode.js b/extralit-frontend/assets/icons/bulk-mode.js deleted file mode 100644 index bcf9f4000..000000000 --- a/extralit-frontend/assets/icons/bulk-mode.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'bulk-mode': { - width: 24, - height: 17, - viewBox: '0 0 24 17', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/change-height.js b/extralit-frontend/assets/icons/change-height.js deleted file mode 100644 index 5e8cc4e6d..000000000 --- a/extralit-frontend/assets/icons/change-height.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'change-height': { - width: 30, - height: 18, - viewBox: '0 0 30 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/check.js b/extralit-frontend/assets/icons/check.js deleted file mode 100644 index 5e23d4436..000000000 --- a/extralit-frontend/assets/icons/check.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'check': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/chevron-down.js b/extralit-frontend/assets/icons/chevron-down.js deleted file mode 100644 index fd7aa4ead..000000000 --- a/extralit-frontend/assets/icons/chevron-down.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'chevron-down': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/chevron-left.js b/extralit-frontend/assets/icons/chevron-left.js deleted file mode 100644 index ebb885b48..000000000 --- a/extralit-frontend/assets/icons/chevron-left.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'chevron-left': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/chevron-right.js b/extralit-frontend/assets/icons/chevron-right.js deleted file mode 100644 index 278012bd4..000000000 --- a/extralit-frontend/assets/icons/chevron-right.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'chevron-right': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/chevron-up.js b/extralit-frontend/assets/icons/chevron-up.js deleted file mode 100644 index 1339d039b..000000000 --- a/extralit-frontend/assets/icons/chevron-up.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'chevron-up': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/clear.js b/extralit-frontend/assets/icons/clear.js deleted file mode 100644 index 6e2e5cca9..000000000 --- a/extralit-frontend/assets/icons/clear.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'clear': { - width: 18, - height: 18, - viewBox: '0 0 18 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/close.js b/extralit-frontend/assets/icons/close.js deleted file mode 100644 index 82d60d85c..000000000 --- a/extralit-frontend/assets/icons/close.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'close': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/code.js b/extralit-frontend/assets/icons/code.js deleted file mode 100644 index a0e30a4c0..000000000 --- a/extralit-frontend/assets/icons/code.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'code': { - width: 20, - height: 20, - viewBox: '0 0 20 20', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/copy.js b/extralit-frontend/assets/icons/copy.js deleted file mode 100644 index ff9a3126d..000000000 --- a/extralit-frontend/assets/icons/copy.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'copy': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/danger.js b/extralit-frontend/assets/icons/danger.js deleted file mode 100644 index b7f30396e..000000000 --- a/extralit-frontend/assets/icons/danger.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'danger': { - width: 90, - height: 82, - viewBox: '0 0 90 82', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/dark-theme.js b/extralit-frontend/assets/icons/dark-theme.js deleted file mode 100644 index 4d093d02e..000000000 --- a/extralit-frontend/assets/icons/dark-theme.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'dark-theme': { - width: 18, - height: 18, - viewBox: '0 0 18 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/discard.js b/extralit-frontend/assets/icons/discard.js deleted file mode 100644 index c81db94a0..000000000 --- a/extralit-frontend/assets/icons/discard.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'discard': { - width: 16, - height: 16, - viewBox: '0 0 16 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/document.js b/extralit-frontend/assets/icons/document.js deleted file mode 100644 index 152fb32a5..000000000 --- a/extralit-frontend/assets/icons/document.js +++ /dev/null @@ -1,11 +0,0 @@ -// document.js - -var icon = require("vue-svgicon"); -icon.register({ - document: { - width: 41, - height: 40, - viewBox: "0 0 41 40", - data: '', - }, -}); diff --git a/extralit-frontend/assets/icons/draggable.js b/extralit-frontend/assets/icons/draggable.js deleted file mode 100644 index 9c31c4018..000000000 --- a/extralit-frontend/assets/icons/draggable.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'draggable': { - width: 6, - height: 10, - viewBox: '0 0 6 10', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/expand-arrows.js b/extralit-frontend/assets/icons/expand-arrows.js deleted file mode 100644 index 38b0050b8..000000000 --- a/extralit-frontend/assets/icons/expand-arrows.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'expand-arrows': { - width: 14, - height: 14, - viewBox: '0 0 14 14', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/exploration.js b/extralit-frontend/assets/icons/exploration.js deleted file mode 100644 index 2d6bbdd1a..000000000 --- a/extralit-frontend/assets/icons/exploration.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'exploration': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/export.js b/extralit-frontend/assets/icons/export.js deleted file mode 100644 index 90b5334af..000000000 --- a/extralit-frontend/assets/icons/export.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'export': { - width: 26, - height: 31, - viewBox: '0 0 26 31', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/external-link.js b/extralit-frontend/assets/icons/external-link.js deleted file mode 100644 index b015a907a..000000000 --- a/extralit-frontend/assets/icons/external-link.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'external-link': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/external.js b/extralit-frontend/assets/icons/external.js deleted file mode 100644 index 4dc4bf282..000000000 --- a/extralit-frontend/assets/icons/external.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'external': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/filter.js b/extralit-frontend/assets/icons/filter.js deleted file mode 100644 index e020a5d89..000000000 --- a/extralit-frontend/assets/icons/filter.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'filter': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/focus-mode.js b/extralit-frontend/assets/icons/focus-mode.js deleted file mode 100644 index cdd9bce9e..000000000 --- a/extralit-frontend/assets/icons/focus-mode.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'focus-mode': { - width: 20, - height: 16, - viewBox: '0 0 20 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/hand-labeling.js b/extralit-frontend/assets/icons/hand-labeling.js deleted file mode 100644 index d71207bea..000000000 --- a/extralit-frontend/assets/icons/hand-labeling.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'hand-labeling': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/high-contrast-theme.js b/extralit-frontend/assets/icons/high-contrast-theme.js deleted file mode 100644 index 775f40856..000000000 --- a/extralit-frontend/assets/icons/high-contrast-theme.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'high-contrast-theme': { - width: 16, - height: 16, - viewBox: '0 0 16 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/import.js b/extralit-frontend/assets/icons/import.js deleted file mode 100644 index a78d0e113..000000000 --- a/extralit-frontend/assets/icons/import.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'import': { - width: 29, - height: 31, - viewBox: '0 0 29 31', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/index.js b/extralit-frontend/assets/icons/index.js deleted file mode 100644 index 36e92d023..000000000 --- a/extralit-frontend/assets/icons/index.js +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable */ -require('./arrow-down') -require('./arrow-up') -require('./assign') -require('./bulk-mode') -require('./change-height') -require('./check') -require('./chevron-down') -require('./chevron-left') -require('./chevron-right') -require('./chevron-up') -require('./clear') -require('./close') -require('./code') -require('./copy') -require('./danger') -require('./dark-theme') -require('./discard') -require('./draggable') -require('./expand-arrows') -require('./exploration') -require('./export') -require('./external-link') -require('./external') -require('./filter') -require('./focus-mode') -require('./hand-labeling') -require('./high-contrast-theme') -require('./import') -require('./info') -require('./kebab') -require('./light-theme') -require('./link') -require('./log-out') -require('./matching') -require('./math-plus') -require('./meatballs') -require('./minimize-arrows') -require('./no-matching') -require('./pen') -require('./plus') -require('./progress') -require('./question-answering') -require('./records') -require('./refresh') -require('./reset') -require('./row-last') -require('./rows') -require('./search') -require('./settings') -require('./shortcuts') -require('./similarity') -require('./smile-sad') -require('./sort') -require('./stats') -require('./suggestion') -require('./support') -require('./system-theme') -require('./text-classification') -require('./text-to-image') -require('./time') -require('./trash-empty') -require('./unavailable') -require('./update') -require('./validate') -require('./weak-labeling') diff --git a/extralit-frontend/assets/icons/info.js b/extralit-frontend/assets/icons/info.js deleted file mode 100644 index da93b6995..000000000 --- a/extralit-frontend/assets/icons/info.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'info': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/kebab.js b/extralit-frontend/assets/icons/kebab.js deleted file mode 100644 index 09eecd5c2..000000000 --- a/extralit-frontend/assets/icons/kebab.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'kebab': { - width: 20, - height: 21, - viewBox: '0 0 20 21', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/light-theme.js b/extralit-frontend/assets/icons/light-theme.js deleted file mode 100644 index 2451caaae..000000000 --- a/extralit-frontend/assets/icons/light-theme.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'light-theme': { - width: 18, - height: 18, - viewBox: '0 0 18 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/link.js b/extralit-frontend/assets/icons/link.js deleted file mode 100644 index 0ffb4e746..000000000 --- a/extralit-frontend/assets/icons/link.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'link': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/log-out.js b/extralit-frontend/assets/icons/log-out.js deleted file mode 100644 index 6d64d3f71..000000000 --- a/extralit-frontend/assets/icons/log-out.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'log-out': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/matching.js b/extralit-frontend/assets/icons/matching.js deleted file mode 100644 index de2601544..000000000 --- a/extralit-frontend/assets/icons/matching.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'matching': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/math-plus.js b/extralit-frontend/assets/icons/math-plus.js deleted file mode 100644 index 790290ee1..000000000 --- a/extralit-frontend/assets/icons/math-plus.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'math-plus': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/meatballs.js b/extralit-frontend/assets/icons/meatballs.js deleted file mode 100644 index ab7e3e560..000000000 --- a/extralit-frontend/assets/icons/meatballs.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'meatballs': { - width: 16, - height: 16, - viewBox: '0 0 30 8', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/minimize-arrows.js b/extralit-frontend/assets/icons/minimize-arrows.js deleted file mode 100644 index ca7369ea7..000000000 --- a/extralit-frontend/assets/icons/minimize-arrows.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'minimize-arrows': { - width: 16, - height: 16, - viewBox: '0 0 16 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/no-matching.js b/extralit-frontend/assets/icons/no-matching.js deleted file mode 100644 index 23ead8219..000000000 --- a/extralit-frontend/assets/icons/no-matching.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'no-matching': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/pen.js b/extralit-frontend/assets/icons/pen.js deleted file mode 100644 index 8349afb86..000000000 --- a/extralit-frontend/assets/icons/pen.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'pen': { - width: 16, - height: 16, - viewBox: '0 0 16 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/plus.js b/extralit-frontend/assets/icons/plus.js deleted file mode 100644 index ac0c3d344..000000000 --- a/extralit-frontend/assets/icons/plus.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'plus': { - width: 16, - height: 16, - viewBox: '0 0 16 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/progress.js b/extralit-frontend/assets/icons/progress.js deleted file mode 100644 index 281aff533..000000000 --- a/extralit-frontend/assets/icons/progress.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'progress': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/question-answering.js b/extralit-frontend/assets/icons/question-answering.js deleted file mode 100644 index 4dd71cd3f..000000000 --- a/extralit-frontend/assets/icons/question-answering.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'question-answering': { - width: 15, - height: 15, - viewBox: '0 0 15 15', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/records.js b/extralit-frontend/assets/icons/records.js deleted file mode 100644 index fccba7d8d..000000000 --- a/extralit-frontend/assets/icons/records.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'records': { - width: 10, - height: 9, - viewBox: '0 0 10 9', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/refresh.js b/extralit-frontend/assets/icons/refresh.js deleted file mode 100644 index 04e4f2d0c..000000000 --- a/extralit-frontend/assets/icons/refresh.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'refresh': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/reset.js b/extralit-frontend/assets/icons/reset.js deleted file mode 100644 index d5f8de52b..000000000 --- a/extralit-frontend/assets/icons/reset.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'reset': { - width: 16, - height: 18, - viewBox: '0 0 16 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/row-last.js b/extralit-frontend/assets/icons/row-last.js deleted file mode 100644 index a4b83f189..000000000 --- a/extralit-frontend/assets/icons/row-last.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'row-last': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/rows.js b/extralit-frontend/assets/icons/rows.js deleted file mode 100644 index 5284efcb6..000000000 --- a/extralit-frontend/assets/icons/rows.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'rows': { - width: 10, - height: 8, - viewBox: '0 0 10 8', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/search.js b/extralit-frontend/assets/icons/search.js deleted file mode 100644 index c359dfbf8..000000000 --- a/extralit-frontend/assets/icons/search.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'search': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/settings.js b/extralit-frontend/assets/icons/settings.js deleted file mode 100644 index 7bd9d4b61..000000000 --- a/extralit-frontend/assets/icons/settings.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'settings': { - width: 800, - height: 800, - viewBox: '0 0 192 192', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/shortcuts.js b/extralit-frontend/assets/icons/shortcuts.js deleted file mode 100644 index 1f9e21f16..000000000 --- a/extralit-frontend/assets/icons/shortcuts.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'shortcuts': { - width: 16, - height: 16, - viewBox: '0 0 32 32', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/similarity.js b/extralit-frontend/assets/icons/similarity.js deleted file mode 100644 index 8723b42f6..000000000 --- a/extralit-frontend/assets/icons/similarity.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'similarity': { - width: 40, - height: 41, - viewBox: '0 0 40 41', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/smile-sad.js b/extralit-frontend/assets/icons/smile-sad.js deleted file mode 100644 index e91b23d85..000000000 --- a/extralit-frontend/assets/icons/smile-sad.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'smile-sad': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/sort.js b/extralit-frontend/assets/icons/sort.js deleted file mode 100644 index 8aaa33b29..000000000 --- a/extralit-frontend/assets/icons/sort.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'sort': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/stats.js b/extralit-frontend/assets/icons/stats.js deleted file mode 100644 index e3e675e17..000000000 --- a/extralit-frontend/assets/icons/stats.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'stats': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/suggestion.js b/extralit-frontend/assets/icons/suggestion.js deleted file mode 100644 index 6f2cfa1f2..000000000 --- a/extralit-frontend/assets/icons/suggestion.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'suggestion': { - width: 12, - height: 12, - viewBox: '0 0 12 12', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/support.js b/extralit-frontend/assets/icons/support.js deleted file mode 100644 index 697516a9d..000000000 --- a/extralit-frontend/assets/icons/support.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'support': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/system-theme.js b/extralit-frontend/assets/icons/system-theme.js deleted file mode 100644 index 93fcafbe7..000000000 --- a/extralit-frontend/assets/icons/system-theme.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'system-theme': { - width: 18, - height: 18, - viewBox: '0 0 18 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/text-classification.js b/extralit-frontend/assets/icons/text-classification.js deleted file mode 100644 index 0612ded0c..000000000 --- a/extralit-frontend/assets/icons/text-classification.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'text-classification': { - width: 15, - height: 15, - viewBox: '0 0 15 15', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/text-to-image.js b/extralit-frontend/assets/icons/text-to-image.js deleted file mode 100644 index 35554ca79..000000000 --- a/extralit-frontend/assets/icons/text-to-image.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'text-to-image': { - width: 15, - height: 15, - viewBox: '0 0 15 15', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/time.js b/extralit-frontend/assets/icons/time.js deleted file mode 100644 index 4ebc75f85..000000000 --- a/extralit-frontend/assets/icons/time.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'time': { - width: 30, - height: 30, - viewBox: '0 0 30 30', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/trash-empty.js b/extralit-frontend/assets/icons/trash-empty.js deleted file mode 100644 index e0e8203ac..000000000 --- a/extralit-frontend/assets/icons/trash-empty.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'trash-empty': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/unavailable.js b/extralit-frontend/assets/icons/unavailable.js deleted file mode 100644 index 0f1945be3..000000000 --- a/extralit-frontend/assets/icons/unavailable.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'unavailable': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/update.js b/extralit-frontend/assets/icons/update.js deleted file mode 100644 index 9f5087301..000000000 --- a/extralit-frontend/assets/icons/update.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'update': { - width: 12, - height: 13, - viewBox: '0 0 12 13', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/validate.js b/extralit-frontend/assets/icons/validate.js deleted file mode 100644 index 96bbf2b36..000000000 --- a/extralit-frontend/assets/icons/validate.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'validate': { - width: 31, - height: 31, - viewBox: '0 0 31 31', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/weak-labeling.js b/extralit-frontend/assets/icons/weak-labeling.js deleted file mode 100644 index 3ffb8cfbe..000000000 --- a/extralit-frontend/assets/icons/weak-labeling.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'weak-labeling': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/babel.config.js b/extralit-frontend/babel.config.js deleted file mode 100644 index 173e4e28b..000000000 --- a/extralit-frontend/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = { - presets: [["@babel/preset-env", { targets: { node: "current" }, loose: true }]], - plugins: [ - ["@babel/plugin-transform-class-properties", { loose: true }], - ["@babel/plugin-transform-private-methods", { loose: true }], - ["@babel/plugin-transform-private-property-in-object", { loose: true }], - ], - env: { - test: { - presets: [["@babel/preset-env", { targets: { node: "current" }, loose: true }], "@babel/preset-typescript"], - }, - }, -}; diff --git a/extralit-frontend/components/base/BaseSvgIcon.test.ts b/extralit-frontend/components/base/BaseSvgIcon.test.ts new file mode 100644 index 000000000..9b1bd29cd --- /dev/null +++ b/extralit-frontend/components/base/BaseSvgIcon.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { mount } from "@vue/test-utils"; +import BaseSvgIcon from "./BaseSvgIcon.vue"; + +describe("BaseSvgIcon", () => { + it("renders an svg for a known icon name with the name as data attribute", () => { + const wrapper = mount(BaseSvgIcon, { props: { name: "check", width: 16, height: 16 } }); + expect(wrapper.find("svg").exists()).toBe(true); + expect(wrapper.attributes("data-icon")).toBe("check"); + }); + + it("applies the requested width/height to the svg", () => { + const wrapper = mount(BaseSvgIcon, { props: { name: "check", width: 16, height: 16 } }); + const svg = wrapper.find("svg"); + expect(svg.attributes("width")).toBe("16px"); + expect(svg.attributes("height")).toBe("16px"); + }); + + it("recolors monochrome fills when a color is supplied", () => { + const wrapper = mount(BaseSvgIcon, { props: { name: "check", color: "#ff0000" } }); + expect(wrapper.html()).toContain('fill="#ff0000"'); + }); + + it("renders nothing for an unknown icon name", () => { + const wrapper = mount(BaseSvgIcon, { props: { name: "definitely-not-an-icon" } }); + expect(wrapper.find("svg").exists()).toBe(false); + }); +}); diff --git a/extralit-frontend/components/base/BaseSvgIcon.vue b/extralit-frontend/components/base/BaseSvgIcon.vue new file mode 100644 index 000000000..843280a53 --- /dev/null +++ b/extralit-frontend/components/base/BaseSvgIcon.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/extralit-frontend/components/base/base-badge/BaseBadge.spec.js b/extralit-frontend/components/base/base-badge/BaseBadge.spec.js new file mode 100644 index 000000000..04591244d --- /dev/null +++ b/extralit-frontend/components/base/base-badge/BaseBadge.spec.js @@ -0,0 +1,33 @@ +import { mount } from "@vue/test-utils"; +import BaseBadge from "./BaseBadge.vue"; + +const ButtonStub = { template: '' }; + +describe("BaseBadge", () => { + it("is not clickable when no listener is attached", async () => { + const wrapper = mount(BaseBadge, { + props: { text: "hello" }, + global: { stubs: { BaseButton: ButtonStub } }, + }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.clickable).toBe(false); + expect(wrapper.find("p").exists()).toBe(true); + expect(wrapper.find(".--clickable").exists()).toBe(false); + }); + + it("is clickable when onOnClick attr is present (Vue 3 @on-click normalization)", async () => { + // In Vue 3, a parent binding `@on-click="handler"` is compiled into the attrs object + // as `onOnClick` (on + PascalCase). Passing it via `attrs` in the test mirrors exactly + // what the compiled parent template produces. + const wrapper = mount(BaseBadge, { + props: { text: "hello" }, + attrs: { onOnClick: () => {} }, + global: { stubs: { BaseButton: ButtonStub } }, + }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.clickable).toBe(true); + expect(wrapper.find(".--clickable").exists()).toBe(true); + }); +}); diff --git a/extralit-frontend/components/base/base-badge/BaseBadge.vue b/extralit-frontend/components/base/base-badge/BaseBadge.vue index f291da028..0b05a33ed 100644 --- a/extralit-frontend/components/base/base-badge/BaseBadge.vue +++ b/extralit-frontend/components/base/base-badge/BaseBadge.vue @@ -27,7 +27,7 @@ export default { }; }, mounted() { - if (this.$listeners["on-click"]) { + if (this.$attrs.onOnClick) { this.clickable = true; } }, diff --git a/extralit-frontend/components/base/base-banner/BaseBanner.vue b/extralit-frontend/components/base/base-banner/BaseBanner.vue index 000f18067..04315e324 100644 --- a/extralit-frontend/components/base/base-banner/BaseBanner.vue +++ b/extralit-frontend/components/base/base-banner/BaseBanner.vue @@ -12,8 +12,6 @@ @@ -78,20 +49,29 @@ export default { height: 100%; } -.PDFView { - max-height: calc(100vh - $topbarHeight); // Set maximum height to 100% of the viewport height - overflow-y: auto; // Enable vertical scrolling if the content exceeds the maximum height -} +.pdf-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: $base-space; + height: 100%; + padding: $base-space * 2; + + &__title { + font-size: 14px; + font-weight: 600; + margin: 0; + } + + &__message { + color: var(--color-dark-grey); + margin: 0; + } -.document__title { - flex: 1; - max-width: calc($sidebarWidth / 2); - overflow-x: auto; - -webkit-overflow-scrolling: touch; - white-space: nowrap; - font-size: 14px; - font-weight: 600; - margin: 0; - padding-left: 18px; + &__link { + color: var(--color-primary); + text-decoration: underline; + } } diff --git a/extralit-frontend/components/base/base-radio-button/BaseRadioButton.spec.js b/extralit-frontend/components/base/base-radio-button/BaseRadioButton.spec.js index 039116e82..58dcc1369 100644 --- a/extralit-frontend/components/base/base-radio-button/BaseRadioButton.spec.js +++ b/extralit-frontend/components/base/base-radio-button/BaseRadioButton.spec.js @@ -3,11 +3,11 @@ import BaseRadioButton from "./BaseRadioButton"; let wrapper = null; const options = { - propsData: { + props: { id: "id", name: "name", value: "1", - model: "1", + modelValue: "1", }, }; beforeEach(() => { @@ -15,16 +15,16 @@ beforeEach(() => { }); afterEach(() => { - wrapper.destroy(); + wrapper.unmount(); }); describe("BaseRadioButtonComponent", () => { it("render the component", () => { - expect(wrapper.is(BaseRadioButton)).toBe(true); + expect(wrapper.findComponent(BaseRadioButton).exists()).toBe(true); }); it("bind disabled class", async () => { wrapper = shallowMount(BaseRadioButton, { - propsData: { + props: { disabled: true, }, }); @@ -32,7 +32,7 @@ describe("BaseRadioButtonComponent", () => { }); it("component is selected when model and value matched", async () => { expect(wrapper.vm.isSelected).toBe(true); - expect(wrapper.props().model).toBe("1"); + expect(wrapper.props().modelValue).toBe("1"); }); it("input is checked when model and value matched", async () => { const radioInput = wrapper.find('input[type="radio"]'); diff --git a/extralit-frontend/components/base/base-radio-button/BaseRadioButton.vue b/extralit-frontend/components/base/base-radio-button/BaseRadioButton.vue index 897385a0c..8f34d18d1 100644 --- a/extralit-frontend/components/base/base-radio-button/BaseRadioButton.vue +++ b/extralit-frontend/components/base/base-radio-button/BaseRadioButton.vue @@ -20,7 +20,7 @@ export default { name: { type: String, }, - model: { + modelValue: { type: [String, Number, Boolean, Object], }, value: { @@ -32,13 +32,10 @@ export default { default: "var(--fg-status-submitted)", }, }, - model: { - prop: "model", - event: "change", - }, + emits: ["change", "update:modelValue"], computed: { isSelected() { - return isEqual(this.model, this.value); + return isEqual(this.modelValue, this.value); }, radioClasses() { return { @@ -56,6 +53,7 @@ export default { toggleCheck() { if (!this.disabled) { this.$emit("change", this.value); + this.$emit("update:modelValue", this.value); } }, }, diff --git a/extralit-frontend/components/base/base-range/BaseRangeMultipleSlider.vue b/extralit-frontend/components/base/base-range/BaseRangeMultipleSlider.vue index 6ef152be2..a6f7a4ab2 100644 --- a/extralit-frontend/components/base/base-range/BaseRangeMultipleSlider.vue +++ b/extralit-frontend/components/base/base-range/BaseRangeMultipleSlider.vue @@ -59,7 +59,7 @@ export default { type: Number, default: 1, }, - sliderValues: { + modelValue: { type: Array, default: () => [0, 1], }, @@ -68,21 +68,18 @@ export default { default: () => this.max / 100, }, }, - model: { - prop: "sliderValues", - event: "onSliderValuesChanged", - }, + emits: ["update:modelValue"], data() { return { - values: this.sliderValues, + values: this.modelValue, }; }, watch: { - sliderValues() { - this.values = this.sliderValues; + modelValue() { + this.values = this.modelValue; }, values() { - this.$emit("onSliderValuesChanged", this.values); + this.$emit("update:modelValue", this.values); }, sliderFrom(newValue) { if (newValue > this.sliderTo) { diff --git a/extralit-frontend/components/base/base-range/BaseRangeSlider.vue b/extralit-frontend/components/base/base-range/BaseRangeSlider.vue index 0cebcc282..10acacc34 100644 --- a/extralit-frontend/components/base/base-range/BaseRangeSlider.vue +++ b/extralit-frontend/components/base/base-range/BaseRangeSlider.vue @@ -16,6 +16,7 @@ diff --git a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js index 64f2b4429..4af681de9 100644 --- a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js +++ b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js @@ -1,22 +1,23 @@ +// @vitest-environment nuxt import { mount } from "@vue/test-utils"; import DatasetConfiguration from "./DatasetConfiguration.vue"; import { ImportHistoryDetails } from "~/v1/domain/entities/import/ImportHistoryDetails"; // Mock dependencies const mockUseDatasetConfiguration = { - getFirstRecord: jest.fn(), - getSuggestedFieldMappings: jest.fn(() => ({})), - configureImportHistoryFields: jest.fn(), - getSuggestedQuestions: jest.fn(() => []), + getFirstRecord: vi.fn(), + getSuggestedFieldMappings: vi.fn(() => ({})), + configureImportHistoryFields: vi.fn(), + getSuggestedQuestions: vi.fn(() => []), firstRecord: { reference: "paper_001", title: "Test Paper" }, }; -jest.mock("./useDatasetConfiguration", () => ({ - useDatasetConfiguration: jest.fn(() => mockUseDatasetConfiguration), +vi.mock("./useDatasetConfiguration", () => ({ + useDatasetConfiguration: vi.fn(() => mockUseDatasetConfiguration), })); -jest.mock("~/v1/domain/entities/import/ImportHistoryDetails", () => ({ - ImportHistoryDetails: jest.fn(), +vi.mock("~/v1/domain/entities/import/ImportHistoryDetails", () => ({ + ImportHistoryDetails: vi.fn(), })); describe("DatasetConfiguration", () => { @@ -26,7 +27,7 @@ describe("DatasetConfiguration", () => { beforeEach(() => { // Reset mocks - jest.clearAllMocks(); + vi.clearAllMocks(); // Mock dataset object mockDataset = { @@ -40,11 +41,11 @@ describe("DatasetConfiguration", () => { type: "rating", }, ], - createFields: jest.fn(() => [ + createFields: vi.fn(() => [ { name: "reference", value: "paper_001" }, { name: "title", value: "Test Paper" }, ]), - changeSubset: jest.fn(), + changeSubset: vi.fn(), }; // Mock ImportHistoryDetails @@ -62,25 +63,24 @@ describe("DatasetConfiguration", () => { }, }; - const ImportHistoryDetails = require("~/v1/domain/entities/import/ImportHistoryDetails"); - ImportHistoryDetails.ImportHistoryDetails.mockImplementation(() => mockImportHistoryDetails); + ImportHistoryDetails.mockImplementation(() => mockImportHistoryDetails); }); afterEach(() => { if (wrapper) { - wrapper.destroy(); + wrapper.unmount(); } - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); describe("HuggingFace Hub Mode", () => { beforeEach(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "hub", }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -116,7 +116,7 @@ describe("DatasetConfiguration", () => { props: ["import-history-details", "loading", "error"], }, BaseIcon: true, - }, + } }, }); }); @@ -142,12 +142,12 @@ describe("DatasetConfiguration", () => { describe("ImportHistory Mode", () => { beforeEach(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "import", importData: mockImportHistoryDetails, }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -183,7 +183,7 @@ describe("DatasetConfiguration", () => { props: ["import-history-details", "loading", "error"], }, BaseIcon: true, - }, + } }, }); }); @@ -212,7 +212,7 @@ describe("DatasetConfiguration", () => { expect(wrapper.emitted("import-dataset-configured")).toBeTruthy(); const emittedEvent = wrapper.emitted("import-dataset-configured")[0][0]; - expect(emittedEvent.dataset).toBe(mockDataset); + expect(emittedEvent.dataset).toEqual(mockDataset); expect(emittedEvent.suggestedMappings).toBeDefined(); expect(emittedEvent.suggestedQuestions).toBeDefined(); }); @@ -221,11 +221,11 @@ describe("DatasetConfiguration", () => { describe("Empty State", () => { beforeEach(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: { ...mockDataset, repoId: null }, dataSource: "hub", }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -250,7 +250,7 @@ describe("DatasetConfiguration", () => { template: '
', props: ["icon-name"], }, - }, + } }, }); }); @@ -265,11 +265,11 @@ describe("DatasetConfiguration", () => { const datasetWithoutQuestions = { ...mockDataset, questions: [] }; wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: datasetWithoutQuestions, dataSource: "hub", }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -291,7 +291,7 @@ describe("DatasetConfiguration", () => { DatasetConfigurationForm: true, ImportHistoryDataPreview: true, BaseIcon: true, - }, + } }, }); expect(wrapper.find(".dataset-config__empty-questions").exists()).toBe(true); @@ -300,11 +300,11 @@ describe("DatasetConfiguration", () => { it("should display questions component when questions exist", () => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "hub", }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -329,7 +329,7 @@ describe("DatasetConfiguration", () => { DatasetConfigurationForm: true, ImportHistoryDataPreview: true, BaseIcon: true, - }, + } }, }); expect(wrapper.find(".mock-questions").exists()).toBe(true); @@ -340,12 +340,12 @@ describe("DatasetConfiguration", () => { describe("Event Handling", () => { beforeEach(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "import", importData: mockImportHistoryDetails, }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -381,7 +381,7 @@ describe("DatasetConfiguration", () => { props: ["import-history-details", "loading", "error"], }, BaseIcon: true, - }, + } }, }); }); @@ -420,12 +420,12 @@ describe("DatasetConfiguration", () => { describe("Watchers", () => { beforeEach(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "import", importData: mockImportHistoryDetails, }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
`, }, @@ -437,7 +437,7 @@ describe("DatasetConfiguration", () => { DatasetConfigurationForm: true, ImportHistoryDataPreview: true, BaseIcon: true, - }, + } }, }); }); @@ -489,17 +489,17 @@ describe("DatasetConfiguration", () => { }); // Mock console.error to avoid noise in tests - const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); // Should not throw error when mounting expect(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "import", importData: mockImportHistoryDetails, }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
` }, VerticalResizable: { template: `
` }, Record: true, @@ -507,7 +507,7 @@ describe("DatasetConfiguration", () => { DatasetConfigurationForm: true, ImportHistoryDataPreview: true, BaseIcon: true, - }, + } }, }); }).not.toThrow(); diff --git a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.vue b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.vue index 7f5f4a37b..bddd13db4 100644 --- a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.vue +++ b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.vue @@ -93,23 +93,24 @@ diff --git a/extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationAddQuestion.vue b/extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationAddQuestion.vue index b5ad31f0a..4ddb8c158 100644 --- a/extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationAddQuestion.vue +++ b/extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationAddQuestion.vue @@ -5,12 +5,12 @@ @visibility="onVisibility" v-if="options.length" > - diff --git a/extralit-frontend/components/features/login/components/LoginInput.vue b/extralit-frontend/components/features/login/components/LoginInput.vue index 7923250fc..610073321 100644 --- a/extralit-frontend/components/features/login/components/LoginInput.vue +++ b/extralit-frontend/components/features/login/components/LoginInput.vue @@ -18,7 +18,7 @@ @blur="isBlurred = true" /> {{ isPasswordVisible ? $t("login.hide") : $t("login.show") }} { diff --git a/extralit-frontend/components/features/user-settings/UserSettingsTheme.vue b/extralit-frontend/components/features/user-settings/UserSettingsTheme.vue index e77534d3c..fd21a9721 100644 --- a/extralit-frontend/components/features/user-settings/UserSettingsTheme.vue +++ b/extralit-frontend/components/features/user-settings/UserSettingsTheme.vue @@ -11,10 +11,6 @@ +``` +(Confirm the real icon directory path and adjust the glob. If icons live as generated JS components under `assets/icons`, instead point the glob at the source SVGs the generator consumed — those are the durable source of truth.) +- [ ] **Step 5: Register globally** in `plugins/svg-icon.ts` so the existing `` tag resolves. Either (a) register under both names: +```ts +import { defineNuxtPlugin } from "#app"; +import BaseSvgIcon from "~/components/base/BaseSvgIcon.vue"; +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("svgicon", BaseSvgIcon); + nuxtApp.vueApp.component("SvgIcon", BaseSvgIcon); +}); +``` +This keeps all 79 `` call sites unchanged. Delete `plugins/directives/svg-icon.element.ts`. +- [ ] **Step 6: Run test, expect PASS. Commit** `git commit -m "feat: custom svg-icon component replacing vue-svgicon"` + +### Task 11: Plugin loader + global middleware → Nuxt 4 + +**Files:** Delete `plugins/index.ts`, `plugins/di/di.ts`, `plugins/axios/axios-cache.ts`, `plugins/axios/axios-global-handler.ts`; Create `middleware/route-guard.global.ts`, `middleware/me.global.ts`; Modify the remaining `plugins/*` to Nuxt-4 `defineNuxtPlugin` shape + +- [ ] **Step 1:** Nuxt 4 auto-imports every `plugins/*.ts`. The old `plugins/index.ts` manual `require.context` loader is obsolete — delete it. Each former sub-plugin becomes its own `plugins/.ts` exporting `defineNuxtPlugin(...)`. Convert: `plugins/language/*`, `plugins/logo/*`, `plugins/extensions/*` (non-filter ones), `plugins/directives/*` (badge, circle, required-field, tooltip, copy-code) — each registers its directive via `nuxtApp.vueApp.directive(...)`. The old signature `export default (context, inject) => {}` becomes `export default defineNuxtPlugin((nuxtApp) => {})`; `inject("x", v)` becomes `nuxtApp.provide("x", v)`. + +- [ ] **Step 2:** Convert `router.middleware: ["route-guard","me"]` to global middleware. Move `middleware/route-guard.ts` → `middleware/route-guard.global.ts` and `middleware/me.ts` → `middleware/me.global.ts`. Rewrite their Nuxt-2 signature `export default ({ $auth, redirect, route }) => {}` to Nuxt-4: +```ts +export default defineNuxtRouteMiddleware((to) => { + const { $auth } = useNuxtApp(); // AuthService, provided in Task 13 + if (!$auth.loggedIn && /* needs auth */) return navigateTo("/sign-in"); + // ...preserve exact original redirect logic, mapping redirect("/") -> navigateTo("/") +}); +``` +Keep the original conditional logic byte-for-byte; only the framework calls change (`redirect(x)`→`navigateTo(x)`, `$auth`→`useNuxtApp().$auth`). + +- [ ] **Step 3: Commit** `git commit -m "refactor: Nuxt 4 plugin + global middleware structure"` + +### Task 12: Axios plugin + error handler + cache (replaces `@nuxtjs/axios`) + +**Files:** Create `plugins/2.axios.ts`; Modify `v1/infrastructure/repositories/AxiosErrorHandler.ts`, `v1/infrastructure/services/useAxiosExtension.ts`; the `NuxtAxiosInstance` type import in ~20 repo files + +- [ ] **Step 1: Rewrite `AxiosErrorHandler.ts`** to use a standard axios response interceptor instead of auth-next's `$axios.onError`: +```ts +import type { AxiosInstance } from "axios"; +import { useNotifications } from "../services"; + +export const loadErrorHandler = (axios: AxiosInstance, t: (k: string) => string) => { + const notification = useNotifications(); + axios.interceptors.response.use( + (r) => r, + (error) => { + const { status, data } = error.response ?? {}; + notification.clear(); + // ...identical priority logic as the current file (businessLogic → detail → http status), + // calling t(key) and notification.notify(...). Re-throw error at the end. + return Promise.reject(error); + } + ); +}; +``` +Preserve the three-tier message-priority logic verbatim. Note the signature change: it now takes `(axios, t)` instead of a Nuxt `context` (the plugin supplies `t` via i18n). + +- [ ] **Step 2: Rewrite `useAxiosExtension.ts`.** Replace `NuxtAxiosInstance` with a plain axios instance + `makePublic`: +```ts +import axios, { type AxiosInstance } from "axios"; +import { loadCache } from "../repositories/AxiosCache"; +import { loadErrorHandler } from "../repositories/AxiosErrorHandler"; + +export interface PublicAxiosInstance extends AxiosInstance { + makePublic: (config?: { enableErrors: boolean }) => AxiosInstance; +} + +export const useAxiosExtension = (base: AxiosInstance, t: (k: string) => string) => { + const makePublic = (config = { enableErrors: true }) => { + const pub = axios.create({ baseURL: base.defaults.baseURL, withCredentials: false, headers: { Authorization: undefined } }); + if (config.enableErrors) loadErrorHandler(pub, t); + loadCache(pub); + return pub; + }; + const create = () => Object.assign(base, { makePublic }) as PublicAxiosInstance; + return create; +}; +``` +(Keep the public-name `PublicNuxtAxiosInstance` as a type alias re-export if other files import it, to avoid churn: `export type PublicNuxtAxiosInstance = PublicAxiosInstance;`.) + +- [ ] **Step 3: Create `plugins/2.axios.ts`** — the single composition root for HTTP + DI: +```ts +import axios from "axios"; +import { defineNuxtPlugin, useRuntimeConfig } from "#app"; +import { useAxiosExtension } from "~/v1/infrastructure/services/useAxiosExtension"; +import { loadCache } from "~/v1/infrastructure/repositories/AxiosCache"; +import { loadErrorHandler } from "~/v1/infrastructure/repositories/AxiosErrorHandler"; + +export default defineNuxtPlugin((nuxtApp) => { + const { $i18n } = nuxtApp as any; + const t = (k: string) => String($i18n.t(k)); + + const instance = axios.create({ baseURL: "/api" }); + // auth header: read token from AuthService (provided by plugins/auth.ts, ordered before this) + instance.interceptors.request.use((cfg) => { + const token = (nuxtApp.$auth as any)?.token; + if (token) cfg.headers.Authorization = `Bearer ${token}`; + return cfg; + }); + loadErrorHandler(instance, t); + loadCache(instance); + + nuxtApp.provide("axios", instance); +}); +``` +Ensure plugin ordering: name files so `auth.ts` (Task 13) loads before `axios.ts` and both before `di.ts`. Nuxt 4 orders plugins alphabetically within `plugins/`; use numeric prefixes (`1.auth.ts`, `2.axios.ts`, `3.di.ts`) to lock order. + +- [ ] **Step 4: Fix the `NuxtAxiosInstance` type imports** in the ~20 repo files: +```bash +grep -rln "@nuxtjs/axios" v1/infrastructure | xargs sed -i \ + -e 's/import { type NuxtAxiosInstance } from "@nuxtjs\/axios";/import type { AxiosInstance } from "axios";/' \ + -e 's/NuxtAxiosInstance/AxiosInstance/g' +``` +Then grep to confirm no `@nuxtjs/axios` references remain. Do **not** change any `this.axios.get/post/...` calls — plain axios shares that API. + +- [ ] **Step 5: Commit** `git commit -m "feat: plain-axios HTTP plugin with ported error handler + cache"` + +### Task 13: `AuthService` (replaces `@nuxtjs/auth-next`) + +**Files:** Create `v1/infrastructure/services/AuthService.ts`, `v1/infrastructure/services/AuthService.test.ts`, `plugins/1.auth.ts`; Modify `v1/domain/services/IAuthService.ts`, `v1/di/di.ts` + +- [ ] **Step 1: Drop the auth-next type from the interface.** In `IAuthService.ts`, replace `import { HTTPResponse } from "@nuxtjs/auth-next";` and change `setUserToken(token: string): Promise;` → `setUserToken(token: string): Promise;`. Keep all other members (`loggedIn`, `user`, `logout`, `setUser`). + +- [ ] **Step 2: Failing test** `AuthService.test.ts`: +```ts +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { AuthService } from "./AuthService"; + +describe("AuthService", () => { + let store: Record; + beforeEach(() => { store = {}; }); + const fakeCookie = (k: string) => ({ get value() { return store[k]; }, set value(v) { store[k] = v; } }); + + it("is logged out with no token", () => { + const a = new AuthService(fakeCookie("t") as any); + expect(a.loggedIn).toBe(false); + }); + it("becomes logged in after setUserToken", async () => { + const a = new AuthService(fakeCookie("t") as any); + await a.setUserToken("ABC"); + expect(a.loggedIn).toBe(true); + expect(a.token).toBe("ABC"); + }); + it("clears token and user on logout", async () => { + const a = new AuthService(fakeCookie("t") as any); + await a.setUserToken("ABC"); + a.setUser({ id: 1 }); + await a.logout(); + expect(a.loggedIn).toBe(false); + expect(a.user).toBeNull(); + }); +}); +``` +- [ ] **Step 3: Run, expect FAIL.** +- [ ] **Step 4: Implement** `AuthService.ts` — a class taking a token-ref (Nuxt `useCookie` ref injected at plugin time, so the class stays unit-testable): +```ts +import type { Ref } from "vue"; +import type { IAuthService } from "~/v1/domain/services/IAuthService"; + +export class AuthService implements IAuthService { + private _user: Record | null = null; + constructor(private readonly tokenRef: Ref) {} + get token() { return this.tokenRef.value ?? null; } + get loggedIn() { return !!this.tokenRef.value; } + get user() { return this._user; } + setUser(user: unknown) { this._user = (user as Record) ?? null; } + async setUserToken(token: string) { this.tokenRef.value = token; } + async logout() { this.tokenRef.value = null; this._user = null; } +} +``` +- [ ] **Step 5: Run, expect PASS.** +- [ ] **Step 6: Create `plugins/1.auth.ts`:** +```ts +import { defineNuxtPlugin, useCookie } from "#app"; +import { AuthService } from "~/v1/infrastructure/services/AuthService"; +export default defineNuxtPlugin((nuxtApp) => { + const token = useCookie("auth_token", { sameSite: "lax" }); + nuxtApp.provide("auth", new AuthService(token)); +}); +``` +- [ ] **Step 7: Rewire DI** in `v1/di/di.ts`: change `const useAuth = () => context.$auth;` → accept the provided service. Since `loadDependencyContainer` currently takes a Nuxt-2 `context`, update its signature to take `nuxtApp` and read `nuxtApp.$auth` / `nuxtApp.$axios`. Replace `const useAxios = useAxiosExtension(context)` with `const useAxios = useAxiosExtension(nuxtApp.$axios, t)`. Create `plugins/3.di.ts` that calls `loadDependencyContainer(useNuxtApp())`. (The old `plugins/di/di.ts` is deleted in Task 11.) + +- [ ] **Step 8: Commit** `git commit -m "feat: custom AuthService token store implementing IAuthService"` + +--- + +## Phase 3 — Mechanical Vue 3 codemods + +### Task 14: Composition-API import swaps (48 files) + +**Files:** every file importing `@nuxtjs/composition-api` + +- [ ] **Step 1:** Map the imports. `ref/computed/watch/onMounted/onBeforeMount/onBeforeUnmount/nextTick/defineComponent` come from `vue`. `useRoute/useRouter` are Nuxt-4 auto-imports (or from `vue-router`). `useContext` → `useNuxtApp`. `useFetch` → Nuxt-4 `useAsyncData`/`useFetch` (semantics differ — see Step 3). + +- [ ] **Step 2: Bulk-swap the pure-Vue imports:** +```bash +grep -rln "@nuxtjs/composition-api" components pages v1 layouts | while read f; do + sed -i 's#from "@nuxtjs/composition-api"#from "vue"#g' "$f"; done +``` +Then for each file still importing `useRoute`, `useRouter`, `useContext`, `useFetch` from `vue` (now wrong), hand-fix: remove those names from the `vue` import and rely on Nuxt auto-imports (`useRoute`, `useRouter`, `useNuxtApp`), replacing `useContext()` usages with `useNuxtApp()` and adjusting `.app`/`.$axios`/`.i18n` member access (`ctx.app.i18n` → `nuxtApp.$i18n`). + +- [ ] **Step 3: `useFetch` (≈8 files).** Nuxt-2 `useFetch(async () => {...})` ran the body on setup. Replace with `useAsyncData(, async () => {...})` or move the call into `onMounted` if it mutates refs imperatively. Convert one file, verify it compiles, then do the rest the same way. Document each converted key. + +- [ ] **Step 4:** `grep -rl "@nuxtjs/composition-api" .` → expect zero (outside node_modules). Commit `git commit -m "refactor: migrate composition-api imports to Vue 3 / Nuxt composables"` + +### Task 15: `slot`/`slot-scope` → `v-slot` (35 files), `$listeners` → `$attrs` (4 files), filters audit + +**Files:** the 35 legacy-slot `.vue` files; `BaseBadge.vue`, `EntityBadge.vue`, `FilterTooltip.vue`, `FilterBadge.vue` + +- [ ] **Step 1: Slots.** For each of the 35 files, convert `