diff --git a/.github/actions/download-frontend-artifact/action.yml b/.github/actions/download-frontend-artifact/action.yml new file mode 100644 index 000000000..d86b0541b --- /dev/null +++ b/.github/actions/download-frontend-artifact/action.yml @@ -0,0 +1,25 @@ +name: Download prebuilt frontend statics +description: >- + Download the latest successful extralit-frontend.yml SPA build into + extralit-frontend/.output/public, so a server build can bake it into the wheel + (and the HF Space built FROM it) as the bundled fallback UI instead of running + npm. The SPA is env-agnostic (relative /api), so the same artifact works + regardless of where the server is deployed. + +inputs: + branch: + description: Branch whose latest successful frontend build to fetch (main or develop). + required: false + default: develop + +runs: + using: composite + steps: + - uses: dawidd6/action-download-artifact@v21 + with: + workflow: extralit-frontend.yml + branch: ${{ inputs.branch }} + name: extralit-frontend + path: extralit-frontend/.output/public + workflow_conclusion: success + if_no_artifact_found: fail diff --git a/.github/workflows/extralit-frontend.build-push-dev-frontend-docker.yml b/.github/workflows/extralit-frontend.build-push-dev-frontend-docker.yml deleted file mode 100644 index 42d17e889..000000000 --- a/.github/workflows/extralit-frontend.build-push-dev-frontend-docker.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: Build Extralit Docker image - -on: - workflow_call: - inputs: - download-python-package: - description: "True if python package should be downloaded" - type: boolean - default: false - image-name: - description: "Name of the image to build" - required: true - type: string - dockerfile: - description: "Path to the Dockerfile to build" - required: true - type: string - platforms: - description: "Platforms to build for" - required: true - type: string - build-args: - description: "Build arguments" - required: false - type: string - default: "" - readme: - description: "Path to the README file" - required: false - type: string - default: "README.md" - outputs: - version: - description: "Version of the Docker image" - value: ${{ jobs.build.outputs.version }} - google-docker-image: - description: The name of the Docker image uploaded to Google Artifact Registry. - value: ${{ jobs.build.outputs.google-docker-image }} - -jobs: - build: - name: Build Docker image - runs-on: ubuntu-latest - defaults: - run: - working-directory: extralit-frontend - - # Grant permissions to `GITHUB_TOKEN` for Google Cloud Workload Identity Provider - permissions: - contents: read - id-token: write - - outputs: - version: ${{ steps.docker-image-tag-from-ref.outputs.docker-image-tag }} - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "18" - - - name: Build Frontend - run: | - npm install - npm run build - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Get Docker image tag from GITHUB_REF - id: docker-image-tag-from-ref - uses: ./.github/actions/docker-image-tag-from-ref - - - name: Generate Docker tags - id: generate-docker-tags - run: | - DOCKER_HUB_TAG="$IMAGE_NAME:$DOCKER_IMAGE_TAG" - - echo "tags=$DOCKER_HUB_TAG" >> $GITHUB_OUTPUT - env: - IMAGE_NAME: ${{ inputs.image-name }} - DOCKER_IMAGE_TAG: ${{ steps.docker-image-tag-from-ref.outputs.docker-image-tag }} - - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.AR_DOCKER_USERNAME }} - password: ${{ secrets.AR_DOCKER_PASSWORD }} - - # Authenticate in GCP using Workload Identity Federation, so we can push the Docker image to the Google Cloud Artifact Registry - # - name: Authenticate to Google Cloud - # id: google-auth - # uses: "google-github-actions/auth@v1" - # with: - # token_format: access_token - # workload_identity_provider: ${{ secrets.GOOGLE_CLOUD_WIP }} - # service_account: ${{ secrets.GOOGLE_CLOUD_SERVICE_ACCOUNT }} - - # - name: Login to Google Artifact Registry - # uses: docker/login-action@v2 - # with: - # registry: europe-docker.pkg.dev - # username: oauth2accesstoken - # password: ${{ steps.google-auth.outputs.access_token }} - - - name: Build and push - uses: docker/build-push-action@v4 - with: - context: extralit-frontend - file: ${{ inputs.dockerfile }} - platforms: ${{ inputs.platforms }} - tags: ${{ steps.generate-docker-tags.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: ${{ inputs.build-args }} - push: true diff --git a/.github/workflows/extralit-frontend.build-push-dev.yml b/.github/workflows/extralit-frontend.build-push-dev.yml new file mode 100644 index 000000000..dc84e421a --- /dev/null +++ b/.github/workflows/extralit-frontend.build-push-dev.yml @@ -0,0 +1,148 @@ +name: Deploy PR preview HF Space + +# Builds a dev-tagged extralit-server image (pr-) and dispatches the HF Space repo to +# deploy an ephemeral Space at https://huggingface.co/spaces/extralit-dev/pr-. +# +# Deliberately decoupled from extralit-server.yml so it does NOT run on every push: +# - auto: only when a PR (touching the server or frontend) is marked "ready for review" +# - manual: workflow_dispatch with a PR number, to (re)deploy a preview on demand +# main/develop deploys keep flowing through extralit-server.yml as before. + +on: + # Note: `ready_for_review` only fires on a draftβ†’ready transition. A PR opened + # directly as non-draft does NOT auto-trigger β€” mark it ready, or use the manual + # workflow_dispatch below. This is intentional: previews never build on push. + pull_request: + types: [ready_for_review] + paths: + - "extralit-server/**" + - "extralit-frontend/**" + - ".github/workflows/extralit-frontend.build-push-dev.yml" + - ".github/workflows/extralit-server.build-docker-images.yml" + + workflow_dispatch: + inputs: + pr_number: + description: "PR number to deploy to extralit-dev/pr-" + required: true + type: string + +concurrency: + # One preview build per PR; cancel superseded runs. + group: pr-preview-${{ github.event.pull_request.number || inputs.pr_number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build `extralit-server` package (frontend baked in) + runs-on: ubuntu-latest + # Fork PRs don't receive secrets (Docker login / dispatch would fail), so skip them. + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.fork == false + + defaults: + run: + shell: bash -l {0} + working-directory: extralit-server + + steps: + - name: Checkout Code πŸ›Ž + uses: actions/checkout@v4 + with: + # Auto path: default merge ref (refs/pull//merge). Manual path: resolve the PR's + # merge ref from pr_number so the build is self-contained β€” no need to run from the + # PR's head branch for the pr- image/Space to reflect that PR's code. + ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/pull/{0}/merge', inputs.pr_number) || github.ref }} + + - name: Install uv + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 + with: + pyproject-file: "extralit-server/pyproject.toml" + enable-cache: true + cache-local-path: ~/.cache/uv + cache-dependency-glob: "extralit-server/uv.lock" + + # No pytest here β€” preview-only path. The live frontend preview for this PR is published to + # Vercel by extralit-frontend.yml; the HF Space only needs a bundled fallback UI, so bake + # the prebuilt develop frontend artifact instead of rebuilding from source. + - name: Download prebuilt frontend statics + uses: ./.github/actions/download-frontend-artifact + with: + branch: develop + + - name: Build package + run: | + set -euo pipefail + # Bake the compiled SPA into the wheel's static dir. Fail loudly if it's missing or + # empty, otherwise the server ships with no statics and 404s every route. + cp -r ../extralit-frontend/.output/public src/extralit_server/static + test -f src/extralit_server/static/index.html + uv build + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: extralit-server + path: extralit-server/dist + + build_docker_images: + name: Build docker images + needs: + - build + uses: ./.github/workflows/extralit-server.build-docker-images.yml + with: + is_release: false + publish_latest: false + # PR number on BOTH paths (manual input, or the PR number on the auto path). This forces + # IMAGE_TAG=pr-, DISPATCH_BRANCH=/merge, and crucially PUBLISH_LATEST=false so a + # preview never republishes the dev :latest tag from unreviewed PR code. + pr_number: ${{ inputs.pr_number || github.event.pull_request.number }} + secrets: inherit + + # Opportunistic: this PR has its own ephemeral HF Space (extralit-dev/pr-), so point the + # PR's Vercel preview at it instead of the shared develop backend. Vercel's native Git + # integration builds the preview; we just set a branch-scoped Preview env var (read by + # extralit-frontend/vercel.ts at build time) and redeploy the branch's latest preview so it + # takes effect now. Best-effort β€” never blocks the preview pipeline. + point-preview-at-pr-space: + name: Point Vercel preview at this PR's HF Space + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.fork == false + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ inputs.pr_number || github.event.pull_request.number }} + HEAD_REF: ${{ github.head_ref }} + steps: + - name: Set branch-scoped preview backend + redeploy + continue-on-error: true + run: | + set -uo pipefail + if [[ -z "${VERCEL_TOKEN:-}" || -z "${VERCEL_PROJECT_ID:-}" ]]; then + echo "Vercel secrets not configured; skipping preview override."; exit 0 + fi + # Resolve the PR head branch (manual dispatch only carries the PR number). + BRANCH="${HEAD_REF:-}" + if [[ -z "$BRANCH" ]]; then + BRANCH=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json headRefName -q .headRefName) + fi + SPACE_URL="https://extralit-dev-pr-${PR_NUMBER}.hf.space" + echo "Pointing preview for branch '$BRANCH' at $SPACE_URL" + npm install -g vercel@latest + # Durable: branch-scoped Preview env var so every build of this branch targets its Space. + # (Branch-scoped add is non-interactive with an explicit branch arg + --value + --yes.) + vercel env rm API_BASE_URL preview "$BRANCH" --token="$VERCEL_TOKEN" --yes 2>/dev/null || true + vercel env add API_BASE_URL preview "$BRANCH" --value "$SPACE_URL" --token="$VERCEL_TOKEN" --yes --force + # Immediate: redeploy the branch's latest preview so the new env applies now. + uid=$(curl -fsSL -H "Authorization: Bearer $VERCEL_TOKEN" \ + "https://api.vercel.com/v6/deployments?projectId=${VERCEL_PROJECT_ID}&teamId=${VERCEL_ORG_ID}&target=preview&limit=30" 2>/dev/null \ + | jq -r --arg b "$BRANCH" '[.deployments[] | select(.meta.githubCommitRef==$b)][0].uid // empty') + if [[ -n "${uid:-}" ]]; then + vercel redeploy "$uid" --token="$VERCEL_TOKEN" || echo "Redeploy failed (non-fatal); env applies on next build." + else + echo "No existing preview for '$BRANCH' yet; env applies on first/next build." + fi diff --git a/.github/workflows/extralit-frontend.deploy-environment.yml b/.github/workflows/extralit-frontend.deploy-environment.yml deleted file mode 100644 index b8301f518..000000000 --- a/.github/workflows/extralit-frontend.deploy-environment.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Deploy Extralit environment - -on: - workflow_dispatch: - inputs: - image-name: - description: The name of the Docker image to deploy. - type: string - default: extralit/extralit-quickstart-for-dev - image-version: - description: The version of the Docker image to deploy. In the form pr-. - type: string - - workflow_call: - inputs: - image-name: - description: The name of the Docker image to deploy. - type: string - image-version: - description: The version of the Docker image to deploy. In the form pr-. - type: string - -jobs: - deploy: - name: Deploy Extralit to Cloud Run - runs-on: ubuntu-latest - - # Grant permissions to `GITHUB_TOKEN` for Google Cloud Workload Identity Provider - permissions: - contents: read - id-token: write - pull-requests: write - - steps: - - uses: actions/checkout@v4 - - # Authenticate in GCP using Workload Identity Federation - - name: Authenticate to Google Cloud - uses: "google-github-actions/auth@v1" - with: - workload_identity_provider: ${{ secrets.GOOGLE_CLOUD_WIP }} - service_account: ${{ secrets.GOOGLE_CLOUD_SERVICE_ACCOUNT }} - - - name: Generate credentials - id: credentials - uses: ./.github/actions/generate-credentials - env: - ENVIRONMENT_CREDENTIALS_SECRET: ${{ secrets.ENVIRONMENT_CREDENTIALS_SECRET }} - - - name: Deploy in Cloud Run - id: deploy - uses: "google-github-actions/deploy-cloudrun@v1" - with: - service: extralit-quickstart-${{ inputs.image-version }} - image: europe-docker.pkg.dev/argilla-ci/${{ inputs.image-name}}:${{ inputs.image-version }} - region: europe-southwest1 - flags: "--min-instances=1 --max-instances=1 --port=3000 --cpu=2000m --memory=4096Mi --no-cpu-throttling --allow-unauthenticated" - env_vars: | - OWNER_PASSWORD=${{ steps.credentials.outputs.owner }} - OWNER_API_KEY=${{ steps.credentials.outputs.owner }} - ADMIN_PASSWORD=${{ steps.credentials.outputs.admin }} - ADMIN_API_KEY=${{ steps.credentials.outputs.admin }} - ANNOTATOR_PASSWORD=${{ steps.credentials.outputs.annotator }} - ANNOTATOR_API_KEY=${{ steps.credentials.outputs.annotator }} - HF_HUB_DISABLE_TELEMETRY=1 - API_BASE_URL=https://dev.extralit.io/ - - - name: Post credentials in Slack - uses: ./.github/actions/slack-post-credentials - if: ${{ github.event_name != 'workflow_dispatch' }} - with: - slack-channel-name: extralit-github - url: ${{ steps.deploy.outputs.url }} - owner: ${{ steps.credentials.outputs.owner }} - admin: ${{ steps.credentials.outputs.admin }} - annotator: ${{ steps.credentials.outputs.annotator }} - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - - - name: Comment PR with Cloud Run URL - uses: thollander/actions-comment-pull-request@v2 - if: ${{ github.event_name != 'workflow_dispatch' }} - with: - message: | - The URL of the deployed environment for this PR is ${{ steps.deploy.outputs.url }} - comment_tag: cloud_run_url - reactions: rocket diff --git a/.github/workflows/extralit-frontend.yml b/.github/workflows/extralit-frontend.yml index 63c41ba51..818359cd7 100644 --- a/.github/workflows/extralit-frontend.yml +++ b/.github/workflows/extralit-frontend.yml @@ -11,26 +11,18 @@ on: branches: - main - develop + - feat/** - releases/** paths: - "extralit-frontend/**" - ".github/workflows/extralit-frontend.yml" - pull_request: - paths: - - "extralit-frontend/**" - - ".github/workflows/extralit-frontend.yml" - - permissions: contents: read - id-token: write - pull-requests: write jobs: build: name: Build extralit-frontend - if: github.event.pull_request.draft == false runs-on: ubuntu-latest defaults: run: @@ -40,26 +32,10 @@ jobs: - name: Checkout Code πŸ›Ž uses: actions/checkout@v4 - - name: Update repo visualizer - uses: githubocto/repo-visualizer@0.7.1 - with: - root_path: "extralit-frontend/" - excluded_paths: "dist,build,node_modules,docs,tests,.swm,assets,.github,package-lock.json,pdm.lock" - excluded_globs: "*.spec.js;**/*.{png,jpg,svg,md};**/!(*.module).ts,**/__pycache__/,**/__mocks__/,LICENSE*,**/.gitignore,**/*.egg-info/,**/.*/" - output_file: "repo-visualizer.svg" - should_push: false - - - name: Upload repo visualizer diagram as artifact - uses: actions/upload-artifact@v4 - with: - name: repo-visualizer - path: repo-visualizer.svg - retention-days: 10 - - name: Setup Node.js βš™οΈ uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - name: Install dependencies πŸ“¦ run: | @@ -86,40 +62,17 @@ jobs: - name: Build package πŸ“¦ env: - # BASE_URL is used in the server to support parameterizable base root path - BASE_URL: "@@baseUrl@@" - DIST_FOLDER: ./dist + # Nuxt 4 SPA (ssr:false) ships as static files served at the site root, so build at + # base "/". (The Nuxt 2 "@@baseUrl@@" placeholder breaks Nuxt 4's prerender crawler; + # parameterizable sub-path hosting is a separate follow-up.) + BASE_URL: "/" run: | - npm run build + # `nuxi generate` prerenders the SPA shell to .output/public (index.html + _nuxt + # assets); `nuxi build` only emits a Nitro server with no static index.html. + npm run generate - name: Upload frontend statics as artifact uses: actions/upload-artifact@v4 with: name: extralit-frontend - path: extralit-frontend/dist - - # build_dev_docker_image: - # name: Build development extralit-frontend docker image - # needs: build - # uses: ./.github/workflows/extralit-frontend.build-push-dev-frontend-docker.yml - # if: | - # !cancelled() && - # github.event_name == 'pull_request' && github.event.pull_request.draft == false - # with: - # image-name: extralit/extralit-frontend-for-dev - # dockerfile: extralit-frontend/dev.frontend.Dockerfile - # platforms: linux/amd64 - # secrets: inherit - - # deploy: - # name: Deploy pr environment - # uses: ./.github/workflows/extralit-frontend.deploy-environment.yml - # needs: build_dev_docker_image - # if: | - # !cancelled() && - # needs.build_dev_docker_image.result == 'success' && - # github.event_name == 'pull_request' && github.event.pull_request.draft == false - # with: - # image-name: extralit/extralit-frontend-for-dev - # image-version: ${{ needs.build_dev_docker_image.outputs.version }} - # secrets: inherit + path: extralit-frontend/.output/public diff --git a/.github/workflows/extralit-pr-preview.yml b/.github/workflows/extralit-pr-preview.yml deleted file mode 100644 index 13c4e3194..000000000 --- a/.github/workflows/extralit-pr-preview.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: Deploy PR preview HF Space - -# Builds a dev-tagged extralit-server image (pr-) and dispatches the HF Space repo to -# deploy an ephemeral Space at https://huggingface.co/spaces/extralit-dev/pr-. -# -# Deliberately decoupled from extralit-server.yml so it does NOT run on every push: -# - auto: only when a PR (touching the server or frontend) is marked "ready for review" -# - manual: workflow_dispatch with a PR number, to (re)deploy a preview on demand -# main/develop deploys keep flowing through extralit-server.yml as before. - -on: - # Note: `ready_for_review` only fires on a draftβ†’ready transition. A PR opened - # directly as non-draft does NOT auto-trigger β€” mark it ready, or use the manual - # workflow_dispatch below. This is intentional: previews never build on push. - pull_request: - types: [ready_for_review] - paths: - - "extralit-server/**" - - "extralit-frontend/**" - - ".github/workflows/extralit-pr-preview.yml" - - ".github/workflows/extralit-server.build-docker-images.yml" - - workflow_dispatch: - inputs: - pr_number: - description: "PR number to deploy to extralit-dev/pr-" - required: true - type: string - -concurrency: - # One preview build per PR; cancel superseded runs. - group: pr-preview-${{ github.event.pull_request.number || inputs.pr_number }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build: - name: Build `extralit-server` package (frontend baked in) - runs-on: ubuntu-latest - # Fork PRs don't receive secrets (Docker login / dispatch would fail), so skip them. - if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.fork == false - - defaults: - run: - shell: bash -l {0} - working-directory: extralit-server - - steps: - - name: Checkout Code πŸ›Ž - uses: actions/checkout@v4 - with: - # Auto path: default merge ref (refs/pull//merge). Manual path: resolve the PR's - # merge ref from pr_number so the build is self-contained β€” no need to run from the - # PR's head branch for the pr- image/Space to reflect that PR's code. - ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/pull/{0}/merge', inputs.pr_number) || github.ref }} - - - name: Install uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 - with: - pyproject-file: "extralit-server/pyproject.toml" - enable-cache: true - cache-local-path: ~/.cache/uv - cache-dependency-glob: "extralit-server/uv.lock" - - # Mirrors the frontend+wheel build in extralit-server.yml (no pytest here β€” this is a - # preview-only path). The compiled frontend is copied into the server's static dir so - # the server image (and the HF Space built FROM it) ships the PR's frontend changes. - - name: Setup Node.js for frontend dependencies - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install frontend dependencies - working-directory: extralit-frontend - env: - BASE_URL: "@@baseUrl@@" - DIST_FOLDER: ./dist - run: | - npm install - npm run build - - - name: Build package - run: | - cp -r ../extralit-frontend/dist src/extralit_server/static - uv build - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: extralit-server - path: extralit-server/dist - - build_docker_images: - name: Build docker images - needs: - - build - uses: ./.github/workflows/extralit-server.build-docker-images.yml - with: - is_release: false - publish_latest: false - # PR number on BOTH paths (manual input, or the PR number on the auto path). This forces - # IMAGE_TAG=pr-, DISPATCH_BRANCH=/merge, and crucially PUBLISH_LATEST=false so a - # preview never republishes the dev :latest tag from unreviewed PR code. - pr_number: ${{ inputs.pr_number || github.event.pull_request.number }} - secrets: inherit diff --git a/.github/workflows/extralit-server.yml b/.github/workflows/extralit-server.yml index 1c1552807..1b5f736f6 100644 --- a/.github/workflows/extralit-server.yml +++ b/.github/workflows/extralit-server.yml @@ -11,15 +11,10 @@ on: branches: - main - develop + - feat/** - releases/** paths: - "extralit-server/**" - - ".github/workflows/extralit-server.yml" - - pull_request: - paths: - - "extralit-server/**" - - ".github/workflows/extralit-server.yml" permissions: id-token: write @@ -131,25 +126,21 @@ jobs: flags: extralit-server token: ${{ secrets.CODECOV_TOKEN }} - # This section is used to build the frontend and copy the build files to the server. - # In the future, static files should be downloaded after the frontend is built and uploaded as an artifact. - - name: Setup Node.js for frontend dependencies - uses: actions/setup-node@v4 + # The server no longer builds the frontend from source. The live UI is published to Vercel + # by extralit-frontend.yml; here we just bake a prebuilt SPA into the wheel as the bundled + # fallback UI (main for release builds, else develop). + - name: Download prebuilt frontend statics + uses: ./.github/actions/download-frontend-artifact with: - node-version: 20 + branch: ${{ github.ref_name == 'main' && 'main' || 'develop' }} - - name: Install frontend dependencies - working-directory: extralit-frontend - env: - BASE_URL: "@@baseUrl@@" - DIST_FOLDER: ./dist - run: | - npm install - npm run build - # End of frontend build section - name: Build package run: | - cp -r ../extralit-frontend/dist src/extralit_server/static + set -euo pipefail + # Bake the compiled SPA into the wheel's static dir. Fail loudly if it's missing or + # empty, otherwise the server ships with no statics and 404s every route. + cp -r ../extralit-frontend/.output/public src/extralit_server/static + test -f src/extralit_server/static/index.html uv build - name: Upload artifact @@ -199,22 +190,6 @@ jobs: - name: Checkout Code πŸ›Ž uses: actions/checkout@v4 - - name: Update repo visualizer - uses: githubocto/repo-visualizer@0.7.1 - with: - root_path: "extralit-server/" - excluded_paths: "dist,build,node_modules,docs,tests,.swm,assets,.github,package-lock.json,uv.lock" - excluded_globs: "*.spec.js;**/*.{png,jpg,svg,md};**/!(*.module).ts,**/__pycache__/,**/__mocks__/,LICENSE*,**/.gitignore,**/*.egg-info/,**/.*/" - output_file: "repo-visualizer.svg" - should_push: false - - - name: Upload repo visualizer diagram as artifact - uses: actions/upload-artifact@v4 - with: - name: repo-visualizer - path: repo-visualizer.svg - retention-days: 10 - - name: Download python package uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/extralit.yml b/.github/workflows/extralit.yml index eed554248..817d199f8 100644 --- a/.github/workflows/extralit.yml +++ b/.github/workflows/extralit.yml @@ -11,19 +11,12 @@ on: branches: - main - develop + - feat/** - releases/** paths: - "extralit/**" - "!extralit/docs/**" - "!extralit/mkdocs.yml" - - ".github/workflows/extralit.yml" - - pull_request: - paths: - - "extralit/**" - - "!extralit/docs/**" - - "!extralit/mkdocs.yml" - - ".github/workflows/extralit.yml" permissions: id-token: write @@ -160,22 +153,6 @@ jobs: - name: Checkout Code πŸ›Ž uses: actions/checkout@v4 - - name: Update repo visualizer - uses: githubocto/repo-visualizer@0.7.1 - with: - root_path: "extralit/" - excluded_paths: "dist,build,node_modules,docs,tests,.swm,assets,.github,package-lock.json,uv.lock" - excluded_globs: "*.spec.js;**/*.{png,jpg,svg,md};**/!(*.module).ts,**/__pycache__/,**/__mocks__/,LICENSE*,**/.gitignore,**/*.egg-info/,**/.*/" - output_file: "repo-visualizer.svg" - should_push: false - - - name: Upload repo visualizer diagram as artifact - uses: actions/upload-artifact@v4 - with: - name: repo-visualizer - path: repo-visualizer.svg - retention-days: 10 - - name: Download python package uses: actions/download-artifact@v4 with: diff --git a/.gitignore b/.gitignore index e54a6f72e..c3ff35bc8 100644 --- a/.gitignore +++ b/.gitignore @@ -172,4 +172,6 @@ output/ **/.playwright-mcp/ **/.nuxt-stale-root/ -.worktree/ \ No newline at end of file +.worktree/ +.vercel +.env* diff --git a/CLAUDE.md b/CLAUDE.md index 1b5d0acea..336513d57 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Architecture Notes - **extralit-server/**: FastAPI + PostgreSQL + Redis Queue -- **extralit-frontend/**: Vue.js/Nuxt.js (Vuex β†’ Pinia migration) +- **extralit-frontend/**: Vue 3 / Nuxt 4 (Vite); Pinia state management - **extralit/**: Python SDK client - **extralit-hf-space/**: Self-contained HF Spaces deployment bundle (Docker; bundles Elasticsearch + Redis + OCR) β€” git submodule - **Vector DB**: Elasticsearch/OpenSearch (separate service) diff --git a/extralit-frontend/.eslintrc.js b/extralit-frontend/.eslintrc.js index ac5402b60..b526a5c77 100644 --- a/extralit-frontend/.eslintrc.js +++ b/extralit-frontend/.eslintrc.js @@ -10,14 +10,17 @@ module.exports = { "plugin:@intlify/vue-i18n/recommended", "plugin:prettier/recommended", "plugin:nuxt/recommended", - "prettier/vue", ], + plugins: ["vue"], settings: { "vue-i18n": { - localeDir: "./translation/*.json", + localeDir: "./translation/*.js", }, }, rules: { + // Formatting is advisory here (parity with the *.ts override and the separate + // `npm run format` step); keeps `lint --quiet` focused on real correctness rules. + "prettier/prettier": "warn", "no-console": process.env.NODE_ENV === "production" ? "error" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", "prefer-const": "warn", @@ -48,14 +51,38 @@ module.exports = { }, globals: { $nuxt: true, + vi: true, + // Nuxt 4 auto-imports, hand-declared to satisfy no-undef. This list can drift; + // the real fix is adopting `@nuxt/eslint` (flat config), which auto-generates + // these globals from the build manifest. Tracked as follow-up, not done here. + defineNuxtPlugin: "readonly", + defineNuxtRouteMiddleware: "readonly", + definePageMeta: "readonly", + navigateTo: "readonly", + abortNavigation: "readonly", + useNuxtApp: "readonly", + useRuntimeConfig: "readonly", + useRoute: "readonly", + useRouter: "readonly", + useState: "readonly", + useCookie: "readonly", + useHead: "readonly", + useSeoMeta: "readonly", + useError: "readonly", + createError: "readonly", + clearError: "readonly", + showError: "readonly", }, + parser: "vue-eslint-parser", parserOptions: { - parser: "@babel/eslint-parser", + parser: "@typescript-eslint/parser", + ecmaVersion: 2022, + sourceType: "module", }, overrides: [ { files: ["**/*.ts"], - extends: ["@nuxtjs/eslint-config-typescript", "prettier"], + extends: ["plugin:@typescript-eslint/recommended", "prettier"], parser: "@typescript-eslint/parser", plugins: ["@typescript-eslint", "prettier"], parserOptions: { project: ["./tsconfig.json"] }, diff --git a/extralit-frontend/.gitignore b/extralit-frontend/.gitignore index 75e854d8d..c3277549b 100644 --- a/extralit-frontend/.gitignore +++ b/extralit-frontend/.gitignore @@ -2,3 +2,11 @@ node_modules/ /test-results/ /playwright-report/ /playwright/.cache/ +.nuxtrc +.nuxt/ +.output/ +.nitro/ +.cache/ +dist/ +.vercel +.env* diff --git a/extralit-frontend/CLAUDE.md b/extralit-frontend/CLAUDE.md index 81ba50731..555eb0f6e 100644 --- a/extralit-frontend/CLAUDE.md +++ b/extralit-frontend/CLAUDE.md @@ -25,7 +25,7 @@ API_BASE_URL=https://extralit-public-demo.hf.space/ npm run dev ## Testing ```bash -npm run test # Jest unit tests +npm run test # Vitest unit tests (run once) npm run test:watch # Watch mode npm run test:coverage # With coverage @@ -34,35 +34,63 @@ npm run e2e:silent # Playwright headless npm run e2e:report # View test report ``` +> Unit tests run on **Vitest** (`vitest.config.ts` + `test/setup.ts`), using +> `@vue/test-utils` v2 and `@nuxt/test-utils`. Specs needing Nuxt runtime context use +> `// @vitest-environment nuxt` or `mockNuxtImport`. +> +> The Playwright e2e suite is inherited from upstream Argilla. The shared login helper +> (`e2e/common/login-and-wait-for.ts`) has been reconciled to Extralit's real sign-in UI: +> it fills `getByLabel("Username"/"Password")`, submits the `"Sign in"` button, mocks +> `/api/v1/token` + `/api/v1/me` offline, and waits for the home/datasets landing at `/` +> (there is no `/datasets` route). This flow is runtime-verified via the CDP browser. The +> per-page specs still need fresh Extralit screenshot baselines (`--update-snapshots`); the +> inherited ones are Argilla's. The local Playwright browser can't launch on the Orin dev +> host (missing OS libs, no sudo) β€” run the headless gate in CI. + ## Code Quality ```bash -npm run lint # ESLint check +npm run lint # ESLint check (eslint 8 + vue-eslint-parser) npm run lint:fix # Fix ESLint issues npm run format # Format with Prettier npm run format:check # Check formatting npm run generate-icons # Generate icon components from SVG + +npx nuxi typecheck # vue-tsc type check +npm run build # Production build (vite/nitro) ``` ## Requirements -- Node.js 18+ +- Node.js 18+ (developed on Node 24) - Backend server running for full functionality ## Architecture -**Migration in progress**: Vuex β†’ Pinia - -- **v1/** directory: New Pinia architecture with domain-driven design -- Domain-driven design with entities, use cases, dependency injection +- **v1/** directory: Pinia + domain-driven design (entities, use cases, dependency injection + via `ts-injecty`). The domain/use-case layer is framework-agnostic; only the Vue/Nuxt + adapters (HTTP, Auth, Icons) were swapped during the Vue 3 / Nuxt 4 migration. - Component hierarchy: base (stateless) β†’ features (page-specific) β†’ global (reusable) +- HTTP: plain `axios` in `plugins/2.axios.ts` (replaced `@nuxtjs/axios`), re-injected into DI. +- Auth: `AuthService` (`v1/infrastructure/services/AuthService.ts`) implementing `IAuthService`, + provided as `$auth` by `plugins/1.auth.ts` (replaced `@nuxtjs/auth-next`). +- Icons: custom `` (`components/base/BaseSvgIcon.vue`) reading `static/icons/*.svg` + (replaced `vue-svgicon`). +- Plugins load in order via numeric prefixes (`1.auth` β†’ `2.axios` β†’ `3.di`); middleware are + Nuxt-4 globals (`middleware/*.global.ts`). + +> **TS posture:** `tsconfig.json` keeps `strict:false` (matching the pre-Vue3 config) and +> disables Nuxt-4's new `verbatimModuleSyntax`/`noImplicitOverride`. Tightening to strict is a +> separate hardening effort. Note: Vite/esbuild (`isolatedModules`) requires type-only imports +> to use the inline `import { type X }` modifier or they throw at runtime in dev. ## Key Technologies -- Vue.js + Nuxt.js -- Pinia (state management, replacing Vuex) -- Jest (unit tests) + Playwright (e2e) -- ESLint + Prettier +- Vue 3.5 + Nuxt 4 (Vite + Nitro) +- Pinia (state management; Vuex fully removed) +- Vitest + @vue/test-utils v2 (unit) + Playwright (e2e) +- @nuxtjs/i18n v10 (vue-i18n v11), @vueuse/core, mitt +- ESLint 8 + Prettier ## Structure diff --git a/extralit-frontend/__mocks__/empty.css b/extralit-frontend/__mocks__/empty.css new file mode 100644 index 000000000..874c6aaa5 --- /dev/null +++ b/extralit-frontend/__mocks__/empty.css @@ -0,0 +1 @@ +/* test stub */ diff --git a/extralit-frontend/assets/icon-template.js.tmp b/extralit-frontend/assets/icon-template.js.tmp deleted file mode 100644 index dc575f361..000000000 --- a/extralit-frontend/assets/icon-template.js.tmp +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - '${name}': { - width: ${width}, - height: ${height}, - viewBox: ${viewBox}, - data: '${data}' - } -}) \ No newline at end of file diff --git a/extralit-frontend/assets/icons/arrow-down.js b/extralit-frontend/assets/icons/arrow-down.js deleted file mode 100644 index 94ddb6ff3..000000000 --- a/extralit-frontend/assets/icons/arrow-down.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'arrow-down': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/arrow-up.js b/extralit-frontend/assets/icons/arrow-up.js deleted file mode 100644 index e70d7960e..000000000 --- a/extralit-frontend/assets/icons/arrow-up.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'arrow-up': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/assign.js b/extralit-frontend/assets/icons/assign.js deleted file mode 100644 index a61f876be..000000000 --- a/extralit-frontend/assets/icons/assign.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'assign': { - width: 10, - height: 10, - viewBox: '0 0 10 10', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/bulk-mode.js b/extralit-frontend/assets/icons/bulk-mode.js deleted file mode 100644 index bcf9f4000..000000000 --- a/extralit-frontend/assets/icons/bulk-mode.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'bulk-mode': { - width: 24, - height: 17, - viewBox: '0 0 24 17', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/change-height.js b/extralit-frontend/assets/icons/change-height.js deleted file mode 100644 index 5e8cc4e6d..000000000 --- a/extralit-frontend/assets/icons/change-height.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'change-height': { - width: 30, - height: 18, - viewBox: '0 0 30 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/check.js b/extralit-frontend/assets/icons/check.js deleted file mode 100644 index 5e23d4436..000000000 --- a/extralit-frontend/assets/icons/check.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'check': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/chevron-down.js b/extralit-frontend/assets/icons/chevron-down.js deleted file mode 100644 index fd7aa4ead..000000000 --- a/extralit-frontend/assets/icons/chevron-down.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'chevron-down': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/chevron-left.js b/extralit-frontend/assets/icons/chevron-left.js deleted file mode 100644 index ebb885b48..000000000 --- a/extralit-frontend/assets/icons/chevron-left.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'chevron-left': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/chevron-right.js b/extralit-frontend/assets/icons/chevron-right.js deleted file mode 100644 index 278012bd4..000000000 --- a/extralit-frontend/assets/icons/chevron-right.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'chevron-right': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/chevron-up.js b/extralit-frontend/assets/icons/chevron-up.js deleted file mode 100644 index 1339d039b..000000000 --- a/extralit-frontend/assets/icons/chevron-up.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'chevron-up': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/clear.js b/extralit-frontend/assets/icons/clear.js deleted file mode 100644 index 6e2e5cca9..000000000 --- a/extralit-frontend/assets/icons/clear.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'clear': { - width: 18, - height: 18, - viewBox: '0 0 18 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/close.js b/extralit-frontend/assets/icons/close.js deleted file mode 100644 index 82d60d85c..000000000 --- a/extralit-frontend/assets/icons/close.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'close': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/code.js b/extralit-frontend/assets/icons/code.js deleted file mode 100644 index a0e30a4c0..000000000 --- a/extralit-frontend/assets/icons/code.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'code': { - width: 20, - height: 20, - viewBox: '0 0 20 20', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/copy.js b/extralit-frontend/assets/icons/copy.js deleted file mode 100644 index ff9a3126d..000000000 --- a/extralit-frontend/assets/icons/copy.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'copy': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/danger.js b/extralit-frontend/assets/icons/danger.js deleted file mode 100644 index b7f30396e..000000000 --- a/extralit-frontend/assets/icons/danger.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'danger': { - width: 90, - height: 82, - viewBox: '0 0 90 82', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/dark-theme.js b/extralit-frontend/assets/icons/dark-theme.js deleted file mode 100644 index 4d093d02e..000000000 --- a/extralit-frontend/assets/icons/dark-theme.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'dark-theme': { - width: 18, - height: 18, - viewBox: '0 0 18 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/discard.js b/extralit-frontend/assets/icons/discard.js deleted file mode 100644 index c81db94a0..000000000 --- a/extralit-frontend/assets/icons/discard.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'discard': { - width: 16, - height: 16, - viewBox: '0 0 16 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/document.js b/extralit-frontend/assets/icons/document.js deleted file mode 100644 index 152fb32a5..000000000 --- a/extralit-frontend/assets/icons/document.js +++ /dev/null @@ -1,11 +0,0 @@ -// document.js - -var icon = require("vue-svgicon"); -icon.register({ - document: { - width: 41, - height: 40, - viewBox: "0 0 41 40", - data: '', - }, -}); diff --git a/extralit-frontend/assets/icons/draggable.js b/extralit-frontend/assets/icons/draggable.js deleted file mode 100644 index 9c31c4018..000000000 --- a/extralit-frontend/assets/icons/draggable.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'draggable': { - width: 6, - height: 10, - viewBox: '0 0 6 10', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/expand-arrows.js b/extralit-frontend/assets/icons/expand-arrows.js deleted file mode 100644 index 38b0050b8..000000000 --- a/extralit-frontend/assets/icons/expand-arrows.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'expand-arrows': { - width: 14, - height: 14, - viewBox: '0 0 14 14', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/exploration.js b/extralit-frontend/assets/icons/exploration.js deleted file mode 100644 index 2d6bbdd1a..000000000 --- a/extralit-frontend/assets/icons/exploration.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'exploration': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/export.js b/extralit-frontend/assets/icons/export.js deleted file mode 100644 index 90b5334af..000000000 --- a/extralit-frontend/assets/icons/export.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'export': { - width: 26, - height: 31, - viewBox: '0 0 26 31', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/external-link.js b/extralit-frontend/assets/icons/external-link.js deleted file mode 100644 index b015a907a..000000000 --- a/extralit-frontend/assets/icons/external-link.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'external-link': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/external.js b/extralit-frontend/assets/icons/external.js deleted file mode 100644 index 4dc4bf282..000000000 --- a/extralit-frontend/assets/icons/external.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'external': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/filter.js b/extralit-frontend/assets/icons/filter.js deleted file mode 100644 index e020a5d89..000000000 --- a/extralit-frontend/assets/icons/filter.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'filter': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/focus-mode.js b/extralit-frontend/assets/icons/focus-mode.js deleted file mode 100644 index cdd9bce9e..000000000 --- a/extralit-frontend/assets/icons/focus-mode.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'focus-mode': { - width: 20, - height: 16, - viewBox: '0 0 20 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/hand-labeling.js b/extralit-frontend/assets/icons/hand-labeling.js deleted file mode 100644 index d71207bea..000000000 --- a/extralit-frontend/assets/icons/hand-labeling.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'hand-labeling': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/high-contrast-theme.js b/extralit-frontend/assets/icons/high-contrast-theme.js deleted file mode 100644 index 775f40856..000000000 --- a/extralit-frontend/assets/icons/high-contrast-theme.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'high-contrast-theme': { - width: 16, - height: 16, - viewBox: '0 0 16 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/import.js b/extralit-frontend/assets/icons/import.js deleted file mode 100644 index a78d0e113..000000000 --- a/extralit-frontend/assets/icons/import.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'import': { - width: 29, - height: 31, - viewBox: '0 0 29 31', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/index.js b/extralit-frontend/assets/icons/index.js deleted file mode 100644 index 36e92d023..000000000 --- a/extralit-frontend/assets/icons/index.js +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable */ -require('./arrow-down') -require('./arrow-up') -require('./assign') -require('./bulk-mode') -require('./change-height') -require('./check') -require('./chevron-down') -require('./chevron-left') -require('./chevron-right') -require('./chevron-up') -require('./clear') -require('./close') -require('./code') -require('./copy') -require('./danger') -require('./dark-theme') -require('./discard') -require('./draggable') -require('./expand-arrows') -require('./exploration') -require('./export') -require('./external-link') -require('./external') -require('./filter') -require('./focus-mode') -require('./hand-labeling') -require('./high-contrast-theme') -require('./import') -require('./info') -require('./kebab') -require('./light-theme') -require('./link') -require('./log-out') -require('./matching') -require('./math-plus') -require('./meatballs') -require('./minimize-arrows') -require('./no-matching') -require('./pen') -require('./plus') -require('./progress') -require('./question-answering') -require('./records') -require('./refresh') -require('./reset') -require('./row-last') -require('./rows') -require('./search') -require('./settings') -require('./shortcuts') -require('./similarity') -require('./smile-sad') -require('./sort') -require('./stats') -require('./suggestion') -require('./support') -require('./system-theme') -require('./text-classification') -require('./text-to-image') -require('./time') -require('./trash-empty') -require('./unavailable') -require('./update') -require('./validate') -require('./weak-labeling') diff --git a/extralit-frontend/assets/icons/info.js b/extralit-frontend/assets/icons/info.js deleted file mode 100644 index da93b6995..000000000 --- a/extralit-frontend/assets/icons/info.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'info': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/kebab.js b/extralit-frontend/assets/icons/kebab.js deleted file mode 100644 index 09eecd5c2..000000000 --- a/extralit-frontend/assets/icons/kebab.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'kebab': { - width: 20, - height: 21, - viewBox: '0 0 20 21', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/light-theme.js b/extralit-frontend/assets/icons/light-theme.js deleted file mode 100644 index 2451caaae..000000000 --- a/extralit-frontend/assets/icons/light-theme.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'light-theme': { - width: 18, - height: 18, - viewBox: '0 0 18 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/link.js b/extralit-frontend/assets/icons/link.js deleted file mode 100644 index 0ffb4e746..000000000 --- a/extralit-frontend/assets/icons/link.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'link': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/log-out.js b/extralit-frontend/assets/icons/log-out.js deleted file mode 100644 index 6d64d3f71..000000000 --- a/extralit-frontend/assets/icons/log-out.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'log-out': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/matching.js b/extralit-frontend/assets/icons/matching.js deleted file mode 100644 index de2601544..000000000 --- a/extralit-frontend/assets/icons/matching.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'matching': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/math-plus.js b/extralit-frontend/assets/icons/math-plus.js deleted file mode 100644 index 790290ee1..000000000 --- a/extralit-frontend/assets/icons/math-plus.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'math-plus': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/meatballs.js b/extralit-frontend/assets/icons/meatballs.js deleted file mode 100644 index ab7e3e560..000000000 --- a/extralit-frontend/assets/icons/meatballs.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'meatballs': { - width: 16, - height: 16, - viewBox: '0 0 30 8', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/minimize-arrows.js b/extralit-frontend/assets/icons/minimize-arrows.js deleted file mode 100644 index ca7369ea7..000000000 --- a/extralit-frontend/assets/icons/minimize-arrows.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'minimize-arrows': { - width: 16, - height: 16, - viewBox: '0 0 16 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/no-matching.js b/extralit-frontend/assets/icons/no-matching.js deleted file mode 100644 index 23ead8219..000000000 --- a/extralit-frontend/assets/icons/no-matching.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'no-matching': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/pen.js b/extralit-frontend/assets/icons/pen.js deleted file mode 100644 index 8349afb86..000000000 --- a/extralit-frontend/assets/icons/pen.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'pen': { - width: 16, - height: 16, - viewBox: '0 0 16 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/plus.js b/extralit-frontend/assets/icons/plus.js deleted file mode 100644 index ac0c3d344..000000000 --- a/extralit-frontend/assets/icons/plus.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'plus': { - width: 16, - height: 16, - viewBox: '0 0 16 16', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/progress.js b/extralit-frontend/assets/icons/progress.js deleted file mode 100644 index 281aff533..000000000 --- a/extralit-frontend/assets/icons/progress.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'progress': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/question-answering.js b/extralit-frontend/assets/icons/question-answering.js deleted file mode 100644 index 4dd71cd3f..000000000 --- a/extralit-frontend/assets/icons/question-answering.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'question-answering': { - width: 15, - height: 15, - viewBox: '0 0 15 15', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/records.js b/extralit-frontend/assets/icons/records.js deleted file mode 100644 index fccba7d8d..000000000 --- a/extralit-frontend/assets/icons/records.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'records': { - width: 10, - height: 9, - viewBox: '0 0 10 9', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/refresh.js b/extralit-frontend/assets/icons/refresh.js deleted file mode 100644 index 04e4f2d0c..000000000 --- a/extralit-frontend/assets/icons/refresh.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'refresh': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/reset.js b/extralit-frontend/assets/icons/reset.js deleted file mode 100644 index d5f8de52b..000000000 --- a/extralit-frontend/assets/icons/reset.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'reset': { - width: 16, - height: 18, - viewBox: '0 0 16 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/row-last.js b/extralit-frontend/assets/icons/row-last.js deleted file mode 100644 index a4b83f189..000000000 --- a/extralit-frontend/assets/icons/row-last.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'row-last': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/rows.js b/extralit-frontend/assets/icons/rows.js deleted file mode 100644 index 5284efcb6..000000000 --- a/extralit-frontend/assets/icons/rows.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'rows': { - width: 10, - height: 8, - viewBox: '0 0 10 8', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/search.js b/extralit-frontend/assets/icons/search.js deleted file mode 100644 index c359dfbf8..000000000 --- a/extralit-frontend/assets/icons/search.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'search': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/settings.js b/extralit-frontend/assets/icons/settings.js deleted file mode 100644 index 7bd9d4b61..000000000 --- a/extralit-frontend/assets/icons/settings.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'settings': { - width: 800, - height: 800, - viewBox: '0 0 192 192', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/shortcuts.js b/extralit-frontend/assets/icons/shortcuts.js deleted file mode 100644 index 1f9e21f16..000000000 --- a/extralit-frontend/assets/icons/shortcuts.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'shortcuts': { - width: 16, - height: 16, - viewBox: '0 0 32 32', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/similarity.js b/extralit-frontend/assets/icons/similarity.js deleted file mode 100644 index 8723b42f6..000000000 --- a/extralit-frontend/assets/icons/similarity.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'similarity': { - width: 40, - height: 41, - viewBox: '0 0 40 41', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/smile-sad.js b/extralit-frontend/assets/icons/smile-sad.js deleted file mode 100644 index e91b23d85..000000000 --- a/extralit-frontend/assets/icons/smile-sad.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'smile-sad': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/sort.js b/extralit-frontend/assets/icons/sort.js deleted file mode 100644 index 8aaa33b29..000000000 --- a/extralit-frontend/assets/icons/sort.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'sort': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/stats.js b/extralit-frontend/assets/icons/stats.js deleted file mode 100644 index e3e675e17..000000000 --- a/extralit-frontend/assets/icons/stats.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'stats': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/suggestion.js b/extralit-frontend/assets/icons/suggestion.js deleted file mode 100644 index 6f2cfa1f2..000000000 --- a/extralit-frontend/assets/icons/suggestion.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'suggestion': { - width: 12, - height: 12, - viewBox: '0 0 12 12', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/support.js b/extralit-frontend/assets/icons/support.js deleted file mode 100644 index 697516a9d..000000000 --- a/extralit-frontend/assets/icons/support.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'support': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/system-theme.js b/extralit-frontend/assets/icons/system-theme.js deleted file mode 100644 index 93fcafbe7..000000000 --- a/extralit-frontend/assets/icons/system-theme.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'system-theme': { - width: 18, - height: 18, - viewBox: '0 0 18 18', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/text-classification.js b/extralit-frontend/assets/icons/text-classification.js deleted file mode 100644 index 0612ded0c..000000000 --- a/extralit-frontend/assets/icons/text-classification.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'text-classification': { - width: 15, - height: 15, - viewBox: '0 0 15 15', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/text-to-image.js b/extralit-frontend/assets/icons/text-to-image.js deleted file mode 100644 index 35554ca79..000000000 --- a/extralit-frontend/assets/icons/text-to-image.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'text-to-image': { - width: 15, - height: 15, - viewBox: '0 0 15 15', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/time.js b/extralit-frontend/assets/icons/time.js deleted file mode 100644 index 4ebc75f85..000000000 --- a/extralit-frontend/assets/icons/time.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'time': { - width: 30, - height: 30, - viewBox: '0 0 30 30', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/trash-empty.js b/extralit-frontend/assets/icons/trash-empty.js deleted file mode 100644 index e0e8203ac..000000000 --- a/extralit-frontend/assets/icons/trash-empty.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'trash-empty': { - width: 41, - height: 40, - viewBox: '0 0 41 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/unavailable.js b/extralit-frontend/assets/icons/unavailable.js deleted file mode 100644 index 0f1945be3..000000000 --- a/extralit-frontend/assets/icons/unavailable.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'unavailable': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/update.js b/extralit-frontend/assets/icons/update.js deleted file mode 100644 index 9f5087301..000000000 --- a/extralit-frontend/assets/icons/update.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'update': { - width: 12, - height: 13, - viewBox: '0 0 12 13', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/validate.js b/extralit-frontend/assets/icons/validate.js deleted file mode 100644 index 96bbf2b36..000000000 --- a/extralit-frontend/assets/icons/validate.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'validate': { - width: 31, - height: 31, - viewBox: '0 0 31 31', - data: '' - } -}) diff --git a/extralit-frontend/assets/icons/weak-labeling.js b/extralit-frontend/assets/icons/weak-labeling.js deleted file mode 100644 index 3ffb8cfbe..000000000 --- a/extralit-frontend/assets/icons/weak-labeling.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -var icon = require('vue-svgicon') -icon.register({ - 'weak-labeling': { - width: 40, - height: 40, - viewBox: '0 0 40 40', - data: '' - } -}) diff --git a/extralit-frontend/babel.config.js b/extralit-frontend/babel.config.js deleted file mode 100644 index 173e4e28b..000000000 --- a/extralit-frontend/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = { - presets: [["@babel/preset-env", { targets: { node: "current" }, loose: true }]], - plugins: [ - ["@babel/plugin-transform-class-properties", { loose: true }], - ["@babel/plugin-transform-private-methods", { loose: true }], - ["@babel/plugin-transform-private-property-in-object", { loose: true }], - ], - env: { - test: { - presets: [["@babel/preset-env", { targets: { node: "current" }, loose: true }], "@babel/preset-typescript"], - }, - }, -}; diff --git a/extralit-frontend/components/base/BaseSvgIcon.test.ts b/extralit-frontend/components/base/BaseSvgIcon.test.ts new file mode 100644 index 000000000..9b1bd29cd --- /dev/null +++ b/extralit-frontend/components/base/BaseSvgIcon.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { mount } from "@vue/test-utils"; +import BaseSvgIcon from "./BaseSvgIcon.vue"; + +describe("BaseSvgIcon", () => { + it("renders an svg for a known icon name with the name as data attribute", () => { + const wrapper = mount(BaseSvgIcon, { props: { name: "check", width: 16, height: 16 } }); + expect(wrapper.find("svg").exists()).toBe(true); + expect(wrapper.attributes("data-icon")).toBe("check"); + }); + + it("applies the requested width/height to the svg", () => { + const wrapper = mount(BaseSvgIcon, { props: { name: "check", width: 16, height: 16 } }); + const svg = wrapper.find("svg"); + expect(svg.attributes("width")).toBe("16px"); + expect(svg.attributes("height")).toBe("16px"); + }); + + it("recolors monochrome fills when a color is supplied", () => { + const wrapper = mount(BaseSvgIcon, { props: { name: "check", color: "#ff0000" } }); + expect(wrapper.html()).toContain('fill="#ff0000"'); + }); + + it("renders nothing for an unknown icon name", () => { + const wrapper = mount(BaseSvgIcon, { props: { name: "definitely-not-an-icon" } }); + expect(wrapper.find("svg").exists()).toBe(false); + }); +}); diff --git a/extralit-frontend/components/base/BaseSvgIcon.vue b/extralit-frontend/components/base/BaseSvgIcon.vue new file mode 100644 index 000000000..843280a53 --- /dev/null +++ b/extralit-frontend/components/base/BaseSvgIcon.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/extralit-frontend/components/base/base-badge/BaseBadge.spec.js b/extralit-frontend/components/base/base-badge/BaseBadge.spec.js new file mode 100644 index 000000000..04591244d --- /dev/null +++ b/extralit-frontend/components/base/base-badge/BaseBadge.spec.js @@ -0,0 +1,33 @@ +import { mount } from "@vue/test-utils"; +import BaseBadge from "./BaseBadge.vue"; + +const ButtonStub = { template: '' }; + +describe("BaseBadge", () => { + it("is not clickable when no listener is attached", async () => { + const wrapper = mount(BaseBadge, { + props: { text: "hello" }, + global: { stubs: { BaseButton: ButtonStub } }, + }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.clickable).toBe(false); + expect(wrapper.find("p").exists()).toBe(true); + expect(wrapper.find(".--clickable").exists()).toBe(false); + }); + + it("is clickable when onOnClick attr is present (Vue 3 @on-click normalization)", async () => { + // In Vue 3, a parent binding `@on-click="handler"` is compiled into the attrs object + // as `onOnClick` (on + PascalCase). Passing it via `attrs` in the test mirrors exactly + // what the compiled parent template produces. + const wrapper = mount(BaseBadge, { + props: { text: "hello" }, + attrs: { onOnClick: () => {} }, + global: { stubs: { BaseButton: ButtonStub } }, + }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.clickable).toBe(true); + expect(wrapper.find(".--clickable").exists()).toBe(true); + }); +}); diff --git a/extralit-frontend/components/base/base-badge/BaseBadge.vue b/extralit-frontend/components/base/base-badge/BaseBadge.vue index f291da028..0b05a33ed 100644 --- a/extralit-frontend/components/base/base-badge/BaseBadge.vue +++ b/extralit-frontend/components/base/base-badge/BaseBadge.vue @@ -27,7 +27,7 @@ export default { }; }, mounted() { - if (this.$listeners["on-click"]) { + if (this.$attrs.onOnClick) { this.clickable = true; } }, diff --git a/extralit-frontend/components/base/base-banner/BaseBanner.vue b/extralit-frontend/components/base/base-banner/BaseBanner.vue index 000f18067..04315e324 100644 --- a/extralit-frontend/components/base/base-banner/BaseBanner.vue +++ b/extralit-frontend/components/base/base-banner/BaseBanner.vue @@ -12,8 +12,6 @@ @@ -78,20 +49,29 @@ export default { height: 100%; } -.PDFView { - max-height: calc(100vh - $topbarHeight); // Set maximum height to 100% of the viewport height - overflow-y: auto; // Enable vertical scrolling if the content exceeds the maximum height -} +.pdf-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: $base-space; + height: 100%; + padding: $base-space * 2; + + &__title { + font-size: 14px; + font-weight: 600; + margin: 0; + } + + &__message { + color: var(--color-dark-grey); + margin: 0; + } -.document__title { - flex: 1; - max-width: calc($sidebarWidth / 2); - overflow-x: auto; - -webkit-overflow-scrolling: touch; - white-space: nowrap; - font-size: 14px; - font-weight: 600; - margin: 0; - padding-left: 18px; + &__link { + color: var(--color-primary); + text-decoration: underline; + } } diff --git a/extralit-frontend/components/base/base-radio-button/BaseRadioButton.spec.js b/extralit-frontend/components/base/base-radio-button/BaseRadioButton.spec.js index 039116e82..58dcc1369 100644 --- a/extralit-frontend/components/base/base-radio-button/BaseRadioButton.spec.js +++ b/extralit-frontend/components/base/base-radio-button/BaseRadioButton.spec.js @@ -3,11 +3,11 @@ import BaseRadioButton from "./BaseRadioButton"; let wrapper = null; const options = { - propsData: { + props: { id: "id", name: "name", value: "1", - model: "1", + modelValue: "1", }, }; beforeEach(() => { @@ -15,16 +15,16 @@ beforeEach(() => { }); afterEach(() => { - wrapper.destroy(); + wrapper.unmount(); }); describe("BaseRadioButtonComponent", () => { it("render the component", () => { - expect(wrapper.is(BaseRadioButton)).toBe(true); + expect(wrapper.findComponent(BaseRadioButton).exists()).toBe(true); }); it("bind disabled class", async () => { wrapper = shallowMount(BaseRadioButton, { - propsData: { + props: { disabled: true, }, }); @@ -32,7 +32,7 @@ describe("BaseRadioButtonComponent", () => { }); it("component is selected when model and value matched", async () => { expect(wrapper.vm.isSelected).toBe(true); - expect(wrapper.props().model).toBe("1"); + expect(wrapper.props().modelValue).toBe("1"); }); it("input is checked when model and value matched", async () => { const radioInput = wrapper.find('input[type="radio"]'); diff --git a/extralit-frontend/components/base/base-radio-button/BaseRadioButton.vue b/extralit-frontend/components/base/base-radio-button/BaseRadioButton.vue index 897385a0c..8f34d18d1 100644 --- a/extralit-frontend/components/base/base-radio-button/BaseRadioButton.vue +++ b/extralit-frontend/components/base/base-radio-button/BaseRadioButton.vue @@ -20,7 +20,7 @@ export default { name: { type: String, }, - model: { + modelValue: { type: [String, Number, Boolean, Object], }, value: { @@ -32,13 +32,10 @@ export default { default: "var(--fg-status-submitted)", }, }, - model: { - prop: "model", - event: "change", - }, + emits: ["change", "update:modelValue"], computed: { isSelected() { - return isEqual(this.model, this.value); + return isEqual(this.modelValue, this.value); }, radioClasses() { return { @@ -56,6 +53,7 @@ export default { toggleCheck() { if (!this.disabled) { this.$emit("change", this.value); + this.$emit("update:modelValue", this.value); } }, }, diff --git a/extralit-frontend/components/base/base-range/BaseRangeMultipleSlider.vue b/extralit-frontend/components/base/base-range/BaseRangeMultipleSlider.vue index 6ef152be2..a6f7a4ab2 100644 --- a/extralit-frontend/components/base/base-range/BaseRangeMultipleSlider.vue +++ b/extralit-frontend/components/base/base-range/BaseRangeMultipleSlider.vue @@ -59,7 +59,7 @@ export default { type: Number, default: 1, }, - sliderValues: { + modelValue: { type: Array, default: () => [0, 1], }, @@ -68,21 +68,18 @@ export default { default: () => this.max / 100, }, }, - model: { - prop: "sliderValues", - event: "onSliderValuesChanged", - }, + emits: ["update:modelValue"], data() { return { - values: this.sliderValues, + values: this.modelValue, }; }, watch: { - sliderValues() { - this.values = this.sliderValues; + modelValue() { + this.values = this.modelValue; }, values() { - this.$emit("onSliderValuesChanged", this.values); + this.$emit("update:modelValue", this.values); }, sliderFrom(newValue) { if (newValue > this.sliderTo) { diff --git a/extralit-frontend/components/base/base-range/BaseRangeSlider.vue b/extralit-frontend/components/base/base-range/BaseRangeSlider.vue index 0cebcc282..10acacc34 100644 --- a/extralit-frontend/components/base/base-range/BaseRangeSlider.vue +++ b/extralit-frontend/components/base/base-range/BaseRangeSlider.vue @@ -16,6 +16,7 @@ diff --git a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js index 64f2b4429..4af681de9 100644 --- a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js +++ b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.spec.js @@ -1,22 +1,23 @@ +// @vitest-environment nuxt import { mount } from "@vue/test-utils"; import DatasetConfiguration from "./DatasetConfiguration.vue"; import { ImportHistoryDetails } from "~/v1/domain/entities/import/ImportHistoryDetails"; // Mock dependencies const mockUseDatasetConfiguration = { - getFirstRecord: jest.fn(), - getSuggestedFieldMappings: jest.fn(() => ({})), - configureImportHistoryFields: jest.fn(), - getSuggestedQuestions: jest.fn(() => []), + getFirstRecord: vi.fn(), + getSuggestedFieldMappings: vi.fn(() => ({})), + configureImportHistoryFields: vi.fn(), + getSuggestedQuestions: vi.fn(() => []), firstRecord: { reference: "paper_001", title: "Test Paper" }, }; -jest.mock("./useDatasetConfiguration", () => ({ - useDatasetConfiguration: jest.fn(() => mockUseDatasetConfiguration), +vi.mock("./useDatasetConfiguration", () => ({ + useDatasetConfiguration: vi.fn(() => mockUseDatasetConfiguration), })); -jest.mock("~/v1/domain/entities/import/ImportHistoryDetails", () => ({ - ImportHistoryDetails: jest.fn(), +vi.mock("~/v1/domain/entities/import/ImportHistoryDetails", () => ({ + ImportHistoryDetails: vi.fn(), })); describe("DatasetConfiguration", () => { @@ -26,7 +27,7 @@ describe("DatasetConfiguration", () => { beforeEach(() => { // Reset mocks - jest.clearAllMocks(); + vi.clearAllMocks(); // Mock dataset object mockDataset = { @@ -40,11 +41,11 @@ describe("DatasetConfiguration", () => { type: "rating", }, ], - createFields: jest.fn(() => [ + createFields: vi.fn(() => [ { name: "reference", value: "paper_001" }, { name: "title", value: "Test Paper" }, ]), - changeSubset: jest.fn(), + changeSubset: vi.fn(), }; // Mock ImportHistoryDetails @@ -62,25 +63,24 @@ describe("DatasetConfiguration", () => { }, }; - const ImportHistoryDetails = require("~/v1/domain/entities/import/ImportHistoryDetails"); - ImportHistoryDetails.ImportHistoryDetails.mockImplementation(() => mockImportHistoryDetails); + ImportHistoryDetails.mockImplementation(() => mockImportHistoryDetails); }); afterEach(() => { if (wrapper) { - wrapper.destroy(); + wrapper.unmount(); } - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); describe("HuggingFace Hub Mode", () => { beforeEach(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "hub", }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -116,7 +116,7 @@ describe("DatasetConfiguration", () => { props: ["import-history-details", "loading", "error"], }, BaseIcon: true, - }, + } }, }); }); @@ -142,12 +142,12 @@ describe("DatasetConfiguration", () => { describe("ImportHistory Mode", () => { beforeEach(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "import", importData: mockImportHistoryDetails, }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -183,7 +183,7 @@ describe("DatasetConfiguration", () => { props: ["import-history-details", "loading", "error"], }, BaseIcon: true, - }, + } }, }); }); @@ -212,7 +212,7 @@ describe("DatasetConfiguration", () => { expect(wrapper.emitted("import-dataset-configured")).toBeTruthy(); const emittedEvent = wrapper.emitted("import-dataset-configured")[0][0]; - expect(emittedEvent.dataset).toBe(mockDataset); + expect(emittedEvent.dataset).toEqual(mockDataset); expect(emittedEvent.suggestedMappings).toBeDefined(); expect(emittedEvent.suggestedQuestions).toBeDefined(); }); @@ -221,11 +221,11 @@ describe("DatasetConfiguration", () => { describe("Empty State", () => { beforeEach(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: { ...mockDataset, repoId: null }, dataSource: "hub", }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -250,7 +250,7 @@ describe("DatasetConfiguration", () => { template: '
', props: ["icon-name"], }, - }, + } }, }); }); @@ -265,11 +265,11 @@ describe("DatasetConfiguration", () => { const datasetWithoutQuestions = { ...mockDataset, questions: [] }; wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: datasetWithoutQuestions, dataSource: "hub", }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -291,7 +291,7 @@ describe("DatasetConfiguration", () => { DatasetConfigurationForm: true, ImportHistoryDataPreview: true, BaseIcon: true, - }, + } }, }); expect(wrapper.find(".dataset-config__empty-questions").exists()).toBe(true); @@ -300,11 +300,11 @@ describe("DatasetConfiguration", () => { it("should display questions component when questions exist", () => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "hub", }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -329,7 +329,7 @@ describe("DatasetConfiguration", () => { DatasetConfigurationForm: true, ImportHistoryDataPreview: true, BaseIcon: true, - }, + } }, }); expect(wrapper.find(".mock-questions").exists()).toBe(true); @@ -340,12 +340,12 @@ describe("DatasetConfiguration", () => { describe("Event Handling", () => { beforeEach(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "import", importData: mockImportHistoryDetails, }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
@@ -381,7 +381,7 @@ describe("DatasetConfiguration", () => { props: ["import-history-details", "loading", "error"], }, BaseIcon: true, - }, + } }, }); }); @@ -420,12 +420,12 @@ describe("DatasetConfiguration", () => { describe("Watchers", () => { beforeEach(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "import", importData: mockImportHistoryDetails, }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
`, }, @@ -437,7 +437,7 @@ describe("DatasetConfiguration", () => { DatasetConfigurationForm: true, ImportHistoryDataPreview: true, BaseIcon: true, - }, + } }, }); }); @@ -489,17 +489,17 @@ describe("DatasetConfiguration", () => { }); // Mock console.error to avoid noise in tests - const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); // Should not throw error when mounting expect(() => { wrapper = mount(DatasetConfiguration, { - propsData: { + props: { dataset: mockDataset, dataSource: "import", importData: mockImportHistoryDetails, }, - stubs: { + global: { stubs: { HorizontalResizable: { template: `
` }, VerticalResizable: { template: `
` }, Record: true, @@ -507,7 +507,7 @@ describe("DatasetConfiguration", () => { DatasetConfigurationForm: true, ImportHistoryDataPreview: true, BaseIcon: true, - }, + } }, }); }).not.toThrow(); diff --git a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.vue b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.vue index 7f5f4a37b..bddd13db4 100644 --- a/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.vue +++ b/extralit-frontend/components/features/dataset-creation/configuration/DatasetConfiguration.vue @@ -93,23 +93,24 @@ diff --git a/extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationAddQuestion.vue b/extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationAddQuestion.vue index b5ad31f0a..4ddb8c158 100644 --- a/extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationAddQuestion.vue +++ b/extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationAddQuestion.vue @@ -5,12 +5,12 @@ @visibility="onVisibility" v-if="options.length" > - diff --git a/extralit-frontend/components/features/login/components/LoginInput.vue b/extralit-frontend/components/features/login/components/LoginInput.vue index 7923250fc..610073321 100644 --- a/extralit-frontend/components/features/login/components/LoginInput.vue +++ b/extralit-frontend/components/features/login/components/LoginInput.vue @@ -18,7 +18,7 @@ @blur="isBlurred = true" /> {{ isPasswordVisible ? $t("login.hide") : $t("login.show") }} { diff --git a/extralit-frontend/components/features/user-settings/UserSettingsTheme.vue b/extralit-frontend/components/features/user-settings/UserSettingsTheme.vue index e77534d3c..fd21a9721 100644 --- a/extralit-frontend/components/features/user-settings/UserSettingsTheme.vue +++ b/extralit-frontend/components/features/user-settings/UserSettingsTheme.vue @@ -11,10 +11,6 @@ +``` +(Confirm the real icon directory path and adjust the glob. If icons live as generated JS components under `assets/icons`, instead point the glob at the source SVGs the generator consumed β€” those are the durable source of truth.) +- [ ] **Step 5: Register globally** in `plugins/svg-icon.ts` so the existing `` tag resolves. Either (a) register under both names: +```ts +import { defineNuxtPlugin } from "#app"; +import BaseSvgIcon from "~/components/base/BaseSvgIcon.vue"; +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("svgicon", BaseSvgIcon); + nuxtApp.vueApp.component("SvgIcon", BaseSvgIcon); +}); +``` +This keeps all 79 `` call sites unchanged. Delete `plugins/directives/svg-icon.element.ts`. +- [ ] **Step 6: Run test, expect PASS. Commit** `git commit -m "feat: custom svg-icon component replacing vue-svgicon"` + +### Task 11: Plugin loader + global middleware β†’ Nuxt 4 + +**Files:** Delete `plugins/index.ts`, `plugins/di/di.ts`, `plugins/axios/axios-cache.ts`, `plugins/axios/axios-global-handler.ts`; Create `middleware/route-guard.global.ts`, `middleware/me.global.ts`; Modify the remaining `plugins/*` to Nuxt-4 `defineNuxtPlugin` shape + +- [ ] **Step 1:** Nuxt 4 auto-imports every `plugins/*.ts`. The old `plugins/index.ts` manual `require.context` loader is obsolete β€” delete it. Each former sub-plugin becomes its own `plugins/.ts` exporting `defineNuxtPlugin(...)`. Convert: `plugins/language/*`, `plugins/logo/*`, `plugins/extensions/*` (non-filter ones), `plugins/directives/*` (badge, circle, required-field, tooltip, copy-code) β€” each registers its directive via `nuxtApp.vueApp.directive(...)`. The old signature `export default (context, inject) => {}` becomes `export default defineNuxtPlugin((nuxtApp) => {})`; `inject("x", v)` becomes `nuxtApp.provide("x", v)`. + +- [ ] **Step 2:** Convert `router.middleware: ["route-guard","me"]` to global middleware. Move `middleware/route-guard.ts` β†’ `middleware/route-guard.global.ts` and `middleware/me.ts` β†’ `middleware/me.global.ts`. Rewrite their Nuxt-2 signature `export default ({ $auth, redirect, route }) => {}` to Nuxt-4: +```ts +export default defineNuxtRouteMiddleware((to) => { + const { $auth } = useNuxtApp(); // AuthService, provided in Task 13 + if (!$auth.loggedIn && /* needs auth */) return navigateTo("/sign-in"); + // ...preserve exact original redirect logic, mapping redirect("/") -> navigateTo("/") +}); +``` +Keep the original conditional logic byte-for-byte; only the framework calls change (`redirect(x)`β†’`navigateTo(x)`, `$auth`β†’`useNuxtApp().$auth`). + +- [ ] **Step 3: Commit** `git commit -m "refactor: Nuxt 4 plugin + global middleware structure"` + +### Task 12: Axios plugin + error handler + cache (replaces `@nuxtjs/axios`) + +**Files:** Create `plugins/2.axios.ts`; Modify `v1/infrastructure/repositories/AxiosErrorHandler.ts`, `v1/infrastructure/services/useAxiosExtension.ts`; the `NuxtAxiosInstance` type import in ~20 repo files + +- [ ] **Step 1: Rewrite `AxiosErrorHandler.ts`** to use a standard axios response interceptor instead of auth-next's `$axios.onError`: +```ts +import type { AxiosInstance } from "axios"; +import { useNotifications } from "../services"; + +export const loadErrorHandler = (axios: AxiosInstance, t: (k: string) => string) => { + const notification = useNotifications(); + axios.interceptors.response.use( + (r) => r, + (error) => { + const { status, data } = error.response ?? {}; + notification.clear(); + // ...identical priority logic as the current file (businessLogic β†’ detail β†’ http status), + // calling t(key) and notification.notify(...). Re-throw error at the end. + return Promise.reject(error); + } + ); +}; +``` +Preserve the three-tier message-priority logic verbatim. Note the signature change: it now takes `(axios, t)` instead of a Nuxt `context` (the plugin supplies `t` via i18n). + +- [ ] **Step 2: Rewrite `useAxiosExtension.ts`.** Replace `NuxtAxiosInstance` with a plain axios instance + `makePublic`: +```ts +import axios, { type AxiosInstance } from "axios"; +import { loadCache } from "../repositories/AxiosCache"; +import { loadErrorHandler } from "../repositories/AxiosErrorHandler"; + +export interface PublicAxiosInstance extends AxiosInstance { + makePublic: (config?: { enableErrors: boolean }) => AxiosInstance; +} + +export const useAxiosExtension = (base: AxiosInstance, t: (k: string) => string) => { + const makePublic = (config = { enableErrors: true }) => { + const pub = axios.create({ baseURL: base.defaults.baseURL, withCredentials: false, headers: { Authorization: undefined } }); + if (config.enableErrors) loadErrorHandler(pub, t); + loadCache(pub); + return pub; + }; + const create = () => Object.assign(base, { makePublic }) as PublicAxiosInstance; + return create; +}; +``` +(Keep the public-name `PublicNuxtAxiosInstance` as a type alias re-export if other files import it, to avoid churn: `export type PublicNuxtAxiosInstance = PublicAxiosInstance;`.) + +- [ ] **Step 3: Create `plugins/2.axios.ts`** β€” the single composition root for HTTP + DI: +```ts +import axios from "axios"; +import { defineNuxtPlugin, useRuntimeConfig } from "#app"; +import { useAxiosExtension } from "~/v1/infrastructure/services/useAxiosExtension"; +import { loadCache } from "~/v1/infrastructure/repositories/AxiosCache"; +import { loadErrorHandler } from "~/v1/infrastructure/repositories/AxiosErrorHandler"; + +export default defineNuxtPlugin((nuxtApp) => { + const { $i18n } = nuxtApp as any; + const t = (k: string) => String($i18n.t(k)); + + const instance = axios.create({ baseURL: "/api" }); + // auth header: read token from AuthService (provided by plugins/auth.ts, ordered before this) + instance.interceptors.request.use((cfg) => { + const token = (nuxtApp.$auth as any)?.token; + if (token) cfg.headers.Authorization = `Bearer ${token}`; + return cfg; + }); + loadErrorHandler(instance, t); + loadCache(instance); + + nuxtApp.provide("axios", instance); +}); +``` +Ensure plugin ordering: name files so `auth.ts` (Task 13) loads before `axios.ts` and both before `di.ts`. Nuxt 4 orders plugins alphabetically within `plugins/`; use numeric prefixes (`1.auth.ts`, `2.axios.ts`, `3.di.ts`) to lock order. + +- [ ] **Step 4: Fix the `NuxtAxiosInstance` type imports** in the ~20 repo files: +```bash +grep -rln "@nuxtjs/axios" v1/infrastructure | xargs sed -i \ + -e 's/import { type NuxtAxiosInstance } from "@nuxtjs\/axios";/import type { AxiosInstance } from "axios";/' \ + -e 's/NuxtAxiosInstance/AxiosInstance/g' +``` +Then grep to confirm no `@nuxtjs/axios` references remain. Do **not** change any `this.axios.get/post/...` calls β€” plain axios shares that API. + +- [ ] **Step 5: Commit** `git commit -m "feat: plain-axios HTTP plugin with ported error handler + cache"` + +### Task 13: `AuthService` (replaces `@nuxtjs/auth-next`) + +**Files:** Create `v1/infrastructure/services/AuthService.ts`, `v1/infrastructure/services/AuthService.test.ts`, `plugins/1.auth.ts`; Modify `v1/domain/services/IAuthService.ts`, `v1/di/di.ts` + +- [ ] **Step 1: Drop the auth-next type from the interface.** In `IAuthService.ts`, replace `import { HTTPResponse } from "@nuxtjs/auth-next";` and change `setUserToken(token: string): Promise;` β†’ `setUserToken(token: string): Promise;`. Keep all other members (`loggedIn`, `user`, `logout`, `setUser`). + +- [ ] **Step 2: Failing test** `AuthService.test.ts`: +```ts +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { AuthService } from "./AuthService"; + +describe("AuthService", () => { + let store: Record; + beforeEach(() => { store = {}; }); + const fakeCookie = (k: string) => ({ get value() { return store[k]; }, set value(v) { store[k] = v; } }); + + it("is logged out with no token", () => { + const a = new AuthService(fakeCookie("t") as any); + expect(a.loggedIn).toBe(false); + }); + it("becomes logged in after setUserToken", async () => { + const a = new AuthService(fakeCookie("t") as any); + await a.setUserToken("ABC"); + expect(a.loggedIn).toBe(true); + expect(a.token).toBe("ABC"); + }); + it("clears token and user on logout", async () => { + const a = new AuthService(fakeCookie("t") as any); + await a.setUserToken("ABC"); + a.setUser({ id: 1 }); + await a.logout(); + expect(a.loggedIn).toBe(false); + expect(a.user).toBeNull(); + }); +}); +``` +- [ ] **Step 3: Run, expect FAIL.** +- [ ] **Step 4: Implement** `AuthService.ts` β€” a class taking a token-ref (Nuxt `useCookie` ref injected at plugin time, so the class stays unit-testable): +```ts +import type { Ref } from "vue"; +import type { IAuthService } from "~/v1/domain/services/IAuthService"; + +export class AuthService implements IAuthService { + private _user: Record | null = null; + constructor(private readonly tokenRef: Ref) {} + get token() { return this.tokenRef.value ?? null; } + get loggedIn() { return !!this.tokenRef.value; } + get user() { return this._user; } + setUser(user: unknown) { this._user = (user as Record) ?? null; } + async setUserToken(token: string) { this.tokenRef.value = token; } + async logout() { this.tokenRef.value = null; this._user = null; } +} +``` +- [ ] **Step 5: Run, expect PASS.** +- [ ] **Step 6: Create `plugins/1.auth.ts`:** +```ts +import { defineNuxtPlugin, useCookie } from "#app"; +import { AuthService } from "~/v1/infrastructure/services/AuthService"; +export default defineNuxtPlugin((nuxtApp) => { + const token = useCookie("auth_token", { sameSite: "lax" }); + nuxtApp.provide("auth", new AuthService(token)); +}); +``` +- [ ] **Step 7: Rewire DI** in `v1/di/di.ts`: change `const useAuth = () => context.$auth;` β†’ accept the provided service. Since `loadDependencyContainer` currently takes a Nuxt-2 `context`, update its signature to take `nuxtApp` and read `nuxtApp.$auth` / `nuxtApp.$axios`. Replace `const useAxios = useAxiosExtension(context)` with `const useAxios = useAxiosExtension(nuxtApp.$axios, t)`. Create `plugins/3.di.ts` that calls `loadDependencyContainer(useNuxtApp())`. (The old `plugins/di/di.ts` is deleted in Task 11.) + +- [ ] **Step 8: Commit** `git commit -m "feat: custom AuthService token store implementing IAuthService"` + +--- + +## Phase 3 β€” Mechanical Vue 3 codemods + +### Task 14: Composition-API import swaps (48 files) + +**Files:** every file importing `@nuxtjs/composition-api` + +- [ ] **Step 1:** Map the imports. `ref/computed/watch/onMounted/onBeforeMount/onBeforeUnmount/nextTick/defineComponent` come from `vue`. `useRoute/useRouter` are Nuxt-4 auto-imports (or from `vue-router`). `useContext` β†’ `useNuxtApp`. `useFetch` β†’ Nuxt-4 `useAsyncData`/`useFetch` (semantics differ β€” see Step 3). + +- [ ] **Step 2: Bulk-swap the pure-Vue imports:** +```bash +grep -rln "@nuxtjs/composition-api" components pages v1 layouts | while read f; do + sed -i 's#from "@nuxtjs/composition-api"#from "vue"#g' "$f"; done +``` +Then for each file still importing `useRoute`, `useRouter`, `useContext`, `useFetch` from `vue` (now wrong), hand-fix: remove those names from the `vue` import and rely on Nuxt auto-imports (`useRoute`, `useRouter`, `useNuxtApp`), replacing `useContext()` usages with `useNuxtApp()` and adjusting `.app`/`.$axios`/`.i18n` member access (`ctx.app.i18n` β†’ `nuxtApp.$i18n`). + +- [ ] **Step 3: `useFetch` (β‰ˆ8 files).** Nuxt-2 `useFetch(async () => {...})` ran the body on setup. Replace with `useAsyncData(, async () => {...})` or move the call into `onMounted` if it mutates refs imperatively. Convert one file, verify it compiles, then do the rest the same way. Document each converted key. + +- [ ] **Step 4:** `grep -rl "@nuxtjs/composition-api" .` β†’ expect zero (outside node_modules). Commit `git commit -m "refactor: migrate composition-api imports to Vue 3 / Nuxt composables"` + +### Task 15: `slot`/`slot-scope` β†’ `v-slot` (35 files), `$listeners` β†’ `$attrs` (4 files), filters audit + +**Files:** the 35 legacy-slot `.vue` files; `BaseBadge.vue`, `EntityBadge.vue`, `FilterTooltip.vue`, `FilterBadge.vue` + +- [ ] **Step 1: Slots.** For each of the 35 files, convert `