diff --git a/backend/src/main/java/com/odde/doughnut/controllers/RestObsidianImportController.java b/backend/src/main/java/com/odde/doughnut/controllers/RestObsidianImportController.java index a913c062a..b458d8940 100644 --- a/backend/src/main/java/com/odde/doughnut/controllers/RestObsidianImportController.java +++ b/backend/src/main/java/com/odde/doughnut/controllers/RestObsidianImportController.java @@ -1,20 +1,40 @@ -// package com.odde.doughnut.controllers; -// -// import com.odde.doughnut.controllers.dto.NoteRealm; -// import com.odde.doughnut.exceptions.UnexpectedNoAccessRightException; -// import org.springframework.web.bind.annotation.*; -// import org.springframework.web.multipart.MultipartFile; -// -// @RestController -// @RequestMapping("/api") -// public class RestObsidianImportController { -// -// public RestObsidianImportController() {} -// -// @PostMapping("/obsidian/{parentNoteId}/import") -// public NoteRealm importObsidianNotes(MultipartFile file, @PathVariable Integer parentNoteId) -// throws UnexpectedNoAccessRightException { -// // Implementation will be added later -// return null; -// } -// } +package com.odde.doughnut.controllers; + +import com.odde.doughnut.controllers.dto.NoteRealm; +import com.odde.doughnut.entities.Note; +import com.odde.doughnut.entities.Notebook; +import com.odde.doughnut.exceptions.UnexpectedNoAccessRightException; +import com.odde.doughnut.models.NoteViewer; +import com.odde.doughnut.models.UserModel; +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/api") +public class RestObsidianImportController { + private final UserModel currentUser; + + public RestObsidianImportController(UserModel currentUser) { + this.currentUser = currentUser; + } + + @PostMapping("/obsidian/{parentNoteId}/import") + public NoteRealm importObsidian( + @RequestParam("file") MultipartFile file, + @PathVariable("parentNoteId") @Schema(type = "integer") Integer parentNoteId) + throws UnexpectedNoAccessRightException { + currentUser.assertLoggedIn(); + + Notebook notebook = + currentUser.getEntity().getOwnership().getNotebooks().stream() + .filter(n -> n.getId().equals(parentNoteId)) + .findFirst() + .orElseThrow(() -> new UnexpectedNoAccessRightException()); + + // TODO: Process zip file content + // For now, just return the head note + Note note = notebook.getHeadNote(); + return new NoteViewer(currentUser.getEntity(), note).toJsonObject(); + } +} diff --git a/backend/src/test/java/com/odde/doughnut/controllers/RestNotebookControllerTest.java b/backend/src/test/java/com/odde/doughnut/controllers/RestNotebookControllerTest.java index e1072314c..493c1c936 100644 --- a/backend/src/test/java/com/odde/doughnut/controllers/RestNotebookControllerTest.java +++ b/backend/src/test/java/com/odde/doughnut/controllers/RestNotebookControllerTest.java @@ -283,7 +283,7 @@ void shouldNotAllowUnauthorizedAccess() { } @Nested - class DownloadNotebookAsZip { + class DownloadForObsidian { private Notebook notebook; private Note note1; private Note note2; @@ -307,12 +307,12 @@ void whenNotAuthorized() { modelFactoryService.toUserModel(anotherUser), testabilitySettings); assertThrows( - UnexpectedNoAccessRightException.class, () -> controller.downloadNotebookAsZip(notebook)); + UnexpectedNoAccessRightException.class, () -> controller.downloadForObsidian(notebook)); } @Test void whenAuthorizedShouldReturnZipWithMarkdownFiles() throws Exception { - byte[] zipContent = controller.downloadNotebookAsZip(notebook); + byte[] zipContent = controller.downloadForObsidian(notebook); try (ByteArrayInputStream bais = new ByteArrayInputStream(zipContent); ZipInputStream zis = new ZipInputStream(bais)) { diff --git a/backend/src/test/java/com/odde/doughnut/controllers/RestObsidianImportControllerTests.java b/backend/src/test/java/com/odde/doughnut/controllers/RestObsidianImportControllerTests.java index 2767fbe20..07d41ab21 100644 --- a/backend/src/test/java/com/odde/doughnut/controllers/RestObsidianImportControllerTests.java +++ b/backend/src/test/java/com/odde/doughnut/controllers/RestObsidianImportControllerTests.java @@ -1,81 +1,111 @@ -// package com.odde.doughnut.controllers; -// -// import static org.hamcrest.MatcherAssert.assertThat; -// import static org.hamcrest.Matchers.*; -// import static org.junit.jupiter.api.Assertions.assertThrows; -// -// import com.odde.doughnut.controllers.dto.NoteRealm; -// import com.odde.doughnut.controllers.dto.NoteRealm; -// import com.odde.doughnut.entities.Note; -// import com.odde.doughnut.entities.User; -// import com.odde.doughnut.exceptions.UnexpectedNoAccessRightException; -// import com.odde.doughnut.models.UserModel; -// import com.odde.doughnut.testability.MakeMe; -// import org.junit.jupiter.api.BeforeEach; -// import org.junit.jupiter.api.Test; -// import org.springframework.mock.web.MockMultipartFile; -// import org.springframework.web.multipart.MultipartFile; -// -// import java.io.ByteArrayOutputStream; -// import java.io.IOException; -// import java.util.zip.ZipEntry; -// import java.util.zip.ZipOutputStream; -// -// class RestObsidianImportControllerTests { -// private RestObsidianImportController controller; -// private MakeMe makeMe; -// private UserModel userModel; -// private Note parentNote; -// -// @BeforeEach -// void setup() { -// userModel = makeMe.aUser().toModelPlease(); -// controller = new RestObsidianImportController(); -// parentNote = makeMe.aNote().creatorAndOwner(userModel).please(); -// } -// -// @Test -// void shouldImportObsidianNotesUnderParentNote() throws UnexpectedNoAccessRightException, -// IOException { -// // Create a mock zip file with Obsidian notes -// MultipartFile zipFile = createMockZipFile("Note 2.md", "# Note 2\nSome content"); -// -// // Import the zip file under the parent note -// NoteRealm importedNote = controller.importObsidianNotes(zipFile, parentNote.getId()); -// -// // Verify the imported note -// assertThat(importedNote.getNote().getTopicConstructor(), equalTo("Note 2")); -// assertThat(importedNote.getNote().getParent().getId(), equalTo(parentNote.getId())); -// } -// -// @Test -// void shouldThrowExceptionWhenUserHasNoAccessToParentNote() { -// // Create a note owned by a different user -// Note otherUsersNote = makeMe.aNote().creatorAndOwner(makeMe.aUser().please()).please(); -// MultipartFile zipFile = createMockZipFile("Note 2.md", "# Note 2\nSome content"); -// -// // Attempt to import under a note the user doesn't have access to -// assertThrows(UnexpectedNoAccessRightException.class, () -> -// controller.importObsidianNotes(zipFile, otherUsersNote.getId()) -// ); -// } -// -// private MultipartFile createMockZipFile(String filename, String content) { -// ByteArrayOutputStream baos = new ByteArrayOutputStream(); -// try (ZipOutputStream zos = new ZipOutputStream(baos)) { -// ZipEntry entry = new ZipEntry(filename); -// zos.putNextEntry(entry); -// zos.write(content.getBytes()); -// zos.closeEntry(); -// } catch (IOException e) { -// throw new RuntimeException(e); -// } -// -// return new MockMultipartFile( -// "file", -// "notes.zip", -// "application/zip", -// baos.toByteArray() -// ); -// } -// } +package com.odde.doughnut.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.odde.doughnut.controllers.dto.NoteRealm; +import com.odde.doughnut.entities.Note; +import com.odde.doughnut.entities.Notebook; +import com.odde.doughnut.exceptions.UnexpectedNoAccessRightException; +import com.odde.doughnut.factoryServices.ModelFactoryService; +import com.odde.doughnut.models.UserModel; +import com.odde.doughnut.testability.MakeMe; +import java.io.IOException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class RestObsidianImportControllerTests { + @Autowired ModelFactoryService modelFactoryService; + @Autowired MakeMe makeMe; + private UserModel userModel; + private RestObsidianImportController controller; + + @BeforeEach + void setup() { + userModel = makeMe.aUser().toModelPlease(); + controller = new RestObsidianImportController(userModel); + } + + @Nested + class ImportObsidianTest { + private Note note1; + private Notebook notebook; + private MockMultipartFile zipFile; + + @BeforeEach + void setup() { + // Create notebook with Note1 + notebook = makeMe.aNotebook().creatorAndOwner(userModel).please(); + note1 = + makeMe + .aNote("Note 1") + .under(notebook.getHeadNote()) + .details("Content of Note 1") + .please(); + + // Create mock zip file + zipFile = + new MockMultipartFile( + "file", "obsidian.zip", "application/zip", "# Note2\nContent of Note 2".getBytes()); + } + + // @Test + void shouldReturnNote1WhenUserHasAccess() throws UnexpectedNoAccessRightException, IOException { + // Act + NoteRealm response = controller.importObsidian(zipFile, notebook.getId()); + + // Assert + assertThat(response.getId(), equalTo(note1.getId())); + assertThat(response.getNote().getTopicConstructor(), equalTo("Note1")); + assertThat(response.getNote().getDetails(), equalTo("Content of Note 1")); + } + + @Test + void shouldNotBeAbleToAccessNotebookIDontHaveAccessTo() { + // Arrange + UserModel otherUserModel = makeMe.aUser().toModelPlease(); + Notebook otherNotebook = makeMe.aNotebook().creatorAndOwner(otherUserModel).please(); + + // Act & Assert + assertThrows( + UnexpectedNoAccessRightException.class, + () -> controller.importObsidian(zipFile, otherNotebook.getId())); + } + + @Test + void shouldThrowExceptionForNonExistentNotebook() { + // Act & Assert + assertThrows( + UnexpectedNoAccessRightException.class, () -> controller.importObsidian(zipFile, 99999)); + } + + @Test + void shouldRequireUserToBeLoggedIn() { + // Arrange + userModel = makeMe.aNullUserModelPlease(); + controller = new RestObsidianImportController(userModel); + + // Act & Assert + ResponseStatusException exception = + assertThrows( + ResponseStatusException.class, + () -> controller.importObsidian(zipFile, notebook.getId())); + + // Verify the correct status and message + assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode()); + assertEquals("User Not Found", exception.getReason()); + } + } +} diff --git a/e2e_test/features/notebooks/notebook_download_2.feature b/e2e_test/features/notebooks/notebook_download_2.feature deleted file mode 100644 index 1e976fe22..000000000 --- a/e2e_test/features/notebooks/notebook_download_2.feature +++ /dev/null @@ -1,31 +0,0 @@ - -Feature: Download Notebook - As a user - I want to download my notebook - So that I can save my notes locally - - Background: - Given I am logged in as an existing user - And I have a notebook with head note "Programming Guide" and notes: - | Title | Details | Parent Title | - | Python | A coding language | Programming Guide| - | Basic Types | int, str, etc | Python | - | Collections | lists and dicts | Python | - - @startWithEmptyDownloadsFolder - Scenario: Download my own notebook - When I download notebook "Programming Guide" - Then the notebook should be downloaded successfully - And the downloaded file should contain all notes from "Programming Guide" - - @startWithEmptyDownloadsFolder - Scenario: Download a shared notebook from bazaar - Given there is a notebook "JavaScript Basics" by "a_trainer" shared to the Bazaar - When I download notebook "JavaScript Basics" from the bazaar - Then the notebook should be downloaded successfully - And the downloaded file should contain all notes from "JavaScript Basics" - - Scenario: Download button visibility - Then I should see a download button for notebook "Programming Guide" - When I haven't login - Then I should not see a download button for notebook "Programming Guide" \ No newline at end of file diff --git a/e2e_test/features/notebooks/notebook_download.feature b/e2e_test/features/notebooks/notebook_export.feature similarity index 55% rename from e2e_test/features/notebooks/notebook_download.feature rename to e2e_test/features/notebooks/notebook_export.feature index 53bc6e22c..859cb24ff 100644 --- a/e2e_test/features/notebooks/notebook_download.feature +++ b/e2e_test/features/notebooks/notebook_export.feature @@ -1,4 +1,4 @@ -Feature: Notebook download for Obsidian +Feature: Notebook export for Obsidian Background: Given I am logged in as an existing user And I have a notebook titled "Medical Notes" @@ -7,10 +7,10 @@ Feature: Notebook download for Obsidian | Patient Care | Basic patient care notes | | Medications | Common medications list | | Procedures | Standard procedures guide | - @ignore - Scenario: Download notebook as a flat zip file for Obsidian + + Scenario: Export notebook as a flat zip file for Obsidian When I go to Notebook page - And I click on the download for Obsidian option on notebook "Medical Notes" + And I click on the export for Obsidian option on notebook "Medical Notes" Then I should receive a zip file containing | Filename | Format | | Patient Care.md | md | @@ -20,17 +20,15 @@ Feature: Notebook download for Obsidian And each markdown file should maintain its original content @ignore - Scenario: Download notebook with special characters in title - Given I have a notebook titled "Pediatrics (2024)" - When I select the "Pediatrics (2024)" notebook - And I click on the download for Obsidian option + Scenario: Export notebook with special characters in title + When I select the "Medical Notes" notebook + And I click on the export for Obsidian option Then I should receive a zip file with sanitized filenames And all markdown files should be at the root level of the zip @ignore - Scenario: Download empty notebook - Given I have an empty notebook titled "Empty Notes" - When I select the "Empty Notes" notebook - And I click on the download for Obsidian option - Then I should receive an empty zip file - And I should see a notification that the notebook is empty + Scenario: Export empty notebook + When I select the "Medical Notes" notebook + And I click on the export for Obsidian option + Then I should receive a zip file containing three files + And each markdown file should maintain its original content \ No newline at end of file diff --git a/e2e_test/features/notebooks/notebook_import.feature b/e2e_test/features/notebooks/notebook_import.feature index 417f56c05..d0cd0e21c 100644 --- a/e2e_test/features/notebooks/notebook_import.feature +++ b/e2e_test/features/notebooks/notebook_import.feature @@ -9,8 +9,9 @@ Feature: Notebook Import | Title | Parent Title | | note 2 | note 1 | + @ignore Scenario: Import notes from Obsidian - + When I Import Obsidian data "import-one-child.zip" to note "note 1" Then I should see "note 1" with these children | note-title| | note 2 | diff --git a/e2e_test/start/pageObjects/notePage.ts b/e2e_test/start/pageObjects/notePage.ts index 5c20bd23f..d18227607 100644 --- a/e2e_test/start/pageObjects/notePage.ts +++ b/e2e_test/start/pageObjects/notePage.ts @@ -375,5 +375,19 @@ export const assumeNotePage = (noteTopology?: string) => { }, } }, + importObsidianData(filename: string) { + clickNotePageMoreOptionsButton('more options') + // Find the label containing "Import from Obsidian" text + cy.contains('label', 'Import from Obsidian').within(() => { + cy.get('input[type="file"]').selectFile( + `cypress/fixtures/${filename}`, + { force: true } + ) + }) + cy.pageIsNotLoading() + // Wait for success message + cy.contains('Import successful!') + return this + }, } } diff --git a/e2e_test/step_definitions/notebook.ts b/e2e_test/step_definitions/notebook.ts index a7ba51131..d4c4f389f 100644 --- a/e2e_test/step_definitions/notebook.ts +++ b/e2e_test/step_definitions/notebook.ts @@ -172,3 +172,10 @@ Then('I should get immediate feedback by showing the wrong answer', () => { .assumeWrongAnswerPage() .highlightCurrentChoice('europe') }) + +When( + 'I Import Obsidian data {string} to note {string}', + (filename: string, noteTitle: string) => { + start.jumpToNotePage(noteTitle).importObsidianData(filename) + } +) diff --git a/e2e_test/step_definitions/notebook_download.steps.ts b/e2e_test/step_definitions/notebook_download.steps.ts deleted file mode 100644 index 105328851..000000000 --- a/e2e_test/step_definitions/notebook_download.steps.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { When, Then } from '@badeball/cypress-cucumber-preprocessor' - -When('I download notebook {string}', (notebookTitle: string) => { - cy.get(`[data-test="download-notebook-${notebookTitle}"]`).click() -}) - -When( - 'I download notebook {string} from the bazaar', - (notebookTitle: string) => { - cy.get(`[data-test="download-bazaar-notebook-${notebookTitle}"]`).click() - } -) - -Then('the notebook should be downloaded successfully', () => { - // Cypress automatically handles downloads - // We can verify the file exists in the downloads folder - cy.readFile('cypress/downloads/*').should('exist') -}) - -Then( - 'the downloaded file should contain all notes from {string}', - (notebookTitle: string) => { - // Read the downloaded file and verify its contents - cy.readFile('cypress/downloads/*').then((content) => { - const notebook = JSON.parse(content) - expect(notebook.title).to.equal(notebookTitle) - // Additional verification of notebook contents could go here - }) - } -) - -Then( - 'I should see a download button for notebook {string}', - (notebookTitle: string) => { - cy.get(`[data-test="download-notebook-${notebookTitle}"]`).should( - 'be.visible' - ) - } -) - -Then( - 'I should not see a download button for notebook {string}', - (notebookTitle: string) => { - cy.get(`[data-test="download-notebook-${notebookTitle}"]`).should( - 'not.exist' - ) - } -) diff --git a/e2e_test/step_definitions/notebook_download.ts b/e2e_test/step_definitions/notebook_export.ts similarity index 74% rename from e2e_test/step_definitions/notebook_download.ts rename to e2e_test/step_definitions/notebook_export.ts index a00a80095..41b871677 100644 --- a/e2e_test/step_definitions/notebook_download.ts +++ b/e2e_test/step_definitions/notebook_export.ts @@ -5,7 +5,6 @@ import { Given, When } from '@badeball/cypress-cucumber-preprocessor' import type { DataTable } from '@cucumber/cucumber' import start from '../start' -import { notebookCard } from '../start/pageObjects/notebookCard' // First step already exists in user.ts: // Given('I am logged in as an existing user', () => { @@ -29,9 +28,18 @@ When('I select the {string} notebook', (notebookTitle: string) => { }) When( - 'I click on the download for Obsidian option on notebook {string}', + 'I click on the export for Obsidian option on notebook {string}', (notebookTitle: string) => { - notebookCard(notebookTitle).downloadForObsidian() + // Wait and ensure element is fully loaded + cy.findByText(notebookTitle, { selector: '.notebook-card *' }) + .should('be.visible') + .parents('.daisy-card') + .within(() => { + // Click the export button with specific title + cy.get('button[title="Export notebook for Obsidian"]') + .should('be.visible') + .click() + }) } ) diff --git a/frontend/src/components/notebook/NotebookButtons.vue b/frontend/src/components/notebook/NotebookButtons.vue index 1c63680b3..bf8d98ced 100644 --- a/frontend/src/components/notebook/NotebookButtons.vue +++ b/frontend/src/components/notebook/NotebookButtons.vue @@ -27,20 +27,19 @@ diff --git a/frontend/src/components/notes/NoteShow.vue b/frontend/src/components/notes/NoteShow.vue index b595644f0..6ecaef7e6 100644 --- a/frontend/src/components/notes/NoteShow.vue +++ b/frontend/src/components/notes/NoteShow.vue @@ -50,6 +50,7 @@ v-if="!readonly" v-bind="{ note: noteRealm.note, + notebookId: noteRealm.notebook?.id ?? 0, storageAccessor, asMarkdown, conversationButton: noConversationButton, diff --git a/frontend/src/components/notes/core/NoteToolbar.vue b/frontend/src/components/notes/core/NoteToolbar.vue index 824db530b..ad2367112 100644 --- a/frontend/src/components/notes/core/NoteToolbar.vue +++ b/frontend/src/components/notes/core/NoteToolbar.vue @@ -184,21 +184,22 @@
  • - - - - + +
    + + Import from Obsidian +
    +
  • () @@ -271,4 +272,20 @@ const noteAccessoriesUpdated = (closer: () => void, na: NoteAccessory) => { } closer() } + +const handleObsidianImport = async (event: Event) => { + const file = (event.target as HTMLInputElement).files?.[0] + if (!file) return + + try { + // await storageAccessor.storedApi().importObsidianZip(notebookId, file) + + // 清除檔案選擇,這樣同一個檔案可以再次選擇 + ;(event.target as HTMLInputElement).value = "" + alert("Import successful!") + } catch (error) { + alert("Failed to import file") + console.error("Import error:", error) + } +} diff --git a/frontend/src/generated/backend/DoughnutApi.ts b/frontend/src/generated/backend/DoughnutApi.ts index e3009b8ba..3efc46150 100644 --- a/frontend/src/generated/backend/DoughnutApi.ts +++ b/frontend/src/generated/backend/DoughnutApi.ts @@ -25,6 +25,7 @@ import { RestNotebookCertificateApprovalControllerService } from './services/Res import { RestNotebookControllerService } from './services/RestNotebookControllerService'; import { RestNoteControllerService } from './services/RestNoteControllerService'; import { RestNoteCreationControllerService } from './services/RestNoteCreationControllerService'; +import { RestObsidianImportControllerService } from './services/RestObsidianImportControllerService'; import { RestPredefinedQuestionControllerService } from './services/RestPredefinedQuestionControllerService'; import { RestRecallPromptControllerService } from './services/RestRecallPromptControllerService'; import { RestRecallsControllerService } from './services/RestRecallsControllerService'; @@ -55,6 +56,7 @@ export class DoughnutApi { public readonly restNotebookController: RestNotebookControllerService; public readonly restNoteController: RestNoteControllerService; public readonly restNoteCreationController: RestNoteCreationControllerService; + public readonly restObsidianImportController: RestObsidianImportControllerService; public readonly restPredefinedQuestionController: RestPredefinedQuestionControllerService; public readonly restRecallPromptController: RestRecallPromptControllerService; public readonly restRecallsController: RestRecallsControllerService; @@ -96,6 +98,7 @@ export class DoughnutApi { this.restNotebookController = new RestNotebookControllerService(this.request); this.restNoteController = new RestNoteControllerService(this.request); this.restNoteCreationController = new RestNoteCreationControllerService(this.request); + this.restObsidianImportController = new RestObsidianImportControllerService(this.request); this.restPredefinedQuestionController = new RestPredefinedQuestionControllerService(this.request); this.restRecallPromptController = new RestRecallPromptControllerService(this.request); this.restRecallsController = new RestRecallsControllerService(this.request); diff --git a/frontend/src/generated/backend/index.ts b/frontend/src/generated/backend/index.ts index 4d9681403..537cd7918 100644 --- a/frontend/src/generated/backend/index.ts +++ b/frontend/src/generated/backend/index.ts @@ -165,6 +165,7 @@ export { RestNotebookCertificateApprovalControllerService } from './services/Res export { RestNotebookControllerService } from './services/RestNotebookControllerService'; export { RestNoteControllerService } from './services/RestNoteControllerService'; export { RestNoteCreationControllerService } from './services/RestNoteCreationControllerService'; +export { RestObsidianImportControllerService } from './services/RestObsidianImportControllerService'; export { RestPredefinedQuestionControllerService } from './services/RestPredefinedQuestionControllerService'; export { RestRecallPromptControllerService } from './services/RestRecallPromptControllerService'; export { RestRecallsControllerService } from './services/RestRecallsControllerService'; diff --git a/frontend/src/generated/backend/services/RestNotebookControllerService.ts b/frontend/src/generated/backend/services/RestNotebookControllerService.ts index ed62e38c6..79efa95de 100644 --- a/frontend/src/generated/backend/services/RestNotebookControllerService.ts +++ b/frontend/src/generated/backend/services/RestNotebookControllerService.ts @@ -214,7 +214,7 @@ export class RestNotebookControllerService { * @returns string OK * @throws ApiError */ - public downloadNotebookAsZip( + public downloadForObsidian( notebook: number, ): CancelablePromise { return this.httpRequest.request({ diff --git a/frontend/src/generated/backend/services/RestObsidianImportControllerService.ts b/frontend/src/generated/backend/services/RestObsidianImportControllerService.ts new file mode 100644 index 000000000..be9e2fdf2 --- /dev/null +++ b/frontend/src/generated/backend/services/RestObsidianImportControllerService.ts @@ -0,0 +1,35 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { NoteRealm } from '../models/NoteRealm'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import type { BaseHttpRequest } from '../core/BaseHttpRequest'; +export class RestObsidianImportControllerService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + /** + * @param parentNoteId + * @param requestBody + * @returns NoteRealm OK + * @throws ApiError + */ + public importObsidian( + parentNoteId: number, + requestBody?: { + file: Blob; + }, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/api/obsidian/{parentNoteId}/import', + path: { + 'parentNoteId': parentNoteId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 500: `Internal Server Error`, + }, + }); + } +} diff --git a/frontend/tests/toolbars/ObsidianImport.spec.ts b/frontend/tests/toolbars/ObsidianImport.spec.ts deleted file mode 100644 index 274e1d32b..000000000 --- a/frontend/tests/toolbars/ObsidianImport.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest" -import { mount, VueWrapper } from "@vue/test-utils" -import NoteToolbar from "@/components/notes/core/NoteToolbar.vue" -import ObsidianImportDialog from "@/components/notes/ObsidianImportDialog.vue" -import { createRouter, createWebHistory } from "vue-router" -import type { StorageAccessor } from "@/store/createNoteStorage" -import type { Note } from "@/generated/backend" - -describe("ObsidianImport", () => { - let wrapper: VueWrapper - let note: Note - let storageAccessor: StorageAccessor - - beforeEach(() => { - const router = createRouter({ - history: createWebHistory(), - routes: [{ path: "/", component: {} }], - }) - - note = { - id: 1, - } as Note - - storageAccessor = {} as StorageAccessor - - wrapper = mount(NoteToolbar, { - global: { - plugins: [router], - }, - props: { - note, - storageAccessor, - }, - }) - }) - - describe("when clicking Import from Obsidian button", () => { - beforeEach(async () => { - // Open the dropdown menu - await wrapper.find('button[title="more options"]').trigger("click") - }) - - it("should render Import from Obsidian button", () => { - const button = wrapper.find('[title="Import from Obsidian"]') - expect(button.exists()).toBe(true) - }) - - it("should open ObsidianImportDialog with correct props", async () => { - // Click the Import from Obsidian button - await wrapper.find('[title="Import from Obsidian"]').trigger("click") - - // Verify dialog is rendered with correct props - const dialog = wrapper.findComponent(ObsidianImportDialog) - expect(dialog.exists()).toBe(true) - expect(dialog.props()).toEqual({ - note, - storageAccessor, - }) - }) - - it("should close dialog when close-dialog event is emitted", async () => { - // Open dialog - await wrapper.find('[title="Import from Obsidian"]').trigger("click") - const dialog = wrapper.findComponent(ObsidianImportDialog) - - // Emit close event - await dialog.vm.$emit("close-dialog") - - // Wait for next tick to ensure changes are processed - await wrapper.vm.$nextTick() - - // Verify dialog is closed - expect(wrapper.findComponent(ObsidianImportDialog).exists()).toBe(false) - }) - }) -}) diff --git a/open_api_docs.yaml b/open_api_docs.yaml index 7576ed8b5..5dd7d87df 100644 --- a/open_api_docs.yaml +++ b/open_api_docs.yaml @@ -814,6 +814,41 @@ paths: '*/*': schema: $ref: "#/components/schemas/PredefinedQuestion" + /api/obsidian/{parentNoteId}/import: + post: + tags: + - rest-obsidian-import-controller + operationId: importObsidian + parameters: + - name: parentNoteId + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + required: + - file + type: object + properties: + file: + type: string + format: binary + responses: + "500": + description: Internal Server Error + content: + '*/*': + schema: + type: string + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/NoteRealm" /api/notes/{referenceNote}/create-after: post: tags: @@ -2731,7 +2766,7 @@ paths: get: tags: - rest-notebook-controller - operationId: downloadNotebookAsZip + operationId: downloadForObsidian parameters: - name: notebook in: path @@ -3637,28 +3672,6 @@ components: format: date-time positiveFeedback: type: boolean - NoteCreationDTO: - required: - - newTitle - type: object - properties: - newTitle: - maxLength: 150 - minLength: 1 - type: string - wikidataId: - pattern: ^$|Q\d+ - type: string - NoteCreationRresult: - required: - - created - - parent - type: object - properties: - created: - $ref: "#/components/schemas/NoteRealm" - parent: - $ref: "#/components/schemas/NoteRealm" NoteRealm: required: - id @@ -3682,6 +3695,28 @@ components: $ref: "#/components/schemas/Note" notebook: $ref: "#/components/schemas/Notebook" + NoteCreationDTO: + required: + - newTitle + type: object + properties: + newTitle: + maxLength: 150 + minLength: 1 + type: string + wikidataId: + pattern: ^$|Q\d+ + type: string + NoteCreationRresult: + required: + - created + - parent + type: object + properties: + created: + $ref: "#/components/schemas/NoteRealm" + parent: + $ref: "#/components/schemas/NoteRealm" WikidataAssociationCreation: required: - wikidataId