Migrate extralit-frontend from Vue 2 / Nuxt 2 to Vue 3 / Nuxt 4#216
Open
JonnyTran wants to merge 55 commits into
Open
Migrate extralit-frontend from Vue 2 / Nuxt 2 to Vue 3 / Nuxt 4#216JonnyTran wants to merge 55 commits into
JonnyTran wants to merge 55 commits into
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pinned (resolved via npm view): nuxt ^4.4.8, vue ^3.5.38, @pinia/nuxt ^0.11.3, pinia ^3.0.4, @nuxtjs/i18n ^10.4.0, @vueuse/core ^14.3.0, mitt ^3.0.1, vuedraggable ^4.1.0 (Vue3 line), @tiptap/vue-3 ^2.4.0 (2.x to match extensions), highlight.js ^11.11.1, vitest ^4.1.8, @nuxt/test-utils ^4.0.3, @vue/test-utils ^2.4.11, happy-dom ^20.10.3, vite-svg-loader ^5.1.1 Deviations from plan (with rationale): - pinia 3.x/@pinia/nuxt 0.11 instead of 2.3/0.9: peer-compatible latest for Vue 3 - @nuxtjs/i18n 10 instead of 9: v9 peers nuxt^3 -> ERESOLVE on nuxt 4 - @vueuse/core 14 / vitest 4: latest stable (onClickOutside API stable) - @nuxt/eslint-config deferred to Task 20 (flat-config migration): adding now forces eslint 9 and ERESOLVEs against the kept eslint-8 stack; lint is baseline-red anyway - removed vue-virtual-scroller (unused, Vue2-only) - kept @jonnytran/vue-pdf-viewer: no vue peer (install OK); pulls vue2 transitively, runtime risk evaluated at Task 16/17 per user instruction - scripts: dev/build/generate->nuxi, start->nuxi preview, test*->vitest Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- @nuxtjs/style-resources -> vite.css.preprocessorOptions.scss with sass loadPaths (absolute path; '@use "abstract" as *' — avoids ~/ which sass's resolver can't handle) - nuxt-compress -> nitro.compressPublicAssets - axios.proxy -> nitro.devProxy (changeOrigin:true), default http://0.0.0.0:6900 - publicRuntimeConfig -> runtimeConfig.public - i18n v10: restructureDir '.' + langDir 'translation' to keep ./translation/*.js layout - router.middleware -> deferred to global middleware (Task 11) - dropped webpack build.extend (md/tabulator babel) — Vite/esbuild handle these Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nuxi prepare succeeds and generates .nuxt/tsconfig.json (auto path aliases ~/ @/). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- vitest.config.ts: happy-dom, globals, TZ=UTC, jest moduleNameMapper -> resolve.alias (~ @ assets), tabulator-tables -> __mocks__ stub - test/setup.ts: ports jest.setup.ts mocks (useTranslate #key# strings, config.global mocks/directives/stubs, IntersectionObserver); drops Nuxt2 buildNuxt() machinery Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
format-number.ts exports formatNumber/formatNumberToK (identical Intl behavior, capital K/M). DatasetTotal.vue calls formatNumberToK(total,2) via methods. Old Vue.filter plugin deleted. TDD: format-number.test.ts green (3/3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- bus.ts: mitt() (was new Vue()); consumers use on/off/emit - Toast.vue: beforeDestroy->beforeUnmount, $on/$off->on/off, $destroy->injected destroy() - api.ts: Vue.extend(Toast) imperative mount -> createApp(Toast,props).mount + app.unmount - bus.test.ts green (2/2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- new Vue({render}).$mount() -> createApp({render}).mount(mountPoint)
- Vue2 bind hook -> Vue3 mounted/updated/unmounted (with app.unmount cleanup)
- moved plugins/directives/tooltip.directive.ts -> plugins/tooltip.ts so Nuxt 4
auto-registers it (nested plugins/directives/*.ts are not auto-scanned)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Vue.use(ClickOutside) global plugin -> defineNuxtPlugin registering directive on
nuxtApp.vueApp via onClickOutside; stop fns tracked in WeakMap, cleaned on unmounted
- supports v-click-outside="fn" and "{ handler }"; 17 consumers unchanged
- moved to top-level plugins/click-outside.ts for Nuxt 4 auto-registration
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- BaseSvgIcon.vue: globs static/icons/*.svg (?raw), rewrites <svg> for width/height and recolors monochrome fills for the color prop; renders via v-html - plugins/svg-icon.ts: registers it globally as svgicon/SvgIcon/svg-icon so the ~77 call sites are unchanged - swept 72 'import "assets/icons/..."' side-effect registrations (broke: required vue-svgicon); deleted generated assets/icons/*.js, barrel, icon-template, generate-icons script - added missing static/icons/document.svg (only icon lacking a source SVG) - BaseSvgIcon.test.ts green (4/4) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- delete plugins/index.ts require.context loader (Nuxt 4 auto-imports plugins/*.ts)
- plugins/extensions.ts: consolidates extensions/* + language/* + logo (inject->provide:{},
Vue.component('draggable') -> vueApp.component, language detector via $i18n shim)
- plugins/directives.ts: badge/circle/required-field (bind->mounted, update->updated) +
copy-code (Vue.extend mount -> render(h(BaseCode)) with host appContext)
- middleware route-guard/me -> 01.route-guard.global.ts / 02.me.global.ts
(numeric prefix preserves original order; redirect()->navigateTo(), $auth via useNuxtApp)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- plugins/2.axios.ts: axios.create({baseURL:'/api'}) + request interceptor (bearer from
$auth) + ported error handler + cache; provide('axios')
- AxiosErrorHandler: $axios.onError -> response interceptor, signature (axios,t), 3-tier
message priority preserved (dropped a stray console.log)
- useAxiosExtension: NuxtAxiosInstance/context -> plain AxiosInstance + makePublic
- 26 repo files: NuxtAxiosInstance -> AxiosInstance (import from axios); PublicNuxtAxiosInstance
-> PublicAxiosInstance. No this.axios.* call sites touched.
- deleted plugins/axios/axios-cache.ts, axios-global-handler.ts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- AuthService: token in a useCookie ref; loggedIn/user/logout/setUser/setUserToken (IAuthService drops @nuxtjs/auth-next HTTPResponse import) - plugins/1.auth.ts provides $auth; plugins/3.di.ts loads ts-injecty container - di.ts: loadDependencyContainer(context) -> (nuxtApp); useAxiosExtension(nuxtApp.$axios, t), useAuth -> nuxtApp.$auth; numeric plugin prefixes lock order 1.auth<2.axios<3.di - AuthService.test.ts green (3/3) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
23 files. ref/computed/watch/lifecycle/etc. now from 'vue'; useRoute/useRouter/useNuxtApp
via Nuxt 4 auto-imports. useContext -> useNuxtApp (ctx.app.i18n->$i18n, ctx.$axios->$axios).
All 7 useFetch bodies mutated local refs -> converted to onMounted (none returned data).
Notable: useRoutes previous-route via router history.state.back; useNewDatasetViewModel
context.error -> showError(createError()); language VM uses a {app:{i18n}} shim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- slot/slot-scope -> v-slot (35 files; element-level slots wrapped in <template #name>) - remove v-on=$listeners (Vue 3 folds listeners into $attrs) (4) - beforeDestroy->beforeUnmount, destroyed->unmounted (14) - @x.native -> @x (9); :prop.sync -> v-model:prop (1, PDFViewer) - this.$set/$delete -> direct assignment / delete (4) Report-only items ($root bus, $nuxt, $destroy) handled separately next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- v1/infrastructure/eventBus.ts (mitt): replaces $root/$nuxt global event bus across 7 annotation components (on-change-record-criteria-filter/page/metadata) - i18n globalInjection:true; $nuxt.$t -> $t / this.$t (3 files) - layouts/app.vue: <Nuxt> -> <slot/>; $nuxt.isOffline -> useOnline() (@vueuse) - drop this.$destroy() in BaseInput/BaseInputContainer (the throw is the real behavior) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- @tiptap/vue-2 -> @tiptap/vue-3 (RenderHTML.vue)
- vuedraggable v2 default-slot -> v4 #item slot (3 files; item-key + transition via
tag='transition-group' + :component-data for DatasetConfigurationForm)
- Nuxt2 async fetch()/$fetchState -> pending flag + load in mounted (2 files)
- AnnotationHelpShortcut md loader: require.context -> import.meta.glob('~/docs/*.md',?raw)
- tabulator/interactjs: no change (construct in mounted + ssr:false); build.transpile set
- pdf-viewer: kept (install OK; wrapper already uses Vue3 v-model:src) — verify at boot
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mi imports - i18n compilation.strictMessage:false (translations contain HTML doc links) - nitro.publicAssets serves static/ at root; <img src=images/..> -> /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) <noreply@anthropic.com>
…-for keys - useLanguageDetector: drop @nuxtjs/i18n entry-point type import (blocked by Nuxt import-protection) -> local loose types - PDFViewer: v-model:src on a prop -> :src (one-way; url is parent-owned) - <template v-for> keys moved onto the <template> tag (SortCategoriesList, SpanAnnotationTextField) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…port, stub Vue-2 pdf-viewer - Port imperative Vue.extend/new...$mount EntityComponent factory to createApp+h - Fix dompurify namespace import -> default import (ESM) - Replace Vue-2-only @jonnytran/vue-pdf-viewer with a placeholder (follow-up: Vue 3 build) - gitignore Nuxt 4 build artifacts (.nuxt/.output/.nitro)
…ross 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.
…, css stub Global spec transforms: jest.* -> vi.* (globals:true), @jest/globals -> vitest, propsData -> props, wrapper.destroy() -> unmount(); alias tabulator css to a stub. Raises run from 482->701 tests (518 passing). 31 files still need VTU v1->v2 and composition-api mock retargeting (next).
…rity)
Ported 31 residual Jest specs across base/annotation/import/pages+usecases to
Vitest + @vue/test-utils v2 (wrapper.is->findComponent, top-level stubs/mocks->global,
findAll().at(n)->[n], emitted('input')->update:modelValue, retargeted
@nuxtjs/composition-api/vue-demi mocks to 'vue'/mockNuxtImport, jest-mock-extended
global alias). Full suite: 735 passed | 3 skipped | 1 todo (77 files), deterministic.
Real Vue-3 component bugs surfaced + fixed minimally:
- BaseFeedbackError: prop `type: Array | null` (=0 via bitwise OR) -> `type: Array, default: null`
- TextField/ChatField/SpanAnnotationTextField: invalid in-template `<style scoped>{{id}}` ->
computed highlightStyles rendered via <component :is="'style'"> (CSS Custom Highlight API)
- DndSelection: this.$refs.items[0] -> optional chaining guard
…xt 4) - .eslintrc.js: replace removed @babel/eslint-parser -> vue-eslint-parser + @typescript-eslint/parser; @nuxtjs/eslint-config-typescript -> plugin:@typescript-eslint/recommended. `npm run lint` clean (0 errors; old 51 'camelcase' errors were from the removed @nuxtjs config). - tsconfig: pin strict:false (original was 'strict:false // Temporal') + disable Nuxt4-new verbatimModuleSyntax/noImplicitOverride to keep the codebase's original TS posture; add vitest/globals+node types for specs. Cuts nuxi typecheck 2134 -> 111 (pre-existing debt). - remove obsolete Nuxt2 vue-shim.d.ts (@nuxt/types / auth-next augmentation; Nuxt 4 auto-types *.vue).
…pe checking Typecheck 111 -> 0 (strict:false posture preserved). Genuine migration runtime bugs fixed (not cast away): - useRoute() returns unwrapped route in Nuxt 4: drop route.value.* -> route.* across useRoutes/useDatasetViewModel/useImportConfigurationViewModel/useHomeViewModel/ useOAuthViewModel/useNewDatasetViewModel (was undefined at runtime) - vue-i18n v11 removed tc(): useTranslate $i18n.tc -> $i18n.t - nuxt.config: drop invalid 'hid' meta key, i18n.bundle.optimizeTranslationDirective, i18n.lazy (defaults in @nuxtjs/i18n v10) - RenderTable/FocusAnnotation: $notification onClick method-shorthand -> arrow (this-rebind left $notification undefined); invalid notification type 'error' -> 'danger' - DocumentsList: BaseTag :text -> :name (rendered empty tags) - DatasetConfigurationCard: restore the v-model local write-back for field type selection (DatasetConfigurationField does not listen to change-type, so dropping v-model silently broke field type changes) -> onTypeChange() writes item.type AND re-emits change-type - types/nuxt.d.ts: augment NuxtApp.$auth / ComponentCustomProperties $auth+$notification - nuxt.config vite.server.allowedHosts:true so a containerised browser can reach dev Gates after changes: vitest 735 passed, nuxi typecheck 0 errors, nuxi build clean.
|
Strix is installed on this repository, but we could not run this PR security review because this workspace does not have an active plan. If you'd like to continue receiving code reviews, you can add a payment method or manage billing here. |
…th snapshots Fixes roborev review 5: base-date.test.ts snapshotted the VueWrapper object graph (2400+ lines incl a leaked absolute path + Vue version), which broke all 10 base-date tests after the repo moved dirs. Snapshot .html() instead and drop stale Jest-keyed entries. Restores full suite to 735 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nuxt 4 file router needs [param] not _param; the build manifest was registering literal /dataset/_id so every /dataset/<uuid> 404'd. Rename all dynamic pages to bracket form (params/route-names preserved — middleware + router.push already assumed them). Add a pages:extend hook to drop co-located viewmodels/specs from the route table. Convert 3 dead Nuxt-2 Options-API middleware() hooks (silently ignored in Nuxt 4) to definePageMeta middleware with navigateTo; fix the hf guard to check params.repoId (was the wrong param 'id', never fired). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes roborev review 3. entityComponentFactory createApp().mount() was never unmounted, and applyEntityStyle() recreates every entity on each scroll/resize/ refresh, so EntityComponent.beforeUnmount (which detaches its scroll listener) never fired — unbounded live apps + orphaned listeners. Factory now returns an unmount disposer; Highlighting tracks disposers and disposes them before each restyle teardown and on unmount(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- annotation-mode page: Nuxt-2 head() hook is ignored in Nuxt 4; use useHead() in setup() so the dataset title actually sets. - error.vue: move from layouts/ (where it was only a dead named layout) to the project root so Nuxt 4 uses it as the error page; drop the dead layout:'error' option, fix legacy <svgicon>/<nuxt-link> tags, clearError() on recover. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Fixes roborev review 6. <component :is="'style'" v-html=highlightStyles> is an HTML-parse sink (a </style> 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/<id> path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Vue 3 normalizes @on-click into $attrs as onOnClick (on + PascalCase), not the kebab on-click the Vue-2 $listeners->$attrs migration left behind. So Badge/Tooltip clickable/clearable state was stuck false. Fix the key in 4 components + add a BaseBadge regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Register eslint-plugin-vue (config referenced vue/attributes-order but never loaded the plugin -> 'rule not found' aborted lint on every file). Lint now runs. - Declare Nuxt 4 auto-import globals (useHead/definePageMeta/navigateTo/...) and the Vitest 'vi' global so migrated code stops tripping no-undef. - prettier/prettier -> warn (parity with the *.ts override + separate npm run format). - i18n localeDir *.json -> *.js (was a no-op glob); drop deprecated prettier/vue. - Drop dead pdfjs-dist from build.transpile and unused core-js dependency. Migration-caused lint errors now 0; ~31 pre-existing non-migration style violations (no-empty/ban-ts-comment/no-namespace in domain code) remain as tracked follow-up — out of scope for the framework migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vue 3 keeps a parent @click as a fallthrough native listener AND fires the explicit $emit('click') unless the event is declared in emits -> double-fire. Declare emits on 17 base/feature components that emit native-named events (click/input/change/mouseover/...). Best practice + removes the double-fire. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the two deliberate-but-unusual migration decisions so they read as choices, not temporary hacks: the pages:extend route filter (preserves domain co-location) and the hand-listed Nuxt auto-import eslint globals (exit path is @nuxt/eslint). No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vue 3 removed the component `model: { prop, event }` option. LoginInput still
declared `model: { prop: "value", event: "input" }` with a `value` prop and an
`input` emit, so the parent `v-model="username"/"password"` on pages/sign-in.vue
(now `:modelValue` + `@update:modelValue`) never connected: typed text never
reached the parent, the submit button stayed permanently disabled, and login
was impossible through the UI.
Migrate LoginInput to the Vue 3 v-model convention (`modelValue` prop +
`update:modelValue` emit), drop the dead `model:` option, and stop mutating the
prop in the autofill watcher (write to local `inputValue` instead).
Also:
- Rewrite the stale Playwright login helper (e2e/common/login-and-wait-for.ts):
real Extralit selectors (getByLabel "Username"/"Password", submit "Sign in"),
mock /api/v1/token + /api/v1/me offline (no backend), and wait for the
home/datasets landing at "/" (there is no "/datasets" route).
- Pre-bundle marked/highlight.js/dompurify/marked-katex in vite.optimizeDeps so
the dev server (and the e2e webServer) stop re-optimizing + reloading on first
navigation to the home view.
Verified at runtime via CDP browser: fresh login fills, button enables, POST
/v1/token + GET /v1/me succeed, redirect lands on "/", user avatar renders.
vitest login/sign-in 10/10, nuxt typecheck 0 errors, eslint 0 errors.
NOTE: ~34 other components still declare the Vue 2 `model:` option; at least 10
are consumed via `v-model` and are broken the same way (annotation filters,
similarity search, dataset creation, dataset settings). Tracked as follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vue 3 dropped the component-level `model: { prop, event }` option entirely, so
every component that relied on it to wire `v-model` was silently broken: the
bound value never flowed in (wrong prop name) and never flowed out (wrong emit).
LoginInput was the first found; a sweep turned up 35 more components carrying the
dead option, 20 of them actually consumed via `v-model` (annotation status/sort/
metadata/response/suggestion filters, similarity search filters, dataset-creation
selectors, bulk-annotation view config, settings description, workspace filter,
pagination size). The 737 unit tests missed all of these because they exercise
each component through its own props/events, never through a parent's `v-model`.
Fix, applied uniformly:
- Components consumed via `v-model`: migrate to the Vue 3 convention — rename the
model prop to `modelValue`, emit `update:modelValue`, drop the `model:` option.
`v-model="x"` call sites are unchanged.
- Dual-purpose base inputs (BaseCheckbox, BaseRadioButton) keep their separate
`value` prop and existing `change`/`input` emits for explicit consumers, and
*additionally* emit `update:modelValue` so both binding styles work.
- Explicit consumers of renamed components rewired to `:model-value` /
`@update:model-value` (DatasetConfigurationForm, DatasetUpdateDialog,
RadioButtonsSelect.base).
- The 14 components consumed only via explicit `:prop`/`@event` (never v-model)
keep their prop/event names; their dead `model:` option is removed as cleanup
so it can't silently swallow a future `v-model`.
- Update BaseRadioButton.spec.js to the `modelValue` prop name.
Verified: nuxt typecheck 0 errors (validates every template prop/emit binding),
vitest 737 passed, and runtime via CDP browser — login flow, home render with no
console errors, and the BaseRadioButton-backed workspace switcher updates state
(?workspace=alpha -> beta) end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Playwright login helper now matches Extralit's real sign-in UI (getByLabel, offline token+me mocks, "/" landing). Per-page specs still need fresh baselines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nuxt 2's @nuxtjs/proxy forwarded the matched route prefix to the target; Nuxt 4's nitro devProxy mounts each rule via h3 `app.use(prefix, …)`, which STRIPS the prefix from the forwarded path. So `/api/v1/token` reached the backend as `/v1/token` — answered with the SPA fallback (200 HTML) for GETs and 405 for the login POST, breaking every API call when running the dev server against a live backend (e.g. API_BASE_URL=…hf.space). Re-append the prefix to each devProxy target origin so the backend receives the full `/api/v1/...` path. Dev-only; the prod build is a static SPA served same-origin with the backend. Verified against the public demo: login POST -> 201, /me/datasets -> 200 JSON. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FieldType and QuestionType extend String (boxed String objects), so `typeof field.type === 'object'`. vue-i18n v9+ (Vue 3) strictly requires the `t()` key to be a primitive string and throws "SyntaxError: Invalid arguments" otherwise; vue-i18n v8 (Vue 2) coerced silently. This crashed the render of every dataset card's field/question badges on the home list and the dataset settings page. Pass `.value` (the primitive string accessor, already the idiom elsewhere) to $t() at the four affected badge sites. Verified against the public demo: home dataset cards render Text/Label/ Multi-label badges with zero console errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BaseInput (via its common mixin) used the Vue 2 `value` prop and emitted only `input`/`change`. Vue 3 removed the `model:` option, so `<BaseInput v-model="x">` now desugars to `:model-value`/`@update:model-value` — neither of which BaseInput spoke. Every v-model consumer was silently disconnected: the home dataset search bar (BaseSearchBar) never filtered, and the "Import from Hugging Face" repo input (ImportFromHub) left its submit button permanently disabled. Apply the same dual-purpose pattern already used by BaseCheckbox/BaseRadioButton/ BaseRange*: accept both `value` and `modelValue`, declare `emits`, emit `update:modelValue` alongside the legacy events, and display `modelValue ?? value`. Legacy `:value`/`@input` consumers keep working unchanged. Verified against the public demo: typing a repo id enables the import submit button, and typing in the datasets search bar filters the list to the match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…new tests for step gating and document actions - Refactored component stubs in ImportFlow.spec.js to ensure proper functionality during tests. - Added new tests for step gating logic, confirming behavior for navigation between steps. - Implemented tests to verify document actions are recorded correctly during analysis updates. - Updated the beforeEach setup to streamline the mounting of the ImportFlow component.
- Updated the subproject commit to indicate a dirty state. - Added @vitest/coverage-v8 version 4.1.8 to package.json and package-lock.json for improved test coverage reporting. - Included additional dependencies for @vitest/coverage-v8 in package-lock.json.
Two migration regressions (Vue2->3 / Nuxt2->4): 1. HF Space 404 on every route. The server build copied the compiled frontend via `cp -r ../extralit-frontend/dist …`, but Nuxt 4's `nuxi build` emits a Nitro server under `.output/`, not a static `dist/`. The cp silently failed (shell lacks `-e`), so the wheel shipped without extralit_server/static/; configure_app_statics() then returns early, never mounts `/`, and FastAPI 404s everything. Switch to `nuxi generate` (static SPA -> .output/public with a real index.html), copy .output/public, build at BASE_URL=/ (the old "@@baseurl@@" placeholder breaks Nuxt 4's prerender crawler and yields //_nuxt paths), and guard the copy with set -euo pipefail + an index.html check so a missing bundle fails loudly. 2. Frontend CI tests + lint both failed for one reason: no `postinstall: nuxi prepare`, so the gitignored .nuxt/tsconfig.json that tsconfig.json extends was absent in CI -> vitest 'Tsconfig not found' (exit 1) and type-aware eslint OOM (exit 134). Add the postinstall hook. Verified locally: tests 77 passed, lint no OOM, generate emits a complete static bundle (index.html + _nuxt + assets).
On an HF Space without HF OAuth configured (providers empty), the welcome page redirects to /sign-in with an omitCTA flag that tells the route guard not to bounce back. The flag was passed as a route `param`, but Vue Router 4 drops params that aren't part of the route path on name-based navigation (it worked as router state under Vue Router 3). So the guard never saw omitCTA, sign-in redirected back to welcome, and the two looped forever — the page sat on its loading logo. Carry omitCTA as a query param instead (survives VR4 named navigation), read it from to.query in the guard, and return after redirecting in beforeMount so the navigating-away component stops fetching state.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full framework migration of
extralit-frontendfrom Vue 2.7 / Nuxt 2.18 (webpack) to Vue 3.5 / Nuxt 4.4 (Vite + Nitro), retaining functionality. The framework-agnostic domain/use-case layer (v1/domain, ts-injecty DI) is untouched; only the Vue/Nuxt-touching adapters were swapped.Adapters swapped
@nuxtjs/axios→ plainaxiosinplugins/2.axios.ts, re-injected into the same DI; error-handler + cache ported.@nuxtjs/auth-next→ smallAuthService implements IAuthService(cookie token store), provided as$authbyplugins/1.auth.ts.vue-svgicon→ custom<svg-icon>(BaseSvgIcon.vue) readingstatic/icons/*.svg, registered globally (call sites unchanged).@nuxtjs/i18nv9→v10 (vue-i18n v11); toast bus →mitt;v-click-outside→@vueuse/core; tooltip/click-outside/svg directives → Vue 3createApp;@nuxtjs/composition-api→vue/Nuxt composables; tiptap-vue-2→3; vuedraggable v2→v4;frontmatter-markdown-loader→marked+import.meta.glob.nuxt.configported to Nuxt 4 (Vite, Nitro dev proxy, i18n v10); Babel/Jest removed; tsconfig extends.nuxt/tsconfig.json.Verification gates (all green)
npx nuxi build— clean, zero warningsnpx vitest run— 735 passed / 3 skipped / 1 todo across 77 files (≥ the 727-test Jest baseline)npx nuxi typecheck— 0 errorsnpm run lint— 0 errors/sign-in(verified in a real Chromium over CDP); router + auth middleware + i18n + SVG icons + DI all working.Real runtime bugs caught & fixed during the migration
useRoute()returns the route unwrapped in Nuxt 4 — removedroute.value.*(×6 files) that would have beenundefined.tc()→t().$notificationonClickmethod-shorthand rebind (thisundefined) → arrow fns; invalid notificationtype:"error"→"danger".<template><style scoped>{{id}}(TextField/ChatField/SpanAnnotation) → computed style via<component :is="'style'">(CSS Custom Highlight API).DatasetConfigurationCard: restored the field-type-selection write-back that the dead Vue-2v-modelhad provided.BaseFeedbackErrorproptype: Array | null(=0via bitwise OR) →type: Array, default: null.Test Plan
npx nuxi buildsucceedsnpx vitest rungreen (735)npx nuxi typecheckcleannpm run lintcleanCaveats / follow-ups (please read before merge)
placeholder="Enter your username"/ button"Enter"which Extralit's form — identical ondevelop— does not render; it alsoroute.fetch()es a real backend). This mismatch pre-exists ondevelop. It needs a dedicated reconciliation pass to become a real gate. (Local Playwright browser also can't launch on the Orin dev host — run in CI.)@jonnytran/vue-pdf-vieweris stubbed. It is Vue-2-only (its bundle +vue-dropzone/vue-in-viewportdeps call the Vue 2 global API at module scope) and cannot run under Vue 3.PDFViewer.vuenow renders a placeholder + "open in new tab" link. Follow-up: publish a Vue-3 build of@jonnytran/vue-pdf-viewer(or swap to e.g.vue-pdf-embed) and restore the toolbar/sidebar/scale/page-nav UI.strict:false. The pre-Vue3 tsconfig was explicitlystrict:false // Temporal; Nuxt 4 defaults to strict. To avoid newly imposing strict-null/no-implicit-any on a codebase that never satisfied them,strict(and Nuxt-4-newverbatimModuleSyntax/noImplicitOverride) are kept off. Tightening to strict is a separate hardening effort.isolatedModulesrequires type-only imports to use the inlineimport { type X }modifier (216 were converted) — otherwise they throw at runtime in dev.🤖 Generated with Claude Code