Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a9d0312
feat(server): add openapi-dump CLI command for v2 schema export
JonnyTran Jul 10, 2026
78c0189
feat(frontend): v2 OpenAPI snapshot + openapi-typescript typegen with…
JonnyTran Jul 10, 2026
ddf9e79
feat(v2-ui): schema/question domain entities + widget-selection mapping
JonnyTran Jul 10, 2026
7c4b0ec
docs
JonnyTran Jul 11, 2026
d62bd00
feat(v2-ui): v2 DI container, SchemaRepository, schemas storage and f…
JonnyTran Jul 12, 2026
c48b946
fix(v2-ui): tighten dtype editor matching and document span exclusion
JonnyTran Jul 12, 2026
4b7d7bb
test(v2-ui): cover getSchema and GetSchemaSettingsUseCase fan-out
JonnyTran Jul 12, 2026
973838a
feat(v2-ui): records domain, V2RecordRepository, search criteria and …
JonnyTran Jul 12, 2026
d874778
feat(v2-ui): /schemas list page with home nav sibling and schemas.* i18n
JonnyTran Jul 12, 2026
02b5931
feat(v2-ui): schema detail page with records table, FTS search and st…
JonnyTran Jul 12, 2026
3e819ad
fix(v2-ui): keep records pager reachable on empty page (roborev job 138)
JonnyTran Jul 12, 2026
ecc68ca
feat(v2-ui): read-only schema settings page with rebuild-index afford…
JonnyTran Jul 12, 2026
ba9ac21
fix(v2-ui): add load-error state to schema settings page (roborev job…
JonnyTran Jul 12, 2026
f896ebc
refactor(frontend): extract v1-free leaf inputs to components/base/in…
JonnyTran Jul 12, 2026
6a4e63a
feat(v2-ui): annotation/projection repositories, value re-wrapping an…
JonnyTran Jul 12, 2026
f1f48e9
test(v2-ui): cover ProjectionRepository encoding/mapping and discard …
JonnyTran Jul 12, 2026
be50a2e
feat(v2-ui): ReferenceReview assembly with name-id join, drafts, orph…
JonnyTran Jul 12, 2026
7917670
feat(v2-ui): submit/save-draft/discard review use-cases with 422 norm…
JonnyTran Jul 12, 2026
96e5509
test(v2-ui): cover multi-schema reference assembly (roborev job 145)
JonnyTran Jul 12, 2026
daa2f45
feat(v2-ui): lean V2TableEditor tabulator wrapper for table questions
JonnyTran Jul 12, 2026
58a231b
fix(v2-ui): stop V2TableEditor rebuilding on self-emit (roborev job 148)
JonnyTran Jul 12, 2026
6402582
feat(v2-ui): pure ProjectionReviewForm with per-type widgets, provena…
JonnyTran Jul 12, 2026
ca15a0b
feat(v2-ui): reference review page wrapping ProjectionReviewForm via …
JonnyTran Jul 12, 2026
f9de2ab
test(v2-ui): cover ReviewCellInput widget-event emit wiring (roborev …
JonnyTran Jul 12, 2026
109709f
test(v2-ui): e2e infra (CDP remote chromium, API seeding) + auth and …
JonnyTran Jul 12, 2026
3f19b48
test(v2-ui): e2e review loop, draft lifecycle and search round-trip s…
JonnyTran Jul 12, 2026
089e6d5
test(v2-ui): pin seeded workspace selection in auth-smoke e2e (robore…
JonnyTran Jul 12, 2026
2139d7a
fix(v2-ui): clean vue-tsc typecheck for v2 (0 errors)
JonnyTran Jul 12, 2026
3746901
test(v2-ui): isolate response-mutating e2e specs to per-spec records …
JonnyTran Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/extralit-frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ jobs:
run: |
npm install

- name: Check generated v2 API types are current 🔒
run: |
npm run gen:api:types
git diff --exit-code -- v2/infrastructure/api/generated

- name: Run lint 🧹
continue-on-error: true
run: |
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/extralit-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ on:
- releases/**
paths:
- "extralit-server/**"
- "extralit-frontend/v2/infrastructure/api/**"

permissions:
id-token: write
Expand Down Expand Up @@ -96,6 +97,12 @@ jobs:
- name: Install dependencies
run: uv sync --dev --extra postgresql

- name: Check frontend v2 OpenAPI snapshot is current 🔒
run: |
uv run python -m extralit_server.cli openapi-dump --output /tmp/openapi-v2.json
diff -u ../extralit-frontend/v2/infrastructure/api/openapi.json /tmp/openapi-v2.json \
|| { echo "::error::v2 OpenAPI drift — run 'npm run gen:api' in extralit-frontend and commit"; exit 1; }

- name: Run tests 📈
id: run-tests
continue-on-error: true
Expand Down
4,805 changes: 4,805 additions & 0 deletions docs/superpowers/plans/2026-07-10-v2-frontend-vertical-slice.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions extralit-frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ node_modules/
dist/
.vercel
.env*

# v2 e2e generated seed output (regenerated per run)
e2e/v2/seed/seed-output.json
24 changes: 22 additions & 2 deletions extralit-frontend/__mocks__/tabulator-tables.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
// Mock implementation for tabulator-tables
export class TabulatorFull {
constructor() {
// Test hooks: how many times the ctor ran (rebuild detection) and the latest instance
// (so tests can fire its stored event handlers). Reset these in a test's beforeEach.
static constructed = 0;
static latest = null;

constructor(element, options) {
this.element = element;
this.options = options || {};
this.handlers = {};
this.rows = [];
this.columns = [];
this.data = [];
// Mock all required methods to prevent errors
this.initialized = true;
TabulatorFull.constructed += 1;
TabulatorFull.latest = this;
}

// Fire a stored handler from a test, e.g. instance.emit("cellEdited", fakeCell).
emit(event, ...args) {
this.handlers[event]?.(...args);
}

addRow(data, position, index) {
Expand Down Expand Up @@ -70,10 +85,15 @@ export class TabulatorFull {
return true;
}

on() {
on(event, callback) {
this.handlers[event] = callback;
return this;
}

destroy() {
return true;
}

redraw() {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<template>
<div class="container" @keydown="keyboardHandler">
<div class="component-header" v-if="showSearch || showCollapseButton">
<div v-if="showSearch || showCollapseButton" class="component-header">
<div class="left-header">
<SearchLabelComponent
v-if="showSearch"
ref="searchComponentRef"
v-model="searchInput"
:searchRef="searchRef"
:search-ref="searchRef"
:placeholder="$t('spanAnnotation.searchLabels')"
/>
</div>
<div class="right-header">
<button
v-if="showCollapseButton"
ref="showLessButtonRef"
type="button"
class="show-less-button cursor-pointer"
v-if="showCollapseButton"
@click="toggleShowLess"
>
<span :class="isExpanded ? '--less' : '--more'" v-text="textToShowInTheCollapseButton" />
Expand All @@ -24,31 +24,31 @@
</div>
</div>
<transition-group
v-if="filteredOptions.length"
ref="inputsAreaRef"
:key="searchInput"
name="shuffle"
:css="modelValue.length < 50"
class="inputs-area"
v-if="filteredOptions.length"
role="group"
aria-multiselectable="multiple"
aria-label="Label-Options"
>
<div
class="input-button"
v-for="(option, index) in visibleOptions"
:key="option.id"
@keydown.enter.prevent
class="input-button"
role="button"
:aria-label="option.text"
@keydown.enter.prevent
>
<input
:id="option.id"
ref="options"
v-model="option.isSelected"
type="checkbox"
:name="option.text"
:id="option.id"
:data-keyboard="keyboards[option.id]"
v-model="option.isSelected"
@change="onSelect(option)"
@focus="onFocus"
@keydown.tab="expandLabelsOnTab(index)"
Expand Down Expand Up @@ -82,7 +82,7 @@
</BaseTooltip>
</div>
</transition-group>
<i class="no-result" v-if="!filteredOptions.length" />
<i v-if="!filteredOptions.length" class="no-result" />
</div>
</template>

Expand All @@ -93,12 +93,6 @@ import { useLabelSelectionViewModel } from "./useLabelSelectionViewModel";

export default {
name: "LabelSelectionComponent",
// Consumed via `v-model="question.answer.values"` from parent (Single/Multi/Rating/Span).
// Vue 3 removed the `model: { prop, event }` option, so `v-model` now desugars to
// `:modelValue` + `@update:modelValue`. The selection list is mutated in place
// (`option.isSelected = ...`), so reactivity flows via the shared array reference;
// we still declare `update:modelValue` for correctness.
emits: ["update:modelValue", "on-focus", "on-selected"],
props: {
maxOptionsToShowBeforeCollapse: {
type: Number,
Expand Down Expand Up @@ -131,39 +125,22 @@ export default {
default: true,
},
},
// Consumed via `v-model="question.answer.values"` from parent (Single/Multi/Rating/Span).
// Vue 3 removed the `model: { prop, event }` option, so `v-model` now desugars to
// `:modelValue` + `@update:modelValue`. The selection list is mutated in place
// (`option.isSelected = ...`), so reactivity flows via the shared array reference;
// we still declare `update:modelValue` for correctness.
emits: ["update:modelValue", "on-focus", "on-selected"],
setup(props) {
return useLabelSelectionViewModel(props);
},
data() {
return {
searchInput: "",
timer: null,
keyCode: "",
};
},
created() {
this.searchRef = `${this.componentId}SearchFilterRef`;
},
watch: {
isFocused: {
immediate: true,
handler(newValue) {
if (newValue) {
this.$nextTick(() => {
const options = this.$refs?.options;
if (options?.some((o) => o.contains(document.activeElement))) {
return;
}

if (options?.length > 0) {
options[0].focus({
preventScroll: true,
});
} else {
this.$refs.searchComponentRef?.searchInputRef.focus();
}
});
}
},
},
},
computed: {
keyboards() {
return this.modelValue.reduce((acc, option, index) => {
Expand Down Expand Up @@ -227,6 +204,32 @@ export default {
return this.maxOptionsToShowBeforeCollapse ?? this.modelValue.length + 1;
},
},
watch: {
isFocused: {
immediate: true,
handler(newValue) {
if (newValue) {
this.$nextTick(() => {
const options = this.$refs?.options;
if (options?.some((o) => o.contains(document.activeElement))) {
return;
}

if (options?.length > 0) {
options[0].focus({
preventScroll: true,
});
} else {
this.$refs.searchComponentRef?.searchInputRef.focus();
}
});
}
},
},
},
created() {
this.searchRef = `${this.componentId}SearchFilterRef`;
},
methods: {
keyboardHandler($event) {
if (this.timer) clearTimeout(this.timer);
Expand Down Expand Up @@ -341,9 +344,6 @@ export default {
return tooltip;
},
},
setup(props) {
return useLabelSelectionViewModel(props);
},
};
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
<template #item="{ element: item }">
<div
:id="item.value"
ref="questions"
:title="item.text"
tabindex="0"
ref="questions"
@keydown="rankWithKeyboard($event, item)"
@focus="onFocus"
>
Expand All @@ -24,15 +24,15 @@
:text="getSuggestedAgent(item)"
minimalist
>
<svgicon width="6" name="draggable" :id="`${item.value}-icon`" aria-label="Dragging Icon" />
<span class="draggable__rank-card__title" v-text="item.text" :id="`${item.value}-span`" />
<svgicon :id="`${item.value}-icon`" width="6" name="draggable" aria-label="Dragging Icon" />
<span :id="`${item.value}-span`" class="draggable__rank-card__title" v-text="item.text" />

<span v-if="isSuggested(item)" class="draggable__suggestion">
<span v-text="getSuggestedRank(item)" />
<svgicon name="suggestion" width="10" height="10" />
<span
class="draggable__suggestion__score"
v-if="getSuggestedScore(item)"
class="draggable__suggestion__score"
v-text="getSuggestedScore(item)"
/>
</span>
Expand All @@ -42,10 +42,10 @@
</draggable>
<div class="draggable__slots-container">
<div
class="draggable__slot"
:class="{ '--active-slot': items.length }"
v-for="{ index, rank, items } in ranking.slots"
:key="index"
class="draggable__slot"
:class="{ '--active-slot': items.length }"
>
<span class="draggable__slot-box--ranking" v-text="rank" />
<draggable
Expand All @@ -60,9 +60,9 @@
<template #item="{ element: item }">
<div
:id="item.value"
ref="items"
:title="item.text"
tabindex="0"
ref="items"
@keydown="rankWithKeyboard($event, item)"
@focus="onFocus"
>
Expand All @@ -72,15 +72,15 @@
minimalist
class="draggable__rank-card--ranked"
>
<svgicon width="6" name="draggable" :id="`${item.value}-icon`" />
<span class="draggable__rank-card__title" v-text="item.text" :id="`${item.value}-span`" />
<svgicon :id="`${item.value}-icon`" width="6" name="draggable" />
<span :id="`${item.value}-span`" class="draggable__rank-card__title" v-text="item.text" />

<span v-if="isSuggested(item)" class="draggable__suggestion">
<span v-text="getSuggestedRank(item)" />
<svgicon name="suggestion" width="10" height="10" />
<span
class="draggable__suggestion__score"
v-if="getSuggestedScore(item)"
class="draggable__suggestion__score"
v-text="getSuggestedScore(item)"
/>
</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { shallowMount } from "@vue/test-utils";
import DndSelectionComponent from "./DndSelection.component";
import { adaptQuestionsToSlots } from "../ranking-adapter";
import { settingsFake, settingsFakeWith12Elements } from "../ranking-fakes";
import { adaptQuestionsToSlots } from "./ranking-adapter";
import { settingsFake, settingsFakeWith12Elements } from "./ranking-fakes";

let wrapper = null;
const options = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@
<div class="container">
<div class="inputs-area" role="radiogroup" aria-label="Label-Options">
<div
class="input-button"
v-for="option in modelValue"
:key="option.id"
@keydown.enter.prevent
class="input-button"
role="button"
:aria-label="option.text"
@keydown.enter.prevent
>
<BaseTooltip
:title="isSuggested(option) ? $t('suggestion.name') : ''"
:text="getSuggestedInfo(option)"
minimalist
>
<input
:id="option.id"
ref="options"
v-model="option.isSelected"
type="checkbox"
:name="option.value"
:id="option.id"
v-model="option.isSelected"
@change="onSelect(option)"
@focus="onFocus"
/>
Expand All @@ -43,9 +43,6 @@
<script>
export default {
name: "RatingMonoSelectionComponent",
// v-model consumer (rating/Rating.component.vue): same Vue 3 migration as
// LabelSelection — rename `options` → `modelValue` and emit `update:modelValue`.
emits: ["update:modelValue", "on-focus", "on-selected"],
props: {
modelValue: {
type: Array,
Expand All @@ -59,6 +56,9 @@ export default {
type: Object,
},
},
// v-model consumer (rating/Rating.component.vue): same Vue 3 migration as
// LabelSelection — rename `options` → `modelValue` and emit `update:modelValue`.
emits: ["update:modelValue", "on-focus", "on-selected"],
watch: {
isFocused: {
immediate: true,
Expand Down
Loading
Loading