diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index 44043f73e..e3f2393b9 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -48,7 +48,11 @@ jobs: node-version: 20 cache: "npm" - - run: npm ci + - name: Install npm dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install --with-deps - name: Run the TypeScript build run: npx tsc @@ -68,7 +72,7 @@ jobs: - name: Validate container run output run: | - CONTAINER_ID=$(docker run -d --log-driver=journald --rm --name trilium_local ${{ env.TEST_TAG }}) + CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host -e TRILIUM_PORT=8082 --volume ./integration-tests/db:/home/node/trilium-data --name trilium_local ${{ env.TEST_TAG }}) echo "Container ID: $CONTAINER_ID" - name: Wait for the healthchecks to pass @@ -78,6 +82,15 @@ jobs: wait-time: 50 require-status: running require-healthy: true + + - name: Run Playwright tests + run: TRILIUM_DOCKER=1 npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: Playwright report (${{ matrix.dockerfile }}) + path: playwright-report/ + retention-days: 30 # Print the entire log of the container thus far, regardless if the healthcheck failed or succeeded - name: Print entire log diff --git a/e2e/i18n.spec.ts b/e2e/i18n.spec.ts new file mode 100644 index 000000000..70713f097 --- /dev/null +++ b/e2e/i18n.spec.ts @@ -0,0 +1,57 @@ +import { test, expect, Page } from "@playwright/test"; +import App from "./support/app"; + +test.afterEach(async ({ page, context }) => { + const app = new App(page, context); + // Ensure English is set after each locale change to avoid any leaks to other tests. + await app.setOption("locale", "en"); +}); + +test("Displays translation on desktop", async ({ page, context }) => { + const app = new App(page, context); + await app.goto(); + + await expect(page.locator("#left-pane .quick-search input")) + .toHaveAttribute("placeholder", "Quick search"); +}); + +test("Displays translation on mobile", async ({ page, context }) => { + const app = new App(page, context); + await app.goto({ isMobile: true }); + + await expect(page.locator("#mobile-sidebar-wrapper .quick-search input")) + .toHaveAttribute("placeholder", "Quick search"); +}); + +test("Displays translations in Settings", async ({ page, context }) => { + const app = new App(page, context); + await app.goto(); + await app.closeAllTabs(); + await app.goToSettings(); + await app.noteTree.getByText("Appearance").click(); + + await expect(app.currentNoteSplit).toContainText("Localization"); + await expect(app.currentNoteSplit).toContainText("Language"); +}); + +test("User can change language from settings", async ({ page, context }) => { + const app = new App(page, context); + await app.goto(); + + await app.closeAllTabs(); + await app.goToSettings(); + await app.noteTree.getByText("Appearance").click(); + + // Check that the default value (English) is set. + await expect(app.currentNoteSplit).toContainText("Theme"); + const languageCombobox = await app.currentNoteSplit.getByRole("combobox").first(); + await expect(languageCombobox).toHaveValue("en"); + + // Select Chinese and ensure the translation is set. + await languageCombobox.selectOption("cn"); + await expect(app.currentNoteSplit).toContainText("主题", { timeout: 15000 }); + + // Select English again. + await languageCombobox.selectOption("en"); + await expect(app.currentNoteSplit).toContainText("Language", { timeout: 15000 }); +}); diff --git a/e2e/layout/tab_bar.spec.ts b/e2e/layout/tab_bar.spec.ts index d5c2b7f15..99391c115 100644 --- a/e2e/layout/tab_bar.spec.ts +++ b/e2e/layout/tab_bar.spec.ts @@ -3,8 +3,8 @@ import App from "../support/app"; const NOTE_TITLE = "Trilium Integration Test DB"; -test("Can drag tabs around", async ({ page }) => { - const app = new App(page); +test("Can drag tabs around", async ({ page, context }) => { + const app = new App(page, context); await app.goto(); // [1]: Trilium Integration Test DB note @@ -29,8 +29,8 @@ test("Can drag tabs around", async ({ page }) => { await expect(app.getTab(0)).toContainText(NOTE_TITLE); }); -test("Can drag tab to new window", async ({ page }) => { - const app = new App(page); +test("Can drag tab to new window", async ({ page, context }) => { + const app = new App(page, context); await app.goto(); await app.closeAllTabs(); @@ -49,11 +49,11 @@ test("Can drag tab to new window", async ({ page }) => { await page.mouse.move(x, y + tabPos.height + 100, { steps: 5 }); await page.mouse.up(); } else { - fail("Unable to determine tab position"); + test.fail(true, "Unable to determine tab position"); } // Wait for the popup to show const popup = await popupPromise; - const popupApp = new App(popup); + const popupApp = new App(popup, context); await expect(popupApp.getActiveTab()).toHaveText(NOTE_TITLE); }); diff --git a/e2e/note_types/code.spec.ts b/e2e/note_types/code.spec.ts index 4f63588ee..f0f204fcc 100644 --- a/e2e/note_types/code.spec.ts +++ b/e2e/note_types/code.spec.ts @@ -1,8 +1,8 @@ import { test, expect, Page } from "@playwright/test"; import App from "../support/app"; -test("Displays lint warnings for backend script", async ({ page }) => { - const app = new App(page); +test("Displays lint warnings for backend script", async ({ page, context }) => { + const app = new App(page, context); await app.goto(); await app.closeAllTabs(); await app.goToNoteInNewTab("Backend script with lint warnings"); @@ -10,7 +10,7 @@ test("Displays lint warnings for backend script", async ({ page }) => { const codeEditor = app.currentNoteSplit.locator(".CodeMirror"); // Expect two warning signs in the gutter. - expect(codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-warning")).toHaveCount(2); + await expect(codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-warning")).toHaveCount(2); // Hover over hello await codeEditor.getByText("hello").first().hover(); @@ -21,8 +21,8 @@ test("Displays lint warnings for backend script", async ({ page }) => { await expectTooltip(page, "'world' is defined but never used."); }); -test("Displays lint errors for backend script", async ({ page }) => { - const app = new App(page); +test("Displays lint errors for backend script", async ({ page, context }) => { + const app = new App(page, context); await app.goto(); await app.closeAllTabs(); await app.goToNoteInNewTab("Backend script with lint errors"); diff --git a/e2e/note_types/mermaid.spec.ts b/e2e/note_types/mermaid.spec.ts new file mode 100644 index 000000000..b06076cc3 --- /dev/null +++ b/e2e/note_types/mermaid.spec.ts @@ -0,0 +1,22 @@ +import { test, expect, Page } from "@playwright/test"; +import App from "../support/app"; + +test("displays simple map", async ({ page, context }) => { + const app = new App(page, context); + await app.goto(); + await app.goToNoteInNewTab("Sample mindmap"); + + await expect(app.currentNoteSplit).toContainText("Hello world"); + await expect(app.currentNoteSplit).toContainText("1"); + await expect(app.currentNoteSplit).toContainText("1a"); +}); + +test("displays note settings", async ({ page, context }) => { + const app = new App(page, context); + await app.goto(); + await app.goToNoteInNewTab("Sample mindmap"); + + await app.currentNoteSplit.getByText("Hello world").click({ force: true }); + const nodeMenu = app.currentNoteSplit.locator(".node-menu"); + await expect(nodeMenu).toBeVisible(); +}); diff --git a/e2e/note_types/text.spec.ts b/e2e/note_types/text.spec.ts index 2a8880f18..d7b878724 100644 --- a/e2e/note_types/text.spec.ts +++ b/e2e/note_types/text.spec.ts @@ -1,8 +1,8 @@ import { test, expect, Page } from "@playwright/test"; import App from "../support/app"; -test("Table of contents is displayed", async ({ page }) => { - const app = new App(page); +test("Table of contents is displayed", async ({ page, context }) => { + const app = new App(page, context); await app.goto(); await app.closeAllTabs(); await app.goToNoteInNewTab("Table of contents"); @@ -36,8 +36,8 @@ test("Table of contents is displayed", async ({ page }) => { await expect(rootList.locator("> ol").nth(1).locator("> ol > ol > ol")).toHaveCount(1); }); -test("Highlights list is displayed", async ({ page }) => { - const app = new App(page); +test("Highlights list is displayed", async ({ page, context }) => { + const app = new App(page, context); await app.goto(); await app.closeAllTabs(); await app.goToNoteInNewTab("Highlights list"); diff --git a/e2e/support/app.ts b/e2e/support/app.ts index 4f36dfb7b..290d9c420 100644 --- a/e2e/support/app.ts +++ b/e2e/support/app.ts @@ -1,27 +1,46 @@ -import { Locator, Page, expect } from "@playwright/test"; +import { expect, Locator, Page } from "@playwright/test"; +import type { BrowserContext } from "@playwright/test"; + +interface GotoOpts { + isMobile?: boolean; +} + +const BASE_URL = "http://127.0.0.1:8082"; export default class App { readonly page: Page; + readonly context: BrowserContext; readonly tabBar: Locator; readonly noteTree: Locator; readonly currentNoteSplit: Locator; readonly sidebar: Locator; - constructor(page: Page) { + constructor(page: Page, context: BrowserContext) { this.page = page; + this.context = context; + this.tabBar = page.locator(".tab-row-widget-container"); this.noteTree = page.locator(".tree-wrapper"); this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)") this.sidebar = page.locator("#right-pane"); } - async goto() { + async goto(opts: GotoOpts = {}) { + await this.context.addCookies([ + { + url: BASE_URL, + name: "trilium-device", + value: opts.isMobile ? "mobile" : "desktop" + } + ]); + await this.page.goto("/", { waitUntil: "networkidle" }); // Wait for the page to load. await expect(this.page.locator(".tree")) .toContainText("Trilium Integration Test"); + await this.closeAllTabs(); } async goToNoteInNewTab(noteTitle: string) { @@ -31,6 +50,10 @@ export default class App { await autocomplete.press("Enter"); } + async goToSettings() { + await this.page.locator(".launcher-button.bx-cog").click(); + } + getTab(tabIndex: number) { return this.tabBar.locator(".note-tab-wrapper").nth(tabIndex); } @@ -39,18 +62,49 @@ export default class App { return this.tabBar.locator(".note-tab[active]"); } + /** + * Closes all the tabs in the client by issuing a command. + */ async closeAllTabs() { - await this.getTab(0).click({ button: "right" }); - await this.page.waitForTimeout(500); // TODO: context menu won't dismiss otherwise - await this.page.getByText("Close all tabs").click({ force: true }); + await this.triggerCommand("closeAllTabs"); } + /** + * Adds a new tab by cliking on the + button near the tab bar. + */ async addNewTab() { await this.page.locator('[data-trigger-command="openNewTab"]').click(); } + /** + * Looks for a given title in the note tree and clicks on it. Useful for selecting option pages in settings in a similar fashion as the user. + * @param title the title of the note to click, as displayed in the note tree. + */ async clickNoteOnNoteTreeByTitle(title: string) { - this.noteTree.getByText(title).click(); + await this.noteTree.getByText(title).click(); + } + + /** + * Executes any Trilium command on the client. + * @param command the command to send. + */ + async triggerCommand(command: string) { + await this.page.evaluate(async (command: string) => { + await (window as any).glob.appContext.triggerCommand(command); + }, command); + } + + async setOption(key: string, value: string) { + const csrfToken = await this.page.evaluate(() => { + return (window as any).glob.csrfToken; + }); + + expect(csrfToken).toBeTruthy(); + await expect(await this.page.request.put(`${BASE_URL}/api/options/${key}/${value}`, { + headers: { + "x-csrf-token": csrfToken + } + })).toBeOK(); } } diff --git a/integration-tests/db/document.db b/integration-tests/db/document.db index a4a44162f..578b95cd7 100644 Binary files a/integration-tests/db/document.db and b/integration-tests/db/document.db differ diff --git a/integration-tests/i18n.spec.ts b/integration-tests/i18n.spec.ts deleted file mode 100644 index 1c3fc1d36..000000000 --- a/integration-tests/i18n.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import test, { expect } from "@playwright/test"; - -test("User can change language from settings", async ({ page }) => { - await page.goto("http://localhost:8082"); - - // Clear all tabs - await page.locator(".note-tab:first-of-type").locator("div").nth(1).click({ button: "right" }); - await page.getByText("Close all tabs").click(); - - // Go to options -> Appearance - await page.locator("#launcher-pane div").filter({ hasText: "Options Open New Window" }).getByRole("button").click(); - await page.locator("#launcher-pane").getByText("Options").click(); - await page.locator("#center-pane").getByText("Appearance").click(); - - // Check that the default value (English) is set. - await expect(page.locator("#center-pane")).toContainText("Theme"); - const languageCombobox = await page.getByRole("combobox").first(); - await expect(languageCombobox).toHaveValue("en"); - - // Select Chinese and ensure the translation is set. - languageCombobox.selectOption("cn"); - await expect(page.locator("#center-pane")).toContainText("主题"); - - // Select English again. - languageCombobox.selectOption("en"); -}); - -test("Restores language on start-up on desktop", async ({ page, context }) => { - await page.goto("http://localhost:8082"); - await expect(page.locator("#launcher-pane").first()).toContainText("Open New Window"); -}); - -test("Restores language on start-up on mobile", async ({ page, context }) => { - await context.addCookies([ - { - url: "http://localhost:8082", - name: "trilium-device", - value: "mobile" - } - ]); - await page.goto("http://localhost:8082"); - await expect(page.locator("#launcher-pane div").first()).toContainText("Open New Window"); -}); diff --git a/package.json b/package.json index d078620b9..50955527b 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "build-frontend-docs": "rimraf ./docs/frontend_api && jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/basic_widget.js src/public/app/widgets/note_context_aware_widget.js src/public/app/widgets/right_panel_widget.js", "build-docs": "npm run build-backend-docs && npm run build-frontend-docs", "webpack": "cross-env node --import ./loader-register.js node_modules/webpack/bin/webpack.js -c webpack.config.ts", + "test-playwright": "playwright test", "test-jasmine": "cross-env TRILIUM_DATA_DIR=./data-test tsx ./node_modules/jasmine/bin/jasmine.js", "test-es6": "tsx -r esm spec-es6/attribute_parser.spec.ts", "test": "npm run test-jasmine && npm run test-es6", diff --git a/playwright.config.ts b/playwright.config.ts index 86de95af0..86bf074ce 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -73,9 +73,9 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ - webServer: { + webServer: !process.env.TRILIUM_DOCKER ? { command: 'npm run integration-mem-db-dev', url: SERVER_URL, - // reuseExistingServer: !process.env.CI, - }, + reuseExistingServer: !process.env.CI, + } : undefined, }); diff --git a/spec/search/becca_mocking.ts b/spec/search/becca_mocking.ts index 76ee955f6..0c65f16b6 100644 --- a/spec/search/becca_mocking.ts +++ b/spec/search/becca_mocking.ts @@ -3,7 +3,7 @@ import BBranch from "../../src/becca/entities/bbranch.js"; import BAttribute from "../../src/becca/entities/battribute.js"; import becca from "../../src/becca/becca.js"; import randtoken from "rand-token"; -import SearchResult from "../../src/services/search/search_result.js"; +import type SearchResult from "../../src/services/search/search_result.js"; import type { NoteType } from "../../src/becca/entities/rows.js"; randtoken.generator({ source: "crypto" }); diff --git a/spec/search/parser.spec.ts b/spec/search/parser.spec.ts index 09c7ce06b..76e557067 100644 --- a/spec/search/parser.spec.ts +++ b/spec/search/parser.spec.ts @@ -1,6 +1,6 @@ import AndExp from "../../src/services/search/expressions/and.js"; import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js"; -import Expression from "../../src/services/search/expressions/expression.js"; +import type Expression from "../../src/services/search/expressions/expression.js"; import LabelComparisonExp from "../../src/services/search/expressions/label_comparison.js"; import NotExp from "../../src/services/search/expressions/not.js"; import NoteContentFulltextExp from "../../src/services/search/expressions/note_content_fulltext.js"; diff --git a/spec/support/etapi.ts b/spec/support/etapi.ts index 3e6e1ac98..d653707d5 100644 --- a/spec/support/etapi.ts +++ b/spec/support/etapi.ts @@ -1,4 +1,4 @@ -import child_process from "child_process"; +import type child_process from "child_process"; let etapiAuthToken: string | undefined; diff --git a/src/becca/becca-interface.ts b/src/becca/becca-interface.ts index 6497dad7d..cdee5d1cd 100644 --- a/src/becca/becca-interface.ts +++ b/src/becca/becca-interface.ts @@ -1,17 +1,17 @@ import sql from "../services/sql.js"; import NoteSet from "../services/search/note_set.js"; import NotFoundError from "../errors/not_found_error.js"; -import BOption from "./entities/boption.js"; -import BNote from "./entities/bnote.js"; -import BEtapiToken from "./entities/betapi_token.js"; -import BAttribute from "./entities/battribute.js"; -import BBranch from "./entities/bbranch.js"; +import type BOption from "./entities/boption.js"; +import type BNote from "./entities/bnote.js"; +import type BEtapiToken from "./entities/betapi_token.js"; +import type BAttribute from "./entities/battribute.js"; +import type BBranch from "./entities/bbranch.js"; import BRevision from "./entities/brevision.js"; import BAttachment from "./entities/battachment.js"; import type { AttachmentRow, BlobRow, RevisionRow } from "./entities/rows.js"; import BBlob from "./entities/bblob.js"; import BRecentNote from "./entities/brecent_note.js"; -import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; +import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; interface AttachmentOpts { includeContentLength?: boolean; diff --git a/src/becca/becca_loader.ts b/src/becca/becca_loader.ts index a461828ac..8397b2dec 100644 --- a/src/becca/becca_loader.ts +++ b/src/becca/becca_loader.ts @@ -12,7 +12,7 @@ import BEtapiToken from "./entities/betapi_token.js"; import cls from "../services/cls.js"; import entityConstructor from "../becca/entity_constructor.js"; import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "./entities/rows.js"; -import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; +import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import ws from "../services/ws.js"; const beccaLoaded = new Promise(async (res, rej) => { diff --git a/src/becca/entities/abstract_becca_entity.ts b/src/becca/entities/abstract_becca_entity.ts index b964a2d43..a36132f3a 100644 --- a/src/becca/entities/abstract_becca_entity.ts +++ b/src/becca/entities/abstract_becca_entity.ts @@ -9,7 +9,7 @@ import cls from "../../services/cls.js"; import log from "../../services/log.js"; import protectedSessionService from "../../services/protected_session.js"; import blobService from "../../services/blob.js"; -import Becca, { type ConstructorData } from "../becca-interface.js"; +import type { default as Becca, ConstructorData } from "../becca-interface.js"; import becca from "../becca.js"; interface ContentOpts { diff --git a/src/becca/entities/battachment.ts b/src/becca/entities/battachment.ts index 1e47fc9bf..9461647af 100644 --- a/src/becca/entities/battachment.ts +++ b/src/becca/entities/battachment.ts @@ -7,8 +7,8 @@ import sql from "../../services/sql.js"; import protectedSessionService from "../../services/protected_session.js"; import log from "../../services/log.js"; import type { AttachmentRow } from "./rows.js"; -import BNote from "./bnote.js"; -import BBranch from "./bbranch.js"; +import type BNote from "./bnote.js"; +import type BBranch from "./bbranch.js"; import noteService from "../../services/notes.js"; const attachmentRoleToNoteTypeMapping = { diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index d83a263ff..acc432127 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -15,7 +15,7 @@ import dayjs from "dayjs"; import utc from "dayjs/plugin/utc.js"; import eventService from "../../services/events.js"; import type { AttachmentRow, AttributeType, NoteRow, NoteType, RevisionRow } from "./rows.js"; -import BBranch from "./bbranch.js"; +import type BBranch from "./bbranch.js"; import BAttribute from "./battribute.js"; import type { NotePojo } from "../becca-interface.js"; import searchService from "../../services/search/services/search.js"; diff --git a/src/becca/entity_constructor.ts b/src/becca/entity_constructor.ts index 83c5bd62c..18f7a14c7 100644 --- a/src/becca/entity_constructor.ts +++ b/src/becca/entity_constructor.ts @@ -1,5 +1,5 @@ import type { ConstructorData } from "./becca-interface.js"; -import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; +import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import BAttachment from "./entities/battachment.js"; import BAttribute from "./entities/battribute.js"; import BBlob from "./entities/bblob.js"; diff --git a/src/becca/similarity.ts b/src/becca/similarity.ts index 5313419d6..66af0ac61 100644 --- a/src/becca/similarity.ts +++ b/src/becca/similarity.ts @@ -3,7 +3,7 @@ import log from "../services/log.js"; import beccaService from "./becca_service.js"; import dateUtils from "../services/date_utils.js"; import { JSDOM } from "jsdom"; -import BNote from "./entities/bnote.js"; +import type BNote from "./entities/bnote.js"; const DEBUG = false; diff --git a/src/etapi/app_info.ts b/src/etapi/app_info.ts index ec5b781c2..ab6deb9dd 100644 --- a/src/etapi/app_info.ts +++ b/src/etapi/app_info.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import type { Router } from "express"; import appInfo from "../services/app_info.js"; import eu from "./etapi_utils.js"; diff --git a/src/etapi/attachments.ts b/src/etapi/attachments.ts index 5d3ce57e4..0404e11a1 100644 --- a/src/etapi/attachments.ts +++ b/src/etapi/attachments.ts @@ -3,7 +3,7 @@ import eu from "./etapi_utils.js"; import mappers from "./mappers.js"; import v from "./validators.js"; import utils from "../services/utils.js"; -import { Router } from "express"; +import type { Router } from "express"; import type { AttachmentRow } from "../becca/entities/rows.js"; import type { ValidatorMap } from "./etapi-interface.js"; diff --git a/src/etapi/attributes.ts b/src/etapi/attributes.ts index 3b9cf3451..91a441117 100644 --- a/src/etapi/attributes.ts +++ b/src/etapi/attributes.ts @@ -3,7 +3,7 @@ import eu from "./etapi_utils.js"; import mappers from "./mappers.js"; import attributeService from "../services/attributes.js"; import v from "./validators.js"; -import { Router } from "express"; +import type { Router } from "express"; import type { AttributeRow } from "../becca/entities/rows.js"; import type { ValidatorMap } from "./etapi-interface.js"; diff --git a/src/etapi/backup.ts b/src/etapi/backup.ts index 3b9f3f874..d73bf1a33 100644 --- a/src/etapi/backup.ts +++ b/src/etapi/backup.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import type { Router } from "express"; import eu from "./etapi_utils.js"; import backupService from "../services/backup.js"; diff --git a/src/etapi/branches.ts b/src/etapi/branches.ts index 59f87c3c8..7263b3161 100644 --- a/src/etapi/branches.ts +++ b/src/etapi/branches.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import type { Router } from "express"; import becca from "../becca/becca.js"; import eu from "./etapi_utils.js"; diff --git a/src/etapi/mappers.ts b/src/etapi/mappers.ts index 33cd2c29f..735e767c2 100644 --- a/src/etapi/mappers.ts +++ b/src/etapi/mappers.ts @@ -1,7 +1,7 @@ -import BAttachment from "../becca/entities/battachment.js"; -import BAttribute from "../becca/entities/battribute.js"; -import BBranch from "../becca/entities/bbranch.js"; -import BNote from "../becca/entities/bnote.js"; +import type BAttachment from "../becca/entities/battachment.js"; +import type BAttribute from "../becca/entities/battribute.js"; +import type BBranch from "../becca/entities/bbranch.js"; +import type BNote from "../becca/entities/bnote.js"; function mapNoteToPojo(note: BNote) { return { diff --git a/src/etapi/notes.ts b/src/etapi/notes.ts index 05b120195..5ab1727b9 100644 --- a/src/etapi/notes.ts +++ b/src/etapi/notes.ts @@ -9,7 +9,7 @@ import searchService from "../services/search/services/search.js"; import SearchContext from "../services/search/search_context.js"; import zipExportService from "../services/export/zip.js"; import zipImportService from "../services/import/zip.js"; -import { type Request, Router } from "express"; +import type { Request, Router } from "express"; import type { ParsedQs } from "qs"; import type { NoteParams } from "../services/note-interface.js"; import type { SearchParams } from "../services/search/services/types.js"; diff --git a/src/etapi/spec.ts b/src/etapi/spec.ts index 5e27be949..925ff51a6 100644 --- a/src/etapi/spec.ts +++ b/src/etapi/spec.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import type { Router } from "express"; import fs from "fs"; import path from "path"; diff --git a/src/etapi/special_notes.ts b/src/etapi/special_notes.ts index 14626710d..23411c987 100644 --- a/src/etapi/special_notes.ts +++ b/src/etapi/special_notes.ts @@ -2,7 +2,7 @@ import specialNotesService from "../services/special_notes.js"; import dateNotesService from "../services/date_notes.js"; import eu from "./etapi_utils.js"; import mappers from "./mappers.js"; -import { Router } from "express"; +import type { Router } from "express"; const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`); const getMonthInvalidError = (month: string) => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`); diff --git a/src/public/app/components/app_context.ts b/src/public/app/components/app_context.ts index ea11d2ea4..15e9b8910 100644 --- a/src/public/app/components/app_context.ts +++ b/src/public/app/components/app_context.ts @@ -14,15 +14,15 @@ import MainTreeExecutors from "./main_tree_executors.js"; import toast from "../services/toast.js"; import ShortcutComponent from "./shortcut_component.js"; import { t, initLocale } from "../services/i18n.js"; -import NoteDetailWidget from "../widgets/note_detail.js"; +import type NoteDetailWidget from "../widgets/note_detail.js"; import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js"; import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js"; import type { Node } from "../services/tree.js"; -import LoadResults from "../services/load_results.js"; +import type LoadResults from "../services/load_results.js"; import type { Attribute } from "../services/attribute_parser.js"; -import NoteTreeWidget from "../widgets/note_tree.js"; -import NoteContext, { type GetTextEditorCallback } from "./note_context.js"; +import type NoteTreeWidget from "../widgets/note_tree.js"; +import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; interface Layout { getRootWidget: (appContext: AppContext) => RootWidget; diff --git a/src/public/app/components/entrypoints.ts b/src/public/app/components/entrypoints.ts index b62cfdeb3..5ba9e3bd8 100644 --- a/src/public/app/components/entrypoints.ts +++ b/src/public/app/components/entrypoints.ts @@ -10,7 +10,7 @@ import bundleService from "../services/bundle.js"; import froca from "../services/froca.js"; import linkService from "../services/link.js"; import { t } from "../services/i18n.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; // TODO: Move somewhere else nicer. export type SqlExecuteResults = unknown[]; diff --git a/src/public/app/components/note_context.ts b/src/public/app/components/note_context.ts index 2bd1981cc..e5b649e56 100644 --- a/src/public/app/components/note_context.ts +++ b/src/public/app/components/note_context.ts @@ -8,7 +8,7 @@ import froca from "../services/froca.js"; import hoistedNoteService from "../services/hoisted_note.js"; import options from "../services/options.js"; import type { ViewScope } from "../services/link.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; interface SetNoteOpts { triggerSwitchEvent?: unknown; diff --git a/src/public/app/entities/fnote.ts b/src/public/app/entities/fnote.ts index ed42257e1..9759f8b07 100644 --- a/src/public/app/entities/fnote.ts +++ b/src/public/app/entities/fnote.ts @@ -5,8 +5,8 @@ import froca from "../services/froca.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import cssClassManager from "../services/css_class_manager.js"; import type { Froca } from "../services/froca-interface.js"; -import FAttachment from "./fattachment.js"; -import FAttribute, { type AttributeType } from "./fattribute.js"; +import type FAttachment from "./fattachment.js"; +import type { default as FAttribute, AttributeType } from "./fattribute.js"; import utils from "../services/utils.js"; const LABEL = "label"; diff --git a/src/public/app/layouts/mobile_layout.ts b/src/public/app/layouts/mobile_layout.ts index 0f31199fd..64e698998 100644 --- a/src/public/app/layouts/mobile_layout.ts +++ b/src/public/app/layouts/mobile_layout.ts @@ -27,7 +27,7 @@ import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolb import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js"; import AboutDialog from "../widgets/dialogs/about.js"; import HelpDialog from "../widgets/dialogs/help.js"; -import AppContext from "../components/app_context.js"; +import type AppContext from "../components/app_context.js"; import TabRowWidget from "../widgets/tab_row.js"; import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js"; diff --git a/src/public/app/menus/launcher_context_menu.ts b/src/public/app/menus/launcher_context_menu.ts index d61e0e372..8c2b157cd 100644 --- a/src/public/app/menus/launcher_context_menu.ts +++ b/src/public/app/menus/launcher_context_menu.ts @@ -5,7 +5,7 @@ import dialogService from "../services/dialog.js"; import server from "../services/server.js"; import { t } from "../services/i18n.js"; import type { SelectMenuItemEventListener } from "../components/events.js"; -import NoteTreeWidget from "../widgets/note_tree.js"; +import type NoteTreeWidget from "../widgets/note_tree.js"; import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js"; type LauncherCommandNames = FilteredCommandNames; @@ -58,7 +58,7 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener< { title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset } ]; - return items.filter((row) => row !== null); + return items.filter((row) => row !== null) as MenuItem[]; } async selectMenuItemHandler({ command }: MenuCommandItem) { diff --git a/src/public/app/menus/tree_context_menu.ts b/src/public/app/menus/tree_context_menu.ts index be7983faa..650432828 100644 --- a/src/public/app/menus/tree_context_menu.ts +++ b/src/public/app/menus/tree_context_menu.ts @@ -9,8 +9,8 @@ import server from "../services/server.js"; import toastService from "../services/toast.js"; import dialogService from "../services/dialog.js"; import { t } from "../services/i18n.js"; -import NoteTreeWidget from "../widgets/note_tree.js"; -import FAttachment from "../entities/fattachment.js"; +import type NoteTreeWidget from "../widgets/note_tree.js"; +import type FAttachment from "../entities/fattachment.js"; import type { SelectMenuItemEventListener } from "../components/events.js"; // TODO: Deduplicate once client/server is well split. @@ -196,7 +196,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener row !== null); + return items.filter((row) => row !== null) as MenuItem[]; } async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem) { diff --git a/src/public/app/services/attribute_renderer.ts b/src/public/app/services/attribute_renderer.ts index a686b7098..6c7b9412d 100644 --- a/src/public/app/services/attribute_renderer.ts +++ b/src/public/app/services/attribute_renderer.ts @@ -1,7 +1,7 @@ import ws from "./ws.js"; import froca from "./froca.js"; -import FAttribute from "../entities/fattribute.js"; -import FNote from "../entities/fnote.js"; +import type FAttribute from "../entities/fattribute.js"; +import type FNote from "../entities/fnote.js"; async function renderAttribute(attribute: FAttribute, renderIsInheritable: boolean) { const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : ""; diff --git a/src/public/app/services/attributes.ts b/src/public/app/services/attributes.ts index b594630c5..767d8c3af 100644 --- a/src/public/app/services/attributes.ts +++ b/src/public/app/services/attributes.ts @@ -1,6 +1,6 @@ import server from "./server.js"; import froca from "./froca.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; import type { AttributeRow } from "./load_results.js"; async function addLabel(noteId: string, name: string, value: string = "") { diff --git a/src/public/app/services/bulk_action.ts b/src/public/app/services/bulk_action.ts index 615e1c6eb..66922ef62 100644 --- a/src/public/app/services/bulk_action.ts +++ b/src/public/app/services/bulk_action.ts @@ -14,7 +14,7 @@ import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js"; import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js"; import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; import { t } from "./i18n.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; const ACTION_GROUPS = [ { diff --git a/src/public/app/services/froca-interface.ts b/src/public/app/services/froca-interface.ts index c678e220b..8d0077989 100644 --- a/src/public/app/services/froca-interface.ts +++ b/src/public/app/services/froca-interface.ts @@ -1,8 +1,8 @@ -import FAttachment from "../entities/fattachment.js"; -import FAttribute from "../entities/fattribute.js"; -import FBlob from "../entities/fblob.js"; -import FBranch from "../entities/fbranch.js"; -import FNote from "../entities/fnote.js"; +import type FAttachment from "../entities/fattachment.js"; +import type FAttribute from "../entities/fattribute.js"; +import type FBlob from "../entities/fblob.js"; +import type FBranch from "../entities/fbranch.js"; +import type FNote from "../entities/fnote.js"; export interface Froca { notes: Record; diff --git a/src/public/app/services/froca_updater.ts b/src/public/app/services/froca_updater.ts index 89de8c5b7..f10720de4 100644 --- a/src/public/app/services/froca_updater.ts +++ b/src/public/app/services/froca_updater.ts @@ -6,7 +6,7 @@ import noteAttributeCache from "./note_attribute_cache.js"; import FBranch, { type FBranchRow } from "../entities/fbranch.js"; import FAttribute, { type FAttributeRow } from "../entities/fattribute.js"; import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; -import FNote, { type FNoteRow } from "../entities/fnote.js"; +import type { default as FNote, FNoteRow } from "../entities/fnote.js"; import type { EntityChange } from "../server_types.js"; async function processEntityChanges(entityChanges: EntityChange[]) { diff --git a/src/public/app/services/frontend_script_api.ts b/src/public/app/services/frontend_script_api.ts index acdff7444..a8160bbff 100644 --- a/src/public/app/services/frontend_script_api.ts +++ b/src/public/app/services/frontend_script_api.ts @@ -15,11 +15,11 @@ import BasicWidget from "../widgets/basic_widget.js"; import SpacedUpdate from "./spaced_update.js"; import shortcutService from "./shortcuts.js"; import dialogService from "./dialog.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; import { t } from "./i18n.js"; -import NoteContext from "../components/note_context.js"; -import NoteDetailWidget from "../widgets/note_detail.js"; -import Component from "../components/component.js"; +import type NoteContext from "../components/note_context.js"; +import type NoteDetailWidget from "../widgets/note_detail.js"; +import type Component from "../components/component.js"; /** * A whole number diff --git a/src/public/app/services/hoisted_note.ts b/src/public/app/services/hoisted_note.ts index 53d62c081..597de9467 100644 --- a/src/public/app/services/hoisted_note.ts +++ b/src/public/app/services/hoisted_note.ts @@ -2,7 +2,7 @@ import appContext from "../components/app_context.js"; import treeService, { type Node } from "./tree.js"; import dialogService from "./dialog.js"; import froca from "./froca.js"; -import NoteContext from "../components/note_context.js"; +import type NoteContext from "../components/note_context.js"; import { t } from "./i18n.js"; function getHoistedNoteId() { diff --git a/src/public/app/services/keyboard_actions.ts b/src/public/app/services/keyboard_actions.ts index bdd13e28b..dfa888620 100644 --- a/src/public/app/services/keyboard_actions.ts +++ b/src/public/app/services/keyboard_actions.ts @@ -1,7 +1,7 @@ import server from "./server.js"; import appContext, { type CommandNames } from "../components/app_context.js"; import shortcutService from "./shortcuts.js"; -import Component from "../components/component.js"; +import type Component from "../components/component.js"; const keyboardActionRepo: Record = {}; diff --git a/src/public/app/services/note_attribute_cache.ts b/src/public/app/services/note_attribute_cache.ts index ae3993cfb..18980036f 100644 --- a/src/public/app/services/note_attribute_cache.ts +++ b/src/public/app/services/note_attribute_cache.ts @@ -1,4 +1,4 @@ -import FAttribute from "../entities/fattribute.js"; +import type FAttribute from "../entities/fattribute.js"; /** * The purpose of this class is to cache the list of attributes for notes. diff --git a/src/public/app/services/note_create.ts b/src/public/app/services/note_create.ts index a7fd5ec76..b85c8277b 100644 --- a/src/public/app/services/note_create.ts +++ b/src/public/app/services/note_create.ts @@ -6,8 +6,8 @@ import froca from "./froca.js"; import treeService from "./tree.js"; import toastService from "./toast.js"; import { t } from "./i18n.js"; -import FNote from "../entities/fnote.js"; -import FBranch from "../entities/fbranch.js"; +import type FNote from "../entities/fnote.js"; +import type FBranch from "../entities/fbranch.js"; import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; interface CreateNoteOpts { diff --git a/src/public/app/services/note_list_renderer.ts b/src/public/app/services/note_list_renderer.ts index e6e305689..2e6652b0f 100644 --- a/src/public/app/services/note_list_renderer.ts +++ b/src/public/app/services/note_list_renderer.ts @@ -5,7 +5,7 @@ import attributeRenderer from "./attribute_renderer.js"; import libraryLoader from "./library_loader.js"; import treeService from "./tree.js"; import utils from "./utils.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; const TPL = `
diff --git a/src/public/app/services/note_tooltip.ts b/src/public/app/services/note_tooltip.ts index f456d7a63..32f79c73d 100644 --- a/src/public/app/services/note_tooltip.ts +++ b/src/public/app/services/note_tooltip.ts @@ -5,7 +5,7 @@ import utils from "./utils.js"; import attributeRenderer from "./attribute_renderer.js"; import contentRenderer from "./content_renderer.js"; import appContext from "../components/app_context.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; import { t } from "./i18n.js"; function setupGlobalTooltip() { diff --git a/src/public/app/services/protected_session_holder.ts b/src/public/app/services/protected_session_holder.ts index e4171f389..647c2f51d 100644 --- a/src/public/app/services/protected_session_holder.ts +++ b/src/public/app/services/protected_session_holder.ts @@ -1,4 +1,4 @@ -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; import server from "./server.js"; function enableProtectedSession() { diff --git a/src/public/app/services/render.ts b/src/public/app/services/render.ts index 0eb02c7a7..bec5cb514 100644 --- a/src/public/app/services/render.ts +++ b/src/public/app/services/render.ts @@ -1,6 +1,6 @@ import server from "./server.js"; import bundleService, { type Bundle } from "./bundle.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; async function render(note: FNote, $el: JQuery) { const relations = note.getRelations("renderNote"); diff --git a/src/public/app/widgets/attribute_widgets/attribute_editor.ts b/src/public/app/widgets/attribute_widgets/attribute_editor.ts index 0119bcd3b..622da6ca9 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_editor.ts +++ b/src/public/app/widgets/attribute_widgets/attribute_editor.ts @@ -3,17 +3,17 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js"; import noteAutocompleteService from "../../services/note_autocomplete.js"; import server from "../../services/server.js"; import contextMenuService from "../../menus/context_menu.js"; -import attributeParser from "../../services/attribute_parser.js"; +import attributeParser, { type Attribute } from "../../services/attribute_parser.js"; import libraryLoader from "../../services/library_loader.js"; import froca from "../../services/froca.js"; import attributeRenderer from "../../services/attribute_renderer.js"; import noteCreateService from "../../services/note_create.js"; import attributeService from "../../services/attributes.js"; import linkService from "../../services/link.js"; -import AttributeDetailWidget from "./attribute_detail.js"; +import type AttributeDetailWidget from "./attribute_detail.js"; import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js"; -import FAttribute, { type AttributeType } from "../../entities/fattribute.js"; -import FNote from "../../entities/fnote.js"; +import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js"; +import type FNote from "../../entities/fnote.js"; const HELP_TEXT = `

${t("attribute_editor.help_text_body1")}

@@ -417,7 +417,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem return null; } - let matchedAttr = null; + let matchedAttr: Attribute | null = null; for (const attr of parsedAttrs) { if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) { diff --git a/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts b/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts index 436f8fbd8..eb24bbd4d 100644 --- a/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts +++ b/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts @@ -2,7 +2,7 @@ import { t } from "../../services/i18n.js"; import server from "../../services/server.js"; import ws from "../../services/ws.js"; import utils from "../../services/utils.js"; -import FAttribute from "../../entities/fattribute.js"; +import type FAttribute from "../../entities/fattribute.js"; interface ActionDefinition { script: string; diff --git a/src/public/app/widgets/containers/container.ts b/src/public/app/widgets/containers/container.ts index 56fe050a0..679fa11a8 100644 --- a/src/public/app/widgets/containers/container.ts +++ b/src/public/app/widgets/containers/container.ts @@ -1,4 +1,4 @@ -import Component, { TypedComponent } from "../../components/component.js"; +import type { default as Component, TypedComponent } from "../../components/component.js"; import BasicWidget, { TypedBasicWidget } from "../basic_widget.js"; export default class Container> extends TypedBasicWidget { diff --git a/src/public/app/widgets/containers/flex_container.ts b/src/public/app/widgets/containers/flex_container.ts index 62660a4ee..274284775 100644 --- a/src/public/app/widgets/containers/flex_container.ts +++ b/src/public/app/widgets/containers/flex_container.ts @@ -1,4 +1,4 @@ -import { TypedComponent } from "../../components/component.js"; +import type { TypedComponent } from "../../components/component.js"; import Container from "./container.js"; export type FlexDirection = "row" | "column"; diff --git a/src/public/app/widgets/containers/launcher.ts b/src/public/app/widgets/containers/launcher.ts index 41024073a..86fbabb96 100644 --- a/src/public/app/widgets/containers/launcher.ts +++ b/src/public/app/widgets/containers/launcher.ts @@ -11,7 +11,7 @@ import utils from "../../services/utils.js"; import TodayLauncher from "../buttons/launcher/today_launcher.js"; import HistoryNavigationButton from "../buttons/history_navigation.js"; import QuickSearchLauncherWidget from "../quick_search_launcher.js"; -import FNote from "../../entities/fnote.js"; +import type FNote from "../../entities/fnote.js"; import type { CommandNames } from "../../components/app_context.js"; interface InnerWidget extends BasicWidget { diff --git a/src/public/app/widgets/containers/right_pane_container.ts b/src/public/app/widgets/containers/right_pane_container.ts index 4325935fe..c7632769b 100644 --- a/src/public/app/widgets/containers/right_pane_container.ts +++ b/src/public/app/widgets/containers/right_pane_container.ts @@ -1,6 +1,6 @@ import FlexContainer from "./flex_container.js"; import splitService from "../../services/resizer.js"; -import RightPanelWidget from "../right_panel_widget.js"; +import type RightPanelWidget from "../right_panel_widget.js"; export default class RightPaneContainer extends FlexContainer { private rightPaneHidden: boolean; diff --git a/src/public/app/widgets/highlights_list.ts b/src/public/app/widgets/highlights_list.ts index 8b8fb06de..a9be2ddfa 100644 --- a/src/public/app/widgets/highlights_list.ts +++ b/src/public/app/widgets/highlights_list.ts @@ -12,7 +12,7 @@ import options from "../services/options.js"; import OnClickButtonWidget from "./buttons/onclick_button.js"; import appContext, { type EventData } from "../components/app_context.js"; import libraryLoader from "../services/library_loader.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; const TPL = `