diff --git a/.editorconfig b/.editorconfig index c965ea8c0..cd301498e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,6 @@ root = true -[*.{js,ts}] +[*.{js,ts,.tsx}] charset = utf-8 end_of_line = lf indent_size = 4 diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets index 77b251a4a..c7255af76 100644 --- a/.vscode/snippets.code-snippets +++ b/.vscode/snippets.code-snippets @@ -20,5 +20,10 @@ "scope": "typescript", "prefix": "jqf", "body": ["private $${1:name}!: JQuery;"] + }, + "region": { + "scope": "css", + "prefix": "region", + "body": ["/* #region ${1:name} */\n$0\n/* #endregion */"] } } diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 4c750a544..ca4f9745f 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -31,16 +31,13 @@ import { StartupChecks } from "./startup_checks.js"; import type { CreateNoteOpts } from "../services/note_create.js"; import { ColumnComponent } from "tabulator-tables"; import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx"; +import type RootContainer from "../widgets/containers/root_container.js"; interface Layout { - getRootWidget: (appContext: AppContext) => RootWidget; + getRootWidget: (appContext: AppContext) => RootContainer; } -interface RootWidget extends Component { - render: () => JQuery; -} - -interface BeforeUploadListener extends Component { +export interface BeforeUploadListener extends Component { beforeUnloadEvent(): boolean; } @@ -85,7 +82,6 @@ export type CommandMappings = { focusTree: CommandData; focusOnTitle: CommandData; focusOnDetail: CommandData; - focusOnSearchDefinition: Required; searchNotes: CommandData & { searchString?: string; ancestorNoteId?: string | null; @@ -323,6 +319,7 @@ export type CommandMappings = { printActiveNote: CommandData; exportAsPdf: CommandData; openNoteExternally: CommandData; + openNoteCustom: CommandData; renderActiveNote: CommandData; unhoist: CommandData; reloadFrontendApp: CommandData; @@ -526,7 +523,7 @@ export type FilteredCommandNames = keyof Pick[]; + beforeUnloadListeners: (WeakRef | (() => boolean))[]; tabManager!: TabManager; layout?: Layout; noteTreeWidget?: NoteTreeWidget; @@ -619,7 +616,7 @@ export class AppContext extends Component { component.triggerCommand(commandName, { $el: $(this) }); }); - this.child(rootWidget); + this.child(rootWidget as Component); this.triggerEvent("initialRenderComplete", {}); } @@ -649,13 +646,17 @@ export class AppContext extends Component { return $(el).closest(".component").prop("component"); } - addBeforeUnloadListener(obj: BeforeUploadListener) { + addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) { if (typeof WeakRef !== "function") { // older browsers don't support WeakRef return; } - this.beforeUnloadListeners.push(new WeakRef(obj)); + if (typeof obj === "object") { + this.beforeUnloadListeners.push(new WeakRef(obj)); + } else { + this.beforeUnloadListeners.push(obj); + } } } @@ -665,25 +666,29 @@ const appContext = new AppContext(window.glob.isMainWindow); $(window).on("beforeunload", () => { let allSaved = true; - appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref()); + appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => typeof wr === "function" || !!wr.deref()); - for (const weakRef of appContext.beforeUnloadListeners) { - const component = weakRef.deref(); + for (const listener of appContext.beforeUnloadListeners) { + if (typeof listener === "object") { + const component = listener.deref(); - if (!component) { - continue; - } + if (!component) { + continue; + } - if (!component.beforeUnloadEvent()) { - console.log(`Component ${component.componentId} is not finished saving its state.`); - - toast.showMessage(t("app_context.please_wait_for_save"), 10000); - - allSaved = false; + if (!component.beforeUnloadEvent()) { + console.log(`Component ${component.componentId} is not finished saving its state.`); + allSaved = false; + } + } else { + if (!listener()) { + allSaved = false; + } } } if (!allSaved) { + toast.showMessage(t("app_context.please_wait_for_save"), 10000); return "some string"; } }); diff --git a/apps/client/src/components/component.ts b/apps/client/src/components/component.ts index 8686a7bb9..9a59b96be 100644 --- a/apps/client/src/components/component.ts +++ b/apps/client/src/components/component.ts @@ -1,6 +1,8 @@ import utils from "../services/utils.js"; import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js"; +type EventHandler = ((data: any) => void); + /** * Abstract class for all components in the Trilium's frontend. * @@ -19,6 +21,7 @@ export class TypedComponent> { initialized: Promise | null; parent?: TypedComponent; _position!: number; + private listeners: Record | null = {}; constructor() { this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`; @@ -76,6 +79,14 @@ export class TypedComponent> { handleEventInChildren(name: T, data: EventData): Promise | null { const promises: Promise[] = []; + // Handle React children. + if (this.listeners?.[name]) { + for (const listener of this.listeners[name]) { + listener(data); + } + } + + // Handle legacy children. for (const child of this.children) { const ret = child.handleEvent(name, data) as Promise; @@ -120,6 +131,35 @@ export class TypedComponent> { return promise; } + + registerHandler(name: T, handler: EventHandler) { + if (!this.listeners) { + this.listeners = {}; + } + + if (!this.listeners[name]) { + this.listeners[name] = []; + } + + if (this.listeners[name].includes(handler)) { + return; + } + + this.listeners[name].push(handler); + } + + removeHandler(name: T, handler: EventHandler) { + if (!this.listeners?.[name]?.includes(handler)) { + return; + } + + this.listeners[name] = this.listeners[name] + .filter(listener => listener !== handler); + + if (!this.listeners[name].length) { + delete this.listeners[name]; + } + } } export default class Component extends TypedComponent {} diff --git a/apps/client/src/components/root_command_executor.ts b/apps/client/src/components/root_command_executor.ts index 8e7df9494..632eb0a88 100644 --- a/apps/client/src/components/root_command_executor.ts +++ b/apps/client/src/components/root_command_executor.ts @@ -43,8 +43,6 @@ export default class RootCommandExecutor extends Component { const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, { activate: true }); - - appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId }); } async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) { diff --git a/apps/client/src/desktop.ts b/apps/client/src/desktop.ts index 2791f0577..57ff4084e 100644 --- a/apps/client/src/desktop.ts +++ b/apps/client/src/desktop.ts @@ -8,7 +8,6 @@ import electronContextMenu from "./menus/electron_context_menu.js"; import glob from "./services/glob.js"; import { t } from "./services/i18n.js"; import options from "./services/options.js"; -import server from "./services/server.js"; import type ElectronRemote from "@electron/remote"; import type Electron from "electron"; import "./stylesheets/bootstrap.scss"; diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index 6099cba35..39015cb24 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -1020,6 +1020,14 @@ class FNote { return this.noteId.startsWith("_options"); } + isTriliumSqlite() { + return this.mime === "text/x-sqlite;schema=trilium"; + } + + isTriliumScript() { + return this.mime.startsWith("application/javascript"); + } + /** * Provides note's date metadata. */ diff --git a/apps/client/src/layouts/desktop_layout.ts b/apps/client/src/layouts/desktop_layout.tsx similarity index 77% rename from apps/client/src/layouts/desktop_layout.ts rename to apps/client/src/layouts/desktop_layout.tsx index 33c1135c8..fece6efd0 100644 --- a/apps/client/src/layouts/desktop_layout.ts +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -4,21 +4,13 @@ import TabRowWidget from "../widgets/tab_row.js"; import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js"; import LeftPaneContainer from "../widgets/containers/left_pane_container.js"; import NoteTreeWidget from "../widgets/note_tree.js"; -import NoteTitleWidget from "../widgets/note_title.js"; -import OwnedAttributeListWidget from "../widgets/ribbon_widgets/owned_attribute_list.js"; -import NoteActionsWidget from "../widgets/buttons/note_actions.js"; +import NoteTitleWidget from "../widgets/note_title.jsx"; import NoteDetailWidget from "../widgets/note_detail.js"; -import RibbonContainer from "../widgets/containers/ribbon_container.js"; -import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; -import InheritedAttributesWidget from "../widgets/ribbon_widgets/inherited_attribute_list.js"; +import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; import NoteListWidget from "../widgets/note_list.js"; -import SearchDefinitionWidget from "../widgets/ribbon_widgets/search_definition.js"; import SqlResultWidget from "../widgets/sql_result.js"; import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js"; -import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js"; -import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js"; -import NotePropertiesWidget from "../widgets/ribbon_widgets/note_properties.js"; -import NoteIconWidget from "../widgets/note_icon.js"; +import NoteIconWidget from "../widgets/note_icon.jsx"; import SearchResultWidget from "../widgets/search_result.js"; import ScrollingContainer from "../widgets/containers/scrolling_container.js"; import RootContainer from "../widgets/containers/root_container.js"; @@ -29,15 +21,8 @@ import SplitNoteContainer from "../widgets/containers/split_note_container.js"; import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js"; import CreatePaneButton from "../widgets/buttons/create_pane_button.js"; import ClosePaneButton from "../widgets/buttons/close_pane_button.js"; -import BasicPropertiesWidget from "../widgets/ribbon_widgets/basic_properties.js"; -import NoteInfoWidget from "../widgets/ribbon_widgets/note_info_widget.js"; -import BookPropertiesWidget from "../widgets/ribbon_widgets/book_properties.js"; -import NoteMapRibbonWidget from "../widgets/ribbon_widgets/note_map.js"; -import NotePathsWidget from "../widgets/ribbon_widgets/note_paths.js"; -import SimilarNotesWidget from "../widgets/ribbon_widgets/similar_notes.js"; import RightPaneContainer from "../widgets/containers/right_pane_container.js"; import EditButton from "../widgets/floating_buttons/edit_button.js"; -import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js"; import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js"; import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js"; import NoteWrapperWidget from "../widgets/note_wrapper.js"; @@ -51,16 +36,13 @@ import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js"; import LauncherContainer from "../widgets/containers/launcher_container.js"; -import RevisionsButton from "../widgets/buttons/revisions_button.js"; import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js"; import ApiLogWidget from "../widgets/api_log.js"; import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js"; -import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js"; import MovePaneButton from "../widgets/buttons/move_pane_button.js"; import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js"; import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js"; import ScrollPaddingWidget from "../widgets/scroll_padding.js"; -import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import options from "../services/options.js"; import utils from "../services/utils.js"; import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; @@ -73,6 +55,7 @@ import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_b import PngExportButton from "../widgets/floating_buttons/png_export_button.js"; import RefreshButton from "../widgets/floating_buttons/refresh_button.js"; import { applyModals } from "./layout_commons.js"; +import Ribbon from "../widgets/ribbon/Ribbon.jsx"; export default class DesktopLayout { @@ -151,37 +134,15 @@ export default class DesktopLayout { .css("min-height", "50px") .css("align-items", "center") .cssBlock(".title-row > * { margin: 5px; }") - .child(new NoteIconWidget()) - .child(new NoteTitleWidget()) + .child() + .child() .child(new SpacerWidget(0, 1)) .child(new MovePaneButton(true)) .child(new MovePaneButton(false)) .child(new ClosePaneButton()) .child(new CreatePaneButton()) ) - .child( - new RibbonContainer() - // the order of the widgets matter. Some of these want to "activate" themselves - // when visible. When this happens to multiple of them, the first one "wins". - // promoted attributes should always win. - .ribbon(new ClassicEditorToolbar()) - .ribbon(new ScriptExecutorWidget()) - .ribbon(new SearchDefinitionWidget()) - .ribbon(new EditedNotesWidget()) - .ribbon(new BookPropertiesWidget()) - .ribbon(new NotePropertiesWidget()) - .ribbon(new FilePropertiesWidget()) - .ribbon(new ImagePropertiesWidget()) - .ribbon(new BasicPropertiesWidget()) - .ribbon(new OwnedAttributeListWidget()) - .ribbon(new InheritedAttributesWidget()) - .ribbon(new NotePathsWidget()) - .ribbon(new NoteMapRibbonWidget()) - .ribbon(new SimilarNotesWidget()) - .ribbon(new NoteInfoWidget()) - .button(new RevisionsButton()) - .button(new NoteActionsWidget()) - ) + .child() .child(new SharedInfoWidget()) .child(new WatchedFileUpdateStatusWidget()) .child( @@ -235,8 +196,8 @@ export default class DesktopLayout { .child(new CloseZenButton()) // Desktop-specific dialogs. - .child(new PasswordNoteSetDialog()) - .child(new UploadAttachmentsDialog()); + .child() + .child(); applyModals(rootContainer); return rootContainer; diff --git a/apps/client/src/layouts/layout_commons.ts b/apps/client/src/layouts/layout_commons.tsx similarity index 61% rename from apps/client/src/layouts/layout_commons.ts rename to apps/client/src/layouts/layout_commons.tsx index 5ee261317..02171db60 100644 --- a/apps/client/src/layouts/layout_commons.ts +++ b/apps/client/src/layouts/layout_commons.tsx @@ -24,48 +24,48 @@ import InfoDialog from "../widgets/dialogs/info.js"; import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js"; import PopupEditorDialog from "../widgets/dialogs/popup_editor.js"; import FlexContainer from "../widgets/containers/flex_container.js"; -import NoteIconWidget from "../widgets/note_icon.js"; -import NoteTitleWidget from "../widgets/note_title.js"; -import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; -import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; +import NoteIconWidget from "../widgets/note_icon"; +import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; import NoteDetailWidget from "../widgets/note_detail.js"; import NoteListWidget from "../widgets/note_list.js"; -import { CallToActionDialog } from "../widgets/dialogs/call_to_action.jsx"; +import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx"; +import NoteTitleWidget from "../widgets/note_title.jsx"; +import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js"; export function applyModals(rootContainer: RootContainer) { rootContainer - .child(new BulkActionsDialog()) - .child(new AboutDialog()) - .child(new HelpDialog()) - .child(new RecentChangesDialog()) - .child(new BranchPrefixDialog()) - .child(new SortChildNotesDialog()) - .child(new IncludeNoteDialog()) - .child(new NoteTypeChooserDialog()) - .child(new JumpToNoteDialog()) - .child(new AddLinkDialog()) - .child(new CloneToDialog()) - .child(new MoveToDialog()) - .child(new ImportDialog()) - .child(new ExportDialog()) - .child(new MarkdownImportDialog()) - .child(new ProtectedSessionPasswordDialog()) - .child(new RevisionsDialog()) - .child(new DeleteNotesDialog()) - .child(new InfoDialog()) - .child(new ConfirmDialog()) - .child(new PromptDialog()) - .child(new IncorrectCpuArchDialog()) + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() + .child() .child(new PopupEditorDialog() .child(new FlexContainer("row") .class("title-row") .css("align-items", "center") .cssBlock(".title-row > * { margin: 5px; }") - .child(new NoteIconWidget()) - .child(new NoteTitleWidget())) - .child(new ClassicEditorToolbar()) + .child() + .child()) + .child() .child(new PromotedAttributesWidget()) .child(new NoteDetailWidget()) .child(new NoteListWidget(true))) - .child(new CallToActionDialog()); + .child(); } diff --git a/apps/client/src/layouts/mobile_layout.ts b/apps/client/src/layouts/mobile_layout.tsx similarity index 90% rename from apps/client/src/layouts/mobile_layout.ts rename to apps/client/src/layouts/mobile_layout.tsx index 10b6d2ebe..4d0fb4143 100644 --- a/apps/client/src/layouts/mobile_layout.ts +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -7,7 +7,6 @@ import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_ import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js"; import ScreenContainer from "../widgets/mobile_widgets/screen_container.js"; import ScrollingContainer from "../widgets/containers/scrolling_container.js"; -import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js"; import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; import EditButton from "../widgets/floating_buttons/edit_button.js"; import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; @@ -19,14 +18,18 @@ import GlobalMenuWidget from "../widgets/buttons/global_menu.js"; import LauncherContainer from "../widgets/containers/launcher_container.js"; import RootContainer from "../widgets/containers/root_container.js"; import SharedInfoWidget from "../widgets/shared_info.js"; -import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; +import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js"; import type AppContext from "../components/app_context.js"; import TabRowWidget from "../widgets/tab_row.js"; import RefreshButton from "../widgets/floating_buttons/refresh_button.js"; -import MobileEditorToolbar from "../widgets/ribbon_widgets/mobile_editor_toolbar.js"; +import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js"; import { applyModals } from "./layout_commons.js"; import CloseZenButton from "../widgets/close_zen_button.js"; +import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx"; +import { useNoteContext } from "../widgets/react/hooks.jsx"; +import { useContext } from "preact/hooks"; +import { ParentComponent } from "../widgets/react/react_utils.jsx"; const MOBILE_CSS = ` - -
- -
-
- - - -`; - -const mentionSetup: MentionFeed[] = [ - { - marker: "@", - feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), - itemRenderer: (_item) => { - const item = _item as Suggestion; - const itemElement = document.createElement("button"); - - itemElement.innerHTML = `${item.highlightedNotePathTitle} `; - - return itemElement; - }, - minimumCharacters: 0 - }, - { - marker: "#", - feed: async (queryText) => { - const names = await server.get(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`); - - return names.map((name) => { - return { - id: `#${name}`, - name: name - }; - }); - }, - minimumCharacters: 0 - }, - { - marker: "~", - feed: async (queryText) => { - const names = await server.get(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`); - - return names.map((name) => { - return { - id: `~${name}`, - name: name - }; - }); - }, - minimumCharacters: 0 - } -]; - -const editorConfig: EditorConfig = { - toolbar: { - items: [] - }, - placeholder: t("attribute_editor.placeholder"), - mention: { - feeds: mentionSetup - }, - licenseKey: "GPL" -}; - -type AttributeCommandNames = FilteredCommandNames; - -export default class AttributeEditorWidget extends NoteContextAwareWidget implements EventListener<"entitiesReloaded">, EventListener<"addNewLabel">, EventListener<"addNewRelation"> { - private attributeDetailWidget: AttributeDetailWidget; - private $editor!: JQuery; - private $addNewAttributeButton!: JQuery; - private $saveAttributesButton!: JQuery; - private $errors!: JQuery; - - private textEditor!: AttributeEditor; - private lastUpdatedNoteId!: string | undefined; - private lastSavedContent!: string; - - constructor(attributeDetailWidget: AttributeDetailWidget) { - super(); - - this.attributeDetailWidget = attributeDetailWidget; - } - - doRender() { - this.$widget = $(TPL); - this.$editor = this.$widget.find(".attribute-list-editor"); - - this.initialized = this.initEditor(); - - this.$editor.on("keydown", async (e) => { - if (e.which === 13) { - // allow autocomplete to fill the result textarea - setTimeout(() => this.save(), 100); - } - - this.attributeDetailWidget.hide(); - }); - - this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160 - - this.$addNewAttributeButton = this.$widget.find(".add-new-attribute-button"); - this.$addNewAttributeButton.on("click", (e) => this.addNewAttribute(e)); - - this.$saveAttributesButton = this.$widget.find(".save-attributes-button"); - this.$saveAttributesButton.on("click", () => this.save()); - - this.$errors = this.$widget.find(".attribute-errors"); - } - - addNewAttribute(e: JQuery.ClickEvent) { - contextMenuService.show({ - x: e.pageX, - y: e.pageY, - orientation: "left", - items: [ - { title: t("attribute_editor.add_new_label"), command: "addNewLabel", uiIcon: "bx bx-hash" }, - { title: t("attribute_editor.add_new_relation"), command: "addNewRelation", uiIcon: "bx bx-transfer" }, - { title: "----" }, - { title: t("attribute_editor.add_new_label_definition"), command: "addNewLabelDefinition", uiIcon: "bx bx-empty" }, - { title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" } - ], - selectMenuItemHandler: ({ command }) => this.handleAddNewAttributeCommand(command) - }); - // Prevent automatic hiding of the context menu due to the button being clicked. - e.stopPropagation(); - } - - // triggered from keyboard shortcut - async addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) { - if (this.isNoteContext(ntxId)) { - await this.refresh(); - - this.handleAddNewAttributeCommand("addNewLabel"); - } - } - - // triggered from keyboard shortcut - async addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) { - if (this.isNoteContext(ntxId)) { - await this.refresh(); - - this.handleAddNewAttributeCommand("addNewRelation"); - } - } - - async handleAddNewAttributeCommand(command: AttributeCommandNames | undefined) { - // TODO: Not sure what the relation between FAttribute[] and Attribute[] is. - const attrs = this.parseAttributes() as FAttribute[]; - - if (!attrs) { - return; - } - - let type: AttributeType; - let name; - let value; - - if (command === "addNewLabel") { - type = "label"; - name = "myLabel"; - value = ""; - } else if (command === "addNewRelation") { - type = "relation"; - name = "myRelation"; - value = ""; - } else if (command === "addNewLabelDefinition") { - type = "label"; - name = "label:myLabel"; - value = "promoted,single,text"; - } else if (command === "addNewRelationDefinition") { - type = "label"; - name = "relation:myRelation"; - value = "promoted,single"; - } else { - return; - } - - // TODO: Incomplete type - //@ts-ignore - attrs.push({ - type, - name, - value, - isInheritable: false - }); - - await this.renderOwnedAttributes(attrs, false); - - this.$editor.scrollTop(this.$editor[0].scrollHeight); - - const rect = this.$editor[0].getBoundingClientRect(); - - setTimeout(() => { - // showing a little bit later because there's a conflict with outside click closing the attr detail - this.attributeDetailWidget.showAttributeDetail({ - allAttributes: attrs, - attribute: attrs[attrs.length - 1], - isOwned: true, - x: (rect.left + rect.right) / 2, - y: rect.bottom, - focus: "name" - }); - }, 100); - } - - async save() { - if (this.lastUpdatedNoteId !== this.noteId) { - // https://github.com/zadam/trilium/issues/3090 - console.warn("Ignoring blur event because a different note is loaded."); - return; - } - - const attributes = this.parseAttributes(); - - if (attributes) { - await server.put(`notes/${this.noteId}/attributes`, attributes, this.componentId); - - this.$saveAttributesButton.fadeOut(); - - // blink the attribute text to give a visual hint that save has been executed - this.$editor.css("opacity", 0); - - // revert back - setTimeout(() => this.$editor.css("opacity", 1), 100); - } - } - - parseAttributes() { - try { - return attributeParser.lexAndParse(this.getPreprocessedData()); - } catch (e: any) { - this.$errors.text(e.message).slideDown(); - } - } - - getPreprocessedData() { - const str = this.textEditor - .getData() - .replace(/]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1") - .replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode - - return $("
").html(str).text(); - } - - async initEditor() { - this.$widget.show(); - - this.$editor.on("click", (e) => this.handleEditorClick(e)); - - this.textEditor = await AttributeEditor.create(this.$editor[0], editorConfig); - this.textEditor.model.document.on("change:data", () => this.dataChanged()); - this.textEditor.editing.view.document.on( - "enter", - (event, data) => { - // disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422 - data.preventDefault(); - event.stop(); - }, - { priority: "high" } - ); - - // disable spellcheck for attribute editor - const documentRoot = this.textEditor.editing.view.document.getRoot(); - if (documentRoot) { - this.textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", documentRoot)); - } - } - - dataChanged() { - this.lastUpdatedNoteId = this.noteId; - - if (this.lastSavedContent === this.textEditor.getData()) { - this.$saveAttributesButton.fadeOut(); - } else { - this.$saveAttributesButton.fadeIn(); - } - - if (this.$errors.is(":visible")) { - // using .hide() instead of .slideUp() since this will also hide the error after confirming - // mention for relation name which suits up. When using.slideUp() error will appear and the slideUp which is weird - this.$errors.hide(); - } - } - - async handleEditorClick(e: JQuery.ClickEvent) { - const pos = this.textEditor.model.document.selection.getFirstPosition(); - - if (pos && pos.textNode && pos.textNode.data) { - const clickIndex = this.getClickIndex(pos); - - let parsedAttrs; - - try { - parsedAttrs = attributeParser.lexAndParse(this.getPreprocessedData(), true); - } catch (e) { - // the input is incorrect because the user messed up with it and now needs to fix it manually - return null; - } - - let matchedAttr: Attribute | null = null; - - for (const attr of parsedAttrs) { - if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) { - matchedAttr = attr; - break; - } - } - - setTimeout(() => { - if (matchedAttr) { - this.$editor.tooltip("hide"); - - this.attributeDetailWidget.showAttributeDetail({ - allAttributes: parsedAttrs, - attribute: matchedAttr, - isOwned: true, - x: e.pageX, - y: e.pageY - }); - } else { - this.showHelpTooltip(); - } - }, 100); - } else { - this.showHelpTooltip(); - } - } - - showHelpTooltip() { - this.attributeDetailWidget.hide(); - - this.$editor.tooltip({ - trigger: "focus", - html: true, - title: HELP_TEXT, - placement: "bottom", - offset: "0,30" - }); - - this.$editor.tooltip("show"); - } - - getClickIndex(pos: ModelPosition) { - let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0); - - let curNode: ModelNode | Text | ModelElement | null = pos.textNode; - - while (curNode?.previousSibling) { - curNode = curNode.previousSibling; - - if ((curNode as ModelElement).name === "reference") { - clickIndex += (curNode.getAttribute("href") as string).length + 1; - } else if ("data" in curNode) { - clickIndex += (curNode.data as string).length; - } - } - - return clickIndex; - } - - async loadReferenceLinkTitle($el: JQuery, href: string) { - const { noteId } = linkService.parseNavigationStateFromUrl(href); - const note = noteId ? await froca.getNote(noteId, true) : null; - const title = note ? note.title : "[missing]"; - - $el.text(title); - } - - async refreshWithNote(note: FNote) { - await this.renderOwnedAttributes(note.getOwnedAttributes(), true); - } - - async renderOwnedAttributes(ownedAttributes: FAttribute[], saved: boolean) { - // attrs are not resorted if position changes after the initial load - ownedAttributes.sort((a, b) => a.position - b.position); - - let htmlAttrs = (await attributeRenderer.renderAttributes(ownedAttributes, true)).html(); - - if (htmlAttrs.length > 0) { - htmlAttrs += " "; - } - - this.textEditor.setData(htmlAttrs); - - if (saved) { - this.lastSavedContent = this.textEditor.getData(); - - this.$saveAttributesButton.fadeOut(0); - } - } - - async createNoteForReferenceLink(title: string) { - let result; - if (this.notePath) { - result = await noteCreateService.createNoteWithTypePrompt(this.notePath, { - activate: false, - title: title - }); - } - - return result?.note?.getBestNotePathString(); - } - - async updateAttributeList(attributes: FAttribute[]) { - await this.renderOwnedAttributes(attributes, false); - } - - focus() { - this.$editor.trigger("focus"); - - this.textEditor.model.change((writer) => { - const documentRoot = this.textEditor.editing.model.document.getRoot(); - if (!documentRoot) { - return; - } - - const positionAt = writer.createPositionAt(documentRoot, "end"); - writer.setSelection(positionAt); - }); - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) { - this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/basic_widget.ts b/apps/client/src/widgets/basic_widget.ts index be7b0bbd7..f49f2382c 100644 --- a/apps/client/src/widgets/basic_widget.ts +++ b/apps/client/src/widgets/basic_widget.ts @@ -1,7 +1,11 @@ +import { isValidElement, VNode } from "preact"; import Component, { TypedComponent } from "../components/component.js"; import froca from "../services/froca.js"; import { t } from "../services/i18n.js"; import toastService from "../services/toast.js"; +import { renderReactWidget } from "./react/react_utils.jsx"; +import { EventNames, EventData } from "../components/app_context.js"; +import { Handler } from "leaflet"; export class TypedBasicWidget> extends TypedComponent { protected attrs: Record; @@ -22,11 +26,14 @@ export class TypedBasicWidget> extends TypedCompon this.childPositionCounter = 10; } - child(...components: T[]) { - if (!components) { + child(..._components: (T | VNode)[]) { + if (!_components) { return this; } + // Convert any React components to legacy wrapped components. + const components = wrapReactWidgets(_components); + super.child(...components); for (const component of components) { @@ -258,3 +265,30 @@ export class TypedBasicWidget> extends TypedCompon * For information on using widgets, see the tutorial {@tutorial widget_basics}. */ export default class BasicWidget extends TypedBasicWidget {} + +export function wrapReactWidgets>(components: (T | VNode)[]) { + const wrappedResult: T[] = []; + for (const component of components) { + if (isValidElement(component)) { + wrappedResult.push(new ReactWrappedWidget(component) as unknown as T); + } else { + wrappedResult.push(component); + } + } + return wrappedResult; +} + +export class ReactWrappedWidget extends BasicWidget { + + private el: VNode; + + constructor(el: VNode) { + super(); + this.el = el; + } + + doRender() { + this.$widget = renderReactWidget(this, this.el); + } + +} diff --git a/apps/client/src/widgets/bookmark_switch.ts b/apps/client/src/widgets/bookmark_switch.ts deleted file mode 100644 index 93d4789aa..000000000 --- a/apps/client/src/widgets/bookmark_switch.ts +++ /dev/null @@ -1,54 +0,0 @@ -import SwitchWidget from "./switch.js"; -import server from "../services/server.js"; -import toastService from "../services/toast.js"; -import { t } from "../services/i18n.js"; -import type FNote from "../entities/fnote.js"; -import type { EventData } from "../components/app_context.js"; - -// TODO: Deduplicate -type Response = { - success: true; -} | { - success: false; - message: string; -} - -export default class BookmarkSwitchWidget extends SwitchWidget { - isEnabled() { - return ( - super.isEnabled() && - // it's not possible to bookmark root because that would clone it under bookmarks and thus create a cycle - !["root", "_hidden"].includes(this.noteId ?? "") - ); - } - - doRender() { - super.doRender(); - - this.switchOnName = t("bookmark_switch.bookmark"); - this.switchOnTooltip = t("bookmark_switch.bookmark_this_note"); - - this.switchOffName = t("bookmark_switch.bookmark"); - this.switchOffTooltip = t("bookmark_switch.remove_bookmark"); - } - - async toggle(state: boolean | null | undefined) { - const resp = await server.put(`notes/${this.noteId}/toggle-in-parent/_lbBookmarks/${!!state}`); - - if (!resp.success && "message" in resp) { - toastService.showError(resp.message); - } - } - - async refreshWithNote(note: FNote) { - const isBookmarked = !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks"); - - this.isToggled = isBookmarked; - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.getBranchRows().find((b) => b.noteId === this.noteId)) { - this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/bulk_actions/BulkAction.tsx b/apps/client/src/widgets/bulk_actions/BulkAction.tsx index 3389d59f5..8b562d0f0 100644 --- a/apps/client/src/widgets/bulk_actions/BulkAction.tsx +++ b/apps/client/src/widgets/bulk_actions/BulkAction.tsx @@ -1,6 +1,7 @@ import { ComponentChildren } from "preact"; import { memo } from "preact/compat"; import AbstractBulkAction from "./abstract_bulk_action"; +import HelpRemoveButtons from "../react/HelpRemoveButtons"; interface BulkActionProps { label: string | ComponentChildren; @@ -24,19 +25,11 @@ const BulkAction = memo(({ label, children, helpText, bulkAction }: BulkActionPr {children}
- - {helpText &&
- -
- {helpText} -
-
} - - bulkAction?.deleteAction()} - /> - + bulkAction?.deleteAction()} + /> ); }); diff --git a/apps/client/src/widgets/bulk_actions/note/move_note.tsx b/apps/client/src/widgets/bulk_actions/note/move_note.tsx index ac5829305..4290726ce 100644 --- a/apps/client/src/widgets/bulk_actions/note/move_note.tsx +++ b/apps/client/src/widgets/bulk_actions/note/move_note.tsx @@ -1,11 +1,11 @@ import { t } from "../../../services/i18n.js"; -import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js"; +import AbstractBulkAction from "../abstract_bulk_action.js"; import BulkAction, { BulkActionText } from "../BulkAction.jsx"; import NoteAutocomplete from "../../react/NoteAutocomplete.jsx"; import { useEffect, useState } from "preact/hooks"; import { useSpacedUpdate } from "../../react/hooks.jsx"; -function MoveNoteBulkActionComponent({ bulkAction, actionDef }: { bulkAction: AbstractBulkAction, actionDef: ActionDefinition }) { +function MoveNoteBulkActionComponent({ bulkAction }: { bulkAction: AbstractBulkAction }) { const [ targetParentNoteId, setTargetParentNoteId ] = useState(); const spacedUpdate = useSpacedUpdate(() => { return bulkAction.saveAction({ targetParentNoteId: targetParentNoteId }) @@ -45,6 +45,6 @@ export default class MoveNoteBulkAction extends AbstractBulkAction { } doRender() { - return + return } } diff --git a/apps/client/src/widgets/bulk_actions/note/rename_note.tsx b/apps/client/src/widgets/bulk_actions/note/rename_note.tsx index 5fe9b8912..682494cd2 100644 --- a/apps/client/src/widgets/bulk_actions/note/rename_note.tsx +++ b/apps/client/src/widgets/bulk_actions/note/rename_note.tsx @@ -1,4 +1,3 @@ -import SpacedUpdate from "../../../services/spaced_update.js"; import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js"; import { t } from "../../../services/i18n.js"; import BulkAction from "../BulkAction.jsx"; diff --git a/apps/client/src/widgets/bulk_actions/relation/add_relation.tsx b/apps/client/src/widgets/bulk_actions/relation/add_relation.tsx index 5e10933a8..59d222ee8 100644 --- a/apps/client/src/widgets/bulk_actions/relation/add_relation.tsx +++ b/apps/client/src/widgets/bulk_actions/relation/add_relation.tsx @@ -1,6 +1,4 @@ -import SpacedUpdate from "../../../services/spaced_update.js"; import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action.js"; -import noteAutocompleteService from "../../../services/note_autocomplete.js"; import { t } from "../../../services/i18n.js"; import BulkAction, { BulkActionText } from "../BulkAction.jsx"; import NoteAutocomplete from "../../react/NoteAutocomplete.jsx"; diff --git a/apps/client/src/widgets/buttons/note_actions.ts b/apps/client/src/widgets/buttons/note_actions.ts deleted file mode 100644 index 069253bfe..000000000 --- a/apps/client/src/widgets/buttons/note_actions.ts +++ /dev/null @@ -1,252 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import utils from "../../services/utils.js"; -import branchService from "../../services/branches.js"; -import dialogService from "../../services/dialog.js"; -import server from "../../services/server.js"; -import toastService from "../../services/toast.js"; -import ws from "../../services/ws.js"; -import appContext, { type EventData } from "../../components/app_context.js"; -import { t } from "../../services/i18n.js"; -import type FNote from "../../entities/fnote.js"; -import type { FAttachmentRow } from "../../entities/fattachment.js"; - -// TODO: Deduplicate with server -interface ConvertToAttachmentResponse { - attachment: FAttachmentRow; -} - -const TPL = /*html*/` -`; - -export default class NoteActionsWidget extends NoteContextAwareWidget { - - private $convertNoteIntoAttachmentButton!: JQuery; - private $findInTextButton!: JQuery; - private $printActiveNoteButton!: JQuery; - private $exportAsPdfButton!: JQuery; - private $showSourceButton!: JQuery; - private $showAttachmentsButton!: JQuery; - private $renderNoteButton!: JQuery; - private $saveRevisionButton!: JQuery; - private $exportNoteButton!: JQuery; - private $importNoteButton!: JQuery; - private $openNoteExternallyButton!: JQuery; - private $openNoteCustomButton!: JQuery; - private $deleteNoteButton!: JQuery; - - isEnabled() { - return this.note?.type !== "launcher"; - } - - doRender() { - this.$widget = $(TPL); - this.$widget.on("show.bs.dropdown", () => { - if (this.note) { - this.refreshVisibility(this.note); - } - }); - - this.$convertNoteIntoAttachmentButton = this.$widget.find("[data-trigger-command='convertNoteIntoAttachment']"); - this.$findInTextButton = this.$widget.find(".find-in-text-button"); - this.$printActiveNoteButton = this.$widget.find(".print-active-note-button"); - this.$exportAsPdfButton = this.$widget.find(".export-as-pdf-button"); - this.$showSourceButton = this.$widget.find(".show-source-button"); - this.$showAttachmentsButton = this.$widget.find(".show-attachments-button"); - this.$renderNoteButton = this.$widget.find(".render-note-button"); - this.$saveRevisionButton = this.$widget.find(".save-revision-button"); - - this.$exportNoteButton = this.$widget.find(".export-note-button"); - this.$exportNoteButton.on("click", () => { - if (this.$exportNoteButton.hasClass("disabled") || !this.noteContext?.notePath) { - return; - } - - this.triggerCommand("showExportDialog", { - notePath: this.noteContext.notePath, - defaultType: "single" - }); - }); - - this.$importNoteButton = this.$widget.find(".import-files-button"); - this.$importNoteButton.on("click", () => { - if (this.noteId) { - this.triggerCommand("showImportDialog", { noteId: this.noteId }); - } - }); - - this.$widget.on("click", ".dropdown-item", () => this.$widget.find("[data-bs-toggle='dropdown']").dropdown("toggle")); - - this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button"); - this.$openNoteCustomButton = this.$widget.find(".open-note-custom-button"); - - this.$deleteNoteButton = this.$widget.find(".delete-note-button"); - this.$deleteNoteButton.on("click", () => { - if (!this.note || this.note.noteId === "root") { - return; - } - - branchService.deleteNotes([this.note.getParentBranches()[0].branchId], true); - }); - } - - async refreshVisibility(note: FNote) { - const isInOptions = note.noteId.startsWith("_options"); - - this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment()); - - this.toggleDisabled(this.$findInTextButton, ["text", "code", "book", "mindMap", "doc"].includes(note.type)); - - this.toggleDisabled(this.$showAttachmentsButton, !isInOptions); - this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type)); - - const canPrint = ["text", "code"].includes(note.type); - this.toggleDisabled(this.$printActiveNoteButton, canPrint); - this.toggleDisabled(this.$exportAsPdfButton, canPrint); - this.$exportAsPdfButton.toggleClass("hidden-ext", !utils.isElectron()); - - this.$renderNoteButton.toggle(note.type === "render"); - - this.toggleDisabled(this.$openNoteExternallyButton, utils.isElectron() && !["search", "book"].includes(note.type)); - this.toggleDisabled( - this.$openNoteCustomButton, - utils.isElectron() && - !utils.isMac() && // no implementation for Mac yet - !["search", "book"].includes(note.type) - ); - - // I don't want to handle all special notes like this, but intuitively user might want to export content of backend log - this.toggleDisabled(this.$exportNoteButton, !["_backendLog"].includes(note.noteId) && !isInOptions); - - this.toggleDisabled(this.$importNoteButton, !["search"].includes(note.type) && !isInOptions); - this.toggleDisabled(this.$deleteNoteButton, !isInOptions); - this.toggleDisabled(this.$saveRevisionButton, !isInOptions); - } - - async convertNoteIntoAttachmentCommand() { - if (!this.note || !(await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title })))) { - return; - } - - const { attachment: newAttachment } = await server.post(`notes/${this.noteId}/convert-to-attachment`); - - if (!newAttachment) { - toastService.showMessage(t("note_actions.convert_into_attachment_failed", { title: this.note.title })); - return; - } - - toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title })); - await ws.waitForMaxKnownEntityChangeId(); - await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, { - viewScope: { - viewMode: "attachments", - attachmentId: newAttachment.attachmentId - } - }); - } - - toggleDisabled($el: JQuery, enable: boolean) { - if (enable) { - $el.removeAttr("disabled"); - } else { - $el.attr("disabled", "disabled"); - } - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.isNoteReloaded(this.noteId)) { - this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/buttons/revisions_button.ts b/apps/client/src/widgets/buttons/revisions_button.ts deleted file mode 100644 index 089c6a4c6..000000000 --- a/apps/client/src/widgets/buttons/revisions_button.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { t } from "../../services/i18n.js"; -import CommandButtonWidget from "./command_button.js"; - -export default class RevisionsButton extends CommandButtonWidget { - constructor() { - super(); - - this.icon("bx-history").title(t("revisions_button.note_revisions")).command("showRevisions").titlePlacement("bottom").class("icon-action"); - } - - isEnabled() { - return super.isEnabled() && !["launcher", "doc"].includes(this.note?.type ?? ""); - } -} diff --git a/apps/client/src/widgets/containers/ribbon_container.ts b/apps/client/src/widgets/containers/ribbon_container.ts deleted file mode 100644 index 9aee7bb67..000000000 --- a/apps/client/src/widgets/containers/ribbon_container.ts +++ /dev/null @@ -1,388 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import keyboardActionsService from "../../services/keyboard_actions.js"; -import attributeService from "../../services/attributes.js"; -import type CommandButtonWidget from "../buttons/command_button.js"; -import type FNote from "../../entities/fnote.js"; -import type { NoteType } from "../../entities/fnote.js"; -import type { EventData, EventNames } from "../../components/app_context.js"; -import type NoteActionsWidget from "../buttons/note_actions.js"; - -const TPL = /*html*/` -
- - -
-
-
-
- -
-
`; - -type ButtonWidget = (CommandButtonWidget | NoteActionsWidget); - -export default class RibbonContainer extends NoteContextAwareWidget { - - private lastActiveComponentId?: string | null; - private lastNoteType?: NoteType; - - private ribbonWidgets: NoteContextAwareWidget[]; - private buttonWidgets: ButtonWidget[]; - private $tabContainer!: JQuery; - private $buttonContainer!: JQuery; - private $bodyContainer!: JQuery; - - constructor() { - super(); - - this.contentSized(); - this.ribbonWidgets = []; - this.buttonWidgets = []; - } - - isEnabled() { - return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default"; - } - - ribbon(widget: NoteContextAwareWidget) { - // TODO: Base class - super.child(widget); - - this.ribbonWidgets.push(widget); - - return this; - } - - button(widget: ButtonWidget) { - super.child(widget); - - this.buttonWidgets.push(widget); - - return this; - } - - doRender() { - this.$widget = $(TPL); - - this.$tabContainer = this.$widget.find(".ribbon-tab-container"); - this.$buttonContainer = this.$widget.find(".ribbon-button-container"); - this.$bodyContainer = this.$widget.find(".ribbon-body-container"); - - for (const ribbonWidget of this.ribbonWidgets) { - this.$bodyContainer.append($('
').attr("data-ribbon-component-id", ribbonWidget.componentId).append(ribbonWidget.render())); - } - - for (const buttonWidget of this.buttonWidgets) { - this.$buttonContainer.append(buttonWidget.render()); - } - - this.$tabContainer.on("click", ".ribbon-tab-title", (e) => { - const $ribbonTitle = $(e.target).closest(".ribbon-tab-title"); - - this.toggleRibbonTab($ribbonTitle); - }); - } - - toggleRibbonTab($ribbonTitle: JQuery, refreshActiveTab = true) { - const activate = !$ribbonTitle.hasClass("active"); - - this.$tabContainer.find(".ribbon-tab-title").removeClass("active"); - this.$bodyContainer.find(".ribbon-body").removeClass("active"); - - if (activate) { - const ribbonComponendId = $ribbonTitle.attr("data-ribbon-component-id"); - - const wasAlreadyActive = this.lastActiveComponentId === ribbonComponendId; - - this.lastActiveComponentId = ribbonComponendId; - - this.$tabContainer.find(`.ribbon-tab-title[data-ribbon-component-id="${ribbonComponendId}"]`).addClass("active"); - this.$bodyContainer.find(`.ribbon-body[data-ribbon-component-id="${ribbonComponendId}"]`).addClass("active"); - - const activeChild = this.getActiveRibbonWidget(); - - if (activeChild && (refreshActiveTab || !wasAlreadyActive) && this.noteContext && this.notePath) { - const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath }); - - if (refreshActiveTab) { - if (handleEventPromise) { - handleEventPromise.then(() => (activeChild as any).focus?.()); // TODO: Base class - } else { - // TODO: Base class - (activeChild as any).focus?.(); - } - } - } - } else { - this.lastActiveComponentId = null; - } - } - - async noteSwitched() { - this.lastActiveComponentId = null; - - await super.noteSwitched(); - } - - async refreshWithNote(note: FNote, noExplicitActivation = false) { - this.lastNoteType = note.type; - - let $ribbonTabToActivate, $lastActiveRibbon; - - this.$tabContainer.empty(); - - for (const ribbonWidget of this.ribbonWidgets) { - // TODO: Base class for ribbon widget - const ret = await (ribbonWidget as any).getTitle(note); - - if (!ret.show) { - continue; - } - - const $ribbonTitle = $('
') - .attr("data-ribbon-component-id", ribbonWidget.componentId) - .attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets - .append( - $('') - .addClass(ret.icon) - .attr("title", ret.title) - .attr("data-toggle-command", (ribbonWidget as any).toggleCommand) - ) // TODO: base class - .append(" ") - .append($('').text(ret.title)); - - this.$tabContainer.append($ribbonTitle); - this.$tabContainer.append('
'); - - if (ret.activate && !this.lastActiveComponentId && !$ribbonTabToActivate && !noExplicitActivation) { - $ribbonTabToActivate = $ribbonTitle; - } - - if (this.lastActiveComponentId === ribbonWidget.componentId) { - $lastActiveRibbon = $ribbonTitle; - } - } - - keyboardActionsService.getActions().then((actions) => { - this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({ - title: () => { - const toggleCommandName = $(this).attr("data-toggle-command"); - const action = actions.find((act) => act.actionName === toggleCommandName); - const title = $(this).attr("data-title"); - - if (action?.effectiveShortcuts && action.effectiveShortcuts.length > 0) { - return `${title} (${action.effectiveShortcuts.join(", ")})`; - } else { - return title ?? ""; - } - } - }); - }); - - if (!$ribbonTabToActivate) { - $ribbonTabToActivate = $lastActiveRibbon; - } - - if ($ribbonTabToActivate) { - this.toggleRibbonTab($ribbonTabToActivate, false); - } else { - this.$bodyContainer.find(".ribbon-body").removeClass("active"); - } - } - - isRibbonTabActive(name: string) { - const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`); - - return $ribbonComponent.hasClass("active"); - } - - ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) { - if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) { - this.toggleRibbonTabWithName("ownedAttributes", ntxId); - } - } - - addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) { - this.ensureOwnedAttributesAreOpen(ntxId); - } - - addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) { - this.ensureOwnedAttributesAreOpen(ntxId); - } - - toggleRibbonTabWithName(name: string, ntxId?: string) { - if (!this.isNoteContext(ntxId)) { - return false; - } - - const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`); - - if ($ribbonComponent) { - this.toggleRibbonTab($ribbonComponent); - } - } - - handleEvent(name: T, data: EventData) { - const PREFIX = "toggleRibbonTab"; - - if (name.startsWith(PREFIX)) { - let componentName = name.substr(PREFIX.length); - componentName = componentName[0].toLowerCase() + componentName.substr(1); - - this.toggleRibbonTabWithName(componentName, (data as any).ntxId); - } else { - return super.handleEvent(name, data); - } - } - - async handleEventInChildren(name: T, data: EventData) { - if (["activeContextChanged", "setNoteContext"].includes(name)) { - // won't trigger .refresh(); - await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">); - } else if (this.isEnabled() || name === "initialRenderComplete") { - const activeRibbonWidget = this.getActiveRibbonWidget(); - - // forward events only to active ribbon tab, inactive ones don't need to be updated - if (activeRibbonWidget) { - await activeRibbonWidget.handleEvent(name, data); - } - - for (const buttonWidget of this.buttonWidgets) { - await buttonWidget.handleEvent(name, data); - } - } - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (!this.note) { - return; - } - - if (this.noteId && loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) { - // note type influences the list of available ribbon tabs the most - // check for the type is so that we don't update on each title rename - this.lastNoteType = this.note.type; - - this.refresh(); - } else if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) { - this.refreshWithNote(this.note, true); - } - } - - async noteTypeMimeChangedEvent() { - // We are ignoring the event which triggers a refresh since it is usually already done by a different - // event and causing a race condition in which the items appear twice. - } - - /** - * Executed as soon as the user presses the "Edit" floating button in a read-only text note. - * - *

- * We need to refresh the ribbon for cases such as the classic editor which relies on the read-only state. - */ - readOnlyTemporarilyDisabledEvent() { - this.refresh(); - } - - getActiveRibbonWidget() { - return this.ribbonWidgets.find((ch) => ch.componentId === this.lastActiveComponentId); - } -} diff --git a/apps/client/src/widgets/dialogs/about.tsx b/apps/client/src/widgets/dialogs/about.tsx index ceaad50ba..7fa9c2390 100644 --- a/apps/client/src/widgets/dialogs/about.tsx +++ b/apps/client/src/widgets/dialogs/about.tsx @@ -1,4 +1,3 @@ -import ReactBasicWidget from "../react/ReactBasicWidget.js"; import Modal from "../react/Modal.js"; import { t } from "../../services/i18n.js"; import { formatDateTime } from "../../utils/formatters.js"; @@ -8,11 +7,11 @@ import openService from "../../services/open.js"; import { useState } from "preact/hooks"; import type { CSSProperties } from "preact/compat"; import type { AppInfo } from "@triliumnext/commons"; -import useTriliumEvent from "../react/hooks.jsx"; +import { useTriliumEvent } from "../react/hooks.jsx"; -function AboutDialogComponent() { - let [appInfo, setAppInfo] = useState(null); - let [shown, setShown] = useState(false); +export default function AboutDialog() { + const [appInfo, setAppInfo] = useState(null); + const [shown, setShown] = useState(false); const forceWordBreak: CSSProperties = { wordBreak: "break-all" }; useTriliumEvent("openAboutDialog", () => setShown(true)); @@ -82,11 +81,3 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro return {directory}; } } - -export default class AboutDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/add_link.tsx b/apps/client/src/widgets/dialogs/add_link.tsx index 1b6828f9b..97440491d 100644 --- a/apps/client/src/widgets/dialogs/add_link.tsx +++ b/apps/client/src/widgets/dialogs/add_link.tsx @@ -1,6 +1,5 @@ import { t } from "../../services/i18n"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import Button from "../react/Button"; import FormRadioGroup from "../react/FormRadioGroup"; import NoteAutocomplete from "../react/NoteAutocomplete"; @@ -11,11 +10,11 @@ import { default as TextTypeWidget } from "../type_widgets/editable_text.js"; import { logError } from "../../services/ws"; import FormGroup from "../react/FormGroup.js"; import { refToJQuerySelector } from "../react/react_utils"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; type LinkType = "reference-link" | "external-link" | "hyper-link"; -function AddLinkDialogComponent() { +export default function AddLinkDialog() { const [ textTypeWidget, setTextTypeWidget ] = useState(); const initialText = useRef(); const [ linkTitle, setLinkTitle ] = useState(""); @@ -160,11 +159,3 @@ function AddLinkDialogComponent() { ); } - -export default class AddLinkDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} diff --git a/apps/client/src/widgets/dialogs/branch_prefix.tsx b/apps/client/src/widgets/dialogs/branch_prefix.tsx index f73feb125..f04280748 100644 --- a/apps/client/src/widgets/dialogs/branch_prefix.tsx +++ b/apps/client/src/widgets/dialogs/branch_prefix.tsx @@ -4,15 +4,14 @@ import { t } from "../../services/i18n.js"; import server from "../../services/server.js"; import toast from "../../services/toast.js"; import Modal from "../react/Modal.jsx"; -import ReactBasicWidget from "../react/ReactBasicWidget.js"; import froca from "../../services/froca.js"; import tree from "../../services/tree.js"; import Button from "../react/Button.jsx"; import FormGroup from "../react/FormGroup.js"; -import useTriliumEvent from "../react/hooks.jsx"; +import { useTriliumEvent } from "../react/hooks.jsx"; import FBranch from "../../entities/fbranch.js"; -function BranchPrefixDialogComponent() { +export default function BranchPrefixDialog() { const [ shown, setShown ] = useState(false); const [ branch, setBranch ] = useState(); const [ prefix, setPrefix ] = useState(branch?.prefix ?? ""); @@ -75,14 +74,6 @@ function BranchPrefixDialogComponent() { ); } -export default class BranchPrefixDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} - async function savePrefix(branchId: string, prefix: string) { await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix }); toast.showMessage(t("branch_prefix.branch_prefix_saved")); diff --git a/apps/client/src/widgets/dialogs/bulk_actions.tsx b/apps/client/src/widgets/dialogs/bulk_actions.tsx index 8d184433f..05033255c 100644 --- a/apps/client/src/widgets/dialogs/bulk_actions.tsx +++ b/apps/client/src/widgets/dialogs/bulk_actions.tsx @@ -1,7 +1,6 @@ import { useEffect, useState, useCallback } from "preact/hooks"; import { t } from "../../services/i18n"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import "./bulk_actions.css"; import { BulkActionAffectedNotes } from "@triliumnext/commons"; import server from "../../services/server"; @@ -12,9 +11,9 @@ import toast from "../../services/toast"; import RenameNoteBulkAction from "../bulk_actions/note/rename_note"; import FNote from "../../entities/fnote"; import froca from "../../services/froca"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function BulkActionComponent() { +export default function BulkActionsDialog() { const [ selectedOrActiveNoteIds, setSelectedOrActiveNoteIds ] = useState(); const [ bulkActionNote, setBulkActionNote ] = useState(); const [ includeDescendants, setIncludeDescendants ] = useState(false); @@ -51,7 +50,7 @@ function BulkActionComponent() { row.type === "label" && row.name === "action" && row.noteId === "_bulkAction")) { refreshExistingActions(); } - }, shown); + }); return ( ); } - -export default class BulkActionsDialog extends ReactBasicWidget { - - get component() { - return - } - -} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/call_to_action.tsx b/apps/client/src/widgets/dialogs/call_to_action.tsx index d493611e3..1ae4a14eb 100644 --- a/apps/client/src/widgets/dialogs/call_to_action.tsx +++ b/apps/client/src/widgets/dialogs/call_to_action.tsx @@ -1,15 +1,11 @@ -import { useState } from "preact/hooks"; +import { useMemo, useState } from "preact/hooks"; import Button from "../react/Button"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; -import { CallToAction, dismissCallToAction, getCallToActions } from "./call_to_action_definitions"; +import { dismissCallToAction, getCallToActions } from "./call_to_action_definitions"; import { t } from "../../services/i18n"; -function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActions: CallToAction[] }) { - if (!activeCallToActions.length) { - return <>; - } - +export default function CallToActionDialog() { + const activeCallToActions = useMemo(() => getCallToActions(), []); const [ activeIndex, setActiveIndex ] = useState(0); const [ shown, setShown ] = useState(true); const activeItem = activeCallToActions[activeIndex]; @@ -22,7 +18,7 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi } } - return ( + return (activeCallToActions.length && ) } - -export class CallToActionDialog extends ReactBasicWidget { - - get component() { - return - } - -} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/clone_to.tsx b/apps/client/src/widgets/dialogs/clone_to.tsx index 173169858..568dec63e 100644 --- a/apps/client/src/widgets/dialogs/clone_to.tsx +++ b/apps/client/src/widgets/dialogs/clone_to.tsx @@ -2,7 +2,6 @@ import { useRef, useState } from "preact/hooks"; import appContext from "../../components/app_context"; import { t } from "../../services/i18n"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import NoteAutocomplete from "../react/NoteAutocomplete"; import froca from "../../services/froca"; import FormGroup from "../react/FormGroup"; @@ -14,9 +13,9 @@ import tree from "../../services/tree"; import branches from "../../services/branches"; import toast from "../../services/toast"; import NoteList from "../react/NoteList"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function CloneToDialogComponent() { +export default function CloneToDialog() { const [ clonedNoteIds, setClonedNoteIds ] = useState(); const [ prefix, setPrefix ] = useState(""); const [ suggestion, setSuggestion ] = useState(null); @@ -83,14 +82,6 @@ function CloneToDialogComponent() { ) } -export default class CloneToDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} - async function cloneNotesTo(notePath: string, clonedNoteIds: string[], prefix?: string) { const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath); if (!noteId || !parentNoteId) { diff --git a/apps/client/src/widgets/dialogs/confirm.tsx b/apps/client/src/widgets/dialogs/confirm.tsx index a4ab173fd..456f11ad2 100644 --- a/apps/client/src/widgets/dialogs/confirm.tsx +++ b/apps/client/src/widgets/dialogs/confirm.tsx @@ -1,10 +1,9 @@ -import ReactBasicWidget from "../react/ReactBasicWidget"; import Modal from "../react/Modal"; import Button from "../react/Button"; import { t } from "../../services/i18n"; import { useState } from "preact/hooks"; import FormCheckbox from "../react/FormCheckbox"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; interface ConfirmDialogProps { title?: string; @@ -13,7 +12,7 @@ interface ConfirmDialogProps { isConfirmDeleteNoteBox?: boolean; } -function ConfirmDialogComponent() { +export default function ConfirmDialog() { const [ opts, setOpts ] = useState(); const [ isDeleteNoteChecked, setIsDeleteNoteChecked ] = useState(false); const [ shown, setShown ] = useState(false); @@ -92,11 +91,3 @@ export interface ConfirmWithTitleOptions { title: string; callback: ConfirmDialogCallback; } - -export default class ConfirmDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/delete_notes.tsx b/apps/client/src/widgets/dialogs/delete_notes.tsx index 54afbb752..b16fafd0a 100644 --- a/apps/client/src/widgets/dialogs/delete_notes.tsx +++ b/apps/client/src/widgets/dialogs/delete_notes.tsx @@ -2,7 +2,6 @@ import { useRef, useState, useEffect } from "preact/hooks"; import { t } from "../../services/i18n.js"; import FormCheckbox from "../react/FormCheckbox.js"; import Modal from "../react/Modal.js"; -import ReactBasicWidget from "../react/ReactBasicWidget.js"; import type { DeleteNotesPreview } from "@triliumnext/commons"; import server from "../../services/server.js"; import froca from "../../services/froca.js"; @@ -10,7 +9,7 @@ import FNote from "../../entities/fnote.js"; import link from "../../services/link.js"; import Button from "../react/Button.jsx"; import Alert from "../react/Alert.jsx"; -import useTriliumEvent from "../react/hooks.jsx"; +import { useTriliumEvent } from "../react/hooks.jsx"; export interface ResolveOptions { proceed: boolean; @@ -30,7 +29,7 @@ interface BrokenRelationData { source: string; } -function DeleteNotesDialogComponent() { +export default function DeleteNotesDialog() { const [ opts, setOpts ] = useState({}); const [ deleteAllClones, setDeleteAllClones ] = useState(false); const [ eraseNotes, setEraseNotes ] = useState(!!opts.forceDeleteAllClones); @@ -140,7 +139,7 @@ function BrokenRelations({ brokenRelations }: { brokenRelations: DeleteNotesPrev const noteIds = brokenRelations .map(relation => relation.noteId) .filter(noteId => noteId) as string[]; - froca.getNotes(noteIds).then(async (notes) => { + froca.getNotes(noteIds).then(async () => { const notesWithBrokenRelations: BrokenRelationData[] = []; for (const attr of brokenRelations) { notesWithBrokenRelations.push({ @@ -171,11 +170,3 @@ function BrokenRelations({ brokenRelations }: { brokenRelations: DeleteNotesPrev return <>; } } - -export default class DeleteNotesDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/export.tsx b/apps/client/src/widgets/dialogs/export.tsx index 3543e3b35..594104b66 100644 --- a/apps/client/src/widgets/dialogs/export.tsx +++ b/apps/client/src/widgets/dialogs/export.tsx @@ -4,14 +4,13 @@ import tree from "../../services/tree"; import Button from "../react/Button"; import FormRadioGroup from "../react/FormRadioGroup"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import "./export.css"; import ws from "../../services/ws"; import toastService, { ToastOptions } from "../../services/toast"; import utils from "../../services/utils"; import open from "../../services/open"; import froca from "../../services/froca"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; interface ExportDialogProps { branchId?: string | null; @@ -19,7 +18,7 @@ interface ExportDialogProps { defaultType?: "subtree" | "single"; } -function ExportDialogComponent() { +export default function ExportDialog() { const [ opts, setOpts ] = useState(); const [ exportType, setExportType ] = useState(opts?.defaultType ?? "subtree"); const [ subtreeFormat, setSubtreeFormat ] = useState("html"); @@ -125,14 +124,6 @@ function ExportDialogComponent() { ); } -export default class ExportDialog extends ReactBasicWidget { - - get component() { - return - } - -} - function exportBranch(branchId: string, type: string, format: string, version: string) { const taskId = utils.randomString(10); const url = open.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${taskId}`); diff --git a/apps/client/src/widgets/dialogs/help.tsx b/apps/client/src/widgets/dialogs/help.tsx index e4b72750d..d5c2f695d 100644 --- a/apps/client/src/widgets/dialogs/help.tsx +++ b/apps/client/src/widgets/dialogs/help.tsx @@ -1,4 +1,3 @@ -import ReactBasicWidget from "../react/ReactBasicWidget.js"; import Modal from "../react/Modal.jsx"; import { t } from "../../services/i18n.js"; import { ComponentChildren } from "preact"; @@ -6,9 +5,9 @@ import { CommandNames } from "../../components/app_context.js"; import RawHtml from "../react/RawHtml.jsx"; import { useEffect, useState } from "preact/hooks"; import keyboard_actions from "../../services/keyboard_actions.js"; -import useTriliumEvent from "../react/hooks.jsx"; +import { useTriliumEvent } from "../react/hooks.jsx"; -function HelpDialogComponent() { +export default function HelpDialog() { const [ shown, setShown ] = useState(false); useTriliumEvent("showCheatsheet", () => setShown(true)); @@ -161,11 +160,3 @@ function Card({ title, children }: { title: string, children: ComponentChildren

) } - -export default class HelpDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} diff --git a/apps/client/src/widgets/dialogs/import.tsx b/apps/client/src/widgets/dialogs/import.tsx index df648b7fb..58f28dd54 100644 --- a/apps/client/src/widgets/dialogs/import.tsx +++ b/apps/client/src/widgets/dialogs/import.tsx @@ -7,11 +7,10 @@ import FormFileUpload from "../react/FormFileUpload"; import FormGroup, { FormMultiGroup } from "../react/FormGroup"; import Modal from "../react/Modal"; import RawHtml from "../react/RawHtml"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import importService, { UploadFilesOptions } from "../../services/import"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function ImportDialogComponent() { +export default function ImportDialog() { const [ parentNoteId, setParentNoteId ] = useState(); const [ noteTitle, setNoteTitle ] = useState(); const [ files, setFiles ] = useState(null); @@ -89,14 +88,6 @@ function ImportDialogComponent() { ); } -export default class ImportDialog extends ReactBasicWidget { - - get component() { - return - } - -} - function boolToString(value: boolean) { return value ? "true" : "false"; } \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/include_note.tsx b/apps/client/src/widgets/dialogs/include_note.tsx index 234fc7646..757d23829 100644 --- a/apps/client/src/widgets/dialogs/include_note.tsx +++ b/apps/client/src/widgets/dialogs/include_note.tsx @@ -4,15 +4,14 @@ import FormGroup from "../react/FormGroup"; import FormRadioGroup from "../react/FormRadioGroup"; import Modal from "../react/Modal"; import NoteAutocomplete from "../react/NoteAutocomplete"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import Button from "../react/Button"; import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete"; import tree from "../../services/tree"; import froca from "../../services/froca"; import EditableTextTypeWidget from "../type_widgets/editable_text"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function IncludeNoteDialogComponent() { +export default function IncludeNoteDialog() { const [textTypeWidget, setTextTypeWidget] = useState(); const [suggestion, setSuggestion] = useState(null); const [boxSize, setBoxSize] = useState("medium"); @@ -70,14 +69,6 @@ function IncludeNoteDialogComponent() { ) } -export default class IncludeNoteDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} - async function includeNote(notePath: string, textTypeWidget: EditableTextTypeWidget) { const noteId = tree.getNoteIdFromUrl(notePath); if (!noteId) { diff --git a/apps/client/src/widgets/dialogs/incorrect_cpu_arch.tsx b/apps/client/src/widgets/dialogs/incorrect_cpu_arch.tsx index e0165b7d0..438cff1f3 100644 --- a/apps/client/src/widgets/dialogs/incorrect_cpu_arch.tsx +++ b/apps/client/src/widgets/dialogs/incorrect_cpu_arch.tsx @@ -3,11 +3,10 @@ import { t } from "../../services/i18n.js"; import utils from "../../services/utils.js"; import Button from "../react/Button.js"; import Modal from "../react/Modal.js"; -import ReactBasicWidget from "../react/ReactBasicWidget.js"; import { useState } from "preact/hooks"; -import useTriliumEvent from "../react/hooks.jsx"; +import { useTriliumEvent } from "../react/hooks.jsx"; -function IncorrectCpuArchDialogComponent() { +export default function IncorrectCpuArchDialogComponent() { const [ shown, setShown ] = useState(false); const downloadButtonRef = useRef(null); useTriliumEvent("showCpuArchWarning", () => setShown(true)); @@ -44,11 +43,3 @@ function IncorrectCpuArchDialogComponent() { ) } - -export default class IncorrectCpuArchDialog extends ReactBasicWidget { - - get component() { - return - } - -} diff --git a/apps/client/src/widgets/dialogs/info.tsx b/apps/client/src/widgets/dialogs/info.tsx index 9eaf81b50..334f43ce6 100644 --- a/apps/client/src/widgets/dialogs/info.tsx +++ b/apps/client/src/widgets/dialogs/info.tsx @@ -1,13 +1,12 @@ import { EventData } from "../../components/app_context"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import Modal from "../react/Modal"; import { t } from "../../services/i18n"; import Button from "../react/Button"; import { useRef, useState } from "preact/hooks"; import { RawHtmlBlock } from "../react/RawHtml"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function ShowInfoDialogComponent() { +export default function InfoDialog() { const [ opts, setOpts ] = useState>(); const [ shown, setShown ] = useState(false); const okButtonRef = useRef(null); @@ -37,11 +36,3 @@ function ShowInfoDialogComponent() { ); } - -export default class InfoDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} diff --git a/apps/client/src/widgets/dialogs/jump_to_note.tsx b/apps/client/src/widgets/dialogs/jump_to_note.tsx index c3185585f..89c438803 100644 --- a/apps/client/src/widgets/dialogs/jump_to_note.tsx +++ b/apps/client/src/widgets/dialogs/jump_to_note.tsx @@ -1,4 +1,3 @@ -import ReactBasicWidget from "../react/ReactBasicWidget"; import Modal from "../react/Modal"; import Button from "../react/Button"; import NoteAutocomplete from "../react/NoteAutocomplete"; @@ -8,14 +7,14 @@ import note_autocomplete, { Suggestion } from "../../services/note_autocomplete" import appContext from "../../components/app_context"; import commandRegistry from "../../services/command_registry"; import { refToJQuerySelector } from "../react/react_utils"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; import shortcutService from "../../services/shortcuts"; const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120; type Mode = "last-search" | "recent-notes" | "commands"; -function JumpToNoteDialogComponent() { +export default function JumpToNoteDialogComponent() { const [ mode, setMode ] = useState(); const [ lastOpenedTs, setLastOpenedTs ] = useState(0); const containerRef = useRef(null); @@ -27,12 +26,12 @@ function JumpToNoteDialogComponent() { async function openDialog(commandMode: boolean) { let newMode: Mode; - let initialText: string = ""; + let initialText = ""; if (commandMode) { newMode = "commands"; initialText = ">"; - } else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText) { + } else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) { // if you open the Jump To dialog soon after using it previously, it can often mean that you // actually want to search for the same thing (e.g., you opened the wrong note at first try) // so we'll keep the content. @@ -142,11 +141,3 @@ function JumpToNoteDialogComponent() { ); } - -export default class JumpToNoteDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/markdown_import.tsx b/apps/client/src/widgets/dialogs/markdown_import.tsx index 4f91278d9..d14d6fb11 100644 --- a/apps/client/src/widgets/dialogs/markdown_import.tsx +++ b/apps/client/src/widgets/dialogs/markdown_import.tsx @@ -5,18 +5,17 @@ import server from "../../services/server"; import toast from "../../services/toast"; import utils from "../../services/utils"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import Button from "../react/Button"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; interface RenderMarkdownResponse { htmlContent: string; } -function MarkdownImportDialogComponent() { +export default function MarkdownImportDialog() { const markdownImportTextArea = useRef(null); - let [ text, setText ] = useState(""); - let [ shown, setShown ] = useState(false); + const [ text, setText ] = useState(""); + const [ shown, setShown ] = useState(false); const triggerImport = useCallback(() => { if (appContext.tabManager.getActiveContextNoteType() !== "text") { @@ -64,14 +63,6 @@ function MarkdownImportDialogComponent() { ) } -export default class MarkdownImportDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} - async function convertMarkdownToHtml(markdownContent: string) { const { htmlContent } = await server.post("other/render-markdown", { markdownContent }); diff --git a/apps/client/src/widgets/dialogs/move_to.tsx b/apps/client/src/widgets/dialogs/move_to.tsx index c80ffc0d0..985934cdf 100644 --- a/apps/client/src/widgets/dialogs/move_to.tsx +++ b/apps/client/src/widgets/dialogs/move_to.tsx @@ -1,4 +1,3 @@ -import ReactBasicWidget from "../react/ReactBasicWidget"; import Modal from "../react/Modal"; import { t } from "../../services/i18n"; import NoteList from "../react/NoteList"; @@ -11,9 +10,9 @@ import tree from "../../services/tree"; import froca from "../../services/froca"; import branches from "../../services/branches"; import toast from "../../services/toast"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function MoveToDialogComponent() { +export default function MoveToDialog() { const [ movedBranchIds, setMovedBranchIds ] = useState(); const [ suggestion, setSuggestion ] = useState(null); const [ shown, setShown ] = useState(false); @@ -67,14 +66,6 @@ function MoveToDialogComponent() { ) } -export default class MoveToDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} - async function moveNotesTo(movedBranchIds: string[] | undefined, parentBranchId: string) { if (movedBranchIds) { await branches.moveToParentNote(movedBranchIds, parentBranchId); diff --git a/apps/client/src/widgets/dialogs/note_type_chooser.tsx b/apps/client/src/widgets/dialogs/note_type_chooser.tsx index 993ffc6ef..bc6e8abc9 100644 --- a/apps/client/src/widgets/dialogs/note_type_chooser.tsx +++ b/apps/client/src/widgets/dialogs/note_type_chooser.tsx @@ -1,4 +1,3 @@ -import ReactBasicWidget from "../react/ReactBasicWidget"; import Modal from "../react/Modal"; import { t } from "../../services/i18n"; import FormGroup from "../react/FormGroup"; @@ -10,7 +9,7 @@ import { MenuCommandItem, MenuItem } from "../../menus/context_menu"; import { TreeCommandNames } from "../../menus/tree_context_menu"; import { Suggestion } from "../../services/note_autocomplete"; import Badge from "../react/Badge"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; export interface ChooseNoteTypeResponse { success: boolean; @@ -26,7 +25,7 @@ const SEPARATOR_TITLE_REPLACEMENTS = [ t("note_type_chooser.templates") ]; -function NoteTypeChooserDialogComponent() { +export default function NoteTypeChooserDialogComponent() { const [ callback, setCallback ] = useState(); const [ shown, setShown ] = useState(false); const [ parentNote, setParentNote ] = useState(); @@ -37,25 +36,23 @@ function NoteTypeChooserDialogComponent() { setShown(true); }); - if (!noteTypes.length) { - useEffect(() => { - note_types.getNoteTypeItems().then(noteTypes => { - let index = -1; + useEffect(() => { + note_types.getNoteTypeItems().then(noteTypes => { + let index = -1; - setNoteTypes((noteTypes ?? []).map((item, _index) => { - if (item.title === "----") { - index++; - return { - title: SEPARATOR_TITLE_REPLACEMENTS[index], - enabled: false - } + setNoteTypes((noteTypes ?? []).map((item) => { + if (item.title === "----") { + index++; + return { + title: SEPARATOR_TITLE_REPLACEMENTS[index], + enabled: false } + } - return item; - })); - }); + return item; + })); }); - } + }, []); function onNoteTypeSelected(value: string) { const [ noteType, templateNoteId ] = value.split(","); @@ -120,11 +117,3 @@ function NoteTypeChooserDialogComponent() { ); } - -export default class NoteTypeChooserDialog extends ReactBasicWidget { - - get component() { - return - } - -} diff --git a/apps/client/src/widgets/dialogs/password_not_set.tsx b/apps/client/src/widgets/dialogs/password_not_set.tsx index 79aaa4fc8..5d0b92e5c 100644 --- a/apps/client/src/widgets/dialogs/password_not_set.tsx +++ b/apps/client/src/widgets/dialogs/password_not_set.tsx @@ -1,12 +1,11 @@ -import ReactBasicWidget from "../react/ReactBasicWidget"; import Modal from "../react/Modal"; import { t } from "../../services/i18n"; import Button from "../react/Button"; import appContext from "../../components/app_context"; import { useState } from "preact/hooks"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function PasswordNotSetDialogComponent() { +export default function PasswordNotSetDialog() { const [ shown, setShown ] = useState(false); useTriliumEvent("showPasswordNotSet", () => setShown(true)); @@ -27,10 +26,3 @@ function PasswordNotSetDialogComponent() { ); } -export default class PasswordNotSetDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} diff --git a/apps/client/src/widgets/dialogs/prompt.tsx b/apps/client/src/widgets/dialogs/prompt.tsx index 8af41ef44..c1ddf94aa 100644 --- a/apps/client/src/widgets/dialogs/prompt.tsx +++ b/apps/client/src/widgets/dialogs/prompt.tsx @@ -2,12 +2,10 @@ import { useRef, useState } from "preact/hooks"; import { t } from "../../services/i18n"; import Button from "../react/Button"; import Modal from "../react/Modal"; -import { Modal as BootstrapModal } from "bootstrap"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import FormTextBox from "../react/FormTextBox"; import FormGroup from "../react/FormGroup"; import { refToJQuerySelector } from "../react/react_utils"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; // JQuery here is maintained for compatibility with existing code. interface ShownCallbackData { @@ -28,7 +26,7 @@ export interface PromptDialogOptions { readOnly?: boolean; } -function PromptDialogComponent() { +export default function PromptDialog() { const modalRef = useRef(null); const formRef = useRef(null); const labelRef = useRef(null); @@ -84,11 +82,3 @@ function PromptDialogComponent() { ); } - -export default class PromptDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} diff --git a/apps/client/src/widgets/dialogs/protected_session_password.tsx b/apps/client/src/widgets/dialogs/protected_session_password.tsx index 137b0b0d4..1584a1ae1 100644 --- a/apps/client/src/widgets/dialogs/protected_session_password.tsx +++ b/apps/client/src/widgets/dialogs/protected_session_password.tsx @@ -3,11 +3,10 @@ import { t } from "../../services/i18n"; import Button from "../react/Button"; import FormTextBox from "../react/FormTextBox"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import protected_session from "../../services/protected_session"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function ProtectedSessionPasswordDialogComponent() { +export default function ProtectedSessionPasswordDialog() { const [ shown, setShown ] = useState(false); const [ password, setPassword ] = useState(""); const inputRef = useRef(null); @@ -38,11 +37,3 @@ function ProtectedSessionPasswordDialogComponent() { ) } - -export default class ProtectedSessionPasswordDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/recent_changes.tsx b/apps/client/src/widgets/dialogs/recent_changes.tsx index 264d2f15d..295650e54 100644 --- a/apps/client/src/widgets/dialogs/recent_changes.tsx +++ b/apps/client/src/widgets/dialogs/recent_changes.tsx @@ -6,7 +6,6 @@ import server from "../../services/server"; import toast from "../../services/toast"; import Button from "../react/Button"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import hoisted_note from "../../services/hoisted_note"; import type { RecentChangeRow } from "@triliumnext/commons"; import froca from "../../services/froca"; @@ -14,39 +13,32 @@ import { formatDateTime } from "../../utils/formatters"; import link from "../../services/link"; import RawHtml from "../react/RawHtml"; import ws from "../../services/ws"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function RecentChangesDialogComponent() { +export default function RecentChangesDialog() { const [ ancestorNoteId, setAncestorNoteId ] = useState(); - const [ groupedByDate, setGroupedByDate ] = useState>(); - const [ needsRefresh, setNeedsRefresh ] = useState(false); + const [ groupedByDate, setGroupedByDate ] = useState>(); + const [ refreshCounter, setRefreshCounter ] = useState(0); const [ shown, setShown ] = useState(false); - useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => { - setNeedsRefresh(true); + useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => { setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId()); setShown(true); }); - if (!groupedByDate || needsRefresh) { - useEffect(() => { - if (needsRefresh) { - setNeedsRefresh(false); - } + useEffect(() => { + server.get(`recent-changes/${ancestorNoteId}`) + .then(async (recentChanges) => { + // preload all notes into cache + await froca.getNotes( + recentChanges.map((r) => r.noteId), + true + ); - server.get(`recent-changes/${ancestorNoteId}`) - .then(async (recentChanges) => { - // preload all notes into cache - await froca.getNotes( - recentChanges.map((r) => r.noteId), - true - ); - - const groupedByDate = groupByDate(recentChanges); - setGroupedByDate(groupedByDate); - }); - }) - } + const groupedByDate = groupByDate(recentChanges); + setGroupedByDate(groupedByDate); + }); + }, [ shown, refreshCounter ]) return ( { server.post("notes/erase-deleted-notes-now").then(() => { - setNeedsRefresh(true); + setRefreshCounter(refreshCounter + 1); toast.showMessage(t("recent_changes.deleted_notes_message")); }); }} @@ -79,7 +71,7 @@ function RecentChangesDialogComponent() { ) } -function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map, setShown: Dispatch> }) { +function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map, setShown: Dispatch> }) { return ( <> { Array.from(groupedByDate.entries()).map(([dateDay, dayChanges]) => { @@ -114,10 +106,6 @@ function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map } function NoteLink({ notePath, title }: { notePath: string, title: string }) { - if (!notePath || !title) { - return null; - } - const [ noteLink, setNoteLink ] = useState | null>(null); useEffect(() => { link.createLink(notePath, { @@ -156,25 +144,19 @@ function DeletedNoteLink({ change, setShown }: { change: RecentChangeRow, setSho ); } -export default class RecentChangesDialog extends ReactBasicWidget { - - get component() { - return - } - -} - function groupByDate(rows: RecentChangeRow[]) { - const groupedByDate = new Map(); + const groupedByDate = new Map(); for (const row of rows) { const dateDay = row.date.substr(0, 10); - if (!groupedByDate.has(dateDay)) { - groupedByDate.set(dateDay, []); + let dateDayArray = groupedByDate.get(dateDay); + if (!dateDayArray) { + dateDayArray = []; + groupedByDate.set(dateDay, dateDayArray); } - groupedByDate.get(dateDay)!.push(row); + dateDayArray.push(row); } return groupedByDate; diff --git a/apps/client/src/widgets/dialogs/revisions.tsx b/apps/client/src/widgets/dialogs/revisions.tsx index 4d6377bba..0fa4f956e 100644 --- a/apps/client/src/widgets/dialogs/revisions.tsx +++ b/apps/client/src/widgets/dialogs/revisions.tsx @@ -8,7 +8,6 @@ import server from "../../services/server"; import toast from "../../services/toast"; import Button from "../react/Button"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import FormList, { FormListItem } from "../react/FormList"; import utils from "../../services/utils"; import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks"; @@ -18,9 +17,9 @@ import type { CSSProperties } from "preact/compat"; import open from "../../services/open"; import ActionButton from "../react/ActionButton"; import options from "../../services/options"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function RevisionsDialogComponent() { +export default function RevisionsDialog() { const [ note, setNote ] = useState(); const [ revisions, setRevisions ] = useState(); const [ currentRevision, setCurrentRevision ] = useState(); @@ -72,6 +71,8 @@ function RevisionsDialogComponent() { onHidden={() => { setShown(false); setNote(undefined); + setCurrentRevision(undefined); + setRevisions(undefined); }} show={shown} > @@ -202,17 +203,9 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi return <>; } - switch (revisionItem.type) { - case "text": { - const contentRef = useRef(null); - useEffect(() => { - if (contentRef.current?.querySelector("span.math-tex")) { - renderMathInElement(contentRef.current, { trust: true }); - } - }); - return
- } + case "text": + return case "code": return
{content}
; case "image": @@ -264,6 +257,16 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi } } +function RevisionContentText({ content }: { content: string | Buffer | undefined }) { + const contentRef = useRef(null); + useEffect(() => { + if (contentRef.current?.querySelector("span.math-tex")) { + renderMathInElement(contentRef.current, { trust: true }); + } + }, [content]); + return
+} + function RevisionFooter({ note }: { note?: FNote }) { if (!note) { return <>; @@ -291,14 +294,6 @@ function RevisionFooter({ note }: { note?: FNote }) { ; } -export default class RevisionsDialog extends ReactBasicWidget { - - get component() { - return - } - -} - async function getNote(noteId?: string | null) { if (noteId) { return await froca.getNote(noteId); diff --git a/apps/client/src/widgets/dialogs/sort_child_notes.tsx b/apps/client/src/widgets/dialogs/sort_child_notes.tsx index d73b84cae..10b69806c 100644 --- a/apps/client/src/widgets/dialogs/sort_child_notes.tsx +++ b/apps/client/src/widgets/dialogs/sort_child_notes.tsx @@ -5,12 +5,11 @@ import FormCheckbox from "../react/FormCheckbox"; import FormRadioGroup from "../react/FormRadioGroup"; import FormTextBox from "../react/FormTextBox"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import server from "../../services/server"; import FormGroup from "../react/FormGroup"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function SortChildNotesDialogComponent() { +export default function SortChildNotesDialog() { const [ parentNoteId, setParentNoteId ] = useState(); const [ sortBy, setSortBy ] = useState("title"); const [ sortDirection, setSortDirection ] = useState("asc"); @@ -89,11 +88,3 @@ function SortChildNotesDialogComponent() {
) } - -export default class SortChildNotesDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/upload_attachments.tsx b/apps/client/src/widgets/dialogs/upload_attachments.tsx index 8f53dd0fe..2a780e616 100644 --- a/apps/client/src/widgets/dialogs/upload_attachments.tsx +++ b/apps/client/src/widgets/dialogs/upload_attachments.tsx @@ -5,13 +5,12 @@ import FormCheckbox from "../react/FormCheckbox"; import FormFileUpload from "../react/FormFileUpload"; import FormGroup from "../react/FormGroup"; import Modal from "../react/Modal"; -import ReactBasicWidget from "../react/ReactBasicWidget"; import options from "../../services/options"; import importService from "../../services/import.js"; import tree from "../../services/tree"; -import useTriliumEvent from "../react/hooks"; +import { useTriliumEvent } from "../react/hooks"; -function UploadAttachmentsDialogComponent() { +export default function UploadAttachmentsDialog() { const [ parentNoteId, setParentNoteId ] = useState(); const [ files, setFiles ] = useState(null); const [ shrinkImages, setShrinkImages ] = useState(options.is("compressImages")); @@ -24,12 +23,12 @@ function UploadAttachmentsDialogComponent() { setShown(true); }); - if (parentNoteId) { - useEffect(() => { - tree.getNoteTitle(parentNoteId).then((noteTitle) => - setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle }))); - }, [parentNoteId]); - } + useEffect(() => { + if (!parentNoteId) return; + + tree.getNoteTitle(parentNoteId).then((noteTitle) => + setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle }))); + }, [parentNoteId]); return ( ); } - -export default class UploadAttachmentsDialog extends ReactBasicWidget { - - get component() { - return ; - } - -} diff --git a/apps/client/src/widgets/editability_select.ts b/apps/client/src/widgets/editability_select.ts deleted file mode 100644 index e7127ca8a..000000000 --- a/apps/client/src/widgets/editability_select.ts +++ /dev/null @@ -1,120 +0,0 @@ -import attributeService from "../services/attributes.js"; -import NoteContextAwareWidget from "./note_context_aware_widget.js"; -import { t } from "../services/i18n.js"; -import type FNote from "../entities/fnote.js"; -import type { EventData } from "../components/app_context.js"; -import { Dropdown } from "bootstrap"; - -type Editability = "auto" | "readOnly" | "autoReadOnlyDisabled"; - -const TPL = /*html*/` - -`; - -export default class EditabilitySelectWidget extends NoteContextAwareWidget { - - private dropdown!: Dropdown; - private $editabilityActiveDesc!: JQuery; - - doRender() { - this.$widget = $(TPL); - - this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]); - - this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc"); - - this.$widget.on("click", ".dropdown-item", async (e) => { - this.dropdown.toggle(); - - const editability = $(e.target).closest("[data-editability]").attr("data-editability"); - - if (!this.note || !this.noteId) { - return; - } - - for (const ownedAttr of this.note.getOwnedLabels()) { - if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) { - await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId); - } - } - - if (editability && editability !== "auto") { - await attributeService.addLabel(this.noteId, editability); - } - }); - } - - async refreshWithNote(note: FNote) { - let editability: Editability = "auto"; - - if (this.note?.isLabelTruthy("readOnly")) { - editability = "readOnly"; - } else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) { - editability = "autoReadOnlyDisabled"; - } - - const labels = { - auto: t("editability_select.auto"), - readOnly: t("editability_select.read_only"), - autoReadOnlyDisabled: t("editability_select.always_editable") - }; - - this.$widget.find(".dropdown-item").removeClass("selected"); - this.$widget.find(`.dropdown-item[data-editability='${editability}']`).addClass("selected"); - - this.$editabilityActiveDesc.text(labels[editability]); - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) { - this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/icon_list.ts b/apps/client/src/widgets/icon_list.ts index 7de49017b..6282d8b60 100644 --- a/apps/client/src/widgets/icon_list.ts +++ b/apps/client/src/widgets/icon_list.ts @@ -1,6 +1,6 @@ // taken from the HTML source of https://boxicons.com/ -interface Category { +export interface Category { name: string; id: number; } diff --git a/apps/client/src/widgets/note_icon.css b/apps/client/src/widgets/note_icon.css new file mode 100644 index 000000000..67eeadecf --- /dev/null +++ b/apps/client/src/widgets/note_icon.css @@ -0,0 +1,59 @@ +.note-icon-widget { + padding-top: 3px; + padding-left: 7px; + margin-right: 0; + width: 50px; + height: 50px; +} + +.note-icon-widget button.note-icon { + font-size: 180%; + background-color: transparent; + border: 1px solid transparent; + cursor: pointer; + padding: 6px; + color: var(--main-text-color); +} + +.note-icon-widget button.note-icon:hover { + border: 1px solid var(--main-border-color); +} + +.note-icon-widget .dropdown-menu { + border-radius: 10px; + border-width: 2px; + box-shadow: 10px 10px 93px -25px black; + padding: 10px 15px 10px 15px !important; +} + +.note-icon-widget .filter-row { + padding-top: 10px; + padding-bottom: 10px; + padding-right: 20px; + display: flex; + align-items: baseline; +} + +.note-icon-widget .filter-row span { + display: block; + padding-left: 15px; + padding-right: 15px; + font-weight: bold; +} + +.note-icon-widget .icon-list { + height: 500px; + overflow: auto; +} + +.note-icon-widget .icon-list span { + display: inline-block; + padding: 10px; + cursor: pointer; + border: 1px solid transparent; + font-size: 180%; +} + +.note-icon-widget .icon-list span:hover { + border: 1px solid var(--main-border-color); +} \ No newline at end of file diff --git a/apps/client/src/widgets/note_icon.ts b/apps/client/src/widgets/note_icon.ts deleted file mode 100644 index b5623db87..000000000 --- a/apps/client/src/widgets/note_icon.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { t } from "../services/i18n.js"; -import NoteContextAwareWidget from "./note_context_aware_widget.js"; -import attributeService from "../services/attributes.js"; -import server from "../services/server.js"; -import type FNote from "../entities/fnote.js"; -import type { EventData } from "../components/app_context.js"; -import type { Icon } from "./icon_list.js"; -import { Dropdown } from "bootstrap"; - -const TPL = /*html*/` -`; - -interface IconToCountCache { - iconClassToCountMap: Record; -} - -export default class NoteIconWidget extends NoteContextAwareWidget { - - private dropdown!: bootstrap.Dropdown; - private $icon!: JQuery; - private $iconList!: JQuery; - private $iconCategory!: JQuery; - private $iconSearch!: JQuery; - private iconToCountCache!: Promise | null; - - doRender() { - this.$widget = $(TPL); - this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]); - - this.$icon = this.$widget.find("button.note-icon"); - this.$iconList = this.$widget.find(".icon-list"); - this.$iconList.on("click", "span", async (e) => { - const clazz = $(e.target).attr("class"); - - if (this.noteId && this.note) { - await attributeService.setLabel(this.noteId, this.note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass", clazz); - } - }); - - this.$iconCategory = this.$widget.find("select[name='icon-category']"); - this.$iconCategory.on("change", () => this.renderDropdown()); - this.$iconCategory.on("click", (e) => e.stopPropagation()); - - this.$iconSearch = this.$widget.find("input[name='icon-search']"); - this.$iconSearch.on("input", () => this.renderDropdown()); - - this.$widget.on("show.bs.dropdown", async () => { - const { categories } = (await import("./icon_list.js")).default; - - this.$iconCategory.empty(); - - for (const category of categories) { - this.$iconCategory.append($("
+ ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/InheritedAttributesTab.tsx b/apps/client/src/widgets/ribbon/InheritedAttributesTab.tsx new file mode 100644 index 000000000..d9eb32b7f --- /dev/null +++ b/apps/client/src/widgets/ribbon/InheritedAttributesTab.tsx @@ -0,0 +1,86 @@ +import { useEffect, useState } from "preact/hooks"; +import { TabContext } from "./ribbon-interface"; +import FAttribute from "../../entities/fattribute"; +import { useLegacyWidget, useTriliumEvent } from "../react/hooks"; +import attributes from "../../services/attributes"; +import { t } from "../../services/i18n"; +import attribute_renderer from "../../services/attribute_renderer"; +import RawHtml from "../react/RawHtml"; +import { joinElements } from "../react/react_utils"; +import AttributeDetailWidget from "../attribute_widgets/attribute_detail"; + +export default function InheritedAttributesTab({ note, componentId }: TabContext) { + const [ inheritedAttributes, setInheritedAttributes ] = useState(); + const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget()); + + function refresh() { + if (!note) return; + const attrs = note.getAttributes().filter((attr) => attr.noteId !== note.noteId); + attrs.sort((a, b) => { + if (a.noteId === b.noteId) { + return a.position - b.position; + } else { + // inherited attributes should stay grouped: https://github.com/zadam/trilium/issues/3761 + return a.noteId < b.noteId ? -1 : 1; + } + }); + + setInheritedAttributes(attrs); + } + + useEffect(refresh, [ note ]); + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) { + refresh(); + } + }); + + return ( +
+
+ {inheritedAttributes?.length ? ( + joinElements(inheritedAttributes.map(attribute => ( + { + setTimeout( + () => + attributeDetailWidget.showAttributeDetail({ + attribute: { + noteId: attribute.noteId, + type: attribute.type, + name: attribute.name, + value: attribute.value, + isInheritable: attribute.isInheritable + }, + isOwned: false, + x: e.pageX, + y: e.pageY + }), + 100 + ); + }} + /> + )), " ") + ) : ( + <>{t("inherited_attribute_list.no_inherited_attributes")} + )} +
+ + {attributeDetailWidgetEl} +
+ ) +} +function InheritedAttribute({ attribute, onClick }: { attribute: FAttribute, onClick: (e: MouseEvent) => void }) { + const [ html, setHtml ] = useState | string>(""); + useEffect(() => { + attribute_renderer.renderAttribute(attribute, false).then(setHtml); + }, []); + + return ( + + ); +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx new file mode 100644 index 000000000..f780eab8b --- /dev/null +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -0,0 +1,136 @@ +import { ConvertToAttachmentResponse } from "@triliumnext/commons"; +import appContext, { CommandNames } from "../../components/app_context"; +import FNote from "../../entities/fnote" +import dialog from "../../services/dialog"; +import { t } from "../../services/i18n" +import server from "../../services/server"; +import toast from "../../services/toast"; +import ws from "../../services/ws"; +import ActionButton from "../react/ActionButton" +import Dropdown from "../react/Dropdown"; +import { FormDropdownDivider, FormListItem } from "../react/FormList"; +import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils"; +import { ParentComponent } from "../react/react_utils"; +import { useContext } from "preact/hooks"; +import NoteContext from "../../components/note_context"; +import branches from "../../services/branches"; + +interface NoteActionsProps { + note?: FNote; + noteContext?: NoteContext; +} + +export default function NoteActions({ note, noteContext }: NoteActionsProps) { + return ( + <> + {note && } + {note && note.type !== "launcher" && } + + ); +} + +function RevisionsButton({ note }: { note: FNote }) { + const isEnabled = !["launcher", "doc"].includes(note?.type ?? ""); + + return (isEnabled && + + ); +} + +function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) { + const parentComponent = useContext(ParentComponent); + const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment(); + const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type); + const isInOptions = note.noteId.startsWith("_options"); + const isPrintable = ["text", "code"].includes(note.type); + const isElectron = getIsElectron(); + const isMac = getIsMac(); + const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type); + const isSearchOrBook = ["search", "book"].includes(note.type); + + return ( + + {canBeConvertedToAttachment && } + {note.type === "render" && } + + + {isElectron && } + + + parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} /> + noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", { + notePath: noteContext.notePath, + defaultType: "single" + })} /> + + + + + + + + + branches.deleteNotes([note.getParentBranches()[0].branchId])} + /> + + + + + ); +} + +function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) { + return {text} +} + +function ConvertToAttachment({ note }: { note: FNote }) { + return ( + { + if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) { + return; + } + + const { attachment: newAttachment } = await server.post(`notes/${note.noteId}/convert-to-attachment`); + + if (!newAttachment) { + toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title })); + return; + } + + toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title })); + await ws.waitForMaxKnownEntityChangeId(); + await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, { + viewScope: { + viewMode: "attachments", + attachmentId: newAttachment.attachmentId + } + }); + }} + >{t("note_actions.convert_into_attachment")} + ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/NoteInfoTab.tsx b/apps/client/src/widgets/ribbon/NoteInfoTab.tsx new file mode 100644 index 000000000..f2add08a4 --- /dev/null +++ b/apps/client/src/widgets/ribbon/NoteInfoTab.tsx @@ -0,0 +1,93 @@ +import { useEffect, useState } from "preact/hooks"; +import { t } from "../../services/i18n"; +import { TabContext } from "./ribbon-interface"; +import { MetadataResponse, NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons"; +import server from "../../services/server"; +import Button from "../react/Button"; +import { formatDateTime } from "../../utils/formatters"; +import { formatSize } from "../../services/utils"; +import LoadingSpinner from "../react/LoadingSpinner"; +import { useTriliumEvent } from "../react/hooks"; + +export default function NoteInfoTab({ note }: TabContext) { + const [ metadata, setMetadata ] = useState(); + const [ isLoading, setIsLoading ] = useState(false); + const [ noteSizeResponse, setNoteSizeResponse ] = useState(); + const [ subtreeSizeResponse, setSubtreeSizeResponse ] = useState(); + + function refresh() { + if (note) { + server.get(`notes/${note?.noteId}/metadata`).then(setMetadata); + } + + setNoteSizeResponse(undefined); + setSubtreeSizeResponse(undefined); + setIsLoading(false); + } + + useEffect(refresh, [ note?.noteId ]); + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + const noteId = note?.noteId; + if (noteId && (loadResults.isNoteReloaded(noteId) || loadResults.isNoteContentReloaded(noteId))) { + refresh(); + } + }); + + return ( +
+ {note && ( + + + + + + + + + + + + + + + + + + + +
{t("note_info_widget.note_id")}:{note.noteId}{t("note_info_widget.created")}:{formatDateTime(metadata?.dateCreated)}{t("note_info_widget.modified")}:{formatDateTime(metadata?.dateModified)}
{t("note_info_widget.type")}: + {note.type}{' '} + { note.mime && ({note.mime}) } + {t("note_info_widget.note_size")}: + {!isLoading && !noteSizeResponse && !subtreeSizeResponse && ( +
+ )} +
+ ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/NoteMapTab.tsx b/apps/client/src/widgets/ribbon/NoteMapTab.tsx new file mode 100644 index 000000000..de7470141 --- /dev/null +++ b/apps/client/src/widgets/ribbon/NoteMapTab.tsx @@ -0,0 +1,53 @@ +import { TabContext } from "./ribbon-interface"; +import NoteMapWidget from "../note_map"; +import { useElementSize, useLegacyWidget, useWindowSize } from "../react/hooks"; +import ActionButton from "../react/ActionButton"; +import { t } from "../../services/i18n"; +import { useEffect, useRef, useState } from "preact/hooks"; + +const SMALL_SIZE_HEIGHT = "300px"; + +export default function NoteMapTab({ noteContext }: TabContext) { + const [ isExpanded, setExpanded ] = useState(false); + const [ height, setHeight ] = useState(SMALL_SIZE_HEIGHT); + const containerRef = useRef(null); + const { windowHeight } = useWindowSize(); + const containerSize = useElementSize(containerRef); + + const [ noteMapContainer, noteMapWidget ] = useLegacyWidget(() => new NoteMapWidget("ribbon"), { + noteContext, + containerClassName: "note-map-container" + }); + + useEffect(() => { + if (isExpanded && containerRef.current && containerSize) { + const height = windowHeight - containerSize.top; + setHeight(height + "px"); + } else { + setHeight(SMALL_SIZE_HEIGHT); + } + }, [ isExpanded, containerRef, windowHeight, containerSize?.top ]); + useEffect(() => noteMapWidget.setDimensions(), [ containerSize?.width, height ]); + + return ( +
+ {noteMapContainer} + + {!isExpanded ? ( + setExpanded(true)} + /> + ) : ( + setExpanded(false)} + /> + )} +
+ ); +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/NotePathsTab.tsx b/apps/client/src/widgets/ribbon/NotePathsTab.tsx new file mode 100644 index 000000000..00249b0b4 --- /dev/null +++ b/apps/client/src/widgets/ribbon/NotePathsTab.tsx @@ -0,0 +1,105 @@ +import { TabContext } from "./ribbon-interface"; +import { t } from "../../services/i18n"; +import Button from "../react/Button"; +import { useTriliumEvent } from "../react/hooks"; +import { useEffect, useMemo, useState } from "preact/hooks"; +import { NotePathRecord } from "../../entities/fnote"; +import NoteLink from "../react/NoteLink"; +import { joinElements } from "../react/react_utils"; + +export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) { + const [ sortedNotePaths, setSortedNotePaths ] = useState(); + + function refresh() { + if (!note) return; + setSortedNotePaths(note + .getSortedNotePathRecords(hoistedNoteId) + .filter((notePath) => !notePath.isHidden)); + } + + useEffect(refresh, [ note?.noteId ]); + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + const noteId = note?.noteId; + if (!noteId) return; + if (loadResults.getBranchRows().find((branch) => branch.noteId === noteId) + || loadResults.isNoteReloaded(noteId)) { + refresh(); + } + }); + + return ( +
+ <> +
+ {sortedNotePaths?.length ? t("note_paths.intro_placed") : t("note_paths.intro_not_placed")} +
+ +
    + {sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => ( + + )) : undefined} +
