diff --git a/.claude/commands/refractor.md b/.claude/commands/refractor.md new file mode 100644 index 000000000..6b8496272 --- /dev/null +++ b/.claude/commands/refractor.md @@ -0,0 +1,17 @@ +--- +description: Refactor code following best practices and design patterns +--- + +## Your task + +Refactor the following code: @$ARGUMENTS + +Guidelines: +1. **Maintain functionality**: Ensure no breaking changes +2. **Improve readability**: Make code more self-documenting +3. **Extract common patterns**: Identify and extract reusable components +4. **Performance optimization**: Improve efficiency where possible +5. **Modern conventions**: Use current best practices +6. **Type safety**: Add or improve type annotations if applicable + +Explain each change and why it's beneficial. \ No newline at end of file diff --git a/.claude/commands/update-claudemd.md b/.claude/commands/update-claudemd.md new file mode 100644 index 000000000..f38b9f223 --- /dev/null +++ b/.claude/commands/update-claudemd.md @@ -0,0 +1,144 @@ +--- +allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(find:*), Bash(grep:*), Bash(wc:*), Bash(ls:*) +description: Automatically update CLAUDE.md file based on recent code changes +--- + +# Update Claude.md File + +## Current Claude.md State +@CLAUDE.md + +## Git Analysis + +### Current Repository Status +!`git status --porcelain` + +### Recent Changes (Last 10 commits) +!`git log --oneline -10` + +### Detailed Recent Changes +!`git log --since="1 week ago" --pretty=format:"%h - %an, %ar : %s" --stat` + +### Recent Diff Analysis +!`git diff HEAD~5 --name-only | head -20` + +### Detailed Diff of Key Changes +!`git diff HEAD~5 -- "*.js" "*.ts" "*.jsx" "*.tsx" "*.py" "*.md" "*.json" | head -200` + +### New Files Added +!`git diff --name-status HEAD~10 | grep "^A" | head -15` + +### Deleted Files +!`git diff --name-status HEAD~10 | grep "^D" | head -10` + +### Modified Core Files +!`git diff --name-status HEAD~10 | grep "^M" | grep -E "(package\.json|README|config|main|index|app)" | head -10` + +## Project Structure Changes +!`find . -name "*.md" -not -path "./node_modules/*" -not -path "./.git/*" | head -10` + +## Configuration Changes +!`git diff HEAD~10 -- package.json tsconfig.json webpack.config.js next.config.js .env* docker* | head -100` + +## API/Route Changes +!`git diff HEAD~10 -- "**/routes/**" "**/api/**" "**/controllers/**" | head -150` + +## Database/Model Changes +!`git diff HEAD~10 -- "**/models/**" "**/schemas/**" "**/migrations/**" | head -100` + +## Your Task + +Based on the current CLAUDE.md content and all the git analysis above, create an updated CLAUDE.md file that: + +## 1. Preserves Important Existing Content +- Keep the core project description and architecture +- Maintain important setup instructions +- Preserve key architectural decisions and patterns +- Keep essential development workflow information + +## 2. Integrates Recent Changes +Analyze the git diff and logs to identify: +- **New Features**: What new functionality was added? +- **API Changes**: New endpoints, modified routes, updated parameters +- **Configuration Updates**: Changes to build tools, dependencies, environment variables +- **File Structure Changes**: New directories, moved files, deleted components +- **Database Changes**: New models, schema updates, migrations +- **Bug Fixes**: Important fixes that affect how the system works +- **Refactoring**: Significant code reorganization or architectural changes + +## 3. Updates Key Sections +Intelligently update these CLAUDE.md sections: + +### Project Overview +- Update description if scope changed +- Note new technologies or frameworks added +- Update version information + +### Architecture +- Document new architectural patterns +- Note significant structural changes +- Update component relationships + +### Setup Instructions +- Add new environment variables +- Update installation steps if dependencies changed +- Note new configuration requirements + +### API Documentation +- Add new endpoints discovered in routes +- Update existing endpoint documentation +- Note authentication or parameter changes + +### Development Workflow +- Update based on new scripts in package.json +- Note new development tools or processes +- Update testing procedures if changed + +### Recent Changes Section +Add a "Recent Updates" section with: +- Summary of major changes from git analysis +- New features and their impact +- Important bug fixes +- Breaking changes developers should know about + +### File Structure +- Update directory explanations for new folders +- Note relocated or reorganized files +- Document new important files + +## 4. Smart Content Management +- **Don't duplicate**: Avoid repeating information already well-documented +- **Prioritize relevance**: Focus on changes that affect how developers work with the code +- **Keep it concise**: Summarize rather than listing every small change +- **Maintain structure**: Follow existing CLAUDE.md organization +- **Add timestamps**: Note when major updates were made + +## 5. Output Format +Provide the complete updated CLAUDE.md content, organized as: + +```markdown +# Project Name + +## Overview +[Updated project description] + +## Architecture +[Updated architecture information] + +## Setup & Installation +[Updated setup instructions] + +## Development Workflow +[Updated development processes] + +## API Documentation +[Updated API information] + +## File Structure +[Updated directory explanations] + +## Recent Updates (Updated: YYYY-MM-DD) +[Summary of recent changes] + +## Important Notes +[Key information for developers] \ No newline at end of file diff --git a/.claude/skills/vue-ui-tracer-bullet.md b/.claude/skills/vue-ui-tracer-bullet.md new file mode 100644 index 000000000..fe9f2d294 --- /dev/null +++ b/.claude/skills/vue-ui-tracer-bullet.md @@ -0,0 +1,181 @@ +--- +description: Build or edit a Vue.js 2 + Nuxt UI component end-to-end using the Tracer Bullet approach — fire one thin wire through the full stack first, confirm it works, then expand. +--- + +## Tracer Bullet Steps + +Work in this order. Do not build stubs; wire one real path per step and verify it renders/passes before expanding. + +1. **Read the adjacent component** most similar to what you're building to internalize current patterns before writing a line. +2. **Create the ViewModel** (`useXxxViewModel.ts`) with one reactive ref and one method — enough to confirm DI and reactivity work. +3. **Create the component** (`Xxx.vue`) with the minimal template that proves the ViewModel connection: one binding, one event. +4. **Wire into parent** — add the `` tag and required props; confirm it mounts and the basic path renders. +5. **Expand** — add remaining computed/watch/methods in the ViewModel, then fill the template. +6. **Add types** in `types.ts` once the shape stabilises. +7. **Write spec** (`Xxx.spec.js`) last, covering the ViewModel logic. + +--- + +## File Anatomy + +``` +components/features// +├── Xxx.vue # Thin template + Options API shell +├── useXxxViewModel.ts # Fat Composition API hook (all logic here) +├── types.ts # Shared TS interfaces for this feature +└── Xxx.spec.js # Jest tests (ViewModel-focused) +``` + +### `Xxx.vue` — what goes where + +| Section | Content | +|---|---| +| `props` | Typed inputs; always provide `default` | +| `emits` | Declare every emitted event | +| `data()` | **Local UI state only** (e.g. `localToggle`, row edits not owned by ViewModel) | +| `computed` | Derives from both `data()` and ViewModel refs via `this.` | +| `watch` | Observe ViewModel refs or computed; call `this.emitUpdate()` etc. | +| `methods` | Event handlers, formatters, local mutations | +| `setup(props)` | **Only line**: `return useXxxViewModel(props)` | + +### `useXxxViewModel.ts` — structure + +```typescript +import { ref, computed, watch } from "@nuxtjs/composition-api"; +import { useResolve } from "ts-injecty"; +import { SomethingUseCase } from "~/v1/domain/usecases/something-use-case"; + +export const useXxxViewModel = (props: { workspace: Workspace | null }) => { + const someUseCase = useResolve(SomethingUseCase); // DI + const isLoading = ref(false); + const data = ref(null); + + const load = async () => { /* ... */ }; + + watch(() => props.workspace?.id, (id) => { if (id) load(); }, { immediate: true }); + + return { isLoading, data, load }; +}; +``` + +--- + +## Critical Pitfall: ts-plugin + Options API + `setup()` + +**Problem**: When a component has both `methods: {}` AND `setup()`, the Vue language plugin types the template `this` as `Vue3Instance` — which only sees `data()` fields and `setup()` returns. Methods from the `methods:{}` block are **invisible to the template type checker**, causing: + +``` +Property 'handleFoo' does not exist on type 'Vue3Instance<...>' +``` + +**This is an IDE-only error** (ts-plugin, not tsc). It does not affect runtime or `npm run build`. + +**Workarounds** (pick one per handler): + +```vue + +@cell-edited="(cell) => handleCellEdit(cell)" + + +setup(props) { + const vm = useXxxViewModel(props); + const handleFoo = () => { /* can't use `this` */ }; + return { ...vm, handleFoo }; +} +``` + +**Rule of thumb**: If the ts-plugin error is on a custom component event (`@cell-edited`, `@table-built`), use Option A. If it's on a standard DOM event (`@click`), it usually resolves itself. + +**Inside computed properties that return config objects** (e.g. Tabulator column configs with callbacks), capture `this` early: + +```typescript +editableTableColumns() { + const vm = this as any; // capture before entering non-Vue callbacks + return [{ + editorParams(cell: any) { + const rows = vm.editableTableData; // safe closure + } + }]; +} +``` + +--- + +## DDD Quick Reference + +``` +v1/ +├── domain/ +│ ├── entities/ # Data shapes — import here everywhere +│ ├── usecases/ # Business logic — one class per operation +│ └── services/ # Interfaces (ports) for repos/services +├── infrastructure/ +│ ├── repositories/ # HTTP implementations of service interfaces +│ └── storage/ # Pinia stores +└── di/di.ts # register() all use cases here +``` + +**Adding a new use case:** +1. Create `v1/domain/usecases/do-thing-use-case.ts` (class with `constructor(private axios)`) +2. Register in `v1/di/di.ts`: `register(DoThingUseCase).withDependency(useAxios).build()` +3. Inject in ViewModel: `const doThing = useResolve(DoThingUseCase)` + +--- + +## SCSS Cheatsheet + +```scss +// Spacing — always multiples of $base-space (8px) +gap: $base-space * 2; // 16px +padding: $base-space * 3; // 24px + +// Color tokens — never hardcode colors +color: var(--fg-primary); +background: var(--bg-accent-grey-1); +border: 1px solid var(--border-field); +// States: --color-success, --color-danger, --color-warning, --bg-action + +// Border radius +border-radius: $border-radius; // 5px (default) +border-radius: $border-radius-m; // 10px (cards/panels) + +// Pierce child component styles +:deep(.tabulator) { ... } + +// Scoped BEM-ish +.my-component { + &__header { ... } + &__body { ... } + &--loading { opacity: 0.6; } +} +``` + +--- + +## Test Pattern + +```javascript +// useXxxViewModel.spec.js +import { useXxxViewModel } from "./useXxxViewModel"; + +jest.mock("ts-injecty", () => ({ useResolve: jest.fn() })); +jest.mock("~/v1/domain/usecases/something-use-case"); + +describe("useXxxViewModel", () => { + let emit; + beforeEach(() => { emit = jest.fn(); }); + + it("initializes with empty state", () => { + const vm = useXxxViewModel({ workspace: null }, { emit }); + expect(vm.data.value).toBeNull(); + }); + + it("loads data when workspace provided", async () => { + const vm = useXxxViewModel({ workspace: { id: "ws1" } }, { emit }); + await vm.load(); + expect(vm.isLoading.value).toBe(false); + }); +}); +``` + +**Testing components**: mount shallowly, stub child components, assert emitted events. See `ImportAnalysisTable.spec.js` for a full example. diff --git a/.gitignore b/.gitignore index 77dd80be5..5d645ef20 100644 --- a/.gitignore +++ b/.gitignore @@ -164,6 +164,5 @@ extralit/site # Development files **/*.db **/*.pdf -.claude/ output/ -.specstory/ \ No newline at end of file +.specstory/ diff --git a/extralit-frontend/components/base/base-render-table/RenderTable.vue b/extralit-frontend/components/base/base-render-table/RenderTable.vue index ff6874489..87b07a051 100644 --- a/extralit-frontend/components/base/base-render-table/RenderTable.vue +++ b/extralit-frontend/components/base/base-render-table/RenderTable.vue @@ -150,6 +150,7 @@ export default { // } // }, // }, + validation: { handler(newValidation, oldValidation) { if (this.isLoaded) { @@ -196,7 +197,7 @@ export default { var configs = this.tableJSON.schema.fields.map((column: DataFrameField) => { const commonConfig = this.generateColumnConfig(column.name); const editableConfig = this.generateColumnEditableConfig(column.name); - return { ...commonConfig, ...editableConfig }; + return { ...commonConfig, ...editableConfig, ...column }; }); if (!this.editable) { @@ -684,7 +685,7 @@ export default { separator: true, }, { - label: "Add column ➡️", + label: "Add column", disabled: !this.editable, action: (e, column) => { this.addColumn(column); @@ -913,6 +914,7 @@ export default { this.tabulator.on("cellEdited", (cell: CellComponent) => { this.updateTableJsonData(); + this.$emit("cell-edited", cell); // const rowPos: number | boolean = cell.getRow().getPosition(); // if (typeof rowPos != 'number' || rowPos < 0 || rowPos > this.tableJSON.data.length) return; // this.$set(this.tableJSON.data[rowPos-1], cell.getColumn().getField(), cell.getValue()); @@ -1021,6 +1023,65 @@ export default { } } } + + // Frozen column header — hardcoded light bg to defeat Tabulator's !important + .tabulator-col.tabulator-frozen { + background-color: hsl(0, 0%, 96%) !important; + will-change: transform; + } + + // Frozen body cells — hardcoded light bg; will-change avoids sticky repaints + .tabulator-cell.tabulator-frozen { + background-color: #fff !important; + will-change: transform; + } + + .tabulator-row-even .tabulator-cell.tabulator-frozen { + background-color: hsl(0, 0%, 100%) !important; + } +} + +// List-editor popup is appended to — must be global with hardcoded light colors +.tabulator-popup-container, +.tabulator-edit-list { + background-color: #fff !important; + color: rgba(0, 0, 0, 0.87) !important; + border: 1px solid hsl(0, 0%, 94%); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + + .tabulator-edit-list-item { + background-color: #fff !important; + color: rgba(0, 0, 0, 0.87) !important; + + &:hover, + &.focused { + background-color: hsl(227, 56%, 92%) !important; + color: hsl(227, 56%, 30%) !important; + } + + // Selected item — prominent brand-color highlight + &.active { + background-color: hsl(227, 56%, 52%) !important; + color: #fff !important; + font-weight: 600; + + &:hover, + &.focused { + background-color: hsl(227, 50%, 44%) !important; + color: #fff !important; + } + } + } + + .tabulator-edit-list-group { + background-color: hsl(0, 0%, 96%) !important; + color: rgba(0, 0, 0, 0.54) !important; + } + + .tabulator-edit-list-notice { + background-color: #fff !important; + color: rgba(0, 0, 0, 0.54) !important; + } } .tabulator { diff --git a/extralit-frontend/components/base/base-render-table/renderTable.test.ts b/extralit-frontend/components/base/base-render-table/renderTable.test.ts index bf42d2ac6..e27d1e2e3 100644 --- a/extralit-frontend/components/base/base-render-table/renderTable.test.ts +++ b/extralit-frontend/components/base/base-render-table/renderTable.test.ts @@ -122,7 +122,7 @@ describe('RenderTable', () => { const menu = wrapper.vm.columnContextMenu(); expect(menu).toContainEqual(expect.objectContaining({ - label: 'Add column ➡️', + label: 'Add column', disabled: false })); }); diff --git a/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue b/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue index 15a62bbc4..a05df91f7 100644 --- a/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue +++ b/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue @@ -6,6 +6,7 @@ :editable="editable" :hasValidValues="hasValidValues" :questions="questions" + :validation="validation || validators" @table-built="$emit('table-built')" @row-click="(e, row) => $emit('row-click', e, row)" @cell-edited="(cell) => $emit('cell-edited', cell)" @@ -71,9 +72,11 @@ export default { computed: { // Convert simple data/columns to TableData format for RenderTable computedTableJSON(): TableData { + // FIX 1: Use "...col" to preserve editor config (dropdowns), validators, and freezing const fields = this.columns.map((col: any) => ({ name: col.field, type: col.type || "string", + ...col // <--- THIS IS THE MAGIC FIX })); const schema = new DataFrameSchema( @@ -89,7 +92,14 @@ export default { null ); - if (this.validation) { + // FIX 2: Handle both 'validation' and 'validators' props + // ImportFileUpload passes :validators, so we must prioritize that. + if (this.validators) { + // We wrap it in a structure RenderTable understands + tableData.validation = { columns: {}, ...this.validation }; + // We might need to pass this directly to the RenderTable prop instead, + // but attaching it to tableData is the safest way for the Schema to know about it. + } else if (this.validation) { tableData.validation = this.validation; } @@ -155,6 +165,22 @@ export default { background: var(--bg-accent-grey-1); overflow: auto; + // Force light-mode colors regardless of [data-theme] + --bg-solid-grey-1: hsl(0, 0%, 98%); + --bg-solid-grey-2: hsl(0, 0%, 96%); + --bg-solid-grey-3: hsl(0, 0%, 90%); + --bg-solid-grey-4: hsl(0, 0%, 90%); + --bg-accent-grey-1: hsl(0, 0%, 100%); + --bg-accent-grey-2: hsl(0, 0%, 100%); + --bg-accent-grey-3: hsl(0, 0%, 98%); + --bg-accent-grey-4: hsl(0, 0%, 98%); + --bg-accent-grey-5: hsl(0, 0%, 100%); + --fg-primary: hsla(0, 0%, 0%, 0.87); + --fg-secondary: hsla(0, 0%, 0%, 0.54); + --fg-tertiary: hsla(0, 0%, 0%, 0.37); + --border-field: hsl(0, 0%, 94%); + --bg-opacity-4: hsla(0, 0%, 0%, 0.04); + // Hide RenderTable's edit buttons when not editable :deep(.table-container) { .__table-buttons { @@ -229,6 +255,24 @@ export default { } } } + + // Frozen column header — override Tabulator's hardcoded bg + .tabulator-col.tabulator-frozen { + background-color: var(--bg-solid-grey-2) !important; + will-change: transform; + } + + // Frozen body cells — prevent dark-bg inheritance + .tabulator-row .tabulator-cell.tabulator-frozen, + .tabulator-row-odd .tabulator-cell.tabulator-frozen { + background-color: var(--bg-accent-grey-1) !important; + will-change: transform; + } + + .tabulator-row-even .tabulator-cell.tabulator-frozen { + background-color: var(--bg-accent-grey-2) !important; + will-change: transform; + } } // When editable, show the buttons diff --git a/extralit-frontend/components/features/annotation/container/mode/useDocumentViewModel.spec.js b/extralit-frontend/components/features/annotation/container/mode/useDocumentViewModel.spec.js index 72cb0ed87..f0bbd72ec 100644 --- a/extralit-frontend/components/features/annotation/container/mode/useDocumentViewModel.spec.js +++ b/extralit-frontend/components/features/annotation/container/mode/useDocumentViewModel.spec.js @@ -111,9 +111,7 @@ describe("useDocumentViewModel", () => { // Get the computed function that was passed to mockComputed const computedCalls = mockComputed.mock.calls; - const hasDocumentLoadedCall = computedCalls.find( - (call) => call[0].toString().includes("document.id") - ); + const hasDocumentLoadedCall = computedCalls.find((call) => call[0].toString().includes("document.id")); expect(hasDocumentLoadedCall).toBeDefined(); const hasDocumentLoaded = hasDocumentLoadedCall[0](); @@ -127,9 +125,7 @@ describe("useDocumentViewModel", () => { // Get the computed function that was passed to mockComputed const computedCalls = mockComputed.mock.calls; - const hasDocumentLoadedCall = computedCalls.find( - (call) => call[0].toString().includes("document.id") - ); + const hasDocumentLoadedCall = computedCalls.find((call) => call[0].toString().includes("document.id")); expect(hasDocumentLoadedCall).toBeDefined(); const hasDocumentLoaded = hasDocumentLoadedCall[0](); @@ -144,7 +140,7 @@ describe("useDocumentViewModel", () => { mockGetDocumentUseCase.createParams.mockReturnValue({ workspace_id: workspaceId, - pmid: "12345" + pmid: "12345", }); useDocumentViewModel({ record: { metadata } }); diff --git a/extralit-frontend/components/features/annotation/settings/SettingsQuestions.vue b/extralit-frontend/components/features/annotation/settings/SettingsQuestions.vue index 0bc8bc48f..1f94b1c9f 100644 --- a/extralit-frontend/components/features/annotation/settings/SettingsQuestions.vue +++ b/extralit-frontend/components/features/annotation/settings/SettingsQuestions.vue @@ -26,7 +26,9 @@
{{$t("question.addLabel")}} {{ $t("question.addLabel") }} diff --git a/extralit-frontend/components/features/documents/DocumentsList.spec.js b/extralit-frontend/components/features/documents/DocumentsList.spec.js index 4bd162ec5..54e72b95d 100644 --- a/extralit-frontend/components/features/documents/DocumentsList.spec.js +++ b/extralit-frontend/components/features/documents/DocumentsList.spec.js @@ -1,6 +1,6 @@ -import { mount } from '@vue/test-utils'; -import DocumentsList from './DocumentsList.vue'; -import { Document } from '~/v1/domain/entities/document/Document'; +import { mount } from "@vue/test-utils"; +import DocumentsList from "./DocumentsList.vue"; +import { Document } from "~/v1/domain/entities/document/Document"; // Mock the view model const mockShowDocumentMetadata = jest.fn(); @@ -13,36 +13,36 @@ const mockViewModel = { totalFiles: 0, showMetadataModal: false, selectedDocumentMetadata: null, - selectedDocumentName: '', + selectedDocumentName: "", loadDocuments: jest.fn(), openDocument: jest.fn(), showDocumentMetadata: mockShowDocumentMetadata, closeMetadataModal: mockCloseMetadataModal, }; -jest.mock('./useDocumentsListViewModel', () => ({ +jest.mock("./useDocumentsListViewModel", () => ({ useDocumentsListViewModel: () => mockViewModel, })); // Mock base components -jest.mock('~/components/base/base-modal/BaseModal.vue', () => ({ - name: 'BaseModal', +jest.mock("~/components/base/base-modal/BaseModal.vue", () => ({ + name: "BaseModal", template: '
', - props: ['modalVisible', 'modalTitle', 'modalClass'], + props: ["modalVisible", "modalTitle", "modalClass"], })); -jest.mock('~/components/base/base-button/BaseButton.vue', () => ({ - name: 'BaseButton', +jest.mock("~/components/base/base-button/BaseButton.vue", () => ({ + name: "BaseButton", template: '', })); -describe('DocumentsList', () => { +describe("DocumentsList", () => { let wrapper; const createWrapper = (props = {}) => { return mount(DocumentsList, { propsData: { - workspaceId: 'test-workspace', + workspaceId: "test-workspace", ...props, }, stubs: { @@ -67,7 +67,7 @@ describe('DocumentsList', () => { mockViewModel.documents = []; mockViewModel.showMetadataModal = false; mockViewModel.selectedDocumentMetadata = null; - mockViewModel.selectedDocumentName = ''; + mockViewModel.selectedDocumentName = ""; mockViewModel.groupedDocuments = []; wrapper = createWrapper(); @@ -77,27 +77,29 @@ describe('DocumentsList', () => { wrapper.destroy(); }); - describe('metadata modal functionality', () => { - it('should show metadata button when document has metadata', async () => { + describe("metadata modal functionality", () => { + it("should show metadata button when document has metadata", async () => { const documentWithMetadata = new Document( - 'doc-1', - 'http://example.com/doc.pdf', - 'test.pdf', - 'pmid123', - 'doi123', + "doc-1", + "http://example.com/doc.pdf", + "test.pdf", + "pmid123", + "doi123", 1, - 'Test Reference', + "Test Reference", [], - { workflow_status: 'completed', analysis_metadata: { ocr_quality: { total_chars: 1000 } } } + { workflow_status: "completed", analysis_metadata: { ocr_quality: { total_chars: 1000 } } } ); // Update the mock view model data instead of using setData mockViewModel.documents = [documentWithMetadata]; - mockViewModel.groupedDocuments = [{ - reference: 'Test Reference', - documents: [documentWithMetadata], - metadata: documentWithMetadata.metadata - }]; + mockViewModel.groupedDocuments = [ + { + reference: "Test Reference", + documents: [documentWithMetadata], + metadata: documentWithMetadata.metadata, + }, + ]; await wrapper.vm.$nextTick(); @@ -106,33 +108,35 @@ describe('DocumentsList', () => { expect(mockViewModel.documents[0].metadata).toBeDefined(); }); - it('should open metadata modal when metadata button is clicked', async () => { + it("should open metadata modal when metadata button is clicked", async () => { const testMetadata = { - workflow_status: 'completed', + workflow_status: "completed", analysis_metadata: { - ocr_quality: { total_chars: 1000, ocr_quality_score: 0.95 } - } + ocr_quality: { total_chars: 1000, ocr_quality_score: 0.95 }, + }, }; const documentWithMetadata = new Document( - 'doc-1', - 'http://example.com/doc.pdf', - 'test-document.pdf', - 'pmid123', - 'doi123', + "doc-1", + "http://example.com/doc.pdf", + "test-document.pdf", + "pmid123", + "doi123", 1, - 'Test Reference', + "Test Reference", [], testMetadata ); // Update the mock view model data mockViewModel.documents = [documentWithMetadata]; - mockViewModel.groupedDocuments = [{ - reference: 'Test Reference', - documents: [documentWithMetadata], - metadata: documentWithMetadata.metadata - }]; + mockViewModel.groupedDocuments = [ + { + reference: "Test Reference", + documents: [documentWithMetadata], + metadata: documentWithMetadata.metadata, + }, + ]; await wrapper.vm.$nextTick(); @@ -144,11 +148,11 @@ describe('DocumentsList', () => { expect(mockShowDocumentMetadata).toHaveBeenCalledTimes(1); }); - it('should close metadata modal when closeMetadataModal is called', () => { + it("should close metadata modal when closeMetadataModal is called", () => { // Set up initial modal state mockViewModel.showMetadataModal = true; - mockViewModel.selectedDocumentMetadata = { some: 'data' }; - mockViewModel.selectedDocumentName = 'test.pdf'; + mockViewModel.selectedDocumentMetadata = { some: "data" }; + mockViewModel.selectedDocumentName = "test.pdf"; // Call the method through the mock mockCloseMetadataModal(); @@ -157,4 +161,4 @@ describe('DocumentsList', () => { expect(mockCloseMetadataModal).toHaveBeenCalledTimes(1); }); }); -}); \ No newline at end of file +}); diff --git a/extralit-frontend/components/features/import/ImportFlow.vue b/extralit-frontend/components/features/import/ImportFlow.vue index 6249bc3e0..ff4e66670 100644 --- a/extralit-frontend/components/features/import/ImportFlow.vue +++ b/extralit-frontend/components/features/import/ImportFlow.vue @@ -381,10 +381,12 @@ export default { hasDataToLose() { // Check if user has uploaded any data that would be lost on close + const analysisTable = this.$refs.analysisTableComponent; return ( (this.bibData.dataframeData && this.bibData.dataframeData.data && this.bibData.dataframeData.data.length > 0) || this.pdfData.totalFiles > 0 || - Object.keys(this.uploadData.confirmedDocuments).length > 0 + Object.keys(this.uploadData.confirmedDocuments).length > 0 || + (analysisTable && analysisTable.editableTableData && analysisTable.editableTableData.length > 0) ); }, diff --git a/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.vue b/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.vue index 81f23e14a..989cb67ee 100644 --- a/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.vue +++ b/extralit-frontend/components/features/import/analysis/ImportAnalysisTable.vue @@ -18,6 +18,29 @@ + +
+
+

