Compare commits

..

40 Commits

Author SHA1 Message Date
Elian Doran
197fa90176 style(sql_console): improve style for frozen cell 2026-01-18 13:09:46 +02:00
Elian Doran
0844914e11 style(sql_console): improve style for highlighted range 2026-01-18 13:05:18 +02:00
Elian Doran
c376b0bbe2 style(sql_console): improve filter spacing 2026-01-18 12:35:42 +02:00
Elian Doran
4491086c55 style(sql_console): improve header & footer inputs 2026-01-18 12:34:00 +02:00
Elian Doran
791697369d style(sql_console): remove background for footer 2026-01-18 12:22:53 +02:00
Elian Doran
28d0bfd229 chore(sql_console): reducing padding in footer 2026-01-18 12:12:05 +02:00
Elian Doran
8182a04eae fix(sql_console): not refreshing when switching between notes 2026-01-18 12:09:01 +02:00
Elian Doran
711828d6b4 fix(shortcuts): triggering in bubbling phase, not capturing 2026-01-18 12:07:23 +02:00
Elian Doran
69e88c1d9f chore(sql_console): set gutter color 2026-01-18 11:48:34 +02:00
Elian Doran
748b87da9a feat(sql_console): improve display for statements 2026-01-18 11:34:31 +02:00
Elian Doran
94dca4cd87 feat(sql_console): report errors inline 2026-01-18 11:13:31 +02:00
Elian Doran
7179701e0f feat(sql_console): improve no results 2026-01-18 10:58:32 +02:00
Elian Doran
af5061646c feat(sql_console): add not yet executed message 2026-01-18 10:36:44 +02:00
Elian Doran
9c4163ad3a feat(sql_console): page size selector 2026-01-18 10:24:55 +02:00
Elian Doran
46c3f5296a chore(sql_console): full-height table 2026-01-18 10:20:15 +02:00
Elian Doran
ebadcfd844 feat(sql_console): enable pagination 2026-01-18 10:14:55 +02:00
Elian Doran
b7d4947462 Merge remote-tracking branch 'origin/main' into feature/sql_console_improvements 2026-01-18 10:08:48 +02:00
Elian Doran
56fcc7adcc chore(sql_console): fix lint warnings 2026-01-17 22:41:55 +02:00
Elian Doran
fb0c7359f1 chore(sql_console): fix typecheck issue 2026-01-17 22:39:32 +02:00
Elian Doran
4c4e5b85e9 chore(sql_console): integrate table reference 2026-01-17 22:38:18 +02:00
Elian Doran
476247beb5 feat(sql_console): increase size for results 2026-01-17 22:29:20 +02:00
Elian Doran
2c87f609f3 feat(sql_console): add filter 2026-01-17 22:13:13 +02:00
Elian Doran
bc79ff6845 feat(sql_console): row numbers 2026-01-17 22:09:24 +02:00
Elian Doran
f10373d54f feat(sql_console): add clipboard 2026-01-17 22:02:47 +02:00
Elian Doran
630d16b722 feat(sql_console): enable sorting 2026-01-17 21:47:02 +02:00
Elian Doran
769f3db21c feat(sql_console): make columns resizable 2026-01-17 21:45:10 +02:00
Elian Doran
c6896a4b33 feat(sql_console): reduce column width 2026-01-17 21:43:21 +02:00
Elian Doran
7c18025098 feat(sql_console): reduce spacing to fit more content 2026-01-17 21:41:06 +02:00
Elian Doran
6ae74b3181 feat(sql_console): make scrolls and headers always visible 2026-01-17 21:35:58 +02:00
Elian Doran
2ecfbbf284 feat(sql_console): improve fit & solve build error 2026-01-17 21:26:44 +02:00
Elian Doran
781de9a1fb feat(sql_console): basic integration of Tabulator 2026-01-17 21:22:22 +02:00
Elian Doran
6972a4b901 fix(note_detail): preview leaks between mermaid & SQL console 2026-01-17 20:58:26 +02:00
Elian Doran
52ed1750ac fix(sql_console): runtime error for inline title 2026-01-17 20:52:53 +02:00
Elian Doran
9010e0b1ce chore(sql_console): reverse preview and code sections 2026-01-17 20:44:03 +02:00
Elian Doran
5053e74447 fix(sql_console): note type switcher showing up 2026-01-17 20:41:45 +02:00
Elian Doran
f294276849 fix(sql_console): full-height not respected 2026-01-17 20:38:44 +02:00
Elian Doran
0740788cc8 chore(sql_console): link stylesheet 2026-01-17 20:32:28 +02:00
Elian Doran
9bac07ce62 chore(sql_console): integrate results into preview of split 2026-01-17 20:29:15 +02:00
Elian Doran
3d8289d394 chore(note_detail): get code editor to show 2026-01-17 20:12:16 +02:00
Elian Doran
5a60fdad8a chore(note_detail): map SQL console to own type widget 2026-01-17 20:09:25 +02:00
25 changed files with 468 additions and 259 deletions

