mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 06:54:23 +01:00
Compare commits
74 Commits
a92d0e4230
...
7f4d8bc8ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f4d8bc8ca | ||
|
|
732494dfc5 | ||
|
|
b8748b856a | ||
|
|
cc71f15700 | ||
|
|
124ef640b1 | ||
|
|
f5e3df0cd2 | ||
|
|
c8431181c8 | ||
|
|
07fb5ab017 | ||
|
|
6735b257b4 | ||
|
|
cef242a9ce | ||
|
|
2923d917e5 | ||
|
|
9a76a9069c | ||
|
|
8e1d796870 | ||
|
|
8b0d4e5c3b | ||
|
|
8ee59e9daa | ||
|
|
4e5c26371e | ||
|
|
436146d829 | ||
|
|
315fcecf57 | ||
|
|
0a57e6e154 | ||
|
|
bdcb84a394 | ||
|
|
213c36ba84 | ||
|
|
995e765276 | ||
|
|
8e81c38c14 | ||
|
|
7378fa4cbd | ||
|
|
af8a5ff0c9 | ||
|
|
eca5a4a072 | ||
|
|
71b3ad5027 | ||
|
|
7864168adc | ||
|
|
f8090d9217 | ||
|
|
09aa22c74b | ||
|
|
8fc8f97879 | ||
|
|
547cdff510 | ||
|
|
cdd08d6971 | ||
|
|
b3cf9c8f2d | ||
|
|
feefa389b4 | ||
|
|
f65be4f368 | ||
|
|
77a014109e | ||
|
|
d3db48c99b | ||
|
|
7e4833e893 | ||
|
|
470d7eee31 | ||
|
|
aada631c0f | ||
|
|
bc4186d216 | ||
|
|
c2a27eff2c | ||
|
|
ca24408a13 | ||
|
|
b9e19e524a | ||
|
|
09c8a778f5 | ||
|
|
3438f1103d | ||
|
|
82a3be06d1 | ||
|
|
f0dead5390 | ||
|
|
b0fdb9fef2 | ||
|
|
71009bddc7 | ||
|
|
66e499a2e1 | ||
|
|
a5ef5eee2f | ||
|
|
bcb29d22f5 | ||
|
|
6ad2b49ab3 | ||
|
|
656e7c069d | ||
|
|
00aa470bf2 | ||
|
|
c6ed0b43fc | ||
|
|
3d8a4d2553 | ||
|
|
42ab0eb804 | ||
|
|
d80d06a9b8 | ||
|
|
3c39026739 | ||
|
|
72c17b22df | ||
|
|
dd1aa23cb6 | ||
|
|
ecdf243e63 | ||
|
|
1e15585a24 | ||
|
|
9d40c0cb26 | ||
|
|
e41e24b044 | ||
|
|
a15d661fd7 | ||
|
|
cb5954e8c7 | ||
|
|
2b55db05e1 | ||
|
|
74bf93059c | ||
|
|
384d8c9c37 | ||
|
|
1bb6149dbe |
@ -25,7 +25,7 @@ import TouchBarComponent from "./touch_bar.js";
|
|||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
import { StartupChecks } from "./startup_checks.js";
|
import { StartupChecks } from "./startup_checks.js";
|
||||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
import type { CreateNoteOpts, CreateNoteWithLinkOpts } from "../services/note_create.js";
|
||||||
import { ColumnComponent } from "tabulator-tables";
|
import { ColumnComponent } from "tabulator-tables";
|
||||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||||
import type RootContainer from "../widgets/containers/root_container.js";
|
import type RootContainer from "../widgets/containers/root_container.js";
|
||||||
@ -359,8 +359,7 @@ export type CommandMappings = {
|
|||||||
|
|
||||||
// Table view
|
// Table view
|
||||||
addNewRow: CommandData & {
|
addNewRow: CommandData & {
|
||||||
customOpts: CreateNoteOpts;
|
customOpts?: CreateNoteWithLinkOpts;
|
||||||
parentNotePath?: string;
|
|
||||||
};
|
};
|
||||||
addNewTableColumn: CommandData & {
|
addNewTableColumn: CommandData & {
|
||||||
columnToEdit?: ColumnComponent;
|
columnToEdit?: ColumnComponent;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import froca from "../services/froca.js";
|
|||||||
import linkService from "../services/link.js";
|
import linkService from "../services/link.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||||
|
import noteCreateService from "../services/note_create.js";
|
||||||
|
|
||||||
export default class Entrypoints extends Component {
|
export default class Entrypoints extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -24,23 +25,9 @@ export default class Entrypoints extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNoteIntoInboxCommand() {
|
async createNoteIntoInboxCommand() {
|
||||||
const inboxNote = await dateNoteService.getInboxNote();
|
await noteCreateService.createNote(
|
||||||
if (!inboxNote) {
|
{ target: "default" }
|
||||||
console.warn("Missing inbox note.");
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { note } = await server.post<CreateChildrenResponse>(`notes/${inboxNote.noteId}/children?target=into`, {
|
|
||||||
content: "",
|
|
||||||
type: "text",
|
|
||||||
isProtected: inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable()
|
|
||||||
});
|
|
||||||
|
|
||||||
await ws.waitForMaxKnownEntityChangeId();
|
|
||||||
|
|
||||||
await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, { activate: true });
|
|
||||||
|
|
||||||
appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
|
async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
|
||||||
|
|||||||
@ -48,10 +48,15 @@ export default class MainTreeExecutors extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(activeNoteContext.notePath, {
|
await noteCreateService.createNote(
|
||||||
|
{
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: activeNoteContext.notePath,
|
||||||
isProtected: activeNoteContext.note.isProtected,
|
isProtected: activeNoteContext.note.isProtected,
|
||||||
saveSelection: false
|
saveSelection: false,
|
||||||
});
|
promptForType: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNoteAfterCommand() {
|
async createNoteAfterCommand() {
|
||||||
@ -72,11 +77,14 @@ export default class MainTreeExecutors extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(parentNotePath, {
|
await noteCreateService.createNote(
|
||||||
|
{
|
||||||
target: "after",
|
target: "after",
|
||||||
|
parentNoteLink: parentNotePath,
|
||||||
targetBranchId: node.data.branchId,
|
targetBranchId: node.data.branchId,
|
||||||
isProtected: isProtected,
|
isProtected: isProtected,
|
||||||
saveSelection: false
|
saveSelection: false
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export default class RootCommandExecutor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
|
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
|
||||||
const noteId = treeService.getNoteIdFromUrl(notePath);
|
const noteId = treeService.getNoteIdFromLink(notePath);
|
||||||
|
|
||||||
this.searchNotesCommand({ ancestorNoteId: noteId });
|
this.searchNotesCommand({ ancestorNoteId: noteId });
|
||||||
}
|
}
|
||||||
@ -240,14 +240,18 @@ export default class RootCommandExecutor extends Component {
|
|||||||
// Create a new AI Chat note at the root level
|
// Create a new AI Chat note at the root level
|
||||||
const rootNoteId = "root";
|
const rootNoteId = "root";
|
||||||
|
|
||||||
const result = await noteCreateService.createNote(rootNoteId, {
|
const result = await noteCreateService.createNote(
|
||||||
|
{
|
||||||
|
parentNoteLink: rootNoteId,
|
||||||
|
target: "into",
|
||||||
title: "New AI Chat",
|
title: "New AI Chat",
|
||||||
type: "aiChat",
|
type: "aiChat",
|
||||||
content: JSON.stringify({
|
content: JSON.stringify({
|
||||||
messages: [],
|
messages: [],
|
||||||
title: "New AI Chat"
|
title: "New AI Chat"
|
||||||
})
|
}),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!result.note) {
|
if (!result.note) {
|
||||||
toastService.showError("Failed to create AI Chat note");
|
toastService.showError("Failed to create AI Chat note");
|
||||||
|
|||||||
@ -74,10 +74,10 @@ export default class TabManager extends Component {
|
|||||||
|
|
||||||
// preload all notes at once
|
// preload all notes at once
|
||||||
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
|
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
|
||||||
[treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId])], true);
|
[treeService.getNoteIdFromLink(tab.notePath), tab.hoistedNoteId])], true);
|
||||||
|
|
||||||
const filteredNoteContexts = noteContextsToOpen.filter((openTab: NoteContextState) => {
|
const filteredNoteContexts = noteContextsToOpen.filter((openTab: NoteContextState) => {
|
||||||
const noteId = treeService.getNoteIdFromUrl(openTab.notePath);
|
const noteId = treeService.getNoteIdFromLink(openTab.notePath);
|
||||||
if (!noteId || !(noteId in froca.notes)) {
|
if (!noteId || !(noteId in froca.notes)) {
|
||||||
// note doesn't exist so don't try to open tab for it
|
// note doesn't exist so don't try to open tab for it
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -283,21 +283,31 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
const parentNotePath = treeService.getNotePath(this.node.getParent());
|
const parentNotePath = treeService.getNotePath(this.node.getParent());
|
||||||
const isProtected = treeService.getParentProtectedStatus(this.node);
|
const isProtected = treeService.getParentProtectedStatus(this.node);
|
||||||
|
|
||||||
noteCreateService.createNote(parentNotePath, {
|
|
||||||
|
noteCreateService.createNote(
|
||||||
|
{
|
||||||
target: "after",
|
target: "after",
|
||||||
|
parentNoteLink: parentNotePath,
|
||||||
targetBranchId: this.node.data.branchId,
|
targetBranchId: this.node.data.branchId,
|
||||||
type: type,
|
type: type,
|
||||||
isProtected: isProtected,
|
isProtected: isProtected,
|
||||||
templateNoteId: templateNoteId
|
templateNoteId: templateNoteId,
|
||||||
});
|
promptForType: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
} else if (command === "insertChildNote") {
|
} else if (command === "insertChildNote") {
|
||||||
const parentNotePath = treeService.getNotePath(this.node);
|
const parentNotePath = treeService.getNotePath(this.node);
|
||||||
|
|
||||||
noteCreateService.createNote(parentNotePath, {
|
noteCreateService.createNote(
|
||||||
|
{
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: parentNotePath,
|
||||||
type: type,
|
type: type,
|
||||||
isProtected: this.node.data.isProtected,
|
isProtected: this.node.data.isProtected,
|
||||||
templateNoteId: templateNoteId
|
templateNoteId: templateNoteId,
|
||||||
});
|
promptForType: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
} else if (command === "openNoteInSplit") {
|
} else if (command === "openNoteInSplit") {
|
||||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||||
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
||||||
|
|||||||
@ -50,7 +50,7 @@ async function checkNoteAccess(notePath: string, noteContext: NoteContext) {
|
|||||||
const hoistedNoteId = noteContext.hoistedNoteId;
|
const hoistedNoteId = noteContext.hoistedNoteId;
|
||||||
|
|
||||||
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes("_hidden") || resolvedNotePath.includes("_lbBookmarks"))) {
|
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes("_hidden") || resolvedNotePath.includes("_lbBookmarks"))) {
|
||||||
const noteId = treeService.getNoteIdFromUrl(resolvedNotePath);
|
const noteId = treeService.getNoteIdFromLink(resolvedNotePath);
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -261,7 +261,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
notePath,
|
notePath,
|
||||||
noteId: treeService.getNoteIdFromUrl(notePath),
|
noteId: treeService.getNoteIdFromLink(notePath),
|
||||||
ntxId,
|
ntxId,
|
||||||
hoistedNoteId,
|
hoistedNoteId,
|
||||||
viewScope,
|
viewScope,
|
||||||
|
|||||||
@ -5,6 +5,24 @@ import froca from "./froca.js";
|
|||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
import commandRegistry from "./command_registry.js";
|
import commandRegistry from "./command_registry.js";
|
||||||
import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5";
|
import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5";
|
||||||
|
import { CreateNoteAction } from "@triliumnext/commons"
|
||||||
|
import FNote from "../entities/fnote.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends CKEditor's MentionFeedObjectItem with extra fields used by Trilium.
|
||||||
|
* These additional props (like action, notePath, name, etc.) carry note
|
||||||
|
* metadata and legacy compatibility info needed for custom autocomplete
|
||||||
|
* and link insertion behavior beyond CKEditor’s base mention support.
|
||||||
|
*/
|
||||||
|
type ExtendedMentionFeedObjectItem = MentionFeedObjectItem & {
|
||||||
|
action?: string;
|
||||||
|
noteTitle?: string;
|
||||||
|
name?: string;
|
||||||
|
link?: string;
|
||||||
|
notePath?: string;
|
||||||
|
parentNoteId?: string;
|
||||||
|
highlightedNotePathTitle?: string;
|
||||||
|
};
|
||||||
|
|
||||||
// this key needs to have this value, so it's hit by the tooltip
|
// this key needs to have this value, so it's hit by the tooltip
|
||||||
const SELECTED_NOTE_PATH_KEY = "data-note-path";
|
const SELECTED_NOTE_PATH_KEY = "data-note-path";
|
||||||
@ -23,14 +41,39 @@ function getSearchDelay(notesCount: number): number {
|
|||||||
}
|
}
|
||||||
let searchDelay = getSearchDelay(notesCount);
|
let searchDelay = getSearchDelay(notesCount);
|
||||||
|
|
||||||
// TODO: Deduplicate with server.
|
// String values ensure stable, human-readable identifiers across serialization (JSON, CKEditor, logs).
|
||||||
|
export enum SuggestionAction {
|
||||||
|
// These values intentionally mirror CreateNoteAction string values 1:1.
|
||||||
|
// This overlap ensures that when a suggestion triggers a note creation callback,
|
||||||
|
// the receiving features (e.g. note creation handlers, CKEditor mentions) can interpret
|
||||||
|
// the action type consistently
|
||||||
|
CreateNote = CreateNoteAction.CreateNote,
|
||||||
|
CreateChildNote = CreateNoteAction.CreateChildNote,
|
||||||
|
CreateAndLinkNote = CreateNoteAction.CreateAndLinkNote,
|
||||||
|
CreateAndLinkChildNote = CreateNoteAction.CreateAndLinkChildNote,
|
||||||
|
|
||||||
|
SearchNotes = "search-notes",
|
||||||
|
ExternalLink = "external-link",
|
||||||
|
Command = "command",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SuggestionMode {
|
||||||
|
SuggestNothing = "nothing",
|
||||||
|
SuggestCreateOnly = "create-only",
|
||||||
|
SuggestCreateAndLink = "create-and-link"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Previously marked for deduplication with a server-side type,
|
||||||
|
// but review on 2025-10-12 (using `rg Suggestion`) found no corresponding
|
||||||
|
// server implementation.
|
||||||
|
// This interface appears to be client-only.
|
||||||
export interface Suggestion {
|
export interface Suggestion {
|
||||||
noteTitle?: string;
|
noteTitle?: string;
|
||||||
externalLink?: string;
|
externalLink?: string;
|
||||||
notePathTitle?: string;
|
notePathTitle?: string;
|
||||||
notePath?: string;
|
notePath?: string;
|
||||||
highlightedNotePathTitle?: string;
|
highlightedNotePathTitle?: string;
|
||||||
action?: string | "create-note" | "search-notes" | "external-link" | "command";
|
action?: SuggestionAction;
|
||||||
parentNoteId?: string;
|
parentNoteId?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
commandId?: string;
|
commandId?: string;
|
||||||
@ -43,7 +86,7 @@ export interface Suggestion {
|
|||||||
export interface Options {
|
export interface Options {
|
||||||
container?: HTMLElement | null;
|
container?: HTMLElement | null;
|
||||||
fastSearch?: boolean;
|
fastSearch?: boolean;
|
||||||
allowCreatingNotes?: boolean;
|
suggestionMode?: SuggestionMode;
|
||||||
allowJumpToSearchNotes?: boolean;
|
allowJumpToSearchNotes?: boolean;
|
||||||
allowExternalLinks?: boolean;
|
allowExternalLinks?: boolean;
|
||||||
/** If set, hides the right-side button corresponding to go to selected note. */
|
/** If set, hides the right-side button corresponding to go to selected note. */
|
||||||
@ -54,110 +97,160 @@ export interface Options {
|
|||||||
isCommandPalette?: boolean;
|
isCommandPalette?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function autocompleteSourceForCKEditor(queryText: string) {
|
async function autocompleteSourceForCKEditor(
|
||||||
return await new Promise<MentionFeedObjectItem[]>((res, rej) => {
|
queryText: string,
|
||||||
|
suggestionMode: SuggestionMode
|
||||||
|
): Promise<MentionFeedObjectItem[]> {
|
||||||
|
// Wrap the callback-based autocompleteSource in a Promise for async/await
|
||||||
|
const rows = await new Promise<Suggestion[]>((resolve) => {
|
||||||
autocompleteSource(
|
autocompleteSource(
|
||||||
queryText,
|
queryText,
|
||||||
(rows) => {
|
(suggestions) => resolve(suggestions),
|
||||||
res(
|
{
|
||||||
rows.map((row) => {
|
suggestionMode,
|
||||||
return {
|
}
|
||||||
action: row.action,
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map internal suggestions to CKEditor mention feed items
|
||||||
|
return rows.map((row): ExtendedMentionFeedObjectItem => ({
|
||||||
|
action: row.action?.toString(),
|
||||||
noteTitle: row.noteTitle,
|
noteTitle: row.noteTitle,
|
||||||
id: `@${row.notePathTitle}`,
|
id: `@${row.notePathTitle}`,
|
||||||
name: row.notePathTitle || "",
|
name: row.notePathTitle || "",
|
||||||
link: `#${row.notePath}`,
|
link: `#${row.notePath}`,
|
||||||
notePath: row.notePath,
|
notePath: row.notePath,
|
||||||
|
parentNoteId: row.parentNoteId,
|
||||||
highlightedNotePathTitle: row.highlightedNotePathTitle
|
highlightedNotePathTitle: row.highlightedNotePathTitle
|
||||||
};
|
}));
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
allowCreatingNotes: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) {
|
async function autocompleteSource(
|
||||||
|
term: string,
|
||||||
|
callback: (rows: Suggestion[]) => void,
|
||||||
|
options: Options = {}
|
||||||
|
) {
|
||||||
// Check if we're in command mode
|
// Check if we're in command mode
|
||||||
if (options.isCommandPalette && term.startsWith(">")) {
|
if (options.isCommandPalette && term.startsWith(">")) {
|
||||||
const commandQuery = term.substring(1).trim();
|
const commandQuery = term.substring(1).trim();
|
||||||
|
|
||||||
// Get commands (all if no query, filtered if query provided)
|
// Get commands (all if no query, filtered if query provided)
|
||||||
const commands = commandQuery.length === 0
|
const commands =
|
||||||
|
commandQuery.length === 0
|
||||||
? commandRegistry.getAllCommands()
|
? commandRegistry.getAllCommands()
|
||||||
: commandRegistry.searchCommands(commandQuery);
|
: commandRegistry.searchCommands(commandQuery);
|
||||||
|
|
||||||
// Convert commands to suggestions
|
// Convert commands to suggestions
|
||||||
const commandSuggestions: Suggestion[] = commands.map(cmd => ({
|
const commandSuggestions: Suggestion[] = commands.map((cmd) => ({
|
||||||
action: "command",
|
action: SuggestionAction.Command,
|
||||||
commandId: cmd.id,
|
commandId: cmd.id,
|
||||||
noteTitle: cmd.name,
|
noteTitle: cmd.name,
|
||||||
notePathTitle: `>${cmd.name}`,
|
notePathTitle: `>${cmd.name}`,
|
||||||
highlightedNotePathTitle: cmd.name,
|
highlightedNotePathTitle: cmd.name,
|
||||||
commandDescription: cmd.description,
|
commandDescription: cmd.description,
|
||||||
commandShortcut: cmd.shortcut,
|
commandShortcut: cmd.shortcut,
|
||||||
icon: cmd.icon
|
icon: cmd.icon,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
cb(commandSuggestions);
|
callback(commandSuggestions);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fastSearch = options.fastSearch === false ? false : true;
|
const fastSearch = options.fastSearch !== false;
|
||||||
if (fastSearch === false) {
|
const trimmedTerm = term.trim();
|
||||||
if (term.trim().length === 0) {
|
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
|
||||||
return;
|
|
||||||
}
|
if (!fastSearch && trimmedTerm.length === 0) return;
|
||||||
cb([
|
|
||||||
|
if (!fastSearch) {
|
||||||
|
callback([
|
||||||
{
|
{
|
||||||
noteTitle: term,
|
noteTitle: trimmedTerm,
|
||||||
highlightedNotePathTitle: t("quick-search.searching")
|
highlightedNotePathTitle: t("quick-search.searching"),
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
|
let results = await server.get<Suggestion[]>(
|
||||||
const length = term.trim().length;
|
`autocomplete?query=${encodeURIComponent(trimmedTerm)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`
|
||||||
|
);
|
||||||
let results = await server.get<Suggestion[]>(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`);
|
|
||||||
|
|
||||||
options.fastSearch = true;
|
options.fastSearch = true;
|
||||||
|
|
||||||
if (length >= 1 && options.allowCreatingNotes) {
|
// --- Create Note suggestions ---
|
||||||
|
if (trimmedTerm.length >= 1) {
|
||||||
|
switch (options.suggestionMode) {
|
||||||
|
case SuggestionMode.SuggestCreateOnly: {
|
||||||
results = [
|
results = [
|
||||||
{
|
{
|
||||||
action: "create-note",
|
action: SuggestionAction.CreateNote,
|
||||||
noteTitle: term,
|
noteTitle: trimmedTerm,
|
||||||
|
parentNoteId: "inbox",
|
||||||
|
highlightedNotePathTitle: t("note_autocomplete.create-note", { term: trimmedTerm }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: SuggestionAction.CreateChildNote,
|
||||||
|
noteTitle: trimmedTerm,
|
||||||
parentNoteId: activeNoteId || "root",
|
parentNoteId: activeNoteId || "root",
|
||||||
highlightedNotePathTitle: t("note_autocomplete.create-note", { term })
|
highlightedNotePathTitle: t("note_autocomplete.create-child-note", { term: trimmedTerm }),
|
||||||
} as Suggestion
|
},
|
||||||
].concat(results);
|
...results,
|
||||||
|
];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (length >= 1 && options.allowJumpToSearchNotes) {
|
case SuggestionMode.SuggestCreateAndLink: {
|
||||||
results = results.concat([
|
|
||||||
{
|
|
||||||
action: "search-notes",
|
|
||||||
noteTitle: term,
|
|
||||||
highlightedNotePathTitle: `${t("note_autocomplete.search-for", { term })} <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
|
|
||||||
results = [
|
results = [
|
||||||
{
|
{
|
||||||
action: "external-link",
|
action: SuggestionAction.CreateAndLinkNote,
|
||||||
externalLink: term,
|
noteTitle: trimmedTerm,
|
||||||
highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term })
|
parentNoteId: "inbox",
|
||||||
} as Suggestion
|
highlightedNotePathTitle: t("note_autocomplete.create-and-link-note", { term: trimmedTerm }),
|
||||||
].concat(results);
|
},
|
||||||
|
{
|
||||||
|
action: SuggestionAction.CreateAndLinkChildNote,
|
||||||
|
noteTitle: trimmedTerm,
|
||||||
|
parentNoteId: activeNoteId || "root",
|
||||||
|
highlightedNotePathTitle: t("note_autocomplete.create-and-link-child-note", { term: trimmedTerm }),
|
||||||
|
},
|
||||||
|
...results,
|
||||||
|
];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(results);
|
default:
|
||||||
|
// CreateMode.None or undefined → no creation suggestions
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Jump to Search Notes ---
|
||||||
|
if (trimmedTerm.length >= 1 && options.allowJumpToSearchNotes) {
|
||||||
|
results = [
|
||||||
|
...results,
|
||||||
|
{
|
||||||
|
action: SuggestionAction.SearchNotes,
|
||||||
|
noteTitle: trimmedTerm,
|
||||||
|
highlightedNotePathTitle: `${t("note_autocomplete.search-for", {
|
||||||
|
term: trimmedTerm,
|
||||||
|
})} <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- External Link suggestion ---
|
||||||
|
if (/^[a-z]+:\/\/.+/i.test(trimmedTerm) && options.allowExternalLinks) {
|
||||||
|
results = [
|
||||||
|
{
|
||||||
|
action: SuggestionAction.ExternalLink,
|
||||||
|
externalLink: trimmedTerm,
|
||||||
|
highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term: trimmedTerm }),
|
||||||
|
},
|
||||||
|
...results,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearText($el: JQuery<HTMLElement>) {
|
function clearText($el: JQuery<HTMLElement>) {
|
||||||
@ -198,6 +291,85 @@ function fullTextSearch($el: JQuery<HTMLElement>, options: Options) {
|
|||||||
$el.autocomplete("val", searchString);
|
$el.autocomplete("val", searchString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderCommandSuggestion(s: Suggestion): string {
|
||||||
|
const icon = s.icon || "bx bx-terminal";
|
||||||
|
const shortcut = s.commandShortcut
|
||||||
|
? `<kbd class="command-shortcut">${s.commandShortcut}</kbd>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="command-suggestion">
|
||||||
|
<span class="command-icon ${icon}"></span>
|
||||||
|
<div class="command-content">
|
||||||
|
<div class="command-name">${s.highlightedNotePathTitle}</div>
|
||||||
|
${s.commandDescription ? `<div class="command-description">${s.commandDescription}</div>` : ""}
|
||||||
|
</div>
|
||||||
|
${shortcut}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNoteSuggestion(s: Suggestion): string {
|
||||||
|
const actionClass =
|
||||||
|
s.action === SuggestionAction.SearchNotes ? "search-notes-action" : "";
|
||||||
|
|
||||||
|
const iconClass = (() => {
|
||||||
|
switch (s.action) {
|
||||||
|
case SuggestionAction.SearchNotes:
|
||||||
|
return "bx bx-search";
|
||||||
|
case SuggestionAction.CreateAndLinkNote:
|
||||||
|
case SuggestionAction.CreateNote:
|
||||||
|
return "bx bx-plus";
|
||||||
|
case SuggestionAction.CreateAndLinkChildNote:
|
||||||
|
case SuggestionAction.CreateChildNote:
|
||||||
|
return "bx bx-plus";
|
||||||
|
case SuggestionAction.ExternalLink:
|
||||||
|
return "bx bx-link-external";
|
||||||
|
default:
|
||||||
|
return s.icon ?? "bx bx-note";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="note-suggestion ${actionClass}" style="display:inline-flex; align-items:center;">
|
||||||
|
<span class="icon ${iconClass}" style="display:inline-block; vertical-align:middle; line-height:1; margin-right:0.4em;"></span>
|
||||||
|
<span class="text" style="display:inline-block; vertical-align:middle;">
|
||||||
|
<span class="search-result-title">${s.highlightedNotePathTitle}</span>
|
||||||
|
${s.highlightedAttributeSnippet
|
||||||
|
? `<span class="search-result-attributes">${s.highlightedAttributeSnippet}</span>`
|
||||||
|
: ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSuggestion(suggestion: Suggestion): string {
|
||||||
|
return suggestion.action === SuggestionAction.Command
|
||||||
|
? renderCommandSuggestion(suggestion)
|
||||||
|
: renderNoteSuggestion(suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapSuggestionToCreateNoteAction(
|
||||||
|
action: SuggestionAction
|
||||||
|
): CreateNoteAction | null {
|
||||||
|
switch (action) {
|
||||||
|
case SuggestionAction.CreateNote:
|
||||||
|
return CreateNoteAction.CreateNote;
|
||||||
|
|
||||||
|
case SuggestionAction.CreateAndLinkNote:
|
||||||
|
return CreateNoteAction.CreateAndLinkNote;
|
||||||
|
|
||||||
|
case SuggestionAction.CreateChildNote:
|
||||||
|
return CreateNoteAction.CreateChildNote;
|
||||||
|
|
||||||
|
case SuggestionAction.CreateAndLinkChildNote:
|
||||||
|
return CreateNoteAction.CreateAndLinkChildNote;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
||||||
if ($el.hasClass("note-autocomplete-input")) {
|
if ($el.hasClass("note-autocomplete-input")) {
|
||||||
// clear any event listener added in previous invocation of this function
|
// clear any event listener added in previous invocation of this function
|
||||||
@ -283,24 +455,21 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
|||||||
$el.autocomplete(
|
$el.autocomplete(
|
||||||
{
|
{
|
||||||
...autocompleteOptions,
|
...autocompleteOptions,
|
||||||
appendTo: document.querySelector("body"),
|
appendTo: document.body,
|
||||||
hint: false,
|
hint: false,
|
||||||
autoselect: true,
|
autoselect: true,
|
||||||
// openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces
|
|
||||||
// re-querying of the autocomplete source which then changes the currently selected suggestion
|
|
||||||
openOnFocus: false,
|
openOnFocus: false,
|
||||||
minLength: 0,
|
minLength: 0,
|
||||||
tabAutocomplete: false
|
tabAutocomplete: false,
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
source: (term, cb) => {
|
source: (term, callback) => {
|
||||||
clearTimeout(debounceTimeoutId);
|
clearTimeout(debounceTimeoutId);
|
||||||
debounceTimeoutId = setTimeout(() => {
|
debounceTimeoutId = setTimeout(() => {
|
||||||
if (isComposingInput) {
|
if (!isComposingInput) {
|
||||||
return;
|
autocompleteSource(term, callback, options);
|
||||||
}
|
}
|
||||||
autocompleteSource(term, cb, options);
|
|
||||||
}, searchDelay);
|
}, searchDelay);
|
||||||
|
|
||||||
if (searchDelay === 0) {
|
if (searchDelay === 0) {
|
||||||
@ -308,101 +477,38 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
displayKey: "notePathTitle",
|
displayKey: "notePathTitle",
|
||||||
templates: {
|
templates: { suggestion: renderSuggestion },
|
||||||
suggestion: (suggestion) => {
|
cache: false,
|
||||||
if (suggestion.action === "command") {
|
|
||||||
let html = `<div class="command-suggestion">`;
|
|
||||||
html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`;
|
|
||||||
html += `<div class="command-content">`;
|
|
||||||
html += `<div class="command-name">${suggestion.highlightedNotePathTitle}</div>`;
|
|
||||||
if (suggestion.commandDescription) {
|
|
||||||
html += `<div class="command-description">${suggestion.commandDescription}</div>`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
if (suggestion.commandShortcut) {
|
|
||||||
html += `<kbd class="command-shortcut">${suggestion.commandShortcut}</kbd>`;
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
// Add special class for search-notes action
|
|
||||||
const actionClass = suggestion.action === "search-notes" ? "search-notes-action" : "";
|
|
||||||
|
|
||||||
// Choose appropriate icon based on action
|
|
||||||
let iconClass = suggestion.icon ?? "bx bx-note";
|
|
||||||
if (suggestion.action === "search-notes") {
|
|
||||||
iconClass = "bx bx-search";
|
|
||||||
} else if (suggestion.action === "create-note") {
|
|
||||||
iconClass = "bx bx-plus";
|
|
||||||
} else if (suggestion.action === "external-link") {
|
|
||||||
iconClass = "bx bx-link-external";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simplified HTML structure without nested divs
|
|
||||||
let html = `<div class="note-suggestion ${actionClass}">`;
|
|
||||||
html += `<span class="icon ${iconClass}"></span>`;
|
|
||||||
html += `<span class="text">`;
|
|
||||||
html += `<span class="search-result-title">${suggestion.highlightedNotePathTitle}</span>`;
|
|
||||||
|
|
||||||
// Add attribute snippet inline if available
|
|
||||||
if (suggestion.highlightedAttributeSnippet) {
|
|
||||||
html += `<span class="search-result-attributes">${suggestion.highlightedAttributeSnippet}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `</span>`;
|
|
||||||
html += `</div>`;
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
|
|
||||||
cache: false
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
|
// TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
|
||||||
($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => {
|
($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => {
|
||||||
if (suggestion.action === "command") {
|
async function doCommand() {
|
||||||
$el.autocomplete("close");
|
$el.autocomplete("close");
|
||||||
$el.trigger("autocomplete:commandselected", [suggestion]);
|
$el.trigger("autocomplete:commandselected", [suggestion]);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestion.action === "external-link") {
|
async function doExternalLink() {
|
||||||
$el.setSelectedNotePath(null);
|
$el.setSelectedNotePath(null);
|
||||||
$el.setSelectedExternalLink(suggestion.externalLink);
|
$el.setSelectedExternalLink(suggestion.externalLink);
|
||||||
|
|
||||||
$el.autocomplete("val", suggestion.externalLink);
|
$el.autocomplete("val", suggestion.externalLink);
|
||||||
|
|
||||||
$el.autocomplete("close");
|
$el.autocomplete("close");
|
||||||
|
|
||||||
$el.trigger("autocomplete:externallinkselected", [suggestion]);
|
$el.trigger("autocomplete:externallinkselected", [suggestion]);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestion.action === "create-note") {
|
async function resolveSuggestionNotePathUnderCurrentHoist(note: FNote) {
|
||||||
const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType();
|
const hoisted = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||||
if (!success) {
|
suggestion.notePath = note.getBestNotePathString(hoisted);
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, {
|
|
||||||
title: suggestion.noteTitle,
|
|
||||||
activate: false,
|
|
||||||
type: noteType,
|
|
||||||
templateNoteId: templateNoteId
|
|
||||||
});
|
|
||||||
|
|
||||||
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
|
||||||
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestion.action === "search-notes") {
|
async function doSearchNotes() {
|
||||||
const searchString = suggestion.noteTitle;
|
const searchString = suggestion.noteTitle;
|
||||||
appContext.triggerCommand("searchNotes", { searchString });
|
appContext.triggerCommand("searchNotes", { searchString });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectNoteFromAutocomplete(suggestion: Suggestion) {
|
||||||
$el.setSelectedNotePath(suggestion.notePath);
|
$el.setSelectedNotePath(suggestion.notePath);
|
||||||
$el.setSelectedExternalLink(null);
|
$el.setSelectedExternalLink(null);
|
||||||
|
|
||||||
@ -411,6 +517,45 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
|||||||
$el.autocomplete("close");
|
$el.autocomplete("close");
|
||||||
|
|
||||||
$el.trigger("autocomplete:noteselected", [suggestion]);
|
$el.trigger("autocomplete:noteselected", [suggestion]);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (suggestion.action) {
|
||||||
|
case SuggestionAction.Command:
|
||||||
|
await doCommand();
|
||||||
|
return;
|
||||||
|
|
||||||
|
case SuggestionAction.ExternalLink:
|
||||||
|
await doExternalLink();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SuggestionAction.CreateNote:
|
||||||
|
case SuggestionAction.CreateAndLinkNote:
|
||||||
|
case SuggestionAction.CreateChildNote:
|
||||||
|
case SuggestionAction.CreateAndLinkChildNote: {
|
||||||
|
const createNoteAction = mapSuggestionToCreateNoteAction(
|
||||||
|
suggestion.action
|
||||||
|
)!;
|
||||||
|
const { note } = await noteCreateService.createNoteFromAction(
|
||||||
|
createNoteAction,
|
||||||
|
true,
|
||||||
|
suggestion.noteTitle,
|
||||||
|
suggestion.parentNoteId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!note) break;
|
||||||
|
|
||||||
|
await resolveSuggestionNotePathUnderCurrentHoist(note);
|
||||||
|
await selectNoteFromAutocomplete(suggestion);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SuggestionAction.SearchNotes:
|
||||||
|
await doSearchNotes();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
await selectNoteFromAutocomplete(suggestion);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$el.on("autocomplete:closed", () => {
|
$el.on("autocomplete:closed", () => {
|
||||||
|
|||||||
@ -10,8 +10,63 @@ import type FNote from "../entities/fnote.js";
|
|||||||
import type FBranch from "../entities/fbranch.js";
|
import type FBranch from "../entities/fbranch.js";
|
||||||
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
|
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
|
||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
|
import dateNoteService from "../services/date_notes.js";
|
||||||
|
import { CreateNoteAction } from "@triliumnext/commons";
|
||||||
|
|
||||||
export interface CreateNoteOpts {
|
/**
|
||||||
|
* Defines the type hierarchy and rules for valid argument combinations
|
||||||
|
* accepted by `note_create`.
|
||||||
|
*
|
||||||
|
* ## Overview
|
||||||
|
* Each variant extends `CreateNoteOpts` and enforces specific constraints to
|
||||||
|
* ensure only valid note creation options are allowed at compile time.
|
||||||
|
*
|
||||||
|
* ## Type Safety
|
||||||
|
* The `PromptingRule` ensures that `promptForType` and `type` stay mutually
|
||||||
|
* exclusive (if prompting, `type` is undefined).
|
||||||
|
*
|
||||||
|
* The type system prevents invalid argument mixes by design — successful type
|
||||||
|
* checks guarantee a valid state, following Curry–Howard correspondence
|
||||||
|
* principles (types as proofs).
|
||||||
|
*
|
||||||
|
* ## Maintenance
|
||||||
|
* If adding or modifying `Opts`, ensure:
|
||||||
|
* - All valid combinations are represented (avoid *false negatives*).
|
||||||
|
* - No invalid ones slip through (avoid *false positives*).
|
||||||
|
*
|
||||||
|
* Hierarchy (general → specific):
|
||||||
|
* - CreateNoteOpts
|
||||||
|
* - CreateNoteWithUrlOpts
|
||||||
|
* - CreateNoteIntoDefaultOpts
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** enforces a truth rule:
|
||||||
|
* - If `promptForType` is true → `type` must be undefined.
|
||||||
|
* - If `promptForType` is false → `type` must be defined.
|
||||||
|
*/
|
||||||
|
type PromptingRule = {
|
||||||
|
promptForType: true;
|
||||||
|
type?: never;
|
||||||
|
} | {
|
||||||
|
promptForType?: false;
|
||||||
|
/**
|
||||||
|
* The note type (e.g. "text", "code", "image", "mermaid", etc.).
|
||||||
|
*
|
||||||
|
* If omitted, the server will automatically default to `"text"`.
|
||||||
|
* TypeScript still enforces explicit typing unless `promptForType` is true,
|
||||||
|
* to encourage clarity at the call site.
|
||||||
|
*/
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base type for all note creation options (domain hypernym).
|
||||||
|
* All specific note option types extend from this.
|
||||||
|
*
|
||||||
|
* Combine with `&` to ensure valid logical combinations.
|
||||||
|
*/
|
||||||
|
type CreateNoteBase = {
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
saveSelection?: boolean;
|
saveSelection?: boolean;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
@ -21,10 +76,34 @@ export interface CreateNoteOpts {
|
|||||||
templateNoteId?: string;
|
templateNoteId?: string;
|
||||||
activate?: boolean;
|
activate?: boolean;
|
||||||
focus?: "title" | "content";
|
focus?: "title" | "content";
|
||||||
target?: string;
|
|
||||||
targetBranchId?: string;
|
|
||||||
textEditor?: CKTextEditor;
|
textEditor?: CKTextEditor;
|
||||||
}
|
} & PromptingRule;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Defines options for creating a note at a specific path.
|
||||||
|
* Serves as a base for "into", "before", and "after" variants,
|
||||||
|
* sharing common URL-related fields.
|
||||||
|
*/
|
||||||
|
export type CreateNoteWithLinkOpts =
|
||||||
|
| (CreateNoteBase & {
|
||||||
|
target: "into";
|
||||||
|
parentNoteLink?: string;
|
||||||
|
// No branch ID needed for "into"
|
||||||
|
})
|
||||||
|
| (CreateNoteBase & {
|
||||||
|
target: "before" | "after";
|
||||||
|
// Either an Url or a Path
|
||||||
|
parentNoteLink?: string;
|
||||||
|
// Required for "before"/"after"
|
||||||
|
targetBranchId: string;
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateNoteIntoDefaultOpts = CreateNoteBase & {
|
||||||
|
target: "default";
|
||||||
|
parentNoteLink?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateNoteOpts = CreateNoteWithLinkOpts | CreateNoteIntoDefaultOpts;
|
||||||
|
|
||||||
interface Response {
|
interface Response {
|
||||||
// TODO: Deduplicate with server once we have client/server architecture.
|
// TODO: Deduplicate with server once we have client/server architecture.
|
||||||
@ -37,7 +116,141 @@ interface DuplicateResponse {
|
|||||||
note: FNote;
|
note: FNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNote(parentNotePath: string | undefined, options: CreateNoteOpts = {}) {
|
// The low level note creation
|
||||||
|
async function createNote(
|
||||||
|
options: CreateNoteOpts
|
||||||
|
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
|
||||||
|
|
||||||
|
let resolvedOptions = { ...options };
|
||||||
|
|
||||||
|
// handle prompts centrally to write once fix for all
|
||||||
|
if (options.promptForType) {
|
||||||
|
const maybeResolvedOptions = await promptForType(options);
|
||||||
|
if (!maybeResolvedOptions) {
|
||||||
|
return { note: null, branch: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedOptions = maybeResolvedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
switch(resolvedOptions.target) {
|
||||||
|
case "default":
|
||||||
|
return createNoteIntoDefaultLocation(resolvedOptions);
|
||||||
|
case "into":
|
||||||
|
case "before":
|
||||||
|
case "after":
|
||||||
|
return createNoteWithLink(resolvedOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A wrapper to standardize note creation
|
||||||
|
async function createNoteFromAction(
|
||||||
|
action: CreateNoteAction,
|
||||||
|
promptForType: boolean,
|
||||||
|
title: string | undefined,
|
||||||
|
parentNoteLink: string | undefined,
|
||||||
|
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
|
||||||
|
switch (action) {
|
||||||
|
case CreateNoteAction.CreateNote: {
|
||||||
|
const resp = await createNote(
|
||||||
|
{
|
||||||
|
target: "default",
|
||||||
|
title: title,
|
||||||
|
activate: true,
|
||||||
|
promptForType,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
case CreateNoteAction.CreateAndLinkNote: {
|
||||||
|
const resp = await createNote(
|
||||||
|
{
|
||||||
|
target: "default",
|
||||||
|
title,
|
||||||
|
activate: false,
|
||||||
|
promptForType,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
case CreateNoteAction.CreateChildNote: {
|
||||||
|
if (!parentNoteLink) {
|
||||||
|
console.warn("createNoteFromAction: Missing parentNoteLink");
|
||||||
|
return { note: null, branch: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await createNote(
|
||||||
|
{
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink,
|
||||||
|
title,
|
||||||
|
activate: true,
|
||||||
|
promptForType,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
case CreateNoteAction.CreateAndLinkChildNote: {
|
||||||
|
if (!parentNoteLink) {
|
||||||
|
console.warn("createNoteFromAction: Missing parentNoteLink");
|
||||||
|
return { note: null, branch: undefined };
|
||||||
|
}
|
||||||
|
const resp = await createNote(
|
||||||
|
{
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: parentNoteLink,
|
||||||
|
title,
|
||||||
|
activate: false,
|
||||||
|
promptForType,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn("Unknown CreateNoteAction:", action);
|
||||||
|
return { note: null, branch: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptForType(
|
||||||
|
options: CreateNoteOpts
|
||||||
|
) : Promise<CreateNoteOpts | null> {
|
||||||
|
const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedOptions: CreateNoteOpts = {
|
||||||
|
...options,
|
||||||
|
promptForType: false,
|
||||||
|
type: noteType,
|
||||||
|
templateNoteId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notePath) {
|
||||||
|
resolvedOptions = {
|
||||||
|
...resolvedOptions,
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: notePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new note under a specified parent note path.
|
||||||
|
*
|
||||||
|
* @param target - Mirrors the `createNote` API in apps/server/src/routes/api/notes.ts.
|
||||||
|
* @param options - Note creation options
|
||||||
|
* @returns A promise resolving with the created note and its branch.
|
||||||
|
*/
|
||||||
|
async function createNoteWithLink(
|
||||||
|
options: CreateNoteWithLinkOpts
|
||||||
|
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
|
||||||
options = Object.assign(
|
options = Object.assign(
|
||||||
{
|
{
|
||||||
activate: true,
|
activate: true,
|
||||||
@ -61,7 +274,8 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
|||||||
[options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml());
|
[options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml());
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
|
const parentNoteLink = options.parentNoteLink;
|
||||||
|
const parentNoteId = treeService.getNoteIdFromLink(parentNoteLink);
|
||||||
|
|
||||||
if (options.type === "mermaid" && !options.content && !options.templateNoteId) {
|
if (options.type === "mermaid" && !options.content && !options.templateNoteId) {
|
||||||
options.content = `graph TD;
|
options.content = `graph TD;
|
||||||
@ -71,7 +285,12 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
|||||||
C-->D;`;
|
C-->D;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { note, branch } = await server.post<Response>(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId || ""}`, {
|
const query =
|
||||||
|
options.target === "into"
|
||||||
|
? `target=${options.target}`
|
||||||
|
: `target=${options.target}&targetBranchId=${options.targetBranchId}`;
|
||||||
|
|
||||||
|
const { note, branch } = await server.post<Response>(`notes/${parentNoteId}/children?${query}`, {
|
||||||
title: options.title,
|
title: options.title,
|
||||||
content: options.content || "",
|
content: options.content || "",
|
||||||
isProtected: options.isProtected,
|
isProtected: options.isProtected,
|
||||||
@ -89,7 +308,7 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
|||||||
|
|
||||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||||
if (activeNoteContext && options.activate) {
|
if (activeNoteContext && options.activate) {
|
||||||
await activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
|
await activeNoteContext.setNote(`${parentNoteId}/${note.noteId}`);
|
||||||
|
|
||||||
if (options.focus === "title") {
|
if (options.focus === "title") {
|
||||||
appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true });
|
appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true });
|
||||||
@ -107,25 +326,46 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new note inside the user's Inbox.
|
||||||
|
*
|
||||||
|
* @param {CreateNoteIntoDefaultOpts} [options] - Optional settings such as title, type, template, or content.
|
||||||
|
* @returns {Promise<{ note: FNote | null; branch: FBranch | undefined }>}
|
||||||
|
* Resolves with the created note and its branch, or `{ note: null, branch: undefined }` if the inbox is missing.
|
||||||
|
*/
|
||||||
|
async function createNoteIntoDefaultLocation(
|
||||||
|
options: CreateNoteIntoDefaultOpts
|
||||||
|
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
|
||||||
|
const inboxNote = await dateNoteService.getInboxNote();
|
||||||
|
if (!inboxNote) {
|
||||||
|
console.warn("Missing inbox note.");
|
||||||
|
// always return a defined object
|
||||||
|
return { note: null, branch: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.isProtected === undefined) {
|
||||||
|
options.isProtected =
|
||||||
|
inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await createNoteWithLink(
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: inboxNote.getBestNotePathString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async function chooseNoteType() {
|
async function chooseNoteType() {
|
||||||
return new Promise<ChooseNoteTypeResponse>((res) => {
|
return new Promise<ChooseNoteTypeResponse>((res) => {
|
||||||
appContext.triggerCommand("chooseNoteType", { callback: res });
|
appContext.triggerCommand("chooseNoteType", { callback: res });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
|
|
||||||
const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
options.type = noteType;
|
|
||||||
options.templateNoteId = templateNoteId;
|
|
||||||
|
|
||||||
return await createNote(notePath || parentNotePath, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* If the first element is heading, parse it out and use it as a new heading. */
|
/* If the first element is heading, parse it out and use it as a new heading. */
|
||||||
function parseSelectedHtml(selectedHtml: string) {
|
function parseSelectedHtml(selectedHtml: string) {
|
||||||
const dom = $.parseHTML(selectedHtml);
|
const dom = $.parseHTML(selectedHtml);
|
||||||
@ -146,7 +386,7 @@ function parseSelectedHtml(selectedHtml: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateSubtree(noteId: string, parentNotePath: string) {
|
async function duplicateSubtree(noteId: string, parentNotePath: string) {
|
||||||
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
|
const parentNoteId = treeService.getNoteIdFromLink(parentNotePath);
|
||||||
const { note } = await server.post<DuplicateResponse>(`notes/${noteId}/duplicate/${parentNoteId}`);
|
const { note } = await server.post<DuplicateResponse>(`notes/${noteId}/duplicate/${parentNoteId}`);
|
||||||
|
|
||||||
await ws.waitForMaxKnownEntityChangeId();
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
@ -159,7 +399,6 @@ async function duplicateSubtree(noteId: string, parentNotePath: string) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
createNote,
|
createNote,
|
||||||
createNoteWithTypePrompt,
|
createNoteFromAction,
|
||||||
duplicateSubtree,
|
duplicateSubtree,
|
||||||
chooseNoteType
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -92,7 +92,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
if (effectivePathSegments.includes(hoistedNoteId) && effectivePathSegments.includes('root')) {
|
if (effectivePathSegments.includes(hoistedNoteId) && effectivePathSegments.includes('root')) {
|
||||||
return effectivePathSegments;
|
return effectivePathSegments;
|
||||||
} else {
|
} else {
|
||||||
const noteId = getNoteIdFromUrl(notePath);
|
const noteId = getNoteIdFromLink(notePath);
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
throw new Error(`Unable to find note with ID: ${noteId}.`);
|
throw new Error(`Unable to find note with ID: ${noteId}.`);
|
||||||
}
|
}
|
||||||
@ -129,7 +129,7 @@ function getParentProtectedStatus(node: Fancytree.FancytreeNode) {
|
|||||||
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
|
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoteIdFromUrl(urlOrNotePath: string | null | undefined) {
|
function getNoteIdFromLink(urlOrNotePath: string | null | undefined) {
|
||||||
if (!urlOrNotePath) {
|
if (!urlOrNotePath) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -306,7 +306,7 @@ export default {
|
|||||||
getParentProtectedStatus,
|
getParentProtectedStatus,
|
||||||
getNotePath,
|
getNotePath,
|
||||||
getNotePathTitleComponents,
|
getNotePathTitleComponents,
|
||||||
getNoteIdFromUrl,
|
getNoteIdFromLink,
|
||||||
getNoteIdAndParentIdFromUrl,
|
getNoteIdAndParentIdFromUrl,
|
||||||
getBranchIdFromUrl,
|
getBranchIdFromUrl,
|
||||||
getNoteTitle,
|
getNoteTitle,
|
||||||
|
|||||||
@ -592,11 +592,6 @@ button.btn-sm {
|
|||||||
color: var(--left-pane-text-color);
|
color: var(--left-pane-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.active:not(.btn-primary) {
|
|
||||||
background-color: var(--button-disabled-background-color) !important;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-block-toolbar-button {
|
.ck.ck-block-toolbar-button {
|
||||||
transform: translateX(7px);
|
transform: translateX(7px);
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
|
|||||||
@ -41,6 +41,9 @@
|
|||||||
--cmd-button-keyboard-shortcut-color: white;
|
--cmd-button-keyboard-shortcut-color: white;
|
||||||
--cmd-button-disabled-opacity: 0.5;
|
--cmd-button-disabled-opacity: 0.5;
|
||||||
|
|
||||||
|
--button-group-active-button-background: #ffffff4e;
|
||||||
|
--button-group-active-button-text-color: white;
|
||||||
|
|
||||||
--icon-button-color: currentColor;
|
--icon-button-color: currentColor;
|
||||||
--icon-button-hover-background: var(--hover-item-background-color);
|
--icon-button-hover-background: var(--hover-item-background-color);
|
||||||
--icon-button-hover-color: var(--hover-item-text-color);
|
--icon-button-hover-color: var(--hover-item-text-color);
|
||||||
|
|||||||
@ -41,6 +41,9 @@
|
|||||||
--cmd-button-keyboard-shortcut-color: black;
|
--cmd-button-keyboard-shortcut-color: black;
|
||||||
--cmd-button-disabled-opacity: 0.5;
|
--cmd-button-disabled-opacity: 0.5;
|
||||||
|
|
||||||
|
--button-group-active-button-background: #00000026;
|
||||||
|
--button-group-active-button-text-color: black;
|
||||||
|
|
||||||
--icon-button-color: currentColor;
|
--icon-button-color: currentColor;
|
||||||
--icon-button-hover-background: var(--hover-item-background-color);
|
--icon-button-hover-background: var(--hover-item-background-color);
|
||||||
--icon-button-hover-color: var(--hover-item-text-color);
|
--icon-button-hover-color: var(--hover-item-text-color);
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
.modal .modal-header .btn-close,
|
.modal .modal-header .btn-close,
|
||||||
.modal .modal-header .help-button,
|
.modal .modal-header .help-button,
|
||||||
|
.modal .modal-header .custom-title-bar-button,
|
||||||
#toast-container .toast .toast-header .btn-close {
|
#toast-container .toast .toast-header .btn-close {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -55,15 +56,17 @@
|
|||||||
font-family: boxicons;
|
font-family: boxicons;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .modal-header .help-button {
|
.modal .modal-header .help-button,
|
||||||
|
.modal .modal-header .custom-title-bar-button {
|
||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
font-size: calc(var(--modal-control-button-size) * .75);
|
font-size: calc(var(--modal-control-button-size) * .70);
|
||||||
font-family: unset;
|
font-family: unset;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .modal-header .btn-close:hover,
|
.modal .modal-header .btn-close:hover,
|
||||||
.modal .modal-header .help-button:hover,
|
.modal .modal-header .help-button:hover,
|
||||||
|
.modal .modal-header .custom-title-bar-button:hover,
|
||||||
#toast-container .toast .toast-header .btn-close:hover {
|
#toast-container .toast .toast-header .btn-close:hover {
|
||||||
background: var(--modal-control-button-hover-background);
|
background: var(--modal-control-button-hover-background);
|
||||||
color: var(--modal-control-button-hover-color);
|
color: var(--modal-control-button-hover-color);
|
||||||
@ -71,6 +74,7 @@
|
|||||||
|
|
||||||
.modal .modal-header .btn-close:active,
|
.modal .modal-header .btn-close:active,
|
||||||
.modal .modal-header .help-button:active,
|
.modal .modal-header .help-button:active,
|
||||||
|
.modal .modal-header .custom-title-bar-button:active,
|
||||||
#toast-container .toast .toast-header .btn-close:active {
|
#toast-container .toast .toast-header .btn-close:active {
|
||||||
transform: scale(.85);
|
transform: scale(.85);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,6 +146,14 @@ button.btn.btn-success kbd {
|
|||||||
outline: 2px solid var(--input-focus-outline-color);
|
outline: 2px solid var(--input-focus-outline-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Button groups */
|
||||||
|
|
||||||
|
/* Active button */
|
||||||
|
:root .btn-group button.btn.active {
|
||||||
|
background-color: var(--button-group-active-button-background);
|
||||||
|
color: var(--button-group-active-button-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Input boxes
|
* Input boxes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1448,6 +1448,14 @@ div.promoted-attribute-cell .multiplicity:has(span) span {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.promoted-attribute-cell.promoted-attribute-label-color {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.promoted-attribute-cell.promoted-attribute-label-color .input-group {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Floating buttons
|
* Floating buttons
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -2098,7 +2098,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "当前正在查看一个只读笔记。",
|
"read-only-note": "当前正在查看一个只读笔记。",
|
||||||
"auto-read-only-note": "这条笔记以只读模式显示便于快速加载。",
|
"auto-read-only-note": "这条笔记以只读模式显示便于快速加载。",
|
||||||
"auto-read-only-learn-more": "了解更多",
|
|
||||||
"edit-note": "编辑笔记"
|
"edit-note": "编辑笔记"
|
||||||
},
|
},
|
||||||
"note-color": {
|
"note-color": {
|
||||||
|
|||||||
@ -2097,7 +2097,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "Aktuelle Notiz wird im Lese-Modus angezeigt.",
|
"read-only-note": "Aktuelle Notiz wird im Lese-Modus angezeigt.",
|
||||||
"auto-read-only-note": "Diese Notiz wird im Nur-Lesen-Modus angezeigt, um ein schnelleres Laden zu ermöglichen.",
|
"auto-read-only-note": "Diese Notiz wird im Nur-Lesen-Modus angezeigt, um ein schnelleres Laden zu ermöglichen.",
|
||||||
"auto-read-only-learn-more": "Mehr erfahren",
|
|
||||||
"edit-note": "Notiz bearbeiten"
|
"edit-note": "Notiz bearbeiten"
|
||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
|
|||||||
@ -112,6 +112,7 @@
|
|||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"title": "Cheatsheet",
|
"title": "Cheatsheet",
|
||||||
|
"editShortcuts": "Edit keyboard shortcuts",
|
||||||
"noteNavigation": "Note navigation",
|
"noteNavigation": "Note navigation",
|
||||||
"goUpDown": "go up/down in the list of notes",
|
"goUpDown": "go up/down in the list of notes",
|
||||||
"collapseExpand": "collapse/expand node",
|
"collapseExpand": "collapse/expand node",
|
||||||
@ -1896,7 +1897,10 @@
|
|||||||
},
|
},
|
||||||
"note_autocomplete": {
|
"note_autocomplete": {
|
||||||
"search-for": "Search for \"{{term}}\"",
|
"search-for": "Search for \"{{term}}\"",
|
||||||
"create-note": "Create and link child note \"{{term}}\"",
|
"create-child-note": "Create child note \"{{term}}\"",
|
||||||
|
"create-note": "Create note \"{{term}}\"",
|
||||||
|
"create-and-link-child-note": "Create and link child note \"{{term}}\"",
|
||||||
|
"create-and-link-note": "Create and link note \"{{term}}\"",
|
||||||
"insert-external-link": "Insert external link to \"{{term}}\"",
|
"insert-external-link": "Insert external link to \"{{term}}\"",
|
||||||
"clear-text-field": "Clear text field",
|
"clear-text-field": "Clear text field",
|
||||||
"show-recent-notes": "Show recent notes",
|
"show-recent-notes": "Show recent notes",
|
||||||
|
|||||||
@ -2096,7 +2096,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "Actualmente, está viendo una nota de solo lectura.",
|
"read-only-note": "Actualmente, está viendo una nota de solo lectura.",
|
||||||
"auto-read-only-note": "Esta nota se muestra en modo de solo lectura para una carga más rápida.",
|
"auto-read-only-note": "Esta nota se muestra en modo de solo lectura para una carga más rápida.",
|
||||||
"auto-read-only-learn-more": "Para saber más",
|
|
||||||
"edit-note": "Editar nota"
|
"edit-note": "Editar nota"
|
||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
|
|||||||
@ -2092,7 +2092,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "Stai visualizzando una nota di sola lettura.",
|
"read-only-note": "Stai visualizzando una nota di sola lettura.",
|
||||||
"auto-read-only-note": "Questa nota viene visualizzata in modalità di sola lettura per un caricamento più rapido.",
|
"auto-read-only-note": "Questa nota viene visualizzata in modalità di sola lettura per un caricamento più rapido.",
|
||||||
"auto-read-only-learn-more": "Per saperne di più",
|
|
||||||
"edit-note": "Modifica nota"
|
"edit-note": "Modifica nota"
|
||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
|
|||||||
@ -2098,7 +2098,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "現在、読み取り専用のノートを表示しています。",
|
"read-only-note": "現在、読み取り専用のノートを表示しています。",
|
||||||
"auto-read-only-note": "このノートは読み込みを高速化するために読み取り専用モードで表示されています。",
|
"auto-read-only-note": "このノートは読み込みを高速化するために読み取り専用モードで表示されています。",
|
||||||
"auto-read-only-learn-more": "さらに詳しく",
|
|
||||||
"edit-note": "ノートを編集"
|
"edit-note": "ノートを編集"
|
||||||
},
|
},
|
||||||
"note-color": {
|
"note-color": {
|
||||||
|
|||||||
@ -2097,7 +2097,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "Vizualizați o notiță în modul doar în citire.",
|
"read-only-note": "Vizualizați o notiță în modul doar în citire.",
|
||||||
"auto-read-only-note": "Această notiță este afișată în modul doar în citire din motive de performanță.",
|
"auto-read-only-note": "Această notiță este afișată în modul doar în citire din motive de performanță.",
|
||||||
"auto-read-only-learn-more": "Mai multe detalii",
|
|
||||||
"edit-note": "Editează notița"
|
"edit-note": "Editează notița"
|
||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
|
|||||||
@ -2098,7 +2098,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "目前正在檢視唯讀筆記。",
|
"read-only-note": "目前正在檢視唯讀筆記。",
|
||||||
"auto-read-only-note": "此筆記以唯讀模式顯示以加快載入速度。",
|
"auto-read-only-note": "此筆記以唯讀模式顯示以加快載入速度。",
|
||||||
"auto-read-only-learn-more": "了解更多",
|
|
||||||
"edit-note": "編輯筆記"
|
"edit-note": "編輯筆記"
|
||||||
},
|
},
|
||||||
"note-color": {
|
"note-color": {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import server from "../../services/server.js";
|
|||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import linkService from "../../services/link.js";
|
import linkService from "../../services/link.js";
|
||||||
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
|
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
|
||||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
import noteAutocompleteService, { SuggestionMode } from "../../services/note_autocomplete.js";
|
||||||
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
|
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
|
||||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||||
import SpacedUpdate from "../../services/spaced_update.js";
|
import SpacedUpdate from "../../services/spaced_update.js";
|
||||||
@ -429,7 +429,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
|||||||
this.$rowTargetNote = this.$widget.find(".attr-row-target-note");
|
this.$rowTargetNote = this.$widget.find(".attr-row-target-note");
|
||||||
this.$inputTargetNote = this.$widget.find(".attr-input-target-note");
|
this.$inputTargetNote = this.$widget.find(".attr-input-target-note");
|
||||||
|
|
||||||
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, { allowCreatingNotes: true }).on("autocomplete:noteselected", (event, suggestion, dataset) => {
|
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, { suggestionMode: SuggestionMode.SuggestCreateAndLink }).on("autocomplete:noteselected", (event, suggestion, dataset) => {
|
||||||
if (!suggestion.notePath) {
|
if (!suggestion.notePath) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import branches from "../../../services/branches";
|
|||||||
import { executeBulkActions } from "../../../services/bulk_action";
|
import { executeBulkActions } from "../../../services/bulk_action";
|
||||||
import froca from "../../../services/froca";
|
import froca from "../../../services/froca";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
import note_create from "../../../services/note_create";
|
import note_create from "../../../services/note_create.js";
|
||||||
import server from "../../../services/server";
|
import server from "../../../services/server";
|
||||||
import { ColumnMap } from "./data";
|
import { ColumnMap } from "./data";
|
||||||
|
|
||||||
@ -39,9 +39,11 @@ export default class BoardApi {
|
|||||||
const parentNotePath = this.parentNote.noteId;
|
const parentNotePath = this.parentNote.noteId;
|
||||||
|
|
||||||
// Create a new note as a child of the parent note
|
// Create a new note as a child of the parent note
|
||||||
const { note: newNote, branch: newBranch } = await note_create.createNote(parentNotePath, {
|
const { note: newNote, branch: newBranch } = await note_create.createNote({
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: parentNotePath,
|
||||||
activate: false,
|
activate: false,
|
||||||
title
|
title,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newNote && newBranch) {
|
if (newNote && newBranch) {
|
||||||
@ -139,13 +141,17 @@ export default class BoardApi {
|
|||||||
async insertRowAtPosition(
|
async insertRowAtPosition(
|
||||||
column: string,
|
column: string,
|
||||||
relativeToBranchId: string,
|
relativeToBranchId: string,
|
||||||
direction: "before" | "after") {
|
direction: "before" | "after"
|
||||||
const { note, branch } = await note_create.createNote(this.parentNote.noteId, {
|
) {
|
||||||
|
const { note, branch } = await note_create.createNote(
|
||||||
|
{
|
||||||
|
target: direction,
|
||||||
|
parentNoteLink: this.parentNote.noteId,
|
||||||
activate: false,
|
activate: false,
|
||||||
targetBranchId: relativeToBranchId,
|
targetBranchId: relativeToBranchId,
|
||||||
target: direction,
|
title: t("board_view.new-item"),
|
||||||
title: t("board_view.new-item")
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!note || !branch) {
|
if (!note || !branch) {
|
||||||
throw new Error("Failed to create note");
|
throw new Error("Failed to create note");
|
||||||
|
|||||||
@ -57,12 +57,18 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
|
|||||||
{
|
{
|
||||||
title: t("board_view.insert-above"),
|
title: t("board_view.insert-above"),
|
||||||
uiIcon: "bx bx-list-plus",
|
uiIcon: "bx bx-list-plus",
|
||||||
handler: () => api.insertRowAtPosition(column, branchId, "before")
|
handler: () => api.insertRowAtPosition(
|
||||||
|
column,
|
||||||
|
branchId,
|
||||||
|
"before")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("board_view.insert-below"),
|
title: t("board_view.insert-below"),
|
||||||
uiIcon: "bx bx-empty",
|
uiIcon: "bx bx-empty",
|
||||||
handler: () => api.insertRowAtPosition(column, branchId, "after")
|
handler: () => api.insertRowAtPosition(
|
||||||
|
column,
|
||||||
|
branchId,
|
||||||
|
"after")
|
||||||
},
|
},
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
{
|
{
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import FormTextArea from "../../react/FormTextArea";
|
|||||||
import FNote from "../../../entities/fnote";
|
import FNote from "../../../entities/fnote";
|
||||||
import NoteAutocomplete from "../../react/NoteAutocomplete";
|
import NoteAutocomplete from "../../react/NoteAutocomplete";
|
||||||
import toast from "../../../services/toast";
|
import toast from "../../../services/toast";
|
||||||
|
import { SuggestionMode } from "../../../services/note_autocomplete";
|
||||||
|
|
||||||
export interface BoardViewData {
|
export interface BoardViewData {
|
||||||
columns?: BoardColumnData[];
|
columns?: BoardColumnData[];
|
||||||
@ -309,7 +310,7 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, is
|
|||||||
noteId={currentValue ?? ""}
|
noteId={currentValue ?? ""}
|
||||||
opts={{
|
opts={{
|
||||||
hideAllButtons: true,
|
hideAllButtons: true,
|
||||||
allowCreatingNotes: true
|
suggestionMode: SuggestionMode.SuggestCreateAndLink
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import Icon from "../../react/Icon.jsx";
|
|||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import froca from "../../../services/froca.js";
|
import froca from "../../../services/froca.js";
|
||||||
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
||||||
|
import { SuggestionMode } from "../../../services/note_autocomplete.js";
|
||||||
|
|
||||||
type ColumnType = LabelType | "relation";
|
type ColumnType = LabelType | "relation";
|
||||||
|
|
||||||
@ -227,7 +228,7 @@ function RelationEditor({ cell, success }: EditorOpts) {
|
|||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
noteId={cell.getValue()}
|
noteId={cell.getValue()}
|
||||||
opts={{
|
opts={{
|
||||||
allowCreatingNotes: true,
|
suggestionMode: SuggestionMode.SuggestCreateAndLink,
|
||||||
hideAllButtons: true
|
hideAllButtons: true
|
||||||
}}
|
}}
|
||||||
noteIdChanged={success}
|
noteIdChanged={success}
|
||||||
|
|||||||
@ -181,8 +181,8 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
|||||||
uiIcon: "bx bx-horizontal-left bx-rotate-90",
|
uiIcon: "bx bx-horizontal-left bx-rotate-90",
|
||||||
enabled: !sorters.length,
|
enabled: !sorters.length,
|
||||||
handler: () => parentComponent?.triggerCommand("addNewRow", {
|
handler: () => parentComponent?.triggerCommand("addNewRow", {
|
||||||
parentNotePath: parentNoteId,
|
|
||||||
customOpts: {
|
customOpts: {
|
||||||
|
parentNoteLink: parentNoteId,
|
||||||
target: "before",
|
target: "before",
|
||||||
targetBranchId: rowData.branchId,
|
targetBranchId: rowData.branchId,
|
||||||
}
|
}
|
||||||
@ -194,9 +194,12 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
|||||||
handler: async () => {
|
handler: async () => {
|
||||||
const branchId = row.getData().branchId;
|
const branchId = row.getData().branchId;
|
||||||
const note = await froca.getBranch(branchId)?.getNote();
|
const note = await froca.getBranch(branchId)?.getNote();
|
||||||
|
if (!note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
parentComponent?.triggerCommand("addNewRow", {
|
parentComponent?.triggerCommand("addNewRow", {
|
||||||
parentNotePath: note?.noteId,
|
|
||||||
customOpts: {
|
customOpts: {
|
||||||
|
parentNoteLink: note.noteId,
|
||||||
target: "after",
|
target: "after",
|
||||||
targetBranchId: branchId,
|
targetBranchId: branchId,
|
||||||
}
|
}
|
||||||
@ -208,8 +211,8 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
|||||||
uiIcon: "bx bx-horizontal-left bx-rotate-270",
|
uiIcon: "bx bx-horizontal-left bx-rotate-270",
|
||||||
enabled: !sorters.length,
|
enabled: !sorters.length,
|
||||||
handler: () => parentComponent?.triggerCommand("addNewRow", {
|
handler: () => parentComponent?.triggerCommand("addNewRow", {
|
||||||
parentNotePath: parentNoteId,
|
|
||||||
customOpts: {
|
customOpts: {
|
||||||
|
parentNoteLink: parentNoteId,
|
||||||
target: "after",
|
target: "after",
|
||||||
targetBranchId: rowData.branchId,
|
targetBranchId: rowData.branchId,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables";
|
import { EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables";
|
||||||
import { CommandListenerData } from "../../../components/app_context";
|
import { CommandListenerData } from "../../../components/app_context";
|
||||||
import note_create, { CreateNoteOpts } from "../../../services/note_create";
|
import note_create from "../../../services/note_create";
|
||||||
import { useLegacyImperativeHandlers } from "../../react/hooks";
|
import { useLegacyImperativeHandlers } from "../../react/hooks";
|
||||||
import { RefObject } from "preact";
|
import { RefObject } from "preact";
|
||||||
import { setAttribute, setLabel } from "../../../services/attributes";
|
import { setAttribute, setLabel } from "../../../services/attributes";
|
||||||
@ -9,17 +9,23 @@ import server from "../../../services/server";
|
|||||||
import branches from "../../../services/branches";
|
import branches from "../../../services/branches";
|
||||||
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
|
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for handling row table editing, including adding new rows.
|
||||||
|
*/
|
||||||
export default function useRowTableEditing(api: RefObject<Tabulator>, attributeDetailWidget: AttributeDetailWidget, parentNotePath: string): Partial<EventCallBackMethods> {
|
export default function useRowTableEditing(api: RefObject<Tabulator>, attributeDetailWidget: AttributeDetailWidget, parentNotePath: string): Partial<EventCallBackMethods> {
|
||||||
// Adding new rows
|
|
||||||
useLegacyImperativeHandlers({
|
useLegacyImperativeHandlers({
|
||||||
addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) {
|
addNewRowCommand({ customOpts }: CommandListenerData<"addNewRow">) {
|
||||||
const notePath = customNotePath ?? parentNotePath;
|
if (!customOpts) {
|
||||||
if (notePath) {
|
customOpts = {
|
||||||
const opts: CreateNoteOpts = {
|
target: "into",
|
||||||
activate: false,
|
};
|
||||||
...customOpts
|
|
||||||
}
|
}
|
||||||
note_create.createNote(notePath, opts).then(({ branch }) => {
|
|
||||||
|
const noteUrl = customOpts.parentNoteLink ?? parentNotePath;
|
||||||
|
if (noteUrl) {
|
||||||
|
customOpts.parentNoteLink = noteUrl;
|
||||||
|
customOpts.activate = false;
|
||||||
|
note_create.createNote(customOpts).then(({ branch }) => {
|
||||||
if (branch) {
|
if (branch) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!api.current) return;
|
if (!api.current) return;
|
||||||
@ -27,6 +33,7 @@ export default function useRowTableEditing(api: RefObject<Tabulator>, attributeD
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -33,8 +33,8 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.popup-editor-dialog .modal-header .title-row > * {
|
.modal.popup-editor-dialog .modal-header .note-title-widget {
|
||||||
margin: 5px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.popup-editor-dialog .modal-body {
|
.modal.popup-editor-dialog .modal-body {
|
||||||
@ -66,12 +66,17 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal.popup-editor-dialog div.promoted-attributes-container {
|
||||||
|
margin-block: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.modal.popup-editor-dialog .classic-toolbar-widget {
|
.modal.popup-editor-dialog .classic-toolbar-widget {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
margin-inline: 8px;
|
||||||
top: 0;
|
top: 0;
|
||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
background: transparent;
|
background: var(--modal-background-color);
|
||||||
z-index: 998;
|
z-index: 998;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,11 @@ export default function PopupEditor() {
|
|||||||
<DialogWrapper>
|
<DialogWrapper>
|
||||||
<Modal
|
<Modal
|
||||||
title={<TitleRow />}
|
title={<TitleRow />}
|
||||||
|
customTitleBarButtons={[{
|
||||||
|
iconClassName: "bx-expand-alt",
|
||||||
|
title: "Switch to full editor",
|
||||||
|
onClick: () => {/* TO DO */}
|
||||||
|
}]}
|
||||||
className="popup-editor-dialog"
|
className="popup-editor-dialog"
|
||||||
size="lg"
|
size="lg"
|
||||||
show={shown}
|
show={shown}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import FormRadioGroup from "../react/FormRadioGroup";
|
|||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
import { useRef, useState, useEffect } from "preact/hooks";
|
import { useRef, useState, useEffect } from "preact/hooks";
|
||||||
import tree from "../../services/tree";
|
import tree from "../../services/tree";
|
||||||
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
|
import note_autocomplete, { SuggestionMode, Suggestion } from "../../services/note_autocomplete";
|
||||||
import { logError } from "../../services/ws";
|
import { logError } from "../../services/ws";
|
||||||
import FormGroup from "../react/FormGroup.js";
|
import FormGroup from "../react/FormGroup.js";
|
||||||
import { refToJQuerySelector } from "../react/react_utils";
|
import { refToJQuerySelector } from "../react/react_utils";
|
||||||
@ -58,7 +58,7 @@ export default function AddLinkDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (suggestion.notePath) {
|
if (suggestion.notePath) {
|
||||||
const noteId = tree.getNoteIdFromUrl(suggestion.notePath);
|
const noteId = tree.getNoteIdFromLink(suggestion.notePath);
|
||||||
if (noteId) {
|
if (noteId) {
|
||||||
setDefaultLinkTitle(noteId);
|
setDefaultLinkTitle(noteId);
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ export default function AddLinkDialog() {
|
|||||||
onChange={setSuggestion}
|
onChange={setSuggestion}
|
||||||
opts={{
|
opts={{
|
||||||
allowExternalLinks: true,
|
allowExternalLinks: true,
|
||||||
allowCreatingNotes: true
|
suggestionMode: SuggestionMode.SuggestCreateAndLink,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import Modal from "../react/Modal.jsx";
|
import Modal from "../react/Modal.jsx";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import { CommandNames } from "../../components/app_context.js";
|
import appContext, { CommandNames } from "../../components/app_context.js";
|
||||||
import RawHtml from "../react/RawHtml.jsx";
|
import RawHtml from "../react/RawHtml.jsx";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import keyboard_actions from "../../services/keyboard_actions.js";
|
import keyboard_actions from "../../services/keyboard_actions.js";
|
||||||
@ -14,6 +14,7 @@ export default function HelpDialog() {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t("help.title")} className="help-dialog use-tn-links" minWidth="90%" size="lg" scrollable
|
title={t("help.title")} className="help-dialog use-tn-links" minWidth="90%" size="lg" scrollable
|
||||||
|
customTitleBarButtons={[{title: t("help.editShortcuts"), iconClassName: "bxs-pencil", onClick: editShortcuts}]}
|
||||||
onHidden={() => setShown(false)}
|
onHidden={() => setShown(false)}
|
||||||
show={shown}
|
show={shown}
|
||||||
>
|
>
|
||||||
@ -160,3 +161,7 @@ function Card({ title, children }: { title: string, children: ComponentChildren
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editShortcuts() {
|
||||||
|
appContext.tabManager.openContextWithNote("_optionsShortcuts", { activate: true });
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ import FormRadioGroup from "../react/FormRadioGroup";
|
|||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete";
|
import { SuggestionMode, Suggestion, triggerRecentNotes } from "../../services/note_autocomplete.js";
|
||||||
import tree from "../../services/tree";
|
import tree from "../../services/tree";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import { useTriliumEvent } from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
@ -50,7 +50,7 @@ export default function IncludeNoteDialog() {
|
|||||||
inputRef={autoCompleteRef}
|
inputRef={autoCompleteRef}
|
||||||
opts={{
|
opts={{
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowCreatingNotes: true
|
suggestionMode: SuggestionMode.SuggestCreateOnly,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@ -71,7 +71,7 @@ export default function IncludeNoteDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function includeNote(notePath: string, editorApi: CKEditorApi, boxSize: BoxSize) {
|
async function includeNote(notePath: string, editorApi: CKEditorApi, boxSize: BoxSize) {
|
||||||
const noteId = tree.getNoteIdFromUrl(notePath);
|
const noteId = tree.getNoteIdFromLink(notePath);
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import Button from "../react/Button";
|
|||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { useRef, useState } from "preact/hooks";
|
import { useRef, useState } from "preact/hooks";
|
||||||
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
|
import note_autocomplete, { SuggestionMode, Suggestion } from "../../services/note_autocomplete.js";
|
||||||
import appContext from "../../components/app_context";
|
import appContext from "../../components/app_context";
|
||||||
import commandRegistry from "../../services/command_registry";
|
import commandRegistry from "../../services/command_registry";
|
||||||
import { refToJQuerySelector } from "../react/react_utils";
|
import { refToJQuerySelector } from "../react/react_utils";
|
||||||
@ -12,34 +12,53 @@ import shortcutService from "../../services/shortcuts";
|
|||||||
|
|
||||||
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
||||||
|
|
||||||
type Mode = "last-search" | "recent-notes" | "commands";
|
enum Mode {
|
||||||
|
LastSearch,
|
||||||
|
RecentNotes,
|
||||||
|
Commands,
|
||||||
|
}
|
||||||
|
|
||||||
export default function JumpToNoteDialogComponent() {
|
export default function JumpToNoteDialogComponent() {
|
||||||
const [ mode, setMode ] = useState<Mode>();
|
const [ mode, setMode ] = useState<Mode>();
|
||||||
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
|
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const autocompleteRef = useRef<HTMLInputElement>(null);
|
const autocompleteRef = useRef<HTMLInputElement>(null);
|
||||||
const [ isCommandMode, setIsCommandMode ] = useState(mode === "commands");
|
const [ isCommandMode, setIsCommandMode ] = useState(mode === Mode.Commands);
|
||||||
const [ initialText, setInitialText ] = useState(isCommandMode ? "> " : "");
|
const [ initialText, setInitialText ] = useState(isCommandMode ? "> " : "");
|
||||||
const actualText = useRef<string>(initialText);
|
const actualText = useRef<string>(initialText);
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
|
|
||||||
async function openDialog(commandMode: boolean) {
|
async function openDialog(requestedMode: Mode) {
|
||||||
let newMode: Mode;
|
let newMode: Mode;
|
||||||
let initialText = "";
|
let initialText = "";
|
||||||
|
|
||||||
if (commandMode) {
|
switch (requestedMode) {
|
||||||
newMode = "commands";
|
case Mode.Commands:
|
||||||
|
newMode = Mode.Commands;
|
||||||
initialText = ">";
|
initialText = ">";
|
||||||
} else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
|
break;
|
||||||
|
|
||||||
|
case Mode.LastSearch:
|
||||||
// if you open the Jump To dialog soon after using it previously, it can often mean that you
|
// 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)
|
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
|
||||||
// so we'll keep the content.
|
// so we'll keep the content.
|
||||||
// if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead.
|
// if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead.
|
||||||
newMode = "last-search";
|
if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
|
||||||
|
newMode = Mode.LastSearch;
|
||||||
initialText = actualText.current;
|
initialText = actualText.current;
|
||||||
} else {
|
} else {
|
||||||
newMode = "recent-notes";
|
newMode = Mode.RecentNotes;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
|
||||||
|
newMode = Mode.LastSearch;
|
||||||
|
initialText = actualText.current;
|
||||||
|
} else {
|
||||||
|
newMode = Mode.RecentNotes;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode !== newMode) {
|
if (mode !== newMode) {
|
||||||
@ -51,8 +70,8 @@ export default function JumpToNoteDialogComponent() {
|
|||||||
setLastOpenedTs(Date.now());
|
setLastOpenedTs(Date.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
useTriliumEvent("jumpToNote", () => openDialog(false));
|
useTriliumEvent("jumpToNote", () => openDialog(Mode.RecentNotes));
|
||||||
useTriliumEvent("commandPalette", () => openDialog(true));
|
useTriliumEvent("commandPalette", () => openDialog(Mode.Commands));
|
||||||
|
|
||||||
async function onItemSelected(suggestion?: Suggestion | null) {
|
async function onItemSelected(suggestion?: Suggestion | null) {
|
||||||
if (!suggestion) {
|
if (!suggestion) {
|
||||||
@ -70,12 +89,12 @@ export default function JumpToNoteDialogComponent() {
|
|||||||
function onShown() {
|
function onShown() {
|
||||||
const $autoComplete = refToJQuerySelector(autocompleteRef);
|
const $autoComplete = refToJQuerySelector(autocompleteRef);
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "last-search":
|
case Mode.LastSearch:
|
||||||
break;
|
break;
|
||||||
case "recent-notes":
|
case Mode.RecentNotes:
|
||||||
note_autocomplete.showRecentNotes($autoComplete);
|
note_autocomplete.showRecentNotes($autoComplete);
|
||||||
break;
|
break;
|
||||||
case "commands":
|
case Mode.Commands:
|
||||||
note_autocomplete.showAllCommands($autoComplete);
|
note_autocomplete.showAllCommands($autoComplete);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -116,7 +135,7 @@ export default function JumpToNoteDialogComponent() {
|
|||||||
container={containerRef}
|
container={containerRef}
|
||||||
text={initialText}
|
text={initialText}
|
||||||
opts={{
|
opts={{
|
||||||
allowCreatingNotes: true,
|
suggestionMode: SuggestionMode.SuggestCreateOnly,
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowJumpToSearchNotes: true,
|
allowJumpToSearchNotes: true,
|
||||||
isCommandPalette: true
|
isCommandPalette: true
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { useEffect, useState } from "preact/hooks";
|
|||||||
import note_types from "../../services/note_types";
|
import note_types from "../../services/note_types";
|
||||||
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
||||||
import { TreeCommandNames } from "../../menus/tree_context_menu";
|
import { TreeCommandNames } from "../../menus/tree_context_menu";
|
||||||
import { Suggestion } from "../../services/note_autocomplete";
|
import { SuggestionMode, Suggestion } from "../../services/note_autocomplete.js";
|
||||||
import Badge from "../react/Badge";
|
import Badge from "../react/Badge";
|
||||||
import { useTriliumEvent } from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
@ -76,6 +76,7 @@ export default function NoteTypeChooserDialogComponent() {
|
|||||||
onHidden={() => {
|
onHidden={() => {
|
||||||
callback?.({ success: false });
|
callback?.({ success: false });
|
||||||
setShown(false);
|
setShown(false);
|
||||||
|
setParentNote(null);
|
||||||
}}
|
}}
|
||||||
show={shown}
|
show={shown}
|
||||||
stackable
|
stackable
|
||||||
@ -85,7 +86,7 @@ export default function NoteTypeChooserDialogComponent() {
|
|||||||
onChange={setParentNote}
|
onChange={setParentNote}
|
||||||
placeholder={t("note_type_chooser.search_placeholder")}
|
placeholder={t("note_type_chooser.search_placeholder")}
|
||||||
opts={{
|
opts={{
|
||||||
allowCreatingNotes: false,
|
suggestionMode: SuggestionMode.SuggestNothing,
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowJumpToSearchNotes: false,
|
allowJumpToSearchNotes: false,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import BasicWidget from "../basic_widget.js";
|
|||||||
import toastService from "../../services/toast.js";
|
import toastService from "../../services/toast.js";
|
||||||
import appContext from "../../components/app_context.js";
|
import appContext from "../../components/app_context.js";
|
||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
import noteAutocompleteService, { SuggestionMode } from "../../services/note_autocomplete.js";
|
||||||
|
|
||||||
import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator } from "./ui.js";
|
import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator } from "./ui.js";
|
||||||
import { formatMarkdown } from "./utils.js";
|
import { formatMarkdown } from "./utils.js";
|
||||||
@ -163,7 +163,7 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
const mentionSetup: MentionFeed[] = [
|
const mentionSetup: MentionFeed[] = [
|
||||||
{
|
{
|
||||||
marker: "@",
|
marker: "@",
|
||||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText, SuggestionMode.SuggestCreateAndLink),
|
||||||
itemRenderer: (item) => {
|
itemRenderer: (item) => {
|
||||||
const suggestion = item as Suggestion;
|
const suggestion = item as Suggestion;
|
||||||
const itemElement = document.createElement("button");
|
const itemElement = document.createElement("button");
|
||||||
|
|||||||
@ -29,7 +29,14 @@ export default function MobileDetailMenu() {
|
|||||||
],
|
],
|
||||||
selectMenuItemHandler: async ({ command }) => {
|
selectMenuItemHandler: async ({ command }) => {
|
||||||
if (command === "insertChildNote") {
|
if (command === "insertChildNote") {
|
||||||
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
const parentNoteUrl = appContext.tabManager.getActiveContextNotePath();
|
||||||
|
|
||||||
|
if (parentNoteUrl) {
|
||||||
|
note_create.createNote({
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: parentNoteUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (command === "delete") {
|
} else if (command === "delete") {
|
||||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
if (!notePath) {
|
if (!notePath) {
|
||||||
|
|||||||
@ -224,7 +224,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
} else if (target.classList.contains("add-note-button")) {
|
} else if (target.classList.contains("add-note-button")) {
|
||||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||||
const parentNotePath = treeService.getNotePath(node);
|
const parentNotePath = treeService.getNotePath(node);
|
||||||
noteCreateService.createNote(parentNotePath, { isProtected: node.data.isProtected });
|
noteCreateService.createNote(
|
||||||
|
{
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: parentNotePath,
|
||||||
|
isProtected: node.data.isProtected
|
||||||
|
},
|
||||||
|
);
|
||||||
} else if (target.classList.contains("enter-workspace-button")) {
|
} else if (target.classList.contains("enter-workspace-button")) {
|
||||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||||
this.triggerCommand("hoistNote", { noteId: node.data.noteId });
|
this.triggerCommand("hoistNote", { noteId: node.data.noteId });
|
||||||
@ -1403,10 +1409,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
let node: Fancytree.FancytreeNode | null | undefined = await this.expandToNote(activeNotePath, false);
|
let node: Fancytree.FancytreeNode | null | undefined = await this.expandToNote(activeNotePath, false);
|
||||||
|
|
||||||
if (node && node.data.noteId !== treeService.getNoteIdFromUrl(activeNotePath)) {
|
if (node && node.data.noteId !== treeService.getNoteIdFromLink(activeNotePath)) {
|
||||||
// if the active note has been moved elsewhere then it won't be found by the path,
|
// if the active note has been moved elsewhere then it won't be found by the path,
|
||||||
// so we switch to the alternative of trying to find it by noteId
|
// so we switch to the alternative of trying to find it by noteId
|
||||||
const noteId = treeService.getNoteIdFromUrl(activeNotePath);
|
const noteId = treeService.getNoteIdFromLink(activeNotePath);
|
||||||
|
|
||||||
if (noteId) {
|
if (noteId) {
|
||||||
const notesById = this.getNodesByNoteId(noteId);
|
const notesById = this.getNodesByNoteId(noteId);
|
||||||
@ -1836,9 +1842,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
const node = this.getActiveNode();
|
const node = this.getActiveNode();
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
const notePath = treeService.getNotePath(node);
|
const notePath = treeService.getNotePath(node);
|
||||||
noteCreateService.createNote(notePath, {
|
noteCreateService.createNote(
|
||||||
|
{
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: notePath,
|
||||||
isProtected: node.data.isProtected
|
isProtected: node.data.isProtected
|
||||||
});
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new TouchBar.TouchBarButton({
|
new TouchBar.TouchBarButton({
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
import { useEffect, useRef, useMemo } from "preact/hooks";
|
import { useEffect, useRef, useMemo } from "preact/hooks";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
@ -7,9 +8,16 @@ import { Modal as BootstrapModal } from "bootstrap";
|
|||||||
import { memo } from "preact/compat";
|
import { memo } from "preact/compat";
|
||||||
import { useSyncedRef } from "./hooks";
|
import { useSyncedRef } from "./hooks";
|
||||||
|
|
||||||
|
interface CustomTitleBarButton {
|
||||||
|
title: string;
|
||||||
|
iconClassName: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
className: string;
|
className: string;
|
||||||
title: string | ComponentChildren;
|
title: string | ComponentChildren;
|
||||||
|
customTitleBarButtons?: (CustomTitleBarButton | null)[];
|
||||||
size: "xl" | "lg" | "md" | "sm";
|
size: "xl" | "lg" | "md" | "sm";
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
/**
|
/**
|
||||||
@ -72,7 +80,7 @@ interface ModalProps {
|
|||||||
noFocus?: boolean;
|
noFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal({ children, className, size, title, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
|
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
|
||||||
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
|
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
|
||||||
const modalInstanceRef = useRef<BootstrapModal>();
|
const modalInstanceRef = useRef<BootstrapModal>();
|
||||||
const elementToFocus = useRef<Element | null>();
|
const elementToFocus = useRef<Element | null>();
|
||||||
@ -148,7 +156,17 @@ export default function Modal({ children, className, size, title, header, footer
|
|||||||
{helpPageId && (
|
{helpPageId && (
|
||||||
<button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button>
|
<button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{titleBarButtons?.filter((b) => b !== null).map((titleBarButton) => (
|
||||||
|
<button type="button"
|
||||||
|
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
|
||||||
|
title={titleBarButton.title}
|
||||||
|
onClick={titleBarButton.onClick}>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")}></button>
|
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")}></button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{onSubmit ? (
|
{onSubmit ? (
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
||||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList";
|
||||||
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
||||||
import { ParentComponent } from "../react/react_utils";
|
import { ParentComponent } from "../react/react_utils";
|
||||||
import { t } from "../../services/i18n"
|
import { t } from "../../services/i18n"
|
||||||
@ -113,8 +113,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
|||||||
function DevelopmentActions({ note }: { note: FNote }) {
|
function DevelopmentActions({ note }: { note: FNote }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormDropdownDivider />
|
<FormListHeader text="Development-only Actions" />
|
||||||
<FormListItem disabled>Development-only Actions</FormListItem>
|
|
||||||
<FormListItem
|
<FormListItem
|
||||||
icon="bx bx-printer"
|
icon="bx bx-printer"
|
||||||
onClick={() => window.open(`/?print=#root/${note.noteId}`, "_blank")}
|
onClick={() => window.open(`/?print=#root/${note.noteId}`, "_blank")}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { MutableRef, useEffect, useImperativeHandle, useMemo, useRef, useState }
|
|||||||
import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5";
|
import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
import server from "../../../services/server";
|
import server from "../../../services/server";
|
||||||
import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete";
|
import note_autocomplete, { SuggestionMode, Suggestion } from "../../../services/note_autocomplete.js";
|
||||||
import CKEditor, { CKEditorApi } from "../../react/CKEditor";
|
import CKEditor, { CKEditorApi } from "../../react/CKEditor";
|
||||||
import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent, useTriliumOption } from "../../react/hooks";
|
import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent, useTriliumOption } from "../../react/hooks";
|
||||||
import FAttribute from "../../../entities/fattribute";
|
import FAttribute from "../../../entities/fattribute";
|
||||||
@ -20,6 +20,7 @@ import type { CommandData, FilteredCommandNames } from "../../../components/app_
|
|||||||
import { AttributeType } from "@triliumnext/commons";
|
import { AttributeType } from "@triliumnext/commons";
|
||||||
import attributes from "../../../services/attributes";
|
import attributes from "../../../services/attributes";
|
||||||
import note_create from "../../../services/note_create";
|
import note_create from "../../../services/note_create";
|
||||||
|
import { CreateNoteAction } from "@triliumnext/commons";
|
||||||
|
|
||||||
type AttributeCommandNames = FilteredCommandNames<CommandData>;
|
type AttributeCommandNames = FilteredCommandNames<CommandData>;
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ const HELP_TEXT = `
|
|||||||
const mentionSetup: MentionFeed[] = [
|
const mentionSetup: MentionFeed[] = [
|
||||||
{
|
{
|
||||||
marker: "@",
|
marker: "@",
|
||||||
feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText),
|
feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText, SuggestionMode.SuggestCreateAndLink),
|
||||||
itemRenderer: (_item) => {
|
itemRenderer: (_item) => {
|
||||||
const item = _item as Suggestion;
|
const item = _item as Suggestion;
|
||||||
const itemElement = document.createElement("button");
|
const itemElement = document.createElement("button");
|
||||||
@ -247,16 +248,18 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
|||||||
|
|
||||||
$el.text(title);
|
$el.text(title);
|
||||||
},
|
},
|
||||||
createNoteForReferenceLink: async (title: string) => {
|
createNoteFromCkEditor: async (
|
||||||
let result;
|
title: string,
|
||||||
if (notePath) {
|
parentNotePath: string | undefined,
|
||||||
result = await note_create.createNoteWithTypePrompt(notePath, {
|
action: CreateNoteAction
|
||||||
activate: false,
|
): Promise<string> => {
|
||||||
title: title
|
const { note } = await note_create.createNoteFromAction(
|
||||||
});
|
action,
|
||||||
}
|
true,
|
||||||
|
title,
|
||||||
return result?.note?.getBestNotePathString();
|
parentNotePath,
|
||||||
|
);
|
||||||
|
return note?.getBestNotePathString() ?? "";
|
||||||
}
|
}
|
||||||
}), [ notePath ]));
|
}), [ notePath ]));
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import FormGroup from "../react/FormGroup";
|
|||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
import "./Empty.css";
|
import "./Empty.css";
|
||||||
import { ParentComponent, refToJQuerySelector } from "../react/react_utils";
|
import { ParentComponent, refToJQuerySelector } from "../react/react_utils";
|
||||||
import note_autocomplete from "../../services/note_autocomplete";
|
import note_autocomplete, { SuggestionMode } from "../../services/note_autocomplete";
|
||||||
import appContext from "../../components/app_context";
|
import appContext from "../../components/app_context";
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import search from "../../services/search";
|
import search from "../../services/search";
|
||||||
@ -38,7 +38,7 @@ function NoteSearch({ ntxId }: { ntxId: string | null }) {
|
|||||||
inputRef={autocompleteRef}
|
inputRef={autocompleteRef}
|
||||||
opts={{
|
opts={{
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowCreatingNotes: true,
|
suggestionMode: SuggestionMode.SuggestCreateOnly,
|
||||||
allowJumpToSearchNotes: true,
|
allowJumpToSearchNotes: true,
|
||||||
}}
|
}}
|
||||||
onChange={suggestion => {
|
onChange={suggestion => {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import note_create from "../../../services/note_create";
|
|||||||
import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } from "../../react/TouchBar";
|
import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } from "../../react/TouchBar";
|
||||||
import { RefObject } from "preact";
|
import { RefObject } from "preact";
|
||||||
import { buildSelectedBackgroundColor } from "../../../components/touch_bar";
|
import { buildSelectedBackgroundColor } from "../../../components/touch_bar";
|
||||||
import { deferred } from "@triliumnext/commons";
|
import { CreateNoteAction, deferred } from "@triliumnext/commons";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,17 +115,18 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
|||||||
},
|
},
|
||||||
loadIncludedNote,
|
loadIncludedNote,
|
||||||
// Creating notes in @-completion
|
// Creating notes in @-completion
|
||||||
async createNoteForReferenceLink(title: string) {
|
async createNoteFromCkEditor (
|
||||||
const notePath = noteContext?.notePath;
|
title: string,
|
||||||
if (!notePath) return;
|
parentNotePath: string | undefined,
|
||||||
|
action: CreateNoteAction
|
||||||
const resp = await note_create.createNoteWithTypePrompt(notePath, {
|
): Promise<string> {
|
||||||
activate: false,
|
const { note }= await note_create.createNoteFromAction(
|
||||||
title: title
|
action,
|
||||||
});
|
true,
|
||||||
|
title,
|
||||||
if (!resp || !resp.note) return;
|
parentNotePath,
|
||||||
return resp.note.getBestNotePathString();
|
)
|
||||||
|
return note?.getBestNotePathString() ?? "";
|
||||||
},
|
},
|
||||||
// Keyboard shortcut
|
// Keyboard shortcut
|
||||||
async followLinkUnderCursorCommand() {
|
async followLinkUnderCursorCommand() {
|
||||||
@ -162,7 +163,9 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
|||||||
// without await as this otherwise causes deadlock through component mutex
|
// without await as this otherwise causes deadlock through component mutex
|
||||||
const parentNotePath = appContext.tabManager.getActiveContextNotePath();
|
const parentNotePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
if (noteContext && parentNotePath) {
|
if (noteContext && parentNotePath) {
|
||||||
note_create.createNote(parentNotePath, {
|
note_create.createNote({
|
||||||
|
parentNoteLink: parentNotePath,
|
||||||
|
target: "into",
|
||||||
isProtected: note.isProtected,
|
isProtected: note.isProtected,
|
||||||
saveSelection: true,
|
saveSelection: true,
|
||||||
textEditor: await noteContext?.getTextEditor()
|
textEditor: await noteContext?.getTextEditor()
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import emojiDefinitionsUrl from "@triliumnext/ckeditor5/src/emoji_definitions/en
|
|||||||
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||||
import { t } from "../../../services/i18n.js";
|
import { t } from "../../../services/i18n.js";
|
||||||
import { getMermaidConfig } from "../../../services/mermaid.js";
|
import { getMermaidConfig } from "../../../services/mermaid.js";
|
||||||
import noteAutocompleteService, { type Suggestion } from "../../../services/note_autocomplete.js";
|
import noteAutocompleteService, { SuggestionMode, type Suggestion } from "../../../services/note_autocomplete.js";
|
||||||
import mimeTypesService from "../../../services/mime_types.js";
|
import mimeTypesService from "../../../services/mime_types.js";
|
||||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||||
import { buildToolbarConfig } from "./toolbar.js";
|
import { buildToolbarConfig } from "./toolbar.js";
|
||||||
@ -181,7 +181,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
|||||||
feeds: [
|
feeds: [
|
||||||
{
|
{
|
||||||
marker: "@",
|
marker: "@",
|
||||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText, SuggestionMode.SuggestCreateAndLink),
|
||||||
itemRenderer: (item) => {
|
itemRenderer: (item) => {
|
||||||
const itemElement = document.createElement("button");
|
const itemElement = document.createElement("button");
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ interface GotoOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BASE_URL = "http://127.0.0.1:8082";
|
const BASE_URL = "http://127.0.0.1:8082";
|
||||||
|
const NUM_OF_CREATE_NOTE_OPTIONS = 2;
|
||||||
|
|
||||||
interface DropdownLocator extends Locator {
|
interface DropdownLocator extends Locator {
|
||||||
selectOptionByText: (text: string) => Promise<void>;
|
selectOptionByText: (text: string) => Promise<void>;
|
||||||
@ -73,7 +74,8 @@ export default class App {
|
|||||||
const resultsSelector = this.currentNoteSplit.locator(".note-detail-empty-results");
|
const resultsSelector = this.currentNoteSplit.locator(".note-detail-empty-results");
|
||||||
await expect(resultsSelector).toContainText(noteTitle);
|
await expect(resultsSelector).toContainText(noteTitle);
|
||||||
await resultsSelector.locator(".aa-suggestion", { hasText: noteTitle })
|
await resultsSelector.locator(".aa-suggestion", { hasText: noteTitle })
|
||||||
.nth(1) // Select the second one, as the first one is "Create a new note"
|
// Select the n+1 one, as the first one is "Create a new note"
|
||||||
|
.nth(NUM_OF_CREATE_NOTE_OPTIONS)
|
||||||
.click();
|
.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
"note_structure_description": "Notizen lassen sich hierarchisch anordnen. Ordner sind nicht nötig, da jede Notiz Unternotizen enthalten kann. Eine einzelne Notiz kann an mehreren Stellen in der Hierarchie hinzugefügt werden.",
|
"note_structure_description": "Notizen lassen sich hierarchisch anordnen. Ordner sind nicht nötig, da jede Notiz Unternotizen enthalten kann. Eine einzelne Notiz kann an mehreren Stellen in der Hierarchie hinzugefügt werden.",
|
||||||
"hoisting_description": "Trennen Sie Ihre persönlichen und beruflichen Notizen ganz einfach, indem Sie sie in einem Arbeitsbereich gruppieren. Dadurch wird Ihre Notizstruktur so fokussiert, dass nur ein bestimmter Satz von Notizen angezeigt wird.",
|
"hoisting_description": "Trennen Sie Ihre persönlichen und beruflichen Notizen ganz einfach, indem Sie sie in einem Arbeitsbereich gruppieren. Dadurch wird Ihre Notizstruktur so fokussiert, dass nur ein bestimmter Satz von Notizen angezeigt wird.",
|
||||||
"hoisting_title": "Arbeitsbereiche und Fokusansicht",
|
"hoisting_title": "Arbeitsbereiche und Fokusansicht",
|
||||||
"attributes_description": "Nutzen Sie Verbindungen zwischen Notizen oder fügen Sie Labels hinzu, um die Kategorisierung zu erleichtern. Mit hervorgehobenen („promoted“) Attributen können Sie strukturierte Informationen erfassen, die sich direkt in Tabellen und Boards verwenden lassen."
|
"attributes_description": "Verwenden Sie Beziehungen zwischen Notizen oder fügen Sie Beschriftungen hinzu, um die Kategorisierung zu vereinfachen. Verwenden Sie hervorgehobene Attribute, um strukturierte Informationen einzugeben, die in Tabellen und Boards verwendet werden können."
|
||||||
},
|
},
|
||||||
"productivity_benefits": {
|
"productivity_benefits": {
|
||||||
"revisions_title": "Notizrevisionen",
|
"revisions_title": "Notizrevisionen",
|
||||||
|
|||||||
@ -87,6 +87,8 @@
|
|||||||
"description_arm64": "ARM 기반 리눅스 배포판에서 aarch64 아키텍처와 호환됩니다."
|
"description_arm64": "ARM 기반 리눅스 배포판에서 aarch64 아키텍처와 호환됩니다."
|
||||||
},
|
},
|
||||||
"note_types": {
|
"note_types": {
|
||||||
"text_title": "텍스트 노트"
|
"text_title": "텍스트 노트",
|
||||||
|
"text_description": "노트는 WYSIWYG 편집기를 사용하며 표, 이미지, 수학 표현식, 구문 강조 기능의 코드 블록을 지원합니다. 특수문자를 사용한 마크다운 유사 구문이나 슬래시(/) 명령으로 텍스트 서식을 빠르게 지정할 수 있습니다.",
|
||||||
|
"code_title": "코드 노트"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import "ckeditor5";
|
import "ckeditor5";
|
||||||
|
import { type CreateNoteAction } from "@triliumnext/commons"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Component {
|
interface Component {
|
||||||
@ -7,7 +8,8 @@ declare global {
|
|||||||
|
|
||||||
interface EditorComponent extends Component {
|
interface EditorComponent extends Component {
|
||||||
loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string): Promise<void>;
|
loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string): Promise<void>;
|
||||||
createNoteForReferenceLink(title: string): Promise<string>;
|
// Must Return Note Path
|
||||||
|
createNoteFromCkEditor(title: string, parentNotePath: string | undefined, action: CreateNoteAction): Promise<string>;
|
||||||
loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>): void;
|
loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Command, Mention, Plugin, ModelRange, type ModelSelectable } from "ckeditor5";
|
import { Command, Mention, Plugin, ModelRange, type ModelSelectable } from "ckeditor5";
|
||||||
|
import { CreateNoteAction } from "@triliumnext/commons"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides the actions taken by the Mentions plugin (triggered by `@` in the text editor, or `~` & `#` in the attribute editor):
|
* Overrides the actions taken by the Mentions plugin (triggered by `@` in the text editor, or `~` & `#` in the attribute editor):
|
||||||
@ -36,9 +37,10 @@ interface MentionOpts {
|
|||||||
|
|
||||||
interface MentionAttribute {
|
interface MentionAttribute {
|
||||||
id: string;
|
id: string;
|
||||||
action?: "create-note";
|
action?: CreateNoteAction;
|
||||||
noteTitle: string;
|
noteTitle: string;
|
||||||
notePath: string;
|
notePath: string;
|
||||||
|
parentNoteId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomMentionCommand extends Command {
|
class CustomMentionCommand extends Command {
|
||||||
@ -56,13 +58,26 @@ class CustomMentionCommand extends Command {
|
|||||||
model.insertContent( writer.createText( mention.id, {} ), range );
|
model.insertContent( writer.createText( mention.id, {} ), range );
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (mention.action === 'create-note') {
|
else if (
|
||||||
|
mention.action === CreateNoteAction.CreateNote ||
|
||||||
|
mention.action === CreateNoteAction.CreateChildNote ||
|
||||||
|
mention.action === CreateNoteAction.CreateAndLinkNote ||
|
||||||
|
mention.action === CreateNoteAction.CreateAndLinkChildNote
|
||||||
|
) {
|
||||||
const editorEl = this.editor.editing.view.getDomRoot();
|
const editorEl = this.editor.editing.view.getDomRoot();
|
||||||
const component = glob.getComponentByEl<EditorComponent>(editorEl);
|
const component = glob.getComponentByEl<EditorComponent>(editorEl);
|
||||||
|
|
||||||
component.createNoteForReferenceLink(mention.noteTitle).then(notePath => {
|
// use parentNoteId as fallback when notePath is missing
|
||||||
|
const parentNotePath = mention.notePath || mention.parentNoteId;
|
||||||
|
|
||||||
|
component
|
||||||
|
.createNoteFromCkEditor(mention.noteTitle, parentNotePath, mention.action)
|
||||||
|
.then(notePath => {
|
||||||
|
if (notePath) {
|
||||||
this.insertReference(range, notePath);
|
this.insertReference(range, notePath);
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Error creating note from CKEditor mention:", err));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.insertReference(range, mention.notePath);
|
this.insertReference(range, mention.notePath);
|
||||||
|
|||||||
@ -10,4 +10,5 @@ export * from "./lib/server_api.js";
|
|||||||
export * from "./lib/shared_constants.js";
|
export * from "./lib/shared_constants.js";
|
||||||
export * from "./lib/ws_api.js";
|
export * from "./lib/ws_api.js";
|
||||||
export * from "./lib/attribute_names.js";
|
export * from "./lib/attribute_names.js";
|
||||||
|
export * from "./lib/create_note_actions.js";
|
||||||
export * from "./lib/utils.js";
|
export * from "./lib/utils.js";
|
||||||
|
|||||||
6
packages/commons/src/lib/create_note_actions.ts
Normal file
6
packages/commons/src/lib/create_note_actions.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export enum CreateNoteAction {
|
||||||
|
CreateNote = "create-note",
|
||||||
|
CreateChildNote = "create-child-note",
|
||||||
|
CreateAndLinkNote = "create-and-link-note",
|
||||||
|
CreateAndLinkChildNote = "create-and-link-child-note"
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user