From e457ae6bac7d490acf02144fce5d9080230d1528 Mon Sep 17 00:00:00 2001 From: JonnyTran Date: Sun, 23 Aug 2026 10:58:26 -0700 Subject: [PATCH 1/3] ci(frontend): replace Prettier with oxfmt, add oxlint as a blocking gate Formatting moves from Prettier to oxfmt (Rust; native for js/ts/json/css/scss, bundled Prettier for .vue/.md), and oxlint joins ESLint as a second linter. - `.oxfmtrc.json` translates `.prettierrc.js` 1:1 (printWidth 120, tabWidth 2, semi, double quotes, trailingComma es5) and absorbs `.prettierignore`, both of which are deleted. `demo/` is newly ignored so format scope matches the ESLint ignore list; `sortPackageJson` is off to keep package.json key order stable. - `.oxlintrc.json` is generated by `@oxlint/migrate` from eslint.config.mjs. Its `jsPlugins` bridge for @intlify/eslint-plugin-vue-i18n is dropped: oxlint has no Vue template parser, so those rules fired "use the latest vue-eslint-parser" on every SFC. ESLint keeps owning template + i18n rules. - eslint.config.mjs swaps eslint-plugin-prettier/recommended for a direct eslint-config-prettier, and adds oxlint.buildFromOxlintConfigFile() so a finding is not reported twice. ESLint warnings drop 2349 -> 1291; errors stay 0. - CI gains blocking `format:check` and `lint:oxc` steps; the ESLint step stays continue-on-error because of the pre-existing advisory warnings. - pre-commit gains a blocking `frontend-format` hook; the stale `frontend-eslint`, `frontend-prettier` and `helmlint` ci.skip entries named hooks that don't exist. --- .github/copilot-instructions.md | 2 +- .github/workflows/extralit-frontend.yml | 14 +- .pre-commit-config.yaml | 13 +- extralit-frontend/.oxfmtrc.json | 20 + extralit-frontend/.oxlintrc.json | 231 +++++++ extralit-frontend/.prettierignore | 12 - extralit-frontend/.prettierrc.js | 7 - extralit-frontend/CLAUDE.md | 12 +- extralit-frontend/eslint.config.mjs | 23 +- extralit-frontend/package-lock.json | 864 ++++++++++++++++++++++-- extralit-frontend/package.json | 15 +- 11 files changed, 1103 insertions(+), 110 deletions(-) create mode 100644 extralit-frontend/.oxfmtrc.json create mode 100644 extralit-frontend/.oxlintrc.json delete mode 100644 extralit-frontend/.prettierignore delete mode 100644 extralit-frontend/.prettierrc.js diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4f6107230..92e188772 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -65,7 +65,7 @@ uv run ruff check # Run ruff linting ### Linting Configuration - Python: Ruff with shared configuration across packages -- Frontend: ESLint + Prettier with TypeScript support +- Frontend: oxlint + ESLint (Vue templates/i18n) for linting, oxfmt for formatting - Pre-commit hooks for code formatting and linting # `extralit-server/` Backend Architecture Overview diff --git a/.github/workflows/extralit-frontend.yml b/.github/workflows/extralit-frontend.yml index d52044506..0d1c1b002 100644 --- a/.github/workflows/extralit-frontend.yml +++ b/.github/workflows/extralit-frontend.yml @@ -51,10 +51,20 @@ jobs: npm run api:types git diff --exit-code -- types/generated/api.d.ts - - name: Run lint 🧹 + - name: Check formatting 🎨 + run: | + npm run format:check + + - name: Run oxlint πŸ¦€ + run: | + npm run lint:oxc + + # ESLint keeps the Vue template + i18n rules oxlint cannot run yet, and carries + # ~1300 pre-existing advisory warnings, so it stays non-gating. + - name: Run eslint 🧹 continue-on-error: true run: | - npm run lint + npm run lint:eslint - name: Run tests with coverage πŸ§ͺ id: run-tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7907352f7..7b8eea7b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,6 +57,15 @@ repos: ############################################################################## - repo: local hooks: + - id: frontend-format + name: "Format extralit-frontend files with oxfmt" + # Same path-rewriting contract as frontend-lint below. Blocking: unlike lint, a + # formatting failure here means oxfmt could not parse the file. + entry: bash -c 'cd extralit-frontend && npx oxfmt --no-error-on-unmatched-pattern "${@#extralit-frontend/}"' -- + language: system + files: '^extralit-frontend/.*\.(js|mjs|cjs|ts|tsx|vue|json|jsonc|css|scss|md)$' + pass_filenames: true + - id: frontend-lint name: "Lint and fix extralit-frontend files" # pre-commit passes repo-relative paths (extralit-frontend/foo.vue). The trailing @@ -78,8 +87,6 @@ ci: autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate" autoupdate_schedule: weekly skip: - - helmlint # Disabling helmlint on CI by now because helm dependency is not available - - frontend-eslint # Requires npm dependencies to be installed in extralit-frontend - - frontend-prettier # Requires npm dependencies to be installed in extralit-frontend + - frontend-format # Requires npm dependencies to be installed in extralit-frontend - frontend-lint # Requires npm dependencies to be installed in extralit-frontend submodules: false diff --git a/extralit-frontend/.oxfmtrc.json b/extralit-frontend/.oxfmtrc.json new file mode 100644 index 000000000..33c0d8255 --- /dev/null +++ b/extralit-frontend/.oxfmtrc.json @@ -0,0 +1,20 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "trailingComma": "es5", + "printWidth": 120, + "tabWidth": 2, + "semi": true, + "singleQuote": false, + "sortPackageJson": false, + "ignorePatterns": [ + "node_modules", + "dist", + ".output", + ".nuxt", + ".nuxt-stale-root", + ".vercel", + "package-lock.json", + "types/generated", + "demo" + ] +} diff --git a/extralit-frontend/.oxlintrc.json b/extralit-frontend/.oxlintrc.json new file mode 100644 index 000000000..bbfffdccb --- /dev/null +++ b/extralit-frontend/.oxlintrc.json @@ -0,0 +1,231 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": [ + "vue", + "unicorn" + ], + "categories": { + "correctness": "off" + }, + "env": { + "builtin": true, + "es2022": true, + "browser": true, + "jest": true, + "node": true + }, + "globals": { + "$nuxt": "readonly", + "vi": "readonly", + "defineNuxtPlugin": "readonly", + "defineNuxtRouteMiddleware": "readonly", + "definePageMeta": "readonly", + "navigateTo": "readonly", + "abortNavigation": "readonly", + "useNuxtApp": "readonly", + "useRuntimeConfig": "readonly", + "useRoute": "readonly", + "useRouter": "readonly", + "useState": "readonly", + "useCookie": "readonly", + "useHead": "readonly", + "useSeoMeta": "readonly", + "useError": "readonly", + "createError": "readonly", + "clearError": "readonly", + "showError": "readonly" + }, + "ignorePatterns": [ + "node_modules/**", + "dist/**", + ".nuxt/**", + ".output/**", + "e2e/**", + "demo/**", + "v1/domain/entities/document/Document.ts", + "v1/domain/usecases/get-extraction-completion-use-case.ts", + "components/base/base-render-table/**", + "types/generated/**" + ], + "rules": { + "constructor-super": "error", + "for-direction": "error", + "getter-return": "error", + "no-async-promise-executor": "error", + "no-case-declarations": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-const-assign": "error", + "no-constant-binary-expression": "warn", + "no-constant-condition": "error", + "no-control-regex": "error", + "no-delete-var": "error", + "no-dupe-class-members": "error", + "no-dupe-else-if": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-empty-static-block": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-fallthrough": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-import-assign": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-loss-of-precision": "error", + "no-misleading-character-class": "error", + "no-new-native-nonconstructor": "error", + "no-nonoctal-decimal-escape": "error", + "no-obj-calls": "error", + "no-prototype-builtins": "error", + "no-redeclare": "error", + "no-regex-spaces": "error", + "no-self-assign": "error", + "no-setter-return": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-this-before-super": "error", + "no-unassigned-vars": "error", + "no-unreachable": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unsafe-optional-chaining": "error", + "no-unused-labels": "error", + "no-unused-private-class-members": "error", + "no-unused-vars": [ + "warn", + { + "ignoreRestSiblings": true + } + ], + "no-useless-backreference": "error", + "no-useless-catch": "error", + "no-useless-escape": "error", + "no-with": "error", + "preserve-caught-error": "warn", + "require-yield": "error", + "use-isnan": "error", + "valid-typeof": "error", + "vue/no-arrow-functions-in-watch": "error", + "vue/no-async-in-computed-properties": "error", + "vue/no-computed-properties-in-data": "error", + "vue/no-deprecated-data-object-declaration": "error", + "vue/no-deprecated-delete-set": "error", + "vue/no-deprecated-destroyed-lifecycle": "warn", + "vue/no-deprecated-events-api": "error", + "vue/no-deprecated-model-definition": "error", + "vue/no-deprecated-props-default-this": "error", + "vue/no-deprecated-vue-config-keycodes": "error", + "vue/no-dupe-keys": "error", + "vue/no-export-in-script-setup": "error", + "vue/no-expose-after-await": "error", + "vue/no-lifecycle-after-await": "error", + "vue/no-reserved-component-names": "error", + "vue/no-reserved-keys": "error", + "vue/no-reserved-props": "error", + "vue/no-shared-component-data": "error", + "vue/no-side-effects-in-computed-properties": "warn", + "vue/no-watch-after-await": "error", + "vue/prefer-import-from-vue": "error", + "vue/require-prop-type-constructor": "warn", + "vue/require-render-return": "error", + "vue/require-slots-as-functions": "error", + "vue/return-in-computed-property": "warn", + "vue/return-in-emits-validator": "error", + "vue/valid-define-emits": "error", + "vue/valid-define-options": "error", + "vue/valid-define-props": "error", + "vue/valid-next-tick": "warn", + "vue/component-definition-name-casing": "warn", + "vue/prop-name-casing": "warn", + "vue/require-default-prop": "warn", + "vue/require-prop-types": "warn", + "vue/no-multiple-slot-args": "warn", + "vue/no-required-prop-with-default": "warn", + "prefer-const": "warn", + "prefer-arrow-callback": "warn" + }, + "overrides": [ + { + "files": [ + "*.yaml", + "**/*.yaml", + "*.yml", + "**/*.yml" + ], + "rules": { + "no-irregular-whitespace": "off" + } + }, + { + "files": [ + "**/*.ts" + ], + "rules": { + "constructor-super": "off", + "getter-return": "off", + "no-class-assign": "off", + "no-const-assign": "off", + "no-dupe-class-members": "off", + "no-dupe-keys": "off", + "no-func-assign": "off", + "no-import-assign": "off", + "no-new-native-nonconstructor": "off", + "no-obj-calls": "off", + "no-redeclare": "off", + "no-setter-return": "off", + "no-this-before-super": "off", + "no-unreachable": "off", + "no-unsafe-negation": "off", + "no-var": "error", + "no-with": "off", + "prefer-const": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "no-array-constructor": "error", + "no-unused-expressions": "error", + "no-unused-vars": [ + "warn", + { + "ignoreRestSiblings": true, + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], + "typescript/ban-ts-comment": "error", + "typescript/no-duplicate-enum-values": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-explicit-any": "warn", + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": [ + "error", + { + "allowDeclarations": true + } + ], + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-require-imports": "error", + "typescript/no-this-alias": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-declaration-merging": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/no-wrapper-object-types": "error", + "typescript/prefer-as-const": "error", + "typescript/prefer-namespace-keyword": "error", + "typescript/triple-slash-reference": "error", + "no-useless-constructor": "off", + "no-throw-literal": "off", + "no-new": "off" + }, + "plugins": [ + "typescript" + ] + } + ] +} diff --git a/extralit-frontend/.prettierignore b/extralit-frontend/.prettierignore deleted file mode 100644 index d090d9b23..000000000 --- a/extralit-frontend/.prettierignore +++ /dev/null @@ -1,12 +0,0 @@ -# Dependencies and build output / generated dirs (prettier has no implicit ignores -# beyond node_modules; without this it tries to parse generated .vue files in .nuxt*). -node_modules -dist -.output -.nuxt -.nuxt-stale-root -.vercel - -# Lockfiles and generated data -package-lock.json -types/generated diff --git a/extralit-frontend/.prettierrc.js b/extralit-frontend/.prettierrc.js deleted file mode 100644 index 77162a72b..000000000 --- a/extralit-frontend/.prettierrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - "trailingComma": "es5", - "printWidth": 120, - "tabWidth": 2, - "semi": true, - "singleQuote": false, -}; \ No newline at end of file diff --git a/extralit-frontend/CLAUDE.md b/extralit-frontend/CLAUDE.md index 7b61d00d0..fb9b958bf 100644 --- a/extralit-frontend/CLAUDE.md +++ b/extralit-frontend/CLAUDE.md @@ -74,10 +74,12 @@ to `localhost` plus a throwaway redis on :6379. ## Code Quality ```bash -npm run lint # ESLint check (eslint 8 + vue-eslint-parser) -npm run lint:fix # Fix ESLint issues -npm run format # Format with Prettier -npm run format:check # Check formatting +npm run lint # oxlint, then eslint 10 (vue-eslint-parser) β€” both --quiet +npm run lint:oxc # oxlint only (the blocking CI gate; ~1s) +npm run lint:eslint # eslint only (Vue template + i18n rules; advisory in CI) +npm run lint:fix # Autofix both linters +npm run format # Format with oxfmt +npm run format:check # Check formatting (blocking in CI) npm run generate-icons # Generate icon components from SVG npx nuxi typecheck # vue-tsc type check @@ -114,7 +116,7 @@ npm run build # Production build (vite/nitro) - Pinia (state management; Vuex fully removed) - Vitest + @vue/test-utils v2 (unit) + Playwright (e2e) - @nuxtjs/i18n v10 (vue-i18n v11), @vueuse/core, mitt -- ESLint 8 + Prettier +- oxlint + ESLint 10 (lint), oxfmt (format) ## Structure diff --git a/extralit-frontend/eslint.config.mjs b/extralit-frontend/eslint.config.mjs index 30970e154..22b0102c0 100644 --- a/extralit-frontend/eslint.config.mjs +++ b/extralit-frontend/eslint.config.mjs @@ -1,15 +1,16 @@ // Flat config (ESLint 10). Replaces the legacy .eslintrc.js + .eslintignore. // // Posture is preserved verbatim from the old config: eslint-plugin-vue's -// `flat/recommended` is all-`warn`, formatting (prettier/prettier) is `warn`, and -// `npm run lint` runs with `--quiet` so only error-level rules gate. The @nuxt/eslint +// `flat/recommended` is all-`warn` and `npm run lint` runs with `--quiet` so only +// error-level rules gate. Formatting is oxfmt's job, not a lint rule. The @nuxt/eslint // module (project-aware auto-import globals) remains the tracked follow-up; until then // the Nuxt 4 auto-imports are hand-declared below, as they were in .eslintrc.js. import js from "@eslint/js"; import globals from "globals"; import pluginVue from "eslint-plugin-vue"; import vueI18n from "@intlify/eslint-plugin-vue-i18n"; -import prettierRecommended from "eslint-plugin-prettier/recommended"; +import eslintConfigPrettier from "eslint-config-prettier"; +import oxlintPlugin from "eslint-plugin-oxlint"; import vueParser from "vue-eslint-parser"; import tsParser from "@typescript-eslint/parser"; import tsPlugin from "@typescript-eslint/eslint-plugin"; @@ -158,15 +159,6 @@ export default [ }, }, - // Prettier last: turns off formatting-conflicting rules and adds prettier/prettier. - // Kept advisory (`warn`) β€” `npm run format` is the source of truth, and `lint --quiet` - // stays focused on correctness. - prettierRecommended, - { - rules: { - "prettier/prettier": "warn", - }, - }, // ── Rules newly promoted to `error` by this toolchain bump (eslint 8->10, // eslint-plugin-vue 8->10) that the previous setup either did not enable or that @@ -200,4 +192,11 @@ export default [ "vue/no-deprecated-destroyed-lifecycle": "warn", }, }, + + // oxlint owns every rule it implements (see .oxlintrc.json); turn those off here so the + // same finding is not reported twice by two linters. + ...oxlintPlugin.buildFromOxlintConfigFile(".oxlintrc.json"), + + // Formatting-conflicting rules off β€” oxfmt is the source of truth. + eslintConfigPrettier, ]; diff --git a/extralit-frontend/package-lock.json b/extralit-frontend/package-lock.json index b54a6ea8d..a148d85ae 100644 --- a/extralit-frontend/package-lock.json +++ b/extralit-frontend/package-lock.json @@ -71,12 +71,13 @@ "cross-env": "^10.1.0", "eslint": "^10.5.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.6", + "eslint-plugin-oxlint": "^1.79.0", "eslint-plugin-vue": "^10.9.2", "globals": "^17.7.0", "happy-dom": "^20.10.6", "openapi-typescript": "^7.13.0", - "prettier": "^3.8.4", + "oxfmt": "^0.64.0", + "oxlint": "^1.79.0", "rimraf": "^6.1.3", "typescript": "^5.4.5", "vite-svg-loader": "^5.1.1", @@ -5037,6 +5038,681 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.64.0.tgz", + "integrity": "sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.64.0.tgz", + "integrity": "sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.64.0.tgz", + "integrity": "sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.64.0.tgz", + "integrity": "sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.64.0.tgz", + "integrity": "sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.64.0.tgz", + "integrity": "sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.64.0.tgz", + "integrity": "sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.64.0.tgz", + "integrity": "sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.64.0.tgz", + "integrity": "sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.64.0.tgz", + "integrity": "sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.64.0.tgz", + "integrity": "sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.64.0.tgz", + "integrity": "sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.64.0.tgz", + "integrity": "sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.64.0.tgz", + "integrity": "sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.64.0.tgz", + "integrity": "sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.64.0.tgz", + "integrity": "sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.64.0.tgz", + "integrity": "sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.64.0.tgz", + "integrity": "sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.64.0.tgz", + "integrity": "sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.79.0.tgz", + "integrity": "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.79.0.tgz", + "integrity": "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.79.0.tgz", + "integrity": "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.79.0.tgz", + "integrity": "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.79.0.tgz", + "integrity": "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.79.0.tgz", + "integrity": "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.79.0.tgz", + "integrity": "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.79.0.tgz", + "integrity": "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.79.0.tgz", + "integrity": "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.79.0.tgz", + "integrity": "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.79.0.tgz", + "integrity": "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.79.0.tgz", + "integrity": "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.79.0.tgz", + "integrity": "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.79.0.tgz", + "integrity": "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.79.0.tgz", + "integrity": "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.79.0.tgz", + "integrity": "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.79.0.tgz", + "integrity": "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.79.0.tgz", + "integrity": "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.79.0.tgz", + "integrity": "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", @@ -10579,35 +11255,17 @@ "eslint": ">=7.0.0" } }, - "node_modules/eslint-plugin-prettier": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", - "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "node_modules/eslint-plugin-oxlint": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-oxlint/-/eslint-plugin-oxlint-1.79.0.tgz", + "integrity": "sha512-KpxU5mvO6gOMYKeJAaebF8lShP763U2lGUjGYmXf6YRvjlw4vME4ID9Ne8fLv8wU6LSImxYFINbzoyELt4Dsvg==", "dev": true, "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.1", - "synckit": "^0.11.13" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" + "jsonc-parser": "^3.3.1" }, "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } + "oxlint": "~1.79.0" } }, "node_modules/eslint-plugin-vue": { @@ -10964,13 +11622,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -13559,6 +14210,13 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/katex": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/katex/-/katex-0.17.0.tgz", @@ -16271,6 +16929,107 @@ "oxc-parser": ">=0.98.0" } }, + "node_modules/oxfmt": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.64.0.tgz", + "integrity": "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinypool": "2.1.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.64.0", + "@oxfmt/binding-android-arm64": "0.64.0", + "@oxfmt/binding-darwin-arm64": "0.64.0", + "@oxfmt/binding-darwin-x64": "0.64.0", + "@oxfmt/binding-freebsd-x64": "0.64.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", + "@oxfmt/binding-linux-arm64-gnu": "0.64.0", + "@oxfmt/binding-linux-arm64-musl": "0.64.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", + "@oxfmt/binding-linux-riscv64-musl": "0.64.0", + "@oxfmt/binding-linux-s390x-gnu": "0.64.0", + "@oxfmt/binding-linux-x64-gnu": "0.64.0", + "@oxfmt/binding-linux-x64-musl": "0.64.0", + "@oxfmt/binding-openharmony-arm64": "0.64.0", + "@oxfmt/binding-win32-arm64-msvc": "0.64.0", + "@oxfmt/binding-win32-ia32-msvc": "0.64.0", + "@oxfmt/binding-win32-x64-msvc": "0.64.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxlint": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.79.0.tgz", + "integrity": "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==", + "devOptional": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.79.0", + "@oxlint/binding-android-arm64": "1.79.0", + "@oxlint/binding-darwin-arm64": "1.79.0", + "@oxlint/binding-darwin-x64": "1.79.0", + "@oxlint/binding-freebsd-x64": "1.79.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", + "@oxlint/binding-linux-arm-musleabihf": "1.79.0", + "@oxlint/binding-linux-arm64-gnu": "1.79.0", + "@oxlint/binding-linux-arm64-musl": "1.79.0", + "@oxlint/binding-linux-ppc64-gnu": "1.79.0", + "@oxlint/binding-linux-riscv64-gnu": "1.79.0", + "@oxlint/binding-linux-riscv64-musl": "1.79.0", + "@oxlint/binding-linux-s390x-gnu": "1.79.0", + "@oxlint/binding-linux-x64-gnu": "1.79.0", + "@oxlint/binding-linux-x64-musl": "1.79.0", + "@oxlint/binding-openharmony-arm64": "1.79.0", + "@oxlint/binding-win32-arm64-msvc": "1.79.0", + "@oxlint/binding-win32-ia32-msvc": "1.79.0", + "@oxlint/binding-win32-x64-msvc": "1.79.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -17169,35 +17928,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", - "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", - "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/pretty-bytes": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz", @@ -19449,6 +20179,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/tinyrainbow": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", diff --git a/extralit-frontend/package.json b/extralit-frontend/package.json index 6b75776f6..8f1aea4c2 100644 --- a/extralit-frontend/package.json +++ b/extralit-frontend/package.json @@ -9,10 +9,12 @@ "build": "nuxi build", "start": "nuxi preview", "generate": "nuxi generate", - "lint": "eslint . --quiet", - "lint:fix": "npm run lint -- --fix", - "format": "prettier --write \"**/*.{ts,vue}\"", - "format:check": "prettier --check \"**/*.{ts,vue}\"", + "lint": "oxlint --quiet && eslint . --quiet", + "lint:oxc": "oxlint --quiet", + "lint:eslint": "eslint . --quiet", + "lint:fix": "oxlint --quiet --fix && eslint . --quiet --fix", + "format": "oxfmt", + "format:check": "oxfmt --check", "precommit": "npm run lint:fix", "e2e": "npx playwright test --ui", "e2e:report": "npx playwright show-report", @@ -86,12 +88,13 @@ "cross-env": "^10.1.0", "eslint": "^10.5.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.6", + "eslint-plugin-oxlint": "^1.79.0", "eslint-plugin-vue": "^10.9.2", "globals": "^17.7.0", "happy-dom": "^20.10.6", "openapi-typescript": "^7.13.0", - "prettier": "^3.8.4", + "oxfmt": "^0.64.0", + "oxlint": "^1.79.0", "rimraf": "^6.1.3", "typescript": "^5.4.5", "vite-svg-loader": "^5.1.1", From 7f404d9575cfbd056813e8365c8761befbfe1660 Mon Sep 17 00:00:00 2001 From: JonnyTran Date: Sun, 23 Aug 2026 11:01:39 -0700 Subject: [PATCH 2/3] style(frontend): reformat with oxfmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical `npx oxfmt` run over extralit-frontend (744 files). The .ts/.vue sources were already Prettier-clean and are almost untouched; the churn is in the file types the old `prettier --write "**/*.{ts,vue}"` glob never covered β€” .js specs, .scss/.css, .md and the extension's popup.html. `static/` is added to ignorePatterns: it holds served-verbatim assets, and oxfmt was expanding the vendored handlebars.min.js bundle. --- extralit-frontend/.oxfmtrc.json | 3 +- extralit-frontend/.oxlintrc.json | 20 +- extralit-frontend/CHANGELOG.md | 17 +- extralit-frontend/README.md | 2 +- extralit-frontend/assets/css/fonts.css | 3 +- extralit-frontend/assets/css/themes.css | 30 +- .../scss/abstract/mixins/_grid-mixins.scss | 324 +++++++------- .../scss/abstract/mixins/_media-queries.scss | 18 +- .../assets/scss/abstract/mixins/_mixins.scss | 28 +- .../scss/abstract/placeholders/_tooltip.scss | 28 +- .../scss/abstract/variables/_variables.scss | 3 +- .../base/base-render-table/RenderTable.vue | 3 +- .../DatasetConfiguration.spec.js | 284 +++++++------ .../analysis/ImportAnalysisTable.spec.js | 46 +- .../history/ImportHistoryDataPreview.spec.js | 122 +++--- .../import/recent/RecentImportCard.spec.js | 402 ++++++++++-------- .../import/recent/RecentImports.spec.js | 346 ++++++++------- extralit-frontend/docs/shortcuts.md | 38 +- .../2026-06-13-vue2-to-vue3-migration.md | 148 +++++-- .../plans/2026-06-13-vue3-remediation.md | 24 +- ...026-06-13-vue2-to-vue3-migration-design.md | 77 ++-- extralit-frontend/e2e/extraction/README.md | 2 +- extralit-frontend/eslint.config.mjs | 1 - extralit-frontend/extension/popup.html | 13 +- extralit-frontend/extension/popup.js | 2 +- extralit-frontend/package.json | 224 +++++----- .../useImportConfigurationViewModel.spec.js | 4 +- 27 files changed, 1202 insertions(+), 1010 deletions(-) diff --git a/extralit-frontend/.oxfmtrc.json b/extralit-frontend/.oxfmtrc.json index 33c0d8255..70303219e 100644 --- a/extralit-frontend/.oxfmtrc.json +++ b/extralit-frontend/.oxfmtrc.json @@ -15,6 +15,7 @@ ".vercel", "package-lock.json", "types/generated", - "demo" + "demo", + "static" ] } diff --git a/extralit-frontend/.oxlintrc.json b/extralit-frontend/.oxlintrc.json index bbfffdccb..400755df6 100644 --- a/extralit-frontend/.oxlintrc.json +++ b/extralit-frontend/.oxlintrc.json @@ -1,9 +1,6 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": [ - "vue", - "unicorn" - ], + "plugins": ["vue", "unicorn"], "categories": { "correctness": "off" }, @@ -152,20 +149,13 @@ }, "overrides": [ { - "files": [ - "*.yaml", - "**/*.yaml", - "*.yml", - "**/*.yml" - ], + "files": ["*.yaml", "**/*.yaml", "*.yml", "**/*.yml"], "rules": { "no-irregular-whitespace": "off" } }, { - "files": [ - "**/*.ts" - ], + "files": ["**/*.ts"], "rules": { "constructor-super": "off", "getter-return": "off", @@ -223,9 +213,7 @@ "no-throw-literal": "off", "no-new": "off" }, - "plugins": [ - "typescript" - ] + "plugins": ["typescript"] } ] } diff --git a/extralit-frontend/CHANGELOG.md b/extralit-frontend/CHANGELOG.md index e20c43712..80ec3b503 100644 --- a/extralit-frontend/CHANGELOG.md +++ b/extralit-frontend/CHANGELOG.md @@ -15,6 +15,7 @@ These are the section headers that we use: --> ## [Extralit] [Unreleased](https://github.com/extralit/extralit/compare/v0.6.1...v0.6.2) + ### Added - Loading indicator on sign-in form button to provide visual feedback during authentication requests. @@ -27,17 +28,21 @@ These are the section headers that we use: `page-content` but no `page-header`) showed the unfilled slot's placeholder as real page copy. ## [Extralit] [v0.6.1](https://github.com/extralit/extralit/compare/v0.5.0...v0.6.1) + ### Added + - Incremental Dataset Import: new `DatasetUpdateDialog` and update workflow in `DatasetConfigurationForm` to update existing datasets with imported data ### Changed + - Refactored the frontend to use a single fetchDocument method that queries documents by any identifier and workspace, replacing the previous fetchDocumentByID and fetchDocumentByPubmedID methods. The view model and use case now expect and handle the new API response format - - Renamed `DatasetConfigurationDialog` to `DatasetCreateDialog` and improved TypeScript typings and prop validations across configuration components - - Improved button area layout, dialog interactions, and hid questions section during update flow to avoid unintended edits +- Renamed `DatasetConfigurationDialog` to `DatasetCreateDialog` and improved TypeScript typings and prop validations across configuration components +- Improved button area layout, dialog interactions, and hid questions section during update flow to avoid unintended edits ## [Extralit] [0.6.0](https://github.com/extralit/extralit/compare/v0.4.1...v0.6.0) ### Added + - Papers Library Importer: Added full import workflow for bulk document ingestion via BibTeX and PDF upload. - Added ImportModal full-page workflow with multi-step navigation for document import. - Implemented BibTeX parsing and file-to-reference matching in ImportBibUpload and ImportPdfUpload components. @@ -48,6 +53,7 @@ These are the section headers that we use: - Enhanced workspace selection and integration with import features. ### Changed + - Renamed `argilla-frontend/` β†’ `extralit-frontend/` (entire frontend directory) - Updated route new dataset view from `/{dataset.repoId}` to `/new/hf/{dataset.repoId}` to prevent url conflicts. - Improved import modal navigation and state persistence across steps. @@ -55,12 +61,12 @@ These are the section headers that we use: - Enhanced error handling and validation for BibTeX and PDF uploads. ### Fixed + - Fixed PDF-to-reference matching and error reporting in import workflow. - Fixed UI state restoration when navigating between import steps. ## Changed - ## [Argilla] [2.8.0](https://github.com/argilla-io/argilla/compare/v2.7.1...v2.8.0) ### Added @@ -104,20 +110,23 @@ These are the section headers that we use: ## [Extralit] [0.4.0](https://github.com/extralit/extralit/compare/v0.3.0...v0.4.0) ### Fixed + - Fixed ES index type for TableField and TableQuestion ## [Extralit] [0.3.0](https://github.com/extralit/extralit/compare/v0.2.3...v0.3.0) ### Added + - Added support for `TableField` for table fields. - Added `TableQuestion` to support table questions. ### Fixed + - Fixed use_table setting update in `TextField` ### Changed -- Refactored `argilla-frontend/components/base/base-render-table/RenderTable.vue` to add `TableData`, `Validation`, `Extraction` entities. +- Refactored `argilla-frontend/components/base/base-render-table/RenderTable.vue` to add `TableData`, `Validation`, `Extraction` entities. ## [Argilla] [2.4.1](https://github.com/argilla-io/argilla/compare/v2.4.0...v2.4.1) diff --git a/extralit-frontend/README.md b/extralit-frontend/README.md index 45755b136..c4aec5bf2 100644 --- a/extralit-frontend/README.md +++ b/extralit-frontend/README.md @@ -72,4 +72,4 @@ To help our community contribute effectively, we have created comprehensive [dev ## πŸ—ΊοΈ Roadmap -We maintain an open [roadmap](https://github.com/orgs/extralit/projects/1/views/1) and welcome community input on our development priorities. Feel free to participate in discussions and share your ideas. \ No newline at end of file +We maintain an open [roadmap](https://github.com/orgs/extralit/projects/1/views/1) and welcome community input on our development priorities. Feel free to participate in discussions and share your ideas. diff --git a/extralit-frontend/assets/css/fonts.css b/extralit-frontend/assets/css/fonts.css index b5e792658..a78561946 100644 --- a/extralit-frontend/assets/css/fonts.css +++ b/extralit-frontend/assets/css/fonts.css @@ -1,6 +1,7 @@ @font-face { font-family: "raptor_v2_premiumbold"; - src: url("/fonts/raptorv2premium-bold-webfont.woff2") format("woff2"), + src: + url("/fonts/raptorv2premium-bold-webfont.woff2") format("woff2"), url("/fonts/raptorv2premium-bold-webfont.woff") format("woff"); font-weight: normal; font-style: normal; diff --git a/extralit-frontend/assets/css/themes.css b/extralit-frontend/assets/css/themes.css index 2a932ca03..cb243c308 100644 --- a/extralit-frontend/assets/css/themes.css +++ b/extralit-frontend/assets/css/themes.css @@ -59,11 +59,7 @@ --bg-field: linear-gradient(45deg, var(--bg-opacity-2), var(--bg-opacity-1)); --border-field: hsl(0, 0%, 94%); --bg-bubble: linear-gradient(45deg, var(--bg-opacity-3), var(--bg-opacity-1)); - --bg-bubble-inverse: linear-gradient( - 45deg, - var(--bg-opacity-1), - var(--bg-opacity-3) - ); + --bg-bubble-inverse: linear-gradient(45deg, var(--bg-opacity-1), var(--bg-opacity-3)); --bg-form: hsl(0, 0%, 100%); --bg-form-button-area: hsl(228, 50%, 96%); --fg-shortcut-key: hsla(0, 0%, 0%, 0.2); @@ -71,11 +67,7 @@ --fg-similarity: hsl(3, 100%, 69%); --fg-highlight: hsl(3, 100%, 69%); --bg-auth: hsl(18, 57%, 91%); - --bg-auth-gradient: linear-gradient( - 178.31deg, - #ffe5d9 1.36%, - #ffd1bc 109.14% - ); + --bg-auth-gradient: linear-gradient(178.31deg, #ffe5d9 1.36%, #ffd1bc 109.14%); --bg-banner-info: hsl(0, 0%, 90%); --bg-banner-warning: hsl(47, 83%, 91%); --bg-banner-error: hsl(3, 100%, 92%); @@ -139,11 +131,7 @@ --bg-field: linear-gradient(45deg, var(--bg-opacity-4), var(--bg-opacity-2)); --border-field: hsl(0, 0%, 18%); --bg-bubble: linear-gradient(45deg, var(--bg-opacity-4), var(--bg-opacity-2)); - --bg-field-inverse: linear-gradient( - 45deg, - var(--bg-opacity-2), - var(--bg-opacity-4) - ); + --bg-field-inverse: linear-gradient(45deg, var(--bg-opacity-2), var(--bg-opacity-4)); --bg-form: hsl(216 12% 14%); --bg-form-button-area: hsla(228, 40.18%, 59.31%, 0.2); --fg-shortcut-key: hsla(0, 0%, 100%, 0.4); @@ -230,11 +218,7 @@ --bg-field: linear-gradient(45deg, var(--bg-opacity-2), var(--bg-opacity-1)); --border-field: hsl(0, 0%, 94%); --bg-bubble: linear-gradient(45deg, var(--bg-opacity-3), var(--bg-opacity-1)); - --bg-bubble-inverse: linear-gradient( - 45deg, - var(--bg-opacity-1), - var(--bg-opacity-3) - ); + --bg-bubble-inverse: linear-gradient(45deg, var(--bg-opacity-1), var(--bg-opacity-3)); --bg-form: hsl(0, 0%, 100%); --bg-form-button-area: hsl(228, 50%, 96%); --fg-shortcut-key: hsla(0, 0%, 0%, 0.2); @@ -242,11 +226,7 @@ --fg-similarity: hsl(3, 100%, 50%); --fg-highlight: hsl(3, 100%, 49%); --bg-auth: hsl(18, 57%, 91%); - --bg-auth-gradient: linear-gradient( - 178.31deg, - #ffe5d9 1.36%, - #ffd1bc 109.14% - ); + --bg-auth-gradient: linear-gradient(178.31deg, #ffe5d9 1.36%, #ffd1bc 109.14%); --bg-banner-info: hsl(0, 0%, 90%); --bg-banner-warning: hsl(47, 83%, 91%); --bg-banner-error: hsl(3, 100%, 92%); diff --git a/extralit-frontend/assets/scss/abstract/mixins/_grid-mixins.scss b/extralit-frontend/assets/scss/abstract/mixins/_grid-mixins.scss index 5d4d03195..88ec48723 100644 --- a/extralit-frontend/assets/scss/abstract/mixins/_grid-mixins.scss +++ b/extralit-frontend/assets/scss/abstract/mixins/_grid-mixins.scss @@ -7,160 +7,184 @@ // MIT License // =================================================================== -@mixin grid($display: flex, $flex-direction: null, $flex-wrap: null, $flex-flow: null, $justify-content: null, $align-items: null, $align-content: null, $gutter: null, $grid-type: skeleton) -{ - box-sizing: border-box; - - @if $display { - display: $display; - } - - @if $flex-direction { - flex-direction: $flex-direction; - } - @if $flex-wrap { - flex-wrap: $flex-wrap; - } - - @if $flex-flow { - flex-flow: $flex-flow; - } - - @if $justify-content { - justify-content: $justify-content; - } - @if $align-items { - align-items: $align-items; - } - @if $align-content { - align-content: $align-content; - } - - @if $grid-type == skeleton { - @if $gutter { - @include grid-margin($margin: 0 $gutter / 2 * -1); - } - } - - @content; +@mixin grid( + $display: flex, + $flex-direction: null, + $flex-wrap: null, + $flex-flow: null, + $justify-content: null, + $align-items: null, + $align-content: null, + $gutter: null, + $grid-type: skeleton +) { + box-sizing: border-box; + + @if $display { + display: $display; + } + + @if $flex-direction { + flex-direction: $flex-direction; + } + @if $flex-wrap { + flex-wrap: $flex-wrap; + } + + @if $flex-flow { + flex-flow: $flex-flow; + } + + @if $justify-content { + justify-content: $justify-content; + } + @if $align-items { + align-items: $align-items; + } + @if $align-content { + align-content: $align-content; + } + + @if $grid-type == skeleton { + @if $gutter { + @include grid-margin($margin: 0 $gutter / 2 * -1); + } + } + + @content; } -@mixin grid-col($col: null, $grid-columns: 12, $col-offset: null, $gutter: null, $condensed: false, $align-self: null, $flex-grow: 0, $flex-shrink: 1, $flex-basis: auto, $order: null, $grid-type: skeleton, $last-child: false) -{ - box-sizing: border-box; - - @if type-of($col) == number and unitless($col) == true { - $flex-grow: 0; - $flex-shrink: 0; - $flex-basis: percentage($col / $grid-columns); - - @if $grid-type == skeleton { - @if $gutter and unit($gutter) == '%' { - $flex-basis: $flex-basis - $gutter; - } @else if $gutter and unitless($gutter) == false { - $flex-basis: calc( #{$flex-basis} - #{$gutter}); - } - - } @else if $grid-type == margin-offset { - @if $gutter and unit($gutter) == '%' { - $flex-basis: (100% - ($gutter * ($grid-columns / $col - 1))) / ($grid-columns / $col); - } @else if $gutter and unitless($gutter) == false { - $flex-basis: calc( #{$flex-basis} - #{$gutter * ($grid-columns / $col - 1) / ($grid-columns / $col)}); - } - } - - @if $col-offset and unit($col-offset) == '%' { - $flex-basis: $flex-basis + $col-offset; - } @else if $col-offset and unitless($col-offset) == false { - $flex-basis: calc( #{$flex-basis} + #{$col-offset}); - } - } @else if type-of($col) == number and unitless($col) == false { - $flex-grow: 0; - $flex-shrink: 0; - $flex-basis: $col; - } @else if type-of($col) == string and $col == 'auto' { - $flex-grow: 1; - $flex-shrink: 1; - $flex-basis: auto; - max-width: 100%; - width: auto; - // flex: 1; - } @else if type-of($col) == string and $col == 'equal' { - $flex-grow: 1; - $flex-shrink: 1; - $flex-basis: 0; - // flex: none; - } @else if type-of($col) == string and $col == 'none' { - $flex-grow: 0; - $flex-shrink: 0; - $flex-basis: auto; - // flex: initial; - } @else if type-of($col) == string and $col == 'initial' { - $flex-grow: 0; - $flex-shrink: 1; - $flex-basis: auto; - } @else if type-of($col) == string and $col == 'breakpoint' { - $flex-grow: 0; - $flex-shrink: 1; - $flex-basis: auto; - width: 100%; - } - - flex: $flex-grow $flex-shrink $flex-basis; - - @if $align-self { - align-self: $align-self; - } - - @if type-of($order) == number { - order: $order; - } - - @if $gutter and unitless($gutter) == false { - @if $grid-type == skeleton { - @if $condensed == true { - @include grid-col-margin($margin: 0 $gutter / 2); - } @else { - @include grid-col-margin($margin: 0 $gutter / 2 $gutter); - } - } @else if $grid-type == margin-offset { - @if type-of($col) == string and $col == 'breakpoint' { - @include grid-col-margin($margin-right: 0); - } @else if $last-child { - @include grid-col-margin($margin-right: 0); - } @else { - @include grid-col-margin($margin-right: $gutter); - } - - @if $condensed == false { - @include grid-col-margin($margin-bottom: $gutter); - } - } - } - - @content; +@mixin grid-col( + $col: null, + $grid-columns: 12, + $col-offset: null, + $gutter: null, + $condensed: false, + $align-self: null, + $flex-grow: 0, + $flex-shrink: 1, + $flex-basis: auto, + $order: null, + $grid-type: skeleton, + $last-child: false +) { + box-sizing: border-box; + + @if type-of($col) == number and unitless($col) == true { + $flex-grow: 0; + $flex-shrink: 0; + $flex-basis: percentage($col / $grid-columns); + + @if $grid-type == skeleton { + @if $gutter and unit($gutter) == "%" { + $flex-basis: $flex-basis - $gutter; + } @else if $gutter and unitless($gutter) == false { + $flex-basis: calc(#{$flex-basis} - #{$gutter}); + } + } @else if $grid-type == margin-offset { + @if $gutter and unit($gutter) == "%" { + $flex-basis: (100% - ($gutter * ($grid-columns / $col - 1))) / ($grid-columns / $col); + } @else if $gutter and unitless($gutter) == false { + $flex-basis: calc(#{$flex-basis} - #{$gutter * ($grid-columns / $col - 1) / ($grid-columns / $col)}); + } + } + + @if $col-offset and unit($col-offset) == "%" { + $flex-basis: $flex-basis + $col-offset; + } @else if $col-offset and unitless($col-offset) == false { + $flex-basis: calc(#{$flex-basis} + #{$col-offset}); + } + } @else if type-of($col) == number and unitless($col) == false { + $flex-grow: 0; + $flex-shrink: 0; + $flex-basis: $col; + } @else if type-of($col) == string and $col == "auto" { + $flex-grow: 1; + $flex-shrink: 1; + $flex-basis: auto; + max-width: 100%; + width: auto; + // flex: 1; + } @else if type-of($col) == string and $col == "equal" { + $flex-grow: 1; + $flex-shrink: 1; + $flex-basis: 0; + // flex: none; + } @else if type-of($col) == string and $col == "none" { + $flex-grow: 0; + $flex-shrink: 0; + $flex-basis: auto; + // flex: initial; + } @else if type-of($col) == string and $col == "initial" { + $flex-grow: 0; + $flex-shrink: 1; + $flex-basis: auto; + } @else if type-of($col) == string and $col == "breakpoint" { + $flex-grow: 0; + $flex-shrink: 1; + $flex-basis: auto; + width: 100%; + } + + flex: $flex-grow $flex-shrink $flex-basis; + + @if $align-self { + align-self: $align-self; + } + + @if type-of($order) == number { + order: $order; + } + + @if $gutter and unitless($gutter) == false { + @if $grid-type == skeleton { + @if $condensed == true { + @include grid-col-margin($margin: 0 $gutter / 2); + } @else { + @include grid-col-margin($margin: 0 $gutter / 2 $gutter); + } + } @else if $grid-type == margin-offset { + @if type-of($col) == string and $col == "breakpoint" { + @include grid-col-margin($margin-right: 0); + } @else if $last-child { + @include grid-col-margin($margin-right: 0); + } @else { + @include grid-col-margin($margin-right: $gutter); + } + + @if $condensed == false { + @include grid-col-margin($margin-bottom: $gutter); + } + } + } + + @content; } -@mixin grid-margin($margin: null, $margin-top: null, $margin-right: null, $margin-bottom: null, $margin-left: null) -{ - @if $margin != null { - margin: $margin; - } - @if $margin-top != null { - margin-top: $margin-top; - } - @if $margin-bottom != null { - margin-bottom: $margin-bottom; - } - @if $margin-left != null { - margin-left: $margin-left; - } - @if $margin-right != null { - margin-right: $margin-right; - } +@mixin grid-margin($margin: null, $margin-top: null, $margin-right: null, $margin-bottom: null, $margin-left: null) { + @if $margin != null { + margin: $margin; + } + @if $margin-top != null { + margin-top: $margin-top; + } + @if $margin-bottom != null { + margin-bottom: $margin-bottom; + } + @if $margin-left != null { + margin-left: $margin-left; + } + @if $margin-right != null { + margin-right: $margin-right; + } } -@mixin grid-col-margin($margin: null, $margin-top: null, $margin-right: null, $margin-bottom: null, $margin-left: null) -{ - @include grid-margin($margin, $margin-top, $margin-right, $margin-bottom, $margin-left); +@mixin grid-col-margin( + $margin: null, + $margin-top: null, + $margin-right: null, + $margin-bottom: null, + $margin-left: null +) { + @include grid-margin($margin, $margin-top, $margin-right, $margin-bottom, $margin-left); } diff --git a/extralit-frontend/assets/scss/abstract/mixins/_media-queries.scss b/extralit-frontend/assets/scss/abstract/mixins/_media-queries.scss index 4aaff0145..25cf31289 100644 --- a/extralit-frontend/assets/scss/abstract/mixins/_media-queries.scss +++ b/extralit-frontend/assets/scss/abstract/mixins/_media-queries.scss @@ -34,10 +34,8 @@ $media-expressions: ( "handheld": "handheld", "landscape": "(orientation: landscape)", "portrait": "(orientation: portrait)", - "retina2x": - "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx)", - "retina3x": - "(-webkit-min-device-pixel-ratio: 3), (min-resolution: 350dpi), (min-resolution: 3dppx)", + "retina2x": "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx)", + "retina3x": "(-webkit-min-device-pixel-ratio: 3), (min-resolution: 350dpi), (min-resolution: 3dppx)", ) !default; // Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals @@ -325,7 +323,7 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; } @for $i from 1 through str-length($value) { $character: str-slice($value, $i, $i); - @if not(index(map-keys($numbers), $character) or $character== ".") { + @if not (index(map-keys($numbers), $character) or $character== ".") { @return to-length(if($minus, -$result, $result), str-slice($value, $i)); } @if $character== "." { @@ -405,10 +403,7 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; $global-media-expressions: $media-expressions; // Update global configuration $breakpoints: map-merge($breakpoints, $tweakpoints) !global; - $media-expressions: map-merge( - $media-expressions, - $tweak-media-expressions - ) !global; + $media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global; @content; // Restore global configuration $breakpoints: $global-breakpoints !global; @@ -492,10 +487,7 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; $global-breakpoints: $breakpoints; $global-media-expressions: $media-expressions; // Update global configuration $breakpoints: map-merge($breakpoints, $tweakpoints) !global; - $media-expressions: map-merge( - $media-expressions, - $tweak-media-expressions - ) !global; + $media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global; @content; // Restore global configuration $breakpoints: $global-breakpoints !global; $media-expressions: $global-media-expressions !global; diff --git a/extralit-frontend/assets/scss/abstract/mixins/_mixins.scss b/extralit-frontend/assets/scss/abstract/mixins/_mixins.scss index da3f6d55b..c18865d4f 100644 --- a/extralit-frontend/assets/scss/abstract/mixins/_mixins.scss +++ b/extralit-frontend/assets/scss/abstract/mixins/_mixins.scss @@ -141,8 +141,7 @@ $direction: normal, $fill-mode: forwards ) { - animation: $name $timing-function $duration $delay $iteration-count $direction - $fill-mode; + animation: $name $timing-function $duration $delay $iteration-count $direction $fill-mode; } // Adds transition @@ -154,27 +153,10 @@ // // Creates two color stops, start and end, by specifying a color and position for each color stop. // Color stops are not available in IE9 and below. -@mixin gradient-vertical( - $start-color: #555, - $end-color: #333, - $start-percent: 0%, - $end-percent: 100% -) { - background-image: -webkit-linear-gradient( - top, - $start-color $start-percent, - $end-color $end-percent - ); // Safari 5.1-6, Chrome 10+ - background-image: -o-linear-gradient( - top, - $start-color $start-percent, - $end-color $end-percent - ); // Opera 12 - background-image: linear-gradient( - to bottom, - $start-color $start-percent, - $end-color $end-percent - ); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ +@mixin gradient-vertical($start-color: #555, $end-color: #333, $start-percent: 0%, $end-percent: 100%) { + background-image: -webkit-linear-gradient(top, $start-color $start-percent, $end-color $end-percent); // Safari 5.1-6, Chrome 10+ + background-image: -o-linear-gradient(top, $start-color $start-percent, $end-color $end-percent); // Opera 12 + background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start-color)}', endColorstr='#{ie-hex-str($end-color)}', GradientType=0); // IE9 and down } diff --git a/extralit-frontend/assets/scss/abstract/placeholders/_tooltip.scss b/extralit-frontend/assets/scss/abstract/placeholders/_tooltip.scss index aeff1d188..1d0756855 100644 --- a/extralit-frontend/assets/scss/abstract/placeholders/_tooltip.scss +++ b/extralit-frontend/assets/scss/abstract/placeholders/_tooltip.scss @@ -45,12 +45,7 @@ $tooltip-border-radius: $border-radius-s; &:before { right: calc(50% - $tooltip-triangle-size); top: 100%; - @include triangle( - top, - $tooltip-triangle-size, - $tooltip-triangle-size, - $tooltip-bg - ); + @include triangle(top, $tooltip-triangle-size, $tooltip-triangle-size, $tooltip-bg); } } %has-tooltip--top { @@ -63,12 +58,7 @@ $tooltip-border-radius: $border-radius-s; &:before { right: calc(50% - $tooltip-triangle-size); top: -10px; - @include triangle( - bottom, - $tooltip-triangle-size, - $tooltip-triangle-size, - $tooltip-bg - ); + @include triangle(bottom, $tooltip-triangle-size, $tooltip-triangle-size, $tooltip-bg); } } %has-tooltip--right { @@ -128,24 +118,14 @@ $tooltip-border-radius: $border-radius-s; left: calc(100% + $tooltip-triangle-size/2); top: 50%; transform: translateY(-50%); - @include triangle( - left, - $tooltip-triangle-size, - $tooltip-triangle-size, - $tooltip-bg - ); + @include triangle(left, $tooltip-triangle-size, $tooltip-triangle-size, $tooltip-bg); } %triangle-right { right: calc(100% + $tooltip-triangle-size/2); top: 50%; transform: translateY(-50%); - @include triangle( - right, - $tooltip-triangle-size, - $tooltip-triangle-size, - $tooltip-bg - ); + @include triangle(right, $tooltip-triangle-size, $tooltip-triangle-size, $tooltip-bg); } @mixin tooltip-mini($position, $offset: 4px) { diff --git a/extralit-frontend/assets/scss/abstract/variables/_variables.scss b/extralit-frontend/assets/scss/abstract/variables/_variables.scss index bf65bb9e8..f978c99ea 100644 --- a/extralit-frontend/assets/scss/abstract/variables/_variables.scss +++ b/extralit-frontend/assets/scss/abstract/variables/_variables.scss @@ -1,8 +1,7 @@ // Fonts //----------- $primary-font-family: "Inter", "Helvetica", "Arial", sans-serif; -$secondary-font-family: "raptor_v2_premiumbold", "Helvetica", "Arial", - sans-serif; +$secondary-font-family: "raptor_v2_premiumbold", "Helvetica", "Arial", sans-serif; $tertiary-font-family: "Roboto Condensed", sans-serif; $quaternary-font-family: "Roboto Mono", monospace; $base-font-size: 14px; diff --git a/extralit-frontend/components/base/base-render-table/RenderTable.vue b/extralit-frontend/components/base/base-render-table/RenderTable.vue index 6dddda673..7f8920e93 100644 --- a/extralit-frontend/components/base/base-render-table/RenderTable.vue +++ b/extralit-frontend/components/base/base-render-table/RenderTable.vue @@ -168,8 +168,7 @@ export default { validation: { handler(newValidation, oldValidation) { if (this.isLoaded) { - if (this.editable) - console.warn("Changes validation", this.tableJSON.schema.schemaName); + if (this.editable) console.warn("Changes validation", this.tableJSON.schema.schemaName); this.tabulator?.setColumns(this.columnsConfig); this.validateTable(); } diff --git a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js index 4af681de9..9ae5b06e9 100644 --- a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js +++ b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js @@ -80,43 +80,45 @@ describe("DatasetConfiguration", () => { dataset: mockDataset, dataSource: "hub", }, - global: { stubs: { - HorizontalResizable: { - template: ` + global: { + stubs: { + HorizontalResizable: { + template: `
`, - props: ["id", "min-height-percent", "top-percent-height"], - }, - VerticalResizable: { - template: ` + props: ["id", "min-height-percent", "top-percent-height"], + }, + VerticalResizable: { + template: `
`, - props: ["id", "left-percent-width"], - }, - Record: { - template: '
Record Preview
', - props: ["recordCriteria", "record"], - }, - QuestionsComponent: { - template: '
Questions
', - props: ["visible-shortcuts", "questions"], - }, - DatasetConfigurationForm: { - template: '
Configuration Form
', - props: ["dataset"], - }, - ImportHistoryDataPreview: { - template: '
Import Preview
', - props: ["import-history-details", "loading", "error"], + props: ["id", "left-percent-width"], + }, + Record: { + template: '
Record Preview
', + props: ["recordCriteria", "record"], + }, + QuestionsComponent: { + template: '
Questions
', + props: ["visible-shortcuts", "questions"], + }, + DatasetConfigurationForm: { + template: '
Configuration Form
', + props: ["dataset"], + }, + ImportHistoryDataPreview: { + template: '
Import Preview
', + props: ["import-history-details", "loading", "error"], + }, + BaseIcon: true, }, - BaseIcon: true, - } }, + }, }); }); @@ -147,43 +149,45 @@ describe("DatasetConfiguration", () => { dataSource: "import", importData: mockImportHistoryDetails, }, - global: { stubs: { - HorizontalResizable: { - template: ` + global: { + stubs: { + HorizontalResizable: { + template: `
`, - props: ["id", "min-height-percent", "top-percent-height"], - }, - VerticalResizable: { - template: ` + props: ["id", "min-height-percent", "top-percent-height"], + }, + VerticalResizable: { + template: `
`, - props: ["id", "left-percent-width"], - }, - Record: { - template: '
Record Preview
', - props: ["recordCriteria", "record"], - }, - QuestionsComponent: { - template: '
Questions
', - props: ["visible-shortcuts", "questions"], - }, - DatasetConfigurationForm: { - template: '
Configuration Form
', - props: ["dataset"], - }, - ImportHistoryDataPreview: { - template: '
Import Preview
', - props: ["import-history-details", "loading", "error"], + props: ["id", "left-percent-width"], + }, + Record: { + template: '
Record Preview
', + props: ["recordCriteria", "record"], + }, + QuestionsComponent: { + template: '
Questions
', + props: ["visible-shortcuts", "questions"], + }, + DatasetConfigurationForm: { + template: '
Configuration Form
', + props: ["dataset"], + }, + ImportHistoryDataPreview: { + template: '
Import Preview
', + props: ["import-history-details", "loading", "error"], + }, + BaseIcon: true, }, - BaseIcon: true, - } }, + }, }); }); @@ -225,32 +229,34 @@ describe("DatasetConfiguration", () => { dataset: { ...mockDataset, repoId: null }, dataSource: "hub", }, - global: { stubs: { - HorizontalResizable: { - template: ` + global: { + stubs: { + HorizontalResizable: { + template: `
`, - }, - VerticalResizable: { - template: ` + }, + VerticalResizable: { + template: `
`, + }, + Record: true, + QuestionsComponent: true, + DatasetConfigurationForm: true, + ImportHistoryDataPreview: true, + BaseIcon: { + template: '
', + props: ["icon-name"], + }, }, - Record: true, - QuestionsComponent: true, - DatasetConfigurationForm: true, - ImportHistoryDataPreview: true, - BaseIcon: { - template: '
', - props: ["icon-name"], - }, - } }, + }, }); }); @@ -269,29 +275,31 @@ describe("DatasetConfiguration", () => { dataset: datasetWithoutQuestions, dataSource: "hub", }, - global: { stubs: { - HorizontalResizable: { - template: ` + global: { + stubs: { + HorizontalResizable: { + template: `
`, - }, - VerticalResizable: { - template: ` + }, + VerticalResizable: { + template: `
`, + }, + Record: true, + QuestionsComponent: true, + DatasetConfigurationForm: true, + ImportHistoryDataPreview: true, + BaseIcon: true, }, - Record: true, - QuestionsComponent: true, - DatasetConfigurationForm: true, - ImportHistoryDataPreview: true, - BaseIcon: true, - } }, + }, }); expect(wrapper.find(".dataset-config__empty-questions").exists()).toBe(true); @@ -304,32 +312,34 @@ describe("DatasetConfiguration", () => { dataset: mockDataset, dataSource: "hub", }, - global: { stubs: { - HorizontalResizable: { - template: ` + global: { + stubs: { + HorizontalResizable: { + template: `
`, - }, - VerticalResizable: { - template: ` + }, + VerticalResizable: { + template: `
`, + }, + Record: true, + QuestionsComponent: { + template: '
Questions Component
', + props: ["visible-shortcuts", "questions"], + }, + DatasetConfigurationForm: true, + ImportHistoryDataPreview: true, + BaseIcon: true, }, - Record: true, - QuestionsComponent: { - template: '
Questions Component
', - props: ["visible-shortcuts", "questions"], - }, - DatasetConfigurationForm: true, - ImportHistoryDataPreview: true, - BaseIcon: true, - } }, + }, }); expect(wrapper.find(".mock-questions").exists()).toBe(true); @@ -345,32 +355,33 @@ describe("DatasetConfiguration", () => { dataSource: "import", importData: mockImportHistoryDetails, }, - global: { stubs: { - HorizontalResizable: { - template: ` + global: { + stubs: { + HorizontalResizable: { + template: `
`, - }, - VerticalResizable: { - template: ` + }, + VerticalResizable: { + template: `
`, - }, - Record: true, - QuestionsComponent: true, - DatasetConfigurationForm: { - template: - "
Configuration Form
", - props: ["dataset"], - }, - ImportHistoryDataPreview: { - template: ` + }, + Record: true, + QuestionsComponent: true, + DatasetConfigurationForm: { + template: + "
Configuration Form
", + props: ["dataset"], + }, + ImportHistoryDataPreview: { + template: `
{ Import Preview
`, - props: ["import-history-details", "loading", "error"], + props: ["import-history-details", "loading", "error"], + }, + BaseIcon: true, }, - BaseIcon: true, - } }, + }, }); }); @@ -425,19 +437,21 @@ describe("DatasetConfiguration", () => { dataSource: "import", importData: mockImportHistoryDetails, }, - global: { stubs: { - HorizontalResizable: { - template: `
`, - }, - VerticalResizable: { - template: `
`, + global: { + stubs: { + HorizontalResizable: { + template: `
`, + }, + VerticalResizable: { + template: `
`, + }, + Record: true, + QuestionsComponent: true, + DatasetConfigurationForm: true, + ImportHistoryDataPreview: true, + BaseIcon: true, }, - Record: true, - QuestionsComponent: true, - DatasetConfigurationForm: true, - ImportHistoryDataPreview: true, - BaseIcon: true, - } }, + }, }); }); @@ -499,15 +513,17 @@ describe("DatasetConfiguration", () => { dataSource: "import", importData: mockImportHistoryDetails, }, - global: { stubs: { - HorizontalResizable: { template: `
` }, - VerticalResizable: { template: `
` }, - Record: true, - QuestionsComponent: true, - DatasetConfigurationForm: true, - ImportHistoryDataPreview: true, - BaseIcon: true, - } }, + global: { + stubs: { + HorizontalResizable: { template: `
` }, + VerticalResizable: { template: `
` }, + Record: true, + QuestionsComponent: true, + DatasetConfigurationForm: true, + ImportHistoryDataPreview: true, + BaseIcon: true, + }, + }, }); }).not.toThrow(); diff --git a/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.spec.js b/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.spec.js index 18f62fb54..296e256cf 100644 --- a/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.spec.js +++ b/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.spec.js @@ -70,15 +70,17 @@ describe("ImportAnalysisTable", () => { workspace: mockWorkspace, loading: false, }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: true, - BaseSimpleTable: { - template: '
', - props: ["data", "columns", "options"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: true, + BaseSimpleTable: { + template: '
', + props: ["data", "columns", "options"], + }, }, - } }, + }, }); }); @@ -171,12 +173,14 @@ describe("ImportAnalysisTable", () => { workspace: mockWorkspace, loading: true, }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: true, - BaseSimpleTable: true, - } }, + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: true, + BaseSimpleTable: true, + }, + }, }); expect(wrapper.find(".loading-state").exists()).toBe(true); @@ -201,12 +205,14 @@ describe("ImportAnalysisTable", () => { workspace: mockWorkspace, loading: false, }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: true, - BaseSimpleTable: true, - } }, + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: true, + BaseSimpleTable: true, + }, + }, }); expect(wrapper.find(".error-state").exists()).toBe(true); diff --git a/extralit-frontend/components/features/import/history/ImportHistoryDataPreview.spec.js b/extralit-frontend/components/features/import/history/ImportHistoryDataPreview.spec.js index 888c523e6..383a107ff 100644 --- a/extralit-frontend/components/features/import/history/ImportHistoryDataPreview.spec.js +++ b/extralit-frontend/components/features/import/history/ImportHistoryDataPreview.spec.js @@ -72,11 +72,13 @@ describe("ImportHistoryDataPreview", () => { loading: true, importHistoryDetails: null, }, - global: { stubs: { - BaseSpinnerComponent: { - template: '
Loading...
', + global: { + stubs: { + BaseSpinnerComponent: { + template: '
Loading...
', + }, }, - } }, + }, }); expect(wrapper.find(".loading-state").exists()).toBe(true); @@ -93,16 +95,18 @@ describe("ImportHistoryDataPreview", () => { error: "Failed to load import data", importHistoryDetails: null, }, - global: { stubs: { - BaseIcon: { - template: '
', - props: ["icon-name"], - }, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseIcon: { + template: '
', + props: ["icon-name"], + }, + BaseButton: { + template: '', + props: ["variant"], + }, }, - } }, + }, }); expect(wrapper.find(".error-state").exists()).toBe(true); @@ -118,14 +122,16 @@ describe("ImportHistoryDataPreview", () => { error: "Network error", importHistoryDetails: null, }, - global: { stubs: { - BaseIcon: true, - BaseButton: { - template: "", - props: ["variant"], - emits: ["click"], + global: { + stubs: { + BaseIcon: true, + BaseButton: { + template: "", + props: ["variant"], + emits: ["click"], + }, }, - } }, + }, }); await wrapper.find("button").trigger("click"); @@ -143,9 +149,11 @@ describe("ImportHistoryDataPreview", () => { error: null, importHistoryDetails: null, }, - global: { stubs: { - BaseIcon: true, - } }, + global: { + stubs: { + BaseIcon: true, + }, + }, }); expect(wrapper.find(".empty-state").exists()).toBe(true); @@ -162,12 +170,14 @@ describe("ImportHistoryDataPreview", () => { error: null, importHistoryDetails: mockImportHistoryDetails, }, - global: { stubs: { - BaseSimpleTable: { - template: '
', - props: ["data", "columns", "options", "loading"], + global: { + stubs: { + BaseSimpleTable: { + template: '
', + props: ["data", "columns", "options", "loading"], + }, }, - } }, + }, }); }); @@ -230,9 +240,11 @@ describe("ImportHistoryDataPreview", () => { error: null, importHistoryDetails: mockImportHistoryDetails, }, - global: { stubs: { - BaseSimpleTable: true, - } }, + global: { + stubs: { + BaseSimpleTable: true, + }, + }, }); }); @@ -284,9 +296,11 @@ describe("ImportHistoryDataPreview", () => { error: null, importHistoryDetails: mockImportHistoryDetails, }, - global: { stubs: { - BaseSimpleTable: true, - } }, + global: { + stubs: { + BaseSimpleTable: true, + }, + }, }); }); @@ -336,9 +350,11 @@ describe("ImportHistoryDataPreview", () => { error: null, importHistoryDetails: mockImportHistoryDetails, }, - global: { stubs: { - BaseSimpleTable: true, - } }, + global: { + stubs: { + BaseSimpleTable: true, + }, + }, }); }); @@ -378,9 +394,11 @@ describe("ImportHistoryDataPreview", () => { error: null, importHistoryDetails: mockImportHistoryDetails, }, - global: { stubs: { - BaseSimpleTable: true, - } }, + global: { + stubs: { + BaseSimpleTable: true, + }, + }, }); }); @@ -426,9 +444,11 @@ describe("ImportHistoryDataPreview", () => { error: null, importHistoryDetails: mockImportHistoryDetails, }, - global: { stubs: { - BaseSimpleTable: true, - } }, + global: { + stubs: { + BaseSimpleTable: true, + }, + }, }); // Should not throw error and should show fallback text @@ -445,9 +465,11 @@ describe("ImportHistoryDataPreview", () => { error: null, importHistoryDetails: mockImportHistoryDetails, }, - global: { stubs: { - BaseSimpleTable: true, - } }, + global: { + stubs: { + BaseSimpleTable: true, + }, + }, }); expect(wrapper.vm.tableColumns).toEqual([]); @@ -462,9 +484,11 @@ describe("ImportHistoryDataPreview", () => { error: null, importHistoryDetails: mockImportHistoryDetails, }, - global: { stubs: { - BaseSimpleTable: true, - } }, + global: { + stubs: { + BaseSimpleTable: true, + }, + }, }); expect(wrapper.vm.tableData).toEqual([]); diff --git a/extralit-frontend/components/features/import/recent/RecentImportCard.spec.js b/extralit-frontend/components/features/import/recent/RecentImportCard.spec.js index 5dd2e823f..c73937a52 100644 --- a/extralit-frontend/components/features/import/recent/RecentImportCard.spec.js +++ b/extralit-frontend/components/features/import/recent/RecentImportCard.spec.js @@ -46,15 +46,17 @@ describe("RecentImportCard Component", () => { it("should render the component with correct structure", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); expect(wrapper.find(".recent-import-card").exists()).toBe(true); @@ -66,15 +68,17 @@ describe("RecentImportCard Component", () => { it("should display filename correctly", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const filename = wrapper.find(".recent-import-card__filename"); @@ -84,15 +88,17 @@ describe("RecentImportCard Component", () => { it("should display statistics correctly", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const stats = wrapper.findAll(".recent-import-card__stat"); @@ -120,15 +126,17 @@ describe("RecentImportCard Component", () => { it("should not display failed stat when failed_count is 0", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecordNoFailures }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const stats = wrapper.findAll(".recent-import-card__stat"); @@ -148,15 +156,17 @@ describe("RecentImportCard Component", () => { wrapper = mount(RecentImportCard, { props: { importRecord: recentRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const dateElement = wrapper.find(".recent-import-card__date"); @@ -171,15 +181,17 @@ describe("RecentImportCard Component", () => { wrapper = mount(RecentImportCard, { props: { importRecord: hoursAgoRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const dateElement = wrapper.find(".recent-import-card__date"); @@ -194,15 +206,17 @@ describe("RecentImportCard Component", () => { wrapper = mount(RecentImportCard, { props: { importRecord: yesterdayRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const dateElement = wrapper.find(".recent-import-card__date"); @@ -217,15 +231,17 @@ describe("RecentImportCard Component", () => { wrapper = mount(RecentImportCard, { props: { importRecord: daysAgoRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const dateElement = wrapper.find(".recent-import-card__date"); @@ -240,15 +256,17 @@ describe("RecentImportCard Component", () => { wrapper = mount(RecentImportCard, { props: { importRecord: oldRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const dateElement = wrapper.find(".recent-import-card__date"); @@ -264,15 +282,17 @@ describe("RecentImportCard Component", () => { wrapper = mount(RecentImportCard, { props: { importRecord: invalidDateRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const dateElement = wrapper.find(".recent-import-card__date"); @@ -284,16 +304,18 @@ describe("RecentImportCard Component", () => { it("should emit click event when card is clicked", async () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - emits: ["click"], - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + emits: ["click"], + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const button = wrapper.find(".mock-base-button"); @@ -308,15 +330,17 @@ describe("RecentImportCard Component", () => { it("should calculate totalPapers correctly", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); expect(wrapper.vm.totalPapers).toBe(15); @@ -330,15 +354,17 @@ describe("RecentImportCard Component", () => { wrapper = mount(RecentImportCard, { props: { importRecord: recordWithoutTotal }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); expect(wrapper.vm.totalPapers).toBe(0); @@ -349,15 +375,17 @@ describe("RecentImportCard Component", () => { it("should apply correct CSS classes", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); expect(wrapper.find(".recent-import-card").exists()).toBe(true); @@ -371,15 +399,17 @@ describe("RecentImportCard Component", () => { it("should apply success styling to success stat", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const successStat = wrapper.find(".recent-import-card__stat--success"); @@ -389,15 +419,17 @@ describe("RecentImportCard Component", () => { it("should apply failed styling to failed stat when failures exist", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const failedStat = wrapper.find(".recent-import-card__stat--failed"); @@ -409,15 +441,17 @@ describe("RecentImportCard Component", () => { it("should have responsive structure for different screen sizes", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); // Verify that elements that need responsive behavior are present @@ -431,15 +465,17 @@ describe("RecentImportCard Component", () => { it("should have proper heading structure", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const filename = wrapper.find(".recent-import-card__filename"); @@ -449,15 +485,17 @@ describe("RecentImportCard Component", () => { it("should provide meaningful text content", () => { wrapper = mount(RecentImportCard, { props: { importRecord: mockImportRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); // Check that all text content is meaningful @@ -480,15 +518,17 @@ describe("RecentImportCard Component", () => { wrapper = mount(RecentImportCard, { props: { importRecord: longFilenameRecord }, - global: { stubs: { - BaseButton: { - template: '', - }, - BaseIcon: { - template: '
', - props: ["iconName"], - }, - } }, + global: { + stubs: { + BaseButton: { + template: '', + }, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, }); const filename = wrapper.find(".recent-import-card__filename"); diff --git a/extralit-frontend/components/features/import/recent/RecentImports.spec.js b/extralit-frontend/components/features/import/recent/RecentImports.spec.js index 5a9413e2e..f8b22aa80 100644 --- a/extralit-frontend/components/features/import/recent/RecentImports.spec.js +++ b/extralit-frontend/components/features/import/recent/RecentImports.spec.js @@ -75,15 +75,17 @@ describe("RecentImports Component", () => { it("should render the component with correct header", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); expect(wrapper.find(".recent-imports__title").text()).toBe("Recent Imports"); @@ -95,18 +97,20 @@ describe("RecentImports Component", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: { + template: '
', + props: ["importRecord"], + }, }, - RecentImportCard: { - template: '
', - props: ["importRecord"], - }, - } }, + }, }); const importCards = wrapper.findAll(".mock-recent-import-card"); @@ -116,15 +120,17 @@ describe("RecentImports Component", () => { it("should display action buttons", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); const viewAllButton = wrapper.find(".recent-imports__view-all-btn"); @@ -139,17 +145,19 @@ describe("RecentImports Component", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinnerComponent: { - template: '
Loading...
', - }, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinnerComponent: { + template: '
Loading...
', + }, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); expect(wrapper.find(".recent-imports__loading").exists()).toBe(true); @@ -162,15 +170,17 @@ describe("RecentImports Component", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); expect(wrapper.find(".recent-imports__loading").exists()).toBe(false); @@ -183,18 +193,20 @@ describe("RecentImports Component", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: { - template: '
', - props: ["iconName"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: { + template: '
', + props: ["iconName"], + }, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - BaseButton: { - template: '', - props: ["variant"], - }, - RecentImportCard: true, - } }, + }, }); const errorSection = wrapper.find(".recent-imports__error"); @@ -208,15 +220,17 @@ describe("RecentImports Component", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); const retryButton = wrapper.find(".recent-imports__error .mock-base-button"); @@ -232,15 +246,17 @@ describe("RecentImports Component", () => { wrapper = mount(RecentImports, { props: { workspace: null }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); const noWorkspaceSection = wrapper.find(".recent-imports__no-workspace"); @@ -254,15 +270,17 @@ describe("RecentImports Component", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); expect(wrapper.find(".recent-imports__no-workspace").exists()).toBe(false); @@ -277,15 +295,17 @@ describe("RecentImports Component", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); const emptySection = wrapper.find(".recent-imports__empty"); @@ -303,18 +323,20 @@ describe("RecentImports Component", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], - }, - RecentImportCard: { - template: '
', - props: ["importRecord"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: { + template: '
', + props: ["importRecord"], + }, }, - } }, + }, }); const firstCard = wrapper.find(".mock-recent-import-card"); @@ -327,15 +349,17 @@ describe("RecentImports Component", () => { it("should emit view-all-imports when View All Imports button is clicked", async () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); const viewAllButton = wrapper.find(".recent-imports__view-all-btn"); @@ -349,15 +373,17 @@ describe("RecentImports Component", () => { it("should call useRecentImportsViewModel with correct props", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); expect(useRecentImportsViewModel).toHaveBeenCalledWith({ workspace: mockWorkspace }); @@ -366,15 +392,17 @@ describe("RecentImports Component", () => { it("should handle workspace prop changes", async () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); const newWorkspace = { id: "workspace-2", name: "New Workspace" }; @@ -389,15 +417,17 @@ describe("RecentImports Component", () => { it("should apply responsive classes correctly", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); // Check that the component has the main class for responsive styling @@ -410,15 +440,17 @@ describe("RecentImports Component", () => { // This test verifies the component structure that supports responsive design wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); // Verify that responsive elements are present @@ -438,15 +470,17 @@ describe("RecentImports Component", () => { it("should have proper heading structure", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); const title = wrapper.find(".recent-imports__title"); @@ -458,15 +492,17 @@ describe("RecentImports Component", () => { wrapper = mount(RecentImports, { props: { workspace: mockWorkspace }, - global: { stubs: { - BaseSpinner: true, - BaseIcon: true, - BaseButton: { - template: '', - props: ["variant"], + global: { + stubs: { + BaseSpinner: true, + BaseIcon: true, + BaseButton: { + template: '', + props: ["variant"], + }, + RecentImportCard: true, }, - RecentImportCard: true, - } }, + }, }); const errorSection = wrapper.find(".recent-imports__error"); diff --git a/extralit-frontend/docs/shortcuts.md b/extralit-frontend/docs/shortcuts.md index ccfbd82fb..bb17267ec 100644 --- a/extralit-frontend/docs/shortcuts.md +++ b/extralit-frontend/docs/shortcuts.md @@ -1,21 +1,21 @@ You can speed up your annotation by using these keyboard shortcuts: -| Action | Keys | -| -------------------------------------- | --------------------------------- | -| Activate form | `β‡₯ Tab` | -| Move between questions (Other) | `Ctrl` `↓` or `Ctrl` `↑` | -| Move between questions (Mac os) | `⌘ Cmd` `↓` or `⌘ Cmd` `↑` | -| Select and unselect label | `1`, `2`, `3` | -| Move between labels or ranking options | `β‡₯ Tab` or `⇧ Shift` `β‡₯ Tab` | -| Select rating and rank | `1`, `2`, `3` | -| Fit span to character selection | Hold `⇧ Shift` | -| Activate text area | `⇧ Shift` `↡ Enter` | -| Exit text area | `Esc` | -| Discard (Mac os) | `⌘ Cmd` `⌫ Backspace` | -| Discard (Other) | `Ctrl` `⌫ Backspace` | -| Save draft (Mac os) | `⌘ Cmd` `S` | -| Save draft (Other) | `Ctrl` `S` | -| Move between pages (Other) | `Ctrl` `β†’` or `Ctrl` `←` | -| Move between pages (Mac os) | `⌘ Cmd` `β†’` or `⌘ Cmd` `←` | -| Submit (Mac os) | `⌘ Cmd` `↡ Enter` | -| Submit (Other) | `Ctrl` `↡ Enter` | +| Action | Keys | +| -------------------------------------- | ---------------------------- | +| Activate form | `β‡₯ Tab` | +| Move between questions (Other) | `Ctrl` `↓` or `Ctrl` `↑` | +| Move between questions (Mac os) | `⌘ Cmd` `↓` or `⌘ Cmd` `↑` | +| Select and unselect label | `1`, `2`, `3` | +| Move between labels or ranking options | `β‡₯ Tab` or `⇧ Shift` `β‡₯ Tab` | +| Select rating and rank | `1`, `2`, `3` | +| Fit span to character selection | Hold `⇧ Shift` | +| Activate text area | `⇧ Shift` `↡ Enter` | +| Exit text area | `Esc` | +| Discard (Mac os) | `⌘ Cmd` `⌫ Backspace` | +| Discard (Other) | `Ctrl` `⌫ Backspace` | +| Save draft (Mac os) | `⌘ Cmd` `S` | +| Save draft (Other) | `Ctrl` `S` | +| Move between pages (Other) | `Ctrl` `β†’` or `Ctrl` `←` | +| Move between pages (Mac os) | `⌘ Cmd` `β†’` or `⌘ Cmd` `←` | +| Submit (Mac os) | `⌘ Cmd` `↡ Enter` | +| Submit (Other) | `Ctrl` `↡ Enter` | 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 index ce8a41213..8065f30d2 100644 --- 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 @@ -4,7 +4,7 @@ **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`. +**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). @@ -17,7 +17,7 @@ ## 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. +- **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`). @@ -26,6 +26,7 @@ ## 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 @@ -37,6 +38,7 @@ - `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` @@ -44,6 +46,7 @@ - `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` --- @@ -57,12 +60,14 @@ - [ ] **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** @@ -72,6 +77,7 @@ 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)** @@ -92,6 +98,7 @@ No code change; proceed. 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** @@ -106,11 +113,13 @@ Keep unchanged: `axios`, `pinia`, `@codescouts/events`, `@jonnytran/vue-pdf-view - [ ] **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** @@ -118,9 +127,11 @@ Keep `e2e*`, `lint`, `format*`, `generate-icons` (revisit `generate-icons` in Ta ```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: )" @@ -139,6 +150,7 @@ git commit -m "build: swap dependency set for Vue 3 / Nuxt 4 (pinned: - [ ] **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"; @@ -195,8 +207,7 @@ export default defineNuxtConfig({ runtimeConfig: { public: { clientVersion: pkg.version, - communityLink: - "https://join.slack.com/t/extralit/shared_invite/zt-3gw1ah8bl-AiVNrkIVYOL4yVGOxN8WFw", + 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", @@ -223,9 +234,11 @@ export default defineNuxtConfig({ 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, @@ -240,11 +253,13 @@ export default { **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` @@ -260,6 +275,7 @@ export default { **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"; @@ -274,6 +290,7 @@ describe("format-number", () => { }); }); ``` + (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.) @@ -291,6 +308,7 @@ describe("format-number", () => { **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"; @@ -303,12 +321,15 @@ describe("toast bus", () => { }); }); ``` + - [ ] **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"` @@ -317,6 +338,7 @@ export default mitt(); **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: @@ -325,6 +347,7 @@ 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"` @@ -334,6 +357,7 @@ Preserve the directive's existing positioning/show/hide behavior exactly; only t **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"; @@ -343,7 +367,11 @@ export default defineNuxtPlugin((nuxtApp) => { 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))); + if (handler) + stops.set( + el, + onClickOutside(el, (e) => handler(e)) + ); }, unmounted(el) { stops.get(el)?.(); @@ -352,6 +380,7 @@ export default defineNuxtPlugin((nuxtApp) => { }); }); ``` + 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"` @@ -363,6 +392,7 @@ Move this file to `plugins/click-outside.ts` (Nuxt 4 auto-registers `plugins/*.t - [ ] **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"; @@ -375,25 +405,37 @@ describe("BaseSvgIcon", () => { }); }); ``` + - [ ] **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"; @@ -402,7 +444,9 @@ export default defineNuxtPlugin((nuxtApp) => { 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 @@ -412,6 +456,7 @@ This keeps all 79 `` call sites unchanged. Delete `plugins/directives/s - [ ] **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 @@ -419,6 +464,7 @@ export default defineNuxtRouteMiddleware((to) => { // ...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"` @@ -428,6 +474,7 @@ Keep the original conditional logic byte-for-byte; only the framework calls chan **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"; @@ -446,9 +493,11 @@ export const loadErrorHandler = (axios: AxiosInstance, t: (k: string) => string) ); }; ``` + 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"; @@ -460,7 +509,11 @@ export interface PublicAxiosInstance extends 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 } }); + const pub = axios.create({ + baseURL: base.defaults.baseURL, + withCredentials: false, + headers: { Authorization: undefined }, + }); if (config.enableErrors) loadErrorHandler(pub, t); loadCache(pub); return pub; @@ -469,9 +522,11 @@ export const useAxiosExtension = (base: AxiosInstance, t: (k: string) => string) 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"; @@ -496,14 +551,17 @@ export default defineNuxtPlugin((nuxtApp) => { 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"` @@ -515,14 +573,24 @@ Then grep to confirm no `@nuxtjs/axios` references remain. Do **not** change any - [ ] **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; } }); + 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); @@ -544,8 +612,10 @@ describe("AuthService", () => { }); }); ``` + - [ ] **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"; @@ -553,16 +623,31 @@ 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; } + 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"; @@ -571,6 +656,7 @@ export default defineNuxtPlugin((nuxtApp) => { 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"` @@ -586,10 +672,12 @@ export default defineNuxtPlugin((nuxtApp) => { - [ ] **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. @@ -621,13 +709,15 @@ Then for each file still importing `useRoute`, `useRouter`, `useContext`, `useFe - [ ] **Step 4: `@jonnytran/vue-pdf-viewer`.** Confirm it renders under Vue 3 (it was pinned for the Vue-2 app). If it imports `vue@2`, replace with a Vue-3-compatible PDF viewer or load the component client-only. Flag to the user if a swap is needed (this is the one library with real uncertainty). - [ ] **Step 5: `.md` loader (replaces `frontmatter-markdown-loader`).** `AnnotationHelpShortcut.vue` currently does `require.context("../../../../docs/", false, /^[^_]+\.md$/, "lazy")` then `await folderContent("./shortcuts.md")` to get rendered HTML. Replace the webpack `require.context` with a Vite glob of raw markdown + `marked` (already a dep): + ```ts import { marked } from "marked"; const docs = import.meta.glob("~/docs/*.md", { query: "?raw", import: "default" }); // ...where it loaded shortcuts.md: const raw = (await docs["/docs/shortcuts.md"]()) as string; -const html = marked.parse(raw); // assign to the same ref that held folderContent body +const html = marked.parse(raw); // assign to the same ref that held folderContent body ``` + Confirm the glob key path (`/docs/shortcuts.md`) by logging `Object.keys(docs)` once. Preserve `dompurify` sanitization if the original sanitized before injecting. - [ ] **Step 6: Commit** `git commit -m "refactor: port draggable, tiptap, tabulator/interactjs, md-loader to Vue 3"` @@ -641,16 +731,20 @@ Confirm the glob key path (`/docs/shortcuts.md`) by logging `Object.keys(docs)` **Files:** as needed to fix runtime errors - [ ] **Step 1: Prepare + boot.** + ```bash npx nuxi prepare npm run dev ``` + Fix compile/boot errors iteratively. Common ones: stray Nuxt-2 `this.$nuxt`/`this.$axios` in components (replace with `useNuxtApp().$axios`), `process.client` β†’ `import.meta.client`, `$config` β†’ `useRuntimeConfig()`. Work until `http://localhost:3000` renders the sign-in page. - [ ] **Step 2: Run the e2e suite on chromium only.** + ```bash npx playwright test --project=chromium ``` + The suite mocks the backend (`e2e/common/*-api-mock.ts`), so no server is needed. - [ ] **Step 3: Drive failures to green page-by-page**, in this order (matches `e2e/` dirs): `login-page`, `datasets-page`, `dataset-setting-page`, `annotation-mode-page`, `import-configuration-workflow`, `user-setting-page`. For each failing spec, fix the underlying component/adapter β€” not the test. If a screenshot baseline differs only by antialiasing within `maxDiffPixelRatio: 0.1`, it passes; if structurally different, fix the component. @@ -666,6 +760,7 @@ The suite mocks the backend (`e2e/common/*-api-mock.ts`), so no server is needed **Files:** Create `vitest.config.ts`, `test/setup.ts` - [ ] **Step 1: `vitest.config.ts`:** + ```ts import { defineVitestConfig } from "@nuxt/test-utils/config"; @@ -679,6 +774,7 @@ export default defineVitestConfig({ }, }); ``` + - [ ] **Step 2: `test/setup.ts`** β€” port anything `jest.setup.ts` did (timezone is set via env; replicate global mocks/stubs). Add stubs for `tabulator-tables` (the old `__mocks__/tabulator-tables.js`) via `vi.mock` if specs rely on it. - [ ] **Step 3: Smoke** `npx vitest run v1/infrastructure/services/format-number.test.ts` β†’ passes (from Task 6). Commit `git commit -m "test: add Vitest + @nuxt/test-utils config"` @@ -709,6 +805,7 @@ export default defineVitestConfig({ - [ ] **Step 3: Production build.** `npm run build` β†’ succeeds. Then `npm run generate` if static output is used by deployment (the HF-space bundle). - [ ] **Step 4: Final acceptance (Definition of Done, spec Β§8):** + ```bash npx vitest run # green npx playwright test --project=chromium # green @@ -716,6 +813,7 @@ npm run lint # clean npx nuxi typecheck # clean npm run build # succeeds ``` + All five green. Commit `git commit -m "chore: lint/type/build clean on Vue 3 / Nuxt 4"` - [ ] **Step 5: Update docs.** Edit `extralit-frontend/CLAUDE.md`: remove "Vuex β†’ Pinia migration in progress" stale note, update Key Technologies to Nuxt 4 / Vue 3 / Vitest, update test commands (`npm run test` now = Vitest). Commit `git commit -m "docs: update frontend CLAUDE.md for Nuxt 4 stack"` @@ -732,10 +830,10 @@ All five green. Commit `git commit -m "chore: lint/type/build clean on Vue 3 / N ## Risk register (carry into execution) -| Risk | Trigger | Mitigation | -|---|---|---| -| `@jonnytran/vue-pdf-viewer` is Vue-2-only | npm install peer error or runtime crash | Task 16 Step 4 β€” swap viewer or client-only load; flag to user early | -| `assets/scss/abstract.scss` not `@use`-safe | Vite scss build error in Task 4 | Refactor into a pure partial (vars/mixins only) | -| i18n v9 `useFetch`-style lazy loading change | Locale not switching | Verify `i18n.config.ts` + `langDir`; spot-check all 4 locales in UI | -| `useFetch` semantic change breaks data load | Blank pages in Task 17 | Task 14 Step 3 β€” convert to `useAsyncData` or `onMounted` per file | -| Plugin ordering (axios before auth before di) | `$auth`/`$axios` undefined at DI load | numeric filename prefixes `1.auth`,`2.axios`,`3.di` | +| Risk | Trigger | Mitigation | +| --------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------- | +| `@jonnytran/vue-pdf-viewer` is Vue-2-only | npm install peer error or runtime crash | Task 16 Step 4 β€” swap viewer or client-only load; flag to user early | +| `assets/scss/abstract.scss` not `@use`-safe | Vite scss build error in Task 4 | Refactor into a pure partial (vars/mixins only) | +| i18n v9 `useFetch`-style lazy loading change | Locale not switching | Verify `i18n.config.ts` + `langDir`; spot-check all 4 locales in UI | +| `useFetch` semantic change breaks data load | Blank pages in Task 17 | Task 14 Step 3 β€” convert to `useAsyncData` or `onMounted` per file | +| Plugin ordering (axios before auth before di) | `$auth`/`$axios` undefined at DI load | numeric filename prefixes `1.auth`,`2.axios`,`3.di` | diff --git a/extralit-frontend/docs/superpowers/plans/2026-06-13-vue3-remediation.md b/extralit-frontend/docs/superpowers/plans/2026-06-13-vue3-remediation.md index 22b65d710..3073475c1 100644 --- a/extralit-frontend/docs/superpowers/plans/2026-06-13-vue3-remediation.md +++ b/extralit-frontend/docs/superpowers/plans/2026-06-13-vue3-remediation.md @@ -11,22 +11,27 @@ Source review IDs (roborev): 2,3,4,5,6 substantive; 8,9,11 = prior-agent "CLI bl ## TIER 1 β€” Real bugs (must fix) ### T1.1 β€” Dynamic routes: `_param` β†’ `[param]` (Nuxt 4 file-router) β€” PENDING FINAL VERIFY -Nuxt 4 requires bracket dynamic segments; `_id.vue` becomes a *literal* `/dataset/_id` route, so `/dataset/` 404s. Static `/sign-in` works (why the app "boots"), but no dynamic flow was ever runtime-tested (handoff gap #5; e2e never green). **Verify via `nuxi prepare` route manifest once Nuxt 4 is installed before executing.** + +Nuxt 4 requires bracket dynamic segments; `_id.vue` becomes a _literal_ `/dataset/_id` route, so `/dataset/` 404s. Static `/sign-in` works (why the app "boots"), but no dynamic flow was ever runtime-tested (handoff gap #5; e2e never green). **Verify via `nuxi prepare` route manifest once Nuxt 4 is installed before executing.** Renames (use `git mv`): + - `pages/new/_id.vue` β†’ `pages/new/[id].vue` - `pages/new/hf/_repoId.vue` β†’ `pages/new/hf/[repoId].vue` - `pages/new/import/_id.vue` β†’ `pages/new/import/[id].vue` - `pages/oauth/_provider/` β†’ `pages/oauth/[provider]/` - `pages/dataset/_id/` (subtree) β†’ `pages/dataset/[id]/` -Then: audit `middleware/01.route-guard.global.ts` (+`02.me`) route-name switches, all `` / `router.push`/`useRoute().params`, and `useRoute().params.id` access. Confirm generated route names. Gate: typecheck + boot + navigate a dataset route in dev. + Then: audit `middleware/01.route-guard.global.ts` (+`02.me`) route-name switches, all `` / `router.push`/`useRoute().params`, and `useRoute().params.id` access. Confirm generated route names. Gate: typecheck + boot + navigate a dataset route in dev. ### T1.2 β€” Span entity factory leak (review 3, MEDIUM) + `v1/.../useSpanAnnotationTextFieldViewModel.ts:48-71` `entityComponentFactory` does `createApp().mount()` but never `unmount()`. `Highlighting.applyEntityStyle()` (`highlighting.ts:323-344`) tears down + recreates on every scroll/resize/hover/span-change β†’ unbounded live apps + orphaned scroll listeners (`EntityComponent.beforeUnmount` never fires). Fix: factory returns `{ element, unmount }`; caller unmounts before `removeChild` and in `Highlighting.unmount()`. ### T1.3 β€” `head()` options hook (B5) + `pages/dataset/[id]/annotation-mode/index.vue:44` uses Nuxt-2 `head(){...}` (no-op in Nuxt 4; title never set). Replace with `useHead(() => ({ title: ... }))` in `setup()`. ### T1.4 β€” base-date snapshot pollution (review 5, MEDIUM) + `components/base/base-date/base-date.test.ts` `toMatchSnapshot()` on the VueWrapper β†’ 2400+ lines incl. leaked absolute path `/home/jonny/...` and `"version":"3.5.38"`. Fix: `expect(baseDate.html()).toMatchSnapshot()` (all 10 assertions), drop `vi.useFakeTimers("modern")` arg, regenerate snapshots, delete stale Jest-keyed entries in `base-date.test.ts.snap` + `BaseSlider.spec.js.snap`. --- @@ -34,26 +39,33 @@ Then: audit `middleware/01.route-guard.global.ts` (+`02.me`) route-name switches ## TIER 2 β€” Real correctness / bounded (should fix) ### T2.1 β€” Transition CSS class rename (C2, 9 files) + Vue 3 renamed `.x-enter`β†’`.x-enter-from`, `.x-leave-to` unchanged. 9 files use old `.x-enter` (silently ignored β†’ broken enter animation): BaseFlowModal, BaseModal, Toast, BaseCardWithTabs, DatasetUpdateDialog, EntityLabelSelection.component, LabelSelection.component, DatasetFilters (`.filterAppear-enter`), ImportFromHub (`.slide-right-enter`). Rename selectors. ### T2.2 β€” `$slots.default` array API (C3, 2 files) + Vue 3: `$slots.default` is a function; VNodes lack `.elm`/`.tag`/`.context`. + - `components/base/base-scroll/SynchronizeScroll.vue:15` reads `.tag`/`.elm` β†’ rewrite via template ref / `onMounted` DOM collection. - `RatingShortcuts.vue:17` reads `$slots.default[0].context?.question...` β†’ refactor to receive `question` as a prop. ### T2.3 β€” `v-html` into ``-breakout sink. TextField.vue:435, ChatField.vue:303, SpanAnnotationTextField.vue:365 β†’ bind as text child `{{ highlightStyles }}` instead. Also drop stray `:key="id"` (TextField vs ChatField inconsistency). ### T2.4 β€” `$attrs` class/style (C5, 4 files) β€” VERIFY each + Vue 3 `$attrs` now includes class/style. Audit BaseBadge, EntityBadge, FilterTooltip, FilterBadge: if they `v-bind="$attrs"` on a non-root element, add `inheritAttrs:false` to avoid double class/style. Fix only the ones that actually double-bind. ### T2.5 β€” Config cleanups + - `.eslintrc.js`: `localeDir: "./translation/*.json"` β†’ `*.js` (i18n lint is currently a no-op) (A5). - `nuxt.config.ts`: remove `pdfjs-dist` from `build.transpile` (dead β€” not a dep) (A7). - `package.json`: remove `core-js` dep (no source import; only vendored handlebars references it) (A1). - `package.json` overrides: verify `@intlify/*` pins match transitive names in `package-lock.json`; drop any inert pin (e.g. nonexistent `@intlify/core`) (review 2). ### T2.6 β€” Nuxt error page (B2) + `layouts/error.vue` is only a named layout, not Nuxt's error page. Create root `error.vue` (move/adapt), replace options `layout:"error"` with `definePageMeta`/``, update legacy ``β†’`` if present. --- @@ -61,23 +73,28 @@ Vue 3 `$attrs` now includes class/style. Audit BaseBadge, EntityBadge, FilterToo ## TIER 3 β€” Best-practice, scoped (judgment) ### T3.1 β€” `emits` declarations (C1) β€” SCOPED, not blanket -105 files `$emit` without `emits`. Blanket-editing all 105 is over-reach. **Scope to the dangerous subset:** components emitting *native-DOM-event names* (`click`,`input`,`change`,`focus`,`blur`,`submit`,`keydown`,`mouseover`,`mouseleave`,`mousedown`,`scroll`) β€” without `emits` these fall through to the root element and double-fire. Audit + fix that subset (esp. base/* inputs/buttons). Pure custom-event components: defer to a follow-up note unless trivially batched. + +105 files `$emit` without `emits`. Blanket-editing all 105 is over-reach. **Scope to the dangerous subset:** components emitting _native-DOM-event names_ (`click`,`input`,`change`,`focus`,`blur`,`submit`,`keydown`,`mouseover`,`mouseleave`,`mousedown`,`scroll`) β€” without `emits` these fall through to the root element and double-fire. Audit + fix that subset (esp. base/* inputs/buttons). Pure custom-event components: defer to a follow-up note unless trivially batched. ### T3.2 β€” `runtimeConfig.public.apiBaseUrl` (A6) β€” optional + `API_BASE_URL` is read at build time only β†’ HF/Docker can't repoint backend without rebuild. Add `runtimeConfig.public.apiBaseUrl` consumed in `plugins/2.axios.ts`; keep `/api` dev proxy. Enables `NUXT_PUBLIC_API_BASE_URL` override. Only if it doesn't destabilize the axios DI wiring. ### T3.3 β€” Review 4 (type-only imports) β€” RESPOND, likely no code change + `nuxi typecheck` already passes at 0 with the deliberate `strict:false`/`verbatimModuleSyntax:false` posture. The reviewer's "value-used-as-type" risk is covered by the passing typecheck + verified boot. Do a quick targeted check (grep flagged `type`-only specifiers for runtime value use); if clean, close with rationale rather than re-enabling strict (separate hardening effort per handoff #4). --- ## Out of scope (note as follow-ups, do not do here) + - Playwright e2e reconciliation (handoff #2) β€” dedicated effort, can't run on this host. - PDF viewer real implementation (handoff #3) β€” user-owned dep rebuild. - Full `strict:true` TS hardening (handoff #4). - `@nuxt/eslint` module swap (A4) / eslintrc ESM (A3) β€” lint passes; enhancement only. ## Verification gate (run after each tier, from `extralit-frontend/`, `NUXT_IGNORE_LOCK=1` prefix) + 1. `npx vitest run` β€” β‰₯735 pass 2. `npx nuxi typecheck` β€” 0 errors 3. `npm run lint` β€” 0 errors @@ -85,4 +102,5 @@ Vue 3 `$attrs` now includes class/style. Audit BaseBadge, EntityBadge, FilterToo 5. Boot `npm run dev`, drive a dynamic route (datasets/annotation) via the CDP browser for T1.1/T1.2. ## Closing roborev + After fixes + green gates: `roborev comment`/`roborev close` reviews 2,3,5,6 (fixed), 4 (responded), 8/9/11 (noise). Needs CLI approval. 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 index 897296f6b..b44bda3e1 100644 --- 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 @@ -12,6 +12,7 @@ Migrate `extralit-frontend` from Vue 2.7 / Nuxt 2.18 to **Vue 3.5 / Nuxt 4**, re 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) @@ -24,30 +25,30 @@ 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** | +| 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 @@ -55,16 +56,16 @@ 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 | +| 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`, @@ -79,6 +80,7 @@ with ts-injecty DI). The migration deliberately keeps that layer's interfaces st 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`). @@ -89,6 +91,7 @@ swaps the **infrastructure adapters** that touch Nuxt/Vue. Three adapter swaps: 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. @@ -98,12 +101,14 @@ swaps the **infrastructure adapters** that touch Nuxt/Vue. Three adapter swaps: 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. @@ -114,6 +119,7 @@ swaps the **infrastructure adapters** that touch Nuxt/Vue. Three adapter swaps: - `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`. @@ -144,6 +150,7 @@ To keep it bisectable despite that, work proceeds in a **fixed dependency order* 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` / ``. @@ -154,6 +161,7 @@ To keep it bisectable despite that, work proceeds in a **fixed dependency order* - **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 @@ -163,6 +171,7 @@ To keep it bisectable despite that, work proceeds in a **fixed dependency order* 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. diff --git a/extralit-frontend/e2e/extraction/README.md b/extralit-frontend/e2e/extraction/README.md index b7c7314d2..72fd6ddb1 100644 --- a/extralit-frontend/e2e/extraction/README.md +++ b/extralit-frontend/e2e/extraction/README.md @@ -2,7 +2,7 @@ Prereqs: full local stack up (`docker-compose up -d`, server on :6900), then: -1. Seed: `npm run e2e:extraction:seed` +1. Seed: `npm run e2e:extraction:seed` 2. Dev server reachable from the browser container: `npm run dev -- --host` 3. Run: ```bash diff --git a/extralit-frontend/eslint.config.mjs b/extralit-frontend/eslint.config.mjs index 22b0102c0..2135cd13b 100644 --- a/extralit-frontend/eslint.config.mjs +++ b/extralit-frontend/eslint.config.mjs @@ -159,7 +159,6 @@ export default [ }, }, - // ── Rules newly promoted to `error` by this toolchain bump (eslint 8->10, // eslint-plugin-vue 8->10) that the previous setup either did not enable or that // eslint-plugin-nuxt's preset disabled. They flag PRE-EXISTING patterns (legacy diff --git a/extralit-frontend/extension/popup.html b/extralit-frontend/extension/popup.html index a42d35316..625ca3c2c 100644 --- a/extralit-frontend/extension/popup.html +++ b/extralit-frontend/extension/popup.html @@ -1,11 +1,8 @@ - + Extralit - +