diff --git a/HANDOVER.md b/HANDOVER.md deleted file mode 100644 index acb05e57a..000000000 --- a/HANDOVER.md +++ /dev/null @@ -1,52 +0,0 @@ -# Session Handover — PR #214 (OSS audit quick wins) - -**Date:** 2026-06-09 -**Branch:** `chore/quick-wins-203-204-211-212` -**PR:** https://github.com/Extralit/extralit/pull/214 (targets `develop`) -**Outcome:** Quick-win bundle for #203/#204/#211/#212 implemented and **verified**. **All PR CI checks green** on `6c7c48a6e` (frontend build, server package + docker images, SDK matrix 3.9–3.13, Scorecard, Snyk). Also unblocked the frontend CI test step that had been red on `develop` since #200. **Ready to merge.** - -> CI note: `build (3.9)` failed once on a transient Docker-registry timeout pulling the `extralitdev/extralit-hf-space:latest` service container (network flake at "Initialize containers", before any code ran); green on re-run. "Publish Release" shows "skipping" — expected on a PR. - ---- - -## PR #214 status — verification results - -| Item | Scope | Status | -|---|---|---| -| **#203** uv cache path | `extralit-server/docker/server/Dockerfile`: `UV_CACHE_DIR=/home/extralit/.cache/uv` + matching `--mount` target | ✅ Code correct & self-consistent (build-time BuildKit cache; only mount in builder stage). Docker build itself covered by CI "Build extralit-server package". | -| **#204** onboarding docs | README fence fix, SDK README snippets, root `AGENTS.md` → per-component `CLAUDE.md`, `examples/README.md`, `CONTRIBUTING.md` dev-env section | ✅ Committed. | -| **#211** Argilla→Extralit frontend rebrand | `BackendEnvironment.argilla`→`.extralit`, `argillaDatasets`→`extralitDatasets` i18n (4 locales + `DatasetList.vue`), `docs.argilla.io`→`docs.extralit.ai` in `ja.js`, 84 license headers | ✅ Verified: `useRunningEnvironment.test.ts` passes, no `BackendEnvironment.argilla` left in source, `extralitDatasets` consistent across all 4 locales + component, frontend build succeeds. | -| **#212** untrack editor/cache dirs | already in `.gitignore`, not tracked | ✅ No-op (confirmed). | -| **(extra)** empty spec from #200 | `components/features/import/file-upload/useImportFileUploadViewModel.spec.ts` was 0 bytes → Jest "must contain at least one test" → **broke frontend CI test step since #200** | ✅ Added `it.todo` placeholder; suite now green. | - -### Test plan (PR description) — results this session -- **`npm run lint`** — PR's own changed files are clean. The 50 errors `lint` reports are all in files **not touched by this PR** (pre-existing repo-wide debt); CI's lint step is `continue-on-error: true`, so they don't gate the PR. -- **`npm run test`** — 723 passed / 3 skipped; the one prior failure (empty `#200` spec) is fixed. -- **`npm run build`** — Nuxt production build succeeds (exit 0) with the rebranded type/i18n key. -- **Docker build** — Dockerfile change reviewed; defer to CI "Build extralit-server package" (heavy locally on ARM). - ---- - -## Key facts / gotchas for next session - -- **`develop`'s frontend CI has been red since #200** (`2ac8134a7`) because of the empty spec — not a #214 regression. This PR fixes it as a side effect. -- **CI lint vs pre-commit:** the pre-commit hook only lints staged files (so the PR "passed lint" locally), but CI runs full-repo `npm run lint`. Full-repo lint has 50 pre-existing errors incl. a **parse error in `pages/index.vue:157`** — worth a separate cleanup issue, out of scope here. -- **Intentionally retained Argilla references** (do not "fix"): `how-to-configure-argilla-on-huggingface/` doc paths (renamed paths 404), `argilla-dev` localStorage key (breaks installed extensions), `argilla.imglab-cdn.net` CDN, E2E mock fixtures, `CHANGELOG.md [Argilla]` entries, NOTICE attribution. - ---- - -## Remaining / next steps - -1. ✅ **CI green** — frontend build, server package + docker images, SDK matrix 3.9–3.13, Scorecard, Snyk all pass. -2. **Merge** — PR is ready; no blocking checks remain. -3. **Follow-ups (not in this PR):** full-repo frontend lint cleanup (50 errors incl. `pages/index.vue` parse error); the security items flagged but unfiled in the original audit (secret rotation, wildcard CORS in `api/handlers/v1/files.py`, `v-html` sanitization); architecture sequence #206→#207→#208; backend #205; schemas/payloads #209; uv workspaces #210. - ---- - -## Map of files touched by PR #214 -- `extralit-server/docker/server/Dockerfile` — #203 uv cache path -- `extralit/README.md`, root `README.md`, `AGENTS.md`, `CONTRIBUTING.md`, `examples/README.md` — #204 docs -- `extralit-frontend/v1/.../environment/{Environment.ts,EnvironmentRepository.ts}`, `v1/infrastructure/types/environment.ts`, `useRunningEnvironment.test.ts` — #211 env rename -- `extralit-frontend/translation/{en,de,es,ja}.js`, `components/features/home/dataset-list/DatasetList.vue` — #211 i18n -- ~84 frontend source files — #211 license-header convention -- `extralit-frontend/components/features/import/file-upload/useImportFileUploadViewModel.spec.ts` — empty-spec CI fix (this session) diff --git a/docs/superpowers/specs/2026-07-19-reference-review.md b/docs/superpowers/specs/2026-07-19-reference-review.md new file mode 100644 index 000000000..a2b5a2deb --- /dev/null +++ b/docs/superpowers/specs/2026-07-19-reference-review.md @@ -0,0 +1,133 @@ +# Handoff — ReferenceReview slice: design/correctness interrogation brief + +**Date:** 2026-07-19 +**Context branch:** `polish/v2-ui-shell-integration` (PR #232). The ReferenceReview slice itself is **already on `develop`** (merged via #230). +**Goal of next session:** interrogate the Presentation, Domain/infra, and Backend layers for **user design, API design, correctness, and performance** — then decide whether to redesign-in-place, or replace. + +--- + +## Why this brief exists (the reframing) + +I was asked whether the reference-review files are half-baked sprawl worth deleting, this is a critical review of the ReferenceReview vertical slice across three layers to determine if it is worth redesigning. + +**Where the value actually sits:** the *presentation* (~360 LOC) is the under-designed, replaceable part. The *domain/infra + backend contract* (~1,200 LOC, well-tested) is the asset a redesign reuses — deleting it means rebuilding it. + +--- + +## The full surface (the "any others") + +### Presentation (~360 LOC) — the redesign target +- `components/v2/review/ReviewRecordCard.vue` (137) — one card per schema-record; renders context as raw `
`, **dumps orphaned values as `JSON.stringify`**, per-record submit/save-draft/discard footer. +- `components/v2/review/ProjectionReviewForm.vue` (32) — thin list wrapper over records. +- `components/v2/review/ReviewCellInput.vue` (151) — dispatches to existing annotation widgets by question type. +- `components/v2/review/ReviewProvenance.vue` (44) — agent/score/source line. +- `pages/references/[...reference].vue` (69) — catch-all route (slashed DOIs), shell-wrapped in #232. + +### Domain / infra (~1,100 LOC) — the asset +- `v2/domain/entities/review/ReferenceReview.ts` (65) — aggregate: reference → ReviewRecord[] (+ cells, context, orphaned values, draft). +- `v2/domain/entities/review/widget-adapters.ts` (88, +120 test) — **maps v2 questions ↔ existing annotation widget option models** (label/rating/ranking/table). High reuse value. +- `v2/domain/entities/review/widget-mapping.ts` (25, +43 test), `SuggestionHint.ts` (22, +20 test), `response-values.ts` (9, +24 test). +- `v2/domain/usecases/get-reference-review-use-case.ts` (131, **+245 test**) — assembles projection + suggestions + responses + drafts into the review model. +- `v2/domain/usecases/submit-reference-review-use-case.ts` (25, +38 test), `save-review-draft-use-case.ts` (16), `discard-review-use-case.ts` (17). +- `v2/infrastructure/storage/ReferenceReviewsStorage.ts` (27) — Pinia store keyed by reference. +- `v2/infrastructure/repositories/ProjectionRepository.ts` (49) — GET `/v2/projection/references/{encoded}`. +- `v2/di/di.ts` — registrations (partial file). +- `components/v2/schemas/V2RecordsTable.vue` — the **entry-point link** `/references/${encodeURIComponent(reference)}?workspace_id=…` (partial file). + +### Backend (~124 LOC + 2 integration tests) +- `extralit-server/src/extralit_server/api/v2/projection.py` (31) — `GET /projection/references/{reference:path}` → `ProjectionView`. Note the deliberate `/projection/...` prefix to avoid the greedy `:path` on `GET /references/{reference:path}` shadowing it. +- `extralit-server/src/extralit_server/contexts/v2/projection.py` (70) — cross-schema assembly. +- `extralit-server/src/extralit_server/api/schemas/v2/projection.py` (23) — `ProjectionView` DTO. +- Tests: `tests/integration/api/v2/test_projection.py`, `tests/integration/contexts/v2/test_projection.py`. + +### e2e (depend on the slice) +- `e2e/v2/{review-loop,draft-lifecycle,slashed-reference}.spec.ts` — `goto` the `/references/` URL directly; exercise suggestion→response, drafts, slashed-DOI. (These pass **serially**; parallel flakes on shared-schema `rebuild-index` contention — see the PR #232 HANDOVER.) + +--- + +## Interrogation matrix (start here next session) + +Concrete hotspots I noticed while mapping — not yet verified as defects, but the places to point the review. + +### Presentation — **user design** (the weakest layer) +- Orphaned values rendered via `JSON.stringify` (`ReviewRecordCard`) — placeholder, not a designed affordance. +- Context shown as bare `
`; provenance is a minimal agent/score line — no visual hierarchy for "what am I reviewing / what changed." +- Multi-record references: one card stacked per schema-record with **no overview, no navigation, no bulk/keyboard flow**; submit/save-draft/discard are per-card only. +- Loading/empty/error states are minimal (single `V2Empty`/`BaseLoading`). +- **Question for design:** what *is* the review unit — a reference, a schema, a single projected record? The current model shows all records for a reference flat; is that the intended mental model? + +### Domain/infra — **correctness** +- `ReviewRecordCard.cleanValues()` drops `null`/`undefined`/`""` before emit. Interaction with **required-question enforcement** (server-side) and with legitimately-empty answers needs a hard look — can a user intentionally clear a value, and does that round-trip? +- `get-reference-review-use-case.ts` merges projection + suggestions + responses + drafts. Check merge precedence (draft vs saved response vs suggestion), and staleness (Pinia `ReferenceReviewsStorage` cache vs re-fetch). +- `ProjectionRepository` does `encodeURIComponent(reference)` and the backend uses `{reference:path}` — verify no double-encode / slash-decoding mismatch (the `slashed-reference` spec covers the happy path; probe `%2F` vs `/`, `#`, `?` in DOIs). +- `widget-adapters` rebuild option arrays on every `modelValue` change (watch in `ReviewCellInput`) — verify no state loss on external resets (draft restore/discard). + +### Domain/infra + Presentation — **performance** +- No pagination on the review page — loads **all** records for a reference at once. Fan-out cost if a reference spans many schemas/records. +- `ReviewCellInput` watch rebuilds label/rating/ranking option arrays per keystroke/change — check re-render cost with many cells. +- Repeated single-record hydration vs batch. + +### Backend — **API design + performance** +- `ProjectionView` shape: is one endpoint returning the whole cross-schema projection the right granularity, or should it paginate / stream / split per schema? +- `contexts/v2/projection.py` (70 LOC) assembles across schemas — check query count (N schemas × per-record hydration from Postgres); watch for N+1. +- `workspace_id` is a **required query param** on the projection GET — confirm that's the right scoping contract (vs deriving from auth/reference). +- Suggestion/response/draft are **separate PUT/POST** calls from the client (see `submit`/`save-draft`/`discard` use-cases) — is a batched review-submit endpoint warranted? +- The `/projection/...` vs `/references/{reference:path}` routing split (shadowing workaround) — is the URL design coherent for the SDK (#231) too? + +### Backend — **correctness** +- FTS/index eventual consistency already characterized (see PR #232 HANDOVER): reseed leaves a cold index; `:rebuild-index` is needed to warm it. Relevant if projection reads from the index anywhere. +- The `prefilter`-kwarg 500 (FTS + scalar filter) was fixed in #232 (`4fa777343`) — confirm the projection path doesn't have a sibling issue. + +--- + +## Options on the table (decision pending — this is yours) + +1. **Leave #232 as-is** — reference-review stays shell-integrated; a later redesign replaces only the presentation (~360 LOC), reusing domain + backend. Lowest friction. The BaseButton fix + 3 e2e specs need the page to render regardless. +2. **Hide the entry point in #232** *(my recommendation if you want it visibly parked)* — remove the `V2RecordsTable → /references` link (and optionally un-shell the page) so it's unreachable/unblessed, but the code + backend + domain remain for the redesign. e2e specs still pass (they `goto` directly). +3. **Revert the whole slice** — separate PR removing frontend + backend projection + e2e, reconciled against SDK #231. Only if the product call is "pull this direction entirely." **Not recommended** — throws away ~1,200 LOC of tested, reusable machinery + a working backend contract you'd rebuild. + +**Recommendation:** #2 to park it pending design, #1 if fine leaving it reachable. Avoid #3. + +--- + +## Next steps + +1. **Decide the parking option** (#1/#2/#3 above) for PR #232 before it merges. If #2, the change is small: drop the `/references` link in `components/v2/schemas/V2RecordsTable.vue` (and optionally revert the `pages/references/[...reference].vue` shell wrap). +2. **Run the design/brainstorm session** on the *presentation* using `superpowers:brainstorming` — anchor on the "what is the review unit?" question above. +3. **Do the layer interrogation** using the matrix: consider a focused `/code-review` (or the multi-agent `/code-review ultra`) scoped to the review + projection files for correctness/perf, in parallel with the UX design work. +4. **Backend API review** — validate the `ProjectionView` granularity and the projection query fan-out (N+1) before building new UI on top. +5. Keep the domain/infra + backend contract as the stable base; treat the four `components/v2/review/*.vue` + the page as replaceable. + +--- + +## Map of important files + +**Presentation (redesign target)** +- `extralit-frontend/components/v2/review/ReviewRecordCard.vue` +- `extralit-frontend/components/v2/review/ProjectionReviewForm.vue` +- `extralit-frontend/components/v2/review/ReviewCellInput.vue` +- `extralit-frontend/components/v2/review/ReviewProvenance.vue` +- `extralit-frontend/pages/references/[...reference].vue` +- `extralit-frontend/pages/references/useReferenceReviewViewModel.ts` (+ `.test.ts`) + +**Domain / infra (asset — reuse)** +- `extralit-frontend/v2/domain/entities/review/` — `ReferenceReview.ts`, `widget-adapters.ts`, `widget-mapping.ts`, `SuggestionHint.ts`, `response-values.ts` (each with tests) +- `extralit-frontend/v2/domain/usecases/` — `get-reference-review-use-case.ts`, `submit-reference-review-use-case.ts`, `save-review-draft-use-case.ts`, `discard-review-use-case.ts` +- `extralit-frontend/v2/infrastructure/storage/ReferenceReviewsStorage.ts` +- `extralit-frontend/v2/infrastructure/repositories/ProjectionRepository.ts` +- `extralit-frontend/v2/di/di.ts` (registrations) +- `extralit-frontend/components/v2/schemas/V2RecordsTable.vue` (entry-point link) + +**Backend** +- `extralit-server/src/extralit_server/api/v2/projection.py` +- `extralit-server/src/extralit_server/contexts/v2/projection.py` +- `extralit-server/src/extralit_server/api/schemas/v2/projection.py` +- `extralit-server/tests/integration/api/v2/test_projection.py` +- `extralit-server/tests/integration/contexts/v2/test_projection.py` + +**e2e** +- `extralit-frontend/e2e/v2/{review-loop,draft-lifecycle,slashed-reference}.spec.ts` + +**Cross-references** +- `HANDOVER.md` — PR #232 (v2 UI shell) session handoff, incl. the serial-vs-parallel e2e reliability note. +- #230 (`8343da1f8`) — original merge of this slice. #231 — open SDK slice that shares the v2 API surface. diff --git a/extralit-frontend/CLAUDE.md b/extralit-frontend/CLAUDE.md index 555eb0f6e..5e6987246 100644 --- a/extralit-frontend/CLAUDE.md +++ b/extralit-frontend/CLAUDE.md @@ -44,8 +44,31 @@ npm run e2e:report # View test report > `/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. +> inherited ones are Argilla's. The local Playwright chromium **does** launch on the Orin +> host (verified 2026-07-18: `chromium.launch({headless:true})` → Chrome 149 after a plain +> `npx playwright install chromium`; no `install-deps`/sudo needed), so the headless gate +> can run locally as well as in CI. + +## v2 e2e suite (`e2e/v2/`, real backend — the v2 slice's integration gate) + +Separate Playwright project (`--project=v2`, `testMatch: v2/**/*.spec.ts`); the legacy +Argilla specs above are **not** a v2 gate. No network mocking — it exercises real bearer +auth on `/api/v2`, slashed-DOI encoding, the suggestion→response loop, drafts and search +freshness. Env knobs (see `e2e/v2/fixtures.ts`): `E2E_API_URL` (default `http://localhost:6900`), +`E2E_BASE_URL`/`BASE_URL` (default `http://localhost:3000`), `E2E_USERNAME`/`E2E_PASSWORD` +(default `extralit`/`12345678`), optional `E2E_CDP_URL` to drive a remote chromium. + +```bash +npm run e2e:v2:seed # uv run ../extralit-server python e2e/v2/seed/seed_v2_e2e.py +npm run dev -- --host # dev server reachable from the browser +npm run e2e:v2 # playwright test --project=v2 (local chromium) +``` + +Requires the full stack up with the server on :6900. On the Orin host the backing services +publish to `localhost` (postgres :5432, minio :9000, elasticsearch :9200) but the compose +`redis` is **not** published, and the server `.env` uses docker-network hostnames +(`minio`/`elasticsearch`/`redis`) — running the server on the host needs those overridden +to `localhost` plus a throwaway redis on :6379. ## Code Quality diff --git a/extralit-frontend/components/base/base-button/BaseButton.vue b/extralit-frontend/components/base/base-button/BaseButton.vue index 89e3dd11f..5542d1133 100644 --- a/extralit-frontend/components/base/base-button/BaseButton.vue +++ b/extralit-frontend/components/base/base-button/BaseButton.vue @@ -46,7 +46,6 @@ + + diff --git a/extralit-frontend/components/v2/common/V2StatusBadge.test.ts b/extralit-frontend/components/v2/common/V2StatusBadge.test.ts new file mode 100644 index 000000000..18519268c --- /dev/null +++ b/extralit-frontend/components/v2/common/V2StatusBadge.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { mount } from "@vue/test-utils"; +import V2StatusBadge from "./V2StatusBadge.vue"; + +const mountBadge = (status: string) => + mount(V2StatusBadge, { + props: { status }, + global: { + stubs: { BaseBadge: { name: "BaseBadge", template: "{{ text }}", props: ["text", "color"] } }, + }, + }); + +describe("V2StatusBadge", () => { + it("passes the status through as the badge text", () => { + expect(mountBadge("published").text()).toContain("published"); + }); + + it("maps known statuses to distinct token colors", () => { + const color = (s: string) => mountBadge(s).findComponent({ name: "BaseBadge" }).props("color"); + expect(color("completed")).not.toBe(color("discarded")); + }); +}); diff --git a/extralit-frontend/components/v2/common/V2StatusBadge.vue b/extralit-frontend/components/v2/common/V2StatusBadge.vue new file mode 100644 index 000000000..1ace22496 --- /dev/null +++ b/extralit-frontend/components/v2/common/V2StatusBadge.vue @@ -0,0 +1,25 @@ + + + diff --git a/extralit-frontend/components/v2/review/ReviewRecordCard.vue b/extralit-frontend/components/v2/review/ReviewRecordCard.vue index 35bf63b52..1ac6da284 100644 --- a/extralit-frontend/components/v2/review/ReviewRecordCard.vue +++ b/extralit-frontend/components/v2/review/ReviewRecordCard.vue @@ -38,15 +38,27 @@
- - - +
@@ -105,7 +117,7 @@ export default defineComponent({ margin-bottom: $base-space * 2; &__errors { - color: var(--color-danger, #c00); + color: var(--color-danger); } &__actions { diff --git a/extralit-frontend/components/v2/schemas/V2RecordsTable.vue b/extralit-frontend/components/v2/schemas/V2RecordsTable.vue index 6027ef085..8e616fb29 100644 --- a/extralit-frontend/components/v2/schemas/V2RecordsTable.vue +++ b/extralit-frontend/components/v2/schemas/V2RecordsTable.vue @@ -18,7 +18,7 @@ {{ formatCell(record.fields[column.name]) }} - {{ record.status }} + diff --git a/extralit-frontend/composables/useEnsureWorkspaces.test.ts b/extralit-frontend/composables/useEnsureWorkspaces.test.ts new file mode 100644 index 000000000..112dc62a8 --- /dev/null +++ b/extralit-frontend/composables/useEnsureWorkspaces.test.ts @@ -0,0 +1,28 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createPinia, setActivePinia } from "pinia"; + +const execute = vi.fn(async () => [{ id: "w-1", name: "e2e-v2" }]); +vi.mock("ts-injecty", () => ({ useResolve: () => ({ execute }) })); + +import { useEnsureWorkspaces } from "./useEnsureWorkspaces"; +import { useWorkspaces } from "~/v1/infrastructure/storage/WorkspaceStorage"; + +describe("useEnsureWorkspaces", () => { + beforeEach(() => { + setActivePinia(createPinia()); + execute.mockClear(); + }); + + it("fetches workspaces when the store is empty", async () => { + const { ensureWorkspaces } = useEnsureWorkspaces(); + await ensureWorkspaces(); + expect(execute).toHaveBeenCalledOnce(); + }); + + it("does not refetch when workspaces are already loaded", async () => { + useWorkspaces().saveWorkspaces([{ id: "w-1", name: "e2e-v2" } as never]); + const { ensureWorkspaces } = useEnsureWorkspaces(); + await ensureWorkspaces(); + expect(execute).not.toHaveBeenCalled(); + }); +}); diff --git a/extralit-frontend/composables/useEnsureWorkspaces.ts b/extralit-frontend/composables/useEnsureWorkspaces.ts new file mode 100644 index 000000000..39102b5ed --- /dev/null +++ b/extralit-frontend/composables/useEnsureWorkspaces.ts @@ -0,0 +1,20 @@ +import { computed } from "vue"; +import { useResolve } from "ts-injecty"; +import { GetWorkspacesUseCase } from "~/v1/domain/usecases/get-workspaces-use-case"; +import { useWorkspaces } from "~/v1/infrastructure/storage/WorkspaceStorage"; + +// v2 pages are reached by direct URL (deep links from the records table, browser reload). +// Only the home page fetches workspaces, so a hard load lands with an empty store and the +// localStorage-pinned selection has nothing to restore against. Hydrate on mount here. +export const useEnsureWorkspaces = () => { + const getWorkspacesUseCase = useResolve(GetWorkspacesUseCase); + const workspacesStore = useWorkspaces(); + const selectedWorkspace = computed(() => workspacesStore.get().selectedWorkspace); + + const ensureWorkspaces = async () => { + if (workspacesStore.get().workspaces.length > 0) return; + await getWorkspacesUseCase.execute(); // saveWorkspaces + pin-restore happen inside the use-case + }; + + return { ensureWorkspaces, selectedWorkspace }; +}; diff --git a/extralit-frontend/composables/useV2Breadcrumbs.test.ts b/extralit-frontend/composables/useV2Breadcrumbs.test.ts new file mode 100644 index 000000000..6215e9865 --- /dev/null +++ b/extralit-frontend/composables/useV2Breadcrumbs.test.ts @@ -0,0 +1,25 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { createPinia, setActivePinia } from "pinia"; +import { useV2Breadcrumbs } from "./useV2Breadcrumbs"; +import { useWorkspaces } from "~/v1/infrastructure/storage/WorkspaceStorage"; + +describe("useV2Breadcrumbs", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("builds Home › workspace › Schemas with a workspace dropdown item", () => { + const store = useWorkspaces(); + store.saveWorkspaces([{ id: "w-1", name: "e2e-v2" } as never]); + store.saveSelectedWorkspace({ id: "w-1", name: "e2e-v2" } as never); + + const crumbs = useV2Breadcrumbs().schemasBreadcrumbs(); + + expect(crumbs.map((c) => c.name)).toEqual(["Home", "e2e-v2", "Schemas"]); + expect(crumbs[1]).toMatchObject({ isWorkspace: true, workspaceId: "w-1" }); + expect(crumbs[2].link).toBe("/schemas"); + }); + + it("appends leaf crumbs", () => { + const crumbs = useV2Breadcrumbs().schemasBreadcrumbs([{ name: "sample_size", link: "/schemas/s-1" }]); + expect(crumbs.at(-1)).toMatchObject({ name: "sample_size", link: "/schemas/s-1" }); + }); +}); diff --git a/extralit-frontend/composables/useV2Breadcrumbs.ts b/extralit-frontend/composables/useV2Breadcrumbs.ts new file mode 100644 index 000000000..1352d55df --- /dev/null +++ b/extralit-frontend/composables/useV2Breadcrumbs.ts @@ -0,0 +1,19 @@ +import { useWorkspaces } from "~/v1/infrastructure/storage/WorkspaceStorage"; +import { type BreadcrumbItem } from "~/v1/infrastructure/types/breadcrumb"; + +export const useV2Breadcrumbs = () => { + const workspacesStore = useWorkspaces(); + + const schemasBreadcrumbs = (leaf: { name: string; link?: string }[] = []): BreadcrumbItem[] => { + const selected = workspacesStore.get().selectedWorkspace; + const crumbs: BreadcrumbItem[] = [{ name: "Home", link: "/" }]; + if (selected) { + crumbs.push({ name: selected.name, isWorkspace: true, workspaceId: selected.id }); + } + crumbs.push({ name: "Schemas", link: "/schemas" }); + crumbs.push(...leaf); + return crumbs; + }; + + return { schemasBreadcrumbs }; +}; diff --git a/extralit-frontend/pages/references/[...reference].vue b/extralit-frontend/pages/references/[...reference].vue index b023c19f7..84d0c6163 100644 --- a/extralit-frontend/pages/references/[...reference].vue +++ b/extralit-frontend/pages/references/[...reference].vue @@ -1,26 +1,37 @@ diff --git a/extralit-frontend/pages/schemas/[id]/index.vue b/extralit-frontend/pages/schemas/[id]/index.vue index 4aa8fb1f3..b59efba6d 100644 --- a/extralit-frontend/pages/schemas/[id]/index.vue +++ b/extralit-frontend/pages/schemas/[id]/index.vue @@ -1,99 +1,150 @@