View File

@@ -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;

View File

@@ -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<SqlExecuteResponse>(`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<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
await appContext.triggerEvent("sqlQueryResults", { ntxId, response });
}
toastService.showMessage(t("entrypoints.note-executed"));

View File

@@ -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(<SharedInfo />)
)
.optChild(!isNewLayout, <PromotedAttributes />)
.child(<SqlTableSchemas />)
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<SearchResult />)
.child(<SqlResults />)
.child(<ScrollPadding />)
)
.child(<ApiLog />)

View File

@@ -110,8 +110,9 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st
}
};
// Add the event listener
element.addEventListener('keydown', listener);
// Add the event listener in capture phase to intercept events before they reach
// child elements like CodeMirror
element.addEventListener('keydown', listener, true);
// Store the binding for later cleanup
const binding: ShortcutBinding = {
@@ -138,15 +139,16 @@ export function removeIndividualBinding(binding: ShortcutBinding) {
if (activeBindingsInNamespace) {
activeBindings.set(key, activeBindingsInNamespace.filter(aBinding => aBinding.handler === binding.handler));
}
binding.element.removeEventListener("keydown", binding.listener);
// Remove listener with capture phase to match how it was added
binding.element.removeEventListener("keydown", binding.listener, true);
}
function removeNamespaceBindings(namespace: string) {
const bindings = activeBindings.get(namespace);
if (bindings) {
// Remove all event listeners for this namespace
bindings.forEach(binding => {
binding.element.removeEventListener('keydown', binding.listener);
// Remove listener with capture phase to match how it was added
binding.element.removeEventListener('keydown', binding.listener, true);
});
activeBindings.delete(namespace);
}

View File

@@ -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;
}
.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);
}
}

View File

@@ -1815,7 +1815,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"

View File

@@ -265,9 +265,13 @@ function useNoteInfo() {
const [ note, setNote ] = useState<FNote | null | undefined>();
const [ type, setType ] = useState<ExtendedNoteType>();
const [ mime, setMime ] = useState<string>();
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;
}

View File

@@ -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%);
}
}

View File

@@ -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<T> extends Omit<Options, "data" | "footerElement" | "index"> {
tabulatorRef: RefObject<VanillaTabulator>;
tabulatorRef?: RefObject<VanillaTabulator>;
className?: string;
data?: T[];
modules?: (new (table: VanillaTabulator) => Module)[];
events?: Partial<EventCallBackMethods>;
index: keyof T;
index?: keyof T;
footerElement?: string | HTMLElement | JSX.Element;
onReady?: () => void;
}
@@ -43,7 +45,9 @@ export default function Tabulator<T>({ className, columns, data, modules, tabula
tabulator.on("tableBuilt", () => {
tabulatorRef.current = tabulator;
externalTabulatorRef.current = tabulator;
if (externalTabulatorRef) {
externalTabulatorRef.current = tabulator;
}
onReady?.();
});
@@ -62,12 +66,12 @@ export default function Tabulator<T>({ 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(() => { columns && tabulatorRef.current?.setColumns(columns); }, [ data ]);
return (
<div ref={containerRef} className={className} />

View File

@@ -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<NoteType>([
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<HTMLDivElement>(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);
}

View File

@@ -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() &&
<div
className="note-type-switcher"
onWheel={onWheelHorizontalScroll}

View File

@@ -12,7 +12,7 @@ import { TypeWidgetProps } from "./type_widgets/type_widget";
* A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one,
* for protected session or attachment information.
*/
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat";
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "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<ExtendedNoteType, NoteTypeMapping> = {
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
}
};

View File

@@ -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;
}
}

View File

@@ -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 (
<div className="no-items">
<Icon icon={icon} />
{text}
{children}
</div>
);
}

View File

@@ -1,11 +1,14 @@
import { useEffect, useRef, useState } from "preact/hooks";
import { useNoteContext } from "./react/hooks";
export default function ScrollPadding() {
const { note, parentComponent, ntxId, viewScope } = useNoteContext();
const ref = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<number>(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 })}
/>
: <div></div>
)
: <div />
);
}

View File

@@ -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;
}
}
}

View File

@@ -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";
@@ -13,6 +13,7 @@ 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 +48,15 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
items.length > 0 ? (
items
) : (
<div className="no-items">
<Icon icon="bx bx-sidebar" />
{t("right_pane.empty_message")}
<NoItems
icon="bx bx-sidebar"
text={t("right_pane.empty_message")}
>
<Button
text={t("right_pane.empty_button")}
triggerCommand="toggleRightPane"
/>
</div>
</NoItems>
)
)}
</div>

View File

@@ -1,7 +0,0 @@
.sql-result-widget {
padding: 15px;
}
.sql-console-result-container td {
white-space: preserve;
}

View File

@@ -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<SqlExecuteResults>();
useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, results }) => {
if (eventNtxId !== ntxId) return;
setResults(results);
})
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium";
return (
<div className={`sql-result-widget ${!isEnabled ? "hidden-ext" : ""}`}>
{isEnabled && (
results?.length === 1 && Array.isArray(results[0]) && results[0].length === 0 ? (
<Alert type="info">
{t("sql_result.no_rows")}
</Alert>
) : (
<div className="sql-console-result-container selectable-text">
{results?.map(rows => {
// inserts, updates
if (typeof rows === "object" && !Array.isArray(rows)) {
return <pre>{JSON.stringify(rows, null, "\t")}</pre>
}
// selects
return <SqlResultTable rows={rows} />
})}
</div>
)
)}
</div>
)
}
function SqlResultTable({ rows }: { rows: object[] }) {
if (!rows.length) return;
return (
<table className="table table-striped">
<thead>
<tr>
{Object.keys(rows[0]).map(key => <th>{key}</th>)}
</tr>
</thead>
<tbody>
{rows.map(row => (
<tr>
{Object.values(row).map(cell => <td>{cell}</td>)}
</tr>
))}
</tbody>
</table>
)
}

View File

@@ -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);
}

View File

@@ -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<SchemaResponse[]>();
useEffect(() => {
server.get<SchemaResponse[]>("sql/schema").then(setSchemas);
}, []);
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium" && schemas;
return (
<div className={`sql-table-schemas-widget ${!isEnabled ? "hidden-ext" : ""}`}>
{isEnabled && (
<>
{t("sql_table_schemas.tables")}{": "}
<span class="sql-table-schemas">
{schemas.map(({ name, columns }) => (
<>
<Dropdown text={name} noSelectButtonStyle hideToggleArrow
>
<table className="table-schema">
{columns.map(column => (
<tr>
<td>{column.name}</td>
<td>{column.type}</td>
</tr>
))}
</table>
</Dropdown>
{" "}
</>
))}
</span>
</>
)}
</div>
)
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,182 @@
import "./SqlConsole.css";
import { SchemaResponse, SqlExecuteResponse } from "@triliumnext/commons";
import { useEffect, useState } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
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 (
<>
<SplitEditor
noteType="code"
{...props}
editorBefore={<SqlTableSchemas {...props} />}
previewContent={<SqlResults key={props.note.noteId} {...props} />}
splitOptions={{
sizes: [ 70, 30 ]
}}
/>
</>
);
}
function SqlResults({ ntxId }: TypeWidgetProps) {
const [ response, setResponse ] = useState<SqlExecuteResponse>();
useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, response }) => {
if (eventNtxId !== ntxId) return;
setResponse(response);
});
// Not yet executed.
if (response === undefined) {
return (
<NoItems
icon="bx bx-data"
text={t("sql_result.not_executed")}
>
<Button
text={t("sql_result.execute_now")}
triggerCommand="runActiveNote"
/>
</NoItems>
);
}
// Executed but failed.
if (response && !response.success) {
return (
<NoItems
icon="bx bx-error"
text={t("sql_result.failed")}
>
<pre className="sql-error-message selectable-text">{response.error}</pre>
</NoItems>
);
}
// Zero results.
if (response?.results.length === 1 && Array.isArray(response.results[0]) && response.results[0].length === 0) {
return (
<NoItems
icon="bx bx-rectangle"
text={t("sql_result.no_rows")}
/>
);
}
return (
<div className="sql-result-widget">
<div className="sql-console-result-container selectable-text">
{response?.results.map((rows, index) => {
// inserts, updates
if (typeof rows === "object" && !Array.isArray(rows)) {
return (
<NoItems
key={index}
icon="bx bx-play"
text={t("sql_result.statement_result")}
>
<pre key={index}>{JSON.stringify(rows, null, "\t")}</pre>
</NoItems>
);
}
// selects
return <SqlResultTable key={index} rows={rows} />;
})}
</div>
</div>
);
}
function SqlResultTable({ rows }: { rows: object[] }) {
if (!rows.length) return;
return (
<Tabulator
layout="fitDataFill"
modules={[ ResizeColumnsModule, SortModule, SelectRangeModule, ClipboardModule, KeybindingsModule, EditModule, ExportModule, SelectRowModule, FormatModule, FrozenColumnsModule, FilterModule, PageModule ]}
selectableRange
clipboard="copy"
clipboardCopyRowRange="range"
clipboardCopyConfig={{
rowHeaders: false,
columnHeaders: false
}}
pagination
paginationSize={15}
paginationSizeSelector
paginationCounter="rows"
height="100%"
columns={[
{
title: "#",
formatter: "rownum",
width: 60,
hozAlign: "right",
frozen: true
},
...Object.keys(rows[0]).map(key => ({
title: key,
field: key,
width: 250,
minWidth: 100,
widthGrow: 1,
resizable: true,
headerFilter: true as const
}))
]}
data={rows}
/>
);
}
export function SqlTableSchemas({ note }: TypeWidgetProps) {
const [ schemas, setSchemas ] = useState<SchemaResponse[]>();
useEffect(() => {
server.get<SchemaResponse[]>("sql/schema").then(setSchemas);
}, []);
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium" && schemas;
return (
<div className={`sql-table-schemas-widget ${!isEnabled ? "hidden-ext" : ""}`}>
{isEnabled && (
<>
{t("sql_table_schemas.tables")}{": "}
<span class="sql-table-schemas">
{schemas.map(({ name, columns }) => (
<Fragment key={name}>
<Dropdown text={name} noSelectButtonStyle hideToggleArrow
>
<table className="table-schema">
{columns.map(column => (
<tr key={column.name}>
<td>{column.name}</td>
<td>{column.type}</td>
</tr>
))}
</table>
</Dropdown>
{" "}
</Fragment>
))}
</span>
</>
)}
</div>
);
}

View File

@@ -1,13 +1,15 @@
import "./SplitEditor.css";
import Split from "@triliumnext/split.js";
import { ComponentChildren } from "preact";
import { useEffect, useRef } from "preact/hooks";
import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer";
import utils, { isMobile } from "../../../services/utils";
import ActionButton, { ActionButtonProps } from "../../react/ActionButton";
import Admonition from "../../react/Admonition";
import { useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
import "./SplitEditor.css";
import Split from "@triliumnext/split.js";
import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer";
import { EditableCode, EditableCodeProps } from "../code/Code";
import { ComponentChildren } from "preact";
import ActionButton, { ActionButtonProps } from "../../react/ActionButton";
export interface SplitEditorProps extends EditableCodeProps {
className?: string;
@@ -15,6 +17,7 @@ export interface SplitEditorProps extends EditableCodeProps {
splitOptions?: Split.Options;
previewContent: ComponentChildren;
previewButtons?: ComponentChildren;
editorBefore?: ComponentChildren;
}
/**
@@ -26,13 +29,14 @@ export interface SplitEditorProps extends EditableCodeProps {
* - Can display errors to the user via {@link setError}.
* - Horizontal or vertical orientation for the editor/preview split, adjustable via the switch split orientation button floating button.
*/
export default function SplitEditor({ note, error, splitOptions, previewContent, previewButtons, className, ...editorProps }: SplitEditorProps) {
export default function SplitEditor({ note, error, splitOptions, previewContent, previewButtons, className, editorBefore, ...editorProps }: SplitEditorProps) {
const splitEditorOrientation = useSplitOrientation();
const [ readOnly ] = useNoteLabelBoolean(note, "readOnly");
const containerRef = useRef<HTMLDivElement>(null);
const editor = (!readOnly &&
<div className="note-detail-split-editor-col">
{editorBefore}
<div className="note-detail-split-editor">
<EditableCode
note={note}
@@ -74,12 +78,12 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
}, [ readOnly, splitEditorOrientation ]);
return (
<div ref={containerRef} className={`note-detail-split note-detail-printable ${"split-" + splitEditorOrientation} ${readOnly ? "split-read-only" : ""} ${className ?? ""}`}>
<div ref={containerRef} className={`note-detail-split note-detail-printable ${`split-${ splitEditorOrientation}`} ${readOnly ? "split-read-only" : ""} ${className ?? ""}`}>
{splitEditorOrientation === "horizontal"
? <>{editor}{preview}</>
: <>{preview}{editor}</>}
? <>{editor}{preview}</>
: <>{preview}{editor}</>}
</div>
)
);
}
export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
@@ -88,7 +92,7 @@ export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
className="tn-tool-button"
noIconActionClass
titlePosition="top"
/>
/>;
}
function useSplitOrientation() {

View File

@@ -3,6 +3,7 @@ import "./becca/becca_loader.js";
import compression from "compression";
import cookieParser from "cookie-parser";
import { renderFile } from "ejs";
import express from "express";
import { auth } from "express-openid-connect";
import helmet from "helmet";
@@ -34,8 +35,7 @@ export default async function buildApp() {
// view engine setup
app.set("views", path.join(assetsDir, "views"));
const ejs = await import("ejs");
app.engine("ejs", (filePath, options, callback) => ejs.renderFile(filePath, options, callback));
app.engine("ejs", (filePath, options, callback) => renderFile(filePath, options, callback));
app.set("view engine", "ejs");
app.use((req, res, next) => {