From 8b76b483b457dd851a73c7c76ab863ebac42fdd8 Mon Sep 17 00:00:00 2001 From: JonnyTran Date: Sat, 13 Jun 2026 00:25:30 +0000 Subject: [PATCH 01/51] =?UTF-8?q?docs:=20add=20Vue=202=E2=86=923=20/=20Nux?= =?UTF-8?q?t=202=E2=86=924=20migration=20design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- ...026-06-13-vue2-to-vue3-migration-design.md | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 extralit-frontend/docs/superpowers/specs/2026-06-13-vue2-to-vue3-migration-design.md diff --git a/extralit-frontend/docs/superpowers/specs/2026-06-13-vue2-to-vue3-migration-design.md b/extralit-frontend/docs/superpowers/specs/2026-06-13-vue2-to-vue3-migration-design.md new file mode 100644 index 000000000..897296f6b --- /dev/null +++ b/extralit-frontend/docs/superpowers/specs/2026-06-13-vue2-to-vue3-migration-design.md @@ -0,0 +1,169 @@ +# Vue 2 → Vue 3 / Nuxt 2 → Nuxt 4 Migration — Design + +**Date:** 2026-06-13 +**Component:** `extralit-frontend` +**Branch:** `feat/vue-v2-to-v3` +**Status:** Approved design → implementation planning + +## 1. Goal & Constraints + +Migrate `extralit-frontend` from Vue 2.7 / Nuxt 2.18 to **Vue 3.5 / Nuxt 4**, retaining +**all current functionality**. The app is a **client-only SPA** (`ssr: false`) — this is +load-bearing and stays true after migration (no new SSR surface to reason about). + +Validation gates, in order of authority: +1. **Playwright e2e on Chromium** — the functional source of truth. The suite mocks the + backend (`e2e/common/*-api-mock.ts`), so it runs without a live server. Every page-level + flow (login, datasets, dataset-settings, annotation-mode, import-config, user-settings) + must pass. +2. **Vitest unit suite** — ported from the 79 Jest specs; component/use-case regression net. +3. `npm run lint` + `tsc` typecheck clean. + +Non-goals: no feature work, no redesign, no SSR adoption, no Vuex→Pinia work (already done), +no gratuitous refactor beyond what the migration forces. + +## 2. Current-State Facts (measured, not assumed) + +| Fact | Value | Implication | +|---|---|---| +| `.vue` components | 247 | Bulk of the work is mechanical, not architectural | +| Files importing `@nuxtjs/composition-api` | 48 | Import-path swap to `vue` / Nuxt composables | +| State management | Pinia (`@pinia/nuxt` 0.2.1) | Vuex / `@vuex-orm/*` deps are **dead** — delete them | +| `$store` / `mapActions` / vuex refs | 9 files | Stale leftovers; verify dead, remove | +| Legacy `slot=` / `slot-scope=` | 35 files | Convert to `v-slot` (Vue 2.6+ supports it) | +| Template filters (`\| filter`) | **1** site (`DatasetTotal.vue`) | Trivial — one computed/helper call | +| `Vue.filter` registrations | `format-number.ts` (2 filters) | Convert to importable helpers | +| `$listeners` usage | 4 files | Fold into `$attrs` (Vue 3 merges them) | +| `new Vue()` event bus | `base-toast/bus.js` | Replace with `mitt` | +| `new Vue()` in tooltip directive | `tooltip.directive.ts` | Re-implement with Vue 3 `createApp`/`render` | +| `::v-deep` / `/deep/` | 0 files | Nothing to do | +| `functional:` SFCs | 0 files | Nothing to do | +| HTTP | `@nuxtjs/axios` (`NuxtAxiosInstance`) injected into ~20 repository classes via ts-injecty | Replace with plain axios in a plugin | +| Auth | `@nuxtjs/auth-next`, **all endpoints disabled** — used only as token store + `loggedIn` flag behind `IAuthService`; OIDC handshake is in extralit-server | Replace with small custom AuthService | +| Icons | `vue-svgicon` 3.x, `` used in 79 files, generated from `static/icons` | Custom Vue 3 `` preserving call signature | +| `v-click-outside` | 18 files, behind custom directive plugin | Swap directive internals → `@vueuse/core onClickOutside` | +| `vuedraggable` 2.x | 1 file | → `vuedraggable@next` (Vue 3) | +| `@tiptap/vue-2` | 1 file | → `@tiptap/vue-3` | +| i18n | `@nuxtjs/i18n` v7 (4 locales, lazy, `no_prefix`) | → v9 (config reshape) | +| Heavy client libs | `tabulator-tables`, `interactjs` | Guard `window`/`document` access in `onMounted` / client-only | +| Unit tests | Jest 29 + `@vue/vue2-jest` + `@vue/test-utils` 1.x, 79 specs | → Vitest 4 + `@vue/test-utils` 2 + `@nuxt/test-utils` 4 | +| e2e | Playwright, backend mocked, 3 browser projects | Keep; **gate on chromium** | + +**Why this codebase is low-risk:** already on Pinia, already SPA-only, already 48 files on +Composition API, near-zero filters/deep-selectors/functional components. The genuine breaking +changes total ~45 files, almost all mechanical. + +## 3. Target Versions + +| Package | From → To | +|---|---| +| `nuxt` | 2.18 → **4.x** (latest stable) | +| `vue` | 2.7.16 → **3.5.x** | +| build | webpack → **Vite** (Nuxt 4 default) | +| `@pinia/nuxt` / `pinia` | 0.2.1 / 2.x → **0.9.x / 2.3.x** | +| `@nuxtjs/i18n` | 7.x → **9.x** | +| test | Jest → **Vitest 4** + `@nuxt/test-utils` 4 + `@vue/test-utils` 2 | +| `@vueuse/core` | (new) → **^11.x** | +| `mitt` | (new) | event bus | + +**Removed entirely:** `@nuxtjs/composition-api`, `@nuxtjs/axios`, `@nuxtjs/auth-next`, +`@nuxtjs/style-resources`, `nuxt-compress`, `nuxt-highlightjs`, `vue-svgicon`, +`v-click-outside`, `@nuxt/typescript-build`, `vue-template-compiler`, `vue-demi`, `vuex`, +`@vuex-orm/*`, `@vue/vue2-jest`, `vue-jest`, `frontmatter-markdown-loader`, +`@tiptap/vue-2`. Decision: `vue-vega` and `nuxt-mq` have **0 usages** → delete (verify first). + +## 4. Architecture of the Change + +The app's **domain/use-case layer is framework-agnostic** (`v1/domain`, `v1/infrastructure` +with ts-injecty DI). The migration deliberately keeps that layer's interfaces stable and only +swaps the **infrastructure adapters** that touch Nuxt/Vue. Three adapter swaps: + +### 4a. HTTP adapter — plain axios in a plugin +- New `plugins/axios.ts` (Nuxt 4 plugin) creates one `axios` instance: `baseURL` `/api`, + request interceptor adds the bearer token from AuthService, response interceptor ports the + existing `AxiosErrorHandler` behavior, plus the cache plugin (`AxiosCache` / `axios-cache`). +- Instance is `provide`d via `useNuxtApp()` **and** registered into the existing ts-injecty + container, so the ~20 repository classes keep their `constructor(private axios)` signature + and `this.axios.get/post/...` call sites **unchanged**. +- `useAxiosExtension` `makePublic()` (used by `OAuthRepository` for unauthenticated calls) is + reimplemented against the plain instance. + +### 4b. Auth adapter — custom AuthService (Pinia-backed) +- New `~100-line` `AuthService implements IAuthService`: `setUserToken(token)`, + `logout()`, `loggedIn` getter, `redirect()`. Token persisted (cookie via `useCookie`, the + Nuxt-4 idiomatic store) and surfaced to the axios request interceptor. +- `middleware/route-guard.ts` and `middleware/me.ts` switch from `$auth.loggedIn` / + `$auth.logout()` to the AuthService (resolved from DI / `useNuxtApp`). +- Login + OAuth use-cases (`auth-login-use-case`, `oauth-login-use-case`) are untouched — + they already call the `IAuthService` interface. Federated OIDC stays in extralit-server. + +### 4c. Icon adapter — custom `` +- A Vue 3 SFC `` + a small generation step that turns `static/icons/*.svg` + into a registry the component renders by `name`. The existing `` + call signature is preserved (mechanical tag rename at most) so 79 sites don't get rewritten. +- The `svg-icon.element.ts` plugin is replaced by global component registration. + +### 4d. Other adapters (small) +- `v-click-outside` directive plugin → internals call `onClickOutside` (`@vueuse/core`); 18 + consumer sites unchanged. +- `tooltip.directive.ts` (`new Vue`) → Vue 3 `createApp`/`render` mount. +- `base-toast/bus.js` (`new Vue` bus) → `mitt`. +- `format-number.ts` (`Vue.filter`) → exported helpers; `DatasetTotal.vue` calls the helper. +- `.md` loader → Vite `unplugin-vue-markdown` (or inline raw import) for frontmatter content. +- `@nuxtjs/style-resources` → `vite.css.preprocessorOptions.scss.additionalData`. +- `nuxt-compress` → Nitro `compressPublicAssets`. `nuxt-highlightjs` → `highlight.js` in a plugin. + +## 5. Config Migration (`nuxt.config.ts`) +- `ssr: false`, `telemetry: false`, `generate.dir` → Nuxt 4 equivalents (`ssr: false` stays; + static output via `nitro.static` / `nuxi generate`). +- `buildModules`/`modules` collapse into Nuxt 4 `modules`: `@pinia/nuxt`, `@nuxtjs/i18n`. +- `axios.proxy` (`/api/`, `/share-your-progress` → `API_BASE_URL`) → Nitro `routeRules` / + `devProxy`. Preserve the dev proxy to `http://0.0.0.0:6900` default. +- `components` auto-import (`pathPrefix: false`, `level: 1`) → Nuxt 4 `components` config. +- `router.middleware` (`route-guard`, `me`) → Nuxt 4 global middleware (`middleware/*.global.ts`). +- `publicRuntimeConfig` → `runtimeConfig.public`. +- webpack `build.extend` rules (md loader, tabulator/mjs babel, terser keep_classnames) → + Vite equivalents; most babel transforms become unnecessary under esbuild/Vite. + +## 6. Sequencing (straight cutover on this branch) + +Per the user's choice, this is a **single cutover branch** (not incremental Phase-0-on-Vue-2). +To keep it bisectable despite that, work proceeds in a **fixed dependency order** with the +**Playwright suite as the running acceptance gate** after the app first boots: + +1. **Deps & config** — rewrite `package.json` (add/remove per §3), `nuxt.config.ts`, + `tsconfig`, `vitest.config.ts`. App will not build yet. +2. **Infra adapters** — axios plugin + DI wiring, AuthService, ``, directives, + event bus, filters→helpers. Get `nuxi dev` to **boot**. +3. **Mechanical Vue-3 codemods** — `slot/slot-scope`→`v-slot` (35), `$listeners`→`$attrs` (4), + `v-model` prop/event rename where custom inputs need it, composition-api import swaps (48). +4. **Heavy libs** — tabulator/interactjs client-guarding, tiptap-vue-3, vuedraggable@next, i18n v9. +5. **Boot → first Playwright run on chromium**; drive remaining failures to green page-by-page. +6. **Unit tests** — port Jest→Vitest (config + `propsData→props`, `contains→exists`, + `createLocalVue→global.plugins`, `emitted('input')→emitted('update:modelValue')`). +7. **Lint + typecheck clean**, remove compat shims, lock versions. + +## 7. Error Handling & Risk +- **Client-only globals** (`window`/`document` at import time in tabulator/interactjs) are the + top regression risk in Nuxt 4 even under `ssr:false` (app-shell pass). Mitigation: dynamic + `import()` in `onMounted`, or `.client.vue` / ``. +- **DI timing**: ts-injecty registrations that previously ran in a Nuxt-2 plugin must run in a + Nuxt-4 plugin with correct ordering (axios + auth registered before repositories resolve). +- **i18n v9 config reshape** is the fiddliest config item; lazy + `no_prefix` + 4 locales must + be preserved and spot-checked in the UI. +- **Rollback**: branch-isolated; `develop` is untouched until merge. + +## 8. Testing Strategy +- **Vitest**: `environment: happy-dom`, `@nuxt/test-utils` for Nuxt auto-imports/`mockNuxtImport`. + Port specs alongside the components they cover; the 79 specs are the unit regression net. +- **Playwright (authoritative)**: run `npx playwright test --project=chromium`. Backend is + mocked, so green chromium == functional parity for covered flows. Firefox/webkit projects + stay in config but chromium is the gate per the request. +- **Definition of done:** chromium e2e green + Vitest green + lint/typecheck clean + app boots + via `npm run dev` and `npm run build` succeeds. + +## 9. Open Items to Verify During Implementation +- Confirm `vuex`, `@vuex-orm/*`, `vue-vega`, `nuxt-mq` are truly unreferenced before deleting. +- Confirm the `.md` frontmatter content's actual consumers (which pages render it) to pick the + Vite markdown approach. +- Confirm whether any test depends on `createLocalVue`-style DI so those get the `global.plugins` rewrite. From 6d9682f8084b1b86d074b22d5a67b1bc79b10d6b Mon Sep 17 00:00:00 2001 From: JonnyTran Date: Sat, 13 Jun 2026 00:34:13 +0000 Subject: [PATCH 02/51] =?UTF-8?q?docs:=20add=20Vue=202=E2=86=923=20/=20Nux?= =?UTF-8?q?t=202=E2=86=924=20implementation=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-13-vue2-to-vue3-migration.md | 741 ++++++++++++++++++ 1 file changed, 741 insertions(+) create mode 100644 extralit-frontend/docs/superpowers/plans/2026-06-13-vue2-to-vue3-migration.md diff --git a/extralit-frontend/docs/superpowers/plans/2026-06-13-vue2-to-vue3-migration.md b/extralit-frontend/docs/superpowers/plans/2026-06-13-vue2-to-vue3-migration.md new file mode 100644 index 000000000..ce8a41213 --- /dev/null +++ b/extralit-frontend/docs/superpowers/plans/2026-06-13-vue2-to-vue3-migration.md @@ -0,0 +1,741 @@ +# Vue 2 → Vue 3 / Nuxt 2 → Nuxt 4 Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate `extralit-frontend` from Vue 2.7 / Nuxt 2.18 (webpack) to Vue 3.5 / Nuxt 4 (Vite), retaining all functionality, gated by Playwright-on-Chromium and a ported Vitest unit suite. + +**Architecture:** The domain/use-case layer (`v1/domain`, `v1/infrastructure` with ts-injecty DI) is framework-agnostic and its interfaces stay frozen. Only the Nuxt/Vue-touching *adapters* are swapped: HTTP (`@nuxtjs/axios` → plain axios in a plugin, re-injected into the same DI), Auth (`@nuxtjs/auth-next` → small Pinia-backed `AuthService` implementing `IAuthService`; OIDC stays in extralit-server), Icons (`vue-svgicon` → custom `` keeping the call signature). Everything else is mechanical Vue-3 codemods + config translation. Straight cutover on `feat/vue-v2-to-v3`. + +**Tech Stack:** Nuxt 4.x, Vue 3.5.x, Vite 6, Pinia 2.3/@pinia/nuxt 0.9, @nuxtjs/i18n 9, Vitest 4 + @nuxt/test-utils 4 + @vue/test-utils 2, @vueuse/core 11, mitt, ts-injecty (kept), Playwright (chromium gate). + +**Reference spec:** `docs/superpowers/specs/2026-06-13-vue2-to-vue3-migration-design.md` + +**Working dir for all commands:** `extralit-frontend/` unless stated otherwise. + +--- + +## Conventions for the implementing engineer + +- This is a **cutover**: the app will not build between Phase 1 and the end of Phase 4. That is expected. Commit at each task boundary anyway — commits are the bisection points. +- **Do not** touch `v1/domain/**` use-case logic or `v1/infrastructure/repositories/*Repository.ts` request code. Their constructors take an injected axios and call `this.axios.get/post/...`; that contract is preserved by the new axios plugin. If a repo fails to compile only because of the `@nuxtjs/axios` *type import*, fix the import (Task 12), nothing else. +- Pin exact versions at install time with `npm view version`; record the resolved versions in the commit message for Task 2. +- After each phase that can run, run the relevant gate (`npm run dev` boot, `npx playwright test --project=chromium`, `npx vitest run`). + +--- + +## File Structure (what gets created / replaced) + +**Created:** +- `vitest.config.ts` — Vitest config (replaces `jest.config.js`) +- `test/setup.ts` — Vitest global setup (replaces `jest.setup.ts` role) +- `plugins/axios.ts` — single Nuxt-4 plugin: builds the axios instance, error handler, cache, DI load +- `v1/infrastructure/services/AuthService.ts` — `implements IAuthService`, Pinia/cookie token store +- `plugins/auth.ts` — instantiates AuthService, provides it to Nuxt + DI +- `components/base/BaseSvgIcon.vue` (+ `plugins/svg-icon.ts`) — custom `` replacement +- `v1/infrastructure/services/format-number.ts` — exported helpers (replaces `Vue.filter`) +- `components/base/base-toast/bus.ts` — `mitt` event bus (replaces `bus.js`) +- `middleware/route-guard.global.ts`, `middleware/me.global.ts` — Nuxt-4 global middleware (replace `router.middleware`) + +**Replaced in place:** +- `package.json`, `nuxt.config.ts`, `tsconfig.json` +- `plugins/directives/click-outside.directive.ts`, `plugins/directives/svg-icon.element.ts`, `plugins/directives/tooltip.directive.ts` +- `v1/infrastructure/services/useAxiosExtension.ts`, `v1/infrastructure/repositories/AxiosErrorHandler.ts` +- `v1/di/di.ts` (the `useAuth`/`useAxios` wiring lines only) +- `v1/domain/services/IAuthService.ts` (drop the `@nuxtjs/auth-next` type import) + +**Deleted:** +- `jest.config.js`, `jest.setup.ts`, `babel.config.js`, `plugins/index.ts`, `plugins/di/di.ts`, `plugins/axios/axios-cache.ts`, `plugins/axios/axios-global-handler.ts` + +--- + +## Phase 0 — Baseline & safety net + +### Task 1: Capture the green baseline + +**Files:** none (read-only) + +- [ ] **Step 1: Record current test + lint state** + +Run and save output to `docs/superpowers/plans/.baseline.txt` (gitignored scratch — do not commit): +```bash +npm ci +npm run test 2>&1 | tail -40 +npx playwright test --project=chromium 2>&1 | tail -30 +npm run lint 2>&1 | tail -20 +``` +Expected: note which specs/e2e currently pass. This is the parity target. If something is already red on `develop`, it is **not** your job to fix it — record it so you don't chase a pre-existing failure later. + +- [ ] **Step 2: Confirm dead-dependency claims** + +```bash +grep -rln "from \"vuex\"\|from 'vuex'\|@vuex-orm\|vue-vega\|nuxt-mq\|\\$mq\b" \ + --include='*.vue' --include='*.ts' --include='*.js' \ + components pages plugins layouts middleware v1 2>/dev/null +``` +Expected: **no output**. If any file prints, add a note to Task 3 to migrate it before deleting the dep. (Spec §9 open item.) + +- [ ] **Step 3: Commit a marker (optional)** + +No code change; proceed. + +--- + +## Phase 1 — Dependencies & configuration (app will stop building after this) + +### Task 2: Rewrite `package.json` + +**Files:** Modify `package.json` + +- [ ] **Step 1: Resolve target versions** + +```bash +for p in nuxt vue @pinia/nuxt pinia @nuxtjs/i18n vitest @nuxt/test-utils @vue/test-utils @vueuse/core mitt vuedraggable @tiptap/vue-3 happy-dom unplugin-vue-markdown vite-svg-loader; do + printf "%s: " "$p"; npm view "$p" version 2>/dev/null || echo "MISSING"; done +``` +Record the printed versions; use them as the pinned `^x.y.z` below. + +- [ ] **Step 2: Apply dependency changes** + +Remove: `@nuxtjs/composition-api`, `@nuxtjs/axios`, `@nuxtjs/auth-next`, `@nuxtjs/style-resources`, `nuxt-compress`, `nuxt-highlightjs`, `nuxt-mq`, `vue-vega`, `vue-svgicon`, `v-click-outside`, `@nuxt/typescript-build`, `@nuxt/types`, `vue-template-compiler`, `vue-demi`, `vuex`, `@vuex-orm/core`, `@vuex-orm/plugin-axios`, `frontmatter-markdown-loader`, `@tiptap/vue-2`, `nuxt` (v2), `vue` (v2), and the Jest stack: `@vue/vue2-jest`, `vue-jest`, `jest`, `jest-environment-jsdom`, `jest-serializer-vue`, `jest-transform-stub`, `babel-jest`, `babel-core`, all `@babel/*`, `@babel/eslint-parser`, `sass-loader`, `postcss-loader`, `postcss-import`. + +Add (deps): `nuxt@^4`, `vue@^3.5`, `@pinia/nuxt@^0.9`, `pinia@^2.3`, `@nuxtjs/i18n@^9`, `@vueuse/core@^11`, `mitt@^3`, `vuedraggable@^4` (the Vue-3 line, pkg name `vuedraggable`), `@tiptap/vue-3@^2.4`, `highlight.js@^11`. +Add (devDeps): `vitest@^4`, `@nuxt/test-utils@^4`, `@vue/test-utils@^2`, `happy-dom@latest`, `vite-svg-loader@latest`, `@nuxt/eslint-config@latest` (replaces `@nuxtjs/eslint-config-typescript`). (`marked` is already a dep and replaces `frontmatter-markdown-loader` — see Task 16 Step 5; no markdown Vite plugin needed since there is exactly one `.md` consumer.) + +Keep unchanged: `axios`, `pinia`, `@codescouts/events`, `@jonnytran/vue-pdf-viewer` (verify Vue-3 support in Task 16; flag if not), tiptap extensions/`@tiptap/pm`, `tabulator-tables`, `interactjs`, `ts-injecty`, `luxon`, `marked*`, `papaparse`, `dompurify`, `sass`, `@playwright/test`, eslint/prettier core. + +- [ ] **Step 3: Update `scripts`** + +Replace `"dev": "nuxt"` → `"dev": "nuxi dev"`, `"build": "nuxi build"`, `"generate": "nuxi generate"`, `"start": "nuxi preview"`. Replace test scripts: +```json +"test": "vitest run", +"test:watch": "vitest", +"test:coverage": "vitest run --coverage", +``` +Keep `e2e*`, `lint`, `format*`, `generate-icons` (revisit `generate-icons` in Task 14). + +- [ ] **Step 4: Install** + +```bash +rm -rf node_modules package-lock.json && npm install +``` +Expected: resolves without peer-dep ERESOLVE. If `@jonnytran/vue-pdf-viewer` or another pinned lib hard-blocks on `vue@2`, STOP and flag — do not `--force` silently. + +- [ ] **Step 5: Commit** +```bash +git add package.json package-lock.json +git commit -m "build: swap dependency set for Vue 3 / Nuxt 4 (pinned: )" +``` + +### Task 3: Delete confirmed-dead Vuex/unused source (if any surfaced in Task 1.2) + +**Files:** as surfaced. If Task 1.2 printed nothing, skip and note "no dead source". + +- [ ] **Step 1:** Remove the unreferenced files, run `grep` again to confirm zero references, commit `chore: remove unused vuex/vue-vega/nuxt-mq source`. + +### Task 4: Rewrite `nuxt.config.ts` for Nuxt 4 + +**Files:** Modify `nuxt.config.ts` + +- [ ] **Step 1: Write the Nuxt-4 config** + +Translate the existing config (preserve every behavior in spec §5). Replace the whole file with: +```ts +import { defineNuxtConfig } from "nuxt/config"; +import svgLoader from "vite-svg-loader"; +import pkg from "./package.json"; + +const BASE_URL = process.env.API_BASE_URL ?? "http://0.0.0.0:6900"; + +export default defineNuxtConfig({ + ssr: false, + telemetry: false, + srcDir: ".", + + app: { + baseURL: process.env.BASE_URL ?? "/", + head: { + title: "Extralit", + meta: [ + { charset: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + { hid: "description", name: "description", content: "" }, + ], + link: [ + { rel: "icon", type: "image/x-icon", href: "favicon.ico" }, + { rel: "apple-touch-icon", sizes: "180x180", href: "apple-touch-icon.png" }, + { rel: "icon", sizes: "32x32", href: "favicon-32x32.png" }, + { rel: "icon", sizes: "16x16", href: "favicon-16x16.png" }, + { rel: "manifest", href: "site.webmanifest" }, + ], + }, + }, + + css: ["~/assets/styles.scss"], + + components: [{ path: "~/components", pathPrefix: false }], + + modules: ["@pinia/nuxt", "@nuxtjs/i18n"], + + i18n: { + locales: [ + { code: "en", name: "English", file: "en.js" }, + { code: "de", name: "Deutsch", file: "de.js" }, + { code: "es", name: "Español", file: "es.js" }, + { code: "ja", name: "日本語", file: "ja.js" }, + ], + detectBrowserLanguage: false, + lazy: true, + langDir: "translation", + defaultLocale: "en", + strategy: "no_prefix", + bundle: { optimizeTranslationDirective: false }, + vueI18n: "./i18n.config.ts", + }, + + runtimeConfig: { + public: { + clientVersion: pkg.version, + communityLink: + "https://join.slack.com/t/extralit/shared_invite/zt-3gw1ah8bl-AiVNrkIVYOL4yVGOxN8WFw", + documentationSite: "https://docs.extralit.ai/", + documentationPersistentStorage: + "https://docs.extralit.ai/latest/getting_started/how-to-configure-argilla-on-huggingface/#persistent-storage", + }, + }, + + nitro: { + compressPublicAssets: true, + devProxy: { + "/api/": { target: BASE_URL, changeOrigin: true }, + "/share-your-progress": { target: BASE_URL, changeOrigin: true }, + }, + }, + + vite: { + plugins: [svgLoader()], + css: { + preprocessorOptions: { + scss: { additionalData: '@use "~/assets/scss/abstract.scss" as *;' }, + }, + }, + }, + + build: { transpile: ["pdfjs-dist", "tabulator-tables"] }, +}); +``` +Notes: `@nuxtjs/style-resources` → `vite.css.preprocessorOptions` (verify `assets/scss/abstract.scss` is `@use`-safe; if it has bare top-level statements, wrap as a partial). `nuxt-compress` → `nitro.compressPublicAssets`. `axios.proxy` → `nitro.devProxy`. `router.middleware` → global middleware files (Task 11). `publicRuntimeConfig` → `runtimeConfig.public`. The webpack `extend` md/tabulator babel rules are gone (Vite + esbuild handle them). + +- [ ] **Step 2: Create `i18n.config.ts`** (referenced above) +```ts +export default { + legacy: false, + fallbackLocale: "en", +}; +``` + +- [ ] **Step 3: Commit** `git add nuxt.config.ts i18n.config.ts && git commit -m "config: port nuxt.config to Nuxt 4 (vite, nitro proxy, i18n v9)"` + +### Task 5: Update `tsconfig.json` and remove Babel/Jest config + +**Files:** Modify `tsconfig.json`; Delete `babel.config.js`, `jest.config.js`, `jest.setup.ts` + +- [ ] **Step 1:** Replace `tsconfig.json` with Nuxt-4 extends: +```json +{ + "extends": "./.nuxt/tsconfig.json" +} +``` +(Run `npx nuxi prepare` once after Task 4 to generate `.nuxt/tsconfig.json`. If the project relies on path aliases `@/` and `~/`, Nuxt 4 provides them automatically; verify after prepare.) + +- [ ] **Step 2:** `git rm babel.config.js jest.config.js jest.setup.ts` + +- [ ] **Step 3: Commit** `git commit -m "config: drop babel/jest config, extend Nuxt 4 tsconfig"` + +--- + +## Phase 2 — Infrastructure adapters (TDD where new code is written) + +### Task 6: `format-number` helpers (replaces `Vue.filter`) + +**Files:** Create `v1/infrastructure/services/format-number.ts`, `v1/infrastructure/services/format-number.test.ts`; Delete `plugins/extensions/format-number.ts`; Modify `components/features/home/dataset-total/DatasetTotal.vue` + +- [ ] **Step 1: Write failing test** `v1/infrastructure/services/format-number.test.ts` +```ts +import { describe, it, expect } from "vitest"; +import { formatNumber, formatNumberToK } from "./format-number"; + +describe("format-number", () => { + it("formats plain numbers with locale grouping", () => { + expect(formatNumber(1000)).toBe("1,000"); + }); + it("formats large numbers to K with fraction digits", () => { + expect(formatNumberToK(12000, 2)).toBe("12k"); + expect(formatNumberToK(1500, 1)).toBe("1.5k"); + }); +}); +``` +(Match the exact output of the current `plugins/extensions/format-number.ts` — open it and copy the `Intl.NumberFormat`/`notation: "compact"` logic verbatim into the helpers so behavior is identical. Adjust the expected strings in this test to the real current output before running.) + +- [ ] **Step 2: Run, expect FAIL** `npx vitest run v1/infrastructure/services/format-number.test.ts` → fails (module not found). (Vitest config lands in Task 18; if it doesn't exist yet, write Task 18 first or run with `npx vitest run --config ./vitest.config.ts` after Task 18. Recommended order: do Task 18 before Task 6’s Step 2.) + +- [ ] **Step 3: Implement** `v1/infrastructure/services/format-number.ts` — export `formatNumber(value)` and `formatNumberToK(number, maximumFractionDigits)` using the same `Intl.NumberFormat` settings the old filters used. + +- [ ] **Step 4: Run, expect PASS.** + +- [ ] **Step 5: Update the one consumer.** In `DatasetTotal.vue`: remove `{{ total | formatNumberToK(2) }}`, import `formatNumberToK` and render `{{ formatNumberToK(total, 2) }}`. Delete `plugins/extensions/format-number.ts`. + +- [ ] **Step 6: Commit** `git commit -m "refactor: replace Vue.filter number formatters with helpers"` + +### Task 7: `mitt` event bus (replaces `new Vue()` bus) + +**Files:** Create `components/base/base-toast/bus.ts`, `components/base/base-toast/bus.test.ts`; Delete `components/base/base-toast/bus.js` + +- [ ] **Step 1: Failing test** +```ts +import { describe, it, expect, vi } from "vitest"; +import bus from "./bus"; +describe("toast bus", () => { + it("emits and receives events", () => { + const handler = vi.fn(); + bus.on("show", handler); + bus.emit("show", { message: "hi" }); + expect(handler).toHaveBeenCalledWith({ message: "hi" }); + }); +}); +``` +- [ ] **Step 2: Run, expect FAIL.** +- [ ] **Step 3: Implement** `bus.ts`: +```ts +import mitt from "mitt"; +export default mitt(); +``` +- [ ] **Step 4: Update consumers.** `grep -rln "base-toast/bus" components` → for each, replace `bus.$emit(...)` → `bus.emit(...)`, `bus.$on(...)` → `bus.on(...)`, `bus.$off(...)` → `bus.off(...)`. Delete `bus.js`. +- [ ] **Step 5: Run test, expect PASS. Commit** `git commit -m "refactor: replace Vue-instance toast bus with mitt"` + +### Task 8: Tooltip directive (replaces `new Vue()` mount) + +**Files:** Modify `plugins/directives/tooltip.directive.ts` + +- [ ] **Step 1:** Read the current file. It does `new Vue({ render: ... }).$mount()` to create a tooltip element. Rewrite using Vue 3: +```ts +import { createApp, h } from "vue"; +// ...inside the directive's mount logic: +const app = createApp({ render: () => h(/* same tooltip vnode */) }); +const mountPoint = document.createElement("div"); +app.mount(mountPoint); +// use mountPoint.firstElementChild as the tooltip node; app.unmount() on cleanup +``` +Preserve the directive's existing positioning/show/hide behavior exactly; only the instance-creation mechanism changes. Convert the Vue-2 directive hooks (`bind`/`unbind`/`update`) to Vue-3 (`mounted`/`unmounted`/`updated`). + +- [ ] **Step 2:** Manual smoke is deferred to Playwright (Task 17). Commit `git commit -m "refactor: port tooltip directive to Vue 3 createApp"` + +### Task 9: click-outside directive → `@vueuse/core` + +**Files:** Modify `plugins/directives/click-outside.directive.ts` + +- [ ] **Step 1:** Replace the `Vue.use(ClickOutside)` global-plugin file with a Nuxt-4 directive registration that preserves the `v-click-outside` directive name used by 18 consumers: +```ts +import { onClickOutside } from "@vueuse/core"; +import { defineNuxtPlugin } from "#app"; + +export default defineNuxtPlugin((nuxtApp) => { + const stops = new WeakMap void>(); + nuxtApp.vueApp.directive("click-outside", { + mounted(el, binding) { + const handler = typeof binding.value === "function" ? binding.value : binding.value?.handler; + if (handler) stops.set(el, onClickOutside(el, (e) => handler(e))); + }, + unmounted(el) { + stops.get(el)?.(); + stops.delete(el); + }, + }); +}); +``` +Move this file to `plugins/click-outside.ts` (Nuxt 4 auto-registers `plugins/*.ts`; the nested `plugins/directives/` dir is no longer auto-scanned the same way — see Task 11 for the plugin-loading change). Verify the 18 consumers use `v-click-outside="fn"` or `v-click-outside="{ handler }"`; support both as above. + +- [ ] **Step 2: Commit** `git commit -m "refactor: reimplement v-click-outside via @vueuse/core"` + +### Task 10: Custom `` (replaces `vue-svgicon`) + +**Files:** Create `components/base/BaseSvgIcon.vue`, `plugins/svg-icon.ts`, `components/base/BaseSvgIcon.test.ts`; Delete `plugins/directives/svg-icon.element.ts` + +- [ ] **Step 1:** Inspect current usage shape: `grep -rho "]*" components pages | head`. Capture the props actually used (typically `name`, `width`, `height`, `color`). The new component must accept the same props. + +- [ ] **Step 2: Failing test** `BaseSvgIcon.test.ts`: +```ts +import { describe, it, expect } from "vitest"; +import { mount } from "@vue/test-utils"; +import BaseSvgIcon from "./BaseSvgIcon.vue"; +describe("BaseSvgIcon", () => { + it("renders an svg with the icon name as data attribute", () => { + const w = mount(BaseSvgIcon, { props: { name: "check", width: 16, height: 16 } }); + expect(w.find("svg").exists()).toBe(true); + expect(w.attributes("data-icon")).toBe("check"); + }); +}); +``` +- [ ] **Step 3: Run, expect FAIL.** +- [ ] **Step 4: Implement.** Use `vite-svg-loader` to import raw SVGs from `static/icons` (or `assets/icons`) by name. Simplest robust approach: a Vite glob import. +```vue + + +``` +(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 ` diff --git a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfigurationForm.vue b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfigurationForm.vue index 70013b82c..41179dc1a 100644 --- a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfigurationForm.vue +++ b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfigurationForm.vue @@ -17,13 +17,15 @@
- + :group="{ name: 'fields' }" ghost-class="config-form__ghost" :disabled="isFocused" + item-key="name" tag="transition-group" + :component-data="{ name: 'config-form__draggable-area-wrapper', type: 'transition', css: false }"> +
@@ -48,13 +50,13 @@
- - +
From f837a302f3d8d4d8850b34eab705d5c391eabaf8 Mon Sep 17 00:00:00 2001 From: JonnyTran Date: Sat, 13 Jun 2026 05:43:18 +0000 Subject: [PATCH 20/51] fix(build): i18n strictMessage, static assets, v-click-outside/vue-demi imports - i18n compilation.strictMessage:false (translations contain HTML doc links) - nitro.publicAssets serves static/ at root; -> /images/.. (2 files) - BaseModal/BaseDropdown: drop local 'v-click-outside' package import (global directive exists) - 24 view-models: import from 'vue-demi' -> 'vue' (Vue 3) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/base/base-dropdown/BaseDropdown.vue | 4 ---- .../components/base/base-modal/BaseModal.vue | 4 ---- .../base/base-render-table/useSchemaTableViewModel.ts | 2 +- .../container/fields/chat-field/useChatFieldViewModel.ts | 2 +- .../container/fields/image-field/ImageField.vue | 2 +- .../useSpanAnnotationTextFieldViewModel.ts | 2 +- .../container/fields/text-field/useTextFieldViewModel.ts | 2 +- .../container/mode/useBulkAnnotationViewModel.ts | 2 +- .../annotation/container/mode/useDocumentViewModel.ts | 2 +- .../container/mode/useFocusAnnotationViewModel.ts | 2 +- .../label-selection/useLabelSelectionViewModel.ts | 2 +- .../container/useRecordFeedbackTaskViewModel.ts | 2 +- .../guidelines/useAnnotationGuidelinesViewModel.ts | 2 +- .../header/metadata-filter/useMetadataFilterViewModel.ts | 2 +- .../header/responses-filter/useResponseFilterViewModel.ts | 2 +- .../annotation/header/sort-filter/useSortRecords.ts | 2 +- .../suggestion-filter/useSuggestionFilterViewModel.ts | 2 +- .../annotation/header/useDatasetsFiltersViewModel.ts | 2 +- .../annotation/progress/useAnnotationProgressViewModel.ts | 2 +- .../annotation/progress/useTeamProgressViewModel.ts | 2 +- .../annotation/settings/useDeleteDatasetViewModel.ts | 2 +- .../configuration/useDatasetConfigurationForm.ts | 2 +- .../components/features/login/useOAuthLoginViewModel.ts | 2 +- extralit-frontend/nuxt.config.ts | 8 +++++++- .../pages/dataset/_id/useDatasetSettingViewModel.ts | 2 +- extralit-frontend/pages/welcome-hf-sign-in.vue | 2 +- extralit-frontend/v1/infrastructure/events/useEvents.ts | 2 +- .../v1/infrastructure/services/useFocusTab.ts | 2 +- extralit-frontend/v1/infrastructure/services/useRole.ts | 2 +- 29 files changed, 33 insertions(+), 35 deletions(-) diff --git a/extralit-frontend/components/base/base-dropdown/BaseDropdown.vue b/extralit-frontend/components/base/base-dropdown/BaseDropdown.vue index c09689371..577272740 100644 --- a/extralit-frontend/components/base/base-dropdown/BaseDropdown.vue +++ b/extralit-frontend/components/base/base-dropdown/BaseDropdown.vue @@ -31,11 +31,7 @@ @@ -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-render-markdown/MarkdownRenderer.vue b/extralit-frontend/components/base/base-render-markdown/MarkdownRenderer.vue index 444ef7788..ef7428c57 100644 --- a/extralit-frontend/components/base/base-render-markdown/MarkdownRenderer.vue +++ b/extralit-frontend/components/base/base-render-markdown/MarkdownRenderer.vue @@ -5,7 +5,7 @@ import { marked } from "marked"; import { markedHighlight } from "marked-highlight"; import hljs from "highlight.js"; -import * as DOMPurify from "dompurify"; +import DOMPurify from "dompurify"; import markedKatex from "marked-katex-extension"; const preprocess = (html) => { diff --git a/extralit-frontend/components/features/annotation/container/fields/span-annotation/useSpanAnnotationTextFieldViewModel.ts b/extralit-frontend/components/features/annotation/container/fields/span-annotation/useSpanAnnotationTextFieldViewModel.ts index 47c7ec5c8..944926566 100644 --- a/extralit-frontend/components/features/annotation/container/fields/span-annotation/useSpanAnnotationTextFieldViewModel.ts +++ b/extralit-frontend/components/features/annotation/container/fields/span-annotation/useSpanAnnotationTextFieldViewModel.ts @@ -1,5 +1,4 @@ -import Vue from "vue"; -import { onMounted, onUnmounted, ref, watch } from "vue"; +import { createApp, h, onMounted, onUnmounted, ref, watch } from "vue"; import { useSearchTextHighlight } from "../useSearchTextHighlight"; import { Highlighting, LoadedSpan, Position } from "./components/highlighting"; import EntityComponent from "./components/EntityComponent.vue"; @@ -37,7 +36,6 @@ export const useSpanAnnotationTextFieldViewModel = (props: { replaceEntity: (entity: Entity) => void, cloneSpanWith: (span: Span, entity: Entity) => void ) => { - const EntityComponentReference = Vue.extend(EntityComponent); const entity = answer.options.find((e) => e.id === span.entity.id); const suggestion = spanQuestion.suggestion?.getSuggestion({ start: span.from, @@ -47,29 +45,30 @@ export const useSpanAnnotationTextFieldViewModel = (props: { const spanInRange = highlighting.value.spans.filter((entity) => entity.from === span.from && entity.to === span.to); - const instance = new EntityComponentReference({ - propsData: { - span, - spanInRange, - entity, - spanQuestion, - entityPosition, - suggestion, - }, - }); - - instance.$on("on-remove-span", removeSpan); - instance.$on("on-hover-span", hoverSpan); - instance.$on("on-add-span-base-on", cloneSpanWith); - instance.$on("on-replace-entity", (newEntity: Entity) => { - selectEntity(newEntity); - - replaceEntity(newEntity); + const app = createApp({ + render: () => + h(EntityComponent, { + span, + spanInRange, + entity, + spanQuestion, + entityPosition, + suggestion, + onOnRemoveSpan: removeSpan, + onOnHoverSpan: hoverSpan, + onOnAddSpanBaseOn: cloneSpanWith, + onOnReplaceEntity: (newEntity: Entity) => { + selectEntity(newEntity); + + replaceEntity(newEntity); + }, + }), }); - instance.$mount(); + const mountPoint = document.createElement("div"); + app.mount(mountPoint); - return instance.$el; + return mountPoint.firstElementChild; }; const updateSelectedEntity = () => { diff --git a/extralit-frontend/translation/en.js b/extralit-frontend/translation/en.js index aead7659d..57a77ee10 100644 --- a/extralit-frontend/translation/en.js +++ b/extralit-frontend/translation/en.js @@ -301,6 +301,8 @@ export default { }, document: { notFound: "Document not found", + viewerUnavailable: "The in-app PDF viewer is being migrated to Vue 3 and is temporarily unavailable.", + openInNewTab: "Open document in a new tab", }, import: { title: "Import documents to {workspaceName}", From 3ec70334a82400b6cdae61b0915ba39457732ba2 Mon Sep 17 00:00:00 2001 From: JonnyTran Date: Sat, 13 Jun 2026 08:10:17 +0000 Subject: [PATCH 24/51] fix(vue3): mark type-only imports with inline `type` modifier (216 across 101 files) Vite/esbuild compiles each module in isolation (isolatedModules) and cannot erase interface/type imports it can't prove are types, so a named value import of an erased type resolves to undefined at runtime and throws in dev ESM (the prod Rollup build only warned). Adding the inline `type` modifier to exactly the flagged specifiers fixes the blank-page mount crash. App now boots to /sign-in. --- .../base/base-breadcrumbs/BaseBreadcrumbs.vue | 2 +- .../base/base-render-table/RenderTable.vue | 6 ++-- .../base/base-render-table/tableUtils.ts | 4 +-- .../useLLMExtractionViewModel.ts | 2 +- .../useReferenceTablesViewModel.ts | 2 +- .../useSchemaTableViewModel.ts | 4 +-- .../base/base-render-table/validatorUtils.ts | 2 +- .../base-simple-table/BaseSimpleTable.vue | 2 +- .../components/highlighting.ts | 4 +-- .../useSpanAnnotationTextFieldViewModel.ts | 6 ++-- .../mode/useBulkAnnotationViewModel.ts | 2 +- .../useRecordFeedbackTaskViewModel.ts | 2 +- ...useDatasetConfigurationNameAndWorkspace.ts | 2 +- .../components/features/global/AppHeader.vue | 2 +- .../import/analysis/ImportAnalysisTable.vue | 6 ++-- .../features/login/useOAuthLoginViewModel.ts | 2 +- .../useAnnotationModeViewModel.ts | 2 +- .../pages/dataset/_id/useDatasetViewModel.ts | 2 +- .../import/useImportConfigurationViewModel.ts | 2 +- .../oauth/_provider/useOAuthViewModel.ts | 2 +- extralit-frontend/pages/useHomeViewModel.ts | 4 +-- .../v1/domain/entities/common/Criteria.ts | 2 +- .../v1/domain/entities/document/Document.ts | 2 +- .../domain/entities/hub/QuestionCreation.ts | 2 +- .../v1/domain/entities/hub/Subset.ts | 6 ++-- .../import/ImportHistoryDatasetBuilder.ts | 6 ++-- .../entities/metadata/MetadataCriteria.ts | 4 +-- .../entities/metadata/MetadataFilter.ts | 2 +- .../v1/domain/entities/oauth/OAuthProvider.ts | 2 +- .../v1/domain/entities/question/Question.ts | 2 +- .../entities/question/QuestionAnswer.ts | 2 +- .../entities/question/QuestionSetting.ts | 2 +- .../v1/domain/entities/question/Suggestion.ts | 2 +- .../v1/domain/entities/record/Record.ts | 2 +- .../v1/domain/entities/record/RecordAnswer.ts | 2 +- .../domain/entities/record/RecordCriteria.ts | 2 +- .../v1/domain/entities/record/Records.ts | 2 +- .../entities/response/ResponseCriteria.ts | 4 +-- .../entities/response/ResponseFilter.ts | 2 +- .../v1/domain/entities/sort/SortCriteria.ts | 2 +- .../entities/suggestion/SuggestionCriteria.ts | 2 +- .../entities/suggestion/SuggestionFilter.ts | 6 ++-- .../v1/domain/entities/table/Extraction.ts | 2 +- .../v1/domain/entities/table/TableData.ts | 4 +-- .../v1/domain/services/IDatasetRepository.ts | 2 +- .../v1/domain/services/IOAuthRepository.ts | 2 +- .../v1/domain/services/RouterService.ts | 2 +- .../v1/domain/usecases/auth-login-use-case.ts | 4 +-- .../usecases/bulk-annotation-use-case.ts | 2 +- .../usecases/create-dataset-use-case.ts | 2 +- .../get-dataset-settings-use-case.ts | 8 ++--- .../update-dataset-setting-use-case.ts | 2 +- .../usecases/delete-dataset-use-case.ts | 2 +- .../export-dataset-to-hub-use-case.ts | 6 ++-- .../usecases/get-dataset-by-id-use-case.ts | 4 +-- .../get-dataset-fields-grouped-use-case.ts | 2 +- .../usecases/get-dataset-progress-use-case.ts | 4 +-- .../get-dataset-questions-filter-use-case.ts | 2 +- .../get-dataset-questions-grouped-use-case.ts | 2 +- ...get-dataset-suggestions-agents-use-case.ts | 2 +- .../usecases/get-dataset-vectors-use-case.ts | 2 +- .../domain/usecases/get-datasets-use-case.ts | 4 +-- ...et-document-by-record-metadata-use-case.ts | 2 +- .../usecases/get-environment-use-case.ts | 2 +- .../get-extraction-completion-use-case.ts | 4 +-- .../get-extraction-schema-use-case.ts | 4 +-- .../usecases/get-import-analysis-use-case.ts | 8 ++--- .../usecases/get-import-history-use-case.ts | 2 +- .../get-records-by-criteria-use-case.ts | 4 +-- .../usecases/get-user-metrics-use-case.ts | 2 +- .../usecases/get-workspaces-use-case.ts | 2 +- .../load-records-to-annotate-use-case.ts | 2 +- .../v1/domain/usecases/load-user-use-case.ts | 4 +-- .../domain/usecases/oauth-login-use-case.ts | 6 ++-- .../usecases/update-dataset-use-case.ts | 2 +- .../repositories/AgentRepository.ts | 2 +- .../repositories/AuthRepository.ts | 4 +-- .../repositories/DatasetRepository.ts | 18 +++++------ .../repositories/DocumentRepository.ts | 2 +- .../repositories/EnvironmentRepository.ts | 6 ++-- .../repositories/FieldRepository.ts | 2 +- .../repositories/HubRepository.ts | 2 +- .../repositories/JobRepository.ts | 2 +- .../repositories/MetadataMetricsRepository.ts | 2 +- .../repositories/MetadataRepository.ts | 4 +-- .../repositories/OAuthRepository.ts | 10 +++---- .../repositories/QuestionRepository.ts | 6 ++-- .../repositories/RecordRepository.ts | 30 +++++++++---------- .../repositories/UserRepository.ts | 4 +-- .../repositories/VectorRepository.ts | 4 +-- .../repositories/WorkspaceRepository.ts | 2 +- .../services/useLocalStorage.ts | 2 +- .../v1/infrastructure/services/useRole.ts | 4 +-- .../storage/DatasetSettingStorage.ts | 2 +- .../infrastructure/storage/DatasetStorage.ts | 2 +- .../infrastructure/storage/DatasetsStorage.ts | 2 +- .../infrastructure/storage/DocumentStorage.ts | 2 +- .../infrastructure/storage/MetricsStorage.ts | 2 +- .../infrastructure/storage/RecordsStorage.ts | 2 +- .../storage/TeamProgressStorage.ts | 2 +- .../storage/WorkspaceStorage.ts | 2 +- 101 files changed, 173 insertions(+), 173 deletions(-) diff --git a/extralit-frontend/components/base/base-breadcrumbs/BaseBreadcrumbs.vue b/extralit-frontend/components/base/base-breadcrumbs/BaseBreadcrumbs.vue index a606a8379..3146ffd37 100644 --- a/extralit-frontend/components/base/base-breadcrumbs/BaseBreadcrumbs.vue +++ b/extralit-frontend/components/base/base-breadcrumbs/BaseBreadcrumbs.vue @@ -50,7 +50,7 @@ diff --git a/extralit-frontend/components/features/import/ImportFlow.vue b/extralit-frontend/components/features/import/ImportFlow.vue index 9a099e053..811eb421e 100644 --- a/extralit-frontend/components/features/import/ImportFlow.vue +++ b/extralit-frontend/components/features/import/ImportFlow.vue @@ -324,7 +324,7 @@ export default { cancelUpload() { if (this.$refs.batchProgressComponent) { - this.$refs.batchProgressComponent.cancelUpload(); + (this.$refs.batchProgressComponent as { cancelUpload: () => void }).cancelUpload(); } }, @@ -378,7 +378,7 @@ export default { hasDataToLose() { // Check if user has uploaded any data that would be lost on close - const analysisTable = this.$refs.analysisTableComponent; + const analysisTable = this.$refs.analysisTableComponent as { editableTableData?: unknown[] }; return ( (this.bibData.dataframeData && this.bibData.dataframeData.data && this.bibData.dataframeData.data.length > 0) || this.pdfData.totalFiles > 0 || @@ -437,18 +437,10 @@ export default { // Reset child components this.$nextTick(() => { - if (this.$refs.fileUploadComponent) { - this.$refs.fileUploadComponent.reset(); - } - if (this.$refs.analysisTableComponent) { - this.$refs.analysisTableComponent.reset(); - } - if (this.$refs.batchProgressComponent) { - this.$refs.batchProgressComponent.reset(); - } - if (this.$refs.summaryComponent) { - this.$refs.summaryComponent.reset(); - } + (this.$refs.fileUploadComponent as { reset?: () => void } | undefined)?.reset(); + (this.$refs.analysisTableComponent as { reset?: () => void } | undefined)?.reset(); + (this.$refs.batchProgressComponent as { reset?: () => void } | undefined)?.reset(); + (this.$refs.summaryComponent as { reset?: () => void } | undefined)?.reset(); }); }, diff --git a/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.vue b/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.vue index f9f88dcfe..509268352 100644 --- a/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.vue +++ b/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.vue @@ -211,7 +211,7 @@ export default { return { ...this.dataframeData, data: filteredData, - }; + } as TableData; }, tableColumns(): TableColumn[] { @@ -726,15 +726,17 @@ export default { // Add missing methods retryAnalysis() { - if (this.$refs.viewModel && this.$refs.viewModel.retryAnalysis) { - this.$refs.viewModel.retryAnalysis(); + const vm = this.$refs.viewModel as { retryAnalysis?: () => void } | undefined; + if (vm && vm.retryAnalysis) { + vm.retryAnalysis(); } }, reset() { this.resetLocalState(); - if (this.$refs.viewModel && this.$refs.viewModel.reset) { - this.$refs.viewModel.reset(); + const vm = this.$refs.viewModel as { reset?: () => void } | undefined; + if (vm && vm.reset) { + vm.reset(); } }, diff --git a/extralit-frontend/components/features/import/history/ImportHistoryDetailsModal.vue b/extralit-frontend/components/features/import/history/ImportHistoryDetailsModal.vue index a291c894d..ee3ae90d6 100644 --- a/extralit-frontend/components/features/import/history/ImportHistoryDetailsModal.vue +++ b/extralit-frontend/components/features/import/history/ImportHistoryDetailsModal.vue @@ -41,7 +41,7 @@
{ - const context = { app: { i18n: useNuxtApp().$i18n } }; + // $i18n is the Nuxt i18n Composer; at runtime it carries the locales/setLocale + // members useLanguageChanger needs, but its static type (vue-i18n Composer) + // shapes `locales` differently, so we cast through the expected context shape. + const context = { app: { i18n: useNuxtApp().$i18n } } as unknown as Parameters< + typeof useLanguageChanger + >[0]; const { change, languages } = useLanguageChanger(context); return { diff --git a/extralit-frontend/nuxt.config.ts b/extralit-frontend/nuxt.config.ts index 09ab6369b..6dd7f1ba0 100644 --- a/extralit-frontend/nuxt.config.ts +++ b/extralit-frontend/nuxt.config.ts @@ -17,7 +17,7 @@ export default defineNuxtConfig({ meta: [ { charset: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, - { hid: "description", name: "description", content: "" }, + { name: "description", content: "" }, ], link: [ { rel: "icon", type: "image/x-icon", href: "favicon.ico" }, @@ -47,10 +47,9 @@ export default defineNuxtConfig({ { code: "ja", name: "日本語", file: "ja.js" }, ], detectBrowserLanguage: false, - lazy: true, + // (v10 removed the `lazy` option; lazy loading is the default with file-based locales.) defaultLocale: "en", strategy: "no_prefix", - bundle: { optimizeTranslationDirective: false }, // Some translation messages intentionally contain HTML (e.g. doc links); // allow them instead of failing the message compiler. compilation: { strictMessage: false, escapeHtml: false }, @@ -81,6 +80,9 @@ export default defineNuxtConfig({ vite: { plugins: [svgLoader()], + // Allow reaching the dev server by arbitrary hostnames (e.g. a containerised + // browser on the same Docker network). Dev-only; the prod build is a static SPA. + server: { allowedHosts: true }, css: { preprocessorOptions: { scss: { diff --git a/extralit-frontend/pages/dataset/_id/useDatasetViewModel.ts b/extralit-frontend/pages/dataset/_id/useDatasetViewModel.ts index f1e740422..9da7a6d8c 100644 --- a/extralit-frontend/pages/dataset/_id/useDatasetViewModel.ts +++ b/extralit-frontend/pages/dataset/_id/useDatasetViewModel.ts @@ -10,7 +10,7 @@ export const useDatasetViewModel = () => { const route = useRoute(); const notification = useNotifications(); const { t } = useTranslate(); - const datasetId = route.value.params.id; + const datasetId = route.params.id as string; const handleError = (response: string) => { let message = ""; diff --git a/extralit-frontend/pages/index.vue b/extralit-frontend/pages/index.vue index 6287f8696..ca5d0dc90 100644 --- a/extralit-frontend/pages/index.vue +++ b/extralit-frontend/pages/index.vue @@ -146,7 +146,7 @@ export default { methods: { onBreadcrumbAction(e) { if (e === "clearFilters") { - this.$refs.datasetList?.clearFilters(); + (this.$refs.datasetList as { clearFilters?: () => void })?.clearFilters(); } }, cardAction(action) { diff --git a/extralit-frontend/pages/new/import/_id.vue b/extralit-frontend/pages/new/import/_id.vue index 9b90b2f72..216cc2074 100644 --- a/extralit-frontend/pages/new/import/_id.vue +++ b/extralit-frontend/pages/new/import/_id.vue @@ -36,13 +36,14 @@ v-else-if="datasetConfig" :dataset="datasetConfig" data-source="import" - :import-data="importHistoryData" + :import-data="importHistoryData as ImportHistoryDetails" @change-subset="handleSubsetChange" />
diff --git a/extralit-frontend/pages/new/_id.vue b/extralit-frontend/pages/new/_id.vue deleted file mode 100644 index 06dd1b331..000000000 --- a/extralit-frontend/pages/new/_id.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/extralit-frontend/pages/new/hf/_repoId.vue b/extralit-frontend/pages/new/hf/[repoId].vue similarity index 90% rename from extralit-frontend/pages/new/hf/_repoId.vue rename to extralit-frontend/pages/new/hf/[repoId].vue index 70e81d82d..48ea041cf 100644 --- a/extralit-frontend/pages/new/hf/_repoId.vue +++ b/extralit-frontend/pages/new/hf/[repoId].vue @@ -11,15 +11,20 @@ + + + diff --git a/extralit-frontend/pages/dataset/[id]/annotation-mode/index.vue b/extralit-frontend/pages/dataset/[id]/annotation-mode/index.vue index e28069ec1..597bc10bd 100644 --- a/extralit-frontend/pages/dataset/[id]/annotation-mode/index.vue +++ b/extralit-frontend/pages/dataset/[id]/annotation-mode/index.vue @@ -39,12 +39,9 @@ export default { }, }, setup() { - return useAnnotationModeViewModel(); - }, - head() { - return { - title: this.dataset.name, - }; + const vm = useAnnotationModeViewModel(); + useHead(() => ({ title: vm.dataset?.name })); + return vm; }, }; From ab9de1b3cc1e4adb11bd575f4236a546c2e5b6f1 Mon Sep 17 00:00:00 2001 From: JonnyTran Date: Sat, 13 Jun 2026 17:16:27 +0000 Subject: [PATCH 35/51] fix(vue3): rename transition start-state classes to *-enter-from Vue 3 renamed .x-enter -> .x-enter-from (and .x-leave -> .x-leave-from); the bare selectors are silently ignored, breaking the enter animation start state across 9 components. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/base/base-card/BaseCardWithTabs.vue | 2 +- .../components/base/base-flow-modal/BaseFlowModal.vue | 4 ++-- extralit-frontend/components/base/base-modal/BaseModal.vue | 4 ++-- extralit-frontend/components/base/base-toast/Toast.vue | 2 +- .../label-selection/LabelSelection.component.vue | 2 +- .../questions/form/span/EntityLabelSelection.component.vue | 2 +- .../components/features/annotation/header/DatasetFilters.vue | 2 +- .../dataset-creation/configuration/DatasetUpdateDialog.vue | 2 +- .../components/features/home/sidebar/ImportFromHub.vue | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/extralit-frontend/components/base/base-card/BaseCardWithTabs.vue b/extralit-frontend/components/base/base-card/BaseCardWithTabs.vue index 0d257a9e4..cf84243e3 100644 --- a/extralit-frontend/components/base/base-card/BaseCardWithTabs.vue +++ b/extralit-frontend/components/base/base-card/BaseCardWithTabs.vue @@ -121,7 +121,7 @@ export default { transition: opacity 0.1s ease; } -.fade-enter, +.fade-enter-from, .fade-leave-to { opacity: 0; } diff --git a/extralit-frontend/components/base/base-flow-modal/BaseFlowModal.vue b/extralit-frontend/components/base/base-flow-modal/BaseFlowModal.vue index cdb791a30..0d01dd752 100644 --- a/extralit-frontend/components/base/base-flow-modal/BaseFlowModal.vue +++ b/extralit-frontend/components/base/base-flow-modal/BaseFlowModal.vue @@ -458,12 +458,12 @@ export default { transition: opacity 0.3s ease; } -.flow-modal-enter, +.flow-modal-enter-from, .flow-modal-leave-to { opacity: 0; } -.flow-modal-enter .flow-modal-container, +.flow-modal-enter-from .flow-modal-container, .flow-modal-leave-to .flow-modal-container { transform: scale(0.95); } diff --git a/extralit-frontend/components/base/base-modal/BaseModal.vue b/extralit-frontend/components/base/base-modal/BaseModal.vue index e34bd7d47..b1f2f4141 100644 --- a/extralit-frontend/components/base/base-modal/BaseModal.vue +++ b/extralit-frontend/components/base/base-modal/BaseModal.vue @@ -198,12 +198,12 @@ export default { margin-top: 0; } -.modal-enter, +.modal-enter-from, .modal-leave-active { opacity: 0; } -.modal-enter .modal-container, +.modal-enter-from .modal-container, .modal-leave-active .modal-container { transform: scale(0.99); } diff --git a/extralit-frontend/components/base/base-toast/Toast.vue b/extralit-frontend/components/base/base-toast/Toast.vue index 588e43356..f579a270f 100644 --- a/extralit-frontend/components/base/base-toast/Toast.vue +++ b/extralit-frontend/components/base/base-toast/Toast.vue @@ -386,7 +386,7 @@ $toast-colors: map-merge( transition: opacity 150ms ease-out; } -.fade-enter, +.fade-enter-from, .fade-leave-to { opacity: 0; } diff --git a/extralit-frontend/components/features/annotation/container/questions/form/shared-components/label-selection/LabelSelection.component.vue b/extralit-frontend/components/features/annotation/container/questions/form/shared-components/label-selection/LabelSelection.component.vue index a39233e40..c4a3fe881 100644 --- a/extralit-frontend/components/features/annotation/container/questions/form/shared-components/label-selection/LabelSelection.component.vue +++ b/extralit-frontend/components/features/annotation/container/questions/form/shared-components/label-selection/LabelSelection.component.vue @@ -516,7 +516,7 @@ input[type="checkbox"] { transition: opacity 0.5s; } -.fade-enter, +.fade-enter-from, .fade-leave-to { opacity: 0; } diff --git a/extralit-frontend/components/features/annotation/container/questions/form/span/EntityLabelSelection.component.vue b/extralit-frontend/components/features/annotation/container/questions/form/span/EntityLabelSelection.component.vue index c2017dcad..45fd25c2d 100644 --- a/extralit-frontend/components/features/annotation/container/questions/form/span/EntityLabelSelection.component.vue +++ b/extralit-frontend/components/features/annotation/container/questions/form/span/EntityLabelSelection.component.vue @@ -324,7 +324,7 @@ export default { transition: opacity 0.5s; } -.fade-enter, +.fade-enter-from, .fade-leave-to { opacity: 0; } diff --git a/extralit-frontend/components/features/annotation/header/DatasetFilters.vue b/extralit-frontend/components/features/annotation/header/DatasetFilters.vue index ee0e9a72b..be5096c32 100644 --- a/extralit-frontend/components/features/annotation/header/DatasetFilters.vue +++ b/extralit-frontend/components/features/annotation/header/DatasetFilters.vue @@ -192,7 +192,7 @@ export default { transition: all 0.3s ease-out; } -.filterAppear-enter, +.filterAppear-enter-from, .filterAppear-leave-to { opacity: 0; transform: translateY(-4px); diff --git a/extralit-frontend/components/features/dataset-creation/configuration/DatasetUpdateDialog.vue b/extralit-frontend/components/features/dataset-creation/configuration/DatasetUpdateDialog.vue index eb4f79272..c3badd96a 100644 --- a/extralit-frontend/components/features/dataset-creation/configuration/DatasetUpdateDialog.vue +++ b/extralit-frontend/components/features/dataset-creation/configuration/DatasetUpdateDialog.vue @@ -365,7 +365,7 @@ export default { .fade-leave-active { transition: opacity 0.3s; } -.fade-enter, +.fade-enter-from, .fade-leave-to { opacity: 0; } diff --git a/extralit-frontend/components/features/home/sidebar/ImportFromHub.vue b/extralit-frontend/components/features/home/sidebar/ImportFromHub.vue index 0e1c6af84..6f7098c28 100644 --- a/extralit-frontend/components/features/home/sidebar/ImportFromHub.vue +++ b/extralit-frontend/components/features/home/sidebar/ImportFromHub.vue @@ -127,7 +127,7 @@ $color-error: var(--color-brand); .slide-right-leave-active { transition: transform 0.3s; } -.slide-right-enter, +.slide-right-enter-from, .slide-right-leave-to { transform: translateX(50px); } From e970473469a60fa328d5b87bff7016626d2b0a72 Mon Sep 17 00:00:00 2001 From: JonnyTran Date: Sat, 13 Jun 2026 17:16:42 +0000 Subject: [PATCH 36/51] fix(vue3): inject highlight CSS as in interpolated ids breaks out); bind the CSS as the element's text child instead. Drop the stray :key. Also make the /new/[id] guard redirect to an absolute /new/ path. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../annotation/container/fields/chat-field/ChatField.vue | 2 +- .../fields/span-annotation/SpanAnnotationTextField.vue | 2 +- .../annotation/container/fields/text-field/TextField.vue | 2 +- extralit-frontend/pages/new/[id].vue | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extralit-frontend/components/features/annotation/container/fields/chat-field/ChatField.vue b/extralit-frontend/components/features/annotation/container/fields/chat-field/ChatField.vue index 91dc7830b..55a1b3665 100644 --- a/extralit-frontend/components/features/annotation/container/fields/chat-field/ChatField.vue +++ b/extralit-frontend/components/features/annotation/container/fields/chat-field/ChatField.vue @@ -33,7 +33,7 @@ - + {{ highlightStyles }} diff --git a/extralit-frontend/components/features/annotation/container/fields/span-annotation/SpanAnnotationTextField.vue b/extralit-frontend/components/features/annotation/container/fields/span-annotation/SpanAnnotationTextField.vue index 163a1dd59..06d0134fa 100644 --- a/extralit-frontend/components/features/annotation/container/fields/span-annotation/SpanAnnotationTextField.vue +++ b/extralit-frontend/components/features/annotation/container/fields/span-annotation/SpanAnnotationTextField.vue @@ -47,7 +47,7 @@ :entity-name="selectedEntity.text" :message="$t('spanAnnotation.shortcutHelper')" /> - + {{ highlightStyles }} diff --git a/extralit-frontend/components/features/annotation/container/fields/text-field/TextField.vue b/extralit-frontend/components/features/annotation/container/fields/text-field/TextField.vue index d1a217bc9..62b5919c6 100644 --- a/extralit-frontend/components/features/annotation/container/fields/text-field/TextField.vue +++ b/extralit-frontend/components/features/annotation/container/fields/text-field/TextField.vue @@ -17,7 +17,7 @@
- + {{ highlightStyles }}
diff --git a/extralit-frontend/pages/new/[id].vue b/extralit-frontend/pages/new/[id].vue index 7df1bad70..8f300a0fc 100644 --- a/extralit-frontend/pages/new/[id].vue +++ b/extralit-frontend/pages/new/[id].vue @@ -4,7 +4,7 @@ definePageMeta({ middleware(to) { if (to.params.id) { - return navigateTo(encodeURIComponent(to.params.id as string)); + return navigateTo(`/new/${encodeURIComponent(to.params.id as string)}`); } else { return navigateTo("/"); } From ce3b87db565c7cc9dbab25842e4a4bb8ede59e7e Mon Sep 17 00:00:00 2001 From: JonnyTran Date: Sat, 13 Jun 2026 17:19:52 +0000 Subject: [PATCH 37/51] fix(vue3): replace removed $slots.default VNode API Vue 3 $slots.default is a function and VNodes lack .elm/.tag/.context. SynchronizeScroll now reads rendered DOM children via $el.children; RatingShortcuts takes a question prop instead of spelunking the slot vnode's .context. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/base/base-scroll/SynchronizeScroll.vue | 10 ++++------ .../questions/form/rating/Rating.component.vue | 2 +- .../questions/form/rating/RatingShortcuts.vue | 8 +++++++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/extralit-frontend/components/base/base-scroll/SynchronizeScroll.vue b/extralit-frontend/components/base/base-scroll/SynchronizeScroll.vue index 3ffbb9805..d79527b53 100644 --- a/extralit-frontend/components/base/base-scroll/SynchronizeScroll.vue +++ b/extralit-frontend/components/base/base-scroll/SynchronizeScroll.vue @@ -12,12 +12,10 @@ export default { }, methods: { synchronizeScrolls() { - const children = this.$slots.default - .filter((child) => !!child.tag) - .map((child) => ({ - isSyncing: false, - element: child.elm, - })); + const children = Array.from(this.$el.children).map((element) => ({ + isSyncing: false, + element, + })); const applyProportionalScroll = (elementScrolling, elementToScroll) => { const elementScrollingTop = elementScrolling.scrollTop; diff --git a/extralit-frontend/components/features/annotation/container/questions/form/rating/Rating.component.vue b/extralit-frontend/components/features/annotation/container/questions/form/rating/Rating.component.vue index 03d3079df..3441542dd 100644 --- a/extralit-frontend/components/features/annotation/container/questions/form/rating/Rating.component.vue +++ b/extralit-frontend/components/features/annotation/container/questions/form/rating/Rating.component.vue @@ -1,5 +1,5 @@