Document Metadata

+
+ + + +
+

Unmapped PDF Files ({{ unmappedPdfFiles.length }})

+
    +
  • {{ file }}
  • +
+
+
+
@@ -82,8 +105,8 @@ export default { default: null, }, pdfData: { - type: Object as () => { matchedFiles: any[] } | null, - default: () => ({ matchedFiles: [] }), + type: Object as () => { matchedFiles: any[]; unmatchedFiles?: any[] } | null, + default: () => ({ matchedFiles: [], unmatchedFiles: [] }), }, workspace: { type: Workspace, @@ -100,6 +123,7 @@ export default { data() { return { localDocumentActions: {} as Record, + editableTableData: [] as any[], }; }, @@ -324,9 +348,102 @@ export default { canConfirmImport() { return this.confirmedCount > 0; }, + + shouldShowEditableTable() { + return (!this.dataframeData || this.dataframeData.data.length === 0) + && this.allPdfFileNames.length > 0; + }, + + allPdfFileNames() { + const matched = (this.pdfData?.matchedFiles || []).map((mf: any) => mf.file?.name ?? mf.filename); + const unmatched = (this.pdfData?.unmatchedFiles || []).map((f: any) => f.name ?? f); + return [...matched, ...unmatched]; + }, + + unmappedPdfFiles() { + const assigned = new Set(); + this.editableTableData.forEach((row: any) => { + (Array.isArray(row.files) ? row.files : row.files ? [row.files] : []) + .forEach((f: string) => assigned.add(f)); + }); + return (this.allPdfFileNames as string[]).filter((n: string) => !assigned.has(n)); + }, + + editableTableColumns() { + const vm = this as any; + return [ + { + field: "reference", + title: "Reference", + frozen: true, + width: 200, + editor: "input", + validator: ["required", "unique"], + }, + { + field: "title", + title: "Title", + width: 300, + editor: "input", + }, + { + field: "files", + title: "Files", + frozen: true, + frozenRight: true, + width: 200, + editor: "list", + validator: ["required"], + editorParams: function (cell: any) { + const table = cell.getTable(); + const row = cell.getRow(); + const field = cell.getField(); + const allFiles = new Set(vm.allPdfFileNames); + const usedFiles = new Set(); + table.getRows().forEach((r: any) => { + if (r === row) return; + const v = r.getData()[field]; + if (!v) return; + (Array.isArray(v) ? v : [v]).forEach((f: string) => usedFiles.add(f)); + }); + const current = cell.getValue() || []; + const currentSet = new Set(Array.isArray(current) ? current : [current]); + const values = [...allFiles] + .filter(f => !usedFiles.has(f) || currentSet.has(f)) + .map(f => ({ label: f, value: f })); + return { + values, + multiselect: true, + autocomplete: false, + listOnEmpty: true, + clearable: true, + }; + }, + formatter: (cell: any) => { + const value = cell.getValue(); + if (!value || (Array.isArray(value) && value.length === 0)) { + return 'No files'; + } + const files = Array.isArray(value) ? value : [value]; + const [first, ...rest] = files; + const suffix = rest.length > 0 ? ` +${rest.length}` : ''; + return `${first}${suffix}`; + }, + }, + ]; + }, }, watch: { + shouldShowEditableTable: { + handler(show: boolean) { + if (show && this.editableTableData.length === 0) { + this.initializeEditableTable(); + } + }, + immediate: true, + }, + analysisResult: { handler(newData: ImportAnalysisResponse) { if (newData) { @@ -459,8 +576,34 @@ export default { const confirmedDocuments: Record = {}; const documentActions = { ...this.documentActions, ...this.localDocumentActions }; + // Handle editable table case: PDFs uploaded without bibliography + if (this.shouldShowEditableTable) { + this.editableTableData.forEach((row: any) => { + const reference = row.reference?.trim(); + const filesArray = Array.isArray(row.files) ? row.files : (row.files ? [row.files] : []); + if (!reference || filesArray.length === 0) return; + confirmedDocuments[reference] = { + document_create: { + reference, + title: row.title || undefined, + authors: row.authors ? [row.authors] : undefined, + year: row.year ? String(row.year) : undefined, + journal: row.journal || undefined, + doi: row.doi || undefined, + workspace_id: this.workspace?.id, + metadata: { + source: "pdf_import", + collections: [this.workspace?.name || "default"], + }, + }, + associated_files: filesArray.map((filename: string) => ({ + filename, + size: this.getFileSize(filename) || 0, + })), + }; + }); // Handle analysis data case (preferred) - if (this.analysisResult && this.analysisResult.documents && Object.keys(this.analysisResult.documents).length > 0) { + } else if (this.analysisResult && this.analysisResult.documents && Object.keys(this.analysisResult.documents).length > 0) { Object.entries(this.analysisResult.documents).forEach(([reference, docInfo]: [string, DocumentImportAnalysis]) => { const finalAction = documentActions[reference] || docInfo.status; const hasFiles = docInfo.associated_files && docInfo.associated_files.length > 0; @@ -540,16 +683,46 @@ export default { }, getFileSize(filename: string): number { - // Try to find the file size from matched files if (this.pdfData?.matchedFiles) { - const matchedFile = this.pdfData.matchedFiles.find((mf: any) => mf.file.name === filename); + const matchedFile = this.pdfData.matchedFiles.find((mf: any) => (mf.file?.name ?? mf.filename) === filename); if (matchedFile) { - return matchedFile.file.size || 0; + return matchedFile.file?.size || matchedFile.size || 0; + } + } + if (this.pdfData?.unmatchedFiles) { + const unmatchedFile = this.pdfData.unmatchedFiles.find((f: any) => (f.name ?? f) === filename); + if (unmatchedFile) { + return unmatchedFile.size || 0; } } return 0; }, + handleTableCellEdit(cell: any) { + this.editableTableData = cell.getTable().getData(); + this.emitUpdate(); + }, + + handleTableBuilt() { + // no-op: data is managed via cell-edited events + }, + + initializeEditableTable() { + const pdfFiles = this.allPdfFileNames as string[]; + this.editableTableData = pdfFiles.length > 0 + ? pdfFiles.map((filename: string) => ({ + reference: null, + title: null, + files: [filename], + })) + : Array.from({ length: 3 }, () => ({ + reference: null, + title: null, + files: [], + })); + this.emitUpdate(); + }, + resetLocalState() { this.localDocumentActions = {}; }, @@ -872,4 +1045,68 @@ export default { background: var(--bg-solid-grey-2); } } + +// Editable table section styles +.editable-table-section { + display: flex; + flex-direction: column; + gap: $base-space * 2; + padding: $base-space * 3; + background: var(--bg-accent-grey-1); + border: 1px solid var(--border-field); + border-radius: $border-radius-m; + min-height: 300px; +} + +.editable-table-header { + margin-bottom: $base-space; + + h3 { + font-size: 1.2rem; + font-weight: 600; + margin-bottom: $base-space; + color: var(--fg-primary); + } + + p { + color: var(--fg-secondary); + font-size: 0.9rem; + margin-bottom: 0; + line-height: 1.4; + + strong { + color: var(--fg-primary); + font-weight: 600; + } + } +} + +.unmapped-pdfs-warning { + margin-top: $base-space * 2; + padding: $base-space * 2; + background: var(--bg-banner-warning); + border: 1px solid var(--color-warning); + border-radius: $border-radius; + + h4 { + margin: 0 0 $base-space 0; + color: var(--fg-primary); + font-size: 1rem; + font-weight: 600; + } + + ul { + margin: 0; + padding-left: $base-space * 3; + max-height: 200px; + overflow-y: auto; + + li { + color: var(--fg-primary); + font-size: 0.9rem; + margin-bottom: calc($base-space / 2); + font-family: $quaternary-font-family; + } + } +} diff --git a/extralit-frontend/components/features/import/analysis/useImportAnalysisTableViewModel.ts b/extralit-frontend/components/features/import/analysis/useImportAnalysisTableViewModel.ts index 99eb2e8eb..e8b3682f0 100644 --- a/extralit-frontend/components/features/import/analysis/useImportAnalysisTableViewModel.ts +++ b/extralit-frontend/components/features/import/analysis/useImportAnalysisTableViewModel.ts @@ -10,6 +10,7 @@ export const useImportAnalysisTableViewModel = (props: { dataframeData: TableData | null; pdfData: { matchedFiles: any[]; + unmatchedFiles?: any[]; } | null; }) => { const importAnalysisUseCase = useResolve(GetImportAnalysisUseCase); @@ -98,6 +99,8 @@ export const useImportAnalysisTableViewModel = (props: { workspaceId: props.workspace?.id, dataframeLength: props.dataframeData?.data?.length, matchedFilesLength: props.pdfData?.matchedFiles?.length, + // Add deep watch on dataframe data to catch cell edits + dataframeDataHash: props.dataframeData ? JSON.stringify(props.dataframeData.data) : null, }), (newVal, oldVal) => { // Only trigger if we have all required data and something actually changed @@ -107,7 +110,8 @@ export const useImportAnalysisTableViewModel = (props: { !oldVal || newVal.workspaceId !== oldVal.workspaceId || newVal.dataframeLength !== oldVal.dataframeLength || - newVal.matchedFilesLength !== oldVal.matchedFilesLength + newVal.matchedFilesLength !== oldVal.matchedFilesLength || + newVal.dataframeDataHash !== oldVal.dataframeDataHash ) { analyzeImport(props.workspace!, props.dataframeData!, props.pdfData!.matchedFiles); } diff --git a/extralit-frontend/components/features/import/file-upload/ImportFileUpload.vue b/extralit-frontend/components/features/import/file-upload/ImportFileUpload.vue index 381d641e3..c426a4ca3 100644 --- a/extralit-frontend/components/features/import/file-upload/ImportFileUpload.vue +++ b/extralit-frontend/components/features/import/file-upload/ImportFileUpload.vue @@ -12,6 +12,7 @@ +
@@ -686,5 +687,6 @@ flex-direction: column; } } + } diff --git a/extralit-frontend/components/features/import/file-upload/useImportFileUploadViewModel.ts b/extralit-frontend/components/features/import/file-upload/useImportFileUploadViewModel.ts index 1ebb8a8ca..aa0cdfec1 100644 --- a/extralit-frontend/components/features/import/file-upload/useImportFileUploadViewModel.ts +++ b/extralit-frontend/components/features/import/file-upload/useImportFileUploadViewModel.ts @@ -36,6 +36,7 @@ export const useImportFileUploadViewModel = (props: any, { emit }: any) => { dataframeData: data.dataframeData || null, rawContent: data.rawContent || "", }; + emitBibUpdate(); }; @@ -233,4 +234,4 @@ export const useImportFileUploadViewModel = (props: any, { emit }: any) => { initializeWithExistingData, reset, }; -}; \ No newline at end of file +}; diff --git a/extralit-frontend/components/features/import/useImportBatchProgressViewModel.spec.js b/extralit-frontend/components/features/import/useImportBatchProgressViewModel.spec.js index ae0d26552..fe139cba3 100644 --- a/extralit-frontend/components/features/import/useImportBatchProgressViewModel.spec.js +++ b/extralit-frontend/components/features/import/useImportBatchProgressViewModel.spec.js @@ -27,7 +27,7 @@ describe("useImportBatchProgressViewModel", () => { workspace: { id: "workspace-1" }, uploadData: { confirmedDocuments: {}, - documentActions: {} + documentActions: {}, }, bibFileName: "test.bib", }; diff --git a/extralit-frontend/package-lock.json b/extralit-frontend/package-lock.json index dcbfd6729..f78bb92f0 100644 --- a/extralit-frontend/package-lock.json +++ b/extralit-frontend/package-lock.json @@ -3468,128 +3468,6 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core/node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/@inquirer/core/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, "node_modules/@interactjs/types": { "version": "1.10.27", "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.27.tgz", @@ -6629,28 +6507,6 @@ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", "dev": true }, - "node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" - } - }, "node_modules/@open-draft/until": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz", @@ -7707,15 +7563,6 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, - "node_modules/@types/statuses": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", - "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", @@ -26379,15 +26226,6 @@ "node": ">= 4" } }, - "node_modules/rettime": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", - "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -28046,21 +27884,6 @@ "integrity": "sha512-qFW7kfadtcaISQIibKAIy0f3eeIXUVi8242Vly1iJfMD79kfEGzfczNuPBN/80hDxHzQJXYbmJ8VipI40hQtfA==", "license": "MIT" }, - "node_modules/tagged-tag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", @@ -28495,30 +28318,6 @@ "@popperjs/core": "^2.9.0" } }, - "node_modules/tldts": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", - "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tldts-core": "^7.0.19" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", - "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -29329,18 +29128,6 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, - "node_modules/until-async": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", - "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/kettanaito" - } - }, "node_modules/upath": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", @@ -30493,15 +30280,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite-node/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/vite-node/node_modules/fdir": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", @@ -30537,40 +30315,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vite-node/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/vite-node/node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/vite-node/node_modules/vite": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.0.tgz", @@ -30646,21 +30390,6 @@ } } }, - "node_modules/vite-node/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/vitest": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", @@ -30747,35 +30476,6 @@ "vitest": ">=2.0.0" } }, - "node_modules/vitest/node_modules/@mswjs/interceptors": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", - "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/vitest/node_modules/@vitest/mocker": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", @@ -30803,90 +30503,6 @@ } } }, - "node_modules/vitest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/vitest/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/vitest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/vitest/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/vitest/node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/vitest/node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -30912,62 +30528,6 @@ } } }, - "node_modules/vitest/node_modules/headers-polyfill": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/vitest/node_modules/msw": { - "version": "2.12.3", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.3.tgz", - "integrity": "sha512-/5rpGC0eK8LlFqsHaBmL19/PVKxu/CCt8pO1vzp9X6SDLsRDh/Ccudkf3Ur5lyaKxJz9ndAx+LaThdv0ySqB6A==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.40.0", - "@open-draft/deferred-promise": "^2.2.0", - "@types/statuses": "^2.0.6", - "cookie": "^1.0.2", - "graphql": "^16.12.0", - "headers-polyfill": "^4.0.2", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "path-to-regexp": "^6.3.0", - "picocolors": "^1.1.1", - "rettime": "^0.7.0", - "statuses": "^2.0.2", - "strict-event-emitter": "^0.5.1", - "tough-cookie": "^6.0.0", - "type-fest": "^5.2.0", - "until-async": "^3.0.2", - "yargs": "^17.7.2" - }, - "bin": { - "msw": "cli/index.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mswjs" - }, - "peerDependencies": { - "typescript": ">= 4.8.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/vitest/node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -30988,94 +30548,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vitest/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/vitest/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vitest/node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/vitest/node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/vitest/node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/vitest/node_modules/type-fest": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.2.0.tgz", - "integrity": "sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "peer": true, - "dependencies": { - "tagged-tag": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/vitest/node_modules/vite": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.0.tgz", @@ -31151,86 +30623,6 @@ } } }, - "node_modules/vitest/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/vitest/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/vitest/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/vitest/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - } - }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -33833,21 +33225,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zip-js-esm": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/zip-js-esm/-/zip-js-esm-1.1.1.tgz",