Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
82 changes: 82 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added - 2025-12-25

#### TableField and TableQuestion Support (#114)

**New Features:**
- Added support for `TableField` type in dataset field configuration
- Table field type now available in field type dropdown
- Users can map JSON table types to TableField
- Added `isTableType` getter for type checking

- Added support for `TableQuestion` type in dataset question configuration
- Table question type now available in question type dropdown
- Dynamic column management (add/remove columns)
- Each column configurable with name, title, and description
- Default initialization with 2 sample columns
- Validation ensures at least one column is defined

**Components:**
- New `DatasetConfigurationTableQuestion.vue` component for table question UI
- Column list with inline editing
- Add/remove column buttons
- Input validation with error display
- Focus event handling for better UX

**Backend Changes:**
- `FieldCreation.ts`: Added table field type support
- Added to `availableFieldTypes` array
- Updated `FieldCreationTypes` type
- Added `isTableType` getter method

- `QuestionCreation.ts`: Added table question type support
- Added to `availableQuestionTypes` array
- Imported and integrated `TableQuestionAnswer`
- Added `isTableType` getter method
- Implemented `createInitialAnswers()` for table questions
- Added validation requiring at least one column

- `Subset.ts`: Added default settings initialization
- Auto-creates 2 default columns when table question added
- Sets `use_table: true` flag

**UI Updates:**
- `DatasetConfigurationQuestion.vue`: Added conditional rendering for table questions
- Added i18n translations for table question UI elements

**Tests:**
- Added unit tests for table question initialization
- Added unit tests for table question validation
- Added comprehensive Vue component tests for `DatasetConfigurationTableQuestion`
- Component rendering tests
- Column management tests (add/remove/update)
- Validation behavior tests
- Event emission tests

**Files Modified:**
- `extralit-frontend/v1/domain/entities/hub/FieldCreation.ts`
- `extralit-frontend/v1/domain/entities/hub/QuestionCreation.ts`
- `extralit-frontend/v1/domain/entities/hub/Subset.ts`
- `extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationQuestion.vue`
- `extralit-frontend/translation/en.js`
- `extralit-frontend/v1/domain/entities/hub/DatasetCreation.test.ts`

**Files Added:**
- `extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationTableQuestion.vue`
- `extralit-frontend/components/features/dataset-creation/configuration/questions/DatasetConfigurationTableQuestion.test.ts`

**Impact:**
- Users can now create and configure table fields in dataset schemas
- Users can now create table questions for table annotation workflows
- No breaking changes to existing question or field types
- Follows existing architectural patterns for extensibility

**Related Issue:** #114
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
v-model="question.settings.options"
@is-focused="$emit('is-focused', $event)"
/>
<DatasetConfigurationTableQuestion
v-else-if="question.settings.type.isTableType"
:question="question"
@is-focused="$emit('is-focused', $event)"
/>
</template>
<span class="separator"></span>
<DatasetConfigurationColumnSelector
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { shallowMount } from "@vue/test-utils";
import DatasetConfigurationTableQuestion from "./DatasetConfigurationTableQuestion.vue";
import { QuestionCreation } from "~/v1/domain/entities/hub/QuestionCreation";
import { Subset } from "~/v1/domain/entities/hub/Subset";

// Mock the Validation component
jest.mock("~/components/base/base-validation/Validation.vue", () => ({
name: "Validation",
template: '<div class="mock-validation"></div>',
props: ["validations"],
}));

// Mock SVG icon
jest.mock("assets/icons/close", () => ({}));

describe("DatasetConfigurationTableQuestion", () => {
let mockSubset: Subset;
let mockQuestion: QuestionCreation;

beforeEach(() => {
// Create a mock subset with required structure
const datasetInfo = {
splits: {
train: {
name: "train",
num_examples: 100,
},
},
features: {},
};

mockSubset = new Subset("default", datasetInfo);

// Create a table question
mockQuestion = new QuestionCreation(
mockSubset,
"table_question",
{
type: "table",
options: [
{ name: "column1", title: "Column 1", description: "" },
{ name: "column2", title: "Column 2", description: "" },
],
use_table: true,
}
);
});

describe("rendering", () => {
it("renders without crashing", () => {
const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

expect(wrapper.exists()).toBe(true);
expect(wrapper.find(".table-config").exists()).toBe(true);
});

it("renders column inputs for each column", () => {
const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

const columnInputs = wrapper.findAll(".table-config__column");
expect(columnInputs.length).toBe(2);
});

it("renders add column button", () => {
const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

expect(wrapper.find(".table-config__add-btn").exists()).toBe(true);
});

it("renders remove button for each column when more than one column", () => {
const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

const removeButtons = wrapper.findAll(".table-config__remove-btn");
expect(removeButtons.length).toBe(2);
});

it("does not render remove button when only one column", () => {
mockQuestion.settings.options = [
{ name: "column1", title: "Column 1", description: "" },
];

const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

expect(wrapper.find(".table-config__remove-btn").exists()).toBe(false);
});
});

describe("column management", () => {
it("adds a new column when add button is clicked", async () => {
const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

const addButton = wrapper.find(".table-config__add-btn");
await addButton.trigger("click");

expect(mockQuestion.settings.options.length).toBe(3);
expect(mockQuestion.settings.options[2]).toEqual({
name: "column3",
title: "Column 3",
description: "",
});
});

it("removes a column when remove button is clicked", async () => {
const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

const removeButtons = wrapper.findAll(".table-config__remove-btn");
await removeButtons.at(0).trigger("click");

expect(mockQuestion.settings.options.length).toBe(1);
expect(mockQuestion.settings.options[0].name).toBe("column2");
});

it("updates column name when input changes", async () => {
const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

const nameInputs = wrapper.findAll(".table-config__input--name");
const firstNameInput = nameInputs.at(0);

await firstNameInput.setValue("new_column_name");
await firstNameInput.trigger("input");

expect(mockQuestion.settings.options[0].name).toBe("new_column_name");
});

it("updates column title when input changes", async () => {
const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

const titleInputs = wrapper.findAll(".table-config__input--title");
const firstTitleInput = titleInputs.at(0);

await firstTitleInput.setValue("New Column Title");
await firstTitleInput.trigger("input");

expect(mockQuestion.settings.options[0].title).toBe("New Column Title");
});
});

describe("validation", () => {
it("displays validation errors when question is invalid", async () => {
mockQuestion.settings.options = [];

const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

const input = wrapper.find(".table-config__input");
await input.trigger("blur");

await wrapper.vm.$nextTick();

expect(wrapper.findComponent({ name: "Validation" }).exists()).toBe(true);
});

it("emits is-focused event on input focus", async () => {
const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

const input = wrapper.find(".table-config__input");
await input.trigger("focus");

expect(wrapper.emitted("is-focused")).toBeTruthy();
expect(wrapper.emitted("is-focused")[0]).toEqual([true]);
});

it("emits is-focused false on input blur", async () => {
const wrapper = shallowMount(DatasetConfigurationTableQuestion, {
propsData: {
question: mockQuestion,
},
mocks: {
$t: (key: string) => key,
},
});

const input = wrapper.find(".table-config__input");
await input.trigger("blur");

expect(wrapper.emitted("is-focused")).toBeTruthy();
expect(wrapper.emitted("is-focused")[0]).toEqual([false]);
});
});
});
Loading