diff --git a/apps/build-docs/package.json b/apps/build-docs/package.json index 80cdef54f..1ab4c6bc5 100644 --- a/apps/build-docs/package.json +++ b/apps/build-docs/package.json @@ -9,7 +9,7 @@ "keywords": [], "author": "Elian Doran ", "license": "AGPL-3.0-only", - "packageManager": "pnpm@10.28.0", + "packageManager": "pnpm@10.28.1", "devDependencies": { "@redocly/cli": "2.14.5", "archiver": "7.0.1", diff --git a/apps/client/package.json b/apps/client/package.json index e289700b2..f42946531 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -27,14 +27,14 @@ "@mermaid-js/layout-elk": "0.2.0", "@mind-elixir/node-menu": "5.0.1", "@popperjs/core": "2.11.8", - "@preact/signals": "2.5.1", + "@preact/signals": "2.6.0", "@triliumnext/ckeditor5": "workspace:*", "@triliumnext/codemirror": "workspace:*", "@triliumnext/commons": "workspace:*", "@triliumnext/highlightjs": "workspace:*", "@triliumnext/share-theme": "workspace:*", "@triliumnext/split.js": "workspace:*", - "@zumer/snapdom": "2.0.1", + "@zumer/snapdom": "2.0.2", "autocomplete.js": "0.38.1", "bootstrap": "5.3.8", "boxicons": "2.1.4", @@ -44,9 +44,9 @@ "draggabilly": "3.0.0", "force-graph": "1.51.0", "globals": "17.0.0", - "i18next": "25.7.4", + "i18next": "25.8.0", "i18next-http-backend": "3.0.2", - "jquery": "3.7.1", + "jquery": "4.0.0", "jquery.fancytree": "2.38.5", "jsplumb": "2.15.6", "katex": "0.16.27", @@ -56,7 +56,7 @@ "mark.js": "8.11.1", "marked": "17.0.1", "mermaid": "11.12.2", - "mind-elixir": "5.5.0", + "mind-elixir": "5.6.1", "normalize.css": "8.0.1", "panzoom": "9.4.3", "preact": "10.28.2", @@ -78,9 +78,9 @@ "@types/reveal.js": "5.2.2", "@types/tabulator-tables": "6.3.1", "copy-webpack-plugin": "13.0.1", - "happy-dom": "20.3.0", - "lightningcss": "1.30.2", + "happy-dom": "20.3.4", + "lightningcss": "1.31.1", "script-loader": "0.7.2", - "vite-plugin-static-copy": "3.1.4" + "vite-plugin-static-copy": "3.1.5" } } \ No newline at end of file diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index b5f203b24..8f6466e01 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -1,6 +1,6 @@ import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type CodeMirror from "@triliumnext/codemirror"; -import { SqlExecuteResults } from "@triliumnext/commons"; +import { SqlExecuteResponse } from "@triliumnext/commons"; import type { NativeImage, TouchBar } from "electron"; import { ColumnComponent } from "tabulator-tables"; @@ -410,7 +410,7 @@ type EventMappings = { addNewLabel: CommandData; addNewRelation: CommandData; sqlQueryResults: CommandData & { - results: SqlExecuteResults; + response: SqlExecuteResponse; }; readOnlyTemporarilyDisabled: { noteContext: NoteContext; diff --git a/apps/client/src/components/entrypoints.ts b/apps/client/src/components/entrypoints.ts index 8a902666f..8fc4e1b3d 100644 --- a/apps/client/src/components/entrypoints.ts +++ b/apps/client/src/components/entrypoints.ts @@ -1,16 +1,17 @@ -import utils from "../services/utils.js"; +import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons"; + +import bundleService from "../services/bundle.js"; import dateNoteService from "../services/date_notes.js"; +import froca from "../services/froca.js"; +import { t } from "../services/i18n.js"; +import linkService from "../services/link.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import server from "../services/server.js"; +import toastService from "../services/toast.js"; +import utils from "../services/utils.js"; +import ws from "../services/ws.js"; import appContext, { type NoteCommandData } from "./app_context.js"; import Component from "./component.js"; -import toastService from "../services/toast.js"; -import ws from "../services/ws.js"; -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 { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons"; export default class Entrypoints extends Component { constructor() { @@ -187,13 +188,8 @@ export default class Entrypoints extends Component { } else if (note.mime.endsWith("env=backend")) { await server.post(`script/run/${note.noteId}`); } else if (note.mime === "text/x-sqlite;schema=trilium") { - const resp = await server.post(`sql/execute/${note.noteId}`); - - if (!resp.success) { - toastService.showError(t("entrypoints.sql-error", { message: resp.error })); - } - - await appContext.triggerEvent("sqlQueryResults", { ntxId: ntxId, results: resp.results }); + const response = await server.post(`sql/execute/${note.noteId}`); + await appContext.triggerEvent("sqlQueryResults", { ntxId, response }); } toastService.showMessage(t("entrypoints.note-executed")); diff --git a/apps/client/src/index.ts b/apps/client/src/index.ts index 795adc8cf..c42ac5d3e 100644 --- a/apps/client/src/index.ts +++ b/apps/client/src/index.ts @@ -16,6 +16,17 @@ async function initJQuery() { const $ = (await import("jquery")).default; window.$ = $; window.jQuery = $; + + // Polyfill removed jQuery methods for autocomplete.js compatibility + ($ as any).isArray = Array.isArray; + ($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; }; + ($ as any).isPlainObject = function(obj: any) { + if (obj == null || typeof obj !== 'object') { return false; } + const proto = Object.getPrototypeOf(obj); + if (proto === null) { return true; } + const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor; + return typeof Ctor === 'function' && Ctor === Object; + }; } async function setupGlob() { @@ -39,22 +50,25 @@ async function loadBootstrapCss() { } function loadStylesheets() { - const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob; + const { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob; + const cssToLoad: string[] = []; - cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`); - cssToLoad.push(`api/fonts`); - cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`); - if (themeCssUrl) { - cssToLoad.push(themeCssUrl); + if (device !== "print") { + cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`); + cssToLoad.push(`api/fonts`); + cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`); + if (themeCssUrl) { + cssToLoad.push(themeCssUrl); + } + if (themeUseNextAsBase === "next") { + cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`); + } else if (themeUseNextAsBase === "next-dark") { + cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`); + } else if (themeUseNextAsBase === "next-light") { + cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`); + } + cssToLoad.push(`${assetPath}/stylesheets/style.css`); } - if (themeUseNextAsBase === "next") { - cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`); - } else if (themeUseNextAsBase === "next-dark") { - cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`); - } else if (themeUseNextAsBase === "next-light") { - cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`); - } - cssToLoad.push(`${assetPath}/stylesheets/style.css`); for (const href of cssToLoad) { const linkEl = document.createElement("link"); @@ -91,10 +105,17 @@ function setBodyAttributes() { } async function loadScripts() { - if (glob.device === "mobile") { - await import("./mobile.js"); - } else { - await import("./desktop.js"); + switch (glob.device) { + case "mobile": + await import("./mobile.js"); + break; + case "print": + await import("./print.js"); + break; + case "desktop": + default: + await import("./desktop.js"); + break; } } diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index ffc94aec3..511f6f9c0 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -46,8 +46,6 @@ import ScrollPadding from "../widgets/scroll_padding.js"; import SearchResult from "../widgets/search_result.jsx"; import SharedInfo from "../widgets/shared_info.jsx"; import RightPanelContainer from "../widgets/sidebar/RightPanelContainer.jsx"; -import SqlResults from "../widgets/sql_result.js"; -import SqlTableSchemas from "../widgets/sql_table_schemas.js"; import TabRowWidget from "../widgets/tab_row.js"; import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx"; import TitleBarButtons from "../widgets/title_bar_buttons.jsx"; @@ -163,11 +161,9 @@ export default class DesktopLayout { .child() ) .optChild(!isNewLayout, ) - .child() .child() .child() .child() - .child() .child() ) .child() diff --git a/apps/client/src/print.tsx b/apps/client/src/print.tsx index 16b41cd42..96461db2d 100644 --- a/apps/client/src/print.tsx +++ b/apps/client/src/print.tsx @@ -29,7 +29,9 @@ async function main() { const froca = (await import("./services/froca")).default; const note = await froca.getNote(noteId); - render(, document.body); + const bodyWrapper = document.createElement("div"); + render(, bodyWrapper); + document.body.appendChild(bodyWrapper); } function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) { diff --git a/apps/client/src/runtime.ts b/apps/client/src/runtime.ts index 4c82481b1..cab174a76 100644 --- a/apps/client/src/runtime.ts +++ b/apps/client/src/runtime.ts @@ -8,6 +8,17 @@ async function loadBootstrap() { } } +// Polyfill removed jQuery methods for autocomplete.js compatibility +($ as any).isArray = Array.isArray; +($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; }; +($ as any).isPlainObject = function(obj: any) { + if (obj == null || typeof obj !== 'object') { return false; } + const proto = Object.getPrototypeOf(obj); + if (proto === null) { return true; } + const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor; + return typeof Ctor === 'function' && Ctor === Object; +}; + (window as any).$ = $; (window as any).jQuery = $; await loadBootstrap(); diff --git a/apps/client/src/services/shortcuts.spec.ts b/apps/client/src/services/shortcuts.spec.ts index 6950c604c..f2170da30 100644 --- a/apps/client/src/services/shortcuts.spec.ts +++ b/apps/client/src/services/shortcuts.spec.ts @@ -1,5 +1,6 @@ -import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; -import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import shortcuts, { isIMEComposing, keyMatches, matchesShortcut } from "./shortcuts.js"; // Mock utils module vi.mock("./utils.js", () => ({ diff --git a/apps/client/src/stylesheets/table.css b/apps/client/src/stylesheets/table.css index 11e401fcb..dace356a9 100644 --- a/apps/client/src/stylesheets/table.css +++ b/apps/client/src/stylesheets/table.css @@ -14,13 +14,13 @@ --row-moving-background-color: var(--accented-background-color); --row-text-color: var(--main-text-color); --row-delimiter-color: var(--more-accented-background-color); - + --cell-horiz-padding-size: 8px; --cell-vert-padding-size: 8px; - + --cell-editable-hover-outline-color: var(--main-border-color); --cell-read-only-text-color: var(--muted-text-color); - + --cell-editing-border-color: var(--main-border-color); --cell-editing-border-width: 2px; --cell-editing-background-color: var(--ck-color-selector-focused-cell-background); @@ -40,10 +40,42 @@ border-bottom: var(--col-header-bottom-border); background: var(--col-header-background-color); color: var(--col-header-text-color); -} + font-weight: normal; -.tabulator .tabulator-col-content { - padding: 8px 4px !important; + .tabulator-col.tabulator-range-highlight { + background: inherit; + color: inherit; + font-weight: bold; + } + + .tabulator-col-content { + padding: 0 !important; + + .tabulator-col-title-holder { + padding: 8px 4px; + } + + &:has(.tabulator-header-filter) { + .tabulator-col-title-holder { + padding: 4px; + padding-bottom: 0; + } + } + + .tabulator-header-filter { + background: var(--main-background-color); + padding: 2px 1px; + + input { + background: var(--main-background-color); + color: var(--main-text-color); + border: 1px solid var(--button-border-color); + border-radius: 3px; + outline: none; + padding: 2px; + } + } + } } @media (hover: hover) and (pointer: fine) { @@ -80,7 +112,6 @@ .tabulator-tableholder { padding-top: 10px; - height: unset !important; /* Don't extend on the full height */ } /* Rows */ @@ -99,6 +130,14 @@ border-top: none; border-bottom: 1px solid var(--row-delimiter-color); color: var(--row-text-color); + + &:last-of-type { + border-bottom: none; + } + + &.tabulator-range-highlight > .tabulator-cell.tabulator-frozen { + font-weight: bold; + } } .tabulator-row.tabulator-row-odd { @@ -120,11 +159,14 @@ margin-inline-end: var(--cell-editing-border-width); } -.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left, .tabulator-row .tabulator-cell { border-inline-end-color: transparent; } +.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left { + border-inline-end-color: var(--main-border-color); +} + .tabulator-row .tabulator-cell:not(.tabulator-editable) { color: var(--cell-read-only-text-color); } @@ -174,10 +216,6 @@ margin: 0; } -.tabulator .tabulator-footer { - color: var(--main-text-color); -} - /* Context menus */ .tabulator-popup-container { @@ -192,8 +230,27 @@ } /* Footer */ - :root .tabulator .tabulator-footer { - border-top: unset; + background: transparent; + color: var(--main-text-color); + border-top: 1px solid var(--main-border-color); padding: 10px 0; -} \ No newline at end of file + + .tabulator-page { + background: var(--button-background-color); + color: var(--button-text-color); + border: 1px solid var(--button-border-color); + border-radius: var(--button-border-radius); + + &:hover { + border-color: var(--hover-item-border-color); + color: var(--button-text-color); + } + } + + select { + background: var(--button-background-color); + color: var(--input-text-color); + border: 1px solid var(--button-border-color); + } +} diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 972d31f84..7276d5da1 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1816,7 +1816,11 @@ "configure_launchbar": "Configure Launchbar" }, "sql_result": { - "no_rows": "No rows have been returned for this query" + "not_executed": "The query has not been executed yet.", + "no_rows": "No rows have been returned for this query", + "failed": "SQL query execution has failed", + "statement_result": "Statement result", + "execute_now": "Execute now" }, "sql_table_schemas": { "tables": "Tables" diff --git a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx index 8bf02d96c..4ad6c520a 100644 --- a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx +++ b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx @@ -7,7 +7,6 @@ import Component from "../components/component"; import NoteContext from "../components/note_context"; import FNote from "../entities/fnote"; import attributes from "../services/attributes"; -import { isExperimentalFeatureEnabled } from "../services/experimental_features"; import froca from "../services/froca"; import { t } from "../services/i18n"; import { copyImageReferenceToClipboard } from "../services/image"; @@ -101,7 +100,8 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) { const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); - const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap") + const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely(); + const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite) && note.isContentAvailable() && isDefaultViewMode; return isEnabled && (); const [ type, setType ] = useState(); const [ mime, setMime ] = useState(); + const refreshIdRef = useRef(0); function refresh() { + const refreshId = ++refreshIdRef.current; + getExtendedWidgetType(actualNote, noteContext).then(type => { + if (refreshId !== refreshIdRef.current) return; setNote(actualNote); setType(type); setMime(actualNote?.mime); @@ -318,6 +322,8 @@ export async function getExtendedWidgetType(note: FNote | null | undefined, note resultingType = "noteMap"; } else if (type === "text" && (await noteContext?.isReadOnly())) { resultingType = "readOnlyText"; + } else if (note.isTriliumSqlite()) { + resultingType = "sqlConsole"; } else if ((type === "code" || type === "mermaid") && (await noteContext?.isReadOnly())) { resultingType = "readOnlyCode"; } else if (type === "text") { @@ -342,9 +348,8 @@ export function checkFullHeight(noteContext: NoteContext | undefined, type: Exte // https://github.com/zadam/trilium/issues/2522 const isBackendNote = noteContext?.noteId === "_backendLog"; - const isSqlNote = noteContext.note?.mime === "text/x-sqlite;schema=trilium"; const isFullHeightNoteType = type && TYPE_MAPPINGS[type].isFullHeight; - return (!noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote) + return (!noteContext?.hasNoteList() && isFullHeightNoteType) || noteContext?.viewScope?.viewMode === "attachments" || isBackendNote; } @@ -358,8 +363,8 @@ function showToast(type: "printing" | "exporting_pdf", progress: number = 0) { }); } -function handlePrintReport(printReport: PrintReport) { - if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) { +function handlePrintReport(printReport?: PrintReport) { + if (printReport?.type === "collection" && printReport.ignoredNoteIds.length > 0) { toast.showPersistent({ id: "print-report", icon: "bx bx-collection", diff --git a/apps/client/src/widgets/collections/NoteList.tsx b/apps/client/src/widgets/collections/NoteList.tsx index b2ceeac44..c9dcb4a23 100644 --- a/apps/client/src/widgets/collections/NoteList.tsx +++ b/apps/client/src/widgets/collections/NoteList.tsx @@ -1,7 +1,7 @@ import "./NoteList.css"; import { WebSocketMessage } from "@triliumnext/commons"; -import { VNode } from "preact"; +import { Component, VNode } from "preact"; import { lazy, Suspense } from "preact/compat"; import { useEffect, useRef, useState } from "preact/hooks"; @@ -123,7 +123,9 @@ export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePa } const ComponentToRender = viewType && props && isEnabled && ( - props.media === "print" ? ViewComponents[viewType].print : ViewComponents[viewType].normal + props.media === "print" + ? ViewComponents[viewType].print ?? ViewComponents[viewType].normal + : ViewComponents[viewType].normal ); return ( diff --git a/apps/client/src/widgets/collections/table/index.css b/apps/client/src/widgets/collections/table/index.css index ff24dda26..897a87b51 100644 --- a/apps/client/src/widgets/collections/table/index.css +++ b/apps/client/src/widgets/collections/table/index.css @@ -4,6 +4,10 @@ height: 100%; user-select: none; padding: 0 5px 0 10px; + + .tabulator-tableholder { + height: unset !important; + } } .table-view-container { @@ -68,4 +72,4 @@ inset-inline-start: 0; font-size: 1.5em; transform: translateY(-50%); -} \ No newline at end of file +} diff --git a/apps/client/src/widgets/collections/table/tabulator.tsx b/apps/client/src/widgets/collections/table/tabulator.tsx index 31fb8d4f8..62d7283b9 100644 --- a/apps/client/src/widgets/collections/table/tabulator.tsx +++ b/apps/client/src/widgets/collections/table/tabulator.tsx @@ -1,18 +1,20 @@ -import { useContext, useEffect, useLayoutEffect, useRef } from "preact/hooks"; -import { EventCallBackMethods, Module, Options, Tabulator as VanillaTabulator } from "tabulator-tables"; import "tabulator-tables/dist/css/tabulator.css"; import "../../../../src/stylesheets/table.css"; -import { ParentComponent, renderReactWidget } from "../../react/react_utils"; -import { JSX } from "preact/jsx-runtime"; + import { isValidElement, RefObject } from "preact"; +import { useContext, useEffect, useLayoutEffect, useRef } from "preact/hooks"; +import { JSX } from "preact/jsx-runtime"; +import { EventCallBackMethods, Module, Options, Tabulator as VanillaTabulator } from "tabulator-tables"; + +import { ParentComponent, renderReactWidget } from "../../react/react_utils"; interface TableProps extends Omit { - tabulatorRef: RefObject; + tabulatorRef?: RefObject; className?: string; data?: T[]; modules?: (new (table: VanillaTabulator) => Module)[]; events?: Partial; - index: keyof T; + index?: keyof T; footerElement?: string | HTMLElement | JSX.Element; onReady?: () => void; } @@ -43,7 +45,9 @@ export default function Tabulator({ className, columns, data, modules, tabula tabulator.on("tableBuilt", () => { tabulatorRef.current = tabulator; - externalTabulatorRef.current = tabulator; + if (externalTabulatorRef) { + externalTabulatorRef.current = tabulator; + } onReady?.(); }); @@ -62,12 +66,15 @@ export default function Tabulator({ className, columns, data, modules, tabula for (const [ eventName, handler ] of Object.entries(events)) { tabulator.off(eventName as keyof EventCallBackMethods, handler); } - } + }; }, Object.values(events ?? {})); // Change in data. - useEffect(() => { tabulatorRef.current?.setData(data) }, [ data ]); - useEffect(() => { columns && tabulatorRef.current?.setColumns(columns)}, [ data]); + useEffect(() => { tabulatorRef.current?.setData(data); }, [ data ]); + useEffect(() => { + if (!columns) return; + tabulatorRef.current?.setColumns(columns); + }, [ columns ]); return (
diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index ce44681e6..907090253 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -7,6 +7,7 @@ import { ComponentChild } from "preact"; import { useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; import { Trans } from "react-i18next"; +import FNote from "../../entities/fnote"; import { ViewScope } from "../../services/link"; import { formatDateTime } from "../../utils/formatters"; import NoteIcon from "../note_icon"; @@ -22,12 +23,12 @@ const supportedNoteTypes = new Set([ export default function InlineTitle() { const { note, parentComponent, viewScope } = useNoteContext(); const type = useNoteProperty(note, "type"); - const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope)); + const [ shown, setShown ] = useState(shouldShow(note, type, viewScope)); const containerRef = useRef(null); const [ titleHidden, setTitleHidden ] = useState(false); useLayoutEffect(() => { - setShown(shouldShow(note?.noteId, type, viewScope)); + setShown(shouldShow(note, type, viewScope)); }, [ note, type, viewScope ]); useLayoutEffect(() => { @@ -69,9 +70,10 @@ export default function InlineTitle() { ); } -function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) { +function shouldShow(note: FNote | null | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) { if (viewScope?.viewMode !== "default") return false; - if (noteId?.startsWith("_options")) return true; + if (note?.noteId?.startsWith("_options")) return true; + if (note?.isTriliumSqlite()) return false; return type && supportedNoteTypes.has(type); } diff --git a/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx b/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx index 8ac68ae83..e345249ad 100644 --- a/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx +++ b/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx @@ -39,7 +39,7 @@ export default function NoteTypeSwitcher() { const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]); const { builtinTemplates, collectionTemplates } = useBuiltinTemplates(); - return (currentNoteType && supportedNoteTypes.has(currentNoteType) && + return (currentNoteType && supportedNoteTypes.has(currentNoteType) && !note?.isTriliumSqlite() &&
| "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat"; +export type ExtendedNoteType = Exclude | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat" | "sqlConsole"; export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element | undefined); type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget); @@ -140,5 +140,10 @@ export const TYPE_MAPPINGS: Record = { view: () => import("./type_widgets/AiChat"), className: "ai-chat-widget-container", isFullHeight: true + }, + sqlConsole: { + view: () => import("./type_widgets/SqlConsole"), + className: "sql-console-widget-container", + isFullHeight: true } }; diff --git a/apps/client/src/widgets/react/NoItems.css b/apps/client/src/widgets/react/NoItems.css new file mode 100644 index 000000000..f9876db6c --- /dev/null +++ b/apps/client/src/widgets/react/NoItems.css @@ -0,0 +1,18 @@ +.no-items { + display: flex; + align-items: center; + justify-content: center; + flex-grow: 1; + flex-direction: column; + padding: 0.75em; + color: var(--muted-text-color); + height: 100%; + + .tn-icon { + font-size: 3em; + } + + button { + margin-top: 1em; + } +} diff --git a/apps/client/src/widgets/react/NoItems.tsx b/apps/client/src/widgets/react/NoItems.tsx new file mode 100644 index 000000000..d7a5a6270 --- /dev/null +++ b/apps/client/src/widgets/react/NoItems.tsx @@ -0,0 +1,21 @@ +import "./NoItems.css"; + +import { ComponentChildren } from "preact"; + +import Icon from "./Icon"; + +interface NoItemsProps { + icon: string; + text: string; + children?: ComponentChildren; +} + +export default function NoItems({ icon, text, children }: NoItemsProps) { + return ( +
+ + {text} + {children} +
+ ); +} diff --git a/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx b/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx index cc36968d3..36b4b8543 100644 --- a/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx +++ b/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx @@ -184,7 +184,8 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: N function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: NoteActionsCustomInnerProps) { const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); - const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap") + const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely(); + const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite) && note.isContentAvailable() && isDefaultViewMode; return isEnabled && (null); const [height, setHeight] = useState(10); - const isEnabled = ["text", "code"].includes(note?.type ?? "") && viewScope?.viewMode === "default"; + const isEnabled = ["text", "code"].includes(note?.type ?? "") + && viewScope?.viewMode === "default" + && !note?.isTriliumSqlite(); const refreshHeight = () => { if (!ref.current) return; @@ -37,6 +40,6 @@ export default function ScrollPadding() { style={{ height }} onClick={() => parentComponent.triggerCommand("scrollToEnd", { ntxId })} /> - :
- ) + :
+ ); } diff --git a/apps/client/src/widgets/sidebar/RightPanelContainer.css b/apps/client/src/widgets/sidebar/RightPanelContainer.css index 4c097c386..2000a20d7 100644 --- a/apps/client/src/widgets/sidebar/RightPanelContainer.css +++ b/apps/client/src/widgets/sidebar/RightPanelContainer.css @@ -40,22 +40,4 @@ body.experimental-feature-new-layout #right-pane { .gutter-vertical + .card .card-header { padding-top: 0; } - - .no-items { - display: flex; - align-items: center; - justify-content: center; - flex-grow: 1; - flex-direction: column; - padding: 0.75em; - color: var(--muted-text-color); - - .tn-icon { - font-size: 3em; - } - - button { - margin-top: 1em; - } - } } diff --git a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx index d28887b9f..082b0a66f 100644 --- a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx +++ b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx @@ -3,7 +3,7 @@ import "./RightPanelContainer.css"; import Split from "@triliumnext/split.js"; import { VNode } from "preact"; -import { useState, useEffect, useRef, useCallback } from "preact/hooks"; +import { useCallback, useEffect, useRef, useState } from "preact/hooks"; import appContext from "../../components/app_context"; import { WidgetsByParent } from "../../services/bundle"; @@ -12,7 +12,7 @@ import options from "../../services/options"; import { DEFAULT_GUTTER_SIZE } from "../../services/resizer"; import Button from "../react/Button"; import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionJson } from "../react/hooks"; -import Icon from "../react/Icon"; +import NoItems from "../react/NoItems"; import LegacyRightPanelWidget from "../right_panel_widget"; import HighlightsList from "./HighlightsList"; import PdfAttachments from "./pdf/PdfAttachments"; @@ -47,14 +47,15 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare items.length > 0 ? ( items ) : ( -
- - {t("right_pane.empty_message")} +
+ ) )}
diff --git a/apps/client/src/widgets/sql_result.css b/apps/client/src/widgets/sql_result.css deleted file mode 100644 index 63b5621ed..000000000 --- a/apps/client/src/widgets/sql_result.css +++ /dev/null @@ -1,7 +0,0 @@ -.sql-result-widget { - padding: 15px; -} - -.sql-console-result-container td { - white-space: preserve; -} \ No newline at end of file diff --git a/apps/client/src/widgets/sql_result.tsx b/apps/client/src/widgets/sql_result.tsx deleted file mode 100644 index 7aaa5739d..000000000 --- a/apps/client/src/widgets/sql_result.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { SqlExecuteResults } from "@triliumnext/commons"; -import { useNoteContext, useTriliumEvent } from "./react/hooks"; -import "./sql_result.css"; -import { useState } from "preact/hooks"; -import Alert from "./react/Alert"; -import { t } from "../services/i18n"; - -export default function SqlResults() { - const { note, ntxId } = useNoteContext(); - const [ results, setResults ] = useState(); - - useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, results }) => { - if (eventNtxId !== ntxId) return; - setResults(results); - }) - - const isEnabled = note?.mime === "text/x-sqlite;schema=trilium"; - return ( -
- {isEnabled && ( - results?.length === 1 && Array.isArray(results[0]) && results[0].length === 0 ? ( - - {t("sql_result.no_rows")} - - ) : ( -
- {results?.map(rows => { - // inserts, updates - if (typeof rows === "object" && !Array.isArray(rows)) { - return
{JSON.stringify(rows, null, "\t")}
- } - - // selects - return - })} -
- ) - )} -
- ) -} - -function SqlResultTable({ rows }: { rows: object[] }) { - if (!rows.length) return; - - return ( - - - - {Object.keys(rows[0]).map(key => )} - - - - - {rows.map(row => ( - - {Object.values(row).map(cell => )} - - ))} - -
{key}
{cell}
- ) -} diff --git a/apps/client/src/widgets/sql_table_schemas.css b/apps/client/src/widgets/sql_table_schemas.css deleted file mode 100644 index d6c1c8f95..000000000 --- a/apps/client/src/widgets/sql_table_schemas.css +++ /dev/null @@ -1,43 +0,0 @@ -.sql-table-schemas-widget { - padding: 12px; - padding-inline-end: 10%; - contain: none !important; -} - -.sql-table-schemas > .dropdown { - display: inline-block !important; -} - -.sql-table-schemas button.btn { - padding: 0.25rem 0.4rem; - font-size: 0.875rem; - line-height: 0.5; - border: 1px solid var(--button-border-color); - border-radius: var(--button-border-radius); - background: var(--button-background-color); - color: var(--button-text-color); - cursor: pointer; -} - -.sql-console-result-container { - width: 100%; - font-size: smaller; - margin-top: 10px; - flex-grow: 1; - overflow: auto; - min-height: 0; -} - -.table-schema td { - padding: 5px; -} - -.dropdown .table-schema { - font-family: var(--monospace-font-family); - font-size: .85em; -} - -/* Data type */ -.dropdown .table-schema td:nth-child(2) { - color: var(--muted-text-color); -} \ No newline at end of file diff --git a/apps/client/src/widgets/sql_table_schemas.tsx b/apps/client/src/widgets/sql_table_schemas.tsx deleted file mode 100644 index 3605c2f95..000000000 --- a/apps/client/src/widgets/sql_table_schemas.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useEffect, useState } from "preact/hooks"; -import { t } from "../services/i18n"; -import { useNoteContext } from "./react/hooks"; -import "./sql_table_schemas.css"; -import { SchemaResponse } from "@triliumnext/commons"; -import server from "../services/server"; -import Dropdown from "./react/Dropdown"; - -export default function SqlTableSchemas() { - const { note } = useNoteContext(); - const [ schemas, setSchemas ] = useState(); - - useEffect(() => { - server.get("sql/schema").then(setSchemas); - }, []); - - const isEnabled = note?.mime === "text/x-sqlite;schema=trilium" && schemas; - return ( -
- {isEnabled && ( - <> - {t("sql_table_schemas.tables")}{": "} - - - {schemas.map(({ name, columns }) => ( - <> - - - {columns.map(column => ( - - - - - ))} -
{column.name}{column.type}
-
- {" "} - - ))} -
- - )} -
- ) -} \ No newline at end of file diff --git a/apps/client/src/widgets/type_widgets/SqlConsole.css b/apps/client/src/widgets/type_widgets/SqlConsole.css new file mode 100644 index 000000000..85b812554 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/SqlConsole.css @@ -0,0 +1,81 @@ +.sql-console-widget-container { + .note-detail-split.split-vertical { + flex-direction: column-reverse; + } + + .note-detail-split-preview { + overflow: auto; + } + + .gutter { + background-color: var(--accented-background-color) !important; + } + + .sql-result-widget { + height: 100%; + + > .sql-console-result-container { + width: 100%; + height: 100%; + font-size: smaller; + flex-grow: 1; + overflow: auto; + min-height: 0; + + > .tabulator { + --cell-vert-padding-size: 4px; + + > .tabulator-tableholder { + padding: 0; + } + + > .tabulator-footer, + > .tabulator-footer .tabulator-footer-contents { + padding: 2px 4px; + } + } + } + } + + .sql-table-schemas-widget { + padding: 12px; + padding-inline-end: 10%; + contain: none !important; + + .sql-table-schemas { + display: flex; + flex-wrap: wrap; + gap: 0.25em; + } + + > .dropdown { + display: inline-block !important; + } + + button.btn { + padding: 0.25rem 0.4rem; + font-size: 0.875rem; + line-height: 0.5; + border: 1px solid var(--button-border-color); + border-radius: var(--button-border-radius); + background: var(--button-background-color); + color: var(--button-text-color); + cursor: pointer; + } + + .table-schema td { + padding: 5px; + } + + .dropdown .table-schema { + font-family: var(--monospace-font-family); + font-size: .85em; + } + + /* Data type */ + .dropdown .table-schema td:nth-child(2) { + color: var(--muted-text-color); + } + } +} + diff --git a/apps/client/src/widgets/type_widgets/SqlConsole.tsx b/apps/client/src/widgets/type_widgets/SqlConsole.tsx new file mode 100644 index 000000000..9c4108003 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/SqlConsole.tsx @@ -0,0 +1,176 @@ +import "./SqlConsole.css"; + +import { SchemaResponse, SqlExecuteResponse } from "@triliumnext/commons"; +import { useEffect, useState } from "preact/hooks"; +import { ClipboardModule, EditModule, ExportModule, FilterModule, FormatModule, FrozenColumnsModule, KeybindingsModule, PageModule, ResizeColumnsModule, SelectRangeModule, SelectRowModule, SortModule } from "tabulator-tables"; + +import { t } from "../../services/i18n"; +import server from "../../services/server"; +import Tabulator from "../collections/table/tabulator"; +import Button from "../react/Button"; +import Dropdown from "../react/Dropdown"; +import { useTriliumEvent } from "../react/hooks"; +import NoItems from "../react/NoItems"; +import SplitEditor from "./helpers/SplitEditor"; +import { TypeWidgetProps } from "./type_widget"; + +export default function SqlConsole(props: TypeWidgetProps) { + return ( + } + previewContent={} + forceOrientation="vertical" + splitOptions={{ + sizes: [ 70, 30 ] + }} + /> + ); +} + +function SqlResults({ ntxId }: TypeWidgetProps) { + const [ response, setResponse ] = useState(); + + useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, response }) => { + if (eventNtxId !== ntxId) return; + setResponse(response); + }); + + // Not yet executed. + if (response === undefined) { + return ( + +