+ +
+ ) +} + +function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: string | null, notePathRecord?: NotePathRecord }) { + const notePath = notePathRecord?.notePath ?? []; + const notePathString = useMemo(() => notePath.join("/"), [ notePath ]); + + const [ classes, icons ] = useMemo(() => { + const classes: string[] = []; + const icons: { icon: string, title: string }[] = []; + + if (notePathString === currentNotePath) { + classes.push("path-current"); + } + + if (!notePathRecord || notePathRecord.isInHoistedSubTree) { + classes.push("path-in-hoisted-subtree"); + } else { + icons.push({ icon: "bx bx-trending-up", title: t("note_paths.outside_hoisted") }) + } + + if (notePathRecord?.isArchived) { + classes.push("path-archived"); + icons.push({ icon: "bx bx-archive", title: t("note_paths.archived") }) + } + + if (notePathRecord?.isSearch) { + classes.push("path-search"); + icons.push({ icon: "bx bx-search", title: t("note_paths.search") }) + } + + return [ classes.join(" "), icons ]; + }, [ notePathString, currentNotePath, notePathRecord ]); + + // Determine the full note path (for the links) of every component of the current note path. + const pathSegments: string[] = []; + const fullNotePaths: string[] = []; + for (const noteId of notePath) { + pathSegments.push(noteId); + fullNotePaths.push(pathSegments.join("/")); + } + + return ( +
  • + {joinElements(fullNotePaths.map(notePath => ( + + )), " / ")} + + {icons.map(({ icon, title }) => ( + + ))} +
  • + ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/NotePropertiesTab.tsx b/apps/client/src/widgets/ribbon/NotePropertiesTab.tsx new file mode 100644 index 000000000..8cc0c2b85 --- /dev/null +++ b/apps/client/src/widgets/ribbon/NotePropertiesTab.tsx @@ -0,0 +1,20 @@ +import { t } from "../../services/i18n"; +import { useNoteLabel } from "../react/hooks"; +import { TabContext } from "./ribbon-interface"; + +/** + * TODO: figure out better name or conceptualize better. + */ +export default function NotePropertiesTab({ note }: TabContext) { + const [ pageUrl ] = useNoteLabel(note, "pageUrl"); + + return ( +
    + { pageUrl && ( +
    + {t("note_properties.this_note_was_originally_taken_from")} {pageUrl} +
    + )} +
    + ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/OwnedAttributesTab.tsx b/apps/client/src/widgets/ribbon/OwnedAttributesTab.tsx new file mode 100644 index 000000000..1bdb6c1c0 --- /dev/null +++ b/apps/client/src/widgets/ribbon/OwnedAttributesTab.tsx @@ -0,0 +1,29 @@ +import { useMemo, useRef } from "preact/hooks"; +import { useLegacyImperativeHandlers, useTriliumEvents } from "../react/hooks"; +import AttributeEditor, { AttributeEditorImperativeHandlers } from "./components/AttributeEditor"; +import { TabContext } from "./ribbon-interface"; + +export default function OwnedAttributesTab({ note, hidden, activate, ntxId, ...restProps }: TabContext) { + const api = useRef(null); + + useTriliumEvents([ "addNewLabel", "addNewRelation" ], ({ ntxId: eventNtxId }) => { + if (ntxId === eventNtxId) { + activate(); + } + }); + + // Interaction with the attribute editor. + useLegacyImperativeHandlers(useMemo(() => ({ + saveAttributesCommand: () => api.current?.save(), + reloadAttributesCommand: () => api.current?.refresh(), + updateAttributeListCommand: ({ attributes }) => api.current?.renderOwnedAttributes(attributes) + }), [ api ])); + + return ( +
    + { note && ( +
    + ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/Ribbon.tsx b/apps/client/src/widgets/ribbon/Ribbon.tsx new file mode 100644 index 000000000..53813b1f4 --- /dev/null +++ b/apps/client/src/widgets/ribbon/Ribbon.tsx @@ -0,0 +1,284 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { t } from "../../services/i18n"; +import { useNoteContext, useNoteProperty, useStaticTooltip, useTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks"; +import "./style.css"; +import { VNode } from "preact"; +import BasicPropertiesTab from "./BasicPropertiesTab"; +import FormattingToolbar from "./FormattingToolbar"; +import { numberObjectsInPlace } from "../../services/utils"; +import { TabContext } from "./ribbon-interface"; +import options from "../../services/options"; +import { EventNames } from "../../components/app_context"; +import FNote from "../../entities/fnote"; +import ScriptTab from "./ScriptTab"; +import EditedNotesTab from "./EditedNotesTab"; +import NotePropertiesTab from "./NotePropertiesTab"; +import NoteInfoTab from "./NoteInfoTab"; +import SimilarNotesTab from "./SimilarNotesTab"; +import FilePropertiesTab from "./FilePropertiesTab"; +import ImagePropertiesTab from "./ImagePropertiesTab"; +import NotePathsTab from "./NotePathsTab"; +import NoteMapTab from "./NoteMapTab"; +import OwnedAttributesTab from "./OwnedAttributesTab"; +import InheritedAttributesTab from "./InheritedAttributesTab"; +import CollectionPropertiesTab from "./CollectionPropertiesTab"; +import SearchDefinitionTab from "./SearchDefinitionTab"; +import NoteActions from "./NoteActions"; +import keyboard_actions from "../../services/keyboard_actions"; +import { KeyboardActionNames } from "@triliumnext/commons"; + +interface TitleContext { + note: FNote | null | undefined; +} + +interface TabConfiguration { + title: string | ((context: TitleContext) => string); + icon: string; + content: (context: TabContext) => VNode | false; + show: boolean | ((context: TitleContext) => boolean | null | undefined); + toggleCommand?: KeyboardActionNames; + activate?: boolean | ((context: TitleContext) => boolean); + /** + * By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed. + */ + stayInDom?: boolean; +} + +const TAB_CONFIGURATION = numberObjectsInPlace([ + { + title: t("classic_editor_toolbar.title"), + icon: "bx bx-text", + show: ({ note }) => note?.type === "text" && options.get("textNoteEditorType") === "ckeditor-classic", + toggleCommand: "toggleRibbonTabClassicEditor", + content: FormattingToolbar, + stayInDom: true + }, + { + title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"), + icon: "bx bx-play", + content: ScriptTab, + activate: true, + show: ({ note }) => note && + (note.isTriliumScript() || note.isTriliumSqlite()) && + (note.hasLabel("executeDescription") || note.hasLabel("executeButton")) + }, + { + title: t("search_definition.search_parameters"), + icon: "bx bx-search", + content: SearchDefinitionTab, + activate: true, + show: ({ note }) => note?.type === "search" + }, + { + title: t("edited_notes.title"), + icon: "bx bx-calendar-edit", + content: EditedNotesTab, + show: ({ note }) => note?.hasOwnedLabel("dateNote"), + activate: ({ note }) => (note?.getPromotedDefinitionAttributes().length === 0 || !options.is("promotedAttributesOpenInRibbon")) && options.is("editedNotesOpenInRibbon") + }, + { + title: t("book_properties.book_properties"), + icon: "bx bx-book", + content: CollectionPropertiesTab, + show: ({ note }) => note?.type === "book", + toggleCommand: "toggleRibbonTabBookProperties" + }, + { + title: t("note_properties.info"), + icon: "bx bx-info-square", + content: NotePropertiesTab, + show: ({ note }) => !!note?.getLabelValue("pageUrl"), + activate: true + }, + { + title: t("file_properties.title"), + icon: "bx bx-file", + content: FilePropertiesTab, + show: ({ note }) => note?.type === "file", + toggleCommand: "toggleRibbonTabFileProperties", + activate: true + }, + { + title: t("image_properties.title"), + icon: "bx bx-image", + content: ImagePropertiesTab, + show: ({ note }) => note?.type === "image", + toggleCommand: "toggleRibbonTabImageProperties", + activate: true, + }, + { + // BasicProperties + title: t("basic_properties.basic_properties"), + icon: "bx bx-slider", + content: BasicPropertiesTab, + show: ({note}) => !note?.isLaunchBarConfig(), + toggleCommand: "toggleRibbonTabBasicProperties" + }, + { + title: t("owned_attribute_list.owned_attributes"), + icon: "bx bx-list-check", + content: OwnedAttributesTab, + show: ({note}) => !note?.isLaunchBarConfig(), + toggleCommand: "toggleRibbonTabOwnedAttributes", + stayInDom: true + }, + { + title: t("inherited_attribute_list.title"), + icon: "bx bx-list-plus", + content: InheritedAttributesTab, + show: ({note}) => !note?.isLaunchBarConfig(), + toggleCommand: "toggleRibbonTabInheritedAttributes" + }, + { + title: t("note_paths.title"), + icon: "bx bx-collection", + content: NotePathsTab, + show: true, + toggleCommand: "toggleRibbonTabNotePaths" + }, + { + title: t("note_map.title"), + icon: "bx bxs-network-chart", + content: NoteMapTab, + show: true, + toggleCommand: "toggleRibbonTabNoteMap" + }, + { + title: t("similar_notes.title"), + icon: "bx bx-bar-chart", + show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"), + content: SimilarNotesTab, + toggleCommand: "toggleRibbonTabSimilarNotes" + }, + { + title: t("note_info_widget.title"), + icon: "bx bx-info-circle", + show: ({ note }) => !!note, + content: NoteInfoTab, + toggleCommand: "toggleRibbonTabNoteInfo" + } +]); + +export default function Ribbon() { + const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext(); + const noteType = useNoteProperty(note, "type"); + const titleContext: TitleContext = { note }; + const [ activeTabIndex, setActiveTabIndex ] = useState(); + const filteredTabs = useMemo( + () => TAB_CONFIGURATION.filter(tab => typeof tab.show === "boolean" ? tab.show : tab.show?.(titleContext)), + [ titleContext, note, noteType ]); + + // Automatically activate the first ribbon tab that needs to be activated whenever a note changes. + useEffect(() => { + const tabToActivate = filteredTabs.find(tab => typeof tab.activate === "boolean" ? tab.activate : tab.activate?.(titleContext)); + if (tabToActivate) { + setActiveTabIndex(tabToActivate.index); + } + }, [ note?.noteId ]); + + // Register keyboard shortcuts. + const eventsToListenTo = useMemo(() => TAB_CONFIGURATION.filter(config => config.toggleCommand).map(config => config.toggleCommand) as EventNames[], []); + useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => { + const correspondingTab = filteredTabs.find(tab => tab.toggleCommand === toggleCommand); + if (correspondingTab) { + if (activeTabIndex !== correspondingTab.index) { + setActiveTabIndex(correspondingTab.index); + } else { + setActiveTabIndex(undefined); + } + } + }, [ filteredTabs, activeTabIndex ])); + + return ( +
    + {noteContext?.viewScope?.viewMode === "default" && ( + <> +
    +
    + {filteredTabs.map(({ title, icon, index, toggleCommand }) => ( + { + if (activeTabIndex !== index) { + setActiveTabIndex(index); + } else { + // Collapse + setActiveTabIndex(undefined); + } + }} + /> + ))} +
    +
    + { note && } +
    +
    + +
    + {filteredTabs.map(tab => { + const isActive = tab.index === activeTabIndex; + if (!isActive && !tab.stayInDom) { + return; + } + + const TabContent = tab.content; + + return ( +
    +
    + ); + })} +
    + + )} +
    + ) +} + +function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) { + const iconRef = useRef(null); + const [ keyboardShortcut, setKeyboardShortcut ] = useState(); + useStaticTooltip(iconRef, { + title: keyboardShortcut?.length ? `${title} (${keyboardShortcut?.join(",")})` : title + }); + + useEffect(() => { + if (toggleCommand) { + keyboard_actions.getAction(toggleCommand).then(action => setKeyboardShortcut(action?.effectiveShortcuts)); + } + }, [toggleCommand]); + + return ( + <> +
    + +   + { active && {title} } +
    + +
    + + ) +} + diff --git a/apps/client/src/widgets/ribbon/ScriptTab.tsx b/apps/client/src/widgets/ribbon/ScriptTab.tsx new file mode 100644 index 000000000..81ac3a3ef --- /dev/null +++ b/apps/client/src/widgets/ribbon/ScriptTab.tsx @@ -0,0 +1,28 @@ +import { t } from "../../services/i18n"; +import Button from "../react/Button"; +import { useNoteLabel } from "../react/hooks"; +import { TabContext } from "./ribbon-interface"; + +export default function ScriptTab({ note }: TabContext) { + const [ executeDescription ] = useNoteLabel(note, "executeDescription"); + const executeTitle = useNoteLabel(note, "executeTitle")[0] || + (note?.isTriliumSqlite() ? t("script_executor.execute_query") : t("script_executor.execute_script")); + + return ( +
    + {executeDescription && ( +
    + {executeDescription} +
    + )} + +
    +
    +
    + ); +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx b/apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx new file mode 100644 index 000000000..6f98c63e1 --- /dev/null +++ b/apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx @@ -0,0 +1,360 @@ +import FormTextArea from "../react/FormTextArea"; +import NoteAutocomplete from "../react/NoteAutocomplete"; +import FormSelect from "../react/FormSelect"; +import Icon from "../react/Icon"; +import FormTextBox from "../react/FormTextBox"; +import { ComponentChildren, VNode } from "preact"; +import FNote from "../../entities/fnote"; +import { removeOwnedAttributesByNameOrType } from "../../services/attributes"; +import { AttributeType } from "@triliumnext/commons"; +import { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip } from "../react/hooks"; +import { t } from "../../services/i18n"; +import { useEffect, useMemo, useRef } from "preact/hooks"; +import appContext from "../../components/app_context"; +import server from "../../services/server"; +import HelpRemoveButtons from "../react/HelpRemoveButtons"; + +export interface SearchOption { + attributeName: string; + attributeType: "label" | "relation"; + icon: string; + label: string; + tooltip?: string; + component: (props: SearchOptionProps) => VNode; + defaultValue?: string; + additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]; +} + +interface SearchOptionProps { + note: FNote; + refreshResults: () => void; + attributeName: string; + attributeType: "label" | "relation"; + additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]; + defaultValue?: string; + error?: { message: string }; +} + +export const SEARCH_OPTIONS: SearchOption[] = [ + { + attributeName: "searchString", + attributeType: "label", + icon: "bx bx-text", + label: t("search_definition.search_string"), + component: SearchStringOption + }, + { + attributeName: "searchScript", + attributeType: "relation", + defaultValue: "root", + icon: "bx bx-code", + label: t("search_definition.search_script"), + component: SearchScriptOption + }, + { + attributeName: "ancestor", + attributeType: "relation", + defaultValue: "root", + icon: "bx bx-filter-alt", + label: t("search_definition.ancestor"), + component: AncestorOption, + additionalAttributesToDelete: [ { type: "label", name: "ancestorDepth" } ] + }, + { + attributeName: "fastSearch", + attributeType: "label", + icon: "bx bx-run", + label: t("search_definition.fast_search"), + tooltip: t("search_definition.fast_search_description"), + component: FastSearchOption + }, + { + attributeName: "includeArchivedNotes", + attributeType: "label", + icon: "bx bx-archive", + label: t("search_definition.include_archived"), + tooltip: t("search_definition.include_archived_notes_description"), + component: IncludeArchivedNotesOption + }, + { + attributeName: "orderBy", + attributeType: "label", + defaultValue: "relevancy", + icon: "bx bx-arrow-from-top", + label: t("search_definition.order_by"), + component: OrderByOption, + additionalAttributesToDelete: [ { type: "label", name: "orderDirection" } ] + }, + { + attributeName: "limit", + attributeType: "label", + defaultValue: "10", + icon: "bx bx-stop", + label: t("search_definition.limit"), + tooltip: t("search_definition.limit_description"), + component: LimitOption + }, + { + attributeName: "debug", + attributeType: "label", + icon: "bx bx-bug", + label: t("search_definition.debug"), + tooltip: t("search_definition.debug_description"), + component: DebugOption + } +]; + +function SearchOption({ note, title, titleIcon, children, help, attributeName, attributeType, additionalAttributesToDelete }: { + note: FNote; + title: string, + titleIcon?: string, + children?: ComponentChildren, + help?: ComponentChildren, + attributeName: string, + attributeType: AttributeType, + additionalAttributesToDelete?: { type: "label" | "relation", name: string }[] +}) { + return ( + + + {titleIcon && <>{" "}} + {title} + + {children} + { + removeOwnedAttributesByNameOrType(note, attributeType, attributeName); + if (additionalAttributesToDelete) { + for (const { type, name } of additionalAttributesToDelete) { + removeOwnedAttributesByNameOrType(note, type, name); + } + } + }} + /> + + ) +} + +function SearchStringOption({ note, refreshResults, error, ...restProps }: SearchOptionProps) { + const [ searchString, setSearchString ] = useNoteLabel(note, "searchString"); + const inputRef = useRef(null); + const currentValue = useRef(searchString ?? ""); + const spacedUpdate = useSpacedUpdate(async () => { + const searchString = currentValue.current; + appContext.lastSearchString = searchString; + setSearchString(searchString); + + if (note.title.startsWith(t("search_string.search_prefix"))) { + await server.put(`notes/${note.noteId}/title`, { + title: `${t("search_string.search_prefix")} ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}…`}` + }); + } + }, 1000); + + // React to errors + const { showTooltip, hideTooltip } = useTooltip(inputRef, { + trigger: "manual", + title: `${t("search_string.error", { error: error?.message })}`, + html: true, + placement: "bottom" + }); + + // Auto-focus. + useEffect(() => inputRef.current?.focus(), []); + + useEffect(() => { + if (error) { + showTooltip(); + setTimeout(() => hideTooltip(), 4000); + } else { + hideTooltip(); + } + }, [ error ]); + + return + {t("search_string.search_syntax")} - {t("search_string.also_see")} {t("search_string.complete_help")} +
      +
    • {t("search_string.full_text_search")}
    • +
    • #abc - {t("search_string.label_abc")}
    • +
    • #year = 2019 - {t("search_string.label_year")}
    • +
    • #rock #pop - {t("search_string.label_rock_pop")}
    • +
    • #rock or #pop - {t("search_string.label_rock_or_pop")}
    • +
    • #year <= 2000 - {t("search_string.label_year_comparison")}
    • +
    • note.dateCreated >= MONTH-1 - {t("search_string.label_date_created")}
    • +
    + } + note={note} {...restProps} + > + { + currentValue.current = text; + spacedUpdate.scheduleUpdate(); + }} + onKeyDown={async (e) => { + if (e.key === "Enter") { + e.preventDefault(); + + // this also in effect disallows new lines in query string. + // on one hand, this makes sense since search string is a label + // on the other hand, it could be nice for structuring long search string. It's probably a niche case though. + await spacedUpdate.updateNowIfNecessary(); + refreshResults(); + } + }} + /> +
    +} + +function SearchScriptOption({ note, ...restProps }: SearchOptionProps) { + const [ searchScript, setSearchScript ] = useNoteRelation(note, "searchScript"); + + return +

    {t("search_script.description1")}

    +

    {t("search_script.description2")}

    +

    {t("search_script.example_title")}

    +
    {t("search_script.example_code")}
    + {t("search_script.note")} + } + note={note} {...restProps} + > + setSearchScript(noteId ?? "root")} + placeholder={t("search_script.placeholder")} + /> +
    +} + +function AncestorOption({ note, ...restProps}: SearchOptionProps) { + const [ ancestor, setAncestor ] = useNoteRelation(note, "ancestor"); + const [ depth, setDepth ] = useNoteLabel(note, "ancestorDepth"); + + const options = useMemo(() => { + const options: { value: string | undefined; label: string }[] = [ + { value: "", label: t("ancestor.depth_doesnt_matter") }, + { value: "eq1", label: `${t("ancestor.depth_eq", { count: 1 })} (${t("ancestor.direct_children")})` } + ]; + + for (let i=2; i<=9; i++) options.push({ value: "eq" + i, label: t("ancestor.depth_eq", { count: i }) }); + for (let i=0; i<=9; i++) options.push({ value: "gt" + i, label: t("ancestor.depth_gt", { count: i }) }); + for (let i=2; i<=9; i++) options.push({ value: "lt" + i, label: t("ancestor.depth_lt", { count: i }) }); + + return options; + }, []); + + return +
    + setAncestor(noteId ?? "root")} + placeholder={t("ancestor.placeholder")} + /> + +
    {t("ancestor.depth_label")}:
    + setDepth(value ? value : null)} + style={{ flexShrink: 3 }} + /> +
    +
    ; +} + +function FastSearchOption({ ...restProps }: SearchOptionProps) { + return +} + +function DebugOption({ ...restProps }: SearchOptionProps) { + return +

    {t("debug.debug_info")}

    + {t("debug.access_info")} + } + {...restProps} + /> +} + +function IncludeArchivedNotesOption({ ...restProps }: SearchOptionProps) { + return +} + +function OrderByOption({ note, ...restProps }: SearchOptionProps) { + const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy"); + const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection"); + + return + + {" "} + + +} + +function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) { + const [ limit, setLimit ] = useNoteLabel(note, "limit"); + + return + + +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx new file mode 100644 index 000000000..4681151fd --- /dev/null +++ b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx @@ -0,0 +1,204 @@ +import { t } from "../../services/i18n"; +import Button from "../react/Button"; +import { TabContext } from "./ribbon-interface"; +import { SaveSearchNoteResponse } from "@triliumnext/commons"; +import attributes from "../../services/attributes"; +import FNote from "../../entities/fnote"; +import toast from "../../services/toast"; +import froca from "../../services/froca"; +import { useContext, useEffect, useState } from "preact/hooks"; +import { ParentComponent } from "../react/react_utils"; +import { useTriliumEvent } from "../react/hooks"; +import appContext from "../../components/app_context"; +import server from "../../services/server"; +import ws from "../../services/ws"; +import tree from "../../services/tree"; +import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions"; +import Dropdown from "../react/Dropdown"; +import Icon from "../react/Icon"; +import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action"; +import { FormListHeader, FormListItem } from "../react/FormList"; +import RenameNoteBulkAction from "../bulk_actions/note/rename_note"; +import { getErrorMessage } from "../../services/utils"; + +export default function SearchDefinitionTab({ note, ntxId }: TabContext) { + const parentComponent = useContext(ParentComponent); + const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>(); + const [ error, setError ] = useState<{ message: string }>(); + + function refreshOptions() { + if (!note) return; + + const availableOptions: SearchOption[] = []; + const activeOptions: SearchOption[] = []; + + for (const searchOption of SEARCH_OPTIONS) { + const attr = note.getAttribute(searchOption.attributeType, searchOption.attributeName); + if (attr) { + activeOptions.push(searchOption); + } else { + availableOptions.push(searchOption); + } + } + + setSearchOptions({ availableOptions, activeOptions }); + } + + async function refreshResults() { + const noteId = note?.noteId; + if (!noteId) { + return; + } + + try { + const result = await froca.loadSearchNote(noteId); + if (result?.error) { + setError({ message: result?.error}) + } else { + setError(undefined); + } + } catch (e: unknown) { + toast.showError(getErrorMessage(e)); + } + + parentComponent?.triggerEvent("searchRefreshed", { ntxId }); + } + + // Refresh the list of available and active options. + useEffect(refreshOptions, [ note ]); + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + if (loadResults.getAttributeRows().find((attrRow) => attributes.isAffecting(attrRow, note))) { + refreshOptions(); + } + }); + + return ( +
    +
    + {note && + + + + + + + {searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete, defaultValue }) => { + const Component = component; + return ; + })} + + + + + + + +
    {t("search_definition.add_search_option")} + {searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => ( +
    +
    +
    +
    + } +
    +
    + ) +} + +function BulkActionsList({ note }: { note: FNote }) { + const [ bulkActions, setBulkActions ] = useState(); + + function refreshBulkActions() { + if (note) { + setBulkActions(bulk_action.parseActions(note)); + } + } + + // React to changes. + useEffect(refreshBulkActions, [ note ]); + useTriliumEvent("entitiesReloaded", ({loadResults}) => { + if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "action" && attributes.isAffecting(attr, note))) { + refreshBulkActions(); + } + }); + + return ( + + {bulkActions?.map(bulkAction => ( + bulkAction.doRender() + ))} + + ) +} + +function AddBulkActionButton({ note }: { note: FNote }) { + return ( + {" "}{t("search_definition.action")}} + noSelectButtonStyle + > + {ACTION_GROUPS.map(({ actions, title }) => ( + <> + + + {actions.map(({ actionName, actionTitle }) => ( + bulk_action.addAction(note.noteId, actionName)}>{actionTitle} + ))} + + ))} + + ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/SimilarNotesTab.tsx b/apps/client/src/widgets/ribbon/SimilarNotesTab.tsx new file mode 100644 index 000000000..57299c92c --- /dev/null +++ b/apps/client/src/widgets/ribbon/SimilarNotesTab.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from "preact/hooks"; +import { TabContext } from "./ribbon-interface"; +import { SimilarNoteResponse } from "@triliumnext/commons"; +import server from "../../services/server"; +import { t } from "../../services/i18n"; +import froca from "../../services/froca"; +import NoteLink from "../react/NoteLink"; + +export default function SimilarNotesTab({ note }: TabContext) { + const [ similarNotes, setSimilarNotes ] = useState(); + + useEffect(() => { + if (note) { + server.get(`similar-notes/${note.noteId}`).then(async similarNotes => { + if (similarNotes) { + const noteIds = similarNotes.flatMap((note) => note.notePath); + await froca.getNotes(noteIds, true); // preload all at once + } + setSimilarNotes(similarNotes); + }); + } + + }, [ note?.noteId ]); + + return ( +
    +
    + {similarNotes?.length ? ( +
    + {similarNotes.map(({notePath, score}) => ( + + ))} +
    + ) : ( + <>{t("similar_notes.no_similar_notes_found")} + )} +
    +
    + ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon_widgets/book_properties_config.ts b/apps/client/src/widgets/ribbon/collection-properties-config.ts similarity index 92% rename from apps/client/src/widgets/ribbon_widgets/book_properties_config.ts rename to apps/client/src/widgets/ribbon/collection-properties-config.ts index 09acb6e99..fe54c25f5 100644 --- a/apps/client/src/widgets/ribbon_widgets/book_properties_config.ts +++ b/apps/client/src/widgets/ribbon/collection-properties-config.ts @@ -9,13 +9,13 @@ interface BookConfig { properties: BookProperty[]; } -interface CheckBoxProperty { +export interface CheckBoxProperty { type: "checkbox", label: string; bindToLabel: string } -interface ButtonProperty { +export interface ButtonProperty { type: "button", label: string; title?: string; @@ -23,7 +23,7 @@ interface ButtonProperty { onClick: (context: BookContext) => void; } -interface NumberProperty { +export interface NumberProperty { type: "number", label: string; bindToLabel: string; @@ -37,11 +37,11 @@ interface ComboBoxItem { } interface ComboBoxGroup { - name: string; + title: string; items: ComboBoxItem[]; } -interface ComboBoxProperty { +export interface ComboBoxProperty { type: "combobox", label: string; bindToLabel: string; @@ -120,19 +120,19 @@ export const bookPropertiesConfig: Record = { defaultValue: DEFAULT_MAP_LAYER_NAME, options: [ { - name: t("book_properties_config.raster"), + title: t("book_properties_config.raster"), items: Object.entries(MAP_LAYERS) .filter(([_, layer]) => layer.type === "raster") .map(buildMapLayer) }, { - name: t("book_properties_config.vector_light"), + title: t("book_properties_config.vector_light"), items: Object.entries(MAP_LAYERS) .filter(([_, layer]) => layer.type === "vector" && !layer.isDarkTheme) .map(buildMapLayer) }, { - name: t("book_properties_config.vector_dark"), + title: t("book_properties_config.vector_dark"), items: Object.entries(MAP_LAYERS) .filter(([_, layer]) => layer.type === "vector" && layer.isDarkTheme) .map(buildMapLayer) diff --git a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx new file mode 100644 index 000000000..d6c609dba --- /dev/null +++ b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx @@ -0,0 +1,429 @@ +import { MutableRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "preact/hooks"; +import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5"; +import { t } from "../../../services/i18n"; +import server from "../../../services/server"; +import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete"; +import CKEditor, { CKEditorApi } from "../../react/CKEditor"; +import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent } from "../../react/hooks"; +import FAttribute from "../../../entities/fattribute"; +import attribute_renderer from "../../../services/attribute_renderer"; +import FNote from "../../../entities/fnote"; +import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; +import attribute_parser, { Attribute } from "../../../services/attribute_parser"; +import ActionButton from "../../react/ActionButton"; +import { escapeQuotes, getErrorMessage } from "../../../services/utils"; +import link from "../../../services/link"; +import froca from "../../../services/froca"; +import contextMenu from "../../../menus/context_menu"; +import type { CommandData, FilteredCommandNames } from "../../../components/app_context"; +import { AttributeType } from "@triliumnext/commons"; +import attributes from "../../../services/attributes"; +import note_create from "../../../services/note_create"; + +type AttributeCommandNames = FilteredCommandNames; + +const HELP_TEXT = ` +

    ${t("attribute_editor.help_text_body1")}

    + +

    ${t("attribute_editor.help_text_body2")}

    + +

    ${t("attribute_editor.help_text_body3")}

    `; + +const mentionSetup: MentionFeed[] = [ + { + marker: "@", + feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText), + itemRenderer: (_item) => { + const item = _item as Suggestion; + const itemElement = document.createElement("button"); + + itemElement.innerHTML = `${item.highlightedNotePathTitle} `; + + return itemElement; + }, + minimumCharacters: 0 + }, + { + marker: "#", + feed: async (queryText) => { + const names = await server.get(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`); + + return names.map((name) => { + return { + id: `#${name}`, + name: name + }; + }); + }, + minimumCharacters: 0 + }, + { + marker: "~", + feed: async (queryText) => { + const names = await server.get(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`); + + return names.map((name) => { + return { + id: `~${name}`, + name: name + }; + }); + }, + minimumCharacters: 0 + } +]; + + +interface AttributeEditorProps { + api: MutableRef; + note: FNote; + componentId: string; + notePath?: string | null; + ntxId?: string | null; + hidden?: boolean; +} + +export interface AttributeEditorImperativeHandlers { + save: () => Promise; + refresh: () => void; + renderOwnedAttributes: (ownedAttributes: FAttribute[]) => Promise; +} + +export default function AttributeEditor({ api, note, componentId, notePath, ntxId, hidden }: AttributeEditorProps) { + const [ currentValue, setCurrentValue ] = useState(""); + const [ state, setState ] = useState<"normal" | "showHelpTooltip" | "showAttributeDetail">(); + const [ error, setError ] = useState(); + const [ needsSaving, setNeedsSaving ] = useState(false); + + const lastSavedContent = useRef(); + const currentValueRef = useRef(currentValue); + const wrapperRef = useRef(null); + const editorRef = useRef(); + + const { showTooltip, hideTooltip } = useTooltip(wrapperRef, { + trigger: "focus", + html: true, + title: HELP_TEXT, + placement: "bottom", + offset: "0,30" + }); + + const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget()); + + useEffect(() => { + if (state === "showHelpTooltip") { + showTooltip(); + } else { + hideTooltip(); + } + }, [ state ]); + + async function renderOwnedAttributes(ownedAttributes: FAttribute[], saved: boolean) { + // attrs are not resorted if position changes after the initial load + ownedAttributes.sort((a, b) => a.position - b.position); + + let htmlAttrs = ("

    " + (await attribute_renderer.renderAttributes(ownedAttributes, true)).html() + "

    "); + + if (saved) { + lastSavedContent.current = htmlAttrs; + setNeedsSaving(false); + } + + if (htmlAttrs.length > 0) { + htmlAttrs += " "; + } + + editorRef.current?.setText(htmlAttrs); + setCurrentValue(htmlAttrs); + } + + function parseAttributes() { + try { + return attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current)); + } catch (e: unknown) { + setError(e); + } + } + + async function save() { + const attributes = parseAttributes(); + if (!attributes || !needsSaving) { + // An error occurred and will be reported to the user, or nothing to save. + return; + } + + await server.put(`notes/${note.noteId}/attributes`, attributes, componentId); + setNeedsSaving(false); + + // blink the attribute text to give a visual hint that save has been executed + if (wrapperRef.current) { + wrapperRef.current.style.opacity = "0"; + setTimeout(() => { + if (wrapperRef.current) { + wrapperRef.current.style.opacity = "1" + } + }, 100); + } + } + + async function handleAddNewAttributeCommand(command: AttributeCommandNames | undefined) { + // TODO: Not sure what the relation between FAttribute[] and Attribute[] is. + const attrs = parseAttributes() as FAttribute[]; + + if (!attrs) { + return; + } + + let type: AttributeType; + let name; + let value; + + if (command === "addNewLabel") { + type = "label"; + name = "myLabel"; + value = ""; + } else if (command === "addNewRelation") { + type = "relation"; + name = "myRelation"; + value = ""; + } else if (command === "addNewLabelDefinition") { + type = "label"; + name = "label:myLabel"; + value = "promoted,single,text"; + } else if (command === "addNewRelationDefinition") { + type = "label"; + name = "relation:myRelation"; + value = "promoted,single"; + } else { + return; + } + + //@ts-expect-error TODO: Incomplete type + attrs.push({ + type, + name, + value, + isInheritable: false + }); + + await renderOwnedAttributes(attrs, false); + + // this.$editor.scrollTop(this.$editor[0].scrollHeight); + const rect = wrapperRef.current?.getBoundingClientRect(); + + setTimeout(() => { + // showing a little bit later because there's a conflict with outside click closing the attr detail + attributeDetailWidget.showAttributeDetail({ + allAttributes: attrs, + attribute: attrs[attrs.length - 1], + isOwned: true, + x: rect ? (rect.left + rect.right) / 2 : 0, + y: rect?.bottom ?? 0, + focus: "name" + }); + }, 100); + } + + // Refresh with note + function refresh() { + renderOwnedAttributes(note.getOwnedAttributes(), true); + } + + useEffect(() => refresh(), [ note ]); + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) { + console.log("Trigger due to entities reloaded"); + refresh(); + } + }); + + // Focus on show. + useEffect(() => { + setTimeout(() => editorRef.current?.focus(), 0); + }, []); + + // Interaction with CKEditor. + useLegacyImperativeHandlers(useMemo(() => ({ + loadReferenceLinkTitle: async ($el: JQuery, href: string) => { + const { noteId } = link.parseNavigationStateFromUrl(href); + const note = noteId ? await froca.getNote(noteId, true) : null; + const title = note ? note.title : "[missing]"; + + $el.text(title); + }, + createNoteForReferenceLink: async (title: string) => { + let result; + if (notePath) { + result = await note_create.createNoteWithTypePrompt(notePath, { + activate: false, + title: title + }); + } + + return result?.note?.getBestNotePathString(); + } + }), [ notePath ])); + + // Keyboard shortcuts + useTriliumEvent("addNewLabel", ({ ntxId: eventNtxId }) => { + if (eventNtxId !== ntxId) return; + handleAddNewAttributeCommand("addNewLabel"); + }); + useTriliumEvent("addNewRelation", ({ ntxId: eventNtxId }) => { + if (eventNtxId !== ntxId) return; + handleAddNewAttributeCommand("addNewRelation"); + }); + + // Imperative API + useImperativeHandle(api, () => ({ + save, + refresh, + renderOwnedAttributes: (attributes) => renderOwnedAttributes(attributes as FAttribute[], false) + }), [ save, refresh, renderOwnedAttributes ]); + + return ( + <> + {!hidden &&
    { + if (e.key === "Enter") { + // allow autocomplete to fill the result textarea + setTimeout(() => save(), 100); + } + }} + > + { + currentValueRef.current = currentValue ?? ""; + + const oldValue = getPreprocessedData(lastSavedContent.current ?? "").trimEnd(); + const newValue = getPreprocessedData(currentValue ?? "").trimEnd(); + setNeedsSaving(oldValue !== newValue); + setError(undefined); + }} + onClick={(e, pos) => { + if (pos && pos.textNode && pos.textNode.data) { + const clickIndex = getClickIndex(pos); + + let parsedAttrs: Attribute[]; + + try { + parsedAttrs = attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current), true); + } catch (e: unknown) { + // the input is incorrect because the user messed up with it and now needs to fix it manually + console.log(e); + return null; + } + + let matchedAttr: Attribute | null = null; + + for (const attr of parsedAttrs) { + if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) { + matchedAttr = attr; + break; + } + } + + setTimeout(() => { + if (matchedAttr) { + attributeDetailWidget.showAttributeDetail({ + allAttributes: parsedAttrs, + attribute: matchedAttr, + isOwned: true, + x: e.pageX, + y: e.pageY + }); + setState("showAttributeDetail"); + } else { + setState("showHelpTooltip"); + } + }, 100); + } else { + setState("showHelpTooltip"); + } + }} + onKeyDown={() => attributeDetailWidget.hide()} + onBlur={() => save()} + disableNewlines disableSpellcheck + /> + + { needsSaving && } + + { + // Prevent automatic hiding of the context menu due to the button being clicked. + e.stopPropagation(); + + contextMenu.show({ + x: e.pageX, + y: e.pageY, + orientation: "left", + items: [ + { title: t("attribute_editor.add_new_label"), command: "addNewLabel", uiIcon: "bx bx-hash" }, + { title: t("attribute_editor.add_new_relation"), command: "addNewRelation", uiIcon: "bx bx-transfer" }, + { title: "----" }, + { title: t("attribute_editor.add_new_label_definition"), command: "addNewLabelDefinition", uiIcon: "bx bx-empty" }, + { title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" } + ], + selectMenuItemHandler: (item) => handleAddNewAttributeCommand(item.command) + }); + }} + /> + + { error && ( +
    + {getErrorMessage(error)} +
    + )} +
    } + + {attributeDetailWidgetEl} + + ) +} + +function getPreprocessedData(currentValue: string) { + const str = currentValue + .replace(/]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1") + .replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode + + return $("
    ").html(str).text(); +} + +function getClickIndex(pos: ModelPosition) { + let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0); + + let curNode: ModelNode | Text | ModelElement | null = pos.textNode; + + while (curNode?.previousSibling) { + curNode = curNode.previousSibling; + + if ((curNode as ModelElement).name === "reference") { + clickIndex += (curNode.getAttribute("href") as string).length + 1; + } else if ("data" in curNode) { + clickIndex += (curNode.data as string).length; + } + } + + return clickIndex; +} diff --git a/apps/client/src/widgets/ribbon/ribbon-interface.ts b/apps/client/src/widgets/ribbon/ribbon-interface.ts new file mode 100644 index 000000000..c40daf2c7 --- /dev/null +++ b/apps/client/src/widgets/ribbon/ribbon-interface.ts @@ -0,0 +1,13 @@ +import NoteContext from "../../components/note_context"; +import FNote from "../../entities/fnote"; + +export interface TabContext { + note: FNote | null | undefined; + hidden: boolean; + ntxId?: string | null; + hoistedNoteId?: string; + notePath?: string | null; + noteContext?: NoteContext; + componentId: string; + activate(): void; +} diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css new file mode 100644 index 000000000..3c6df6ba8 --- /dev/null +++ b/apps/client/src/widgets/ribbon/style.css @@ -0,0 +1,472 @@ +.ribbon-container { + margin-bottom: 5px; +} + +.ribbon-top-row { + display: flex; + min-height: 36px; +} + +.ribbon-tab-container { + display: flex; + flex-direction: row; + justify-content: center; + margin-left: 10px; + flex-grow: 1; + flex-flow: row wrap; +} + +.ribbon-tab-title { + color: var(--muted-text-color); + border-bottom: 1px solid var(--main-border-color); + min-width: 24px; + flex-basis: 24px; + max-width: max-content; + flex-grow: 10; + user-select: none; +} + +.ribbon-tab-title .bx { + font-size: 150%; + position: relative; + top: 3px; +} + +.ribbon-tab-title.active { + color: var(--main-text-color); + border-bottom: 3px solid var(--main-text-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ribbon-tab-title:hover { + cursor: pointer; +} + +.ribbon-tab-title:hover { + color: var(--main-text-color); +} + +.ribbon-tab-title:first-of-type { + padding-left: 10px; +} + +.ribbon-tab-spacer { + flex-basis: 0; + min-width: 0; + max-width: 35px; + flex-grow: 1; + border-bottom: 1px solid var(--main-border-color); +} + +.ribbon-tab-spacer:last-of-type { + flex-grow: 1; + flex-basis: 0; + min-width: 0; + max-width: 10000px; +} + +.ribbon-button-container { + display: flex; + border-bottom: 1px solid var(--main-border-color); + margin-right: 5px; +} + +.ribbon-button-container > * { + position: relative; + top: -3px; + margin-left: 10px; +} + +.ribbon-body { + border-bottom: 1px solid var(--main-border-color); + margin-left: 10px; + margin-right: 5px; /* needs to have this value so that the bottom border is the same width as the top one */ +} + +.ribbon-body.active { + display: block; +} + +.ribbon-tab-title.active .ribbon-tab-title-label { + display: inline; +} + +/* #region Basic Properties */ + +.basic-properties-widget { + padding: 0px 12px 6px 12px; + display: flex; + align-items: baseline; + flex-wrap: wrap; +} + +.basic-properties-widget > * { + margin-top: 9px; + margin-bottom: 2px; + margin-right: 30px; +} + +.note-type-container, +.editability-select-container, +.note-language-container { + display: flex; + align-items: center; +} + +.note-type-dropdown { + max-height: 500px; + overflow-y: auto; + overflow-x: hidden; +} + +.editability-dropdown { + width: 300px; +} + +/* #endregion */ + +/* #region Formatting Toolbar */ + +.classic-toolbar-widget { + --ck-color-toolbar-background: transparent; + --ck-color-button-default-background: transparent; + --ck-color-button-default-disabled-background: transparent; + min-height: 39px; +} + +.classic-toolbar-widget .ck.ck-toolbar { + border: none; +} + +.classic-toolbar-widget .ck.ck-button.ck-disabled { + opacity: 0.3; +} + +/* #endregion */ + +/* #region Script Tab */ +.script-runner-widget { + padding: 12px; + color: var(--muted-text-color); +} + +.execute-description { + margin-bottom: 10px; +} +/* #endregion */ + +/* #region Note info */ +.note-info-widget { + padding: 12px; +} + +.note-info-widget-table { + max-width: 100%; + display: block; + overflow-x: auto; + white-space: nowrap; +} + +.note-info-widget-table td, .note-info-widget-table th { + padding: 5px; +} + +.note-info-mime { + max-width: 13em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +/* #endregion */ + +/* #region Similar Notes */ +.similar-notes-wrapper { + max-height: 200px; + overflow: auto; + padding: 12px; +} + +.similar-notes-wrapper a { + display: inline-block; + border: 1px dotted var(--main-border-color); + border-radius: 20px; + background-color: var(--accented-background-color); + padding: 0 10px 0 10px; + margin: 0 3px 0 3px; + max-width: 10em; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} +/* #endregion */ + +/* #region File Properties */ +.file-table { + width: 100%; + margin-top: 10px; +} + +.file-table th, .file-table td { + padding: 5px; + overflow-wrap: anywhere; +} + +.file-buttons { + padding: 10px; + display: flex; + justify-content: space-evenly; +} +/* #endregion */ + +/* #region Note paths */ +.note-paths-widget { + padding: 12px; + max-height: 300px; + overflow-y: auto; +} + +.note-path-list { + margin-top: 10px; +} + +.note-path-list .path-current a { + font-weight: bold; +} + +.note-path-list .path-archived a { + color: var(--muted-text-color) !important; +} + +.note-path-list .path-search a { + font-style: italic; +} +/* #endregion */ + +/* #region Note map */ +.note-map-ribbon-widget { + position: relative; +} + +.note-map-ribbon-widget .note-map-container { + height: 100%; +} + +.note-map-ribbon-widget .open-full-button, .note-map-ribbon-widget .collapse-button { + position: absolute; + right: 5px; + bottom: 5px; + z-index: 1000; +} + +.style-resolver { + color: var(--muted-text-color); + display: none; +} +/* #endregion */ + +/* #region Attribute editor */ +.attribute-list-editor { + border: 0 !important; + outline: 0 !important; + box-shadow: none !important; + padding: 0 0 0 5px !important; + margin: 0 !important; + max-height: 100px; + overflow: auto; + transition: opacity .1s linear; +} + +.attribute-list-editor.ck-content .mention { + color: var(--muted-text-color) !important; + background: transparent !important; +} + +.save-attributes-button { + color: var(--muted-text-color); + position: absolute; + bottom: 14px; + right: 25px; + cursor: pointer; + border: 1px solid transparent; + font-size: 130%; +} + +.add-new-attribute-button { + color: var(--muted-text-color); + position: absolute; + bottom: 13px; + right: 0; + cursor: pointer; + border: 1px solid transparent; + font-size: 130%; +} + +.add-new-attribute-button:hover, .save-attributes-button:hover { + border: 1px solid var(--button-border-color); + border-radius: var(--button-border-radius); + background: var(--button-background-color); + color: var(--button-text-color); +} + +.attribute-errors { + color: red; + padding: 5px 50px 0px 5px; /* large right padding to avoid buttons */ +} +/* #endregion */ + +/* #region Owned Attributes */ +.attribute-list { + margin-left: 7px; + margin-right: 7px; + margin-top: 5px; + margin-bottom: 2px; + position: relative; +} + +.attribute-list-editor p { + margin: 0 !important; +} + +.attribute-list .attr-detail, +.inherited-attributes-widget .attr-detail { + contain: none; +} + +/* #endregion */ + +/* #region Inherited attributes */ +.inherited-attributes-widget { + position: relative; +} + +.inherited-attributes-container { + color: var(--muted-text-color); + max-height: 200px; + overflow: auto; + padding: 14px 12px 13px 12px; +} +/* #endregion */ + +/* #region Book properties */ +.book-properties-widget { + padding: 12px 12px 6px 12px; + display: flex; +} + +.book-properties-widget > * { + margin-right: 15px; +} + +.book-properties-container { + display: flex; + align-items: center; +} + +.book-properties-container > div { + margin-right: 15px; +} + +.book-properties-container > .type-number > label { + display: flex; + align-items: baseline; +} + +.book-properties-container input[type="checkbox"] { + margin-right: 5px; +} + +.book-properties-container label { + display: flex; + justify-content: center; + align-items: center; + text-overflow: clip; + white-space: nowrap; +} +/* #endregion */ + +/* #region Search definition */ +.search-setting-table { + margin-top: 0; + margin-bottom: 7px; + width: 100%; + border-collapse: separate; + border-spacing: 10px; +} + +.search-setting-table div { + white-space: nowrap; +} + +.search-setting-table .title-column { + /* minimal width so that table remains static sized and most space remains for middle column with settings */ + width: 50px; + white-space: nowrap; +} + +.search-setting-table .button-column { + /* minimal width so that table remains static sized and most space remains for middle column with settings */ + width: 50px; + white-space: nowrap; + text-align: right; + vertical-align: middle; +} + +.search-setting-table .button-column .dropdown { + display: inline-block !important; +} + +.search-setting-table .button-column .dropdown-menu { + white-space: normal; +} + +.search-setting-table .button-column > * { + vertical-align: middle; +} + +.attribute-list hr { + height: 1px; + border-color: var(--main-border-color); + position: relative; + top: 4px; + margin-top: 5px; + margin-bottom: 0; +} + +.search-definition-widget input:invalid { + border: 3px solid red; +} + +.add-search-option button { + margin: 3px; +} + +.dropdown-header { + background-color: var(--accented-background-color); +} +/* #endregion */ + +/* #region Note actions */ +.note-actions { + width: 35px; + height: 35px; +} + +.note-actions .dropdown-menu { + min-width: 15em; +} + +.note-actions .dropdown-item .bx { + position: relative; + top: 3px; + font-size: 120%; + margin-right: 5px; +} + +.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover { + color: var(--muted-text-color) !important; + background-color: transparent !important; + pointer-events: none; /* makes it unclickable */ +} +/* #endregion */ \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon_widgets/basic_properties.ts b/apps/client/src/widgets/ribbon_widgets/basic_properties.ts deleted file mode 100644 index b78e80a8d..000000000 --- a/apps/client/src/widgets/ribbon_widgets/basic_properties.ts +++ /dev/null @@ -1,129 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import NoteTypeWidget from "../note_type.js"; -import ProtectedNoteSwitchWidget from "../protected_note_switch.js"; -import EditabilitySelectWidget from "../editability_select.js"; -import BookmarkSwitchWidget from "../bookmark_switch.js"; -import SharedSwitchWidget from "../shared_switch.js"; -import { t } from "../../services/i18n.js"; -import TemplateSwitchWidget from "../template_switch.js"; -import type FNote from "../../entities/fnote.js"; -import NoteLanguageWidget from "../note_language.js"; - -const TPL = /*html*/` -
    - - -
    - ${t("basic_properties.note_type")}:   -
    - -
    - -
    - ${t("basic_properties.editable")}:   -
    - -
    - -
    - -
    - -
    - ${t("basic_properties.language")}:   -
    -
    `; - -export default class BasicPropertiesWidget extends NoteContextAwareWidget { - - private noteTypeWidget: NoteTypeWidget; - private protectedNoteSwitchWidget: ProtectedNoteSwitchWidget; - private editabilitySelectWidget: EditabilitySelectWidget; - private bookmarkSwitchWidget: BookmarkSwitchWidget; - private sharedSwitchWidget: SharedSwitchWidget; - private templateSwitchWidget: TemplateSwitchWidget; - private noteLanguageWidget: NoteLanguageWidget; - - constructor() { - super(); - - this.noteTypeWidget = new NoteTypeWidget().contentSized(); - this.protectedNoteSwitchWidget = new ProtectedNoteSwitchWidget().contentSized(); - this.editabilitySelectWidget = new EditabilitySelectWidget().contentSized(); - this.bookmarkSwitchWidget = new BookmarkSwitchWidget().contentSized(); - this.sharedSwitchWidget = new SharedSwitchWidget().contentSized(); - this.templateSwitchWidget = new TemplateSwitchWidget().contentSized(); - this.noteLanguageWidget = new NoteLanguageWidget().contentSized(); - - this.child( - this.noteTypeWidget, - this.protectedNoteSwitchWidget, - this.editabilitySelectWidget, - this.bookmarkSwitchWidget, - this.sharedSwitchWidget, - this.templateSwitchWidget, - this.noteLanguageWidget); - } - - get name() { - return "basicProperties"; - } - - get toggleCommand() { - return "toggleRibbonBasicProperties"; - } - - getTitle() { - return { - show: !this.note?.isLaunchBarConfig(), - title: t("basic_properties.basic_properties"), - icon: "bx bx-slider" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$widget.find(".note-type-container").append(this.noteTypeWidget.render()); - this.$widget.find(".protected-note-switch-container").append(this.protectedNoteSwitchWidget.render()); - this.$widget.find(".editability-select-container").append(this.editabilitySelectWidget.render()); - this.$widget.find(".bookmark-switch-container").append(this.bookmarkSwitchWidget.render()); - this.$widget.find(".shared-switch-container").append(this.sharedSwitchWidget.render()); - this.$widget.find(".template-switch-container").append(this.templateSwitchWidget.render()); - this.$widget.find(".note-language-container").append(this.noteLanguageWidget.render()); - } - - async refreshWithNote(note: FNote) { - await super.refreshWithNote(note); - if (!this.note) { - return; - } - - this.$widget.find(".editability-select-container").toggle(this.note && ["text", "code", "mermaid"].includes(this.note.type)); - this.$widget.find(".note-language-container").toggle(this.note && ["text"].includes(this.note.type)); - } -} diff --git a/apps/client/src/widgets/ribbon_widgets/book_properties.ts b/apps/client/src/widgets/ribbon_widgets/book_properties.ts deleted file mode 100644 index 47c1a83f1..000000000 --- a/apps/client/src/widgets/ribbon_widgets/book_properties.ts +++ /dev/null @@ -1,262 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import attributeService from "../../services/attributes.js"; -import { t } from "../../services/i18n.js"; -import type FNote from "../../entities/fnote.js"; -import type { EventData } from "../../components/app_context.js"; -import { bookPropertiesConfig, BookProperty } from "./book_properties_config.js"; -import attributes from "../../services/attributes.js"; -import type { ViewTypeOptions } from "../../services/note_list_renderer.js"; - -const VIEW_TYPE_MAPPINGS: Record = { - grid: t("book_properties.grid"), - list: t("book_properties.list"), - calendar: t("book_properties.calendar"), - table: t("book_properties.table"), - geoMap: t("book_properties.geo-map"), - board: t("book_properties.board") -}; - -const TPL = /*html*/` -
    - - -
    - ${t("book_properties.view_type")}:    - - -
    - -
    -
    -
    -`; - -export default class BookPropertiesWidget extends NoteContextAwareWidget { - - private $viewTypeSelect!: JQuery; - private $propertiesContainer!: JQuery; - private labelsToWatch: string[] = []; - - get name() { - return "bookProperties"; - } - - get toggleCommand() { - return "toggleRibbonTabBookProperties"; - } - - isEnabled() { - return this.note && this.note.type === "book"; - } - - getTitle() { - return { - show: this.isEnabled(), - title: t("book_properties.book_properties"), - icon: "bx bx-book" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$viewTypeSelect = this.$widget.find(".view-type-select"); - this.$viewTypeSelect.on("change", () => this.toggleViewType(String(this.$viewTypeSelect.val()))); - - this.$propertiesContainer = this.$widget.find(".book-properties-container"); - } - - async refreshWithNote(note: FNote) { - if (!this.note) { - return; - } - - const viewType = this.note.getLabelValue("viewType") || "grid"; - - this.$viewTypeSelect.val(viewType); - - this.$propertiesContainer.empty(); - - const bookPropertiesData = bookPropertiesConfig[viewType]; - if (bookPropertiesData) { - for (const property of bookPropertiesData.properties) { - this.$propertiesContainer.append(this.renderBookProperty(property)); - this.labelsToWatch.push(property.bindToLabel); - } - } - } - - async toggleViewType(type: string) { - if (!this.noteId) { - return; - } - - if (!VIEW_TYPE_MAPPINGS.hasOwnProperty(type)) { - throw new Error(t("book_properties.invalid_view_type", { type })); - } - - await attributeService.setLabel(this.noteId, "viewType", type); - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.getAttributeRows().find((attr) => - attr.noteId === this.noteId - && (attr.name === "viewType" || this.labelsToWatch.includes(attr.name ?? "")))) { - this.refresh(); - } - } - - renderBookProperty(property: BookProperty) { - const $container = $("
    "); - $container.addClass(`type-${property.type}`); - const note = this.note; - if (!note) { - return $container; - } - switch (property.type) { - case "checkbox": - const $label = $("
    - - - -
    `; - -export default class FilePropertiesWidget extends NoteContextAwareWidget { - - private $fileNoteId!: JQuery; - private $fileName!: JQuery; - private $fileType!: JQuery; - private $fileSize!: JQuery; - private $downloadButton!: JQuery; - private $openButton!: JQuery; - private $uploadNewRevisionButton!: JQuery; - private $uploadNewRevisionInput!: JQuery; - - get name() { - return "fileProperties"; - } - - get toggleCommand() { - return "toggleRibbonTabFileProperties"; - } - - isEnabled() { - return this.note && this.note.type === "file"; - } - - getTitle() { - return { - show: this.isEnabled(), - activate: true, - title: t("file_properties.title"), - icon: "bx bx-file" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - this.$fileNoteId = this.$widget.find(".file-note-id"); - this.$fileName = this.$widget.find(".file-filename"); - this.$fileType = this.$widget.find(".file-filetype"); - this.$fileSize = this.$widget.find(".file-filesize"); - this.$downloadButton = this.$widget.find(".file-download"); - this.$openButton = this.$widget.find(".file-open"); - this.$uploadNewRevisionButton = this.$widget.find(".file-upload-new-revision"); - this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input"); - - this.$downloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId)); - this.$openButton.on("click", () => this.noteId && this.note && openService.openNoteExternally(this.noteId, this.note.mime)); - - this.$uploadNewRevisionButton.on("click", () => { - this.$uploadNewRevisionInput.trigger("click"); - }); - - this.$uploadNewRevisionInput.on("change", async () => { - const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below - this.$uploadNewRevisionInput.val(""); - - const result = await server.upload(`notes/${this.noteId}/file`, fileToUpload); - - if (result.uploaded) { - toastService.showMessage(t("file_properties.upload_success")); - - this.refresh(); - } else { - toastService.showError(t("file_properties.upload_failed")); - } - }); - } - - async refreshWithNote(note: FNote) { - this.$widget.show(); - - if (!this.note) { - return; - } - - this.$fileNoteId.text(note.noteId); - this.$fileName.text(note.getLabelValue("originalFileName") || "?"); - this.$fileType.text(note.mime); - - const blob = await this.note.getBlob(); - - this.$fileSize.text(utils.formatSize(blob?.contentLength ?? 0)); - - // open doesn't work for protected notes since it works through a browser which isn't in protected session - this.$openButton.toggle(!note.isProtected); - this.$downloadButton.toggle(!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()); - this.$uploadNewRevisionButton.toggle(!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()); - } -} diff --git a/apps/client/src/widgets/ribbon_widgets/image_properties.ts b/apps/client/src/widgets/ribbon_widgets/image_properties.ts deleted file mode 100644 index 7412561e7..000000000 --- a/apps/client/src/widgets/ribbon_widgets/image_properties.ts +++ /dev/null @@ -1,136 +0,0 @@ -import server from "../../services/server.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import toastService from "../../services/toast.js"; -import openService from "../../services/open.js"; -import utils from "../../services/utils.js"; -import { t } from "../../services/i18n.js"; -import type FNote from "../../entities/fnote.js"; - -const TPL = /*html*/` -
    -
    - - ${t("image_properties.original_file_name")}: - - - - - ${t("image_properties.file_type")}: - - - - - ${t("image_properties.file_size")}: - - -
    - -
    - - - - - - - -
    - - -
    `; - -export default class ImagePropertiesWidget extends NoteContextAwareWidget { - - private $copyReferenceToClipboardButton!: JQuery; - private $uploadNewRevisionButton!: JQuery; - private $uploadNewRevisionInput!: JQuery; - private $fileName!: JQuery; - private $fileType!: JQuery; - private $fileSize!: JQuery; - private $openButton!: JQuery; - private $imageDownloadButton!: JQuery; - - get name() { - return "imageProperties"; - } - - get toggleCommand() { - return "toggleRibbonTabImageProperties"; - } - - isEnabled() { - return this.note && this.note.type === "image"; - } - - getTitle() { - return { - show: this.isEnabled(), - activate: true, - title: t("image_properties.title"), - icon: "bx bx-image" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$copyReferenceToClipboardButton = this.$widget.find(".image-copy-reference-to-clipboard"); - this.$copyReferenceToClipboardButton.on("click", () => this.triggerEvent(`copyImageReferenceToClipboard`, { ntxId: this.noteContext?.ntxId })); - - this.$uploadNewRevisionButton = this.$widget.find(".image-upload-new-revision"); - this.$uploadNewRevisionInput = this.$widget.find(".image-upload-new-revision-input"); - - this.$fileName = this.$widget.find(".image-filename"); - this.$fileType = this.$widget.find(".image-filetype"); - this.$fileSize = this.$widget.find(".image-filesize"); - - this.$openButton = this.$widget.find(".image-open"); - this.$openButton.on("click", () => this.noteId && this.note && openService.openNoteExternally(this.noteId, this.note.mime)); - - this.$imageDownloadButton = this.$widget.find(".image-download"); - this.$imageDownloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId)); - - this.$uploadNewRevisionButton.on("click", () => { - this.$uploadNewRevisionInput.trigger("click"); - }); - - this.$uploadNewRevisionInput.on("change", async () => { - const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below - this.$uploadNewRevisionInput.val(""); - - const result = await server.upload(`images/${this.noteId}`, fileToUpload); - - if (result.uploaded) { - toastService.showMessage(t("image_properties.upload_success")); - - await utils.clearBrowserCache(); - - this.refresh(); - } else { - toastService.showError(t("image_properties.upload_failed", { message: result.message })); - } - }); - } - - async refreshWithNote(note: FNote) { - this.$widget.show(); - - const blob = await this.note?.getBlob(); - - this.$fileName.text(note.getLabelValue("originalFileName") || "?"); - this.$fileSize.text(utils.formatSize(blob?.contentLength ?? 0)); - this.$fileType.text(note.mime); - } -} diff --git a/apps/client/src/widgets/ribbon_widgets/inherited_attribute_list.ts b/apps/client/src/widgets/ribbon_widgets/inherited_attribute_list.ts deleted file mode 100644 index 2e2ac4615..000000000 --- a/apps/client/src/widgets/ribbon_widgets/inherited_attribute_list.ts +++ /dev/null @@ -1,119 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import AttributeDetailWidget from "../attribute_widgets/attribute_detail.js"; -import attributeRenderer from "../../services/attribute_renderer.js"; -import attributeService from "../../services/attributes.js"; -import { t } from "../../services/i18n.js"; -import type FNote from "../../entities/fnote.js"; -import type { EventData } from "../../components/app_context.js"; - -const TPL = /*html*/` -
    - - -
    -
    `; - -export default class InheritedAttributesWidget extends NoteContextAwareWidget { - - private attributeDetailWidget: AttributeDetailWidget; - - private $container!: JQuery; - - get name() { - return "inheritedAttributes"; - } - - get toggleCommand() { - return "toggleRibbonTabInheritedAttributes"; - } - - constructor() { - super(); - - this.attributeDetailWidget = new AttributeDetailWidget().contentSized().setParent(this); - - this.child(this.attributeDetailWidget); - } - - getTitle() { - return { - show: !this.note?.isLaunchBarConfig(), - title: t("inherited_attribute_list.title"), - icon: "bx bx-list-plus" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$container = this.$widget.find(".inherited-attributes-container"); - this.$widget.append(this.attributeDetailWidget.render()); - } - - async refreshWithNote(note: FNote) { - this.$container.empty(); - - const inheritedAttributes = this.getInheritedAttributes(note); - - if (inheritedAttributes.length === 0) { - this.$container.append(t("inherited_attribute_list.no_inherited_attributes")); - return; - } - - for (const attribute of inheritedAttributes) { - const $attr = (await attributeRenderer.renderAttribute(attribute, false)).on("click", (e) => { - setTimeout( - () => - this.attributeDetailWidget.showAttributeDetail({ - attribute: { - noteId: attribute.noteId, - type: attribute.type, - name: attribute.name, - value: attribute.value, - isInheritable: attribute.isInheritable - }, - isOwned: false, - x: e.pageX, - y: e.pageY - }), - 100 - ); - }); - - this.$container.append($attr).append(" "); - } - } - - getInheritedAttributes(note: FNote) { - const attrs = note.getAttributes().filter((attr) => attr.noteId !== this.noteId); - - attrs.sort((a, b) => { - if (a.noteId === b.noteId) { - return a.position - b.position; - } else { - // inherited attributes should stay grouped: https://github.com/zadam/trilium/issues/3761 - return a.noteId < b.noteId ? -1 : 1; - } - }); - - return attrs; - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) { - this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/ribbon_widgets/note_info_widget.ts b/apps/client/src/widgets/ribbon_widgets/note_info_widget.ts deleted file mode 100644 index efe95007d..000000000 --- a/apps/client/src/widgets/ribbon_widgets/note_info_widget.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { formatDateTime } from "../../utils/formatters.js"; -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import server from "../../services/server.js"; -import utils from "../../services/utils.js"; -import type { EventData } from "../../components/app_context.js"; -import type FNote from "../../entities/fnote.js"; - -const TPL = /*html*/` -
    - - - - - - - - - - - - - - - - - - -
    ${t("note_info_widget.note_id")}:${t("note_info_widget.created")}:${t("note_info_widget.modified")}:
    ${t("note_info_widget.type")}: - - - ${t("note_info_widget.note_size")}: - - - - - -
    -
    -`; - -// TODO: Deduplicate with server -interface NoteSizeResponse { - noteSize: number; -} - -interface SubtreeSizeResponse { - subTreeNoteCount: number; - subTreeSize: number; -} - -interface MetadataResponse { - dateCreated: number; - dateModified: number; -} - -export default class NoteInfoWidget extends NoteContextAwareWidget { - - private $noteId!: JQuery; - private $dateCreated!: JQuery; - private $dateModified!: JQuery; - private $type!: JQuery; - private $mime!: JQuery; - private $noteSizesWrapper!: JQuery; - private $noteSize!: JQuery; - private $subTreeSize!: JQuery; - private $calculateButton!: JQuery; - - get name() { - return "noteInfo"; - } - - get toggleCommand() { - return "toggleRibbonTabNoteInfo"; - } - - isEnabled() { - return !!this.note; - } - - getTitle() { - return { - show: this.isEnabled(), - title: t("note_info_widget.title"), - icon: "bx bx-info-circle" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$noteId = this.$widget.find(".note-info-note-id"); - this.$dateCreated = this.$widget.find(".note-info-date-created"); - this.$dateModified = this.$widget.find(".note-info-date-modified"); - this.$type = this.$widget.find(".note-info-type"); - this.$mime = this.$widget.find(".note-info-mime"); - - this.$noteSizesWrapper = this.$widget.find(".note-sizes-wrapper"); - this.$noteSize = this.$widget.find(".note-size"); - this.$subTreeSize = this.$widget.find(".subtree-size"); - - this.$calculateButton = this.$widget.find(".calculate-button"); - this.$calculateButton.on("click", async () => { - this.$noteSizesWrapper.show(); - this.$calculateButton.hide(); - - this.$noteSize.empty().append($('')); - this.$subTreeSize.empty().append($('')); - - const noteSizeResp = await server.get(`stats/note-size/${this.noteId}`); - this.$noteSize.text(utils.formatSize(noteSizeResp.noteSize)); - - const subTreeResp = await server.get(`stats/subtree-size/${this.noteId}`); - - if (subTreeResp.subTreeNoteCount > 1) { - this.$subTreeSize.text(t("note_info_widget.subtree_size", { size: utils.formatSize(subTreeResp.subTreeSize), count: subTreeResp.subTreeNoteCount })); - } else { - this.$subTreeSize.text(""); - } - }); - } - - async refreshWithNote(note: FNote) { - const metadata = await server.get(`notes/${this.noteId}/metadata`); - - this.$noteId.text(note.noteId); - this.$dateCreated.text(formatDateTime(metadata.dateCreated)).attr("title", metadata.dateCreated); - - this.$dateModified.text(formatDateTime(metadata.dateModified)).attr("title", metadata.dateModified); - - this.$type.text(note.type); - - if (note.mime) { - this.$mime.text(`(${note.mime})`); - } else { - this.$mime.empty(); - } - - this.$calculateButton.show(); - this.$noteSizesWrapper.hide(); - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (this.noteId && (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId))) { - this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/ribbon_widgets/note_map.ts b/apps/client/src/widgets/ribbon_widgets/note_map.ts deleted file mode 100644 index b47de7b95..000000000 --- a/apps/client/src/widgets/ribbon_widgets/note_map.ts +++ /dev/null @@ -1,131 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import NoteMapWidget from "../note_map.js"; -import { t } from "../../services/i18n.js"; - -const TPL = /*html*/` -
    - - - - - -
    -
    `; - -export default class NoteMapRibbonWidget extends NoteContextAwareWidget { - - private openState!: "small" | "full"; - private noteMapWidget: NoteMapWidget; - private $container!: JQuery; - private $openFullButton!: JQuery; - private $collapseButton!: JQuery; - - constructor() { - super(); - - this.noteMapWidget = new NoteMapWidget("ribbon"); - this.child(this.noteMapWidget); - } - - get name() { - return "noteMap"; - } - - get toggleCommand() { - return "toggleRibbonTabNoteMap"; - } - - getTitle() { - return { - show: this.isEnabled(), - title: t("note_map.title"), - icon: "bx bxs-network-chart" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - this.$container = this.$widget.find(".note-map-container"); - this.$container.append(this.noteMapWidget.render()); - - this.openState = "small"; - - this.$openFullButton = this.$widget.find(".open-full-button"); - this.$openFullButton.on("click", () => { - this.setFullHeight(); - - this.$openFullButton.hide(); - this.$collapseButton.show(); - - this.openState = "full"; - - this.noteMapWidget.setDimensions(); - }); - - this.$collapseButton = this.$widget.find(".collapse-button"); - this.$collapseButton.on("click", () => { - this.setSmallSize(); - - this.$openFullButton.show(); - this.$collapseButton.hide(); - - this.openState = "small"; - - this.noteMapWidget.setDimensions(); - }); - - const handleResize = () => { - if (!this.noteMapWidget.graph) { - // no graph has been even rendered - return; - } - - if (this.openState === "full") { - this.setFullHeight(); - } else if (this.openState === "small") { - this.setSmallSize(); - } - }; - - new ResizeObserver(handleResize).observe(this.$widget[0]); - } - - setSmallSize() { - const SMALL_SIZE_HEIGHT = 300; - const width = this.$widget.width() ?? 0; - - this.$widget.find(".note-map-container").height(SMALL_SIZE_HEIGHT).width(width); - } - - setFullHeight() { - const { top } = this.$widget[0].getBoundingClientRect(); - - const height = ($(window).height() ?? 0) - top; - const width = this.$widget.width() ?? 0; - - this.$widget.find(".note-map-container") - .height(height) - .width(width); - } -} diff --git a/apps/client/src/widgets/ribbon_widgets/note_paths.ts b/apps/client/src/widgets/ribbon_widgets/note_paths.ts deleted file mode 100644 index 8681bfd21..000000000 --- a/apps/client/src/widgets/ribbon_widgets/note_paths.ts +++ /dev/null @@ -1,154 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import treeService from "../../services/tree.js"; -import linkService from "../../services/link.js"; -import { t } from "../../services/i18n.js"; -import type FNote from "../../entities/fnote.js"; -import type { NotePathRecord } from "../../entities/fnote.js"; -import type { EventData } from "../../components/app_context.js"; - -const TPL = /*html*/` -
    - - -
    - -
      - - -
      `; - -export default class NotePathsWidget extends NoteContextAwareWidget { - - private $notePathIntro!: JQuery; - private $notePathList!: JQuery; - - get name() { - return "notePaths"; - } - - get toggleCommand() { - return "toggleRibbonTabNotePaths"; - } - - getTitle() { - return { - show: true, - title: t("note_paths.title"), - icon: "bx bx-collection" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$notePathIntro = this.$widget.find(".note-path-intro"); - this.$notePathList = this.$widget.find(".note-path-list"); - } - - async refreshWithNote(note: FNote) { - this.$notePathList.empty(); - - if (!this.note || this.noteId === "root") { - this.$notePathList.empty().append(await this.getRenderedPath(["root"])); - - return; - } - - const sortedNotePaths = this.note.getSortedNotePathRecords(this.hoistedNoteId).filter((notePath) => !notePath.isHidden); - - if (sortedNotePaths.length > 0) { - this.$notePathIntro.text(t("note_paths.intro_placed")); - } else { - this.$notePathIntro.text(t("note_paths.intro_not_placed")); - } - - const renderedPaths: JQuery[] = []; - - for (const notePathRecord of sortedNotePaths) { - const notePath = notePathRecord.notePath; - - renderedPaths.push(await this.getRenderedPath(notePath, notePathRecord)); - } - - this.$notePathList.empty().append(...renderedPaths); - } - - async getRenderedPath(notePath: string[], notePathRecord: NotePathRecord | null = null) { - const $pathItem = $("
    • "); - const pathSegments: string[] = []; - const lastIndex = notePath.length - 1; - - for (let i = 0; i < notePath.length; i++) { - const noteId = notePath[i]; - pathSegments.push(noteId); - const title = await treeService.getNoteTitle(noteId); - const $noteLink = await linkService.createLink(pathSegments.join("/"), { title }); - - $noteLink.find("a").addClass("no-tooltip-preview tn-link"); - $pathItem.append($noteLink); - - if (i != lastIndex) { - $pathItem.append(" / "); - } - } - - const icons: string[] = []; - - if (this.notePath === notePath.join("/")) { - $pathItem.addClass("path-current"); - } - - if (!notePathRecord || notePathRecord.isInHoistedSubTree) { - $pathItem.addClass("path-in-hoisted-subtree"); - } else { - icons.push(``); - } - - if (notePathRecord?.isArchived) { - $pathItem.addClass("path-archived"); - - icons.push(``); - } - - if (notePathRecord?.isSearch) { - $pathItem.addClass("path-search"); - - icons.push(``); - } - - if (icons.length > 0) { - $pathItem.append(` ${icons.join(" ")}`); - } - - return $pathItem; - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) || (this.noteId != null && loadResults.isNoteReloaded(this.noteId))) { - this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/ribbon_widgets/note_properties.ts b/apps/client/src/widgets/ribbon_widgets/note_properties.ts deleted file mode 100644 index eb104411d..000000000 --- a/apps/client/src/widgets/ribbon_widgets/note_properties.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type FNote from "../../entities/fnote.js"; -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; - -const TPL = /*html*/` -
      - - -
      - ${t("note_properties.this_note_was_originally_taken_from")} -
      -
      `; - -/** - * TODO: figure out better name or conceptualize better. - */ -export default class NotePropertiesWidget extends NoteContextAwareWidget { - - private $pageUrl!: JQuery; - - isEnabled() { - return this.note && !!this.note.getLabelValue("pageUrl"); - } - - getTitle() { - return { - show: this.isEnabled(), - activate: true, - title: t("note_properties.info"), - icon: "bx bx-info-square" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$pageUrl = this.$widget.find(".page-url"); - } - - async refreshWithNote(note: FNote) { - const pageUrl = note.getLabelValue("pageUrl"); - - this.$pageUrl - .attr("href", pageUrl) - .attr("title", pageUrl) - .text(pageUrl ?? ""); - } -} diff --git a/apps/client/src/widgets/ribbon_widgets/owned_attribute_list.ts b/apps/client/src/widgets/ribbon_widgets/owned_attribute_list.ts deleted file mode 100644 index dab466569..000000000 --- a/apps/client/src/widgets/ribbon_widgets/owned_attribute_list.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import AttributeDetailWidget from "../attribute_widgets/attribute_detail.js"; -import AttributeEditorWidget from "../attribute_widgets/attribute_editor.js"; -import type { CommandListenerData } from "../../components/app_context.js"; -import type FAttribute from "../../entities/fattribute.js"; - -const TPL = /*html*/` -
      - - -
      -
      -`; - -export default class OwnedAttributeListWidget extends NoteContextAwareWidget { - - private attributeDetailWidget: AttributeDetailWidget; - private attributeEditorWidget: AttributeEditorWidget; - private $title!: JQuery; - - get name() { - return "ownedAttributes"; - } - - get toggleCommand() { - return "toggleRibbonTabOwnedAttributes"; - } - - constructor() { - super(); - - this.attributeDetailWidget = new AttributeDetailWidget().contentSized().setParent(this); - - this.attributeEditorWidget = new AttributeEditorWidget(this.attributeDetailWidget).contentSized().setParent(this); - - this.child(this.attributeEditorWidget, this.attributeDetailWidget); - } - - getTitle() { - return { - show: !this.note?.isLaunchBarConfig(), - title: t("owned_attribute_list.owned_attributes"), - icon: "bx bx-list-check" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$widget.find(".attr-editor-placeholder").replaceWith(this.attributeEditorWidget.render()); - this.$widget.append(this.attributeDetailWidget.render()); - - this.$title = $("
      "); - } - - async saveAttributesCommand() { - await this.attributeEditorWidget.save(); - } - - async reloadAttributesCommand() { - await this.attributeEditorWidget.refresh(); - } - - async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) { - // TODO: See why we need FAttribute[] and Attribute[] - await this.attributeEditorWidget.updateAttributeList(attributes as FAttribute[]); - } - - focus() { - this.attributeEditorWidget.focus(); - } -} diff --git a/apps/client/src/widgets/ribbon_widgets/script_executor.ts b/apps/client/src/widgets/ribbon_widgets/script_executor.ts deleted file mode 100644 index 3b0b96278..000000000 --- a/apps/client/src/widgets/ribbon_widgets/script_executor.ts +++ /dev/null @@ -1,76 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import keyboardActionService from "../../services/keyboard_actions.js"; -import { t } from "../../services/i18n.js"; -import type FNote from "../../entities/fnote.js"; - -const TPL = /*html*/` -
      - - -
      - -
      - -
      -
      `; - -export default class ScriptExecutorWidget extends NoteContextAwareWidget { - - private $executeButton!: JQuery; - private $executeDescription!: JQuery; - - isEnabled() { - return ( - super.isEnabled() && - this.note && - (this.note.mime.startsWith("application/javascript") || this.isTriliumSqlite()) && - (this.note.hasLabel("executeDescription") || this.note.hasLabel("executeButton")) - ); - } - - isTriliumSqlite() { - return this.note?.mime === "text/x-sqlite;schema=trilium"; - } - - getTitle() { - return { - show: this.isEnabled(), - activate: true, - title: this.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"), - icon: "bx bx-play" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$executeButton = this.$widget.find(".execute-button"); - this.$executeDescription = this.$widget.find(".execute-description"); - } - - async refreshWithNote(note: FNote) { - const executeTitle = note.getLabelValue("executeButton") || (this.isTriliumSqlite() ? t("script_executor.execute_query") : t("script_executor.execute_script")); - - this.$executeButton.text(executeTitle); - this.$executeButton.attr("title", executeTitle); - keyboardActionService.updateDisplayedShortcuts(this.$widget); - - const executeDescription = note.getLabelValue("executeDescription"); - - if (executeDescription) { - this.$executeDescription.show().html(executeDescription); - } else { - this.$executeDescription.empty().hide(); - } - } -} diff --git a/apps/client/src/widgets/ribbon_widgets/search_definition.ts b/apps/client/src/widgets/ribbon_widgets/search_definition.ts deleted file mode 100644 index 2439b590f..000000000 --- a/apps/client/src/widgets/ribbon_widgets/search_definition.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { t } from "../../services/i18n.js"; -import server from "../../services/server.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import froca from "../../services/froca.js"; -import ws from "../../services/ws.js"; -import toastService from "../../services/toast.js"; -import treeService from "../../services/tree.js"; - -import SearchString from "../search_options/search_string.js"; -import FastSearch from "../search_options/fast_search.js"; -import Ancestor from "../search_options/ancestor.js"; -import IncludeArchivedNotes from "../search_options/include_archived_notes.js"; -import OrderBy from "../search_options/order_by.js"; -import SearchScript from "../search_options/search_script.js"; -import Limit from "../search_options/limit.js"; -import Debug from "../search_options/debug.js"; -import appContext, { type EventData } from "../../components/app_context.js"; -import bulkActionService from "../../services/bulk_action.js"; -import { Dropdown } from "bootstrap"; -import type FNote from "../../entities/fnote.js"; -import type { AttributeType } from "../../entities/fattribute.js"; -import { renderReactWidget } from "../react/ReactBasicWidget.jsx"; - -const TPL = /*html*/` -
      - - -
      - - - - - - - - - - - - -
      ${t("search_definition.add_search_option")} - - - - - - - - - - - - - - - - - -
      -
      - - - - - -
      -
      -
      -
      `; - -const OPTION_CLASSES = [SearchString, SearchScript, Ancestor, FastSearch, IncludeArchivedNotes, OrderBy, Limit, Debug]; - -// TODO: Deduplicate with server -interface SaveSearchNoteResponse { - notePath: string; -} - -export default class SearchDefinitionWidget extends NoteContextAwareWidget { - - private $component!: JQuery; - private $actionList!: JQuery; - private $searchOptions!: JQuery; - private $searchButton!: JQuery; - private $searchAndExecuteButton!: JQuery; - private $saveToNoteButton!: JQuery; - private $actionOptions!: JQuery; - - get name() { - return "searchDefinition"; - } - - isEnabled() { - return this.note && this.note.type === "search"; - } - - getTitle() { - return { - show: this.isEnabled(), - activate: true, - title: t("search_definition.search_parameters"), - icon: "bx bx-search" - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - this.$component = this.$widget.find(".search-definition-widget"); - this.$actionList = this.$widget.find(".action-list"); - - for (const actionGroup of bulkActionService.ACTION_GROUPS) { - this.$actionList.append($('